Different rounding functions were also my first thought, but they only shift the problem. Let's assume our sprite's height is
approximately 40, so we want to get 20. It's easy to construct cases that fail:
// std::ceil()
ceil(bounds.height / 2.f) == ceil(40.0001 / 2) == 21
// std::floor()
floor(bounds.height / 2.f) == floor(39.9999 / 2) == 19
// std::trunc()
trunc(bounds.height / 2.f) == trunc(39.9999 / 2) == 19
I think the most robust solution is really to be far from the rounding "boundaries", for example by adding +0.5.
Or use integer division in the first place.
// Bad: std::round(bounds.height / 2.f)
// Good: round((bounds.height + 0.5f) / 2.f)
// We want to map both 39 and 40 to 20. This works:
round((40.0001 + 0.5) / 2) == round(20.25005) == 20
round((39.9999 + 0.5) / 2) == round(20.24995) == 20
round((38.9999 + 0.5) / 2) == round(19.75005) == 20
round((38.9999 + 0.5) / 2) == round(19.74995) == 20
If values can be negative (not relevant for sizes), we should replace
std::round(x) with a custom rounding function
std::floor(x + 0.5), which rounds towards positive infinity independent of the sign. Otherwise, jittering will still occur for values around zero.
Hiura, taking history into account may generally be a good method to smooth such patterns, but here it looks more like symptom treatment than solving the underlying problem