diff --git a/ps2xIOP/src/modules/cri_dtx.cpp b/ps2xIOP/src/modules/cri_dtx.cpp index 8014d38..55386f5 100644 --- a/ps2xIOP/src/modules/cri_dtx.cpp +++ b/ps2xIOP/src/modules/cri_dtx.cpp @@ -1181,13 +1181,10 @@ namespace ps2x::iop::detail continue; } - appendToSjrmtData(sjrmt->second, - chunkDataAddress, - chunkLength); + appendToSjrmtData(sjrmt->second, chunkDataAddress, chunkLength); + (void)writeGuestPod(m_host, commandAddress + 4u, sjx->second.eeObjectAddress); constexpr uint8_t roomLine = 0u; - (void)m_host.writeGuest(commandAddress + 1u, - &roomLine, - sizeof(roomLine)); + (void)m_host.writeGuest(commandAddress + 1u, &roomLine, sizeof(roomLine)); } consumeActivePs2RnaStreamsLocked(); } diff --git a/ps2xRecomp/src/lib/mmi_translation_helpers.cpp b/ps2xRecomp/src/lib/mmi_translation_helpers.cpp index 4f7a366..a2bcb77 100644 --- a/ps2xRecomp/src/lib/mmi_translation_helpers.cpp +++ b/ps2xRecomp/src/lib/mmi_translation_helpers.cpp @@ -456,7 +456,7 @@ namespace ps2recomp { // Swaps halfwords 1<->3 and 5<->7 within the 128-bit register return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shufflelo_epi16(_mm_shufflehi_epi16(GPR_VEC(ctx, {}), _MM_SHUFFLE(2,3,0,1)), _MM_SHUFFLE(2,3,0,1)));", - inst.rd, inst.rs); + inst.rd, inst.rt); } @@ -465,7 +465,7 @@ namespace ps2recomp // Reverses the order of the 8 halfwords return fmt::format("{{ __m128i mask = _mm_setr_epi8(14,15, 12,13, 10,11, 8,9, 6,7, 4,5, 2,3, 0,1); " "SET_GPR_VEC(ctx, {}, PS2_SHUFFLE_EPI8(GPR_VEC(ctx, {}), mask)); }}", - inst.rd, inst.rs); + inst.rd, inst.rt); } @@ -514,7 +514,7 @@ namespace ps2recomp std::string CodeGenerator::translatePEXEW(const Instruction &inst) { return fmt::format("SET_GPR_VEC(ctx, {}, PS2_PEXEW(GPR_VEC(ctx, {})));", - inst.rd, inst.rs); + inst.rd, inst.rt); } @@ -522,7 +522,7 @@ namespace ps2recomp { // Rotates words left by 3: [d,c,b,a] -> [a,d,c,b] return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shuffle_epi32(GPR_VEC(ctx, {}), _MM_SHUFFLE(0,3,2,1)));", - inst.rd, inst.rs); + inst.rd, inst.rt); } @@ -562,7 +562,7 @@ namespace ps2recomp { // Parallel Exchange Center Halfword (same as MMI2 PEXEH) return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shufflelo_epi16(_mm_shufflehi_epi16(GPR_VEC(ctx, {}), _MM_SHUFFLE(2,3,0,1)), _MM_SHUFFLE(2,3,0,1)));", - inst.rd, inst.rs); + inst.rd, inst.rt); } @@ -571,7 +571,7 @@ namespace ps2recomp // Parallel Copy Halfword (Broadcast lower 16 bits of each 64-bit half) return fmt::format("{{ __m128i src = GPR_VEC(ctx, {}); uint16_t l = _mm_extract_epi16(src, 0); uint16_t h = _mm_extract_epi16(src, 4); \n" " SET_GPR_VEC(ctx, {}, _mm_set_epi16(h,h,h,h, l,l,l,l)); }}", - inst.rs, inst.rd); + inst.rt, inst.rd); } @@ -579,7 +579,7 @@ namespace ps2recomp { // Parallel Exchange Center Word (Swaps words 0<>2, 1<>3) return fmt::format("SET_GPR_VEC(ctx, {}, _mm_shuffle_epi32(GPR_VEC(ctx, {}), _MM_SHUFFLE(1,0,3,2)));", - inst.rd, inst.rs); + inst.rd, inst.rt); } diff --git a/ps2xRuntime/include/runtime/ee_scheduler.h b/ps2xRuntime/include/runtime/ee_scheduler.h index a9ecc28..fae8b85 100644 --- a/ps2xRuntime/include/runtime/ee_scheduler.h +++ b/ps2xRuntime/include/runtime/ee_scheduler.h @@ -279,6 +279,7 @@ public: // Kernel object API. All calls except postEvent/requestStop execute on the // EE executor and therefore need no host synchronization. + void setupCurrentThread(uint32_t stack, uint32_t stackSize, uint32_t gp); int createThread(const EeThreadCreateParams ¶ms); int deleteThread(int id, uint32_t &ownedStack); int startThread(int id, uint32_t arg, const R5900Context &caller, bool interruptSafe); @@ -416,6 +417,7 @@ private: bool m_rescheduleRequested = false; bool m_timeSliceExpired = false; bool m_insideInterrupt = false; + uint32_t m_pendingEeTimerInterrupts = 0; uint64_t m_eeCycle = 0; uint64_t m_sliceEndCycle = kDefaultTimeSliceCycles; std::thread::id m_executorThread{}; diff --git a/ps2xRuntime/include/runtime/ps2_memory.h b/ps2xRuntime/include/runtime/ps2_memory.h index 3bd8728..9972e42 100644 --- a/ps2xRuntime/include/runtime/ps2_memory.h +++ b/ps2xRuntime/include/runtime/ps2_memory.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -306,6 +307,12 @@ public: bool writeIORegister(uint32_t address, uint32_t value); uint32_t readIORegister(uint32_t address); + // EE timers advance from the scheduler's emulated EE-cycle clock. The + // returned mask uses bits 0..3 for newly raised TIM0..TIM3 interrupts. + uint32_t advanceEeTimers(uint64_t eeCycles) noexcept; + [[nodiscard]] uint64_t cyclesUntilNextEeTimerInterrupt() const noexcept; + void resetEeTimers() noexcept; + using GifPacketCallback = std::function; void setGifPacketCallback(GifPacketCallback cb) { m_gifPacketCallback = std::move(cb); } void setGifArbiter(GifArbiter *arbiter) { m_gifArbiter = arbiter; } @@ -432,10 +439,17 @@ public: bool isScratchpad(uint32_t address) const; uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit); const uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit) const; - void updateEeTimer0Counter(); + struct EeTimer + { + uint32_t count = 0; + uint32_t mode = 0; + uint32_t compare = 0; + uint32_t hold = 0; + uint64_t clockRemainder = 0; + }; + + std::array m_eeTimers{}; void queueCompletedDmacCause(uint32_t cause); - uint64_t m_timer0LastHostNs = 0; - uint64_t m_timer0FractionNs = 0; }; #endif // PS2_MEMORY_H diff --git a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp index 1167207..3a6ec7d 100644 --- a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp +++ b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp @@ -41,6 +41,15 @@ namespace return (microseconds * EeScheduler::kEeClockHz + 999999ull) / 1000000ull; } + std::chrono::nanoseconds eeCyclesToHostDuration(uint64_t cycles) + { + constexpr uint64_t kNanosecondsPerSecond = 1000000000ull; + const uint64_t wholeSeconds = cycles / EeScheduler::kEeClockHz; + const uint64_t remainingCycles = cycles % EeScheduler::kEeClockHz; + const uint64_t remainingNanoseconds = (remainingCycles * kNanosecondsPerSecond + EeScheduler::kEeClockHz - 1u) / EeScheduler::kEeClockHz; + return std::chrono::seconds(wholeSeconds) + std::chrono::nanoseconds(remainingNanoseconds); + } + constexpr uint64_t kVBlankPeriodCycles = microsecondsToEeCycles(16667u); constexpr uint64_t kVBlankDurationCycles = microsecondsToEeCycles(500u); constexpr uint64_t kAlarmTickCycles = microsecondsToEeCycles(kAlarmTickMicroseconds); @@ -101,6 +110,7 @@ void EeScheduler::reset(uint8_t *rdram, const R5900Context &mainContext) m_rescheduleRequested = false; m_timeSliceExpired = false; m_insideInterrupt = false; + m_pendingEeTimerInterrupts = 0u; m_eeCycle = 0u; m_sliceEndCycle = kDefaultTimeSliceCycles; m_stopRequested.store(false, std::memory_order_release); @@ -121,12 +131,15 @@ void EeScheduler::reset(uint8_t *rdram, const R5900Context &mainContext) m_gsVSyncCallbackGp = 0; m_gsVSyncCallbackSp = 0; m_runtime.memory().gs().vsyncTick.store(0u, std::memory_order_release); + m_runtime.memory().resetEeTimers(); GuestThread main{}; main.id = kMainThreadId; main.context = mainContext; main.entry = mainContext.pc; - main.stack = getRegU32(&mainContext, 29); + // $sp is live execution state, not the stable initial stack descriptor + // returned by ReferThreadStatus. SetupThread records that metadata. + main.stack = 0u; main.gp = getRegU32(&mainContext, 28); main.initialPriority = 0; main.currentPriority = 0; @@ -375,7 +388,13 @@ bool EeScheduler::checkpointDue(uint32_t cycles) noexcept void EeScheduler::accountCycles(uint32_t cycles) noexcept { - m_eeCycle += std::max(1u, cycles); + const uint64_t elapsed = std::max(1u, cycles); + m_eeCycle += elapsed; + m_pendingEeTimerInterrupts |= m_runtime.memory().advanceEeTimers(elapsed); + if (m_pendingEeTimerInterrupts != 0u) + { + m_checkpointPending.store(true, std::memory_order_release); + } } bool EeScheduler::isExecutingGuest() const noexcept @@ -383,6 +402,21 @@ bool EeScheduler::isExecutingGuest() const noexcept return m_guestExecuting.load(std::memory_order_acquire); } +void EeScheduler::setupCurrentThread(uint32_t stack, uint32_t stackSize, uint32_t gp) +{ + assertExecutor(); + GuestThread *target = currentThread(); + if (!target) + { + return; + } + + target->stack = stack; + target->stackSize = stackSize; + target->gp = gp; + publishSnapshot(); +} + int EeScheduler::createThread(const EeThreadCreateParams ¶ms) { assertExecutor(); @@ -1702,6 +1736,15 @@ void EeScheduler::processPendingEvents() { assertExecutor(); processDueDeadlines(); + const uint32_t timerInterrupts = m_pendingEeTimerInterrupts; + m_pendingEeTimerInterrupts = 0u; + for (uint32_t timer = 0u; timer < 4u; ++timer) + { + if ((timerInterrupts & (1u << timer)) != 0u) + { + dispatchIrq(false, 9u + timer); + } + } std::deque pending; { std::lock_guard lock(m_eventMutex); @@ -1950,30 +1993,55 @@ void EeScheduler::waitForEvent() { return; } - if (m_deadlines.empty()) + const uint64_t timerCycles = m_runtime.memory().cyclesUntilNextEeTimerInterrupt(); + const bool hasTimerDeadline = timerCycles != std::numeric_limits::max(); + if (m_deadlines.empty() && !hasTimerDeadline) { 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) - { - if (left.deadlineCycle != right.deadlineCycle) + uint64_t deadlineCycle = 0u; + auto hostDeadline = std::chrono::steady_clock::time_point::max(); + if (!m_deadlines.empty()) + { + const auto next = std::min_element(m_deadlines.begin(), m_deadlines.end(), + [](const ScheduledEvent &left, const ScheduledEvent &right) { - return left.deadlineCycle < right.deadlineCycle; - } - return left.sequence < right.sequence; - }); - const uint64_t deadlineCycle = next->deadlineCycle; - const auto hostDeadline = next->hostDeadline; + if (left.deadlineCycle != right.deadlineCycle) + { + return left.deadlineCycle < right.deadlineCycle; + } + return left.sequence < right.sequence; + }); + deadlineCycle = next->deadlineCycle; + hostDeadline = next->hostDeadline; + } + if (hasTimerDeadline) + { + const auto timerHostDeadline = std::chrono::steady_clock::now() + eeCyclesToHostDuration(timerCycles); + if (timerHostDeadline < hostDeadline) + { + deadlineCycle = m_eeCycle + timerCycles; + hostDeadline = timerHostDeadline; + } + } + 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); + const uint64_t elapsed = deadlineCycle > m_eeCycle ? deadlineCycle - m_eeCycle : 0u; + lock.unlock(); + uint64_t remaining = elapsed; + while (remaining > 0u) + { + const uint32_t step = static_cast(std::min(remaining, std::numeric_limits::max())); + accountCycles(step); + remaining -= step; + } m_checkpointPending.store(true, std::memory_order_release); } } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h index 52fa84a..a7fae20 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h @@ -29,8 +29,8 @@ namespace uint32_t g_cdStreamingEndLbn = 0xFFFFFFFFu; bool g_cdInitialized = false; - constexpr uint32_t kIopHeapBase = 0x01A00000; - constexpr uint32_t kIopHeapLimit = 0x01F00000; + constexpr uint32_t kIopHeapBase = 0x04000000; + constexpr uint32_t kIopHeapLimit = 0x04500000; constexpr uint32_t kIopHeapAlign = 64; uint32_t g_iopHeapNext = kIopHeapBase; diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp index b48633b..6a07fe2 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp @@ -4,7 +4,10 @@ #include "../../ps2_iop_transport.h" #include "runtime/ps2_address.h" +#include +#include #include +#include namespace ps2_stubs { @@ -59,6 +62,7 @@ namespace ps2_stubs std::unordered_map g_sifSregs; std::unordered_map g_sifCmdHandlers; std::map g_sifHeapAllocations; + std::array g_sifHeapStorage{}; uint32_t g_sifCmdBuffer = 0u; uint32_t g_sifSysCmdBuffer = 0u; bool g_sifCmdInitialized = false; @@ -158,6 +162,9 @@ namespace ps2_stubs } g_sifHeapAllocations[candidate] = alignedSize; + std::fill_n(g_sifHeapStorage.data() + (candidate - kIopHeapBase), + alignedSize, + uint8_t{0}); g_iopHeapNext = candidate + alignedSize; return candidate; } @@ -183,9 +190,29 @@ namespace ps2_stubs { std::lock_guard lock(g_sifHeapMutex); g_sifHeapAllocations.clear(); + g_sifHeapStorage.fill(0u); g_iopHeapNext = kIopHeapBase; } + bool isAllocatedSifHeapRangeLocked(uint32_t address, size_t size) + { + if (address < kIopHeapBase || address >= kIopHeapLimit || size > static_cast(kIopHeapLimit - address)) + { + return false; + } + + auto it = g_sifHeapAllocations.upper_bound(address); + if (it == g_sifHeapAllocations.begin()) + { + return false; + } + --it; + + const uint64_t allocationEnd = static_cast(it->first) + it->second; + const uint64_t rangeEnd = static_cast(address) + size; + return address >= it->first && rangeEnd <= allocationEnd; + } + bool isCopyableGuestAddress(uint32_t addr) { if (Ps2AddressInRange(addr, PS2_SCRATCHPAD_BASE, PS2_SCRATCHPAD_SIZE)) @@ -211,39 +238,40 @@ namespace ps2_stubs return false; } - bool canCopyGuestByteRange(const uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes) + bool canCopyAddressRange(const uint8_t *rdram, uint32_t address, uint32_t sizeBytes) { - if (!rdram) + if (isSifIopHeapRange(address, sizeBytes)) + { + return true; + } + if (isSifIopHeapAddress(address) || !rdram) { return false; } - if (sizeBytes == 0u) { return true; } - + if (sizeBytes - 1u > std::numeric_limits::max() - address) + { + return false; + } for (uint32_t i = 0u; i < sizeBytes; ++i) { - const uint32_t srcByteAddr = srcAddr + i; - const uint32_t dstByteAddr = dstAddr + i; - - if (!isCopyableGuestAddress(srcByteAddr) || !isCopyableGuestAddress(dstByteAddr)) - { - return false; - } - - const uint8_t *src = getConstMemPtr(rdram, srcByteAddr); - const uint8_t *dst = getConstMemPtr(rdram, dstByteAddr); - if (!src || !dst) + const uint32_t byteAddress = address + i; + if (!isCopyableGuestAddress(byteAddress) ||getConstMemPtr(rdram, byteAddress) == nullptr) { return false; } } - return true; } + bool canCopyGuestByteRange(const uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes) + { + return canCopyAddressRange(rdram, srcAddr, sizeBytes) && canCopyAddressRange(rdram, dstAddr, sizeBytes); + } + bool copyGuestByteRange(uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes) { if (!canCopyGuestByteRange(rdram, dstAddr, srcAddr, sizeBytes)) @@ -256,6 +284,49 @@ namespace ps2_stubs return true; } + const bool sourceIsIop = isSifIopHeapRange(srcAddr, sizeBytes); + const bool destinationIsIop = isSifIopHeapRange(dstAddr, sizeBytes); + if (sourceIsIop || destinationIsIop) + { + std::vector payload(sizeBytes); + if (sourceIsIop) + { + if (!readSifIopHeap(srcAddr, payload.data(), payload.size())) + { + return false; + } + } + else + { + for (uint32_t i = 0u; i < sizeBytes; ++i) + { + const uint8_t *src = getConstMemPtr(rdram, srcAddr + i); + if (!src) + { + return false; + } + payload[i] = *src; + } + } + + if (destinationIsIop) + { + return writeSifIopHeap(dstAddr, payload.data(), payload.size()); + } + + ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr); + for (uint32_t i = 0u; i < sizeBytes; ++i) + { + uint8_t *dst = getMemPtr(rdram, dstAddr + i); + if (!dst) + { + return false; + } + *dst = payload[i]; + } + return true; + } + ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr); const uint64_t srcBegin = srcAddr; @@ -293,6 +364,71 @@ namespace ps2_stubs } } + bool isSifIopHeapAddress(uint32_t address) + { + return address >= kIopHeapBase && address < kIopHeapLimit; + } + + bool isSifIopHeapRange(uint32_t address, size_t size) + { + std::lock_guard lock(g_sifHeapMutex); + return isAllocatedSifHeapRangeLocked(address, size); + } + + bool readSifIopHeap(uint32_t address, void *destination, size_t size) + { + if (!destination && size != 0u) + { + return false; + } + std::lock_guard lock(g_sifHeapMutex); + if (!isAllocatedSifHeapRangeLocked(address, size)) + { + return false; + } + if (size != 0u) + { + std::memcpy(destination, + g_sifHeapStorage.data() + (address - kIopHeapBase), + size); + } + return true; + } + + bool writeSifIopHeap(uint32_t address, const void *source, size_t size) + { + if (!source && size != 0u) + { + return false; + } + std::lock_guard lock(g_sifHeapMutex); + if (!isAllocatedSifHeapRangeLocked(address, size)) + { + return false; + } + if (size != 0u) + { + std::memcpy(g_sifHeapStorage.data() + (address - kIopHeapBase), + source, + size); + } + return true; + } + + bool zeroSifIopHeap(uint32_t address, size_t size) + { + std::lock_guard lock(g_sifHeapMutex); + if (!isAllocatedSifHeapRangeLocked(address, size)) + { + return false; + } + if (size != 0u) + { + std::memset(g_sifHeapStorage.data() + (address - kIopHeapBase), 0, size); + } + return true; + } + void resetSifState() { std::lock_guard lock(g_sifCmdStateMutex); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.h b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.h index 7d7f35c..777e1cb 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.h @@ -2,8 +2,16 @@ #include "ps2_stubs.h" +#include + namespace ps2_stubs { + bool isSifIopHeapAddress(uint32_t address); + bool isSifIopHeapRange(uint32_t address, size_t size); + bool readSifIopHeap(uint32_t address, void *destination, size_t size); + bool writeSifIopHeap(uint32_t address, const void *source, size_t size); + bool zeroSifIopHeap(uint32_t address, size_t size); + void sceSifCmdIntrHdlr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceSifLoadModule(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceSifSendCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp index e6819b6..3530e65 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp @@ -495,6 +495,8 @@ namespace ps2_syscalls const uint32_t stack = getRegU32(ctx, 5); const int32_t stackSizeSigned = static_cast(getRegU32(ctx, 6)); const uint32_t currentSp = getRegU32(ctx, 29); + EeScheduler &scheduler = runtime->eeScheduler(); + scheduler.bindMainContextForSyscall(*ctx, rdram); if (gp != 0u) { @@ -502,6 +504,10 @@ namespace ps2_syscalls } uint32_t sp = currentSp; + uint32_t initialStack = 0u; + const uint32_t stackSize = stackSizeSigned > 0 + ? static_cast(stackSizeSigned) + : 0u; if (stack == 0xFFFFFFFFu) { if (stackSizeSigned > 0) @@ -534,6 +540,16 @@ namespace ps2_syscalls } sp &= ~0xFu; + if (stack == 0xFFFFFFFFu) + { + initialStack = sp; + } + else if (stack != 0u) + { + initialStack = stack; + } + + scheduler.setupCurrentThread(initialStack, stackSize, getRegU32(ctx, 28)); setReturnU32(ctx, sp); } diff --git a/ps2xRuntime/src/lib/ps2_iop_host.cpp b/ps2xRuntime/src/lib/ps2_iop_host.cpp index 1a165a6..8709ed1 100644 --- a/ps2xRuntime/src/lib/ps2_iop_host.cpp +++ b/ps2xRuntime/src/lib/ps2_iop_host.cpp @@ -2,6 +2,7 @@ #include "ps2_runtime.h" #include "ps2_stubs.h" +#include "Kernel/Stubs/SIF.h" #include "runtime/ps2_memory.h" #include "Kernel/Stubs/MemoryCard.h" #include "Kernel/Syscalls/Common.h" @@ -134,6 +135,11 @@ bool PS2IopHostAdapter::readGuest(uint32_t address, void *destination, size_t si { return false; } + if (ps2_stubs::isSifIopHeapAddress(address)) + { + return ps2_stubs::readSifIopHeap(address, destination, size); + } + uint8_t *source = nullptr; if (!guestRange(address, size, source)) { @@ -152,6 +158,11 @@ bool PS2IopHostAdapter::writeGuest(uint32_t address, const void *source, size_t { return false; } + if (ps2_stubs::isSifIopHeapAddress(address)) + { + return ps2_stubs::writeSifIopHeap(address, source, size); + } + uint8_t *destination = nullptr; if (!guestRange(address, size, destination)) { @@ -168,6 +179,11 @@ bool PS2IopHostAdapter::writeGuest(uint32_t address, const void *source, size_t bool PS2IopHostAdapter::zeroGuest(uint32_t address, size_t size) { + if (ps2_stubs::isSifIopHeapAddress(address)) + { + return ps2_stubs::zeroSifIopHeap(address, size); + } + uint8_t *destination = nullptr; if (!guestRange(address, size, destination)) { @@ -184,6 +200,12 @@ bool PS2IopHostAdapter::zeroGuest(uint32_t address, size_t size) bool PS2IopHostAdapter::normalizeGuestAddress(uint32_t address, uint32_t &normalized) const { + if (ps2_stubs::isSifIopHeapAddress(address)) + { + normalized = address; + return ps2_stubs::isSifIopHeapRange(address, 0u); + } + bool scratchpad = false; if (!ps2ResolveGuestPointer(address, normalized, scratchpad) || scratchpad) { diff --git a/ps2xRuntime/src/lib/ps2_memory.cpp b/ps2xRuntime/src/lib/ps2_memory.cpp index aabacfc..01bbcc2 100644 --- a/ps2xRuntime/src/lib/ps2_memory.cpp +++ b/ps2xRuntime/src/lib/ps2_memory.cpp @@ -3,8 +3,8 @@ #include "runtime/ps2_gs_gpu.h" #include "ps2_log.h" #include -#include #include +#include #include #include #include @@ -141,26 +141,55 @@ namespace } while (!csr.compare_exchange_weak(expected, desired)); } - constexpr uint32_t kEeTimer0Count = 0x10000000u; - constexpr uint32_t kEeTimer0Mode = 0x10000010u; - constexpr uint32_t kEeTimer0Compare = 0x10000020u; - constexpr uint32_t kEeTimer0Hold = 0x10000030u; + constexpr std::array kEeTimerBases = { + 0x10000000u, + 0x10000800u, + 0x10001000u, + 0x10001800u, + }; + constexpr uint32_t kEeTimerCountOffset = 0x00u; + constexpr uint32_t kEeTimerModeOffset = 0x10u; + constexpr uint32_t kEeTimerCompareOffset = 0x20u; + constexpr uint32_t kEeTimerHoldOffset = 0x30u; + constexpr uint32_t kEeTimerModeClksMask = 0x3u; + constexpr uint32_t kEeTimerModeConfigMask = 0x3FFu; + constexpr uint32_t kEeTimerModeStatusMask = 0xC00u; + constexpr uint32_t kEeTimerModeZret = 1u << 6; constexpr uint32_t kEeTimerModeCue = 1u << 7; - constexpr uint64_t kEeTimer0TicksPerSecond = 15720ull; - constexpr uint64_t kNanosecondsPerSecond = 1000000000ull; + constexpr uint32_t kEeTimerModeCmpe = 1u << 8; + constexpr uint32_t kEeTimerModeOvfe = 1u << 9; + constexpr uint32_t kEeTimerModeEquf = 1u << 10; + constexpr uint32_t kEeTimerModeOvff = 1u << 11; + constexpr uint64_t kEeClockHz = 294912000ull; + constexpr std::array kEeTimerClockHz = { + 147456000ull, + 9216000ull, + 576000ull, + 15734ull, + }; - inline bool isEeTimer0Register(uint32_t address) + inline bool decodeEeTimerRegister(uint32_t address, size_t &timerIndex, uint32_t &offset) { - return address == kEeTimer0Count || - address == kEeTimer0Mode || - address == kEeTimer0Compare || - address == kEeTimer0Hold; + for (size_t index = 0; index < kEeTimerBases.size(); ++index) + { + const uint32_t candidateOffset = address - kEeTimerBases[index]; + if (candidateOffset == kEeTimerCountOffset || + candidateOffset == kEeTimerModeOffset || + candidateOffset == kEeTimerCompareOffset || + (index < 2u && candidateOffset == kEeTimerHoldOffset)) + { + timerIndex = index; + offset = candidateOffset; + return true; + } + } + return false; } - inline uint64_t steadyClockNs() + constexpr uint64_t ticksUntilMatch(uint32_t count, uint32_t target) { - using namespace std::chrono; - return static_cast(duration_cast(steady_clock::now().time_since_epoch()).count()); + const uint32_t distance = (target - count) & 0xFFFFu; + return distance == 0u ? 0x10000ull : static_cast(distance); } struct DmaTagView @@ -302,8 +331,7 @@ bool PS2Memory::initialize(size_t ramSize) m_path3MaskedFifo.clear(); m_vif1PendingPath2ImageQwc = 0u; m_vif1PendingPath2DirectHl = false; - m_timer0LastHostNs = 0; - m_timer0FractionNs = 0; + resetEeTimers(); try { @@ -371,37 +399,121 @@ bool PS2Memory::initialize(size_t ramSize) } } -void PS2Memory::updateEeTimer0Counter() +void PS2Memory::resetEeTimers() noexcept { - const uint64_t nowNs = steadyClockNs(); - if (m_timer0LastHostNs == 0u) + m_eeTimers = {}; +} + +uint32_t PS2Memory::advanceEeTimers(uint64_t eeCycles) noexcept +{ + if (eeCycles == 0u) { - m_timer0LastHostNs = nowNs; - return; + return 0u; } - const uint32_t mode = m_ioRegisters.count(kEeTimer0Mode) ? m_ioRegisters[kEeTimer0Mode] : 0u; - if ((mode & kEeTimerModeCue) == 0u) + uint32_t interruptMask = 0u; + for (size_t index = 0; index < m_eeTimers.size(); ++index) { - m_timer0LastHostNs = nowNs; - m_timer0FractionNs = 0u; - return; - } + EeTimer &timer = m_eeTimers[index]; + if ((timer.mode & kEeTimerModeCue) == 0u) + { + continue; + } - const uint64_t elapsedNs = nowNs - m_timer0LastHostNs; - m_timer0LastHostNs = nowNs; - if (elapsedNs == 0u) - { - return; - } + const uint64_t clockHz = kEeTimerClockHz[timer.mode & kEeTimerModeClksMask]; + const uint64_t wholeSeconds = eeCycles / kEeClockHz; + const uint64_t remainingCycles = eeCycles % kEeClockHz; + const uint64_t scaled = remainingCycles * clockHz + timer.clockRemainder; + const uint64_t ticks = wholeSeconds * clockHz + scaled / kEeClockHz; + timer.clockRemainder = scaled % kEeClockHz; + if (ticks == 0u) + { + continue; + } - const uint64_t scaled = elapsedNs * kEeTimer0TicksPerSecond + m_timer0FractionNs; - const uint64_t ticks = scaled / kNanosecondsPerSecond; - m_timer0FractionNs = scaled % kNanosecondsPerSecond; - if (ticks != 0u) - { - m_ioRegisters[kEeTimer0Count] = m_ioRegisters[kEeTimer0Count] + static_cast(ticks); + const uint32_t oldCount = timer.count & 0xFFFFu; + const uint32_t compare = timer.compare & 0xFFFFu; + const uint64_t compareDistance = ticksUntilMatch(oldCount, compare); + const uint64_t overflowDistance = 0x10000ull - oldCount; + const bool zeroReturn = (timer.mode & kEeTimerModeZret) != 0u; + const bool compareReached = ticks >= compareDistance; + bool overflowReached = false; + + if (zeroReturn) + { + overflowReached = ticks >= overflowDistance && overflowDistance <= compareDistance; + if (compareReached) + { + const uint64_t remaining = ticks - compareDistance; + timer.count = compare == 0u + ? static_cast(remaining & 0xFFFFu) + : static_cast(remaining % compare); + } + else + { + timer.count = static_cast((oldCount + ticks) & 0xFFFFu); + } + } + else + { + overflowReached = ticks >= overflowDistance; + timer.count = static_cast((oldCount + ticks) & 0xFFFFu); + } + + if (compareReached && (timer.mode & kEeTimerModeCmpe) != 0u && (timer.mode & kEeTimerModeEquf) == 0u) + { + timer.mode |= kEeTimerModeEquf; + interruptMask |= 1u << index; + } + if (overflowReached && (timer.mode & kEeTimerModeOvfe) != 0u && (timer.mode & kEeTimerModeOvff) == 0u) + { + timer.mode |= kEeTimerModeOvff; + interruptMask |= 1u << index; + } } + return interruptMask; +} + +uint64_t PS2Memory::cyclesUntilNextEeTimerInterrupt() const noexcept +{ + uint64_t nearest = std::numeric_limits::max(); + for (const EeTimer &timer : m_eeTimers) + { + if ((timer.mode & kEeTimerModeCue) == 0u) + { + continue; + } + + const uint32_t count = timer.count & 0xFFFFu; + const uint32_t compare = timer.compare & 0xFFFFu; + const uint64_t compareDistance = ticksUntilMatch(count, compare); + const uint64_t overflowDistance = 0x10000ull - count; + uint64_t eventTicks = std::numeric_limits::max(); + + if ((timer.mode & kEeTimerModeCmpe) != 0u && + (timer.mode & kEeTimerModeEquf) == 0u) + { + eventTicks = compareDistance; + } + const bool overflowCanOccur = (timer.mode & kEeTimerModeZret) == 0u || + overflowDistance <= compareDistance; + if (overflowCanOccur && + (timer.mode & kEeTimerModeOvfe) != 0u && + (timer.mode & kEeTimerModeOvff) == 0u) + { + eventTicks = std::min(eventTicks, overflowDistance); + } + if (eventTicks == std::numeric_limits::max()) + { + continue; + } + + const uint64_t clockHz = kEeTimerClockHz[timer.mode & kEeTimerModeClksMask]; + const uint64_t numerator = eventTicks * kEeClockHz - timer.clockRemainder; + const uint64_t cycles = (numerator + clockHz - 1u) / clockHz; + nearest = std::min(nearest, std::max(1u, cycles)); + } + return nearest; } bool PS2Memory::isScratchpad(uint32_t address) const @@ -991,22 +1103,36 @@ void PS2Memory::write128(uint32_t address, __m128i value) bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) { - if (isEeTimer0Register(address)) + size_t timerIndex = 0u; + uint32_t timerOffset = 0u; + if (decodeEeTimerRegister(address, timerIndex, timerOffset)) { - if (address == kEeTimer0Count) + EeTimer &timer = m_eeTimers[timerIndex]; + switch (timerOffset) { - m_ioRegisters[address] = value; - m_timer0LastHostNs = steadyClockNs(); - m_timer0FractionNs = 0u; - return true; + case kEeTimerCountOffset: + timer.count = value & 0xFFFFu; + timer.clockRemainder = 0u; + break; + case kEeTimerModeOffset: + { + const uint32_t previousMode = timer.mode; + const uint32_t status = (previousMode & kEeTimerModeStatusMask) &~(value & kEeTimerModeStatusMask); + timer.mode = (value & kEeTimerModeConfigMask) | status; + if (((previousMode ^ timer.mode) & (kEeTimerModeClksMask | kEeTimerModeCue)) != 0u) + { + timer.clockRemainder = 0u; + } + break; } - - updateEeTimer0Counter(); - m_ioRegisters[address] = value; - m_timer0LastHostNs = steadyClockNs(); - if (address == kEeTimer0Mode) - { - m_timer0FractionNs = 0u; + case kEeTimerCompareOffset: + timer.compare = value & 0xFFFFu; + break; + case kEeTimerHoldOffset: + timer.hold = value & 0xFFFFu; + break; + default: + return false; } return true; } @@ -2037,6 +2163,26 @@ int PS2Memory::pollDmaRegisters() uint32_t PS2Memory::readIORegister(uint32_t address) { + size_t timerIndex = 0u; + uint32_t timerOffset = 0u; + if (decodeEeTimerRegister(address, timerIndex, timerOffset)) + { + const EeTimer &timer = m_eeTimers[timerIndex]; + switch (timerOffset) + { + case kEeTimerCountOffset: + return timer.count & 0xFFFFu; + case kEeTimerModeOffset: + return timer.mode & (kEeTimerModeConfigMask | kEeTimerModeStatusMask); + case kEeTimerCompareOffset: + return timer.compare & 0xFFFFu; + case kEeTimerHoldOffset: + return timer.hold & 0xFFFFu; + default: + return 0u; + } + } + if (isGsPrivReg(address)) { // NB: unreachable from read8/16/32/64 today, same reasoning as the write @@ -2077,19 +2223,6 @@ uint32_t PS2Memory::readIORegister(uint32_t address) } if (address >= 0x10000000 && address < 0x10010000) { - if (address >= 0x10000000 && address < 0x10000100) - { - if (isEeTimer0Register(address)) - { - if (address == kEeTimer0Count) - { - updateEeTimer0Counter(); - } - auto timerIt = m_ioRegisters.find(address); - return timerIt != m_ioRegisters.end() ? timerIt->second : 0u; - } - } - if (address >= 0x10008000 && address < 0x1000F000) { if ((address & 0xFF) == 0x00) diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 38bc945..a6ed05b 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -1320,14 +1320,13 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram, return false; } - if (kind == GuestBranchKind::Return) + if (!isCall) { if (!hasFunction(targetPc)) { reportMissingFunction(rdram, ctx, targetPc, sourcePc, kind, debugName); } - // Prevent nested dispatch. ctx->pc = targetPc; return false; } @@ -1362,11 +1361,6 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram, return false; } - if (!isCall) - { - return false; - } - if (ctx->pc == entryPc) { ctx->pc = fallthroughPc; diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 1dee8db..2dd9c53 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -1042,7 +1042,7 @@ void register_code_generator_tests() "QFSRV should map to PS2_QFSRV with rs/rt ordering"); }); - tc.Run("PCPYLD and PEXEW use runtime helper macros", [](TestCase &t) { + tc.Run("PCPYLD uses runtime helper macro", [](TestCase &t) { CodeGenerator gen({}, {}); Instruction pcpyld{}; @@ -1057,18 +1057,45 @@ void register_code_generator_tests() std::string pcpyldOut = gen.translateInstruction(pcpyld); t.IsTrue(pcpyldOut.find("PS2_PCPYLD(GPR_VEC(ctx, 7), GPR_VEC(ctx, 8))") != std::string::npos, "PCPYLD should use PS2_PCPYLD helper"); + }); - Instruction pexew{}; - pexew.isMMI = true; - pexew.opcode = OPCODE_MMI; - pexew.function = MMI_MMI2; - pexew.sa = MMI2_PEXEW; - pexew.rd = 9; - pexew.rs = 10; + tc.Run("Unary MMI permutations read their source from rt", [](TestCase &t) { + CodeGenerator gen({}, {}); - std::string pexewOut = gen.translateInstruction(pexew); - t.IsTrue(pexewOut.find("PS2_PEXEW(GPR_VEC(ctx, 10))") != std::string::npos, - "PEXEW should use PS2_PEXEW helper"); + struct UnaryMmiCase + { + const char *name; + uint8_t function; + uint8_t subfunction; + }; + + const std::vector cases = { + {"PEXEH", MMI_MMI2, MMI2_PEXEH}, + {"PREVH", MMI_MMI2, MMI2_PREVH}, + {"PEXEW", MMI_MMI2, MMI2_PEXEW}, + {"PROT3W", MMI_MMI2, MMI2_PROT3W}, + {"PEXCH", MMI_MMI3, MMI3_PEXCH}, + {"PCPYH", MMI_MMI3, MMI3_PCPYH}, + {"PEXCW", MMI_MMI3, MMI3_PEXCW}, + }; + + for (const UnaryMmiCase &item : cases) + { + Instruction inst{}; + inst.isMMI = true; + inst.opcode = OPCODE_MMI; + inst.function = item.function; + inst.sa = item.subfunction; + inst.rd = 3; + inst.rs = 4; + inst.rt = 5; + + const std::string out = gen.translateInstruction(inst); + t.IsTrue(out.find("GPR_VEC(ctx, 5)") != std::string::npos, + std::string(item.name) + " should read its source from rt"); + t.IsTrue(out.find("GPR_VEC(ctx, 4)") == std::string::npos, + std::string(item.name) + " should not read its source from rs"); + } }); tc.Run("VU0 macro mappings cover all S1/S2 enums", [](TestCase &t) { diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 3bcb7a5..aa8013c 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -8,10 +8,8 @@ #include "Stubs/GS.h" #include -#include #include #include -#include #include namespace @@ -194,7 +192,7 @@ void register_ps2_memory_tests() t.Equals(mem.translateAddress(PS2_SCRATCHPAD_ALIAS_BASE + 0x123u), 0x123u, "0xF000 scratchpad alias should translate to local offset"); }); - tc.Run("EE timer0 count advances while enabled and can be reset", [](TestCase &t) + tc.Run("EE timer0 count advances from scheduler cycles and can be reset", [](TestCase &t) { PS2Memory mem; t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); @@ -204,17 +202,111 @@ void register_ps2_memory_tests() constexpr uint32_t kTimer0Compare = 0x10000020u; t.IsTrue(mem.writeIORegister(kTimer0Count, 0u), "timer count reset write should succeed"); - t.IsTrue(mem.writeIORegister(kTimer0Compare, 1u), "timer compare write should succeed"); - t.IsTrue(mem.writeIORegister(kTimer0Mode, 0x283u), "timer mode write should be retained"); - t.Equals(mem.readIORegister(kTimer0Mode), 0x283u, "timer mode should be readable"); + t.IsTrue(mem.writeIORegister(kTimer0Compare, 0xFFFFu), "timer compare write should succeed"); + t.IsTrue(mem.writeIORegister(kTimer0Mode, 0x82u), "timer mode write should be retained"); + t.Equals(mem.readIORegister(kTimer0Mode), 0x82u, "timer mode should be readable"); - std::this_thread::sleep_for(std::chrono::milliseconds(3)); + mem.advanceEeTimers(8u * 512u); const uint32_t firstCount = mem.readIORegister(kTimer0Count); - t.IsTrue(firstCount > 0u, "enabled timer count should advance from host time"); + t.Equals(firstCount, 8u, "BUSCLK/256 should increment once per 512 EE cycles"); t.IsTrue(mem.writeIORegister(kTimer0Count, 0u), "timer count second reset should succeed"); + mem.advanceEeTimers(512u); const uint32_t resetCount = mem.readIORegister(kTimer0Count); - t.IsTrue(resetCount <= firstCount, "timer reset should restart the count window"); + t.Equals(resetCount, 1u, "timer reset should restart the deterministic count window"); + }); + + tc.Run("EE timers 0 through 3 expose independent COUNT MODE and COMP registers", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kTimerBases[] = { + 0x10000000u, + 0x10000800u, + 0x10001000u, + 0x10001800u, + }; + constexpr uint32_t kBusClockDiv256Cue = 0x82u; + for (uint32_t index = 0u; index < 4u; ++index) + { + const uint32_t base = kTimerBases[index]; + t.IsTrue(mem.writeIORegister(base, 0x100u + index), "timer COUNT write should succeed"); + t.IsTrue(mem.writeIORegister(base + 0x10u, kBusClockDiv256Cue), "timer MODE write should succeed"); + t.IsTrue(mem.writeIORegister(base + 0x20u, 0x200u + index), "timer COMP write should succeed"); + } + + mem.advanceEeTimers(512u); + for (uint32_t index = 0u; index < 4u; ++index) + { + const uint32_t base = kTimerBases[index]; + t.Equals(mem.readIORegister(base), 0x101u + index, "each timer should advance its own COUNT"); + t.Equals(mem.readIORegister(base + 0x10u), kBusClockDiv256Cue, "each timer should retain MODE"); + t.Equals(mem.readIORegister(base + 0x20u), 0x200u + index, "each timer should retain COMP"); + } + + t.IsTrue(mem.writeIORegister(kTimerBases[0] + 0x30u, 0x12345u), "Timer0 HOLD write should succeed"); + t.IsTrue(mem.writeIORegister(kTimerBases[1] + 0x30u, 0x23456u), "Timer1 HOLD write should succeed"); + t.Equals(mem.readIORegister(kTimerBases[0] + 0x30u), 0x2345u, "Timer0 HOLD should be 16-bit"); + t.Equals(mem.readIORegister(kTimerBases[1] + 0x30u), 0x3456u, "Timer1 HOLD should be 16-bit"); + }); + + tc.Run("EE Timer2 compare and overflow flags raise INTC_TIM2 and clear on write-one", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kTimer2Count = 0x10001000u; + constexpr uint32_t kTimer2Mode = 0x10001010u; + constexpr uint32_t kTimer2Compare = 0x10001020u; + constexpr uint32_t kCue = 1u << 7u; + constexpr uint32_t kCmpe = 1u << 8u; + constexpr uint32_t kOvfe = 1u << 9u; + constexpr uint32_t kEquf = 1u << 10u; + constexpr uint32_t kOvff = 1u << 11u; + constexpr uint32_t kBusClockDiv256 = 2u; + + mem.writeIORegister(kTimer2Count, 0u); + mem.writeIORegister(kTimer2Compare, 8u); + mem.writeIORegister(kTimer2Mode, kBusClockDiv256 | kCue | kCmpe | kEquf | kOvff); + + t.Equals(mem.advanceEeTimers(7u * 512u), 0u, "compare should not fire before COUNT reaches COMP"); + t.Equals(mem.readIORegister(kTimer2Count), 7u, "Timer2 should expose its live 16-bit count"); + t.Equals(mem.advanceEeTimers(512u), 1u << 2u, "Timer2 compare should raise the TIM2 interrupt bit"); + t.IsTrue((mem.readIORegister(kTimer2Mode) & kEquf) != 0u, "Timer2 compare should latch EQUF"); + + mem.writeIORegister(kTimer2Mode, mem.readIORegister(kTimer2Mode) | kEquf); + t.IsTrue((mem.readIORegister(kTimer2Mode) & kEquf) == 0u, "writing one should clear EQUF"); + + mem.writeIORegister(kTimer2Count, 0xFFFFu); + mem.writeIORegister(kTimer2Mode, kBusClockDiv256 | kCue | kOvfe | kOvff); + t.Equals(mem.advanceEeTimers(512u), 1u << 2u, "Timer2 overflow should raise the TIM2 interrupt bit"); + t.Equals(mem.readIORegister(kTimer2Count), 0u, "Timer2 count should wrap at 16 bits"); + t.IsTrue((mem.readIORegister(kTimer2Mode) & kOvff) != 0u, "Timer2 overflow should latch OVFF"); + + mem.writeIORegister(kTimer2Mode, mem.readIORegister(kTimer2Mode) | kOvff); + t.IsTrue((mem.readIORegister(kTimer2Mode) & kOvff) == 0u, "writing one should clear OVFF"); + }); + + tc.Run("EE timer zero-return clears COUNT on compare", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kTimer0Count = 0x10000000u; + constexpr uint32_t kTimer0Mode = 0x10000010u; + constexpr uint32_t kTimer0Compare = 0x10000020u; + constexpr uint32_t kZret = 1u << 6u; + constexpr uint32_t kCue = 1u << 7u; + constexpr uint32_t kCmpe = 1u << 8u; + constexpr uint32_t kEquf = 1u << 10u; + + mem.writeIORegister(kTimer0Count, 0u); + mem.writeIORegister(kTimer0Compare, 3u); + mem.writeIORegister(kTimer0Mode, kZret | kCue | kCmpe | kEquf); + + t.Equals(mem.advanceEeTimers(6u), 1u, "Timer0 compare should raise TIM0 after three BUSCLK ticks"); + t.Equals(mem.readIORegister(kTimer0Count), 0u, "ZRET should clear COUNT when it equals COMP"); }); tc.Run("scratchpad alias accesses the same bytes as base", [](TestCase &t) diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index 118cc5c..cbf6434 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -166,6 +166,13 @@ namespace } } + std::atomic gGuestJumpTargetCount{0u}; + + void testGuestJumpTargetHandler(uint8_t *, R5900Context *, PS2Runtime *) + { + gGuestJumpTargetCount.fetch_add(1u, std::memory_order_relaxed); + } + std::atomic gMpegStreamCallbackCount{0u}; std::atomic gMpegStreamCallbackMpeg{0u}; std::atomic gMpegStreamCallbackType{0u}; @@ -399,6 +406,32 @@ void register_ps2_runtime_expansion_tests() "callee should still execute normally"); }); + tc.Run("dispatchGuestBranch jump returns to central dispatcher without nesting", [](TestCase &t) + { + PS2Runtime runtime; + runtime.registerFunction(0x3400u, &testGuestJumpTargetHandler); + gGuestJumpTargetCount.store(0u, std::memory_order_relaxed); + + R5900Context ctx{}; + ctx.pc = 0x2000u; + + const bool continuedInCaller = runtime.dispatchGuestBranch( + nullptr, + &ctx, + 0x3400u, + 0x2000u, + 0u, + PS2Runtime::GuestBranchKind::IndirectJump, + "test-jr"); + + t.IsFalse(continuedInCaller, + "jump should stop the current generated wrapper"); + t.Equals(gGuestJumpTargetCount.load(std::memory_order_relaxed), 0u, + "jump target must not execute on a nested host stack frame"); + t.Equals(ctx.pc, 0x3400u, + "central dispatcher should receive the exact jump target"); + }); + tc.Run("dispatchGuestBranch call returns false when callee transfers elsewhere", [](TestCase &t) { PS2Runtime runtime; diff --git a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp index be4c1ad..ccf4b70 100644 --- a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp +++ b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp @@ -59,6 +59,17 @@ namespace constexpr uint32_t kEventWaitPc = 0x00160400u; constexpr uint32_t kEventResumePc = 0x00160410u; constexpr uint32_t kEventProducerPc = 0x00160420u; + constexpr uint32_t kTimer2WaitPc = 0x00160500u; + constexpr uint32_t kTimer2ResumePc = 0x00160510u; + constexpr uint32_t kTimer2HandlerPc = 0x00160520u; + + constexpr uint32_t kTimer2Count = 0x10001000u; + constexpr uint32_t kTimer2Mode = 0x10001010u; + constexpr uint32_t kTimer2Compare = 0x10001020u; + constexpr uint32_t kTimerModeBusClockDiv256 = 2u; + constexpr uint32_t kTimerModeCue = 1u << 7u; + constexpr uint32_t kTimerModeCmpe = 1u << 8u; + constexpr uint32_t kTimerModeEquf = 1u << 10u; constexpr uint32_t kVSyncFlagAddr = 0x1800u; constexpr uint32_t kVSyncTickAddr = 0x1810u; @@ -71,6 +82,7 @@ namespace uint32_t g_vsyncFlag = 0; uint64_t g_vsyncTick = 0; uint64_t g_vsyncCsr = 0; + std::atomic g_timer2Resumed{false}; void setRegU32(R5900Context &ctx, int reg, uint32_t value) { @@ -247,6 +259,41 @@ namespace ctx->pc = 0u; runtime->requestStop(); } + + void schedulerTimer2Handler(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + g_dispatchTrace.push_back(2); + PS2Memory &memory = runtime->memory(); + memory.writeIORegister(kTimer2Mode, memory.readIORegister(kTimer2Mode) | kTimerModeEquf); + runtime->eeScheduler().signalSemaphore(g_testSemaphoreId, true); + ctx->pc = 0u; + } + + void schedulerTimer2Wait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + g_dispatchTrace.push_back(1); + EeScheduler &scheduler = runtime->eeScheduler(); + g_testSemaphoreId = scheduler.createSemaphore(0, 1, 0u, 0u); + scheduler.addIrqHandler(false, 11u, kTimer2HandlerPc, true, 0u, 0u, 0u); + + PS2Memory &memory = runtime->memory(); + memory.writeIORegister(kTimer2Count, 0u); + memory.writeIORegister(kTimer2Compare, 8u); + memory.writeIORegister(kTimer2Mode, + kTimerModeBusClockDiv256 | kTimerModeCue | kTimerModeCmpe | kTimerModeEquf); + + ctx->pc = kTimer2ResumePc; + scheduler.waitSemaphore(g_testSemaphoreId); + } + + void schedulerTimer2Resume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + g_dispatchTrace.push_back(3); + g_resumedResult = getRegS32(*ctx, 2); + g_timer2Resumed.store(true, std::memory_order_release); + ctx->pc = 0u; + runtime->requestStop(); + } } void register_ps2_runtime_interrupt_tests() @@ -478,6 +525,52 @@ void register_ps2_runtime_interrupt_tests() } }); + tc.Run("EE Timer2 compare IRQ wakes a DelayThread-style semaphore wait", [](TestCase &t) + { + TestEnv env; + t.IsTrue(env.runtime.memory().initialize(), "runtime memory initialize should succeed"); + env.runtime.registerFunction(kTimer2WaitPc, schedulerTimer2Wait); + env.runtime.registerFunction(kTimer2ResumePc, schedulerTimer2Resume); + env.runtime.registerFunction(kTimer2HandlerPc, schedulerTimer2Handler); + + g_dispatchTrace.clear(); + g_resumedResult = -1; + g_timer2Resumed.store(false, std::memory_order_release); + R5900Context mainContext{}; + mainContext.pc = kTimer2WaitPc; + std::atomic schedulerThrew{false}; + std::thread gameThread([&]() + { + try + { + env.runtime.eeScheduler().reset(env.rdram.data(), mainContext); + env.runtime.eeScheduler().run(); + } + catch (...) + { + schedulerThrew.store(true, std::memory_order_release); + } + }); + + const bool resumed = waitUntil([]() + { + return g_timer2Resumed.load(std::memory_order_acquire); + }, std::chrono::milliseconds(150)); + if (!resumed) + { + env.runtime.requestStop(); + } + gameThread.join(); + + t.IsTrue(resumed, "Timer2 compare should dispatch INTC_TIM2 and wake the semaphore waiter"); + t.IsFalse(schedulerThrew.load(std::memory_order_acquire), "Timer2 IRQ path should not throw"); + const std::vector expected{1, 2, 3}; + t.IsTrue(g_dispatchTrace == expected, + "Timer2 flow should run wait, interrupt handler, then the resumed thread"); + t.Equals(g_resumedResult, g_testSemaphoreId, + "the Timer2 handler should hand the semaphore directly to the waiter"); + }); + tc.Run("scheduler stop wakes an idle VSync wait without a timeout", [](TestCase &t) { TestEnv env; diff --git a/ps2xTest/src/ps2_runtime_kernel_tests.cpp b/ps2xTest/src/ps2_runtime_kernel_tests.cpp index e552df6..2542d48 100644 --- a/ps2xTest/src/ps2_runtime_kernel_tests.cpp +++ b/ps2xTest/src/ps2_runtime_kernel_tests.cpp @@ -1049,6 +1049,46 @@ void register_ps2_runtime_kernel_tests() t.Equals(setupSp & 0xFu, 0u, "SetupThread should always return a 16-byte aligned stack pointer"); }); + tc.Run("SetupThread exposes stable main stack metadata through ReferThreadStatus", [](TestCase &t) + { + TestEnv env; + constexpr uint32_t kInitialLoaderSp = PS2_RAM_SIZE - 0x10u; + constexpr uint32_t kMainStackSize = 0x00020000u; + constexpr uint32_t kExpectedStack = PS2_RAM_SIZE - kMainStackSize; + constexpr uint32_t kMainGp = 0x0036A7F0u; + + env.ctx.pc = 0x00100000u; + setRegU32(env.ctx, 29, kInitialLoaderSp); + setRegU32(env.ctx, 4, kMainGp); + setRegU32(env.ctx, 5, 0xFFFFFFFFu); + setRegU32(env.ctx, 6, kMainStackSize); + t.IsTrue(callSyscall(0x3Cu, env.rdram.data(), &env.ctx, &env.runtime), + "SetupThread syscall should dispatch"); + t.Equals(::getRegU32(&env.ctx, 2), kExpectedStack, + "automatic main stack should start below the reserved top-of-RDRAM area"); + + // ReferThreadStatus can be called after many nested frames have moved $sp. + // It must report the initial stack recorded by SetupThread, not this live snapshot. + constexpr uint32_t kTransientSp = kExpectedStack - 0x80u; + setRegU32(env.ctx, 29, kTransientSp); + setRegU32(env.ctx, 4, 0u); + setRegU32(env.ctx, 5, K_STATUS_ADDR); + t.IsTrue(callSyscall(0x30u, env.rdram.data(), &env.ctx, &env.runtime), + "ReferThreadStatus syscall should dispatch"); + t.Equals(getRegS32(env.ctx, 2), KE_OK, "ReferThreadStatus should accept the current-thread id alias"); + + EeThreadStatusAbi status{}; + std::memcpy(&status, env.rdram.data() + K_STATUS_ADDR, sizeof(status)); + t.Equals(status.stack, kExpectedStack, + "main thread status must expose SetupThread's stable initial stack"); + t.Equals(status.stack_size, static_cast(kMainStackSize), + "main thread status must preserve SetupThread's stack size"); + t.Equals(status.gp_reg, kMainGp, + "main thread status must preserve SetupThread's global pointer"); + t.IsTrue(status.stack != kInitialLoaderSp && status.stack != kTransientSp, + "main thread status must never expose a live stack-pointer snapshot"); + }); + tc.Run("OSD config2 syscalls round-trip extended config", [](TestCase &t) { TestEnv env; diff --git a/ps2xTest/src/ps2_sif_dma_tests.cpp b/ps2xTest/src/ps2_sif_dma_tests.cpp index 9198d17..46eddea 100644 --- a/ps2xTest/src/ps2_sif_dma_tests.cpp +++ b/ps2xTest/src/ps2_sif_dma_tests.cpp @@ -1,8 +1,10 @@ #include "MiniTest.h" #include "ps2_runtime.h" +#include "ps2_iop_host.h" #include "ps2_iop_transport.h" #include "ps2_syscalls.h" #include "ps2_stubs.h" +#include "Kernel/Stubs/SIF.h" #include "runtime/ee_scheduler.h" #include @@ -197,6 +199,79 @@ void register_ps2_sif_dma_tests() t.IsTrue(getRegS32(env.ctx, 2) < 0, "sceSifDmaStat should be negative when transfer is complete"); }); + tc.Run("IOP heap DMA uses private backing instead of aliasing EE RDRAM", [](TestCase &t) + { + TestEnv env; + + constexpr uint32_t kDescAddr = 0x00020040u; + constexpr uint32_t kSrcAddr = 0x00020140u; + constexpr uint32_t kRoundTripAddr = 0x00020240u; + constexpr uint32_t kFormerAliasAddr = 0x01A53880u; + constexpr uint32_t kIopBlockSize = 0x880u; + + std::array payload{}; + for (size_t i = 0; i < payload.size(); ++i) + { + payload[i] = static_cast(0x80u + i); + } + std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size()); + std::memset(env.rdram.data() + kRoundTripAddr, 0, payload.size()); + std::memset(env.rdram.data() + kFormerAliasAddr, 0x5Au, payload.size()); + + setRegU32(env.ctx, 4, kIopBlockSize); + ps2_stubs::sceSifAllocIopHeap(env.rdram.data(), &env.ctx, &env.runtime); + const uint32_t iopAddress = ::getRegU32(&env.ctx, 2); + t.IsTrue(iopAddress >= PS2_RAM_SIZE, + "sceSifAllocIopHeap should return an address outside EE RDRAM"); + + Ps2SifDmaTransfer desc{ + kSrcAddr, + iopAddress, + static_cast(payload.size()), + 0}; + std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc)); + setRegU32(env.ctx, 4, kDescAddr); + setRegU32(env.ctx, 5, 1u); + ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); + t.IsTrue(getRegS32(env.ctx, 2) > 0, + "EE-to-IOP DMA should accept a private IOP heap destination"); + + const std::array aliasSentinel{ + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A}; + t.IsTrue(std::memcmp(env.rdram.data() + kFormerAliasAddr, + aliasSentinel.data(), aliasSentinel.size()) == 0, + "IOP DMA must not overwrite the old 0x01A00000 EE alias range"); + + PS2IopHostAdapter host(env.runtime); + auto scope = host.enterCall(&env.ctx, env.rdram.data()); + uint32_t normalized = 0u; + std::array hostReadback{}; + t.IsTrue(host.normalizeGuestAddress(iopAddress, normalized) && + normalized == iopAddress, + "IOP modules should preserve private IOP heap addresses"); + t.IsTrue(host.readGuest(iopAddress, hostReadback.data(), hostReadback.size()) && + hostReadback == payload, + "IOP modules should read the private heap backing"); + + desc = { + iopAddress, + kRoundTripAddr, + static_cast(payload.size()), + 0}; + std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc)); + setRegU32(env.ctx, 4, kDescAddr); + setRegU32(env.ctx, 5, 1u); + ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); + t.IsTrue(getRegS32(env.ctx, 2) > 0, + "IOP-to-EE DMA should accept a private IOP heap source"); + t.IsTrue(std::memcmp(env.rdram.data() + kRoundTripAddr, + payload.data(), payload.size()) == 0, + "IOP-to-EE DMA should round-trip the payload"); + }); + tc.Run("isceSifSetDma and isceSifSetDChain alias the SIF DMA helpers", [](TestCase &t) { TestEnv env; @@ -438,6 +513,8 @@ void register_ps2_sif_dma_tests() t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the SJX transport"); t.Equals(env.rdram[kEeWorkAddr + 0x11u], static_cast(0u), "SJX DMA ack should rewrite the response line to room so EE recycles the chunk"); + t.Equals(readGuestU32(env.rdram.data(), kEeWorkAddr + 0x14u), 0x12345678u, + "SJX DMA ack should translate the remote handle back to the EE callback object"); t.Equals(readGuestU32(env.rdram.data(), kEeWorkAddr + kWorkLen - sizeof(uint32_t)), 2u, "SJX DMA ack should still advance the EE footer ticket");