XENON (NGPC) how we got the frame rate stable
The big one: the emulator runs without wait states. NgpCraft can model them, but only if you ask it to. The core ships with Default cart_wait=0. Silicon-calibrated values, confirmed on our own hardware with the emulator author's calibration ROM: cart_wait 3, cart_data_wait 0, ldir_cost 14 (the datasheet's 7 is a floor), vram_wait 3.
That is a factor of 2.88 same scene, same build: 99,638 vs 286,958 cycles per frame. Code that reads a lot of instruction bytes is otherwise ~3.4x too cheap, which is exactly why plausible optimisations kept measuring "exactly zero" for us. It also explained our hardware readings: the 30 fps build needed 3.01 VBlanks per frame — the game never ran at 30 fps.
Two measurement traps. You cannot measure a block that creates work by switching it off: our enemy dispatch loop reported 11.9 % that way, the real figure is 1.3 % (the rest was "no enemies, so no drawing and no collisions"). And the test scene is part of the measurement the same pass was worth 18 %, 8 % and 3 % in three different scenes. We optimised the wrong one for a full round and saw nothing of it on hardware.
What helped, 45 % together:
Skip off-screen objects entirely: 27 %. Ours had an animation tick running independently of their position, so the draw routine searched every frame for screen rows that didn't exist for them. But the guard must be cheaper than what it prevents — our first version used the same search as the guard and was 48 % slower than no guard at all.
Linear searches out of per-frame loops: 16 %. One ran per cell instead of per row; a one-entry cache was enough.
Function calls out of cell loops two per cell, each re-deriving the same values.
Struct sizes to powers of two: 1–13 %. Otherwise cc900 emits a 16-bit multiply for every field[i].x (26 cycles vs 2 for a register move). It can backfire on small structs though: field displacements leave the 8-bit range and the code grows. Padding 18 → 32 bytes cost us 1.2 %.
Pointers instead of indices only where register pressure is low the same change: −2.5 % in a draw loop, +1.9 % in an update loop.
What didn't help: flicker multiplexing for player shots (+1.4 %), drawing every second frame (only pays as a whole; the separable parts are 1.7 % together), halving the enemy update (+9.6 %, enemies stop despawning).
Result: a constant 20 fps with headroom instead of a wobbly 20-to-15. That's as far as we got with this object count — a pro can probably squeeze out a bit more.
Sorry for the German comments ?
If you find anything else, that would be great, though I doubt it'll be possible to get the project running at a steady 30 fps.
I've updated GitHub

