diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp index bd161d47..1525cf60 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp @@ -502,6 +502,9 @@ namespace ps2_stubs uint32_t nextCallbackHandle = 1u; uint64_t cdStreamGeneration = 0u; bool currentCdStreamEofSeen = false; + std::vector cdStreamStagedBytes; // host-fed ES held before a decoder exists + uint64_t cdStreamStageGeneration = 0u; // generation the stage belongs to + bool cdStreamStageOverflowed = false; // cap hit -> stage abandoned this generation uint32_t feedEsTraceCount = 0u; uint32_t demuxPssTraceCount = 0u; uint32_t demuxRingTraceCount = 0u; @@ -1154,7 +1157,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) { @@ -1184,10 +1188,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()) @@ -1528,6 +1535,9 @@ namespace ps2_stubs g_mpeg_stub_state.nextCallbackHandle = 1u; g_mpeg_stub_state.cdStreamGeneration = 0u; g_mpeg_stub_state.currentCdStreamEofSeen = false; + g_mpeg_stub_state.cdStreamStagedBytes.clear(); + g_mpeg_stub_state.cdStreamStageGeneration = 0u; + g_mpeg_stub_state.cdStreamStageOverflowed = false; g_mpeg_stub_state.feedEsTraceCount = 0u; g_mpeg_stub_state.demuxPssTraceCount = 0u; g_mpeg_stub_state.demuxRingTraceCount = 0u; @@ -1551,6 +1561,8 @@ namespace ps2_stubs std::lock_guard lock(g_mpeg_stub_mutex); ++g_mpeg_stub_state.cdStreamGeneration; g_mpeg_stub_state.currentCdStreamEofSeen = false; + g_mpeg_stub_state.cdStreamStagedBytes.clear(); + g_mpeg_stub_state.cdStreamStageOverflowed = false; g_mpeg_stub_state.feedEsTraceCount = 0u; g_mpeg_stub_state.demuxPssTraceCount = 0u; g_mpeg_stub_state.demuxRingTraceCount = 0u; @@ -1573,6 +1585,8 @@ namespace ps2_stubs { std::lock_guard lock(g_mpeg_stub_mutex); g_mpeg_stub_state.currentCdStreamEofSeen = true; + g_mpeg_stub_state.cdStreamStagedBytes.clear(); + g_mpeg_stub_state.cdStreamStageOverflowed = false; bool changed = false; for (auto &[mpegAddr, playback] : g_mpeg_stub_state.playbackByMpeg) { @@ -1599,6 +1613,68 @@ 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; + } + + // No decoder on the current generation took these bytes: stage them until the + // first sceMpegCreate on this generation; afterwards feed routes live. + if (routedCount == 0u && !g_mpeg_stub_state.cdStreamStageOverflowed) + { + if (g_mpeg_stub_state.cdStreamStageGeneration != g_mpeg_stub_state.cdStreamGeneration) + { + g_mpeg_stub_state.cdStreamStagedBytes.clear(); + g_mpeg_stub_state.cdStreamStageGeneration = g_mpeg_stub_state.cdStreamGeneration; + } + + if (g_mpeg_stub_state.cdStreamStagedBytes.size() + size > kMpegHostFeedStageCapBytes) + { + // Dropping the tail would hand a later decoder a gapped stream (staged + // prefix, then a hole, then live feed); discard the whole stage instead + // and let the next decoder resync cleanly on live feed. + g_mpeg_stub_state.cdStreamStagedBytes.clear(); + g_mpeg_stub_state.cdStreamStageOverflowed = true; + } + else + { + g_mpeg_stub_state.cdStreamStagedBytes.insert( + g_mpeg_stub_state.cdStreamStagedBytes.end(), data, data + size); + } + } + } + g_mpeg_cv.notify_all(); + return size; + } + void sceMpegFlush(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; @@ -1736,7 +1812,19 @@ namespace ps2_stubs { std::lock_guard lock(g_mpeg_stub_mutex); - getPlaybackState(param_1) = makeFreshPlaybackState(); + auto &pb = getPlaybackState(param_1); + pb = makeFreshPlaybackState(); + if (!g_mpeg_stub_state.cdStreamStageOverflowed && + !g_mpeg_stub_state.cdStreamStagedBytes.empty() && + g_mpeg_stub_state.cdStreamStageGeneration == pb.cdStreamGeneration) + { + std::vector replayEvents; // host feed: none consumed + appendPssBytes(param_1, pb, + g_mpeg_stub_state.cdStreamStagedBytes.data(), + g_mpeg_stub_state.cdStreamStagedBytes.size(), + 0u, replayEvents, /*trackGuestAddrs=*/false); + g_mpeg_stub_state.cdStreamStagedBytes.clear(); + } } const uint32_t puVar4 = uVar3 + 0x108u; diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h index bd9a8d5a..8075cb79 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.h @@ -4,9 +4,25 @@ namespace ps2_stubs { + // Upper bound on how many host-fed bytes are held in the pre-decoder stage + // (see feedMpegCdStreamBytes below) before the stage is abandoned for the + // rest of the current CD-stream generation. Internal tuning knob, not part + // of the API contract. + constexpr size_t kMpegHostFeedStageCapBytes = 4u * 1024u * 1024u; + 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(). Order-insensitive w.r.t. + // sceMpegCreate: bytes fed before any decoder exists on the current CD-stream + // generation are staged (bounded; see kMpegHostFeedStageCapBytes) and replayed + // into the next decoder created on that generation. Staged bytes are discarded on + // stream stop/restart. Returns `size` while a CD stream is active (routed or + // staged), 0 only 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 c31b4d42..8ffb5904 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -322,6 +322,36 @@ namespace ctx->pc = 0u; } + // A minimal decodable program-stream packet (PES header + one MPEG-1 video + // sequence), shared by the host-feed MPEG tests below. Returns a fresh + // mutable copy each call so callers may slice/consume it freely. + std::vector makeMpegHostFeedTestPacket() + { + static 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()); + return packet; + } + } void register_ps2_runtime_expansion_tests() @@ -1137,6 +1167,325 @@ 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 packet = makeMpegHostFeedTestPacket(); + + 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("host feed staged before a decoder opens reaches the later-created decoder", [](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; + + ps2_stubs::notifyMpegCdStreamStart(); + + const std::vector packet = makeMpegHostFeedTestPacket(); + const size_t consumed = ps2_stubs::feedMpegCdStreamBytes(packet.data(), packet.size()); + t.Equals(consumed, packet.size(), + "feed before any decoder exists should still report the stream as active (staged, not dropped)"); + + 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::notifyMpegCdStreamEof(); + + 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, + "the later-created decoder should decode the staged prefix"); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x08u), 0u, + "first decoded frame should report frame index zero"); + }); + + tc.Run("host feed split across a packet boundary decodes", [](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 packet = makeMpegHostFeedTestPacket(); + constexpr size_t kSplitAt = 40; // inside the PES payload (payload starts at byte 9) + const size_t firstConsumed = ps2_stubs::feedMpegCdStreamBytes(packet.data(), kSplitAt); + t.Equals(firstConsumed, kSplitAt, + "first half of a split feed should be routed to the open decoder"); + const size_t secondConsumed = ps2_stubs::feedMpegCdStreamBytes( + packet.data() + kSplitAt, packet.size() - kSplitAt); + t.Equals(secondConsumed, packet.size() - kSplitAt, + "second half of a split feed should be routed to the open decoder"); + + ps2_stubs::notifyMpegCdStreamEof(); + + 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, + "a packet split across two host feeds should still decode"); + }); + + tc.Run("host feed decodes across a CD-stream restart", [](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 packetGen1 = makeMpegHostFeedTestPacket(); + ps2_stubs::feedMpegCdStreamBytes(packetGen1.data(), packetGen1.size()); + ps2_stubs::notifyMpegCdStreamEof(); + + 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, + "generation 1 host-fed stream should decode"); + + ps2_stubs::notifyMpegCdStreamStart(); + const std::vector packetGen2 = makeMpegHostFeedTestPacket(); + ps2_stubs::feedMpegCdStreamBytes(packetGen2.data(), packetGen2.size()); + ps2_stubs::notifyMpegCdStreamEof(); + + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, nullptr); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x00u), 16u, + "generation 2 host-fed stream should decode after a restart"); + }); + + tc.Run("host feed staging is bounded and recovers after overflow", [](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; + + ps2_stubs::notifyMpegCdStreamStart(); + + const std::vector packet = makeMpegHostFeedTestPacket(); + ps2_stubs::feedMpegCdStreamBytes(packet.data(), packet.size()); + + const std::vector junk(ps2_stubs::kMpegHostFeedStageCapBytes + 1u, 0xAAu); + ps2_stubs::feedMpegCdStreamBytes(junk.data(), junk.size()); + + 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::notifyMpegCdStreamEof(); + + R5900Context pictureCtx{}; + setRegU32(pictureCtx, 4, kMpegAddr); + setRegU32(pictureCtx, 5, kImageAddr); + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, nullptr); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x00u), 320u, + "overflow should discard the staged prefix; with nothing decodable, width should stay the " + "safe seeded default instead of the fed stream's real width"); + + // Delete the generation-1 decoder so the recovery leg's feed genuinely has no + // open decoder to route to and exercises staging again, rather than routing live + // to a decoder that notifyMpegCdStreamStart would otherwise carry over. + R5900Context deleteCtx{}; + setRegU32(deleteCtx, 4, kMpegAddr); + ps2_stubs::sceMpegDelete(rdram.data(), &deleteCtx, nullptr); + + // Recovery leg: the overflow flag is per generation, so a fresh generation + // stages and decodes normally again. + ps2_stubs::notifyMpegCdStreamStart(); + const std::vector packetGen2 = makeMpegHostFeedTestPacket(); + ps2_stubs::feedMpegCdStreamBytes(packetGen2.data(), packetGen2.size()); + + R5900Context createCtx2{}; + setRegU32(createCtx2, 4, kMpegAddr); + setRegU32(createCtx2, 5, kWorkAddr); + setRegU32(createCtx2, 6, 0x2000u); + ps2_stubs::sceMpegCreate(rdram.data(), &createCtx2, nullptr); + t.IsTrue(::getRegU32(&createCtx2, 2) != 0u, + "sceMpegCreate should return a nonzero handle for the recovery decoder"); + + ps2_stubs::notifyMpegCdStreamEof(); + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, nullptr); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x00u), 16u, + "the overflow flag should clear per generation so the next generation stages and decodes normally"); + }); + + tc.Run("host feed staged before EOF is discarded, not resurrected after EOF", [](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; + + ps2_stubs::notifyMpegCdStreamStart(); + + const std::vector packet = makeMpegHostFeedTestPacket(); + ps2_stubs::feedMpegCdStreamBytes(packet.data(), packet.size()); + + ps2_stubs::notifyMpegCdStreamEof(); + + 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"); + + R5900Context pictureCtx{}; + setRegU32(pictureCtx, 4, kMpegAddr); + setRegU32(pictureCtx, 5, kImageAddr); + ps2_stubs::sceMpegGetPicture(rdram.data(), &pictureCtx, nullptr); + t.Equals(Ps2FastRead32(rdram.data(), kMpegAddr + 0x00u), 320u, + "the stage should be discarded at EOF; a post-EOF decoder must not replay a dead generation's " + "prefix (width should stay the safe seeded default, not the fed stream's real width)"); + }); + + 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;