feat: bad wip mpeg fix for code veronica

This commit is contained in:
Ran-j
2026-07-25 21:26:31 -03:00
parent cc941d2f4f
commit 9f99d615c5
8 changed files with 375 additions and 39 deletions
+2
View File
@@ -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
+2 -5
View File
@@ -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<void(R5900Context &)> 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<void(R5900Context &)> completion = {});
[[noreturn]] void waitExternal(EeWaitReason reason, uint32_t type, uint64_t token, std::function<void(R5900Context &)> completion = {});
[[nodiscard]] GuestThread *thread(int id);
[[nodiscard]] const GuestThread *thread(int id) const;
-2
View File
@@ -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<uint64_t> csr;
std::atomic<uint64_t> vsyncTick;
uint64_t imr; // Interrupt mask
+13 -13
View File
@@ -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<uint32_t>(id);
});
{ return scheduled.event.type == EeEventType::Alarm &&
scheduled.event.id == static_cast<uint32_t>(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<void(R5900Context &)> 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<uint32_t>(item.waiters.size())});
item.bits,
item.initBits,
item.attr,
static_cast<uint32_t>(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)
+3 -4
View File
@@ -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<uint32_t>(bytes),
hitStreamEnd || g_cdStreamingLbn == g_cdStreamingEndLbn);
}
else
{
+168 -15
View File
@@ -477,6 +477,8 @@ namespace ps2_stubs
std::vector<uint32_t> pssGuestAddrs;
std::deque<MpegDecodedFrame> decodedFrames;
std::unique_ptr<MpegFfmpegDecoder> decoder;
uint64_t pictureIntervalQ32 = 0u;
uint64_t nextPictureTickQ32 = std::numeric_limits<uint64_t>::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<size_t>::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<uint32_t>(data[i + 5u]) >> 4u);
const uint32_t height = ((static_cast<uint32_t>(data[i + 5u]) & 0x0Fu) << 8u) |
static_cast<uint32_t>(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<uint64_t>::max();
playback.waitingForVideoSequenceHeader = false;
playback.decoderFailed = false;
playback.decoder.reset();
@@ -1064,6 +1118,36 @@ namespace ps2_stubs
flushDecoderIfEnded(playback);
}
void finalizeCdStreamEofUnlocked(std::vector<uint32_t> &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<uint32_t> &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<std::mutex> 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<std::mutex> 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<uint32_t> completedMpegIds;
bool changed = false;
{
std::lock_guard<std::mutex> 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<MpegStreamCallbackEvent> callbackEvents;
std::vector<uint32_t> completedMpegIds;
size_t consumed = 0u;
size_t decodedCount = 0u;
uint32_t traceIdx = 0u;
bool eofChanged = false;
{
std::lock_guard<std::mutex> 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<MpegStreamCallbackEvent> callbackEvents;
std::vector<uint32_t> completedMpegIds;
size_t consumed = 0u;
size_t decodedCount = 0u;
uint32_t traceIdx = 0u;
bool eofChanged = false;
{
std::lock_guard<std::mutex> 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<uint64_t>::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<int32_t>(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<uint32_t>(frame.width);
@@ -1948,10 +2095,16 @@ namespace ps2_stubs
(void)rdram;
std::lock_guard<std::mutex> 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);
}
+1
View File
@@ -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);
@@ -151,6 +151,10 @@ namespace
std::atomic<uint32_t> gMpegWaitStage{0u};
std::atomic<uint32_t> gMpegNoDuplicateStage{0u};
std::atomic<uint32_t> gMpegNoDuplicateProducerStage{0u};
std::atomic<uint32_t> gMpegPacingStage{0u};
std::atomic<uint32_t> gMpegPacingFirstProducerStage{0u};
std::atomic<uint32_t> gMpegPacingSecondProducerStage{0u};
std::atomic<uint64_t> 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<uint8_t> rdram(PS2_RAM_SIZE, 0u);
ps2_stubs::resetMpegStubState();
ps2_stubs::notifyMpegCdStreamStart();
constexpr uint32_t kMpegAddr = 0x00126000u;
constexpr uint32_t kPacketAddr = 0x00127000u;
const std::vector<uint8_t> 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<uint32_t>(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<uint32_t>(finalPacket.size()));
ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &firstDemuxCtx, &runtime);
t.Equals(getRegS32(firstDemuxCtx, 2), static_cast<int32_t>(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<uint32_t>(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<uint32_t>(finalPacket.size()));
ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &finalDemuxCtx, &runtime);
t.Equals(getRegS32(finalDemuxCtx, 2), static_cast<int32_t>(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<uint8_t> rdram(PS2_RAM_SIZE, 0u);
ps2_stubs::resetMpegStubState();
ps2_stubs::notifyMpegCdStreamStart();
constexpr uint32_t kSequencePacketAddr = 0x00127000u;
const std::vector<uint8_t> 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<uint32_t>(sequencePacket.size()));
setRegU32(demuxContext, 7, kSequencePacketAddr);
setRegU32(demuxContext, 8, static_cast<uint32_t>(sequencePacket.size()));
ps2_stubs::sceMpegDemuxPssRing(rdram.data(), &demuxContext, &runtime);
t.Equals(
getRegS32(demuxContext, 2),
static_cast<int32_t>(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<uint8_t> rdram(PS2_RAM_SIZE, 0u);