stage: fix pause menu related crashes

There were two distinct things going on here:

1. If we receive multiple buffered TE_GAME_PAUSE events in the same
   frame, we'd process all of them and create a pause menu for each.
   This could theoretically overflow the evloop stack and crash the
   game.
2. The `stage_comain` task starts before scheduling the stage main
   loop via `eventloop_enter`. It initializes systems that depend on
   tasks, and then immediatelly enters its per-frame async loop,
   finishing its first iteration before yielding back to `_stage_enter`
   and thus allowing `eventloop_enter` to be finally called. If there is
   a TE_GAME_PAUSE event in the queue at this point, it would be handled
   right there, and a pause menu would be created before the stage main
   loop is scheduled. This messes things up quite a bit, leaking a
   "zombie" pause menu into the evloop stack. After the stage is
   destroyed, the evloop would try to switch to the frame created for
   this menu. The menu's draw function would then attempt to reference
   free'd resources of the destroyed stage, crashing the game. This
   crash has actually been observed and reported (thanks @0kalekale)

To fix #1, the stage now tracks its paused state and refuses to open a
pause menu if one already exists.

To fix #2, `stage_comain` now yields before starting its async loop, to
let the stage set up its main loop early.

Note that because the stage main loop runs all coroutine tasks before
incrementing the frame counter, `stage_comain`'s per-frame logic would
execute twice on frame 0. This is obviously wrong, but this behavior
must be preserved to maintain compatibility with v1.4 replays. For that
reason, the `stage_comain` loop now skips its first YIELD. This hack can
be removed once v1.4 compat is no longer a concern.
This commit is contained in:
Andrei Alexeyev 2024-04-20 21:27:31 +02:00
parent e54b144973
commit 4a50c85574
No known key found for this signature in database
GPG key ID: 72D26128040B9690
2 changed files with 42 additions and 15 deletions

View file

@ -40,6 +40,7 @@ typedef struct StageFrameState {
bool quicksave_is_automatic;
bool quickload_requested;
bool was_skipping;
bool paused;
uint32_t dynstage_generation;
int transition_delay;
int desync_check_freq;
@ -255,7 +256,17 @@ static void stage_enter_ingame_menu(MenuData *m, BGM *bgm, CallChain next) {
enter_menu(m, CALLCHAIN(stage_leave_ingame_menu, ctx));
}
void stage_pause(void) {
static void stage_unpause(CallChainResult ccr) {
StageFrameState *fstate = ccr.ctx;
fstate->paused = false;
}
static void stage_pause(StageFrameState *fstate) {
if(fstate->paused) {
log_debug("Pause request ignored, already paused");
return;
}
if(global.gameover == GAMEOVER_TRANSITIONING || stage_is_skip_mode()) {
return;
}
@ -268,7 +279,8 @@ void stage_pause(void) {
m = create_ingame_menu();
}
stage_enter_ingame_menu(m, NULL, NO_CALLCHAIN);
fstate->paused = true;
stage_enter_ingame_menu(m, NULL, CALLCHAIN(stage_unpause, fstate));
}
void stage_gameover(void) {
@ -288,6 +300,7 @@ void stage_gameover(void) {
}
static bool stage_input_common(SDL_Event *event, void *arg) {
StageFrameState *fstate = NOT_NULL(arg);
TaiseiEvent type = TAISEI_EVENT(event->type);
int32_t code = event->user.code;
@ -306,7 +319,7 @@ static bool stage_input_common(SDL_Event *event, void *arg) {
break;
case TE_GAME_PAUSE:
stage_pause();
stage_pause(fstate);
break;
default:
@ -522,10 +535,12 @@ static void leave_replay_mode(StageFrameState *fstate, ReplayState *rp_in) {
static void replay_input(StageFrameState *fstate) {
if(!should_skip_frame(fstate)) {
events_poll((EventHandler[]){
{ .proc =
stage_is_demo_mode()
? stage_input_handler_demo
: stage_input_handler_replay
{
.proc =
stage_is_demo_mode()
? stage_input_handler_demo
: stage_input_handler_replay,
.arg = fstate,
},
{ NULL }
}, EFLAG_GAME);
@ -1011,6 +1026,14 @@ static void stage_end_loop(void *ctx);
static void stage_stub_proc(void) { }
static void stage_preload_stub_proc(ResourceGroup *rg) { }
static void process_input(StageFrameState *fstate) {
if(global.replay.input.replay != NULL) {
replay_input(fstate);
} else {
stage_input(fstate);
}
}
TASK(stage_comain, { StageFrameState *fstate; }) {
StageFrameState *fstate = ARGS.fstate;
StageInfo *stage = fstate->stage;
@ -1022,13 +1045,12 @@ TASK(stage_comain, { StageFrameState *fstate; }) {
display_stage_title(stage);
}
for(;;YIELD) {
if(global.replay.input.replay != NULL) {
replay_input(fstate);
} else {
stage_input(fstate);
}
YIELD;
bool first_frame = true;
for(;;) {
process_input(fstate);
process_boss(&global.boss);
process_enemies(&global.enemies);
process_projectiles(&global.projs, true);
@ -1053,6 +1075,13 @@ TASK(stage_comain, { StageFrameState *fstate; }) {
ent_damage(&global.boss->ent, &(DamageInfo) { 400, DMG_PLAYER_SHOT } );
}
}
if(first_frame) {
// HACK: Run frame 0 logic twice for compatibility with a v1.4 bug
first_frame = false;
} else {
YIELD;
}
}
}

View file

@ -35,8 +35,6 @@ typedef struct StageClearBonus {
void stage_enter(StageInfo *stage, ResourceGroup *rg, CallChain next);
void stage_finish(int gameover);
void stage_pause(void);
void stage_gameover(void);
void stage_start_bgm(const char *bgm);