Check the movie decoder under its lock in DecodeAsync, so a sample after Finalize cannot hold the movie frame rate cap.

ov_decode tested v->dav1d without a lock and then queued the sample under v->lock and ran set_live(v, 1). A
DecodeAsync on one guest thread racing Finalize on another could pass the test, wait for the lock while finalize
closed the decoder and cleared the queues, and then queue the sample and mark the finalized decoder live again:
movies_live stayed 1 with no decoder until the destructor or the next Initialize, and frame_rate.c movie_cap_hz kept
the movie's frame rate cap meanwhile.

ov_decode now reads v->dav1d again under v->lock and returns without queueing when it is gone. finalize takes v->lock
after it clears v->dav1d, so the read either sees it set (the sample is queued before finalize's clear_queues, which
drops it, and ov_finalize or the destructor then clears live) or sees NULL.

Not reproduced before the change: the game's own ordering normally keeps DecodeAsync and Finalize apart, and no unit
check exists without dav1d stubs. movie_planes_check passes, and the compile check adds no warnings. The movie
play-test's "frame rate: the movie ended" line after each movie confirms it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 17:28:29 -05:00
parent af06160284
commit 6029321eff

View File

@@ -782,6 +782,15 @@ static void ov_decode(Cpu *c) {
if (skip_ns >= 0) rt_log("movie: movies end after %.1f s (NX2NAVI_MOVIE_SKIP)", skip_ns / 1e9);
}
plat_mutex_lock(&v->lock);
// Finalize on another thread may have closed the decoder since the test above. finalize takes v->lock after it
// clears v->dav1d, so read here under v->lock it is either still set (the sample is queued before finalize's
// clear_queues, which drops it, and ov_finalize or the destructor then clears live) or already NULL: no sample
// makes a finalized decoder live again, which would hold the movie frame rate cap (frame_rate.c movie_cap_hz).
if (!v->dav1d) {
plat_mutex_unlock(&v->lock);
ret_x(c, 0);
return;
}
if (skip_ns >= 0 && v->eos_queued) { // after the early end: what the demuxer still sends is dropped
plat_mutex_unlock(&v->lock);
ret_x(c, 0);