diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index da4a81b..585e446 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -249,6 +249,7 @@ inline void ps2TraceGuestWrite(uint8_t *rdram, (void)valueHi; (void)op; (void)ctx; + // TODO we dont need this anymore so on next release it will be deleted } inline void ps2TraceGuestRangeWrite(uint8_t *rdram, @@ -262,6 +263,7 @@ inline void ps2TraceGuestRangeWrite(uint8_t *rdram, (void)size; (void)op; (void)ctx; + // TODO we dont need this anymore so on next release it will be deleted } class PS2Runtime diff --git a/ps2xRuntime/include/runtime/ee_scheduler.h b/ps2xRuntime/include/runtime/ee_scheduler.h index 8fb1e26..03ac89a 100644 --- a/ps2xRuntime/include/runtime/ee_scheduler.h +++ b/ps2xRuntime/include/runtime/ee_scheduler.h @@ -322,13 +322,10 @@ public: [[nodiscard]] uint64_t currentVSyncTick() const noexcept; uint32_t setGsVSyncCallback(uint32_t callback, uint32_t gp, uint32_t sp); - [[noreturn]] void waitVSync(uint64_t afterTick, int fixedResult = -1); + [[noreturn]] void waitVSync(uint64_t afterTick, int fixedResult = -1, std::function completion = {}); void completeVSync(uint64_t tick); void completeExternalWait(uint32_t type, uint64_t token, int result); - [[noreturn]] void waitExternal(EeWaitReason reason, - uint32_t type, - uint64_t token, - std::function completion = {}); + [[noreturn]] void waitExternal(EeWaitReason reason, uint32_t type, uint64_t token, std::function completion = {}); [[nodiscard]] GuestThread *thread(int id); [[nodiscard]] const GuestThread *thread(int id) const; diff --git a/ps2xRuntime/include/runtime/ps2_memory.h b/ps2xRuntime/include/runtime/ps2_memory.h index cd96b5a..dae74b4 100644 --- a/ps2xRuntime/include/runtime/ps2_memory.h +++ b/ps2xRuntime/include/runtime/ps2_memory.h @@ -204,8 +204,6 @@ struct GSRegisters uint64_t extdata; // External data uint64_t extwrite; // External write uint64_t bgcolor; // Background color - // Status remains atomic because the renderer/UI can observe it while the - // single EE executor updates SIGNAL, FINISH and FIELD. std::atomic csr; std::atomic vsyncTick; uint64_t imr; // Interrupt mask diff --git a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp index 9901f01..551bc95 100644 --- a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp +++ b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp @@ -1004,10 +1004,8 @@ int EeScheduler::cancelAlarm(int id) { std::lock_guard lock(m_eventMutex); std::erase_if(m_deadlines, [id](const ScheduledEvent &scheduled) - { - return scheduled.event.type == EeEventType::Alarm && - scheduled.event.id == static_cast(id); - }); + { return scheduled.event.type == EeEventType::Alarm && + scheduled.event.id == static_cast(id); }); updateNextDeadline(); } return KE_OK; @@ -1235,9 +1233,12 @@ uint32_t EeScheduler::setGsVSyncCallback(uint32_t callback, uint32_t gp, uint32_ return previous; } -[[noreturn]] void EeScheduler::waitVSync(uint64_t afterTick, int fixedResult) +[[noreturn]] void EeScheduler::waitVSync(uint64_t afterTick, int fixedResult, std::function completion) { - blockCurrent(EeWaitState{EeWaitReason::VSync, EeVSyncWait{afterTick, fixedResult}}); + blockCurrent(EeWaitState{ + EeWaitReason::VSync, + EeVSyncWait{afterTick, fixedResult}, + std::move(completion)}); } void EeScheduler::completeVSync(uint64_t tick) @@ -1438,10 +1439,10 @@ void EeScheduler::publishSnapshot() for (const auto &[id, item] : m_eventFlags) { next.eventFlags.push_back(EeEventFlagSnapshot{id, - item.bits, - item.initBits, - item.attr, - static_cast(item.waiters.size())}); + item.bits, + item.initBits, + item.attr, + static_cast(item.waiters.size())}); } std::sort(next.eventFlags.begin(), next.eventFlags.end(), [](const auto &left, const auto &right) { return left.id < right.id; }); @@ -1686,8 +1687,7 @@ void EeScheduler::processDueDeadlines() { return left.event.id < right.event.id; } - return left.sequence < right.sequence; - }); + return left.sequence < right.sequence; }); for (ScheduledEvent &scheduled : due) { if (scheduled.event.type == EeEventType::VBlankStart) @@ -1806,7 +1806,7 @@ void EeScheduler::finishEventWaiters(EeEventFlag &flag, bool interruptSafe) bool EeScheduler::eventCondition(uint32_t current, uint32_t requested, uint32_t mode) { return (mode & WEF_OR) != 0u ? (current & requested) != 0u - : (current & requested) == requested; + : (current & requested) == requested; } int EeScheduler::waitObjectId(const EeWaitState &wait) diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp index 1d6db89..4c20a21 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp @@ -571,10 +571,9 @@ namespace ps2_stubs { std::memset(rdram + offset + bytes, 0, requestedBytes - bytes); } - if (hitStreamEnd || g_cdStreamingLbn == g_cdStreamingEndLbn) - { - notifyMpegCdStreamEof(runtime); - } + notifyMpegCdStreamDataProduced( + static_cast(bytes), + hitStreamEnd || g_cdStreamingLbn == g_cdStreamingEndLbn); } else { diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp index 8e1e2dd..caa84a2 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp @@ -477,6 +477,8 @@ namespace ps2_stubs std::vector pssGuestAddrs; std::deque decodedFrames; std::unique_ptr decoder; + uint64_t pictureIntervalQ32 = 0u; + uint64_t nextPictureTickQ32 = std::numeric_limits::max(); }; struct MpegStreamCallbackEvent @@ -495,6 +497,9 @@ namespace ps2_stubs bool initialized = false; uint32_t nextCallbackHandle = 1u; uint64_t cdStreamGeneration = 0u; + uint64_t cdStreamBytesProduced = 0u; + uint64_t cdStreamBytesDemuxed = 0u; + bool cdStreamEofPending = false; bool currentCdStreamEofSeen = false; uint32_t feedEsTraceCount = 0u; uint32_t demuxPssTraceCount = 0u; @@ -523,6 +528,51 @@ namespace ps2_stubs constexpr uint8_t kMpegPrivateStream1 = 0xBDu; constexpr size_t kStartCodeNotFound = std::numeric_limits::max(); constexpr uint32_t kMpegCallbackDataSize = 0x20u; + constexpr uint64_t kPictureClockOne = 1ull << 32u; + + uint64_t mpegPictureIntervalQ32(uint8_t frameRateCode) + { + uint64_t frameRateNumerator = 0u; + uint64_t frameRateDenominator = 1u; + switch (frameRateCode) + { + case 1u: + frameRateNumerator = 24000u; + frameRateDenominator = 1001u; + break; + case 2u: + frameRateNumerator = 24u; + break; + case 3u: + frameRateNumerator = 25u; + break; + case 4u: + frameRateNumerator = 30000u; + frameRateDenominator = 1001u; + break; + case 5u: + frameRateNumerator = 30u; + break; + case 6u: + frameRateNumerator = 50u; + break; + case 7u: + frameRateNumerator = 60000u; + frameRateDenominator = 1001u; + break; + case 8u: + frameRateNumerator = 60u; + break; + default: + return 0u; + } + + // EE VSync runs at the NTSC field rate. Q32 preserves fractional + // cadences such as 24 fps without accumulating host-time drift. + const uint64_t denominator = 1001u * frameRateNumerator; + const uint64_t numerator = (60000u * frameRateDenominator) << 32u; + return std::max(kPictureClockOne, (numerator + denominator / 2u) / denominator); + } uint32_t align16(uint32_t value) { @@ -644,12 +694,12 @@ namespace ps2_stubs size_t findMpegSequenceHeader(const uint8_t *data, size_t size) { - if (!data || size < 7u) + if (!data || size < 8u) { return kStartCodeNotFound; } - for (size_t i = 0; i + 6u < size; ++i) + for (size_t i = 0; i + 7u < size; ++i) { if (data[i] == 0x00u && data[i + 1u] == 0x00u && @@ -660,7 +710,9 @@ namespace ps2_stubs (static_cast(data[i + 5u]) >> 4u); const uint32_t height = ((static_cast(data[i + 5u]) & 0x0Fu) << 8u) | static_cast(data[i + 6u]); - if (width != 0u && height != 0u && width <= 4096u && height <= 4096u) + const uint8_t frameRateCode = data[i + 7u] & 0x0Fu; + if (width != 0u && height != 0u && width <= 4096u && height <= 4096u && + mpegPictureIntervalQ32(frameRateCode) != 0u) { return i; } @@ -781,6 +833,8 @@ namespace ps2_stubs data = playback.videoSequenceSyncBuffer.data(); size = playback.videoSequenceSyncBuffer.size(); + playback.pictureIntervalQ32 = mpegPictureIntervalQ32(data[7u] & 0x0Fu); + playback.nextPictureTickQ32 = std::numeric_limits::max(); playback.waitingForVideoSequenceHeader = false; playback.decoderFailed = false; playback.decoder.reset(); @@ -1064,6 +1118,36 @@ namespace ps2_stubs flushDecoderIfEnded(playback); } + void finalizeCdStreamEofUnlocked(std::vector &completedMpegIds, bool &changed) + { + g_mpeg_stub_state.cdStreamEofPending = false; + g_mpeg_stub_state.currentCdStreamEofSeen = true; + for (auto &[mpegAddr, playback] : g_mpeg_stub_state.playbackByMpeg) + { + completedMpegIds.push_back(mpegAddr); + if (!playback.sawInput || playback.streamEnded) + { + continue; + } + + finishPlaybackStream(mpegAddr, playback); + changed = true; + } + } + + void recordCdStreamBytesDemuxedUnlocked( + size_t consumed, + std::vector &completedMpegIds, + bool &changed) + { + g_mpeg_stub_state.cdStreamBytesDemuxed += consumed; + if (g_mpeg_stub_state.cdStreamEofPending && + g_mpeg_stub_state.cdStreamBytesDemuxed >= g_mpeg_stub_state.cdStreamBytesProduced) + { + finalizeCdStreamEofUnlocked(completedMpegIds, changed); + } + } + void appendPssBytes(uint32_t mpegAddr, MpegPlaybackState &playback, const uint8_t *data, @@ -1385,6 +1469,9 @@ namespace ps2_stubs g_mpeg_stub_state.initialized = false; g_mpeg_stub_state.nextCallbackHandle = 1u; g_mpeg_stub_state.cdStreamGeneration = 0u; + g_mpeg_stub_state.cdStreamBytesProduced = 0u; + g_mpeg_stub_state.cdStreamBytesDemuxed = 0u; + g_mpeg_stub_state.cdStreamEofPending = false; g_mpeg_stub_state.currentCdStreamEofSeen = false; g_mpeg_stub_state.feedEsTraceCount = 0u; g_mpeg_stub_state.demuxPssTraceCount = 0u; @@ -1424,6 +1511,9 @@ namespace ps2_stubs (void)runtime; std::lock_guard lock(g_mpeg_stub_mutex); ++g_mpeg_stub_state.cdStreamGeneration; + g_mpeg_stub_state.cdStreamBytesProduced = 0u; + g_mpeg_stub_state.cdStreamBytesDemuxed = 0u; + g_mpeg_stub_state.cdStreamEofPending = false; g_mpeg_stub_state.currentCdStreamEofSeen = false; g_mpeg_stub_state.feedEsTraceCount = 0u; g_mpeg_stub_state.demuxPssTraceCount = 0u; @@ -1442,24 +1532,23 @@ namespace ps2_stubs }); } + void notifyMpegCdStreamDataProduced(uint32_t byteCount, bool endOfStream) + { + std::lock_guard lock(g_mpeg_stub_mutex); + g_mpeg_stub_state.cdStreamBytesProduced += byteCount; + if (endOfStream) + { + g_mpeg_stub_state.cdStreamEofPending = true; + } + } + void notifyMpegCdStreamEof(PS2Runtime *runtime) { std::vector completedMpegIds; bool changed = false; { std::lock_guard lock(g_mpeg_stub_mutex); - g_mpeg_stub_state.currentCdStreamEofSeen = true; - for (auto &[mpegAddr, playback] : g_mpeg_stub_state.playbackByMpeg) - { - completedMpegIds.push_back(mpegAddr); - if (!playback.sawInput || playback.streamEnded) - { - continue; - } - - finishPlaybackStream(mpegAddr, playback); - changed = true; - } + finalizeCdStreamEofUnlocked(completedMpegIds, changed); } if (changed) @@ -1702,17 +1791,28 @@ namespace ps2_stubs const uint32_t byteCount = getRegU32(ctx, 6); std::vector callbackEvents; + std::vector completedMpegIds; size_t consumed = 0u; size_t decodedCount = 0u; uint32_t traceIdx = 0u; + bool eofChanged = false; { std::lock_guard lock(g_mpeg_stub_mutex); MpegPlaybackState &playback = getPlaybackState(mpegAddr); consumed = appendGuestBytes(mpegAddr, playback, rdram, dataAddr, byteCount, callbackEvents); + recordCdStreamBytesDemuxedUnlocked(consumed, completedMpegIds, eofChanged); decodedCount = playback.decodedFrames.size(); traceIdx = g_mpeg_stub_state.demuxPssTraceCount++; } runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); + for (const uint32_t completedMpegId : completedMpegIds) + { + if (completedMpegId != mpegAddr) + { + runtime->eeScheduler().completeExternalWait( + kMpegPictureWaitType, completedMpegId, KE_OK); + } + } if (traceIdx < 32u) { @@ -1752,9 +1852,11 @@ namespace ps2_stubs const uint32_t ringSize = readAbiArg4(rdram, ctx); std::vector callbackEvents; + std::vector completedMpegIds; size_t consumed = 0u; size_t decodedCount = 0u; uint32_t traceIdx = 0u; + bool eofChanged = false; { std::lock_guard lock(g_mpeg_stub_mutex); MpegPlaybackState &playback = getPlaybackState(mpegAddr); @@ -1767,10 +1869,19 @@ namespace ps2_stubs ringBaseAddr, ringSize, callbackEvents); + recordCdStreamBytesDemuxedUnlocked(consumed, completedMpegIds, eofChanged); decodedCount = playback.decodedFrames.size(); traceIdx = g_mpeg_stub_state.demuxRingTraceCount++; } runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); + for (const uint32_t completedMpegId : completedMpegIds) + { + if (completedMpegId != mpegAddr) + { + runtime->eeScheduler().completeExternalWait( + kMpegPictureWaitType, completedMpegId, KE_OK); + } + } if (traceIdx < 32u) { @@ -1875,6 +1986,42 @@ namespace ps2_stubs if (!playback.decodedFrames.empty()) { + if (playback.pictureIntervalQ32 != 0u) + { + const uint64_t currentTick = runtime->eeScheduler().currentVSyncTick(); + const uint64_t currentTickQ32 = currentTick << 32u; + if (playback.nextPictureTickQ32 == std::numeric_limits::max()) + { + playback.nextPictureTickQ32 = currentTickQ32; + } + + if (currentTickQ32 < playback.nextPictureTickQ32) + { + const uint64_t eligibleTick = + (playback.nextPictureTickQ32 + kPictureClockOne - 1u) >> 32u; + lock.unlock(); + runtime->eeScheduler().waitVSync( + eligibleTick - 1u, + -1, + [rdram, runtime](R5900Context &resumeContext) + { + if (static_cast(getRegU32(&resumeContext, 2)) < 0) + { + return; + } + sceMpegGetPicture(rdram, &resumeContext, runtime); + }); + } + + // If decoding fell more than one frame behind, resume from the + // current field instead of releasing a burst of stale pictures. + if (currentTickQ32 - playback.nextPictureTickQ32 >= playback.pictureIntervalQ32) + { + playback.nextPictureTickQ32 = currentTickQ32; + } + playback.nextPictureTickQ32 += playback.pictureIntervalQ32; + } + frame = std::move(playback.decodedFrames.front()); playback.decodedFrames.pop_front(); playback.width = static_cast(frame.width); @@ -1948,10 +2095,16 @@ namespace ps2_stubs (void)rdram; std::lock_guard lock(g_mpeg_stub_mutex); const uint64_t cdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; + const uint64_t cdStreamBytesProduced = g_mpeg_stub_state.cdStreamBytesProduced; + const uint64_t cdStreamBytesDemuxed = g_mpeg_stub_state.cdStreamBytesDemuxed; + const bool cdStreamEofPending = g_mpeg_stub_state.cdStreamEofPending; const bool currentCdStreamEofSeen = g_mpeg_stub_state.currentCdStreamEofSeen; resetMpegStubStateUnlocked(); g_mpeg_stub_state.initialized = true; g_mpeg_stub_state.cdStreamGeneration = cdStreamGeneration; + g_mpeg_stub_state.cdStreamBytesProduced = cdStreamBytesProduced; + g_mpeg_stub_state.cdStreamBytesDemuxed = cdStreamBytesDemuxed; + g_mpeg_stub_state.cdStreamEofPending = cdStreamEofPending; g_mpeg_stub_state.currentCdStreamEofSeen = currentCdStreamEofSeen; setReturnU32(ctx, 0u); } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h index 76225e9..bbf7edf 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h @@ -7,6 +7,7 @@ namespace ps2_stubs void resetMpegStubState(); void enqueueMpegDecodedFrameForTesting(uint32_t mpegAddr); void notifyMpegCdStreamStart(PS2Runtime *runtime = nullptr); + void notifyMpegCdStreamDataProduced(uint32_t byteCount, bool endOfStream); void notifyMpegCdStreamEof(PS2Runtime *runtime = nullptr); void sceMpegFlush(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceMpegAddBs(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index bc8d8bf..fba12b8 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -151,6 +151,10 @@ namespace std::atomic gMpegWaitStage{0u}; std::atomic gMpegNoDuplicateStage{0u}; std::atomic gMpegNoDuplicateProducerStage{0u}; + std::atomic gMpegPacingStage{0u}; + std::atomic gMpegPacingFirstProducerStage{0u}; + std::atomic gMpegPacingSecondProducerStage{0u}; + std::atomic gMpegPacingResumeTick{0u}; constexpr uint32_t kMpegWaitMainPc = 0x00125000u; constexpr uint32_t kMpegWaitResumePc = 0x00125010u; @@ -162,6 +166,13 @@ namespace constexpr uint32_t kMpegNoDuplicateProducerPc = 0x00125050u; constexpr uint32_t kMpegNoDuplicateHandle = 0x00124000u; constexpr uint32_t kMpegNoDuplicateImage = 0x00131000u; + constexpr uint32_t kMpegPacingMainPc = 0x00125060u; + constexpr uint32_t kMpegPacingAfterFirstPc = 0x00125070u; + constexpr uint32_t kMpegPacingAfterSecondPc = 0x00125080u; + constexpr uint32_t kMpegPacingFirstProducerPc = 0x00125090u; + constexpr uint32_t kMpegPacingSecondProducerPc = 0x001250A0u; + constexpr uint32_t kMpegPacingHandle = 0x00124800u; + constexpr uint32_t kMpegPacingImage = 0x00132000u; constexpr uint32_t kIpuInitMainPc = 0x00125100u; constexpr uint32_t kIpuInitResumePc = 0x00125104u; constexpr uint32_t kIpuSetD4Pc = 0x00126428u; @@ -251,6 +262,51 @@ namespace ctx->pc = 0u; } + void testMpegPacingMain(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + setRegU32(*ctx, 4, kMpegPacingHandle); + setRegU32(*ctx, 5, kMpegPacingImage); + ctx->pc = kMpegPacingAfterFirstPc; + ps2_stubs::sceMpegGetPicture(rdram, ctx, runtime); + } + + void testMpegPacingAfterFirst(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + { + gMpegPacingStage.store(1u, std::memory_order_release); + setRegU32(*ctx, 4, kMpegPacingHandle); + setRegU32(*ctx, 5, kMpegPacingImage); + ctx->pc = kMpegPacingAfterSecondPc; + ps2_stubs::sceMpegGetPicture(rdram, ctx, runtime); + } + + void testMpegPacingAfterSecond(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + gMpegPacingResumeTick.store( + runtime->eeScheduler().currentVSyncTick(), + std::memory_order_release); + gMpegPacingStage.store(3u, std::memory_order_release); + ctx->pc = 0u; + runtime->requestStop(); + } + + void testMpegPacingFirstProducer(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + gMpegPacingFirstProducerStage.store( + gMpegPacingStage.load(std::memory_order_acquire), + std::memory_order_release); + runtime->eeScheduler().postEvent(EeEvent{EeEventType::VBlankStart, 0u, 0u}); + ctx->pc = 0u; + } + + void testMpegPacingSecondProducer(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + gMpegPacingSecondProducerStage.store( + gMpegPacingStage.load(std::memory_order_acquire), + std::memory_order_release); + runtime->eeScheduler().postEvent(EeEvent{EeEventType::VBlankStart, 0u, 0u}); + ctx->pc = 0u; + } + void testRecordMpegStreamCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { if (!rdram || !ctx) @@ -694,6 +750,66 @@ void register_ps2_runtime_expansion_tests() "resumed GetPicture should publish the configured height"); }); + tc.Run("CD EOF becomes visible only after the final guest MPEG demux", [](TestCase &t) + { + PS2Runtime runtime; + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + ps2_stubs::notifyMpegCdStreamStart(); + + constexpr uint32_t kMpegAddr = 0x00126000u; + constexpr uint32_t kPacketAddr = 0x00127000u; + const std::vector finalPacket = { + 0x00u, 0x00u, 0x01u, 0xE0u, + 0x00u, 0x04u, + 0x80u, 0x00u, 0x00u, + 0x00u}; + std::memcpy(rdram.data() + kPacketAddr, finalPacket.data(), finalPacket.size()); + + ps2_stubs::notifyMpegCdStreamDataProduced( + static_cast(finalPacket.size()), true); + + R5900Context beforeDemuxCtx{}; + setRegU32(beforeDemuxCtx, 4, kMpegAddr); + ps2_stubs::sceMpegIsEnd(rdram.data(), &beforeDemuxCtx, &runtime); + t.Equals(getRegS32(beforeDemuxCtx, 2), 0, + "physical CD EOF must not end MPEG before the final guest buffer is demuxed"); + + constexpr uint32_t kFirstPartSize = 5u; + R5900Context firstDemuxCtx{}; + setRegU32(firstDemuxCtx, 4, kMpegAddr); + setRegU32(firstDemuxCtx, 5, kPacketAddr); + setRegU32(firstDemuxCtx, 6, kFirstPartSize); + setRegU32(firstDemuxCtx, 7, kPacketAddr); + setRegU32(firstDemuxCtx, 8, static_cast(finalPacket.size())); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &firstDemuxCtx, &runtime); + t.Equals(getRegS32(firstDemuxCtx, 2), static_cast(kFirstPartSize), + "the partial final MPEG buffer should be consumed"); + + R5900Context midwayCtx{}; + setRegU32(midwayCtx, 4, kMpegAddr); + ps2_stubs::sceMpegIsEnd(rdram.data(), &midwayCtx, &runtime); + t.Equals(getRegS32(midwayCtx, 2), 0, + "a partial final MPEG demux must keep physical CD EOF pending"); + + const uint32_t remainingSize = static_cast(finalPacket.size()) - kFirstPartSize; + R5900Context finalDemuxCtx{}; + setRegU32(finalDemuxCtx, 4, kMpegAddr); + setRegU32(finalDemuxCtx, 5, kPacketAddr + kFirstPartSize); + setRegU32(finalDemuxCtx, 6, remainingSize); + setRegU32(finalDemuxCtx, 7, kPacketAddr); + setRegU32(finalDemuxCtx, 8, static_cast(finalPacket.size())); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &finalDemuxCtx, &runtime); + t.Equals(getRegS32(finalDemuxCtx, 2), static_cast(remainingSize), + "the final guest MPEG bytes should be consumed before EOF is committed"); + + R5900Context afterDemuxCtx{}; + setRegU32(afterDemuxCtx, 4, kMpegAddr); + ps2_stubs::sceMpegIsEnd(rdram.data(), &afterDemuxCtx, &runtime); + t.Equals(getRegS32(afterDemuxCtx, 2), 1, + "MPEG should end after the pending final guest buffer is demuxed"); + }); + tc.Run("sceMpegGetPicture waits for new decoder output instead of duplicating the last frame", [](TestCase &t) { PS2Runtime runtime; @@ -726,6 +842,76 @@ void register_ps2_runtime_expansion_tests() "only the injected decoder frame should be counted as served"); }); + tc.Run("sceMpegGetPicture resumes its HLE operation at the MPEG frame cadence", [](TestCase &t) + { + PS2Runtime runtime; + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + ps2_stubs::notifyMpegCdStreamStart(); + + constexpr uint32_t kSequencePacketAddr = 0x00127000u; + const std::vector sequencePacket = { + 0x00u, 0x00u, 0x01u, 0xE0u, + 0x00u, 0x0Bu, + 0x80u, 0x00u, 0x00u, + 0x00u, 0x00u, 0x01u, 0xB3u, + 0x14u, 0x01u, 0x60u, 0x14u}; + std::memcpy( + rdram.data() + kSequencePacketAddr, + sequencePacket.data(), + sequencePacket.size()); + + R5900Context demuxContext{}; + runtime.eeScheduler().reset(rdram.data(), demuxContext); + setRegU32(demuxContext, 4, kMpegPacingHandle); + setRegU32(demuxContext, 5, kSequencePacketAddr); + setRegU32(demuxContext, 6, static_cast(sequencePacket.size())); + setRegU32(demuxContext, 7, kSequencePacketAddr); + setRegU32(demuxContext, 8, static_cast(sequencePacket.size())); + ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &demuxContext, &runtime); + t.Equals( + getRegS32(demuxContext, 2), + static_cast(sequencePacket.size()), + "the 29.97 fps MPEG sequence header should be consumed"); + + ps2_stubs::enqueueMpegDecodedFrameForTesting(kMpegPacingHandle); + ps2_stubs::enqueueMpegDecodedFrameForTesting(kMpegPacingHandle); + runtime.registerFunction(kMpegPacingMainPc, testMpegPacingMain); + runtime.registerFunction(kMpegPacingAfterFirstPc, testMpegPacingAfterFirst); + runtime.registerFunction(kMpegPacingAfterSecondPc, testMpegPacingAfterSecond); + runtime.registerFunction(kMpegPacingFirstProducerPc, testMpegPacingFirstProducer); + runtime.registerFunction(kMpegPacingSecondProducerPc, testMpegPacingSecondProducer); + gMpegPacingStage.store(0u, std::memory_order_release); + gMpegPacingFirstProducerStage.store(0u, std::memory_order_release); + gMpegPacingSecondProducerStage.store(0u, std::memory_order_release); + gMpegPacingResumeTick.store(0u, std::memory_order_release); + + R5900Context mainContext{}; + mainContext.pc = kMpegPacingMainPc; + EeScheduler &ee = runtime.eeScheduler(); + ee.reset(rdram.data(), mainContext); + const int firstProducerId = ee.createThread(EeThreadCreateParams{ + 0u, kMpegPacingFirstProducerPc, 0u, 0u, 0u, 10, 0u}); + const int secondProducerId = ee.createThread(EeThreadCreateParams{ + 0u, kMpegPacingSecondProducerPc, 0u, 0u, 0u, 11, 0u}); + t.IsTrue(firstProducerId > 1 && secondProducerId > firstProducerId, + "two VSync producer guest threads should be created"); + t.Equals(ee.startThread(firstProducerId, 0u, mainContext, false), 0, + "the first VSync producer should become ready"); + t.Equals(ee.startThread(secondProducerId, 0u, mainContext, false), 0, + "the second VSync producer should become ready"); + ee.run(); + + t.Equals(gMpegPacingFirstProducerStage.load(std::memory_order_acquire), 1u, + "the second GetPicture should wait before the first VSync"); + t.Equals(gMpegPacingSecondProducerStage.load(std::memory_order_acquire), 1u, + "29.97 fps must still be waiting after one 59.94 Hz VSync"); + t.Equals(gMpegPacingResumeTick.load(std::memory_order_acquire), 2ull, + "the pending GetPicture should complete after two VSync ticks"); + t.Equals(gMpegPacingStage.load(std::memory_order_acquire), 3u, + "VSync completion must re-enter GetPicture before its guest continuation"); + }); + tc.Run("sceSdRemote isolates voice transfers from block streaming state", [](TestCase &t) { std::vector rdram(PS2_RAM_SIZE, 0u);