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:
1 parent
91908d1f15
commit
496de1a31f
10 files changed
+171
-81
No files matched your search
+64
-38
@@ -323,6 +323,14 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
|
||||
const bool hard = state.hardMode;
|
||||
const bool over = state.gameOver;
|
||||
|
||||
if (over) {
|
||||
if (bg_ctx.game_over_fade < 31) {
|
||||
bg_ctx.game_over_fade++;
|
||||
}
|
||||
} else {
|
||||
bg_ctx.game_over_fade = 0;
|
||||
}
|
||||
|
||||
// 1. Hardware Scrolling Update
|
||||
if (state.scene == Scene::Menu) {
|
||||
// Menu: Majestic downward drift for the cyber-horizon feel
|
||||
@@ -337,11 +345,7 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
|
||||
static_cast<uint16_t>(bg_ctx.scroll_sub_x_fp + 24U);
|
||||
} else {
|
||||
// In-Game Playfield:
|
||||
if (over) {
|
||||
// Gentle halt on game over
|
||||
bg_ctx.scroll_sub_x_fp =
|
||||
static_cast<uint16_t>(bg_ctx.scroll_sub_x_fp + 4U);
|
||||
} else if (cursed) {
|
||||
if (cursed && !over) {
|
||||
// High-speed warp and glitch jitter when cursed!
|
||||
const int16_t jitter =
|
||||
(bg_ctx.anim_phase % 4U == 0U) ? 0x0180 : -0x0100;
|
||||
@@ -353,7 +357,7 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
|
||||
bg_ctx.scroll_sub_y_fp = static_cast<uint16_t>(
|
||||
bg_ctx.scroll_sub_y_fp + 0x00E0U + (warp_intensity >> 1U));
|
||||
} else {
|
||||
// Smooth ambient drift scaling gradually with intensity
|
||||
// Smooth ambient drift scaling gradually with intensity (decelerating gently if game over)
|
||||
const uint16_t base_x = hard ? 24U : 16U;
|
||||
const uint16_t base_y = hard ? 20U : 12U;
|
||||
const uint16_t max_x = hard ? 76U : 56U;
|
||||
@@ -362,17 +366,34 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
|
||||
base_x + ((static_cast<uint32_t>(intensity_31) * (max_x - base_x)) / 31U));
|
||||
const uint16_t speed_y = static_cast<uint16_t>(
|
||||
base_y + ((static_cast<uint32_t>(intensity_31) * (max_y - base_y)) / 31U));
|
||||
|
||||
uint16_t sub_step_x = speed_x;
|
||||
uint16_t sub_step_y = speed_y;
|
||||
if (bg_ctx.game_over_fade > 0) {
|
||||
const uint8_t fade = bg_ctx.game_over_fade;
|
||||
sub_step_x = static_cast<uint16_t>(
|
||||
speed_x + ((static_cast<int>(4U - speed_x) * fade) / 31));
|
||||
sub_step_y = static_cast<uint16_t>(
|
||||
speed_y - ((static_cast<int>(speed_y) * fade) / 31));
|
||||
}
|
||||
bg_ctx.scroll_sub_x_fp =
|
||||
static_cast<uint16_t>(bg_ctx.scroll_sub_x_fp + speed_x);
|
||||
static_cast<uint16_t>(bg_ctx.scroll_sub_x_fp + sub_step_x);
|
||||
bg_ctx.scroll_sub_y_fp =
|
||||
static_cast<uint16_t>(bg_ctx.scroll_sub_y_fp + speed_y);
|
||||
static_cast<uint16_t>(bg_ctx.scroll_sub_y_fp + sub_step_y);
|
||||
}
|
||||
|
||||
// Top screen HUD / horizon drift scaling with intensity
|
||||
const uint16_t speed_main_y = static_cast<uint16_t>(
|
||||
// Top screen HUD / horizon drift scaling with intensity (halting on game over)
|
||||
uint16_t speed_main_y = static_cast<uint16_t>(
|
||||
16U + ((static_cast<uint32_t>(intensity_31) * 24U) / 31U));
|
||||
const uint16_t speed_main_x = static_cast<uint16_t>(
|
||||
uint16_t speed_main_x = static_cast<uint16_t>(
|
||||
8U + ((static_cast<uint32_t>(intensity_31) * 12U) / 31U));
|
||||
if (bg_ctx.game_over_fade > 0) {
|
||||
const uint8_t fade = bg_ctx.game_over_fade;
|
||||
speed_main_y = static_cast<uint16_t>(
|
||||
speed_main_y - ((static_cast<int>(speed_main_y) * fade) / 31));
|
||||
speed_main_x = static_cast<uint16_t>(
|
||||
speed_main_x - ((static_cast<int>(speed_main_x) * fade) / 31));
|
||||
}
|
||||
bg_ctx.scroll_main_y_fp =
|
||||
static_cast<uint16_t>(bg_ctx.scroll_main_y_fp + speed_main_y);
|
||||
bg_ctx.scroll_main_x_fp =
|
||||
@@ -401,18 +422,7 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
|
||||
uint16_t col_frame = 0;
|
||||
uint16_t col_frame_accent = 0;
|
||||
|
||||
if (over) {
|
||||
// Somber dark charcoal / muted red
|
||||
col_faint = make_rgb15(3, 2, 2);
|
||||
col_subtle = make_rgb15(6, 3, 3);
|
||||
col_primary = make_rgb15(10, 4, 4);
|
||||
col_star_dim = make_rgb15(5, 2, 2);
|
||||
col_star_med = make_rgb15(8, 3, 3);
|
||||
col_star_bright = make_rgb15(12, 4, 4);
|
||||
col_star_flare = make_rgb15(16, 6, 6);
|
||||
col_frame = make_rgb15(8, 3, 3);
|
||||
col_frame_accent = make_rgb15(12, 4, 4);
|
||||
} else if (cursed) {
|
||||
if (cursed && !over) {
|
||||
// Radioactive neon purple / violet storm
|
||||
const bool strobe = (bg_ctx.anim_phase % 6U < 3U);
|
||||
col_faint = make_rgb15(8, 1, 10);
|
||||
@@ -488,26 +498,42 @@ void bg_update(BackgroundContext &bg_ctx, const GameState &state,
|
||||
col_frame_accent = lerp_color(c_frame_accent_0, c_frame_accent_max, intensity_31);
|
||||
}
|
||||
|
||||
// Smoothly blend toward somber dark charcoal / muted red tone if game over
|
||||
if (bg_ctx.game_over_fade > 0) {
|
||||
const uint8_t fade = bg_ctx.game_over_fade;
|
||||
col_faint = lerp_color(col_faint, make_rgb15(3, 2, 2), fade);
|
||||
col_subtle = lerp_color(col_subtle, make_rgb15(6, 3, 3), fade);
|
||||
col_primary = lerp_color(col_primary, make_rgb15(10, 4, 4), fade);
|
||||
col_star_dim = lerp_color(col_star_dim, make_rgb15(5, 2, 2), fade);
|
||||
col_star_med = lerp_color(col_star_med, make_rgb15(8, 3, 3), fade);
|
||||
col_star_bright = lerp_color(col_star_bright, make_rgb15(12, 4, 4), fade);
|
||||
col_star_flare = lerp_color(col_star_flare, make_rgb15(16, 6, 6), fade);
|
||||
col_frame = lerp_color(col_frame, make_rgb15(8, 3, 3), fade);
|
||||
col_frame_accent = lerp_color(col_frame_accent, make_rgb15(12, 4, 4), fade);
|
||||
}
|
||||
|
||||
// Deep space backdrop color dynamically illuminates with intensity
|
||||
if (in_game && !over) {
|
||||
if (in_game) {
|
||||
uint16_t bg_sub_target = 0;
|
||||
uint16_t bg_main_target = 0;
|
||||
if (hard) {
|
||||
BG_PALETTE_SUB[0] = lerp_color(make_rgb15(3, 1, 1), make_rgb15(6, 2, 2), intensity_31);
|
||||
BG_PALETTE[0] = lerp_color(make_rgb15(2, 1, 1), make_rgb15(5, 1, 2), intensity_31);
|
||||
bg_sub_target = lerp_color(make_rgb15(3, 1, 1), make_rgb15(6, 2, 2), intensity_31);
|
||||
bg_main_target = lerp_color(make_rgb15(2, 1, 1), make_rgb15(5, 1, 2), intensity_31);
|
||||
} else {
|
||||
BG_PALETTE_SUB[0] = lerp_color(make_rgb15(3, 4, 7), make_rgb15(5, 7, 12), intensity_31);
|
||||
BG_PALETTE[0] = lerp_color(make_rgb15(2, 3, 6), make_rgb15(3, 5, 10), intensity_31);
|
||||
bg_sub_target = lerp_color(make_rgb15(3, 4, 7), make_rgb15(5, 7, 12), intensity_31);
|
||||
bg_main_target = lerp_color(make_rgb15(2, 3, 6), make_rgb15(3, 5, 10), intensity_31);
|
||||
}
|
||||
|
||||
if (bg_ctx.game_over_fade > 0) {
|
||||
BG_PALETTE_SUB[0] = lerp_color(bg_sub_target, make_rgb15(3, 2, 2), bg_ctx.game_over_fade);
|
||||
BG_PALETTE[0] = lerp_color(bg_main_target, make_rgb15(2, 1, 1), bg_ctx.game_over_fade);
|
||||
} else {
|
||||
BG_PALETTE_SUB[0] = bg_sub_target;
|
||||
BG_PALETTE[0] = bg_main_target;
|
||||
}
|
||||
} else if (over) {
|
||||
BG_PALETTE_SUB[0] = make_rgb15(3, 2, 2);
|
||||
BG_PALETTE[0] = make_rgb15(2, 1, 1);
|
||||
} else {
|
||||
if (hard) {
|
||||
BG_PALETTE_SUB[0] = make_rgb15(3, 1, 1);
|
||||
BG_PALETTE[0] = make_rgb15(2, 1, 1);
|
||||
} else {
|
||||
BG_PALETTE_SUB[0] = make_rgb15(2, 2, 6);
|
||||
BG_PALETTE[0] = make_rgb15(2, 2, 6);
|
||||
}
|
||||
BG_PALETTE_SUB[0] = make_rgb15(2, 2, 6);
|
||||
BG_PALETTE[0] = make_rgb15(2, 2, 6);
|
||||
}
|
||||
|
||||
// Animate twinkling stars using sine oscillations
|
||||
|
||||
@@ -65,6 +65,7 @@ bool handle_input_frame(InputContext &ctx) {
|
||||
if ((ctx.state->paused || ctx.state->gameOver) && (keys != 0U) && ((keys & KEY_B) != 0U)) {
|
||||
ctx.state->scene = Scene::Menu;
|
||||
ctx.state->paused = false;
|
||||
ctx.state->gameOver = false;
|
||||
setBackdropColor(RGB15(2, 2, 6));
|
||||
setBackdropColorSub(RGB15(2, 2, 6));
|
||||
draw_menu(*ctx.state, *ctx.renderAssets, audio_is_music_enabled(*ctx.audio));
|
||||
@@ -182,6 +183,8 @@ bool handle_input_frame(InputContext &ctx) {
|
||||
} else {
|
||||
ctx.init_game();
|
||||
}
|
||||
} else if (ctx.state->gameOver && (keys != 0U) && ((keys & KEY_A) != 0U)) {
|
||||
ctx.init_game();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -47,6 +47,7 @@ bool check_collision_self(GameState &state) {
|
||||
for (auto it = state.snake.begin() + 1; it != state.snake.end(); ++it) {
|
||||
if ((*it)[0] == headpos[0] && (*it)[1] == headpos[1]) {
|
||||
state.gameOver = true;
|
||||
state.gameOverReason = GameOverReason::SelfCollision;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -91,6 +92,7 @@ bool check_collision_apples_rotten(GameState &state) {
|
||||
state.bad_apples.erase(it);
|
||||
if (state.snakelength < 2) {
|
||||
state.gameOver = true;
|
||||
state.gameOverReason = GameOverReason::RottenAppleDepleted;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -471,6 +473,7 @@ bool check_collision_barriers(GameState &state) {
|
||||
const auto &head = state.snake.front();
|
||||
if (is_barrier_cell(state, head[0], head[1], false)) {
|
||||
state.gameOver = true;
|
||||
state.gameOverReason = GameOverReason::BarrierCollision;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
+51
-24
@@ -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 ");
|
||||
|
||||
+29
-11
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user