fix: decouple VBlank ISR and main loop to prevent IRQ stack overflow

- Make handle_vblank strictly increment frame counter (frame++)
- Move heavy frame logic (background updates, OAM updates, palette animations, and console stats) to update_frame() on the main thread synchronized with swiWaitForVBlank()
- Eliminate 256-byte ARM9 IRQ stack (sp_irq) overflow caused by bg_update and iprintf that was corrupting DTCM stack and causing data aborts
- Preserve all playfield entities on game over with top-screen defeat summary and smooth background deceleration

Co-authored-by: Antigravity <antigravity@google.com>
This commit is contained in:
phillipandAntigravity committed 2026-10-04 00:21:09 +02:00
1 parent 91908d1f15
commit 496de1a31f
10 files changed
+171 -81

No files matched your search

+51 -24
View File
@@ -153,38 +153,62 @@ void draw_apples_cursed(const GameState &state, const RenderAssets &assets,
} // namespace
void draw_game_over_screen(const GameState &state) {
iprintf("\x1b[1;2H============================");
iprintf("\x1b[2;2H * GAME OVER * ");
iprintf("\x1b[3;2H============================");
iprintf("\x1b[5;4HFinal Score: %5i", state.score);
iprintf("\x1b[6;4HApples Eaten: %5i", state.applesEaten);
iprintf("\x1b[7;4HSnake Length: %5i", state.snakelength);
iprintf("\x1b[8;4HDifficulty: %s", state.hardMode ? "Hard " : "Normal");
iprintf("\x1b[10;4HCause of Defeat:");
const char *cause = "Collision";
switch (state.gameOverReason) {
case GameOverReason::SelfCollision:
cause = "Bite Collision (Hit Body)";
break;
case GameOverReason::BarrierCollision:
cause = "Energy Barrier Impact";
break;
case GameOverReason::RottenAppleDepleted:
cause = "Depleted by Rotten Apple";
break;
default:
cause = "Collision";
break;
}
iprintf("\x1b[11;4H %s", cause);
iprintf("\x1b[14;2H----------------------------");
iprintf("\x1b[16;4HA / START : Play Again");
iprintf("\x1b[17;4HB : Main Menu");
iprintf("\x1b[18;4HR : Swap Screens");
iprintf("\x1b[20;2H============================");
}
void invalidate_hud(RenderAssets &assets) { assets.hudCache.dirty = true; }
void draw_game(const GameState &state, const RenderAssets &assets) {
// OAM slots below BARRIER_OAM_SLOTS are owned by draw_barriers() (VBlank)
uint16_t oamId = BARRIER_OAM_SLOTS;
if (!state.gameOver) {
draw_apples(state, assets, oamId);
draw_apples_rotten(state, assets, oamId);
draw_apples_cursed(state, assets, oamId);
draw_snake(state, assets, oamId);
draw_apples(state, assets, oamId);
draw_apples_rotten(state, assets, oamId);
draw_apples_cursed(state, assets, oamId);
draw_snake(state, assets, oamId);
// Hide all unused sprites up to hardware limit (128) to eliminate trailing
// ghost sprites
if (oamId < 128) {
oamClear(&oamSub, oamId, 128 - oamId);
}
return;
// Hide all unused sprites up to hardware limit (128) to eliminate trailing
// ghost sprites
if (oamId < 128) {
oamClear(&oamSub, oamId, 128 - oamId);
}
// On Game Over, hide all sprites so none are left dangling
oamClear(&oamSub, 0, 128);
iprintf("\x1b[11;12HGAME OVER!");
consoleSelect(assets.topScreen);
iprintf("\x1b[10;10H Score: %i ", state.score);
iprintf("\x1b[11;12HGAME OVER!");
}
void draw_barriers(const GameState &state, const RenderAssets &assets,
uint8_t frame) {
if (state.scene != Scene::Playing || state.gameOver || !state.hardMode) {
if (state.scene != Scene::Playing || !state.hardMode) {
oamClear(&oamSub, 0, BARRIER_OAM_SLOTS);
return;
}
@@ -257,6 +281,12 @@ void draw_barriers(const GameState &state, const RenderAssets &assets,
void draw_stats(const GameState &state, RenderAssets &assets, bool internals,
bool musicEnabled, uint8_t frame) {
if (state.gameOver) {
return;
}
HudCache &cache = assets.hudCache;
if (internals) {
iprintf("\x1b[0;0HFrame = %i\nTick = %i\ndir = %i", frame, state.ticks,
state.snakeDirection);
@@ -279,7 +309,6 @@ void draw_stats(const GameState &state, RenderAssets &assets, bool internals,
}
}
HudCache &cache = assets.hudCache;
const bool cursed = (state.curseTicksRemaining > 0);
const bool needs_score_update = cache.dirty || (cache.score != state.score);
@@ -305,9 +334,7 @@ void draw_stats(const GameState &state, RenderAssets &assets, bool internals,
if (needs_pause_update) {
if (!state.paused) {
if (state.gameOver) {
iprintf("\x1b[23;0HB = Main Menu");
} else if (cache.paused) {
if (cache.paused) {
// Clear any leftover pause strings if previously paused
iprintf("\x1b[21;0H ");
iprintf("\x1b[22;0H ");