Rounding Errors - EverestAPI/CelesteTAS-EverestInterop GitHub Wiki
Authors: Molly
Many, many calculations in Celeste's code rely on code constructs designed to represent non-integer numbers. These constructs are often assigned values (literal or calculated) that they cannot exactly represent, meaning the stored values differ from the given values by very small amounts -- and subsequently, the results of any calculations using those stored values differ from what would be expected using the given values by a small amount. This difference is typically referred to as a "rounding error", and this page aims to list common sources and consequences of them.
Note
This list is currently quite limited -- when sources of rounding errors are encountered in the wild, addition to this page would be greatly appreciated!
As an example of a rounding error, consider this input sequence:
# start after colliding with the ground
4,Z
1,R,J
3,R
1,R,J
3,R
# now touching the ground with very high Y subpixel position
4,R,K
# clears 1 tile in 4 frames
This performs a hyper that lands with a Y subpixel position so high that a ground jump can clear a wall 1 tile (8 pixels) high in 4 frames. Using a varJumpSpeed
of -105 and a DeltaTime of 1/60, that should be barely impossible -- those values give a vertical speed of exactly 1.75 pixels per frame, which would cause Madeline to gain exactly 7 pixels of height in 4 frames, and to go from the closest integer position (used for collision checking) being touching the ground to being 8 pixels above the ground requires moving more than 7 pixels, no matter how small the additional amount. What makes this possible is the rounding error caused by DeltaTime being slightly higher than 1/60, which is enough to move Madeline the additional amount in the 4 frames rather than having to wait a 5th frame.
"Imprecision" is usually used to describe effects of a code construct's precision in contexts involving values that cannot be represented with that precision.
For example, due to float imprecision, speed-based movement cannot give Madeline a position with absolute value higher than 2^24 = 16,777,216. (Speed moves Madeline 1 pixel at a time, but floats have 23-bit significands, so with an exponent of 24, the least significant bit has a value of 2.)
A strat or tech is typically said to be "rounding error precise" if it requires a rounding error to occur in order to be possible or achieve a desired outcome (e.g. save time).
As with any level of precision, whether something is rounding error precise is not necessarily related to how difficult it is to perform. There may be a known set of inputs to set up a rounding error from a common set of circumstances, such as the hyper bunnyhop described at the beginning of this page -- this can often be used RTA, especially with pausebuffering. There may be a way to use analogue inputs to move Madeline by precise amounts, such as from a feather or water -- this is practically impossible to utilize RTA, but it all but guarantees that a TAS can set up a rounding error.
The C# TimeSpan
type's smallest unit is called a "tick" and defined here as one ten-millionth of a second (i.e. 100 nanoseconds or 0.1 microseconds).
Many values related to time are defined in terms of seconds, and Engine.DeltaTime
is the value used to get per-frame effects based on those values. Engine.DeltaTime
is a float calculated each frame, and when the game is at 100% speed, it will always be equal to the TimeSpan
Engine.TargetElapsedTime
, which uses the default value of 166667 ticks (0.0166667 seconds) provided in the Microsoft.Xna.Framework.Game
constructor. The closest possible representation in a float is about 0.0166666992.
DeltaTime is used for many duration- and rate-related calculations in Celeste. One extremely common example is in Madeline's speed -- for example, a straight dash gives 240 speed on one axis, so if DeltaTime was exactly 1/60, Madeline would move exactly 4 pixels per frame. However, the actual stored value of DeltaTime causes 240 speed to move Madeline approximately 4.000008 pixels per frame instead, rarely causing her to move 5 pixels in one frame instead of 4.
The C# float
type, which implements IEEE 754-2019, includes an 8-bit signed exponent (-128 to 127) and a 23-bit significand. Thus, its maximum precision is 2 ^ (exponent - 23).
One instance of float imprecision many TASers are aware of is Engine.Scene.TimeActive
's rate of increase. TimeActive is a float of which the main use most TASers are concerned with is preventing spinners and lightning from becoming Active
. Every frame, DeltaTime gets added to TimeActive, but as TimeActive's exponent grows larger, the closest representable value to that sum grows further away from the actual value (i.e. the rounding error increases). This rounding error causes the interval on which the hazard must be stunned to change (i.e. the hazard's "group" to "drift") more and more often. Eventually, when TimeActive reaches 2^19, the closest representable value becomes the same as the value before the addition (since ~1/60 is closer to 0 than 1/16), causing TimeActive to stop increasing altogether.