Precision Drift in t() Over Long Uptimes
Bug Description
Over hours, floating-point division yields cumulative precision drift.
This results in noticeable glitching in all applications that derive any of their values from t()
at any point during their _update()
or _draw()
functions.
Summary
- Root cause:
lua54_time()
uses60.0f
(32-bit), resulting in precision drift - Fix: change
60.0f
=>60.0
- Alternatives: new
stat(STAT_SEC_TICKS)
orstat(STAT_TICKS_RUNNING, n)
If this is behaving as intended or you go with the "Fix", I implore you to consider implementing one of the stat()
solutions!
Context
time()
is the lua-side of the c functionlua54_time()
lua54_time()
returnscproc->ticks_running / 60.0
as a lua_Numberticks_running
is a once-per-tick integer counter- (tick = 1/60 seconds) =>
t()
increments ~0.01667 seconds per tick
- (tick = 1/60 seconds) =>
- No reliable sub-second API for long-running apps
stat(STAT_EPOCH)
=> returns the current unix timestamp => whole secondsdate("!*t")
=> constructs a table of time data from the current unix timestamp => whole seconds
Reproduction
- Run the bunny screensaver (or any loop using
t()
for timing)- To avoid accidents with the screensaver ending, run the screensaver as a normal cartridge
- Let it run for several hours
- Observe growing visual artifacts as frame data gets calculated incorrectly
Actual Behavior
- Frames step by ~0.01667 s, floating-point accumulation drifts over time
- After extended uptime, precision loss becomes significant enough for artifacts to become pronounced
Expected Behavior
- Consistent 1/60 s timing indefinitely, with no precision drift
Workarounds
Whole-Second Fallback
This only works if the user doesn't care about partial seconds, which is a fairly niche use-case
Bit-Split Delay Hack
This workaround gives us 32 times the amount of time before we experience precision errors, but that still means that for long-running applications, someone leaving picotron running (as users are want to do), this still means that a few days tops are all you get.
Identification
Having dug into the binary some, it appears that cproc's field for tracking the number of ticks the process has been alive is a long, and the assembly confirms that it's loaded as a double (through long-to-double conversion). However, the 60.0
is loaded as a 32-bit float and the division is performed as between 2 floats. This means that for the purposes of lua54_time()
, cproc->ticks_alive
is a 32-bit float.
I only included the most relevant disassembly. For the full disassembly of
lua54_time()
, please see the "Reference" heading.
Conclusion
Confirmed: 60.0
is loaded as a 32-bit value, so cproc->ticks_running
gets cast to a single-precision floating-point value.
I suspect the cause of this is an f
suffix on the 60.0
as such:
int lua54_time(lua_State *L) { lua_pushnumber(L, cproc->ticks_running / 60.0f); return 1; } |
Proposed Solutions
I propose one or more of the following:
- Modifying
lua54_time()
's behaviour to return the result of a double-precision division - introducing a new
stat(STAT_SEC_TICKS)
for determining how many ticks into a second the process is, usingcproc->ticks_running % 60
or equiv - introducing a new
stat(STAT_TICKS_RUNNING, n)
for returning the integer result ofcproc->ticks_running % n
.
Reasoning / Justification
These are not expected, just suggestions for potential solutions. Of course, this all assumes this was unintentional. If this is behaving as intended, I implore you to still consider implementing one of the proposed stat()
solutions.
- Modifying
lua54_time()
's behaviour to return the result of a double-precision division- it's the most simple and effective solution
- potentially a few cycles slower, but practically the difference is none
- even if it did use an additional 4 bytes in the executable, it would be practically no change. And it would not use an extra 4 bytes to do this because of data alignment anyhow. There are unused bytes between
FLOAT_60_0
and the next value, enough that this change would not affect the program in any meaningful way.
- Introducing a new
stat(STAT_SEC_TICKS)
for determining how many ticks into a second the process is.- this would function as a precision counter-part to
t()
- It lets users solve the problem for themselves if they need the higher precision.
- This is most powerful in conjunction with
stat(STAT_EPOCH)
, wherestat(STAT_SEC_TICKS)
would allow users to achieve additional precision in any timing data.
- this would function as a precision counter-part to
- Introducing a new
stat(STAT_TICKS_RUNNING, n)
for returning the integer value ofcproc->ticks_running % n
stat(STAT_TICKS_RUNNING, n?)
where the return value iscproc->ticks_running % n
and n defaults to1
- this would function as a precision alternative to
t()
, allowing users to dostat(STAT_TICKS_RUNNING, 60)
to get the same value asstat(STAT_SEC_TICKS)
, or dostat(STAT_TICKS_RUNNING, 1)
to just get the number of ticks the process has been running. This is more flexible solution.
Reference
Full lua54_time()
disassembly:
[Please log in to post a comment]