diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp index c9f50293a..ece376e10 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp @@ -1123,7 +1123,8 @@ namespace ps2_stubs const uint8_t *data, size_t size, uint32_t guestAddr, - std::vector &callbackEvents) + std::vector &callbackEvents, + bool trackGuestAddrs = true) { if (!data || size == 0) { @@ -1153,10 +1154,13 @@ namespace ps2_stubs playback.sawInput = true; playback.pssBuffer.insert(playback.pssBuffer.end(), data, data + size); - playback.pssGuestAddrs.reserve(playback.pssGuestAddrs.size() + size); - for (size_t i = 0; i < size; ++i) + if (trackGuestAddrs) { - playback.pssGuestAddrs.push_back(guestAddr + static_cast(i)); + playback.pssGuestAddrs.reserve(playback.pssGuestAddrs.size() + size); + for (size_t i = 0; i < size; ++i) + { + playback.pssGuestAddrs.push_back(guestAddr + static_cast(i)); + } } processPssBuffer(mpegAddr, playback, callbackEvents); if (!playback.decodedFrames.empty()) @@ -1559,6 +1563,43 @@ namespace ps2_stubs } } + size_t feedMpegCdStreamBytes(const uint8_t *data, size_t size) + { + if (!data || size == 0u) + { + return 0u; + } + + size_t routedCount = 0u; + { + std::lock_guard lock(g_mpeg_stub_mutex); + + // Active only between notifyMpegCdStreamStart() and notifyMpegCdStreamEof(). + const bool cdStreamActive = + g_mpeg_stub_state.cdStreamGeneration != 0u && + !g_mpeg_stub_state.currentCdStreamEofSeen; + if (!cdStreamActive) + { + return 0u; + } + + // Host-fed data carries no guest addresses, so no callback event is queued. + // TODO(host-feed callbacks): queue on MpegPlaybackState for a guest syscall to drain. + std::vector callbackEvents; + for (auto &[mpegAddr, playback] : g_mpeg_stub_state.playbackByMpeg) + { + if (playback.cdStreamGeneration != g_mpeg_stub_state.cdStreamGeneration) + { + continue; + } + appendPssBytes(mpegAddr, playback, data, size, 0u, callbackEvents, false); + ++routedCount; + } + } + g_mpeg_cv.notify_all(); + return routedCount != 0u ? size : 0u; + } + void sceMpegFlush(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h index bd9a8d5a4..4234ddaad 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h @@ -7,6 +7,12 @@ namespace ps2_stubs void resetMpegStubState(); void notifyMpegCdStreamStart(); void notifyMpegCdStreamEof(); + // Push `size` bytes of the active CD movie stream's program-stream/PES data + // into the guest-driven MPEG decoder from host memory. Must be bracketed by + // notifyMpegCdStreamStart()/notifyMpegCdStreamEof(). Returns `size` once the + // bytes are routed to the open decoder(s), or 0 when no CD stream is active. + // Thread-safe; stream callbacks are not dispatched on this path. + size_t feedMpegCdStreamBytes(const uint8_t *data, size_t size); void sceMpegFlush(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceMpegAddBs(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceMpegAddCallback(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 165ffbb57..f56f32f20 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -1003,6 +1003,135 @@ void register_ps2_runtime_expansion_tests() "repeated temporary starvation should continue advancing from the held frame"); }); + tc.Run("host-fed CD stream decodes without a guest demux call", [](TestCase &t) + { + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + + constexpr uint32_t kMpegAddr = 0x00123000u; + constexpr uint32_t kWorkAddr = 0x00140000u; + constexpr uint32_t kImageAddr = 0x00160000u; + + R5900Context createCtx{}; + setRegU32(createCtx, 4, kMpegAddr); + setRegU32(createCtx, 5, kWorkAddr); + setRegU32(createCtx, 6, 0x2000u); + ps2_stubs::sceMpegCreate(rdram.data(), &createCtx, nullptr); + t.IsTrue(::getRegU32(&createCtx, 2) != 0u, + "sceMpegCreate should return a nonzero handle"); + + ps2_stubs::notifyMpegCdStreamStart(); + + const std::vector es = { + 0x00u, 0x00u, 0x01u, 0xB3u, 0x01u, 0x00u, 0x10u, 0x12u, 0xFFu, 0xFFu, 0xE0u, 0x18u, + 0x00u, 0x00u, 0x01u, 0xB5u, 0x14u, 0x8Au, 0x00u, 0x01u, 0x00u, 0x17u, 0x00u, 0x00u, + 0x01u, 0xB8u, 0x00u, 0x08u, 0x00u, 0x40u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x0Fu, + 0xFFu, 0xF8u, 0x00u, 0x00u, 0x01u, 0xB5u, 0x8Fu, 0xFFu, 0xF3u, 0x41u, 0x80u, 0x00u, + 0x00u, 0x01u, 0x01u, 0x13u, 0xF8u, 0x7Du, 0x29u, 0x48u, 0x88u, 0x00u, 0x00u, 0x01u, + 0xB3u, 0x01u, 0x00u, 0x10u, 0x12u, 0xFFu, 0xFFu, 0xE0u, 0x18u, 0x00u, 0x00u, 0x01u, + 0xB5u, 0x14u, 0x8Au, 0x00u, 0x01u, 0x00u, 0x17u, 0x00u, 0x00u, 0x01u, 0xB8u, 0x00u, + 0x08u, 0x00u, 0xC0u, 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x0Fu, 0xFFu, 0xF8u, 0x00u, + 0x00u, 0x01u, 0xB5u, 0x8Fu, 0xFFu, 0xF3u, 0x41u, 0x80u, 0x00u, 0x00u, 0x01u, 0x01u, + 0x13u, 0xF8u, 0x7Du, 0x29u, 0x48u, 0x88u, 0x00u, 0x00u, 0x01u, 0xB3u, 0x01u, 0x00u, + 0x10u, 0x12u, 0xFFu, 0xFFu, 0xE0u, 0x18u, 0x00u, 0x00u, 0x01u, 0xB5u, 0x14u, 0x8Au, + 0x00u, 0x01u, 0x00u, 0x17u, 0x00u, 0x00u, 0x01u, 0xB8u, 0x00u, 0x08u, 0x01u, 0x40u, + 0x00u, 0x00u, 0x01u, 0x00u, 0x00u, 0x0Fu, 0xFFu, 0xF8u, 0x00u, 0x00u, 0x01u, 0xB5u, + 0x8Fu, 0xFFu, 0xF3u, 0x41u, 0x80u, 0x00u, 0x00u, 0x01u, 0x01u, 0x13u, 0xF8u, 0x7Du, + 0x29u, 0x48u, 0x88u}; + + std::vector packet = { + 0x00u, 0x00u, 0x01u, 0xE0u, + 0x00u, static_cast(es.size() + 3u), + 0x80u, 0x00u, 0x00u}; + packet.insert(packet.end(), es.begin(), es.end()); + + const size_t consumed = ps2_stubs::feedMpegCdStreamBytes(packet.data(), packet.size()); + t.Equals(consumed, packet.size(), + "feedMpegCdStreamBytes should report the whole packet consumed"); + + ps2_stubs::notifyMpegCdStreamEof(); + + t.Equals(ps2_stubs::feedMpegCdStreamBytes(packet.data(), packet.size()), static_cast(0u), + "feedMpegCdStreamBytes after CD EOF should be a no-op"); + + R5900Context pictureCtx{}; + setRegU32(pictureCtx, 4, kMpegAddr); + setRegU32(pictureCtx, 5, kImageAddr); + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, nullptr); + + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x00u), 16u, + "host-fed stream should decode to the expected frame width"); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x08u), 0u, + "host-fed stream should report frame index zero"); + }); + + tc.Run("feedMpegCdStreamBytes before a CD stream start is a no-op", [](TestCase &t) + { + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + + constexpr uint32_t kMpegAddr = 0x00123000u; + constexpr uint32_t kWorkAddr = 0x00140000u; + + R5900Context createCtx{}; + setRegU32(createCtx, 4, kMpegAddr); + setRegU32(createCtx, 5, kWorkAddr); + setRegU32(createCtx, 6, 0x2000u); + ps2_stubs::sceMpegCreate(rdram.data(), &createCtx, nullptr); + t.IsTrue(::getRegU32(&createCtx, 2) != 0u, + "sceMpegCreate should return a nonzero handle"); + + const std::vector packet = { + 0x00u, 0x00u, 0x01u, 0xE0u, 0x00u, 0x04u, 0x80u, 0x00u, 0x00u, 0xAAu}; + + t.Equals(ps2_stubs::feedMpegCdStreamBytes(packet.data(), packet.size()), static_cast(0u), + "feedMpegCdStreamBytes with no active CD stream (handle exists but generation 0) should not consume bytes"); + }); + + tc.Run("host feed fans out to every open decoder and reports the input range once", [](TestCase &t) + { + std::vector rdram(PS2_RAM_SIZE, 0u); + ps2_stubs::resetMpegStubState(); + + constexpr uint32_t kMpegA = 0x00123000u; + constexpr uint32_t kWorkA = 0x00140000u; + constexpr uint32_t kMpegB = 0x00133000u; + constexpr uint32_t kWorkB = 0x00150000u; + + R5900Context createCtxA{}; + setRegU32(createCtxA, 4, kMpegA); + setRegU32(createCtxA, 5, kWorkA); + setRegU32(createCtxA, 6, 0x2000u); + ps2_stubs::sceMpegCreate(rdram.data(), &createCtxA, nullptr); + t.IsTrue(::getRegU32(&createCtxA, 2) != 0u, + "sceMpegCreate should return a nonzero handle for decoder A"); + + R5900Context createCtxB{}; + setRegU32(createCtxB, 4, kMpegB); + setRegU32(createCtxB, 5, kWorkB); + setRegU32(createCtxB, 6, 0x2000u); + ps2_stubs::sceMpegCreate(rdram.data(), &createCtxB, nullptr); + t.IsTrue(::getRegU32(&createCtxB, 2) != 0u, + "sceMpegCreate should return a nonzero handle for decoder B"); + + ps2_stubs::notifyMpegCdStreamStart(); + + const std::vector programEnd = {0x00u, 0x00u, 0x01u, 0xB9u}; + const size_t consumed = ps2_stubs::feedMpegCdStreamBytes(programEnd.data(), programEnd.size()); + t.Equals(consumed, programEnd.size(), + "feedMpegCdStreamBytes should report the input range once, not per decoder"); + + R5900Context isEndCtxA{}; + setRegU32(isEndCtxA, 4, kMpegA); + ps2_stubs::sceMpegIsEnd(rdram.data(), &isEndCtxA, nullptr); + t.Equals(getRegS32(isEndCtxA, 2), 1, "decoder A should have received the fed bytes"); + + R5900Context isEndCtxB{}; + setRegU32(isEndCtxB, 4, kMpegB); + ps2_stubs::sceMpegIsEnd(rdram.data(), &isEndCtxB, nullptr); + t.Equals(getRegS32(isEndCtxB, 2), 1, "decoder B should have received the fed bytes"); + }); + tc.Run("sceMpegGetPicture releases an old waiter when the CD stream restarts", [](TestCase &t) { PS2Runtime runtime;