You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fix frame rate limiting blocking forever when the clock cannot observe sleep
The frame rate limiting loop in flecs_insert_sleep slept a fixed
interval and remeasured until the frame reached the target time. When
the clock does not advance during a sleep, the loop never exits: a clock
that is only stepped by the host in between frames (emscripten without
asyncify, simulated clocks) blocks ecs_progress forever as soon as a
target FPS is set. A clock too coarse to observe an individual sleep
does exit, but only by spinning through many sleep and measure calls per
clock tick.
Sleep intervals now escalate when the clock is not keeping up. The clock
counts as keeping up when it advances by at least a quarter of the
interval just slept, which measures whether get_time and sleep agree
about the passage of time rather than how fast the clock runs, so a host
that virtualizes both consistently still paces correctly at any speed.
On a coarse clock, escalation grows the interval until it crosses a
clock tick, which keeps pacing working at reduced granularity. On a
clock that does not advance at all, a stall budget of one frame period
that never refills within a frame ends it. The budget not refilling
matters, because a budget that resets whenever the clock advances a
little lets clock jitter replay the escalation chain indefinitely on a
clock that advances just enough to be counted once per chain. Backing
the budget is a ceiling on the number of times a frame may sleep, which
no clock reaches: an interval that keeps up advances the measured delta
by a quarter of itself while the exit condition needs only eight initial
intervals of advance, and one that does not keep up is charged to the
budget. Giving the clock complete freedom to answer each read with
whichever value prolongs the loop, and searching that exhaustively, the
worst case is 36 iterations and 4.75 frame periods of requested sleep,
or 41 iterations if the clock may also run backwards. The ceiling is
128. The ecs_set_target_fps documentation states the resulting contract:
a clock that observes at least a quarter of each interval reaches the
target and pays the disagreement in blocking time, and one below that
has the budget end its frame.
ecs_set_target_fps also rejects targets that are not zero or within
1e-9 to 1e9. The frame time is the reciprocal of the target, so a
subnormal target yields an infinite frame time and sleep interval, and
the value can arrive from outside the application through the REST world
summary endpoint; the range keeps the frame time finite and within
[1e-9, 1e9] by construction, rejects NaN through the comparison, and
compiles for any type ecs_ftime_t is defined to, including the integer
configuration in the custom build tests. In release builds the check
compiles out and is backstopped by ecs_sleepf, which now refuses
durations it cannot convert to the seconds and nanoseconds the OS sleep
API takes: converting a value of 2^31 seconds or more to int is
undefined behavior, and the same comparison rejects NaN. A refused sleep
returns immediately, is measured as the clock not keeping up, and rate
limiting is skipped for the frame instead of waiting.
Tests cover a stalled clock with a target FPS set, with both real and
no-op sleep functions; pacing accuracy against a sleep-driven simulated
clock at three work levels; time conservation on a coarse clock with a
target FPS; clocks just above and just below the keeping-up threshold,
which pin that the first reaches the target and the second stops where
the budget runs out; dither and host-stepped clocks, which bound the
total sleep requested per frame; the fps argument checks; and that ecs_sleepf never forwards
an unrepresentable duration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0 commit comments