CNA-GAP-063: A fixed-step TargetElapsedTime above 500 ms never produces an Update from Game::Tick, essentially the same ceiling XNA 4.0 has
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.
With IsFixedTimeStep true, Game::Tick clamps the accumulator to MaxElapsedTime (500 ms) before its update loop, so a TargetElapsedTime above 500 ms is never reached: Update does not run while Draw keeps running, at about 1 / (target - 500 ms) per second (twice a second for a one-second target). XNA 4.0's Tick has essentially the same ceiling (about 508 ms, because of its 1/64 snap), but neither updates nor draws above it, so the limit is inherited; the setter accepts such a value silently.
- Identifier
CNA-GAP-063- Category
- Functional gap
- Subsystem
- Core & runtime
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- Microsoft::Xna::Framework::Game::setTargetElapsedTimeProperty(const System::TimeSpan&) with IsFixedTimeStep true; Game::Tick()
Expected behaviour
Microsoft's documentation says a fixed-step Game tries to call Update on the interval in TargetElapsedTime, and the setter rejects only zero and negative values (as CNA's does), so a one-second target might be expected to update once a second. Neither XNA 4.0 nor CNA delivers that: the effective contract is a target of at most about 500 ms. CNA's Game.hpp documents only 'positive and non-zero'.
Actual behaviour at TARGET
Game::Tick waits until accumulatedElapsedTime_ reaches TargetElapsedTime_, polls events, then clamps the accumulator to MaxElapsedTime (500 ms); the update loop 'while accumulated >= TargetElapsedTime_' therefore never runs for a target above 500 ms, stepCount stays zero, and the tick still draws, about twice a second. XNA 4.0 (IL of Game.Tick and GameClock) clamps the per-tick elapsed time to 500 ms, snaps an elapsed value within 1/64 of the target to the target, divides the sum with its leftover accumulator by the target and, when the quotient is zero, returns before GameClock.AdvanceFrameTime and before storing the sum, so its accumulator never reaches a target above about 508 ms and it neither updates nor draws. The difference is that CNA keeps drawing and that a target between 500 and 508 ms works in XNA only.
Source locations
modules/runtime/src/Game.cpp— Game::Tick (wait loops, MaxElapsedTime clamp, fixed-step while loop); Game::setTargetElapsedTimeProperty; Game::MaxElapsedTimemodules/runtime/include/Microsoft/Xna/Framework/Game.hpp— setTargetElapsedTimeProperty documents only 'positive and non-zero'
Evidence
Checked by reading Game::Tick in Game.cpp at 009d40f5, and compared with the IL of XNA 4.0's Game.Tick and set_TargetElapsedTime (a disassembly of Microsoft.Xna.Framework.Game) and with FNA's Game.Tick. The failure follows mechanically from the order “clamp, then compare”. Not executed.
Independent re-verification: Checked by reading Game::Tick at 009d40f5 and the disassembled IL of XNA 4.0's Game.Tick and GameClock (xna4-decomp dlls); the earlier claim that XNA accumulates across ticks and updates once per second is not supported by that IL. Not executed.
Independent re-verification: Checked by reading Game::Tick in Game.cpp at 009d40f5, and compared with the IL of XNA 4.0's Game.Tick and GameClock and with FNA's Game.Tick. The failure follows mechanically from the order 'clamp, then compare'. A pure-Python model of both Tick functions on a virtual clock (audit run, not the real code) agrees: at 505 and 507 ms XNA updates and CNA does not, and from about 508 ms XNA neither updates nor draws while CNA never updates and draws at 1 / (target - 500 ms). The Focused reproduction should end 'XNA 4.0 would not update either; at 009d40f5 game.updates == 0 and Draw runs about twice a second'.
Focused reproduction
Illustrative, not compiled:
class SlowGame : public Game {
public:
int updates = 0;
SlowGame() { setTargetElapsedTimeProperty(System::TimeSpan::FromSeconds(1.0)); }
protected:
void Update(GameTime& t) override { ++updates; Game::Update(t); }
};
SlowGame game;
GraphicsDeviceManager graphics(&game);
for (int i = 0; i < 4; ++i) game.RunOneFrame(); // about 2.5 s of wall time
// XNA 4.0 would not update either; at 009d40f5 game.updates == 0 and Draw runs about twice a second
Current tests
GamePlatformTimingTests.cpp drives single frames with a 1 ms target; GameClockFirstUpdateTests.cpp pins the first-update and total-time rules at ordinary targets. No test uses a target above MaxElapsedTime.
Regression test
A clock test with a scripted performance counter (the platform decorator used by the timing tests) that sets a 1 s target, advances the counter by 1.2 s per tick and expects one update per target interval, plus the boundary case of exactly 500 ms.
Blast radius
Only fixed-step games whose TargetElapsedTime is longer than 500 ms (slow simulations, clocks, turn timers, tests that step slowly). Variable-step games and all ordinary frame rates are unaffected. The browser loop has its own clock and a 250 ms cap and is not covered by this entry.
Workaround
Keep TargetElapsedTime at or below 500 ms and count ticks in Update, or use IsFixedTimeStep = false and accumulate ElapsedGameTime yourself.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- Architecture
- Runtime lifecycle: what one Tick does
- Internals
- One frame source trace: the clock
- Known issues
- Functional gap index