[Bugfix] Timer::test re-phases on a wide step — last_ran = now discards skipped intervals AND the residual #397
Labels
No labels
Alpha Release Requirement
Bugfix
Demo Target
Documentation
Major Feature
Minor Feature
priority:tier1-active
priority:tier2-foundation
priority:tier3-future
priority:tier4-deferred
Refactoring & Cleanup
system:animation
system:documentation
system:grid
system:input
system:performance
system:procgen
system:python-binding
system:rendering
system:ui-hierarchy
Tiny Feature
workflow:blocked
workflow:needs-benchmark
workflow:needs-documentation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
john/McRogueFace#397
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while implementing #396. This issue is about the behavior #396 deliberately did NOT change —
that work added a new bounded
advance()primitive that sidesteps the problem;step()itself stillhas it.
Behavior
Timer::teststampslast_ran = nowrather thanlast_ran += interval. Combined with one frametesting each timer at most once, a single wide step does two lossy things, not one:
step(10.0)fires a 1000 ms repeating timer once, at t=10000, not ten times.This half is well understood and is what #396 addressed.
nine skipped intervals and any residual are discarded rather than deferred. The timer's phase
relative to its original schedule is permanently lost.
Half 2 is the nastier one. Under-firing produces a sparse trace; re-phasing produces a trace that is
sparse and misaligned, and nothing downstream can reconstruct the original cadence to notice.
Why it wasn't fixed in #396
GameEngine::advance(int ms)avoids it by construction: it finds the nearest pending deadline withinthe remaining span and runs one frame landing exactly on it, so
last_ran = nowhappens to be exactand no drift accumulates.
step()was left unchanged on purpose — "run one frame" and "let a span oftime pass" are different operations, and folding them together would remove the one-frame primitive
and shift
current_frameaccounting under every existing headless test.So the bug is now documented (
step()'s docstring states one frame tests each timer at most once,and points at
advance()), but still reachable by anyone callingstep()with a wide dt.Question this issue needs to answer
Is
last_ran = nowever the intended semantic? There is a defensible reading where a timer thatmisses deadlines should not then fire a burst to catch up. If so, the fix is not to change
step()but to make the choice explicit and named, rather than an accident of assignment order. If not,
last_ran += intervalwith explicit catch-up is the correction, and it needs a decision about whathappens when many intervals have elapsed.
Note the related constraint from the simulation work driving #396: advancement should be driven by
elapsed time with catch-up, never by frame count, because a frame-driven world slows down exactly
when the consumer is slow.
Acceptance
Whatever the decision, it needs a test asserting phase behavior across a wide step — currently nothing
covers it, which is why this survived.
Related: #396 (the clock exposure and
advance()primitive), #394 (unseeded behavior RNG — the otherdeterminism gap).