diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index 94be058..4259e12 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -382,7 +382,7 @@ public: EeScheduler &eeScheduler(); const EeScheduler &eeScheduler() const; void postEeEvent(EeEvent event); - bool eeCheckpointDue() const noexcept; + bool eeCheckpointDue(uint32_t cycles = 32u) noexcept; [[noreturn]] void eeWaitVSyncTicks(uint32_t ticks, uint32_t resumePc); struct EeExitHandlerRegistration diff --git a/ps2xRuntime/include/runtime/ee_scheduler.h b/ps2xRuntime/include/runtime/ee_scheduler.h index 454cd94..a9ecc28 100644 --- a/ps2xRuntime/include/runtime/ee_scheduler.h +++ b/ps2xRuntime/include/runtime/ee_scheduler.h @@ -214,6 +214,9 @@ struct EeEventFlagSnapshot struct EeKernelSnapshot { uint64_t sequence = 0; + uint64_t eeCycle = 0; + uint64_t sliceEndCycle = 0; + uint64_t nextEventCycle = 0; int runningThreadId = 0; std::vector threads; std::vector semaphores; @@ -255,6 +258,10 @@ public: static constexpr int kFirstThreadId = 2; static constexpr int kLastThreadId = 255; static constexpr int kPriorityCount = 128; + static constexpr uint64_t kEeClockHz = 294912000ull; + static constexpr uint32_t kGeneratedCheckpointCycles = 32u; + static constexpr uint32_t kGuestDispatchCycles = 8u; + static constexpr uint64_t kDefaultTimeSliceCycles = 65536ull; explicit EeScheduler(PS2Runtime &runtime); ~EeScheduler(); @@ -266,7 +273,8 @@ public: void run(); void requestStop(); void postEvent(EeEvent event); - [[nodiscard]] bool checkpointDue() const noexcept; + [[nodiscard]] bool checkpointDue(uint32_t cycles = kGeneratedCheckpointCycles) noexcept; + void accountCycles(uint32_t cycles) noexcept; [[nodiscard]] bool isExecutingGuest() const noexcept; // Kernel object API. All calls except postEvent/requestStop execute on the @@ -349,7 +357,8 @@ public: private: struct ScheduledEvent { - std::chrono::steady_clock::time_point deadline{}; + uint64_t deadlineCycle = 0; + std::chrono::steady_clock::time_point hostDeadline{}; EeEvent event{}; uint64_t sequence = 0; }; @@ -375,8 +384,10 @@ private: static int waitObjectId(const EeWaitState &wait); void writeGuestU32(uint32_t address, uint32_t value); void waitForEvent(); - void scheduleEvent(std::chrono::steady_clock::time_point deadline, EeEvent event); + void scheduleEvent(uint64_t deadlineCycle, std::chrono::steady_clock::time_point hostDeadline, EeEvent event); void updateNextDeadline(); + [[nodiscard]] bool hasReadyAtOrAbovePriority(int priority) const; + void renewTimeSlice(); void copyMainContextToRuntime(); PS2Runtime &m_runtime; @@ -403,7 +414,10 @@ private: uint32_t m_enabledDmacMask = 0xFFFFFFFFu; int m_currentThreadId = 0; bool m_rescheduleRequested = false; + bool m_timeSliceExpired = false; bool m_insideInterrupt = false; + uint64_t m_eeCycle = 0; + uint64_t m_sliceEndCycle = kDefaultTimeSliceCycles; std::thread::id m_executorThread{}; std::atomic m_running{false}; std::atomic m_guestExecuting{false}; @@ -425,7 +439,7 @@ private: uint32_t m_gsVSyncCallbackGp = 0; uint32_t m_gsVSyncCallbackSp = 0; std::unordered_map m_invocationStackTops; - std::atomic m_nextDeadlineNanoseconds{0}; + std::atomic m_nextDeadlineCycle{0}; mutable std::mutex m_snapshotMutex; EeKernelSnapshot m_snapshot; diff --git a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp index 3adde69..1167207 100644 --- a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp +++ b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp @@ -36,6 +36,15 @@ namespace constexpr uint64_t kAlarmTickMicroseconds = 64u; constexpr uint32_t kDebugPublishDispatchInterval = 4096u; + constexpr uint64_t microsecondsToEeCycles(uint64_t microseconds) + { + return (microseconds * EeScheduler::kEeClockHz + 999999ull) / 1000000ull; + } + + constexpr uint64_t kVBlankPeriodCycles = microsecondsToEeCycles(16667u); + constexpr uint64_t kVBlankDurationCycles = microsecondsToEeCycles(500u); + constexpr uint64_t kAlarmTickCycles = microsecondsToEeCycles(kAlarmTickMicroseconds); + template int allocatePositiveId(int &nextId, const Map &objects) { @@ -90,7 +99,10 @@ void EeScheduler::reset(uint8_t *rdram, const R5900Context &mainContext) m_enabledDmacMask = 0xFFFFFFFFu; m_currentThreadId = 0; m_rescheduleRequested = false; + m_timeSliceExpired = false; m_insideInterrupt = false; + m_eeCycle = 0u; + m_sliceEndCycle = kDefaultTimeSliceCycles; m_stopRequested.store(false, std::memory_order_release); m_checkpointPending.store(false, std::memory_order_release); m_debugPublishCountdown = 0u; @@ -121,7 +133,8 @@ void EeScheduler::reset(uint8_t *rdram, const R5900Context &mainContext) main.status = EeThreadStatus::Ready; m_threads.emplace(main.id, std::move(main)); m_readyQueues[0].push_back(kMainThreadId); - scheduleEvent(std::chrono::steady_clock::now() + kVBlankPeriod, + scheduleEvent(m_eeCycle + kVBlankPeriodCycles, + std::chrono::steady_clock::now() + kVBlankPeriod, EeEvent{EeEventType::VBlankStart, 0, 0}); publishSnapshot(); } @@ -159,6 +172,7 @@ void EeScheduler::run() m_pendingInvocations.pop_front(); owner->status = EeThreadStatus::Running; m_currentThreadId = owner->id; + renewTimeSlice(); if (getRegU32(&invocation.context, 29) == 0u) { SET_GPR_U32(&invocation.context, 29, invocationStackTop()); @@ -258,11 +272,14 @@ void EeScheduler::run() } PS2Runtime::RecompiledFunction function = m_runtime.lookupFunction(context.pc); + if (checkpointDue(kGuestDispatchCycles)) + { + continue; + } + try { - m_insideInterrupt = !running->invocations.empty() && - running->invocations.back().kind == GuestInvocationKind::Interrupt; - m_checkpointPending.store(false, std::memory_order_release); + m_insideInterrupt = !running->invocations.empty() && running->invocations.back().kind == GuestInvocationKind::Interrupt; m_guestExecuting.store(true, std::memory_order_release); function(m_rdram, &context, &m_runtime); m_guestExecuting.store(false, std::memory_order_release); @@ -286,9 +303,10 @@ void EeScheduler::run() { GuestThread *preempted = currentThread(); assert(preempted != nullptr); - enqueueReady(*preempted, true); + enqueueReady(*preempted, !m_timeSliceExpired); m_currentThreadId = 0; m_rescheduleRequested = false; + m_timeSliceExpired = false; } } @@ -316,27 +334,48 @@ void EeScheduler::postEvent(EeEvent event) { std::lock_guard lock(m_eventMutex); m_events.push_back(event); + m_checkpointPending.store(true, std::memory_order_release); } - m_checkpointPending.store(true, std::memory_order_release); m_eventCv.notify_one(); } -bool EeScheduler::checkpointDue() const noexcept +bool EeScheduler::checkpointDue(uint32_t cycles) noexcept { + accountCycles(cycles); + if (m_checkpointPending.load(std::memory_order_acquire) || m_stopRequested.load(std::memory_order_acquire)) { return true; } - const int64_t deadline = m_nextDeadlineNanoseconds.load(std::memory_order_acquire); - if (deadline == 0) + + const uint64_t nextEventCycle = m_nextDeadlineCycle.load(std::memory_order_acquire); + if (nextEventCycle != 0u && m_eeCycle >= nextEventCycle) + { + m_checkpointPending.store(true, std::memory_order_release); + return true; + } + + if (m_eeCycle < m_sliceEndCycle) { return false; } - const int64_t now = std::chrono::duration_cast( - std::chrono::steady_clock::now().time_since_epoch()) - .count(); - return now >= deadline; + + const GuestThread *running = currentThread(); + if (running != nullptr && hasReadyAtOrAbovePriority(running->currentPriority)) + { + m_rescheduleRequested = true; + m_timeSliceExpired = true; + return true; + } + + renewTimeSlice(); + return false; +} + +void EeScheduler::accountCycles(uint32_t cycles) noexcept +{ + m_eeCycle += std::max(1u, cycles); } bool EeScheduler::isExecutingGuest() const noexcept @@ -737,6 +776,7 @@ void EeScheduler::transferIfRequested(bool interruptSafe) m_currentThreadId = 0; } m_rescheduleRequested = false; + m_timeSliceExpired = false; publishSnapshot(); throw EeDispatcherTransfer{}; } @@ -998,8 +1038,8 @@ int EeScheduler::setAlarm(uint16_t ticks, } m_alarms.emplace(id, EeAlarm{id, ticks, handler, argument, gp, sp}); const uint64_t tickCount = ticks == 0u ? 1u : static_cast(ticks); - scheduleEvent(std::chrono::steady_clock::now() + - std::chrono::microseconds(tickCount * kAlarmTickMicroseconds), + scheduleEvent(m_eeCycle + tickCount * kAlarmTickCycles, + std::chrono::steady_clock::now() + std::chrono::microseconds(tickCount * kAlarmTickMicroseconds), EeEvent{EeEventType::Alarm, static_cast(id), 0}); return id; } @@ -1409,6 +1449,9 @@ void EeScheduler::publishSnapshot() { EeKernelSnapshot next{}; next.sequence = ++m_snapshotSequence; + next.eeCycle = m_eeCycle; + next.sliceEndCycle = m_sliceEndCycle; + next.nextEventCycle = m_nextDeadlineCycle.load(std::memory_order_acquire); next.runningThreadId = m_currentThreadId; next.threads.reserve(m_threads.size()); for (const auto &[id, item] : m_threads) @@ -1550,6 +1593,7 @@ void EeScheduler::makeRunning(GuestThread &item) assert(item.status == EeThreadStatus::Ready); item.status = EeThreadStatus::Running; m_currentThreadId = item.id; + renewTimeSlice(); } void EeScheduler::makeDormant(GuestThread &item) @@ -1643,13 +1687,15 @@ void EeScheduler::applyPendingPreemption() if (m_currentThreadId == 0) { m_rescheduleRequested = false; + m_timeSliceExpired = false; return; } GuestThread *self = currentThread(); assert(self != nullptr); - enqueueReady(*self, true); + enqueueReady(*self, !m_timeSliceExpired); m_currentThreadId = 0; m_rescheduleRequested = false; + m_timeSliceExpired = false; } void EeScheduler::processPendingEvents() @@ -1665,49 +1711,100 @@ void EeScheduler::processPendingEvents() { processEvent(event); } - m_checkpointPending.store(false, std::memory_order_release); + + { + std::lock_guard lock(m_eventMutex); + const uint64_t nextEventCycle = m_nextDeadlineCycle.load(std::memory_order_acquire); + const bool cycleEventDue = nextEventCycle != 0u && m_eeCycle >= nextEventCycle; + const bool pendingWork = !m_events.empty() || cycleEventDue || m_stopRequested.load(std::memory_order_acquire); + m_checkpointPending.store(pendingWork, std::memory_order_release); + } applyPendingPreemption(); } void EeScheduler::processDueDeadlines() { - const auto now = std::chrono::steady_clock::now(); - std::vector due; + for (;;) { - std::lock_guard lock(m_eventMutex); - auto firstFuture = std::partition(m_deadlines.begin(), m_deadlines.end(), [now](const ScheduledEvent &item) - { return item.deadline <= now; }); - due.insert(due.end(), - std::make_move_iterator(m_deadlines.begin()), - std::make_move_iterator(firstFuture)); - m_deadlines.erase(m_deadlines.begin(), firstFuture); - updateNextDeadline(); - } - std::sort(due.begin(), due.end(), [](const ScheduledEvent &left, const ScheduledEvent &right) - { - if (left.deadline != right.deadline) - { - return left.deadline < right.deadline; - } - if (left.event.type != right.event.type) - { - return left.event.type < right.event.type; - } - if (left.event.id != right.event.id) - { - return left.event.id < right.event.id; - } - return left.sequence < right.sequence; }); - for (ScheduledEvent &scheduled : due) - { - if (scheduled.event.type == EeEventType::VBlankStart) + std::vector due; + std::chrono::steady_clock::time_point pacingDeadline{}; { - scheduleEvent(scheduled.deadline + kVBlankDuration, - EeEvent{EeEventType::VBlankEnd, 0, m_vsyncTick + 1u}); - scheduleEvent(scheduled.deadline + kVBlankPeriod, - EeEvent{EeEventType::VBlankStart, 0, 0}); + std::unique_lock lock(m_eventMutex); + const auto now = std::chrono::steady_clock::now(); + for (const ScheduledEvent &item : m_deadlines) + { + if (item.deadlineCycle <= m_eeCycle && + (pacingDeadline == std::chrono::steady_clock::time_point{} || + item.hostDeadline < pacingDeadline)) + { + pacingDeadline = item.hostDeadline; + } + } + + if (pacingDeadline == std::chrono::steady_clock::time_point{}) + { + updateNextDeadline(); + return; + } + + if (now < pacingDeadline) + { + m_eventCv.wait_until(lock, pacingDeadline, [this]() + { return !m_events.empty() || + m_stopRequested.load(std::memory_order_acquire); }); + if (!m_events.empty() || m_stopRequested.load(std::memory_order_acquire)) + { + updateNextDeadline(); + return; + } + } + + const auto pacedNow = std::chrono::steady_clock::now(); + auto firstFuture = std::partition(m_deadlines.begin(), m_deadlines.end(), + [this, pacedNow](const ScheduledEvent &item) + { return item.deadlineCycle <= m_eeCycle && + item.hostDeadline <= pacedNow; }); + due.insert(due.end(), + std::make_move_iterator(m_deadlines.begin()), + std::make_move_iterator(firstFuture)); + m_deadlines.erase(m_deadlines.begin(), firstFuture); + updateNextDeadline(); + } + + std::sort(due.begin(), due.end(), [](const ScheduledEvent &left, const ScheduledEvent &right) + { + if (left.deadlineCycle != right.deadlineCycle) + { + return left.deadlineCycle < right.deadlineCycle; + } + if (left.event.type != right.event.type) + { + return left.event.type < right.event.type; + } + if (left.event.id != right.event.id) + { + return left.event.id < right.event.id; + } + return left.sequence < right.sequence; }); + + if (due.empty()) + { + return; + } + + for (ScheduledEvent &scheduled : due) + { + if (scheduled.event.type == EeEventType::VBlankStart) + { + scheduleEvent(scheduled.deadlineCycle + kVBlankDurationCycles, + scheduled.hostDeadline + kVBlankDuration, + EeEvent{EeEventType::VBlankEnd, 0, m_vsyncTick + 1u}); + scheduleEvent(scheduled.deadlineCycle + kVBlankPeriodCycles, + scheduled.hostDeadline + kVBlankPeriod, + EeEvent{EeEventType::VBlankStart, 0, 0}); + } + processEvent(scheduled.event); } - processEvent(scheduled.event); } } @@ -1849,23 +1946,45 @@ void EeScheduler::writeGuestU32(uint32_t address, uint32_t value) void EeScheduler::waitForEvent() { std::unique_lock lock(m_eventMutex); + if (!m_events.empty() || m_stopRequested.load(std::memory_order_acquire)) + { + return; + } if (m_deadlines.empty()) { m_eventCv.wait(lock, [this]() { return !m_events.empty() || m_stopRequested.load(std::memory_order_acquire); }); return; } - const auto next = std::min_element(m_deadlines.begin(), m_deadlines.end(), [](const ScheduledEvent &left, const ScheduledEvent &right) - { return left.deadline < right.deadline; }) - ->deadline; - m_eventCv.wait_until(lock, next); + + const auto next = std::min_element(m_deadlines.begin(), m_deadlines.end(), + [](const ScheduledEvent &left, const ScheduledEvent &right) + { + if (left.deadlineCycle != right.deadlineCycle) + { + return left.deadlineCycle < right.deadlineCycle; + } + return left.sequence < right.sequence; + }); + const uint64_t deadlineCycle = next->deadlineCycle; + const auto hostDeadline = next->hostDeadline; + const bool signaled = m_eventCv.wait_until(lock, hostDeadline, [this]() + { return !m_events.empty() || + m_stopRequested.load(std::memory_order_acquire); }); + if (!signaled) + { + m_eeCycle = std::max(m_eeCycle, deadlineCycle); + m_checkpointPending.store(true, std::memory_order_release); + } } -void EeScheduler::scheduleEvent(std::chrono::steady_clock::time_point deadline, EeEvent event) +void EeScheduler::scheduleEvent(uint64_t deadlineCycle, + std::chrono::steady_clock::time_point hostDeadline, + EeEvent event) { { std::lock_guard lock(m_eventMutex); - m_deadlines.push_back(ScheduledEvent{deadline, event, ++m_eventSequence}); + m_deadlines.push_back(ScheduledEvent{deadlineCycle, hostDeadline, event, ++m_eventSequence}); updateNextDeadline(); } m_eventCv.notify_one(); @@ -1875,15 +1994,38 @@ void EeScheduler::updateNextDeadline() { if (m_deadlines.empty()) { - m_nextDeadlineNanoseconds.store(0, std::memory_order_release); + m_nextDeadlineCycle.store(0u, std::memory_order_release); return; } - const auto it = std::min_element(m_deadlines.begin(), m_deadlines.end(), [](const ScheduledEvent &left, const ScheduledEvent &right) - { return left.deadline < right.deadline; }); - const int64_t nanoseconds = std::chrono::duration_cast( - it->deadline.time_since_epoch()) - .count(); - m_nextDeadlineNanoseconds.store(nanoseconds, std::memory_order_release); + const auto it = std::min_element(m_deadlines.begin(), m_deadlines.end(), + [](const ScheduledEvent &left, const ScheduledEvent &right) + { + if (left.deadlineCycle != right.deadlineCycle) + { + return left.deadlineCycle < right.deadlineCycle; + } + return left.sequence < right.sequence; + }); + m_nextDeadlineCycle.store(it->deadlineCycle, std::memory_order_release); +} + +bool EeScheduler::hasReadyAtOrAbovePriority(int priority) const +{ + const int last = std::clamp(priority, 0, kPriorityCount - 1); + for (int p = 0; p <= last; ++p) + { + if (!m_readyQueues[static_cast(p)].empty()) + { + return true; + } + } + return false; +} + +void EeScheduler::renewTimeSlice() +{ + m_sliceEndCycle = m_eeCycle + kDefaultTimeSliceCycles; + m_timeSliceExpired = false; } void EeScheduler::copyMainContextToRuntime() diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp index 4c20a21..56a978e 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp @@ -1,14 +1,173 @@ #include "Common.h" #include "CD.h" #include "MPEG.h" +#include "runtime/ee_scheduler.h" namespace ps2_stubs { namespace { - uint32_t g_cdStReadTraceCount = 0u; - } + constexpr uint32_t kCdStreamBlocking = 1u; + constexpr uint32_t kDvdSectorsPerSecondX1 = 675u; + constexpr uint32_t kDvdSectorsPerSecondX4 = kDvdSectorsPerSecondX1 * 4u; + constexpr uint64_t kNtScFieldsPerSecondNumerator = 60000u; + constexpr uint64_t kNtScFieldsPerSecondDenominator = 1001u; + struct CdStreamTimingState + { + bool initialized = false; + bool active = false; + bool paused = false; + uint32_t capacitySectors = 64u; + uint32_t bankCount = 4u; + uint32_t sectorsPerBank = 16u; + uint32_t sectorsPerSecond = kDvdSectorsPerSecondX4; + uint64_t producedSectors = 0u; + uint64_t consumedSectors = 0u; + uint64_t productionRemainder = 0u; + uint64_t lastVSyncTick = 0u; + }; + + uint32_t g_cdStReadTraceCount = 0u; + CdStreamTimingState g_cdStreamTiming; + + uint64_t currentCdStreamTick(PS2Runtime *runtime) + { + return runtime != nullptr ? runtime->eeScheduler().currentVSyncTick() : 0u; + } + + uint32_t dvdStreamSectorsPerSecond(uint8_t spindleControl) + { + switch (spindleControl) + { + case 2u: // SCECdSpinX1 + return kDvdSectorsPerSecondX1; + case 3u: // SCECdSpinX2 + return kDvdSectorsPerSecondX1 * 2u; + case 11u: // SCECdSpin1p6 + return 1080u; + case 4u: // SCECdSpinX4 + case 0u: // SCECdSpinStm / max + case 1u: // optimized + case 20u: // max + default: + return kDvdSectorsPerSecondX4; + } + } + + void resetCdStreamProduction(PS2Runtime *runtime) + { + g_cdStreamTiming.producedSectors = 0u; + g_cdStreamTiming.consumedSectors = 0u; + g_cdStreamTiming.productionRemainder = 0u; + g_cdStreamTiming.lastVSyncTick = currentCdStreamTick(runtime); + } + + uint64_t totalCdStreamSectors() + { + if (g_cdStreamingEndLbn == 0xFFFFFFFFu || g_cdStreamingEndLbn < g_cdStreamingLbn) + { + return std::numeric_limits::max(); + } + return g_cdStreamTiming.consumedSectors + static_cast(g_cdStreamingEndLbn - g_cdStreamingLbn); + } + + void updateCdStreamProduction(PS2Runtime *runtime) + { + if (!g_cdStreamTiming.active || g_cdStreamTiming.paused || runtime == nullptr) + { + return; + } + + const uint64_t tick = currentCdStreamTick(runtime); + if (tick <= g_cdStreamTiming.lastVSyncTick) + { + return; + } + + const uint64_t elapsedTicks = tick - g_cdStreamTiming.lastVSyncTick; + g_cdStreamTiming.lastVSyncTick = tick; + + // 59.94 Hz field clock: sectors = fields * sectors/s * 1001 / 60000. + const uint64_t unitsPerTick = static_cast(g_cdStreamTiming.sectorsPerSecond) * kNtScFieldsPerSecondDenominator; + const uint64_t accumulated = g_cdStreamTiming.productionRemainder + elapsedTicks * unitsPerTick; + const uint64_t elapsedProduction = accumulated / kNtScFieldsPerSecondNumerator; + g_cdStreamTiming.productionRemainder = accumulated % kNtScFieldsPerSecondNumerator; + if (elapsedProduction == 0u) + { + return; + } + + const uint64_t buffered = g_cdStreamTiming.producedSectors - g_cdStreamTiming.consumedSectors; + const uint64_t effectiveCapacity = std::max(1u, g_cdStreamTiming.capacitySectors); + const uint64_t space = buffered < effectiveCapacity ? effectiveCapacity - buffered : 0u; + uint64_t newlyProduced = std::min(elapsedProduction, space); + + const uint64_t streamTotal = totalCdStreamSectors(); + if (streamTotal != std::numeric_limits::max()) + { + const uint64_t remainingToProduce = streamTotal > g_cdStreamTiming.producedSectors + ? streamTotal - g_cdStreamTiming.producedSectors + : 0u; + newlyProduced = std::min(newlyProduced, remainingToProduce); + } + + g_cdStreamTiming.producedSectors += newlyProduced; + if (elapsedProduction > newlyProduced) + { + g_cdStreamTiming.productionRemainder = 0u; + } + } + + uint32_t bufferedCdStreamSectors(PS2Runtime *runtime) + { + updateCdStreamProduction(runtime); + const uint64_t buffered = g_cdStreamTiming.producedSectors - g_cdStreamTiming.consumedSectors; + return static_cast(std::min(buffered, std::numeric_limits::max())); + } + + uint32_t readableCdStreamSectors(PS2Runtime *runtime) + { + const uint32_t buffered = bufferedCdStreamSectors(runtime); + if (buffered == 0u) + { + return 0u; + } + + const uint64_t streamTotal = totalCdStreamSectors(); + if (streamTotal != std::numeric_limits::max() && g_cdStreamTiming.producedSectors >= streamTotal) + { + return buffered; + } + + const uint32_t bank = std::max(1u, g_cdStreamTiming.sectorsPerBank); + return (buffered / bank) * bank; + } + + uint64_t cdStreamWakeTickForSectors(PS2Runtime *runtime, uint32_t sectorsNeeded) + { + const uint64_t now = currentCdStreamTick(runtime); + if (sectorsNeeded == 0u || g_cdStreamTiming.sectorsPerSecond == 0u) + { + return now; + } + + const uint64_t requiredUnits = static_cast(sectorsNeeded) * kNtScFieldsPerSecondNumerator; + const uint64_t remainingUnits = requiredUnits > g_cdStreamTiming.productionRemainder + ? requiredUnits - g_cdStreamTiming.productionRemainder + : 0u; + const uint64_t unitsPerTick = static_cast(g_cdStreamTiming.sectorsPerSecond) * kNtScFieldsPerSecondDenominator; + const uint64_t ticks = std::max(1u, (remainingUnits + unitsPerTick - 1u) / unitsPerTick); + return now + ticks; + } + + void restartCdStreamAt(uint32_t lbn, PS2Runtime *runtime) + { + g_cdStreamingLbn = lbn; + g_cdStreamingEndLbn = cdStreamingEndLbnForStart(lbn); + resetCdStreamProduction(runtime); + } + } CdDebugSnapshot getCdDebugSnapshot() { @@ -41,9 +200,7 @@ namespace ps2_stubs snapshot.files.push_back(std::move(row)); } std::sort(snapshot.files.begin(), snapshot.files.end(), [](const CdDebugFileEntry &a, const CdDebugFileEntry &b) - { - return a.baseLbn < b.baseLbn; - }); + { return a.baseLbn < b.baseLbn; }); return snapshot; } @@ -241,6 +398,7 @@ namespace ps2_stubs { g_cdInitialized = true; g_lastCdError = 0; + g_cdStreamTiming = {}; setReturnS32(ctx, 1); } @@ -492,8 +650,7 @@ namespace ps2_stubs void sceCdSeek(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - g_cdStreamingLbn = getRegU32(ctx, 4); - g_cdStreamingEndLbn = cdStreamingEndLbnForStart(g_cdStreamingLbn); + restartCdStreamAt(getRegU32(ctx, 4), runtime); setReturnS32(ctx, 1); } @@ -509,6 +666,23 @@ namespace ps2_stubs void sceCdStInit(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + const uint32_t bufferSectors = getRegU32(ctx, 4); + const uint32_t bankCount = getRegU32(ctx, 5); + const uint32_t bufferAddr = getRegU32(ctx, 6); + + if (bufferSectors == 0u || bankCount == 0u || bufferAddr == 0u || bufferSectors / bankCount == 0u) + { + setReturnS32(ctx, 0); + return; + } + + g_cdStreamTiming.initialized = true; + g_cdStreamTiming.active = false; + g_cdStreamTiming.paused = false; + g_cdStreamTiming.capacitySectors = bufferSectors; + g_cdStreamTiming.bankCount = bankCount; + g_cdStreamTiming.sectorsPerBank = std::max(1u, bufferSectors / bankCount); + resetCdStreamProduction(runtime); setReturnS32(ctx, 1); } @@ -519,89 +693,216 @@ namespace ps2_stubs void sceCdStPause(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + updateCdStreamProduction(runtime); + g_cdStreamTiming.paused = true; setReturnS32(ctx, 1); } + namespace + { + struct CdStReadContinuation + { + uint32_t requestedSectors = 0u; + uint32_t buffer = 0u; + uint32_t errorAddress = 0u; + uint32_t sectorsRead = 0u; + }; + + void finishCdStRead(uint8_t *rdram, R5900Context *ctx, const CdStReadContinuation &state, int32_t error) + { + if (int32_t *errorOut = reinterpret_cast(getMemPtr(rdram, state.errorAddress)); errorOut) + { + *errorOut = error; + } + setReturnS32(ctx, static_cast(state.sectorsRead)); + } + + void continueCdStRead(uint8_t *rdram, + R5900Context *ctx, + PS2Runtime *runtime, + CdStReadContinuation state) + { + if (!g_cdStreamTiming.active || state.requestedSectors == 0u) + { + finishCdStRead(rdram, ctx, state, 0); + return; + } + + for (;;) + { + uint32_t remaining = state.requestedSectors - state.sectorsRead; + bool atEnd = false; + if (g_cdStreamingEndLbn != 0xFFFFFFFFu) + { + if (g_cdStreamingLbn >= g_cdStreamingEndLbn) + { + remaining = 0u; + atEnd = true; + } + else + { + const uint32_t streamRemaining = g_cdStreamingEndLbn - g_cdStreamingLbn; + if (remaining > streamRemaining) + { + remaining = streamRemaining; + atEnd = true; + } + } + } + + if (remaining == 0u) + { + if (atEnd || (g_cdStreamingEndLbn != 0xFFFFFFFFu && g_cdStreamingLbn >= g_cdStreamingEndLbn)) + { + notifyMpegCdStreamEof(runtime); + } + finishCdStRead(rdram, ctx, state, 0); + return; + } + + uint32_t available = readableCdStreamSectors(runtime); + if (runtime == nullptr) + { + available = remaining; + } + + if (available == 0u) + { + if (!runtime) + { + finishCdStRead(rdram, ctx, state, 0); + return; + } + + const uint32_t bank = std::max(1u, g_cdStreamTiming.sectorsPerBank); + const uint32_t wakeSectors = std::min(remaining, bank); + const uint32_t buffered = bufferedCdStreamSectors(runtime); + const uint32_t needed = wakeSectors > buffered ? wakeSectors - buffered : 1u; + const uint64_t wakeTick = cdStreamWakeTickForSectors(runtime, needed); + runtime->eeScheduler().waitVSync( + wakeTick - 1u, + -1, + [rdram, runtime, state](R5900Context &resumeContext) + { + if (static_cast(getRegU32(&resumeContext, 2)) < 0) + { + return; + } + continueCdStRead(rdram, &resumeContext, runtime, state); + }); + } + + uint32_t sectors = std::min(remaining, available); + const uint64_t destination64 = static_cast(state.buffer) + static_cast(state.sectorsRead) * kCdSectorSize; + const uint32_t destination = static_cast(destination64); + const uint32_t offset = destination & PS2_RAM_MASK; + const size_t maxBytes = PS2_RAM_SIZE - offset; + sectors = std::min(sectors, static_cast(maxBytes / kCdSectorSize)); + + if (sectors == 0u) + { + g_lastCdError = -1; + finishCdStRead(rdram, ctx, state, g_lastCdError); + return; + } + + const uint32_t readLbn = g_cdStreamingLbn; + const size_t readBytes = static_cast(sectors) * kCdSectorSize; + if (!readCdSectors(readLbn, sectors, rdram + offset, readBytes)) + { + finishCdStRead(rdram, ctx, state, g_lastCdError); + return; + } + + g_cdStreamingLbn += sectors; + if (runtime == nullptr) + { + g_cdStreamTiming.producedSectors += sectors; + } + g_cdStreamTiming.consumedSectors += sectors; + state.sectorsRead += sectors; + + const bool hitStreamEnd = g_cdStreamingEndLbn != 0xFFFFFFFFu && g_cdStreamingLbn >= g_cdStreamingEndLbn; + notifyMpegCdStreamDataProduced(static_cast(readBytes), hitStreamEnd); + + if (g_cdStReadTraceCount < 32u) + { + std::cerr << "[sceCdStRead] requested=" << state.requestedSectors + << " accumulated=" << state.sectorsRead + << " chunk=" << sectors + << " buffered=" << readableCdStreamSectors(runtime) + << " lbn=0x" << std::hex << readLbn + << " end=0x" << g_cdStreamingEndLbn + << std::dec << std::endl; + ++g_cdStReadTraceCount; + } + + if (state.sectorsRead >= state.requestedSectors || hitStreamEnd) + { + finishCdStRead(rdram, ctx, state, 0); + return; + } + + // STMBLK is implemented by repeatedly consuming the stream ring. + // If a complete bank is still available, keep draining it before + // yielding. Otherwise the next iteration parks on the producer. + } + } + } + void sceCdStRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - uint32_t requestedSectors = getRegU32(ctx, 4); - uint32_t sectors = requestedSectors; - uint32_t buf = getRegU32(ctx, 5); - uint32_t errAddr = getRegU32(ctx, 7); + const uint32_t requestedSectors = getRegU32(ctx, 4); + const uint32_t buffer = getRegU32(ctx, 5); + const uint32_t mode = getRegU32(ctx, 6); + const uint32_t errorAddress = getRegU32(ctx, 7); - uint32_t offset = buf & PS2_RAM_MASK; - size_t requestedBytes = static_cast(requestedSectors) * kCdSectorSize; - const size_t maxBytes = PS2_RAM_SIZE - offset; - if (requestedBytes > maxBytes) + CdStReadContinuation state{}; + state.requestedSectors = requestedSectors; + state.buffer = buffer; + state.errorAddress = errorAddress; + + if (int32_t *errorOut = reinterpret_cast(getMemPtr(rdram, errorAddress)); errorOut) { - requestedBytes = maxBytes; + *errorOut = 0; } - bool hitStreamEnd = false; + if (!g_cdStreamTiming.active || requestedSectors == 0u) + { + setReturnS32(ctx, 0); + return; + } + + if (mode == kCdStreamBlocking) + { + continueCdStRead(rdram, ctx, runtime, state); + return; + } + + uint32_t remaining = requestedSectors; if (g_cdStreamingEndLbn != 0xFFFFFFFFu) { - if (g_cdStreamingLbn >= g_cdStreamingEndLbn) + remaining = g_cdStreamingLbn < g_cdStreamingEndLbn + ? std::min(remaining, g_cdStreamingEndLbn - g_cdStreamingLbn) + : 0u; + } + + const uint32_t available = runtime != nullptr + ? readableCdStreamSectors(runtime) + : remaining; + const uint32_t sectors = std::min(remaining, available); + if (sectors == 0u) + { + if (g_cdStreamingEndLbn != 0xFFFFFFFFu && g_cdStreamingLbn >= g_cdStreamingEndLbn) { - sectors = 0u; - hitStreamEnd = true; - } - else - { - const uint32_t remaining = g_cdStreamingEndLbn - g_cdStreamingLbn; - if (sectors > remaining) - { - sectors = remaining; - hitStreamEnd = true; - } + notifyMpegCdStreamEof(runtime); } + setReturnS32(ctx, 0); + return; } - size_t bytes = static_cast(sectors) * kCdSectorSize; - if (bytes > maxBytes) - { - bytes = maxBytes; - } - - const uint32_t readLbn = g_cdStreamingLbn; - const bool ok = (sectors > 0u) && readCdSectors(readLbn, sectors, rdram + offset, bytes); - if (ok) - { - g_cdStreamingLbn += sectors; - if (requestedBytes > bytes) - { - std::memset(rdram + offset + bytes, 0, requestedBytes - bytes); - } - notifyMpegCdStreamDataProduced( - static_cast(bytes), - hitStreamEnd || g_cdStreamingLbn == g_cdStreamingEndLbn); - } - else - { - if (requestedBytes > 0u) - { - std::memset(rdram + offset, 0, requestedBytes); - } - notifyMpegCdStreamEof(runtime); - } - - if (int32_t *err = reinterpret_cast(getMemPtr(rdram, errAddr)); err) - { - *err = ok ? 0 : g_lastCdError; - } - - if (g_cdStReadTraceCount < 32u) - { - std::cerr << "[sceCdStRead] sectors=" << requestedSectors - << " read=" << sectors - << " buf=0x" << std::hex << buf - << " lbn=0x" << readLbn - << " end=0x" << g_cdStreamingEndLbn - << std::dec << " ok=" << ok - << " bytes=" << bytes << std::endl; - ++g_cdStReadTraceCount; - } - - setReturnS32(ctx, ok ? static_cast(sectors) : 0); + state.requestedSectors = sectors; + continueCdStRead(rdram, ctx, runtime, state); } void sceCdStream(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -611,43 +912,62 @@ namespace ps2_stubs void sceCdStResume(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + g_cdStreamTiming.paused = false; + g_cdStreamTiming.lastVSyncTick = currentCdStreamTick(runtime); setReturnS32(ctx, 1); } void sceCdStSeek(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - g_cdStreamingLbn = getRegU32(ctx, 4); - g_cdStreamingEndLbn = cdStreamingEndLbnForStart(g_cdStreamingLbn); + restartCdStreamAt(getRegU32(ctx, 4), runtime); setReturnS32(ctx, 1); } void sceCdStSeekF(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - g_cdStreamingLbn = getRegU32(ctx, 4); - g_cdStreamingEndLbn = cdStreamingEndLbnForStart(g_cdStreamingLbn); + restartCdStreamAt(getRegU32(ctx, 4), runtime); setReturnS32(ctx, 1); } void sceCdStStart(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - g_cdStreamingLbn = getRegU32(ctx, 4); - g_cdStreamingEndLbn = cdStreamingEndLbnForStart(g_cdStreamingLbn); + const uint32_t lbn = getRegU32(ctx, 4); + const uint32_t modeAddr = getRegU32(ctx, 5); + uint8_t spindleControl = 0u; + if (const uint8_t *mode = getConstMemPtr(rdram, modeAddr); mode) + { + spindleControl = mode[1u]; + } + + restartCdStreamAt(lbn, runtime); + g_cdStreamTiming.active = true; + g_cdStreamTiming.paused = false; + g_cdStreamTiming.sectorsPerSecond = dvdStreamSectorsPerSecond(spindleControl); g_cdStReadTraceCount = 0u; notifyMpegCdStreamStart(runtime); std::cerr << "[sceCdStStart] lbn=0x" << std::hex << g_cdStreamingLbn - << " endLbn=0x" << g_cdStreamingEndLbn << std::dec << std::endl; + << " endLbn=0x" << g_cdStreamingEndLbn << std::dec + << " rate=" << g_cdStreamTiming.sectorsPerSecond << " sectors/s" + << " buffer=" << g_cdStreamTiming.capacitySectors << " sectors" + << " bank=" << g_cdStreamTiming.sectorsPerBank << " sectors" + << std::endl; setReturnS32(ctx, 1); } void sceCdStStat(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - setReturnS32(ctx, 0); + const uint32_t buffered = bufferedCdStreamSectors(runtime); + const uint32_t bankSize = std::max(1u, g_cdStreamTiming.sectorsPerBank); + setReturnS32(ctx, static_cast((buffered / bankSize) * bankSize)); } void sceCdStStop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + updateCdStreamProduction(runtime); + g_cdStreamTiming.active = false; + g_cdStreamTiming.paused = false; notifyMpegCdStreamEof(runtime); setReturnS32(ctx, 1); } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp index caa84a2..44eaef4 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp @@ -29,6 +29,8 @@ namespace ps2_stubs { int width = 0; int height = 0; + int repeatPict = 0; + int64_t pts90k = -1; std::vector rgba; }; @@ -46,13 +48,14 @@ namespace ps2_stubs void configureFfmpegLogLevel() { static std::once_flag s_once; - std::call_once(s_once, [] { + std::call_once(s_once, [] + { #if AGRESSIVE_LOGS - av_log_set_level(AV_LOG_WARNING); + av_log_set_level(AV_LOG_WARNING); #else - av_log_set_level(AV_LOG_ERROR); + av_log_set_level(AV_LOG_ERROR); #endif - }); + }); } class MpegFfmpegDecoder @@ -68,7 +71,7 @@ namespace ps2_stubs MpegFfmpegDecoder(const MpegFfmpegDecoder &) = delete; MpegFfmpegDecoder &operator=(const MpegFfmpegDecoder &) = delete; - bool feed(const uint8_t *data, size_t size, std::deque &frames) + bool feed(const uint8_t *data, size_t size, std::deque &frames, int64_t pts90k = -1, int64_t dts90k = -1) { if (!data || size == 0) { @@ -91,6 +94,8 @@ namespace ps2_stubs const uint8_t *cursor = data; size_t remaining = size; + int64_t parserPts = pts90k >= 0 ? pts90k : AV_NOPTS_VALUE; + int64_t parserDts = dts90k >= 0 ? dts90k : AV_NOPTS_VALUE; while (remaining > 0) { uint8_t *packetData = nullptr; @@ -104,8 +109,8 @@ namespace ps2_stubs &packetSize, cursor, chunk, - AV_NOPTS_VALUE, - AV_NOPTS_VALUE, + parserPts, + parserDts, 0); if (used < 0) { @@ -121,10 +126,16 @@ namespace ps2_stubs cursor += used; remaining -= static_cast(used); + if (used > 0) + { + parserPts = AV_NOPTS_VALUE; + parserDts = AV_NOPTS_VALUE; + } + if (packetSize > 0) { ++totalPacketsSent; - if (!sendPacket(packetData, static_cast(packetSize), frames)) + if (!sendPacket(packetData, static_cast(packetSize), frames, m_parser->pts, m_parser->dts)) { return false; } @@ -168,7 +179,7 @@ namespace ps2_stubs AV_NOPTS_VALUE, 0); (void)used; - if (packetSize > 0 && !sendPacket(packetData, static_cast(packetSize), frames)) + if (packetSize > 0 && !sendPacket(packetData, static_cast(packetSize), frames, m_parser->pts, m_parser->dts)) { return false; } @@ -253,7 +264,12 @@ namespace ps2_stubs } m_codecCtx->thread_count = 1; - m_codecCtx->skip_frame = AVDISCARD_NONKEY; + m_codecCtx->pkt_timebase = AVRational{1, 90000}; + // feedElementaryStream() does not create the decoder until a valid + // MPEG sequence header has been found. Dropping non-key pictures + // here therefore throws away real presentation frames and makes + // movies finish early once the EE is fast enough to drain them. + m_codecCtx->skip_frame = AVDISCARD_DEFAULT; m_codecCtx->err_recognition = 0; const int ret = avcodec_open2(m_codecCtx, codec, nullptr); if (ret < 0) @@ -265,11 +281,14 @@ namespace ps2_stubs m_initialized = true; m_drained = false; - m_seenKeyframe = false; return true; } - bool sendPacket(const uint8_t *data, size_t size, std::deque &frames) + bool sendPacket(const uint8_t *data, + size_t size, + std::deque &frames, + int64_t pts = AV_NOPTS_VALUE, + int64_t dts = AV_NOPTS_VALUE) { if (!data || size == 0) { @@ -284,6 +303,8 @@ namespace ps2_stubs return false; } std::memcpy(m_packet->data, data, size); + m_packet->pts = pts; + m_packet->dts = dts; int ret = avcodec_send_packet(m_codecCtx, m_packet); if (ret == AVERROR(EAGAIN)) @@ -332,12 +353,6 @@ namespace ps2_stubs return true; } - if (!m_seenKeyframe) - { - m_seenKeyframe = true; - m_codecCtx->skip_frame = AVDISCARD_DEFAULT; - } - if (!convertFrame(frames)) { av_frame_unref(m_frame); @@ -391,6 +406,10 @@ namespace ps2_stubs MpegDecodedFrame decoded; decoded.width = width; decoded.height = height; + decoded.repeatPict = std::max(0, m_frame->repeat_pict); + decoded.pts90k = m_frame->best_effort_timestamp != AV_NOPTS_VALUE + ? m_frame->best_effort_timestamp + : -1; decoded.rgba.resize(static_cast(width) * static_cast(height) * 4u); uint8_t *dstData[4] = {decoded.rgba.data(), nullptr, nullptr, nullptr}; @@ -423,14 +442,13 @@ namespace ps2_stubs AVPixelFormat m_swsFormat = AV_PIX_FMT_NONE; bool m_initialized = false; bool m_drained = false; - bool m_seenKeyframe = false; }; #else // TODO class MpegFfmpegDecoder { public: - bool feed(const uint8_t *, size_t, std::deque &) + bool feed(const uint8_t *, size_t, std::deque &, int64_t = -1, int64_t = -1) { static bool s_warnedNoFfmpeg = false; if (!s_warnedNoFfmpeg) @@ -460,6 +478,12 @@ namespace ps2_stubs bool stream = false; }; + constexpr uint64_t kPictureClockOne = 1ull << 32u; + // NTSC-style fields at ~59.94 Hz to keep MPEG timing yet (29.97 fps). + constexpr uint64_t kDefaultPictureIntervalQ32 = 2ull * kPictureClockOne; + constexpr size_t kMpegTimingScanLimit = 4096u; + constexpr size_t kMaxDecodedPicturesAhead = 8u; + struct MpegPlaybackState { uint32_t picturesServed = 0u; @@ -468,6 +492,7 @@ namespace ps2_stubs uint32_t decodeMode = 0u; uint32_t imageBufferAddr = 0u; bool sawInput = false; + bool sawSequenceEnd = false; bool streamEnded = false; bool decoderFailed = false; uint64_t cdStreamGeneration = 0u; @@ -477,8 +502,16 @@ namespace ps2_stubs std::vector pssGuestAddrs; std::deque decodedFrames; std::unique_ptr decoder; - uint64_t pictureIntervalQ32 = 0u; + uint8_t frameRateCode = 0u; + uint8_t frameRateExtensionN = 0u; + uint8_t frameRateExtensionD = 0u; + bool hasFrameRateExtension = false; + std::vector videoTimingScanBuffer; + uint64_t pictureIntervalQ32 = kDefaultPictureIntervalQ32; uint64_t nextPictureTickQ32 = std::numeric_limits::max(); + uint64_t presentationEndTickQ32 = std::numeric_limits::max(); + int64_t firstPresentedPts90k = -1; + uint64_t ptsPresentationBaseTickQ32 = 0u; }; struct MpegStreamCallbackEvent @@ -528,9 +561,8 @@ 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 mpegPictureIntervalQ32(uint8_t frameRateCode, uint8_t frameRateExtensionN = 0u, uint8_t frameRateExtensionD = 0u) { uint64_t frameRateNumerator = 0u; uint64_t frameRateDenominator = 1u; @@ -567,13 +599,179 @@ namespace ps2_stubs 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; + const uint64_t extensionNumerator = static_cast(frameRateExtensionN) + 1u; + const uint64_t extensionDenominator = static_cast(frameRateExtensionD) + 1u; + const uint64_t denominator = 1001u * frameRateNumerator * extensionNumerator; + const uint64_t numerator = (60000u * frameRateDenominator * extensionDenominator) << 32u; return std::max(kPictureClockOne, (numerator + denominator / 2u) / denominator); } + uint32_t readMpegBits(const uint8_t *data, size_t bitOffset, uint32_t bitCount) + { + uint32_t value = 0u; + for (uint32_t bit = 0u; bit < bitCount; ++bit) + { + const size_t absoluteBit = bitOffset + bit; + const uint8_t source = data[absoluteBit >> 3u]; + value = (value << 1u) | ((source >> (7u - static_cast(absoluteBit & 7u))) & 1u); + } + return value; + } + + void updateMpegPictureTiming(MpegPlaybackState &playback, const uint8_t *data, size_t size) + { + if (!data || size == 0u) + { + return; + } + + playback.videoTimingScanBuffer.insert(playback.videoTimingScanBuffer.end(), data, data + size); + if (playback.videoTimingScanBuffer.size() > kMpegTimingScanLimit) + { + const size_t discard = playback.videoTimingScanBuffer.size() - kMpegTimingScanLimit; + playback.videoTimingScanBuffer.erase(playback.videoTimingScanBuffer.begin(), playback.videoTimingScanBuffer.begin() + static_cast(discard)); + } + + const std::vector &buffer = playback.videoTimingScanBuffer; + size_t lastSequenceHeader = kStartCodeNotFound; + for (size_t i = 0u; i + 7u < buffer.size(); ++i) + { + if (buffer[i + 0u] == 0x00u && + buffer[i + 1u] == 0x00u && + buffer[i + 2u] == 0x01u && + buffer[i + 3u] == 0xB3u) + { + const uint8_t frameRateCode = buffer[i + 7u] & 0x0Fu; + if (mpegPictureIntervalQ32(frameRateCode) != 0u) + { + lastSequenceHeader = i; + } + } + } + + if (lastSequenceHeader == kStartCodeNotFound) + { + return; + } + + playback.frameRateCode = buffer[lastSequenceHeader + 7u] & 0x0Fu; + playback.frameRateExtensionN = 0u; + playback.frameRateExtensionD = 0u; + playback.hasFrameRateExtension = false; + playback.pictureIntervalQ32 = mpegPictureIntervalQ32(playback.frameRateCode); + + for (size_t i = lastSequenceHeader + 8u; i + 9u < buffer.size(); ++i) + { + if (buffer[i + 0u] != 0x00u || + buffer[i + 1u] != 0x00u || + buffer[i + 2u] != 0x01u) + { + continue; + } + + if (buffer[i + 3u] == 0xB3u) + { + break; + } + if (buffer[i + 3u] != 0xB5u) + { + continue; + } + + const uint8_t *extension = buffer.data() + i + 4u; + if (readMpegBits(extension, 0u, 4u) != 1u) + { + continue; + } + + playback.frameRateExtensionN = static_cast(readMpegBits(extension, 41u, 2u)); + playback.frameRateExtensionD = static_cast(readMpegBits(extension, 43u, 5u)); + playback.hasFrameRateExtension = true; + playback.pictureIntervalQ32 = mpegPictureIntervalQ32( + playback.frameRateCode, + playback.frameRateExtensionN, + playback.frameRateExtensionD); + break; + } + + if (playback.pictureIntervalQ32 == 0u) + { + playback.pictureIntervalQ32 = kDefaultPictureIntervalQ32; + } + } + + uint64_t decodedFrameIntervalQ32(const MpegPlaybackState &playback, + const MpegDecodedFrame &frame) + { + const uint64_t base = playback.pictureIntervalQ32 != 0u + ? playback.pictureIntervalQ32 + : kDefaultPictureIntervalQ32; + const uint64_t fields = static_cast(2 + std::max(0, frame.repeatPict)); + return std::max(kPictureClockOne, (base * fields + 1u) / 2u); + } + + constexpr uint64_t kMpegPtsWrap = 1ull << 33u; + constexpr uint64_t kMpegPtsHalfWrap = 1ull << 32u; + + int64_t mpegPtsDelta90k(int64_t fromPts, int64_t toPts) + { + if (fromPts < 0 || toPts < 0) + { + return 0; + } + + uint64_t from = static_cast(fromPts) & (kMpegPtsWrap - 1u); + uint64_t to = static_cast(toPts) & (kMpegPtsWrap - 1u); + uint64_t delta = (to - from) & (kMpegPtsWrap - 1u); + if (delta >= kMpegPtsHalfWrap) + { + return -static_cast(kMpegPtsWrap - delta); + } + return static_cast(delta); + } + + uint64_t mpegPtsDeltaToVSyncQ32(uint64_t delta90k) + { + // 90 kHz MPEG clock -> NTSC field clock (60000/1001 Hz): + // fields = pts * 60000 / (90000 * 1001) = pts * 2 / 3003. + constexpr uint64_t kPtsDivisor = 3003u; + const uint64_t whole = delta90k / kPtsDivisor; + const uint64_t remainder = delta90k % kPtsDivisor; + const uint64_t wholeQ32 = whole * 2u * kPictureClockOne; + const uint64_t remainderQ32 = ((remainder * 2u * kPictureClockOne) + kPtsDivisor / 2u) / kPtsDivisor; + return wholeQ32 + remainderQ32; + } + + uint64_t presentationTickForFrame(MpegPlaybackState &playback, const MpegDecodedFrame &frame, uint64_t currentTickQ32) + { + if (frame.pts90k < 0) + { + if (playback.nextPictureTickQ32 == std::numeric_limits::max()) + { + playback.nextPictureTickQ32 = currentTickQ32; + } + return playback.nextPictureTickQ32; + } + + if (playback.firstPresentedPts90k < 0) + { + playback.firstPresentedPts90k = frame.pts90k; + playback.ptsPresentationBaseTickQ32 = currentTickQ32; + return currentTickQ32; + } + + const int64_t delta = mpegPtsDelta90k(playback.firstPresentedPts90k, frame.pts90k); + if (delta >= 0) + { + return playback.ptsPresentationBaseTickQ32 + mpegPtsDeltaToVSyncQ32(static_cast(delta)); + } + + const uint64_t backwards = mpegPtsDeltaToVSyncQ32(static_cast(-delta)); + return playback.ptsPresentationBaseTickQ32 > backwards + ? playback.ptsPresentationBaseTickQ32 - backwards + : 0u; + } + uint32_t align16(uint32_t value) { return (value + 15u) & ~15u; @@ -622,8 +820,7 @@ namespace ps2_stubs uint16_t readBe16(const uint8_t *p) { - return static_cast((static_cast(p[0]) << 8u) | - static_cast(p[1])); + return static_cast((static_cast(p[0]) << 8u) | static_cast(p[1])); } bool isVideoStreamId(uint8_t streamId) @@ -721,49 +918,90 @@ namespace ps2_stubs return kStartCodeNotFound; } - size_t parsePesPayloadOffset(const uint8_t *packet, size_t packetSize) + struct MpegPesHeader { + size_t payloadOffset = 0u; + int64_t pts90k = -1; + int64_t dts90k = -1; + }; + + int64_t decodePesTimestamp90k(const uint8_t *p, size_t remaining) + { + if (!p || remaining < 5u) + { + return -1; + } + + const uint64_t value = + (static_cast((p[0] >> 1u) & 0x07u) << 30u) | + (static_cast(p[1]) << 22u) | + (static_cast((p[2] >> 1u) & 0x7Fu) << 15u) | + (static_cast(p[3]) << 7u) | + static_cast((p[4] >> 1u) & 0x7Fu); + return static_cast(value & (kMpegPtsWrap - 1u)); + } + + MpegPesHeader parsePesHeader(const uint8_t *packet, size_t packetSize) + { + MpegPesHeader result{}; + result.payloadOffset = packetSize; if (!packet || packetSize <= 6u) { - return packetSize; + return result; } size_t pos = 6u; if (packetSize >= 9u && (packet[pos] & 0xC0u) == 0x80u) { - return std::min(packetSize, 9u + static_cast(packet[pos + 2u])); + const uint8_t ptsDtsFlags = packet[pos + 1u] & 0xC0u; + const size_t headerDataLength = static_cast(packet[pos + 2u]); + const size_t optionalStart = 9u; + const size_t optionalEnd = std::min(packetSize, optionalStart + headerDataLength); + if ((ptsDtsFlags == 0x80u || ptsDtsFlags == 0xC0u) && optionalEnd >= optionalStart + 5u) + { + result.pts90k = decodePesTimestamp90k(packet + optionalStart, optionalEnd - optionalStart); + } + if (ptsDtsFlags == 0xC0u && optionalEnd >= optionalStart + 10u) + { + result.dts90k = decodePesTimestamp90k(packet + optionalStart + 5u, optionalEnd - optionalStart - 5u); + } + result.payloadOffset = optionalEnd; + return result; } + // MPEG-1 PES. while (pos < packetSize && packet[pos] == 0xFFu) { ++pos; } - if (pos + 1u < packetSize && (packet[pos] & 0xC0u) == 0x40u) { pos += 2u; } - if (pos >= packetSize) { - return packetSize; + return result; } - const uint8_t flags = packet[pos]; - if ((flags & 0xF0u) == 0x20u) + const uint8_t marker = packet[pos] & 0xF0u; + if (marker == 0x20u && pos + 5u <= packetSize) { + result.pts90k = decodePesTimestamp90k(packet + pos, packetSize - pos); pos += 5u; } - else if ((flags & 0xF0u) == 0x30u) + else if (marker == 0x30u && pos + 10u <= packetSize) { + result.pts90k = decodePesTimestamp90k(packet + pos, packetSize - pos); + result.dts90k = decodePesTimestamp90k(packet + pos + 5u, packetSize - pos - 5u); pos += 10u; } - else if (flags == 0x0Fu) + else if (packet[pos] == 0x0Fu) { - pos += 1u; + ++pos; } - return std::min(packetSize, pos); + result.payloadOffset = std::min(packetSize, pos); + return result; } void flushDecoderIfEnded(MpegPlaybackState &playback) @@ -774,7 +1012,7 @@ namespace ps2_stubs } } - void feedElementaryStream(MpegPlaybackState &playback, const uint8_t *data, size_t size) + void feedElementaryStream(MpegPlaybackState &playback, const uint8_t *data, size_t size, int64_t pts90k = -1, int64_t dts90k = -1) { if (!data || size == 0) { @@ -800,6 +1038,7 @@ namespace ps2_stubs } playback.sawInput = true; + updateMpegPictureTiming(playback, data, size); if (playback.waitingForVideoSequenceHeader) { playback.videoSequenceSyncBuffer.insert( @@ -833,8 +1072,14 @@ namespace ps2_stubs data = playback.videoSequenceSyncBuffer.data(); size = playback.videoSequenceSyncBuffer.size(); - playback.pictureIntervalQ32 = mpegPictureIntervalQ32(data[7u] & 0x0Fu); + if (playback.pictureIntervalQ32 == 0u) + { + playback.pictureIntervalQ32 = kDefaultPictureIntervalQ32; + } playback.nextPictureTickQ32 = std::numeric_limits::max(); + playback.presentationEndTickQ32 = std::numeric_limits::max(); + playback.firstPresentedPts90k = -1; + playback.ptsPresentationBaseTickQ32 = 0u; playback.waitingForVideoSequenceHeader = false; playback.decoderFailed = false; playback.decoder.reset(); @@ -843,7 +1088,7 @@ namespace ps2_stubs if (containsMpegSequenceEnd(data, size)) { - playback.streamEnded = true; + playback.sawSequenceEnd = true; playback.cdStreamGeneration = g_mpeg_stub_state.cdStreamGeneration; } @@ -852,7 +1097,7 @@ namespace ps2_stubs playback.decoder = std::make_unique(); } - if (!playback.decoder->feed(data, size, playback.decodedFrames)) + if (!playback.decoder->feed(data, size, playback.decodedFrames, pts90k, dts90k)) { playback.decoder.reset(); playback.waitingForVideoSequenceHeader = true; @@ -909,13 +1154,17 @@ namespace ps2_stubs uint32_t streamType, uint32_t dataAddr, uint32_t len, - std::vector &callbackEvents) + std::vector &callbackEvents, + int64_t pts90k = -1, + int64_t dts90k = -1) { MpegStreamCallbackEvent event{}; event.mpegAddr = mpegAddr; event.streamType = streamType; event.dataAddr = dataAddr; event.len = len; + event.pts = pts90k >= 0 ? static_cast(pts90k) : 0xFFFFFFFFFFFFFFFFull; + event.dts = dts90k >= 0 ? static_cast(dts90k) : 0xFFFFFFFFFFFFFFFFull; event.callbacks = matchingStreamCallbacks(mpegAddr, streamType); if (!event.callbacks.empty()) { @@ -1067,7 +1316,8 @@ namespace ps2_stubs if (isVideoStreamId(streamId)) { - const size_t payloadStart = parsePesPayloadOffset(buffer.data(), packetEnd); + const MpegPesHeader pes = parsePesHeader(buffer.data(), packetEnd); + const size_t payloadStart = pes.payloadOffset; if (payloadStart < packetEnd) { if (payloadStart < playback.pssGuestAddrs.size()) @@ -1077,17 +1327,22 @@ namespace ps2_stubs kMpegStrM2V, playback.pssGuestAddrs[payloadStart], static_cast(packetEnd - payloadStart), - callbackEvents); + callbackEvents, + pes.pts90k, + pes.dts90k); } feedElementaryStream( playback, buffer.data() + payloadStart, - packetEnd - payloadStart); + packetEnd - payloadStart, + pes.pts90k, + pes.dts90k); } } else if (isAudioStreamId(streamId)) { - const size_t payloadStart = parsePesPayloadOffset(buffer.data(), packetEnd); + const MpegPesHeader pes = parsePesHeader(buffer.data(), packetEnd); + const size_t payloadStart = pes.payloadOffset; if (payloadStart < packetEnd && payloadStart < playback.pssGuestAddrs.size()) { queueStreamCallbackEvent( @@ -1095,13 +1350,17 @@ namespace ps2_stubs kMpegStrPCM, playback.pssGuestAddrs[payloadStart], static_cast(packetEnd - payloadStart), - callbackEvents); + callbackEvents, + pes.pts90k, + pes.dts90k); queueStreamCallbackEvent( mpegAddr, kMpegStrADPCM, playback.pssGuestAddrs[payloadStart], static_cast(packetEnd - payloadStart), - callbackEvents); + callbackEvents, + pes.pts90k, + pes.dts90k); } } @@ -1135,6 +1394,25 @@ namespace ps2_stubs } } + bool mpegDemuxBackpressured(const MpegPlaybackState &playback) + { + // Let EOF finalization drain any tail that is already in the guest + // ring, otherwise bound decode lead to a handful of pictures. + // + // Important: do not park sceMpegDemuxPss/Ring here. Code Veronica + // explicitly wakes its video thread before every demux call and that + // thread sleeps again after presenting one picture. A single host + // decoder feed can enqueue more than kMaxDecodedPicturesAhead frames; + // parking the producer then leaves the consumer asleep after draining + // just one frame, with nobody left to issue the next WakeupThread. + // Returning 0 bytes consumed instead leaves the guest ring intact and + // lets the game's producer loop wake the consumer again. Backpressure + // still propagates naturally to sceCdStRead because the ring does not + // advance while this is true. + return !g_mpeg_stub_state.currentCdStreamEofSeen && + playback.decodedFrames.size() >= kMaxDecodedPicturesAhead; + } + void recordCdStreamBytesDemuxedUnlocked( size_t consumed, std::vector &completedMpegIds, @@ -1575,15 +1853,23 @@ namespace ps2_stubs { (void)rdram; const uint32_t mpegAddr = getRegU32(ctx, 4); + bool wakePictureWaiter = false; { std::lock_guard lock(g_mpeg_stub_mutex); MpegPlaybackState &playback = getPlaybackState(mpegAddr); + const size_t framesBefore = playback.decodedFrames.size(); if (playback.decoder) { playback.decoder->flush(playback.decodedFrames); } + wakePictureWaiter = playback.decodedFrames.size() != framesBefore || + playback.streamEnded || + playback.decoderFailed; + } + if (wakePictureWaiter) + { + runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); } - runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); setReturnS32(ctx, 0); } @@ -1594,9 +1880,11 @@ namespace ps2_stubs const uint32_t byteCount = getRegU32(ctx, 6); size_t copied = 0u; + bool wakePictureWaiter = false; { std::lock_guard lock(g_mpeg_stub_mutex); MpegPlaybackState &playback = getPlaybackState(mpegAddr); + const size_t framesBefore = playback.decodedFrames.size(); while (copied < byteCount) { const uint32_t curAddr = dataAddr + static_cast(copied); @@ -1610,9 +1898,13 @@ namespace ps2_stubs feedElementaryStream(playback, src, chunk); copied += chunk; } + wakePictureWaiter = playback.decodedFrames.size() != framesBefore || playback.streamEnded || playback.decoderFailed; } - runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); + if (wakePictureWaiter) + { + runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); + } setReturnS32(ctx, static_cast(copied)); } @@ -1793,24 +2085,46 @@ namespace ps2_stubs std::vector callbackEvents; std::vector completedMpegIds; size_t consumed = 0u; + size_t decodedBefore = 0u; size_t decodedCount = 0u; uint32_t traceIdx = 0u; bool eofChanged = false; + bool backpressured = 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); + decodedBefore = playback.decodedFrames.size(); + backpressured = mpegDemuxBackpressured(playback); + if (!backpressured) + { + 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); + + if (backpressured) + { + if (traceIdx < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:DemuxPss:BACKPRESSURE] mpeg=0x" << std::hex << mpegAddr << std::dec << " decoded=" << decodedCount << std::endl; + }); + } + setReturnS32(ctx, 0); + return; + } + const bool currentStreamCompleted = std::find(completedMpegIds.begin(), completedMpegIds.end(), mpegAddr) != completedMpegIds.end(); + if (decodedCount != decodedBefore || eofChanged || currentStreamCompleted) + { + runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); + } for (const uint32_t completedMpegId : completedMpegIds) { if (completedMpegId != mpegAddr) { - runtime->eeScheduler().completeExternalWait( - kMpegPictureWaitType, completedMpegId, KE_OK); + runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, completedMpegId, KE_OK); } } @@ -1854,32 +2168,56 @@ namespace ps2_stubs std::vector callbackEvents; std::vector completedMpegIds; size_t consumed = 0u; + size_t decodedBefore = 0u; size_t decodedCount = 0u; uint32_t traceIdx = 0u; bool eofChanged = false; + bool backpressured = false; { std::lock_guard lock(g_mpeg_stub_mutex); MpegPlaybackState &playback = getPlaybackState(mpegAddr); - consumed = appendGuestRingBytes( - mpegAddr, - playback, - rdram, - dataAddr, - availableBytes, - ringBaseAddr, - ringSize, - callbackEvents); - recordCdStreamBytesDemuxedUnlocked(consumed, completedMpegIds, eofChanged); + decodedBefore = playback.decodedFrames.size(); + backpressured = mpegDemuxBackpressured(playback); + if (!backpressured) + { + consumed = appendGuestRingBytes( + mpegAddr, + playback, + rdram, + dataAddr, + availableBytes, + ringBaseAddr, + ringSize, + callbackEvents); + recordCdStreamBytesDemuxedUnlocked(consumed, completedMpegIds, eofChanged); + } decodedCount = playback.decodedFrames.size(); traceIdx = g_mpeg_stub_state.demuxRingTraceCount++; } - runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); + + if (backpressured) + { + if (traceIdx < 32u) + { + PS2_IF_AGRESSIVE_LOGS({ + std::cerr << "[MPEG:DemuxPssRing:BACKPRESSURE] mpeg=0x" << std::hex << mpegAddr + << std::dec << " decoded=" << decodedCount + << " avail=" << availableBytes << std::endl; + }); + } + setReturnS32(ctx, 0); + return; + } + const bool currentStreamCompleted = std::find(completedMpegIds.begin(), completedMpegIds.end(), mpegAddr) != completedMpegIds.end(); + if (decodedCount != decodedBefore || eofChanged || currentStreamCompleted) + { + runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, mpegAddr, KE_OK); + } for (const uint32_t completedMpegId : completedMpegIds) { if (completedMpegId != mpegAddr) { - runtime->eeScheduler().completeExternalWait( - kMpegPictureWaitType, completedMpegId, KE_OK); + runtime->eeScheduler().completeExternalWait(kMpegPictureWaitType, completedMpegId, KE_OK); } } @@ -1986,40 +2324,38 @@ namespace ps2_stubs if (!playback.decodedFrames.empty()) { - if (playback.pictureIntervalQ32 != 0u) + const uint64_t currentTick = runtime->eeScheduler().currentVSyncTick(); + const uint64_t currentTickQ32 = currentTick << 32u; + const MpegDecodedFrame &nextFrame = playback.decodedFrames.front(); + const uint64_t frameIntervalQ32 = decodedFrameIntervalQ32(playback, nextFrame); + uint64_t presentationTargetQ32 = presentationTickForFrame(playback, nextFrame, currentTickQ32); + + if (currentTickQ32 > presentationTargetQ32 && currentTickQ32 - presentationTargetQ32 >= frameIntervalQ32) { - const uint64_t currentTick = runtime->eeScheduler().currentVSyncTick(); - const uint64_t currentTickQ32 = currentTick << 32u; - if (playback.nextPictureTickQ32 == std::numeric_limits::max()) + const uint64_t correction = currentTickQ32 - presentationTargetQ32; + presentationTargetQ32 = currentTickQ32; + if (nextFrame.pts90k >= 0 && playback.firstPresentedPts90k >= 0) { - playback.nextPictureTickQ32 = currentTickQ32; + playback.ptsPresentationBaseTickQ32 += correction; } + 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 (currentTickQ32 < presentationTargetQ32) + { + const uint64_t eligibleTick = (presentationTargetQ32 + kPictureClockOne - 1u) >> 32u; + lock.unlock(); + runtime->eeScheduler().waitVSync( + eligibleTick - 1u, + -1, + [rdram, runtime](R5900Context &resumeContext) + { + if (static_cast(getRegU32(&resumeContext, 2)) < 0) { - 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; + return; + } + sceMpegGetPicture(rdram, &resumeContext, runtime); + }); } frame = std::move(playback.decodedFrames.front()); @@ -2030,6 +2366,8 @@ namespace ps2_stubs height = playback.height; frameCount = playback.picturesServed; playback.picturesServed += 1u; + playback.nextPictureTickQ32 = presentationTargetQ32 + frameIntervalQ32; + playback.presentationEndTickQ32 = playback.nextPictureTickQ32; haveFrame = true; if (g_mpeg_stub_state.pictureTraceCount < 32u) { @@ -2117,20 +2455,38 @@ namespace ps2_stubs std::lock_guard lock(g_mpeg_stub_mutex); g_mpeg_stub_state.initialized = true; MpegPlaybackState &playback = getPlaybackState(mpegAddr); - const bool ended = playback.streamEnded || (playback.decoderFailed && playback.sawInput); + // Only the producer/demux EOF is authoritative. A sequence_end_code can + // be observed while more PSS data is still buffered, and a decoder + // failure before producer EOF may still recover on a later sequence. + const bool producerEnded = + g_mpeg_stub_state.currentCdStreamEofSeen && + playback.cdStreamGeneration == g_mpeg_stub_state.cdStreamGeneration; + const bool ended = producerEnded && + (playback.streamEnded || (playback.decoderFailed && playback.sawInput)); + const uint64_t presentationEnd = playback.presentationEndTickQ32; + const uint64_t currentTickQ32 = runtime != nullptr + ? (runtime->eeScheduler().currentVSyncTick() << 32u) + : std::numeric_limits::max(); + const bool presentationComplete = + presentationEnd == std::numeric_limits::max() || + currentTickQ32 >= presentationEnd; if (g_mpeg_stub_state.isEndTraceCount < 16u) { PS2_IF_AGRESSIVE_LOGS({ std::cerr << "[MPEG:IsEnd] mpeg=0x" << std::hex << mpegAddr << std::dec << " ended=" << ended + << " producerEof=" << producerEnded + << " seqEnd=" << playback.sawSequenceEnd + << " streamEnded=" << playback.streamEnded + << " presentationComplete=" << presentationComplete << " frames=" << playback.decodedFrames.size() << " sawInput=" << playback.sawInput << std::endl; }); ++g_mpeg_stub_state.isEndTraceCount; } - setReturnS32(ctx, (ended && playback.decodedFrames.empty()) ? 1 : 0); + setReturnS32(ctx, (ended && playback.decodedFrames.empty() && presentationComplete) ? 1 : 0); } void sceMpegIsRefBuffEmpty(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) diff --git a/ps2xRuntime/src/lib/ps2_debug_panel.cpp b/ps2xRuntime/src/lib/ps2_debug_panel.cpp index 71c6479..cf0b439 100644 --- a/ps2xRuntime/src/lib/ps2_debug_panel.cpp +++ b/ps2xRuntime/src/lib/ps2_debug_panel.cpp @@ -767,6 +767,10 @@ namespace { const EeKernelSnapshot snapshot = runtime.eeScheduler().snapshot(); ImGui::Text("Threads: %zu running=%d", snapshot.threads.size(), snapshot.runningThreadId); + ImGui::Text("EE cycle: %llu slice end: %llu next event: %llu", + static_cast(snapshot.eeCycle), + static_cast(snapshot.sliceEndCycle), + static_cast(snapshot.nextEventCycle)); if (ImGui::BeginTable("threads", 11, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 320))) { ImGui::TableSetupColumn("ID"); diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index ae0b2b0..38bc945 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -1312,6 +1312,14 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram, ctx->pc = targetPc; const bool isCall = (kind == GuestBranchKind::DirectCall || kind == GuestBranchKind::IndirectCall); + // Every inter-function transfer is also a deterministic EE safe point. + // Backward edges inside generated functions use eeCheckpointDue(), while + // this charge bounds straight-line call chains that have no local loop. + if (m_eeScheduler && m_eeScheduler->checkpointDue(EeScheduler::kGuestDispatchCycles)) + { + return false; + } + if (kind == GuestBranchKind::Return) { if (!hasFunction(targetPc)) @@ -2165,9 +2173,9 @@ void PS2Runtime::postEeEvent(EeEvent event) m_eeScheduler->postEvent(event); } -bool PS2Runtime::eeCheckpointDue() const noexcept +bool PS2Runtime::eeCheckpointDue(uint32_t cycles) noexcept { - return m_eeScheduler->checkpointDue(); + return m_eeScheduler->checkpointDue(cycles); } [[noreturn]] void PS2Runtime::eeWaitVSyncTicks(uint32_t ticks, uint32_t resumePc)