diff --git a/ps2xIOP/src/builtin_profiles.cpp b/ps2xIOP/src/builtin_profiles.cpp index b4db00c25..ac537ad9b 100644 --- a/ps2xIOP/src/builtin_profiles.cpp +++ b/ps2xIOP/src/builtin_profiles.cpp @@ -70,7 +70,8 @@ namespace ps2x::iop::detail .responseCounterOffset = 4u, .zeroReceiveBuffer = true, .signalNowaitCompletion = true, - .suppressedCompletionCallbacks = {0x001FFD70u}, + .completeQueuedPlayStreams = true, + .suppressedCompletionCallbacks = {}, }; } diff --git a/ps2xIOP/src/module_factories.h b/ps2xIOP/src/module_factories.h index 42b1c3034..8e0f422fa 100644 --- a/ps2xIOP/src/module_factories.h +++ b/ps2xIOP/src/module_factories.h @@ -107,6 +107,7 @@ namespace ps2x::iop::detail uint32_t responseCounterOffset = 0u; bool zeroReceiveBuffer = true; bool signalNowaitCompletion = false; + bool completeQueuedPlayStreams = false; std::vector suppressedCompletionCallbacks; }; diff --git a/ps2xIOP/src/modules/sound_update_stub.cpp b/ps2xIOP/src/modules/sound_update_stub.cpp index 933ebe5ee..68c1c8cb2 100644 --- a/ps2xIOP/src/modules/sound_update_stub.cpp +++ b/ps2xIOP/src/modules/sound_update_stub.cpp @@ -7,11 +7,20 @@ #include #include #include +#include namespace ps2x::iop::detail { namespace { + constexpr uint16_t kPlayStreamCommand = 1u; + constexpr uint32_t kResponseRecordStride = 0x20u; + constexpr uint32_t kPackedStreamOffset = 4u; + constexpr uint32_t kStreamSlotMask = 0x3Fu; + constexpr uint32_t kStreamSlotCount = 48u; + constexpr uint32_t kCommandStreamSlotShift = 8u; + constexpr uint32_t kResponseStreamSlotShift = 4u; + class SoundUpdateStubService final : public IopService { public: @@ -34,6 +43,7 @@ namespace ps2x::iop::detail { std::lock_guard lock(m_mutex); m_updateCounter = 0u; + m_completedStreamCount = 0u; } [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override @@ -62,13 +72,23 @@ namespace ps2x::iop::detail (void)m_host.zeroGuest(request.receive.address, request.receive.size); } + std::vector activeStreamSlots; + if (m_bindings.completeQueuedPlayStreams && request.receive.address != 0u) + { + // PlayStream leaves the EE slot in state 2. One active record moves it + // to state 1; the following empty update lets SOUND_CopyIOPBuffer clear it. + activeStreamSlots = findQueuedPlayStreams(request); + trimToReceiveCapacity(activeStreamSlots, request.receive.size); + } + uint32_t counter = 0u; { std::lock_guard lock(m_mutex); counter = ++m_updateCounter; + m_completedStreamCount += activeStreamSlots.size(); } - constexpr uint32_t activeStreams = 0u; + const uint32_t activeStreams = static_cast(activeStreamSlots.size()); if (request.receive.address != 0u && request.receive.size >= m_bindings.activeStreamCountOffset + sizeof(activeStreams)) { @@ -76,10 +96,20 @@ namespace ps2x::iop::detail (void)m_host.writeGuest(address, &activeStreams, sizeof(activeStreams)); } + for (size_t index = 0u; index < activeStreamSlots.size(); ++index) + { + const uint32_t packedStream = activeStreamSlots[index] << kResponseStreamSlotShift; + const uint32_t offset = m_bindings.activeStreamCountOffset + static_cast(index) * kResponseRecordStride + kPackedStreamOffset; + const uint32_t address = request.receive.address + offset; + (void)m_host.writeGuest(address, &packedStream, sizeof(packedStream)); + } + + const uint32_t counterOffset = m_bindings.responseCounterOffset + + activeStreams * kResponseRecordStride; if (request.receive.address != 0u && - request.receive.size >= m_bindings.responseCounterOffset + sizeof(counter)) + request.receive.size >= counterOffset + sizeof(counter)) { - const uint32_t address = request.receive.address + m_bindings.responseCounterOffset; + const uint32_t address = request.receive.address + counterOffset; (void)m_host.writeGuest(address, &counter, sizeof(counter)); } @@ -90,14 +120,98 @@ namespace ps2x::iop::detail { std::lock_guard lock(m_mutex); metrics.push_back({"update_counter", m_updateCounter, false}); + metrics.push_back({"completed_streams", m_completedStreamCount, false}); } private: + [[nodiscard]] std::vector findQueuedPlayStreams(const RpcRequest &request) const + { + std::vector slots; + if (request.send.address == 0u || request.send.size < sizeof(uint16_t)) + { + return slots; + } + + uint16_t commandCount = 0u; + if (!m_host.readGuest(request.send.address, &commandCount, sizeof(commandCount))) + { + return slots; + } + + uint32_t offset = sizeof(commandCount); + for (uint32_t commandIndex = 0u; commandIndex < commandCount; ++commandIndex) + { + constexpr uint32_t headerSize = sizeof(uint16_t) * 2u; + if (offset > request.send.size || request.send.size - offset < headerSize) + { + break; + } + + std::array header{}; + if (!m_host.readGuest(request.send.address + offset, + header.data(), + sizeof(header))) + { + break; + } + offset += headerSize; + + const uint32_t argumentBytes = + static_cast(header[1]) * sizeof(uint16_t); + if (argumentBytes > request.send.size - offset) + { + break; + } + + if (header[0] == kPlayStreamCommand && header[1] >= 2u) + { + uint16_t encodedSlot = 0u; + if (m_host.readGuest(request.send.address + offset + sizeof(uint16_t), + &encodedSlot, + sizeof(encodedSlot))) + { + const uint32_t slot = + (encodedSlot >> kCommandStreamSlotShift) & kStreamSlotMask; + if (slot < kStreamSlotCount && + std::find(slots.begin(), slots.end(), slot) == slots.end()) + { + slots.push_back(slot); + } + } + } + + offset += argumentBytes; + } + return slots; + } + + void trimToReceiveCapacity(std::vector &slots, uint32_t receiveSize) const + { + size_t count = 0u; + for (; count < slots.size(); ++count) + { + const uint64_t recordOffset = + static_cast(m_bindings.activeStreamCountOffset) + + static_cast(count) * kResponseRecordStride + + kPackedStreamOffset; + const uint64_t counterOffset = + static_cast(m_bindings.responseCounterOffset) + + static_cast(count + 1u) * kResponseRecordStride; + if (recordOffset + sizeof(uint32_t) > receiveSize || + counterOffset + sizeof(uint32_t) > receiveSize) + { + break; + } + } + slots.resize(count); + } + IopHost &m_host; SoundUpdateStubBindings m_bindings; std::array m_sids; mutable std::mutex m_mutex; uint32_t m_updateCounter = 0u; + uint64_t m_completedStreamCount = 0u; }; } diff --git a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h index 4ffeaa0d1..ec219863d 100644 --- a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h +++ b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h @@ -34,8 +34,10 @@ namespace ps2recomp bool recompile(); void generateOutput(); void printReport() const; + const RecompilerReporter::Counters &reportCounters() const { return m_reporter.counters(); } static StubTarget resolveStubTarget(const std::string& name); + static bool IsCorrectnessCriticalFunctionName(const std::string &name); static size_t DiscoverAdditionalEntryPoints( std::vector &functions, std::unordered_map> &decodedFunctions, @@ -65,6 +67,7 @@ namespace ps2recomp std::unordered_set m_stubFunctions; std::unordered_set m_stubFunctionStarts; std::unordered_map m_stubHandlerBindingsByStart; + std::unordered_set m_correctnessCriticalFunctionStarts; std::map m_generatedStubs; std::unordered_map m_functionRenames; std::unordered_map> m_resumeEntryTargetsByOwner; @@ -74,6 +77,9 @@ namespace ps2recomp void discoverAdditionalEntryPoints(); bool shouldSkipFunction(const Function &function) const; bool isStubFunction(const Function &function) const; + bool isCorrectnessCriticalFunction(const Function &function) const; + bool hasResolvedStubHandler(const Function &function) const; + void collectCorrectnessCriticalFunctionStarts(); bool generateFunctionHeader(); bool generateStubHeader(); bool writeToFile(const std::string &path, const std::string &content); diff --git a/ps2xRecomp/include/ps2recomp/recompiler_reporter.h b/ps2xRecomp/include/ps2recomp/recompiler_reporter.h index 949b69d87..83620bb43 100644 --- a/ps2xRecomp/include/ps2recomp/recompiler_reporter.h +++ b/ps2xRecomp/include/ps2recomp/recompiler_reporter.h @@ -46,6 +46,8 @@ namespace ps2recomp size_t unhandledInstructions = 0; size_t indirectFallbackPromotions = 0; size_t indirectFallbackEntries = 0; + size_t correctnessCriticalGuestFallbacks = 0; + size_t correctnessCriticalFailures = 0; }; void progress(const std::string &message); @@ -63,6 +65,8 @@ namespace ps2recomp void recordDecodeFailure(); void recordAdditionalEntryPoints(size_t count); void recordGeneratedFunctions(size_t count); + void recordCorrectnessCriticalGuestFallback(); + void recordCorrectnessCriticalFailure(); void recordIndirectFallbackPromotion(const std::string &functionName, const std::vector &jumpAddresses, size_t promotedEntryCount); diff --git a/ps2xRecomp/src/lib/ps2_recompiler.cpp b/ps2xRecomp/src/lib/ps2_recompiler.cpp index df7e356da..f252ec483 100644 --- a/ps2xRecomp/src/lib/ps2_recompiler.cpp +++ b/ps2xRecomp/src/lib/ps2_recompiler.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -750,6 +751,7 @@ namespace ps2recomp m_stubFunctions.clear(); m_stubFunctionStarts.clear(); m_stubHandlerBindingsByStart.clear(); + m_correctnessCriticalFunctionStarts.clear(); for (const auto &name : m_config.skipFunctions) { @@ -809,6 +811,7 @@ namespace ps2recomp m_symbols = m_elfParser->extractSymbols(); m_sections = m_elfParser->getSections(); m_relocations = m_elfParser->getRelocations(); + collectCorrectnessCriticalFunctionStarts(); if (m_functions.empty()) { @@ -937,24 +940,81 @@ namespace ps2recomp size_t processedCount = 0; size_t failedCount = 0; + size_t correctnessCriticalFailureCount = 0; + + for (uint32_t initializerStart : m_correctnessCriticalFunctionStarts) + { + const auto functionIt = std::find_if( + m_functions.begin(), m_functions.end(), + [initializerStart](const Function &function) + { return function.start == initializerStart; }); + if (functionIt == m_functions.end()) + { + const auto bindingIt = m_stubHandlerBindingsByStart.find(initializerStart); + if (bindingIt != m_stubHandlerBindingsByStart.end() && + resolveStubTarget(bindingIt->second) != StubTarget::Unknown) + { + Function manualInitializer{}; + manualInitializer.name = "manual_initializer_" + bindingIt->second; + manualInitializer.start = initializerStart; + manualInitializer.end = initializerStart + 4u; + m_functions.push_back(std::move(manualInitializer)); + m_reporter.info( + "correctness-critical", + "Synthesized initializer entry for resolved handler '" + + bindingIt->second + "'"); + continue; + } + + ++correctnessCriticalFailureCount; + m_reporter.recordCorrectnessCriticalFailure(); + m_reporter.errorAt( + "correctness-critical", + ".ctors/.init_array", + initializerStart, + "Initializer table target has no discovered guest function or manual handler"); + } + } + for (auto &function : m_functions) { m_reporter.recordFunctionProcessed(); + const bool correctnessCritical = isCorrectnessCriticalFunction(function); if (isStubFunction(function)) { - function.isStub = true; - function.isSkipped = false; - m_reporter.recordFunctionStubbed(); - continue; + if (!correctnessCritical || hasResolvedStubHandler(function)) + { + function.isStub = true; + function.isSkipped = false; + m_reporter.recordFunctionStubbed(); + continue; + } + + m_reporter.recordCorrectnessCriticalGuestFallback(); + m_reporter.warningAt( + "correctness-critical", + function.name, + function.start, + "Unresolved initializer stub ignored; recompiling the original guest function"); } if (shouldSkipFunction(function)) { - function.isSkipped = true; - function.isStub = false; - m_reporter.recordFunctionSkipped(); - continue; + if (!correctnessCritical) + { + function.isSkipped = true; + function.isStub = false; + m_reporter.recordFunctionSkipped(); + continue; + } + + m_reporter.recordCorrectnessCriticalGuestFallback(); + m_reporter.warningAt( + "correctness-critical", + function.name, + function.start, + "Initializer skip ignored; recompiling the original guest function"); } if (!decodeFunction(function)) @@ -962,11 +1022,26 @@ namespace ps2recomp ++failedCount; m_reporter.recordDecodeFailure(); m_reporter.recordFunctionSkipped(); - m_reporter.warningAt("decode", function.name, function.start, "Skipping function due decode failure"); function.isSkipped = true; + if (correctnessCritical) + { + ++correctnessCriticalFailureCount; + m_reporter.recordCorrectnessCriticalFailure(); + m_reporter.errorAt( + "correctness-critical", + function.name, + function.start, + "Initializer could not be recompiled and has no resolved manual handler"); + } + else + { + m_reporter.warningAt("decode", function.name, function.start, "Skipping function due decode failure"); + } continue; } + function.isStub = false; + function.isSkipped = false; function.isRecompiled = true; m_reporter.recordFunctionRecompiled(); #if _DEBUG @@ -992,7 +1067,7 @@ namespace ps2recomp } m_reporter.progress("recompilation pass completed"); - return true; + return correctnessCriticalFailureCount == 0u; } catch (const std::exception &e) { @@ -1964,6 +2039,64 @@ namespace ps2recomp return ps2_runtime_calls::isStubName(function.name); } + bool PS2Recompiler::IsCorrectnessCriticalFunctionName(const std::string &name) + { + static constexpr const char *kPrefixes[] = { + "__ct__", + "__sinit_", + "_GLOBAL__sub_I_", + "GLOBAL__sub_I_", + "__static_initialization_and_destruction_0", + "__do_global_ctors", + }; + + for (const char *prefix : kPrefixes) + { + if (name.rfind(prefix, 0u) == 0u) + return true; + } + return false; + } + + bool PS2Recompiler::isCorrectnessCriticalFunction(const Function &function) const + { + return IsCorrectnessCriticalFunctionName(function.name) || + m_correctnessCriticalFunctionStarts.contains(function.start); + } + + bool PS2Recompiler::hasResolvedStubHandler(const Function &function) const + { + std::string handlerName = function.name; + const auto bindingIt = m_stubHandlerBindingsByStart.find(function.start); + if (bindingIt != m_stubHandlerBindingsByStart.end() && !bindingIt->second.empty()) + handlerName = bindingIt->second; + return resolveStubTarget(handlerName) != StubTarget::Unknown; + } + + void PS2Recompiler::collectCorrectnessCriticalFunctionStarts() + { + m_correctnessCriticalFunctionStarts.clear(); + for (const Section §ion : m_sections) + { + if (section.name != ".ctors" && + section.name != ".init_array" && + section.name != ".preinit_array") + { + continue; + } + if (section.data == nullptr || section.size < sizeof(uint32_t)) + continue; + + for (uint32_t offset = 0; offset + sizeof(uint32_t) <= section.size; offset += sizeof(uint32_t)) + { + uint32_t target = 0u; + std::memcpy(&target, section.data + offset, sizeof(target)); + if (target != 0u && target != 0xFFFFFFFFu) + m_correctnessCriticalFunctionStarts.insert(target); + } + } + } + bool PS2Recompiler::writeToFile(const std::string &path, const std::string &content) { std::ofstream file(path); diff --git a/ps2xRecomp/src/lib/recompiler_reporter.cpp b/ps2xRecomp/src/lib/recompiler_reporter.cpp index 8c08f96c6..85a5ea470 100644 --- a/ps2xRecomp/src/lib/recompiler_reporter.cpp +++ b/ps2xRecomp/src/lib/recompiler_reporter.cpp @@ -113,6 +113,18 @@ namespace ps2recomp m_counters.generatedFunctions += count; } + void RecompilerReporter::recordCorrectnessCriticalGuestFallback() + { + std::lock_guard lock(m_mutex); + ++m_counters.correctnessCriticalGuestFallbacks; + } + + void RecompilerReporter::recordCorrectnessCriticalFailure() + { + std::lock_guard lock(m_mutex); + ++m_counters.correctnessCriticalFailures; + } + void RecompilerReporter::recordIndirectFallbackPromotion(const std::string &functionName, const std::vector &jumpAddresses, size_t promotedEntryCount) @@ -183,6 +195,8 @@ namespace ps2recomp os << "Indirect fallback promotions: " << m_counters.indirectFallbackPromotions << " (" << m_counters.indirectFallbackEntries << " fallback entries)" << std::endl; os << "Unhandled instructions: " << m_counters.unhandledInstructions << std::endl; + os << "Correctness-critical guest fallbacks: " << m_counters.correctnessCriticalGuestFallbacks + << ", failures: " << m_counters.correctnessCriticalFailures << std::endl; size_t warnings = 0; size_t errors = 0; diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 36748400d..0a80a984b 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -15,6 +15,7 @@ option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime log 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) +option(PS2X_ENABLE_DEBUG_UI "Build the desktop runtime debug UI" ON) if(PS2X_ENABLE_SCCACHE) find_program(PS2X_SCCACHE_PROGRAM sccache) @@ -82,50 +83,50 @@ else() ) FetchContent_MakeAvailable(raylib) - if(NOT PS2X_IS_ANDROID) - FetchContent_Declare( - imgui - GIT_REPOSITORY https://github.com/ocornut/imgui.git - GIT_TAG "docking" - GIT_SHALLOW TRUE - ) - FetchContent_GetProperties(imgui) + if(PS2X_ENABLE_DEBUG_UI AND NOT PS2X_IS_ANDROID) + FetchContent_Declare( + imgui + GIT_REPOSITORY https://github.com/ocornut/imgui.git + GIT_TAG "v1.92.7-docking" + GIT_SHALLOW TRUE + ) + FetchContent_GetProperties(imgui) - if(NOT imgui_POPULATED) - FetchContent_Populate(imgui) - endif() + if(NOT imgui_POPULATED) + FetchContent_Populate(imgui) + endif() - set(PS2X_IMGUI_SOURCE_DIR "${imgui_SOURCE_DIR}") + set(PS2X_IMGUI_SOURCE_DIR "${imgui_SOURCE_DIR}") - add_library(imgui STATIC - "${PS2X_IMGUI_SOURCE_DIR}/imgui.cpp" - "${PS2X_IMGUI_SOURCE_DIR}/imgui_draw.cpp" - "${PS2X_IMGUI_SOURCE_DIR}/imgui_tables.cpp" - "${PS2X_IMGUI_SOURCE_DIR}/imgui_widgets.cpp" - "${PS2X_IMGUI_SOURCE_DIR}/imgui_demo.cpp" - ) - target_include_directories(imgui PUBLIC "${PS2X_IMGUI_SOURCE_DIR}") + add_library(imgui STATIC + "${PS2X_IMGUI_SOURCE_DIR}/imgui.cpp" + "${PS2X_IMGUI_SOURCE_DIR}/imgui_draw.cpp" + "${PS2X_IMGUI_SOURCE_DIR}/imgui_tables.cpp" + "${PS2X_IMGUI_SOURCE_DIR}/imgui_widgets.cpp" + "${PS2X_IMGUI_SOURCE_DIR}/imgui_demo.cpp" + ) + target_include_directories(imgui PUBLIC "${PS2X_IMGUI_SOURCE_DIR}") - FetchContent_Declare( - rlImGui - GIT_REPOSITORY https://github.com/raylib-extras/rlImGui.git - GIT_TAG "Raylib_5_5" - GIT_SHALLOW TRUE - ) - FetchContent_GetProperties(rlImGui) + FetchContent_Declare( + rlImGui + GIT_REPOSITORY https://github.com/raylib-extras/rlImGui.git + GIT_TAG "Raylib_5_5" + GIT_SHALLOW TRUE + ) + FetchContent_GetProperties(rlImGui) - if(NOT rlimgui_POPULATED) - FetchContent_Populate(rlImGui) - endif() + if(NOT rlimgui_POPULATED) + FetchContent_Populate(rlImGui) + endif() - set(PS2X_RLIMGUI_SOURCE_DIR "${rlimgui_SOURCE_DIR}") + set(PS2X_RLIMGUI_SOURCE_DIR "${rlimgui_SOURCE_DIR}") - add_library(rlImGui STATIC - "${PS2X_RLIMGUI_SOURCE_DIR}/rlImGui.cpp" - ) - target_include_directories(rlImGui PUBLIC "${PS2X_RLIMGUI_SOURCE_DIR}") - target_link_libraries(rlImGui PUBLIC raylib imgui) - endif() # NOT PS2X_IS_ANDROID + add_library(rlImGui STATIC + "${PS2X_RLIMGUI_SOURCE_DIR}/rlImGui.cpp" + ) + target_include_directories(rlImGui PUBLIC "${PS2X_RLIMGUI_SOURCE_DIR}") + target_link_libraries(rlImGui PUBLIC raylib imgui) + endif() endif() add_library(ps2_host_backend INTERFACE) @@ -509,7 +510,7 @@ target_link_libraries(ps2EntryRunner ps2_runtime ) -if(NOT PS2X_IS_VITA AND NOT PS2X_IS_ANDROID) +if(PS2X_ENABLE_DEBUG_UI AND NOT PS2X_IS_VITA AND NOT PS2X_IS_ANDROID) target_sources(ps2EntryRunner PRIVATE src/lib/ps2_debug_panel.cpp ) diff --git a/ps2xRuntime/include/runtime/ps2_gs_gpu.h b/ps2xRuntime/include/runtime/ps2_gs_gpu.h index 1991f950d..ef4429067 100644 --- a/ps2xRuntime/include/runtime/ps2_gs_gpu.h +++ b/ps2xRuntime/include/runtime/ps2_gs_gpu.h @@ -462,8 +462,9 @@ class GS using WriteVramFunc = std::function; using ReadVramFunc = std::function; - std::array m_read_vram_funcs{ }; - std::array m_write_vram_funcs{ }; + static constexpr size_t kPsmHandlerCount = 1u << 6u; + std::array m_read_vram_funcs{ }; + std::array m_write_vram_funcs{ }; }; inline u32 GS::ReadVram(u32 psm, u32 base, u32 bw, u32 x, u32 y) const diff --git a/ps2xRuntime/include/runtime/ps2_vu1.h b/ps2xRuntime/include/runtime/ps2_vu1.h index cdbdec9c7..8b5930f94 100644 --- a/ps2xRuntime/include/runtime/ps2_vu1.h +++ b/ps2xRuntime/include/runtime/ps2_vu1.h @@ -59,6 +59,16 @@ class VU1Interpreter bool lowerBeforeUpper = false; }; + struct FlagPipelineEntry + { + uint32_t mac = 0; + uint32_t status = 0; + bool valid = false; + bool writesSticky = false; + }; + + static constexpr uint32_t kFlagPipelineLatency = 4u; + VU1State m_state; std::vector m_decodedCodeCache; const uint8_t *m_cachedVuCode = nullptr; @@ -66,6 +76,11 @@ class VU1Interpreter uint32_t m_cachedCodeSize = 0; uint64_t m_cachedCodeGeneration = 0; bool m_decodedCodeCacheValid = false; + FlagPipelineEntry m_flagPipeline[kFlagPipelineLatency]{}; + FlagPipelineEntry m_pendingFlagUpdate{}; + uint32_t m_flagPipelineHead = 0; + uint32_t m_workingMac = 0; + uint32_t m_workingStatus = 0; void run(uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, @@ -82,6 +97,15 @@ class VU1Interpreter 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 updateFmacFlags(float *result, uint8_t dest); + void queueFsset(uint16_t immediate); + void beginFlagPipelineCycle(); + void endFlagPipelineCycle(); + void flushFlagPipeline(); + void commitFlagPipelineEntry(FlagPipelineEntry &entry); + bool hasPendingFlagPipelineEntries() const; float broadcast(const float *vf, uint8_t bc); }; diff --git a/ps2xRuntime/src/lib/ps2_gs_gpu.cpp b/ps2xRuntime/src/lib/ps2_gs_gpu.cpp index f1f2bde9f..0defc2a5f 100644 --- a/ps2xRuntime/src/lib/ps2_gs_gpu.cpp +++ b/ps2xRuntime/src/lib/ps2_gs_gpu.cpp @@ -365,7 +365,7 @@ GS::GS() InitLookupTables(); - for (usz i = 0; i < 0x3F; ++i) + for (usz i = 0; i < m_read_vram_funcs.size(); ++i) { switch (i) { diff --git a/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp b/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp index 9c143559e..d10b1f2c0 100644 --- a/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp +++ b/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp @@ -132,29 +132,67 @@ namespace } } - struct AlphaTestResult + struct PixelWriteMask { - bool writeFramebuffer; - bool preserveDestinationAlpha; + bool writeRgb = true; + bool writeAlpha = true; + bool writeDepth = true; + + bool writesFramebuffer() const + { + return writeRgb || writeAlpha; + } + + bool writesAnything() const + { + return writesFramebuffer() || writeDepth; + } }; - AlphaTestResult classifyAlphaTest(uint64_t testReg, uint8_t alpha) + PixelWriteMask classifyAlphaTest(uint64_t testReg, uint8_t alpha, uint8_t framePsm) { const bool pass = passesAlphaTest(testReg, alpha); if (pass) - return {true, false}; + return {}; // TEST.AFAIL controls what happens when the alpha comparison fails. switch (static_cast((testReg >> 12) & 0x3u)) { case 1: // FB_ONLY - return {true, false}; + return {true, true, false}; + case 2: // ZB_ONLY + return {false, false, true}; case 3: // RGB_ONLY - return {true, true}; + // RGB_ONLY is only distinct for RGBA32. The GS treats it as + // FB_ONLY for RGB24 and RGBA16 framebuffers. + if (framePsm == GS_PSM_CT32) + return {true, false, false}; + return {true, true, false}; case 0: // KEEP - case 2: // ZB_ONLY default: - return {false, false}; + return {false, false, false}; + } + } + + bool passesDestinationAlphaTest(uint64_t testReg, uint8_t framePsm, uint32_t rawFramebufferPixel) + { + const bool date = ((testReg >> 14) & 0x1u) != 0u; + if (!date) + return true; + + const bool datm = ((testReg >> 15) & 0x1u) != 0u; + switch (framePsm) + { + case GS_PSM_CT32: + return (((rawFramebufferPixel >> 31) & 0x1u) != 0u) == datm; + case GS_PSM_CT16: + case GS_PSM_CT16S: + return (((rawFramebufferPixel >> 15) & 0x1u) != 0u) == datm; + case GS_PSM_CT24: + // RGB24 has no destination alpha, so DATE always passes. + return true; + default: + return true; } } @@ -415,38 +453,51 @@ void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, y < ctx.scissor.y0 || y > ctx.scissor.y1) return; - const AlphaTestResult alphaTest = classifyAlphaTest(ctx.test, a); - - if (!alphaTest.writeFramebuffer) - return; - - u8* vram = gs->m_vram; - const u32 fbp = GSInternal::framePageBaseToBlock(ctx.frame.fbp); const u32 fbw = std::max(ctx.frame.fbw, 1u); const u32 fpsm = ctx.frame.psm; - const u32 fmsk = ctx.frame.fbmsk; const u32 zbp = GSInternal::framePageBaseToBlock(ctx.zbuf.zbp); const u32 zpsm = ctx.zbuf.psm; + const PixelWriteMask writeMask = classifyAlphaTest(ctx.test, a, static_cast(fpsm)); + if (!writeMask.writesAnything()) + return; + const bool alphaBlendEnabled = gs->m_prim.abe; - const bool destinationAlpha = alphaTest.preserveDestinationAlpha; + const bool preserveDestinationAlpha = + writeMask.writeRgb && !writeMask.writeAlpha && fpsm == GS_PSM_CT32; + const bool destinationAlphaTestNeedsRead = + ((ctx.test >> 14) & 0x1u) != 0u && + (fpsm == GS_PSM_CT32 || fpsm == GS_PSM_CT16 || fpsm == GS_PSM_CT16S); // small optimization, avoid reading the framebuffer for simple draws // TODO: only one address lookup for rmw - const bool frmw = (ctx.frame.fbmsk != 0) || alphaBlendEnabled || destinationAlpha; + const bool frmw = + destinationAlphaTestNeedsRead || + (writeMask.writesFramebuffer() && + ((ctx.frame.fbmsk != 0) || alphaBlendEnabled || preserveDestinationAlpha)); + u32 rawFramebufferPixel = 0; u32 fbrgba = 0; if (frmw) { - fbrgba = gs->ReadVram(fpsm, fbp, fbw, x, y); + rawFramebufferPixel = gs->ReadVram(fpsm, fbp, fbw, x, y); + fbrgba = rawFramebufferPixel; if (bitsPerPixel(fpsm) == 16) { fbrgba = Rgba5551ToRgba8888(fbrgba); } + else if (fpsm == GS_PSM_CT24) + { + // The GS supplies 0x80 as destination alpha for RGB24 blending. + fbrgba |= 0x80000000u; + } } + if (!passesDestinationAlphaTest(ctx.test, static_cast(fpsm), rawFramebufferPixel)) + return; + uint ztest_method = (ctx.test >> 17) & 3; @@ -472,81 +523,81 @@ void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, return; } - const u8 srcR = r; - const u8 srcG = g; - const u8 srcB = b; - - if (gs->m_prim.abe) + if (writeMask.writesFramebuffer()) { - uint8_t dr = fbrgba & 0xFF; - uint8_t dg = (fbrgba >> 8) & 0xFF; - uint8_t db = (fbrgba >> 16) & 0xFF; - uint8_t da = (fbrgba >> 24) & 0xFF; + const u8 srcR = r; + const u8 srcG = g; + const u8 srcB = b; - // PABE disables alpha blending when the source alpha MSB is clear. - if (!(gs->m_pabe && (a & 0x80u) == 0u)) + if (gs->m_prim.abe) { - uint64_t alphaReg = ctx.alpha; - uint8_t asel = alphaReg & 3; - uint8_t bsel = (alphaReg >> 2) & 3; - uint8_t csel = (alphaReg >> 4) & 3; - uint8_t dsel = (alphaReg >> 6) & 3; - uint8_t fix = static_cast((alphaReg >> 32) & 0xFF); - - auto pickRGB = [&](uint8_t sel, int cs, int cd) -> int + uint8_t dr = fbrgba & 0xFF; + uint8_t dg = (fbrgba >> 8) & 0xFF; + uint8_t db = (fbrgba >> 16) & 0xFF; + uint8_t da = (fbrgba >> 24) & 0xFF; + + // PABE disables alpha blending when the source alpha MSB is clear. + if (!(gs->m_pabe && (a & 0x80u) == 0u)) + { + uint64_t alphaReg = ctx.alpha; + uint8_t asel = alphaReg & 3; + uint8_t bsel = (alphaReg >> 2) & 3; + uint8_t csel = (alphaReg >> 4) & 3; + uint8_t dsel = (alphaReg >> 6) & 3; + uint8_t fix = static_cast((alphaReg >> 32) & 0xFF); + + auto pickRGB = [&](uint8_t sel, int cs, int cd) -> int + { + if (sel == 0) + return cs; + if (sel == 1) + return cd; + return 0; + }; + int cAlpha = (csel == 0) ? a : (csel == 1) ? da + : fix; + + r = clampU8(((pickRGB(asel, r, dr) - pickRGB(bsel, r, dr)) * cAlpha >> 7) + pickRGB(dsel, r, dr)); + g = clampU8(((pickRGB(asel, g, dg) - pickRGB(bsel, g, dg)) * cAlpha >> 7) + pickRGB(dsel, g, dg)); + b = clampU8(((pickRGB(asel, b, db) - pickRGB(bsel, b, db)) * cAlpha >> 7) + pickRGB(dsel, b, db)); + } + else { - if (sel == 0) - return cs; - if (sel == 1) - return cd; - return 0; - }; - int cAlpha = (csel == 0) ? a : (csel == 1) ? da - : fix; - - r = clampU8(((pickRGB(asel, r, dr) - pickRGB(bsel, r, dr)) * cAlpha >> 7) + pickRGB(dsel, r, dr)); - g = clampU8(((pickRGB(asel, g, dg) - pickRGB(bsel, g, dg)) * cAlpha >> 7) + pickRGB(dsel, g, dg)); - b = clampU8(((pickRGB(asel, b, db) - pickRGB(bsel, b, db)) * cAlpha >> 7) + pickRGB(dsel, b, db)); + r = srcR; + g = srcG; + b = srcB; + } } - else + + if (writeMask.writeAlpha && + (ctx.fba & 0x1ull) != 0ull && + ctx.frame.psm != GS_PSM_CT24) { - r = srcR; - g = srcG; - b = srcB; + a = static_cast(a | 0x80u); } - } - u32 fbmask = ctx.frame.fbmsk; - bool zmask = ctx.zbuf.zmask; + u32 pixel = pack32(r, g, b, a); - if (!alphaTest.preserveDestinationAlpha && - (ctx.fba & 0x1ull) != 0ull && - ctx.frame.psm != GS_PSM_CT24) - { - a = static_cast(a | 0x80u); - } + if (ctx.frame.fbmsk != 0) + { + pixel = (pixel & ~ctx.frame.fbmsk) | (fbrgba & ctx.frame.fbmsk); + } - u32 pixel = pack32(r, g, b, a); + if (preserveDestinationAlpha) + { + pixel = (pixel & 0x00FFFFFFu) | (fbrgba & 0xFF000000u); + } - if (fbmask != 0) - { - pixel = (pixel & ~fbmask) | (fbrgba & fbmask); - } + // format conversion + if (bitsPerPixel(fpsm) == 16) + { + pixel = Rgba8888ToRgba5551(pixel); + } - if (alphaTest.preserveDestinationAlpha) - { - pixel = (pixel & 0x00FFFFFFu) | (fbrgba & 0xFF000000u); + gs->WriteVram(fpsm, fbp, fbw, x, y, pixel); } - - // format conversion - if (bitsPerPixel(fpsm) == 16) - { - pixel = Rgba8888ToRgba5551(pixel); - } - - gs->WriteVram(fpsm, fbp, fbw, x, y, pixel); - if (!zmask) + if (writeMask.writeDepth && !ctx.zbuf.zmask) { gs->WriteVram(zpsm, zbp, fbw, x, y, z); } diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index b79e386a5..6f6584b78 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -2159,9 +2159,12 @@ void PS2Runtime::yieldGuestExecutionAfterWake() bool PS2Runtime::shouldPreemptGuestExecution() { + constexpr uint32_t kContendedYieldInterval = 1024u; + constexpr uint32_t kUncontendedYieldInterval = 16384u; + thread_local uint32_t s_backEdgeYieldCounter = 0u; const uint32_t waiterCount = m_guestExecutionWaiters.load(std::memory_order_acquire); - const uint32_t yieldInterval = (waiterCount != 0u) ? 64u : 100u; + const uint32_t yieldInterval = (waiterCount != 0u) ? kContendedYieldInterval : kUncontendedYieldInterval; if (++s_backEdgeYieldCounter < yieldInterval) { return false; diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp index 6f24894c5..90fdac927 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp @@ -14,6 +14,11 @@ void VU1Interpreter::reset() std::memset(&m_state, 0, sizeof(m_state)); m_state.vf[0][3] = 1.0f; // VF0.w = 1.0 m_state.q = 1.0f; + std::memset(m_flagPipeline, 0, sizeof(m_flagPipeline)); + m_pendingFlagUpdate = {}; + m_flagPipelineHead = 0; + m_workingMac = 0; + m_workingStatus = 0; } float VU1Interpreter::broadcast(const float *vf, uint8_t bc) @@ -38,6 +43,139 @@ void VU1Interpreter::applyDestAcc(const float *result, uint8_t dest) applyDest(m_state.acc, result, dest); } +void VU1Interpreter::updateFmacFlags(float *result, uint8_t dest) +{ + uint32_t mac = 0u; + + for (uint32_t component = 0; component < 4u; ++component) + { + const uint32_t laneBit = 1u << (3u - component); + if ((dest & laneBit) == 0u) + continue; + + uint32_t bits = 0u; + std::memcpy(&bits, &result[component], sizeof(bits)); + const uint32_t exponent = (bits >> 23) & 0xFFu; + const uint32_t magnitude = bits & 0x7FFFFFFFu; + const uint32_t sign = bits & 0x80000000u; + + if (sign != 0u) + mac |= laneBit << 4; + + if (magnitude == 0u) + { + mac |= laneBit; + } + else if (exponent == 0u) + { + // VU FMAC units flush denormals to signed zero and report Z+U. + mac |= laneBit; + mac |= laneBit << 8; + bits = sign; + std::memcpy(&result[component], &bits, sizeof(bits)); + } + else if (exponent == 0xFFu) + { + // Infinity and NaN are represented as signed maximum finite values. + mac |= laneBit << 12; + bits = sign | 0x7F7FFFFFu; + std::memcpy(&result[component], &bits, sizeof(bits)); + } + } + + uint32_t status = 0u; + if ((mac & 0x000Fu) != 0u) + status |= 0x1u; + if ((mac & 0x00F0u) != 0u) + status |= 0x2u; + if ((mac & 0x0F00u) != 0u) + status |= 0x4u; + if ((mac & 0xF000u) != 0u) + status |= 0x8u; + + m_workingMac = mac; + m_workingStatus = status; + m_pendingFlagUpdate = {m_workingMac, m_workingStatus, true, false}; +} + +void VU1Interpreter::applyFmacDest(float *dst, float *result, uint8_t dest) +{ + updateFmacFlags(result, dest); + applyDest(dst, result, dest); +} + +void VU1Interpreter::applyFmacDestAcc(float *result, uint8_t dest) +{ + updateFmacFlags(result, dest); + applyDestAcc(result, dest); +} + +void VU1Interpreter::queueFsset(uint16_t immediate) +{ + m_workingStatus = + (static_cast(immediate) & 0xFC0u) | + (m_workingStatus & 0x3Fu); + m_pendingFlagUpdate = {m_workingMac, m_workingStatus, true, true}; +} + +void VU1Interpreter::commitFlagPipelineEntry(FlagPipelineEntry &entry) +{ + if (!entry.valid) + return; + + if (entry.writesSticky) + { + m_state.status = + (m_state.status & 0x30u) | + (entry.status & 0xFC0u) | + (entry.status & 0xFu); + } + else + { + const uint32_t current = entry.status & 0xFu; + m_state.status = + (m_state.status & 0xFF0u) | + current | + (current << 6); + } + m_state.mac = entry.mac; + entry = {}; +} + +void VU1Interpreter::beginFlagPipelineCycle() +{ + commitFlagPipelineEntry(m_flagPipeline[m_flagPipelineHead]); + m_pendingFlagUpdate = {}; +} + +void VU1Interpreter::endFlagPipelineCycle() +{ + if (m_pendingFlagUpdate.valid) + m_flagPipeline[m_flagPipelineHead] = m_pendingFlagUpdate; + m_pendingFlagUpdate = {}; + m_flagPipelineHead = (m_flagPipelineHead + 1u) % kFlagPipelineLatency; +} + +void VU1Interpreter::flushFlagPipeline() +{ + for (uint32_t i = 0; i < kFlagPipelineLatency; ++i) + { + commitFlagPipelineEntry(m_flagPipeline[m_flagPipelineHead]); + m_flagPipelineHead = (m_flagPipelineHead + 1u) % kFlagPipelineLatency; + } + m_pendingFlagUpdate = {}; +} + +bool VU1Interpreter::hasPendingFlagPipelineEntries() const +{ + for (const FlagPipelineEntry &entry : m_flagPipeline) + { + if (entry.valid) + return true; + } + return false; +} + VU1Interpreter::DecodedInstructionPair VU1Interpreter::decodeInstructionPair(const uint8_t *vuCode, uint32_t pc) const { DecodedInstructionPair decoded; @@ -78,7 +216,7 @@ VU1Interpreter::DecodedInstructionPair VU1Interpreter::getDecodedInstructionPair return decodeInstructionPair(vuCode, pc); } - const bool trackedVu1Code = vuCode == memory->getVU1Code(); + const bool trackedVu1Code = memory != nullptr && vuCode == memory->getVU1Code(); if (!trackedVu1Code) { return decodeInstructionPair(vuCode, pc); @@ -135,11 +273,19 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t maxCycles) { + if (!hasPendingFlagPipelineEntries()) + { + m_workingMac = m_state.mac; + m_workingStatus = m_state.status; + } + + bool programEnded = false; for (uint32_t cycle = 0; cycle < maxCycles; ++cycle) { if (m_state.pc + 8 > codeSize) break; + beginFlagPipelineCycle(); const DecodedInstructionPair decoded = getDecodedInstructionPairForPc(vuCode, codeSize, memory, m_state.pc); // LOI is controlled by the upper I-bit. The lower word is the float immediate. @@ -163,6 +309,7 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, execUpper(decoded.upper); execLower(decoded.lower, vuData, dataSize, gs, memory, decoded.upper); } + endFlagPipelineCycle(); // Enforce VF0 invariant m_state.vf[0][0] = 0.0f; @@ -193,9 +340,15 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, } if (m_state.ebit) + { + programEnded = true; break; + } if (decoded.eBit) m_state.ebit = true; } + + if (programEnded) + flushFlagPipeline(); } diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp index f89ba7bc4..b5520973b 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp @@ -157,28 +157,32 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x14: // FSEQ { - uint16_t imm12 = instr & 0xFFF; - if (1 != 0) - m_state.vi[1] = ((m_state.status & 0xFFF) == imm12) ? 1 : 0; + const uint8_t it = VIT(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 { - m_state.status = (instr >> 6) & 0xFC0; + const uint16_t imm12 = static_cast((((instr >> 21) & 0x1u) << 11) | (instr & 0x7FFu)); + queueFsset(imm12); return; } case 0x16: // FSAND { - uint16_t imm12 = instr & 0xFFF; - if (1 != 0) - m_state.vi[1] = (int32_t)(m_state.status & imm12); + const uint8_t it = VIT(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 { - uint16_t imm12 = instr & 0xFFF; - if (1 != 0) - m_state.vi[1] = ((m_state.status | imm12) == 0xFFF) ? 1 : 0; + const uint8_t it = VIT(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: // FMAND diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp index 27dde2d81..9e1adcafc 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp @@ -31,7 +31,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] + bc; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; } case 0x04: @@ -42,7 +42,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] - bc; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; } case 0x08: @@ -53,7 +53,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * bc; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; } case 0x0C: @@ -64,7 +64,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * bc; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; } case 0x10: @@ -97,13 +97,13 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, op & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] * bc; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; } case 0x1C: // MULq for (int c = 0; c < 4; c++) result[c] = vs[c] * m_state.q; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x1D: // MAXi for (int c = 0; c < 4; c++) @@ -113,7 +113,7 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x1E: // MULi for (int c = 0; c < 4; c++) result[c] = vs[c] * m_state.i; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x1F: // MINIi for (int c = 0; c < 4; c++) @@ -123,57 +123,57 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x20: // ADDq for (int c = 0; c < 4; c++) result[c] = vs[c] + m_state.q; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x21: // MADDq for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * m_state.q; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x22: // ADDi for (int c = 0; c < 4; c++) result[c] = vs[c] + m_state.i; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x23: // MADDi for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * m_state.i; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x24: // SUBq for (int c = 0; c < 4; c++) result[c] = vs[c] - m_state.q; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x25: // MSUBq for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * m_state.q; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x26: // SUBi for (int c = 0; c < 4; c++) result[c] = vs[c] - m_state.i; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x27: // MSUBi for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * m_state.i; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x28: // ADD for (int c = 0; c < 4; c++) result[c] = vs[c] + vt[c]; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x29: // MADD for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * vt[c]; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x2A: // MUL for (int c = 0; c < 4; c++) result[c] = vs[c] * vt[c]; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x2B: // MAX for (int c = 0; c < 4; c++) @@ -183,19 +183,19 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x2C: // SUB for (int c = 0; c < 4; c++) result[c] = vs[c] - vt[c]; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x2D: // MSUB for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * vt[c]; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x2E: // OPMSUB result[0] = m_state.acc[0] - vs[1] * vt[2]; result[1] = m_state.acc[1] - vs[2] * vt[0]; result[2] = m_state.acc[2] - vs[0] * vt[1]; result[3] = 0.0f; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x2F: // MINI for (int c = 0; c < 4; c++) @@ -225,7 +225,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, specialOp & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] + bc; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; } case 0x04: @@ -236,7 +236,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, specialOp & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] - bc; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; } case 0x08: @@ -247,7 +247,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, specialOp & 3); for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * bc; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; } case 0x0C: @@ -258,7 +258,7 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, specialOp & 3); for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * bc; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; } case 0x10: // ITOF0 @@ -337,13 +337,13 @@ void VU1Interpreter::execUpper(uint32_t instr) float bc = broadcast(vt, specialOp & 3); for (int c = 0; c < 4; c++) result[c] = vs[c] * bc; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; } case 0x1C: // MULAq for (int c = 0; c < 4; c++) result[c] = vs[c] * m_state.q; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x1D: // ABS for (int c = 0; c < 4; c++) @@ -353,7 +353,7 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x1E: // MULAi for (int c = 0; c < 4; c++) result[c] = vs[c] * m_state.i; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x1F: // CLIP { @@ -371,74 +371,74 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x20: // ADDAq for (int c = 0; c < 4; c++) result[c] = vs[c] + m_state.q; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x21: // MADDAq for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * m_state.q; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x22: // ADDAi for (int c = 0; c < 4; c++) result[c] = vs[c] + m_state.i; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x23: // MADDAi for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * m_state.i; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x24: // SUBAq for (int c = 0; c < 4; c++) result[c] = vs[c] - m_state.q; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x25: // MSUBAq for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * m_state.q; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x26: // SUBAi for (int c = 0; c < 4; c++) result[c] = vs[c] - m_state.i; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x27: // MSUBAi for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * m_state.i; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x28: // ADDA for (int c = 0; c < 4; c++) result[c] = vs[c] + vt[c]; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x29: // MADDA for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] + vs[c] * vt[c]; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x2A: // MULA for (int c = 0; c < 4; c++) result[c] = vs[c] * vt[c]; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x2C: // SUBA for (int c = 0; c < 4; c++) result[c] = vs[c] - vt[c]; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x2D: // MSUBA for (int c = 0; c < 4; c++) result[c] = m_state.acc[c] - vs[c] * vt[c]; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x2E: // OPMULA result[0] = vs[1] * vt[2]; result[1] = vs[2] * vt[0]; result[2] = vs[0] * vt[1]; result[3] = 0.0f; - applyDestAcc(result, dest); + applyFmacDestAcc(result, dest); return; case 0x2F: case 0x30: // NOP diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 4c3e8ec1c..654481274 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -297,6 +297,57 @@ namespace t.Equals(probe, expectedBase, message); runtime.guestFree(probe); } + + struct GsPixelTestResult + { + uint32_t framebuffer = 0u; + uint32_t depth = 0u; + }; + + GsPixelTestResult drawGsPixelForTests(uint8_t framePsm, + uint64_t testReg, + bool zmask, + uint32_t initialFramebuffer, + uint32_t initialDepth, + uint8_t sourceAlpha) + { + constexpr uint32_t kFrameBlock = 0u; + constexpr uint32_t kDepthBlock = 32u; + constexpr uint32_t kSourceDepth = 0x22222222u; + + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + gs.WriteVram(framePsm, kFrameBlock, 1u, 0u, 0u, initialFramebuffer); + gs.WriteVram(GS_PSM_Z32, kDepthBlock, 1u, 0u, 0u, initialDepth); + + const uint64_t frame = + (1ull << 16) | + (static_cast(framePsm) << 24); + const uint64_t zbuf = + 1ull | + (static_cast(zmask ? 1u : 0u) << 32); + const uint64_t rgbaq = + (0x12ull << 0) | + (0x34ull << 8) | + (0x56ull << 16) | + (static_cast(sourceAlpha) << 24) | + (0x3F800000ull << 32); + + gs.writeRegister(GS_REG_FRAME_1, frame); + gs.writeRegister(GS_REG_ZBUF_1, zbuf); + gs.writeRegister(GS_REG_SCISSOR_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, testReg); + gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT)); + gs.writeRegister(GS_REG_RGBAQ, rgbaq); + gs.writeRegister(GS_REG_XYZ2, static_cast(kSourceDepth) << 32); + + return { + gs.ReadVram(framePsm, kFrameBlock, 1u, 0u, 0u), + gs.ReadVram(GS_PSM_Z32, kDepthBlock, 1u, 0u, 0u), + }; + } } void register_ps2_gs_tests() @@ -1557,6 +1608,20 @@ void register_ps2_gs_tests() t.Equals(regs.display2, display2, "A+D should write GS DISPLAY2"); }); + tc.Run("reserved PSM 0x3F uses null VRAM handlers", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0xA5u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + t.Equals(gs.ReadVram(0x3Fu, 0u, 1u, 0u, 0u), 0u, + "reserved PSM reads should use the null handler"); + + gs.WriteVram(0x3Fu, 0u, 1u, 0u, 0u, 0x0005180Bu); + t.Equals(static_cast(vram[0]), 0xA5u, + "reserved PSM writes should leave VRAM unchanged"); + }); + tc.Run("PSMT4 address mapping matches GS manual layout", [](TestCase &t) { constexpr uint32_t kBaseBlock = 0u; @@ -2975,97 +3040,158 @@ void register_ps2_gs_tests() "linear filtering should preserve the shared opaque alpha from the CLUT entries"); }); - tc.Run("GS alpha test AFAIL framebuffer-only still writes the pixel", [](TestCase &t) + tc.Run("GS alpha-test AFAIL independently masks framebuffer and depth", [](TestCase &t) { - std::vector vram(PS2_GS_VRAM_SIZE, 0u); - GS gs; - gs.init(vram.data(), static_cast(vram.size()), nullptr); - - constexpr uint64_t kFrame = - (0ull << 0) | - (1ull << 16) | - (static_cast(GS_PSM_CT32) << 24); - constexpr uint64_t kZbuf = (1ull << 32); - constexpr uint64_t kScissor = - (0ull << 0) | - (0ull << 16) | - (0ull << 32) | - (0ull << 48); - constexpr uint64_t kTest = + constexpr uint32_t kInitialFramebuffer = 0xAB030201u; + constexpr uint32_t kInitialDepth = 0x11111111u; + constexpr uint64_t kTestBase = 1ull | // ATE - (5ull << 1) | // ATST = GEQUAL + (5ull << 1) | // ATST = GEQUAL (0x80ull << 4) | // AREF - (1ull << 12) | // AFAIL = FB_ONLY + (1ull << 16) | // ZTE (1ull << 17); // ZTST = ALWAYS - constexpr uint64_t kPrim = - static_cast(GS_PRIM_POINT); - constexpr uint64_t kRgbaq = - (0x12ull << 0) | - (0x34ull << 8) | - (0x56ull << 16) | - (0x00ull << 24) | - (0x3F800000ull << 32); // q = 1.0f - gs.writeRegister(GS_REG_FRAME_1, kFrame); - gs.writeRegister(GS_REG_ZBUF_1, kZbuf); - gs.writeRegister(GS_REG_SCISSOR_1, kScissor); - gs.writeRegister(GS_REG_TEST_1, kTest); - gs.writeRegister(GS_REG_PRIM, kPrim); - gs.writeRegister(GS_REG_RGBAQ, kRgbaq); - gs.writeRegister(GS_REG_XYZ2, 0ull); - - uint32_t pixel = 0u; - std::memcpy(&pixel, vram.data(), sizeof(pixel)); - t.Equals(pixel, 0x00563412u, - "AFAIL=FB_ONLY should still update the framebuffer when the alpha test fails"); + const GsPixelTestResult keep = + drawGsPixelForTests(GS_PSM_CT32, kTestBase | (0ull << 12), false, + kInitialFramebuffer, kInitialDepth, 0x00u); + t.Equals(keep.framebuffer, kInitialFramebuffer, + "AFAIL=KEEP should preserve the framebuffer"); + t.Equals(keep.depth, kInitialDepth, + "AFAIL=KEEP should preserve depth"); + + const GsPixelTestResult framebufferOnly = + drawGsPixelForTests(GS_PSM_CT32, kTestBase | (1ull << 12), false, + kInitialFramebuffer, kInitialDepth, 0x00u); + t.Equals(framebufferOnly.framebuffer, 0x00563412u, + "AFAIL=FB_ONLY should update RGBA"); + t.Equals(framebufferOnly.depth, kInitialDepth, + "AFAIL=FB_ONLY should preserve depth"); + + const GsPixelTestResult depthOnly = + drawGsPixelForTests(GS_PSM_CT32, kTestBase | (2ull << 12), false, + kInitialFramebuffer, kInitialDepth, 0x00u); + t.Equals(depthOnly.framebuffer, kInitialFramebuffer, + "AFAIL=ZB_ONLY should preserve the framebuffer"); + t.Equals(depthOnly.depth, 0x22222222u, + "AFAIL=ZB_ONLY should update depth"); + + const GsPixelTestResult rgbOnly = + drawGsPixelForTests(GS_PSM_CT32, kTestBase | (3ull << 12), false, + kInitialFramebuffer, kInitialDepth, 0x00u); + t.Equals(rgbOnly.framebuffer, 0xAB563412u, + "AFAIL=RGB_ONLY should preserve destination alpha on CT32"); + t.Equals(rgbOnly.depth, kInitialDepth, + "AFAIL=RGB_ONLY should preserve depth"); }); - tc.Run("GS alpha test AFAIL RGB-only preserves destination alpha", [](TestCase &t) + tc.Run("GS RGB_ONLY falls back to FB_ONLY outside CT32", [](TestCase &t) { - std::vector vram(PS2_GS_VRAM_SIZE, 0u); - GS gs; - gs.init(vram.data(), static_cast(vram.size()), nullptr); - - constexpr uint64_t kFrame = - (0ull << 0) | - (1ull << 16) | - (static_cast(GS_PSM_CT32) << 24); - constexpr uint64_t kZbuf = (1ull << 32); - constexpr uint64_t kScissor = - (0ull << 0) | - (0ull << 16) | - (0ull << 32) | - (0ull << 48); + constexpr uint32_t kInitialDepth = 0x11111111u; constexpr uint64_t kTest = - 1ull | // ATE - (5ull << 1) | // ATST = GEQUAL - (0x80ull << 4) | // AREF - (3ull << 12) | // AFAIL = RGB_ONLY - (1ull << 17); // ZTST = ALWAYS - constexpr uint64_t kPrim = - static_cast(GS_PRIM_POINT); - constexpr uint64_t kRgbaq = - (0x12ull << 0) | - (0x34ull << 8) | - (0x56ull << 16) | - (0x00ull << 24) | - (0x3F800000ull << 32); // q = 1.0f - constexpr uint32_t kExisting = 0xAB030201u; - - std::memcpy(vram.data(), &kExisting, sizeof(kExisting)); + 1ull | + (5ull << 1) | + (0x80ull << 4) | + (3ull << 12) | + (1ull << 16) | + (1ull << 17); + + const GsPixelTestResult ct24 = + drawGsPixelForTests(GS_PSM_CT24, kTest, false, + 0x00030201u, kInitialDepth, 0x00u); + t.Equals(ct24.framebuffer, 0x00563412u, + "RGB_ONLY should write the full CT24 framebuffer pixel"); + t.Equals(ct24.depth, kInitialDepth, + "RGB_ONLY-as-FB_ONLY should preserve CT24 depth"); + + const GsPixelTestResult ct16 = + drawGsPixelForTests(GS_PSM_CT16, kTest, false, + 0x8001u, kInitialDepth, 0x00u); + t.Equals(ct16.framebuffer, 0x28C2u, + "RGB_ONLY should write RGB and alpha for CT16"); + t.Equals(ct16.depth, kInitialDepth, + "RGB_ONLY-as-FB_ONLY should preserve CT16 depth"); + }); - gs.writeRegister(GS_REG_FRAME_1, kFrame); - gs.writeRegister(GS_REG_ZBUF_1, kZbuf); - gs.writeRegister(GS_REG_SCISSOR_1, kScissor); - gs.writeRegister(GS_REG_TEST_1, kTest); - gs.writeRegister(GS_REG_PRIM, kPrim); - gs.writeRegister(GS_REG_RGBAQ, kRgbaq); - gs.writeRegister(GS_REG_XYZ2, 0ull); + tc.Run("GS ZMSK suppresses depth without suppressing framebuffer writes", [](TestCase &t) + { + constexpr uint64_t kTest = + 1ull | + (5ull << 1) | + (0x80ull << 4) | + (1ull << 16) | + (1ull << 17); + const GsPixelTestResult result = + drawGsPixelForTests(GS_PSM_CT32, kTest, true, + 0xAB030201u, 0x11111111u, 0x80u); + + t.Equals(result.framebuffer, 0x80563412u, + "a passing alpha test should write the framebuffer"); + t.Equals(result.depth, 0x11111111u, + "ZMSK should preserve depth"); + }); - uint32_t pixel = 0u; - std::memcpy(&pixel, vram.data(), sizeof(pixel)); - t.Equals(pixel, 0xAB563412u, - "AFAIL=RGB_ONLY should update RGB while preserving destination alpha"); + tc.Run("GS DATE and DATM inspect the framebuffer-format alpha bit", [](TestCase &t) + { + constexpr uint32_t kInitialDepth = 0x11111111u; + constexpr uint64_t kTestBase = + (1ull << 14) | // DATE + (1ull << 16) | // ZTE + (1ull << 17); // ZTST = ALWAYS + + const GsPixelTestResult ct32ZeroPass = + drawGsPixelForTests(GS_PSM_CT32, kTestBase, false, + 0x00030201u, kInitialDepth, 0x80u); + t.Equals(ct32ZeroPass.framebuffer, 0x80563412u, + "DATM=0 should accept a clear CT32 alpha bit"); + t.Equals(ct32ZeroPass.depth, 0x22222222u, + "a passing CT32 DATE should allow depth"); + + const GsPixelTestResult ct32OneFail = + drawGsPixelForTests(GS_PSM_CT32, kTestBase, false, + 0x80030201u, kInitialDepth, 0x80u); + t.Equals(ct32OneFail.framebuffer, 0x80030201u, + "DATM=0 should reject a set CT32 alpha bit"); + t.Equals(ct32OneFail.depth, kInitialDepth, + "a failing CT32 DATE should reject depth"); + + const GsPixelTestResult ct32OnePass = + drawGsPixelForTests(GS_PSM_CT32, kTestBase | (1ull << 15), false, + 0x80030201u, kInitialDepth, 0x80u); + t.Equals(ct32OnePass.framebuffer, 0x80563412u, + "DATM=1 should accept a set CT32 alpha bit"); + + const GsPixelTestResult ct16ZeroPass = + drawGsPixelForTests(GS_PSM_CT16, kTestBase, false, + 0x0001u, kInitialDepth, 0x80u); + t.Equals(ct16ZeroPass.framebuffer, 0xA8C2u, + "DATM=0 should accept a clear CT16 alpha bit"); + + const GsPixelTestResult ct16OneFail = + drawGsPixelForTests(GS_PSM_CT16, kTestBase, false, + 0x8001u, kInitialDepth, 0x80u); + t.Equals(ct16OneFail.framebuffer, 0x8001u, + "DATM=0 should reject a set CT16 alpha bit"); + t.Equals(ct16OneFail.depth, kInitialDepth, + "a failing CT16 DATE should reject depth"); + + const GsPixelTestResult ct16OnePass = + drawGsPixelForTests(GS_PSM_CT16, kTestBase | (1ull << 15), false, + 0x8001u, kInitialDepth, 0x80u); + t.Equals(ct16OnePass.framebuffer, 0xA8C2u, + "DATM=1 should accept a set CT16 alpha bit"); + + const GsPixelTestResult ct24DatmZero = + drawGsPixelForTests(GS_PSM_CT24, kTestBase, false, + 0x00030201u, kInitialDepth, 0x80u); + const GsPixelTestResult ct24DatmOne = + drawGsPixelForTests(GS_PSM_CT24, kTestBase | (1ull << 15), false, + 0x00030201u, kInitialDepth, 0x80u); + t.Equals(ct24DatmZero.framebuffer, 0x00563412u, + "CT24 DATE should pass for DATM=0"); + t.Equals(ct24DatmOne.framebuffer, 0x00563412u, + "CT24 DATE should pass for DATM=1"); + t.Equals(ct24DatmOne.depth, 0x22222222u, + "CT24 DATE should not block depth"); }); tc.Run("GS triangle fan subpixel quad fills rows without interior holes", [](TestCase &t) diff --git a/ps2xTest/src/ps2_iop_tests.cpp b/ps2xTest/src/ps2_iop_tests.cpp index 59340ba2f..d75b213bf 100644 --- a/ps2xTest/src/ps2_iop_tests.cpp +++ b/ps2xTest/src/ps2_iop_tests.cpp @@ -440,6 +440,69 @@ void register_ps2_iop_tests() "reset should restore per-instance service state"); }); + tc.Run("LotR sound update completes queued PlayStream slots", [](TestCase &t) + { + FakeIopHost host; + ps2x::iop::IopSubsystem subsystem(host); + std::string error; + t.IsTrue(subsystem.configure({"SLUS_205.78", 0u, 0u}, &error), + "LotR profile should configure"); + + constexpr uint32_t kSendAddress = 0x0800u; + constexpr uint32_t kReceiveAddress = 0x1000u; + constexpr uint16_t kStreamSlot = 7u; + const std::array playStreamPacket = { + 1u, // command count + 1u, // PlayStream + 7u, // argument count + 0u, + static_cast(kStreamSlot << 8u), + 0u, + 0u, + 0u, + 0u, + 0u, + }; + t.IsTrue(host.writeGuest(kSendAddress, + playStreamPacket.data(), + sizeof(playStreamPacket)), + "PlayStream command packet should fit in guest memory"); + + ps2x::iop::RpcRequest request{}; + request.sid = 0x00012345u; + request.send = {kSendAddress, sizeof(playStreamPacket)}; + request.receive = {kReceiveAddress, 0x100u}; + + t.IsTrue(subsystem.handleRpc(request).handled, + "LotR sound service should handle PlayStream"); + t.Equals(host.readWord(kReceiveAddress), 1u, + "PlayStream response should expose one active record"); + const uint32_t packedStream = host.readWord(kReceiveAddress + 4u); + t.Equals((packedStream >> 4u) & 0x3Fu, + static_cast(kStreamSlot), + "active record should identify the queued EE stream slot"); + t.Equals(host.readWord(kReceiveAddress + 0x24u), 1u, + "response counter should follow the active record"); + + const std::array statusPacket = { + 1u, // command count + 9u, // GetStatus + 2u, // argument count + kStreamSlot, + 0u, + }; + t.IsTrue(host.writeGuest(kSendAddress, statusPacket.data(), sizeof(statusPacket)), + "GetStatus command packet should fit in guest memory"); + request.send.size = sizeof(statusPacket); + + t.IsTrue(subsystem.handleRpc(request).handled, + "LotR sound service should handle the following status update"); + t.Equals(host.readWord(kReceiveAddress), 0u, + "the update after PlayStream should report no active records"); + t.Equals(host.readWord(kReceiveAddress + 4u), 2u, + "empty response counter should return to the base offset"); + }); + tc.Run("TSNDDRV uses profile checksum bindings without writing invalid ports", [](TestCase &t) { FakeIopHost host(0x02000000u); diff --git a/ps2xTest/src/ps2_recompiler_tests.cpp b/ps2xTest/src/ps2_recompiler_tests.cpp index 3980bde44..4dce6227f 100644 --- a/ps2xTest/src/ps2_recompiler_tests.cpp +++ b/ps2xTest/src/ps2_recompiler_tests.cpp @@ -155,6 +155,102 @@ static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path return writer.save(elfPath.string()); } +static bool writeMinimalMipsElfWithInitializer(const std::filesystem::path &elfPath, + const std::string &functionName, + uint32_t initializerTarget) +{ + ELFIO::elfio writer; + writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB); + writer.set_os_abi(ELFIO::ELFOSABI_NONE); + writer.set_type(ELFIO::ET_EXEC); + writer.set_machine(ELFIO::EM_MIPS); + writer.set_entry(0x00100000u); + + ELFIO::section *text = writer.sections.add(".text"); + text->set_type(ELFIO::SHT_PROGBITS); + text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR); + text->set_addr_align(4); + text->set_address(0x00100000u); + const std::array textWords = { + 0x03E00008u, // jr $ra + 0x00000000u, // nop + }; + text->set_data(reinterpret_cast(textWords.data()), + static_cast(textWords.size() * sizeof(uint32_t))); + + ELFIO::section *ctors = writer.sections.add(".ctors"); + ctors->set_type(ELFIO::SHT_PROGBITS); + ctors->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_WRITE); + ctors->set_addr_align(4); + ctors->set_address(0x00200000u); + ctors->set_data(reinterpret_cast(&initializerTarget), + static_cast(sizeof(initializerTarget))); + + ELFIO::section *strtab = writer.sections.add(".strtab"); + strtab->set_type(ELFIO::SHT_STRTAB); + strtab->set_addr_align(1); + + ELFIO::section *symtab = writer.sections.add(".symtab"); + symtab->set_type(ELFIO::SHT_SYMTAB); + symtab->set_info(1); + symtab->set_link(strtab->get_index()); + symtab->set_addr_align(4); + symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB)); + + ELFIO::symbol_section_accessor symbols(writer, symtab); + ELFIO::string_section_accessor strings(strtab); + symbols.add_symbol(strings, "", 0, 0, + ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF); + symbols.add_symbol(strings, functionName.c_str(), text->get_address(), text->get_size(), + ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index()); + + ELFIO::segment *textSegment = writer.segments.add(); + textSegment->set_type(ELFIO::PT_LOAD); + textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X); + textSegment->set_align(0x1000); + textSegment->add_section_index(text->get_index(), text->get_addr_align()); + + ELFIO::segment *dataSegment = writer.segments.add(); + dataSegment->set_type(ELFIO::PT_LOAD); + dataSegment->set_flags(ELFIO::PF_R | ELFIO::PF_W); + dataSegment->set_align(0x1000); + dataSegment->add_section_index(ctors->get_index(), ctors->get_addr_align()); + + return writer.save(elfPath.string()); +} + +static bool writeRecompilerTestConfig(const std::filesystem::path &configPath, + const std::filesystem::path &elfPath, + const std::filesystem::path &outputPath, + const std::vector &skip, + const std::vector &stubs = {}) +{ + std::ofstream config(configPath); + if (!config) + return false; + + config << "[general]\n"; + config << "input = \"" << elfPath.generic_string() << "\"\n"; + config << "output = \"" << outputPath.generic_string() << "\"\n"; + config << "skip = ["; + for (size_t i = 0; i < skip.size(); ++i) + { + if (i != 0u) + config << ", "; + config << '"' << skip[i] << '"'; + } + config << "]\n"; + config << "stubs = ["; + for (size_t i = 0; i < stubs.size(); ++i) + { + if (i != 0u) + config << ", "; + config << '"' << stubs[i] << '"'; + } + config << "]\n"; + return static_cast(config); +} + void register_ps2_recompiler_tests() { MiniTest::Case("PS2Recompiler", [](TestCase &tc) @@ -890,6 +986,98 @@ void register_ps2_recompiler_tests() "__sbprintf should be left for recompilation"); }); + tc.Run("initializer skips fall back to guest recompilation", [](TestCase &t) { + const std::string uniqueSuffix = + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const std::filesystem::path tempRoot = + std::filesystem::temp_directory_path() / ("ps2recomp-initializer-" + uniqueSuffix); + const std::filesystem::path elfPath = tempRoot / "initializer.elf"; + const std::filesystem::path configPath = tempRoot / "initializer.toml"; + const std::filesystem::path outputPath = tempRoot / "output"; + std::filesystem::create_directories(tempRoot); + + const bool elfWritten = + writeMinimalMipsElfWithInitializer(elfPath, "__sinit_test.cpp", 0x00100000u); + const bool configWritten = + writeRecompilerTestConfig(configPath, elfPath, outputPath, {"__sinit_test.cpp"}); + t.IsTrue(elfWritten && configWritten, + "initializer regression inputs should be generated"); + + if (elfWritten && configWritten) + { + PS2Recompiler recompiler(configPath.string()); + t.IsTrue(recompiler.initialize(), + "initializer regression config should initialize"); + t.IsTrue(recompiler.recompile(), + "a decodable skipped initializer should use guest fallback"); + const RecompilerReporter::Counters &counters = recompiler.reportCounters(); + t.Equals(counters.correctnessCriticalGuestFallbacks, static_cast(1u), + "the ignored initializer skip should be reported"); + t.Equals(counters.correctnessCriticalFailures, static_cast(0u), + "guest fallback should avoid a correctness-critical failure"); + t.Equals(counters.functionsSkipped, static_cast(0u), + "the initializer should not remain skipped"); + t.Equals(counters.functionsRecompiled, static_cast(1u), + "the original initializer body should be recompiled"); + } + + std::error_code removeError; + std::filesystem::remove_all(tempRoot, removeError); + }); + + tc.Run("missing constructor-table targets fail recompilation", [](TestCase &t) { + const std::string uniqueSuffix = + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const std::filesystem::path tempRoot = + std::filesystem::temp_directory_path() / ("ps2recomp-missing-initializer-" + uniqueSuffix); + const std::filesystem::path elfPath = tempRoot / "initializer.elf"; + const std::filesystem::path configPath = tempRoot / "initializer.toml"; + const std::filesystem::path outputPath = tempRoot / "output"; + std::filesystem::create_directories(tempRoot); + + const bool elfWritten = + writeMinimalMipsElfWithInitializer(elfPath, "ordinary_entry", 0x00100040u); + const bool configWritten = + writeRecompilerTestConfig(configPath, elfPath, outputPath, {}); + t.IsTrue(elfWritten && configWritten, + "missing-initializer regression inputs should be generated"); + + if (elfWritten && configWritten) + { + { + PS2Recompiler recompiler(configPath.string()); + t.IsTrue(recompiler.initialize(), + "missing-initializer regression config should initialize"); + t.IsFalse(recompiler.recompile(), + "an unresolved .ctors target should be correctness-fatal"); + t.Equals(recompiler.reportCounters().correctnessCriticalFailures, + static_cast(1u), + "the unresolved constructor target should appear in the report"); + } + + const bool overrideWritten = + writeRecompilerTestConfig( + configPath, elfPath, outputPath, {}, + {"memclr@0x00100040"}); + t.IsTrue(overrideWritten, + "manual initializer override config should be generated"); + if (overrideWritten) + { + PS2Recompiler overridden(configPath.string()); + t.IsTrue(overridden.initialize(), + "manual initializer override should initialize"); + t.IsTrue(overridden.recompile(), + "a resolved address-bound handler should satisfy the constructor target"); + t.Equals(overridden.reportCounters().functionsStubbed, + static_cast(1u), + "the resolved manual initializer should be emitted as a stub binding"); + } + } + + std::error_code removeError; + std::filesystem::remove_all(tempRoot, removeError); + }); + tc.Run("respect max length for .cpp filenames", [](TestCase& t) { t.IsTrue(PS2Recompiler::ClampFilenameLength("ReallyLongFunctionNameReallyLongFunctionNameReallyLongFunctionName_0x12345678",".cpp",50).length() <= 50,"Function name must be max 50 characters"); diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index c31b4d425..3d70c0606 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -182,7 +182,7 @@ namespace } bool shouldPreempt = false; - for (int attempt = 0; attempt < 256 && + for (int attempt = 0; attempt < 2048 && !shouldPreempt; ++attempt) { @@ -571,6 +571,30 @@ void register_ps2_runtime_expansion_tests() t.IsFalse(innerPending, "inner scope must stay untouched"); }); + tc.Run("guest preemption policy amortizes uncontended back-edge checks", [](TestCase &t) + { + PS2Runtime runtime; + uint32_t firstPreemptionCall = 0u; + + // Use a fresh host thread so this assertion starts with a fresh + // thread-local back-edge counter. + std::thread worker([&]() + { + for (uint32_t call = 1u; call <= 32768u; ++call) + { + if (runtime.shouldPreemptGuestExecution()) + { + firstPreemptionCall = call; + break; + } + } + }); + worker.join(); + + t.Equals(firstPreemptionCall, 16384u, + "uncontended parser loops should amortize dispatcher handoffs across many back edges"); + }); + tc.Run("guest preemption policy requests a dispatcher handoff when another guest thread contends", [](TestCase &t) { PS2Runtime runtime; diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index 7a4371fbf..a4a1514ac 100644 --- a/ps2xTest/src/ps2_sif_rpc_tests.cpp +++ b/ps2xTest/src/ps2_sif_rpc_tests.cpp @@ -162,7 +162,7 @@ namespace constexpr uint32_t K_DTX_DISPATCH_RESULT_ADDR = 0x0002D800u; constexpr uint32_t K_DTX_DISPATCH_RESULT_MARKER = 0xD15CA7C1u; - void lotrSoundEndCallbackShouldNotRun(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) + void lotrSoundEndCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; (void)runtime; @@ -637,7 +637,7 @@ void register_ps2_sif_rpc_tests() PS2Runtime::setIoPaths(oldPaths); }); - tc.Run("LotR sound RPC completes HLE callback without invoking guest loop", [](TestCase &t) + tc.Run("LotR sound RPC invokes guest callback to consume HLE response", [](TestCase &t) { TestEnv env; configureProfile(env, "SLUS_205.78"); @@ -648,7 +648,7 @@ void register_ps2_sif_rpc_tests() constexpr uint32_t kRecvAddr = 0x0003C000u; constexpr uint32_t kEndFunc = 0x001FFD70u; - env.runtime.registerFunction(kEndFunc, lotrSoundEndCallbackShouldNotRun); + env.runtime.registerFunction(kEndFunc, lotrSoundEndCallback); g_lotrSoundCallbackHits = 0u; SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); @@ -681,8 +681,8 @@ void register_ps2_sif_rpc_tests() SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should succeed for LotR sound RPC"); - t.Equals(g_lotrSoundCallbackHits.load(), 0u, - "HLE-completed LotR sound callback should not invoke the guest callback"); + t.Equals(g_lotrSoundCallbackHits.load(), 1u, + "LotR SOUND_JP callback should consume the HLE response"); t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 0u, "LotR sound response should report no active stream records"); t.IsTrue(readGuestStruct(env.rdram.data(), kRecvAddr + 4u) != 0u, diff --git a/ps2xTest/src/ps2_vu1_tests.cpp b/ps2xTest/src/ps2_vu1_tests.cpp index a66da893c..543f90d6f 100644 --- a/ps2xTest/src/ps2_vu1_tests.cpp +++ b/ps2xTest/src/ps2_vu1_tests.cpp @@ -7,11 +7,12 @@ #include #include +#include #include namespace { - constexpr uint32_t kVuUpperNop = 0u; + constexpr uint32_t kVuUpperNop = 0x000002FFu; struct Vu1Fixture { @@ -81,6 +82,21 @@ namespace static_cast(op & 0x3Fu); } + uint32_t makeVuFlagImmediate(uint8_t opcode, uint8_t targetVi, uint16_t immediate) + { + return (static_cast(opcode & 0x7Fu) << 25) | + (static_cast((immediate >> 11) & 0x1u) << 21) | + (static_cast(targetVi & 0xFu) << 16) | + static_cast(immediate & 0x7FFu); + } + + uint32_t makeVuFlagRegister(uint8_t opcode, uint8_t targetVi, uint8_t sourceVi) + { + return (static_cast(opcode & 0x7Fu) << 25) | + (static_cast(targetVi & 0xFu) << 16) | + (static_cast(sourceVi & 0xFu) << 11); + } + uint32_t makeVuLq(uint8_t dest, uint8_t targetVf, uint8_t baseVi, int16_t imm) { return (static_cast(dest & 0xFu) << 21) | @@ -555,5 +571,152 @@ void register_ps2_vu1_tests() } t.IsTrue(imageOk, "MSCAL-triggered XGKICK should route PATH1 packet into GS VRAM"); }); + + tc.Run("standalone VU1 code honors the nullable PS2Memory API", [](TestCase &t) + { + std::vector code(8u, 0u); + std::vector data(16u, 0u); + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + VU1Interpreter vu1; + vu1.execute(code.data(), static_cast(code.size()), + data.data(), static_cast(data.size()), + gs, nullptr, 0u, 0u, 0u, 1u); + + t.Equals(vu1.state().pc, 0u, + "external code should execute and wrap without dereferencing a null memory tracker"); + }); + + tc.Run("VU1 status-immediate ops decode IMM12 and their target VI", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 0u, + makeVuFlagImmediate(0x14u, 5u, 0x812u), + kVuUpperNop); + writeVuInstructionPair(fx.code, 8u, + makeVuFlagImmediate(0x16u, 6u, 0x810u), + kVuUpperNop); + writeVuInstructionPair(fx.code, 16u, + makeVuFlagImmediate(0x17u, 7u, 0x040u), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().status = 0x812u; + 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[5], 1, + "FSEQ should compare all 12 immediate bits and write IT"); + t.Equals(vu1.state().vi[6], 0x810, + "FSAND should return the masked 12-bit status in IT"); + t.Equals(vu1.state().vi[7], 0x852, + "FSOR should return the 12-bit OR value rather than a boolean"); + t.Equals(vu1.state().vi[1], 0, + "status-immediate ops should not hardcode VI1"); + }); + + tc.Run("VU1 FSSET enters the four-cycle flag pipeline", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 0u, + makeVuFlagImmediate(0x15u, 0u, 0xA80u), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().status = 0x015u; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 4u); + t.Equals(vu1.state().status, 0x015u, + "FSSET should not be visible before four cycles elapse"); + + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 1u); + t.Equals(vu1.state().status, 0xA95u, + "FSSET should replace sticky bits while preserving current and D/I bits"); + }); + + tc.Run("VU1 FMAC flags respect destination lanes and become visible after four cycles", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 0u, 0u, + makeVuUpper(0x28u, 0xAu, 2u, 1u, 3u)); + writeVuInstructionPair(fx.code, 8u, + makeVuFlagRegister(0x1Au, 6u, 7u), + kVuUpperNop); + writeVuInstructionPair(fx.code, 32u, + makeVuFlagRegister(0x18u, 4u, 5u), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][2] = -3.0f; + vu1.state().vf[2][0] = -1.0f; + vu1.state().vf[2][2] = 1.0f; + vu1.state().vi[5] = 0xFFFF; + vu1.state().vi[7] = 0; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 4u); + + t.Equals(vu1.state().mac, 0u, + "FMAC flags should remain hidden during the first four issue cycles"); + t.Equals(vu1.state().vi[6], 1, + "FMEQ before the commit cycle should observe the old MAC flags"); + + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 1u); + + t.Equals(vu1.state().mac, 0x28u, + "ADD.xz should report zero on x and sign on z only"); + t.Equals(vu1.state().status, 0xC3u, + "FMAC commit should update current Z/S and accumulate their sticky bits"); + t.Equals(vu1.state().vi[4], 0x28, + "FMAND on the commit cycle should observe the new MAC flags"); + }); + + tc.Run("VU1 FMAC normalizes overflow and underflow while reporting O and U", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 0u, 0u, + makeVuUpper(0x2Au, 0xFu, 2u, 1u, 3u)); + + VU1Interpreter vu1; + vu1.state().vf[1][0] = std::numeric_limits::max(); + vu1.state().vf[1][1] = std::numeric_limits::denorm_min(); + vu1.state().vf[1][2] = -2.0f; + vu1.state().vf[1][3] = 0.0f; + vu1.state().vf[2][0] = 2.0f; + vu1.state().vf[2][1] = 1.0f; + vu1.state().vf[2][2] = 1.0f; + vu1.state().vf[2][3] = 1.0f; + + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 5u); + + t.Equals(vu1.state().mac, 0x8425u, + "MUL should report x overflow, y underflow+zero, z sign and w zero"); + t.Equals(vu1.state().status, 0x3CFu, + "current and sticky status should summarize Z/S/U/O"); + t.Equals(vu1.state().vf[3][0], std::numeric_limits::max(), + "overflow should saturate to maximum finite magnitude"); + t.Equals(vu1.state().vf[3][1], 0.0f, + "denormal output should flush to zero"); + }); }); }