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

CNA snapshot 009d40f5  ·  Known Issues › Functional gaps  ·  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.

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

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.

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