diff --git a/CMakeLists.txt b/CMakeLists.txt index c7c7af58d..1b36f2260 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.21) +cmake_minimum_required(VERSION 3.24) if(NOT DEFINED CMAKE_TOOLCHAIN_FILE AND DEFINED ENV{VITASDK}) set(PS2X_VITA_TOOLCHAIN_FILE "$ENV{VITASDK}/share/vita.toolchain.cmake") @@ -17,7 +17,7 @@ option(PS2X_BUILD_RECOMP "Build ps2xRecomp" ON) option(PS2X_BUILD_RUNTIME "Build ps2xRuntime" ON) option(PS2X_BUILD_ANALYZER "Build ps2xAnalyzer" ON) option(PS2X_BUILD_TEST "Build ps2xTest" ON) -option(PS2X_BUILD_STUDIO "Build ps2xStudio" ON) +option(PS2X_BUILD_STUDIO "Build ps2xStudio" OFF) if(ANDROID) message(STATUS "Android target detected, building runtime only") @@ -28,6 +28,10 @@ if(ANDROID) endif() set(PS2X_IS_ARM_TARGET OFF) +if(PS2X_BUILD_TEST OR PS2X_BUILD_STUDIO) + enable_testing() +endif() + set(PS2X_IS_AARCH64_TARGET OFF) if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64|ARM64") set(PS2X_IS_ARM_TARGET ON) @@ -79,20 +83,22 @@ if(PS2X_IS_ARM_TARGET) endif() endif() -if(PS2X_BUILD_RECOMP) +# Include modules required by enabled consumers even if their own option is OFF. +# Tests use all three libraries; Studio also uses the test library. +if(PS2X_BUILD_RECOMP OR PS2X_BUILD_ANALYZER OR PS2X_BUILD_TEST OR PS2X_BUILD_STUDIO) add_subdirectory("ps2xRecomp") endif() -if(PS2X_BUILD_RUNTIME) +if(PS2X_BUILD_RUNTIME OR PS2X_BUILD_TEST OR PS2X_BUILD_STUDIO) add_subdirectory("ps2xIOP") add_subdirectory("ps2xRuntime") endif() -if(PS2X_BUILD_ANALYZER) +if(PS2X_BUILD_ANALYZER OR PS2X_BUILD_TEST OR PS2X_BUILD_STUDIO) add_subdirectory("ps2xAnalyzer") endif() -if(PS2X_BUILD_TEST) +if(PS2X_BUILD_TEST OR PS2X_BUILD_STUDIO) add_subdirectory("ps2xTest") endif() diff --git a/ps2xIOP/CMakeLists.txt b/ps2xIOP/CMakeLists.txt index f8d3e4437..343483b11 100644 --- a/ps2xIOP/CMakeLists.txt +++ b/ps2xIOP/CMakeLists.txt @@ -62,6 +62,11 @@ if(PS2X_IOP_BUILD_TESTS) target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) add_test(NAME ps2_iop_import_version_tests COMMAND ps2_iop_import_version_tests) + add_executable(ps2_iop_execution_tests tests/iop_execution_tests.cpp) + target_link_libraries(ps2_iop_execution_tests PRIVATE ps2_iop) + target_include_directories(ps2_iop_execution_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + add_test(NAME ps2_iop_execution_tests COMMAND ps2_iop_execution_tests) + endif() install(TARGETS ps2_iop diff --git a/ps2xIOP/include/ps2x/iop/iop_subsystem.h b/ps2xIOP/include/ps2x/iop/iop_subsystem.h index 68fec81cf..2b6d84bb5 100644 --- a/ps2xIOP/include/ps2x/iop/iop_subsystem.h +++ b/ps2xIOP/include/ps2x/iop/iop_subsystem.h @@ -27,6 +27,7 @@ namespace ps2x::iop [[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments = nullptr, uint32_t argumentSize = 0); [[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result = nullptr); void runEeCycles(uint64_t eeCycles) noexcept; + [[nodiscard]] const ExecutionFault &fault() const noexcept; [[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const; [[nodiscard]] bool canBindRpc(uint32_t sid) const noexcept; diff --git a/ps2xIOP/include/ps2x/iop/iop_types.h b/ps2xIOP/include/ps2x/iop/iop_types.h index 349144091..ad2242931 100644 --- a/ps2xIOP/include/ps2x/iop/iop_types.h +++ b/ps2xIOP/include/ps2x/iop/iop_types.h @@ -4,9 +4,17 @@ #include #include #include +#include namespace ps2x::iop { + struct ExecutionFault + { + bool active = false; + uint32_t pc = 0; + std::array reason{}; + }; + struct GuestBuffer { uint32_t address = 0; diff --git a/ps2xIOP/src/emulator/core/iop_cpu.cpp b/ps2xIOP/src/emulator/core/iop_cpu.cpp index e09642e30..c44833803 100644 --- a/ps2xIOP/src/emulator/core/iop_cpu.cpp +++ b/ps2xIOP/src/emulator/core/iop_cpu.cpp @@ -48,14 +48,20 @@ namespace ps2x::iop::detail cpu.cop0[12] = (status & ~0x3Fu) | ((status << 2u) & 0x3Fu); cpu.pc = (status & (1u << 22u)) ? 0xBFC00180u : 0x80000080u; cpu.branchPending = false; + if (cpu.pendingLoad && cpu.pendingLoadReg != 0u) + cpu.gpr[cpu.pendingLoadReg] = cpu.pendingLoadValue; cpu.pendingLoad = false; cpu.exception = true; } bool IopCpuCore::executeInstruction(IopCpuState &cpu) + { + return executeInstruction(cpu, m_memory.read32(cpu.pc)); + } + + bool IopCpuCore::executeInstruction(IopCpuState &cpu, uint32_t instruction) { const uint32_t pc = cpu.pc; - const uint32_t instruction = m_memory.read32(pc); const bool wasDelaySlot = cpu.branchPending; const uint32_t priorBranchTarget = cpu.branchTarget; @@ -82,11 +88,8 @@ namespace ps2x::iop::detail auto branch = [&](bool condition) { - if (condition) - { - newBranch = true; - newBranchTarget = nextPc + (static_cast(simm) << 2u); - } + newBranch = true; + newBranchTarget = condition ? nextPc + (static_cast(simm) << 2u) : nextPc + 4u; }; auto write = [&](uint32_t reg, uint32_t value) { diff --git a/ps2xIOP/src/emulator/core/iop_cpu.h b/ps2xIOP/src/emulator/core/iop_cpu.h index f23605092..870cf0ec4 100644 --- a/ps2xIOP/src/emulator/core/iop_cpu.h +++ b/ps2xIOP/src/emulator/core/iop_cpu.h @@ -31,6 +31,7 @@ namespace ps2x::iop::detail explicit IopCpuCore(IopMemory &memory) noexcept; [[nodiscard]] bool executeInstruction(IopCpuState &cpu); + [[nodiscard]] bool executeInstruction(IopCpuState &cpu, uint32_t instruction); void raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional badAddress = std::nullopt) const; private: diff --git a/ps2xIOP/src/emulator/core/iop_kernel.cpp b/ps2xIOP/src/emulator/core/iop_kernel.cpp index 4832d8f5b..fab12b68f 100644 --- a/ps2xIOP/src/emulator/core/iop_kernel.cpp +++ b/ps2xIOP/src/emulator/core/iop_kernel.cpp @@ -32,6 +32,20 @@ namespace ps2x::iop::detail m_nextSemaphoreId = 1; m_nextEventFlagId = 1; m_currentThread = nullptr; + m_nextReady = nullptr; + m_nextWakeCycle = UINT64_MAX; + m_scheduleDirty = true; + m_hasDeadThreads = false; + } + + void IopKernel::setThreadState(IopThread &thread, IopThreadState state) + { + if (thread.state == state) + return; + thread.state = state; + m_scheduleDirty = true; + if (state == IopThreadState::Dead) + m_hasDeadThreads = true; } bool IopKernel::dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle) @@ -62,6 +76,7 @@ namespace ps2x::iop::detail } const int id = thread.id; m_threads.emplace(id, std::move(thread)); + m_scheduleDirty = true; setV0(id); return true; } @@ -74,6 +89,14 @@ namespace ps2x::iop::detail setV0(-1); return true; } + if (&it->second == m_currentThread) + { + setThreadState(it->second, IopThreadState::Dead); + cpu.stopped = cpu.yielded = true; + setV0(0); + return true; + } + m_scheduleDirty = true; if (it->second.stackBase != 0u) (void)m_memory.freeAllocation(it->second.stackBase); m_threads.erase(it); @@ -98,7 +121,7 @@ namespace ps2x::iop::detail thread.cpu.gpr[28] = cpu.gpr[28]; thread.cpu.gpr[29] = alignUp(thread.stackBase + thread.stackSize, 16u) - 16u; thread.cpu.gpr[31] = kThreadReturnSentinel; - thread.state = IopThreadState::Ready; + setThreadState(thread, IopThreadState::Ready); setV0(0); return true; } @@ -106,7 +129,7 @@ namespace ps2x::iop::detail case 9: // ExitDeleteThread if (m_currentThread != nullptr) { - m_currentThread->state = ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant; + setThreadState(*m_currentThread, ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant); cpu.stopped = true; cpu.yielded = true; } @@ -122,7 +145,7 @@ namespace ps2x::iop::detail setV0(-1); return true; } - it->second.state = IopThreadState::Dormant; + setThreadState(it->second, IopThreadState::Dormant); setV0(0); return true; } @@ -143,6 +166,7 @@ namespace ps2x::iop::detail return true; } it->second.priority = std::clamp(cpu.gpr[5], 1u, 126u); + m_scheduleDirty = true; setV0(0); return true; } @@ -166,7 +190,7 @@ namespace ps2x::iop::detail it->second.state == IopThreadState::Semaphore || it->second.state == IopThreadState::EventFlag) { - it->second.state = IopThreadState::Ready; + setThreadState(it->second, IopThreadState::Ready); } setV0(0); return true; @@ -202,7 +226,7 @@ namespace ps2x::iop::detail return true; } if (it->second.state == IopThreadState::Sleep) - it->second.state = IopThreadState::Ready; + setThreadState(it->second, IopThreadState::Ready); else ++it->second.wakeupCount; setV0(0); @@ -233,7 +257,7 @@ namespace ps2x::iop::detail setV0(-1); return true; } - it->second.state = IopThreadState::Suspended; + setThreadState(it->second, IopThreadState::Suspended); if (m_currentThread == &it->second) cpu.yielded = true; setV0(0); @@ -250,7 +274,7 @@ namespace ps2x::iop::detail return true; } if (it->second.state == IopThreadState::Suspended) - it->second.state = IopThreadState::Ready; + setThreadState(it->second, IopThreadState::Ready); setV0(0); return true; } @@ -410,7 +434,7 @@ namespace ps2x::iop::detail setV0(-419); else if (m_currentThread != nullptr) { - m_currentThread->state = IopThreadState::Semaphore; + setThreadState(*m_currentThread, IopThreadState::Semaphore); m_currentThread->waitId = id; cpu.yielded = true; setV0(0); @@ -466,7 +490,7 @@ namespace ps2x::iop::detail if (best != nullptr && semaphore != m_semaphores.end() && semaphore->second.current > 0) { --semaphore->second.current; - best->state = IopThreadState::Ready; + setThreadState(*best, IopThreadState::Ready); best->waitId = 0; } } @@ -492,7 +516,7 @@ namespace ps2x::iop::detail thread.cpu.gpr[2] = 0u; if ((thread.waitMode & 0x10u) != 0u) event.bits = 0u; - thread.state = IopThreadState::Ready; + setThreadState(thread, IopThreadState::Ready); thread.waitId = 0; thread.waitBits = 0; thread.waitMode = 0; @@ -553,7 +577,7 @@ namespace ps2x::iop::detail { if (thread.state == IopThreadState::EventFlag && thread.waitId == id) { - thread.state = IopThreadState::Ready; + setThreadState(thread, IopThreadState::Ready); thread.waitId = 0; thread.cpu.gpr[2] = static_cast(-1); } @@ -609,7 +633,7 @@ namespace ps2x::iop::detail setV0(-418); else if (m_currentThread != nullptr) { - m_currentThread->state = IopThreadState::EventFlag; + setThreadState(*m_currentThread, IopThreadState::EventFlag); m_currentThread->waitId = event->second.id; m_currentThread->waitBits = bits; m_currentThread->waitMode = mode; @@ -656,7 +680,7 @@ namespace ps2x::iop::detail { if (m_currentThread == nullptr) return; - m_currentThread->state = IopThreadState::Sleep; + setThreadState(*m_currentThread, IopThreadState::Sleep); cpu.yielded = true; } @@ -665,27 +689,33 @@ namespace ps2x::iop::detail if (m_currentThread == nullptr) return; m_currentThread->wakeCycle = wakeCycle; - m_currentThread->state = IopThreadState::Delay; + setThreadState(*m_currentThread, IopThreadState::Delay); + m_scheduleDirty = true; cpu.yielded = true; } IopThread *IopKernel::beginNextReady(uint64_t currentCycle) { - for (auto &[id, thread] : m_threads) + if (m_scheduleDirty || currentCycle >= m_nextWakeCycle) { - if (thread.state == IopThreadState::Delay && thread.wakeCycle <= currentCycle) - thread.state = IopThreadState::Ready; - } - - IopThread *next = nullptr; - for (auto &[id, thread] : m_threads) - { - if (thread.state != IopThreadState::Ready) - continue; - if (next == nullptr || thread.priority < next->priority || - (thread.priority == next->priority && thread.id < next->id)) - next = &thread; + m_nextReady = nullptr; + m_nextWakeCycle = UINT64_MAX; + for (auto &[id, thread] : m_threads) + { + if (thread.state == IopThreadState::Delay) + { + if (thread.wakeCycle <= currentCycle) + thread.state = IopThreadState::Ready; + else + m_nextWakeCycle = std::min(m_nextWakeCycle, thread.wakeCycle); + } + if (thread.state == IopThreadState::Ready && (m_nextReady == nullptr || thread.priority < m_nextReady->priority || + (thread.priority == m_nextReady->priority && thread.id < m_nextReady->id))) + m_nextReady = &thread; + } + m_scheduleDirty = false; } + IopThread *next = m_nextReady; if (next == nullptr) return nullptr; @@ -698,6 +728,8 @@ namespace ps2x::iop::detail uint64_t IopKernel::nextWakeCycle(uint64_t fallback) const { + if (!m_scheduleDirty) + return std::min(fallback, m_nextWakeCycle); uint64_t nextWake = fallback; for (const auto &[id, thread] : m_threads) { @@ -709,8 +741,8 @@ namespace ps2x::iop::detail void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel) { - if (thread.cpu.pc == returnSentinel || thread.cpu.stopped) - thread.state = IopThreadState::Dormant; + if (thread.state != IopThreadState::Dead && (thread.cpu.pc == returnSentinel || thread.cpu.stopped)) + setThreadState(thread, IopThreadState::Dormant); else if (thread.state == IopThreadState::Running) thread.state = IopThreadState::Ready; m_currentThread = nullptr; @@ -719,6 +751,9 @@ namespace ps2x::iop::detail void IopKernel::cleanupDeadThreads() { + if (!m_hasDeadThreads) + return; + m_hasDeadThreads = false; for (auto thread = m_threads.begin(); thread != m_threads.end();) { if (thread->second.state != IopThreadState::Dead) @@ -726,6 +761,14 @@ namespace ps2x::iop::detail ++thread; continue; } + if (&thread->second == m_currentThread) + { + thread->second.cpu.stopped = thread->second.cpu.yielded = true; + m_hasDeadThreads = true; + ++thread; + continue; + } + m_scheduleDirty = true; if (thread->second.stackBase != 0u) (void)m_memory.freeAllocation(thread->second.stackBase); thread = m_threads.erase(thread); @@ -738,7 +781,7 @@ namespace ps2x::iop::detail { const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc); if (pc >= base && pc < base + size) - thread.state = IopThreadState::Dead; + setThreadState(thread, IopThreadState::Dead); } } } diff --git a/ps2xIOP/src/emulator/core/iop_kernel.h b/ps2xIOP/src/emulator/core/iop_kernel.h index 497710244..2482fb05e 100644 --- a/ps2xIOP/src/emulator/core/iop_kernel.h +++ b/ps2xIOP/src/emulator/core/iop_kernel.h @@ -5,6 +5,7 @@ #include #include #include +#include namespace ps2x::iop::detail { @@ -90,14 +91,19 @@ namespace ps2x::iop::detail void wakeOneSemaphore(int id); [[nodiscard]] static bool eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode); void wakeEventWaiters(EventFlag &event); + void setThreadState(IopThread &thread, IopThreadState state); IopMemory &m_memory; std::map m_threads; - std::map m_semaphores; - std::map m_eventFlags; + std::unordered_map m_semaphores; + std::unordered_map m_eventFlags; uint32_t m_nextThreadId = 1; uint32_t m_nextSemaphoreId = 1; uint32_t m_nextEventFlagId = 1; IopThread *m_currentThread = nullptr; + IopThread *m_nextReady = nullptr; + uint64_t m_nextWakeCycle = UINT64_MAX; + bool m_scheduleDirty = true; + bool m_hasDeadThreads = false; }; } diff --git a/ps2xIOP/src/emulator/core/iop_memory.cpp b/ps2xIOP/src/emulator/core/iop_memory.cpp index 25e9b36d1..de1b4f1f3 100644 --- a/ps2xIOP/src/emulator/core/iop_memory.cpp +++ b/ps2xIOP/src/emulator/core/iop_memory.cpp @@ -96,7 +96,7 @@ namespace ps2x::iop::detail const uint32_t phys = physicalAddress(address); if (phys < RamSize) { - m_ram[phys] = value; + std::memcpy(m_ram.data() + phys, &value, sizeof(value)); markOwned(phys, sizeof(value)); return; } @@ -167,7 +167,7 @@ namespace ps2x::iop::detail return false; if (size != 0u) { - std::memcpy(m_ram.data() + phys, source, size); + std::memmove(m_ram.data() + phys, source, size); markOwned(phys, size); } return true; diff --git a/ps2xIOP/src/emulator/core/iop_memory.h b/ps2xIOP/src/emulator/core/iop_memory.h index a932a5d87..a9f8cccff 100644 --- a/ps2xIOP/src/emulator/core/iop_memory.h +++ b/ps2xIOP/src/emulator/core/iop_memory.h @@ -67,6 +67,7 @@ namespace ps2x::iop::detail void setInterruptMask(uint32_t value) noexcept { m_interruptMask = value; } void setInterruptControl(uint32_t value) noexcept { m_interruptControl = value & 1u; } + [[nodiscard]] bool hasDmaStart() const noexcept { return m_dmaStart.has_value(); } [[nodiscard]] std::optional takeDmaStart() noexcept; [[nodiscard]] std::span ram() const noexcept { return m_ram; } diff --git a/ps2xIOP/src/emulator/imports/iop_imports.cpp b/ps2xIOP/src/emulator/imports/iop_imports.cpp index f13267fe1..b019ec48e 100644 --- a/ps2xIOP/src/emulator/imports/iop_imports.cpp +++ b/ps2xIOP/src/emulator/imports/iop_imports.cpp @@ -47,8 +47,11 @@ namespace ps2x::iop::detail std::optional IopImportRegistry::decode(uint32_t pc) const { - if (m_memory.read32(pc) != 0x03E00008u) - return std::nullopt; + return decode(pc, m_memory.read32(pc)); + } + + std::optional IopImportRegistry::decodeStub(uint32_t pc) const + { const uint32_t delay = m_memory.read32(pc + 4u); if ((delay & 0xFFFF0000u) != 0x24000000u) return std::nullopt; diff --git a/ps2xIOP/src/emulator/imports/iop_imports.h b/ps2xIOP/src/emulator/imports/iop_imports.h index cae8d91a0..c2eba0992 100644 --- a/ps2xIOP/src/emulator/imports/iop_imports.h +++ b/ps2xIOP/src/emulator/imports/iop_imports.h @@ -26,6 +26,12 @@ namespace ps2x::iop::detail void reset(); [[nodiscard]] std::optional decode(uint32_t pc) const; + [[nodiscard]] std::optional decode(uint32_t pc, uint32_t instruction) const + { + if (instruction != 0x03E00008u) // Import stubs begin with jr ra. + return std::nullopt; + return decodeStub(pc); + } [[nodiscard]] bool registerExportTable(uint32_t address); [[nodiscard]] bool releaseExportTable(uint32_t address); [[nodiscard]] uint32_t findTable(std::string_view library, std::optional version = std::nullopt) const; @@ -34,6 +40,7 @@ namespace ps2x::iop::detail void eraseRange(uint32_t base, uint32_t size); private: + [[nodiscard]] std::optional decodeStub(uint32_t pc) const; struct ExportLibrary { uint32_t tableAddress = 0; diff --git a/ps2xIOP/src/emulator/imports/iop_timrman.cpp b/ps2xIOP/src/emulator/imports/iop_timrman.cpp index 3d80897c2..0a6de4b6a 100644 --- a/ps2xIOP/src/emulator/imports/iop_timrman.cpp +++ b/ps2xIOP/src/emulator/imports/iop_timrman.cpp @@ -56,6 +56,7 @@ namespace ps2x::iop::detail } m_holdMode = 0u; m_servicing = false; + m_deadlineDirty = true; } uint32_t IopTimrman::timerId(size_t index) noexcept @@ -155,6 +156,7 @@ namespace ps2x::iop::detail bool IopTimrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle) { + m_deadlineDirty = true; const uint32_t a0 = cpu.gpr[4]; const uint32_t a1 = cpu.gpr[5]; const uint32_t a2 = cpu.gpr[6]; @@ -416,9 +418,10 @@ namespace ps2x::iop::detail void IopTimrman::serviceDue(uint64_t currentCycle, IopGuestExecutor &executor) { - if (m_servicing) + if (m_servicing || nextEventCycle(UINT64_MAX) > currentCycle) return; m_servicing = true; + m_deadlineDirty = true; struct ServiceGuard { bool &flag; @@ -465,12 +468,18 @@ namespace ps2x::iop::detail uint64_t IopTimrman::nextEventCycle(uint64_t fallback) const noexcept { - uint64_t next = fallback; - for (const Timer &timer : m_timers) + if (m_servicing) + return fallback; + if (m_deadlineDirty) { - next = std::min(next, timer.compareCycle); - next = std::min(next, timer.overflowCycle); + m_nextEventCycle = UINT64_MAX; + for (const Timer &timer : m_timers) + { + m_nextEventCycle = std::min(m_nextEventCycle, timer.compareCycle); + m_nextEventCycle = std::min(m_nextEventCycle, timer.overflowCycle); + } + m_deadlineDirty = false; } - return next; + return std::min(fallback, m_nextEventCycle); } } diff --git a/ps2xIOP/src/emulator/imports/iop_timrman.h b/ps2xIOP/src/emulator/imports/iop_timrman.h index 11b53b602..1c2fb7ca2 100644 --- a/ps2xIOP/src/emulator/imports/iop_timrman.h +++ b/ps2xIOP/src/emulator/imports/iop_timrman.h @@ -63,5 +63,7 @@ namespace ps2x::iop::detail std::array m_timers{}; uint32_t m_holdMode = 0u; bool m_servicing = false; + mutable bool m_deadlineDirty = true; + mutable uint64_t m_nextEventCycle = UINT64_MAX; }; } diff --git a/ps2xIOP/src/emulator/iop_emulator.cpp b/ps2xIOP/src/emulator/iop_emulator.cpp index e5f63a9fe..f0e8643de 100644 --- a/ps2xIOP/src/emulator/iop_emulator.cpp +++ b/ps2xIOP/src/emulator/iop_emulator.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include namespace ps2x::iop::detail { @@ -125,6 +127,7 @@ namespace ps2x::iop::detail timrman.reset(); ioman.reset(); pendingDmaInterrupts.clear(); + nextDmaInterruptCycle = UINT64_MAX; pendingGuestCallbacks.clear(); nextModuleId = 1; moduleCursor = kModuleLoadBase; @@ -132,7 +135,7 @@ namespace ps2x::iop::detail totalInstructions = 0; eeCycleCarry = 0; activeCpu = nullptr; - lastError.clear(); + executionFault = {}; servicingDmaInterrupts = false; servicingGuestCallbacks = false; callDepth = 0u; @@ -175,9 +178,19 @@ namespace ps2x::iop::detail } void schedulePendingDma() + { + schedulePendingDma(totalCycles); + } + + void schedulePendingDma(uint64_t issueCycle) { if (const auto dma = memory.takeDmaStart()) - pendingDmaInterrupts[dma->irq] = totalCycles + dma->delayCycles; + { + pendingDmaInterrupts[dma->irq] = issueCycle + dma->delayCycles; + nextDmaInterruptCycle = UINT64_MAX; + for (const auto &[irq, cycle] : pendingDmaInterrupts) + nextDmaInterruptCycle = std::min(nextDmaInterruptCycle, cycle); + } } bool readRam(uint32_t address, void *destination, size_t size) const @@ -226,7 +239,7 @@ namespace ps2x::iop::detail if (!pending) return false; cpu.cop0[13] |= 0x400u; - cpuCore.raiseException(cpu, 0u, cpu.pc, false); + cpuCore.raiseException(cpu, 0u, cpu.pc, cpu.branchPending); return true; } @@ -358,6 +371,15 @@ namespace ps2x::iop::detail return ImportDisposition::Missing; } + void fail(uint32_t pc, const char *reason) noexcept + { + if (executionFault.active) + return; + executionFault.active = true; + executionFault.pc = pc; + std::snprintf(executionFault.reason.data(), executionFault.reason.size(), "%s", reason); + } + bool step(CpuState &cpu) { if (cpu.stopped) @@ -369,16 +391,23 @@ namespace ps2x::iop::detail } if (physicalAddress(cpu.pc) >= kRamSize) { - std::ostringstream out; - out << "[IOP] execution outside RAM pc=0x" << std::hex << cpu.pc; - log(LogLevel::Error, out.str()); + fail(cpu.pc, "Instruction fetch outside IOP RAM"); cpu.stopped = true; return false; } if (checkInterrupt(cpu)) return true; - if (const auto import = imports.decode(cpu.pc)) + if (cpu.pc & 3u) + { + cpuCore.raiseException(cpu, 4u, cpu.pc, cpu.branchPending, cpu.pc); + ++totalInstructions; + ++totalCycles; + return true; + } + + const uint32_t instruction = memory.read32(cpu.pc); + if (const auto import = imports.decode(cpu.pc, instruction)) { const ImportDisposition disposition = dispatchImport(*import, cpu); ++totalInstructions; @@ -390,28 +419,47 @@ namespace ps2x::iop::detail return !cpu.stopped; } - const bool running = cpuCore.executeInstruction(cpu); - schedulePendingDma(); + const bool executed = cpuCore.executeInstruction(cpu, instruction); ++totalInstructions; ++totalCycles; - return running; + if (memory.hasDmaStart()) + schedulePendingDma(totalCycles - 1); + if (!executed) return false; + return !cpu.stopped; } uint32_t runCpu(CpuState &cpu, uint32_t instructionBudget) { - CpuState *previous = activeCpu; + struct ActiveGuard + { + CpuState *&slot; + CpuState *previous; + ~ActiveGuard() { slot = previous; } + } guard{activeCpu, activeCpu}; activeCpu = &cpu; const uint64_t start = totalInstructions; - while (!cpu.stopped && !cpu.yielded && totalInstructions - start < instructionBudget) + try { - if (!step(cpu)) - break; - if (!servicingDmaInterrupts && !pendingDmaInterrupts.empty()) + while (!executionFault.active && !cpu.stopped && !cpu.yielded && totalInstructions - start < instructionBudget) + { servicePendingDmaInterrupts(); - if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty()) servicePendingGuestCallbacks(); + timrman.serviceDue(totalCycles, *this); + const auto consumed = totalInstructions - start; + if (consumed >= instructionBudget || executionFault.active) + break; + if (!step(cpu)) + break; + } + } + catch (const std::exception &error) + { + fail(cpu.pc, error.what()); + } + catch (...) + { + fail(cpu.pc, "Unknown IOP execution failure"); } - activeCpu = previous; return static_cast(totalInstructions - start); } @@ -483,16 +531,18 @@ namespace ps2x::iop::detail // Not that good to use exception handling for control flow but will do for now void servicePendingDmaInterrupts() { - if (servicingDmaInterrupts || pendingDmaInterrupts.empty()) + if (servicingDmaInterrupts || totalCycles < nextDmaInterruptCycle) return; servicingDmaInterrupts = true; std::vector completed; + nextDmaInterruptCycle = UINT64_MAX; for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();) { if (it->second > totalCycles) { + nextDmaInterruptCycle = std::min(nextDmaInterruptCycle, it->second); ++it; continue; } @@ -558,7 +608,7 @@ namespace ps2x::iop::detail try { const uint64_t target = totalCycles + cycles; - while (totalCycles < target) + while (!executionFault.active && totalCycles < target) { servicePendingDmaInterrupts(); servicePendingGuestCallbacks(); @@ -567,8 +617,7 @@ namespace ps2x::iop::detail if (!next) { uint64_t nextWake = kernel.nextWakeCycle(target); - for (const auto &[irq, completionCycle] : pendingDmaInterrupts) - nextWake = std::min(nextWake, completionCycle); + nextWake = std::min(nextWake, nextDmaInterruptCycle); if (!pendingGuestCallbacks.empty()) nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first); nextWake = timrman.nextEventCycle(nextWake); @@ -582,9 +631,13 @@ namespace ps2x::iop::detail ++totalCycles; } } + catch (const std::exception &error) + { + fail(activeCpu ? activeCpu->pc : 0u, error.what()); + } catch (...) { - // Runtime scheduling must never throw through EeScheduler::accountCycles(). + fail(activeCpu ? activeCpu->pc : 0u, "Unknown IOP scheduling failure"); } } @@ -696,6 +749,7 @@ namespace ps2x::iop::detail IopLoadcore loadcore; std::map modules; std::map pendingDmaInterrupts; + uint64_t nextDmaInterruptCycle = UINT64_MAX; std::multimap pendingGuestCallbacks; uint32_t nextModuleId = 1; uint32_t moduleCursor = kModuleLoadBase; @@ -703,7 +757,7 @@ namespace ps2x::iop::detail uint64_t totalInstructions = 0; uint64_t eeCycleCarry = 0; CpuState *activeCpu = nullptr; - std::string lastError; + ExecutionFault executionFault; bool servicingDmaInterrupts = false; bool servicingGuestCallbacks = false; uint32_t callDepth = 0u; @@ -748,6 +802,11 @@ namespace ps2x::iop::detail m_impl->runCycles(iopCycles); } + const ExecutionFault &IopEmulator::fault() const noexcept + { + return m_impl->executionFault; + } + RpcResult IopEmulator::handleRpc(const RpcRequest &request) { return m_impl->rpc.handleRpc(request, *m_impl); diff --git a/ps2xIOP/src/emulator/iop_emulator.h b/ps2xIOP/src/emulator/iop_emulator.h index 01b48df57..db764ab09 100644 --- a/ps2xIOP/src/emulator/iop_emulator.h +++ b/ps2xIOP/src/emulator/iop_emulator.h @@ -25,6 +25,7 @@ namespace ps2x::iop::detail [[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize); [[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result); void runEeCycles(uint64_t eeCycles) noexcept; + [[nodiscard]] const ExecutionFault &fault() const noexcept; [[nodiscard]] RpcResult handleRpc(const RpcRequest &request); [[nodiscard]] bool hasRpcServer(uint32_t sid) const noexcept; void onSifTransfer(const SifTransfer &transfer); diff --git a/ps2xIOP/src/iop_subsystem.cpp b/ps2xIOP/src/iop_subsystem.cpp index b49815540..e7f7ac49b 100644 --- a/ps2xIOP/src/iop_subsystem.cpp +++ b/ps2xIOP/src/iop_subsystem.cpp @@ -169,6 +169,11 @@ namespace ps2x::iop m_impl->emulator.runEeCycles(eeCycles); } + const ExecutionFault &IopSubsystem::fault() const noexcept + { + return m_impl->emulator.fault(); + } + RpcAbi IopSubsystem::selectRpcAbi(const RpcAbiRequest &request) const { for (const auto &service : m_impl->coreServices) diff --git a/ps2xIOP/tests/iop_execution_tests.cpp b/ps2xIOP/tests/iop_execution_tests.cpp new file mode 100644 index 000000000..f0da22a90 --- /dev/null +++ b/ps2xIOP/tests/iop_execution_tests.cpp @@ -0,0 +1,261 @@ +#include "iop_compat_test_support.h" +#include "emulator/iop_emulator.h" +#include "emulator/core/iop_cpu.h" +#include "emulator/core/iop_memory.h" +#include "emulator/imports/iop_imports.h" + +namespace +{ + using namespace iop_test; + using namespace ps2x::iop::detail; + + // Real guest stores start SPU DMA; intrman invokes the guest handler below. + struct DmaFixture + { + Host host; + IopEmulator iop{host}; + Irx image{0x10000, 0x900}; + uint32_t offset = 0; + std::array starts{}; + std::array delays{}; + std::array deadlines{}; + + DmaFixture() + { + emit({0x27bdfff0, 0xafbf000c}); // save ra + for (unsigned irq : {0x24u, 0x28u}) + { + emit({0x24040000 | irq, 0x24050000, 0x3c060001, 0x34c60300, + 0x24070000 | irq, 0x0c004185, 0}); // RegisterIntrHandler + emit({0x24040000 | irq, 0x0c004187, 0}); // EnableIntr + } + image.words(0x600, {0x41e00000, 0, 0x0101, 0x72746e69, 0x006e616d, + 0x03e00008, 0x24000004, 0x03e00008, 0x24000006, 0, 0}); + image.words(0x300, { + 0x3c080001, 0x8d090800, 0, // t1 = completed count (load delay) + 0x00095080, 0x01485021, 0xad440810, // trace[count] = irq + 0x25290001, 0xad090800, + 0x8d090804, 0, 0x1120000c, 0, // optionally rearm DMA from the handler + 0xad000804, 0x3c091f80, 0x352910c0, + 0x240a0020, 0xad2a0004, 0x3c0a0100, 0xad2a0008, + 0x24090050, 0x2529ffff, 0x1520fffe, 0, // handler runs past the new deadline + 0x03e00008, 0, + }); + } + + void emit(std::initializer_list words) + { + image.words(offset, words); + offset += uint32_t(words.size()) * 4; + } + + void start(unsigned channel, uint32_t delay) + { + emit({0x3c081f80, channel ? 0x35081500u : 0x350810c0u, + 0x24090000 | (delay / 2), 0xad090004, 0x3c090100}); + starts[channel] = offset; + delays[channel] = delay; + emit({0xad090008}); + } + + void load() + { + const std::initializer_list epilogue{0x8fbf000c, 0x27bd0010, 0x00001021, 0x03e00008, 0}; + image.words(offset, epilogue); + const uint32_t entryEnd = offset + uint32_t(epilogue.size()) * 4; + image.install(host); + const auto result = iop.loadModuleBuffer(0x1000, nullptr, 0); + require(result.moduleId > 0 && result.startResult == 0, "DMA IRX initialization failed"); + for (unsigned channel = 0; channel < 2; ++channel) + if (delays[channel]) + deadlines[channel] = iop.cycles() - (entryEnd - starts[channel]) / 4 + delays[channel]; + } + + uint32_t word(uint32_t address) + { + uint32_t value = 0; + require(iop.readMemory(address, &value, sizeof(value)), "IOP trace read failed"); + return value; + } + + void advanceTo(uint64_t cycle) + { + require(cycle >= iop.cycles(), "test attempted to reverse time"); + iop.runEeCycles((cycle - iop.cycles()) * 8); + } + }; + + void dmaDeadlines() + { + DmaFixture f; + f.start(0, 1000); + f.start(1, 200); + f.load(); + f.advanceTo(f.deadlines[1]); + require(f.word(0x10800) == 0, "idle DMA dispatched before its deadline was serviced"); + f.iop.runEeCycles(8); + require(f.word(0x10800) == 1 && f.word(0x10810) == 0x28, "earliest DMA did not wake the idle IOP"); + f.advanceTo(f.deadlines[0]); + require(f.word(0x10800) == 1, "later DMA dispatched early"); + f.iop.runEeCycles(8); + require(f.word(0x10800) == 2 && f.word(0x10814) == 0x24, "later DMA was lost"); + f.iop.runEeCycles(8000); + require(f.word(0x10800) == 2, "DMA completion dispatched twice"); + } + + void dmaReplacementAndReset() + { + DmaFixture f; + f.start(0, 200); + f.start(1, 400); + f.start(0, 1000); // replace the earliest event with a later one + f.load(); + f.advanceTo(f.deadlines[1]); + require(f.word(0x10800) == 0, "replaced DMA deadline survived"); + f.iop.runEeCycles(8); + require(f.word(0x10800) == 1 && f.word(0x10810) == 0x28, "replacement hid the other channel"); + f.iop.reset(); + f.iop.runEeCycles(16000); + require(f.iop.instructions() == 0, "reset retained a DMA callback"); + f.load(); + f.advanceTo(f.deadlines[1]); + f.iop.runEeCycles(8); + require(f.word(0x10800) == 1, "DMA scheduling did not recover after reset"); + } + + void dmaReentrantHandler() + { + DmaFixture f; + f.start(0, 200); + f.image.words(0x804, {1}); + f.load(); + f.advanceTo(f.deadlines[0]); + f.iop.runEeCycles(8); + require(f.word(0x10800) == 1, "DMA handler recursively dispatched a new completion"); + f.iop.runEeCycles(8); + require(f.word(0x10800) == 2 && f.word(0x10814) == 0x24, + "DMA started inside its handler was lost"); + } + + void dmaEqualDeadlines() + { + DmaFixture f; + f.start(1, 206); + f.start(0, 200); // Six instructions later: same completion cycle. + f.load(); + require(f.deadlines[0] == f.deadlines[1], "fixture deadlines differ"); + f.advanceTo(f.deadlines[0]); + f.iop.runEeCycles(8); + require(f.word(0x10800) == 2 && f.word(0x10810) == 0x24 && f.word(0x10814) == 0x28, + "simultaneous DMA callbacks changed IRQ order"); + } + + void dmaWhileExecuting() + { + DmaFixture f; + f.start(0, 200); + // Keep guest execution active beyond completion; no idle path is involved. + f.emit({0x24100050, 0x2610ffff, 0x1600fffe, 0}); + f.load(); + require(f.word(0x10800) == 1 && f.word(0x10810) == 0x24, + "DMA did not dispatch during guest execution"); + } + + void instructionChanges() + { + IopMemory memory; + IopCpuCore core(memory); + IopImportRegistry imports(memory); + IopCpuState cpu{}; + const auto step = [&]() + { + const uint32_t instruction = memory.read32(cpu.pc); + require(!imports.decode(cpu.pc, instruction), "ordinary instruction mistaken for an import"); + return core.executeInstruction(cpu, instruction); + }; + cpu.pc = 0x80001000; + memory.write32(0x1000, 0x24020011); // addiu v0, zero, 0x11 + require(step() && cpu.gpr[2] == 0x11, "first instruction failed"); + cpu.pc = 0x80001000; + memory.write32(0x1000, 0x24020022); + require(step() && cpu.gpr[2] == 0x22, "modified instruction was not refetched"); + cpu.pc = 0x1000; + memory.write32(0x1000, 0x8c020800); // lw v0, 0x800(zero) + memory.write32(0x1004, 0x24430001); // addiu v1, v0, 1 (sees old v0) + memory.write32(0x800, 0x44); + require(step() && cpu.gpr[2] == 0x22, "load delay changed"); + require(step() && cpu.gpr[2] == 0x44 && cpu.gpr[3] == 0x23, + "instruction sharing changed load delay semantics"); + cpu.pc = 0x1000; + memory.write32(0x1000, 0x10000001); // beq zero, zero, +1 + memory.write32(0x1004, 0x24020033); + require(step() && cpu.pc == 0x1004 && cpu.branchPending, "branch delay slot was skipped"); + require(step() && cpu.pc == 0x1008 && cpu.gpr[2] == 0x33, "branch delay slot did not execute"); + cpu = {}; + cpu.pc = 0x1000; + memory.write32(0x1000, 0x14200001); + memory.write32(0x1004, 0x8c020001); + require(step() && cpu.branchPending, "untaken branch must retain its delay slot"); + cpu.pendingLoad = true; + cpu.pendingLoadReg = 9; + cpu.pendingLoadValue = 55; + require(step() && cpu.exception && cpu.cop0[14] == 0x1000 && + (cpu.cop0[13] & 0x8000007c) == 0x80000010 && cpu.cop0[8] == 1 && + cpu.gpr[9] == 55 && !cpu.pendingLoad, + "delay-slot address exception must preserve EPC, BD and the prior load"); + } + + void modifiedImportStub() + { + IopMemory memory; + IopImportRegistry imports(memory); + memory.write32(0x1000, 0x41e00000); + memory.write16(0x1008, 0x0101); + constexpr char name[8] = "tstlib"; + require(memory.writeRam(0x100c, name, sizeof(name)), "import name write failed"); + memory.write32(0x1014, 0x03e00008); + memory.write32(0x1018, 0x24000003); + const auto decode = [&]() { return imports.decode(0x80001014, memory.read32(0x80001014)); }; + auto call = decode(); + require(call && call->ordinal == 3 && call->version == 0x0101, "shared fetch lost import metadata"); + memory.write16(0x1008, 0x0102); + memory.write32(0x1018, 0x24000007); + call = decode(); + require(call && call->ordinal == 7 && call->version == 0x0102, "import metadata became stale"); + memory.write32(0x1014, 0x24020011); + require(!decode(), "patched import stub still dispatched as an import"); + } + + void executionFault() + { + Host host; + IopEmulator iop(host); + Irx image{0x10000, 0x100}; + image.words(0, {0x3c080100, 0x01000008, 0}); + image.install(host); + (void)iop.loadModuleBuffer(0x1000, nullptr, 0); + require(iop.fault().active && iop.fault().pc == 0x01000000, + "Invalid execution must expose a persistent fault with the guest PC"); + require(iop.fault().reason[0] != 0, "Execution faults must retain a diagnostic reason"); + const auto before = iop.instructions(); + iop.runEeCycles(100000); + require(iop.instructions() == before, "A faulted IOP must stop executing guest instructions"); + iop.reset(); + require(!iop.fault().active, "Reset must clear the explicit execution fault"); + } +} + +int main() +{ + const Test tests[] = { + {"DMA deadlines and idle wakeup", dmaDeadlines}, + {"DMA replacement and reset", dmaReplacementAndReset}, + {"DMA scheduled from a running IRQ handler", dmaReentrantHandler}, + {"Simultaneous DMA IRQ order", dmaEqualDeadlines}, + {"DMA dispatch while guest instructions execute", dmaWhileExecuting}, + {"Modified instructions and load delay", instructionChanges}, + {"Modified import stub and metadata", modifiedImportStub}, + {"Explicit execution fault and reset", executionFault}, + }; + return run(tests); +} diff --git a/ps2xIOP/tests/iop_import_tests.cpp b/ps2xIOP/tests/iop_import_tests.cpp index 303561844..b35798227 100644 --- a/ps2xIOP/tests/iop_import_tests.cpp +++ b/ps2xIOP/tests/iop_import_tests.cpp @@ -274,6 +274,128 @@ namespace return passed; } + bool testKernelSchedulingTransitions() + { + IopMemory memory; + IopKernel kernel(memory); + IopCpuState caller{}; + const auto createThread = [&](uint32_t entry, uint32_t priority) + { + memory.write32(0x2008, entry); + memory.write32(0x200c, 0x100); + memory.write32(0x2010, priority); + caller.gpr[4] = 0x2000; + (void)kernel.dispatchThreadImport(4, caller, 0); + const int id = static_cast(caller.gpr[2]); + caller.gpr[4] = static_cast(id); + (void)kernel.dispatchThreadImport(6, caller, 0); + return id; + }; + const auto select = [&](int id, uint64_t cycle) + { + IopThread *thread = kernel.beginNextReady(cycle); + if (!expect(thread && thread->id == id, "scheduler selected the wrong thread")) + return static_cast(nullptr); + return thread; + }; + const auto threadImport = [&](uint16_t ordinal, int id, uint32_t value = 0) + { + caller.gpr[4] = static_cast(id); + caller.gpr[5] = value; + return kernel.dispatchThreadImport(ordinal, caller, 0) && caller.gpr[2] == 0; + }; + constexpr uint32_t sentinel = 0xfffffffcu; + const int first = createThread(0x10000, 32); + const int second = createThread(0x20000, 64); + for (int i = 0; i < 128; ++i) + { + auto *thread = select(first, 0); + if (!thread) return false; + kernel.endTimeslice(*thread, sentinel); + } + if (!threadImport(14, second, 16)) return false; + auto *thread = select(second, 0); + if (!thread) return false; + kernel.delayCurrentUntil(100, thread->cpu); + kernel.endTimeslice(*thread, sentinel); + if (!expect(kernel.nextWakeCycle(1000) == 100, "delay deadline lost after blocking")) return false; + thread = select(first, 99); + if (!thread) return false; + kernel.endTimeslice(*thread, sentinel); + thread = select(second, 100); + if (!thread) return false; + kernel.sleepCurrent(thread->cpu); + kernel.endTimeslice(*thread, sentinel); + thread = select(first, 101); + if (!thread) return false; + kernel.endTimeslice(*thread, sentinel); + if (!threadImport(25, second)) return false; + thread = select(second, 101); + if (!thread) return false; + kernel.endTimeslice(*thread, sentinel); + if (!threadImport(29, second)) return false; + thread = select(first, 101); + if (!thread) return false; + kernel.endTimeslice(*thread, sentinel); + if (!threadImport(31, second)) return false; + + memory.write32(0x202c, 1); // semaphore maximum, initially empty + caller.gpr[4] = 0x2020; + (void)kernel.dispatchSemaphoreImport(4, caller); + const uint32_t semaphore = caller.gpr[2]; + thread = select(second, 101); + if (!thread) return false; + thread->cpu.gpr[4] = semaphore; + (void)kernel.dispatchSemaphoreImport(8, thread->cpu); + kernel.endTimeslice(*thread, sentinel); + thread = select(first, 101); + if (!thread) return false; + kernel.endTimeslice(*thread, sentinel); + caller.gpr[4] = semaphore; + (void)kernel.dispatchSemaphoreImport(6, caller); + thread = select(second, 101); + if (!thread) return false; + + const int event = kernel.createInternalEventFlag(0, 0, 0); + thread->cpu.gpr[4] = static_cast(event); + thread->cpu.gpr[5] = 1; + thread->cpu.gpr[6] = 0; + thread->cpu.gpr[7] = 0; + (void)kernel.dispatchEventImport(10, thread->cpu); + kernel.endTimeslice(*thread, sentinel); + thread = select(first, 101); + if (!thread) return false; + kernel.endTimeslice(*thread, sentinel); + if (!kernel.setInternalEventFlag(event, 1)) return false; + const int third = createThread(0x30000, 16); + thread = select(second, 101); // Equal priorities retain ascending ID order. + if (!thread) return false; + (void)kernel.dispatchThreadImport(9, thread->cpu, 101); + kernel.endTimeslice(*thread, sentinel); + if (!expect(kernel.threadCount() == 2, "ExitDeleteThread must not turn Dead into Dormant")) return false; + thread = select(third, 101); + if (!thread) return false; + kernel.terminateThreadsInRange(0x30000, 0x100); + kernel.cleanupDeadThreads(); + if (!expect(kernel.threadCount() == 2 && thread->cpu.stopped, "active CPU reference must survive module unload")) return false; + kernel.endTimeslice(*thread, sentinel); + if (!expect(kernel.threadCount() == 1, "unloaded thread must retire after its timeslice")) return false; + thread = select(first, 101); + if (!thread) return false; + kernel.delayCurrentUntil(1000, thread->cpu); + kernel.endTimeslice(*thread, sentinel); + if (!expect(kernel.beginNextReady(999) == nullptr && kernel.nextWakeCycle(2000) == 1000, + "idle scheduling must preserve the next wake deadline")) return false; + thread = select(first, 1000); + if (!thread) return false; + kernel.endTimeslice(*thread, sentinel); + if (!threadImport(5, first)) return false; + if (!expect(kernel.beginNextReady(1000) == nullptr, "deletion must invalidate the cached ready thread")) return false; + kernel.reset(); + return expect(kernel.beginNextReady(0) == nullptr && kernel.threadCount() == 0, + "reset must discard scheduling state"); + } + bool testTimrmanPeriodicCallback() { IopTimrman timrman; @@ -320,18 +442,38 @@ namespace if (!expect(executor.calls == 0u, "timer callback ran too early")) return false; timrman.serviceDue(200u, executor); - return expect(executor.calls == 1u, "timer callback did not run") && + if (!(expect(executor.calls == 1u, "timer callback did not run") && expect(executor.lastAddress == 0x12340u, "timer called the wrong handler") && expect(executor.lastArgument == 0x45670u, "timer passed the wrong common argument") && expect(executor.lastGp == 0x89AB0u, "timer callback lost the registering module GP") && - expect(timrman.nextEventCycle(1000u) == 300u, "timer callback return did not rearm compare"); + expect(timrman.nextEventCycle(1000u) == 300u, "timer callback return did not rearm compare"))) + return false; + cpu.gpr[4] = timerId; + cpu.gpr[5] = 80u; + if (!timrman.dispatchImport(9u, cpu, 210u) || + !expect(timrman.nextEventCycle(1000u) == 230u, "counter write did not shorten timer deadline") || + !expect(timrman.nextEventCycle(220u) == 220u, "timer deadline exceeded caller budget")) + return false; + timrman.serviceDue(229u, executor); + if (!expect(executor.calls == 1u, "rescheduled timer callback ran early")) return false; + timrman.serviceDue(230u, executor); + if (!expect(executor.calls == 2u && timrman.nextEventCycle(1000u) == 330u, + "rescheduled timer did not fire and rearm")) return false; + if (!timrman.dispatchImport(24u, cpu, 240u) || + !expect(timrman.nextEventCycle(1000u) == 1000u, "stopped timer retained its deadline")) + return false; + if (!timrman.dispatchImport(23u, cpu, 250u) || + !expect(timrman.nextEventCycle(1000u) == 350u, "restarted timer retained its old deadline")) + return false; + timrman.reset(); + return expect(timrman.nextEventCycle(1000u) == 1000u, "reset retained timer deadline"); } } int main() { if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() || - !testTimrmanPeriodicCallback()) + !testTimrmanPeriodicCallback() || !testKernelSchedulingTransitions()) return 1; std::cout << "ps2xIOP import tests passed\n"; return 0; diff --git a/ps2xRecomp/include/ps2recomp/Translators/vu_translator.h b/ps2xRecomp/include/ps2recomp/Translators/vu_translator.h index fb672b604..095d0ea30 100644 --- a/ps2xRecomp/include/ps2recomp/Translators/vu_translator.h +++ b/ps2xRecomp/include/ps2recomp/Translators/vu_translator.h @@ -15,6 +15,7 @@ namespace ps2recomp std::string translate(const Instruction &inst); private: + std::string translateOperation(const Instruction &inst); CodeGenerator &m_codeGenerator; }; } diff --git a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h index dd622753f..3f443aaba 100644 --- a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h +++ b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h @@ -62,6 +62,7 @@ namespace ps2recomp std::vector m_functions; std::vector m_symbols; std::vector
m_sections; + std::vector m_relocations; std::unordered_map> m_decodedFunctions; diff --git a/ps2xRecomp/include/ps2recomp/types.h b/ps2xRecomp/include/ps2recomp/types.h index 9bd687df8..c3154ed9d 100644 --- a/ps2xRecomp/include/ps2recomp/types.h +++ b/ps2xRecomp/include/ps2recomp/types.h @@ -1,6 +1,7 @@ #ifndef PS2RECOMP_TYPES_H #define PS2RECOMP_TYPES_H + #include #include #include @@ -175,6 +176,7 @@ namespace ps2recomp // Recompiler configuration struct RecompilerConfig { + std::string inputPath; std::string outputPath; std::string ghidraMapPath; diff --git a/ps2xRecomp/src/lib/config_manager.cpp b/ps2xRecomp/src/lib/config_manager.cpp index 97dc2ddb4..ecf33caac 100644 --- a/ps2xRecomp/src/lib/config_manager.cpp +++ b/ps2xRecomp/src/lib/config_manager.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace ps2recomp { @@ -61,6 +62,9 @@ namespace ps2recomp } } config.outputWorkerThreads = static_cast(clampedOutputWorkers); + if (data.contains("vu1")) + throw std::runtime_error("The [vu1] catalog has been retired. Remove [vu1] and [[vu1.programs]]; VU0/VU1 use the runtime interpreter."); + config.patchSyscalls = toml::find_or(general, "patch_syscalls", config.patchSyscalls); config.patchCop0 = toml::find_or(general, "patch_cop0", config.patchCop0); config.patchCache = toml::find_or(general, "patch_cache", config.patchCache); diff --git a/ps2xRecomp/src/lib/control_flow_emitter.cpp b/ps2xRecomp/src/lib/control_flow_emitter.cpp index 4bf8dc550..004bd1b38 100644 --- a/ps2xRecomp/src/lib/control_flow_emitter.cpp +++ b/ps2xRecomp/src/lib/control_flow_emitter.cpp @@ -314,7 +314,7 @@ namespace ps2recomp } else { - m_ss << fmt::format("{}if (ctx->pc != 0x{:X}u) {{ return; }}\n", indent, fallthroughPc()); + m_ss << fmt::format("{}if (runtime->eeTransferPending() || ctx->pc != 0x{:X}u) {{ return; }}\n", indent, fallthroughPc()); } return true; diff --git a/ps2xRecomp/src/lib/function_table_emitter.cpp b/ps2xRecomp/src/lib/function_table_emitter.cpp index b82b25974..e62b7c7a3 100644 --- a/ps2xRecomp/src/lib/function_table_emitter.cpp +++ b/ps2xRecomp/src/lib/function_table_emitter.cpp @@ -152,6 +152,7 @@ namespace ps2recomp ss << "#include \n"; ss << "#include \"ps2_syscalls.h\"\n\n"; + ss << "extern const uint32_t g_ps2RecompiledEeAbiVersion = 4u;\n"; ss << "extern const uint32_t g_ps2RecompiledFunctionTableBase = 0x" << std::hex << tableBase << "u;\n"; ss << "extern const uint32_t g_ps2RecompiledFunctionTableEnd = 0x" << std::hex << tableEnd << "u;\n"; ss << "extern const uint32_t g_ps2RecompiledFunctionTableSlotCount = " << std::dec << slotCount << "u;\n"; diff --git a/ps2xRecomp/src/lib/instruction_translator.cpp b/ps2xRecomp/src/lib/instruction_translator.cpp index 768409759..eaaa37f52 100644 --- a/ps2xRecomp/src/lib/instruction_translator.cpp +++ b/ps2xRecomp/src/lib/instruction_translator.cpp @@ -211,9 +211,12 @@ namespace ps2recomp inst.rt, genWrite(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), "bits")); case OPCODE_LDC2: - return fmt::format("ctx->vu0_vf[{}] = _mm_castsi128_ps({});", inst.rt, genRead(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); + return fmt::format("runtime->beforeVu0Access(ctx, 0x{:08X}u);\nctx->vu0_vf[{}] = _mm_castsi128_ps({});\nruntime->afterVu0Access(ctx, 0x{:08X}u);", + inst.raw, inst.rt, genRead(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate)), inst.raw); case OPCODE_SDC2: - return genWrite(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("_mm_castps_si128(ctx->vu0_vf[{}])", inst.rt)) + ";"; + return fmt::format("runtime->beforeVu0Access(ctx, 0x{:08X}u);\n{};\nruntime->afterVu0Access(ctx, 0x{:08X}u);", + inst.raw, genWrite(128, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), + fmt::format("_mm_castps_si128(ctx->vu0_vf[{}])", inst.rt)), inst.raw); case OPCODE_DADDI: return fmt::format( "{{ int64_t src = (int64_t)GPR_S64(ctx, {}); " diff --git a/ps2xRecomp/src/lib/ps2_recompiler.cpp b/ps2xRecomp/src/lib/ps2_recompiler.cpp index bb35a7a3c..0b556ca26 100644 --- a/ps2xRecomp/src/lib/ps2_recompiler.cpp +++ b/ps2xRecomp/src/lib/ps2_recompiler.cpp @@ -1190,6 +1190,7 @@ namespace ps2recomp { try { + m_functionRenames.clear(); auto makeName = [&](const Function &function) -> std::string diff --git a/ps2xRecomp/src/lib/special_translator.cpp b/ps2xRecomp/src/lib/special_translator.cpp index b01a93771..24effa37f 100644 --- a/ps2xRecomp/src/lib/special_translator.cpp +++ b/ps2xRecomp/src/lib/special_translator.cpp @@ -41,7 +41,7 @@ namespace ps2recomp case SPECIAL_JALR: return fmt::format("// JALR ${}, ${} - Handled by branch logic", inst.rd, inst.rs); case SPECIAL_SYSCALL: - return fmt::format("ctx->pc = 0x{:X}u;\nruntime->handleSyscall(rdram, ctx, 0x{:X}u);", + return fmt::format("ctx->pc = 0x{:X}u;\nruntime->handleSyscall(rdram, ctx, 0x{:X}u);\nif (runtime->eeTransferPending()) {{ return; }}", inst.address + 4u, (inst.raw >> 6) & 0xFFFFFu); case SPECIAL_BREAK: diff --git a/ps2xRecomp/src/lib/vu_translator.cpp b/ps2xRecomp/src/lib/vu_translator.cpp index d0fc95d56..d37978800 100644 --- a/ps2xRecomp/src/lib/vu_translator.cpp +++ b/ps2xRecomp/src/lib/vu_translator.cpp @@ -17,6 +17,12 @@ namespace ps2recomp } std::string VuTranslator::translate(const Instruction &inst) + { + return fmt::format("runtime->beforeVu0Access(ctx, 0x{:08X}u);\n{}\nruntime->afterVu0Access(ctx, 0x{:08X}u);", + inst.raw, translateOperation(inst), inst.raw); + } + + std::string VuTranslator::translateOperation(const Instruction &inst) { uint8_t format = inst.rs; // Use parsed rs field for COP2 format uint8_t rt = inst.rt; @@ -215,7 +221,7 @@ namespace ps2recomp case VU0_S2_VRSQRT: return m_codeGenerator.translateVU_VRSQRT(inst); case VU0_S2_VWAITQ: - return fmt::format("// VWAITQ (Q already resolved in this runtime)"); + return {}; case VU0_S2_VMTIR: return m_codeGenerator.translateVU_VMTIR(inst); case VU0_S2_VMFIR: diff --git a/ps2xRecomp/src/runner/main.cpp b/ps2xRecomp/src/runner/main.cpp index 64c77973d..ad50bf381 100644 --- a/ps2xRecomp/src/runner/main.cpp +++ b/ps2xRecomp/src/runner/main.cpp @@ -1,4 +1,6 @@ #include "ps2recomp/ps2_recompiler.h" +#include "ps2recomp/config_manager.h" +#include "ps2recomp/elf_parser.h" #include #include @@ -8,6 +10,7 @@ void printUsage() { std::cout << "PS2Recomp - A static recompiler for PlayStation 2 ELF files\n"; std::cout << "Usage: ps2recomp \n"; + std::cout << " config.toml: Configuration file for the recompiler\n"; } @@ -23,6 +26,8 @@ int main(int argc, char *argv[]) try { + if (configPath == "--vu1-only") + throw std::runtime_error("--vu1-only has been retired. Recompile the EE normally; VU0/VU1 use the runtime interpreter."); PS2Recompiler recompiler(configPath); if (!recompiler.initialize()) @@ -50,4 +55,4 @@ int main(int argc, char *argv[]) std::cerr << "Error: " << e.what() << std::endl; return 1; } -} \ No newline at end of file +} diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 589d76850..5b9a1426c 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -5,13 +5,23 @@ project(PS2Runtime VERSION 0.1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(PS2X_GS_BACKEND "PARALLEL_GS" CACHE STRING "GS backend: CPU or PARALLEL_GS") +set_property(CACHE PS2X_GS_BACKEND PROPERTY STRINGS CPU PARALLEL_GS) +if(NOT PS2X_GS_BACKEND MATCHES "^(CPU|PARALLEL_GS)$") + message(FATAL_ERROR "PS2X_GS_BACKEND must be CPU or PARALLEL_GS") +endif() +if(PS2X_GS_BACKEND STREQUAL "PARALLEL_GS" AND + (ANDROID OR NOT (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux"))) + message(FATAL_ERROR "PARALLEL_GS is supported only on Windows and Linux desktop") +endif() + option(PS2X_ENABLE_RUNNER_UNITY_BUILD "Build ps2EntryRunner with CMake unity build" ON) set(PS2X_RUNNER_UNITY_BUILD_BATCH_SIZE 32 CACHE STRING "Unity build batch size for ps2EntryRunner") option(PS2X_ENABLE_RUNNER_PCH "Precompile the heavy runtime headers for ps2EntryRunner" ON) option(PS2X_ENABLE_SCCACHE "Use sccache as compiler launcher when available" ON) -option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" ON) -option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" ON) +option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" OFF) +option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" OFF) option(PS2X_ENABLE_IOP_RPC_TRACE "Log unhandled IOP/SIF RPC trace suggestions" ON) option(PS2X_STRICT_RETURN_DIAGNOSTICS "Route generated JR $ra returns through runtime branch diagnostics" OFF) option(PS2X_SHOW_WINDOWS_CONSOLE "Show a console window for ps2EntryRunner on Windows release builds" ON) @@ -389,13 +399,21 @@ add_library(ps2_runtime STATIC src/lib/ps2_rom_device.cpp src/lib/ps2_vfs.cpp src/lib/ps2_runtime.cpp + src/lib/frame_timing.cpp src/lib/ps2_vif1_interpreter.cpp - src/lib/vu/ps2_vu1_core.cpp - src/lib/vu/ps2_vu1_upper.cpp - src/lib/vu/ps2_vu1_lower.cpp + src/lib/vu/interpreter/vu_executor.cpp + src/lib/vu/interpreter/vu_memory.cpp + src/lib/vu/vu_unit.cpp + src/lib/vu/vu_runtime.cpp + src/lib/vu/vu_context.cpp src/lib/games_database.cpp ) +if(PS2X_GS_BACKEND STREQUAL "PARALLEL_GS") + message(STATUS "Using ParallelGS backend for PS2 graphics") + include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ParallelGS.cmake") +endif() + if(PS2X_ENABLE_RUNTIME_LOGS) target_compile_definitions(ps2_runtime PUBLIC PS2_RUNTIME_LOGS=1 @@ -442,6 +460,7 @@ file(GLOB RUNNER_SRC_FILES CONFIGURE_DEPENDS ) set(RUNNER_MAIN_CPP "${CMAKE_CURRENT_SOURCE_DIR}/src/runner/main.cpp") +list(FILTER RUNNER_SRC_FILES EXCLUDE REGEX "vu1_programs[.]cpp$") set(ROOT_MAIN_CPP "${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp") if(EXISTS "${RUNNER_MAIN_CPP}") @@ -608,3 +627,16 @@ endif() install(DIRECTORY include/ DESTINATION include ) + +if(MSVC) + set(PS2X_VU_FP_OPTIONS /fp:strict "$<$:/Ob2>" "$<$:/Oi>") +else() + set(PS2X_VU_FP_OPTIONS -fno-fast-math -ffp-contract=off -frounding-math) +endif() + +set_source_files_properties( + src/lib/vu/interpreter/vu_executor.cpp + src/lib/vu/interpreter/vu_memory.cpp + src/lib/vu/vu_unit.cpp src/lib/vu/vu_runtime.cpp + src/lib/vu/vu_context.cpp + PROPERTIES COMPILE_OPTIONS "${PS2X_VU_FP_OPTIONS}") diff --git a/ps2xRuntime/cmake/ParallelGS.cmake b/ps2xRuntime/cmake/ParallelGS.cmake new file mode 100644 index 000000000..3d6d8aada --- /dev/null +++ b/ps2xRuntime/cmake/ParallelGS.cmake @@ -0,0 +1,44 @@ +include(FetchContent) +set(PARALLEL_GS_STANDALONE ON CACHE BOOL "Build parallel-gs as a library" FORCE) +set(GRANITE_TOOLS OFF CACHE BOOL "" FORCE) +set(GRANITE_INSTALL_TARGETS OFF CACHE BOOL "" FORCE) + +if(MSVC AND NOT CMAKE_MSVC_RUNTIME_LIBRARY) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() + +FetchContent_Declare(parallel_gs + GIT_REPOSITORY https://github.com/Arntzen-Software/parallel-gs.git + GIT_TAG 3a66c1976170cbc2cb53a3593fabbc7c4b2ccfbd + GIT_SUBMODULES_RECURSE TRUE + EXCLUDE_FROM_ALL +) + +# Workaround for my pc large path remove this later hahaha +if(CMAKE_HOST_WIN32) + set(_ps2x_git_config_count "$ENV{GIT_CONFIG_COUNT}") + set(_ps2x_git_config_index 0) + if(NOT _ps2x_git_config_count STREQUAL "") + set(_ps2x_git_config_index "${_ps2x_git_config_count}") + endif() + set(ENV{GIT_CONFIG_KEY_${_ps2x_git_config_index}} "core.longpaths") + set(ENV{GIT_CONFIG_VALUE_${_ps2x_git_config_index}} "true") + math(EXPR _ps2x_git_config_next "${_ps2x_git_config_index} + 1") + set(ENV{GIT_CONFIG_COUNT} "${_ps2x_git_config_next}") +endif() + +FetchContent_MakeAvailable(parallel_gs) +if(CMAKE_HOST_WIN32) + set(ENV{GIT_CONFIG_COUNT} "${_ps2x_git_config_count}") + unset(ENV{GIT_CONFIG_KEY_${_ps2x_git_config_index}}) + unset(ENV{GIT_CONFIG_VALUE_${_ps2x_git_config_index}}) +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/ParallelGSTransferTails.cmake") +target_sources(ps2_runtime PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/../src/lib/gs/gs_parallel_backend.cpp" + "${CMAKE_CURRENT_LIST_DIR}/../src/lib/gs/gs_gl_interop.cpp" +) +target_link_libraries(ps2_runtime PRIVATE parallel-gs) +target_compile_definitions(ps2_runtime PUBLIC PS2X_GS_PARALLEL=1) +install(FILES "${parallel_gs_SOURCE_DIR}/COPYING.LGPLv3" DESTINATION share/licenses/parallel-gs) diff --git a/ps2xRuntime/cmake/ParallelGSTransferTails.cmake b/ps2xRuntime/cmake/ParallelGSTransferTails.cmake new file mode 100644 index 000000000..2ba773631 --- /dev/null +++ b/ps2xRuntime/cmake/ParallelGSTransferTails.cmake @@ -0,0 +1,24 @@ +file(READ "${parallel_gs_SOURCE_DIR}/gs/gs_interface.cpp" gs_interface_source) + +function(patch_gs_transfer before after) + string(FIND "${gs_interface_source}" "${before}" position) + if(position EQUAL -1) + message(FATAL_ERROR "parallel-gs transfer patch does not match the pinned source") + endif() + string(REPLACE "${before}" "${after}" gs_interface_source "${gs_interface_source}") + set(gs_interface_source "${gs_interface_source}" PARENT_SCOPE) +endfunction() + +patch_gs_transfer("get_bits_per_pixel(transfer_state.copy.bitbltbuf.desc.DPSM)) / 64;" "get_bits_per_pixel(transfer_state.copy.bitbltbuf.desc.DPSM) + 63) / 64;") +patch_gs_transfer("get_bits_per_pixel(transfer_state.copy.bitbltbuf.desc.SPSM)) / 8;" "get_bits_per_pixel(transfer_state.copy.bitbltbuf.desc.SPSM) + 7) / 8;") +patch_gs_transfer("transfer_state.fifo_readback.reserve(required_bytes);" "transfer_state.fifo_readback.reserve((required_bytes + 15) & ~15u);\n\t\tif (required_bytes) memset(transfer_state.fifo_readback.data(), 0, (required_bytes + 15) & ~15u);") +patch_gs_transfer("transfer_state.fifo_readback_128b_size = required_bytes / 16;" "transfer_state.fifo_readback_128b_size = (required_bytes + 15) / 16;") + +set(patched_interface "${parallel_gs_BINARY_DIR}/gs_interface_transfer_tails.cpp") +file(CONFIGURE OUTPUT "${patched_interface}" CONTENT "${gs_interface_source}" @ONLY) + +get_target_property(gs_sources parallel-gs SOURCES) +list(REMOVE_ITEM gs_sources gs_interface.cpp) +set_property(TARGET parallel-gs PROPERTY SOURCES "${gs_sources}") + +target_sources(parallel-gs PRIVATE "${patched_interface}") diff --git a/ps2xRuntime/include/ps2_debug_panel.h b/ps2xRuntime/include/ps2_debug_panel.h index b9933d508..c0a345f50 100644 --- a/ps2xRuntime/include/ps2_debug_panel.h +++ b/ps2xRuntime/include/ps2_debug_panel.h @@ -1,6 +1,10 @@ #ifndef PS2_DEBUG_PANEL_H #define PS2_DEBUG_PANEL_H +#include +#include +#include + class PS2Runtime; class PS2DebugPanel @@ -20,6 +24,12 @@ class PS2DebugPanel bool m_showRegisters = true; unsigned int m_memoryAddress = 0x00100000u; unsigned int m_memoryBytes = 0x100u; + std::chrono::steady_clock::time_point m_fpsSampleStart{}; + std::array m_lastDisplayFlips{}; + uint64_t m_lastSdkPresents = 0; + uint64_t m_hostFramesInSample = 0; + double m_gameFps = 0.0; + double m_hostFps = 0.0; }; #endif // PS2_DEBUG_PANEL_H diff --git a/ps2xRuntime/include/ps2_log.h b/ps2xRuntime/include/ps2_log.h index a42b1ba9e..cc3e2d628 100644 --- a/ps2xRuntime/include/ps2_log.h +++ b/ps2xRuntime/include/ps2_log.h @@ -31,6 +31,8 @@ ps2_log::append_runtime_log_text(_ps2_runtime_error_text); \ } while (0) +#define RUNTIME_WARNING(x) std::cout << x + namespace ps2_log { struct RuntimeLogEntry diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index 4b9242cb4..08461332e 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -28,7 +28,7 @@ #include "runtime/gs/ps2_gif_arbiter.h" #include "runtime/ps2_memory.h" #include "runtime/gs/gs_frontend.h" -#include "runtime/ps2_vu1.h" +#include "runtime/vu/vu_runtime.h" #include "runtime/ps2_audio.h" #include "runtime/ps2_pad.h" #include "runtime/ps2_rom_device.h" @@ -361,6 +361,8 @@ class PS2Runtime void SignalException(R5900Context *ctx, PS2Exception exception); + void beforeVu0Access(R5900Context *ctx, uint32_t instruction); + void afterVu0Access(R5900Context *ctx, uint32_t instruction); void executeVU0Microprogram(uint8_t *rdram, R5900Context *ctx, uint32_t address); void vu0StartMicroProgram(uint8_t *rdram, R5900Context *ctx, uint32_t address); @@ -394,7 +396,8 @@ class PS2Runtime const EeScheduler &eeScheduler() const; void postEeEvent(EeEvent event); bool eeCheckpointDue(uint32_t cycles = 32u) noexcept; - [[noreturn]] void eeWaitVSyncTicks(uint32_t ticks, uint32_t resumePc); + [[nodiscard]] bool eeTransferPending() const noexcept; + void eeWaitVSyncTicks(uint32_t ticks, uint32_t resumePc); struct EeExitHandlerRegistration { @@ -442,10 +445,10 @@ class PS2Runtime inline const GS &gs() const { return m_gs; } inline GifArbiter &gifArbiter() { return m_gifArbiter; } inline const GifArbiter &gifArbiter() const { return m_gifArbiter; } - inline VU1Interpreter &vu0() { return m_vu0; } - inline const VU1Interpreter &vu0() const { return m_vu0; } - inline VU1Interpreter &vu1() { return m_vu1; } - inline const VU1Interpreter &vu1() const { return m_vu1; } + ps2vu::Unit &vu0(); + const ps2vu::Unit &vu0() const; + ps2vu::Unit &vu1(); + const ps2vu::Unit &vu1() const; inline PS2AudioBackend &audioBackend() { return m_audioBackend; } inline const PS2AudioBackend &audioBackend() const { return m_audioBackend; } @@ -497,8 +500,8 @@ class PS2Runtime PSPadBackend m_padBackend; PS2RomDevice m_romDevice; PS2Vfs m_vfs; - VU1Interpreter m_vu0{VU1Interpreter::Unit::VU0}; - VU1Interpreter m_vu1{VU1Interpreter::Unit::VU1}; + std::unique_ptr m_vu; + R5900Context m_cpuContext; std::unique_ptr m_eeScheduler; mutable std::mutex m_eeKernelStateMutex; @@ -546,6 +549,7 @@ class PS2Runtime }; // Generated by ps2xRecomp in ps2xRuntime/src/runner/register_functions.cpp. +extern const uint32_t g_ps2RecompiledEeAbiVersion; extern const uint32_t g_ps2RecompiledFunctionTableBase; extern const uint32_t g_ps2RecompiledFunctionTableEnd; extern const uint32_t g_ps2RecompiledFunctionTableSlotCount; diff --git a/ps2xRuntime/include/runtime/ee_scheduler.h b/ps2xRuntime/include/runtime/ee_scheduler.h index f247ff998..30f67c665 100644 --- a/ps2xRuntime/include/runtime/ee_scheduler.h +++ b/ps2xRuntime/include/runtime/ee_scheduler.h @@ -16,12 +16,6 @@ #include #include -// This exception is the EE equivalent of a longjmp to the dispatcher. It is -// not an error and must only be caught at EeScheduler::run(). -struct EeDispatcherTransfer final -{ -}; - enum class EeThreadStatus : uint8_t { Running, @@ -280,6 +274,7 @@ class EeScheduler void postEvent(EeEvent event); [[nodiscard]] bool checkpointDue(uint32_t cycles = kGeneratedCheckpointCycles) noexcept; void accountCycles(uint32_t cycles) noexcept; + [[nodiscard]] bool transferPending() const noexcept { return m_transferPending; } [[nodiscard]] bool isExecutingGuest() const noexcept; // Kernel object API. All calls except postEvent/requestStop execute on the @@ -288,7 +283,7 @@ class EeScheduler int createThread(const EeThreadCreateParams ¶ms); int deleteThread(int id, uint32_t &ownedStack); int startThread(int id, uint32_t arg, const R5900Context &caller, bool interruptSafe); - [[noreturn]] void exitCurrent(bool deleteThread); + void exitCurrent(bool deleteThread); int terminateThread(int id, uint32_t &ownedStack, bool interruptSafe); int suspendThread(int id, bool interruptSafe); int resumeThread(int id, bool interruptSafe); @@ -316,8 +311,8 @@ class EeScheduler int setAlarm(uint16_t ticks, uint32_t handler, uint32_t argument, uint32_t gp, uint32_t sp); int cancelAlarm(int id); void queueInvocation(GuestInvocation invocation); - [[noreturn]] void invokeCurrent(GuestInvocation invocation); - [[noreturn]] void invokeCurrentSequence(std::vector invocations); + void invokeCurrent(GuestInvocation invocation); + void invokeCurrentSequence(std::vector invocations); [[nodiscard]] bool hasInvocation(GuestInvocationKind kind, uint64_t tag) const; [[nodiscard]] uint32_t invocationStackTop(); @@ -336,10 +331,10 @@ class EeScheduler [[nodiscard]] uint64_t currentVSyncTick() const noexcept; uint32_t setGsVSyncCallback(uint32_t callback, uint32_t gp, uint32_t sp); - [[noreturn]] void waitVSync(uint64_t afterTick, int fixedResult = -1, std::function completion = {}); + void waitVSync(uint64_t afterTick, int fixedResult = -1, std::function completion = {}); void completeVSync(uint64_t tick); void completeExternalWait(uint32_t type, uint64_t token, int result); - [[noreturn]] void waitExternal(EeWaitReason reason, uint32_t type, uint64_t token, std::function completion = {}); + void waitExternal(EeWaitReason reason, uint32_t type, uint64_t token, std::function completion = {}); [[nodiscard]] GuestThread *thread(int id); [[nodiscard]] const GuestThread *thread(int id) const; @@ -378,7 +373,7 @@ class EeScheduler void makeRunning(GuestThread &thread); void makeDormant(GuestThread &thread); void removeFromWaitObject(GuestThread &thread); - [[noreturn]] void blockCurrent(EeWaitState wait); + void blockCurrent(EeWaitState wait); void makeReady(GuestThread &thread, int result, bool interruptSafe); void requestPreemptionIfHigher(const GuestThread &readyThread, bool interruptSafe); void applyPendingPreemption(); @@ -421,6 +416,7 @@ class EeScheduler uint32_t m_enabledIntcMask = 0xFFFFFFFFu; uint32_t m_enabledDmacMask = 0xFFFFFFFFu; int m_currentThreadId = 0; + bool m_transferPending = false; bool m_rescheduleRequested = false; bool m_timeSliceExpired = false; bool m_insideInterrupt = false; diff --git a/ps2xRuntime/include/runtime/gs/gs_backend.h b/ps2xRuntime/include/runtime/gs/gs_backend.h index 3f00d01b5..076fa460b 100644 --- a/ps2xRuntime/include/runtime/gs/gs_backend.h +++ b/ps2xRuntime/include/runtime/gs/gs_backend.h @@ -5,6 +5,8 @@ #include #include +struct GSDebugSnapshot; + class GSRasterBackend { public: @@ -12,6 +14,12 @@ class GSRasterBackend virtual void Initialize(uint8_t *vram, uint32_t vramSize) = 0; virtual void Reset() = 0; + + virtual bool UsesRawCommands() const { return false; } + virtual void ProcessGIF(uint32_t, const uint8_t *, uint32_t) {} + virtual void WriteRegisterRaw(uint8_t, uint64_t) {} + virtual void ReadRegisterState(GSDebugSnapshot &) const {} + virtual uint64_t GetReadbackCount() const { return 0; } virtual void Submit(const GSPrimitiveBatch &batch) = 0; virtual void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) = 0; diff --git a/ps2xRuntime/include/runtime/gs/gs_frontend.h b/ps2xRuntime/include/runtime/gs/gs_frontend.h index 4c3f6410a..3d6ed38f5 100644 --- a/ps2xRuntime/include/runtime/gs/gs_frontend.h +++ b/ps2xRuntime/include/runtime/gs/gs_frontend.h @@ -105,8 +105,10 @@ class GS void init(uint8_t *vram, uint32_t vramSize, struct GSRegisters *privRegs = nullptr); void reset(); void setRasterBackend(std::unique_ptr backend); + void shutdownBackend(); + uint64_t getReadbackCount() const; - void processGIFPacket(const uint8_t *data, uint32_t sizeBytes); + void processGIFPacket(const uint8_t *data, uint32_t sizeBytes, uint32_t path = 3); bool processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes); void uploadImageNative(uint64_t bitbltbuf, uint64_t trxpos, @@ -119,9 +121,9 @@ class GS const uint8_t *lockDisplaySnapshot(uint32_t &outSize); void unlockDisplaySnapshot(); uint32_t getLastDisplayBaseBytes() const; - const GSFrameReg &getContextFrame(int index) const + GSFrameReg getContextFrame(int index) const { - return m_ctx[(index != 0) ? 1 : 0].frame; + return getDebugSnapshot().ctx[(index != 0) ? 1 : 0].frame; } GSDebugSnapshot getDebugSnapshot() const; std::vector getDebugHistory() const; @@ -130,6 +132,7 @@ class GS void setDebugHistoryPaused(bool paused); bool getPreferredDisplaySource(GSFrameReg &outSource, uint32_t &outDestFbp) const; void latchHostPresentationFrame(); + std::shared_ptr getLatchedGpuFrame(uint32_t &width, uint32_t &height, float &aspectRatio) const; bool copyLatchedHostPresentationFrame(std::vector &outPixels, uint32_t &outWidth, uint32_t &outHeight, @@ -223,6 +226,8 @@ class GS uint32_t m_preferredDisplayDestFbp = 0; bool m_hasPreferredDisplaySource = false; std::vector m_hostPresentationFrame; + std::shared_ptr m_hostPresentationGpuFrame; + float m_hostPresentationAspectRatio = 0.0f; uint32_t m_hostPresentationWidth = 0; uint32_t m_hostPresentationHeight = 0; uint32_t m_hostPresentationDisplayFbp = 0; diff --git a/ps2xRuntime/include/runtime/gs/gs_parallel_backend.h b/ps2xRuntime/include/runtime/gs/gs_parallel_backend.h new file mode 100644 index 000000000..c95d8bc7b --- /dev/null +++ b/ps2xRuntime/include/runtime/gs/gs_parallel_backend.h @@ -0,0 +1,6 @@ +#pragma once +#include "runtime/gs/gs_backend.h" + +struct GSRegisters; + +std::unique_ptr CreateParallelGSBackend(GSRegisters ®isters); diff --git a/ps2xRuntime/include/runtime/gs/gs_types.h b/ps2xRuntime/include/runtime/gs/gs_types.h index 836a3ff49..3cec45444 100644 --- a/ps2xRuntime/include/runtime/gs/gs_types.h +++ b/ps2xRuntime/include/runtime/gs/gs_types.h @@ -4,6 +4,7 @@ #include #include #include +#include enum GSPrimType : uint8_t { @@ -277,6 +278,8 @@ struct GSTransferSnapshot struct GSPresentationRequest { uint64_t pmode = 0; + uint64_t smode1 = 0; + uint64_t syncv = 0; uint64_t smode2 = 0; uint64_t dispfb1 = 0; uint64_t display1 = 0; @@ -284,24 +287,35 @@ struct GSPresentationRequest uint64_t display2 = 0; uint64_t bgcolor = 0; uint64_t vsyncTick = 0; + uint32_t field = 0; GSFrameReg contextFrames[2]{}; GSFrameReg preferredSource{}; uint32_t preferredDestFbp = 0; bool hasPreferredSource = false; }; +class GSGpuFrame +{ +public: + virtual ~GSGpuFrame() = default; + virtual uint32_t AcquireTexture() = 0; + virtual void ReleaseTexture() = 0; +}; + struct PresentationFrame { std::vector pixels; + std::shared_ptr gpu; uint32_t width = 0; uint32_t height = 0; uint32_t displayFbp = 0; uint32_t sourceFbp = 0; bool usedPreferred = false; + float aspectRatio = 0.0f; explicit operator bool() const { - return !pixels.empty() && width != 0u && height != 0u; + return (gpu || !pixels.empty()) && width != 0u && height != 0u; } }; diff --git a/ps2xRuntime/include/runtime/gs/ps2_gif_arbiter.h b/ps2xRuntime/include/runtime/gs/ps2_gif_arbiter.h index 20120830b..f82d3e005 100644 --- a/ps2xRuntime/include/runtime/gs/ps2_gif_arbiter.h +++ b/ps2xRuntime/include/runtime/gs/ps2_gif_arbiter.h @@ -23,10 +23,12 @@ struct GifArbiterPacket class GifArbiter { public: - using ProcessPacketFn = std::function; + using ProcessPacketFn = std::function; GifArbiter() = default; explicit GifArbiter(ProcessPacketFn processFn); + explicit GifArbiter(std::function processFn) + : GifArbiter([fn = std::move(processFn)](GifPathId, const uint8_t *data, uint32_t size) { fn(data, size); }) {} void setProcessPacketFn(ProcessPacketFn fn) { m_processFn = std::move(fn); } diff --git a/ps2xRuntime/include/runtime/ps2_memory.h b/ps2xRuntime/include/runtime/ps2_memory.h index ea86a9cb2..d05be1511 100644 --- a/ps2xRuntime/include/runtime/ps2_memory.h +++ b/ps2xRuntime/include/runtime/ps2_memory.h @@ -10,6 +10,9 @@ #include #include #include +#include +#include + #include "gs/ps2_gif_arbiter.h" #if defined(_MSC_VER) @@ -210,8 +213,23 @@ struct GSRegisters uint64_t imr; // Interrupt mask uint64_t busdir; // Bus direction uint64_t siglblid; // Signal label ID + + // Host diagnostics, not guest registers. Count enabled CRTC buffer switches, + // independently of host redraws and without counting both circuits as frames. + std::atomic displayFlipCount[2]{}; + // Completed libgs swaps can present a frame without changing DISPFB.FBP. + std::atomic sdkPresentCount{0}; + + void writeDisplayFramebuffer(unsigned circuit, uint64_t value) + { + uint64_t ® = circuit == 0 ? dispfb1 : dispfb2; + // FBP only: field offsets and display configuration changes are not flips. + if (((reg ^ value) & 0x1ffu) != 0 && (pmode & (1ull << circuit))) + displayFlipCount[circuit].fetch_add(1, std::memory_order_relaxed); + reg = value; + } }; -static_assert(sizeof(GSRegisters) == (20u * sizeof(uint64_t)), "GSRegisters layout changed unexpectedly"); +static_assert(offsetof(GSRegisters, displayFlipCount) == (20u * sizeof(uint64_t)), "GSRegisters register layout changed unexpectedly"); static_assert(alignof(GSRegisters) == alignof(uint64_t), "GSRegisters alignment must remain 64-bit"); static_assert(std::atomic::is_always_lock_free, "GS CSR atomic must be lock-free on all supported targets"); @@ -318,15 +336,19 @@ class PS2Memory void setGifArbiter(GifArbiter *arbiter) { m_gifArbiter = arbiter; } using Vu1MscalCallback = std::function; - void setVu1MscalCallback(Vu1MscalCallback cb) { m_vu1MscalCallback = std::move(cb); } + void setVu1MscalCallback(Vu1MscalCallback cb) { m_vuMscalCallback[1] = std::move(cb); } + void setVu0MscalCallback(Vu1MscalCallback cb) { m_vuMscalCallback[0] = std::move(cb); } using Vu1MscntCallback = std::function; - void setVu1MscntCallback(Vu1MscntCallback cb) { m_vu1MscntCallback = std::move(cb); } + void setVu1MscntCallback(Vu1MscntCallback cb) { m_vuMscntCallback[1] = std::move(cb); } + void setVu0MscntCallback(Vu1MscntCallback cb) { m_vuMscntCallback[0] = std::move(cb); } + using Vu1FlushCallback = std::function; + void setVu1FlushCallback(Vu1FlushCallback cb) { m_vuFlushCallback[1] = std::move(cb); } + void setVu0FlushCallback(Vu1FlushCallback cb) { m_vuFlushCallback[0] = std::move(cb); } - uint8_t *getVU1Code() { return m_vu1Code; } const uint8_t *getVU1Code() const { return m_vu1Code; } + bool writeVuCode(bool vu1, uint32_t offset, std::span bytes); uint8_t *getVU1Data() { return m_vu1Data; } const uint8_t *getVU1Data() const { return m_vu1Data; } - uint8_t *getVU0Code() { return m_vu0Code; } const uint8_t *getVU0Code() const { return m_vu0Code; } uint8_t *getVU0Data() { return m_vu0Data; } const uint8_t *getVU0Data() const { return m_vu0Data; } @@ -399,8 +421,9 @@ class PS2Memory GifPacketCallback m_gifPacketCallback; GifArbiter *m_gifArbiter = nullptr; - Vu1MscalCallback m_vu1MscalCallback; - Vu1MscntCallback m_vu1MscntCallback; + Vu1MscalCallback m_vuMscalCallback[2]; + Vu1MscntCallback m_vuMscntCallback[2]; + Vu1FlushCallback m_vuFlushCallback[2]; uint8_t *m_vu0Code = nullptr; uint8_t *m_vu0Data = nullptr; diff --git a/ps2xRuntime/include/runtime/ps2_vu1.h b/ps2xRuntime/include/runtime/ps2_vu1.h deleted file mode 100644 index 67c8184fd..000000000 --- a/ps2xRuntime/include/runtime/ps2_vu1.h +++ /dev/null @@ -1,299 +0,0 @@ -#ifndef PS2_VU1_H -#define PS2_VU1_H - -#include -#include - -class GS; -class PS2Memory; - -struct VU1State -{ - float vf[32][4]; - int32_t vi[16]; - float acc[4]; - float q; - float p; - float i; - uint32_t r; - uint32_t pc; - uint32_t mac; - uint32_t clip; - uint32_t status; - uint64_t cycles; - bool ebit; - bool haltAfterDelaySlot; - bool dBitEnabled; - bool tBitEnabled; - bool stoppedByD; - bool stoppedByT; - uint32_t top; // VIF TOP visible to XTOP - uint32_t itop; // VIF ITOP visible to XITOP - - bool branchPending; - uint32_t branchTarget; - uint32_t branchDelay; -}; - -class VU1Interpreter -{ -public: - enum class Unit : uint8_t - { - VU0, - VU1 - }; - - explicit VU1Interpreter(Unit unit = Unit::VU1); - - void reset(); - - void execute(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory = nullptr, - uint32_t startPC = 0, uint32_t top = 0, uint32_t itop = 0, - uint32_t maxCycles = 65536); - - void resume(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory = nullptr, - uint32_t top = 0, uint32_t itop = 0, uint32_t maxCycles = 65536); - - VU1State &state() { return m_state; } - const VU1State &state() const { return m_state; } - -private: - enum Pipeline : uint8_t - { - PipelineNone = 0, - PipelineFmac, - PipelineLsu, - PipelineFdiv, - PipelineEfu, - PipelineIalu, - PipelineBranch, - PipelineXgkick - }; - - struct VfAccess - { - uint8_t reg = 0; - uint8_t lanes = 0; - }; - - struct InstructionUsage - { - std::array vfRead{}; - VfAccess vfWrite{}; - uint8_t vfReadCount = 0; - uint16_t viRead = 0; - uint16_t viWrite = 0; - uint8_t accRead = 0; - uint8_t accWrite = 0; - uint8_t latency = 0; - uint8_t vfLatency = 0; - uint8_t viLatency = 0; - Pipeline pipeline = PipelineNone; - bool waitQ = false; - bool waitP = false; - bool readsClip = false; - bool writesClip = false; - bool delaysNextBranchRead = false; - bool reserved = false; - }; - - struct DecodedInstructionPair - { - uint32_t lower = 0; - uint32_t upper = 0; - InstructionUsage lowerUsage{}; - InstructionUsage upperUsage{}; - bool iBit = false; - bool eBit = false; - bool mBit = false; - bool dBit = false; - bool tBit = false; - uint8_t upperVfShadowReg = 0; - uint8_t suppressedLowerVf = 0; - }; - - struct FlagPipelineEntry - { - uint64_t readyCycle = 0; - uint64_t issueCycle = 0; - uint32_t mac = 0; - uint32_t status = 0; - uint32_t extraSticky = 0; - uint32_t clip = 0; - bool valid = false; - bool writesMac = false; - bool writesStatus = false; - bool writesSticky = false; - bool writesClip = false; - }; - - struct ScalarPipelineEntry - { - uint64_t readyCycle = 0; - float value = 0.0f; - uint32_t statusDi = 0; - bool valid = false; - }; - - struct PendingStore - { - uint64_t readyCycle = 0; - uint32_t address = 0; - std::array words{}; - uint8_t laneMask = 0; - bool valid = false; - }; - - struct PendingVfWrite - { - uint64_t readyCycle = 0; - uint64_t sequence = 0; - std::array value{}; - uint8_t reg = 0; - uint8_t laneMask = 0; - bool valid = false; - }; - - struct PendingViWrite - { - uint64_t readyCycle = 0; - uint64_t sequence = 0; - int32_t value = 0; - uint8_t reg = 0; - bool valid = false; - }; - - struct PendingAccWrite - { - uint64_t readyCycle = 0; - uint64_t sequence = 0; - std::array value{}; - uint8_t laneMask = 0; - bool valid = false; - }; - - struct XgkickPipeline - { - static constexpr uint32_t kBufferSize = 0x10000u; - std::array packet{}; - uint32_t sourceAddress = 0; - uint32_t totalBytes = 0; - uint32_t copiedBytes = 0; - uint32_t currentTagEnd = 0; - uint32_t cycleCredit = 0; - uint64_t issueCycle = 0; - bool active = false; - bool currentTagEop = false; - }; - - static constexpr uint32_t kFmacLatency = 4u; - static constexpr uint32_t kAccForwardLatency = 1u; - static constexpr uint32_t kMaxFlagEntries = 8u; - static constexpr uint32_t kMaxPendingStores = 8u; - static constexpr uint32_t kMaxPendingVfWrites = 16u; - static constexpr uint32_t kMaxPendingViWrites = 8u; - static constexpr uint32_t kMaxPendingAccWrites = 8u; - static constexpr uint32_t kMaxDecodedPairs = 0x4000u / 8u; - - Unit m_unit; - VU1State m_state; - std::array m_decodedCodeCache{}; - const uint8_t *m_cachedVuCode = nullptr; - const PS2Memory *m_cachedMemory = nullptr; - uint32_t m_cachedCodeSize = 0; - uint64_t m_cachedCodeGeneration = 0; - bool m_decodedCodeCacheValid = false; - - std::array m_flagPipeline{}; - ScalarPipelineEntry m_fdiv{}; - std::array m_efu{}; - std::array m_storePipeline{}; - std::array m_vfWritePipeline{}; - std::array m_viWritePipeline{}; - std::array m_accWritePipeline{}; - XgkickPipeline m_xgkick{}; - std::array, 32> m_vfReady{}; - std::array m_viReady{}; - std::array m_accReady{}; - std::array, 32> m_vfLatestWrite{}; - std::array m_viLatestWrite{}; - std::array m_accLatestWrite{}; - - uint64_t m_cycle = 0; - uint64_t m_nextWriteSequence = 0; - uint64_t m_efuResourceReady = 0; - uint32_t m_workingClip = 0; - uint32_t m_currentUpperInstruction = 0; - int32_t m_viBranchBackupValue = 0; - uint8_t m_viBranchBackupReg = 0; - bool m_viBranchBackupValid = false; - uint8_t *m_activeVuData = nullptr; - uint32_t m_activeVuDataSize = 0; - GS *m_activeGs = nullptr; - PS2Memory *m_activeMemory = nullptr; - bool m_stopRequested = false; - bool m_pendingHaltD = false; - bool m_pendingHaltT = false; - - void run(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory, uint32_t maxCycles); - - InstructionUsage decodeUpperUsage(uint32_t upper) const; - InstructionUsage decodeLowerUsage(uint32_t lower) const; - static void addVfRead(InstructionUsage &usage, uint8_t reg, uint8_t lanes); - static void addVfWrite(InstructionUsage &usage, uint8_t reg, uint8_t lanes); - static uint8_t vfReadLanes(const InstructionUsage &usage, uint8_t reg); - DecodedInstructionPair decodeInstructionPair(const uint8_t *vuCode, uint32_t pc) const; - DecodedInstructionPair getDecodedInstructionPairForPc(const uint8_t *vuCode, uint32_t codeSize, PS2Memory *memory, uint32_t pc); - void rebuildDecodedCodeCache(const uint8_t *vuCode, uint32_t codeSize, const PS2Memory *memory, uint64_t generation); - - void execUpper(uint32_t instr); - void execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr); - - void applyDest(float *dst, const float *result, uint8_t dest); - void applyDestAcc(const float *result, uint8_t dest); - void applyFmacDest(float *dst, float *result, uint8_t dest); - void applyFmacDestAcc(float *result, uint8_t dest); - void normalizeFmacResult(float *result, uint8_t dest, uint8_t laneFlags[4]); - bool calculateFmacExactResult(uint32_t component, long double &result) const; - uint8_t normalizeFmacExactResult(float &value, long double exactResult) const; - uint32_t calculateFmacProductSticky(uint8_t dest) const; - void updateFmacFlags(const uint8_t laneFlags[4], uint8_t dest, uint32_t extraSticky); - void queueFsset(uint16_t immediate); - void queueClip(uint32_t clip); - void queueFcset(uint32_t clip); - void queueQ(float value, uint32_t latency, uint32_t statusDi); - void queueP(float value, uint32_t latency); - void queueStore(uint32_t address, const uint32_t words[4], uint8_t laneMask); - void queueVfWrite(uint8_t reg, uint8_t laneMask, const float value[4], uint32_t latency); - void queueViWrite(uint8_t reg, int32_t value, uint32_t latency); - void queueAccWrite(uint8_t laneMask, const float value[4], uint32_t latency); - void startXgkick(uint32_t qwordAddress); - - void resetScheduler(); - void commitReadyPipelines(); - void advanceOneCycle(); - void advanceTo(uint64_t targetCycle); - void flushPipelines(); - void progressXgkick(); - void finishXgkick(); - uint64_t calculatePairReadyCycle(const DecodedInstructionPair &decoded) const; - void markPairWrites(const DecodedInstructionPair &decoded); - bool pipelinesPending() const; - - float normalizeOperand(float value) const; - float normalizeResult(float value, uint32_t &laneFlags) const; - uint32_t microAddressMask() const; - int32_t readBranchVi(uint8_t reg) const; - void recordViWriteForBranch(uint8_t reg, int32_t oldValue); - void reportReservedInstruction(bool upper, uint32_t instruction); - float broadcast(const float *vf, uint8_t bc); -}; - -#endif diff --git a/ps2xRuntime/include/runtime/vu/interpreter/vu_executor.h b/ps2xRuntime/include/runtime/vu/interpreter/vu_executor.h new file mode 100644 index 000000000..a03f5cf14 --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/interpreter/vu_executor.h @@ -0,0 +1,295 @@ +#ifndef PS2_VU_EXECUTOR_H +#define PS2_VU_EXECUTOR_H + +#include "runtime/vu/vu_types.h" +#include "runtime/vu/vu_state.h" +#include "runtime/vu/vu_pipeline_events.h" +#include +#include +#include +#include +#include +#include + +class GS; +class PS2Memory; +namespace ps2vu +{ + class Unit; + struct MemoryServices; +} + +class VUExecutor +{ +public: + enum class Unit : uint8_t + { + VU0, + VU1 + }; + + explicit VUExecutor(Unit unit = Unit::VU1); + ~VUExecutor(); + + void reset(); + + void execute(const uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, + PS2Memory *memory = nullptr, uint32_t startPC = 0, uint32_t top = 0, uint32_t itop = 0, + uint32_t maxCycles = 65536); + + void resume(const uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, + PS2Memory *memory = nullptr, uint32_t top = 0, uint32_t itop = 0, uint32_t maxCycles = 65536); + + VU1State &state() + { + return m_state; + } + const VU1State &state() const + { + return m_state; + } + +private: + friend class ps2vu::Unit; + ps2vu::MemoryServices *m_services = nullptr; + void readData(uint32_t address, void *words); + void writeData(uint32_t address, const uint32_t *words, uint8_t lanes); + using InstructionUsage = ps2vu::InstructionUsage; + using DecodedInstructionPair = ps2vu::DecodedInstructionPair; + using VfAccess = ps2vu::VfAccess; + using enum ps2vu::Pipeline; + static constexpr auto kRegisterReadyCount = ps2vu::kRegisterReadyCount; + static constexpr auto kViReadyBase = ps2vu::kViReadyBase; + static constexpr auto kAccReadyBase = ps2vu::kAccReadyBase; + + struct FlagPipelineEntry + { + uint64_t issueCycle = 0; + uint32_t mac = 0; + uint32_t status = 0; + uint32_t extraSticky = 0; + uint32_t clip = 0; + bool valid = false; + bool writesMac = false; + bool writesStatus = false; + bool writesSticky = false; + bool writesClip = false; + }; + + struct ScalarPipelineEntry + { + uint64_t readyCycle = 0; + float value = 0.0f; + uint32_t statusDi = 0; + bool valid = false; + }; + + struct PendingStore + { + uint32_t address = 0; + std::array words{}; + uint8_t laneMask = 0; + bool valid = false; + }; + + struct PendingVfWrite + { + uint64_t sequence = 0; + std::array value{}; + uint8_t reg = 0; + uint8_t laneMask = 0; + bool valid = false; + }; + + struct PendingViWrite + { + uint64_t sequence = 0; + int32_t value = 0; + uint8_t reg = 0; + bool valid = false; + }; + + struct PendingAccWrite + { + uint64_t sequence = 0; + std::array value{}; + uint8_t laneMask = 0; + bool valid = false; + }; + + struct XgkickPipeline + { + static constexpr uint32_t kBufferSize = 0x10000u; + std::array packet{}; + uint32_t sourceAddress = 0; + uint32_t totalBytes = 0; + uint32_t copiedBytes = 0; + uint32_t currentTagEnd = 0; + uint32_t cycleCredit = 0; + uint64_t issueCycle = 0; + bool active = false; + bool currentTagEop = false; + + void reset() + { + sourceAddress = totalBytes = copiedBytes = currentTagEnd = cycleCredit = 0; + issueCycle = 0; + active = currentTagEop = false; + } + }; + + static constexpr uint32_t kFmacLatency = 4u; + static constexpr uint32_t kAccForwardLatency = 1u; + static constexpr uint32_t kMaxFlagEntries = ps2vu::PipelineEvents::capacity(ps2vu::WritebackKind::Flag); + static constexpr uint32_t kMaxPendingStores = ps2vu::PipelineEvents::capacity(ps2vu::WritebackKind::Store); + static constexpr uint32_t kMaxPendingVfWrites = ps2vu::PipelineEvents::capacity(ps2vu::WritebackKind::Vf); + static constexpr uint32_t kMaxPendingViWrites = ps2vu::PipelineEvents::capacity(ps2vu::WritebackKind::Vi); + static constexpr uint32_t kMaxPendingAccWrites = ps2vu::PipelineEvents::capacity(ps2vu::WritebackKind::Acc); + + bool m_programEnded = false; + Unit m_unit; + VU1State m_state; + + std::array m_flagPipeline{}; + ScalarPipelineEntry m_fdiv{}; + std::array m_efu{}; + std::array m_storePipeline{}; + std::array m_vfWritePipeline{}; + std::array m_viWritePipeline{}; + std::array m_accWritePipeline{}; + + uint32_t m_flagActive = 0; + uint32_t m_efuActive = 0; + uint32_t m_storeActive = 0; + uint32_t m_vfWriteActive = 0; + uint32_t m_viWriteActive = 0; + uint32_t m_accWriteActive = 0; + + static constexpr uint64_t kNoPipelineEvent = std::numeric_limits::max(); + uint64_t m_nextPipelineCycle = kNoPipelineEvent; + ps2vu::PipelineEvents m_pipelineEvents; + bool m_schedulerClean = true; + + XgkickPipeline m_xgkick{}; + + std::array m_registerReady{}; + std::array, 32> m_vfCommittedWrite{}; + std::array m_viCommittedWrite{}; + std::array m_accLatestWrite{}; + + uint64_t m_cycle = 0; + uint64_t m_nextWriteSequence = 0; + uint64_t m_efuResourceReady = 0; + uint32_t m_workingClip = 0; + struct UpperOperands + { + float vs[4], vt[4], acc[4], q, i; + } m_upperOperands{}; + int32_t m_viBranchBackupValue = 0; + uint8_t m_viBranchBackupReg = 0; + bool m_viBranchBackupValid = false; + uint8_t *m_activeVuData = nullptr; + uint32_t m_activeVuDataSize = 0; + GS *m_activeGs = nullptr; + PS2Memory *m_activeMemory = nullptr; + bool m_stopRequested = false; + bool m_pendingHaltD = false; + bool m_pendingHaltT = false; + + template + bool issuePair(const DecodedInstructionPair &decoded, + Upper upper, + Lower lower, + uint32_t codeSize, + uint64_t budgetEnd); + + void run(const uint8_t *vuCode, + uint32_t codeSize, + uint8_t *vuData, + uint32_t dataSize, + GS *gs, + PS2Memory *memory, + uint32_t maxCycles, + bool drain = true); + + template + void execUpper(Word instr, float *vfResult, float *accResult); + template + void execLower(Word instr, uint8_t *vuData, uint32_t dataSize, uint32_t upperInstr); + + void applyDest(float *dst, const float *result, uint8_t dest); + template + void applyFmacDest(Word instruction, float *dst, float *result, uint8_t dest); + template + void normalizeFmacResult(Word instruction, float *result, uint8_t dest, uint8_t laneFlags[4]); + template + bool calculateFmacExactResult(Word instruction, uint32_t component, long double &result) const; + uint8_t normalizeFmacExactResult(float &value, long double exactResult) const; + template + uint32_t calculateFmacProductSticky(Word instruction, uint8_t dest) const; + void updateFmacFlags(const uint8_t laneFlags[4], uint8_t dest, uint32_t extraSticky); + void queueFsset(uint16_t immediate); + void queueClip(uint32_t clip); + void queueFcset(uint32_t clip); + void queueQ(float value, uint32_t latency, uint32_t statusDi); + void queueP(float value, uint32_t latency); + void queueStore(uint32_t address, const uint32_t words[4], uint8_t laneMask); + void queueVfWrite(uint8_t reg, uint8_t laneMask, const float value[4], uint32_t latency); + void queueViWrite(uint8_t reg, int32_t value, uint32_t latency); + void queueAccWrite(uint8_t laneMask, const float value[4], uint32_t latency); + void startXgkick(uint32_t qwordAddress); + + template + Entry *allocatePipelineEntry(std::array &entries, uint32_t &active, uint64_t readyCycle) + { + static_assert(Capacity == ps2vu::PipelineEvents::capacity(Kind)); + const uint32_t slot = std::countr_zero(~active); + if (slot >= Capacity) + return nullptr; + m_pipelineEvents.schedule(m_cycle, static_cast(readyCycle - m_cycle), slot); + active |= 1u << slot; + Entry &entry = entries[slot]; + if constexpr (Kind == ps2vu::WritebackKind::Flag) + { + entry.valid = true; + entry.writesMac = entry.writesStatus = entry.writesSticky = entry.writesClip = false; + } + else if constexpr (Kind == ps2vu::WritebackKind::Efu) + { + entry.valid = true; + entry.readyCycle = readyCycle; + } + if (readyCycle < m_nextPipelineCycle) + m_nextPipelineCycle = readyCycle; + return &entry; + } + + void resetScheduler(); + void commitReadyPipelines(); + void advanceOneCycle(); + void advanceTo(uint64_t targetCycle); + void flushPipelines(); + void progressXgkick(uint32_t elapsedCycles = 1u); + void finishXgkick(); + uint64_t calculatePairReadyCycle(const DecodedInstructionPair &decoded) const; + void markPairWrites(const DecodedInstructionPair &decoded); + bool pipelinesPending() const; + + static float normalizeOperand(float value) + { + uint32_t bits = std::bit_cast(value); + const uint32_t exponent = bits & 0x7F800000u; + if (exponent == 0u) + bits &= 0x80000000u; + else if (exponent == 0x7F800000u) + bits = (bits & 0x80000000u) | 0x7F7FFFFFu; + return std::bit_cast(bits); + } + float normalizeResult(float value, uint32_t &laneFlags) const; + uint32_t microAddressMask() const; + int32_t readBranchVi(uint8_t reg) const; + void recordViWriteForBranch(uint8_t reg, int32_t oldValue); + void reportReservedInstruction(bool upper, uint32_t instruction); +}; + +#endif diff --git a/ps2xRuntime/include/runtime/vu/interpreter/vu_fmac.inl b/ps2xRuntime/include/runtime/vu/interpreter/vu_fmac.inl new file mode 100644 index 000000000..b4dcc7ca0 --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/interpreter/vu_fmac.inl @@ -0,0 +1,356 @@ +#pragma once +#include "vu_inline.h" +#include +#include +#include +#include + +inline float VUExecutor::normalizeResult(float value, uint32_t &laneFlags) const +{ + uint32_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + const uint32_t sign = bits & 0x80000000u; + const uint32_t magnitude = bits & 0x7FFFFFFFu; + const uint32_t exponent = (bits >> 23) & 0xFFu; + + laneFlags = sign != 0u ? 0x2u : 0u; + if (magnitude == 0u) + { + laneFlags |= 0x1u; + } + else if (exponent == 0u) + { + laneFlags |= 0x5u; + bits = sign; + } + else if (exponent == 0xFFu) + { + laneFlags |= 0x8u; + bits = sign | 0x7F7FFFFFu; + } + + std::memcpy(&value, &bits, sizeof(value)); + return value; +} + +inline void VUExecutor::applyDest(float *dst, const float *result, uint8_t dest) +{ + if (dest & 0x8u) + dst[0] = result[0]; + if (dest & 0x4u) + dst[1] = result[1]; + if (dest & 0x2u) + dst[2] = result[2]; + if (dest & 0x1u) + dst[3] = result[3]; +} + +template +inline void VUExecutor::normalizeFmacResult(Word instruction, float *result, uint8_t dest, uint8_t laneFlags[4]) +{ + for (uint32_t component = 0; component < 4u; ++component) + { + laneFlags[component] = 0u; + if ((dest & ps2vu::laneForComponent(component)) == 0u) + continue; + + long double exactResult = 0.0L; + if (calculateFmacExactResult(instruction, component, exactResult)) + { + laneFlags[component] = normalizeFmacExactResult(result[component], exactResult); + continue; + } + + uint32_t flags = 0u; + result[component] = normalizeResult(result[component], flags); + laneFlags[component] = static_cast(flags); + } +} + +template +inline bool VUExecutor::calculateFmacExactResult(Word instruction, uint32_t component, long double &result) const +{ + const uint32_t upper = instruction; + const uint8_t op = static_cast(upper & 0x3Fu); + const uint8_t special = op >= 0x3Cu ? static_cast((upper & 3u) | ((upper >> 4) & 0x7Cu)) : 0xFFu; + const auto vs = [this](uint32_t lane) { return static_cast(m_upperOperands.vs[lane]); }; + + const auto vt = [this](uint32_t lane) { return static_cast(m_upperOperands.vt[lane]); }; + + const auto acc = [this](uint32_t lane) { return static_cast(m_upperOperands.acc[lane]); }; + + const long double q = m_upperOperands.q; + const long double i = m_upperOperands.i; + + if (op < 0x3Cu) + { + if (op <= 0x03u) + result = vs(component) + vt(op & 3u); + else if (op <= 0x07u) + result = vs(component) - vt(op & 3u); + else if (op <= 0x0Bu) + result = acc(component) + vs(component) * vt(op & 3u); + else if (op <= 0x0Fu) + result = acc(component) - vs(component) * vt(op & 3u); + else if (op >= 0x18u && op <= 0x1Bu) + result = vs(component) * vt(op & 3u); + else + { + switch (op) + { + case 0x1Cu: + result = vs(component) * q; + break; + case 0x1Eu: + result = vs(component) * i; + break; + case 0x20u: + result = vs(component) + q; + break; + case 0x21u: + result = acc(component) + vs(component) * q; + break; + case 0x22u: + result = vs(component) + i; + break; + case 0x23u: + result = acc(component) + vs(component) * i; + break; + case 0x24u: + result = vs(component) - q; + break; + case 0x25u: + result = acc(component) - vs(component) * q; + break; + case 0x26u: + result = vs(component) - i; + break; + case 0x27u: + result = acc(component) - vs(component) * i; + break; + case 0x28u: + result = vs(component) + vt(component); + break; + case 0x29u: + result = acc(component) + vs(component) * vt(component); + break; + case 0x2Au: + result = vs(component) * vt(component); + break; + case 0x2Cu: + result = vs(component) - vt(component); + break; + case 0x2Du: + result = acc(component) - vs(component) * vt(component); + break; + case 0x2Eu: { + static constexpr uint8_t left[4] = {1u, 2u, 0u, 3u}; + static constexpr uint8_t right[4] = {2u, 0u, 1u, 3u}; + result = component == 3u ? 0.0L : acc(component) - vs(left[component]) * vt(right[component]); + break; + } + default: + return false; + } + } + return true; + } + + if (special <= 0x03u) + result = vs(component) + vt(special & 3u); + else if (special <= 0x07u) + result = vs(component) - vt(special & 3u); + else if (special <= 0x0Bu) + result = acc(component) + vs(component) * vt(special & 3u); + else if (special <= 0x0Fu) + result = acc(component) - vs(component) * vt(special & 3u); + else if (special >= 0x18u && special <= 0x1Bu) + result = vs(component) * vt(special & 3u); + else + { + switch (special) + { + case 0x1Cu: + result = vs(component) * q; + break; + case 0x1Eu: + result = vs(component) * i; + break; + case 0x20u: + result = vs(component) + q; + break; + case 0x21u: + result = acc(component) + vs(component) * q; + break; + case 0x22u: + result = vs(component) + i; + break; + case 0x23u: + result = acc(component) + vs(component) * i; + break; + case 0x24u: + result = vs(component) - q; + break; + case 0x25u: + result = acc(component) - vs(component) * q; + break; + case 0x26u: + result = vs(component) - i; + break; + case 0x27u: + result = acc(component) - vs(component) * i; + break; + case 0x28u: + result = vs(component) + vt(component); + break; + case 0x29u: + result = acc(component) + vs(component) * vt(component); + break; + case 0x2Au: + result = vs(component) * vt(component); + break; + case 0x2Cu: + result = vs(component) - vt(component); + break; + case 0x2Du: + result = acc(component) - vs(component) * vt(component); + break; + case 0x2Eu: { + static constexpr uint8_t left[4] = {1u, 2u, 0u, 3u}; + static constexpr uint8_t right[4] = {2u, 0u, 1u, 3u}; + result = component == 3u ? 0.0L : vs(left[component]) * vt(right[component]); + break; + } + default: + return false; + } + } + return true; +} + +inline uint8_t VUExecutor::normalizeFmacExactResult(float &value, long double exactResult) const +{ + const uint64_t exactBits = std::bit_cast(static_cast(exactResult)); + const bool negative = (exactBits >> 63) != 0; + const long double magnitude = negative ? -exactResult : exactResult; + const long double maximum = static_cast(std::numeric_limits::max()); + const long double minimum = static_cast(std::numeric_limits::min()); + uint8_t flags = negative ? 0x2u : 0u; + + uint32_t bits = negative ? 0x80000000u : 0u; + if (magnitude == 0.0L) + { + flags |= 0x1u; + std::memcpy(&value, &bits, sizeof(value)); + } + else if (magnitude > maximum) + { + flags |= 0x8u; + bits |= 0x7F7FFFFFu; + std::memcpy(&value, &bits, sizeof(value)); + } + else if (magnitude < minimum) + { + flags |= 0x5u; + std::memcpy(&value, &bits, sizeof(value)); + } + + return flags; +} + +template inline uint32_t VUExecutor::calculateFmacProductSticky(Word instruction, uint8_t dest) const +{ + uint32_t extraSticky = 0u; + const uint32_t upper = instruction; + const uint8_t op = static_cast(upper & 0x3Fu); + const uint8_t special = op >= 0x3Cu ? static_cast((upper & 3u) | ((upper >> 4) & 0x7Cu)) : 0xFFu; + const bool productSum = (op >= 0x08u && op <= 0x0Fu) || op == 0x21u || op == 0x23u || op == 0x25u || op == 0x27u || + op == 0x29u || op == 0x2Du || op == 0x2Eu || (special >= 0x08u && special <= 0x0Fu) || + special == 0x21u || special == 0x23u || special == 0x25u || special == 0x27u || + special == 0x29u || special == 0x2Du; + + if (!productSum) + return 0u; + + for (uint32_t component = 0; component < 4u; ++component) + { + if ((dest & ps2vu::laneForComponent(component)) == 0u) + continue; + + static constexpr uint8_t crossLeft[4] = {1u, 2u, 0u, 3u}; + static constexpr uint8_t crossRight[4] = {2u, 0u, 1u, 3u}; + + const uint8_t leftComponent = op == 0x2Eu ? crossLeft[component] : static_cast(component); + const float left = m_upperOperands.vs[leftComponent]; + float right = 0.0f; + if ((op >= 0x08u && op <= 0x0Fu) || (special >= 0x08u && special <= 0x0Fu)) + { + right = m_upperOperands.vt[(op >= 0x08u && op <= 0x0Fu ? op : special) & 3u]; + } + else if (op == 0x21u || op == 0x25u || special == 0x21u || special == 0x25u) + { + right = m_upperOperands.q; + } + else if (op == 0x23u || op == 0x27u || special == 0x23u || special == 0x27u) + { + right = m_upperOperands.i; + } + else if (op == 0x2Eu) + { + right = m_upperOperands.vt[crossRight[component]]; + } + else + { + right = m_upperOperands.vt[component]; + } + + float product = left * right; + const long double exactProduct = static_cast(left) * static_cast(right); + const uint8_t productFlags = normalizeFmacExactResult(product, exactProduct); + extraSticky |= productFlags & 0xFu; + } + return extraSticky; +} + +inline void VUExecutor::updateFmacFlags(const uint8_t laneFlags[4], uint8_t dest, uint32_t extraSticky) +{ + if (dest == 0u) + return; + + static constexpr auto flagPlanes = [] { + std::array table{}; + for (uint32_t flags = 0; flags < table.size(); ++flags) + for (uint32_t condition = 0; condition < 4u; ++condition) + table[flags] |= ((flags >> condition) & 1u) << (condition * 4u); + return table; + }(); + + const uint32_t x = (dest & 8u) ? laneFlags[0] : 0u; + const uint32_t y = (dest & 4u) ? laneFlags[1] : 0u; + const uint32_t z = (dest & 2u) ? laneFlags[2] : 0u; + const uint32_t w = (dest & 1u) ? laneFlags[3] : 0u; + const uint32_t mac = (flagPlanes[x] << 3) | (flagPlanes[y] << 2) | (flagPlanes[z] << 1) | flagPlanes[w]; + const uint32_t status = x | y | z | w; + + auto *entry = allocatePipelineEntry(m_flagPipeline, m_flagActive, m_cycle + kFmacLatency); + if (!entry) + { + reportReservedInstruction(true, 0xFFFFFFFFu); + return; + } + + entry->issueCycle = m_cycle; + entry->mac = mac; + entry->status = status; + entry->extraSticky = extraSticky; + entry->writesMac = true; + entry->writesStatus = true; +} + +template inline void VUExecutor::applyFmacDest(Word instruction, float *dst, float *result, uint8_t dest) +{ + uint8_t laneFlags[4]{}; + normalizeFmacResult(instruction, result, dest, laneFlags); + updateFmacFlags(laneFlags, dest, calculateFmacProductSticky(instruction, dest)); + applyDest(dst, result, dest); +} diff --git a/ps2xRuntime/include/runtime/vu/interpreter/vu_hazards.inl b/ps2xRuntime/include/runtime/vu/interpreter/vu_hazards.inl new file mode 100644 index 000000000..2ebb5f2a2 --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/interpreter/vu_hazards.inl @@ -0,0 +1,64 @@ +#pragma once +#include "vu_inline.h" +#include +#include +#include + +inline uint64_t VUExecutor::calculatePairReadyCycle(const DecodedInstructionPair &decoded) const +{ + uint64_t ready = m_cycle; + for (uint32_t i = 0; i < decoded.readDependencyCount; ++i) + ready = std::max(ready, m_registerReady[decoded.readDependencies[i]]); + + if (decoded.lowerUsage.pipeline == PipelineFdiv && m_fdiv.valid) + ready = std::max(ready, m_fdiv.readyCycle); + if (decoded.lowerUsage.pipeline == PipelineEfu) + ready = std::max(ready, m_efuResourceReady); + if (decoded.lowerUsage.waitQ && m_fdiv.valid) + ready = std::max(ready, m_fdiv.readyCycle); + if (decoded.lowerUsage.waitP) + { + for (const ScalarPipelineEntry &entry : m_efu) + if (entry.valid) + ready = std::max(ready, entry.readyCycle - 1u); + } + if (decoded.lowerUsage.pipeline == PipelineXgkick && m_xgkick.active) + ready = std::max(ready, m_cycle + 1u); + return ready; +} + +inline void VUExecutor::markPairWrites(const DecodedInstructionPair &decoded) +{ + const VfAccess lowerWrite = decoded.lowerUsage.vfWrite; + if (lowerWrite.reg != 0u && decoded.suppressedLowerVf != lowerWrite.reg) + { + const uint32_t latency = + decoded.lowerUsage.vfLatency != 0u ? decoded.lowerUsage.vfLatency : decoded.lowerUsage.latency; + for (uint32_t component = 0; component < 4u; ++component) + { + if ((lowerWrite.lanes & ps2vu::laneForComponent(component)) != 0u) + m_registerReady[4u * lowerWrite.reg + component] = m_cycle + latency; + } + } + + const VfAccess upperWrite = decoded.upperUsage.vfWrite; + if (upperWrite.reg != 0u) + { + const uint32_t latency = + decoded.upperUsage.vfLatency != 0u ? decoded.upperUsage.vfLatency : decoded.upperUsage.latency; + for (uint32_t component = 0; component < 4u; ++component) + { + if ((upperWrite.lanes & ps2vu::laneForComponent(component)) != 0u) + m_registerReady[4u * upperWrite.reg + component] = m_cycle + latency; + } + } + + for (uint32_t regs = decoded.lowerUsage.viWrite & ~1u; regs != 0u; regs &= regs - 1u) + m_registerReady[kViReadyBase + std::countr_zero(regs)] = + m_cycle + (decoded.lowerUsage.viLatency != 0u ? decoded.lowerUsage.viLatency : decoded.lowerUsage.latency); + for (uint32_t component = 0; component < 4u; ++component) + { + if ((decoded.upperUsage.accWrite & ps2vu::laneForComponent(component)) != 0u) + m_registerReady[kAccReadyBase + component] = m_cycle + kAccForwardLatency; + } +} diff --git a/ps2xRuntime/include/runtime/vu/interpreter/vu_inline.h b/ps2xRuntime/include/runtime/vu/interpreter/vu_inline.h new file mode 100644 index 000000000..32477ef9a --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/interpreter/vu_inline.h @@ -0,0 +1,6 @@ +#pragma once +#if defined(_MSC_VER) +#define PS2VU_INLINE __forceinline +#else +#define PS2VU_INLINE inline __attribute__((always_inline)) +#endif diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp b/ps2xRuntime/include/runtime/vu/interpreter/vu_lower.inl similarity index 72% rename from ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp rename to ps2xRuntime/include/runtime/vu/interpreter/vu_lower.inl index 94be78914..0a1433320 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp +++ b/ps2xRuntime/include/runtime/vu/interpreter/vu_lower.inl @@ -1,26 +1,15 @@ -#include "runtime/ps2_vu1.h" -#include "runtime/gs/ps2_gif_arbiter.h" -#include "runtime/gs/gs_frontend.h" -#include "runtime/ps2_memory.h" -#include "ps2_vu1_detail.h" - +#pragma once #include #include #include -namespace +namespace ps2vu { - float vuEatan(float value) - { - constexpr float coefficients[] = { - 0.999999344348907f, - -0.333298563957214f, - 0.199465364217758f, - -0.13085337519646f, - 0.096420042216778f, - -0.055909886956215f, - 0.021861229091883f, - -0.004054057877511f}; + inline float vuEatan(float value) + { + constexpr float coefficients[] = {0.999999344348907f, -0.333298563957214f, 0.199465364217758f, + -0.13085337519646f, 0.096420042216778f, -0.055909886956215f, + 0.021861229091883f, -0.004054057877511f}; constexpr float quarterPi = 0.785398185253143f; const float squared = value * value; @@ -30,14 +19,10 @@ namespace return quarterPi + value * polynomial; } - float vuEsin(float value) - { - constexpr float coefficients[] = { - 1.0f, - -0.166666567325592f, - 0.008333025500178f, - -0.000198074136279f, - 0.000002601886990f}; + inline float vuEsin(float value) + { + constexpr float coefficients[] = {1.0f, -0.166666567325592f, 0.008333025500178f, -0.000198074136279f, + 0.000002601886990f}; const float squared = value * value; float polynomial = coefficients[4]; @@ -46,15 +31,10 @@ namespace return value * polynomial; } - float vuEexp(float value) - { - constexpr float coefficients[] = { - 0.249998688697815f, - 0.031257584691048f, - 0.002591371303424f, - 0.000171562001924f, - 0.000005430199963f, - 0.000000690600018f}; + inline float vuEexp(float value) + { + constexpr float coefficients[] = {0.249998688697815f, 0.031257584691048f, 0.002591371303424f, + 0.000171562001924f, 0.000005430199963f, 0.000000690600018f}; float polynomial = coefficients[5]; for (int index = 4; index >= 0; --index) @@ -66,46 +46,44 @@ namespace } } -// ============================================================================ -// Lower instructions -// ============================================================================ -void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr) +template +inline void VUExecutor::execLower(Word instr, uint8_t *vuData, uint32_t dataSize, uint32_t upperInstr) { + using namespace ps2vu; (void)upperInstr; - if (instr == 0x00000000 || instr == 0x8000033C) // NOP + if (instr == 0x00000000 || instr == 0x8000033C) return; uint8_t opHi = (instr >> 25) & 0x7F; const uint32_t pcMask = microAddressMask(); - // The lower instruction encoding uses bits 31:25 for the primary opcode switch (opHi) { - case 0x00: // LQ (Load Quadword from VU data memory) + case 0x00: { - uint8_t it = FT(instr); // VF destination - uint8_t is = VIS(instr); // VI base + uint8_t it = vfTarget(instr); + uint8_t is = viSource(instr); uint8_t dest = (instr >> 21) & 0xF; - int16_t imm = IMM11(instr); + int16_t imm = immediate11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); + readData(addr, tmp); applyDest(m_state.vf[it], tmp, dest); } return; } - case 0x01: // SQ (Store Quadword to VU data memory) + case 0x01: { - uint8_t is = FS(instr); // VF source - uint8_t it = VIT(instr); // VI base + uint8_t is = vfSource(instr); + uint8_t it = viTarget(instr); uint8_t dest = (instr >> 21) & 0xF; - int16_t imm = IMM11(instr); + int16_t imm = immediate11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[it] + imm)) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { uint32_t words[4]{}; std::memcpy(words, m_state.vf[is], sizeof(words)); @@ -113,15 +91,15 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } return; } - case 0x04: // ILW (Integer Load Word from VU data memory) + case 0x04: { - uint8_t it = VIT(instr); // VI destination - uint8_t is = VIS(instr); // VI base + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); uint8_t dest = (instr >> 21) & 0xF; - int16_t imm = IMM11(instr); + int16_t imm = immediate11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { int comp = 0; if (dest & 0x8) @@ -133,21 +111,23 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz else comp = 3; uint32_t v; - std::memcpy(&v, vuData + addr + comp * 4, 4); + uint32_t words[4]; + readData(addr, words); + v = words[comp]; if (it != 0) m_state.vi[it] = (int32_t)(int16_t)(v & 0xFFFF); } return; } - case 0x05: // ISW (Integer Store Word to VU data memory) + case 0x05: { - uint8_t it = VIT(instr); // VI source - uint8_t is = VIS(instr); // VI base + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); uint8_t dest = (instr >> 21) & 0xF; - int16_t imm = IMM11(instr); + int16_t imm = immediate11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { const uint32_t val = static_cast(static_cast(m_state.vi[it] & 0xFFFF)); const uint32_t words[4] = {val, val, val, val}; @@ -155,124 +135,124 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } return; } - case 0x08: // IADDIU + case 0x08: { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); int16_t imm = (int16_t)(instr & 0x7FF) | ((instr >> 10) & 0x7800); if (it != 0) m_state.vi[it] = (int16_t)(m_state.vi[is] + imm); return; } - case 0x09: // ISUBIU + case 0x09: { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); int16_t imm = (int16_t)(instr & 0x7FF) | ((instr >> 10) & 0x7800); if (it != 0) m_state.vi[it] = (int16_t)(m_state.vi[is] - imm); return; } - case 0x10: // FCEQ + case 0x10: { uint32_t imm24 = instr & 0xFFFFFF; if (1 != 0) m_state.vi[1] = ((m_state.clip & 0xFFFFFF) == imm24) ? 1 : 0; return; } - case 0x11: // FCSET + case 0x11: { queueFcset(instr & 0xFFFFFFu); return; } - case 0x12: // FCAND + case 0x12: { uint32_t imm24 = instr & 0xFFFFFF; if (1 != 0) m_state.vi[1] = ((m_state.clip & imm24) != 0) ? 1 : 0; return; } - case 0x13: // FCOR + case 0x13: { uint32_t imm24 = instr & 0xFFFFFF; if (1 != 0) m_state.vi[1] = ((m_state.clip | imm24) == 0xFFFFFF) ? 1 : 0; return; } - case 0x14: // FSEQ + case 0x14: { - const uint8_t it = VIT(instr); + const uint8_t it = viTarget(instr); const uint16_t imm12 = static_cast((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu)); if (it != 0) m_state.vi[it] = ((m_state.status & 0xFFFu) == imm12) ? 1 : 0; return; } - case 0x15: // FSSET + case 0x15: { const uint16_t imm12 = static_cast((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu)); queueFsset(imm12); return; } - case 0x16: // FSAND + case 0x16: { - const uint8_t it = VIT(instr); + const uint8_t it = viTarget(instr); const uint16_t imm12 = static_cast((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu)); if (it != 0) m_state.vi[it] = static_cast((m_state.status & 0xFFFu) & imm12); return; } - case 0x17: // FSOR + case 0x17: { - const uint8_t it = VIT(instr); + const uint8_t it = viTarget(instr); const uint16_t imm12 = static_cast((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu)); if (it != 0) m_state.vi[it] = static_cast((m_state.status & 0xFFFu) | imm12); return; } - case 0x18: // FMEQ + case 0x18: { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); if (it != 0) m_state.vi[it] = ((m_state.mac & 0xFFFF) == (uint32_t)(uint16_t)m_state.vi[is]) ? 1 : 0; return; } - case 0x1A: // FMAND + case 0x1A: { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); if (it != 0) m_state.vi[it] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]); return; } - case 0x1B: // FMOR + case 0x1B: { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); if (it != 0) m_state.vi[it] = (int32_t)(m_state.mac | (uint32_t)(uint16_t)m_state.vi[is]); return; } - case 0x1C: // FCGET + case 0x1C: { - const uint8_t it = VIT(instr); + const uint8_t it = viTarget(instr); if (it != 0) m_state.vi[it] = static_cast(m_state.clip & 0x0FFFu); return; } - case 0x20: // B (unconditional branch) + case 0x20: { - int16_t imm = IMM11(instr); + int16_t imm = immediate11(instr); uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; m_state.branchPending = true; m_state.branchTarget = target; m_state.branchDelay = 1; return; } - case 0x21: // BAL (Branch and link) + case 0x21: { - uint8_t it = VIT(instr); - int16_t imm = IMM11(instr); + uint8_t it = viTarget(instr); + int16_t imm = immediate11(instr); uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; if (it != 0) m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); @@ -281,19 +261,19 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.branchDelay = 1; return; } - case 0x24: // JR + case 0x24: { - uint8_t is = VIS(instr); + uint8_t is = viSource(instr); uint32_t target = ((uint32_t)(uint16_t)readBranchVi(is) * 8u) & pcMask; m_state.branchPending = true; m_state.branchTarget = target; m_state.branchDelay = 1; return; } - case 0x25: // JALR + case 0x25: { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); uint32_t target = ((uint32_t)(uint16_t)readBranchVi(is) * 8u) & pcMask; if (it != 0) m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); @@ -302,11 +282,11 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.branchDelay = 1; return; } - case 0x28: // IBEQ + case 0x28: { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); + int16_t imm = immediate11(instr); if ((int16_t)readBranchVi(is) == (int16_t)readBranchVi(it)) { uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; @@ -316,11 +296,11 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } return; } - case 0x29: // IBNE + case 0x29: { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); + uint8_t it = viTarget(instr); + uint8_t is = viSource(instr); + int16_t imm = immediate11(instr); if ((int16_t)readBranchVi(is) != (int16_t)readBranchVi(it)) { uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; @@ -330,10 +310,10 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } return; } - case 0x2C: // IBLTZ + case 0x2C: { - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); + uint8_t is = viSource(instr); + int16_t imm = immediate11(instr); if ((int16_t)readBranchVi(is) < 0) { uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; @@ -343,10 +323,10 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } return; } - case 0x2D: // IBGTZ + case 0x2D: { - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); + uint8_t is = viSource(instr); + int16_t imm = immediate11(instr); if ((int16_t)readBranchVi(is) > 0) { uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; @@ -356,10 +336,10 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } return; } - case 0x2E: // IBLEZ + case 0x2E: { - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); + uint8_t is = viSource(instr); + int16_t imm = immediate11(instr); if ((int16_t)readBranchVi(is) <= 0) { uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; @@ -369,10 +349,10 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } return; } - case 0x2F: // IBGEZ + case 0x2F: { - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); + uint8_t is = viSource(instr); + int16_t imm = immediate11(instr); if ((int16_t)readBranchVi(is) >= 0) { uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; @@ -383,38 +363,38 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz return; } - case 0x40: // Lower1 / lower special. Bit31 set; low 6 bits select integer or special op. + case 0x40: { const uint8_t funct = instr & 0x3Fu; - const uint8_t vfT = FT(instr); - const uint8_t vfS = FS(instr); - const uint8_t viT = VIT(instr); - const uint8_t viS = VIS(instr); - const uint8_t viD = VID(instr); + const uint8_t vfT = vfTarget(instr); + const uint8_t vfS = vfSource(instr); + const uint8_t viT = viTarget(instr); + const uint8_t viS = viSource(instr); + const uint8_t viD = viDestination(instr); const uint8_t dest = (instr >> 21) & 0xF; switch (funct) { - case 0x30: // IADD + case 0x30: if (viD != 0) m_state.vi[viD] = (int16_t)(m_state.vi[viS] + m_state.vi[viT]); return; - case 0x31: // ISUB + case 0x31: if (viD != 0) m_state.vi[viD] = (int16_t)(m_state.vi[viS] - m_state.vi[viT]); return; - case 0x32: // IADDI + case 0x32: { int16_t imm5 = (int16_t)((int32_t)((instr >> 6) & 0x1F) << 27 >> 27); if (viT != 0) m_state.vi[viT] = (int16_t)(m_state.vi[viS] + imm5); return; } - case 0x34: // IAND + case 0x34: if (viD != 0) m_state.vi[viD] = m_state.vi[viS] & m_state.vi[viT]; return; - case 0x35: // IOR + case 0x35: if (viD != 0) m_state.vi[viD] = m_state.vi[viS] | m_state.vi[viT]; return; @@ -422,43 +402,43 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz case 0x3C: case 0x3D: case 0x3E: - case 0x3F: // Lower1 special. Dobie decodes this as (instr & 3) | ((instr >> 4) & 0x7C). + case 0x3F: { const uint8_t funct2 = (uint8_t)((instr & 0x3u) | ((instr >> 4) & 0x7Cu)); switch (funct2) { - case 0x30: // MOVE + case 0x30: { float tmp[4]; std::memcpy(tmp, m_state.vf[vfS], 16); applyDest(m_state.vf[vfT], tmp, dest); return; } - case 0x31: // MR32 (rotate right by 32 bits = shift xyzw -> yzwx) + case 0x31: { float tmp[4] = {m_state.vf[vfS][1], m_state.vf[vfS][2], m_state.vf[vfS][3], m_state.vf[vfS][0]}; applyDest(m_state.vf[vfT], tmp, dest); return; } - case 0x34: // LQI (Load Quadword, post-increment) + case 0x34: { uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); + readData(addr, tmp); applyDest(m_state.vf[vfT], tmp, dest); } if (viS != 0) m_state.vi[viS] = (int16_t)(m_state.vi[viS] + 1); return; } - case 0x35: // SQI (Store Quadword, post-increment) + case 0x35: { uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viT]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { uint32_t words[4]{}; std::memcpy(words, m_state.vf[vfS], sizeof(words)); @@ -468,27 +448,27 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.vi[viT] = (int16_t)(m_state.vi[viT] + 1); return; } - case 0x36: // LQD (Load Quadword, pre-decrement) + case 0x36: { if (viS != 0) m_state.vi[viS] = (int16_t)(m_state.vi[viS] - 1); uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); + readData(addr, tmp); applyDest(m_state.vf[vfT], tmp, dest); } return; } - case 0x37: // SQD (Store Quadword, pre-decrement) + case 0x37: { if (viT != 0) m_state.vi[viT] = (int16_t)(m_state.vi[viT] - 1); uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viT]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { uint32_t words[4]{}; std::memcpy(words, m_state.vf[vfS], sizeof(words)); @@ -496,7 +476,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } return; } - case 0x38: // DIV + case 0x38: { int fsf = (instr >> 21) & 0x3; int ftf = (instr >> 23) & 0x3; @@ -507,9 +487,8 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz if (den == 0.0f) { statusDi = num == 0.0f ? 0x10u : 0x20u; - result = std::signbit(num) != std::signbit(den) - ? -std::numeric_limits::max() - : std::numeric_limits::max(); + result = std::signbit(num) != std::signbit(den) ? -std::numeric_limits::max() + : std::numeric_limits::max(); } else { @@ -520,15 +499,14 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz queueQ(result, 7u, statusDi); return; } - case 0x39: // SQRT + case 0x39: { int ftf = (instr >> 23) & 0x3; const float val = normalizeOperand(m_state.vf[vfT][ftf]); - queueQ(std::sqrt(std::fabs(val)), 7u, - val < 0.0f ? 0x10u : 0u); + queueQ(std::sqrt(std::fabs(val)), 7u, val < 0.0f ? 0x10u : 0u); return; } - case 0x3A: // RSQRT + case 0x3A: { int fsf = (instr >> 21) & 0x3; int ftf = (instr >> 23) & 0x3; @@ -542,21 +520,18 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz else { statusDi = num == 0.0f ? 0x10u : 0x20u; - result = std::signbit(num) - ? -std::numeric_limits::max() - : std::numeric_limits::max(); + result = std::signbit(num) ? -std::numeric_limits::max() : std::numeric_limits::max(); } uint32_t ignoredFlags = 0u; result = normalizeResult(result, ignoredFlags); queueQ(result, 13u, statusDi); return; } - case 0x3B: // WAITQ + case 0x3B: return; - case 0x3C: // MTIR (Move To Integer Register) + case 0x3C: { - // MTIR encodes a two-bit fsf component selector in bits - // 22:21. It is not a four-bit destination mask. + const uint32_t comp = (instr >> 21) & 0x3u; uint32_t fval; std::memcpy(&fval, &m_state.vf[vfS][comp], 4); @@ -564,7 +539,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.vi[viT] = (int32_t)(int16_t)(fval & 0xFFFF); return; } - case 0x3D: // MFIR (Move From Integer Register) + case 0x3D: { float result[4]; int32_t val = (int32_t)(int16_t)(m_state.vi[viS] & 0xFFFF); @@ -575,11 +550,11 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz applyDest(m_state.vf[vfT], result, dest); return; } - case 0x3E: // ILWR - integer load word from address in VI[is] + case 0x3E: { uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { int comp = 0; if (dest & 0x8) @@ -591,26 +566,27 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz else comp = 3; uint32_t v; - std::memcpy(&v, vuData + addr + comp * 4, 4); + uint32_t words[4]; + readData(addr, words); + v = words[comp]; if (viT != 0) m_state.vi[viT] = (int32_t)(int16_t)(v & 0xFFFF); } return; } - case 0x3F: // ISWR - integer store word to address in VI[is] + case 0x3F: { uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) + addr &= m_unit == Unit::VU1 ? dataSize - 1 : 0x4fffu; + if (addr + 16 <= dataSize || (m_unit == Unit::VU0 && (addr & 0x4000u))) { - const uint32_t val = - static_cast(static_cast(m_state.vi[viT] & 0xFFFF)); + const uint32_t val = static_cast(static_cast(m_state.vi[viT] & 0xFFFF)); const uint32_t words[4] = {val, val, val, val}; queueStore(addr, words, dest); } return; } - case 0x40: // RNEXT + case 0x40: { const uint32_t x = (m_state.r >> 4) & 1u; const uint32_t y = (m_state.r >> 22) & 1u; @@ -622,7 +598,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz applyDest(m_state.vf[vfT], result, dest); return; } - case 0x41: // RGET + case 0x41: { float value = 0.0f; std::memcpy(&value, &m_state.r, sizeof(value)); @@ -630,7 +606,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz applyDest(m_state.vf[vfT], result, dest); return; } - case 0x42: // RINIT + case 0x42: { const uint32_t component = (instr >> 21) & 3u; uint32_t bits = 0u; @@ -638,7 +614,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.r = 0x3F800000u | (bits & 0x007FFFFFu); return; } - case 0x43: // RXOR + case 0x43: { const uint32_t component = (instr >> 21) & 3u; uint32_t bits = 0u; @@ -646,28 +622,28 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.r = 0x3F800000u | ((m_state.r ^ bits) & 0x007FFFFFu); return; } - case 0x64: // MFP (Move From P register) + case 0x64: { float result[4] = {m_state.p, m_state.p, m_state.p, m_state.p}; applyDest(m_state.vf[vfT], result, dest); return; } - case 0x68: // XTOP - move current VIF1 TOP into VI register + case 0x68: { if (viT != 0) m_state.vi[viT] = (int32_t)(m_state.top & 0x3FFu); return; } - case 0x69: // XITOP - move current VIF1 ITOP into VI register + case 0x69: { if (viT != 0) m_state.vi[viT] = (int32_t)(m_state.itop & 0x3FFu); return; } - case 0x6C: // XGKICK - send GIF packet from VU1 data memory + case 0x6C: startXgkick(static_cast(static_cast(m_state.vi[viS]))); return; - case 0x70: // ESADD + case 0x70: { const float x = normalizeOperand(m_state.vf[vfS][0]); const float y = normalizeOperand(m_state.vf[vfS][1]); @@ -675,7 +651,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz queueP(x * x + y * y + z * z, 11u); return; } - case 0x71: // ERSADD + case 0x71: { const float x = normalizeOperand(m_state.vf[vfS][0]); const float y = normalizeOperand(m_state.vf[vfS][1]); @@ -684,7 +660,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz queueP(sum != 0.0f ? 1.0f / sum : sum, 18u); return; } - case 0x72: // ELENG + case 0x72: { const float x = normalizeOperand(m_state.vf[vfS][0]); const float y = normalizeOperand(m_state.vf[vfS][1]); @@ -692,7 +668,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz queueP(std::sqrt(x * x + y * y + z * z), 18u); return; } - case 0x73: // ERLENG + case 0x73: { const float x = normalizeOperand(m_state.vf[vfS][0]); const float y = normalizeOperand(m_state.vf[vfS][1]); @@ -701,21 +677,21 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz queueP(len != 0.0f ? 1.0f / len : len, 24u); return; } - case 0x74: // EATANxy + case 0x74: { const float x = normalizeOperand(m_state.vf[vfS][0]); const float y = normalizeOperand(m_state.vf[vfS][1]); queueP(x != 0.0f ? vuEatan(y / x) : 0.0f, 54u); return; } - case 0x75: // EATANxz + case 0x75: { const float x = normalizeOperand(m_state.vf[vfS][0]); const float z = normalizeOperand(m_state.vf[vfS][2]); queueP(x != 0.0f ? vuEatan(z / x) : 0.0f, 54u); return; } - case 0x76: // ESUM + case 0x76: { float sum = 0.0f; for (uint32_t component = 0; component < 4u; ++component) @@ -723,7 +699,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz queueP(sum, 12u); return; } - case 0x77: // ERSQRT + case 0x77: { const uint32_t component = (instr >> 21) & 3u; const float value = normalizeOperand(m_state.vf[vfS][component]); @@ -737,36 +713,36 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz queueP(result, 18u); return; } - case 0x78: // ESQRT + case 0x78: { const uint32_t component = (instr >> 21) & 3u; const float value = normalizeOperand(m_state.vf[vfS][component]); queueP(value >= 0.0f ? std::sqrt(value) : value, 12u); return; } - case 0x79: // ESIN + case 0x79: { const uint32_t component = (instr >> 21) & 3u; const float value = normalizeOperand(m_state.vf[vfS][component]); queueP(vuEsin(value), 29u); return; } - case 0x7A: // ERCPR + case 0x7A: { const uint32_t component = (instr >> 21) & 3u; const float value = normalizeOperand(m_state.vf[vfS][component]); queueP(value != 0.0f ? 1.0f / value : value, 12u); return; } - case 0x7B: // WAITP + case 0x7B: return; - case 0x7C: // EATAN + case 0x7C: { const uint32_t component = (instr >> 21) & 3u; queueP(vuEatan(normalizeOperand(m_state.vf[vfS][component])), 54u); return; } - case 0x7D: // EEXP + case 0x7D: { const uint32_t component = (instr >> 21) & 3u; queueP(vuEexp(normalizeOperand(m_state.vf[vfS][component])), 44u); diff --git a/ps2xRuntime/include/runtime/vu/interpreter/vu_pair.inl b/ps2xRuntime/include/runtime/vu/interpreter/vu_pair.inl new file mode 100644 index 000000000..a2843c9d6 --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/interpreter/vu_pair.inl @@ -0,0 +1,161 @@ +#pragma once +#include +#include + +#include "vu_inline.h" + +template +PS2VU_INLINE bool VUExecutor::issuePair(const DecodedInstructionPair &decoded, + Upper upper, + Lower lower, + uint32_t codeSize, + uint64_t budgetEnd) +{ + commitReadyPipelines(); + if (decoded.upperUsage.reserved || decoded.lowerUsage.reserved) + { + reportReservedInstruction(decoded.upperUsage.reserved, + decoded.upperUsage.reserved ? decoded.upper : decoded.lower); + return false; + } + + uint64_t readyCycle = calculatePairReadyCycle(decoded); + while (readyCycle > m_cycle) + { + if (readyCycle >= budgetEnd) + { + advanceTo(budgetEnd); + return false; + } + advanceTo(readyCycle); + readyCycle = calculatePairReadyCycle(decoded); + } + + if (m_cycle >= budgetEnd) + return false; + + if (decoded.mBit) + m_state.mBit = true; + + uint8_t writtenVi = 0u; + int32_t oldVi = 0; + const uint32_t viWrites = decoded.lowerUsage.viWrite & ~1u; + if (viWrites != 0u) + { + writtenVi = static_cast(std::countr_zero(viWrites)); + oldVi = m_state.vi[writtenVi]; + } + + const VfAccess upperWrite = decoded.upperUsage.vfWrite; + const VfAccess lowerWrite = decoded.lowerUsage.vfWrite; + const bool hasUpperWrite = upperWrite.reg != 0u; + const bool hasLowerWrite = lowerWrite.reg != 0u && decoded.suppressedLowerVf != lowerWrite.reg; + float upperResult[4]{}; + float oldLowerVf[4]{}; + if (lowerWrite.reg != 0u) + std::memcpy(oldLowerVf, m_state.vf[lowerWrite.reg], sizeof(oldLowerVf)); + + upper(hasUpperWrite ? upperResult : m_state.vf[0], upperResult); + if (decoded.iBit) + { + float immediate = 0.0f; + std::memcpy(&immediate, &decoded.lower, sizeof(immediate)); + m_state.i = normalizeOperand(immediate); + } + else + lower(); + + m_viBranchBackupValid = false; + + if (hasUpperWrite) + { + const uint32_t latency = + decoded.upperUsage.vfLatency != 0u ? decoded.upperUsage.vfLatency : decoded.upperUsage.latency; + queueVfWrite(upperWrite.reg, upperWrite.lanes, upperResult, latency); + } + + if (lowerWrite.reg != 0u) + { + if (hasLowerWrite) + { + const uint32_t latency = + decoded.lowerUsage.vfLatency != 0u ? decoded.lowerUsage.vfLatency : decoded.lowerUsage.latency; + queueVfWrite(lowerWrite.reg, lowerWrite.lanes, m_state.vf[lowerWrite.reg], latency); + } + + std::memcpy(m_state.vf[lowerWrite.reg], oldLowerVf, sizeof(oldLowerVf)); + } + + if (decoded.upperUsage.accWrite != 0u) + { + queueAccWrite(decoded.upperUsage.accWrite, upperResult, kAccForwardLatency); + } + + if (writtenVi != 0u) + { + const int32_t newVi = m_state.vi[writtenVi]; + m_state.vi[writtenVi] = oldVi; + const uint32_t latency = + decoded.lowerUsage.viLatency != 0u ? decoded.lowerUsage.viLatency : decoded.lowerUsage.latency; + queueViWrite(writtenVi, newVi, latency); + } + + markPairWrites(decoded); + if (writtenVi != 0u && decoded.lowerUsage.delaysNextBranchRead) + recordViWriteForBranch(writtenVi, oldVi); + + m_state.vf[0][0] = 0.0f; + m_state.vf[0][1] = 0.0f; + m_state.vf[0][2] = 0.0f; + m_state.vf[0][3] = 1.0f; + m_state.vi[0] = 0; + + uint32_t nextPc = m_state.pc + 8u; + if (nextPc >= codeSize) + nextPc = 0u; + m_state.pc = nextPc; + + if (m_state.branchPending) + { + if (m_state.branchDelay == 0u) + { + m_state.pc = m_state.branchTarget & microAddressMask(); + m_state.branchPending = false; + } + else + { + --m_state.branchDelay; + } + } + + const bool dHalt = decoded.dBit && m_state.dBitEnabled; + const bool tHalt = decoded.tBit && m_state.tBitEnabled; + const bool haltBit = dHalt || tHalt; + const bool haltBranch = haltBit && decoded.lowerUsage.pipeline == PipelineBranch; + + if (m_state.haltAfterDelaySlot) + { + m_state.stoppedByD = m_pendingHaltD; + m_state.stoppedByT = m_pendingHaltT; + m_programEnded = true; + } + else if (m_state.ebit) + m_programEnded = true; + else if (haltBit && !haltBranch) + { + m_state.stoppedByD = dHalt; + m_state.stoppedByT = tHalt; + m_programEnded = true; + } + else if (decoded.eBit) + m_state.ebit = true; + else if (haltBranch) + { + m_state.haltAfterDelaySlot = true; + m_pendingHaltD = dHalt; + m_pendingHaltT = tHalt; + } + + advanceOneCycle(); + return !m_programEnded && m_cycle < budgetEnd && !m_stopRequested; +} diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp b/ps2xRuntime/include/runtime/vu/interpreter/vu_upper.inl similarity index 65% rename from ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp rename to ps2xRuntime/include/runtime/vu/interpreter/vu_upper.inl index 69742546f..08e4c1ace 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp +++ b/ps2xRuntime/include/runtime/vu/interpreter/vu_upper.inl @@ -1,13 +1,11 @@ -#include "runtime/ps2_vu1.h" -#include "ps2_vu1_detail.h" - +#pragma once #include #include #include -namespace +namespace ps2vu { - int32_t vuFloatToInt(float value, float scale) + inline int32_t vuFloatToInt(float value, float scale) { const double scaled = static_cast(value) * static_cast(scale); if (scaled >= static_cast(std::numeric_limits::max())) @@ -18,88 +16,80 @@ namespace } } -// ============================================================================ -// Upper instructions (FMAC pipeline) -// ============================================================================ -void VU1Interpreter::execUpper(uint32_t instr) +template inline void VUExecutor::execUpper(Word instr, float *vfResult, float *accResult) { - m_currentUpperInstruction = instr; - uint8_t dest = DEST(instr); - uint8_t ft = FT(instr); - uint8_t fs = FS(instr); - uint8_t fd = FD(instr); + using namespace ps2vu; + uint8_t dest = destMask(instr); + uint8_t ft = vfTarget(instr); + uint8_t fs = vfSource(instr); uint8_t op = instr & 0x3F; - float *vd = m_state.vf[fd]; - float normalizedVs[4]; - float normalizedVt[4]; - float normalizedAcc[4]; + const uint8_t special = static_cast((instr & 3u) | ((instr >> 4) & 0x7Cu)); + if (op >= 0x3Cu && (special == 0x2Fu || special == 0x30u)) + return; + + float *vd = vfResult; + auto &operands = m_upperOperands; for (uint32_t component = 0; component < 4u; ++component) { - normalizedVs[component] = normalizeOperand(m_state.vf[fs][component]); - normalizedVt[component] = normalizeOperand(m_state.vf[ft][component]); - normalizedAcc[component] = normalizeOperand(m_state.acc[component]); + operands.vs[component] = normalizeOperand(m_state.vf[fs][component]); + operands.vt[component] = normalizeOperand(m_state.vf[ft][component]); + operands.acc[component] = normalizeOperand(m_state.acc[component]); } - const float *vs = normalizedVs; - const float *vt = normalizedVt; - const float *acc = normalizedAcc; - const float q = normalizeOperand(m_state.q); - const float i = normalizeOperand(m_state.i); + const float *vs = operands.vs; + const float *vt = operands.vt; + const float *acc = operands.acc; + const float q = operands.q = normalizeOperand(m_state.q); + const float i = operands.i = normalizeOperand(m_state.i); float result[4]; - // Upper opcode decoding (bits 5:0 of upper word) switch (op) { case 0x00: case 0x01: case 0x02: - case 0x03: // ADDbc - { - float bc = broadcast(vt, op & 3); + case 0x03: { + float bc = vt[op & 3]; for (int c = 0; c < 4; c++) result[c] = vs[c] + bc; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; } case 0x04: case 0x05: case 0x06: - case 0x07: // SUBbc - { - float bc = broadcast(vt, op & 3); + case 0x07: { + float bc = vt[op & 3]; for (int c = 0; c < 4; c++) result[c] = vs[c] - bc; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; } case 0x08: case 0x09: case 0x0A: - case 0x0B: // MADDbc - { - float bc = broadcast(vt, op & 3); + case 0x0B: { + float bc = vt[op & 3]; for (int c = 0; c < 4; c++) result[c] = acc[c] + vs[c] * bc; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; } case 0x0C: case 0x0D: case 0x0E: - case 0x0F: // MSUBbc - { - float bc = broadcast(vt, op & 3); + case 0x0F: { + float bc = vt[op & 3]; for (int c = 0; c < 4; c++) result[c] = acc[c] - vs[c] * bc; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; } case 0x10: case 0x11: case 0x12: - case 0x13: // MAXbc - { - float bc = broadcast(vt, op & 3); + case 0x13: { + float bc = vt[op & 3]; for (int c = 0; c < 4; c++) result[c] = (vs[c] > bc) ? vs[c] : bc; applyDest(vd, result, dest); @@ -108,9 +98,8 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x14: case 0x15: case 0x16: - case 0x17: // MINIbc - { - float bc = broadcast(vt, op & 3); + case 0x17: { + float bc = vt[op & 3]; for (int c = 0; c < 4; c++) result[c] = (vs[c] < bc) ? vs[c] : bc; applyDest(vd, result, dest); @@ -119,176 +108,166 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x18: case 0x19: case 0x1A: - case 0x1B: // MULbc - { - float bc = broadcast(vt, op & 3); + case 0x1B: { + float bc = vt[op & 3]; for (int c = 0; c < 4; c++) result[c] = vs[c] * bc; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; } - case 0x1C: // MULq + case 0x1C: for (int c = 0; c < 4; c++) result[c] = vs[c] * q; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x1D: // MAXi + case 0x1D: for (int c = 0; c < 4; c++) result[c] = (vs[c] > i) ? vs[c] : i; applyDest(vd, result, dest); return; - case 0x1E: // MULi + case 0x1E: for (int c = 0; c < 4; c++) result[c] = vs[c] * i; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x1F: // MINIi + case 0x1F: for (int c = 0; c < 4; c++) result[c] = (vs[c] < i) ? vs[c] : i; applyDest(vd, result, dest); return; - case 0x20: // ADDq + case 0x20: for (int c = 0; c < 4; c++) result[c] = vs[c] + q; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x21: // MADDq + case 0x21: for (int c = 0; c < 4; c++) result[c] = acc[c] + vs[c] * q; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x22: // ADDi + case 0x22: for (int c = 0; c < 4; c++) result[c] = vs[c] + i; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x23: // MADDi + case 0x23: for (int c = 0; c < 4; c++) result[c] = acc[c] + vs[c] * i; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x24: // SUBq + case 0x24: for (int c = 0; c < 4; c++) result[c] = vs[c] - q; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x25: // MSUBq + case 0x25: for (int c = 0; c < 4; c++) result[c] = acc[c] - vs[c] * q; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x26: // SUBi + case 0x26: for (int c = 0; c < 4; c++) result[c] = vs[c] - i; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x27: // MSUBi + case 0x27: for (int c = 0; c < 4; c++) result[c] = acc[c] - vs[c] * i; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x28: // ADD + case 0x28: for (int c = 0; c < 4; c++) result[c] = vs[c] + vt[c]; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x29: // MADD + case 0x29: for (int c = 0; c < 4; c++) result[c] = acc[c] + vs[c] * vt[c]; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x2A: // MUL + case 0x2A: for (int c = 0; c < 4; c++) result[c] = vs[c] * vt[c]; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x2B: // MAX + case 0x2B: for (int c = 0; c < 4; c++) result[c] = (vs[c] > vt[c]) ? vs[c] : vt[c]; applyDest(vd, result, dest); return; - case 0x2C: // SUB + case 0x2C: for (int c = 0; c < 4; c++) result[c] = vs[c] - vt[c]; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x2D: // MSUB + case 0x2D: for (int c = 0; c < 4; c++) result[c] = acc[c] - vs[c] * vt[c]; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x2E: // OPMSUB + case 0x2E: result[0] = acc[0] - vs[1] * vt[2]; result[1] = acc[1] - vs[2] * vt[0]; result[2] = acc[2] - vs[0] * vt[1]; result[3] = 0.0f; - applyFmacDest(vd, result, dest); + applyFmacDest(instr, vd, result, dest); return; - case 0x2F: // MINI + case 0x2F: for (int c = 0; c < 4; c++) result[c] = (vs[c] < vt[c]) ? vs[c] : vt[c]; applyDest(vd, result, dest); return; - // Upper special group (low op 0x3C..0x3F). - // Like lower1 special, the real selector is not just bits 5:0. Dobie decodes: - // op = (instr & 0x3) | ((instr >> 4) & 0x7C) - // Several instructions in this group also use FT as the destination, not FD. case 0x3C: case 0x3D: case 0x3E: - case 0x3F: - { + case 0x3F: { const uint8_t specialOp = static_cast((instr & 0x3u) | ((instr >> 4) & 0x7Cu)); - float *vtDest = m_state.vf[ft]; + float *vtDest = vfResult; switch (specialOp) { case 0x00: case 0x01: case 0x02: - case 0x03: // ADDAbc - { - float bc = broadcast(vt, specialOp & 3); + case 0x03: { + float bc = vt[specialOp & 3]; for (int c = 0; c < 4; c++) result[c] = vs[c] + bc; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; } case 0x04: case 0x05: case 0x06: - case 0x07: // SUBAbc - { - float bc = broadcast(vt, specialOp & 3); + case 0x07: { + float bc = vt[specialOp & 3]; for (int c = 0; c < 4; c++) result[c] = vs[c] - bc; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; } case 0x08: case 0x09: case 0x0A: - case 0x0B: // MADDAbc - { - float bc = broadcast(vt, specialOp & 3); + case 0x0B: { + float bc = vt[specialOp & 3]; for (int c = 0; c < 4; c++) result[c] = acc[c] + vs[c] * bc; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; } case 0x0C: case 0x0D: case 0x0E: - case 0x0F: // MSUBAbc - { - float bc = broadcast(vt, specialOp & 3); + case 0x0F: { + float bc = vt[specialOp & 3]; for (int c = 0; c < 4; c++) result[c] = acc[c] - vs[c] * bc; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; } - case 0x10: // ITOF0 + case 0x10: for (int c = 0; c < 4; c++) { int32_t iv; @@ -297,7 +276,7 @@ void VU1Interpreter::execUpper(uint32_t instr) } applyDest(vtDest, result, dest); return; - case 0x11: // ITOF4 + case 0x11: for (int c = 0; c < 4; c++) { int32_t iv; @@ -306,7 +285,7 @@ void VU1Interpreter::execUpper(uint32_t instr) } applyDest(vtDest, result, dest); return; - case 0x12: // ITOF12 + case 0x12: for (int c = 0; c < 4; c++) { int32_t iv; @@ -315,7 +294,7 @@ void VU1Interpreter::execUpper(uint32_t instr) } applyDest(vtDest, result, dest); return; - case 0x13: // ITOF15 + case 0x13: for (int c = 0; c < 4; c++) { int32_t iv; @@ -324,7 +303,7 @@ void VU1Interpreter::execUpper(uint32_t instr) } applyDest(vtDest, result, dest); return; - case 0x14: // FTOI0 + case 0x14: for (int c = 0; c < 4; c++) { int32_t iv = vuFloatToInt(vs[c], 1.0f); @@ -332,7 +311,7 @@ void VU1Interpreter::execUpper(uint32_t instr) } applyDest(vtDest, result, dest); return; - case 0x15: // FTOI4 + case 0x15: for (int c = 0; c < 4; c++) { int32_t iv = vuFloatToInt(vs[c], 16.0f); @@ -340,7 +319,7 @@ void VU1Interpreter::execUpper(uint32_t instr) } applyDest(vtDest, result, dest); return; - case 0x16: // FTOI12 + case 0x16: for (int c = 0; c < 4; c++) { int32_t iv = vuFloatToInt(vs[c], 4096.0f); @@ -348,7 +327,7 @@ void VU1Interpreter::execUpper(uint32_t instr) } applyDest(vtDest, result, dest); return; - case 0x17: // FTOI15 + case 0x17: for (int c = 0; c < 4; c++) { int32_t iv = vuFloatToInt(vs[c], 32768.0f); @@ -359,37 +338,34 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x18: case 0x19: case 0x1A: - case 0x1B: // MULAbc - { - float bc = broadcast(vt, specialOp & 3); + case 0x1B: { + float bc = vt[specialOp & 3]; for (int c = 0; c < 4; c++) result[c] = vs[c] * bc; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; } - case 0x1C: // MULAq + case 0x1C: for (int c = 0; c < 4; c++) result[c] = vs[c] * q; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x1D: // ABS + case 0x1D: for (int c = 0; c < 4; c++) result[c] = std::fabs(vs[c]); applyDest(vtDest, result, dest); return; - case 0x1E: // MULAi + case 0x1E: for (int c = 0; c < 4; c++) result[c] = vs[c] * i; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x1F: // CLIP - { + case 0x1F: { uint32_t wBits = 0u; std::memcpy(&wBits, &m_state.vf[ft][3], sizeof(wBits)); const int32_t limit = (wBits & 0x7F800000u) != 0u ? static_cast(wBits & 0x7FFFFFFFu) : 0x007FFFFF; - const auto exceedsClipPlane = [limit](float value, uint32_t signMask) - { + const auto exceedsClipPlane = [limit](float value, uint32_t signMask) { uint32_t bits = 0u; std::memcpy(&bits, &value, sizeof(bits)); bits ^= signMask; @@ -414,80 +390,80 @@ void VU1Interpreter::execUpper(uint32_t instr) queueClip(flags); return; } - case 0x20: // ADDAq + case 0x20: for (int c = 0; c < 4; c++) result[c] = vs[c] + q; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x21: // MADDAq + case 0x21: for (int c = 0; c < 4; c++) result[c] = acc[c] + vs[c] * q; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x22: // ADDAi + case 0x22: for (int c = 0; c < 4; c++) result[c] = vs[c] + i; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x23: // MADDAi + case 0x23: for (int c = 0; c < 4; c++) result[c] = acc[c] + vs[c] * i; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x24: // SUBAq + case 0x24: for (int c = 0; c < 4; c++) result[c] = vs[c] - q; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x25: // MSUBAq + case 0x25: for (int c = 0; c < 4; c++) result[c] = acc[c] - vs[c] * q; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x26: // SUBAi + case 0x26: for (int c = 0; c < 4; c++) result[c] = vs[c] - i; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x27: // MSUBAi + case 0x27: for (int c = 0; c < 4; c++) result[c] = acc[c] - vs[c] * i; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x28: // ADDA + case 0x28: for (int c = 0; c < 4; c++) result[c] = vs[c] + vt[c]; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x29: // MADDA + case 0x29: for (int c = 0; c < 4; c++) result[c] = acc[c] + vs[c] * vt[c]; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x2A: // MULA + case 0x2A: for (int c = 0; c < 4; c++) result[c] = vs[c] * vt[c]; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x2C: // SUBA + case 0x2C: for (int c = 0; c < 4; c++) result[c] = vs[c] - vt[c]; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x2D: // MSUBA + case 0x2D: for (int c = 0; c < 4; c++) result[c] = acc[c] - vs[c] * vt[c]; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; - case 0x2E: // OPMULA + case 0x2E: result[0] = vs[1] * vt[2]; result[1] = vs[2] * vt[0]; result[2] = vs[0] * vt[1]; result[3] = 0.0f; - applyFmacDestAcc(result, dest); + applyFmacDest(instr, accResult, result, dest); return; case 0x2F: - case 0x30: // NOP + case 0x30: return; default: reportReservedInstruction(true, instr); diff --git a/ps2xRuntime/include/runtime/vu/vu_decoder.h b/ps2xRuntime/include/runtime/vu/vu_decoder.h new file mode 100644 index 000000000..951a16c88 --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/vu_decoder.h @@ -0,0 +1,513 @@ +#pragma once +#include "vu_fields.h" +#include "vu_types.h" +#include +#include +#include + +namespace ps2vu +{ + inline constexpr uint32_t kFmacLatency = 4; + constexpr uint8_t laneForComponent(uint32_t c) + { + return static_cast(1u << (3u - c)); + } + + constexpr void addVfRead(InstructionUsage &usage, uint8_t reg, uint8_t lanes) + { + if (lanes == 0u) + return; + for (uint32_t index = 0; index < usage.vfReadCount; ++index) + { + if (usage.vfRead[index].reg == reg) + { + usage.vfRead[index].lanes |= lanes; + return; + } + } + if (usage.vfReadCount < usage.vfRead.size()) + usage.vfRead[usage.vfReadCount++] = {reg, lanes}; + } + + constexpr void addVfWrite(InstructionUsage &usage, uint8_t reg, uint8_t lanes) + { + if (reg == 0u || lanes == 0u) + return; + if (usage.vfWrite.reg == 0u) + usage.vfWrite = {reg, lanes}; + else if (usage.vfWrite.reg == reg) + usage.vfWrite.lanes |= lanes; + } + + constexpr InstructionUsage decodeUpperUsage(uint32_t upper) + { + InstructionUsage usage; + usage.pipeline = PipelineFmac; + usage.latency = kFmacLatency; + + const uint8_t op = static_cast(upper & 0x3Fu); + const uint8_t dest = destMask(upper); + const uint8_t fs = vfSource(upper); + const uint8_t ft = vfTarget(upper); + const uint8_t fd = vfDestination(upper); + + if (op <= 0x2Fu) + { + addVfRead(usage, fs, dest); + addVfWrite(usage, fd, dest); + if (op <= 0x1Bu) + addVfRead(usage, ft, laneForComponent(op & 3u)); + else if (op >= 0x28u) + addVfRead(usage, ft, op == 0x2Eu ? 0xEu : dest); + if (op == 0x08u || op == 0x09u || op == 0x0Au || op == 0x0Bu || op == 0x0Cu || op == 0x0Du || op == 0x0Eu || + op == 0x0Fu || op == 0x21u || op == 0x23u || op == 0x25u || op == 0x27u || op == 0x29u || op == 0x2Du || + op == 0x2Eu) + { + usage.accRead = dest; + } + return usage; + } + + if (op >= 0x3Cu) + { + const uint8_t special = static_cast((upper & 3u) | ((upper >> 4) & 0x7Cu)); + const bool writesAcc = special <= 0x0Fu || (special >= 0x18u && special <= 0x1Cu) || special == 0x1Eu || + (special >= 0x20u && special <= 0x2Au) || (special >= 0x2Cu && special <= 0x2Eu); + if (writesAcc) + { + addVfRead(usage, fs, dest); + if (special <= 0x1Bu) + addVfRead(usage, ft, laneForComponent(special & 3u)); + else if ((special >= 0x28u && special <= 0x2Eu)) + addVfRead(usage, ft, special == 0x2Eu ? 0xEu : dest); + usage.accWrite = dest; + if ((special >= 0x08u && special <= 0x0Fu) || special == 0x21u || special == 0x23u || + special == 0x25u || special == 0x27u || special == 0x29u || special == 0x2Du) + { + usage.accRead = dest; + } + } + else if (special >= 0x10u && special <= 0x17u) + { + addVfRead(usage, fs, dest); + addVfWrite(usage, ft, dest); + } + else if (special == 0x1Du) + { + addVfRead(usage, fs, dest); + addVfWrite(usage, ft, dest); + } + else if (special == 0x1Fu) + { + addVfRead(usage, fs, 0xEu); + addVfRead(usage, ft, 0x1u); + usage.writesClip = true; + } + else if (special != 0x2Fu && special != 0x30u) + { + usage.reserved = true; + } + return usage; + } + + usage.reserved = true; + return usage; + } + + constexpr InstructionUsage decodeLowerUsage(uint32_t lower, bool vu1) + { + InstructionUsage usage; + if (lower == 0u || lower == 0x8000033Cu) + return usage; + + const uint8_t opHi = static_cast((lower >> 25) & 0x7Fu); + const uint8_t vfT = vfTarget(lower); + const uint8_t vfS = vfSource(lower); + const uint8_t viT = viTarget(lower); + const uint8_t viS = viSource(lower); + const uint8_t viD = viDestination(lower); + const uint8_t dest = destMask(lower); + auto readVi = [&](uint8_t reg) + { + if (reg != 0u) + usage.viRead |= static_cast(1u << reg); + }; + auto writeVi = [&](uint8_t reg) + { + if (reg != 0u) + usage.viWrite |= static_cast(1u << reg); + }; + + switch (opHi) + { + case 0x00: + usage.pipeline = PipelineLsu; + usage.latency = 4u; + readVi(viS); + addVfWrite(usage, vfT, dest); + return usage; + case 0x01: + usage.pipeline = PipelineLsu; + usage.latency = 1u; + readVi(viT); + addVfRead(usage, vfS, dest); + return usage; + case 0x04: + usage.pipeline = PipelineLsu; + usage.latency = 4u; + readVi(viS); + writeVi(viT); + return usage; + case 0x05: + usage.pipeline = PipelineLsu; + usage.latency = 1u; + readVi(viS); + readVi(viT); + return usage; + case 0x08: + case 0x09: + usage.pipeline = PipelineIalu; + usage.latency = 1u; + usage.delaysNextBranchRead = true; + readVi(viS); + writeVi(viT); + return usage; + case 0x10: + case 0x12: + case 0x13: + usage.pipeline = PipelineIalu; + usage.latency = 1u; + usage.readsClip = true; + writeVi(1u); + return usage; + case 0x11: + usage.pipeline = PipelineFmac; + usage.latency = kFmacLatency; + usage.writesClip = true; + return usage; + case 0x14: + case 0x16: + case 0x17: + usage.pipeline = PipelineIalu; + usage.latency = 1u; + writeVi(viT); + return usage; + case 0x15: + usage.pipeline = PipelineFmac; + usage.latency = kFmacLatency; + return usage; + case 0x18: + case 0x1A: + case 0x1B: + usage.pipeline = PipelineIalu; + usage.latency = 1u; + readVi(viS); + writeVi(viT); + return usage; + case 0x1C: + usage.pipeline = PipelineIalu; + usage.latency = 1u; + usage.readsClip = true; + writeVi(viT); + return usage; + case 0x20: + usage.pipeline = PipelineBranch; + return usage; + case 0x21: + usage.pipeline = PipelineBranch; + usage.latency = 1u; + writeVi(viT); + return usage; + case 0x24: + usage.pipeline = PipelineBranch; + readVi(viS); + return usage; + case 0x25: + usage.pipeline = PipelineBranch; + usage.latency = 1u; + readVi(viS); + writeVi(viT); + return usage; + case 0x28: + case 0x29: + usage.pipeline = PipelineBranch; + readVi(viS); + readVi(viT); + return usage; + case 0x2C: + case 0x2D: + case 0x2E: + case 0x2F: + usage.pipeline = PipelineBranch; + readVi(viS); + return usage; + case 0x40: + break; + default: + usage.reserved = true; + return usage; + } + + const uint8_t direct = static_cast(lower & 0x3Fu); + if (direct == 0x30u || direct == 0x31u || direct == 0x34u || direct == 0x35u) + { + usage.pipeline = PipelineIalu; + usage.latency = 1u; + usage.delaysNextBranchRead = true; + readVi(viS); + readVi(viT); + writeVi(viD); + return usage; + } + if (direct == 0x32u) + { + usage.pipeline = PipelineIalu; + usage.latency = 1u; + usage.delaysNextBranchRead = true; + readVi(viS); + writeVi(viT); + return usage; + } + if (direct < 0x3Cu) + { + usage.reserved = true; + return usage; + } + + const uint8_t special = static_cast((lower & 3u) | ((lower >> 4) & 0x7Cu)); + switch (special) + { + case 0x30: + case 0x31: + usage.pipeline = PipelineFmac; + usage.latency = 4u; + addVfRead(usage, vfS, special == 0x31u ? 0xFu : dest); + addVfWrite(usage, vfT, dest); + break; + case 0x34: + case 0x36: + usage.pipeline = PipelineLsu; + usage.latency = 4u; + usage.viLatency = 1u; + usage.delaysNextBranchRead = true; + readVi(viS); + writeVi(viS); + addVfWrite(usage, vfT, dest); + break; + case 0x35: + case 0x37: + usage.pipeline = PipelineLsu; + usage.latency = 1u; + usage.delaysNextBranchRead = true; + readVi(viT); + writeVi(viT); + addVfRead(usage, vfS, dest); + break; + case 0x38: + usage.pipeline = PipelineFdiv; + usage.latency = 7u; + addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); + addVfRead(usage, vfT, laneForComponent((lower >> 23) & 3u)); + break; + case 0x39: + usage.pipeline = PipelineFdiv; + usage.latency = 7u; + addVfRead(usage, vfT, laneForComponent((lower >> 23) & 3u)); + break; + case 0x3A: + usage.pipeline = PipelineFdiv; + usage.latency = 13u; + addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); + addVfRead(usage, vfT, laneForComponent((lower >> 23) & 3u)); + break; + case 0x3B: + usage.pipeline = PipelineFdiv; + usage.waitQ = true; + break; + case 0x3C: + usage.pipeline = PipelineIalu; + usage.latency = 1u; + usage.delaysNextBranchRead = true; + addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); + writeVi(viT); + break; + case 0x3D: + usage.pipeline = PipelineFmac; + usage.latency = 4u; + readVi(viS); + addVfWrite(usage, vfT, dest); + break; + case 0x3E: + usage.pipeline = PipelineLsu; + usage.latency = 4u; + readVi(viS); + writeVi(viT); + break; + case 0x3F: + usage.pipeline = PipelineLsu; + usage.latency = 1u; + readVi(viS); + readVi(viT); + break; + case 0x40: + case 0x41: + usage.pipeline = PipelineFmac; + usage.latency = 4u; + addVfWrite(usage, vfT, dest); + break; + case 0x42: + case 0x43: + usage.pipeline = PipelineIalu; + usage.latency = 1u; + addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); + break; + case 0x64: + if (!vu1) + { + usage.reserved = true; + break; + } + usage.pipeline = PipelineFmac; + usage.latency = 4u; + addVfWrite(usage, vfT, dest); + break; + case 0x68: + case 0x69: + usage.pipeline = PipelineIalu; + usage.latency = 1u; + writeVi(viT); + break; + case 0x6C: + if (!vu1) + { + usage.reserved = true; + break; + } + usage.pipeline = PipelineXgkick; + usage.latency = 2u; + readVi(viS); + break; + case 0x70: + case 0x71: + case 0x72: + case 0x73: + case 0x74: + case 0x75: + case 0x76: + case 0x77: + case 0x78: + case 0x79: + case 0x7A: + case 0x7C: + case 0x7D: + if (!vu1) + { + usage.reserved = true; + break; + } + usage.pipeline = PipelineEfu; + switch (special) + { + case 0x70: + usage.latency = 11u; + break; + case 0x71: + case 0x72: + case 0x77: + usage.latency = 18u; + break; + case 0x73: + usage.latency = 24u; + break; + case 0x74: + case 0x75: + case 0x7C: + usage.latency = 54u; + break; + case 0x76: + case 0x78: + case 0x7A: + usage.latency = 12u; + break; + case 0x79: + usage.latency = 29u; + break; + case 0x7D: + usage.latency = 44u; + break; + default: + break; + } + if (special >= 0x70u && special <= 0x73u) + addVfRead(usage, vfS, 0xEu); + else if (special == 0x74u) + addVfRead(usage, vfS, 0xCu); + else if (special == 0x75u) + addVfRead(usage, vfS, 0xAu); + else if (special == 0x76u) + addVfRead(usage, vfS, 0xFu); + else + addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); + break; + case 0x7B: + if (!vu1) + { + usage.reserved = true; + break; + } + usage.pipeline = PipelineEfu; + usage.waitP = true; + break; + default: + usage.reserved = true; + break; + } + return usage; + } + + constexpr void addDependency(DecodedInstructionPair &decoded, uint32_t index) + { + for (uint32_t i = 0; i < decoded.readDependencyCount; ++i) + if (decoded.readDependencies[i] == index) + return; + decoded.readDependencies[decoded.readDependencyCount++] = static_cast(index); + } + + constexpr DecodedInstructionPair decodeInstructionPair(uint32_t lower, uint32_t upper, bool vu1 = true) + { + DecodedInstructionPair decoded; + decoded.lower = lower; + decoded.upper = upper; + decoded.iBit = (decoded.upper & 0x80000000u) != 0u; + decoded.eBit = (decoded.upper & 0x40000000u) != 0u; + decoded.mBit = (decoded.upper & 0x20000000u) != 0u; + decoded.dBit = (decoded.upper & 0x10000000u) != 0u; + decoded.tBit = (decoded.upper & 0x08000000u) != 0u; + decoded.upperUsage = decodeUpperUsage(decoded.upper); + if (!decoded.iBit) + decoded.lowerUsage = decodeLowerUsage(decoded.lower, vu1); + + const uint8_t upperWriteReg = decoded.upperUsage.vfWrite.reg; + if (upperWriteReg != 0u && decoded.lowerUsage.vfWrite.reg == upperWriteReg) + decoded.suppressedLowerVf = upperWriteReg; + + for (uint32_t unit = 0; unit != 2; ++unit) + { + const auto &usage = unit == 0 ? decoded.upperUsage : decoded.lowerUsage; + for (uint32_t i = 0; i < usage.vfReadCount; ++i) + { + const auto &access = usage.vfRead[i]; + for (uint32_t component = 0; component < 4u; ++component) + if ((access.lanes & laneForComponent(component)) != 0u) + addDependency(decoded, 4u * access.reg + component); + } + for (uint32_t regs = usage.viRead & ~1u; regs != 0u; regs &= regs - 1u) + addDependency(decoded, kViReadyBase + std::countr_zero(regs)); + for (uint32_t component = 0; component < 4u; ++component) + if ((usage.accRead & laneForComponent(component)) != 0u) + addDependency(decoded, kAccReadyBase + component); + } + return decoded; + } + +} diff --git a/ps2xRuntime/include/runtime/vu/vu_fields.h b/ps2xRuntime/include/runtime/vu/vu_fields.h new file mode 100644 index 000000000..c5bff76df --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/vu_fields.h @@ -0,0 +1,64 @@ +#pragma once +#include + +namespace ps2vu +{ + + constexpr uint8_t destMask(uint32_t i) + { + return (uint8_t)((i >> 21) & 0xF); + } + constexpr uint8_t vfTarget(uint32_t i) + { + return (uint8_t)((i >> 16) & 0x1F); + } + constexpr uint8_t vfSource(uint32_t i) + { + return (uint8_t)((i >> 11) & 0x1F); + } + constexpr uint8_t vfDestination(uint32_t i) + { + return (uint8_t)((i >> 6) & 0x1F); + } + constexpr uint8_t broadcastComponent(uint32_t i) + { + return (uint8_t)(i & 0x3); + } + + constexpr uint8_t lowerTarget(uint32_t i) + { + return (uint8_t)((i >> 16) & 0x1F); + } + constexpr uint8_t lowerSource(uint32_t i) + { + return (uint8_t)((i >> 11) & 0x1F); + } + constexpr uint8_t lowerDestination(uint32_t i) + { + return (uint8_t)((i >> 6) & 0x1F); + } + constexpr uint8_t viTarget(uint32_t i) + { + return (uint8_t)((i >> 16) & 0xF); + } + constexpr uint8_t viSource(uint32_t i) + { + return (uint8_t)((i >> 11) & 0xF); + } + constexpr uint8_t viDestination(uint32_t i) + { + return (uint8_t)((i >> 6) & 0xF); + } + constexpr int16_t immediate11(uint32_t i) + { + return (int16_t)(int32_t)((int32_t)(i << 21) >> 21); + } + constexpr int16_t immediate15(uint32_t i) + { + uint32_t lo11 = i & 0x7FF; + uint32_t hi4 = (i >> 21) & 0xF; + uint32_t raw = (hi4 << 11) | lo11; + return (int16_t)(int32_t)((int32_t)(raw << 17) >> 17); + } + +} diff --git a/ps2xRuntime/include/runtime/vu/vu_pipeline_events.h b/ps2xRuntime/include/runtime/vu/vu_pipeline_events.h new file mode 100644 index 000000000..91734a8f7 --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/vu_pipeline_events.h @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace ps2vu +{ + enum class WritebackKind : uint32_t + { + Flag, + Fdiv, + Efu, + Store, + Vf, + Vi, + Acc + }; + + class PipelineEvents + { + public: + static constexpr uint64_t noEvent = std::numeric_limits::max(); + static constexpr uint32_t cycleCount = 64; + + static constexpr uint32_t capacity(WritebackKind kind) + { + const auto index = static_cast(kind); + return offsets[index + 1] - offsets[index]; + } + + template static uint32_t slots(uint64_t events) + { + return static_cast(events >> offsets[static_cast(Kind)]) & + ((1u << capacity(Kind)) - 1u); + } + + template void schedule(uint64_t cycle, uint32_t latency, uint32_t slot) + { + if (latency == 0 || latency >= cycleCount || slot >= capacity(Kind)) + throw std::logic_error("Invalid VU pipeline event"); + const uint32_t bucket = static_cast((cycle + latency) & (cycleCount - 1)); + m_events[bucket] |= uint64_t{1} << (offsets[static_cast(Kind)] + slot); + m_cycles |= uint64_t{1} << bucket; + } + + uint64_t take(uint64_t cycle) + { + const uint32_t bucket = static_cast(cycle & (cycleCount - 1)); + m_cycles &= ~(uint64_t{1} << bucket); + return std::exchange(m_events[bucket], 0); + } + + uint64_t nextCycle(uint64_t cycle) const + { + if (m_cycles == 0) + return noEvent; + return cycle + std::countr_zero(std::rotr(m_cycles, static_cast(cycle & (cycleCount - 1)))); + } + + void reset() + { + m_events.fill(0); + m_cycles = 0; + } + + private: + static constexpr std::array offsets{0, 8, 9, 11, 19, 35, 43, 51}; + std::array m_events{}; + uint64_t m_cycles = 0; + }; +} diff --git a/ps2xRuntime/include/runtime/vu/vu_runtime.h b/ps2xRuntime/include/runtime/vu/vu_runtime.h new file mode 100644 index 000000000..d716e6d4a --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/vu_runtime.h @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include +#include "vu_state.h" + +class PS2Memory; +struct R5900Context; +namespace ps2vu +{ + class Unit; +} + +namespace ps2vu +{ + class Runtime + { + public: + Runtime(PS2Memory &memory, std::function advanceClock); + ~Runtime(); + Unit &unit(bool vu1) noexcept; + void start0(R5900Context &context, uint32_t pc); + void startVif0(R5900Context &context, uint32_t pc, uint32_t itop, bool resume); + void start1(uint32_t pc, uint32_t top, uint32_t itop, bool resume); + void advance(uint32_t cycles, R5900Context &context) noexcept; + void beforeAccess(R5900Context &context, uint32_t instruction); + void afterAccess(R5900Context &context, uint32_t instruction); + void flush1(bool gif); + void flush0(R5900Context &context); + bool failed() const noexcept; + void reportFailure() const; + + private: + static bool submitGif(void *, const uint8_t *, uint32_t) noexcept; + static bool readShared(void *, uint64_t, uint32_t, uint32_t *) noexcept; + static bool writeShared(void *, uint64_t, uint32_t, const uint32_t *, uint8_t) noexcept; + void advanceUnit(bool vu1, uint64_t cycle) noexcept; + void run(bool vu1, uint32_t cycles); + void wait(bool vu1); + PS2Memory &m_memory; + std::function m_advanceClock; + uint64_t m_cycle = 0; + R5900Context *m_accessContext = nullptr; + bool m_contextDirty = false; + std::unique_ptr m_units[2]; + }; + + void importContext(const R5900Context &context, VU1State &state) noexcept; + void exportContext(const VU1State &state, R5900Context &context) noexcept; +} diff --git a/ps2xRuntime/include/runtime/vu/vu_state.h b/ps2xRuntime/include/runtime/vu/vu_state.h new file mode 100644 index 000000000..6bea7df8b --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/vu_state.h @@ -0,0 +1,30 @@ +#pragma once +#include + +struct VU1State +{ + float vf[32][4]; + int32_t vi[16]; + float acc[4]; + float q; + float p; + float i; + uint32_t r; + uint32_t pc; + uint32_t mac; + uint32_t clip; + uint32_t status; + uint64_t cycles; + bool ebit; + bool haltAfterDelaySlot; + bool dBitEnabled; + bool tBitEnabled; + bool stoppedByD; + bool stoppedByT; + bool mBit; + uint32_t top; + uint32_t itop; + bool branchPending; + uint32_t branchTarget; + uint32_t branchDelay; +}; diff --git a/ps2xRuntime/include/runtime/vu/vu_types.h b/ps2xRuntime/include/runtime/vu/vu_types.h new file mode 100644 index 000000000..694897814 --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/vu_types.h @@ -0,0 +1,67 @@ +#pragma once +#include +#include + +namespace ps2vu +{ + enum Pipeline : uint8_t + { + PipelineNone = 0, + PipelineFmac, + PipelineLsu, + PipelineFdiv, + PipelineEfu, + PipelineIalu, + PipelineBranch, + PipelineXgkick + }; + + struct VfAccess + { + uint8_t reg = 0; + uint8_t lanes = 0; + }; + + struct InstructionUsage + { + std::array vfRead{}; + VfAccess vfWrite{}; + uint8_t vfReadCount = 0; + uint16_t viRead = 0; + uint16_t viWrite = 0; + uint8_t accRead = 0; + uint8_t accWrite = 0; + uint8_t latency = 0; + uint8_t vfLatency = 0; + uint8_t viLatency = 0; + Pipeline pipeline = PipelineNone; + bool waitQ = false; + bool waitP = false; + bool readsClip = false; + bool writesClip = false; + bool delaysNextBranchRead = false; + bool reserved = false; + }; + + inline constexpr uint32_t kVfReadyCount = 32u * 4u; + inline constexpr uint32_t kViReadyBase = kVfReadyCount; + inline constexpr uint32_t kAccReadyBase = kViReadyBase + 16u; + inline constexpr uint32_t kRegisterReadyCount = kAccReadyBase + 4u; + + struct DecodedInstructionPair + { + uint32_t lower = 0; + uint32_t upper = 0; + InstructionUsage lowerUsage{}; + InstructionUsage upperUsage{}; + bool iBit = false; + bool eBit = false; + bool mBit = false; + bool dBit = false; + bool tBit = false; + uint8_t suppressedLowerVf = 0; + std::array readDependencies{}; + uint8_t readDependencyCount = 0; + }; + +} diff --git a/ps2xRuntime/include/runtime/vu/vu_unit.h b/ps2xRuntime/include/runtime/vu/vu_unit.h new file mode 100644 index 000000000..313e3dbe3 --- /dev/null +++ b/ps2xRuntime/include/runtime/vu/vu_unit.h @@ -0,0 +1,88 @@ +#pragma once +#include "interpreter/vu_executor.h" +#include + +namespace ps2vu +{ + enum class UnitStatus : uint8_t + { + Ready, + Running, + Stopped, + Fault + }; + enum class StartSource : uint8_t + { + Ee, + Vif + }; + enum class ExitReason : uint8_t + { + Budget, + Event, + Halt, + Fault + }; + + struct ExecutionExit + { + ExitReason reason; + uint32_t pc; + uint64_t cycles; + }; + + struct MemoryServices + { + uint8_t *data = nullptr; + void *context = nullptr; + bool (*readShared)(void *, uint64_t, uint32_t, uint32_t *) noexcept = nullptr; + bool (*writeShared)(void *, uint64_t, uint32_t, const uint32_t *, uint8_t) noexcept = nullptr; + bool (*submitGif)(void *, const uint8_t *, uint32_t) noexcept = nullptr; + }; + + struct UnitFailure + { + uint32_t pc = 0; + std::array reason{}; + }; + + struct Readiness + { + std::array registers{}; + uint32_t q = 0; + }; + + class Unit + { + public: + Unit(bool vu1, std::span code, MemoryServices services); + Unit(const Unit &) = delete; + Unit &operator=(const Unit &) = delete; + + public: + void reset() noexcept; + bool start(uint32_t pc, uint32_t top, uint32_t itop, StartSource source = StartSource::Vif) noexcept; + bool resume(uint32_t top, uint32_t itop) noexcept; + void forceBreak() noexcept; + + public: + ExecutionExit advance(uint32_t cycles) noexcept; + VU1State &state() noexcept { return m_executor.state(); } + const VU1State &state() const noexcept { return m_executor.state(); } + Readiness readiness() const noexcept; + UnitStatus status() const noexcept { return m_status; } + const UnitFailure &failure() const noexcept { return m_failure; } + bool transferring() const noexcept { return m_executor.m_xgkick.active; } + bool pending() const noexcept { return m_status == UnitStatus::Running || m_executor.pipelinesPending(); } + + private: + void begin(uint32_t top, uint32_t itop) noexcept; + void fail(const char *reason) noexcept; + VUExecutor m_executor; + MemoryServices m_services; + std::span m_code; + uint32_t m_dataSize; + UnitFailure m_failure; + UnitStatus m_status = UnitStatus::Ready; + }; +} diff --git a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp index c5e31c48d..355b646aa 100644 --- a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp +++ b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp @@ -107,6 +107,7 @@ void EeScheduler::reset(uint8_t *rdram, const R5900Context &mainContext) m_enabledIntcMask = 0xFFFFFFFFu; m_enabledDmacMask = 0xFFFFFFFFu; m_currentThreadId = 0; + m_transferPending = false; m_rescheduleRequested = false; m_timeSliceExpired = false; m_insideInterrupt = false; @@ -159,6 +160,7 @@ void EeScheduler::run() while (!m_stopRequested.load(std::memory_order_acquire)) { + m_transferPending = false; processPendingEvents(); if (m_stopRequested.load(std::memory_order_acquire)) { @@ -202,14 +204,8 @@ void EeScheduler::run() { auto completion = std::move(running->resumeCompletion); running->resumeCompletion = {}; - try - { - completion(running->activeContext()); - } - catch (const EeDispatcherTransfer &) - { - } - if (m_currentThreadId == 0) + completion(running->activeContext()); + if (m_transferPending || m_currentThreadId == 0) { continue; } @@ -236,13 +232,7 @@ void EeScheduler::run() running->invocations.pop_back(); if (completed.onComplete) { - try - { - completed.onComplete(completed.context, running->activeContext()); - } - catch (const EeDispatcherTransfer &) - { - } + completed.onComplete(completed.context, running->activeContext()); } continue; } @@ -300,13 +290,9 @@ void EeScheduler::run() m_guestExecuting.store(false, std::memory_order_release); m_insideInterrupt = false; } - catch (const EeDispatcherTransfer &) - { - m_guestExecuting.store(false, std::memory_order_release); - m_insideInterrupt = false; - } catch (...) { + m_insideInterrupt = false; m_guestExecuting.store(false, std::memory_order_release); m_running.store(false, std::memory_order_release); publishSnapshot(); @@ -361,6 +347,7 @@ bool EeScheduler::checkpointDue(uint32_t cycles) noexcept if (m_checkpointPending.load(std::memory_order_acquire) || m_stopRequested.load(std::memory_order_acquire)) { + m_transferPending = true; return true; } @@ -368,6 +355,7 @@ bool EeScheduler::checkpointDue(uint32_t cycles) noexcept if (nextEventCycle != 0u && m_eeCycle >= nextEventCycle) { m_checkpointPending.store(true, std::memory_order_release); + m_transferPending = true; return true; } @@ -381,6 +369,7 @@ bool EeScheduler::checkpointDue(uint32_t cycles) noexcept { m_rescheduleRequested = true; m_timeSliceExpired = true; + m_transferPending = true; return true; } @@ -508,7 +497,7 @@ int EeScheduler::startThread(int id, uint32_t arg, const R5900Context &caller, b return KE_OK; } -[[noreturn]] void EeScheduler::exitCurrent(bool deleteThreadRecord) +void EeScheduler::exitCurrent(bool deleteThreadRecord) { assertExecutor(); GuestThread *exiting = currentThread(); @@ -526,7 +515,7 @@ int EeScheduler::startThread(int id, uint32_t arg, const R5900Context &caller, b m_runtime.guestFree(ownedStack); } publishSnapshot(); - throw EeDispatcherTransfer{}; + m_transferPending = true; } int EeScheduler::terminateThread(int id, uint32_t &ownedStack, bool interruptSafe) @@ -815,7 +804,7 @@ void EeScheduler::transferIfRequested(bool interruptSafe) m_rescheduleRequested = false; m_timeSliceExpired = false; publishSnapshot(); - throw EeDispatcherTransfer{}; + m_transferPending = true; } int EeScheduler::createSemaphore(int initCount, int maxCount, uint32_t attr, uint32_t option) @@ -1106,7 +1095,7 @@ void EeScheduler::queueInvocation(GuestInvocation invocation) m_checkpointPending.store(true, std::memory_order_release); } -[[noreturn]] void EeScheduler::invokeCurrent(GuestInvocation invocation) +void EeScheduler::invokeCurrent(GuestInvocation invocation) { assertExecutor(); GuestThread *owner = currentThread(); @@ -1118,10 +1107,10 @@ void EeScheduler::queueInvocation(GuestInvocation invocation) invocation.sequence = ++m_invocationSequence; owner->invocations.push_back(std::move(invocation)); publishSnapshot(); - throw EeDispatcherTransfer{}; + m_transferPending = true; } -[[noreturn]] void EeScheduler::invokeCurrentSequence(std::vector invocations) +void EeScheduler::invokeCurrentSequence(std::vector invocations) { assertExecutor(); GuestThread *owner = currentThread(); @@ -1137,7 +1126,7 @@ void EeScheduler::queueInvocation(GuestInvocation invocation) owner->invocations.push_back(std::move(*it)); } publishSnapshot(); - throw EeDispatcherTransfer{}; + m_transferPending = true; } bool EeScheduler::hasInvocation(GuestInvocationKind kind, uint64_t tag) const @@ -1320,7 +1309,7 @@ uint32_t EeScheduler::setGsVSyncCallback(uint32_t callback, uint32_t gp, uint32_ return previous; } -[[noreturn]] void EeScheduler::waitVSync(uint64_t afterTick, int fixedResult, std::function completion) +void EeScheduler::waitVSync(uint64_t afterTick, int fixedResult, std::function completion) { blockCurrent(EeWaitState{ EeWaitReason::VSync, @@ -1383,7 +1372,7 @@ void EeScheduler::completeExternalWait(uint32_t type, uint64_t token, int result publishSnapshot(); } -[[noreturn]] void EeScheduler::waitExternal(EeWaitReason reason, +void EeScheduler::waitExternal(EeWaitReason reason, uint32_t type, uint64_t token, std::function completion) @@ -1688,7 +1677,7 @@ void EeScheduler::blockCurrent(EeWaitState wait) self->status = self->suspendCount == 0 ? EeThreadStatus::Waiting : EeThreadStatus::WaitingSuspended; m_currentThreadId = 0; publishSnapshot(); - throw EeDispatcherTransfer{}; + m_transferPending = true; } void EeScheduler::makeReady(GuestThread &item, int result, bool interruptSafe) @@ -1736,6 +1725,7 @@ void EeScheduler::applyPendingPreemption() assert(self != nullptr); enqueueReady(*self, !m_timeSliceExpired); m_currentThreadId = 0; + m_transferPending = false; m_rescheduleRequested = false; m_timeSliceExpired = false; } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp index 56a978eae..ac348148f 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/CD.cpp @@ -790,6 +790,7 @@ namespace ps2_stubs } continueCdStRead(rdram, &resumeContext, runtime, state); }); + return; } uint32_t sectors = std::min(remaining, available); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp index 9c5491990..8e4f52b90 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/GS.cpp @@ -1,5 +1,7 @@ #include "Common.h" #include "GS.h" +#include "runtime/frame_timing.h" +#include "../Syscalls/System.h" #include "ps2_log.h" #include "runtime/gs/ps2_gs_common.h" #include "runtime/gs/ps2_gs_psmct16.h" @@ -11,8 +13,7 @@ namespace ps2_stubs { uint64_t makeClearPrim(bool useContext2) { - return static_cast(GS_PRIM_SPRITE) | - (static_cast(useContext2 ? 1u : 0u) << 9); + return static_cast(GS_PRIM_SPRITE) | (static_cast(useContext2 ? 1u : 0u) << 9); } uint64_t makeClearRgbaq(uint32_t rgba) @@ -22,8 +23,7 @@ namespace ps2_stubs uint64_t makeClearXyz(int32_t x, int32_t y) { - return static_cast(static_cast(x << 4)) | - (static_cast(static_cast(y << 4)) << 16); + return static_cast(static_cast(x << 4)) | (static_cast(static_cast(y << 4)) << 16); } void seedGsClearPacket(GsClearMem &clear, @@ -798,55 +798,20 @@ namespace ps2_stubs return; } - g_gparam.interlace = static_cast(interlace & 0x1); - g_gparam.omode = static_cast(omode & 0xFF); - g_gparam.ffmode = static_cast(ffmode & 0x1); + g_gparam.interlace = static_cast(interlace & 0x1); + g_gparam.omode = static_cast(omode & 0xFF); + g_gparam.ffmode = static_cast(ffmode & 0x1); writeGsGParamToScratch(runtime); - uint64_t pmode = makePmode(1, 0, 0, 0, 0, 0x80); - uint64_t smode2 = (interlace & 0x1) | ((ffmode & 0x1) << 1); - uint64_t dispfb = makeDispFb(0, 10, 0, 0, 0); - uint64_t display = makeDisplay(0, 0, 0, 0, 639, 447); - uint64_t bgcolor = 0ULL; if (runtime) { - uint32_t pktAddr = runtime->guestMalloc(128u, 16u); - if (pktAddr != 0u) - { - uint8_t *pkt = getMemPtr(rdram, pktAddr); - if (pkt) - { - uint64_t *q = reinterpret_cast(pkt); - q[0] = makeGiftagAplusD(7u); - q[1] = 0xEULL; - q[2] = pmode; - q[3] = 0x41ULL; - q[4] = smode2; - q[5] = 0x42ULL; - q[6] = dispfb; - q[7] = 0x59ULL; - q[8] = display; - q[9] = 0x5aULL; - q[10] = dispfb; - q[11] = 0x5bULL; - q[12] = display; - q[13] = 0x5cULL; - q[14] = bgcolor; - q[15] = 0x5fULL; - constexpr uint32_t GIF_CHANNEL = 0x1000A000; - constexpr uint32_t CHCR_STR_MODE0 = 0x101u; - auto &mem = runtime->memory(); - mem.writeIORegister(GIF_CHANNEL + 0x10u, pktAddr); - mem.writeIORegister(GIF_CHANNEL + 0x20u, 8u); - mem.writeIORegister(GIF_CHANNEL + 0x00u, CHCR_STR_MODE0); - mem.processPendingTransfers(); - runtime->guestFree(pktAddr); - } - else - { - runtime->guestFree(pktAddr); - } - } + auto ®s = runtime->memory().gs(); + ps2_syscalls::configureGsCrt(regs, interlace & 1u, omode & 0xffu, ffmode & 1u); + + regs.pmode = makePmode(1, 0, 0, 0, 0, 0x80); + regs.dispfb1 = regs.dispfb2 = makeDispFb(0, 10, 0, 0, 0); + regs.display1 = regs.display2 = makeDefaultGsDispEnv(0, 640, 448).display; + regs.bgcolor = 0; } } @@ -887,11 +852,7 @@ namespace ps2_stubs } const uint32_t fbw = std::max(1u, (w + 63u) / 64u); - const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u); - const uint64_t smode2 = - (static_cast(g_gparam.interlace & 0x1u) << 0) | - (static_cast(g_gparam.ffmode & 0x1u) << 1); - const uint64_t display = makeDisplay(636u, 32u, 0u, 0u, w - 1u, h - 1u); + const GsDispEnvMem displayEnv = makeDefaultGsDispEnv(psm, w, h); const int32_t drawWidth = static_cast(w); const int32_t drawHeight = static_cast(h); @@ -908,11 +869,8 @@ namespace ps2_stubs const uint64_t dispfb1 = makeDispFb(0u, fbw, psm, 0u, 0u); GsDBuffDcMem db{}; - db.disp[0].pmode = pmode; - db.disp[0].smode2 = smode2; + db.disp[0] = displayEnv; db.disp[0].dispfb = dispfb0; - db.disp[0].display = display; - db.disp[0].bgcolor = 0u; db.disp[1] = db.disp[0]; db.disp[1].dispfb = dispfb1; @@ -958,12 +916,7 @@ namespace ps2_stubs } const uint32_t fbw = std::max(1u, (w + 63u) / 64u); - const uint64_t pmode = makePmode(1u, 1u, 0u, 0u, 0u, 0x80u); - const uint64_t smode2 = - (static_cast(g_gparam.interlace & 0x1u) << 0) | - (static_cast(g_gparam.ffmode & 0x1u) << 1); - const uint64_t dispfb = makeDispFb(0u, fbw, psm, 0u, 0u); - const uint64_t display = makeDisplay(636u, 32u, 0u, 0u, w - 1u, h - 1u); + const GsDispEnvMem displayEnv = makeDefaultGsDispEnv(psm, w, h); const int32_t drawWidth = static_cast(w); const int32_t drawHeight = static_cast(h); @@ -976,11 +929,7 @@ namespace ps2_stubs } GsDBuffMem db{}; - db.disp[0].pmode = pmode; - db.disp[0].smode2 = smode2; - db.disp[0].dispfb = dispfb; - db.disp[0].display = display; - db.disp[0].bgcolor = 0u; + db.disp[0] = displayEnv; db.disp[1] = db.disp[0]; db.giftag0 = {makeGiftagAplusD(14u), 0x0E0E0E0E0E0E0E0EULL}; @@ -1002,21 +951,16 @@ namespace ps2_stubs uint32_t psm = getRegU32(ctx, 5); uint32_t w = getRegU32(ctx, 6); uint32_t h = getRegU32(ctx, 7); - const GsTrailingArgs2 trailing = decodeGsTrailingArgs2(rdram, ctx); - uint32_t dx = trailing.arg0; - uint32_t dy = trailing.arg1; + const int32_t dx = static_cast(getRegU32(ctx, 8)); + const int32_t dy = static_cast(getRegU32(ctx, 9)); if (w == 0) w = 640; if (h == 0) h = 448; - uint32_t fbw = (w + 63) / 64; - uint64_t dispfb = makeDispFb(0, fbw, psm, 0, 0); - uint64_t display = makeDisplay(dx, dy, 0, 0, w - 1, h - 1); - - writeGsDispEnv(rdram, envAddr, display, dispfb); - setReturnS32(ctx, 0); + const GsDispEnvMem env = makeDefaultGsDispEnv(psm, w, h, dx, dy); + setReturnS32(ctx, writeGsDispEnv(rdram, envAddr, env) ? 0 : -1); } void sceGsSetDefDrawEnv(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -1183,6 +1127,7 @@ namespace ps2_stubs applyGsClearPacket(runtime, db.clear1); } + runtime->memory().gs().sdkPresentCount.fetch_add(1, std::memory_order_relaxed); setReturnS32(ctx, static_cast(which ^ 1u)); } @@ -1208,6 +1153,7 @@ namespace ps2_stubs applyGsRegPairs(runtime, reinterpret_cast(&db.draw1), 8u); } + runtime->memory().gs().sdkPresentCount.fetch_add(1, std::memory_order_relaxed); setReturnS32(ctx, static_cast(which ^ 1u)); } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h index b1bb6d851..dce1a6e97 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h @@ -1486,11 +1486,12 @@ namespace { struct GsGParam { - uint8_t interlace; - uint8_t omode; - uint8_t ffmode; - uint8_t version; + uint16_t interlace; + uint16_t omode; + uint16_t ffmode; + uint16_t version; }; + static_assert(sizeof(GsGParam) == 8, "sceGsGetGParam exposes four 16-bit fields"); struct GsDispEnvMem { @@ -1624,6 +1625,35 @@ namespace (static_cast(dh & 0x07FF) << 44); } + static GsDispEnvMem makeDefaultGsDispEnv(uint32_t psm, uint32_t width, uint32_t height, + int32_t dx = 0, int32_t dy = 0) + { + GsDispEnvMem env{}; + env.pmode = 0x66; // EN2, CRTMD=1, MMOD=1, AMOD=1 (libgs default). + env.smode2 = g_gparam.interlace ? (1u | ((g_gparam.ffmode & 1u) << 1)) : 2u; + env.dispfb = makeDispFb(0, (width + 63u) / 64u, psm, 0, 0); + + if (g_gparam.omode == 2 || g_gparam.omode == 3) + { + // Note libgs DISPLAY dimensions are output clocks/scanlines. In analog + // modes 640 framebuffer pixels span 2560 clocks (MAGH=3). + const uint32_t magnification = (width + 2559u) / width; + const int32_t originX = g_gparam.omode == 2 ? 636 : 656; + const int32_t originY = g_gparam.omode == 2 ? 25 : 36; + const uint32_t displayHeight = + g_gparam.interlace && g_gparam.ffmode ? height * 2u : height; + env.display = makeDisplay(originX + dx * static_cast(magnification), + originY * (g_gparam.interlace ? 2 : 1) + dy, + magnification - 1u, 0, width * magnification - 1u, + displayHeight - 1u); + } + else + { + env.display = makeDisplay(dx, dy, 0, 0, width - 1u, height - 1u); + } + return env; + } + static uint64_t makeFrame(uint32_t fbp, uint32_t fbw, uint32_t psm, uint32_t fbmsk) { return (static_cast(fbp & 0x1FFu) << 0) | @@ -1805,15 +1835,11 @@ namespace return true; } - static bool writeGsDispEnv(uint8_t *rdram, uint32_t addr, uint64_t display, uint64_t dispfb) + static bool writeGsDispEnv(uint8_t *rdram, uint32_t addr, const GsDispEnvMem &env) { uint8_t *ptr = getMemPtr(rdram, addr); if (!ptr) return false; - GsDispEnvMem env{}; - std::memcpy(&env, ptr, sizeof(env)); - env.dispfb = dispfb; - env.display = display; std::memcpy(ptr, &env, sizeof(env)); return true; } @@ -1881,9 +1907,9 @@ namespace auto ®s = runtime->memory().gs(); regs.pmode = env.pmode; regs.smode2 = env.smode2; - regs.dispfb1 = env.dispfb; + regs.writeDisplayFramebuffer(0, env.dispfb); regs.display1 = env.display; - regs.dispfb2 = env.dispfb; + regs.writeDisplayFramebuffer(1, env.dispfb); regs.display2 = env.display; regs.bgcolor = env.bgcolor; } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/IPU.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/IPU.cpp index e851bd256..17317741f 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/IPU.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/IPU.cpp @@ -86,6 +86,7 @@ namespace ps2_stubs completeIpuInit(rdram, &parent, runtime); }; scheduler.invokeCurrent(std::move(invocation)); + return; } completeIpuInit(rdram, ctx, runtime); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp index 44eaef4d9..0ff3d5bd9 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/MPEG.cpp @@ -2320,6 +2320,7 @@ namespace ps2_stubs } sceMpegGetPicture(rdram, &resumeContext, runtime); }); + return; } if (!playback.decodedFrames.empty()) @@ -2356,6 +2357,7 @@ namespace ps2_stubs } sceMpegGetPicture(rdram, &resumeContext, runtime); }); + return; } frame = std::move(playback.decodedFrames.front()); diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp index 07fcd2944..a0b71b087 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp @@ -698,6 +698,7 @@ namespace ps2_syscalls finishCall(&completed, parent); }; runtime->eeScheduler().invokeCurrent(std::move(invocation)); + return; } finishCall(nullptr, *ctx); } diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp index 961401614..e49fc5b43 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp @@ -3,26 +3,30 @@ namespace ps2_syscalls { + void configureGsCrt(GSRegisters &gs, uint32_t interlaced, uint32_t videoMode, uint32_t frameMode) + { + gs.smode2 = (static_cast(interlaced) & 0x1ull) | ((static_cast(frameMode) & 0x1ull) << 1); + + const uint64_t cmod = videoMode == 3 ? 3u : videoMode == 2 ? 2u : 0u; + const uint64_t lc = (videoMode == 0x51 || videoMode == 0x52) ? 22u : + (videoMode >= 0x1a && videoMode <= 0x4a) ? 15u : 32u; + gs.smode1 = (gs.smode1 & ~((127ull << 3) | (3ull << 13))) | (lc << 3) | (cmod << 13); + + if ((gs.pmode & 0x3ull) == 0ull) + { + gs.pmode |= 0x1ull; + } + } + void GsSetCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { int interlaced = getRegU32(ctx, 4); // $a0 - 0=non-interlaced, 1=interlaced - int videoMode = getRegU32(ctx, 5); // $a1 - 0=NTSC, 1=PAL, 2=VESA, 3=HiVision + int videoMode = getRegU32(ctx, 5); // $a1 - GS CRT mode (2=NTSC, 3=PAL, 0x50=480p) int frameMode = getRegU32(ctx, 6); // $a2 - 0=field, 1=frame if (runtime) { - auto &gs = runtime->memory().gs(); - const uint64_t smode2 = - (static_cast(interlaced) & 0x1ull) | - ((static_cast(frameMode) & 0x1ull) << 1); - - gs.smode2 = smode2; - - // Keep CRT1 enabled after the BIOS syscall selects a display mode. - if ((gs.pmode & 0x3ull) == 0ull) - { - gs.pmode |= 0x1ull; - } + configureGsCrt(runtime->memory().gs(), interlaced, videoMode, frameMode); } RUNTIME_LOG("PS2 GsSetCrt: interlaced=" << interlaced @@ -421,6 +425,7 @@ namespace ps2_syscalls parent.r[2] = completed.r[2]; }; scheduler.invokeCurrent(std::move(invocation)); + return true; } static bool tryResolveGuestSyscallMirrorAddr(uint32_t syscallIndex, uint32_t &guestAddr) diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/System.h b/ps2xRuntime/src/lib/Kernel/Syscalls/System.h index c9b9991c1..c5ef3aa9f 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/System.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/System.h @@ -4,6 +4,7 @@ namespace ps2_syscalls { + void configureGsCrt(GSRegisters &gs, uint32_t interlaced, uint32_t videoMode, uint32_t frameMode); bool dispatchSyscallOverride(uint32_t syscallNumber, uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void GsSetCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void SetGsCrt(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp index 5110ee2c2..dab6002d2 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Thread.cpp @@ -67,7 +67,7 @@ namespace ps2_syscalls return 0; } - [[noreturn]] void exitThreadWithHandlers(int tid, + void exitThreadWithHandlers(int tid, R5900Context *ctx, PS2Runtime *runtime, bool deleteThread) @@ -94,6 +94,7 @@ namespace ps2_syscalls if (invocations.empty()) { ee.exitCurrent(deleteThread); + return; } invocations.back().onComplete = [runtime, deleteThread](const R5900Context &, R5900Context &) { diff --git a/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp b/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp index f0ad5070b..9c55e1cd7 100644 --- a/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp +++ b/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp @@ -518,6 +518,8 @@ void GSCpuBackend::Initialize(uint8_t *vram, uint32_t vramSize) if (vram && vramSize < GSMem::MEMORY_SIZE) throw std::invalid_argument("GS CPU backend requires at least 4 MiB of VRAM"); + RUNTIME_WARNING("GS CPU backend is experimental and may not be fully compatible with all games."); + std::lock_guard lock(m_mutex); m_vram = vram; m_vramSize = vramSize; diff --git a/ps2xRuntime/src/lib/gs/gs_frontend.cpp b/ps2xRuntime/src/lib/gs/gs_frontend.cpp index 565892816..ea231c9df 100644 --- a/ps2xRuntime/src/lib/gs/gs_frontend.cpp +++ b/ps2xRuntime/src/lib/gs/gs_frontend.cpp @@ -168,6 +168,7 @@ void GS::reset() { std::lock_guard presentationLock(m_presentationMutex); m_hostPresentationFrame.clear(); + m_hostPresentationGpuFrame.reset(); m_hostPresentationWidth = 0u; m_hostPresentationHeight = 0u; m_hostPresentationDisplayFbp = 0u; @@ -260,6 +261,8 @@ GSDebugSnapshot GS::getDebugSnapshot() const snapshot.hasHostPresentationFrame = m_hasHostPresentationFrame; } snapshot.localToHostPendingBytes = transfer.localToHostPendingBytes; + if (m_backend && m_backend->UsesRawCommands()) + m_backend->ReadRegisterState(snapshot); return snapshot; } @@ -509,6 +512,9 @@ GSPresentationRequest GS::buildPresentationRequestUnlocked() const if (!m_privRegs) return request; request.pmode = m_privRegs->pmode; + request.smode1 = m_privRegs->smode1; + request.syncv = m_privRegs->syncv; + request.field = (m_privRegs->csr.load() >> 13u) & 1u; request.smode2 = m_privRegs->smode2; request.dispfb1 = m_privRegs->dispfb1; request.display1 = m_privRegs->display1; @@ -533,6 +539,7 @@ void GS::latchHostPresentationFrame() { std::lock_guard presentationLock(m_presentationMutex); m_hostPresentationFrame.clear(); + m_hostPresentationGpuFrame.reset(); m_hasHostPresentationFrame = false; m_hostPresentationWidth = m_hostPresentationHeight = 0u; return; @@ -560,6 +567,8 @@ void GS::latchHostPresentationFrame() { std::lock_guard presentationLock(m_presentationMutex); m_hostPresentationFrame = std::move(frame.pixels); + m_hostPresentationGpuFrame = std::move(frame.gpu); + m_hostPresentationAspectRatio = frame.aspectRatio; m_hostPresentationWidth = width; m_hostPresentationHeight = height; m_hostPresentationDisplayFbp = displayFbp; @@ -638,9 +647,16 @@ bool GS::copyLatchedHostPresentationFrame(std::vector &outPixels, return true; } -void GS::processGIFPacket(const uint8_t *data, uint32_t sizeBytes) +void GS::processGIFPacket(const uint8_t *data, uint32_t sizeBytes, uint32_t path) { std::lock_guard lock(m_stateMutex); + if (m_backend && m_backend->UsesRawCommands()) + { + if (data && sizeBytes && (sizeBytes & 15u) == 0u) + m_backend->ProcessGIF(path, data, sizeBytes); + return; + } + if (!data || sizeBytes < 16 || !m_backend) return; @@ -745,6 +761,13 @@ bool GS::processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes) if (!validatePackedGifPacket(data, sizeBytes)) return false; + if (m_backend->UsesRawCommands()) + { + m_backend->ProcessGIF(3, data, sizeBytes); + ++m_nativePackedGIFPacketCount; + return true; + } + const bool processed = visitPackedGifPacket(data, sizeBytes, [&](const PackedGifPacketTag &tag) { m_curQ = 1.0f; @@ -1067,6 +1090,11 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value) void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value) { + if (m_backend && m_backend->UsesRawCommands()) + { + m_backend->WriteRegisterRaw(regAddr, value); + return; + } const bool interestingReg = regAddr == GS_REG_PRIM || regAddr == GS_REG_RGBAQ || @@ -1499,7 +1527,7 @@ void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value) } case 0x59: if (m_privRegs) - m_privRegs->dispfb1 = value; + m_privRegs->writeDisplayFramebuffer(0, value); break; case 0x5a: if (m_privRegs) @@ -1507,7 +1535,7 @@ void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value) break; case 0x5b: if (m_privRegs) - m_privRegs->dispfb2 = value; + m_privRegs->writeDisplayFramebuffer(1, value); break; case 0x5c: if (m_privRegs) @@ -1617,13 +1645,14 @@ void GS::processImageData(const uint8_t *data, uint32_t sizeBytes) bool GS::clearFramebufferContext(uint32_t contextIndex, uint32_t rgba) { std::lock_guard lock(m_stateMutex); - return m_backend && m_backend->ClearFramebuffer(m_ctx[(contextIndex != 0u) ? 1 : 0], rgba); + return m_backend && m_backend->ClearFramebuffer(getDebugSnapshot().ctx[(contextIndex != 0u) ? 1 : 0], rgba); } bool GS::clearActiveFramebuffer(uint32_t rgba) { std::lock_guard lock(m_stateMutex); - return m_backend && m_backend->ClearFramebuffer(activeContext(), rgba); + const auto state = getDebugSnapshot(); + return m_backend && m_backend->ClearFramebuffer(state.ctx[state.prim.ctxt ? 1 : 0], rgba); } uint32_t GS::consumeLocalToHostBytes(uint8_t *dst, uint32_t maxBytes) @@ -1733,3 +1762,31 @@ void GS::updatePreferredDisplaySourceForDraw(const GSPrimitiveBatch &batch) m_hasPreferredDisplaySource = true; } } + +std::shared_ptr GS::getLatchedGpuFrame(uint32_t &width, uint32_t &height, float &aspectRatio) const +{ + std::lock_guard lock(m_presentationMutex); + if (!m_hostPresentationGpuFrame) + return {}; + width = m_hostPresentationWidth; + height = m_hostPresentationHeight; + aspectRatio = m_hostPresentationAspectRatio; + return m_hostPresentationGpuFrame; +} + +void GS::shutdownBackend() +{ + std::lock_guard stateLock(m_stateMutex); + std::lock_guard backendLock(m_backendLifetimeMutex); + std::lock_guard presentationLock(m_presentationMutex); + m_hostPresentationGpuFrame.reset(); + m_hostPresentationFrame.clear(); + m_hasHostPresentationFrame = false; + m_backend.reset(); +} + +uint64_t GS::getReadbackCount() const +{ + std::lock_guard lock(m_stateMutex); + return m_backend ? m_backend->GetReadbackCount() : 0; +} diff --git a/ps2xRuntime/src/lib/gs/gs_gl_interop.cpp b/ps2xRuntime/src/lib/gs/gs_gl_interop.cpp new file mode 100644 index 000000000..6546750ef --- /dev/null +++ b/ps2xRuntime/src/lib/gs/gs_gl_interop.cpp @@ -0,0 +1,410 @@ +#include "gs_gl_interop.h" +#include "context.hpp" +#define GLFW_INCLUDE_NONE +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#define GL_CALL __stdcall +#else +#include +#define GL_CALL +#endif + +namespace +{ + constexpr unsigned Texture2D = 0x0DE1, ShaderRead = 0x9591, General = 0x958D; + constexpr auto MemoryHandle = Vulkan::ExternalHandle::get_opaque_memory_handle_type(); + constexpr auto SemaphoreHandle = Vulkan::ExternalHandle::get_opaque_semaphore_handle_type(); + constexpr VkImageUsageFlags ImageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + + [[noreturn]] void fail(const std::string &message) + { + throw std::runtime_error("parallel-gs interop: " + message); + } + template + T load(const char *name) + { + auto proc = glfwGetProcAddress(name); + if (!proc) + fail(std::string("missing OpenGL entry point ") + name); + return reinterpret_cast(proc); + } + + struct GL + { +#define GL_FN(ret, name, args) \ + using name##Proc = ret(GL_CALL *) args; \ + name##Proc name = load("gl" #name) + GL_FN(void, GetIntegerv, (unsigned, int *)); + GL_FN(void, GetUnsignedBytei_vEXT, (unsigned, unsigned, unsigned char *)); + GL_FN(unsigned, GetError, ()); + GL_FN(void, CreateMemoryObjectsEXT, (int, unsigned *)); + GL_FN(void, DeleteMemoryObjectsEXT, (int, const unsigned *)); + GL_FN(void, MemoryObjectParameterivEXT, (unsigned, unsigned, const int *)); + GL_FN(void, GenTextures, (int, unsigned *)); + GL_FN(void, DeleteTextures, (int, const unsigned *)); + GL_FN(void, BindTexture, (unsigned, unsigned)); + GL_FN(void, TexParameteri, (unsigned, unsigned, int)); + GL_FN(void, TexStorageMem2DEXT, (unsigned, int, unsigned, int, int, unsigned, uint64_t)); + GL_FN(void, GenSemaphoresEXT, (int, unsigned *)); + GL_FN(void, DeleteSemaphoresEXT, (int, const unsigned *)); + GL_FN(void, WaitSemaphoreEXT, (unsigned, unsigned, const unsigned *, unsigned, const unsigned *, const unsigned *)); + GL_FN(void, SignalSemaphoreEXT, (unsigned, unsigned, const unsigned *, unsigned, const unsigned *, const unsigned *)); + GL_FN(void, Flush, ()); + GL_FN(void, Finish, ()); +#ifdef _WIN32 + GL_FN(void, ImportMemoryWin32HandleEXT, (unsigned, uint64_t, unsigned, void *)); + GL_FN(void, ImportSemaphoreWin32HandleEXT, (unsigned, unsigned, void *)); +#else + GL_FN(void, ImportMemoryFdEXT, (unsigned, uint64_t, unsigned, int)); + GL_FN(void, ImportSemaphoreFdEXT, (unsigned, unsigned, int)); +#endif +#undef GL_FN + void check(const char *operation) + { + auto error = GetError(); + if (error) + fail(std::string(operation) + " (GL error " + std::to_string(error) + ")"); + } + void import(unsigned object, Vulkan::ExternalHandle handle, uint64_t size = 0) + { + if (!handle) + fail("could not export Vulkan handle"); +#ifdef _WIN32 + if (size) + ImportMemoryWin32HandleEXT(object, size, 0x9587, handle.handle); + else + ImportSemaphoreWin32HandleEXT(object, 0x9587, handle.handle); + CloseHandle(handle.handle); // Win32 imports retain their own reference. +#else + if (size) + ImportMemoryFdEXT(object, size, 0x9586, handle.handle); + else + ImportSemaphoreFdEXT(object, 0x9586, handle.handle); + // A successful fd import transfers ownership to OpenGL. + auto error = GetError(); + if (error) + { + close(handle.handle); + fail("fd import failed (GL error " + std::to_string(error) + ")"); + } +#endif + check("external object import"); + } + }; + + struct DeviceState + { + GL gl; + Vulkan::Context context; + Vulkan::Device device; + DeviceState() + { + if (!Vulkan::Context::init_loader(nullptr)) + fail("Vulkan loader unavailable"); + + context.set_num_thread_indices(1); + if (!context.init_instance(nullptr, 0)) + fail("Vulkan instance initialization failed"); + + int uuidCount = 0; + gl.GetIntegerv(0x9596, &uuidCount); // GL_NUM_DEVICE_UUIDS_EXT + std::vector> uuids(uuidCount); + + for (int i = 0; i < uuidCount; ++i) + gl.GetUnsignedBytei_vEXT(0x9597, i, uuids[i].data()); + + gl.check("device UUID query"); + uint32_t count = 0; + vkEnumeratePhysicalDevices(context.get_instance(), &count, nullptr); + std::vector devices(count); + vkEnumeratePhysicalDevices(context.get_instance(), &count, devices.data()); + VkPhysicalDevice selected = VK_NULL_HANDLE; + + for (auto gpu : devices) + { + VkPhysicalDeviceIDProperties id{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ID_PROPERTIES}; + VkPhysicalDeviceProperties2 props{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &id}; + vkGetPhysicalDeviceProperties2(gpu, &props); + for (const auto &uuid : uuids) + if (memcmp(uuid.data(), id.deviceUUID, VK_UUID_SIZE) == 0) + selected = gpu; + } + + if (!selected) + fail("no Vulkan device matches the OpenGL device UUID"); + + VkPhysicalDeviceExternalImageFormatInfo external{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_IMAGE_FORMAT_INFO}; + external.handleType = MemoryHandle; + + VkPhysicalDeviceImageFormatInfo2 format{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_FORMAT_INFO_2, &external}; + format.format = VK_FORMAT_R8G8B8A8_UNORM; + format.type = VK_IMAGE_TYPE_2D; + format.tiling = VK_IMAGE_TILING_OPTIMAL; + format.usage = ImageUsage; + VkExternalImageFormatProperties externalProps{VK_STRUCTURE_TYPE_EXTERNAL_IMAGE_FORMAT_PROPERTIES}; + VkImageFormatProperties2 props{VK_STRUCTURE_TYPE_IMAGE_FORMAT_PROPERTIES_2, &externalProps}; + if (vkGetPhysicalDeviceImageFormatProperties2(selected, &format, &props) != VK_SUCCESS || + !(externalProps.externalMemoryProperties.externalMemoryFeatures & VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT)) + fail("RGBA8 external image memory is not exportable"); + VkPhysicalDeviceExternalSemaphoreInfo semInfo{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_SEMAPHORE_INFO}; + semInfo.handleType = SemaphoreHandle; + VkExternalSemaphoreProperties semProps{VK_STRUCTURE_TYPE_EXTERNAL_SEMAPHORE_PROPERTIES}; + vkGetPhysicalDeviceExternalSemaphoreProperties(selected, &semInfo, &semProps); + if (!(semProps.externalSemaphoreFeatures & VK_EXTERNAL_SEMAPHORE_FEATURE_EXPORTABLE_BIT)) + fail("binary external semaphores are not exportable"); + + // Granite enables the platform external-object extensions when available. + if (!context.init_device(selected, VK_NULL_HANDLE, nullptr, 0)) + fail("Vulkan device initialization failed"); + + device.set_context(context); + device.init_frame_contexts(2); + device.next_frame_context(); + } + }; + + struct Slot final : GSGpuFrame + { + std::shared_ptr owner; + Vulkan::ImageHandle image; + Vulkan::Semaphore ready, released; + unsigned texture = 0, memory = 0, glReady = 0, glReleased = 0; + bool produced = false, pendingReady = false, pendingRelease = false, acquired = false; + + explicit Slot(std::shared_ptr state) : owner(std::move(state)) + { + } + + Vulkan::ExternalHandle exportSemaphore(const Vulkan::Semaphore &semaphore) + { + // Opaque handles share the permanent payload, so export before the first + // signal is legal. Granite's helper also caters for SYNC_FD and forbids it. + auto &d = owner->device; + Vulkan::ExternalHandle handle; +#ifdef _WIN32 + VkSemaphoreGetWin32HandleInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_GET_WIN32_HANDLE_INFO_KHR}; + info.semaphore = semaphore->get_semaphore(); + info.handleType = SemaphoreHandle; + if (d.get_device_table().vkGetSemaphoreWin32HandleKHR(d.get_device(), &info, &handle.handle) != VK_SUCCESS) + fail("could not export Win32 semaphore"); +#else + VkSemaphoreGetFdInfoKHR info{VK_STRUCTURE_TYPE_SEMAPHORE_GET_FD_INFO_KHR}; + info.semaphore = semaphore->get_semaphore(); + info.handleType = SemaphoreHandle; + if (d.get_device_table().vkGetSemaphoreFdKHR(d.get_device(), &info, &handle.handle) != VK_SUCCESS) + fail("could not export semaphore fd"); +#endif + return handle; + } + + void initialize(unsigned width, unsigned height) + { + auto &d = owner->device; + auto &gl = owner->gl; + + auto info = Vulkan::ImageCreateInfo::immutable_2d_image(width, height, VK_FORMAT_R8G8B8A8_UNORM, false); + info.usage = ImageUsage; + info.initial_layout = VK_IMAGE_LAYOUT_UNDEFINED; + info.misc = Vulkan::IMAGE_MISC_EXTERNAL_MEMORY_BIT; + image = d.create_image(info); + if (!image) + fail("external image allocation failed"); + + ready = d.request_semaphore_external(VK_SEMAPHORE_TYPE_BINARY, SemaphoreHandle); + released = d.request_semaphore_external(VK_SEMAPHORE_TYPE_BINARY, SemaphoreHandle); + if (!ready || !released) + fail("external semaphore allocation failed"); + + gl.CreateMemoryObjectsEXT(1, &memory); + + const int dedicated = 1; + gl.MemoryObjectParameterivEXT(memory, 0x9581, &dedicated); + gl.check("memory object parameters"); + gl.import(memory, image->export_handle(), image->get_allocation().get_size()); + + int previous = 0; + gl.GetIntegerv(0x8069, &previous); + gl.GenTextures(1, &texture); + gl.BindTexture(Texture2D, texture); + gl.TexStorageMem2DEXT(Texture2D, 1, 0x8058, width, height, memory, image->get_allocation().get_offset()); + gl.TexParameteri(Texture2D, 0x2801, 0x2601); + gl.TexParameteri(Texture2D, 0x2800, 0x2601); + gl.TexParameteri(Texture2D, 0x2802, 0x812F); + gl.TexParameteri(Texture2D, 0x2803, 0x812F); + gl.BindTexture(Texture2D, previous); + gl.check("shared texture storage"); + gl.GenSemaphoresEXT(1, &glReady); + gl.GenSemaphoresEXT(1, &glReleased); + gl.check("GL semaphore creation"); + gl.import(glReady, exportSemaphore(ready)); + gl.import(glReleased, exportSemaphore(released)); + gl.check("shared texture creation"); + } + + ~Slot() override + { + // Slots are retired only on the GL thread after both APIs have finished. + auto &gl = owner->gl; + if (texture) + gl.DeleteTextures(1, &texture); + if (memory) + gl.DeleteMemoryObjectsEXT(1, &memory); + if (glReady) + gl.DeleteSemaphoresEXT(1, &glReady); + if (glReleased) + gl.DeleteSemaphoresEXT(1, &glReleased); + } + + uint32_t AcquireTexture() override + { + auto &gl = owner->gl; + if (pendingReady) + { + gl.WaitSemaphoreEXT(glReady, 0, nullptr, 1, &texture, &ShaderRead); + pendingReady = false; + } + else if (pendingRelease) + { + // Repeated host frame: consume the previous GL signal before signalling again. + gl.WaitSemaphoreEXT(glReleased, 0, nullptr, 1, &texture, &General); + pendingRelease = false; + } + acquired = true; + gl.check("frame acquire"); + return texture; + } + + void ReleaseTexture() override + { + if (!acquired) + fail("release without acquire"); + owner->gl.SignalSemaphoreEXT(glReleased, 0, nullptr, 1, &texture, &General); + owner->gl.Flush(); + owner->gl.check("frame release"); + pendingRelease = true; + acquired = false; + } + + void copy(const Vulkan::Image &source) + { + auto &d = owner->device; + if (acquired) + fail("attempted to overwrite an acquired frame"); + + if (pendingReady) + { + AcquireTexture(); + ReleaseTexture(); + } + + if (pendingRelease) + { + auto wait = d.request_semaphore(VK_SEMAPHORE_TYPE_BINARY, released->get_semaphore(), false); + wait->signal_external(); + wait->set_signal_is_foreign_queue(); + d.add_wait_semaphore(Vulkan::CommandBuffer::Type::Generic, std::move(wait), VK_PIPELINE_STAGE_2_TRANSFER_BIT, true); + pendingRelease = false; + } + + auto cmd = d.request_command_buffer(); + + if (produced) + cmd->acquire_image_barrier(*image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + else + cmd->image_barrier(*image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_2_NONE, 0, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + + cmd->copy_image(*image, source); + cmd->release_image_barrier(*image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); + d.submit(cmd); + // Borrow the owned external binary semaphore for this one submission. + auto signal = d.request_semaphore(VK_SEMAPHORE_TYPE_BINARY, ready->get_semaphore(), false); + d.submit_empty(Vulkan::CommandBuffer::Type::Generic, nullptr, signal.get()); + signal->wait_external(); + produced = pendingReady = true; + } + }; +} + +class GSGLInterop::Impl +{ +public: + std::shared_ptr state; + std::array, 2> slots; + unsigned next = 0; + + Impl() + { + if (!glfwGetCurrentContext()) + fail("a current OpenGL context is required"); + const char *required[] = {"GL_EXT_memory_object", "GL_EXT_semaphore", +#ifdef _WIN32 + "GL_EXT_memory_object_win32", "GL_EXT_semaphore_win32" +#else + "GL_EXT_memory_object_fd", "GL_EXT_semaphore_fd" +#endif + }; + for (auto name : required) + if (!glfwExtensionSupported(name)) + fail(std::string("missing ") + name); + state = std::make_shared(); + // Validate actual GL import before boot, even if the game never scans out. + for (auto &slot : slots) + { + slot = std::make_shared(state); + slot->initialize(1, 1); + } + } + + ~Impl() + { + state->gl.Finish(); + state->device.wait_idle(); + } +}; + +GSGLInterop::GSGLInterop() : m_impl(std::make_unique()) +{ +} +GSGLInterop::~GSGLInterop() = default; + +Vulkan::Device &GSGLInterop::device() +{ + return m_impl->state->device; +} + +PresentationFrame GSGLInterop::present(const ParallelGS::ScanoutResult &scanout) +{ + if (!scanout.image) + return {}; + + if (scanout.image->get_format() != VK_FORMAT_R8G8B8A8_UNORM) + fail("unexpected scanout format"); + + const auto width = scanout.image->get_width(), height = scanout.image->get_height(); + auto &slot = m_impl->slots[m_impl->next++ % 2]; + if (slot->image->get_width() != width || slot->image->get_height() != height) + { + m_impl->state->gl.Finish(); + device().wait_idle(); + slot = std::make_shared(m_impl->state); + slot->initialize(width, height); + } + slot->copy(*scanout.image); + PresentationFrame frame; + frame.gpu = slot; + frame.width = width; + frame.height = height; + frame.aspectRatio = 4.0f / 3.0f; + if (scanout.mode_width && scanout.mode_height && scanout.internal_height) + frame.aspectRatio *= (float(scanout.internal_width) / scanout.mode_width) / + (float(scanout.internal_height) / scanout.mode_height); + return frame; +} diff --git a/ps2xRuntime/src/lib/gs/gs_gl_interop.h b/ps2xRuntime/src/lib/gs/gs_gl_interop.h new file mode 100644 index 000000000..002fa404d --- /dev/null +++ b/ps2xRuntime/src/lib/gs/gs_gl_interop.h @@ -0,0 +1,15 @@ +#pragma once +#include "runtime/gs/gs_types.h" +#include "gs_renderer.hpp" + +class GSGLInterop +{ +public: + GSGLInterop(); + ~GSGLInterop(); + Vulkan::Device &device(); + PresentationFrame present(const ParallelGS::ScanoutResult &scanout); +private: + class Impl; + std::unique_ptr m_impl; +}; diff --git a/ps2xRuntime/src/lib/gs/gs_parallel_backend.cpp b/ps2xRuntime/src/lib/gs/gs_parallel_backend.cpp new file mode 100644 index 000000000..572a702db --- /dev/null +++ b/ps2xRuntime/src/lib/gs/gs_parallel_backend.cpp @@ -0,0 +1,501 @@ +#include "runtime/gs/gs_parallel_backend.h" +#include "runtime/gs/gs_frontend.h" +#include "runtime/gs/ps2_gs_memory.h" +#include "runtime/ps2_memory.h" +#include "ps2_log.h" +#include "gs_gl_interop.h" +#include "gs_interface.hpp" +#include "thread_id.hpp" +#include +#include +#include +#include + +namespace +{ + constexpr size_t VramSize = 4 * 1024 * 1024; + uint64_t word(const void *data) + { + uint64_t value; + std::memcpy(&value, data, 8); + return value; + } + + unsigned bitsPerPixel(unsigned psm) + { + switch (psm & 63) + { + case GS_PSM_CT24: + case GS_PSM_Z24: + return 24; + case GS_PSM_CT16: + case GS_PSM_CT16S: + case GS_PSM_Z16: + case GS_PSM_Z16S: + return 16; + case GS_PSM_T8: + case GS_PSM_T8H: + return 8; + case GS_PSM_T4: + case GS_PSM_T4HL: + case GS_PSM_T4HH: + return 4; + default: + return 32; + } + } + + using Reader = uint32_t (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t); + using Writer = void (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); + struct PixelAccess + { + Reader read; + Writer write; + }; + + PixelAccess pixelAccess(unsigned psm) + { + using namespace GSMem; + switch (psm & 63) + { +#define PSM(id, name) \ + case GS_PSM_##id: \ + return {Read##name, Write##name} + PSM(CT32, CT32); + PSM(CT24, CT24); + PSM(CT16, CT16); + PSM(CT16S, CT16S); + PSM(T8, P8); + PSM(T8H, P8H); + PSM(T4, P4); + PSM(T4HL, P4HL); + PSM(T4HH, P4HH); + PSM(Z32, Z32); + PSM(Z24, Z24); + PSM(Z16, Z16); + PSM(Z16S, Z16S); +#undef PSM + default: + return {ReadNull, WriteNull}; + } + } + + class ParallelBackend final : public GSRasterBackend, private ParallelGS::SignalInterface + { + // All GSInterface/Device entry points are serialized, including presentation. + mutable std::recursive_mutex mutex; + GSGLInterop interop; + mutable ParallelGS::GSInterface gs; + GSRegisters &priv; + struct Lock + { + std::lock_guard guard; + explicit Lock(std::recursive_mutex &m) : guard(m) { Util::register_thread_index(0); } + }; + uint64_t bitblt = 0, trxreg = 0; + GSTransferSnapshot transfer; + size_t transferBytes = 0, uploadedBytes = 0; + std::array uploadTail{}; + size_t uploadTailSize = 0; + std::array fifoTail{}; + size_t fifoOffset = 16; + mutable uint64_t readbacks = 0; + + void observeRegister(uint8_t addr, uint64_t value) + { + if (addr == GS_REG_BITBLTBUF) + bitblt = value; + if (addr == GS_REG_TRXREG) + trxreg = value; + if (addr == GS_REG_TRXDIR) + { + transfer = {}; + transfer.direction = value & 3; + transfer.totalPixels = uint32_t(trxreg & 0xfff) * uint32_t((trxreg >> 32) & 0xfff); + unsigned psm = unsigned(bitblt >> (transfer.direction == 1 ? 24 : 56)) & 63; + transferBytes = (size_t(transfer.totalPixels) * bitsPerPixel(psm) + 7) / 8; + transfer.localToHostPendingBytes = transfer.direction == 1 ? transferBytes : 0; + if (transfer.direction == 1 && transferBytes) + ++readbacks; + transfer.copiedPixels = transfer.direction == 2 ? transfer.totalPixels : 0; + uploadedBytes = uploadTailSize = 0; + fifoOffset = fifoTail.size(); + } + + if (addr == GS_REG_HWREG) + observeImage(8); + } + void observeImage(size_t bytes) + { + if (transfer.direction != 0) + return; + uploadedBytes = std::min(transferBytes, uploadedBytes + bytes); + transfer.copiedPixels = std::min(transfer.totalPixels, uploadedBytes * 8 / bitsPerPixel(unsigned(bitblt >> 56))); + auto width = uint32_t(trxreg & 0xfff); + if (width) + { + transfer.x = transfer.copiedPixels % width; + transfer.y = transfer.copiedPixels / width; + } + } + + // Observe transfer boundaries only; the upstream decoder alone executes commands. + // Start from upstream's saved PATH state, including packets split across DMA calls. + void observeGIF(uint32_t path, const uint8_t *data, size_t size) + { + auto state = gs.get_gif_path(path); + for (size_t offset = 0; offset < size;) + { + if (state.loop == state.tag.NLOOP) + { + std::memcpy(&state.tag, data + offset, 16); + state.reg = state.loop = 0; + offset += 16; + continue; + } + unsigned nreg = state.tag.NREG ? state.tag.NREG : 16; + if (state.tag.FLG >= 2) + { + size_t count = std::min(state.tag.NLOOP - state.loop, (size - offset) / 16); + observeImage(count * 16); + state.loop += uint32_t(count); + offset += count * 16; + } + else + { + // Only packed A+D can address the transfer registers (>= 0x50). + const uint64_t descriptors = word(reinterpret_cast(&state.tag) + 8); + if (state.tag.FLG == 0 && ((descriptors >> (state.reg * 4)) & 15) == 14) + observeRegister(uint8_t(word(data + offset + 8) & 127), word(data + offset)); + unsigned count = state.tag.FLG == 0 ? 1 : 2; + while (count-- && state.loop < state.tag.NLOOP) + if (++state.reg == nreg) + { + state.reg = 0; + ++state.loop; + } + offset += 16; + } + } + } + + bool on_signal(uint64_t value) override + { + auto mask = uint32_t(value >> 32); + auto id = (uint32_t(priv.siglblid) & ~mask) | (uint32_t(value) & mask); + priv.siglblid = (priv.siglblid & 0xffffffff00000000ull) | id; + priv.csr.fetch_or(1); + return false; + } + + bool on_finish(uint64_t) override + { + gs.flush(); + interop.device().wait_idle(); + priv.csr.fetch_or(2); + return false; + } + + bool on_label(uint64_t value) override + { + auto mask = uint32_t(value >> 32); + auto id = (uint32_t(priv.siglblid >> 32) & ~mask) | (uint32_t(value) & mask); + priv.siglblid = (uint64_t(id) << 32) | uint32_t(priv.siglblid); + return false; + } + + public: + explicit ParallelBackend(GSRegisters ®isters) : priv(registers) + { + if (!gs.init(&interop.device(), ParallelGS::GSOptions{})) + throw std::runtime_error("parallel-gs: GS initialization failed; check Vulkan feature diagnostics above"); + gs.set_signal_interface(this); + } + + ~ParallelBackend() override + { + Lock lock(mutex); + gs.flush(); + interop.device().wait_idle(); + } + + bool UsesRawCommands() const override + { + return true; + } + + uint64_t GetReadbackCount() const override + { + Lock lock(mutex); + return readbacks; + } + + void Initialize(uint8_t *vram, uint32_t size) override + { + Lock lock(mutex); + if (!vram || size < VramSize) + throw std::invalid_argument("parallel-gs requires 4 MiB initial VRAM"); + std::memcpy(gs.map_vram_write(0, VramSize), vram, VramSize); + gs.end_vram_write(0, VramSize); + } + + void Reset() override + { + Lock lock(mutex); + gs.flush(); + interop.device().wait_idle(); + gs.reset_context_state(); + bitblt = trxreg = transferBytes = uploadedBytes = uploadTailSize = 0; + fifoOffset = 16; + transfer = {}; + } + + void ProcessGIF(uint32_t path, const uint8_t *data, uint32_t size) override + { + Lock lock(mutex); + if (path < 1 || path > 3 || size % 16) + throw std::invalid_argument("parallel-gs: invalid GIF path or size"); + if (!data || !size) + return; + observeGIF(path, data, size); + gs.gif_transfer(path, data, size); + } + + void WriteRegisterRaw(uint8_t addr, uint64_t value) override + { + Lock lock(mutex); + if (addr >= 128) + return; // Reserved addresses never index upstream's 128-entry table. + observeRegister(addr, value); + // Compatibility aliases used by native stubs, not actual GIF registers. + switch (addr) + { + case 0x59: + priv.writeDisplayFramebuffer(0, value); + return; + case 0x5a: + priv.display1 = value; + return; + case 0x5b: + priv.writeDisplayFramebuffer(1, value); + return; + case 0x5c: + priv.display2 = value; + return; + case 0x5f: + priv.bgcolor = value; + return; + } + gs.write_register(static_cast(addr), value); + } + + void Submit(const GSPrimitiveBatch &) override + { + throw std::logic_error("parallel-gs received a CPU primitive batch"); + } + + void LoadClut(const GSTex0Reg &, const GSTexClutReg &) override + { + } + + void BeginTransfer(const GSTransferCommand &) override + { + throw std::logic_error("parallel-gs requires raw transfer registers"); + } + + void UploadImage(const uint8_t *data, uint32_t size) override + { + Lock lock(mutex); + if (!data || transfer.direction != 0) + return; + size = uint32_t(std::min(size, transferBytes - uploadedBytes)); + observeImage(size); + while (size) + { + auto n = std::min(size, 8 - uploadTailSize); + std::memcpy(uploadTail.data() + uploadTailSize, data, n); + uploadTailSize += n; + data += n; + size -= uint32_t(n); + if (uploadTailSize == 8 || uploadedBytes == transferBytes && size == 0) + { + std::fill(uploadTail.begin() + uploadTailSize, uploadTail.end(), 0); + gs.write_register(ParallelGS::RegisterAddr::HWREG, word(uploadTail.data())); + uploadTailSize = 0; + } + } + } + + void Flush() override + { + Lock lock(mutex); + gs.flush(); + } + + void TextureFlush() override + { + WriteRegisterRaw(GS_REG_TEXFLUSH, 0); + } + + void Sync(GSSyncReason reason) override + { + Lock lock(mutex); + if (reason == GSSyncReason::Finish || reason == GSSyncReason::Reset) + { + gs.flush(); + interop.device().wait_idle(); + } + // map_vram_read and the upstream FIFO synchronize their own readbacks. + } + + PresentationFrame Present(const GSPresentationRequest &request) override + { + Lock lock(mutex); + gs.flush(); + auto &p = gs.get_priv_register_state(); + p.qwords_lo[0] = request.pmode; + p.qwords_lo[2] = request.smode1; + p.qwords_lo[4] = request.smode2; + p.qwords_lo[12] = request.syncv; + p.qwords_lo[14] = request.dispfb1; + p.qwords_lo[16] = request.display1; + p.qwords_lo[18] = request.dispfb2; + p.qwords_lo[20] = request.display2; + p.qwords_lo[28] = request.bgcolor; + ParallelGS::VSyncInfo info{}; + info.phase = request.field; + info.dst_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + info.dst_stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT; + info.dst_access = VK_ACCESS_2_TRANSFER_READ_BIT; + info.crtc_offsets = true; + return interop.present(gs.vsync(info)); + } + + uint32_t ReadVram(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y) const override + { + Lock lock(mutex); + auto *mapped = const_cast(static_cast(gs.map_vram_read(0, VramSize))); + ++readbacks; + return pixelAccess(psm).read(mapped, base, bw, x, y); + } + + void WriteVram(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y, uint32_t value) override + { + Lock lock(mutex); + gs.map_vram_read(0, VramSize); // Preserve untouched pixels and packed-format lanes. + auto *mapped = static_cast(gs.map_vram_write(0, VramSize)); + ++readbacks; + pixelAccess(psm).write(mapped, base, bw, x, y, value); + gs.end_vram_write(0, VramSize); + } + + void SnapshotVram(std::vector &out) const override + { + Lock lock(mutex); + auto *mapped = static_cast(gs.map_vram_read(0, VramSize)); + ++readbacks; + out.assign(mapped, mapped + VramSize); + } + + bool ClearFramebuffer(const GSContext &ctx, uint32_t rgba) override + { + Lock lock(mutex); + if (!ctx.frame.fbw || (ctx.frame.psm != GS_PSM_CT32 && ctx.frame.psm != GS_PSM_CT24 && + ctx.frame.psm != GS_PSM_CT16 && ctx.frame.psm != GS_PSM_CT16S)) + return false; + + gs.map_vram_read(0, VramSize); // FBMSK and untouched VRAM require current contents. + auto *mapped = static_cast(gs.map_vram_write(0, VramSize)); + ++readbacks; + auto access = pixelAccess(ctx.frame.psm); + + if (ctx.fba & 1) + rgba |= 0x80000000u; + if (bitsPerPixel(ctx.frame.psm) == 16) + rgba = ((rgba >> 3) & 31) | ((rgba >> 6) & 0x3e0) | ((rgba >> 9) & 0x7c00) | ((rgba >> 16) & 0x8000); + + for (uint32_t y = ctx.scissor.y0; y <= ctx.scissor.y1; ++y) + for (uint32_t x = ctx.scissor.x0; x <= ctx.scissor.x1; ++x) + { + auto old = access.read(mapped, ctx.frame.fbp * 32, ctx.frame.fbw, x, y); + access.write(mapped, ctx.frame.fbp * 32, ctx.frame.fbw, x, y, + (rgba & ~ctx.frame.fbmsk) | (old & ctx.frame.fbmsk)); + } + gs.end_vram_write(0, VramSize); + return true; + } + + uint32_t ConsumeLocalToHostBytes(uint8_t *dst, uint32_t maxBytes) override + { + Lock lock(mutex); + if (!dst) + return 0; + auto count = uint32_t(std::min(maxBytes, transfer.localToHostPendingBytes)); + for (uint32_t i = 0; i < count; ++i) + { + if (fifoOffset == 16) + { + gs.read_transfer_fifo(fifoTail.data(), 1); + fifoOffset = 0; + } + dst[i] = fifoTail[fifoOffset++]; + } + transfer.localToHostPendingBytes -= count; + return count; + } + + GSTransferSnapshot GetTransferSnapshot() const override + { + Lock lock(mutex); + return transfer; + } + + void ReadRegisterState(GSDebugSnapshot &s) const override + { + Lock lock(mutex); + const auto &r = gs.get_register_state(); + for (unsigned i = 0; i < 2; ++i) + { + auto &c = s.ctx[i]; + const auto &v = r.ctx[i]; + auto f = v.frame.bits, t = v.tex0.bits, sc = v.scissor.bits, z = v.zbuf.bits; + c.frame = {uint32_t(f & 511), uint32_t((f >> 16) & 63), uint8_t((f >> 24) & 63), uint32_t(f >> 32)}; + c.zbuf = {uint32_t(z & 511), uint8_t(((z >> 24) & 15) | 0x30), bool((z >> 32) & 1)}; + c.scissor = {uint16_t(sc & 2047), uint16_t((sc >> 16) & 2047), uint16_t((sc >> 32) & 2047), uint16_t((sc >> 48) & 2047)}; + c.xyoffset = {uint16_t(v.xyoffset.bits), uint16_t(v.xyoffset.bits >> 32)}; + c.tex0 = {uint32_t(t & 16383), uint8_t((t >> 14) & 63), uint8_t((t >> 20) & 63), uint8_t((t >> 26) & 15), + uint8_t((t >> 30) & 15), uint8_t((t >> 34) & 1), uint8_t((t >> 35) & 3), uint32_t((t >> 37) & 16383), + uint8_t((t >> 51) & 15), uint8_t((t >> 55) & 1), uint8_t((t >> 56) & 31), uint8_t(t >> 61)}; + c.tex1 = v.tex1.bits; + c.miptbp1 = v.miptbl_1_3.bits; + c.miptbp2 = v.miptbl_4_6.bits; + c.clamp = v.clamp.bits; + c.alpha = v.alpha.bits; + c.test = v.test.bits; + c.fba = v.fba.bits; + } + + auto p = r.prim.bits; + s.prim = {GSPrimType(p & 7), bool(p & 8), bool(p & 16), bool(p & 32), bool(p & 64), bool(p & 128), bool(p & 256), bool(p & 512), bool(p & 1024)}; + auto b = r.bitbltbuf.bits, t = r.trxpos.bits, a = r.texa.bits, c = r.texclut.bits; + s.texa = {uint8_t(a), bool(a & 0x8000), uint8_t(a >> 32)}; + s.texclut = {uint8_t(c & 63), uint8_t((c >> 6) & 63), uint16_t((c >> 12) & 1023)}; + s.scanmsk = r.scanmsk.bits; + s.dimx = r.dimx.bits; + s.dthe = r.dthe.bits; + s.colclamp = r.colclamp.bits; + s.bitbltbuf = {uint32_t(b & 16383), uint8_t((b >> 16) & 63), uint8_t((b >> 24) & 63), + uint32_t((b >> 32) & 16383), uint8_t((b >> 48) & 63), uint8_t((b >> 56) & 63)}; + + s.trxpos = {uint16_t(t & 2047), uint16_t((t >> 16) & 2047), uint16_t((t >> 32) & 2047), uint16_t((t >> 48) & 2047), uint8_t((t >> 59) & 3)}; + s.trxreg = {uint16_t(r.trxreg.bits & 4095), uint16_t((r.trxreg.bits >> 32) & 4095)}; + s.trxdir = uint32_t(r.trxdir.bits & 3); + } + }; +} + +std::unique_ptr CreateParallelGSBackend(GSRegisters ®isters) +{ + RUNTIME_WARNING("GS Parallel backend is experimental and may not be fully compatible with all games."); + return std::make_unique(registers); +} diff --git a/ps2xRuntime/src/lib/gs/ps2_gif_arbiter.cpp b/ps2xRuntime/src/lib/gs/ps2_gif_arbiter.cpp index c289ba0f2..6088598cd 100644 --- a/ps2xRuntime/src/lib/gs/ps2_gif_arbiter.cpp +++ b/ps2xRuntime/src/lib/gs/ps2_gif_arbiter.cpp @@ -56,7 +56,7 @@ void GifArbiter::drain() auto &pkt = m_queue[i]; if (!pkt.data.empty()) { - m_processFn(pkt.data.data(), static_cast(pkt.data.size())); + m_processFn(pkt.pathId, pkt.data.data(), static_cast(pkt.data.size())); } } m_queue.clear(); diff --git a/ps2xRuntime/src/lib/ps2_debug_panel.cpp b/ps2xRuntime/src/lib/ps2_debug_panel.cpp index c9aa2c6a4..01be01333 100644 --- a/ps2xRuntime/src/lib/ps2_debug_panel.cpp +++ b/ps2xRuntime/src/lib/ps2_debug_panel.cpp @@ -2182,6 +2182,7 @@ void PS2DebugPanel::initialize() if (!m_initialized) { rlImGuiSetup(true); + m_fpsSampleStart = {}; m_initialized = true; } #endif @@ -2205,6 +2206,40 @@ void PS2DebugPanel::draw(PS2Runtime &runtime) { return; } + + const auto now = std::chrono::steady_clock::now(); + const auto ®s = runtime.memory().gs(); + const uint64_t sdkPresents = regs.sdkPresentCount.load(std::memory_order_relaxed); + const std::array flips{ + regs.displayFlipCount[0].load(std::memory_order_relaxed), + regs.displayFlipCount[1].load(std::memory_order_relaxed)}; + if (m_fpsSampleStart == std::chrono::steady_clock::time_point{} || + flips[0] < m_lastDisplayFlips[0] || flips[1] < m_lastDisplayFlips[1] || sdkPresents < m_lastSdkPresents) + { + m_fpsSampleStart = now; + m_lastDisplayFlips = flips; + m_lastSdkPresents = sdkPresents; + m_hostFramesInSample = 0; + m_gameFps = m_hostFps = 0.0; + } + else + { + ++m_hostFramesInSample; + const double seconds = std::chrono::duration(now - m_fpsSampleStart).count(); + if (seconds >= 1.0) + { + // Once SDK swaps are observed, keep using their counter even when + // it stops: a stalled game must report zero, not host activity. + const uint64_t gameFrames = sdkPresents ? sdkPresents - m_lastSdkPresents : + std::max(flips[0] - m_lastDisplayFlips[0], flips[1] - m_lastDisplayFlips[1]); + m_gameFps = double(gameFrames) / seconds; + m_hostFps = double(m_hostFramesInSample) / seconds; + m_lastDisplayFlips = flips; + m_lastSdkPresents = sdkPresents; + m_hostFramesInSample = 0; + m_fpsSampleStart = now; + } + } if (IsKeyPressed(KEY_F1)) { @@ -2227,6 +2262,13 @@ void PS2DebugPanel::draw(PS2Runtime &runtime) ImGui::EndMenuBar(); } + ImGui::Text("Game FPS (%s): %.1f | Host FPS: %.1f", sdkPresents ? "SDK swaps" : "estimated flips", m_gameFps, m_hostFps); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Game: completed sceGsSwapDBuff/sceGsSwapDBuffDc calls per second, when used.\n" + "Without SDK swaps: estimate from framebuffer base changes on the busiest GS circuit.\n" + "These sources are never added together. This measures presentation, not simulation FPS.\n" + "Direct fixed-buffer rendering may show 0; raster effects may overcount the estimate.\n" + "Host: window redraws per second. Both rates use a one-second sample."); if (ImGui::BeginTabBar("debug-tabs")) { if (ImGui::BeginTabItem("CPU")) diff --git a/ps2xRuntime/src/lib/ps2_memory.cpp b/ps2xRuntime/src/lib/ps2_memory.cpp index 37b6a4dec..34694381f 100644 --- a/ps2xRuntime/src/lib/ps2_memory.cpp +++ b/ps2xRuntime/src/lib/ps2_memory.cpp @@ -97,6 +97,16 @@ namespace } } + inline void writeGsRegister(GSRegisters &gs, uint64_t *reg, uint64_t value) + { + if (reg == &gs.dispfb1) + gs.writeDisplayFramebuffer(0, value); + else if (reg == &gs.dispfb2) + gs.writeDisplayFramebuffer(1, value); + else + *reg = value; + } + constexpr uint32_t kGsCsrRegOffset = 0x1000u; // Atomically apply a 32-bit write to one half (off=0 low dword, off=4 high @@ -863,6 +873,26 @@ __m128i PS2Memory::read128(uint32_t address) return _mm_setzero_si128(); } +bool PS2Memory::writeVuCode(bool vu1, uint32_t offset, std::span bytes) +{ + auto *code = vu1 ? m_vu1Code : m_vu0Code; + if (!code) + throw std::logic_error("VU code memory is not initialized"); + + const auto size = vu1 ? PS2_VU1_CODE_SIZE : PS2_VU0_CODE_SIZE; + inRange(offset, bytes.size(), size, "VU code write", offset); + const bool changed = !bytes.empty() && std::memcmp(code + offset, bytes.data(), bytes.size()) != 0; + if (changed) + { + std::memmove(code + offset, bytes.data(), bytes.size()); + if (vu1) + markVU1CodeModified(); + else + markVU0CodeModified(); + } + return changed; +} + void PS2Memory::write8(uint32_t address, uint8_t value) { const bool scratch = isScratchpad(address); @@ -883,11 +913,10 @@ void PS2Memory::write8(uint32_t address, uint8_t value) if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint8_t), vuOffset, vuLimit)) { (void)vuLimit; - vuMem[vuOffset] = value; - if (vuMem == m_vu0Code) - markVU0CodeModified(); - else if (vuMem == m_vu1Code) - markVU1CodeModified(); + if (vuMem == m_vu0Code || vuMem == m_vu1Code) + writeVuCode(vuMem == m_vu1Code, vuOffset, {&value, 1}); + else + vuMem[vuOffset] = value; return; } } @@ -926,11 +955,10 @@ void PS2Memory::write16(uint32_t address, uint16_t value) uint32_t vuLimit = 0; if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint16_t), vuOffset, vuLimit)) { - storeScalar(vuMem, vuOffset, vuLimit, value, "write16 vu", address); - if (vuMem == m_vu0Code) - markVU0CodeModified(); - else if (vuMem == m_vu1Code) - markVU1CodeModified(); + if (vuMem == m_vu0Code || vuMem == m_vu1Code) + writeVuCode(vuMem == m_vu1Code, vuOffset, {reinterpret_cast(&value), sizeof(value)}); + else + storeScalar(vuMem, vuOffset, vuLimit, value, "write16 vu", address); return; } } @@ -965,7 +993,7 @@ void PS2Memory::write32(uint32_t address, uint32_t value) { uint64_t mask = 0xFFFFFFFFULL << (off * 8); uint64_t newVal = (*reg & ~mask) | ((uint64_t)value << (off * 8)); - *reg = newVal; + writeGsRegister(gs_regs, reg, newVal); } return; } @@ -990,11 +1018,10 @@ void PS2Memory::write32(uint32_t address, uint32_t value) uint32_t vuLimit = 0; if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint32_t), vuOffset, vuLimit)) { - storeScalar(vuMem, vuOffset, vuLimit, value, "write32 vu", address); - if (vuMem == m_vu0Code) - markVU0CodeModified(); - else if (vuMem == m_vu1Code) - markVU1CodeModified(); + if (vuMem == m_vu0Code || vuMem == m_vu1Code) + writeVuCode(vuMem == m_vu1Code, vuOffset, {reinterpret_cast(&value), sizeof(value)}); + else + storeScalar(vuMem, vuOffset, vuLimit, value, "write32 vu", address); return; } } @@ -1022,7 +1049,7 @@ void PS2Memory::write64(uint32_t address, uint64_t value) } else if (uint64_t *reg = gsRegPtr(gs_regs, address)) { - *reg = value; + writeGsRegister(gs_regs, reg, value); } return; } @@ -1045,11 +1072,10 @@ void PS2Memory::write64(uint32_t address, uint64_t value) uint32_t vuLimit = 0; if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint64_t), vuOffset, vuLimit)) { - storeScalar(vuMem, vuOffset, vuLimit, value, "write64 vu", address); - if (vuMem == m_vu0Code) - markVU0CodeModified(); - else if (vuMem == m_vu1Code) - markVU1CodeModified(); + if (vuMem == m_vu0Code || vuMem == m_vu1Code) + writeVuCode(vuMem == m_vu1Code, vuOffset, {reinterpret_cast(&value), sizeof(value)}); + else + storeScalar(vuMem, vuOffset, vuLimit, value, "write64 vu", address); return; } } @@ -1103,11 +1129,10 @@ void PS2Memory::write128(uint32_t address, __m128i value) if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(__m128i), vuOffset, vuLimit)) { inRange(vuOffset, sizeof(__m128i), vuLimit, "write128 vu", address); - _mm_storeu_si128(reinterpret_cast<__m128i *>(vuMem + vuOffset), value); - if (vuMem == m_vu0Code) - markVU0CodeModified(); - else if (vuMem == m_vu1Code) - markVU1CodeModified(); + if (vuMem == m_vu0Code || vuMem == m_vu1Code) + writeVuCode(vuMem == m_vu1Code, vuOffset, {reinterpret_cast(&value), sizeof(value)}); + else + _mm_storeu_si128(reinterpret_cast<__m128i *>(vuMem + vuOffset), value); return; } } @@ -1173,7 +1198,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) else if (uint64_t *reg = gsRegPtr(gs_regs, address)) { const uint64_t mask = 0xFFFFFFFFull << (off * 8u); - *reg = (*reg & ~mask) | (static_cast(value) << (off * 8u)); + writeGsRegister(gs_regs, reg, (*reg & ~mask) | (static_cast(value) << (off * 8u))); } m_gsWriteCount.fetch_add(1, std::memory_order_relaxed); return true; diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 45cb9d267..2f41712aa 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -1,16 +1,23 @@ #include "ps2_runtime.h" +#include "runtime/frame_timing.h" #include "ps2_log.h" #include "ps2_stubs.h" #include "ps2_syscalls.h" #include "game_overrides.h" #include "ps2_runtime_macros.h" #include "runtime/gs/gs_frontend.h" +#ifdef PS2X_GS_PARALLEL +#include "runtime/gs/gs_parallel_backend.h" +#endif #include "runtime/ee_scheduler.h" #include "ThreadNaming.h" #include "Kernel/Stubs/Audio.h" #include "Kernel/Stubs/GS.h" #include "Kernel/Stubs/MPEG.h" #include "ps2_host_backend.h" +#ifdef PS2X_GS_PARALLEL +#include "rlgl.h" +#endif #include "ps2_iop_host.h" #include "ps2x/iop/iop_subsystem.h" @@ -22,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -195,88 +203,6 @@ namespace return tlbRefill ? EXCEPTION_VECTOR_TLB_REFILL : EXCEPTION_VECTOR_GENERAL; } - void seedVu0IdleSuccess(R5900Context *ctx) - { - if (!ctx) - { - return; - } - - ctx->vu0_clip_flags = 0; - ctx->vu0_clip_flags2 = 0; - ctx->vu0_mac_flags = 0; - ctx->vu0_status = 0; - ctx->vu0_q = 1.0f; - ctx->vu0_r = _mm_castsi128_ps(_mm_set1_epi32(0x3F800000)); - ctx->vu0_vpu_stat = 0; - ctx->vu0_vpu_stat2 = 0; - } - - void copyVu0ContextToState(const R5900Context *ctx, VU1State &state) - { - std::memset(&state, 0, sizeof(state)); - - for (uint32_t i = 0; i < 32u; ++i) - { - _mm_storeu_ps(state.vf[i], ctx->vu0_vf[i]); - } - for (uint32_t i = 0; i < 16u; ++i) - { - state.vi[i] = static_cast(ctx->vi[i]); - } - - _mm_storeu_ps(state.acc, ctx->vu0_acc); - state.q = ctx->vu0_q; - state.p = ctx->vu0_p; - state.i = ctx->vu0_i; - alignas(16) uint32_t rWords[4]{}; - _mm_storeu_si128(reinterpret_cast<__m128i *>(rWords), _mm_castps_si128(ctx->vu0_r)); - state.r = 0x3F800000u | (rWords[0] & 0x007FFFFFu); - state.pc = ctx->vu0_pc; - state.mac = ctx->vu0_mac_flags; - state.clip = ctx->vu0_clip_flags; - state.status = ctx->vu0_status; - state.itop = ctx->vu0_itop; - state.dBitEnabled = (ctx->vu0_fbrst & (1u << 2)) != 0u; - state.tBitEnabled = (ctx->vu0_fbrst & (1u << 3)) != 0u; - - state.vf[0][0] = 0.0f; - state.vf[0][1] = 0.0f; - state.vf[0][2] = 0.0f; - state.vf[0][3] = 1.0f; - state.vi[0] = 0; - } - - void copyVu0StateToContext(const VU1State &state, R5900Context *ctx) - { - for (uint32_t i = 0; i < 32u; ++i) - { - ctx->vu0_vf[i] = _mm_loadu_ps(state.vf[i]); - } - for (uint32_t i = 0; i < 16u; ++i) - { - ctx->vi[i] = static_cast(state.vi[i]); - } - - ctx->vu0_acc = _mm_loadu_ps(state.acc); - ctx->vu0_q = state.q; - ctx->vu0_p = state.p; - ctx->vu0_i = state.i; - ctx->vu0_r = _mm_castsi128_ps(_mm_set1_epi32(static_cast(state.r))); - ctx->vu0_mac_flags = state.mac; - ctx->vu0_clip_flags = state.clip; - ctx->vu0_clip_flags2 = state.clip; - ctx->vu0_status = static_cast(state.status); - ctx->vu0_itop = state.itop; - ctx->vu0_pc = state.pc; - ctx->vu0_tpc = state.pc; - ctx->vu0_vpu_stat = (ctx->vu0_vpu_stat & 0xFF00u) | (state.stoppedByD ? (1u << 1) : 0u) | (state.stoppedByT ? (1u << 2) : 0u); - ctx->vu0_vpu_stat2 = 0; - - ctx->vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f); - ctx->vi[0] = 0; - } - void raiseCop0Exception(R5900Context *ctx, uint32_t exceptionCode, bool tlbRefill = false) { if (ctx->in_delay_slot) @@ -394,7 +320,12 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint s_lastPresentationTick = currentTick; s_hasLatchedInitialFrame = true; } - else if (s_hasUploadedFrame) +#ifdef PS2X_GS_PARALLEL + float aspect = 0.0f; + rt->gs().getLatchedGpuFrame(outWidth, outHeight, aspect); + return; +#endif + if (!needsLatch && s_hasUploadedFrame) { outWidth = (s_lastWidth != 0u) ? s_lastWidth : FB_WIDTH; outHeight = (s_lastHeight != 0u) ? s_lastHeight : DEFAULT_DISPLAY_HEIGHT; @@ -479,6 +410,10 @@ static void UploadFrame(Texture2D &tex, PS2Runtime *rt, uint32_t &outWidth, uint PS2Runtime::PS2Runtime() { + if (g_ps2RecompiledEeAbiVersion != 4u) + { + throw std::runtime_error("EE generated code ABI mismatch: regenerate the game with this recompiler"); + } m_iopHost = std::make_unique(*this); m_iopSubsystem = std::make_unique(*m_iopHost); @@ -549,6 +484,7 @@ PS2Runtime::~PS2Runtime() if (IsWindowReady()) { + m_gs.shutdownBackend(); CloseWindow(); } @@ -607,7 +543,22 @@ void PS2Runtime::notifyIopSifTransfer(uint8_t *rdram, const ps2x::iop::SifTransf void PS2Runtime::advanceIopEeCycles(uint64_t eeCycles) noexcept { + if (m_vu) + { + auto *context = m_eeScheduler->currentContext(); + auto remaining = eeCycles; + while (remaining) + { + const auto cycles = static_cast(std::min(remaining, UINT32_MAX)); + m_vu->advance(cycles, context ? *context : m_cpuContext); + remaining -= cycles; + } + if (m_vu->failed()) + m_eeScheduler->requestStop(); + } m_iopSubsystem->runEeCycles(eeCycles); + if (m_iopSubsystem->fault().active) + m_eeScheduler->requestStop(); } void PS2Runtime::resetIop() @@ -665,49 +616,30 @@ bool PS2Runtime::syncCoreSubsystems() } m_gs.init(gsVram, static_cast(PS2_GS_VRAM_SIZE), &m_memory.gs()); - m_gifArbiter.setProcessPacketFn([this](const uint8_t *data, uint32_t size) - { m_gs.processGIFPacket(data, size); }); + m_gifArbiter.setProcessPacketFn([this](GifPathId path, const uint8_t *data, uint32_t size) + { m_gs.processGIFPacket(data, size, static_cast(path)); }); m_memory.setGifArbiter(&m_gifArbiter); - m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t top, uint32_t itop) - { - R5900Context *cpuContext = m_eeScheduler ? m_eeScheduler->currentContext() : nullptr; - if (!cpuContext) - { - cpuContext = &m_cpuContext; - } - m_vu1.state().dBitEnabled = - (cpuContext->vu0_fbrst & (1u << 10)) != 0u; - m_vu1.state().tBitEnabled = - (cpuContext->vu0_fbrst & (1u << 11)) != 0u; - m_vu1.execute(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE, - m_memory.getVU1Data(), PS2_VU1_DATA_SIZE, - m_gs, &m_memory, startPC, top, itop, 65536); - cpuContext->vu0_vpu_stat = - (cpuContext->vu0_vpu_stat & ~0x0600u) | - (m_vu1.state().stoppedByD ? 0x0200u : 0u) | - (m_vu1.state().stoppedByT ? 0x0400u : 0u); }); + m_vu = std::make_unique(m_memory, [this](uint32_t cycles) + { m_eeScheduler->accountCycles(cycles); }); + m_memory.setVu1MscalCallback([this](uint32_t pc, uint32_t top, uint32_t itop) + { m_vu->start1(pc, top, itop, false); }); m_memory.setVu1MscntCallback([this](uint32_t top, uint32_t itop) + { m_vu->start1(0, top, itop, true); }); + m_memory.setVu1FlushCallback([this](bool gif) + { m_vu->flush1(gif); }); + m_memory.setVu0MscalCallback([this](uint32_t pc, uint32_t, uint32_t itop) + { + auto *context = m_eeScheduler->currentContext(); + m_vu->startVif0(context ? *context : m_cpuContext, pc, itop, false); }); + m_memory.setVu0MscntCallback([this](uint32_t, uint32_t itop) { - R5900Context *cpuContext = m_eeScheduler ? m_eeScheduler->currentContext() : nullptr; - if (!cpuContext) - { - cpuContext = &m_cpuContext; - } - m_vu1.state().dBitEnabled = - (cpuContext->vu0_fbrst & (1u << 10)) != 0u; - m_vu1.state().tBitEnabled = - (cpuContext->vu0_fbrst & (1u << 11)) != 0u; - m_vu1.resume(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE, - m_memory.getVU1Data(), PS2_VU1_DATA_SIZE, - m_gs, &m_memory, top, itop, 65536); - cpuContext->vu0_vpu_stat = - (cpuContext->vu0_vpu_stat & ~0x0600u) | - (m_vu1.state().stoppedByD ? 0x0200u : 0u) | - (m_vu1.state().stoppedByT ? 0x0400u : 0u); }); + auto *context = m_eeScheduler->currentContext(); + m_vu->startVif0(context ? *context : m_cpuContext, 0, itop, true); }); + m_memory.setVu0FlushCallback([this](bool) + { + auto *context = m_eeScheduler->currentContext(); + m_vu->flush0(context ? *context : m_cpuContext); }); resetIop(); - m_vu0.reset(); - m_vu1.reset(); - m_boundRdram = rdram; m_boundGSVram = gsVram; return true; @@ -735,6 +667,9 @@ bool PS2Runtime::initialize(const char *title) InitWindow(HOST_WINDOW_WIDTH, HOST_WINDOW_HEIGHT, title); InitAudioDevice(); m_audioBackend.setAudioReady(IsAudioDeviceReady()); +#endif +#ifdef PS2X_GS_PARALLEL + m_gs.setRasterBackend(CreateParallelGSBackend(m_memory.gs())); #endif SetTargetFPS(60); if (m_debugUiInitCallback) @@ -1398,7 +1333,7 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram, const uint32_t entryPc = ctx->pc; targetFn(rdram, ctx, this); - if (isStopRequested() || ctx->pc == 0u) + if (eeTransferPending() || isStopRequested() || ctx->pc == 0u) { return false; } @@ -1423,29 +1358,40 @@ void PS2Runtime::SignalException(R5900Context *ctx, PS2Exception exception) exception == EXCEPTION_TLB_REFILL); } -void PS2Runtime::executeVU0Microprogram(uint8_t *rdram, R5900Context *ctx, uint32_t address) +ps2vu::Unit &PS2Runtime::vu0() { - (void)rdram; + return m_vu->unit(false); +} - uint8_t *const vu0Code = m_memory.getVU0Code(); - uint8_t *const vu0Data = m_memory.getVU0Data(); - const uint32_t startPC = address & ~0x7u; +const ps2vu::Unit &PS2Runtime::vu0() const +{ + return m_vu->unit(false); +} - if (!vu0Code || !vu0Data || startPC + 8u > PS2_VU0_CODE_SIZE) - { - seedVu0IdleSuccess(ctx); - return; - } +ps2vu::Unit &PS2Runtime::vu1() +{ + return m_vu->unit(true); +} + +const ps2vu::Unit &PS2Runtime::vu1() const +{ + return m_vu->unit(true); +} + +void PS2Runtime::beforeVu0Access(R5900Context *ctx, uint32_t instruction) +{ + m_vu->beforeAccess(*ctx, instruction); +} - m_vu0.reset(); - copyVu0ContextToState(ctx, m_vu0.state()); - m_vu0.execute(vu0Code, PS2_VU0_CODE_SIZE, - vu0Data, PS2_VU0_DATA_SIZE, - m_gs, &m_memory, - startPC, 0u, ctx->vu0_itop, 4096); - copyVu0StateToContext(m_vu0.state(), ctx); +void PS2Runtime::afterVu0Access(R5900Context *ctx, uint32_t instruction) +{ + m_vu->afterAccess(*ctx, instruction); } +void PS2Runtime::executeVU0Microprogram(uint8_t *, R5900Context *ctx, uint32_t address) +{ + m_vu->start0(*ctx, address); +} void PS2Runtime::vu0StartMicroProgram(uint8_t *rdram, R5900Context *ctx, uint32_t address) { // VCALLMS and VCALLMSR both route here. @@ -2214,7 +2160,12 @@ bool PS2Runtime::eeCheckpointDue(uint32_t cycles) noexcept return m_eeScheduler->checkpointDue(cycles); } -[[noreturn]] void PS2Runtime::eeWaitVSyncTicks(uint32_t ticks, uint32_t resumePc) +bool PS2Runtime::eeTransferPending() const noexcept +{ + return m_eeScheduler->transferPending(); +} + +void PS2Runtime::eeWaitVSyncTicks(uint32_t ticks, uint32_t resumePc) { const uint64_t currentTick = m_eeScheduler->currentVSyncTick(); const uint64_t waitTicks = std::max(1u, ticks); @@ -2371,6 +2322,10 @@ void PS2Runtime::run() { m_eeScheduler->reset(m_memory.getRDRAM(), m_cpuContext); m_eeScheduler->run(); + if (m_vu) m_vu->reportFailure(); + const auto &iopFault = m_iopSubsystem->fault(); + if (iopFault.active) + std::cerr << "IOP execution failed at PC=0x" << std::hex << iopFault.pc << std::dec << ": " << iopFault.reason.data() << '\n'; uint32_t pc = m_debugPc.load(std::memory_order_relaxed); RUNTIME_LOG("Game thread returned. PC=0x" << std::hex << pc << " RA=0x" << static_cast(_mm_extract_epi32(m_cpuContext.r[31], 0)) << std::dec << std::endl); @@ -2386,79 +2341,105 @@ void PS2Runtime::run() gameThreadFinished.store(true, std::memory_order_release); }); uint64_t tick = 0; - while (!isStopRequested() && !gameThreadFinished.load(std::memory_order_acquire)) + std::exception_ptr presentationError; + try { - PS2_IF_AGRESSIVE_LOGS({ - tick++; - if ((tick % 120) == 0) - { - uint64_t curDma = m_memory.dmaStartCount(); - uint64_t curGif = m_memory.gifCopyCount(); - uint64_t curGs = m_memory.gsWriteCount(); - uint64_t curVif = m_memory.vifWriteCount(); - const GSRegisters &gs = m_memory.gs(); - const uint32_t dbgPc = m_debugPc.load(std::memory_order_relaxed); - const uint32_t dbgRa = m_debugRa.load(std::memory_order_relaxed); - const uint32_t dbgSp = m_debugSp.load(std::memory_order_relaxed); - const uint32_t dbgGp = m_debugGp.load(std::memory_order_relaxed); - const auto eeSnapshot = m_eeScheduler->snapshot(); - - RUNTIME_LOG("[run:tick] tick=" << tick - << " pc=0x" << std::hex << dbgPc - << " ra=0x" << dbgRa - << " sp=0x" << dbgSp - << " gp=0x" << dbgGp - << " dispfb1=0x" << gs.dispfb1 - << " display1=0x" << gs.display1 - << std::dec - << " activeThreads=" << eeSnapshot.threads.size() - << " dma=" << curDma - << " gif=" << curGif - << " gsw=" << curGs - << " vif=" << curVif - << std::endl); + while (!isStopRequested() && !gameThreadFinished.load(std::memory_order_acquire)) + { + PS2_IF_AGRESSIVE_LOGS({ + tick++; + if ((tick % 120) == 0) + { + uint64_t curDma = m_memory.dmaStartCount(); + uint64_t curGif = m_memory.gifCopyCount(); + uint64_t curGs = m_memory.gsWriteCount(); + uint64_t curVif = m_memory.vifWriteCount(); + const GSRegisters &gs = m_memory.gs(); + const uint32_t dbgPc = m_debugPc.load(std::memory_order_relaxed); + const uint32_t dbgRa = m_debugRa.load(std::memory_order_relaxed); + const uint32_t dbgSp = m_debugSp.load(std::memory_order_relaxed); + const uint32_t dbgGp = m_debugGp.load(std::memory_order_relaxed); + const auto eeSnapshot = m_eeScheduler->snapshot(); + + RUNTIME_LOG("[run:tick] tick=" << tick + << " pc=0x" << std::hex << dbgPc + << " ra=0x" << dbgRa + << " sp=0x" << dbgSp + << " gp=0x" << dbgGp + << " dispfb1=0x" << gs.dispfb1 + << " display1=0x" << gs.display1 + << std::dec + << " activeThreads=" << eeSnapshot.threads.size() + << " dma=" << curDma + << " gif=" << curGif + << " gsw=" << curGs + << " vif=" << curVif + << std::endl); + } + }); + uint32_t presentWidth = FB_WIDTH; + uint32_t presentHeight = DEFAULT_DISPLAY_HEIGHT; + UploadFrame(frameTex, this, presentWidth, presentHeight); + + Texture2D presentationTexture = frameTex; + float aspectRatio = 0.0f; +#ifdef PS2X_GS_PARALLEL + auto gpuFrame = m_gs.getLatchedGpuFrame(presentWidth, presentHeight, aspectRatio); + presentationTexture = {}; + if (gpuFrame) + presentationTexture = Texture2D{gpuFrame->AcquireTexture(), int(presentWidth), int(presentHeight), 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8}; +#endif + BeginDrawing(); + ClearBackground(BLACK); + const float srcWidth = static_cast(std::max(1u, presentWidth)); + const float srcHeight = static_cast(std::max(1u, presentHeight)); + const float screenWidth = static_cast(GetScreenWidth()); + const float screenHeight = static_cast(GetScreenHeight()); + const float displayWidth = aspectRatio > 0 ? srcHeight * aspectRatio : srcWidth; + const float scale = std::min(screenWidth / displayWidth, screenHeight / srcHeight); + const float dstWidth = displayWidth * scale; + const float dstHeight = srcHeight * scale; + const Rectangle srcRect{0.0f, 0.0f, srcWidth, srcHeight}; + const Rectangle dstRect{ + (screenWidth - dstWidth) * 0.5f, + (screenHeight - dstHeight) * 0.5f, + dstWidth, + dstHeight}; + if (presentationTexture.id) + DrawTexturePro(presentationTexture, srcRect, dstRect, Vector2{0.0f, 0.0f}, 0.0f, WHITE); +#ifdef PS2X_GS_PARALLEL + if (gpuFrame) + { + rlDrawRenderBatchActive(); + gpuFrame->ReleaseTexture(); } - }); - uint32_t presentWidth = FB_WIDTH; - uint32_t presentHeight = DEFAULT_DISPLAY_HEIGHT; - UploadFrame(frameTex, this, presentWidth, presentHeight); - - BeginDrawing(); - ClearBackground(BLACK); - const float srcWidth = static_cast(std::max(1u, presentWidth)); - const float srcHeight = static_cast(std::max(1u, presentHeight)); - const float screenWidth = static_cast(GetScreenWidth()); - const float screenHeight = static_cast(GetScreenHeight()); - const float scale = std::min(screenWidth / srcWidth, screenHeight / srcHeight); - const float dstWidth = srcWidth * scale; - const float dstHeight = srcHeight * scale; - const Rectangle srcRect{0.0f, 0.0f, srcWidth, srcHeight}; - const Rectangle dstRect{ - (screenWidth - dstWidth) * 0.5f, - (screenHeight - dstHeight) * 0.5f, - dstWidth, - dstHeight}; - DrawTexturePro(frameTex, srcRect, dstRect, Vector2{0.0f, 0.0f}, 0.0f, WHITE); - if (m_debugUiInitialized && m_debugUiDrawCallback) - { - m_debugUiDrawCallback(*this, m_debugUiUserData); - } - EndDrawing(); +#endif + if (m_debugUiInitialized && m_debugUiDrawCallback) + { + m_debugUiDrawCallback(*this, m_debugUiUserData); + } + EndDrawing(); - if (WindowShouldClose()) - { - RUNTIME_LOG("[run] window close requested, breaking out of loop"); - requestStop(); - break; + if (WindowShouldClose()) + { + RUNTIME_LOG("[run] window close requested, breaking out of loop"); + requestStop(); + break; + } } } + catch (...) + { + presentationError = std::current_exception(); + } requestStop(); if (gameThread.joinable()) { gameThread.join(); } + flushGuestPresentationTimings(); if (m_debugUiInitialized && m_debugUiShutdownCallback) { @@ -2466,7 +2447,11 @@ void PS2Runtime::run() m_debugUiInitialized = false; } UnloadTexture(frameTex); + m_gs.shutdownBackend(); CloseWindow(); + if (presentationError) + std::rethrow_exception(presentationError); + RUNTIME_LOG("[run] exiting loop"); } diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index b02c7ca69..90b0b1f07 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -142,6 +142,19 @@ void PS2Memory::processVIF0Data(const uint8_t *data, uint32_t sizeBytes) } else if (opcode == VIF_FLUSHE || opcode == VIF_FLUSH || opcode == VIF_FLUSHA) { + if (m_vuFlushCallback[0]) m_vuFlushCallback[0](opcode != VIF_FLUSHE); + continue; + } + else if (opcode == VIF_MSCAL || opcode == VIF_MSCALF) + { + vif0_regs.itop = vif0_regs.itops & 0x3ff; + if (m_vuMscalCallback[0]) m_vuMscalCallback[0]((imm & 0x1ffu) * 8, 0, vif0_regs.itop); + continue; + } + else if (opcode == VIF_MSCNT) + { + vif0_regs.itop = vif0_regs.itops & 0x3ff; + if (m_vuMscntCallback[0]) m_vuMscntCallback[0](0, vif0_regs.itop); continue; } else if (opcode == VIF_STMASK) @@ -170,6 +183,7 @@ void PS2Memory::processVIF0Data(const uint8_t *data, uint32_t sizeBytes) } else if (opcode == VIF_MPG) { + if (m_vuFlushCallback[0]) m_vuFlushCallback[0](false); const uint32_t destAddr = static_cast(imm & 0x1FFu) * 8u; const uint32_t instructionCount = (num == 0u) ? 256u : static_cast(num); const uint32_t mpgBytes = instructionCount * 8u; @@ -181,8 +195,7 @@ void PS2Memory::processVIF0Data(const uint8_t *data, uint32_t sizeBytes) copyBytes = PS2_VU0_CODE_SIZE - destAddr; if (pos + copyBytes <= sizeBytes) { - std::memcpy(m_vu0Code + destAddr, data + pos, copyBytes); - markVU0CodeModified(); + writeVuCode(false, destAddr, {data + pos, copyBytes}); } } @@ -398,11 +411,13 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) } else if (opcode == VIF_FLUSHE || opcode == VIF_FLUSH || opcode == VIF_FLUSHA) { + if (m_vuFlushCallback[1]) m_vuFlushCallback[1](opcode != VIF_FLUSHE); continue; } else if (opcode == VIF_MSCAL || opcode == VIF_MSCALF) { - uint32_t startPC = (uint32_t)imm * 8u; + if (opcode == VIF_MSCALF && m_vuFlushCallback[1]) m_vuFlushCallback[1](true); + uint32_t startPC = (imm & 0x7ffu) * 8u; const uint32_t runTop = vif1_regs.tops & 0x3FFu; const uint32_t runItop = vif1_regs.itops & 0x3FFu; @@ -416,8 +431,8 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) vif1_regs.tops = (vif1_regs.base + vif1_regs.ofst) & 0x3FFu; vif1_regs.stat ^= (1u << 7); // toggle DBF - if (m_vu1MscalCallback) - m_vu1MscalCallback(startPC, runTop, runItop); + if (m_vuMscalCallback[1]) + m_vuMscalCallback[1](startPC, runTop, runItop); continue; } else if (opcode == VIF_MSCNT) @@ -434,8 +449,8 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) vif1_regs.tops = (vif1_regs.base + vif1_regs.ofst) & 0x3FFu; vif1_regs.stat ^= (1u << 7); // toggle DBF - if (m_vu1MscntCallback) - m_vu1MscntCallback(runTop, runItop); + if (m_vuMscntCallback[1]) + m_vuMscntCallback[1](runTop, runItop); continue; } else if (opcode == VIF_STMASK) @@ -466,6 +481,7 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) } else if (opcode == VIF_MPG) { + if (m_vuFlushCallback[1]) m_vuFlushCallback[1](false); uint32_t destAddr = (uint32_t)imm * 8u; const uint32_t instructionCount = (num == 0u) ? 256u : static_cast(num); const uint32_t mpgBytes = instructionCount * 8u; @@ -476,8 +492,7 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) copyBytes = PS2_VU1_CODE_SIZE - destAddr; if (pos + copyBytes <= sizeBytes) { - std::memcpy(m_vu1Code + destAddr, data + pos, copyBytes); - markVU1CodeModified(); + writeVuCode(true, destAddr, {data + pos, copyBytes}); } } pos += mpgBytes; diff --git a/ps2xRuntime/src/lib/vu/interpreter/vu_executor.cpp b/ps2xRuntime/src/lib/vu/interpreter/vu_executor.cpp new file mode 100644 index 000000000..2d6df4bff --- /dev/null +++ b/ps2xRuntime/src/lib/vu/interpreter/vu_executor.cpp @@ -0,0 +1,582 @@ +#include "runtime/vu/interpreter/vu_executor.h" +#include "runtime/vu/vu_unit.h" +#include "runtime/gs/ps2_gif_arbiter.h" +#include "runtime/gs/gs_frontend.h" +#include "runtime/ps2_memory.h" + +#include "runtime/vu/vu_decoder.h" +#include "runtime/vu/interpreter/vu_fmac.inl" +#include "runtime/vu/interpreter/vu_hazards.inl" +#include "runtime/vu/interpreter/vu_upper.inl" +#include "runtime/vu/interpreter/vu_lower.inl" +#include "runtime/vu/interpreter/vu_pair.inl" + +using namespace ps2vu; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +VUExecutor::VUExecutor(Unit unit) : m_unit(unit) +{ + reset(); +} + +VUExecutor::~VUExecutor() = default; + +void VUExecutor::resetScheduler() +{ + if (!m_schedulerClean) + { + m_flagActive = m_efuActive = m_storeActive = 0; + m_vfWriteActive = m_viWriteActive = m_accWriteActive = 0; + m_nextPipelineCycle = kNoPipelineEvent; + m_pipelineEvents.reset(); + m_flagPipeline = {}; + m_fdiv = {}; + m_efu = {}; + m_storePipeline = {}; + m_vfWritePipeline = {}; + m_viWritePipeline = {}; + m_accWritePipeline = {}; + m_registerReady = {}; + m_vfCommittedWrite = {}; + m_viCommittedWrite = {}; + m_accLatestWrite = {}; + m_nextWriteSequence = 0; + m_efuResourceReady = 0; + } + m_schedulerClean = true; + m_xgkick.reset(); + m_workingClip = m_state.clip; + m_viBranchBackupValue = 0; + m_viBranchBackupReg = 0; + m_viBranchBackupValid = false; + m_stopRequested = false; + m_pendingHaltD = false; + m_pendingHaltT = false; +} + +void VUExecutor::reset() +{ + std::memset(&m_state, 0, sizeof(m_state)); + m_state.vf[0][3] = 1.0f; + m_state.q = 1.0f; + m_state.r = 0x3F800000u; + m_cycle = 0; + resetScheduler(); +} + +uint32_t VUExecutor::microAddressMask() const +{ + return m_unit == Unit::VU1 ? 0x3FFFu : 0x0FFFu; +} + +int32_t VUExecutor::readBranchVi(uint8_t reg) const +{ + if (reg == 0u) + return 0; + if (m_viBranchBackupValid && m_viBranchBackupReg == reg) + { + return m_viBranchBackupValue; + } + return m_state.vi[reg]; +} + +void VUExecutor::recordViWriteForBranch(uint8_t reg, int32_t oldValue) +{ + if (reg == 0u) + return; + m_viBranchBackupValue = oldValue; + m_viBranchBackupReg = reg; + m_viBranchBackupValid = true; +} + +void VUExecutor::queueFsset(uint16_t immediate) +{ + for (FlagPipelineEntry &entry : m_flagPipeline) + { + if (entry.valid && entry.issueCycle == m_cycle) + entry.writesStatus = false; + } + + if (auto *entry = allocatePipelineEntry(m_flagPipeline, m_flagActive, m_cycle + kFmacLatency)) + { + entry->issueCycle = m_cycle; + entry->status = static_cast(immediate) & 0xFC0u; + entry->writesSticky = true; + return; + } + reportReservedInstruction(false, 0xFFFFFFFEu); +} + +void VUExecutor::queueClip(uint32_t clip) +{ + m_workingClip = ((m_workingClip << 6) | (clip & 0x3Fu)) & 0xFFFFFFu; + if (auto *entry = allocatePipelineEntry(m_flagPipeline, m_flagActive, m_cycle + kFmacLatency)) + { + entry->issueCycle = m_cycle; + entry->clip = m_workingClip; + entry->writesClip = true; + return; + } + reportReservedInstruction(true, 0xFFFFFFFDu); +} + +void VUExecutor::queueFcset(uint32_t clip) +{ + m_workingClip = clip & 0xFFFFFFu; + for (FlagPipelineEntry &entry : m_flagPipeline) + { + if (entry.valid && entry.issueCycle == m_cycle) + entry.writesClip = false; + } + if (auto *entry = allocatePipelineEntry(m_flagPipeline, m_flagActive, m_cycle + kFmacLatency)) + { + entry->issueCycle = m_cycle; + entry->clip = m_workingClip; + entry->writesClip = true; + return; + } + reportReservedInstruction(false, 0xFFFFFFFAu); +} + +void VUExecutor::queueQ(float value, uint32_t latency, uint32_t statusDi) +{ + uint32_t ignoredFlags = 0u; + value = normalizeResult(value, ignoredFlags); + m_pipelineEvents.schedule(m_cycle, latency, 0); + m_fdiv.valid = true; + m_fdiv.readyCycle = m_cycle + latency; + m_nextPipelineCycle = std::min(m_nextPipelineCycle, m_fdiv.readyCycle); + m_fdiv.value = value; + m_fdiv.statusDi = statusDi & 0x30u; +} + +void VUExecutor::queueP(float value, uint32_t latency) +{ + uint32_t ignoredFlags = 0u; + value = normalizeResult(value, ignoredFlags); + if (auto *entry = allocatePipelineEntry(m_efu, m_efuActive, m_cycle + latency)) + { + entry->value = value; + m_efuResourceReady = m_cycle + (latency > 0u ? latency - 1u : 0u); + return; + } + reportReservedInstruction(false, 0xFFFFFFF9u); +} + +void VUExecutor::queueStore(uint32_t address, const uint32_t words[4], uint8_t laneMask) +{ + if (m_unit == Unit::VU0 && (address & 0x4000u)) + { + writeData(address, words, laneMask); + return; + } + if (auto *store = allocatePipelineEntry(m_storePipeline, m_storeActive, m_cycle + 1u)) + { + store->address = address; + store->laneMask = laneMask; + std::copy(words, words + 4, store->words.begin()); + return; + } + reportReservedInstruction(false, 0xFFFFFFFCu); +} + +void VUExecutor::queueVfWrite(uint8_t reg, uint8_t laneMask, const float value[4], uint32_t latency) +{ + if (reg == 0u || laneMask == 0u) + return; + if (auto *write = allocatePipelineEntry(m_vfWritePipeline, m_vfWriteActive, m_cycle + latency)) + { + write->sequence = ++m_nextWriteSequence; + write->reg = reg; + write->laneMask = laneMask; + std::copy(value, value + 4, write->value.begin()); + return; + } + reportReservedInstruction(false, 0xFFFFFFF7u); +} + +void VUExecutor::queueViWrite(uint8_t reg, int32_t value, uint32_t latency) +{ + if (reg == 0u) + return; + if (auto *write = allocatePipelineEntry(m_viWritePipeline, m_viWriteActive, m_cycle + latency)) + { + write->sequence = ++m_nextWriteSequence; + write->reg = reg; + write->value = value; + return; + } + reportReservedInstruction(false, 0xFFFFFFF6u); +} + +void VUExecutor::queueAccWrite(uint8_t laneMask, const float value[4], uint32_t latency) +{ + if (laneMask == 0u) + return; + if (auto *write = allocatePipelineEntry(m_accWritePipeline, m_accWriteActive, m_cycle + latency)) + { + write->sequence = ++m_nextWriteSequence; + write->laneMask = laneMask; + std::copy(value, value + 4, write->value.begin()); + for (uint32_t component = 0; component < 4u; ++component) + { + if ((laneMask & laneForComponent(component)) != 0u) + m_accLatestWrite[component] = write->sequence; + } + return; + } + reportReservedInstruction(true, 0xFFFFFFF5u); +} + +void VUExecutor::commitReadyPipelines() +{ + if (m_cycle < m_nextPipelineCycle) + return; + const uint64_t events = m_pipelineEvents.take(m_cycle); + m_nextPipelineCycle = m_pipelineEvents.nextCycle(m_cycle); + for (uint32_t pending = PipelineEvents::slots(events); pending != 0u; pending &= pending - 1u) + { + const uint32_t slot = std::countr_zero(pending); + FlagPipelineEntry &entry = m_flagPipeline[slot]; + m_flagActive &= ~(1u << slot); + + if (entry.writesMac) + m_state.mac = entry.mac; + if (entry.writesStatus) + { + const uint32_t current = entry.status & 0xFu; + m_state.status = (m_state.status & 0xFF0u) | current | ((current | entry.extraSticky) << 6); + } + if (entry.writesSticky) + { + m_state.status = (m_state.status & 0x03Fu) | (entry.status & 0xFC0u); + } + if (entry.writesClip) + m_state.clip = entry.clip; + entry.valid = false; + } + + if (PipelineEvents::slots(events) != 0) + { + m_state.q = m_fdiv.value; + const uint32_t currentDi = m_fdiv.statusDi & 0x30u; + m_state.status = (m_state.status & 0xFCFu) | currentDi | (currentDi << 6); + m_fdiv = {}; + } + + for (uint32_t pending = PipelineEvents::slots(events); pending != 0u; pending &= pending - 1u) + { + const uint32_t slot = std::countr_zero(pending); + ScalarPipelineEntry &entry = m_efu[slot]; + m_efuActive &= ~(1u << slot); + m_state.p = entry.value; + entry.valid = false; + } + + for (uint32_t pending = PipelineEvents::slots(events); pending != 0u; pending &= pending - 1u) + { + const uint32_t slot = std::countr_zero(pending); + PendingStore &store = m_storePipeline[slot]; + m_storeActive &= ~(1u << slot); + writeData(store.address, store.words.data(), store.laneMask); + } + + for (uint32_t pending = PipelineEvents::slots(events); pending != 0u; pending &= pending - 1u) + { + const uint32_t slot = std::countr_zero(pending); + PendingVfWrite &write = m_vfWritePipeline[slot]; + m_vfWriteActive &= ~(1u << slot); + for (uint32_t component = 0; component < 4u; ++component) + { + if ((write.laneMask & laneForComponent(component)) != 0u && + m_vfCommittedWrite[write.reg][component] <= write.sequence) + { + m_state.vf[write.reg][component] = write.value[component]; + m_vfCommittedWrite[write.reg][component] = write.sequence; + } + } + } + + for (uint32_t pending = PipelineEvents::slots(events); pending != 0u; pending &= pending - 1u) + { + const uint32_t slot = std::countr_zero(pending); + PendingViWrite &write = m_viWritePipeline[slot]; + m_viWriteActive &= ~(1u << slot); + if (m_viCommittedWrite[write.reg] <= write.sequence) + { + m_state.vi[write.reg] = static_cast(write.value); + m_viCommittedWrite[write.reg] = write.sequence; + } + } + + for (uint32_t pending = PipelineEvents::slots(events); pending != 0u; pending &= pending - 1u) + { + const uint32_t slot = std::countr_zero(pending); + PendingAccWrite &write = m_accWritePipeline[slot]; + m_accWriteActive &= ~(1u << slot); + for (uint32_t component = 0; component < 4u; ++component) + { + if ((write.laneMask & laneForComponent(component)) != 0u && m_accLatestWrite[component] == write.sequence) + { + m_state.acc[component] = write.value[component]; + } + } + } +} + +void VUExecutor::progressXgkick(uint32_t elapsedCycles) +{ + if (!m_xgkick.active || !m_activeVuData || m_activeVuDataSize == 0u) + return; + + m_xgkick.cycleCredit += elapsedCycles; + while (m_xgkick.active && m_xgkick.cycleCredit >= 2u) + { + m_xgkick.cycleCredit -= 2u; + if (m_xgkick.copiedBytes > XgkickPipeline::kBufferSize - 16u) + { + reportReservedInstruction(false, 0xFFFFFFFBu); + m_xgkick.active = false; + return; + } + + const uint32_t qwordOffset = m_xgkick.copiedBytes; + for (uint32_t i = 0; i < 16u; ++i) + { + const uint32_t source = (m_xgkick.sourceAddress + m_xgkick.copiedBytes + i) % m_activeVuDataSize; + m_xgkick.packet[m_xgkick.copiedBytes + i] = m_activeVuData[source]; + } + m_xgkick.copiedBytes += 16u; + + if (m_xgkick.currentTagEnd == 0u) + { + uint64_t tagLo = 0; + std::memcpy(&tagLo, m_xgkick.packet.data() + qwordOffset, sizeof(tagLo)); + const uint32_t nloop = static_cast(tagLo & 0x7FFFu); + const uint32_t format = static_cast((tagLo >> 58) & 0x3u); + uint32_t nreg = static_cast((tagLo >> 60) & 0xFu); + if (nreg == 0u) + nreg = 16u; + + uint64_t tagBytes = 16u; + if (format == 0u) + tagBytes += static_cast(nloop) * nreg * 16u; + else if (format == 1u) + tagBytes += ((static_cast(nloop) * nreg + 1u) & ~1ull) * 8u; + else if (format == 2u) + tagBytes += static_cast(nloop) * 16u; + else + { + reportReservedInstruction(false, 0xFFFFFFF8u); + m_xgkick.active = false; + return; + } + + if (tagBytes > XgkickPipeline::kBufferSize - qwordOffset) + { + reportReservedInstruction(false, 0xFFFFFFFBu); + m_xgkick.active = false; + return; + } + m_xgkick.currentTagEnd = qwordOffset + static_cast(tagBytes); + m_xgkick.currentTagEop = ((tagLo >> 15) & 1u) != 0u; + if (m_xgkick.currentTagEop) + m_xgkick.totalBytes = m_xgkick.currentTagEnd; + } + + if (m_xgkick.copiedBytes >= m_xgkick.currentTagEnd) + { + if (m_xgkick.currentTagEop) + finishXgkick(); + else + { + + m_xgkick.currentTagEnd = 0u; + m_xgkick.currentTagEop = false; + } + } + } +} + +void VUExecutor::finishXgkick() +{ + if (!m_xgkick.active) + return; + + if (m_services) + { + if (!m_services->submitGif || !m_services->submitGif(m_services->context, m_xgkick.packet.data(), m_xgkick.totalBytes)) + throw std::runtime_error("VU GIF submission failed"); + } + else if (m_activeMemory) + m_activeMemory->submitGifPacket(GifPathId::Path1, m_xgkick.packet.data(), m_xgkick.totalBytes); + else if (m_activeGs) + m_activeGs->processGIFPacket(m_xgkick.packet.data(), m_xgkick.totalBytes, 1); + m_xgkick.active = false; +} + +void VUExecutor::startXgkick(uint32_t qwordAddress) +{ + if (m_unit != Unit::VU1 || !m_activeVuData || m_activeVuDataSize < 16u) + return; + + const uint32_t sourceAddress = (qwordAddress * 16u) % m_activeVuDataSize; + m_xgkick.reset(); + m_xgkick.active = true; + m_xgkick.sourceAddress = sourceAddress; + m_xgkick.cycleCredit = 1u; + m_xgkick.issueCycle = m_cycle; +} + +void VUExecutor::advanceOneCycle() +{ + ++m_cycle; + m_state.cycles = m_cycle; + + + commitReadyPipelines(); + progressXgkick(); +} + +void VUExecutor::advanceTo(uint64_t targetCycle) +{ + while (m_cycle < targetCycle) + { + + + uint64_t next = std::min(targetCycle, m_nextPipelineCycle); + if (m_xgkick.active) + next = std::min(next, m_cycle + (2u - m_xgkick.cycleCredit)); + next = std::max(next, m_cycle + 1u); + const uint64_t elapsed = next - m_cycle; + m_cycle = next; + m_state.cycles = m_cycle; + commitReadyPipelines(); + if (m_xgkick.active) + progressXgkick(static_cast(elapsed)); + } +} + +bool VUExecutor::pipelinesPending() const +{ + return m_nextPipelineCycle != kNoPipelineEvent || m_xgkick.active; +} + +void VUExecutor::flushPipelines() +{ + while (pipelinesPending()) + { + uint64_t next = m_nextPipelineCycle; + if (m_xgkick.active) + next = std::min(next, m_cycle + (2u - m_xgkick.cycleCredit)); + advanceTo(std::max(next, m_cycle + 1u)); + } +} + +void VUExecutor::reportReservedInstruction(bool upper, uint32_t instruction) +{ + if (m_unit == Unit::VU1) + { + std::ostringstream message; + message << "Reserved VU1 " << (upper ? "upper" : "lower") << " instruction at PC=0x" << std::hex << m_state.pc + << " word=0x" << instruction; + throw std::runtime_error(message.str()); + } + + RUNTIME_ERROR("[VU" << (m_unit == Unit::VU1 ? "1" : "0") << " reserved " << (upper ? "upper" : "lower") + << "] cycle=" << m_cycle << " pc=0x" << std::hex << m_state.pc << " instruction=0x" + << instruction << std::dec << '\n'); + m_stopRequested = true; +} + +void VUExecutor::execute(const uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, + PS2Memory *memory, uint32_t startPC, uint32_t top, uint32_t itop, uint32_t maxCycles) +{ + resetScheduler(); + m_state.pc = startPC & microAddressMask(); + m_state.ebit = false; + m_state.haltAfterDelaySlot = false; + m_state.stoppedByD = false; + m_state.stoppedByT = false; + m_state.top = top; + m_state.itop = itop; + m_state.branchPending = false; + m_state.branchTarget = 0; + m_state.branchDelay = 0; + m_state.vf[0][0] = 0.0f; + m_state.vf[0][1] = 0.0f; + m_state.vf[0][2] = 0.0f; + m_state.vf[0][3] = 1.0f; + run(vuCode, codeSize, vuData, dataSize, &gs, memory, maxCycles); +} + +void VUExecutor::resume(const uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, + PS2Memory *memory, uint32_t top, uint32_t itop, uint32_t maxCycles) +{ + m_state.top = top; + m_state.itop = itop; + m_state.stoppedByD = false; + m_state.stoppedByT = false; + run(vuCode, codeSize, vuData, dataSize, &gs, memory, maxCycles); +} + +void VUExecutor::run(const uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS *gs, PS2Memory *memory, + uint32_t maxCycles, bool drain) +{ + m_schedulerClean = false; + m_activeVuData = vuData; + m_activeVuDataSize = dataSize; + m_activeGs = gs; + m_activeMemory = memory; + + struct RoundingScope + { + int previous = std::fegetround(); + RoundingScope() + { + if (previous == -1 || std::fesetround(FE_TOWARDZERO) != 0) + throw std::runtime_error("Cannot select VU floating-point rounding mode"); + } + ~RoundingScope() + { + if (previous != -1) + std::fesetround(previous); + } + } rounding; + + const uint64_t budgetEnd = m_cycle + maxCycles; + m_programEnded = false; + while (m_cycle < budgetEnd && !m_stopRequested) + { + if (m_state.pc + 8u > codeSize) + break; + uint32_t words[2]; + std::memcpy(words, vuCode + m_state.pc, sizeof(words)); + const auto decoded = decodeInstructionPair(words[0], words[1], m_unit == Unit::VU1); + + if (!issuePair( + decoded, [&](float *vf, float *acc) { execUpper(decoded.upper, vf, acc); }, + [&] { execLower(decoded.lower, vuData, dataSize, decoded.upper); }, codeSize, budgetEnd)) + break; + } + + if (m_programEnded && drain) + { + flushPipelines(); + m_state.ebit = false; + m_state.haltAfterDelaySlot = false; + m_pendingHaltD = false; + m_pendingHaltT = false; + } + m_state.cycles = m_cycle; +} diff --git a/ps2xRuntime/src/lib/vu/interpreter/vu_memory.cpp b/ps2xRuntime/src/lib/vu/interpreter/vu_memory.cpp new file mode 100644 index 000000000..4451f3257 --- /dev/null +++ b/ps2xRuntime/src/lib/vu/interpreter/vu_memory.cpp @@ -0,0 +1,29 @@ +#include "runtime/vu/vu_unit.h" +#include +#include + +void VUExecutor::readData(uint32_t address, void *result) +{ + if (m_unit == Unit::VU0 && (address & 0x4000u)) + { + uint32_t words[4]{}; + if (!m_services || !m_services->readShared || !m_services->readShared(m_services->context, m_cycle, address & 0x3ffu, words)) + throw std::runtime_error("VU shared register read failed"); + std::memcpy(result, words, sizeof(words)); + } + else + std::memcpy(result, m_activeVuData + (address & (m_activeVuDataSize - 1)), 16); +} + +void VUExecutor::writeData(uint32_t address, const uint32_t *words, uint8_t lanes) +{ + if (m_unit == Unit::VU0 && (address & 0x4000u)) + { + if (!m_services || !m_services->writeShared || !m_services->writeShared(m_services->context, m_cycle, address & 0x3ffu, words, lanes)) + throw std::runtime_error("VU shared register write failed"); + } + else if (m_activeVuData) + for (uint32_t lane = 0; lane < 4; ++lane) + if (lanes & (8u >> lane)) + std::memcpy(m_activeVuData + (address & (m_activeVuDataSize - 1)) + lane * 4, words + lane, 4); +} diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp deleted file mode 100644 index bd75bba31..000000000 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp +++ /dev/null @@ -1,1838 +0,0 @@ -#include "runtime/ps2_vu1.h" -#include "runtime/gs/ps2_gif_arbiter.h" -#include "runtime/gs/gs_frontend.h" -#include "runtime/ps2_memory.h" -#include "ps2_vu1_detail.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace -{ - constexpr uint8_t laneForComponent(uint32_t component) - { - return static_cast(1u << (3u - component)); - } -} - -void VU1Interpreter::addVfRead(InstructionUsage &usage, uint8_t reg, uint8_t lanes) -{ - if (lanes == 0u) - return; - for (uint32_t index = 0; index < usage.vfReadCount; ++index) - { - if (usage.vfRead[index].reg == reg) - { - usage.vfRead[index].lanes |= lanes; - return; - } - } - if (usage.vfReadCount < usage.vfRead.size()) - usage.vfRead[usage.vfReadCount++] = {reg, lanes}; -} - -void VU1Interpreter::addVfWrite(InstructionUsage &usage, uint8_t reg, uint8_t lanes) -{ - if (reg == 0u || lanes == 0u) - return; - if (usage.vfWrite.reg == 0u) - usage.vfWrite = {reg, lanes}; - else if (usage.vfWrite.reg == reg) - usage.vfWrite.lanes |= lanes; -} - -uint8_t VU1Interpreter::vfReadLanes(const InstructionUsage &usage, uint8_t reg) -{ - for (uint32_t index = 0; index < usage.vfReadCount; ++index) - { - if (usage.vfRead[index].reg == reg) - return usage.vfRead[index].lanes; - } - return 0u; -} - -VU1Interpreter::VU1Interpreter(Unit unit) - : m_unit(unit) -{ - reset(); -} - -void VU1Interpreter::resetScheduler() -{ - m_flagPipeline = {}; - m_fdiv = {}; - m_efu = {}; - m_storePipeline = {}; - m_vfWritePipeline = {}; - m_viWritePipeline = {}; - m_accWritePipeline = {}; - m_xgkick = {}; - m_vfReady = {}; - m_viReady = {}; - m_accReady = {}; - m_vfLatestWrite = {}; - m_viLatestWrite = {}; - m_accLatestWrite = {}; - m_nextWriteSequence = 0; - m_efuResourceReady = 0; - m_workingClip = m_state.clip; - m_viBranchBackupValue = 0; - m_viBranchBackupReg = 0; - m_viBranchBackupValid = false; - m_stopRequested = false; - m_pendingHaltD = false; - m_pendingHaltT = false; -} - -void VU1Interpreter::reset() -{ - std::memset(&m_state, 0, sizeof(m_state)); - m_state.vf[0][3] = 1.0f; - m_state.q = 1.0f; - m_state.r = 0x3F800000u; - m_cycle = 0; - resetScheduler(); -} - -float VU1Interpreter::broadcast(const float *vf, uint8_t bc) -{ - return normalizeOperand(vf[bc & 3u]); -} - -float VU1Interpreter::normalizeOperand(float value) const -{ - uint32_t bits = 0; - std::memcpy(&bits, &value, sizeof(bits)); - const uint32_t exponent = (bits >> 23) & 0xFFu; - if (exponent == 0u) - { - bits &= 0x80000000u; - } - else if (exponent == 0xFFu) - { - bits = (bits & 0x80000000u) | 0x7F7FFFFFu; - } - std::memcpy(&value, &bits, sizeof(value)); - return value; -} - -float VU1Interpreter::normalizeResult(float value, uint32_t &laneFlags) const -{ - uint32_t bits = 0; - std::memcpy(&bits, &value, sizeof(bits)); - const uint32_t sign = bits & 0x80000000u; - const uint32_t magnitude = bits & 0x7FFFFFFFu; - const uint32_t exponent = (bits >> 23) & 0xFFu; - - laneFlags = sign != 0u ? 0x2u : 0u; - if (magnitude == 0u) - { - laneFlags |= 0x1u; - } - else if (exponent == 0u) - { - laneFlags |= 0x5u; - bits = sign; - } - else if (exponent == 0xFFu) - { - laneFlags |= 0x8u; - bits = sign | 0x7F7FFFFFu; - } - - std::memcpy(&value, &bits, sizeof(value)); - return value; -} - -uint32_t VU1Interpreter::microAddressMask() const -{ - return m_unit == Unit::VU1 ? 0x3FFFu : 0x0FFFu; -} - -int32_t VU1Interpreter::readBranchVi(uint8_t reg) const -{ - if (reg == 0u) - return 0; - if (m_viBranchBackupValid && - m_viBranchBackupReg == reg) - { - return m_viBranchBackupValue; - } - return m_state.vi[reg]; -} - -void VU1Interpreter::recordViWriteForBranch(uint8_t reg, int32_t oldValue) -{ - if (reg == 0u) - return; - m_viBranchBackupValue = oldValue; - m_viBranchBackupReg = reg; - m_viBranchBackupValid = true; -} - -void VU1Interpreter::applyDest(float *dst, const float *result, uint8_t dest) -{ - if (dest & 0x8u) - dst[0] = result[0]; - if (dest & 0x4u) - dst[1] = result[1]; - if (dest & 0x2u) - dst[2] = result[2]; - if (dest & 0x1u) - dst[3] = result[3]; -} - -void VU1Interpreter::applyDestAcc(const float *result, uint8_t dest) -{ - applyDest(m_state.acc, result, dest); -} - -void VU1Interpreter::normalizeFmacResult(float *result, uint8_t dest, - uint8_t laneFlags[4]) -{ - for (uint32_t component = 0; component < 4u; ++component) - { - laneFlags[component] = 0u; - if ((dest & laneForComponent(component)) == 0u) - continue; - - long double exactResult = 0.0L; - if (calculateFmacExactResult(component, exactResult)) - { - laneFlags[component] = normalizeFmacExactResult(result[component], exactResult); - continue; - } - - uint32_t flags = 0u; - result[component] = normalizeResult(result[component], flags); - laneFlags[component] = static_cast(flags); - } -} - -bool VU1Interpreter::calculateFmacExactResult(uint32_t component, - long double &result) const -{ - const uint32_t upper = m_currentUpperInstruction; - const uint8_t op = static_cast(upper & 0x3Fu); - const uint8_t special = op >= 0x3Cu - ? static_cast((upper & 3u) | ((upper >> 4) & 0x7Cu)) - : 0xFFu; - const uint8_t fs = FS(upper); - const uint8_t ft = FT(upper); - - const auto operand = [this](float value) - { - return static_cast(normalizeOperand(value)); - }; - const auto vs = [&](uint32_t lane) - { - return operand(m_state.vf[fs][lane]); - }; - const auto vt = [&](uint32_t lane) - { - return operand(m_state.vf[ft][lane]); - }; - const auto acc = [&](uint32_t lane) - { - return operand(m_state.acc[lane]); - }; - - const long double q = operand(m_state.q); - const long double i = operand(m_state.i); - - if (op < 0x3Cu) - { - if (op <= 0x03u) - result = vs(component) + vt(op & 3u); - else if (op <= 0x07u) - result = vs(component) - vt(op & 3u); - else if (op <= 0x0Bu) - result = acc(component) + vs(component) * vt(op & 3u); - else if (op <= 0x0Fu) - result = acc(component) - vs(component) * vt(op & 3u); - else if (op >= 0x18u && op <= 0x1Bu) - result = vs(component) * vt(op & 3u); - else - { - switch (op) - { - case 0x1Cu: - result = vs(component) * q; - break; - case 0x1Eu: - result = vs(component) * i; - break; - case 0x20u: - result = vs(component) + q; - break; - case 0x21u: - result = acc(component) + vs(component) * q; - break; - case 0x22u: - result = vs(component) + i; - break; - case 0x23u: - result = acc(component) + vs(component) * i; - break; - case 0x24u: - result = vs(component) - q; - break; - case 0x25u: - result = acc(component) - vs(component) * q; - break; - case 0x26u: - result = vs(component) - i; - break; - case 0x27u: - result = acc(component) - vs(component) * i; - break; - case 0x28u: - result = vs(component) + vt(component); - break; - case 0x29u: - result = acc(component) + vs(component) * vt(component); - break; - case 0x2Au: - result = vs(component) * vt(component); - break; - case 0x2Cu: - result = vs(component) - vt(component); - break; - case 0x2Du: - result = acc(component) - vs(component) * vt(component); - break; - case 0x2Eu: - { - static constexpr uint8_t left[4] = {1u, 2u, 0u, 3u}; - static constexpr uint8_t right[4] = {2u, 0u, 1u, 3u}; - result = component == 3u - ? 0.0L - : acc(component) - vs(left[component]) * vt(right[component]); - break; - } - default: - return false; - } - } - return true; - } - - if (special <= 0x03u) - result = vs(component) + vt(special & 3u); - else if (special <= 0x07u) - result = vs(component) - vt(special & 3u); - else if (special <= 0x0Bu) - result = acc(component) + vs(component) * vt(special & 3u); - else if (special <= 0x0Fu) - result = acc(component) - vs(component) * vt(special & 3u); - else if (special >= 0x18u && special <= 0x1Bu) - result = vs(component) * vt(special & 3u); - else - { - switch (special) - { - case 0x1Cu: - result = vs(component) * q; - break; - case 0x1Eu: - result = vs(component) * i; - break; - case 0x20u: - result = vs(component) + q; - break; - case 0x21u: - result = acc(component) + vs(component) * q; - break; - case 0x22u: - result = vs(component) + i; - break; - case 0x23u: - result = acc(component) + vs(component) * i; - break; - case 0x24u: - result = vs(component) - q; - break; - case 0x25u: - result = acc(component) - vs(component) * q; - break; - case 0x26u: - result = vs(component) - i; - break; - case 0x27u: - result = acc(component) - vs(component) * i; - break; - case 0x28u: - result = vs(component) + vt(component); - break; - case 0x29u: - result = acc(component) + vs(component) * vt(component); - break; - case 0x2Au: - result = vs(component) * vt(component); - break; - case 0x2Cu: - result = vs(component) - vt(component); - break; - case 0x2Du: - result = acc(component) - vs(component) * vt(component); - break; - case 0x2Eu: - { - static constexpr uint8_t left[4] = {1u, 2u, 0u, 3u}; - static constexpr uint8_t right[4] = {2u, 0u, 1u, 3u}; - result = component == 3u - ? 0.0L - : vs(left[component]) * vt(right[component]); - break; - } - default: - return false; - } - } - return true; -} - -uint8_t VU1Interpreter::normalizeFmacExactResult(float &value, - long double exactResult) const -{ - const bool negative = std::signbit(exactResult); - const long double magnitude = std::fabs(exactResult); - const long double maximum = static_cast(std::numeric_limits::max()); - const long double minimum = static_cast(std::numeric_limits::min()); - uint8_t flags = negative ? 0x2u : 0u; - - uint32_t bits = negative ? 0x80000000u : 0u; - if (magnitude == 0.0L) - { - flags |= 0x1u; - std::memcpy(&value, &bits, sizeof(value)); - } - else if (magnitude > maximum) - { - flags |= 0x8u; - bits |= 0x7F7FFFFFu; - std::memcpy(&value, &bits, sizeof(value)); - } - else if (magnitude < minimum) - { - flags |= 0x5u; - std::memcpy(&value, &bits, sizeof(value)); - } - - return flags; -} - -uint32_t VU1Interpreter::calculateFmacProductSticky(uint8_t dest) const -{ - uint32_t extraSticky = 0u; - const uint32_t upper = m_currentUpperInstruction; - const uint8_t op = static_cast(upper & 0x3Fu); - const uint8_t special = op >= 0x3Cu ? static_cast((upper & 3u) | ((upper >> 4) & 0x7Cu)) : 0xFFu; - const bool productSum = - (op >= 0x08u && op <= 0x0Fu) || - op == 0x21u || op == 0x23u || op == 0x25u || op == 0x27u || - op == 0x29u || op == 0x2Du || op == 0x2Eu || - (special >= 0x08u && special <= 0x0Fu) || - special == 0x21u || special == 0x23u || special == 0x25u || - special == 0x27u || special == 0x29u || special == 0x2Du; - if (!productSum) - return 0u; - - const uint8_t fs = FS(upper); - const uint8_t ft = FT(upper); - for (uint32_t component = 0; component < 4u; ++component) - { - if ((dest & laneForComponent(component)) == 0u) - continue; - static constexpr uint8_t crossLeft[4] = {1u, 2u, 0u, 3u}; - static constexpr uint8_t crossRight[4] = {2u, 0u, 1u, 3u}; - const uint8_t leftComponent = op == 0x2Eu ? crossLeft[component] : static_cast(component); - const float left = normalizeOperand(m_state.vf[fs][leftComponent]); - float right = 0.0f; - if ((op >= 0x08u && op <= 0x0Fu) || (special >= 0x08u && special <= 0x0Fu)) - { - right = normalizeOperand(m_state.vf[ft][(op >= 0x08u && op <= 0x0Fu ? op : special) & 3u]); - } - else if (op == 0x21u || op == 0x25u || special == 0x21u || special == 0x25u) - { - right = normalizeOperand(m_state.q); - } - else if (op == 0x23u || op == 0x27u || special == 0x23u || special == 0x27u) - { - right = normalizeOperand(m_state.i); - } - else if (op == 0x2Eu) - { - right = normalizeOperand(m_state.vf[ft][crossRight[component]]); - } - else - { - right = normalizeOperand(m_state.vf[ft][component]); - } - - float product = left * right; - const long double exactProduct = static_cast(left) * static_cast(right); - const uint8_t productFlags = normalizeFmacExactResult(product, exactProduct); - // Product-sum instructions report Z/S/U/O from the add/subtract result - // as current flags, while every product condition accumulates into the - // corresponding sticky flag. - extraSticky |= productFlags & 0xFu; - } - return extraSticky; -} - -void VU1Interpreter::updateFmacFlags(const uint8_t laneFlags[4], uint8_t dest, - uint32_t extraSticky) -{ - if (dest == 0u) - return; - - uint32_t mac = 0u; - uint32_t status = 0u; - for (uint32_t component = 0; component < 4u; ++component) - { - const uint8_t lane = laneForComponent(component); - if ((dest & lane) == 0u) - continue; - - const uint32_t flags = laneFlags[component]; - if ((flags & 0x1u) != 0u) - mac |= lane; - if ((flags & 0x2u) != 0u) - mac |= static_cast(lane) << 4; - if ((flags & 0x4u) != 0u) - mac |= static_cast(lane) << 8; - if ((flags & 0x8u) != 0u) - mac |= static_cast(lane) << 12; - status |= flags; - } - - FlagPipelineEntry *entry = nullptr; - for (FlagPipelineEntry &candidate : m_flagPipeline) - { - if (!candidate.valid) - { - entry = &candidate; - break; - } - } - if (!entry) - { - reportReservedInstruction(true, 0xFFFFFFFFu); - return; - } - - *entry = {}; - entry->valid = true; - entry->issueCycle = m_cycle; - entry->readyCycle = m_cycle + kFmacLatency; - entry->mac = mac; - entry->status = status; - entry->extraSticky = extraSticky; - entry->writesMac = true; - entry->writesStatus = true; -} - -void VU1Interpreter::applyFmacDest(float *dst, float *result, uint8_t dest) -{ - uint8_t laneFlags[4]{}; - normalizeFmacResult(result, dest, laneFlags); - updateFmacFlags(laneFlags, dest, calculateFmacProductSticky(dest)); - applyDest(dst, result, dest); -} - -void VU1Interpreter::applyFmacDestAcc(float *result, uint8_t dest) -{ - uint8_t laneFlags[4]{}; - normalizeFmacResult(result, dest, laneFlags); - updateFmacFlags(laneFlags, dest, calculateFmacProductSticky(dest)); - applyDestAcc(result, dest); -} - -void VU1Interpreter::queueFsset(uint16_t immediate) -{ - for (FlagPipelineEntry &entry : m_flagPipeline) - { - if (entry.valid && entry.issueCycle == m_cycle) - entry.writesStatus = false; - } - - for (FlagPipelineEntry &entry : m_flagPipeline) - { - if (!entry.valid) - { - entry = {}; - entry.valid = true; - entry.issueCycle = m_cycle; - entry.readyCycle = m_cycle + kFmacLatency; - entry.status = static_cast(immediate) & 0xFC0u; - entry.writesSticky = true; - return; - } - } - reportReservedInstruction(false, 0xFFFFFFFEu); -} - -void VU1Interpreter::queueClip(uint32_t clip) -{ - m_workingClip = ((m_workingClip << 6) | (clip & 0x3Fu)) & 0xFFFFFFu; - for (FlagPipelineEntry &entry : m_flagPipeline) - { - if (!entry.valid) - { - entry = {}; - entry.valid = true; - entry.issueCycle = m_cycle; - entry.readyCycle = m_cycle + kFmacLatency; - entry.clip = m_workingClip; - entry.writesClip = true; - return; - } - } - reportReservedInstruction(true, 0xFFFFFFFDu); -} - -void VU1Interpreter::queueFcset(uint32_t clip) -{ - m_workingClip = clip & 0xFFFFFFu; - for (FlagPipelineEntry &entry : m_flagPipeline) - { - if (entry.valid && entry.issueCycle == m_cycle) - entry.writesClip = false; - } - for (FlagPipelineEntry &entry : m_flagPipeline) - { - if (!entry.valid) - { - entry = {}; - entry.valid = true; - entry.issueCycle = m_cycle; - entry.readyCycle = m_cycle + kFmacLatency; - entry.clip = m_workingClip; - entry.writesClip = true; - return; - } - } - reportReservedInstruction(false, 0xFFFFFFFAu); -} - -void VU1Interpreter::queueQ(float value, uint32_t latency, uint32_t statusDi) -{ - uint32_t ignoredFlags = 0u; - value = normalizeResult(value, ignoredFlags); - m_fdiv.valid = true; - m_fdiv.readyCycle = m_cycle + latency; - m_fdiv.value = value; - m_fdiv.statusDi = statusDi & 0x30u; -} - -void VU1Interpreter::queueP(float value, uint32_t latency) -{ - uint32_t ignoredFlags = 0u; - value = normalizeResult(value, ignoredFlags); - for (ScalarPipelineEntry &entry : m_efu) - { - if (!entry.valid) - { - entry.valid = true; - entry.readyCycle = m_cycle + latency; - entry.value = value; - // EFU throughput is one cycle shorter than result visibility. - m_efuResourceReady = m_cycle + (latency > 0u ? latency - 1u : 0u); - return; - } - } - reportReservedInstruction(false, 0xFFFFFFF9u); -} - -void VU1Interpreter::queueStore(uint32_t address, const uint32_t words[4], uint8_t laneMask) -{ - for (PendingStore &store : m_storePipeline) - { - if (!store.valid) - { - store.valid = true; - store.readyCycle = m_cycle + 1u; - store.address = address; - store.laneMask = laneMask; - std::copy(words, words + 4, store.words.begin()); - return; - } - } - reportReservedInstruction(false, 0xFFFFFFFCu); -} - -void VU1Interpreter::queueVfWrite(uint8_t reg, uint8_t laneMask, - const float value[4], uint32_t latency) -{ - if (reg == 0u || laneMask == 0u) - return; - for (PendingVfWrite &write : m_vfWritePipeline) - { - if (!write.valid) - { - write = {}; - write.valid = true; - write.readyCycle = m_cycle + latency; - write.sequence = ++m_nextWriteSequence; - write.reg = reg; - write.laneMask = laneMask; - std::copy(value, value + 4, write.value.begin()); - for (uint32_t component = 0; component < 4u; ++component) - { - if ((laneMask & laneForComponent(component)) != 0u) - m_vfLatestWrite[reg][component] = write.sequence; - } - return; - } - } - reportReservedInstruction(false, 0xFFFFFFF7u); -} - -void VU1Interpreter::queueViWrite(uint8_t reg, int32_t value, uint32_t latency) -{ - if (reg == 0u) - return; - for (PendingViWrite &write : m_viWritePipeline) - { - if (!write.valid) - { - write = {}; - write.valid = true; - write.readyCycle = m_cycle + latency; - write.sequence = ++m_nextWriteSequence; - write.reg = reg; - write.value = value; - m_viLatestWrite[reg] = write.sequence; - return; - } - } - reportReservedInstruction(false, 0xFFFFFFF6u); -} - -void VU1Interpreter::queueAccWrite(uint8_t laneMask, const float value[4], uint32_t latency) -{ - if (laneMask == 0u) - return; - for (PendingAccWrite &write : m_accWritePipeline) - { - if (!write.valid) - { - write = {}; - write.valid = true; - write.readyCycle = m_cycle + latency; - write.sequence = ++m_nextWriteSequence; - write.laneMask = laneMask; - std::copy(value, value + 4, write.value.begin()); - for (uint32_t component = 0; component < 4u; ++component) - { - if ((laneMask & laneForComponent(component)) != 0u) - m_accLatestWrite[component] = write.sequence; - } - return; - } - } - reportReservedInstruction(true, 0xFFFFFFF5u); -} - -void VU1Interpreter::commitReadyPipelines() -{ - for (FlagPipelineEntry &entry : m_flagPipeline) - { - if (!entry.valid || entry.readyCycle > m_cycle) - continue; - - if (entry.writesMac) - m_state.mac = entry.mac; - if (entry.writesStatus) - { - const uint32_t current = entry.status & 0xFu; - m_state.status = (m_state.status & 0xFF0u) | current | ((current | entry.extraSticky) << 6); - } - if (entry.writesSticky) - { - m_state.status = (m_state.status & 0x03Fu) | (entry.status & 0xFC0u); - } - if (entry.writesClip) - m_state.clip = entry.clip; - entry = {}; - } - - if (m_fdiv.valid && m_fdiv.readyCycle <= m_cycle) - { - m_state.q = m_fdiv.value; - const uint32_t currentDi = m_fdiv.statusDi & 0x30u; - m_state.status = (m_state.status & 0xFCFu) | currentDi | (currentDi << 6); - m_fdiv = {}; - } - - for (ScalarPipelineEntry &entry : m_efu) - { - if (entry.valid && entry.readyCycle <= m_cycle) - { - m_state.p = entry.value; - entry = {}; - } - } - - for (PendingStore &store : m_storePipeline) - { - if (!store.valid || store.readyCycle > m_cycle) - continue; - if (m_activeVuData && store.address + 16u <= m_activeVuDataSize) - { - uint32_t oldWords[4]{}; - std::memcpy(oldWords, m_activeVuData + store.address, sizeof(oldWords)); - for (uint32_t component = 0; component < 4u; ++component) - { - if ((store.laneMask & laneForComponent(component)) != 0u) - oldWords[component] = store.words[component]; - } - std::memcpy(m_activeVuData + store.address, oldWords, sizeof(oldWords)); - } - store = {}; - } - - for (PendingVfWrite &write : m_vfWritePipeline) - { - if (!write.valid || write.readyCycle > m_cycle) - continue; - for (uint32_t component = 0; component < 4u; ++component) - { - if ((write.laneMask & laneForComponent(component)) != 0u && - m_vfLatestWrite[write.reg][component] == write.sequence) - { - m_state.vf[write.reg][component] = write.value[component]; - } - } - write = {}; - } - - for (PendingViWrite &write : m_viWritePipeline) - { - if (!write.valid || write.readyCycle > m_cycle) - continue; - if (m_viLatestWrite[write.reg] == write.sequence) - m_state.vi[write.reg] = static_cast(write.value); - write = {}; - } - - for (PendingAccWrite &write : m_accWritePipeline) - { - if (!write.valid || write.readyCycle > m_cycle) - continue; - for (uint32_t component = 0; component < 4u; ++component) - { - if ((write.laneMask & laneForComponent(component)) != 0u && - m_accLatestWrite[component] == write.sequence) - { - m_state.acc[component] = write.value[component]; - } - } - write = {}; - } -} - -void VU1Interpreter::progressXgkick() -{ - if (!m_xgkick.active || !m_activeVuData || m_activeVuDataSize == 0u) - return; - - ++m_xgkick.cycleCredit; - while (m_xgkick.active && m_xgkick.cycleCredit >= 2u) - { - m_xgkick.cycleCredit -= 2u; - if (m_xgkick.copiedBytes > XgkickPipeline::kBufferSize - 16u) - { - reportReservedInstruction(false, 0xFFFFFFFBu); - m_xgkick.active = false; - return; - } - - const uint32_t qwordOffset = m_xgkick.copiedBytes; - for (uint32_t i = 0; i < 16u; ++i) - { - const uint32_t source = (m_xgkick.sourceAddress + m_xgkick.copiedBytes + i) % m_activeVuDataSize; - m_xgkick.packet[m_xgkick.copiedBytes + i] = m_activeVuData[source]; - } - m_xgkick.copiedBytes += 16u; - - if (m_xgkick.currentTagEnd == 0u) - { - uint64_t tagLo = 0; - std::memcpy(&tagLo, m_xgkick.packet.data() + qwordOffset, sizeof(tagLo)); - const uint32_t nloop = static_cast(tagLo & 0x7FFFu); - const uint32_t format = static_cast((tagLo >> 58) & 0x3u); - uint32_t nreg = static_cast((tagLo >> 60) & 0xFu); - if (nreg == 0u) - nreg = 16u; - - uint64_t tagBytes = 16u; - if (format == 0u) - tagBytes += static_cast(nloop) * nreg * 16u; - else if (format == 1u) - tagBytes += ((static_cast(nloop) * nreg + 1u) & ~1ull) * 8u; - else if (format == 2u) - tagBytes += static_cast(nloop) * 16u; - else - { - reportReservedInstruction(false, 0xFFFFFFF8u); - m_xgkick.active = false; - return; - } - - if (tagBytes > XgkickPipeline::kBufferSize - qwordOffset) - { - reportReservedInstruction(false, 0xFFFFFFFBu); - m_xgkick.active = false; - return; - } - m_xgkick.currentTagEnd = qwordOffset + static_cast(tagBytes); - m_xgkick.currentTagEop = ((tagLo >> 15) & 1u) != 0u; - if (m_xgkick.currentTagEop) - m_xgkick.totalBytes = m_xgkick.currentTagEnd; - } - - if (m_xgkick.copiedBytes >= m_xgkick.currentTagEnd) - { - if (m_xgkick.currentTagEop) - finishXgkick(); - else - { - // The next transferred qword is another GIFtag. - m_xgkick.currentTagEnd = 0u; - m_xgkick.currentTagEop = false; - } - } - } -} - -void VU1Interpreter::finishXgkick() -{ - if (!m_xgkick.active) - return; - - if (m_activeMemory) - m_activeMemory->submitGifPacket(GifPathId::Path1, m_xgkick.packet.data(), m_xgkick.totalBytes); - else if (m_activeGs) - m_activeGs->processGIFPacket(m_xgkick.packet.data(), m_xgkick.totalBytes); - m_xgkick.active = false; -} - -void VU1Interpreter::startXgkick(uint32_t qwordAddress) -{ - if (m_unit != Unit::VU1 || !m_activeVuData || m_activeVuDataSize < 16u) - return; - - const uint32_t sourceAddress = (qwordAddress * 16u) % m_activeVuDataSize; - m_xgkick = {}; - m_xgkick.active = true; - m_xgkick.sourceAddress = sourceAddress; - m_xgkick.cycleCredit = 1u; // XGKICK's issue cycle counts toward PATH1. - m_xgkick.issueCycle = m_cycle; -} - -void VU1Interpreter::advanceOneCycle() -{ - ++m_cycle; - m_state.cycles = m_cycle; - // LSU commits become visible at the cycle boundary before PATH1 consumes - // its next qword from VU memory. - commitReadyPipelines(); - progressXgkick(); -} - -void VU1Interpreter::advanceTo(uint64_t targetCycle) -{ - while (m_cycle < targetCycle) - advanceOneCycle(); -} - -bool VU1Interpreter::pipelinesPending() const -{ - if (m_fdiv.valid || m_xgkick.active) - return true; - for (const ScalarPipelineEntry &entry : m_efu) - if (entry.valid) - return true; - for (const FlagPipelineEntry &entry : m_flagPipeline) - if (entry.valid) - return true; - for (const PendingStore &store : m_storePipeline) - if (store.valid) - return true; - for (const PendingVfWrite &write : m_vfWritePipeline) - if (write.valid) - return true; - for (const PendingViWrite &write : m_viWritePipeline) - if (write.valid) - return true; - for (const PendingAccWrite &write : m_accWritePipeline) - if (write.valid) - return true; - return false; -} - -void VU1Interpreter::flushPipelines() -{ - while (pipelinesPending()) - advanceOneCycle(); -} - -uint64_t VU1Interpreter::calculatePairReadyCycle(const DecodedInstructionPair &decoded) const -{ - uint64_t ready = m_cycle; - const InstructionUsage *usages[2] = { - &decoded.upperUsage, - &decoded.lowerUsage}; - for (const InstructionUsage *usage : usages) - { - if (!usage) - continue; - for (uint32_t index = 0; index < usage->vfReadCount; ++index) - { - const VfAccess &access = usage->vfRead[index]; - for (uint32_t component = 0; component < 4u; ++component) - { - if ((access.lanes & laneForComponent(component)) != 0u) - ready = std::max(ready, m_vfReady[access.reg][component]); - } - } - for (uint32_t reg = 1; reg < m_viReady.size(); ++reg) - { - if ((usage->viRead & (1u << reg)) != 0u) - ready = std::max(ready, m_viReady[reg]); - } - for (uint32_t component = 0; component < 4u; ++component) - { - if ((usage->accRead & laneForComponent(component)) != 0u) - ready = std::max(ready, m_accReady[component]); - } - } - - if (decoded.lowerUsage.pipeline == PipelineFdiv && m_fdiv.valid) - ready = std::max(ready, m_fdiv.readyCycle); - if (decoded.lowerUsage.pipeline == PipelineEfu) - ready = std::max(ready, m_efuResourceReady); - if (decoded.lowerUsage.waitQ && m_fdiv.valid) - ready = std::max(ready, m_fdiv.readyCycle); - if (decoded.lowerUsage.waitP) - { - for (const ScalarPipelineEntry &entry : m_efu) - if (entry.valid) - ready = std::max(ready, entry.readyCycle); - } - if (decoded.lowerUsage.pipeline == PipelineXgkick && m_xgkick.active) - ready = std::max(ready, m_cycle + 1u); - return ready; -} - -void VU1Interpreter::markPairWrites(const DecodedInstructionPair &decoded) -{ - const VfAccess lowerWrite = decoded.lowerUsage.vfWrite; - if (lowerWrite.reg != 0u && - decoded.suppressedLowerVf != lowerWrite.reg) - { - const uint32_t latency = decoded.lowerUsage.vfLatency != 0u - ? decoded.lowerUsage.vfLatency - : decoded.lowerUsage.latency; - for (uint32_t component = 0; component < 4u; ++component) - { - if ((lowerWrite.lanes & laneForComponent(component)) != 0u) - m_vfReady[lowerWrite.reg][component] = m_cycle + latency; - } - } - - const VfAccess upperWrite = decoded.upperUsage.vfWrite; - if (upperWrite.reg != 0u) - { - const uint32_t latency = decoded.upperUsage.vfLatency != 0u - ? decoded.upperUsage.vfLatency - : decoded.upperUsage.latency; - for (uint32_t component = 0; component < 4u; ++component) - { - if ((upperWrite.lanes & laneForComponent(component)) != 0u) - m_vfReady[upperWrite.reg][component] = m_cycle + latency; - } - } - - for (uint32_t reg = 1; reg < m_viReady.size(); ++reg) - { - if ((decoded.lowerUsage.viWrite & (1u << reg)) != 0u) - m_viReady[reg] = m_cycle + (decoded.lowerUsage.viLatency != 0u ? decoded.lowerUsage.viLatency : decoded.lowerUsage.latency); - } - for (uint32_t component = 0; component < 4u; ++component) - { - if ((decoded.upperUsage.accWrite & laneForComponent(component)) != 0u) - m_accReady[component] = m_cycle + kAccForwardLatency; - } -} - -VU1Interpreter::InstructionUsage VU1Interpreter::decodeUpperUsage(uint32_t upper) const -{ - InstructionUsage usage; - usage.pipeline = PipelineFmac; - usage.latency = kFmacLatency; - - const uint8_t op = static_cast(upper & 0x3Fu); - const uint8_t dest = DEST(upper); - const uint8_t fs = FS(upper); - const uint8_t ft = FT(upper); - const uint8_t fd = FD(upper); - - if (op <= 0x2Fu) - { - addVfRead(usage, fs, dest); - addVfWrite(usage, fd, dest); - if (op <= 0x1Bu) - addVfRead(usage, ft, laneForComponent(op & 3u)); - else if (op >= 0x28u) - addVfRead(usage, ft, op == 0x2Eu ? 0xEu : dest); - if (op == 0x08u || op == 0x09u || op == 0x0Au || op == 0x0Bu || - op == 0x0Cu || op == 0x0Du || op == 0x0Eu || op == 0x0Fu || - op == 0x21u || op == 0x23u || op == 0x25u || op == 0x27u || - op == 0x29u || op == 0x2Du || op == 0x2Eu) - { - usage.accRead = dest; - } - return usage; - } - - if (op >= 0x3Cu) - { - const uint8_t special = static_cast((upper & 3u) | ((upper >> 4) & 0x7Cu)); - const bool writesAcc = - special <= 0x0Fu || - (special >= 0x18u && special <= 0x1Cu) || - special == 0x1Eu || - (special >= 0x20u && special <= 0x2Au) || - (special >= 0x2Cu && special <= 0x2Eu); - if (writesAcc) - { - addVfRead(usage, fs, dest); - if (special <= 0x1Bu) - addVfRead(usage, ft, laneForComponent(special & 3u)); - else if ((special >= 0x28u && special <= 0x2Eu)) - addVfRead(usage, ft, special == 0x2Eu ? 0xEu : dest); - usage.accWrite = dest; - if ((special >= 0x08u && special <= 0x0Fu) || - special == 0x21u || special == 0x23u || special == 0x25u || - special == 0x27u || special == 0x29u || special == 0x2Du) - { - usage.accRead = dest; - } - } - else if (special >= 0x10u && special <= 0x17u) - { - addVfRead(usage, fs, dest); - addVfWrite(usage, ft, dest); - } - else if (special == 0x1Du) - { - addVfRead(usage, fs, dest); - addVfWrite(usage, ft, dest); - } - else if (special == 0x1Fu) - { - addVfRead(usage, fs, 0xEu); - addVfRead(usage, ft, 0x1u); - usage.writesClip = true; - } - else if (special != 0x2Fu && special != 0x30u) - { - usage.reserved = true; - } - return usage; - } - - usage.reserved = true; - return usage; -} - -VU1Interpreter::InstructionUsage VU1Interpreter::decodeLowerUsage(uint32_t lower) const -{ - InstructionUsage usage; - if (lower == 0u || lower == 0x8000033Cu) - return usage; - - const uint8_t opHi = static_cast((lower >> 25) & 0x7Fu); - const uint8_t vfT = FT(lower); - const uint8_t vfS = FS(lower); - const uint8_t viT = VIT(lower); - const uint8_t viS = VIS(lower); - const uint8_t viD = VID(lower); - const uint8_t dest = DEST(lower); - auto readVi = [&](uint8_t reg) - { - if (reg != 0u) - usage.viRead |= static_cast(1u << reg); - }; - auto writeVi = [&](uint8_t reg) - { - if (reg != 0u) - usage.viWrite |= static_cast(1u << reg); - }; - - switch (opHi) - { - case 0x00: - usage.pipeline = PipelineLsu; - usage.latency = 4u; - readVi(viS); - addVfWrite(usage, vfT, dest); - return usage; - case 0x01: - usage.pipeline = PipelineLsu; - usage.latency = 1u; - readVi(viT); - addVfRead(usage, vfS, dest); - return usage; - case 0x04: - usage.pipeline = PipelineLsu; - usage.latency = 4u; - readVi(viS); - writeVi(viT); - return usage; - case 0x05: - usage.pipeline = PipelineLsu; - usage.latency = 1u; - readVi(viS); - readVi(viT); - return usage; - case 0x08: - case 0x09: - usage.pipeline = PipelineIalu; - usage.latency = 1u; - usage.delaysNextBranchRead = true; - readVi(viS); - writeVi(viT); - return usage; - case 0x10: - case 0x12: - case 0x13: - usage.pipeline = PipelineIalu; - usage.latency = 1u; - usage.readsClip = true; - writeVi(1u); - return usage; - case 0x11: - usage.pipeline = PipelineFmac; - usage.latency = kFmacLatency; - usage.writesClip = true; - return usage; - case 0x14: - case 0x16: - case 0x17: - usage.pipeline = PipelineIalu; - usage.latency = 1u; - writeVi(viT); - return usage; - case 0x15: - usage.pipeline = PipelineFmac; - usage.latency = kFmacLatency; - return usage; - case 0x18: - case 0x1A: - case 0x1B: - usage.pipeline = PipelineIalu; - usage.latency = 1u; - readVi(viS); - writeVi(viT); - return usage; - case 0x1C: - usage.pipeline = PipelineIalu; - usage.latency = 1u; - usage.readsClip = true; - writeVi(viT); - return usage; - case 0x20: - usage.pipeline = PipelineBranch; - return usage; - case 0x21: - usage.pipeline = PipelineBranch; - usage.latency = 1u; - writeVi(viT); - return usage; - case 0x24: - usage.pipeline = PipelineBranch; - readVi(viS); - return usage; - case 0x25: - usage.pipeline = PipelineBranch; - usage.latency = 1u; - readVi(viS); - writeVi(viT); - return usage; - case 0x28: - case 0x29: - usage.pipeline = PipelineBranch; - readVi(viS); - readVi(viT); - return usage; - case 0x2C: - case 0x2D: - case 0x2E: - case 0x2F: - usage.pipeline = PipelineBranch; - readVi(viS); - return usage; - case 0x40: - break; - default: - usage.reserved = true; - return usage; - } - - const uint8_t direct = static_cast(lower & 0x3Fu); - if (direct == 0x30u || direct == 0x31u || direct == 0x34u || direct == 0x35u) - { - usage.pipeline = PipelineIalu; - usage.latency = 1u; - usage.delaysNextBranchRead = true; - readVi(viS); - readVi(viT); - writeVi(viD); - return usage; - } - if (direct == 0x32u) - { - usage.pipeline = PipelineIalu; - usage.latency = 1u; - usage.delaysNextBranchRead = true; - readVi(viS); - writeVi(viT); - return usage; - } - if (direct < 0x3Cu) - { - usage.reserved = true; - return usage; - } - - const uint8_t special = static_cast((lower & 3u) | ((lower >> 4) & 0x7Cu)); - switch (special) - { - case 0x30: - case 0x31: - usage.pipeline = PipelineFmac; - usage.latency = 4u; - addVfRead(usage, vfS, special == 0x31u ? 0xFu : dest); - addVfWrite(usage, vfT, dest); - break; - case 0x34: - case 0x36: - usage.pipeline = PipelineLsu; - usage.latency = 4u; - usage.viLatency = 1u; - usage.delaysNextBranchRead = true; - readVi(viS); - writeVi(viS); - addVfWrite(usage, vfT, dest); - break; - case 0x35: - case 0x37: - usage.pipeline = PipelineLsu; - usage.latency = 1u; - usage.delaysNextBranchRead = true; - readVi(viT); - writeVi(viT); - addVfRead(usage, vfS, dest); - break; - case 0x38: - usage.pipeline = PipelineFdiv; - usage.latency = 7u; - addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); - addVfRead(usage, vfT, laneForComponent((lower >> 23) & 3u)); - break; - case 0x39: - usage.pipeline = PipelineFdiv; - usage.latency = 7u; - addVfRead(usage, vfT, laneForComponent((lower >> 23) & 3u)); - break; - case 0x3A: - usage.pipeline = PipelineFdiv; - usage.latency = 13u; - addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); - addVfRead(usage, vfT, laneForComponent((lower >> 23) & 3u)); - break; - case 0x3B: - usage.pipeline = PipelineFdiv; - usage.waitQ = true; - break; - case 0x3C: - usage.pipeline = PipelineIalu; - usage.latency = 1u; - usage.delaysNextBranchRead = true; - addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); - writeVi(viT); - break; - case 0x3D: - usage.pipeline = PipelineFmac; - usage.latency = 4u; - readVi(viS); - addVfWrite(usage, vfT, dest); - break; - case 0x3E: - usage.pipeline = PipelineLsu; - usage.latency = 4u; - readVi(viS); - writeVi(viT); - break; - case 0x3F: - usage.pipeline = PipelineLsu; - usage.latency = 1u; - readVi(viS); - readVi(viT); - break; - case 0x40: - case 0x41: - usage.pipeline = PipelineFmac; - usage.latency = 4u; - addVfWrite(usage, vfT, dest); - break; - case 0x42: - case 0x43: - usage.pipeline = PipelineIalu; - usage.latency = 1u; - addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); - break; - case 0x64: - if (m_unit == Unit::VU0) - { - usage.reserved = true; - break; - } - usage.pipeline = PipelineFmac; - usage.latency = 4u; - addVfWrite(usage, vfT, dest); - break; - case 0x68: - case 0x69: - usage.pipeline = PipelineIalu; - usage.latency = 1u; - writeVi(viT); - break; - case 0x6C: - if (m_unit == Unit::VU0) - { - usage.reserved = true; - break; - } - usage.pipeline = PipelineXgkick; - usage.latency = 2u; - readVi(viS); - break; - case 0x70: - case 0x71: - case 0x72: - case 0x73: - case 0x74: - case 0x75: - case 0x76: - case 0x77: - case 0x78: - case 0x79: - case 0x7A: - case 0x7C: - case 0x7D: - if (m_unit == Unit::VU0) - { - usage.reserved = true; - break; - } - usage.pipeline = PipelineEfu; - switch (special) - { - case 0x70: - usage.latency = 11u; - break; - case 0x71: - case 0x72: - case 0x77: - usage.latency = 18u; - break; - case 0x73: - usage.latency = 24u; - break; - case 0x74: - case 0x75: - case 0x7C: - usage.latency = 54u; - break; - case 0x76: - case 0x78: - case 0x7A: - usage.latency = 12u; - break; - case 0x79: - usage.latency = 29u; - break; - case 0x7D: - usage.latency = 44u; - break; - default: - break; - } - if (special >= 0x70u && special <= 0x73u) - addVfRead(usage, vfS, 0xEu); - else if (special == 0x74u) - addVfRead(usage, vfS, 0xCu); - else if (special == 0x75u) - addVfRead(usage, vfS, 0xAu); - else if (special == 0x76u) - addVfRead(usage, vfS, 0xFu); - else - addVfRead(usage, vfS, laneForComponent((lower >> 21) & 3u)); - break; - case 0x7B: - if (m_unit == Unit::VU0) - { - usage.reserved = true; - break; - } - usage.pipeline = PipelineEfu; - usage.waitP = true; - break; - default: - usage.reserved = true; - break; - } - return usage; -} - -VU1Interpreter::DecodedInstructionPair VU1Interpreter::decodeInstructionPair(const uint8_t *vuCode, uint32_t pc) const -{ - DecodedInstructionPair decoded; - std::memcpy(&decoded.lower, vuCode + pc, sizeof(decoded.lower)); - std::memcpy(&decoded.upper, vuCode + pc + sizeof(decoded.lower), sizeof(decoded.upper)); - decoded.iBit = (decoded.upper & 0x80000000u) != 0u; - decoded.eBit = (decoded.upper & 0x40000000u) != 0u; - decoded.mBit = (decoded.upper & 0x20000000u) != 0u; - decoded.dBit = (decoded.upper & 0x10000000u) != 0u; - decoded.tBit = (decoded.upper & 0x08000000u) != 0u; - decoded.upperUsage = decodeUpperUsage(decoded.upper); - if (!decoded.iBit) - decoded.lowerUsage = decodeLowerUsage(decoded.lower); - - const uint8_t upperWriteReg = decoded.upperUsage.vfWrite.reg; - if (upperWriteReg != 0u && (vfReadLanes(decoded.lowerUsage, upperWriteReg) != 0u || decoded.lowerUsage.vfWrite.reg == upperWriteReg)) - { - decoded.upperVfShadowReg = upperWriteReg; - if (decoded.lowerUsage.vfWrite.reg == upperWriteReg) - decoded.suppressedLowerVf = upperWriteReg; - } - return decoded; -} - -void VU1Interpreter::rebuildDecodedCodeCache(const uint8_t *vuCode, uint32_t codeSize, - const PS2Memory *memory, uint64_t generation) -{ - const uint32_t pairCount = std::min(codeSize / 8u, kMaxDecodedPairs); - for (uint32_t i = 0; i < pairCount; ++i) - m_decodedCodeCache[i] = decodeInstructionPair(vuCode, i * 8u); - - m_cachedVuCode = vuCode; - m_cachedMemory = memory; - m_cachedCodeSize = codeSize; - m_cachedCodeGeneration = generation; - m_decodedCodeCacheValid = true; -} - -VU1Interpreter::DecodedInstructionPair VU1Interpreter::getDecodedInstructionPairForPc( - const uint8_t *vuCode, uint32_t codeSize, PS2Memory *memory, uint32_t pc) -{ - if ((pc & 7u) != 0u) - return decodeInstructionPair(vuCode, pc); - - const bool trackedVu1Code = memory != nullptr && - ((m_unit == Unit::VU1 && vuCode == memory->getVU1Code()) || - (m_unit == Unit::VU0 && vuCode == memory->getVU0Code())); - if (!trackedVu1Code) - return decodeInstructionPair(vuCode, pc); - - const uint64_t generation = m_unit == Unit::VU1 ? memory->getVU1CodeGeneration() : memory->getVU0CodeGeneration(); - if (!m_decodedCodeCacheValid || - m_cachedVuCode != vuCode || - m_cachedMemory != memory || - m_cachedCodeSize != codeSize || - m_cachedCodeGeneration != generation) - { - rebuildDecodedCodeCache(vuCode, codeSize, memory, generation); - } - const uint32_t pairIndex = pc / 8u; - if (pairIndex >= kMaxDecodedPairs) - return decodeInstructionPair(vuCode, pc); - return m_decodedCodeCache[pairIndex]; -} - -void VU1Interpreter::reportReservedInstruction(bool upper, uint32_t instruction) -{ - RUNTIME_ERROR( - "[VU" << (m_unit == Unit::VU1 ? "1" : "0") - << " reserved " << (upper ? "upper" : "lower") - << "] cycle=" << m_cycle - << " pc=0x" << std::hex << m_state.pc - << " instruction=0x" << instruction - << std::dec << '\n'); - m_stopRequested = true; -} - -void VU1Interpreter::execute(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory, - uint32_t startPC, uint32_t top, uint32_t itop, - uint32_t maxCycles) -{ - resetScheduler(); - m_state.pc = startPC & microAddressMask(); - m_state.ebit = false; - m_state.haltAfterDelaySlot = false; - m_state.stoppedByD = false; - m_state.stoppedByT = false; - m_state.top = top; - m_state.itop = itop; - m_state.branchPending = false; - m_state.branchTarget = 0; - m_state.branchDelay = 0; - m_state.vf[0][0] = 0.0f; - m_state.vf[0][1] = 0.0f; - m_state.vf[0][2] = 0.0f; - m_state.vf[0][3] = 1.0f; - run(vuCode, codeSize, vuData, dataSize, gs, memory, maxCycles); -} - -void VU1Interpreter::resume(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory, - uint32_t top, uint32_t itop, uint32_t maxCycles) -{ - m_state.top = top; - m_state.itop = itop; - m_state.stoppedByD = false; - m_state.stoppedByT = false; - run(vuCode, codeSize, vuData, dataSize, gs, memory, maxCycles); -} - -void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory, uint32_t maxCycles) -{ - m_activeVuData = vuData; - m_activeVuDataSize = dataSize; - m_activeGs = &gs; - m_activeMemory = memory; - - const int previousRoundingMode = std::fegetround(); - const bool useVuRounding = std::fesetround(FE_TOWARDZERO) == 0; - const uint64_t budgetEnd = m_cycle + maxCycles; - bool programEnded = false; - while (m_cycle < budgetEnd && !m_stopRequested) - { - commitReadyPipelines(); - if (m_state.pc + 8u > codeSize) - break; - - const DecodedInstructionPair decoded = getDecodedInstructionPairForPc(vuCode, codeSize, memory, m_state.pc); - if (decoded.upperUsage.reserved || decoded.lowerUsage.reserved) - { - reportReservedInstruction(decoded.upperUsage.reserved, decoded.upperUsage.reserved ? decoded.upper : decoded.lower); - break; - } - - uint64_t readyCycle = calculatePairReadyCycle(decoded); - while (readyCycle > m_cycle) - { - if (readyCycle >= budgetEnd) - { - advanceTo(budgetEnd); - break; - } - advanceTo(readyCycle); - readyCycle = calculatePairReadyCycle(decoded); - } - if (m_cycle >= budgetEnd) - break; - - uint8_t writtenVi = 0u; - int32_t oldVi = 0; - for (uint32_t reg = 1; reg < 16u; ++reg) - { - if ((decoded.lowerUsage.viWrite & (1u << reg)) != 0u) - { - writtenVi = static_cast(reg); - oldVi = m_state.vi[reg]; - break; - } - } - - const VfAccess upperWrite = decoded.upperUsage.vfWrite; - const VfAccess lowerWrite = decoded.lowerUsage.vfWrite; - const bool hasUpperWrite = upperWrite.reg != 0u; - const bool hasLowerWrite = lowerWrite.reg != 0u && decoded.suppressedLowerVf != lowerWrite.reg; - const bool hasDistinctLowerWrite = hasLowerWrite && (!hasUpperWrite || lowerWrite.reg != upperWrite.reg); - float oldUpperVf[4]{}; - float newUpperVf[4]{}; - float oldLowerVf[4]{}; - float newLowerVf[4]{}; - float oldAcc[4]{}; - float newAcc[4]{}; - if (hasUpperWrite) - std::memcpy(oldUpperVf, m_state.vf[upperWrite.reg], sizeof(oldUpperVf)); - if (hasDistinctLowerWrite) - std::memcpy(oldLowerVf, m_state.vf[lowerWrite.reg], sizeof(oldLowerVf)); - if (decoded.upperUsage.accWrite != 0u) - std::memcpy(oldAcc, m_state.acc, sizeof(oldAcc)); - - if (decoded.iBit) - { - execUpper(decoded.upper); - float immediate = 0.0f; - std::memcpy(&immediate, &decoded.lower, sizeof(immediate)); - m_state.i = normalizeOperand(immediate); - } - else if (decoded.upperVfShadowReg != 0u) - { - float oldVf[4]{}; - float upperVf[4]{}; - std::memcpy(oldVf, - m_state.vf[decoded.upperVfShadowReg], - sizeof(oldVf)); - execUpper(decoded.upper); - std::memcpy(upperVf, - m_state.vf[decoded.upperVfShadowReg], - sizeof(upperVf)); - std::memcpy(m_state.vf[decoded.upperVfShadowReg], - oldVf, - sizeof(oldVf)); - execLower(decoded.lower, vuData, dataSize, gs, memory, decoded.upper); - std::memcpy(m_state.vf[decoded.upperVfShadowReg], - upperVf, - sizeof(upperVf)); - } - else - { - execUpper(decoded.upper); - execLower(decoded.lower, vuData, dataSize, gs, memory, decoded.upper); - } - - m_viBranchBackupValid = false; - - if (hasUpperWrite) - { - std::memcpy(newUpperVf, m_state.vf[upperWrite.reg], sizeof(newUpperVf)); - std::memcpy(m_state.vf[upperWrite.reg], oldUpperVf, sizeof(oldUpperVf)); - const uint32_t latency = - decoded.upperUsage.vfLatency != 0u - ? decoded.upperUsage.vfLatency - : decoded.upperUsage.latency; - queueVfWrite(upperWrite.reg, upperWrite.lanes, newUpperVf, latency); - } - if (hasDistinctLowerWrite) - { - std::memcpy(newLowerVf, m_state.vf[lowerWrite.reg], sizeof(newLowerVf)); - std::memcpy(m_state.vf[lowerWrite.reg], oldLowerVf, sizeof(oldLowerVf)); - const uint32_t latency = decoded.lowerUsage.vfLatency != 0u - ? decoded.lowerUsage.vfLatency - : decoded.lowerUsage.latency; - queueVfWrite(lowerWrite.reg, lowerWrite.lanes, newLowerVf, latency); - } - if (decoded.upperUsage.accWrite != 0u) - { - std::memcpy(newAcc, m_state.acc, sizeof(newAcc)); - std::memcpy(m_state.acc, oldAcc, sizeof(oldAcc)); - // ACC is forwarded to the next upper instruction. Its arithmetic - // flags still use the normal four-cycle FMAC timeline. - queueAccWrite(decoded.upperUsage.accWrite, newAcc, - kAccForwardLatency); - } - if (writtenVi != 0u) - { - const int32_t newVi = m_state.vi[writtenVi]; - m_state.vi[writtenVi] = oldVi; - const uint32_t latency = - decoded.lowerUsage.viLatency != 0u - ? decoded.lowerUsage.viLatency - : decoded.lowerUsage.latency; - queueViWrite(writtenVi, newVi, latency); - } - - markPairWrites(decoded); - if (writtenVi != 0u && decoded.lowerUsage.delaysNextBranchRead) - recordViWriteForBranch(writtenVi, oldVi); - - m_state.vf[0][0] = 0.0f; - m_state.vf[0][1] = 0.0f; - m_state.vf[0][2] = 0.0f; - m_state.vf[0][3] = 1.0f; - m_state.vi[0] = 0; - - uint32_t nextPc = m_state.pc + 8u; - if (nextPc >= codeSize) - nextPc = 0u; - m_state.pc = nextPc; - - if (m_state.branchPending) - { - if (m_state.branchDelay == 0u) - { - m_state.pc = m_state.branchTarget & microAddressMask(); - m_state.branchPending = false; - } - else - { - --m_state.branchDelay; - } - } - - const bool dHalt = decoded.dBit && m_state.dBitEnabled; - const bool tHalt = decoded.tBit && m_state.tBitEnabled; - const bool haltBit = dHalt || tHalt; - const bool haltBranch = haltBit && decoded.lowerUsage.pipeline == PipelineBranch; - - if (m_state.haltAfterDelaySlot) - { - m_state.stoppedByD = m_pendingHaltD; - m_state.stoppedByT = m_pendingHaltT; - programEnded = true; - } - else if (m_state.ebit) - programEnded = true; - else if (haltBit && !haltBranch) - { - m_state.stoppedByD = dHalt; - m_state.stoppedByT = tHalt; - programEnded = true; - } - else if (decoded.eBit) - m_state.ebit = true; - else if (haltBranch) - { - m_state.haltAfterDelaySlot = true; - m_pendingHaltD = dHalt; - m_pendingHaltT = tHalt; - } - - advanceOneCycle(); - if (programEnded) - break; - } - - if (programEnded) - { - flushPipelines(); - m_state.ebit = false; - m_state.haltAfterDelaySlot = false; - m_pendingHaltD = false; - m_pendingHaltT = false; - } - m_state.cycles = m_cycle; - if (useVuRounding && previousRoundingMode != -1) - std::fesetround(previousRoundingMode); -} diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h b/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h deleted file mode 100644 index ed0ee7145..000000000 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h +++ /dev/null @@ -1,29 +0,0 @@ -#ifndef PS2_VU1_DETAIL_H -#define PS2_VU1_DETAIL_H - -#include - -// Instruction field extraction helpers -static inline uint8_t DEST(uint32_t i) { return (uint8_t)((i >> 21) & 0xF); } -static inline uint8_t FT(uint32_t i) { return (uint8_t)((i >> 16) & 0x1F); } -static inline uint8_t FS(uint32_t i) { return (uint8_t)((i >> 11) & 0x1F); } -static inline uint8_t FD(uint32_t i) { return (uint8_t)((i >> 6) & 0x1F); } -static inline uint8_t BC(uint32_t i) { return (uint8_t)(i & 0x3); } - -// Lower instruction field helpers -static inline uint8_t LIT(uint32_t i) { return (uint8_t)((i >> 16) & 0x1F); } -static inline uint8_t LIS(uint32_t i) { return (uint8_t)((i >> 11) & 0x1F); } -static inline uint8_t LID(uint32_t i) { return (uint8_t)((i >> 6) & 0x1F); } -static inline uint8_t VIT(uint32_t i) { return (uint8_t)((i >> 16) & 0xF); } -static inline uint8_t VIS(uint32_t i) { return (uint8_t)((i >> 11) & 0xF); } -static inline uint8_t VID(uint32_t i) { return (uint8_t)((i >> 6) & 0xF); } -static inline int16_t IMM11(uint32_t i) { return (int16_t)(int32_t)((int32_t)(i << 21) >> 21); } -static inline int16_t IMM15(uint32_t i) -{ - uint32_t lo11 = i & 0x7FF; - uint32_t hi4 = (i >> 21) & 0xF; - uint32_t raw = (hi4 << 11) | lo11; - return (int16_t)(int32_t)((int32_t)(raw << 17) >> 17); -} - -#endif diff --git a/ps2xRuntime/src/lib/vu/vu_context.cpp b/ps2xRuntime/src/lib/vu/vu_context.cpp new file mode 100644 index 000000000..3fc775860 --- /dev/null +++ b/ps2xRuntime/src/lib/vu/vu_context.cpp @@ -0,0 +1,70 @@ +#include "runtime/vu/vu_runtime.h" +#include "ps2_runtime.h" +namespace ps2vu +{ + void importContext(const R5900Context &context, VU1State &state) noexcept + { + + + for (uint32_t i = 0; i < 32u; ++i) + { + _mm_storeu_ps(state.vf[i], context.vu0_vf[i]); + } + for (uint32_t i = 0; i < 16u; ++i) + { + state.vi[i] = static_cast(context.vi[i]); + } + + _mm_storeu_ps(state.acc, context.vu0_acc); + state.q = context.vu0_q; + state.p = context.vu0_p; + state.i = context.vu0_i; + alignas(16) uint32_t rWords[4]{}; + _mm_storeu_si128(reinterpret_cast<__m128i *>(rWords), _mm_castps_si128(context.vu0_r)); + state.r = 0x3F800000u | (rWords[0] & 0x007FFFFFu); + state.pc = context.vu0_pc; + state.mac = context.vu0_mac_flags; + state.clip = context.vu0_clip_flags; + state.status = context.vu0_status; + state.itop = context.vu0_itop; + state.dBitEnabled = (context.vu0_fbrst & (1u << 2)) != 0u; + state.tBitEnabled = (context.vu0_fbrst & (1u << 3)) != 0u; + + state.vf[0][0] = 0.0f; + state.vf[0][1] = 0.0f; + state.vf[0][2] = 0.0f; + state.vf[0][3] = 1.0f; + state.vi[0] = 0; + } + + void exportContext(const VU1State &state, R5900Context &context) noexcept + { + for (uint32_t i = 0; i < 32u; ++i) + { + context.vu0_vf[i] = _mm_loadu_ps(state.vf[i]); + } + for (uint32_t i = 0; i < 16u; ++i) + { + context.vi[i] = static_cast(state.vi[i]); + } + + context.vu0_acc = _mm_loadu_ps(state.acc); + context.vu0_q = state.q; + context.vu0_p = state.p; + context.vu0_i = state.i; + context.vu0_r = _mm_castsi128_ps(_mm_set1_epi32(static_cast(state.r))); + context.vu0_mac_flags = state.mac; + context.vu0_clip_flags = state.clip; + context.vu0_clip_flags2 = state.clip; + context.vu0_status = static_cast(state.status); + context.vu0_itop = state.itop; + context.vu0_pc = state.pc; + context.vu0_tpc = state.pc; + context.vu0_vpu_stat = (context.vu0_vpu_stat & 0xFF00u) | (state.stoppedByD ? (1u << 1) : 0u) | (state.stoppedByT ? (1u << 2) : 0u); + context.vu0_vpu_stat2 = 0; + + context.vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f); + context.vi[0] = 0; + } + +} diff --git a/ps2xRuntime/src/lib/vu/vu_runtime.cpp b/ps2xRuntime/src/lib/vu/vu_runtime.cpp new file mode 100644 index 000000000..67e38ec9a --- /dev/null +++ b/ps2xRuntime/src/lib/vu/vu_runtime.cpp @@ -0,0 +1,427 @@ +#include "runtime/vu/vu_runtime.h" +#include "runtime/vu/vu_unit.h" +#include "runtime/vu/vu_decoder.h" +#include "ps2_runtime.h" +#include +#include +#include +#include +#include + +namespace ps2vu +{ + Runtime::Runtime(PS2Memory &memory, std::function advanceClock) + : m_memory(memory), m_advanceClock(std::move(advanceClock)) + { + MemoryServices services; + services.context = this; + services.readShared = readShared; + services.writeShared = writeShared; + services.submitGif = submitGif; + for (uint32_t index = 0; index < 2; ++index) + { + const bool vu1 = index != 0; + services.data = vu1 ? memory.getVU1Data() : memory.getVU0Data(); + m_units[index] = std::make_unique(vu1, std::span(vu1 ? memory.getVU1Code() : memory.getVU0Code(), vu1 ? PS2_VU1_CODE_SIZE : PS2_VU0_CODE_SIZE), services); + } + } + + Runtime::~Runtime() = default; + Unit &Runtime::unit(bool vu1) noexcept { return *m_units[vu1]; } + + bool Runtime::failed() const noexcept + { + return m_units[0]->status() == UnitStatus::Fault || m_units[1]->status() == UnitStatus::Fault; + } + + void Runtime::reportFailure() const + { + for (uint32_t index = 0; index < 2; ++index) + if (m_units[index]->status() == UnitStatus::Fault) + { + const auto &failure = m_units[index]->failure(); + throw std::runtime_error("VU" + std::to_string(index) + " execution failed at PC " + + std::to_string(failure.pc) + ": " + failure.reason.data()); + } + } + + void Runtime::run(bool vu1, uint32_t cycles) + { + auto &target = unit(vu1); + while (cycles) + { + if (vu1 && unit(false).status() == UnitStatus::Running) + { + m_advanceClock(1); + reportFailure(); + --cycles; + if (target.status() != UnitStatus::Running) + return; + continue; + } + + const auto exit = target.advance(cycles); + if (!vu1) + m_contextDirty = true; + + if (exit.cycles) + m_advanceClock(static_cast(exit.cycles)); + + reportFailure(); + cycles -= static_cast(exit.cycles); + if (exit.reason != ExitReason::Event || !exit.cycles) + return; + } + } + + void Runtime::wait(bool vu1) + { + while (unit(vu1).status() == UnitStatus::Running) + run(vu1, 64); + reportFailure(); + } + + void Runtime::start0(R5900Context &context, uint32_t pc) + { + m_accessContext = &context; + const bool running = unit(false).status() == UnitStatus::Running; + wait(false); + if (running) + exportContext(unit(false).state(), context); + + importContext(context, unit(false).state()); + if (!unit(false).start(pc, 0, context.vu0_itop, StartSource::Ee)) + reportFailure(); + + exportContext(unit(false).state(), context); + m_accessContext = nullptr; + } + + void Runtime::startVif0(R5900Context &context, uint32_t pc, uint32_t itop, bool resume) + { + m_accessContext = &context; + const bool running = unit(false).status() == UnitStatus::Running; + + wait(false); + + if (running) + exportContext(unit(false).state(), context); + + importContext(context, unit(false).state()); + const bool started = resume ? unit(false).resume(0, itop) : unit(false).start(pc, 0, itop); + m_accessContext = nullptr; + if (!started) + { + reportFailure(); + throw std::runtime_error("VIF startup requested while VU0 is stopped"); + } + exportContext(unit(false).state(), context); + } + + void Runtime::start1(uint32_t pc, uint32_t top, uint32_t itop, bool resume) + { + wait(true); + const bool started = resume ? unit(true).resume(top, itop) : unit(true).start(pc, top, itop); + if (!started) + { + reportFailure(); + throw std::runtime_error("VIF startup requested while VU1 is stopped"); + } + } + + void Runtime::flush0(R5900Context &context) + { + m_accessContext = &context; + wait(false); + m_accessContext = nullptr; + } + + void Runtime::flush1(bool gif) + { + wait(true); + if (gif) + while (unit(true).transferring()) + run(true, 2); + } + + void Runtime::advance(uint32_t cycles, R5900Context &context) noexcept + { + auto &activeContext = m_accessContext ? *m_accessContext : context; + m_cycle += cycles; + + auto &vu0 = unit(false); + const bool updateContext = vu0.pending() || m_contextDirty; + if (updateContext && !m_contextDirty && vu0.status() != UnitStatus::Running) + importContext(activeContext, vu0.state()); + + advanceUnit(false, m_cycle); + advanceUnit(true, m_cycle); + + if (updateContext) + exportContext(vu0.state(), activeContext); + + m_contextDirty = false; + activeContext.vu0_vpu_stat = (activeContext.vu0_vpu_stat & ~0x0701u) | + (vu0.status() == UnitStatus::Running ? 1u : 0u) | + (unit(true).status() == UnitStatus::Running ? 0x100u : 0u) | + (unit(true).state().stoppedByD ? 0x200u : 0u) | (unit(true).state().stoppedByT ? 0x400u : 0u); + } + + void Runtime::advanceUnit(bool vu1, uint64_t cycle) noexcept + { + auto &target = unit(vu1); + if (target.state().cycles >= cycle) + return; + + if (!target.pending() && !target.transferring()) + { + if (target.status() != UnitStatus::Fault) + target.state().cycles = cycle; + return; + } + + while (target.state().cycles < cycle) + { + const auto remaining = static_cast(std::min(cycle - target.state().cycles, UINT32_MAX)); + const auto exit = target.advance(remaining); + if (!exit.cycles || exit.reason == ExitReason::Fault) + break; + } + } + + void Runtime::beforeAccess(R5900Context &context, uint32_t instruction) + { + const auto opcode = instruction >> 26; + if (opcode == 0x36 || opcode == 0x3e) + { + const auto target = (instruction >> 16) & 31; + if (unit(false).pending() || m_contextDirty) + context.vu0_vf[target] = _mm_loadu_ps(unit(false).state().vf[target]); + reportFailure(); + return; + } + + const auto format = (instruction >> 21) & 31; + const auto reg = (instruction >> 11) & 31; + auto &vu0 = unit(false); + if (!vu0.pending() && !m_contextDirty) + { + if (format == 5 || format == 6) + vu0.state().mBit = false; + reportFailure(); + return; + } + + m_accessContext = &context; + if (vu0.status() != UnitStatus::Running) + importContext(context, vu0.state()); + + const bool interlocked = (instruction & 1) != 0; + const bool writeTransfer = format == 5 || format == 6; + if (interlocked && writeTransfer) + { + while (vu0.status() == UnitStatus::Running && !vu0.state().mBit) + run(false, 1); + } + else if (format >= 16 || interlocked) + wait(false); + + if (writeTransfer) + vu0.state().mBit = false; + + const auto &ready = vu0.readiness(); + uint32_t delay = 0; + if ((format == 1 || format == 5) && interlocked) + for (uint32_t lane = 0; lane < 4; ++lane) + delay = std::max(delay, uint32_t(ready.registers[reg * 4 + lane])); + else if ((format == 2 || format == 6) && reg < 16 && interlocked) + delay = ready.registers[kViReadyBase + reg]; + else if (format >= 16) + { + const uint32_t op = instruction & 63; + if (op != 0x38 && op != 0x39) + { + const auto special = ((instruction >> 4) & 0x7c) | (instruction & 3); + const bool lower = op >= 0x30 && (op < 0x3c || special >= 0x30); + const auto decoded = decodeInstructionPair(lower ? 0x80000000u | (instruction & 0x1ffffff) : 0x8000033cu, + lower ? 0x2ffu : instruction & 0x1ffffff, false); + for (uint32_t index = 0; index < decoded.readDependencyCount; ++index) + delay = std::max(delay, uint32_t(ready.registers[decoded.readDependencies[index]])); + for (const auto &usage : {decoded.upperUsage, decoded.lowerUsage}) + { + if (usage.vfWrite.reg) + for (uint32_t lane = 0; lane < 4; ++lane) + delay = std::max(delay, uint32_t(ready.registers[usage.vfWrite.reg * 4 + lane])); + for (uint32_t reg = 1; reg < 16; ++reg) + if (usage.viWrite & (1u << reg)) + delay = std::max(delay, uint32_t(ready.registers[kViReadyBase + reg])); + } + if (decoded.lowerUsage.waitQ || decoded.lowerUsage.pipeline == PipelineFdiv) + delay = std::max(delay, uint32_t(ready.q)); + } + } + + if (delay) + run(false, delay); + + exportContext(vu0.state(), context); + m_accessContext = nullptr; + } + + void Runtime::afterAccess(R5900Context &context, uint32_t instruction) + { + const auto opcode = instruction >> 26; + if (opcode == 0x36) + { + const auto target = (instruction >> 16) & 31; + if (target) + _mm_storeu_ps(unit(false).state().vf[target], context.vu0_vf[target]); + else + context.vu0_vf[0] = _mm_set_ps(1, 0, 0, 0); + return; + } + + if (opcode == 0x3e) + return; + if (unit(false).pending()) + importContext(context, unit(false).state()); + + const auto format = (instruction >> 21) & 31; + const auto reg = (instruction >> 11) & 31; + if (format == 6 && reg == 28) + { + const auto value = static_cast(_mm_cvtsi128_si32(context.r[(instruction >> 16) & 31])); + for (uint32_t index = 0; index < 2; ++index) + { + auto &target = *m_units[index]; + const auto bits = value >> (index * 8); + if (bits & 2) + target.reset(); + else if (bits & 1) + target.forceBreak(); + target.state().dBitEnabled = (bits & 4) != 0; + target.state().tBitEnabled = (bits & 8) != 0; + if (bits & 2) + target.state().cycles = m_cycle; + } + if (value & 2) + exportContext(unit(false).state(), context); + } + + if (format == 6 && reg == 31) + { + m_accessContext = &context; + wait(true); + const auto &state = unit(true).state(); + unit(true).start((context.vu0_cmsar1 & 0x7ff) * 8, state.top, state.itop, StartSource::Ee); + m_accessContext = nullptr; + } + } + + bool Runtime::submitGif(void *context, const uint8_t *data, uint32_t size) noexcept + { + try + { + static_cast(context)->m_memory.submitGifPacket(GifPathId::Path1, data, size); + return true; + } + catch (...) + { + return false; + } + } + + bool Runtime::readShared(void *context, uint64_t cycle, uint32_t offset, uint32_t *words) noexcept + { + auto &runtime = *static_cast(context); + runtime.advanceUnit(true, cycle); + if (runtime.failed()) + return false; + + auto &state = runtime.unit(true).state(); + const auto reg = (offset & 0x3ff) >> 4; + std::fill_n(words, 4, 0u); + + if (reg < 32) + std::memcpy(words, state.vf[reg], 16); + else if (reg < 48) + words[0] = static_cast(state.vi[reg - 32]); + else + switch (reg - 32) + { + case 16: + words[0] = state.status; + break; + case 17: + words[0] = state.mac; + break; + case 18: + words[0] = state.clip; + break; + case 20: + words[0] = state.r; + break; + case 21: + words[0] = std::bit_cast(state.i); + break; + case 22: + words[0] = std::bit_cast(state.q); + break; + case 23: + words[0] = std::bit_cast(state.p); + break; + case 26: + words[0] = state.pc / 8; + break; + default: + break; + } + return true; + } + + bool Runtime::writeShared(void *context, uint64_t cycle, uint32_t offset, const uint32_t *words, uint8_t lanes) noexcept + { + auto &runtime = *static_cast(context); + runtime.advanceUnit(true, cycle); + if (runtime.failed()) + return false; + + auto &target = runtime.unit(true); + auto &state = target.state(); + const auto reg = (offset & 0x3ff) >> 4; + if (reg && reg < 32) + for (uint32_t lane = 0; lane < 4; ++lane) + if (lanes & (8u >> lane)) + state.vf[reg][lane] = std::bit_cast(words[lane]); + + if (!(lanes & 8)) + return true; + + if (reg > 32 && reg < 48) + state.vi[reg - 32] = static_cast(words[0]); + else + switch (reg - 32) + { + case 16: + state.status = (state.status & 0x3f) | (words[0] & 0xfc0); + break; + case 18: + state.clip = words[0] & 0xffffff; + break; + case 20: + state.r = 0x3f800000 | (words[0] & 0x7fffff); + break; + case 21: + state.i = std::bit_cast(words[0]); + break; + case 22: + state.q = std::bit_cast(words[0]); + break; + case 31: + return target.start((words[0] & 0x7ff) * 8, state.top, state.itop, StartSource::Ee); + default: + break; + } + return true; + } +} diff --git a/ps2xRuntime/src/lib/vu/vu_unit.cpp b/ps2xRuntime/src/lib/vu/vu_unit.cpp new file mode 100644 index 000000000..8fe013a87 --- /dev/null +++ b/ps2xRuntime/src/lib/vu/vu_unit.cpp @@ -0,0 +1,129 @@ +#include "runtime/vu/vu_unit.h" +#include +#include +#include + +namespace ps2vu +{ + Unit::Unit(bool vu1, std::span code, MemoryServices services) + : m_executor(vu1 ? VUExecutor::Unit::VU1 : VUExecutor::Unit::VU0), m_services(services), + m_code(code), m_dataSize(vu1 ? 16384 : 4096) + { + m_executor.m_services = &m_services; + m_executor.m_activeVuData = services.data; + m_executor.m_activeVuDataSize = m_dataSize; + } + + void Unit::reset() noexcept + { + m_executor.reset(); + m_failure = {}; + m_status = UnitStatus::Ready; + } + + void Unit::fail(const char *reason) noexcept + { + if (m_status == UnitStatus::Fault) + return; + m_failure.pc = state().pc; + std::snprintf(m_failure.reason.data(), m_failure.reason.size(), "%s", reason); + m_status = UnitStatus::Fault; + } + + bool Unit::start(uint32_t pc, uint32_t top, uint32_t itop, StartSource source) noexcept + { + if (m_status == UnitStatus::Fault || m_status == UnitStatus::Running) + return false; + if (m_status == UnitStatus::Stopped && source == StartSource::Vif) + return false; + if ((pc & 7) || pc >= m_code.size()) + { + state().pc = pc; + fail("Invalid VU entry address"); + return false; + } + state().pc = pc; + begin(top, itop); + return true; + } + + bool Unit::resume(uint32_t top, uint32_t itop) noexcept + { + if (m_status != UnitStatus::Ready) + return false; + begin(top, itop); + return true; + } + + void Unit::forceBreak() noexcept + { + if (m_status != UnitStatus::Fault) + m_status = UnitStatus::Stopped; + } + + void Unit::begin(uint32_t top, uint32_t itop) noexcept + { + auto &s = state(); + s.top = top; + s.itop = itop; + s.ebit = false; + s.mBit = false; + s.haltAfterDelaySlot = false; + s.stoppedByD = false; + s.stoppedByT = false; + s.branchPending = false; + s.branchDelay = 0; + m_executor.m_pendingHaltD = false; + m_executor.m_pendingHaltT = false; + m_executor.m_viBranchBackupValid = false; + m_executor.m_programEnded = false; + m_executor.m_stopRequested = false; + m_status = UnitStatus::Running; + } + + Readiness Unit::readiness() const noexcept + { + Readiness result; + const auto cycle = state().cycles; + for (size_t index = 0; index < result.registers.size(); ++index) + if (m_executor.m_registerReady[index] > cycle) + result.registers[index] = static_cast(m_executor.m_registerReady[index] - cycle); + + if (m_executor.m_fdiv.valid && m_executor.m_fdiv.readyCycle > cycle) + result.q = static_cast(m_executor.m_fdiv.readyCycle - cycle); + + return result; + } + + ExecutionExit Unit::advance(uint32_t cycles) noexcept + { + if (m_status == UnitStatus::Fault) + return {ExitReason::Fault, state().pc, 0}; + const auto initial = state().cycles; + m_executor.m_cycle = initial; + try + { + if (m_status == UnitStatus::Running) + { + m_executor.run(m_code.data(), static_cast(m_code.size()), m_services.data, m_dataSize, nullptr, nullptr, cycles, false); + if (m_executor.m_stopRequested) + fail("VU instruction execution failed"); + else if (m_executor.m_programEnded) + m_status = state().stoppedByD || state().stoppedByT ? UnitStatus::Stopped : UnitStatus::Ready; + } + else + m_executor.advanceTo(initial + cycles); + } + catch (const std::exception &error) + { + fail(error.what()); + } + catch (...) + { + fail("Unknown VU execution failure"); + } + return {m_status == UnitStatus::Fault ? ExitReason::Fault : m_executor.m_programEnded ? ExitReason::Halt + : ExitReason::Budget, + state().pc, state().cycles - initial}; + } +} diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 5db04b923..53818e6a5 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -8,6 +8,12 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) include(CTest) if(BUILD_TESTING) add_subdirectory(gs_cache) + if(PS2X_GS_BACKEND STREQUAL "PARALLEL_GS") + add_executable(ps2_gs_parallel_tests gs_parallel_tests.cpp ) + target_link_libraries(ps2_gs_parallel_tests PRIVATE ps2_runtime) + add_test(NAME gs.parallel_gpu COMMAND ps2_gs_parallel_tests) + set_tests_properties(gs.parallel_gpu PROPERTIES LABELS "gs;gpu;interop" TIMEOUT 120) + endif() endif() # Static library with test logic (no main), used by ps2xStudio @@ -21,6 +27,7 @@ target_include_directories(ps2_test_function_table PRIVATE ) add_library(ps2_test_lib STATIC + src/code_generator_tests.cpp src/r5900_decoder_tests.cpp src/elf_analyzer_tests.cpp @@ -29,7 +36,9 @@ add_library(ps2_test_lib STATIC src/ps2_runtime_kernel_tests.cpp src/ps2_runtime_interrupt_tests.cpp src/ps2_memory_tests.cpp + src/ps2_vu1_tests.cpp + src/ps2_vu_pipeline_tests.cpp src/ps2_vu_tests.cpp src/ps2_gs_tests.cpp src/ps2_iop_tests.cpp @@ -54,14 +63,32 @@ target_link_libraries(ps2_test_lib PRIVATE ps2_iop ) +add_executable(ps2_vu1_tests src/vu1_tests_main.cpp $) +target_include_directories(ps2_vu1_tests PRIVATE include) +target_link_libraries(ps2_vu1_tests PRIVATE ps2_test_lib ps2_recomp_lib ps2_runtime) +if(MSVC) + target_link_options(ps2_vu1_tests PRIVATE /STACK:8388608) +endif() +if(BUILD_TESTING) + add_test(NAME vu1.regressions COMMAND ps2_vu1_tests) +endif() + if(UNIX AND NOT APPLE) - target_link_libraries(ps2_test_lib PRIVATE ${CMAKE_DL_LIBS}) + target_link_libraries(ps2_test_lib PRIVATE + ${CMAKE_DL_LIBS}) endif() add_executable(ps2x_tests src/main.cpp $ ) +add_test(NAME runtime.regressions COMMAND ps2x_tests) +set_tests_properties(runtime.regressions PROPERTIES RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}") + +find_package(Python3 COMPONENTS Interpreter) +if(Python3_Interpreter_FOUND) + add_test(NAME runtime.frame_measurement COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/frame_times_test.py") +endif() option(PRINT_GENERATED_CODE "Print generated code in tests" OFF) if(PRINT_GENERATED_CODE) diff --git a/ps2xTest/fixtures/vu1.vif.hex b/ps2xTest/fixtures/vu1.vif.hex new file mode 100644 index 000000000..6c72e4b22 --- /dev/null +++ b/ps2xTest/fixtures/vu1.vif.hex @@ -0,0 +1,148 @@ +4a010000 00000000 00000000 +4a010000 00000000 00000030 +4a010000 00000000 000209ff +4a010000 00000000 010208e8 +4a010000 00000000 010208e9 +4a010000 00000000 01020abc +4a010000 00000000 014208e8 +4a010000 00000000 01e208ea +4a010000 00000000 01e2093c +4a010000 00000000 01e31128 +4a010000 00000000 400002ff +4a010000 00000000 400208ea +4a010000 00000000 402208ea +4a010000 00000000 404208ea +4a010000 00000000 406208ea +4a010000 00000000 408208ea +4a010000 00000000 40a208ea +4a010000 00000000 40c208ea +4a010000 00000000 40e208ea +4a010000 00000000 410208ea +4a010000 00000000 412208ea +4a010000 00000000 414208ea +4a010000 00000000 416208ea +4a010000 00000000 418208ea +4a010000 00000000 41a208ea +4a010000 00000000 41c208ea +4a010000 00000000 41e208ea +4a010000 00a40801 000002ff +4a010000 03e10800 01e31068 +4a010000 09020800 000002ff +4a010000 10010001 000002ff +4a010000 10010001 010208e8 +4a010000 10010002 000002ff +4a010000 10010003 080002ff +4a010000 10010004 100002ff +4a010000 10020805 000002ff +4a010000 28250012 000002ff +4a010000 2a200280 000002ff +4a010000 2a200280 014208e8 +4a010000 2c010001 000002ff +4a010000 40000001 080002ff +4a010000 40000002 000002ff +4a010000 40e00000 81e008a2 +4a010000 800006fc 00000000 +4a010000 800006fc 000002ff +4a010000 80000efc 00000000 +4a010000 80000efc 000002ff +4a010000 80000efc 400002ff +4a010000 80000f3c 000002ff +4a010000 80000f3d 000002ff +4a010000 80000f3e 000002ff +4a010000 80000f3f 000002ff +4a010000 80000f7c 000002ff +4a010000 80000f7d 000002ff +4a010000 80000f7e 000002ff +4a010000 80000f7f 000002ff +4a010000 80000fbc 000002ff +4a010000 80000fbd 000002ff +4a010000 80000fbe 000002ff +4a010000 80000ffc 000002ff +4a010000 80000ffd 000002ff +4a010000 800206bc 000002ff +4a010000 80020bbc 000002ff +4a010000 80020bfc 000002ff +4a010000 81020b3c 01031068 +4a010000 81220bbc 000002ff +4a010000 81e10b7c 01031068 +4a010001 00000000 00000000 +4a010001 00000000 000002ff +4a010001 00000000 00821928 +4a010001 00000000 01021928 +4a010001 00000000 01041969 +4a010001 03422001 000002ff +4a010001 03e02001 01e208e9 +4a010001 03e22000 000002ff +4a010001 10001007 000002ff +4a010001 10010001 000002ff +4a010001 10010007 000002ff +4a010001 10020005 000002ff +4a010001 10020006 000002ff +4a010001 1002000b 000002ff +4a010001 10031001 000002ff +4a010001 2c260010 000002ff +4a010001 30063800 000002ff +4a010001 38030000 000002ff +4a010001 48000800 000002ff +4a010001 52000802 000002ff +4a010001 52000802 01001928 +4a010001 800003bf 000002ff +4a010001 800007bf 000002ff +4a010001 80000f3e 000002ff +4a010001 800016fc 000002ff +4a010001 80020bbc 01e108ea +4a010001 800303bd 000002ff +4a010001 800306bd 000002ff +4a010001 80230bfc 000002ff +4a010002 00000000 00000000 +4a010002 00000000 000002ff +4a010002 00000000 400002ff +4a010002 10020002 000002ff +4a010002 2e070040 000002ff +4a010002 52000802 000002ff +4a010002 800003bf 000002ff +4a010002 800007bf 000002ff +4a010002 80000efc 000002ff +4a010002 80000ffc 000002ff +4a010002 800110f0 000002ff +4a010002 80020bbc 000002ff +4a010002 80440bfc 000002ff +4a010002 8100143e 000002ff +4a010002 818303bd 000002ff +4a010003 00000000 00000000 +4a010003 00000000 000002ff +4a010003 00000000 400002ff +4a010003 10030003 000002ff +4a010003 10030007 000002ff +4a010003 800003bf 000002ff +4a010003 80000ffc 000002ff +4a010003 80650bfc 000002ff +4a010003 8103043c 000002ff +4a010004 00000000 00000000 +4a010004 00000000 000002ff +4a010004 10040004 000002ff +4a010004 34042800 000002ff +4a010004 38040000 000002ff +4a010004 800007bf 000002ff +4a010005 00000000 00000000 +4a010005 03e0180c 01e0195c +4a010005 10050005 000002ff +4a010006 00000000 00000000 +4a010006 00000000 000002ff +4a010007 00000000 000002ff +4a010008 00000000 000002ff +4a010009 00000000 000002ff +4a01000a 00000000 000002ff +4a01000b 00000000 000002ff +4a01000c 00000000 000002ff +4a01000d 00000000 000002ff +4a01000e 00000000 000002ff +4a01000f 00000000 000002ff +4a010010 00000000 000002ff +4a100100 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 400002ff 00000000 000002ff +4a100120 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 00000000 01e208e8 400007f1 000002ff 00000000 000002ff +4a010000 00000000 010208ea +4a010000 2a000400 000002ff +4a010000 22123456 000002ff +4a010000 2a000080 000002ff +4a010000 00000000 000002ff diff --git a/ps2xTest/gs_cache/CMakeLists.txt b/ps2xTest/gs_cache/CMakeLists.txt index 6b7210a68..febd60641 100644 --- a/ps2xTest/gs_cache/CMakeLists.txt +++ b/ps2xTest/gs_cache/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(ps2_gs_cache_backend_under_test STATIC "${PS2_GS_RUNTIME_DIR}/src/lib/gs/gs_cpu_backend.cpp" "${PS2_GS_RUNTIME_DIR}/src/lib/gs/gs_frontend.cpp" "${PS2_GS_RUNTIME_DIR}/src/lib/gs/ps2_gs_memory.cpp" + "${PS2_GS_RUNTIME_DIR}/src/lib/gs/ps2_gif_arbiter.cpp" ) target_include_directories(ps2_gs_cache_backend_under_test PUBLIC "${PS2_GS_RUNTIME_DIR}/include") target_compile_features(ps2_gs_cache_backend_under_test PUBLIC cxx_std_20) @@ -41,6 +42,8 @@ function(add_gs_cache_suite target source prefix) endforeach() endfunction() +add_gs_cache_suite(ps2_gs_raw_routing_tests gs_raw_routing_tests.cpp routing raw_commands) + add_gs_cache_suite(ps2_gs_texture_cache_tests gs_texture_cache_tests.cpp texture unaligned_texture unaligned_wrap stale_mirror page_alternation flush_visibility upload_visibility local_copy_visibility raster_visibility diff --git a/ps2xTest/gs_cache/gs_raw_routing_tests.cpp b/ps2xTest/gs_cache/gs_raw_routing_tests.cpp new file mode 100644 index 000000000..f3080db85 --- /dev/null +++ b/ps2xTest/gs_cache/gs_raw_routing_tests.cpp @@ -0,0 +1,170 @@ +#include "runtime/gs/gs_frontend.h" +#include "runtime/gs/ps2_gif_arbiter.h" +#include "runtime/ps2_memory.h" +#include +#include +#include + +namespace +{ + void check(bool condition, const char *message) + { + if (!condition) + throw std::runtime_error(message); + } + struct RawBackend final : GSRasterBackend + { + std::vector packets; + std::vector> registers; + std::vector image; + unsigned batches = 0, cluts = 0, transfers = 0, snapshots = 0; + + void Initialize(uint8_t *, uint32_t) override + { + } + + void Reset() override + { + } + + bool UsesRawCommands() const override + { + return true; + } + + void ProcessGIF(uint32_t path, const uint8_t *data, uint32_t size) override + { + packets.push_back({GifPathId(path), false, false, {data, data + size}}); + } + + void WriteRegisterRaw(uint8_t addr, uint64_t value) override + { + registers.emplace_back(addr, value); + } + + void ReadRegisterState(GSDebugSnapshot &s) const override + { + s.ctx[0].frame.fbp = 37; + } + + void Submit(const GSPrimitiveBatch &) override + { + ++batches; + } + + void LoadClut(const GSTex0Reg &, const GSTexClutReg &) override + { + ++cluts; + } + + void BeginTransfer(const GSTransferCommand &) override + { + ++transfers; + } + + void UploadImage(const uint8_t *data, uint32_t size) override + { + image.insert(image.end(), data, data + size); + } + + void Flush() override + { + } + + void TextureFlush() override + { + } + + void Sync(GSSyncReason) override + { + } + + PresentationFrame Present(const GSPresentationRequest &) override + { + return {}; + } + + bool ClearFramebuffer(const GSContext &, uint32_t) override + { + return true; + } + + uint32_t ConsumeLocalToHostBytes(uint8_t *, uint32_t) override + { + + return 0; + } + uint32_t ReadVram(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) const override + { + return 0; + } + + void WriteVram(uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t) override + { + } + + void SnapshotVram(std::vector &) const override + { + throw std::runtime_error("unexpected raw VRAM snapshot"); + } + + GSTransferSnapshot GetTransferSnapshot() const override + { + return {}; + } + }; +} +int main() +{ + try + { + std::vector vram(4 * 1024 * 1024); + GSRegisters priv{}; + GS gs; + gs.init(vram.data(), uint32_t(vram.size()), &priv); + auto backend = std::make_unique(); + auto &raw = *backend; + gs.setRasterBackend(std::move(backend)); + const uint64_t packed[] = {1ull | (1ull << 15) | (1ull << 60), 14, 0xff112233, GS_REG_RGBAQ}; + auto *packet = reinterpret_cast(packed); + GifArbiter arbiter([&](GifPathId path, const uint8_t *data, uint32_t size) + { gs.processGIFPacket(data, size, uint32_t(path)); }); + for (auto path : {GifPathId::Path3, GifPathId::Path1, GifPathId::Path2}) + arbiter.submit(path, packet, sizeof(packed)); + arbiter.drain(); + arbiter.drain(); + check(raw.packets.size() == 3, "arbiter duplicated or lost a packet"); + for (unsigned i = 0; i < 3; ++i) + { + check(uint32_t(raw.packets[i].pathId) == i + 1, "arbiter lost PATH identity"); + check(raw.packets[i].data == std::vector(packet, packet + sizeof(packed)), "arbiter changed GIF bytes"); + } + check(gs.processNativePackedGIFPacket(packet, sizeof(packed)), "native packed rejected"); + check(raw.packets.size() == 4 && raw.packets.back().pathId == GifPathId::Path3, "native packed route incorrect"); + check(raw.registers.empty(), "GIF was also decoded by CPU frontend"); + gs.processGIFPacket(packet, 16, 1); + gs.processGIFPacket(packet + 16, 16, 1); + check(raw.packets.size() == 6 && raw.packets[4].data.size() == 16, "split GIF was discarded"); + const uint8_t pixels[] = {1, 2, 3, 4, 5}; + gs.uploadImageNative(1, 2, 3, 0, pixels, sizeof(pixels)); + check(raw.registers == std::vector>{{GS_REG_BITBLTBUF, 1}, {GS_REG_TRXPOS, 2}, {GS_REG_TRXREG, 3}, {GS_REG_TRXDIR, 0}}, "native upload duplicated setup"); + check(raw.image == std::vector(pixels, pixels + sizeof(pixels)), "native upload changed bytes"); + gs.writeRegister(GS_REG_SIGNAL, ~0ull); + gs.writeRegister(GS_REG_FINISH, 0); + gs.writeRegister(GS_REG_LABEL, ~0ull); + check(priv.csr == 0 && priv.siglblid == 0, "frontend executed raw backend IRQ effects twice"); + gs.writeRegister(GS_REG_TEX0_1, 0); + gs.writeRegister(GS_REG_XYZ2, 0); + check(raw.batches == 0 && raw.cluts == 0 && raw.transfers == 0, "CPU work leaked into raw route"); + check(gs.getDebugSnapshot().ctx[0].frame.fbp == 37 && gs.getContextFrame(0).fbp == 37, "diagnostics used stale CPU state"); + gs.latchHostPresentationFrame(); + gs.shutdownBackend(); + std::cout << "Raw GIF routing, PATH, native shortcuts and side-effect ownership passed\n"; + return 0; + } + catch (const std::exception &e) + { + std::cerr << e.what() << '\n'; + return 1; + } +} diff --git a/ps2xTest/gs_parallel/CMakeLists.txt b/ps2xTest/gs_parallel/CMakeLists.txt new file mode 100644 index 000000000..c2d09b490 --- /dev/null +++ b/ps2xTest/gs_parallel/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.21) +project(PS2ParallelGSTests LANGUAGES C CXX) + +include(CTest) +include(FetchContent) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + +FetchContent_Declare(raylib GIT_REPOSITORY https://github.com/raysan5/raylib.git GIT_TAG 5.5) +FetchContent_MakeAvailable(raylib) + +get_filename_component(runtime "../../ps2xRuntime" ABSOLUTE BASE_DIR "${CMAKE_CURRENT_LIST_DIR}") +add_library(ps2_runtime STATIC + "${runtime}/src/lib/gs/gs_frontend.cpp" + "${runtime}/src/lib/gs/gs_cpu_backend.cpp" + "${runtime}/src/lib/gs/ps2_gs_memory.cpp") + +target_include_directories(ps2_runtime PUBLIC "${runtime}/include") + +target_link_libraries(ps2_runtime PUBLIC raylib) + +if(NOT MSVC AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + target_compile_options(ps2_runtime PRIVATE -msse4.1) +endif() + +include("${runtime}/cmake/ParallelGS.cmake") + +add_executable(ps2_gs_parallel_tests ../gs_parallel_tests.cpp) +target_link_libraries(ps2_gs_parallel_tests PRIVATE ps2_runtime) + +add_test(NAME gs.parallel_gpu COMMAND ps2_gs_parallel_tests) +set_tests_properties(gs.parallel_gpu PROPERTIES LABELS "gs;gpu;interop" TIMEOUT 120) diff --git a/ps2xTest/gs_parallel_tests.cpp b/ps2xTest/gs_parallel_tests.cpp new file mode 100644 index 000000000..525f52570 --- /dev/null +++ b/ps2xTest/gs_parallel_tests.cpp @@ -0,0 +1,312 @@ +#include "runtime/gs/gs_frontend.h" +#include "runtime/gs/gs_parallel_backend.h" +#include "runtime/ps2_memory.h" +#include "raylib.h" +#include "rlgl.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + void check(bool ok, const char *message) + { + if (!ok) + throw std::runtime_error(message); + } + void setupTransfer(GS &gs, unsigned psm, unsigned width, unsigned height, unsigned direction, unsigned src = 0, unsigned dst = 0) + { + gs.writeRegister(GS_REG_BITBLTBUF, uint64_t(src) | (1ull << 16) | (uint64_t(psm) << 24) | + (uint64_t(dst) << 32) | (1ull << 48) | (uint64_t(psm) << 56)); + gs.writeRegister(GS_REG_TRXPOS, 0); + gs.writeRegister(GS_REG_TRXREG, width | (uint64_t(height) << 32)); + gs.writeRegister(GS_REG_TRXDIR, direction); + } + void testTransfer(GS &gs, unsigned psm, unsigned bpp) + { + constexpr unsigned count = 13; + const auto bytes = (count * bpp + 7) / 8; + std::vector input(bytes), output(bytes); + for (unsigned i = 0; i < bytes; ++i) + input[i] = uint8_t(31 + i * 7); + if (bpp == 4) + input.back() &= 15; + uint64_t bitblt = (1ull << 16) | (uint64_t(psm) << 24) | (1ull << 48) | (uint64_t(psm) << 56); + // Exercise the native upload and its final incomplete HWREG word. + gs.uploadImageNative(bitblt, 0, count | (1ull << 32), 0, input.data(), unsigned(input.size())); + if (bpp == 4) + { + // GS FIFO does not support 4-bit readback; check these uploads/copies in VRAM. + for (unsigned base : {0u, 64u}) + { + if (base) + setupTransfer(gs, psm, count, 1, 2, 0, base); + for (unsigned x = 0; x < count; ++x) + check(gs.ReadVram(psm, base, 1, x, 0) == ((input[x / 2] >> ((x & 1) * 4)) & 15), "4-bit transfer mismatch"); + } + std::cout << "PSM " << psm << ": indexed upload and local copy passed\n"; + return; + } + setupTransfer(gs, psm, count, 1, 1); + check(gs.consumeLocalToHostBytes(output.data(), 3) == 3, "short FIFO read failed"); + check(gs.consumeLocalToHostBytes(output.data() + 3, bytes - 3) == bytes - 3, "FIFO tail lost"); + if (input != output) + throw std::runtime_error("upload/FIFO mismatch for PSM " + std::to_string(psm)); + check(gs.consumeLocalToHostBytes(output.data(), 1) == 0, "FIFO exceeded transfer bounds"); + setupTransfer(gs, psm, count, 1, 2, 0, 64); + setupTransfer(gs, psm, count, 1, 1, 64); + check(gs.consumeLocalToHostBytes(output.data(), bytes) == bytes && input == output, "local copy mismatch"); + std::cout << "PSM " << psm << ": native upload, partial FIFO, local copy passed\n"; + } + void sprite(GS &gs, uint32_t color, uint32_t depth = 0, bool textured = false) + { + gs.writeRegister(GS_REG_PRIM, GS_PRIM_SPRITE | (textured ? 0x110 : 0)); + gs.writeRegister(GS_REG_RGBAQ, color | (uint64_t(0x3f800000) << 32)); + gs.writeRegister(GS_REG_UV, 0); + gs.writeRegister(GS_REG_XYZ2, uint64_t(depth) << 32); + gs.writeRegister(GS_REG_UV, (2ull * 16) | ((2ull * 16) << 16)); + gs.writeRegister(GS_REG_XYZ2, (8ull * 16) | ((8ull * 16) << 16) | (uint64_t(depth) << 32)); + } + void testRaster(GS &gs) + { + gs.writeRegister(GS_REG_FRAME_1, 1ull << 16); + gs.writeRegister(GS_REG_SCISSOR_1, (63ull << 16) | (63ull << 48)); + gs.writeRegister(GS_REG_XYOFFSET_1, 0); + gs.writeRegister(GS_REG_ZBUF_1, 32 | (1ull << 32)); + gs.writeRegister(GS_REG_TEST_1, 1ull << 16 | 1ull << 17); + gs.writeRegister(GS_REG_PRMODECONT, 1); + sprite(gs, 0x80402010); + check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80402010, "solid sprite raster failed"); + gs.writeRegister(GS_REG_ZBUF_1, 32); + gs.writeRegister(GS_REG_TEST_1, 1ull << 16 | 2ull << 17); // GEQUAL + gs.WriteVram(GS_PSM_Z32, 32 * 32, 1, 2, 2, 100); + sprite(gs, 0x80ffffff, 99); + check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80402010, "depth rejection failed"); + sprite(gs, 0x80a0b0c0, 101); + check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80a0b0c0, "depth acceptance failed"); + gs.writeRegister(GS_REG_TEST_1, 1ull << 16 | 1ull << 17); + gs.writeRegister(GS_REG_ZBUF_1, 32 | (1ull << 32)); + for (unsigned y = 0; y < 2; ++y) + for (unsigned x = 0; x < 2; ++x) + gs.WriteVram(GS_PSM_T8, 256, 1, x, y, 1); + gs.WriteVram(GS_PSM_CT32, 512, 1, 1, 0, 0x80224466); + gs.writeRegister(GS_REG_TEXCLUT, 1); + gs.writeRegister(GS_REG_TEX0_1, 256 | (1ull << 14) | (uint64_t(GS_PSM_T8) << 20) | + (1ull << 26) | (1ull << 30) | (1ull << 34) | (1ull << 35) | + (512ull << 37) | (1ull << 55) | (1ull << 61)); + sprite(gs, 0x80808080, 0, true); + check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80224466, "indexed texture/CLUT failed"); + // Fixed-factor blending: (Cs - Cd) * FIX / 128 + Cd, FIX = 64. + gs.writeRegister(GS_REG_ALPHA_1, (1ull << 2) | (2ull << 4) | (1ull << 6) | (64ull << 32)); + gs.writeRegister(GS_REG_PRIM, GS_PRIM_SPRITE | 64); + gs.writeRegister(GS_REG_RGBAQ, 0x806688aa); + gs.writeRegister(GS_REG_XYZ2, 0); + gs.writeRegister(GS_REG_XYZ2, 128 | (128ull << 16)); + check(gs.ReadVram(GS_PSM_CT32, 0, 1, 2, 2) == 0x80446688, "blending failed"); + gs.writeRegister(GS_REG_SIGNAL, (0xffffffffull << 32) | 123); + gs.writeRegister(GS_REG_LABEL, (0xffffffffull << 32) | 456); + gs.writeRegister(GS_REG_FINISH, 0); + std::cout << "Raster, depth, CLUT/indexed texture, blending and FINISH passed\n"; + } + void testGIF(GS &gs) + { + const uint64_t tag[] = {1ull | (1ull << 15) | (1ull << 60), 14}; + const uint64_t one[] = {17ull << 16, GS_REG_FRAME_1}; + const uint64_t two[] = {23ull << 16, GS_REG_FRAME_2}; + // Incomplete tags on independent paths must survive interleaving. + gs.processGIFPacket(reinterpret_cast(tag), 16, 1); + gs.processGIFPacket(reinterpret_cast(tag), 16, 2); + gs.processGIFPacket(reinterpret_cast(two), 16, 2); + gs.processGIFPacket(reinterpret_cast(one), 16, 1); + auto snapshot = gs.getDebugSnapshot(); + check(snapshot.ctx[0].frame.fbw == 17 && snapshot.ctx[1].frame.fbw == 23, "interleaved GIF paths corrupted register state"); + + uint64_t packet[] = { + 4ull | (1ull << 60), + 14, + (1ull << 48) | (128ull << 32), + GS_REG_BITBLTBUF, + 0, + GS_REG_TRXPOS, + 3ull | (1ull << 32), + GS_REG_TRXREG, + 0, + GS_REG_TRXDIR, + 1ull | (1ull << 15) | (2ull << 58), + 0, + 0x1020304050607080ull, + 0x90a0b0c0ull, + }; + gs.processGIFPacket(reinterpret_cast(packet), sizeof(packet), 3); + check(gs.ReadVram(0, 128, 1, 0, 0) == 0x50607080 && + gs.ReadVram(0, 128, 1, 1, 0) == 0x10203040 && + gs.ReadVram(0, 128, 1, 2, 0) == 0x90a0b0c0, + "GIF IMAGE differs from native upload"); + std::cout << "Interleaved PATH1/2 and GIF IMAGE transfer passed\n"; + } + void testPendingPresentation(GS &gs, GSRasterBackend &backend) + { + GSPresentationRequest request{}; + request.pmode = 1 | (1ull << 5) | (0xffull << 8); + request.smode1 = 32ull << 3; + request.dispfb1 = 10ull << 9; + request.display1 = 318 | (50ull << 12) | (1ull << 23) | (1279ull << 32) | (447ull << 44); + std::vector pixels(640 * 448); + const auto before = backend.GetReadbackCount(); + for (unsigned pass = 0; pass < 8; ++pass) + { + backend.Flush(); + // Force the legal interleaving of a game upload between the frontend's + // Flush and Present, without relying on thread scheduling or sleeps. + const uint32_t color = pass & 1 ? 0xff225488 : 0xff6688aa; + std::fill(pixels.begin(), pixels.end(), color); + gs.uploadImageNative(10ull << 48, 0, 640ull | (448ull << 32), 0, + reinterpret_cast(pixels.data()), + uint32_t(pixels.size() * sizeof(uint32_t))); + std::cout << "Present with pending GS upload: " << pass << std::endl; + auto frame = backend.Present(request); + check(frame.gpu && frame.width && frame.height, "pending upload produced no scanout"); + Texture2D texture{frame.gpu->AcquireTexture(), int(frame.width), int(frame.height), 1, + PIXELFORMAT_UNCOMPRESSED_R8G8B8A8}; + Image image = LoadImageFromTexture(texture); + Color center = GetImageColor(image, int(frame.width / 2), int(frame.height / 2)); + UnloadImage(image); + rlDrawRenderBatchActive(); + frame.gpu->ReleaseTexture(); + check(center.r == uint8_t(color) && center.g == uint8_t(color >> 8) && + center.b == uint8_t(color >> 16), + "pending upload missing from scanout"); + } + check(backend.GetReadbackCount() == before, "pending presentation requested VRAM readback"); + std::cout << "Uploads between Flush and Present passed\n"; + } + void testPresentation(GS &gs, GSRegisters &priv) + { + // 480p CRTC, only circuit 1 enabled. Use a known color for interop readback. + gs.writeRegister(GS_REG_FRAME_1, 10ull << 16); + gs.writeRegister(GS_REG_SCISSOR_1, (639ull << 16) | (447ull << 48)); + check(gs.clearFramebufferContext(0, 0xff225488), "scanout setup clear failed"); + gs.writeRegister(GS_REG_SCISSOR_1, (639ull << 16) | (223ull << 48)); + check(gs.clearFramebufferContext(0, 0xff6688aa), "scanout top-band clear failed"); + gs.writeRegister(GS_REG_FRAME_2, 160 | (10ull << 16)); + gs.writeRegister(GS_REG_SCISSOR_2, (639ull << 16) | (447ull << 48)); + check(gs.clearFramebufferContext(1, 0xff448822), "second circuit clear failed"); + // CRTC ALP uses 0..255, unlike the drawing ALPHA.FIX factor (0x80 = 1). + priv.pmode = 1 | (1ull << 5) | (0xffull << 8); + priv.smode1 = 32ull << 3; + priv.smode2 = 0; + priv.dispfb1 = 10ull << 9; + priv.display1 = 318 | (50ull << 12) | (1ull << 23) | (1279ull << 32) | (447ull << 44); + const auto before = gs.getReadbackCount(); + for (unsigned pass = 0; pass < 6; ++pass) + { + if (pass == 2) + { + priv.pmode = 3 | (1ull << 5) | (0x80ull << 8); + priv.dispfb2 = 160 | (10ull << 9); + priv.display2 = priv.display1; + } + if (pass == 3) + { + priv.pmode &= ~2ull; + priv.smode1 |= 2ull << 13; + priv.smode2 = 3; + priv.display1 = 636 | (50ull << 12) | (3ull << 23) | (2559ull << 32) | (447ull << 44); + } + priv.csr = (pass & 1) << 13; + priv.vsyncTick++; + gs.latchHostPresentationFrame(); + uint32_t w = 0, h = 0; + float aspect = 0; + auto gpu = gs.getLatchedGpuFrame(w, h, aspect); + check(gpu && w && h && aspect > 0, "CRTC returned no GPU frame"); + std::vector pixels; + check(!gs.copyLatchedHostPresentationFrame(pixels, w, h), "presentation produced CPU pixels"); + gpu = gs.getLatchedGpuFrame(w, h, aspect); + for (unsigned repeat = 0; repeat < 2; ++repeat) + { + Texture2D texture{gpu->AcquireTexture(), int(w), int(h), 1, PIXELFORMAT_UNCOMPRESSED_R8G8B8A8}; + if ((pass == 0 || pass == 2) && repeat == 0) + { + // Test-only explicit readback proves that imported GL memory contains scanout. + Image image = LoadImageFromTexture(texture); + Color center = GetImageColor(image, int(w / 2), int(h * 3 / 4)); + Color top = GetImageColor(image, int(w / 2), int(h / 4)); + UnloadImage(image); + std::cout << "Scanout pass " << pass << ": " << w << 'x' << h + << " center RGB=" << unsigned(center.r) << ',' << unsigned(center.g) << ',' << unsigned(center.b) + << " top RGB=" << unsigned(top.r) << ',' << unsigned(top.g) << ',' << unsigned(top.b) << '\n'; + if (pass == 0) + { + check(center.r == 0x88 && center.g == 0x54 && center.b == 0x22, "interop scanout pixels mismatch"); + check(top.r == 0xaa && top.g == 0x88 && top.b == 0x66, "scanout orientation mismatch"); + } + else + check(center.r == 0x55 && center.g == 0x6e && center.b == 0x33, "dual-circuit composition mismatch"); + } + BeginDrawing(); + ClearBackground(BLACK); + DrawTexturePro(texture, {0, 0, float(w), float(h)}, {0, 0, 320, 240}, {0, 0}, 0, WHITE); + rlDrawRenderBatchActive(); + gpu->ReleaseTexture(); + EndDrawing(); + } + if (pass == 2) + SetWindowSize(400, 300); + } + check(gs.getReadbackCount() == before, "presentation requested VRAM readback"); + std::cout << "Progressive/interlaced scanout, repeated frames and resize passed\n"; + } +} +int main() +{ + try + { + GSRegisters regs{}; + auto backend = CreateParallelGSBackend(regs); + return 1; + } + catch (const std::runtime_error &) + { + } // Initialization before a context must fail explicitly. + SetConfigFlags(FLAG_WINDOW_HIDDEN); + InitWindow(320, 240, "parallel-gs validation"); + int result = 0; + try + { + GS gs; + GSRegisters priv{}; + std::vector vram(4 * 1024 * 1024); + gs.init(vram.data(), uint32_t(vram.size()), &priv); + auto backend = CreateParallelGSBackend(priv); + auto *rawBackend = backend.get(); + gs.setRasterBackend(std::move(backend)); + priv.pmode = 3; + gs.writeRegister(0x59, 32); + gs.writeRegister(0x59, 32); // Repeated register write is not another flip. + gs.writeRegister(0x5b, 32); + check(priv.displayFlipCount[0].load() == 1 && priv.displayFlipCount[1].load() == 1, + "parallel backend display flips missing or duplicated"); + testPendingPresentation(gs, *rawBackend); + for (auto [psm, bits] : std::array, 13>{{{0, 32}, {1, 24}, {2, 16}, {10, 16}, {19, 8}, {20, 4}, {27, 8}, {36, 4}, {44, 4}, {48, 32}, {49, 24}, {50, 16}, {58, 16}}}) + testTransfer(gs, psm, bits); + testGIF(gs); + testRaster(gs); + check((priv.csr & 3) == 3 && priv.siglblid == (456ull << 32 | 123), "IRQ side effects incorrect"); + testPresentation(gs, priv); + gs.reset(); + testRaster(gs); + gs.shutdownBackend(); + std::cout << "Reset and shutdown passed\n"; + } + catch (const std::exception &e) + { + std::cerr << "GPU validation failed: " << e.what() << '\n'; + result = 1; + } + CloseWindow(); + return result; +} diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 463b6cff1..d8182dfb2 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -224,6 +224,8 @@ void register_code_generator_tests() const std::string generated = gen.generateFunction(func, {syscall, after}, false); const size_t continuation = generated.find("ctx->pc = 0x9004u;"); const size_t dispatch = generated.find("runtime->handleSyscall(rdram, ctx, 0x44u);"); + const size_t transfer = generated.find("if (runtime->eeTransferPending()) { return; }", dispatch); + t.IsTrue(transfer != std::string::npos, "syscalls must return before executing the continuation when suspended"); t.IsTrue(continuation != std::string::npos, "generated syscall must publish the next guest PC"); @@ -1020,6 +1022,25 @@ void register_code_generator_tests() t.IsTrue(ctc1Code.find("ignored") == std::string::npos, "CTC1 FCR31 should not be ignored"); }); + tc.Run("VU memory transfers synchronize the shared micro register state", [](TestCase &t) + { + CodeGenerator gen({}, {}); + for (const auto opcode : {OPCODE_LDC2, OPCODE_SDC2}) + { + Instruction instruction{}; + instruction.opcode = opcode; + instruction.rs = 18; + instruction.rt = 4; + instruction.raw = (uint32_t(opcode) << 26) | (18u << 21) | (4u << 16); + const auto code = gen.translateInstruction(instruction); + const auto before = code.find("runtime->beforeVu0Access(ctx,"); + const auto transfer = code.find("ctx->vu0_vf[4]"); + const auto after = code.find("runtime->afterVu0Access(ctx,"); + t.IsTrue(before != std::string::npos && before < transfer && transfer < after && after != std::string::npos, + "LQC2 and SQC2 must bracket the memory transfer with VU synchronization"); + } + }); + tc.Run("VU CFC2/CTC2 access VI registers directly", [](TestCase& t) { CodeGenerator gen({}, {}); @@ -1519,6 +1540,8 @@ void register_code_generator_tests() func, {makeJal(0xA040, 0xB040), makeNop(0xA044)}, false); const size_t continuation = generated.find("ctx->pc = 0xA048u;"); const size_t handler = generated.find("ps2_syscalls::GetThreadId(rdram, ctx, runtime);"); + t.IsTrue(generated.find("if (runtime->eeTransferPending() || ctx->pc != 0xA048u)", handler) != std::string::npos, + "HLE calls must test transfer before accessing a potentially deleted context"); t.IsTrue(continuation != std::string::npos, "a resolved HLE JAL should publish its fallthrough PC"); diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index 40a68b16b..90a7a0c33 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -11,6 +11,8 @@ void register_ps2_runtime_kernel_tests(); void register_ps2_runtime_interrupt_tests(); void register_ps2_memory_tests(); void register_ps2_vu1_tests(); +void register_ps2_vu_pipeline_tests(); + void register_ps2_vu_tests(); void register_ps2_gs_tests(); void register_ps2_iop_tests(); @@ -33,6 +35,8 @@ int main() register_ps2_runtime_interrupt_tests(); register_ps2_memory_tests(); register_ps2_vu1_tests(); + register_ps2_vu_pipeline_tests(); + register_ps2_vu_tests(); register_ps2_gs_tests(); register_ps2_iop_tests(); diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 0458a8cc8..a3f349971 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -503,7 +503,7 @@ void register_ps2_gs_tests() std::vector rdram(PS2_RAM_SIZE, 0u); R5900Context ctx{}; setRegU32(ctx, 4, 1u); // interlaced - setRegU32(ctx, 5, 0u); // NTSC + setRegU32(ctx, 5, 2u); // NTSC (GS CRT mode 0x02) setRegU32(ctx, 6, 0u); // field mode runtime.memory().gs().pmode = 0u; @@ -512,6 +512,10 @@ void register_ps2_gs_tests() t.Equals(runtime.memory().gs().smode2, 0x1ull, "GsSetCrt should publish interlaced field mode through SMODE2"); + t.Equals((runtime.memory().gs().smode1 >> 3) & 127ull, 32ull, + "GsSetCrt should program the analog clock for the hardware CRTC"); + t.Equals((runtime.memory().gs().smode1 >> 13) & 3ull, 2ull, + "GsSetCrt should program NTSC in SMODE1"); t.Equals(runtime.memory().gs().pmode & 0x3ull, 0x1ull, "GsSetCrt should leave CRT1 enabled for presentation"); t.Equals(getRegU32Test(ctx, 2), 0u, @@ -557,8 +561,9 @@ void register_ps2_gs_tests() std::memcpy(&xyoffset10Addr, rdram.data() + kEnvAddr + kXYOffset1AddrOffset, sizeof(xyoffset10Addr)); t.Equals((dispfb0 >> 9) & 0x3Fu, 10ull, "dbuff display env should seed FBW from width"); - t.Equals((display0 >> 32) & 0x0FFFull, 639ull, "dbuff display env should seed DW from width"); - t.Equals((display0 >> 44) & 0x07FFull, 447ull, "dbuff display env should seed DH from height"); + t.Equals((display0 >> 23) & 0x0Full, 3ull, "NTSC should use four output clocks per pixel"); + t.Equals((display0 >> 32) & 0x0FFFull, 2559ull, "DW is measured in output clocks, not framebuffer pixels"); + t.Equals((display0 >> 44) & 0x07FFull, 895ull, "interlaced frame mode should double the supplied height"); t.Equals((frame10 >> 16) & 0x3Full, 10ull, "dbuff draw env should seed FRAME FBW from width"); t.Equals(frame10Addr, 0x4Cull, "dbuff draw env should seed FRAME_1 register id"); t.Equals(xyoffset10 & 0xFFFFull, 0x6C00ull, "dbuff draw env should seed OFX in 12.4 fixed point"); @@ -572,6 +577,12 @@ void register_ps2_gs_tests() dispfb1 = (dispfb1 & ~0x1FFull) | 151ull; std::memcpy(rdram.data() + kEnvAddr + kDispEnvSize + kDispFbOffset, &dispfb1, sizeof(dispfb1)); + // Exercise both circuits so the SDK shortcut must update the same + // per-circuit counters as native GS and MMIO writes. + const uint64_t pmode = 3u; + std::memcpy(rdram.data() + kEnvAddr, &pmode, sizeof(pmode)); + std::memcpy(rdram.data() + kEnvAddr + kDispEnvSize, &pmode, sizeof(pmode)); + std::memset(&ctx, 0, sizeof(ctx)); setRegU32(ctx, 4, kEnvAddr); setRegU32(ctx, 5, 1u); @@ -579,8 +590,92 @@ void register_ps2_gs_tests() t.Equals(runtime.memory().gs().dispfb1 & 0x1FFull, 151ull, "sceGsSwapDBuffDc should program GS to the selected display page"); - t.Equals((runtime.memory().gs().display1 >> 32) & 0x0FFFull, 639ull, + t.Equals((runtime.memory().gs().display1 >> 32) & 0x0FFFull, 2559ull, "sceGsSwapDBuffDc should preserve the display width from the seeded env"); + auto ®s = runtime.memory().gs(); + t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "SDK swap must count the first display flip"); + t.Equals(regs.displayFlipCount[1].load(), uint64_t(1), "SDK swap must count the second circuit independently"); + ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); + t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "repeating the SDK swap must not add a flip"); + setRegU32(ctx, 5, 0u); + ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); + t.Equals(regs.displayFlipCount[0].load(), uint64_t(2), "SDK swap back to the other page must count"); + t.Equals(regs.displayFlipCount[1].load(), uint64_t(2), "SDK swap back must count for both circuits"); + t.Equals(regs.sdkPresentCount.load(), uint64_t(3), "each completed SDK swap counts once, not once per circuit"); + + // Retail games can alternate draw buffers while both display envs + // point at one scanout page. FBP changes cannot measure their FPS. + std::memcpy(rdram.data() + kEnvAddr + kDispEnvSize + kDispFbOffset, &dispfb0, sizeof(dispfb0)); + setRegU32(ctx, 5, 1u); + ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); + setRegU32(ctx, 5, 0u); + ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, &runtime); + t.Equals(regs.sdkPresentCount.load(), uint64_t(5), "fixed scanout page must still count SDK presentations"); + t.Equals(regs.displayFlipCount[0].load(), uint64_t(2), "SDK presentation must not fabricate a buffer flip"); + ps2_stubs::sceGsSwapDBuff(rdram.data(), &ctx, &runtime); + t.Equals(regs.sdkPresentCount.load(), uint64_t(6), "non-DC SDK swap also counts once"); + ps2_stubs::sceGsSwapDBuffDc(rdram.data(), &ctx, nullptr); + t.Equals(regs.sdkPresentCount.load(), uint64_t(6), "failed swap must not count as a presentation"); + }); + + tc.Run("sceGsSetDefDispEnv matches libgs display timing and initializes every register", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); + std::vector rdram(PS2_RAM_SIZE, 0u); + constexpr uint32_t envAddr = 0x4000u; + struct ModeCase + { + uint32_t mode, interlace, frame, width, height; + int32_t dx, dy; + uint64_t smode2, expectedDx, expectedDy, magh, dw, dh; + }; + // Expected fields from sceGsSetDefDispEnv at 0x100248 in SLUS_201.84. + const ModeCase cases[] = { + {2, 1, 1, 640, 224, 0, 0, 3, 636, 50, 3, 2559, 447}, + {2, 1, 0, 640, 448, 0, 0, 1, 636, 50, 3, 2559, 447}, + {2, 0, 0, 640, 224, 0, 0, 2, 636, 25, 3, 2559, 223}, + {3, 1, 1, 512, 256, 0, 0, 3, 656, 72, 4, 2559, 511}, + {3, 0, 1, 640, 256, 0, 0, 2, 656, 36, 3, 2559, 255}, + {2, 1, 1, 640, 224, -4, -2, 3, 620, 48, 3, 2559, 447}, + }; + for (const auto &mode : cases) + { + R5900Context ctx{}; + setRegU32(ctx, 4, 0u); + setRegU32(ctx, 5, mode.interlace); + setRegU32(ctx, 6, mode.mode); + setRegU32(ctx, 7, mode.frame); + ps2_stubs::sceGsResetGraph(rdram.data(), &ctx, &runtime); + ps2_stubs::sceGsGetGParam(rdram.data(), &ctx, &runtime); + const uint32_t params = getRegU32Test(ctx, 2); + t.Equals(runtime.memory().read16(params), static_cast(mode.interlace), "GParam interlace occupies a halfword"); + t.Equals(runtime.memory().read16(params + 2), static_cast(mode.mode), "GParam output mode is at offset 2"); + t.Equals(runtime.memory().read16(params + 4), static_cast(mode.frame), "GParam frame mode is at offset 4"); + t.Equals(runtime.memory().read16(params + 6), uint16_t(3), "GParam version is at offset 6"); + std::memset(rdram.data() + envAddr, 0xcd, 40); + setRegU32(ctx, 4, envAddr); + setRegU32(ctx, 5, 0u); + setRegU32(ctx, 6, mode.width); + setRegU32(ctx, 7, mode.height); + setRegU32(ctx, 8, static_cast(mode.dx)); + setRegU32(ctx, 9, static_cast(mode.dy)); + setRegU32(ctx, 29, 0x3000u); + const uint32_t staleStackArgs[2] = {123u, 456u}; + std::memcpy(rdram.data() + 0x3010, staleStackArgs, sizeof(staleStackArgs)); + ps2_stubs::sceGsSetDefDispEnv(rdram.data(), &ctx, &runtime); + uint64_t regs[5]{}; + std::memcpy(regs, rdram.data() + envAddr, sizeof(regs)); + t.Equals(regs[0], 0x66ull, "PMODE should enable circuit 2 with fixed alpha"); + t.Equals(regs[1], mode.smode2, "SMODE2 should follow the libgs mode"); + t.Equals(regs[2], uint64_t((mode.width + 63) / 64) << 9, "DISPFB should encode the framebuffer stride"); + t.Equals(regs[3] & 0xfffull, mode.expectedDx, "DX should include the CRT origin and scaled offset"); + t.Equals((regs[3] >> 12) & 0x7ffull, mode.expectedDy, "DY should include the CRT origin"); + t.Equals((regs[3] >> 23) & 15ull, mode.magh, "MAGH should match the output clock multiplier"); + t.Equals((regs[3] >> 32) & 0xfffull, mode.dw, "DW should cover the output clocks"); + t.Equals((regs[3] >> 44) & 0x7ffull, mode.dh, "DH should match field or frame mode"); + t.Equals(regs[4], 0ull, "BGCOLOR should be initialized even in dirty guest memory"); + } }); tc.Run("sceGsSetDefDBuffDc seeds a clear packet and swap clears the draw buffer", [](TestCase &t) @@ -4061,7 +4156,7 @@ void register_ps2_gs_tests() "TRXDIR payload must encode dir=0 (host-to-local)"); }); - tc.Run("sceGsResetGraph frees its temporary GIF packet", [](TestCase &t) + tc.Run("sceGsResetGraph programs the CRT without sending privileged registers through GIF", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "runtime memory initialize should succeed"); @@ -4076,8 +4171,21 @@ void register_ps2_gs_tests() t.Equals(static_cast(getRegU32Test(ctx, 2)), 0, "sceGsResetGraph should succeed in reset mode"); + const auto ®s = runtime.memory().gs(); + t.Equals((regs.smode1 >> 3) & 127ull, 32ull, "reset should select the analog clock"); + t.Equals((regs.smode1 >> 13) & 3ull, 2ull, "reset should select NTSC"); + t.Equals(regs.smode2, 3ull, "reset should select interlaced frame mode"); + t.Equals(regs.pmode & 3ull, 1ull, "reset should enable circuit 1"); + t.Equals(regs.dispfb1, 10ull << 9, "reset should set the display buffer width"); + t.Equals(regs.dispfb2, regs.dispfb1, "reset should initialize both display buffers"); + t.Equals(runtime.gs().getDebugSnapshot().ctx[1].alpha, 0ull, + "PMODE must not be sent to GIF register 0x41 (ALPHA_2)"); + + setRegU32(ctx, 6, 3u); + ps2_stubs::sceGsResetGraph(rdram.data(), &ctx, &runtime); + t.Equals((regs.smode1 >> 13) & 3ull, 3ull, "reset should also select PAL"); expectGuestHeapReusable(t, runtime, - "sceGsResetGraph should free its temporary GIF packet"); + "sceGsResetGraph should leave the guest heap reusable"); }); tc.Run("sceGsSyncV resumes through the scheduler with deterministic field parity", [](TestCase &t) diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 0b22fe03c..7ed5efc99 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -159,6 +159,40 @@ void register_ps2_memory_tests() { MiniTest::Case("PS2Memory", [](TestCase &tc) { + tc.Run("GS display flip counters track guest buffer changes independently of host VSync", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "memory initialization"); + auto ®s = mem.gs(); + mem.write64(0x12000000u, 3); // Enable both display circuits. + mem.write64(0x12000070u, 32 | (10ull << 9)); + mem.write32(0x12000090u, 32 | (10u << 9)); + t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "64-bit flip counted"); + t.Equals(regs.displayFlipCount[1].load(), uint64_t(1), "32-bit flip counted independently"); + mem.write32(0x12000074u, 1u << 11); // DBY/field offset only. + mem.write32(0x12000070u, 32 | (20u << 9)); // Same FBP, different width. + for (unsigned i = 0; i < 60; ++i) + { + ++regs.vsyncTick; + mem.write32(0x12000070u, 32 | (20u << 9)); + } + t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "repeats, field offsets and host VSync are not flips"); + mem.write64(0x12000000u, 0); + mem.write64(0x12000070u, 64); + t.Equals(regs.displayFlipCount[0].load(), uint64_t(1), "disabled display excluded"); + mem.write64(0x12000000u, 3); + GS gs; + gs.init(mem.getGSVRAM(), PS2_GS_VRAM_SIZE, ®s); + gs.writeRegister(0x59, 96); + gs.writeRegister(0x5b, 96); + t.Equals(regs.displayFlipCount[0].load(), uint64_t(2), "native GS register path counted"); + t.Equals(regs.displayFlipCount[1].load(), uint64_t(2), "second native circuit counted"); + gs.shutdownBackend(); + t.IsTrue(mem.initialize(), "memory reset"); + t.Equals(regs.displayFlipCount[0].load(), uint64_t(0), "reset clears counters"); + t.Equals(regs.displayFlipCount[1].load(), uint64_t(0), "reset clears both circuits"); + }); + tc.Run("uncached aliases map to same RDRAM bytes", [](TestCase &t) { PS2Memory mem; @@ -394,7 +428,6 @@ void register_ps2_memory_tests() packet.push_back(static_cast(i & 0xFFu)); } - std::memset(mem.getVU1Code(), 0, PS2_VU1_CODE_SIZE); mem.processVIF1Data(packet.data(), static_cast(packet.size())); const uint8_t *vu1Code = mem.getVU1Code(); @@ -2216,6 +2249,39 @@ void register_ps2_memory_tests() t.IsTrue(imageOk, "raw image continuation after packed setup should not be decoded as VIF/GIF registers"); }); + tc.Run("VU code writes are visible through aliases and MPG", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "memory initializes"); + for (bool vu1 : {false, true}) + { + const uint32_t base = vu1 ? PS2_VU1_CODE_BASE : PS2_VU0_CODE_BASE; + mem.write8(base | 0x80000000u, 1); + mem.write16(base + 2, 2); + mem.write32(base + 4, 3); + mem.write64(base + 8, 4); + t.Equals(mem.read8(base), uint8_t(1), "byte alias is visible"); + t.Equals(mem.read16(base + 2), uint16_t(2), "halfword is visible"); + t.Equals(mem.read32(base + 4), 3u, "word is visible"); + t.Equals(mem.read64(base + 8), uint64_t(4), "doubleword is visible"); + mem.write128(base, _mm_set1_epi32(5)); + t.Equals(mem.read32(base + 12), 5u, "quadword is visible"); + std::vector packet; + appendU32(packet, makeVifCmd(0x4a, 1, 0)); + appendU64(packet, 0x000002ff8000033cull); + for (uint32_t upload = 0; upload < 2; ++upload) + { + if (vu1) mem.processVIF1Data(packet.data(), static_cast(packet.size())); + else mem.processVIF0Data(packet.data(), static_cast(packet.size())); + t.Equals(mem.read64(base), uint64_t(0x000002ff8000033cull), "MPG code is visible after each upload"); + } + } + const std::array bytes{0, 0, 1, 2}; + t.IsTrue(mem.writeVuCode(true, 4094, bytes), "crossing upload changes code"); + t.IsFalse(mem.writeVuCode(true, 4094, bytes), "identical upload leaves bytes unchanged"); + t.Equals(mem.read16(PS2_VU1_CODE_BASE + 4096), uint16_t(0x0201), "crossing bytes are visible"); + }); + tc.Run("unaligned accesses throw", [](TestCase &t) { PS2Memory mem; diff --git a/ps2xTest/src/ps2_recompiler_tests.cpp b/ps2xTest/src/ps2_recompiler_tests.cpp index 86e5e3f9f..1883b7465 100644 --- a/ps2xTest/src/ps2_recompiler_tests.cpp +++ b/ps2xTest/src/ps2_recompiler_tests.cpp @@ -1280,6 +1280,25 @@ void register_ps2_recompiler_tests() } }); + tc.Run("retired VU catalogs require explicit migration", [](TestCase &t) { + const auto path = std::filesystem::temp_directory_path() / + ("ps2recomp-vu-migration-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".toml"); + { + std::ofstream file(path); + file << "[general]\ninput = 'dummy.elf'\noutput = 'out'\n[[vu1.programs]]\nformat = 'raw'\npath = 'code.bin'\n"; + } + bool diagnosed = false; + try { ConfigManager(path.string()).loadConfig(); } + catch (const std::exception &error) + { + const std::string message = error.what(); + diagnosed = message.find("retired") != std::string::npos && + message.find("interpreter") != std::string::npos; + } + std::filesystem::remove(path); + t.IsTrue(diagnosed, "old catalogs must fail with the interpreter migration instructions"); + }); + tc.Run("config manager parses jump_tables table entries", [](TestCase &t) { const auto uniqueSuffix = std::to_string( static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index 4772f3fb7..5df8a8d4c 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -9,6 +9,7 @@ #include "ps2_stubs.h" #include "runtime/gs/gs_frontend.h" #include "runtime/ee_scheduler.h" +#include "runtime/vu/vu_unit.h" #include "runtime/gs/ps2_gs_psmct32.h" #include "ps2_runtime_macros.h" #include "Stubs/MPEG.h" @@ -101,10 +102,9 @@ namespace 0x3Cu; } - void writeVuInstructionPair(uint8_t *code, uint32_t pc, uint32_t lower, uint32_t upper) + void writeVuInstructionPair(PS2Memory &memory, uint32_t pc, uint32_t lower, uint32_t upper) { - std::memcpy(code + pc, &lower, sizeof(lower)); - std::memcpy(code + pc + sizeof(lower), &upper, sizeof(upper)); + memory.write64(PS2_VU0_CODE_BASE + pc, uint64_t(lower) | (uint64_t(upper) << 32)); } uint64_t packVuInstructionPair(uint32_t lower, uint32_t upper) @@ -229,6 +229,10 @@ namespace setRegU32(*ctx, 5, kMpegWaitImage); ctx->pc = kMpegWaitResumePc; ps2_stubs::sceMpegGetPicture(rdram, ctx, runtime); + if (runtime->eeTransferPending()) + { + return; + } } void testMpegWaitResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) @@ -257,10 +261,18 @@ namespace setRegU32(*ctx, 4, kMpegNoDuplicateHandle); setRegU32(*ctx, 5, kMpegNoDuplicateImage); ps2_stubs::sceMpegGetPicture(rdram, ctx, runtime); + if (runtime->eeTransferPending()) + { + return; + } gMpegNoDuplicateStage.store(1u, std::memory_order_release); ctx->pc = kMpegNoDuplicateResumePc; ps2_stubs::sceMpegGetPicture(rdram, ctx, runtime); + if (runtime->eeTransferPending()) + { + return; + } gMpegNoDuplicateStage.store(4u, std::memory_order_release); ctx->pc = 0u; @@ -1131,15 +1143,173 @@ void register_ps2_runtime_expansion_tests() t.Equals(mem.vif1_regs.itop, 0x21u, "MSCNT should keep latching ITOP from ITOPS"); }); + tc.Run("VIF startup and FLUSHE synchronize the selected VU", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "memory initializes"); + t.IsTrue(runtime.syncCoreSubsystems(), "VU services bind"); + for (uint32_t unit = 0; unit < 2; ++unit) + { + const auto base = unit ? PS2_VU1_CODE_BASE : PS2_VU0_CODE_BASE; + runtime.memory().write64(base, packVuInstructionPair(makeVuIaddiu(1, 0, 7), 0x400002ff)); + runtime.memory().write64(base + 8, packVuInstructionPair(0x8000033c, 0x2ff)); + const uint32_t start[] = {0x04000011, 0x14000000}; + const uint32_t flush = 0x10000000; + auto &target = unit ? runtime.vu1() : runtime.vu0(); + if (unit) runtime.memory().processVIF1Data(reinterpret_cast(start), sizeof(start)); + else runtime.memory().processVIF0Data(reinterpret_cast(start), sizeof(start)); + t.IsTrue(target.status() == ps2vu::UnitStatus::Running, "MSCAL launches the selected unit"); + t.Equals(target.state().vi[1], int16_t(0), "VIF startup does not run instructions early"); + if (unit) runtime.memory().processVIF1Data(reinterpret_cast(&flush), sizeof(flush)); + else runtime.memory().processVIF0Data(reinterpret_cast(&flush), sizeof(flush)); + t.IsTrue(target.status() == ps2vu::UnitStatus::Ready, "FLUSHE waits for the selected unit to finish"); + t.Equals(target.state().vi[1], int16_t(7), "the microprogram executes before FLUSHE returns"); + t.Equals(target.state().itop, 17u, "VIF startup latches ITOPS into ITOP"); + } + }); + + tc.Run("VU runtime preserves asynchronous startup and scalar completion", [](TestCase &t) + { + PS2Memory memory; + t.IsTrue(memory.initialize(), "memory initializes"); + R5900Context context{}; + ps2vu::Runtime *active = nullptr; + ps2vu::Runtime runtime(memory, [&](uint32_t cycles) { active->advance(cycles, context); }); + active = &runtime; + memory.write64(PS2_VU0_CODE_BASE, + packVuInstructionPair(0x800003bcu | (1u << 11) | (2u << 16), 0x400002ff)); + memory.write64(PS2_VU0_CODE_BASE + 8, packVuInstructionPair(0x8000033c, 0x2ff)); + context.vu0_q = 1; + context.vu0_vf[1] = _mm_set1_ps(8); + context.vu0_vf[2] = _mm_set1_ps(2); + runtime.start0(context, 0); + t.IsTrue(runtime.unit(false).status() == ps2vu::UnitStatus::Running, "startup launches the unit"); + t.Equals(runtime.unit(false).state().cycles, uint64_t(0), "startup consumes no execution budget"); + runtime.advance(2, context); + t.IsTrue(runtime.unit(false).status() == ps2vu::UnitStatus::Ready, "E and its delay pair finish at cycle two"); + t.Equals(context.vu0_q, 1.0f, "Q remains pending after E"); + runtime.advance(4, context); + t.Equals(context.vu0_q, 1.0f, "idle cycles do not publish Q early"); + runtime.advance(1, context); + t.Equals(context.vu0_q, 4.0f, "Q is visible to EE at cycle seven"); + }); + + tc.Run("VU0 noninterlocked memory transfers share the active micro register state", [](TestCase &t) + { + PS2Memory memory; + t.IsTrue(memory.initialize(), "memory initializes"); + R5900Context context{}; + ps2vu::Runtime *active = nullptr; + ps2vu::Runtime runtime(memory, [&](uint32_t cycles) { active->advance(cycles, context); }); + active = &runtime; + const uint32_t add = (15u << 21) | (4u << 11) | (6u << 6) | 0x28u; + memory.write64(PS2_VU0_CODE_BASE, packVuInstructionPair(0x8000033c, add | 0x40000000u)); + memory.write64(PS2_VU0_CODE_BASE + 8, packVuInstructionPair(0x8000033c, 0x2ff)); + context.vu0_vf[4] = _mm_set1_ps(2); + runtime.start0(context, 0); + constexpr uint32_t load = (0x36u << 26) | (18u << 21) | (4u << 16); + runtime.beforeAccess(context, load); + t.Equals(runtime.unit(false).state().cycles, uint64_t(0), "LQC2 must not wait for the microprogram"); + context.vu0_vf[4] = _mm_set1_ps(7); + runtime.afterAccess(context, load); + t.Equals(runtime.unit(false).state().vf[4][0], 7.0f, "LQC2 updates the register seen by the running microprogram"); + runtime.advance(4, context); + t.Equals(_mm_cvtss_f32(context.vu0_vf[6]), 7.0f, "micro execution must retain the EE load"); + runtime.start0(context, 0); + constexpr uint32_t store = (0x3eu << 26) | (18u << 21) | (6u << 16); + const auto cycle = runtime.unit(false).state().cycles; + runtime.beforeAccess(context, store); + t.Equals(runtime.unit(false).state().cycles, cycle, "SQC2 must not wait for the microprogram"); + t.Equals(_mm_cvtss_f32(context.vu0_vf[6]), 7.0f, "SQC2 reads the current architectural register"); + runtime.afterAccess(context, store); + constexpr uint32_t loadZero = 0xd8000000u; + context.vu0_vf[0] = _mm_set1_ps(9); + runtime.afterAccess(context, loadZero); + t.Equals(_mm_cvtss_f32(context.vu0_vf[0]), 0.0f, "LQC2 cannot overwrite VF00"); + }); + + tc.Run("VU interpreter sees code writes between execution slices", [](TestCase &t) + { + PS2Memory memory; + t.IsTrue(memory.initialize(), "memory initializes"); + R5900Context context{}; + ps2vu::Runtime runtime(memory, [](uint32_t) {}); + for (bool vu1 : {false, true}) + { + const auto base = vu1 ? PS2_VU1_CODE_BASE : PS2_VU0_CODE_BASE; + memory.write64(base, packVuInstructionPair(makeVuIaddiu(1, 0, 3), 0x2ff)); + memory.write64(base + 8, packVuInstructionPair(makeVuIaddiu(2, 0, 5), 0x400002ff)); + memory.write64(base + 16, packVuInstructionPair(0x8000033c, 0x2ff)); + auto &unit = runtime.unit(vu1); + t.IsTrue(unit.start(0, 0, 0), "unit starts"); + auto exit = unit.advance(1); + t.Equals(exit.cycles, uint64_t(1), "budget stops after one pair"); + t.Equals(unit.state().pc, 8u, "next pair remains unexecuted"); + memory.write64((base + 8) | 0x80000000u, + packVuInstructionPair(makeVuIaddiu(2, 0, 9), 0x400002ff)); + exit = unit.advance(2); + t.Equals(exit.cycles, uint64_t(2), "end and delay pair consume two cycles"); + t.Equals(unit.state().vi[2], int32_t(9), "resume fetches the modified code"); + t.IsTrue(unit.status() == ps2vu::UnitStatus::Ready, "end returns the unit to ready"); + } + }); + + tc.Run("VU0 shared register loads observe VU1 at each issue cycle", [](TestCase &t) + { + PS2Memory memory; + t.IsTrue(memory.initialize(), "memory initializes"); + R5900Context context{}; + ps2vu::Runtime *active = nullptr; + ps2vu::Runtime runtime(memory, [&](uint32_t cycles) { active->advance(cycles, context); }); + active = &runtime; + for (uint32_t index = 0; index < 4; ++index) + { + memory.write64(PS2_VU1_CODE_BASE + index * 8, + packVuInstructionPair(makeVuIaddiu(1, 0, index + 1), index == 2 ? 0x400002ff : 0x2ff)); + const auto lower = index < 3 ? (4u << 25) | (8u << 21) | ((index + 1) << 16) | (5u << 11) : 0x8000033cu; + memory.write64(PS2_VU0_CODE_BASE + index * 8, + packVuInstructionPair(lower, index == 2 ? 0x400002ff : 0x2ff)); + } + context.vi[5] = 0x421; + runtime.start1(0, 0, 0, false); + runtime.start0(context, 0); + runtime.advance(10, context); + runtime.reportFailure(); + t.Equals(context.vi[1], uint16_t(0), "first load observes cycle zero"); + t.Equals(context.vi[2], uint16_t(1), "second load observes cycle one"); + t.Equals(context.vi[3], uint16_t(2), "third load observes cycle two"); + t.Equals(runtime.unit(true).state().vi[1], int16_t(4), "VU1 completes its remaining pairs"); + }); + + tc.Run("VU0 interlocked write releases on M without ending the microprogram", [](TestCase &t) + { + PS2Memory memory; + t.IsTrue(memory.initialize(), "memory initializes"); + R5900Context context{}; + ps2vu::Runtime *active = nullptr; + ps2vu::Runtime runtime(memory, [&](uint32_t cycles) { active->advance(cycles, context); }); + active = &runtime; + memory.write64(PS2_VU0_CODE_BASE, packVuInstructionPair(0x8000033c, 0x200002ff)); + memory.write64(PS2_VU0_CODE_BASE + 8, packVuInstructionPair(makeVuIaddiu(1, 0, 7), 0x400002ff)); + memory.write64(PS2_VU0_CODE_BASE + 16, packVuInstructionPair(0x8000033c, 0x2ff)); + runtime.start0(context, 0); + runtime.beforeAccess(context, 0x48a00801); + t.IsTrue(runtime.unit(false).status() == ps2vu::UnitStatus::Running, "M releases the transfer while VU0 runs"); + t.Equals(runtime.unit(false).state().cycles, uint64_t(1), "interlock stops at the M pair boundary"); + t.Equals(context.vi[1], uint16_t(0), "later instruction has not run"); + context.vu0_vf[1] = _mm_set1_ps(3); + runtime.afterAccess(context, 0x48a00801); + runtime.advance(2, context); + t.Equals(context.vi[1], uint16_t(7), "execution continues after the transfer"); + }); + tc.Run("VU0 microprogram executes against VU0 code and data memory", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed"); t.IsTrue(runtime.syncCoreSubsystems(), "runtime core subsystems should bind"); - uint8_t *const code = runtime.memory().getVU0Code(); uint8_t *const data = runtime.memory().getVU0Data(); - std::memset(code, 0, PS2_VU0_CODE_SIZE); std::memset(data, 0, PS2_VU0_DATA_SIZE); const float input[4] = {1.0f, 2.0f, 3.0f, 4.0f}; @@ -1147,12 +1317,13 @@ void register_ps2_runtime_expansion_tests() constexpr uint32_t kVuNop = 0x0000003Fu; constexpr uint32_t kVuEndNop = 0x4000003Fu; - writeVuInstructionPair(code, 0u, makeVuLq(0xFu, 1u, 0u, 0), kVuNop); - writeVuInstructionPair(code, 8u, 0u, makeVuAdd(0xFu, 2u, 1u, 1u)); - writeVuInstructionPair(code, 16u, makeVuSq(0xFu, 2u, 0u, 1), kVuEndNop); + writeVuInstructionPair(runtime.memory(), 0u, makeVuLq(0xFu, 1u, 0u, 0), kVuNop); + writeVuInstructionPair(runtime.memory(), 8u, 0u, makeVuAdd(0xFu, 2u, 1u, 1u)); + writeVuInstructionPair(runtime.memory(), 16u, makeVuSq(0xFu, 2u, 0u, 1), kVuEndNop); R5900Context ctx{}; runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u); + runtime.beforeVu0Access(&ctx, 0x48201001u); float output[4]{}; std::memcpy(output, data + 16u, sizeof(output)); @@ -1173,15 +1344,13 @@ void register_ps2_runtime_expansion_tests() t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed"); t.IsTrue(runtime.syncCoreSubsystems(), "runtime core subsystems should bind"); - uint8_t *const code = runtime.memory().getVU0Code(); - std::memset(code, 0, PS2_VU0_CODE_SIZE); constexpr uint32_t kVuUpperNop = 0x000002FFu; constexpr uint32_t kVuUpperEndNop = 0x400002FFu; writeVuInstructionPair( - code, 0u, + runtime.memory(), 0u, makeVuLowerSpecial(0x40u, 0u, 1u, 0x8u), kVuUpperEndNop); // RNEXT.x vf1 - writeVuInstructionPair(code, 8u, 0u, kVuUpperNop); + writeVuInstructionPair(runtime.memory(), 8u, 0u, kVuUpperNop); constexpr uint32_t seed = 0x3FC00000u; const uint32_t x = (seed >> 4) & 1u; @@ -1192,6 +1361,7 @@ void register_ps2_runtime_expansion_tests() ctx.vu0_r = _mm_castsi128_ps( _mm_set1_epi32(static_cast(seed))); runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u); + runtime.beforeVu0Access(&ctx, 0x48200801u); alignas(16) uint32_t rWords[4]{}; _mm_storeu_si128(reinterpret_cast<__m128i *>(rWords), @@ -1204,7 +1374,7 @@ void register_ps2_runtime_expansion_tests() t.Equals(vf1Words[0], expected, "RNEXT should expose the same R value through VF1.x"); }); - tc.Run("VU0 direct MicroMem writes invalidate the fixed decode cache", [](TestCase &t) + tc.Run("VU0 direct MicroMem writes invalidate native blocks", [](TestCase &t) { PS2Runtime runtime; t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed"); @@ -1221,6 +1391,7 @@ void register_ps2_runtime_expansion_tests() R5900Context first{}; runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &first, 0u); + runtime.beforeVu0Access(&first, 0x48400801u); t.Equals(static_cast(first.vi[1]), 1u, "first cached VU0 microprogram should execute"); @@ -1229,6 +1400,7 @@ void register_ps2_runtime_expansion_tests() packVuInstructionPair(makeVuIaddiu(1u, 0u, 2), kVuUpperEndNop)); R5900Context second{}; runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &second, 0u); + runtime.beforeVu0Access(&second, 0x48400801u); t.Equals(static_cast(second.vi[1]), 2u, "VU0 cache should rebuild after a direct MicroMem write"); }); @@ -1239,19 +1411,18 @@ void register_ps2_runtime_expansion_tests() t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed"); t.IsTrue(runtime.syncCoreSubsystems(), "runtime core subsystems should bind"); - uint8_t *const code = runtime.memory().getVU0Code(); - std::memset(code, 0, PS2_VU0_CODE_SIZE); constexpr uint32_t kVuUpperNop = 0x000002FFu; writeVuInstructionPair( - code, 0u, makeVuIaddiu(1u, 0u, 7), + runtime.memory(), 0u, makeVuIaddiu(1u, 0u, 7), kVuUpperNop | 0x08000000u); writeVuInstructionPair( - code, 8u, makeVuIaddiu(2u, 0u, 9), + runtime.memory(), 8u, makeVuIaddiu(2u, 0u, 9), kVuUpperNop); R5900Context ctx{}; ctx.vu0_fbrst = 1u << 3; // TE0 runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u); + runtime.beforeVu0Access(&ctx, 0x48400801u); t.Equals(static_cast(ctx.vi[1]), 7u, "the T-marked instruction should execute"); diff --git a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp index 4261bface..19050ab31 100644 --- a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp +++ b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp @@ -715,20 +715,26 @@ void register_ps2_runtime_interrupt_tests() mainContext.pc = kIdleVSyncWaitPc; std::atomic schedulerDone{false}; std::atomic schedulerThrew{false}; + std::atomic schedulerStarted{false}; std::thread gameThread([&]() { try { env.runtime.eeScheduler().reset(env.rdram.data(), mainContext); + schedulerStarted.store(true, std::memory_order_release); + schedulerStarted.notify_one(); env.runtime.eeScheduler().run(); } catch (...) { schedulerThrew.store(true, std::memory_order_release); } + schedulerStarted.store(true, std::memory_order_release); + schedulerStarted.notify_one(); schedulerDone.store(true, std::memory_order_release); }); + schedulerStarted.wait(false, std::memory_order_acquire); const bool becameIdle = waitUntil([&]() { const EeKernelSnapshot snapshot = env.runtime.eeScheduler().snapshot(); return snapshot.runningThreadId == 0 && diff --git a/ps2xTest/src/ps2_runtime_kernel_tests.cpp b/ps2xTest/src/ps2_runtime_kernel_tests.cpp index 63b12d927..f8873b686 100644 --- a/ps2xTest/src/ps2_runtime_kernel_tests.cpp +++ b/ps2xTest/src/ps2_runtime_kernel_tests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -132,6 +133,10 @@ namespace void overrideRecursiveFindAddressHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { runtime->handleSyscall(rdram, ctx, 0x83u); + if (runtime->eeTransferPending()) + { + return; + } ctx->pc = ::getRegU32(ctx, 31); } @@ -443,6 +448,10 @@ namespace gSchedulerTrace->push_back(31); setRegU32(*ctx, 4, static_cast(gSchedulerSemaphoreId)); SignalSema(rdram, ctx, runtime); + if (runtime->eeTransferPending()) + { + return; + } ctx->pc = 0u; runtime->requestStop(); } @@ -471,6 +480,10 @@ namespace ctx->pc = K_EVENT_FIFO_A_RESUME; runtime->eeScheduler().waitEventFlag(gEventFifoId, 1u, WEF_OR | WEF_CLEAR, K_EVENT_FIFO_RESULT_A); + if (runtime->eeTransferPending()) + { + return; + } } gEventFifoTrace.push_back(4); ctx->pc = 0u; @@ -484,6 +497,10 @@ namespace ctx->pc = K_EVENT_FIFO_B_RESUME; runtime->eeScheduler().waitEventFlag(gEventFifoId, 1u, WEF_OR | WEF_CLEAR, K_EVENT_FIFO_RESULT_B); + if (runtime->eeTransferPending()) + { + return; + } } gEventFifoTrace.push_back(6); ctx->pc = 0u; @@ -549,6 +566,174 @@ void register_ps2_runtime_kernel_tests() { MiniTest::Case("PS2RuntimeKernel", [](TestCase &tc) { + tc.Run("nested calls yield even when the blocked PC equals both return addresses", [](TestCase &t) + { + TestEnv env; + auto &ee = env.runtime.eeScheduler(); + env.ctx.pc = 0x302000u; + ee.reset(env.rdram.data(), env.ctx); + ee.bindMainContextForSyscall(env.ctx, env.rdram.data()); + env.runtime.registerFunction(0x302010u, [](uint8_t *ram, R5900Context *ctx, PS2Runtime *runtime) + { + if (!runtime->dispatchGuestBranch(ram, ctx, 0x302020u, 0x302010u, 0x302030u, + PS2Runtime::GuestBranchKind::DirectCall, "transfer regression")) return; + SET_GPR_U32(ctx, 8, 0xBADu); + }); + env.runtime.registerFunction(0x302020u, [](uint8_t *ram, R5900Context *ctx, PS2Runtime *runtime) + { + ctx->pc = 0x302030u; + SleepThread(ram, ctx, runtime); + if (runtime->eeTransferPending()) + { + return; + } + SET_GPR_U32(ctx, 9, 0xBADu); + }); + env.runtime.registerFunction(0x302030u, [](uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + SET_GPR_U32(ctx, 10, runtime->eeTransferPending() ? 0xBADu : 1u); + ctx->pc = 0u; + runtime->requestStop(); + }); + auto *context = ee.currentContext(); + const bool continued = env.runtime.dispatchGuestBranch(env.rdram.data(), context, 0x302010u, + 0x302000u, 0x302030u, PS2Runtime::GuestBranchKind::DirectCall, "transfer regression"); + t.IsTrue(!continued && ee.transferPending(), "yield must propagate across all guest callers"); + t.IsTrue(ee.thread(1)->status == EeThreadStatus::Waiting, "the thread must remain blocked"); + t.Equals(::getRegU32(context, 8), 0u, "the caller must not execute after a nested yield"); + t.Equals(::getRegU32(context, 9), 0u, "the callee must not execute after a blocking syscall"); + ee.wakeupThread(1, false); + ee.run(); + t.Equals(::getRegU32(&ee.thread(1)->context, 10), 1u, "a new dispatch must clear the old transfer"); + }); + + tc.Run("checkpoint yields propagate through a matching caller continuation", [](TestCase &t) + { + TestEnv env; + auto &ee = env.runtime.eeScheduler(); + ee.reset(env.rdram.data(), env.ctx); + ee.bindMainContextForSyscall(env.ctx, env.rdram.data()); + env.runtime.registerFunction(0x302080u, [](uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + ctx->pc = 0x302084u; + runtime->postEeEvent({EeEventType::VBlankStart, 0u, 0u}); + if (runtime->eeCheckpointDue(1u)) return; + SET_GPR_U32(ctx, 8, 0xBADu); + }); + const bool continued = env.runtime.dispatchGuestBranch(env.rdram.data(), ee.currentContext(), + 0x302080u, 0x302000u, 0x302084u, PS2Runtime::GuestBranchKind::DirectCall, "checkpoint regression"); + t.IsTrue(!continued && ee.transferPending(), "a checkpoint must reach the outer dispatcher"); + t.Equals(::getRegU32(ee.currentContext(), 8), 0u, "guest work after the checkpoint must remain pending"); + }); + + tc.Run("thread deletion unwinds dispatch without reading the deleted context", [](TestCase &t) + { + TestEnv env; + auto &ee = env.runtime.eeScheduler(); + ee.reset(env.rdram.data(), env.ctx); + env.runtime.registerFunction(0x302040u, [](uint8_t *ram, R5900Context *ctx, PS2Runtime *runtime) + { + ExitDeleteThread(ram, ctx, runtime); + }); + env.runtime.registerFunction(0x302048u, [](uint8_t *ram, R5900Context *ctx, PS2Runtime *runtime) + { + const bool continued = runtime->dispatchGuestBranch(ram, ctx, 0x302040u, + 0x302048u, 0x30204Cu, PS2Runtime::GuestBranchKind::DirectCall, "transfer regression"); + ram[0x1900u] = continued ? 1u : 0u; + ram[0x1901u] = runtime->eeTransferPending() ? 1u : 0u; + runtime->requestStop(); + }); + const int worker = ee.createThread(EeThreadCreateParams{0, 0x302048u, 0x20000u, 0x800u, 0, 5, 0}); + ee.startThread(worker, 0u, env.ctx, false); + ee.run(); + t.Equals(env.rdram[0x1900u], uint8_t{0}, "deletion must transfer out of every caller"); + t.Equals(env.rdram[0x1901u], uint8_t{1}, "the deletion transfer must remain pending"); + t.IsTrue(ee.thread(worker) == nullptr, "ExitDeleteThread must remove the owning context"); + }); + + tc.Run("available semaphore and accumulated wakeup do not yield", [](TestCase &t) + { + TestEnv env; + auto &ee = env.runtime.eeScheduler(); + ee.reset(env.rdram.data(), env.ctx); + ee.wakeupThread(1, false); + ee.bindMainContextForSyscall(env.ctx, env.rdram.data()); + const int semaphore = ee.createSemaphore(1, 1, 0u, 0u); + ee.waitSemaphore(semaphore); + t.IsTrue(!ee.transferPending(), "an immediately available token must continue inline"); + ee.sleepCurrent(); + t.IsTrue(!ee.transferPending(), "a pending wakeup must satisfy sleep without a transfer"); + t.Equals(ee.currentThreadId(), 1, "nonblocking calls must retain the running thread"); + }); + + tc.Run("a resumed host completion can schedule a guest callback before its parent continues", [](TestCase &t) + { + TestEnv env; + auto &ee = env.runtime.eeScheduler(); + env.ctx.pc = 0x302060u; + env.runtime.registerFunction(0x302060u, [](uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + ctx->pc = 0x302064u; + runtime->eeScheduler().waitExternal(EeWaitReason::Mpeg, 1u, 123u, + [runtime](R5900Context &parent) + { + GuestInvocation callback{}; + callback.kind = GuestInvocationKind::HleCall; + callback.context = parent; + callback.context.pc = 0x302068u; + callback.onComplete = [](const R5900Context &result, R5900Context &owner) + { + SET_GPR_U32(&owner, 2, ::getRegU32(&result, 2)); + }; + runtime->eeScheduler().invokeCurrent(std::move(callback)); + }); + }); + env.runtime.registerFunction(0x302064u, [](uint8_t *ram, R5900Context *ctx, PS2Runtime *runtime) + { + ram[0x1900u] = static_cast(::getRegU32(ctx, 2)); + ram[0x1901u] = runtime->eeTransferPending() ? 1u : 0u; + ctx->pc = 0u; + runtime->requestStop(); + }); + env.runtime.registerFunction(0x302068u, [](uint8_t *, R5900Context *ctx, PS2Runtime *) + { + SET_GPR_U32(ctx, 2, 42u); + ctx->pc = 0u; + }); + env.runtime.registerFunction(0x30206Cu, [](uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + ctx->pc = 0u; + runtime->eeScheduler().completeExternalWait(1u, 123u, 0); + }); + ee.reset(env.rdram.data(), env.ctx); + const int worker = ee.createThread(EeThreadCreateParams{0, 0x30206Cu, 0x20000u, 0x800u, 0, 5, 0}); + ee.startThread(worker, 0u, env.ctx, false); + ee.run(); + t.Equals(env.rdram[0x1900u], uint8_t{42}, "the parent must observe the completed callback result"); + t.Equals(env.rdram[0x1901u], uint8_t{0}, "the callback transfer must be cleared at the next dispatch"); + t.IsTrue(ee.thread(1)->invocations.empty(), "the callback frame must be removed exactly once"); + }); + + tc.Run("real guest failures still propagate and release host scopes", [](TestCase &t) + { + TestEnv env; + env.ctx.pc = 0x302050u; + gGuestActive.store(0); + env.runtime.registerFunction(env.ctx.pc, [](uint8_t *, R5900Context *, PS2Runtime *runtime) + { + GuestExecutionProbe probe(runtime); + throw std::runtime_error("guest failure probe"); + }); + auto &ee = env.runtime.eeScheduler(); + ee.reset(env.rdram.data(), env.ctx); + bool caught = false; + try { ee.run(); } + catch (const std::runtime_error &error) { caught = std::string(error.what()) == "guest failure probe"; } + t.IsTrue(caught, "actual failures must not be swallowed by cooperative dispatch"); + t.Equals(gGuestActive.load(), 0, "host scope destructors must still run"); + t.IsTrue(!ee.isExecutingGuest(), "an exception must clear the executing state"); + }); + tc.Run("unsigned loads and ABI word writes extend independently", [](TestCase &t) { constexpr uint64_t kUpper = 0x1122334455667788ull; @@ -702,16 +887,8 @@ void register_ps2_runtime_kernel_tests() t.Equals(ee.deleteThread(id, ownedStack), KE_OK, "a dormant thread record should be deletable"); t.IsTrue(ee.thread(id) == nullptr, "deleted thread must leave every scheduler collection"); - bool slept = false; - try - { - ee.sleepCurrent(); - } - catch (const EeDispatcherTransfer &) - { - slept = true; - } - t.IsTrue(slept && ee.thread(EeScheduler::kMainThreadId)->status == EeThreadStatus::Waiting, + ee.sleepCurrent(); + t.IsTrue(ee.transferPending() && ee.thread(EeScheduler::kMainThreadId)->status == EeThreadStatus::Waiting, "SleepThread should transfer the running context into a typed wait"); t.Equals(ee.suspendThread(EeScheduler::kMainThreadId, false), KE_OK, "suspending a waiter should produce WAIT-SUSPEND"); @@ -735,13 +912,7 @@ void register_ps2_runtime_kernel_tests() ee.reset(env.rdram.data(), env.ctx); ee.bindMainContextForSyscall(env.ctx, env.rdram.data()); semaId = ee.createSemaphore(0, 1, 0u, 0u); - try - { - ee.waitSemaphore(semaId); - } - catch (const EeDispatcherTransfer &) - { - } + ee.waitSemaphore(semaId); }; TestEnv signaled; diff --git a/ps2xTest/src/ps2_vu1_tests.cpp b/ps2xTest/src/ps2_vu1_tests.cpp index 0754b69fc..3b4352511 100644 --- a/ps2xTest/src/ps2_vu1_tests.cpp +++ b/ps2xTest/src/ps2_vu1_tests.cpp @@ -3,12 +3,15 @@ #include "runtime/gs/gs_frontend.h" #include "runtime/gs/ps2_gs_psmct32.h" #include "runtime/ps2_memory.h" -#include "runtime/ps2_vu1.h" +#include "runtime/vu/interpreter/vu_executor.h" +#include #include #include #include #include +#include +#include #include namespace @@ -19,7 +22,7 @@ namespace { PS2Memory mem; GS gs; - uint8_t *code = nullptr; + const uint8_t *code = nullptr; uint8_t *data = nullptr; bool initialize() @@ -29,7 +32,6 @@ namespace gs.init(mem.getGSVRAM(), static_cast(PS2_GS_VRAM_SIZE), &mem.gs()); code = mem.getVU1Code(); data = mem.getVU1Data(); - std::memset(code, 0, PS2_VU1_CODE_SIZE); std::memset(data, 0, PS2_VU1_DATA_SIZE); return code != nullptr && data != nullptr; } @@ -171,10 +173,9 @@ namespace return makeVuLowerSpecial(0x39u, 0u, ft, 0u, static_cast((ftf & 0x3u) << 2)); } - void writeVuInstructionPair(uint8_t *code, uint32_t pc, uint32_t lower, uint32_t upper) + void writeVuInstructionPair(PS2Memory &memory, uint32_t pc, uint32_t lower, uint32_t upper) { - std::memcpy(code + pc, &lower, sizeof(lower)); - std::memcpy(code + pc + sizeof(lower), &upper, sizeof(upper)); + memory.write64(PS2_VU1_CODE_BASE + pc, uint64_t(lower) | (uint64_t(upper) << 32)); } uint64_t packVuInstructionPair(uint32_t lower, uint32_t upper) @@ -222,9 +223,9 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, 0u, makeVuUpper(0x28u, 0xAu, 2u, 1u, 3u)); // ADD.xz vf3, vf1, vf2 + writeVuInstructionPair(fx.mem, 0u, 0u, makeVuUpper(0x28u, 0xAu, 2u, 1u, 3u)); // ADD.xz vf3, vf1, vf2 - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[1][1] = 2.0f; vu1.state().vf[1][2] = 3.0f; @@ -259,9 +260,9 @@ void register_ps2_vu1_tests() uint32_t lowerImmediate = 0u; std::memcpy(&lowerImmediate, &newI, sizeof(newI)); const uint32_t upperAddiWithIBit = makeVuUpper(0x22u, 0xFu, 0u, 1u, 2u) | 0x80000000u; // ADDi.xyzw vf2, vf1 - writeVuInstructionPair(fx.code, 0u, lowerImmediate, upperAddiWithIBit); + writeVuInstructionPair(fx.mem, 0u, lowerImmediate, upperAddiWithIBit); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().i = 2.0f; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[1][1] = 2.0f; @@ -289,10 +290,10 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, 0u, + fx.mem, 0u, 0u, makeVuUpperSpecial(0x10u, 0xFu, 2u, 1u)); - VU1Interpreter vu1; + VUExecutor vu1; const int32_t raw[4] = {1, -16, 4096, -32768}; std::memcpy(vu1.state().vf[1], raw, sizeof(raw)); vu1.execute(fx.code, PS2_VU1_CODE_SIZE, @@ -317,7 +318,7 @@ void register_ps2_vu1_tests() for (uint32_t component = 0; component < 4u; ++component) { writeVuInstructionPair( - fx.code, component * 8u, + fx.mem, component * 8u, makeVuLowerSpecial(0x3Cu, 1u, static_cast(component + 2u), 0u, @@ -325,7 +326,7 @@ void register_ps2_vu1_tests() kVuUpperNop); } - VU1Interpreter vu1; + VUExecutor vu1; const uint32_t raw[4] = {0x00001111u, 0x00002222u, 0x00003333u, 0x00004444u}; std::memcpy(vu1.state().vf[1], raw, sizeof(raw)); vu1.execute(fx.code, PS2_VU1_CODE_SIZE, @@ -351,10 +352,10 @@ void register_ps2_vu1_tests() const float destQw[4] = {-1.0f, -2.0f, -3.0f, -4.0f}; writeVuQword(fx.data, 3u, sourceQw); writeVuQword(fx.data, 5u, destQw); - writeVuInstructionPair(fx.code, 0u, makeVuLq(0x5u, 4u, 1u, 1), kVuUpperNop); // LQ.yw vf4, 1(vi1) - writeVuInstructionPair(fx.code, 8u, makeVuSq(0xAu, 4u, 2u, 1), kVuUpperNop); // SQ.xz vf4, 1(vi2) + writeVuInstructionPair(fx.mem, 0u, makeVuLq(0x5u, 4u, 1u, 1), kVuUpperNop); // LQ.yw vf4, 1(vi1) + writeVuInstructionPair(fx.mem, 8u, makeVuSq(0xAu, 4u, 2u, 1), kVuUpperNop); // SQ.xz vf4, 1(vi2) - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = 2; vu1.state().vi[2] = 4; vu1.state().vf[4][0] = 100.0f; @@ -388,11 +389,11 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, makeVuIaddiu(2u, 1u, 5), kVuUpperNop); // IADDIU vi2, vi1, 5 - writeVuInstructionPair(fx.code, 8u, makeVuIaddiu(0u, 2u, 7), kVuUpperNop); // IADDIU vi0, vi2, 7 - writeVuInstructionPair(fx.code, 16u, makeVuLowerDirect(0x30u, 2u, 1u, 3u), kVuUpperNop); // IADD vi3, vi2, vi1 + writeVuInstructionPair(fx.mem, 0u, makeVuIaddiu(2u, 1u, 5), kVuUpperNop); // IADDIU vi2, vi1, 5 + writeVuInstructionPair(fx.mem, 8u, makeVuIaddiu(0u, 2u, 7), kVuUpperNop); // IADDIU vi0, vi2, 7 + writeVuInstructionPair(fx.mem, 16u, makeVuLowerDirect(0x30u, 2u, 1u, 3u), kVuUpperNop); // IADD vi3, vi2, vi1 - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[0] = 99; vu1.state().vi[1] = 10; @@ -408,10 +409,10 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, makeVuLowerSpecial(0x68u, 0u, 2u), kVuUpperNop); // XTOP vi2 - writeVuInstructionPair(fx.code, 8u, makeVuLowerSpecial(0x69u, 0u, 3u), kVuUpperNop); // XITOP vi3 + writeVuInstructionPair(fx.mem, 0u, makeVuLowerSpecial(0x68u, 0u, 2u), kVuUpperNop); // XTOP vi2 + writeVuInstructionPair(fx.mem, 8u, makeVuLowerSpecial(0x69u, 0u, 3u), kVuUpperNop); // XITOP vi3 - VU1Interpreter vu1; + VUExecutor vu1; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0x123u, 0x2ABu, 2u); t.Equals(vu1.state().vi[2], 0x123, "XTOP should move TOP into the target VI register"); @@ -423,12 +424,12 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, makeVuBranch(2), kVuUpperNop); // target pc = 24 - writeVuInstructionPair(fx.code, 8u, makeVuIaddiu(1u, 0u, 1), kVuUpperNop); // delay slot - writeVuInstructionPair(fx.code, 16u, makeVuIaddiu(2u, 0u, 99), kVuUpperNop); // skipped - writeVuInstructionPair(fx.code, 24u, makeVuIaddiu(3u, 0u, 7), kVuUpperNop); // branch target + writeVuInstructionPair(fx.mem, 0u, makeVuBranch(2), kVuUpperNop); // target pc = 24 + writeVuInstructionPair(fx.mem, 8u, makeVuIaddiu(1u, 0u, 1), kVuUpperNop); // delay slot + writeVuInstructionPair(fx.mem, 16u, makeVuIaddiu(2u, 0u, 99), kVuUpperNop); // skipped + writeVuInstructionPair(fx.mem, 24u, makeVuIaddiu(3u, 0u, 7), kVuUpperNop); // branch target - VU1Interpreter vu1; + VUExecutor vu1; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 3u); t.Equals(vu1.state().vi[1], 1, "branch delay slot should execute"); @@ -441,12 +442,12 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, + writeVuInstructionPair(fx.mem, 0u, makeVuSq(0xFu, 1u, 1u, 0), // SQ.xyzw vf1, 0(vi1) makeVuUpper(0x28u, 0xFu, 3u, 2u, 1u)); // ADD.xyzw vf1, vf2, vf3 - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = 6; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[1][1] = 2.0f; @@ -485,12 +486,12 @@ void register_ps2_vu1_tests() const float loaded[4] = {90.0f, 91.0f, 92.0f, 93.0f}; writeVuQword(fx.data, 5u, loaded); writeVuInstructionPair( - fx.code, + fx.mem, 0u, makeVuLowerSpecial(0x34u, 1u, 1u, 0u, 0xFu), makeVuUpper(0x28u, 0x8u, 3u, 2u, 1u)); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = 5; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[1][1] = 2.0f; @@ -528,11 +529,11 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, + fx.mem, 0u, makeVuLowerSpecial(0x30u, 1u, 2u, 0u, 0x8u), makeVuUpper(0x28u, 0x8u, 3u, 2u, 1u)); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 10.0f; vu1.state().vf[2][0] = 20.0f; vu1.state().vf[3][0] = 1.0f; @@ -555,13 +556,13 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, 0u, + fx.mem, 0u, 0u, makeVuUpper(0x28u, 0x8u, 2u, 1u, 3u)); writeVuInstructionPair( - fx.code, 8u, 0u, + fx.mem, 8u, 0u, makeVuUpper(0x28u, 0x8u, 2u, 3u, 4u)); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[2][0] = 2.0f; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, @@ -585,13 +586,13 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, 0u, + fx.mem, 0u, 0u, makeVuUpperSpecial(0x28u, 0x8u, 2u, 1u)); // ADDA.x acc, vf1, vf2 writeVuInstructionPair( - fx.code, 8u, 0u, + fx.mem, 8u, 0u, makeVuUpper(0x29u, 0x8u, 4u, 3u, 5u)); // MADD.x vf5, vf3, vf4 - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[2][0] = 10.0f; vu1.state().vf[3][0] = 2.0f; @@ -616,11 +617,11 @@ void register_ps2_vu1_tests() const uint32_t source[4] = {0x1234u, 0u, 0u, 0u}; std::memcpy(fx.data + 2u * 16u, source, sizeof(source)); writeVuInstructionPair( - fx.code, 0u, makeVuIlw(0x8u, 2u, 1u, 0), kVuUpperNop); + fx.mem, 0u, makeVuIlw(0x8u, 2u, 1u, 0), kVuUpperNop); writeVuInstructionPair( - fx.code, 8u, makeVuIaddiu(3u, 2u, 1), kVuUpperNop); + fx.mem, 8u, makeVuIaddiu(3u, 2u, 1), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = 2; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, @@ -639,12 +640,12 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, makeVuDiv(1u, 2u, 1u, 2u), kVuUpperNop); // Q = vf1.y / vf2.z - writeVuInstructionPair(fx.code, 8u, makeVuLowerSpecial(0x3Bu, 0u), kVuUpperNop); // WAITQ - writeVuInstructionPair(fx.code, 16u, makeVuSqrt(3u, 3u), kVuUpperNop); // Q = sqrt(abs(vf3.w)) - writeVuInstructionPair(fx.code, 24u, makeVuLowerSpecial(0x3Bu, 0u), kVuUpperNop); // WAITQ + writeVuInstructionPair(fx.mem, 0u, makeVuDiv(1u, 2u, 1u, 2u), kVuUpperNop); // Q = vf1.y / vf2.z + writeVuInstructionPair(fx.mem, 8u, makeVuLowerSpecial(0x3Bu, 0u), kVuUpperNop); // WAITQ + writeVuInstructionPair(fx.mem, 16u, makeVuSqrt(3u, 3u), kVuUpperNop); // Q = sqrt(abs(vf3.w)) + writeVuInstructionPair(fx.mem, 24u, makeVuLowerSpecial(0x3Bu, 0u), kVuUpperNop); // WAITQ - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][1] = 18.0f; vu1.state().vf[2][2] = 3.0f; vu1.state().vf[3][3] = 25.0f; @@ -664,19 +665,19 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, + fx.mem, 0u, makeVuDiv(1u, 2u, 0u, 0u), kVuUpperNop); // Q = vf1.x / vf2.x writeVuInstructionPair( - fx.code, 8u, + fx.mem, 8u, makeVuSqrt(3u, 0u), kVuUpperNop); // Must wait for the shared FDIV unit. writeVuInstructionPair( - fx.code, 16u, + fx.mem, 16u, makeVuLowerSpecial(0x3Bu, 0u), kVuUpperNop); // WAITQ - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 18.0f; vu1.state().vf[2][0] = 3.0f; vu1.state().vf[3][0] = 25.0f; @@ -696,13 +697,13 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, makeVuDiv(1u, 2u, 0u, 0u), + fx.mem, 0u, makeVuDiv(1u, 2u, 0u, 0u), kVuUpperNop); writeVuInstructionPair( - fx.code, 8u, makeVuLowerSpecial(0x3Bu, 0u), + fx.mem, 8u, makeVuLowerSpecial(0x3Bu, 0u), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 0.0f; vu1.state().vf[2][0] = 0.0f; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, @@ -727,23 +728,23 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, + fx.mem, 0u, makeVuLowerSpecial(0x70u, 1u), kVuUpperNop); // ESADD P, vf1 writeVuInstructionPair( - fx.code, 8u, + fx.mem, 8u, makeVuLowerSpecial(0x7Bu, 0u), kVuUpperNop); // WAITP writeVuInstructionPair( - fx.code, 16u, + fx.mem, 16u, makeVuLowerSpecial(0x42u, 2u, 0u, 0u, 0x8u), kVuUpperNop); // RINIT R, vf2.x writeVuInstructionPair( - fx.code, 24u, + fx.mem, 24u, makeVuLowerSpecial(0x40u, 0u, 3u, 0u, 0x8u), kVuUpperNop); // RNEXT.x vf3 - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[1][1] = 2.0f; vu1.state().vf[1][2] = 3.0f; @@ -771,19 +772,19 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, + fx.mem, 0u, makeVuLowerSpecial(0x70u, 1u), kVuUpperNop); // ESADD: result at cycle 11, resource free at 10. writeVuInstructionPair( - fx.code, 8u, + fx.mem, 8u, makeVuLowerSpecial(0x72u, 1u), kVuUpperNop); // ELENG: must issue at cycle 10. writeVuInstructionPair( - fx.code, 16u, + fx.mem, 16u, makeVuLowerSpecial(0x7Bu, 0u), kVuUpperNop); // WAITP waits for ELENG at cycle 28. - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[1][1] = 2.0f; vu1.state().vf[1][2] = 3.0f; @@ -815,17 +816,17 @@ void register_ps2_vu1_tests() for (const EfuCase &efu : cases) { - std::memset(fx.code, 0, PS2_VU1_CODE_SIZE); + fx.mem.writeVuCode(true, 0, std::array{}); writeVuInstructionPair( - fx.code, 0u, + fx.mem, 0u, makeVuLowerSpecial(efu.opcode, 1u, 0u, 0u, 0u), kVuUpperNop); writeVuInstructionPair( - fx.code, 8u, + fx.mem, 8u, makeVuLowerSpecial(0x7Bu, 0u), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 0.25f; vu1.state().vf[1][1] = 0.5f; vu1.state().vf[1][2] = 0.75f; @@ -848,16 +849,16 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, 0u, + fx.mem, 0u, 0u, kVuUpperNop | 0x40000000u); writeVuInstructionPair( - fx.code, 8u, makeVuIaddiu(1u, 0u, 7), + fx.mem, 8u, makeVuIaddiu(1u, 0u, 7), kVuUpperNop); writeVuInstructionPair( - fx.code, 16u, makeVuIaddiu(2u, 0u, 9), + fx.mem, 16u, makeVuIaddiu(2u, 0u, 9), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 32u); @@ -945,22 +946,22 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, makeVuIaddiu(1u, 0u, 1), + fx.mem, 0u, makeVuIaddiu(1u, 0u, 1), kVuUpperNop); writeVuInstructionPair( - fx.code, 8u, makeVuIbne(1u, 0u, 2), + fx.mem, 8u, makeVuIbne(1u, 0u, 2), kVuUpperNop); writeVuInstructionPair( - fx.code, 16u, makeVuIaddiu(2u, 0u, 2), + fx.mem, 16u, makeVuIaddiu(2u, 0u, 2), kVuUpperNop); writeVuInstructionPair( - fx.code, 24u, makeVuIaddiu(3u, 0u, 3), + fx.mem, 24u, makeVuIaddiu(3u, 0u, 3), kVuUpperNop); writeVuInstructionPair( - fx.code, 32u, makeVuIaddiu(4u, 0u, 4), + fx.mem, 32u, makeVuIaddiu(4u, 0u, 4), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 4u); @@ -1021,23 +1022,23 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, + fx.mem, 0u, makeVuFlagImmediate(0x16u, 1u, 0x001u), kVuUpperNop); writeVuInstructionPair( - fx.code, 8u, makeVuIbne(1u, 0u, 2), + fx.mem, 8u, makeVuIbne(1u, 0u, 2), kVuUpperNop); writeVuInstructionPair( - fx.code, 16u, makeVuIaddiu(2u, 0u, 2), + fx.mem, 16u, makeVuIaddiu(2u, 0u, 2), kVuUpperNop); writeVuInstructionPair( - fx.code, 24u, makeVuIaddiu(3u, 0u, 3), + fx.mem, 24u, makeVuIaddiu(3u, 0u, 3), kVuUpperNop); writeVuInstructionPair( - fx.code, 32u, makeVuIaddiu(4u, 0u, 4), + fx.mem, 32u, makeVuIaddiu(4u, 0u, 4), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().status = 0x001u; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, @@ -1059,22 +1060,22 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, makeVuIaddiu(1u, 0u, 2), + fx.mem, 0u, makeVuIaddiu(1u, 0u, 2), kVuUpperNop); writeVuInstructionPair( - fx.code, 8u, makeVuJr(1u), + fx.mem, 8u, makeVuJr(1u), kVuUpperNop); writeVuInstructionPair( - fx.code, 16u, makeVuIaddiu(2u, 0u, 2), + fx.mem, 16u, makeVuIaddiu(2u, 0u, 2), kVuUpperNop); writeVuInstructionPair( - fx.code, 24u, makeVuIaddiu(3u, 0u, 3), + fx.mem, 24u, makeVuIaddiu(3u, 0u, 3), kVuUpperNop); writeVuInstructionPair( - fx.code, 32u, makeVuIaddiu(4u, 0u, 4), + fx.mem, 32u, makeVuIaddiu(4u, 0u, 4), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = 4; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, @@ -1095,7 +1096,7 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - VU1Interpreter vu1; + VUExecutor vu1; fx.mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t top, uint32_t itop) { vu1.execute(fx.code, @@ -1126,7 +1127,7 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - VU1Interpreter vu1; + VUExecutor vu1; fx.mem.write64(PS2_VU1_CODE_BASE, packVuInstructionPair(makeVuIaddiu(1u, 0u, 1), kVuUpperNop)); vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); t.Equals(vu1.state().vi[1], 1, "first execution should use the original direct write"); @@ -1151,9 +1152,8 @@ void register_ps2_vu1_tests() GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); - uint8_t *vuCode = mem.getVU1Code(); + const uint8_t *vuCode = mem.getVU1Code(); uint8_t *vuData = mem.getVU1Data(); - std::memset(vuCode, 0, PS2_VU1_CODE_SIZE); std::memset(vuData, 0, PS2_VU1_DATA_SIZE); constexpr uint32_t kLastQw = (PS2_VU1_DATA_SIZE / 16u) - 1u; @@ -1168,11 +1168,11 @@ void register_ps2_vu1_tests() } const uint32_t lower = makeVuLowerSpecial(0x6Cu, 1u); - std::memcpy(vuCode + 0u, &lower, sizeof(lower)); + mem.write32(PS2_VU1_CODE_BASE, lower); const uint32_t upper = 0u; - std::memcpy(vuCode + 4u, &upper, sizeof(upper)); + mem.write32(PS2_VU1_CODE_BASE + 4, upper); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = static_cast(kLastQw); vu1.execute(vuCode, PS2_VU1_CODE_SIZE, @@ -1217,25 +1217,24 @@ void register_ps2_vu1_tests() GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); - uint8_t *code = mem.getVU1Code(); + const uint8_t *code = mem.getVU1Code(); uint8_t *data = mem.getVU1Data(); - std::memset(code, 0, PS2_VU1_CODE_SIZE); std::memset(data, 0xFF, PS2_VU1_DATA_SIZE); const uint64_t imageTag = makeGifTag(1u, GIF_FMT_IMAGE, 0u, true); std::memset(data, 0, 16u); std::memcpy(data, &imageTag, sizeof(imageTag)); writeVuInstructionPair( - code, 0u, + mem, 0u, makeVuLowerSpecial(0x6Cu, 1u), kVuUpperNop); writeVuInstructionPair( - code, 8u, + mem, 8u, makeVuSq(0xFu, 4u, 2u, 0), kVuUpperNop); - writeVuInstructionPair(code, 16u, 0u, kVuUpperNop); + writeVuInstructionPair(mem, 16u, 0u, kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = 0; vu1.state().vi[2] = 1; const float replacement[4] = {0.0f, 0.0f, 0.0f, 1.0f}; @@ -1270,9 +1269,8 @@ void register_ps2_vu1_tests() std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); - uint8_t *code = mem.getVU1Code(); + const uint8_t *code = mem.getVU1Code(); uint8_t *data = mem.getVU1Data(); - std::memset(code, 0, PS2_VU1_CODE_SIZE); std::memset(data, 0, PS2_VU1_DATA_SIZE); const uint64_t imageTag = makeGifTag(1u, GIF_FMT_IMAGE, 0u, true); @@ -1281,17 +1279,17 @@ void register_ps2_vu1_tests() std::memset(data + 16u, 0x11, 16u); std::memset(data + 48u, 0x22, 16u); writeVuInstructionPair( - code, 0u, + mem, 0u, makeVuLowerSpecial(0x6Cu, 1u), kVuUpperNop); writeVuInstructionPair( - code, 8u, + mem, 8u, makeVuLowerSpecial(0x6Cu, 2u), kVuUpperNop); - writeVuInstructionPair(code, 16u, 0u, kVuUpperNop); - writeVuInstructionPair(code, 24u, 0u, kVuUpperNop); + writeVuInstructionPair(mem, 16u, 0u, kVuUpperNop); + writeVuInstructionPair(mem, 24u, 0u, kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = 0; vu1.state().vi[2] = 2; vu1.execute(code, PS2_VU1_CODE_SIZE, @@ -1326,26 +1324,25 @@ void register_ps2_vu1_tests() GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); - uint8_t *code = mem.getVU1Code(); + const uint8_t *code = mem.getVU1Code(); uint8_t *data = mem.getVU1Data(); - std::memset(code, 0, PS2_VU1_CODE_SIZE); std::memset(data, 0, PS2_VU1_DATA_SIZE); const uint64_t imageTag = makeGifTag(1u, GIF_FMT_IMAGE, 0u, true); std::memcpy(data, &imageTag, sizeof(imageTag)); writeVuInstructionPair( - code, 0u, 0u, + mem, 0u, 0u, makeVuUpper(0x28u, 0xFu, 3u, 2u, 4u)); writeVuInstructionPair( - code, 8u, + mem, 8u, makeVuSq(0xFu, 4u, 2u, 0), kVuUpperNop); writeVuInstructionPair( - code, 16u, + mem, 16u, makeVuLowerSpecial(0x6Cu, 1u), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vi[1] = 0; vu1.state().vi[2] = 1; const float a[4] = {0.0f, 0.0f, 0.0f, 0.0f}; @@ -1397,15 +1394,14 @@ void register_ps2_vu1_tests() gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); gs.writeRegister(GS_REG_TRXDIR, 0ull); - uint8_t *vuCode = mem.getVU1Code(); + const uint8_t *vuCode = mem.getVU1Code(); uint8_t *vuData = mem.getVU1Data(); - std::memset(vuCode, 0, PS2_VU1_CODE_SIZE); std::memset(vuData, 0, PS2_VU1_DATA_SIZE); const uint32_t lower = makeVuLowerSpecial(0x6Cu, 0u); - std::memcpy(vuCode + 0u, &lower, sizeof(lower)); + mem.write32(PS2_VU1_CODE_BASE, lower); const uint32_t upper = 0u; - std::memcpy(vuCode + 4u, &upper, sizeof(upper)); + mem.write32(PS2_VU1_CODE_BASE + 4, upper); const uint64_t gifTag = makeGifTag(1u, GIF_FMT_IMAGE, 0u, true); std::memcpy(vuData + 0u, &gifTag, sizeof(gifTag)); @@ -1416,7 +1412,7 @@ void register_ps2_vu1_tests() vuData[16u + i] = static_cast(0x90u + i); } - VU1Interpreter vu1; + VUExecutor vu1; mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t top, uint32_t itop) { vu1.execute(vuCode, @@ -1459,7 +1455,7 @@ void register_ps2_vu1_tests() GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); - VU1Interpreter vu1; + VUExecutor vu1; vu1.execute(code.data(), static_cast(code.size()), data.data(), static_cast(data.size()), gs, nullptr, 0u, 0u, 0u, 1u); @@ -1473,17 +1469,17 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, + writeVuInstructionPair(fx.mem, 0u, makeVuFlagImmediate(0x14u, 5u, 0x812u), kVuUpperNop); - writeVuInstructionPair(fx.code, 8u, + writeVuInstructionPair(fx.mem, 8u, makeVuFlagImmediate(0x16u, 6u, 0x810u), kVuUpperNop); - writeVuInstructionPair(fx.code, 16u, + writeVuInstructionPair(fx.mem, 16u, makeVuFlagImmediate(0x17u, 7u, 0x040u), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().status = 0x812u; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, @@ -1504,11 +1500,11 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, + writeVuInstructionPair(fx.mem, 0u, makeVuFlagImmediate(0x15u, 0u, 0xA80u), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().status = 0x015u; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, @@ -1528,13 +1524,13 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, + writeVuInstructionPair(fx.mem, 0u, makeVuFlagImmediate(0x15u, 0u, 0xA80u), makeVuUpper(0x28u, 0xAu, 2u, 1u, 3u)); for (uint32_t pc = 8u; pc <= 32u; pc += 8u) - writeVuInstructionPair(fx.code, pc, 0u, kVuUpperNop); + writeVuInstructionPair(fx.mem, pc, 0u, kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[1][2] = -3.0f; vu1.state().vf[2][0] = -1.0f; @@ -1555,22 +1551,22 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, 0u, + writeVuInstructionPair(fx.mem, 0u, 0u, makeVuUpper(0x28u, 0xAu, 2u, 1u, 3u)); - writeVuInstructionPair(fx.code, 8u, + writeVuInstructionPair(fx.mem, 8u, makeVuFlagRegister(0x18u, 6u, 7u), kVuUpperNop); - writeVuInstructionPair(fx.code, 16u, + writeVuInstructionPair(fx.mem, 16u, 0u, kVuUpperNop); - writeVuInstructionPair(fx.code, 24u, + writeVuInstructionPair(fx.mem, 24u, 0u, kVuUpperNop); - writeVuInstructionPair(fx.code, 32u, + writeVuInstructionPair(fx.mem, 32u, makeVuFlagRegister(0x1Au, 4u, 5u), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 1.0f; vu1.state().vf[1][2] = -3.0f; vu1.state().vf[2][0] = -1.0f; @@ -1605,20 +1601,20 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, 0u, + fx.mem, 0u, 0u, makeVuUpperSpecial(0x1Fu, 0u, 2u, 1u)); writeVuInstructionPair( - fx.code, 8u, + fx.mem, 8u, makeVuFlagRegister(0x1Cu, 3u, 0u), kVuUpperNop); - writeVuInstructionPair(fx.code, 16u, 0u, kVuUpperNop); - writeVuInstructionPair(fx.code, 24u, 0u, kVuUpperNop); + writeVuInstructionPair(fx.mem, 16u, 0u, kVuUpperNop); + writeVuInstructionPair(fx.mem, 24u, 0u, kVuUpperNop); writeVuInstructionPair( - fx.code, 32u, + fx.mem, 32u, makeVuFlagRegister(0x1Cu, 4u, 0u), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = 2.0f; vu1.state().vf[2][3] = 1.0f; vu1.execute(fx.code, PS2_VU1_CODE_SIZE, @@ -1638,10 +1634,10 @@ void register_ps2_vu1_tests() Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, 0u, + writeVuInstructionPair(fx.mem, 0u, 0u, makeVuUpper(0x2Au, 0xFu, 2u, 1u, 3u)); - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().vf[1][0] = std::numeric_limits::max(); vu1.state().vf[1][1] = std::numeric_limits::min(); vu1.state().vf[1][2] = -2.0f; @@ -1671,10 +1667,10 @@ void register_ps2_vu1_tests() t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); writeVuInstructionPair( - fx.code, 0u, 0u, + fx.mem, 0u, 0u, makeVuUpper(0x29u, 0x8u, 2u, 1u, 3u)); // MADD.x - VU1Interpreter vu1; + VUExecutor vu1; vu1.state().acc[0] = 1.0f; vu1.state().vf[1][0] = std::numeric_limits::min(); vu1.state().vf[2][0] = 0.5f; @@ -1690,20 +1686,295 @@ void register_ps2_vu1_tests() "the underflowing product should set sticky Z and U"); }); + tc.Run("cycle budget splitting preserves pipelines and PATH1 observations", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + std::vector> packets; + fx.mem.setGifPacketCallback([&](const uint8_t *data, uint32_t size) + { + packets.emplace_back(data, data + size); + }); + writeTrackedVuInstructionPair(fx, 0u, makeVuLowerSpecial(0x6Cu, 0u), kVuUpperNop); + writeTrackedVuInstructionPair(fx, 8u, makeVuSq(0xFu, 4u, 0u, 1), + makeVuUpper(0x29u, 0xFu, 2u, 1u, 3u)); + writeTrackedVuInstructionPair(fx, 16u, makeVuDiv(1u, 2u, 0u, 0u), kVuUpperNop); + writeTrackedVuInstructionPair(fx, 24u, makeVuLowerSpecial(0x7Cu, 1u), kVuUpperNop); + writeTrackedVuInstructionPair(fx, 32u, makeVuLowerSpecial(0x7Bu, 0u), kVuUpperNop); + writeTrackedVuInstructionPair(fx, 40u, makeVuSq(0xFu, 3u, 0u, 12), + makeVuUpper(0x1Cu, 0xFu, 0u, 3u, 5u)); + for (uint32_t pc = 48u; pc < 512u; pc += 8u) + writeTrackedVuInstructionPair(fx, pc, 0u, kVuUpperNop); + + const auto prepare = [&](VUExecutor &vu) + { + std::memset(fx.data, 0xAB, PS2_VU1_DATA_SIZE); + const uint64_t tag = makeGifTag(8u, GIF_FMT_IMAGE, 0u, true); + std::memset(fx.data, 0, 16u); + std::memcpy(fx.data, &tag, sizeof(tag)); + const float a[4] = {0.25f, 0.5f, 0.75f, 1.0f}; + const float b[4] = {2.0f, -2.0f, 4.0f, -4.0f}; + std::memcpy(vu.state().vf[1], a, sizeof(a)); + std::memcpy(vu.state().vf[2], b, sizeof(b)); + std::memcpy(vu.state().vf[4], b, sizeof(b)); + }; + VUExecutor whole; + prepare(whole); + whole.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, + fx.gs, &fx.mem, 0u, 0u, 0u, 70u); + const VU1State expected = whole.state(); + const std::vector expectedData(fx.data, fx.data + PS2_VU1_DATA_SIZE); + const auto expectedPackets = packets; + t.Equals(expectedPackets.size(), size_t(1u), "reference run should emit one packet"); + + for (const uint32_t slice : {1u, 2u, 3u, 7u, 13u}) + { + VUExecutor sliced; + prepare(sliced); + packets.clear(); + sliced.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, + fx.gs, &fx.mem, 0u, 0u, 0u, 0u); + for (uint32_t elapsed = 0; elapsed < 70u;) + { + const uint32_t budget = std::min(slice, 70u - elapsed); + sliced.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, + fx.gs, &fx.mem, 0u, 0u, budget); + elapsed += budget; + t.Equals(sliced.state().cycles, uint64_t(elapsed), "resume must stop at its budget"); + } + const auto &actual = sliced.state(); + t.IsTrue(std::memcmp(actual.vf, expected.vf, sizeof(actual.vf)) == 0, "VF bits must match"); + t.IsTrue(std::memcmp(actual.vi, expected.vi, sizeof(actual.vi)) == 0, "VI values must match"); + t.IsTrue(std::memcmp(actual.acc, expected.acc, sizeof(actual.acc)) == 0, "ACC bits must match"); + t.Equals(actual.q, expected.q, "Q result must match"); + t.Equals(actual.p, expected.p, "P result must match"); + t.Equals(actual.pc, expected.pc, "PC must match"); + t.Equals(actual.mac, expected.mac, "MAC flags must match"); + t.Equals(actual.status, expected.status, "STATUS flags must match"); + t.Equals(actual.clip, expected.clip, "CLIP flags must match"); + t.IsTrue(std::memcmp(fx.data, expectedData.data(), expectedData.size()) == 0, "store bytes must match"); + t.IsTrue(packets == expectedPackets, "PATH1 bytes and packet order must match"); + } + }); + + tc.Run("XGKICK reuse submits only the new packet across reset and memory wrap", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + std::vector> packets; + fx.mem.setGifPacketCallback([&](const uint8_t *data, uint32_t size) + { + packets.emplace_back(data, data + size); + }); + writeTrackedVuInstructionPair(fx, 0u, makeVuLowerSpecial(0x6Cu, 1u), kVuUpperNop | (1u << 30)); + writeTrackedVuInstructionPair(fx, 8u, 0u, kVuUpperNop); + VUExecutor vu; + unsigned run = 0; + for (const uint32_t qwords : {128u, 1u, 0u, 64u, 2u}) + { + const uint32_t start = PS2_VU1_DATA_SIZE - 16u; + const uint64_t tag = makeGifTag(static_cast(qwords), GIF_FMT_IMAGE, 0u, true); + std::vector expected((qwords + 1u) * 16u, static_cast(++run)); + std::memset(expected.data(), 0, 16u); + std::memcpy(expected.data(), &tag, sizeof(tag)); + for (uint32_t i = 0; i < expected.size(); ++i) + fx.data[(start + i) % PS2_VU1_DATA_SIZE] = expected[i]; + if (run % 2u == 0u) + vu.reset(); + vu.state().vi[1] = start / 16u; + packets.clear(); + vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem); + t.Equals(packets.size(), size_t(1u), "one complete packet should be emitted"); + if (!packets.empty()) + t.IsTrue(packets.front() == expected, "packet must contain no stale bytes after reuse"); + } + }); + + tc.Run("reset discards pending scalar vector flags and PATH1 work", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + unsigned packets = 0; + fx.mem.setGifPacketCallback([&](const uint8_t *, uint32_t) { ++packets; }); + const uint64_t tag = makeGifTag(128u, GIF_FMT_IMAGE, 0u, true); + std::memcpy(fx.data, &tag, sizeof(tag)); + writeTrackedVuInstructionPair(fx, 0u, makeVuLowerSpecial(0x6Cu, 0u), kVuUpperNop); + writeTrackedVuInstructionPair(fx, 8u, makeVuDiv(1u, 2u, 0u, 0u), + makeVuUpper(0x2Au, 0xFu, 1u, 1u, 3u)); + writeTrackedVuInstructionPair(fx, 16u, makeVuLowerSpecial(0x7Cu, 1u), kVuUpperNop); + writeTrackedVuInstructionPair(fx, 24u, 0u, kVuUpperNop | (1u << 30)); + writeTrackedVuInstructionPair(fx, 32u, 0u, kVuUpperNop); + VUExecutor vu; + vu.state().vf[1][0] = std::numeric_limits::max(); + vu.state().vf[2][0] = 2.0f; + vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, + fx.gs, &fx.mem, 0u, 0u, 0u, 3u); + vu.reset(); + // The runtime imports VU0 state after reset and before execute. + vu.state().clip = 0x123456u; + vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, + fx.gs, &fx.mem, 24u); + t.Equals(vu.state().cycles, uint64_t(2u), "reset should discard old event deadlines"); + t.Equals(vu.state().vf[3][0], 0.0f, "old vector result must not commit"); + t.Equals(vu.state().q, 1.0f, "old Q result must not commit"); + t.Equals(vu.state().p, 0.0f, "old P result must not commit"); + t.Equals(vu.state().status, 0u, "old flags must not commit"); + t.Equals(vu.state().clip, 0x123456u, "imported CLIP must survive execute"); + t.Equals(packets, 0u, "abandoned PATH1 packet must not be submitted"); + }); + + tc.Run("reused flag entries keep only the current writeback effects", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU fixture should initialize"); + struct Step + { + uint32_t lower, upper, mac, status, clip; + }; + const Step steps[] = { + {0u, makeVuUpper(0x2Au, 8u, 2u, 1u, 3u), 8u, 0xC1u, 0x777777u}, + {makeVuFlagImmediate(0x15u, 0u, 0x400u), kVuUpperNop, 8u, 0x401u, 0x777777u}, + {0u, makeVuUpper(0x28u, 8u, 2u, 1u, 3u), 0u, 0x400u, 0x777777u}, + {(0x11u << 25) | 0x123456u, kVuUpperNop, 0u, 0x400u, 0x123456u}, + {makeVuFlagImmediate(0x15u, 0u, 0x80u), kVuUpperNop, 0u, 0x80u, 0x777777u} + }; + for (const auto unit : {VUExecutor::Unit::VU0, VUExecutor::Unit::VU1}) + { + VUExecutor vu(unit); + vu.state().vf[2][0] = 2.0f; + vu.state().status = 0x80u; + writeTrackedVuInstructionPair(fx, 0u, 0u, kVuUpperNop); + writeTrackedVuInstructionPair(fx, 8u, 0u, kVuUpperNop); + vu.execute(fx.code, 16u, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 0u); + for (uint32_t repetition = 0; repetition < 40; ++repetition) + { + for (const auto &step : steps) + { + vu.state().clip = 0x777777u; + writeTrackedVuInstructionPair(fx, 0u, step.lower, step.upper); + vu.resume(fx.code, 16u, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 1u); + writeTrackedVuInstructionPair(fx, 0u, 0u, kVuUpperNop); + vu.resume(fx.code, 16u, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 3u); + t.Equals(vu.state().mac, step.mac, "a reused entry must not replay a MAC write"); + t.Equals(vu.state().status, step.status, + "only current and sticky effects from this instruction apply"); + t.Equals(vu.state().clip, step.clip, "a reused entry must not replay a CLIP write"); + } + } + } + }); + + tc.Run("FMAC packs each flag plane for every destination mask", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + const float left[4] = {-std::numeric_limits::max(), std::numeric_limits::min(), 0.0f, 1.0f}; + const float right[4] = {2.0f, 0.5f, -1.0f, 2.0f}; + const uint32_t flags[4] = {0xAu, 0x5u, 0x3u, 0x0u}; + const uint32_t resultBits[4] = {0xFF7FFFFFu, 0u, 0x80000000u, 0x40000000u}; + for (uint8_t mask = 0; mask < 16; ++mask) + { + VUExecutor vu; + std::memcpy(vu.state().vf[1], left, sizeof(left)); + std::memcpy(vu.state().vf[2], right, sizeof(right)); + std::fill_n(vu.state().vf[3], 4, 7.0f); + writeTrackedVuInstructionPair(fx, 0u, 0u, makeVuUpper(0x2Au, mask, 2u, 1u, 3u) | (1u << 30)); + writeTrackedVuInstructionPair(fx, 8u, 0u, kVuUpperNop); + vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem); + uint32_t expectedMac = 0, expectedStatus = 0; + for (uint32_t lane = 0; lane < 4; ++lane) + { + const uint32_t laneBit = 8u >> lane; + if ((mask & laneBit) != 0) + { + expectedStatus |= flags[lane]; + for (uint32_t condition = 0; condition < 4; ++condition) + if ((flags[lane] & (1u << condition)) != 0) + expectedMac |= laneBit << (condition * 4u); + } + const uint32_t expectedBits = (mask & laneBit) ? resultBits[lane] : 0x40E00000u; + uint32_t actualBits; + std::memcpy(&actualBits, &vu.state().vf[3][lane], sizeof(actualBits)); + t.Equals(actualBits, expectedBits, "only selected lanes receive the normalized result"); + } + t.Equals(vu.state().mac, expectedMac, "Z/S/U/O occupy distinct MAC bit planes"); + t.Equals(vu.state().status, expectedStatus | (expectedStatus << 6), "current and sticky conditions must agree"); + } + }); + + tc.Run("code changed during a stall refreshes decoded lane dependencies on resume", [](TestCase &t) + { + for (bool tracked : {false, true}) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + const auto writePair = [&](uint32_t pc, uint32_t upper) + { + if (tracked) + writeTrackedVuInstructionPair(fx, pc, 0u, upper); + else + writeVuInstructionPair(fx.mem, pc, 0u, upper); + }; + PS2Memory *memory = tracked ? &fx.mem : nullptr; + writePair(0u, makeVuUpper(0x28u, 8u, 2u, 1u, 3u)); + writePair(8u, makeVuUpper(0x28u, 8u, 2u, 3u, 4u)); + writePair(16u, kVuUpperNop | (1u << 30)); + writePair(24u, kVuUpperNop); + VUExecutor vu; + vu.state().vf[3][1] = 7.0f; + vu.state().vf[2][1] = 1.0f; + vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, memory, 0u, 0u, 0u, 2u); + t.Equals(vu.state().pc, 8u, "the decoded consumer must stall on the pending X producer"); + writePair(8u, makeVuUpper(0x28u, 4u, 2u, 3u, 4u)); + vu.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, memory, 0u, 0u, 1u); + t.Equals(vu.state().pc, 16u, "new Y dependency must not wait for the old X producer"); + t.Equals(vu.state().cycles, uint64_t(3u), "resume must honor its cycle budget"); + vu.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, memory); + t.Equals(vu.state().vf[4][1], 8.0f, "the replacement instruction must use the Y lanes"); + } + }); + + tc.Run("native blocks resume in the middle and finish the E delay slot", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + for (uint32_t pair = 0; pair < 16; ++pair) + writeVuInstructionPair(fx.mem, 0x800 + pair * 8, 0, + pair < 14 ? 0x01e208e8 : pair == 14 ? 0x400002ff : 0x2ff); + VUExecutor vu; + vu.state().vf[1][0] = 1.25f; + vu.state().vf[2][0] = 2.5f; + vu.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0x800, 0, 0, 3); + t.Equals(vu.state().pc, 0x818u, "Budget stops inside the generated block"); + vu.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0, 0, 64); + t.Equals(vu.state().vf[3][0], 3.75f, "Compiled FMAC result must commit"); + t.Equals(vu.state().pc, 0x880u, "E executes exactly one delay slot"); + t.IsFalse(vu.state().ebit, "Ending the program clears the E latch"); + }); + tc.Run("reserved opcodes stop before executing or corrupting state", [](TestCase &t) { Vu1Fixture fx; t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); - writeVuInstructionPair(fx.code, 0u, 0u, 0x30u); + writeVuInstructionPair(fx.mem, 0u, 0u, 0x30u); writeVuInstructionPair( - fx.code, 8u, makeVuIaddiu(1u, 0u, 7), + fx.mem, 8u, makeVuIaddiu(1u, 0u, 7), kVuUpperNop); - VU1Interpreter vu1; + VUExecutor vu1; + bool trapped = false; + try + { vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 8u); + } + catch (const std::runtime_error &) + { + trapped = true; + } + t.IsTrue(trapped, "Reserved VU1 instructions must trap"); t.Equals(vu1.state().cycles, static_cast(0u), "reserved opcode should stop before consuming its issue cycle"); diff --git a/ps2xTest/src/ps2_vu_pipeline_tests.cpp b/ps2xTest/src/ps2_vu_pipeline_tests.cpp new file mode 100644 index 000000000..9e859424c --- /dev/null +++ b/ps2xTest/src/ps2_vu_pipeline_tests.cpp @@ -0,0 +1,132 @@ +#include "MiniTest.h" +#include "runtime/vu/vu_pipeline_events.h" + +#include +#include + +namespace +{ + using ps2vu::PipelineEvents; + using ps2vu::WritebackKind; + + struct Event + { + uint64_t cycle; + uint32_t slot; + }; + + template void compareSchedule(TestCase &t) + { + PipelineEvents events; + std::vector pending; + uint32_t random = 0x9E3779B9u; + for (uint64_t cycle = 0; cycle < 8192; ++cycle) + { + uint32_t expected = 0; + for (const auto &event : pending) + if (event.cycle == cycle) + expected |= 1u << event.slot; + t.Equals(PipelineEvents::slots(events.take(cycle)), expected, + "only events due at this cycle should be delivered"); + std::erase_if(pending, [cycle](const Event &event) { return event.cycle == cycle; }); + + for (uint32_t slot = 0; slot < PipelineEvents::capacity(Kind); ++slot) + { + random = random * 1664525u + 1013904223u; + if (((random >> 24) & 3u) != 0 || + std::any_of(pending.begin(), pending.end(), + [slot](const Event &event) { return event.slot == slot; })) + continue; + const uint32_t latency = 1u + ((random >> 8) % 63u); + events.schedule(cycle, latency, slot); + pending.push_back({cycle + latency, slot}); + } + + uint64_t next = PipelineEvents::noEvent; + for (const auto &event : pending) + next = std::min(next, event.cycle); + t.Equals(events.nextCycle(cycle), next, "next event must match the ordered reference"); + if (cycle % 257u == 256u) + { + events.reset(); + pending.clear(); + t.Equals(events.nextCycle(cycle), PipelineEvents::noEvent, "reset must discard pending work"); + } + } + } +} + +void register_ps2_vu_pipeline_tests() +{ + MiniTest::Case("VU pipeline events", [](TestCase &tc) { + tc.Run("mixed latencies match an ordered reference across wraps and resets", [](TestCase &t) { + compareSchedule(t); + compareSchedule(t); + compareSchedule(t); + compareSchedule(t); + compareSchedule(t); + compareSchedule(t); + compareSchedule(t); + }); + + tc.Run("simultaneous writebacks retain every pipeline and slot", [](TestCase &t) { + PipelineEvents events; + const auto schedule = [&]() { + for (uint32_t slot = 0; slot < PipelineEvents::capacity(Kind); ++slot) + events.schedule(61, 4, slot); + }; + schedule.operator()(); + schedule.operator()(); + schedule.operator()(); + schedule.operator()(); + schedule.operator()(); + schedule.operator()(); + schedule.operator()(); + t.Equals(events.nextCycle(61), uint64_t(65), "first deadline must cross the calendar boundary"); + t.Equals(events.take(64), uint64_t(0), "nothing may commit early"); + const uint64_t ready = events.take(65); + const auto check = [&]() { + t.Equals(PipelineEvents::slots(ready), (1u << PipelineEvents::capacity(Kind)) - 1u, + "no simultaneous writeback may be lost or assigned to " + "another pipeline"); + }; + check.operator()(); + check.operator()(); + check.operator()(); + check.operator()(); + check.operator()(); + check.operator()(); + check.operator()(); + t.Equals(events.nextCycle(65), PipelineEvents::noEvent, "drained schedule must be empty"); + t.Equals(events.take(129), uint64_t(0), "a wrapped bucket must not replay old events"); + }); + + tc.Run("invalid events fail without changing the schedule", [](TestCase &t) { + PipelineEvents events; + for (const uint32_t latency : {0u, 64u, 65u}) + { + bool rejected = false; + try + { + events.schedule(0, latency, 0); + } + catch (const std::logic_error &) + { + rejected = true; + } + t.IsTrue(rejected, "an invalid latency must be rejected"); + } + bool rejected = false; + try + { + events.schedule(0, 4, 16); + } + catch (const std::logic_error &) + { + rejected = true; + } + t.IsTrue(rejected, "an invalid slot must be rejected"); + t.Equals(events.nextCycle(0), PipelineEvents::noEvent, "invalid events must leave no pending work"); + }); + }); +} diff --git a/ps2xTest/src/test_function_table.cpp b/ps2xTest/src/test_function_table.cpp index be6a8889f..e1518f447 100644 --- a/ps2xTest/src/test_function_table.cpp +++ b/ps2xTest/src/test_function_table.cpp @@ -4,6 +4,7 @@ #include // For Unit tests link ps2_runtime without the generated runner source. +extern const uint32_t g_ps2RecompiledEeAbiVersion = 4u; extern const uint32_t g_ps2RecompiledFunctionTableBase = 0x00000000u; extern const uint32_t g_ps2RecompiledFunctionTableEnd = PS2_RAM_SIZE; extern const uint32_t g_ps2RecompiledFunctionTableSlotCount = (g_ps2RecompiledFunctionTableEnd - g_ps2RecompiledFunctionTableBase) >> 2; diff --git a/ps2xTest/src/vu1_tests_main.cpp b/ps2xTest/src/vu1_tests_main.cpp new file mode 100644 index 000000000..8f9c99774 --- /dev/null +++ b/ps2xTest/src/vu1_tests_main.cpp @@ -0,0 +1,17 @@ +#include "MiniTest.h" + +void register_ps2_vu1_tests(); +void register_ps2_vu_pipeline_tests(); + +void register_ps2_vu_tests(); +void reset_ps2_test_function_table(); + +int main() +{ + MiniTest::BeforeEach(reset_ps2_test_function_table); + register_ps2_vu1_tests(); + register_ps2_vu_pipeline_tests(); + + register_ps2_vu_tests(); + return MiniTest::Run(); +}