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

+29 -11
View File
@@ -205,19 +205,33 @@ void setup() {
}
void handle_vblank() {
frame++;
}
void update_frame() {
bg_update(bgContext, gameState, frame);
if (gameState.scene == Scene::Playing) {
consoleSelect(&topScreen);
draw_stats(gameState, renderAssets, internals,
audio_is_music_enabled(audioContext), frame);
if (!gameState.paused) {
tick_barriers_frame(gameState);
if (gameState.gameOver) {
HudCache &cache = renderAssets.hudCache;
if (cache.dirty || (cache.gameOver != gameState.gameOver)) {
consoleClear();
draw_game_over_screen(gameState);
cache.gameOver = gameState.gameOver;
cache.dirty = false;
}
} else {
draw_stats(gameState, renderAssets, internals,
audio_is_music_enabled(audioContext), frame);
if (!gameState.paused) {
tick_barriers_frame(gameState);
}
}
draw_barriers(gameState, renderAssets, frame);
}
// Dynamic pulsing radioactive glow for Cursed Apples (smooth 60fps breathing)
const uint8_t pulse = (frame >> 2U) & 7U; // 8-step cycle
const auto pulse = static_cast<uint8_t>(
(static_cast<uint32_t>(frame) >> 2U) & 7U); // 8-step cycle
const uint8_t wave =
(pulse < 4U) ? pulse
: static_cast<uint8_t>(7U - pulse); // 0, 1, 2, 3, 3, 2, 1, 0
@@ -233,8 +247,9 @@ void handle_vblank() {
// Dynamic festering mold & spore respiration for Rotten Apples (smooth 60fps
// organic pulse)
const uint8_t rot_pulse = ((frame + 3U) >> 2U) &
7U; // 8-step cycle, phase-shifted from cursed apple
const auto rot_pulse = static_cast<uint8_t>(
(static_cast<uint32_t>(frame + 3U) >> 2U) &
7U); // 8-step cycle, phase-shifted from cursed apple
const uint8_t rot_wave =
(rot_pulse < 4U)
? rot_pulse
@@ -292,6 +307,7 @@ void do_tick() {
if (!was_game_over && gameOver) {
audio_stop_music();
audio_play_fail(audioContext);
invalidate_hud(renderAssets);
}
// consoleSelect(&bottomScreen);
draw_game(gameState, renderAssets);
@@ -309,6 +325,7 @@ void init_game() {
score = 0;
ticks = 0;
gameOver = false;
gameState.gameOverReason = GameOverReason::None;
snakeDirection = 1;
gameState.curseTicksRemaining = 0;
apples.clear();
@@ -338,10 +355,6 @@ void init_game() {
}
int main(void) {
// Set up VBlank handler
irqSet(IRQ_VBLANK, handle_vblank);
setup();
setBackdropColor(RGB15(2, 2, 6));
setBackdropColorSub(RGB15(2, 2, 6));
@@ -354,7 +367,12 @@ int main(void) {
inputContext.topScreen = &topScreen;
inputContext.init_game = init_game;
// Set up VBlank handler only after all subsystems, assets, and contexts are initialized
irqSet(IRQ_VBLANK, handle_vblank);
while (1) {
swiWaitForVBlank();
update_frame();
if (!handle_input_frame(inputContext)) {
break;
}