CNA-BUG-080: The Emscripten Game loop (EmscriptenMainLoopCallback) does not implement Game's loop contract

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

In the browser, Game ignores IsFixedTimeStep = false, SuppressDraw and ResetElapsedTime, uses FNA's clock order instead of the XNA rule the desktop loop implements, and turns a frame exception into a normal return from Run() without Exiting.

Identifier
CNA-BUG-080
Category
Bug
Subsystem
Core & runtime
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Medium (a triage suggestion, not a project priority)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
Microsoft::Xna::Framework::Game::Run() under Emscripten; IsFixedTimeStep, SuppressDraw(), ResetElapsedTime(), GameTime, the Exiting event

Expected behaviour

The same Game contract as the native Game::Tick path: variable-step updates when IsFixedTimeStep is false; SuppressDraw() and ResetElapsedTime() honoured; the XNA clock rule CNA measured and pins on the desktop (first update with zero ElapsedGameTime, TotalGameTime as the time before the step); IsRunningSlowly meaningful; and a frame exception leaving Run() as it does natively (Run() logs and rethrows).

Actual behaviour at TARGET

Game::EmscriptenMainLoopCallback keeps its own millisecond accumulator and, on every frame: caps the delta at 250 ms; drains whole TargetElapsedTime steps whatever IsFixedTimeStep says; gives the first update a full step; adds the step to TotalGameTime before Update; forces IsRunningSlowly to false; draws only if an update ran; and never reads suppressDraw_, forceElapsedTimeToZero_ or isSuspended_. Its catch blocks log a frame exception, clear the loop slot and return, so RunLoop ends, Run() calls EndRun() and AfterLoop() and returns normally, and Exiting is never raised.

Source locations

Evidence

Checked by reading both loop bodies in Game.cpp at 009d40f5. The native rule is pinned by GameClockFirstUpdateTests.cpp, whose header records values measured on XNA 4.0. No CNA document describes the browser loop's different contract; the libcna.com guide warns about it. Not executed, and no browser build was run.

Independently observed as a separate finding (merged): EmscriptenMainLoopCallback catches every exception from a frame, logs it and clears the loop slot, so RunLoop ends, Run calls EndRun/AfterLoop and returns, and the game's caller cannot observe the failure.

Independently observed as a separate finding (merged): The browser frame callback keeps its own accumulator, caps a frame at 250 ms, always drains fixed steps and never reads isSuspended_, suppressDraw_ or forceElapsedTimeToZero_, so pause/background handling and slow-frame semantics differ from the native loop.

Focused reproduction

No focused reproduction is known. Nothing has been invented here; the evidence above is what exists.

Current tests

The clock tests run on the native loop only. The web examples exercise the browser loop end to end but assert no clock values; nothing tests IsFixedTimeStep = false, SuppressDraw() or frame-exception propagation under Emscripten.

Regression test

Factor the frame body so that its timing decisions can run without a browser, then run GameClockFirstUpdateTests' scenarios and a SuppressDraw()/variable-step scenario against it; add a browser smoke test that throws from Update and expects Run() to propagate it.

Blast radius

Every game built for the web target: variable-step games run fixed-step, state that accumulates per update is one step ahead of the desktop build, SuppressDraw() has no effect, and a crash in a frame looks to main() like a normal exit. Native targets are unaffected.

Workaround

For code that must behave identically on desktop and web, do not depend on the zero first update, on variable-step elapsed times, on IsRunningSlowly or on SuppressDraw(); subscribe to Exiting for cleanup only on the desktop and check the log for fatal frame errors on the web.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index