replay: don't leave MOVIE_END set when playback init fails (#19622)

bsv_movie_init_playback() reads the first checkpoint and the first
frame's events before the handle is installed. Every short-read branch
in bsv_movie_load_checkpoint() and bsv_movie_read_next_events() (the
latter called with end_movie=true from bsv_movie_reset_playback()) sets
BSV_FLAG_MOVIE_END on the global input state. When init then fails, the
handle is freed and never enqueued, so BSV_FLAG_MOVIE_PLAYBACK is never
set alongside MOVIE_END.

The run loop checks MOVIE_END after every frame and issues
movie_stop() + CMD_EVENT_PAUSE. movie_stop() only clears MOVIE_END via
movie_stop_playback()/movie_stop_record(), which it never reaches
without the PLAYBACK/RECORDING flag, so the flag stays set forever:
every pause toggle runs exactly one frame and re-pauses, input looks
dead, and since Close Content goes through the same movie_stop() the
flag carries into the next core and content. Only a process restart
clears it. A truncated replay is one trigger; a replay halted before
any frame was recorded (EOF at the first frame) is another.

Fix:
- bsv_movie_init_internal(): clear MOVIE_END on the error path. A
  handle that failed to initialise leaves no flag behind.
- movie_stop(): also clear MOVIE_END when no movie is active, so a
  stray flag never survives Close Content.
- bsv_movie_start_playback(): push the "failed to load movie file"
  OSD message on failure. It runs from the task callback after
  CMD_EVENT_PLAY_REPLAY already returned success, so the command's own
  failure message never fired for an unreadable file.

Signed-off-by: LibretroAdmin <[email protected]>
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-09-26 01:39:33 +02:00
1 parent b40db2251a
commit 368b842086
1 file changed
+24 -3
+24 -3
View File
@@ -251,6 +251,17 @@ static bsv_movie_t *bsv_movie_init_internal(const char *path, enum rarch_movie_t
return handle;
error:
/* The playback init path reads the first checkpoint and the first
* frame's events before the handle is installed. On a short read
* those readers set BSV_FLAG_MOVIE_END on the global input state,
* but the handle is freed here and never enqueued, so the PLAYBACK
* flag is never set alongside it. Left behind, MOVIE_END makes the
* run loop issue CMD_EVENT_PAUSE after every frame while
* movie_stop() has no PLAYBACK/RECORDING flag to clear it through:
* a permanent pause that survives Close Content and only ends with
* a process restart (#19622). A handle that failed to initialise
* must not leave any flag behind. */
input_state_get_ptr()->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_END;
if (handle)
bsv_movie_free(handle);
return NULL;
@@ -295,9 +306,14 @@ static bool bsv_movie_start_playback(input_driver_state_t *input_st, char *path)
input_st->bsv_movie_state_handle. */
if (!(state = bsv_movie_init_internal(path, RARCH_MOVIE_PLAYBACK)))
{
RARCH_ERR("[Replay] %s: \"%s\".\n",
msg_hash_to_str(MSG_FAILED_TO_LOAD_MOVIE_FILE),
path);
/* This runs from the task callback, after CMD_EVENT_PLAY_REPLAY
* has already returned success, so the command's own failure
* message never fires for an unreadable file. Tell the user
* here, as the record path does. */
_msg = msg_hash_to_str(MSG_FAILED_TO_LOAD_MOVIE_FILE);
runloop_msg_queue_push(_msg, strlen(_msg), 1, 180, true, NULL,
MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
RARCH_ERR("[Replay] %s: \"%s\".\n", _msg, path);
return false;
}
@@ -459,6 +475,11 @@ bool movie_stop(input_driver_state_t *input_st)
return movie_stop_playback(input_st);
else if (input_st->bsv_movie_state.flags & BSV_FLAG_MOVIE_RECORDING)
return movie_stop_record(input_st);
/* No movie is active, so nothing above cleared the flags. Drop a
* stray MOVIE_END here too: the run loop pauses every frame while
* it is set, and this is also what Close Content calls, so it must
* not carry the flag into the next content. */
input_st->bsv_movie_state.flags &= ~BSV_FLAG_MOVIE_END;
if (input_st->bsv_movie_state_handle)
RARCH_ERR("[Replay] Didn't really stop movie!\n");
return true;