Spent the last few days testing the optimizations and pushing them further.
We went at it pretty systematically this time. Split the whole project into small pieces, built every mechanic as an A/B pair against a reference build
and measured every pair on real hardware and in the calibrated emulator, always in the same session. Ended up with roughly 200 test ROMs.
The biggest wins, measured on the device in VBlanks per 30 frames, lower is faster:
worm hole search no longer scans the whole map, saved 16
spawn loop with a wave window, saved 9
mount matching cached, saved 8
ship probe cut from 12 point checks to at most 4 tile lookups, plus the wall
worm invariants, saved 6
music moved completely to the Z80 with its own sequencer, saved 5
boss checks skipped while no boss is alive, saved 3
metasprite chaining confirmed, removing the chain made it 5 worse
Bottom line, we went from about 146 to about 110, so roughly a quarter of the CPU time is gone.
The most interesting part was the gap between emulator and hardware, even though we had calibrated it before. For pure number crunching it is now within 5 to 10 % (sprite drawing factor 1.05, enemy update 1.08, terrain reload 1.00).
But it does not really know WHAT costs performance. Two cost classes are missing:
1. OAM and RAM bus traffic. A single extra work RAM read per sprite (40 to 60 sprites per frame) costs about 9 % on the device and exactly zero in the emulator. With the chaining the sign even flips.
2. Code size and branch density. A rewrite with completely identical behavior measured 1.6 % in the emulator but 7.7 % on the device. Instruction fetch dominates so hard on this silicon that compact code beats unrolled code.
My takeaway. The emulator finds and dissects the candidates, but the device decides the sign. A few supposedly safe rewrites died exactly that way, 9 and 10 VBlanks worse on the device with a neutral or even positive emulator prediction.
Code wise there is not much left to get. What remains is on the asset side (metasprite density in the tightest band, the b planes).
@sodthor, here is the honest scorecard for your suggestions, you earned it.
Went in and got confirmed on hardware (your first patch, all 31 hunks):
put_cell and put_cell_word as macros with a plane pointer and the flip bit moved up front, saves a far call and a branch per call UnsetSprite as a macro instead of a library function, 72 far calls gone
the pointer math in SetSpritePosition moved behind the early exit the mirrored word variants hoisted out of the anim_update cell loop the multiplication taken out of lvl1_put_row and the invariant cut of the inner collision loops
All together about 1.5 % in the frozen test scenes. And the measurement itself was worth gold, the emulator prediction was right down to the single unit.
Funny detail, when we checked your second patch it was almost entirely in our code already, 28 of 30 hunks matched. We had worked our way to the same spots independently, which was a nice confirmation for both sides.
Not taken, and every one of these was measured, not guessed:
The (spr >> 1414). cc900 silently masks the shift amount to 4 bits, so it becomes >> 6 instead of >> 14. Most of the gain at that spot (7.3 %) was skipped work, not faster code.
With the correct >> 14 an honest 1.5 % remains. Nasty compiler trap that we would never have found without you.
FPS 30. Pure game design decision, we stay at 20.
Bitsets instead of the .active loops. The counter test showed the scan is not the expensive part, the loop bodies are.
The SetSprite variant with the shadow read back. Exactly zero difference in the emulator but 10 VBlanks slower on the device. One extra work RAM read per sprite is really expensive on this silicon. So you accidentally uncovered the biggest gap in the emulator cost model, and that is now case B in our improvement report for Tixul.
And now the most important part.
THANK YOU for the great support! Your patch kicked off this whole measuring campaign, your hints put us on the right track more than once, and even the suggestions that did not make it into the game taught us things about the hardware we would never have learned otherwise.
This is exactly what homebrew collaboration should look like.
We went at it pretty systematically this time. Split the whole project into small pieces, built every mechanic as an A/B pair against a reference build
and measured every pair on real hardware and in the calibrated emulator, always in the same session. Ended up with roughly 200 test ROMs.
The biggest wins, measured on the device in VBlanks per 30 frames, lower is faster:
worm hole search no longer scans the whole map, saved 16
spawn loop with a wave window, saved 9
mount matching cached, saved 8
ship probe cut from 12 point checks to at most 4 tile lookups, plus the wall
worm invariants, saved 6
music moved completely to the Z80 with its own sequencer, saved 5
boss checks skipped while no boss is alive, saved 3
metasprite chaining confirmed, removing the chain made it 5 worse
Bottom line, we went from about 146 to about 110, so roughly a quarter of the CPU time is gone.
The most interesting part was the gap between emulator and hardware, even though we had calibrated it before. For pure number crunching it is now within 5 to 10 % (sprite drawing factor 1.05, enemy update 1.08, terrain reload 1.00).
But it does not really know WHAT costs performance. Two cost classes are missing:
1. OAM and RAM bus traffic. A single extra work RAM read per sprite (40 to 60 sprites per frame) costs about 9 % on the device and exactly zero in the emulator. With the chaining the sign even flips.
2. Code size and branch density. A rewrite with completely identical behavior measured 1.6 % in the emulator but 7.7 % on the device. Instruction fetch dominates so hard on this silicon that compact code beats unrolled code.
My takeaway. The emulator finds and dissects the candidates, but the device decides the sign. A few supposedly safe rewrites died exactly that way, 9 and 10 VBlanks worse on the device with a neutral or even positive emulator prediction.
Code wise there is not much left to get. What remains is on the asset side (metasprite density in the tightest band, the b planes).
@sodthor, here is the honest scorecard for your suggestions, you earned it.
Went in and got confirmed on hardware (your first patch, all 31 hunks):
put_cell and put_cell_word as macros with a plane pointer and the flip bit moved up front, saves a far call and a branch per call UnsetSprite as a macro instead of a library function, 72 far calls gone
the pointer math in SetSpritePosition moved behind the early exit the mirrored word variants hoisted out of the anim_update cell loop the multiplication taken out of lvl1_put_row and the invariant cut of the inner collision loops
All together about 1.5 % in the frozen test scenes. And the measurement itself was worth gold, the emulator prediction was right down to the single unit.
Funny detail, when we checked your second patch it was almost entirely in our code already, 28 of 30 hunks matched. We had worked our way to the same spots independently, which was a nice confirmation for both sides.
Not taken, and every one of these was measured, not guessed:
The (spr >> 1414). cc900 silently masks the shift amount to 4 bits, so it becomes >> 6 instead of >> 14. Most of the gain at that spot (7.3 %) was skipped work, not faster code.
With the correct >> 14 an honest 1.5 % remains. Nasty compiler trap that we would never have found without you.
FPS 30. Pure game design decision, we stay at 20.
Bitsets instead of the .active loops. The counter test showed the scan is not the expensive part, the loop bodies are.
The SetSprite variant with the shadow read back. Exactly zero difference in the emulator but 10 VBlanks slower on the device. One extra work RAM read per sprite is really expensive on this silicon. So you accidentally uncovered the biggest gap in the emulator cost model, and that is now case B in our improvement report for Tixul.
And now the most important part.
THANK YOU for the great support! Your patch kicked off this whole measuring campaign, your hints put us on the right track more than once, and even the suggestions that did not make it into the game taught us things about the hardware we would never have learned otherwise.
This is exactly what homebrew collaboration should look like.

