diff --git a/ps2xIOP/src/builtin_profiles.cpp b/ps2xIOP/src/builtin_profiles.cpp index b4db00c..ac537ad 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 42b1c30..8e0f422 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 933ebe5..68c1c8c 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)); } - if (request.receive.address != 0u && - request.receive.size >= m_bindings.responseCounterOffset + sizeof(counter)) + for (size_t index = 0u; index < activeStreamSlots.size(); ++index) { - const uint32_t address = request.receive.address + m_bindings.responseCounterOffset; + 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 >= counterOffset + sizeof(counter)) + { + 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/instructions.h b/ps2xRecomp/include/ps2recomp/instructions.h index 3b2beb6..bf138a6 100644 --- a/ps2xRecomp/include/ps2recomp/instructions.h +++ b/ps2xRecomp/include/ps2recomp/instructions.h @@ -670,37 +670,21 @@ namespace ps2recomp // VU0_VLDQ = 0x1F // VU0 Load/Store Quad with Decrement // }; - // VU0 Control Register Numbers (used with CFC2/CTC2) + // VU0 COP2 control register numbers used by CFC2/CTC2. + // Registers 0..15 address VI0..VI15 directly. enum VU0ControlRegisters { - VU0_CR_STATUS = 0, // Status/Control register - VU0_CR_MAC = 1, // MAC flags register - VU0_CR_CLIP = 5, // Clipping flags register - VU0_CR_R = 3, // R register (Random number) - VU0_CR_I = 4, // I register (Immediate) - - // Add missing registers - VU0_CR_VPU_STAT = 2, // VPU-STAT register - VU0_CR_TPC = 6, // T (program counter) register - VU0_CR_CMSAR0 = 7, // Call/return address 0 - VU0_CR_FBRST = 8, // VIF/VU reset register - VU0_CR_VPU_STAT2 = 9, // VPU-STAT register 2 - VU0_CR_TPC2 = 10, // T (program counter) register 2 - VU0_CR_CMSAR1 = 11, // Call/return address 1 - VU0_CR_FBRST2 = 12, // VIF/VU reset register 2 - VU0_CR_VPU_STAT3 = 13, // VPU-STAT register 3 - VU0_CR_CMSAR2 = 14, // Call/return address 2 - VU0_CR_FBRST3 = 15, // VIF/VU reset register 3 - VU0_CR_VPU_STAT4 = 16, // VPU-STAT register 4 - VU0_CR_CMSAR3 = 17, // Call/return address 3 - VU0_CR_FBRST4 = 18, // VIF/VU reset register 4 - VU0_CR_ACC = 20, // Accumulator register - VU0_CR_INFO = 21, // Information register - VU0_CR_CLIP2 = 22, // Clipping flags register 2 - VU0_CR_P = 26, // P register - VU0_CR_XITOP = 27, // XITOP register - VU0_CR_ITOP = 28, // ITOP register - VU0_CR_TOP = 29 // TOP register + VU0_CR_STATUS = 16, + VU0_CR_MAC = 17, + VU0_CR_CLIP = 18, + VU0_CR_R = 20, + VU0_CR_I = 21, + VU0_CR_Q = 22, + VU0_CR_TPC = 26, + VU0_CR_CMSAR0 = 27, + VU0_CR_FBRST = 28, + VU0_CR_VPU_STAT = 29, + VU0_CR_CMSAR1 = 31 }; enum VU0OPSFunctions { diff --git a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h index 4ffeaa0..ec21986 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 949b69d..83620bb 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 df7e356..f252ec4 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 8c08f96..85a5ea4 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/ps2xRecomp/src/lib/vu_translation_helpers.cpp b/ps2xRecomp/src/lib/vu_translation_helpers.cpp index 435d356..509fd07 100644 --- a/ps2xRecomp/src/lib/vu_translation_helpers.cpp +++ b/ps2xRecomp/src/lib/vu_translation_helpers.cpp @@ -209,8 +209,7 @@ namespace ps2recomp return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " "__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, vft, vft, shuffle_pattern, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, @@ -230,8 +229,7 @@ namespace ps2recomp return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], {})); " "__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, vft, vft, shuffle_pattern, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, @@ -285,8 +283,7 @@ namespace ps2recomp return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " "__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, vft, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, @@ -301,8 +298,7 @@ namespace ps2recomp return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); " "__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, @@ -317,8 +313,7 @@ namespace ps2recomp return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); " "__m128 res = PS2_VADD(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, @@ -402,12 +397,13 @@ namespace ps2recomp uint8_t vfs = inst.rd; uint8_t vft = inst.rt; uint8_t dest_mask = inst.vectorInfo.vectorField; - return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " + return fmt::format("{{ __m128 fs_yzx = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(3,0,2,1)); " + "__m128 ft_zxy = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(3,1,0,2)); " + "__m128 mul_res = PS2_VMUL(fs_yzx, ft_zxy); " "__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", - vfs, vft, + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", + vfs, vfs, vft, vft, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, vfd, vfd); @@ -450,8 +446,7 @@ namespace ps2recomp return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); " "__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, vft, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, @@ -509,8 +504,7 @@ namespace ps2recomp return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_q)); " "__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, @@ -525,8 +519,7 @@ namespace ps2recomp return fmt::format("{{ __m128 mul_res = PS2_VMUL(ctx->vu0_vf[{}], _mm_set1_ps(ctx->vu0_i)); " "__m128 res = PS2_VSUB(ctx->vu0_acc, mul_res); " "__m128i mask = _mm_set_epi32({}, {}, {}, {}); " - "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); " - "ctx->vu0_acc = res; }}", + "ctx->vu0_vf[{}] = _mm_blendv_ps(ctx->vu0_vf[{}], res, _mm_castsi128_ps(mask)); }}", vfs, (dest_mask & 0x1) ? -1 : 0, (dest_mask & 0x2) ? -1 : 0, (dest_mask & 0x4) ? -1 : 0, (dest_mask & 0x8) ? -1 : 0, @@ -730,8 +723,11 @@ namespace ps2recomp uint8_t vfs = inst.rd; uint8_t vft = inst.rt; uint8_t dest_mask = inst.vectorInfo.vectorField; - return fmt::format("{{ __m128 res = PS2_VMUL(ctx->vu0_vf[{}], ctx->vu0_vf[{}]); ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", - vfs, vft, codegen::vuMaskExpr(dest_mask)); + return fmt::format("{{ __m128 fs_yzx = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(3,0,2,1)); " + "__m128 ft_zxy = _mm_shuffle_ps(ctx->vu0_vf[{}], ctx->vu0_vf[{}], _MM_SHUFFLE(3,1,0,2)); " + "__m128 res = PS2_VMUL(fs_yzx, ft_zxy); " + "ctx->vu0_acc = _mm_blendv_ps(ctx->vu0_acc, res, {}); }}", + vfs, vfs, vft, vft, codegen::vuMaskExpr(dest_mask)); } std::string CodeGenerator::translateVU_VITOF(const Instruction &inst, int shift) diff --git a/ps2xRecomp/src/lib/vu_translator.cpp b/ps2xRecomp/src/lib/vu_translator.cpp index 6148c98..d0fc95d 100644 --- a/ps2xRecomp/src/lib/vu_translator.cpp +++ b/ps2xRecomp/src/lib/vu_translator.cpp @@ -29,124 +29,77 @@ namespace ps2recomp return fmt::format("SET_GPR_VEC(ctx, {}, _mm_castps_si128(ctx->vu0_vf[{}]));", rt, rd); case COP2_CFC2: { - switch (rd) // Control register number is in rd + // CFC2/CTC2 use the same 5-bit register field for VI0..VI15 and the VU special control registers. + if (rd < 16) + { + return fmt::format("SET_GPR_U32(ctx, {}, static_cast(ctx->vi[{}]));", rt, rd); + } + + switch (rd) { case VU0_CR_STATUS: return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_status);", rt); case VU0_CR_MAC: return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_mac_flags);", rt); - case VU0_CR_VPU_STAT: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat);", rt); + case VU0_CR_CLIP: + return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_clip_flags & 0x00FFFFFFu);", rt); case VU0_CR_R: - return fmt::format("SET_GPR_VEC(ctx, {}, _mm_castps_si128(ctx->vu0_r));", rt); + return fmt::format("SET_GPR_U32(ctx, {}, static_cast(_mm_cvtsi128_si32(_mm_castps_si128(ctx->vu0_r))));", rt); case VU0_CR_I: return fmt::format("{{ uint32_t bits; std::memcpy(&bits, &ctx->vu0_i, sizeof(bits)); SET_GPR_U32(ctx, {}, bits); }}", rt); - case VU0_CR_CLIP: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_clip_flags);", rt); + case VU0_CR_Q: + return fmt::format("{{ uint32_t bits; std::memcpy(&bits, &ctx->vu0_q, sizeof(bits)); SET_GPR_U32(ctx, {}, bits); }}", rt); case VU0_CR_TPC: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_tpc);", rt); + return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_tpc >> 3);", rt); case VU0_CR_CMSAR0: return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_cmsar0);", rt); case VU0_CR_FBRST: return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_fbrst);", rt); - case VU0_CR_VPU_STAT2: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat2);", rt); - case VU0_CR_TPC2: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_tpc2);", rt); + case VU0_CR_VPU_STAT: + return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat);", rt); case VU0_CR_CMSAR1: return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_cmsar1);", rt); - case VU0_CR_FBRST2: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_fbrst2);", rt); - case VU0_CR_VPU_STAT3: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat3);", rt); - case VU0_CR_CMSAR2: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_cmsar2);", rt); - case VU0_CR_FBRST3: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_fbrst3);", rt); - case VU0_CR_VPU_STAT4: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_vpu_stat4);", rt); - case VU0_CR_CMSAR3: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_cmsar3);", rt); - case VU0_CR_FBRST4: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_fbrst4);", rt); - case VU0_CR_ACC: - return fmt::format("SET_GPR_VEC(ctx, {}, _mm_castps_si128(ctx->vu0_acc));", rt); - case VU0_CR_INFO: // I dd found on offical docs but ok - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_info);", rt); - case VU0_CR_CLIP2: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_clip_flags2);", rt); - case VU0_CR_P: - return fmt::format("{{ uint32_t bits; std::memcpy(&bits, &ctx->vu0_p, sizeof(bits)); SET_GPR_U32(ctx, {}, bits); }}", rt); - case VU0_CR_XITOP: // Maybe this does not exist, maybe we handle to vu0_itop - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_xitop);", rt); - case VU0_CR_ITOP: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_itop);", rt); - case VU0_CR_TOP: - return fmt::format("SET_GPR_U32(ctx, {}, ctx->vu0_top);", rt); default: - return fmt::format("// Unimplemented CFC2 VU CReg: {}", rt); + return fmt::format("// Unimplemented CFC2 VU control register: {}", rd); } } case COP2_QMTC2: return fmt::format("ctx->vu0_vf[{}] = _mm_castsi128_ps(GPR_VEC(ctx, {}));", rd, rt); case COP2_CTC2: { - switch (rd) // Control register number is in rd + if (rd < 16) + { + if (rd == 0) + { + return "// CTC2 write to VI0 ignored"; + } + return fmt::format("ctx->vi[{}] = static_cast(GPR_U32(ctx, {}));", rd, rt); + } + + switch (rd) { case VU0_CR_STATUS: - return fmt::format("ctx->vu0_status = GPR_U32(ctx, {}) & 0xFFFF;", rt); + return fmt::format("ctx->vu0_status = static_cast(GPR_U32(ctx, {}) & 0xFFFFu);", rt); case VU0_CR_MAC: - return fmt::format("ctx->vu0_mac_flags = GPR_U32(ctx, {});", rt); + case VU0_CR_TPC: case VU0_CR_VPU_STAT: - return fmt::format("ctx->vu0_vpu_stat = GPR_U32(ctx, {});", rt); + return fmt::format("// CTC2 write to read-only VU control register {} ignored", rd); case VU0_CR_CLIP: - return fmt::format("ctx->vu0_clip_flags = GPR_U32(ctx, {});", rt); + return fmt::format("ctx->vu0_clip_flags = GPR_U32(ctx, {}) & 0x00FFFFFFu;", rt); case VU0_CR_R: - return fmt::format("ctx->vu0_r = _mm_castsi128_ps(GPR_VEC(ctx, {}));", rt); + return fmt::format("ctx->vu0_r = _mm_castsi128_ps(_mm_set1_epi32(static_cast(GPR_U32(ctx, {}))));", rt); case VU0_CR_I: return fmt::format("{{ uint32_t tmp = GPR_U32(ctx, {}); std::memcpy(&ctx->vu0_i, &tmp, sizeof(tmp)); }}", rt); - case VU0_CR_TPC: - return fmt::format("ctx->vu0_tpc = GPR_U32(ctx, {});", rt); + case VU0_CR_Q: + return fmt::format("{{ uint32_t tmp = GPR_U32(ctx, {}); std::memcpy(&ctx->vu0_q, &tmp, sizeof(tmp)); }}", rt); case VU0_CR_CMSAR0: return fmt::format("ctx->vu0_cmsar0 = GPR_U32(ctx, {});", rt); case VU0_CR_FBRST: - return fmt::format("ctx->vu0_fbrst = GPR_U32(ctx, {});", rt); - case VU0_CR_VPU_STAT2: - return fmt::format("ctx->vu0_vpu_stat2 = GPR_U32(ctx, {});", rt); - case VU0_CR_TPC2: - return fmt::format("ctx->vu0_tpc2 = GPR_U32(ctx, {});", rt); + return fmt::format("ctx->vu0_fbrst = GPR_U32(ctx, {}) & 0x00000C0Cu;", rt); case VU0_CR_CMSAR1: return fmt::format("ctx->vu0_cmsar1 = GPR_U32(ctx, {});", rt); - case VU0_CR_FBRST2: - return fmt::format("ctx->vu0_fbrst2 = GPR_U32(ctx, {});", rt); - case VU0_CR_VPU_STAT3: - return fmt::format("ctx->vu0_vpu_stat3 = GPR_U32(ctx, {});", rt); - case VU0_CR_CMSAR2: - return fmt::format("ctx->vu0_cmsar2 = GPR_U32(ctx, {});", rt); - case VU0_CR_FBRST3: - return fmt::format("ctx->vu0_fbrst3 = GPR_U32(ctx, {});", rt); - case VU0_CR_VPU_STAT4: - return fmt::format("ctx->vu0_vpu_stat4 = GPR_U32(ctx, {});", rt); - case VU0_CR_CMSAR3: - return fmt::format("ctx->vu0_cmsar3 = GPR_U32(ctx, {});", rt); - case VU0_CR_FBRST4: - return fmt::format("ctx->vu0_fbrst4 = GPR_U32(ctx, {});", rt); - case VU0_CR_ACC: - return fmt::format("ctx->vu0_acc = _mm_castsi128_ps(GPR_VEC(ctx, {}));", rt); - case VU0_CR_INFO: - return fmt::format("ctx->vu0_info = GPR_U32(ctx, {});", rt); - case VU0_CR_CLIP2: - return fmt::format("ctx->vu0_clip_flags2 = GPR_U32(ctx, {});", rt); - case VU0_CR_P: - return fmt::format("{{ uint32_t tmp = GPR_U32(ctx, {}); std::memcpy(&ctx->vu0_p, &tmp, sizeof(tmp)); }}", rt); - case VU0_CR_XITOP: - return fmt::format("ctx->vu0_xitop = GPR_U32(ctx, {}) & 0x3FF;", rt); - case VU0_CR_ITOP: - return fmt::format("ctx->vu0_itop = GPR_U32(ctx, {}) & 0x3FF;", rt); - case VU0_CR_TOP: - return fmt::format("ctx->vu0_top = GPR_U32(ctx, {}) & 0x3FF;", rt); default: - return fmt::format("// Unimplemented CTC2 VU CReg: {}", rd); + return fmt::format("// Unimplemented CTC2 VU control register: {}", rd); } } case COP2_BC: diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 3674840..0a80a98 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) + if(NOT imgui_POPULATED) + FetchContent_Populate(imgui) + endif() + + 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}") + + 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() + + 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() - - 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}") - - 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() - - 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 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/ps2_log.h b/ps2xRuntime/include/ps2_log.h index 341dfcc..a42b1ba 100644 --- a/ps2xRuntime/include/ps2_log.h +++ b/ps2xRuntime/include/ps2_log.h @@ -19,6 +19,18 @@ #define AGRESSIVE_LOGS 0 #endif +#define RUNTIME_ERROR(x) \ + do \ + { \ + std::ostringstream _ps2_runtime_error_stream; \ + _ps2_runtime_error_stream << x; \ + const std::string _ps2_runtime_error_text = \ + _ps2_runtime_error_stream.str(); \ + \ + std::cerr << _ps2_runtime_error_text; \ + ps2_log::append_runtime_log_text(_ps2_runtime_error_text); \ + } while (0) + namespace ps2_log { struct RuntimeLogEntry diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index dda11c1..01df48c 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -527,8 +527,8 @@ private: std::unique_ptr m_iopSubsystem; PS2AudioBackend m_audioBackend; PSPadBackend m_padBackend; - VU1Interpreter m_vu0; - VU1Interpreter m_vu1; + VU1Interpreter m_vu0{VU1Interpreter::Unit::VU0}; + VU1Interpreter m_vu1{VU1Interpreter::Unit::VU1}; R5900Context m_cpuContext; mutable std::recursive_mutex m_guestExecutionMutex; mutable std::atomic m_guestExecutionWaiters{0u}; diff --git a/ps2xRuntime/include/runtime/ps2_gs_gpu.h b/ps2xRuntime/include/runtime/ps2_gs_gpu.h index 1991f95..bb0b140 100644 --- a/ps2xRuntime/include/runtime/ps2_gs_gpu.h +++ b/ps2xRuntime/include/runtime/ps2_gs_gpu.h @@ -399,12 +399,15 @@ private: GSContext m_ctx[2]; GSPrimReg m_prim{}; + GSPrimReg m_primRegister{}; + GSPrimReg m_prmodeRegister{}; uint8_t m_curR = 0x80, m_curG = 0x80, m_curB = 0x80, m_curA = 0x80; float m_curQ = 1.0f; float m_curS = 0.0f, m_curT = 0.0f; uint16_t m_curU = 0, m_curV = 0; uint8_t m_curFog = 0; + uint8_t m_fogR = 0, m_fogG = 0, m_fogB = 0; bool m_prmodecont = true; bool m_pabe = false; @@ -462,8 +465,9 @@ private: 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_gs_rasterizer.h b/ps2xRuntime/include/runtime/ps2_gs_rasterizer.h index c4b1040..17547de 100644 --- a/ps2xRuntime/include/runtime/ps2_gs_rasterizer.h +++ b/ps2xRuntime/include/runtime/ps2_gs_rasterizer.h @@ -9,7 +9,7 @@ class GSRasterizer { public: void drawPrimitive(GS *gs); - void writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a); + void writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t fog); uint32_t sampleTexture(GS *gs, float s, float t, float q, uint16_t u, uint16_t v); uint32_t lookupCLUT(GS *gs, uint8_t index, uint32_t cbp, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm); diff --git a/ps2xRuntime/include/runtime/ps2_memory.h b/ps2xRuntime/include/runtime/ps2_memory.h index 30fc455..6ca157b 100644 --- a/ps2xRuntime/include/runtime/ps2_memory.h +++ b/ps2xRuntime/include/runtime/ps2_memory.h @@ -285,6 +285,7 @@ public: uint64_t gifCopyCount() const { return m_gifCopyCount.load(std::memory_order_relaxed); } uint64_t gsWriteCount() const { return m_gsWriteCount.load(std::memory_order_relaxed); } uint64_t vifWriteCount() const { return m_vifWriteCount.load(std::memory_order_relaxed); } + uint64_t getVU0CodeGeneration() const { return m_vu0CodeGeneration.load(std::memory_order_relaxed); } uint64_t getVU1CodeGeneration() const { return m_vu1CodeGeneration.load(std::memory_order_relaxed); } // Read/write memory @@ -372,6 +373,7 @@ public: std::atomic m_gifCopyCount{0}; std::atomic m_gsWriteCount{0}; std::atomic m_vifWriteCount{0}; + std::atomic m_vu0CodeGeneration{0}; std::atomic m_vu1CodeGeneration{0}; // I/O registers std::unordered_map m_ioRegisters; @@ -431,6 +433,7 @@ public: bool isAddressInRegion(uint32_t address, const CodeRegion ®ion); void markModified(uint32_t address, uint32_t size); + void markVU0CodeModified() { m_vu0CodeGeneration.fetch_add(1, std::memory_order_relaxed); } void markVU1CodeModified() { m_vu1CodeGeneration.fetch_add(1, std::memory_order_relaxed); } bool isScratchpad(uint32_t address) const; uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit); diff --git a/ps2xRuntime/include/runtime/ps2_vu1.h b/ps2xRuntime/include/runtime/ps2_vu1.h index cdbdec9..67c8184 100644 --- a/ps2xRuntime/include/runtime/ps2_vu1.h +++ b/ps2xRuntime/include/runtime/ps2_vu1.h @@ -1,8 +1,8 @@ #ifndef PS2_VU1_H #define PS2_VU1_H +#include #include -#include class GS; class PS2Memory; @@ -15,13 +15,20 @@ struct VU1State 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; - uint32_t top; // VIF1 TOP visible to VU1 XTOP - uint32_t itop; // VIF1 ITOP visible to VU1 XITOP + 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; @@ -31,7 +38,13 @@ struct VU1State class VU1Interpreter { public: - VU1Interpreter(); + enum class Unit : uint8_t + { + VU0, + VU1 + }; + + explicit VU1Interpreter(Unit unit = Unit::VU1); void reset(); @@ -50,38 +63,236 @@ public: 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 lowerBeforeUpper = 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::vector m_decodedCodeCache; + 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); + 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); }; diff --git a/ps2xRuntime/src/lib/ps2_gs_gpu.cpp b/ps2xRuntime/src/lib/ps2_gs_gpu.cpp index f1f2bde..749a49b 100644 --- a/ps2xRuntime/src/lib/ps2_gs_gpu.cpp +++ b/ps2xRuntime/src/lib/ps2_gs_gpu.cpp @@ -23,6 +23,21 @@ namespace static constexpr uint32_t kHostFrameWidth = 640u; static constexpr uint32_t kHostFrameHeight = 512u; + GSPrimReg decodePrimRegister(uint64_t value) + { + GSPrimReg prim{}; + prim.type = static_cast(value & 0x7u); + prim.iip = ((value >> 3) & 1u) != 0u; + prim.tme = ((value >> 4) & 1u) != 0u; + prim.fge = ((value >> 5) & 1u) != 0u; + prim.abe = ((value >> 6) & 1u) != 0u; + prim.aa1 = ((value >> 7) & 1u) != 0u; + prim.fst = ((value >> 8) & 1u) != 0u; + prim.ctxt = ((value >> 9) & 1u) != 0u; + prim.fix = ((value >> 10) & 1u) != 0u; + return prim; + } + uint16_t encodeFramePixelPSMCT16(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { return static_cast(((r >> 3) & 0x1Fu) | @@ -109,7 +124,8 @@ namespace bool validatePackedGifPacket(const uint8_t *data, uint32_t sizeBytes) { - return visitPackedGifPacket(data, sizeBytes, [](const PackedGifPacketTag &) { return true; }); + return visitPackedGifPacket(data, sizeBytes, [](const PackedGifPacketTag &) + { return true; }); } void decodeDisplaySize(uint64_t display64, uint32_t &outWidth, uint32_t &outHeight) @@ -265,7 +281,7 @@ namespace return count; } - bool clearFramebufferRect(GS* gs, const GSContext &ctx, uint32_t rgba) + bool clearFramebufferRect(GS *gs, const GSContext &ctx, uint32_t rgba) { if (ctx.frame.fbw == 0u) { @@ -365,7 +381,7 @@ GS::GS() InitLookupTables(); - for (usz i = 0; i < 0x3F; ++i) + for (usz i = 0; i < m_read_vram_funcs.size(); ++i) { switch (i) { @@ -444,6 +460,8 @@ void GS::reset() std::lock_guard lock(m_stateMutex); std::memset(m_ctx, 0, sizeof(m_ctx)); m_prim = {}; + m_primRegister = {}; + m_prmodeRegister = {}; m_curR = 0x80; m_curG = 0x80; m_curB = 0x80; @@ -454,6 +472,9 @@ void GS::reset() m_curU = 0; m_curV = 0; m_curFog = 0; + m_fogR = 0; + m_fogG = 0; + m_fogB = 0; m_prmodecont = true; m_pabe = false; m_texa = {0u, false, 0u}; @@ -553,7 +574,6 @@ GSDebugSnapshot GS::getDebugSnapshot() const return snapshot; } - std::vector GS::getDebugHistory() const { std::lock_guard lock(m_stateMutex); @@ -1315,7 +1335,6 @@ void GS::processGIFPacket(const uint8_t *data, uint32_t sizeBytes) } }); - uint32_t offset = 0; while (offset + 16 <= sizeBytes) { @@ -1394,7 +1413,7 @@ bool GS::processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes) return false; const bool processed = visitPackedGifPacket(data, sizeBytes, [&](const PackedGifPacketTag &tag) - { + { m_curQ = 1.0f; recordGifTagDebugEventUnlocked(sizeBytes, tag.nloop, GIF_FMT_PACKED, tag.nreg); @@ -1415,8 +1434,7 @@ bool GS::processNativePackedGIFPacket(const uint8_t *data, uint32_t sizeBytes) } } - return true; - }); + return true; }); if (!processed) return false; @@ -1775,15 +1793,17 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value) { case GS_REG_PRIM: { - m_prim.type = static_cast(value & 0x7); - m_prim.iip = ((value >> 3) & 1) != 0; - m_prim.tme = ((value >> 4) & 1) != 0; - m_prim.fge = ((value >> 5) & 1) != 0; - m_prim.abe = ((value >> 6) & 1) != 0; - m_prim.aa1 = ((value >> 7) & 1) != 0; - m_prim.fst = ((value >> 8) & 1) != 0; - m_prim.ctxt = ((value >> 9) & 1) != 0; - m_prim.fix = ((value >> 10) & 1) != 0; + m_primRegister = decodePrimRegister(value); + if (m_prmodecont) + { + m_prim = m_primRegister; + } + else + { + // PRIM always selects the primitive topology. With AC=0, all + // rendering attributes remain sourced from PRMODE. + m_prim.type = m_primRegister.type; + } m_vtxCount = 0; m_vtxIndex = 0; break; @@ -1912,21 +1932,24 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value) break; } case GS_REG_PRMODECONT: + { m_prmodecont = (value & 1) != 0; + const GSPrimType type = m_primRegister.type; + m_prim = m_prmodecont ? m_primRegister : m_prmodeRegister; + m_prim.type = type; break; + } case GS_REG_PRMODE: + { + m_prmodeRegister = decodePrimRegister(value); if (!m_prmodecont) { - m_prim.iip = ((value >> 3) & 1) != 0; - m_prim.tme = ((value >> 4) & 1) != 0; - m_prim.fge = ((value >> 5) & 1) != 0; - m_prim.abe = ((value >> 6) & 1) != 0; - m_prim.aa1 = ((value >> 7) & 1) != 0; - m_prim.fst = ((value >> 8) & 1) != 0; - m_prim.ctxt = ((value >> 9) & 1) != 0; - m_prim.fix = ((value >> 10) & 1) != 0; + const GSPrimType type = m_primRegister.type; + m_prim = m_prmodeRegister; + m_prim.type = type; } break; + } case GS_REG_TEXCLUT: m_texclut.cbw = static_cast(value & 0x3Fu); m_texclut.cou = static_cast((value >> 6) & 0x3Fu); @@ -2041,9 +2064,13 @@ void GS::writeRegister(uint8_t regAddr, uint64_t value) case GS_REG_PABE: m_pabe = (value & 1u) != 0u; break; + case GS_REG_FOGCOL: + m_fogR = static_cast(value & 0xFFu); + m_fogG = static_cast((value >> 8) & 0xFFu); + m_fogB = static_cast((value >> 16) & 0xFFu); + break; case GS_REG_TEXFLUSH: case GS_REG_SCANMSK: - case GS_REG_FOGCOL: case GS_REG_DIMX: case GS_REG_DTHE: case GS_REG_COLCLAMP: @@ -2180,7 +2207,6 @@ void GS::performLocalToLocalTransfer() } break; - // left -> right // bottom -> top (invert y) case 1: @@ -2271,9 +2297,6 @@ void GS::vertexKick(bool drawing) } }); - if (!drawing) - return; - int needed = 0; switch (m_prim.type) { @@ -2305,8 +2328,11 @@ void GS::vertexKick(bool drawing) if (m_vtxCount < needed) return; - m_rasterizer.drawPrimitive(this); - recordDrawDebugEventUnlocked(needed); + if (drawing) + { + m_rasterizer.drawPrimitive(this); + recordDrawDebugEventUnlocked(needed); + } switch (m_prim.type) { diff --git a/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp b/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp index 9c14355..96cff26 100644 --- a/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp +++ b/ps2xRuntime/src/lib/ps2_gs_rasterizer.cpp @@ -14,7 +14,6 @@ #include #include #include -#include using namespace GSInternal; @@ -27,8 +26,8 @@ namespace u16 Rgba8888ToRgba5551(u32 c) { - uint32_t r = ((c >> 0) & 0xFF) >> 3; - uint32_t g = ((c >> 8) & 0xFF) >> 3; + uint32_t r = ((c >> 0) & 0xFF) >> 3; + uint32_t g = ((c >> 8) & 0xFF) >> 3; uint32_t b = ((c >> 16) & 0xFF) >> 3; uint32_t a = ((c >> 24) & 0xFF) >> 7; @@ -37,8 +36,8 @@ namespace u32 Rgba5551ToRgba8888(u16 c) { - u32 r = ((c >> 0) & 0x1F) << 3; - u32 g = ((c >> 5) & 0x1F) << 3; + u32 r = ((c >> 0) & 0x1F) << 3; + u32 g = ((c >> 5) & 0x1F) << 3; u32 b = ((c >> 10) & 0x1F) << 3; u32 a = ((c >> 15) & 0x01) << 7; @@ -101,6 +100,28 @@ namespace std::atomic s_debugPixelCount{0}; std::atomic s_debugContext1PrimitiveCount{0}; std::atomic s_debugFbp150PixelCount{0}; + + int wrapTextureCoordinate(int coordinate, + int textureSize, + uint8_t mode, + uint16_t regionMin, + uint16_t regionMax) + { + switch (mode & 0x3u) + { + case 0: // REPEAT + return static_cast(static_cast(coordinate) & static_cast(textureSize - 1)); + case 1: // CLAMP + return clampInt(coordinate, 0, textureSize - 1); + case 2: // REGION_CLAMP + return std::min(std::max(coordinate, static_cast(regionMin)), static_cast(regionMax)); + case 3: // REGION_REPEAT + return static_cast((static_cast(coordinate) & static_cast(regionMin)) | static_cast(regionMax)); + default: + return coordinate; + } + } + bool passesAlphaTest(uint64_t testReg, uint8_t alpha) { if ((testReg & 0x1u) == 0u) @@ -132,29 +153,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}; - case 3: // RGB_ONLY - return {true, true}; - case 0: // KEEP + return {true, true, false}; case 2: // ZB_ONLY + return {false, false, true}; + case 3: // RGB_ONLY + // 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 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; } } @@ -218,36 +277,52 @@ namespace uint32_t swizzleClutIndexCSM1(uint32_t index) { - return (index & 0xE7u) | ((index & 0x08u) << 1u) | ((index & 0x10u) >> 1u); + // CSM1 swaps address bits 3 and 4. Preserve the remaining bits: + // 16-bit CLUTs expose a ninth address bit through CSA[4]. + return (index & ~0x18u) | ((index & 0x08u) << 1u) | ((index & 0x10u) >> 1u); } // TODO: clut cache - uint32_t resolveClutIndex(uint8_t index, uint8_t csm, uint8_t csa, uint8_t sourcePsm) + uint32_t resolveClutIndex(uint8_t index, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm) { uint32_t clutIndex = static_cast(index); + // CSM2 addresses the source directly through TEXCLUT. CSA is required + // to be zero there, so it must not offset the source coordinates. + if (csm != 0u) + return (sourcePsm == GS_PSM_T4 || + sourcePsm == GS_PSM_T4HH || + sourcePsm == GS_PSM_T4HL) + ? (clutIndex & 0x0Fu) + : clutIndex; + + const bool is16BitClut = cpsm == GS_PSM_CT16 || cpsm == GS_PSM_CT16S; + const uint32_t csaMask = is16BitClut ? 0x1Fu : 0x0Fu; + const uint32_t clutIndexMask = is16BitClut ? 0x1FFu : 0x0FFu; + const uint32_t clutBase = (static_cast(csa) & csaMask) << 4u; + switch (sourcePsm) { case GS_PSM_T4: case GS_PSM_T4HH: case GS_PSM_T4HL: - { - clutIndex = (static_cast(csa) << 4u) | (clutIndex & 0x0Fu); - - if (csm == 0u) - clutIndex = swizzleClutIndexCSM1(clutIndex); - } - break; + clutIndex = clutBase + (clutIndex & 0x0Fu); + break; case GS_PSM_T8: case GS_PSM_T8H: - if (csm == 0) - clutIndex = swizzleClutIndexCSM1(clutIndex); + clutIndex = clutBase + clutIndex; break; default: - break; + return clutIndex; } - return clutIndex; + return swizzleClutIndexCSM1(clutIndex & clutIndexMask); + } + + int textureDimension(uint8_t exponent) + { + // TEX0.TW/TH saturate at 1024 pixels on the GS. + return 1 << std::min(exponent, 10u); } bool tex1UsesLinearFilter(uint64_t tex1) @@ -399,7 +474,7 @@ void GSRasterizer::drawPrimitive(GS *gs) const auto &ctx = gs->activeContext(); int px = static_cast(v.x) - (ctx.xyoffset.ofx >> 4); int py = static_cast(v.y) - (ctx.xyoffset.ofy >> 4); - writePixel(gs, px, py, static_cast(v.z), v.r, v.g, v.b, v.a); + writePixel(gs, px, py, static_cast(v.z), v.r, v.g, v.b, v.a, v.fog); break; } default: @@ -407,51 +482,72 @@ void GSRasterizer::drawPrimitive(GS *gs) } } -void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a) +void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t fog) { const auto &ctx = gs->activeContext(); - - if (x < ctx.scissor.x0 || x > ctx.scissor.x1 || - y < ctx.scissor.y0 || y > ctx.scissor.y1) + if (x < ctx.scissor.x0 || x > ctx.scissor.x1 || y < ctx.scissor.y0 || y > ctx.scissor.y1) return; - const AlphaTestResult alphaTest = classifyAlphaTest(ctx.test, a); + if (gs->m_prim.fge) + { + const uint32_t inverseFog = 255u - fog; + auto applyFog = [&](uint8_t input, uint8_t fogColor) -> uint8_t + { + return static_cast(((static_cast(fog) * input) >> 8) + ((inverseFog * fogColor) >> 8)); + }; - if (!alphaTest.writeFramebuffer) - return; + r = applyFog(r, gs->m_fogR); + g = applyFog(g, gs->m_fogG); + b = applyFog(b, gs->m_fogB); + } - u8* vram = gs->m_vram; - - const u32 fbp = GSInternal::framePageBaseToBlock(ctx.frame.fbp); - const u32 fbw = std::max(ctx.frame.fbw, 1u); + 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 uint32_t ztestMethod = static_cast((ctx.test >> 17) & 3u); 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; + } } - uint ztest_method = (ctx.test >> 17) & 3; - + if (!passesDestinationAlphaTest(ctx.test, static_cast(fpsm), rawFramebufferPixel)) + { + return; + } bool zpass = false; - switch (ztest_method) + uint32_t storedZ = 0u; + switch (ztestMethod) { case 0: zpass = false; @@ -460,10 +556,12 @@ void GSRasterizer::writePixel(GS *gs, int x, int y, int z, uint8_t r, uint8_t g, zpass = true; break; case 2: - zpass = z >= gs->ReadVram(zpsm, zbp, fbw, x, y); + storedZ = gs->ReadVram(zpsm, zbp, fbw, x, y); + zpass = static_cast(z) >= storedZ; break; case 3: - zpass = z > gs->ReadVram(zpsm, zbp, fbw, x, y); + storedZ = gs->ReadVram(zpsm, zbp, fbw, x, y); + zpass = static_cast(z) > storedZ; break; } @@ -472,81 +570,79 @@ 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); + uint8_t dr = fbrgba & 0xFF; + uint8_t dg = (fbrgba >> 8) & 0xFF; + uint8_t db = (fbrgba >> 16) & 0xFF; + uint8_t da = (fbrgba >> 24) & 0xFF; - auto pickRGB = [&](uint8_t sel, int cs, int cd) -> int + // PABE disables alpha blending when the source alpha MSB is clear. + if (!(gs->m_pabe && (a & 0x80u) == 0u)) { - if (sel == 0) - return cs; - if (sel == 1) - return cd; - return 0; - }; - int cAlpha = (csel == 0) ? a : (csel == 1) ? da - : fix; + 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); - 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)); + 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 + { + 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 pixel = pack32(r, g, b, a); + + if (ctx.frame.fbmsk != 0) + { + pixel = (pixel & ~ctx.frame.fbmsk) | (fbrgba & ctx.frame.fbmsk); + } + + if (preserveDestinationAlpha) + { + pixel = (pixel & 0x00FFFFFFu) | (fbrgba & 0xFF000000u); + } + + // format conversion + if (bitsPerPixel(fpsm) == 16) + { + pixel = Rgba8888ToRgba5551(pixel); + } + + gs->WriteVram(fpsm, fbp, fbw, x, y, pixel); } - u32 fbmask = ctx.frame.fbmsk; - bool zmask = ctx.zbuf.zmask; - - if (!alphaTest.preserveDestinationAlpha && - (ctx.fba & 0x1ull) != 0ull && - ctx.frame.psm != GS_PSM_CT24) - { - a = static_cast(a | 0x80u); - } - - u32 pixel = pack32(r, g, b, a); - - if (fbmask != 0) - { - pixel = (pixel & ~fbmask) | (fbrgba & fbmask); - } - - if (alphaTest.preserveDestinationAlpha) - { - pixel = (pixel & 0x00FFFFFFu) | (fbrgba & 0xFF000000u); - } - - // 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); } @@ -560,12 +656,11 @@ uint32_t GSRasterizer::lookupCLUT(GS *gs, uint8_t csa, uint8_t sourcePsm) { - const uint32_t clutIndex = resolveClutIndex(index, csm, csa, sourcePsm); + const uint32_t clutIndex = resolveClutIndex(index, cpsm, csm, csa, sourcePsm); const uint32_t clutWidth = (gs->m_texclut.cbw != 0u) ? static_cast(gs->m_texclut.cbw) : 1u; const uint32_t clutX = static_cast(gs->m_texclut.cou) + (clutIndex & 0x0Fu); const uint32_t clutY = static_cast(gs->m_texclut.cov) + (clutIndex >> 4); - switch (cpsm) { case GS_PSM_CT32: @@ -588,8 +683,15 @@ uint32_t GSRasterizer::sampleTexture(GS *gs, float s, float t, float q, uint16_t const auto &ctx = gs->activeContext(); const auto &tex = ctx.tex0; - int texW = 1 << tex.tw; - int texH = 1 << tex.th; + const int texW = textureDimension(tex.tw); + const int texH = textureDimension(tex.th); + const uint64_t clamp = ctx.clamp; + const uint8_t wrapU = static_cast(clamp & 0x3u); + const uint8_t wrapV = static_cast((clamp >> 2) & 0x3u); + const uint16_t minU = static_cast((clamp >> 4) & 0x3FFu); + const uint16_t maxU = static_cast((clamp >> 14) & 0x3FFu); + const uint16_t minV = static_cast((clamp >> 24) & 0x3FFu); + const uint16_t maxV = static_cast((clamp >> 34) & 0x3FFu); float texUf, texVf; if (gs->m_prim.fst) @@ -606,8 +708,8 @@ uint32_t GSRasterizer::sampleTexture(GS *gs, float s, float t, float q, uint16_t auto samplePoint = [&](int sampleU, int sampleV) -> uint32_t { - sampleU = clampInt(sampleU, 0, texW - 1); - sampleV = clampInt(sampleV, 0, texH - 1); + sampleU = wrapTextureCoordinate(sampleU, texW, wrapU, minU, maxU); + sampleV = wrapTextureCoordinate(sampleV, texH, wrapV, minV, maxV); u32 out = gs->ReadVram(tex.psm, tex.tbp0, tex.tbw, sampleU, sampleV); @@ -747,12 +849,8 @@ void GSRasterizer::drawSprite(GS *gs) if (gs->m_prim.tme) { const auto &tex = ctx.tex0; - int texW = 1 << tex.tw; - int texH = 1 << tex.th; - if (texW == 0) - texW = 1; - if (texH == 0) - texH = 1; + const int texW = textureDimension(tex.tw); + const int texH = textureDimension(tex.th); float u0f, v0f, u1f, v1f; if (gs->m_prim.fst) @@ -799,10 +897,7 @@ void GSRasterizer::drawSprite(GS *gs) } else { - texel = sampleTexture(gs, - texUf / static_cast(texW), - texVf / static_cast(texH), - 1.0f, 0u, 0u); + texel = sampleTexture(gs, texUf / static_cast(texW), texVf / static_cast(texH), 1.0f, 0u, 0u); } uint8_t tr = static_cast(texel & 0xFF); @@ -811,7 +906,7 @@ void GSRasterizer::drawSprite(GS *gs) uint8_t ta = static_cast((texel >> 24) & 0xFF); const TextureCombineResult color = combineTexture(tex, r, g, b, a, tr, tg, tb, ta); - writePixel(gs, x, y, z1, color.r, color.g, color.b, color.a); + writePixel(gs, x, y, z1, color.r, color.g, color.b, color.a, v1.fog); } } } @@ -819,7 +914,7 @@ void GSRasterizer::drawSprite(GS *gs) { for (int y = drawY0; y <= drawY1; ++y) for (int x = drawX0; x <= drawX1; ++x) - writePixel(gs, x, y, z1, r, g, b, a); + writePixel(gs, x, y, z1, r, g, b, a, v1.fog); } } @@ -904,15 +999,12 @@ void GSRasterizer::drawTriangle(GS *gs) } else { - const float invQ0 = 1.0f / fabsQ(v0.q); - const float invQ1 = 1.0f / fabsQ(v1.q); - const float invQ2 = 1.0f / fabsQ(v2.q); - const float sOverQ = (v0.s * invQ0) * w0 + (v1.s * invQ1) * w1 + (v2.s * invQ2) * w2; - const float tOverQ = (v0.t * invQ0) * w0 + (v1.t * invQ1) * w1 + (v2.t * invQ2) * w2; - const float invQ = invQ0 * w0 + invQ1 * w1 + invQ2 * w2; - iq = (std::fabs(invQ) > 1.0e-8f) ? (1.0f / invQ) : 1.0f; - is = sOverQ * iq; - it = tOverQ * iq; + // The GS DDA interpolates the homogeneous S, T and Q + // values. Texel coordinates are calculated from S/Q and + // T/Q only after interpolation. + is = v0.s * w0 + v1.s * w1 + v2.s * w2; + it = v0.t * w0 + v1.t * w1 + v2.t * w2; + iq = v0.q * w0 + v1.q * w1 + v2.q * w2; iu = 0; iv = 0; } @@ -937,7 +1029,8 @@ void GSRasterizer::drawTriangle(GS *gs) a = color.a; } - writePixel(gs, x, y, static_cast(z + 0.5), r, g, b, a); + const uint8_t fog = clampU8(static_cast(v0.fog * w0 + v1.fog * w1 + v2.fog * w2)); + writePixel(gs, x, y, static_cast(z + 0.5), r, g, b, a, fog); } } } @@ -987,8 +1080,8 @@ void GSRasterizer::drawLine(GS *gs) } double z = (v0.z + (v1.z - v0.z) * t); - - writePixel(gs, x0, y0, static_cast(z), r, g, b, a); + const uint8_t fog = clampU8(static_cast(v0.fog + (v1.fog - v0.fog) * t)); + writePixel(gs, x0, y0, static_cast(z), r, g, b, a, fog); if (x0 == x1 && y0 == y1) break; diff --git a/ps2xRuntime/src/lib/ps2_memory.cpp b/ps2xRuntime/src/lib/ps2_memory.cpp index 3d59328..aabacfc 100644 --- a/ps2xRuntime/src/lib/ps2_memory.cpp +++ b/ps2xRuntime/src/lib/ps2_memory.cpp @@ -351,6 +351,7 @@ bool PS2Memory::initialize(size_t ramSize) m_vu1Data = new uint8_t[PS2_VU1_DATA_SIZE]; std::memset(m_vu1Code, 0, PS2_VU1_CODE_SIZE); std::memset(m_vu1Data, 0, PS2_VU1_DATA_SIZE); + markVU0CodeModified(); markVU1CodeModified(); // Initialize VIF registers @@ -765,7 +766,9 @@ void PS2Memory::write8(uint32_t address, uint8_t value) { (void)vuLimit; vuMem[vuOffset] = value; - if (vuMem == m_vu1Code) + if (vuMem == m_vu0Code) + markVU0CodeModified(); + else if (vuMem == m_vu1Code) markVU1CodeModified(); return; } @@ -806,7 +809,9 @@ void PS2Memory::write16(uint32_t address, uint16_t value) if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint16_t), vuOffset, vuLimit)) { storeScalar(vuMem, vuOffset, vuLimit, value, "write16 vu", address); - if (vuMem == m_vu1Code) + if (vuMem == m_vu0Code) + markVU0CodeModified(); + else if (vuMem == m_vu1Code) markVU1CodeModified(); return; } @@ -868,7 +873,9 @@ void PS2Memory::write32(uint32_t address, uint32_t value) if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint32_t), vuOffset, vuLimit)) { storeScalar(vuMem, vuOffset, vuLimit, value, "write32 vu", address); - if (vuMem == m_vu1Code) + if (vuMem == m_vu0Code) + markVU0CodeModified(); + else if (vuMem == m_vu1Code) markVU1CodeModified(); return; } @@ -921,7 +928,9 @@ void PS2Memory::write64(uint32_t address, uint64_t value) if (uint8_t *vuMem = mapVuMemory(physAddr, sizeof(uint64_t), vuOffset, vuLimit)) { storeScalar(vuMem, vuOffset, vuLimit, value, "write64 vu", address); - if (vuMem == m_vu1Code) + if (vuMem == m_vu0Code) + markVU0CodeModified(); + else if (vuMem == m_vu1Code) markVU1CodeModified(); return; } @@ -962,7 +971,9 @@ void PS2Memory::write128(uint32_t address, __m128i value) { inRange(vuOffset, sizeof(__m128i), vuLimit, "write128 vu", address); _mm_storeu_si128(reinterpret_cast<__m128i *>(vuMem + vuOffset), value); - if (vuMem == m_vu1Code) + if (vuMem == m_vu0Code) + markVU0CodeModified(); + else if (vuMem == m_vu1Code) markVU1CodeModified(); return; } diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index b79e386..cc8d203 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -209,6 +209,7 @@ namespace 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; } @@ -230,11 +231,16 @@ namespace 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; @@ -258,6 +264,7 @@ namespace 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; @@ -265,7 +272,7 @@ namespace ctx->vu0_itop = state.itop; ctx->vu0_pc = state.pc; ctx->vu0_tpc = state.pc; - ctx->vu0_vpu_stat = 0; + 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); @@ -523,6 +530,9 @@ PS2Runtime::PS2Runtime() // R0 is always zero in MIPS m_cpuContext.r[0] = _mm_set1_epi32(0); + m_cpuContext.vu0_vf[0] = _mm_set_ps(1.0f, 0.0f, 0.0f, 0.0f); + m_cpuContext.vu0_q = 1.0f; + m_cpuContext.vu0_r = _mm_castsi128_ps(_mm_set1_epi32(0x3F800000)); // Stack pointer (SP) and global pointer (GP) will be set by the loaded ELF @@ -647,13 +657,31 @@ bool PS2Runtime::syncCoreSubsystems() { m_gs.processGIFPacket(data, size); }); m_memory.setGifArbiter(&m_gifArbiter); m_memory.setVu1MscalCallback([this](uint32_t startPC, uint32_t top, uint32_t itop) - { 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); }); + { + m_vu1.state().dBitEnabled = + (m_cpuContext.vu0_fbrst & (1u << 10)) != 0u; + m_vu1.state().tBitEnabled = + (m_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); + m_cpuContext.vu0_vpu_stat = + (m_cpuContext.vu0_vpu_stat & ~0x0600u) | + (m_vu1.state().stoppedByD ? 0x0200u : 0u) | + (m_vu1.state().stoppedByT ? 0x0400u : 0u); }); m_memory.setVu1MscntCallback([this](uint32_t top, uint32_t itop) - { m_vu1.resume(m_memory.getVU1Code(), PS2_VU1_CODE_SIZE, - m_memory.getVU1Data(), PS2_VU1_DATA_SIZE, - m_gs, &m_memory, top, itop, 65536); }); + { + m_vu1.state().dBitEnabled = + (m_cpuContext.vu0_fbrst & (1u << 10)) != 0u; + m_vu1.state().tBitEnabled = + (m_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); + m_cpuContext.vu0_vpu_stat = + (m_cpuContext.vu0_vpu_stat & ~0x0600u) | + (m_vu1.state().stoppedByD ? 0x0200u : 0u) | + (m_vu1.state().stoppedByT ? 0x0400u : 0u); }); resetIop(); m_vu0.reset(); m_vu1.reset(); @@ -1157,6 +1185,10 @@ void PS2Runtime::reportMissingFunction(uint8_t *rdram, const uint32_t gp = static_cast(_mm_extract_epi32(ctx->r[28], 0)); const uint32_t a0 = static_cast(_mm_extract_epi32(ctx->r[4], 0)); const uint32_t a1 = static_cast(_mm_extract_epi32(ctx->r[5], 0)); + const uint32_t a2 = static_cast(_mm_extract_epi32(ctx->r[6], 0)); + const uint32_t a3 = static_cast(_mm_extract_epi32(ctx->r[7], 0)); + const uint32_t s0 = static_cast(_mm_extract_epi32(ctx->r[16], 0)); + const uint32_t s1 = static_cast(_mm_extract_epi32(ctx->r[17], 0)); const uint32_t v0 = static_cast(_mm_extract_epi32(ctx->r[2], 0)); const uint32_t v1 = static_cast(_mm_extract_epi32(ctx->r[3], 0)); @@ -1194,6 +1226,27 @@ void PS2Runtime::reportMissingFunction(uint8_t *rdram, readGuestU32Offset(a0, 0x08u, a0Word8) && readGuestU32Offset(a0, 0x0cu, a0WordC); + uint32_t s0Word0 = 0u; + uint32_t s0Word4 = 0u; + uint32_t s0Word8 = 0u; + uint32_t s0WordC = 0u; + const bool s0Readable = + readGuestU32Offset(s0, 0x00u, s0Word0) && + readGuestU32Offset(s0, 0x04u, s0Word4) && + readGuestU32Offset(s0, 0x08u, s0Word8) && + readGuestU32Offset(s0, 0x0cu, s0WordC); + + uint32_t recordWord0 = 0u; + uint32_t recordWord4 = 0u; + uint32_t recordWord8 = 0u; + uint32_t recordWordC = 0u; + const bool recordReadable = + s0Readable && s0Word4 != 0u && + readGuestU32Offset(s0Word4, 0x00u, recordWord0) && + readGuestU32Offset(s0Word4, 0x04u, recordWord4) && + readGuestU32Offset(s0Word4, 0x08u, recordWord8) && + readGuestU32Offset(s0Word4, 0x0cu, recordWordC); + uint32_t vtableSlot0 = 0u; uint32_t vtableSlot4 = 0u; uint32_t vtableSlot8 = 0u; @@ -1218,6 +1271,10 @@ void PS2Runtime::reportMissingFunction(uint8_t *rdram, << " gp=0x" << gp << " a0=0x" << a0 << " a1=0x" << a1 + << " a2=0x" << a2 + << " a3=0x" << a3 + << " s0=0x" << s0 + << " s1=0x" << s1 << " v0=0x" << v0 << " v1=0x" << v1 << " a0Readable=" << (a0Readable ? "yes" : "no") @@ -1225,6 +1282,16 @@ void PS2Runtime::reportMissingFunction(uint8_t *rdram, << " a0[4]=0x" << a0Word4 << " a0[8]=0x" << a0Word8 << " a0[c]=0x" << a0WordC + << " s0Readable=" << (s0Readable ? "yes" : "no") + << " s0[0]=0x" << s0Word0 + << " s0[4]=0x" << s0Word4 + << " s0[8]=0x" << s0Word8 + << " s0[c]=0x" << s0WordC + << " recordReadable=" << (recordReadable ? "yes" : "no") + << " record[0]=0x" << recordWord0 + << " record[4]=0x" << recordWord4 + << " record[8]=0x" << recordWord8 + << " record[c]=0x" << recordWordC << " vtableReadable=" << (vtableReadable ? "yes" : "no") << " vtbl[0]=0x" << vtableSlot0 << " vtbl[4]=0x" << vtableSlot4 @@ -2152,16 +2219,19 @@ void PS2Runtime::yieldGuestExecutionAfterWake() { GuestExecutionReleaseScope releaseGuestExecution(this); std::unique_lock lock(m_guestExecutionHandoffMutex); - m_guestExecutionHandoffCv.wait_for(lock, std::chrono::milliseconds(2), [&]() + m_guestExecutionHandoffCv.wait_for(lock, std::chrono::milliseconds(1), [&]() { return m_guestExecutionHandoffEpoch.load(std::memory_order_acquire) != handoffEpoch; }); } } 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/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index c0af2a3..05fc764 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -144,7 +144,10 @@ void PS2Memory::processVIF0Data(const uint8_t *data, uint32_t sizeBytes) if (destAddr + copyBytes > PS2_VU0_CODE_SIZE) copyBytes = PS2_VU0_CODE_SIZE - destAddr; if (pos + copyBytes <= sizeBytes) + { std::memcpy(m_vu0Code + destAddr, data + pos, copyBytes); + markVU0CodeModified(); + } } pos += mpgBytes; diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp index 6f24894..dbaead4 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp @@ -1,36 +1,192 @@ #include "runtime/ps2_vu1.h" +#include "runtime/ps2_gif_arbiter.h" +#include "runtime/ps2_gs_gpu.h" #include "runtime/ps2_memory.h" #include "ps2_vu1_detail.h" +#include +#include +#include +#include +#include #include +#include +#include +#include -VU1Interpreter::VU1Interpreter() +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; // VF0.w = 1.0 + 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 vf[bc & 3]; + 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 & 0x8) - dst[0] = result[0]; // x - if (dest & 0x4) - dst[1] = result[1]; // y - if (dest & 0x2) - dst[2] = result[2]; // z - if (dest & 0x1) - dst[3] = result[3]; // w + 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) @@ -38,27 +194,1349 @@ 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); - decoded.iBit = ((decoded.upper >> 31) & 1u) != 0u; - decoded.eBit = ((decoded.upper >> 30) & 1u) != 0u; - decoded.lowerBeforeUpper = !decoded.iBit && vuLowerShouldRunBeforeUpper(decoded.upper, 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 = codeSize / 8u; - m_decodedCodeCache.resize(pairCount); + 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; @@ -67,37 +1545,43 @@ void VU1Interpreter::rebuildDecodedCodeCache(const uint8_t *vuCode, uint32_t cod m_decodedCodeCacheValid = true; } -VU1Interpreter::DecodedInstructionPair VU1Interpreter::getDecodedInstructionPairForPc(const uint8_t *vuCode, - uint32_t codeSize, - PS2Memory *memory, - uint32_t pc) +VU1Interpreter::DecodedInstructionPair VU1Interpreter::getDecodedInstructionPairForPc( + const uint8_t *vuCode, uint32_t codeSize, PS2Memory *memory, uint32_t pc) { - // Only 8-byte aligned VU instruction pairs can use the decode cache. if ((pc & 7u) != 0u) - { return decodeInstructionPair(vuCode, pc); - } - const bool trackedVu1Code = vuCode == memory->getVU1Code(); + 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 = memory->getVU1CodeGeneration(); - const bool rebuild = - !m_decodedCodeCacheValid || + 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; - - if (rebuild) + 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]; +} - return m_decodedCodeCache[pc / 8u]; +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, @@ -106,8 +1590,12 @@ void VU1Interpreter::execute(uint8_t *vuCode, uint32_t codeSize, uint32_t startPC, uint32_t top, uint32_t itop, uint32_t maxCycles) { - m_state.pc = startPC & 0x3FFFu; + 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; @@ -125,9 +1613,10 @@ void VU1Interpreter::resume(uint8_t *vuCode, uint32_t codeSize, GS &gs, PS2Memory *memory, uint32_t top, uint32_t itop, uint32_t maxCycles) { - m_state.ebit = false; m_state.top = top; m_state.itop = itop; + m_state.stoppedByD = false; + m_state.stoppedByT = false; run(vuCode, codeSize, vuData, dataSize, gs, memory, maxCycles); } @@ -135,28 +1624,97 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t maxCycles) { - for (uint32_t cycle = 0; cycle < maxCycles; ++cycle) + 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) { - if (m_state.pc + 8 > codeSize) + 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)); - // LOI is controlled by the upper I-bit. The lower word is the float immediate. - // DobieStation executes the upper instruction first, then commits lower into I. if (decoded.iBit) { - // LOI is special: the upper instruction sees the old I value, then LOI loads I. execUpper(decoded.upper); - std::memcpy(&m_state.i, &decoded.lower, sizeof(decoded.lower)); + float immediate = 0.0f; + std::memcpy(&immediate, &decoded.lower, sizeof(immediate)); + m_state.i = normalizeOperand(immediate); } - else if (decoded.lowerBeforeUpper) + else if (decoded.upperVfShadowReg != 0u) { - // VU upper/lower execute as a pair. If the upper op writes a VF register - // that the lower op reads or also writes, Dobie runs the lower side first - // so it observes the old VF value and the upper write has priority. - execLower(decoded.lower, vuData, dataSize, gs, memory, decoded.upper); + 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 { @@ -164,26 +1722,67 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, execLower(decoded.lower, vuData, dataSize, gs, memory, decoded.upper); } - // Enforce VF0 invariant + 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; - // Enforce VI0 invariant m_state.vi[0] = 0; - uint32_t nextPC = m_state.pc + 8; - if (nextPC >= codeSize) - nextPC = 0; - m_state.pc = nextPC; + uint32_t nextPc = m_state.pc + 8u; + if (nextPc >= codeSize) + nextPc = 0u; + m_state.pc = nextPc; - // VU branch/jump has a delay slot. Branch handlers set a pending target; - // we execute one sequential instruction before committing the branch. if (m_state.branchPending) { - if (m_state.branchDelay == 0) + if (m_state.branchDelay == 0u) { - m_state.pc = m_state.branchTarget & 0x3FFFu; + m_state.pc = m_state.branchTarget & microAddressMask(); m_state.branchPending = false; } else @@ -192,10 +1791,48 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, } } - if (m_state.ebit) - break; + 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 (decoded.eBit) + 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 index 40acd76..ed0ee71 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h @@ -26,132 +26,4 @@ static inline int16_t IMM15(uint32_t i) return (int16_t)(int32_t)((int32_t)(raw << 17) >> 17); } - -static inline uint8_t vuUpperVfWriteReg(uint32_t upper) -{ - const uint8_t op = upper & 0x3Fu; - const uint8_t dest = DEST(upper); - const uint8_t ft = FT(upper); - const uint8_t fd = FD(upper); - - if (dest == 0u) - return 0u; - - if (op <= 0x2Fu) - return fd; - - if (op >= 0x3Cu) - { - const uint8_t specialOp = static_cast((upper & 0x3u) | ((upper >> 4) & 0x7Cu)); - switch (specialOp) - { - // Upper special ops that write a VF register use FT as destination. - case 0x10: // ITOF0 - case 0x11: // ITOF4 - case 0x12: // ITOF12 - case 0x13: // ITOF15 - case 0x14: // FTOI0 - case 0x15: // FTOI4 - case 0x16: // FTOI12 - case 0x17: // FTOI15 - case 0x1D: // ABS - return ft; - default: - return 0u; // ACC/NOP/CLIP/etc. - } - } - - return 0u; -} - -static inline void vuSetRegBit(uint32_t &mask, uint8_t reg) -{ - if (reg != 0u && reg < 32u) - mask |= (1u << reg); -} - -static inline void vuLowerVfReadWriteMasks(uint32_t lower, uint32_t &readMask, uint32_t &writeMask) -{ - readMask = 0u; - writeMask = 0u; - - if (lower == 0u || lower == 0x8000033Cu) - return; - - const uint8_t opHi = static_cast((lower >> 25) & 0x7Fu); - const uint8_t it = LIT(lower); - const uint8_t is = LIS(lower); - - if ((lower & 0x80000000u) != 0u) - { - const uint8_t funct = lower & 0x3Fu; - if (funct >= 0x3Cu && funct <= 0x3Fu) - { - const uint8_t specialOp = static_cast((lower & 0x3u) | ((lower >> 4) & 0x7Cu)); - switch (specialOp) - { - case 0x30: // MOVE - case 0x31: // MR32 - vuSetRegBit(readMask, is); - vuSetRegBit(writeMask, it); - return; - case 0x34: // LQI - case 0x36: // LQD - vuSetRegBit(writeMask, it); - return; - case 0x35: // SQI - case 0x37: // SQD - vuSetRegBit(readMask, is); - return; - case 0x38: // DIV - case 0x3A: // RSQRT - vuSetRegBit(readMask, is); - vuSetRegBit(readMask, it); - return; - case 0x39: // SQRT - vuSetRegBit(readMask, it); - return; - case 0x3C: // MTIR - case 0x3E: // ILWR source base is integer, but field source is VF for MTIR only. - if (specialOp == 0x3C) - vuSetRegBit(readMask, is); - return; - case 0x3D: // MFIR - case 0x64: // MFP - vuSetRegBit(writeMask, it); - return; - default: - return; - } - } - return; - } - - switch (opHi) - { - case 0x00: // LQ - vuSetRegBit(writeMask, it); - return; - case 0x01: // SQ - vuSetRegBit(readMask, is); - return; - default: - return; - } -} - -static inline bool vuLowerShouldRunBeforeUpper(uint32_t upper, uint32_t lower) -{ - const uint8_t upperWrite = vuUpperVfWriteReg(upper); - if (upperWrite == 0u) - return false; - - uint32_t lowerReads = 0u; - uint32_t lowerWrites = 0u; - vuLowerVfReadWriteMasks(lower, lowerReads, lowerWrites); - - const uint32_t upperBit = (1u << upperWrite); - return ((lowerReads | lowerWrites) & upperBit) != 0u; -} - -#endif \ No newline at end of file +#endif diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp index f89ba7b..7d235d7 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp @@ -7,7 +7,64 @@ #include #include #include -#include + +namespace +{ + 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; + float polynomial = coefficients[7]; + for (int index = 6; index >= 0; --index) + polynomial = coefficients[index] + squared * polynomial; + return quarterPi + value * polynomial; + } + + 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]; + for (int index = 3; index >= 0; --index) + polynomial = coefficients[index] + squared * polynomial; + return value * polynomial; + } + + 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) + polynomial = coefficients[index] + value * polynomial; + polynomial = 1.0f + value * polynomial; + polynomial *= polynomial; + polynomial *= polynomial; + return polynomial != 0.0f ? 1.0f / polynomial : std::numeric_limits::max(); + } +} // ============================================================================ // Lower instructions @@ -19,14 +76,15 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz 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) { - uint8_t it = FT(instr); // VF destination - uint8_t is = VIS(instr); // VI base + uint8_t it = FT(instr); // VF destination + uint8_t is = VIS(instr); // VI base uint8_t dest = (instr >> 21) & 0xF; int16_t imm = IMM11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; @@ -41,32 +99,24 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x01: // SQ (Store Quadword to VU data memory) { - uint8_t is = FS(instr); // VF source - uint8_t it = VIT(instr); // VI base + uint8_t is = FS(instr); // VF source + uint8_t it = VIT(instr); // VI base uint8_t dest = (instr >> 21) & 0xF; int16_t imm = IMM11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[it] + imm)) * 16u; addr &= (dataSize - 1); if (addr + 16 <= dataSize) { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - if (dest & 0x8) - tmp[0] = m_state.vf[is][0]; - if (dest & 0x4) - tmp[1] = m_state.vf[is][1]; - if (dest & 0x2) - tmp[2] = m_state.vf[is][2]; - if (dest & 0x1) - tmp[3] = m_state.vf[is][3]; - std::memcpy(vuData + addr, tmp, 16); + uint32_t words[4]{}; + std::memcpy(words, m_state.vf[is], sizeof(words)); + queueStore(addr, words, dest); } return; } case 0x04: // ILW (Integer Load Word from VU data memory) { - uint8_t it = VIT(instr); // VI destination - uint8_t is = VIS(instr); // VI base + uint8_t it = VIT(instr); // VI destination + uint8_t is = VIS(instr); // VI base uint8_t dest = (instr >> 21) & 0xF; int16_t imm = IMM11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; @@ -91,23 +141,17 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x05: // ISW (Integer Store Word to VU data memory) { - uint8_t it = VIT(instr); // VI source - uint8_t is = VIS(instr); // VI base + uint8_t it = VIT(instr); // VI source + uint8_t is = VIS(instr); // VI base uint8_t dest = (instr >> 21) & 0xF; int16_t imm = IMM11(instr); uint32_t addr = ((uint32_t)(int32_t)(m_state.vi[is] + imm)) * 16u; addr &= (dataSize - 1); if (addr + 16 <= dataSize) { - uint32_t val = (uint32_t)(uint16_t)(m_state.vi[it] & 0xFFFF); - if (dest & 0x8) - std::memcpy(vuData + addr + 0, &val, 4); - if (dest & 0x4) - std::memcpy(vuData + addr + 4, &val, 4); - if (dest & 0x2) - std::memcpy(vuData + addr + 8, &val, 4); - if (dest & 0x1) - std::memcpy(vuData + addr + 12, &val, 4); + const uint32_t val = static_cast(static_cast(m_state.vi[it] & 0xFFFF)); + const uint32_t words[4] = {val, val, val, val}; + queueStore(addr, words, dest); } return; } @@ -138,7 +182,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz } case 0x11: // FCSET { - m_state.clip = instr & 0xFFFFFF; + queueFcset(instr & 0xFFFFFFu); return; } case 0x12: // FCAND @@ -157,39 +201,35 @@ 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; - return; - } - case 0x18: // FMAND - { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); + 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] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]); + m_state.vi[it] = static_cast((m_state.status & 0xFFFu) | imm12); return; } - case 0x1A: // FMEQ + case 0x18: // FMEQ { uint8_t it = VIT(instr); uint8_t is = VIS(instr); @@ -197,7 +237,15 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.vi[it] = ((m_state.mac & 0xFFFF) == (uint32_t)(uint16_t)m_state.vi[is]) ? 1 : 0; return; } - case 0x1C: // FMOR + case 0x1A: // FMAND + { + uint8_t it = VIT(instr); + uint8_t is = VIS(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 { uint8_t it = VIT(instr); uint8_t is = VIS(instr); @@ -205,10 +253,17 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz m_state.vi[it] = (int32_t)(m_state.mac | (uint32_t)(uint16_t)m_state.vi[is]); return; } + case 0x1C: // FCGET + { + const uint8_t it = VIT(instr); + if (it != 0) + m_state.vi[it] = static_cast(m_state.clip & 0x0FFFu); + return; + } case 0x20: // B (unconditional branch) { int16_t imm = IMM11(instr); - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; m_state.branchPending = true; m_state.branchTarget = target; m_state.branchDelay = 1; @@ -218,7 +273,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { uint8_t it = VIT(instr); int16_t imm = IMM11(instr); - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; if (it != 0) m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); m_state.branchPending = true; @@ -229,7 +284,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz case 0x24: // JR { uint8_t is = VIS(instr); - uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF; + uint32_t target = ((uint32_t)(uint16_t)readBranchVi(is) * 8u) & pcMask; m_state.branchPending = true; m_state.branchTarget = target; m_state.branchDelay = 1; @@ -239,7 +294,7 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { uint8_t it = VIT(instr); uint8_t is = VIS(instr); - uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF; + 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); m_state.branchPending = true; @@ -252,12 +307,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz uint8_t it = VIT(instr); uint8_t is = VIS(instr); int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] == (int16_t)m_state.vi[it]) + if ((int16_t)readBranchVi(is) == (int16_t)readBranchVi(it)) { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } @@ -266,12 +321,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz uint8_t it = VIT(instr); uint8_t is = VIS(instr); int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] != (int16_t)m_state.vi[it]) + if ((int16_t)readBranchVi(is) != (int16_t)readBranchVi(it)) { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } @@ -279,12 +334,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { uint8_t is = VIS(instr); int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] < 0) + if ((int16_t)readBranchVi(is) < 0) { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } @@ -292,12 +347,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { uint8_t is = VIS(instr); int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] > 0) + if ((int16_t)readBranchVi(is) > 0) { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } @@ -305,12 +360,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { uint8_t is = VIS(instr); int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] <= 0) + if ((int16_t)readBranchVi(is) <= 0) { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } @@ -318,12 +373,12 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { uint8_t is = VIS(instr); int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] >= 0) + if ((int16_t)readBranchVi(is) >= 0) { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + uint32_t target = (m_state.pc + 8 + imm * 8) & pcMask; m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; + m_state.branchTarget = target; + m_state.branchDelay = 1; } return; } @@ -338,95 +393,6 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz const uint8_t viD = VID(instr); const uint8_t dest = (instr >> 21) & 0xF; - auto doXgkick = [&]() - { - if (!vuData || dataSize < 16u) - return; - - auto wrapOffset = [&](uint32_t off) -> uint32_t - { - return off % dataSize; - }; - - auto read64Wrap = [&](uint32_t off) -> uint64_t - { - uint8_t bytes[8]; - for (uint32_t i = 0; i < 8u; ++i) - { - bytes[i] = vuData[wrapOffset(off + i)]; - } - uint64_t value = 0; - std::memcpy(&value, bytes, sizeof(value)); - return value; - }; - - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - addr = wrapOffset(addr); - uint32_t pktOff = addr; - uint32_t totalBytes = 0u; - bool done = false; - - for (int safety = 0; safety < 256 && !done; ++safety) - { - uint64_t tagLo = read64Wrap(pktOff); - uint32_t nloop = (uint32_t)(tagLo & 0x7FFFu); - uint8_t flg = (uint8_t)((tagLo >> 58) & 0x3u); - uint32_t nreg = (uint32_t)((tagLo >> 60) & 0xFu); - if (nreg == 0u) - nreg = 16u; - bool eop = ((tagLo >> 15) & 0x1ull) != 0ull; - - uint32_t pktSize = 16u; - if (flg == 0u) - { - pktSize += nloop * nreg * 16u; - } - else if (flg == 1u) - { - uint32_t regs = nloop * nreg; - pktSize += regs * 8u; - if ((regs & 1u) != 0u) - pktSize += 8u; - } - else if (flg == 2u) - { - pktSize += nloop * 16u; - } - - if (pktSize == 0u) - break; - - totalBytes += pktSize; - pktOff = wrapOffset(pktOff + pktSize); - if (eop) - done = true; - } - - if (totalBytes == 0u) - return; - - if (addr + totalBytes <= dataSize) - { - if (memory) - memory->submitGifPacket(GifPathId::Path1, vuData + addr, totalBytes); - else - gs.processGIFPacket(vuData + addr, totalBytes); - } - else - { - std::vector wrappedPacket(totalBytes); - for (uint32_t i = 0; i < totalBytes; ++i) - { - wrappedPacket[i] = vuData[wrapOffset(addr + i)]; - } - - if (memory) - memory->submitGifPacket(GifPathId::Path1, wrappedPacket.data(), totalBytes); - else - gs.processGIFPacket(wrappedPacket.data(), totalBytes); - } - }; - switch (funct) { case 0x30: // IADD @@ -494,17 +460,9 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz addr &= (dataSize - 1); if (addr + 16 <= dataSize) { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - if (dest & 0x8) - tmp[0] = m_state.vf[vfS][0]; - if (dest & 0x4) - tmp[1] = m_state.vf[vfS][1]; - if (dest & 0x2) - tmp[2] = m_state.vf[vfS][2]; - if (dest & 0x1) - tmp[3] = m_state.vf[vfS][3]; - std::memcpy(vuData + addr, tmp, 16); + uint32_t words[4]{}; + std::memcpy(words, m_state.vf[vfS], sizeof(words)); + queueStore(addr, words, dest); } if (viT != 0) m_state.vi[viT] = (int16_t)(m_state.vi[viT] + 1); @@ -532,17 +490,9 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz addr &= (dataSize - 1); if (addr + 16 <= dataSize) { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - if (dest & 0x8) - tmp[0] = m_state.vf[vfS][0]; - if (dest & 0x4) - tmp[1] = m_state.vf[vfS][1]; - if (dest & 0x2) - tmp[2] = m_state.vf[vfS][2]; - if (dest & 0x1) - tmp[3] = m_state.vf[vfS][3]; - std::memcpy(vuData + addr, tmp, 16); + uint32_t words[4]{}; + std::memcpy(words, m_state.vf[vfS], sizeof(words)); + queueStore(addr, words, dest); } return; } @@ -550,46 +500,64 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz { int fsf = (instr >> 21) & 0x3; int ftf = (instr >> 23) & 0x3; - float num = m_state.vf[vfS][fsf]; - float den = m_state.vf[vfT][ftf]; - if (den != 0.0f) - m_state.q = num / den; + const float num = normalizeOperand(m_state.vf[vfS][fsf]); + const float den = normalizeOperand(m_state.vf[vfT][ftf]); + uint32_t statusDi = 0u; + float result = 0.0f; + 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(); + } else - m_state.q = (num >= 0.0f) ? std::numeric_limits::max() : -std::numeric_limits::max(); + { + result = num / den; + } + uint32_t ignoredFlags = 0u; + result = normalizeResult(result, ignoredFlags); + queueQ(result, 7u, statusDi); return; } case 0x39: // SQRT { int ftf = (instr >> 23) & 0x3; - float val = m_state.vf[vfT][ftf]; - m_state.q = std::sqrt(std::fabs(val)); + const float val = normalizeOperand(m_state.vf[vfT][ftf]); + queueQ(std::sqrt(std::fabs(val)), 7u, + val < 0.0f ? 0x10u : 0u); return; } case 0x3A: // RSQRT { int fsf = (instr >> 21) & 0x3; int ftf = (instr >> 23) & 0x3; - float num = m_state.vf[vfS][fsf]; - float den = std::sqrt(std::fabs(m_state.vf[vfT][ftf])); + const float num = normalizeOperand(m_state.vf[vfS][fsf]); + const float radicand = normalizeOperand(m_state.vf[vfT][ftf]); + const float den = std::sqrt(std::fabs(radicand)); + uint32_t statusDi = radicand < 0.0f ? 0x10u : 0u; + float result = 0.0f; if (den != 0.0f) - m_state.q = num / den; + result = num / den; else - m_state.q = std::numeric_limits::max(); + { + statusDi = num == 0.0f ? 0x10u : 0x20u; + 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 return; case 0x3C: // MTIR (Move To Integer Register) { - int comp = 0; - if (dest & 0x8) - comp = 0; - else if (dest & 0x4) - comp = 1; - else if (dest & 0x2) - comp = 2; - else - comp = 3; + // 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); if (viT != 0) @@ -635,26 +603,49 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz addr &= (dataSize - 1); if (addr + 16 <= dataSize) { - uint32_t val = (uint32_t)(uint16_t)(m_state.vi[viT] & 0xFFFF); - if (dest & 0x8) - std::memcpy(vuData + addr + 0, &val, 4); - if (dest & 0x4) - std::memcpy(vuData + addr + 4, &val, 4); - if (dest & 0x2) - std::memcpy(vuData + addr + 8, &val, 4); - if (dest & 0x1) - std::memcpy(vuData + addr + 12, &val, 4); + 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 + { + const uint32_t x = (m_state.r >> 4) & 1u; + const uint32_t y = (m_state.r >> 22) & 1u; + m_state.r = ((m_state.r << 1) ^ x ^ y) & 0x007FFFFFu; + m_state.r |= 0x3F800000u; + float value = 0.0f; + std::memcpy(&value, &m_state.r, sizeof(value)); + const float result[4] = {value, value, value, value}; + applyDest(m_state.vf[vfT], result, dest); return; + } case 0x41: // RGET + { + float value = 0.0f; + std::memcpy(&value, &m_state.r, sizeof(value)); + const float result[4] = {value, value, value, value}; + applyDest(m_state.vf[vfT], result, dest); return; + } case 0x42: // RINIT + { + const uint32_t component = (instr >> 21) & 3u; + uint32_t bits = 0u; + std::memcpy(&bits, &m_state.vf[vfS][component], sizeof(bits)); + m_state.r = 0x3F800000u | (bits & 0x007FFFFFu); return; + } case 0x43: // RXOR + { + const uint32_t component = (instr >> 21) & 3u; + uint32_t bits = 0u; + std::memcpy(&bits, &m_state.vf[vfS][component], sizeof(bits)); + m_state.r = 0x3F800000u | ((m_state.r ^ bits) & 0x007FFFFFu); return; + } case 0x64: // MFP (Move From P register) { float result[4] = {m_state.p, m_state.p, m_state.p, m_state.p}; @@ -674,45 +665,125 @@ void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSiz return; } case 0x6C: // XGKICK - send GIF packet from VU1 data memory - doXgkick(); + startXgkick(static_cast(static_cast(m_state.vi[viS]))); return; case 0x70: // ESADD + { + const float x = normalizeOperand(m_state.vf[vfS][0]); + const float y = normalizeOperand(m_state.vf[vfS][1]); + const float z = normalizeOperand(m_state.vf[vfS][2]); + queueP(x * x + y * y + z * z, 11u); return; + } case 0x71: // ERSADD + { + const float x = normalizeOperand(m_state.vf[vfS][0]); + const float y = normalizeOperand(m_state.vf[vfS][1]); + const float z = normalizeOperand(m_state.vf[vfS][2]); + const float sum = x * x + y * y + z * z; + queueP(sum != 0.0f ? 1.0f / sum : sum, 18u); return; + } case 0x72: // ELENG { - float s = m_state.vf[vfS][0] * m_state.vf[vfS][0] + m_state.vf[vfS][1] * m_state.vf[vfS][1] + m_state.vf[vfS][2] * m_state.vf[vfS][2]; - m_state.p = std::sqrt(s); + const float x = normalizeOperand(m_state.vf[vfS][0]); + const float y = normalizeOperand(m_state.vf[vfS][1]); + const float z = normalizeOperand(m_state.vf[vfS][2]); + queueP(std::sqrt(x * x + y * y + z * z), 18u); return; } case 0x73: // ERLENG { - float s = m_state.vf[vfS][0] * m_state.vf[vfS][0] + m_state.vf[vfS][1] * m_state.vf[vfS][1] + m_state.vf[vfS][2] * m_state.vf[vfS][2]; - float len = std::sqrt(s); - m_state.p = (len != 0.0f) ? (1.0f / len) : std::numeric_limits::max(); + const float x = normalizeOperand(m_state.vf[vfS][0]); + const float y = normalizeOperand(m_state.vf[vfS][1]); + const float z = normalizeOperand(m_state.vf[vfS][2]); + const float len = std::sqrt(x * x + y * y + z * z); + queueP(len != 0.0f ? 1.0f / len : len, 24u); + return; + } + case 0x74: // EATANxy + { + 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 + { + 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 + { + float sum = 0.0f; + for (uint32_t component = 0; component < 4u; ++component) + sum += normalizeOperand(m_state.vf[vfS][component]); + queueP(sum, 12u); + return; + } + case 0x77: // ERSQRT + { + const uint32_t component = (instr >> 21) & 3u; + const float value = normalizeOperand(m_state.vf[vfS][component]); + float result = value; + if (result >= 0.0f) + { + result = std::sqrt(result); + if (result != 0.0f) + result = 1.0f / result; + } + queueP(result, 18u); + return; + } + case 0x78: // ESQRT + { + 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 + { + const uint32_t component = (instr >> 21) & 3u; + const float value = normalizeOperand(m_state.vf[vfS][component]); + queueP(vuEsin(value), 29u); return; } case 0x7A: // ERCPR { - int fsf = (instr >> 21) & 0x3; - float val = m_state.vf[vfS][fsf]; - m_state.p = (val != 0.0f) ? (1.0f / val) : std::numeric_limits::max(); + 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 return; - case 0x7D: // EATAN / EATANxy / EATANxz placeholder + case 0x7C: // EATAN + { + const uint32_t component = (instr >> 21) & 3u; + queueP(vuEatan(normalizeOperand(m_state.vf[vfS][component])), 54u); return; + } + case 0x7D: // EEXP + { + const uint32_t component = (instr >> 21) & 3u; + queueP(vuEexp(normalizeOperand(m_state.vf[vfS][component])), 44u); + return; + } default: + reportReservedInstruction(false, instr); return; } } default: + reportReservedInstruction(false, instr); return; } } default: + reportReservedInstruction(false, instr); break; } } diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp index 27dde2d..6974254 100644 --- a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp @@ -3,12 +3,27 @@ #include #include +#include + +namespace +{ + int32_t vuFloatToInt(float value, float scale) + { + const double scaled = static_cast(value) * static_cast(scale); + if (scaled >= static_cast(std::numeric_limits::max())) + return std::numeric_limits::max(); + if (scaled <= static_cast(std::numeric_limits::min())) + return std::numeric_limits::min(); + return static_cast(scaled); + } +} // ============================================================================ // Upper instructions (FMAC pipeline) // ============================================================================ void VU1Interpreter::execUpper(uint32_t instr) { + m_currentUpperInstruction = instr; uint8_t dest = DEST(instr); uint8_t ft = FT(instr); uint8_t fs = FS(instr); @@ -16,8 +31,20 @@ void VU1Interpreter::execUpper(uint32_t instr) uint8_t op = instr & 0x3F; float *vd = m_state.vf[fd]; - const float *vs = m_state.vf[fs]; - const float *vt = m_state.vf[ft]; + float normalizedVs[4]; + float normalizedVt[4]; + float normalizedAcc[4]; + 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]); + } + 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); float result[4]; // Upper opcode decoding (bits 5:0 of upper word) @@ -31,7 +58,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 +69,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: @@ -52,8 +79,8 @@ 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); + result[c] = acc[c] + vs[c] * bc; + applyFmacDest(vd, result, dest); return; } case 0x0C: @@ -63,8 +90,8 @@ 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); + result[c] = acc[c] - vs[c] * bc; + applyFmacDest(vd, result, dest); return; } case 0x10: @@ -97,83 +124,83 @@ 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); + result[c] = vs[c] * q; + applyFmacDest(vd, result, dest); return; case 0x1D: // MAXi for (int c = 0; c < 4; c++) - result[c] = (vs[c] > m_state.i) ? vs[c] : m_state.i; + result[c] = (vs[c] > i) ? vs[c] : i; applyDest(vd, result, dest); return; case 0x1E: // MULi for (int c = 0; c < 4; c++) - result[c] = vs[c] * m_state.i; - applyDest(vd, result, dest); + result[c] = vs[c] * i; + applyFmacDest(vd, result, dest); return; case 0x1F: // MINIi for (int c = 0; c < 4; c++) - result[c] = (vs[c] < m_state.i) ? vs[c] : m_state.i; + result[c] = (vs[c] < i) ? vs[c] : i; applyDest(vd, result, dest); return; case 0x20: // ADDq for (int c = 0; c < 4; c++) - result[c] = vs[c] + m_state.q; - applyDest(vd, result, dest); + result[c] = vs[c] + q; + 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); + result[c] = acc[c] + vs[c] * q; + 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); + result[c] = vs[c] + i; + 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); + result[c] = acc[c] + vs[c] * i; + 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); + result[c] = vs[c] - q; + 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); + result[c] = acc[c] - vs[c] * q; + 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); + result[c] = vs[c] - i; + 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); + result[c] = acc[c] - vs[c] * i; + 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); + result[c] = acc[c] + vs[c] * vt[c]; + 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 +210,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); + result[c] = acc[c] - vs[c] * vt[c]; + 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[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; - applyDest(vd, result, dest); + applyFmacDest(vd, result, dest); return; case 0x2F: // MINI for (int c = 0; c < 4; c++) @@ -225,7 +252,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 +263,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: @@ -246,8 +273,8 @@ 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); + result[c] = acc[c] + vs[c] * bc; + applyFmacDestAcc(result, dest); return; } case 0x0C: @@ -257,15 +284,15 @@ 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); + result[c] = acc[c] - vs[c] * bc; + applyFmacDestAcc(result, dest); return; } case 0x10: // ITOF0 for (int c = 0; c < 4; c++) { int32_t iv; - std::memcpy(&iv, &vs[c], 4); + std::memcpy(&iv, &m_state.vf[fs][c], 4); result[c] = static_cast(iv); } applyDest(vtDest, result, dest); @@ -274,7 +301,7 @@ void VU1Interpreter::execUpper(uint32_t instr) for (int c = 0; c < 4; c++) { int32_t iv; - std::memcpy(&iv, &vs[c], 4); + std::memcpy(&iv, &m_state.vf[fs][c], 4); result[c] = static_cast(iv) / 16.0f; } applyDest(vtDest, result, dest); @@ -283,7 +310,7 @@ void VU1Interpreter::execUpper(uint32_t instr) for (int c = 0; c < 4; c++) { int32_t iv; - std::memcpy(&iv, &vs[c], 4); + std::memcpy(&iv, &m_state.vf[fs][c], 4); result[c] = static_cast(iv) / 4096.0f; } applyDest(vtDest, result, dest); @@ -292,7 +319,7 @@ void VU1Interpreter::execUpper(uint32_t instr) for (int c = 0; c < 4; c++) { int32_t iv; - std::memcpy(&iv, &vs[c], 4); + std::memcpy(&iv, &m_state.vf[fs][c], 4); result[c] = static_cast(iv) / 32768.0f; } applyDest(vtDest, result, dest); @@ -300,7 +327,7 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x14: // FTOI0 for (int c = 0; c < 4; c++) { - int32_t iv = static_cast(vs[c]); + int32_t iv = vuFloatToInt(vs[c], 1.0f); std::memcpy(&result[c], &iv, 4); } applyDest(vtDest, result, dest); @@ -308,7 +335,7 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x15: // FTOI4 for (int c = 0; c < 4; c++) { - int32_t iv = static_cast(vs[c] * 16.0f); + int32_t iv = vuFloatToInt(vs[c], 16.0f); std::memcpy(&result[c], &iv, 4); } applyDest(vtDest, result, dest); @@ -316,7 +343,7 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x16: // FTOI12 for (int c = 0; c < 4; c++) { - int32_t iv = static_cast(vs[c] * 4096.0f); + int32_t iv = vuFloatToInt(vs[c], 4096.0f); std::memcpy(&result[c], &iv, 4); } applyDest(vtDest, result, dest); @@ -324,7 +351,7 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x17: // FTOI15 for (int c = 0; c < 4; c++) { - int32_t iv = static_cast(vs[c] * 32768.0f); + int32_t iv = vuFloatToInt(vs[c], 32768.0f); std::memcpy(&result[c], &iv, 4); } applyDest(vtDest, result, dest); @@ -337,13 +364,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); + result[c] = vs[c] * q; + applyFmacDestAcc(result, dest); return; case 0x1D: // ABS for (int c = 0; c < 4; c++) @@ -352,98 +379,118 @@ void VU1Interpreter::execUpper(uint32_t instr) return; case 0x1E: // MULAi for (int c = 0; c < 4; c++) - result[c] = vs[c] * m_state.i; - applyDestAcc(result, dest); + result[c] = vs[c] * i; + applyFmacDestAcc(result, dest); return; case 0x1F: // CLIP { - float w = std::fabs(vt[3]); - uint32_t flags = 0; - if (vs[0] > +w) flags |= 0x01; - if (vs[0] < -w) flags |= 0x02; - if (vs[1] > +w) flags |= 0x04; - if (vs[1] < -w) flags |= 0x08; - if (vs[2] > +w) flags |= 0x10; - if (vs[2] < -w) flags |= 0x20; - m_state.clip = (m_state.clip << 6) | flags; + 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) + { + uint32_t bits = 0u; + std::memcpy(&bits, &value, sizeof(bits)); + bits ^= signMask; + int32_t orderedBits = 0; + std::memcpy(&orderedBits, &bits, sizeof(orderedBits)); + return orderedBits > limit; + }; + + uint32_t flags = 0u; + if (exceedsClipPlane(m_state.vf[fs][0], 0x00000000u)) + flags |= 0x01u; + if (exceedsClipPlane(m_state.vf[fs][0], 0x80000000u)) + flags |= 0x02u; + if (exceedsClipPlane(m_state.vf[fs][1], 0x00000000u)) + flags |= 0x04u; + if (exceedsClipPlane(m_state.vf[fs][1], 0x80000000u)) + flags |= 0x08u; + if (exceedsClipPlane(m_state.vf[fs][2], 0x00000000u)) + flags |= 0x10u; + if (exceedsClipPlane(m_state.vf[fs][2], 0x80000000u)) + flags |= 0x20u; + queueClip(flags); return; } case 0x20: // ADDAq for (int c = 0; c < 4; c++) - result[c] = vs[c] + m_state.q; - applyDestAcc(result, dest); + result[c] = vs[c] + q; + 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); + result[c] = acc[c] + vs[c] * q; + applyFmacDestAcc(result, dest); return; case 0x22: // ADDAi for (int c = 0; c < 4; c++) - result[c] = vs[c] + m_state.i; - applyDestAcc(result, dest); + result[c] = vs[c] + i; + 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); + result[c] = acc[c] + vs[c] * i; + applyFmacDestAcc(result, dest); return; case 0x24: // SUBAq for (int c = 0; c < 4; c++) - result[c] = vs[c] - m_state.q; - applyDestAcc(result, dest); + result[c] = vs[c] - q; + 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); + result[c] = acc[c] - vs[c] * q; + applyFmacDestAcc(result, dest); return; case 0x26: // SUBAi for (int c = 0; c < 4; c++) - result[c] = vs[c] - m_state.i; - applyDestAcc(result, dest); + result[c] = vs[c] - i; + 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); + result[c] = acc[c] - vs[c] * i; + 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); + result[c] = acc[c] + vs[c] * vt[c]; + 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); + result[c] = acc[c] - vs[c] * vt[c]; + 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 return; default: + reportReservedInstruction(true, instr); return; } } @@ -453,6 +500,7 @@ void VU1Interpreter::execUpper(uint32_t instr) case 0x32: case 0x33: default: + reportReservedInstruction(true, instr); return; } } diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 9e442b8..39dbc65 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -117,6 +117,15 @@ target_link_libraries(ps2x_tests PRIVATE ps2_runtime ) +# VU/GS tests intentionally instantiate large interpreter state. +# Windows executables default to a much smaller stack than Linux, and +# sanitizer/debug instrumentation can push these test frames over 1 MiB. +if(MSVC) + target_link_options(ps2x_tests PRIVATE "/STACK:8388608") +elseif(MINGW) + target_link_options(ps2x_tests PRIVATE "--stack,8388608") +endif() + if(COMMAND ps2x_stage_ffmpeg_runtime_dlls) ps2x_stage_ffmpeg_runtime_dlls(ps2x_tests) endif() diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index b1e8e0c..064caed 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -7,6 +7,7 @@ #include #include #include +#include using namespace ps2recomp; @@ -823,32 +824,71 @@ void register_code_generator_tests() t.IsTrue(ctc1Code.find("ignored") == std::string::npos, "CTC1 FCR31 should not be ignored"); }); - tc.Run("VU CReg access uses CFC2/CTC2", [](TestCase &t) { - CodeGenerator gen({}, {}); + tc.Run("VU CFC2/CTC2 access VI registers directly", [](TestCase& t) + { + CodeGenerator gen({}, {}); - Instruction cfc2{}; - cfc2.opcode = OPCODE_COP2; - cfc2.rs = COP2_CFC2; - cfc2.rt = 2; - cfc2.rd = VU0_CR_STATUS; + Instruction cfc2{}; + cfc2.opcode = OPCODE_COP2; + cfc2.rs = COP2_CFC2; + cfc2.rt = 2; + cfc2.rd = 11; - std::string cfc2Code = gen.translateInstruction(cfc2); - printGeneratedCode("VU CReg access uses CFC2/CTC2 (CFC2)", cfc2Code); - t.IsTrue(cfc2Code.find("SET_GPR_U32(ctx, 2") != std::string::npos, "CFC2 should write to rt"); - t.IsTrue(cfc2Code.find("ctx->vu0_status") != std::string::npos, "CFC2 STATUS should read vu0_status"); - t.IsTrue(cfc2Code.find("Unimplemented CFC2 VU CReg") == std::string::npos, "CFC2 should not hit unimplemented CReg path"); + std::string cfc2Code = gen.translateInstruction(cfc2); + printGeneratedCode("VU CFC2/CTC2 access VI registers directly (CFC2)", cfc2Code); - Instruction ctc2{}; - ctc2.opcode = OPCODE_COP2; - ctc2.rs = COP2_CTC2; - ctc2.rt = 3; - ctc2.rd = VU0_CR_ITOP; + t.IsTrue(cfc2Code.find("SET_GPR_U32(ctx, 2") != std::string::npos, "CFC2 should write to rt"); - std::string ctc2Code = gen.translateInstruction(ctc2); - printGeneratedCode("VU CReg access uses CFC2/CTC2 (CTC2)", ctc2Code); - t.IsTrue(ctc2Code.find("ctx->vu0_itop") != std::string::npos, "CTC2 ITOP should write vu0_itop"); - t.IsTrue(ctc2Code.find("GPR_U32(ctx, 3) & 0x3FF") != std::string::npos, "CTC2 ITOP should mask to 10 bits"); - t.IsTrue(ctc2Code.find("Unimplemented CTC2 VU CReg") == std::string::npos, "CTC2 should not hit unimplemented CReg path"); + t.IsTrue(cfc2Code.find("ctx->vi[11]") != std::string::npos, "CFC2 VI11 should read VI11"); + + t.IsTrue(cfc2Code.find("vu0_cmsar1") == std::string::npos, "CFC2 VI11 must not read CMSAR1"); + + t.IsTrue(cfc2Code.find("Unimplemented") == std::string::npos, "CFC2 VI11 should be implemented"); + + Instruction ctc2{}; + ctc2.opcode = OPCODE_COP2; + ctc2.rs = COP2_CTC2; + ctc2.rt = 3; + ctc2.rd = 4; + + std::string ctc2Code = gen.translateInstruction(ctc2); + printGeneratedCode("VU CFC2/CTC2 access VI registers directly (CTC2)", ctc2Code); + + t.IsTrue(ctc2Code.find("ctx->vi[4]") != std::string::npos, "CTC2 VI4 should write VI4"); + t.IsTrue(ctc2Code.find("static_cast(GPR_U32(ctx, 3))") != std::string::npos, "CTC2 VI4 should store the low 16 bits"); + t.IsTrue(ctc2Code.find("vu0_i") == std::string::npos, "CTC2 VI4 must not write the I register"); + t.IsTrue(ctc2Code.find("Unimplemented") == std::string::npos, "CTC2 VI4 should be implemented"); + }); + + tc.Run("VU special control registers use hardware indices", [](TestCase& t) + { + CodeGenerator gen({}, {}); + + Instruction cfc2{}; + cfc2.opcode = OPCODE_COP2; + cfc2.rs = COP2_CFC2; + cfc2.rt = 2; + cfc2.rd = VU0_CR_STATUS; + + std::string cfc2Code = gen.translateInstruction(cfc2); + printGeneratedCode("VU special control registers use hardware indices (STATUS)", cfc2Code); + + t.IsTrue(cfc2Code.find("SET_GPR_U32(ctx, 2") != std::string::npos,"CFC2 should write to rt"); + t.IsTrue(cfc2Code.find("ctx->vu0_status") != std::string::npos, "CFC2 STATUS should read vu0_status"); + t.IsTrue(cfc2Code.find("Unimplemented") == std::string::npos,"CFC2 STATUS should be implemented"); + + Instruction ctc2{}; + ctc2.opcode = OPCODE_COP2; + ctc2.rs = COP2_CTC2; + ctc2.rt = 3; + ctc2.rd = VU0_CR_FBRST; + + std::string ctc2Code = gen.translateInstruction(ctc2); + printGeneratedCode("VU special control registers use hardware indices (FBRST)", ctc2Code); + + t.IsTrue(ctc2Code.find("ctx->vu0_fbrst") != std::string::npos, "CTC2 register 28 should write FBRST"); + t.IsTrue(ctc2Code.find("vu0_itop") == std::string::npos, "CTC2 register 28 must not write ITOP"); + t.IsTrue(ctc2Code.find("Unimplemented") == std::string::npos, "CTC2 FBRST should be implemented"); }); tc.Run("scalar logical immediates emit low64 operations", [](TestCase &t) { @@ -1095,6 +1135,70 @@ void register_code_generator_tests() t.IsTrue(out.find("ctx->vu0_vf[25]") == std::string::npos, "S1 q/i must not use rs(format) as register index"); }); + tc.Run("VU0 destination MADD and MSUB forms preserve ACC", [](TestCase &t) { + Instruction inst{}; + inst.rt = 7; + inst.rd = 11; + inst.sa = 3; + inst.function = 0; + inst.vectorInfo.vectorField = 0xE; + + CodeGenerator gen({}, {}); + const std::vector> emitted = { + {"MADD field", gen.translateVU_VMADD_Field(inst)}, + {"MADD", gen.translateVU_VMADD(inst)}, + {"MADDq", gen.translateVU_VMADDq(inst)}, + {"MADDi", gen.translateVU_VMADDi(inst)}, + {"MSUB field", gen.translateVU_VMSUB_Field(inst)}, + {"MSUB", gen.translateVU_VMSUB(inst)}, + {"MSUBq", gen.translateVU_VMSUBq(inst)}, + {"MSUBi", gen.translateVU_VMSUBi(inst)}, + {"OPMSUB", gen.translateVU_VOPMSUB(inst)}, + }; + + for (const auto &[name, code] : emitted) + { + const std::string message = + std::string(name) + " writes VF and must not overwrite ACC"; + t.IsTrue(code.find("ctx->vu0_acc = res") == std::string::npos, + message.c_str()); + t.IsTrue(code.find("PS2_VADD(ctx->vu0_acc") != std::string::npos || + code.find("PS2_VSUB(ctx->vu0_acc") != std::string::npos, + (std::string(name) + " must still read ACC").c_str()); + } + + const std::string madda = gen.translateVU_VMADDA(inst); + t.IsTrue(madda.find("ctx->vu0_acc =") != std::string::npos, + "MADDA must continue writing ACC"); + }); + + tc.Run("VU0 OPMULA and OPMSUB use cross-product lane permutations", [](TestCase &t) { + Instruction inst{}; + inst.rt = 7; + inst.rd = 11; + inst.sa = 3; + inst.vectorInfo.vectorField = 0xE; + + CodeGenerator gen({}, {}); + const std::string opmula = gen.translateVU_VOPMULA(inst); + const std::string opmsub = gen.translateVU_VOPMSUB(inst); + + for (const std::string *code : {&opmula, &opmsub}) + { + t.IsTrue(code->find("_MM_SHUFFLE(3,0,2,1)") != std::string::npos, + "OPM source Fs must be permuted to y,z,x"); + t.IsTrue(code->find("_MM_SHUFFLE(3,1,0,2)") != std::string::npos, + "OPM source Ft must be permuted to z,x,y"); + t.IsTrue(code->find("PS2_VMUL(fs_yzx, ft_zxy)") != std::string::npos, + "OPM product must use the permuted operands"); + } + + t.IsTrue(opmula.find("ctx->vu0_acc =") != std::string::npos, + "OPMULA must write the permuted product to ACC"); + t.IsTrue(opmsub.find("ctx->vu0_acc = res") == std::string::npos, + "OPMSUB must preserve ACC after producing the cross product"); + }); + tc.Run("VU0 S2 vector ops use rd as source and rt as destination", [](TestCase &t) { Instruction inst{}; inst.opcode = OPCODE_COP2; diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 4c3e8ec..cfb725a 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -5,6 +5,7 @@ #include "ps2_syscalls.h" #include "runtime/ps2_gs_gpu.h" #include "runtime/ps2_gs_memory.h" +#include "runtime/ps2_gs_rasterizer.h" #include "runtime/ps2_gs_psmct32.h" #include "runtime/ps2_gs_psmt4.h" #include "runtime/ps2_gs_psmt8.h" @@ -297,6 +298,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() @@ -623,6 +675,126 @@ void register_ps2_gs_tests() "context-targeted clear should leave the other context framebuffer untouched"); }); + tc.Run("XYZ3 culls a triangle strip primitive without desynchronizing the vertex queue", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint32_t kColor = 0xFF0000FFu; + constexpr uint64_t kFrame = + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + constexpr uint64_t kZbuf = (1ull << 32); + constexpr uint64_t kScissor = + (6ull << 16) | + (6ull << 48); + + auto xyz = [](uint32_t x, uint32_t y) -> uint64_t + { + return static_cast(x * 16u) | + (static_cast(y * 16u) << 16); + }; + + 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_XYOFFSET_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, 0x30000ull); + gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_TRISTRIP)); + gs.writeRegister(GS_REG_RGBAQ, kColor); + + // ABC is rejected by XYZ3. D must then draw BCD, not stale ABC. + gs.writeRegister(GS_REG_XYZ2, xyz(0u, 0u)); + gs.writeRegister(GS_REG_XYZ2, xyz(6u, 0u)); + gs.writeRegister(GS_REG_XYZ3, xyz(0u, 6u)); + gs.writeRegister(GS_REG_XYZ2, xyz(6u, 6u)); + + t.Equals(readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 1u), 0u, + "XYZ3 should suppress the completed ABC triangle"); + t.Equals(readReferencePSMCT32Pixel(vram, 0u, 1u, 4u, 4u), kColor, + "the next XYZ2 should draw BCD from the advanced strip queue"); + }); + + tc.Run("GS fog blends the shaded color toward FOGCOL before framebuffer blending", [](TestCase &t) + { + auto renderFoggedPoint = [](bool fogEnabled, uint8_t fog, uint32_t fogColor = 0u) -> uint32_t + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint64_t kFrame = + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + constexpr uint64_t kZbuf = (1ull << 32); + constexpr uint64_t kWhite = 0x80FFFFFFull; + + gs.writeRegister(GS_REG_FRAME_1, kFrame); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, 0ull); + gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, 0x30000ull); + gs.writeRegister(GS_REG_FOGCOL, fogColor); + gs.writeRegister( + GS_REG_PRIM, + static_cast(GS_PRIM_POINT) | + (static_cast(fogEnabled ? 1u : 0u) << 5)); + gs.writeRegister(GS_REG_RGBAQ, kWhite); + gs.writeRegister(GS_REG_FOG, static_cast(fog) << 56); + gs.writeRegister(GS_REG_XYZ2, 0ull); + + return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); + }; + + t.Equals(renderFoggedPoint(false, 0x80u), 0x80FFFFFFu, + "FOG and FOGCOL must not affect primitives with FGE disabled"); + t.Equals(renderFoggedPoint(true, 0x80u), 0x807F7F7Fu, + "F=0x80 over black FOGCOL should halve the point RGB and preserve alpha"); + t.Equals(renderFoggedPoint(true, 0x00u), 0x80000000u, + "F=0 should replace the point RGB with black FOGCOL"); + t.Equals(renderFoggedPoint(true, 0x00u, 0x00302010u), 0x802F1F0Fu, + "F=0 should replace point RGB with the programmed FOGCOL"); + }); + + tc.Run("PRMODE supplies primitive attributes while PRMODECONT AC is clear", [](TestCase &t) + { + auto renderPoint = [](bool usePrmodeAttributes) -> uint32_t + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint64_t kFrame = + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + constexpr uint64_t kZbuf = (1ull << 32); + + gs.writeRegister(GS_REG_FRAME_1, kFrame); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, 0ull); + gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, 0x30000ull); + gs.writeRegister(GS_REG_FOGCOL, 0ull); + gs.writeRegister(GS_REG_RGBAQ, 0x80FFFFFFull); + gs.writeRegister(GS_REG_FOG, 0ull); + gs.writeRegister(GS_REG_PRMODE, 1ull << 5); + gs.writeRegister(GS_REG_PRMODECONT, usePrmodeAttributes ? 0ull : 1ull); + + // FGE is clear in PRIM. AC decides whether that clear bit or + // PRMODE's set bit supplies the effective fog enable. + gs.writeRegister(GS_REG_PRIM, static_cast(GS_PRIM_POINT)); + gs.writeRegister(GS_REG_XYZ2, 0ull); + + return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); + }; + + t.Equals(renderPoint(true), 0x80000000u, + "AC=0 should retain FGE from PRMODE across a PRIM write"); + t.Equals(renderPoint(false), 0x80FFFFFFu, + "AC=1 should source FGE from PRIM instead of PRMODE"); + }); + tc.Run("PABE bypasses alpha blend for low-alpha source pixels", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); @@ -1557,6 +1729,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; @@ -2488,6 +2674,161 @@ void register_ps2_gs_tests() "T8 CSM1 CLUT sampling should read CT32-uploaded palette entries through GS swizzled addressing"); }); + tc.Run("GS T8 CSM1 applies CSA and masks CSA bit 4 for CT32 CLUTs", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint32_t kTexTbp = 64u; + constexpr uint32_t kClutCbp = 128u; + constexpr uint64_t kFrameReg = + (0ull << 0) | + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + constexpr uint64_t kZbuf = (1ull << 32); + constexpr uint64_t kTex0 = + (static_cast(kTexTbp) << 0) | + (1ull << 14) | + (static_cast(GS_PSM_T8) << 20) | + (0ull << 26) | + (0ull << 30) | + (1ull << 34) | + (1ull << 35) | + (static_cast(kClutCbp) << 37) | + (static_cast(GS_PSM_CT32) << 51) | + (17ull << 56); + constexpr uint64_t kPrim = + static_cast(GS_PRIM_SPRITE) | + (1ull << 4) | + (1ull << 8); + constexpr uint32_t kExpectedColor = 0xFF204080u; + constexpr uint32_t kWrongNoCsaColor = 0xFF00FF00u; + constexpr uint32_t kWrongBit4Color = 0xFFFF0000u; + + const uint32_t texOff = GSPSMT8::addrPSMT8(kTexTbp, 1u, 0u, 0u); + vram[texOff] = 0u; + + // CSA=17 is CSA=1 for a CT32 CLUT. Logical entry 16 is at + // physical CSM1 entry 8 after address bits 3 and 4 are swapped. + gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 0u, 0u, kWrongNoCsaColor); + gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 8u, 0u, kExpectedColor); + gs.WriteVram(GS_PSM_CT32, kClutCbp, 1u, 8u, 16u, kWrongBit4Color); + + gs.writeRegister(GS_REG_FRAME_1, kFrameReg); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, 0ull); + gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, 0x30000ull); + gs.writeRegister(GS_REG_ALPHA_1, 0ull); + gs.writeRegister(GS_REG_TEX0_1, kTex0); + gs.writeRegister(GS_REG_PRIM, kPrim); + gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); + gs.writeRegister(GS_REG_UV, 0ull); + gs.writeRegister(GS_REG_XYZ2, 0ull); + gs.writeRegister(GS_REG_UV, 0ull); + gs.writeRegister(GS_REG_XYZ2, 0ull); + + uint32_t pixel = 0u; + std::memcpy(&pixel, vram.data(), sizeof(pixel)); + t.Equals(pixel, kExpectedColor, + "T8 CSM1 should offset by CSA while CT32 ignores the fifth CSA bit"); + }); + + tc.Run("GS T4 CSM1 preserves CSA bit 4 for CT16 CLUTs", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint32_t kTexTbp = 64u; + constexpr uint32_t kClutCbp = 128u; + constexpr uint64_t kFrameReg = + (0ull << 0) | + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + constexpr uint64_t kZbuf = (1ull << 32); + constexpr uint64_t kTex0 = + (static_cast(kTexTbp) << 0) | + (1ull << 14) | + (static_cast(GS_PSM_T4) << 20) | + (0ull << 26) | + (0ull << 30) | + (1ull << 34) | + (1ull << 35) | + (static_cast(kClutCbp) << 37) | + (static_cast(GS_PSM_CT16) << 51) | + (16ull << 56); + constexpr uint64_t kTexa = (0x80ull << 32); + constexpr uint64_t kPrim = + static_cast(GS_PRIM_SPRITE) | + (1ull << 4) | + (1ull << 8); + constexpr uint16_t kExpectedRed = 0x801Fu; + constexpr uint16_t kWrongGreen = 0x83E0u; + constexpr uint32_t kExpectedColor = 0x800000F8u; + + writePSMT4Texel(vram, kTexTbp, 1u, 0u, 0u, 1u); + + // CSA=16 selects the upper half of a CT16 CLUT. CSM1 swaps bits + // 3 and 4 but must preserve address bit 8. + gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 0u, kWrongGreen); + gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 16u, kExpectedRed); + + gs.writeRegister(GS_REG_FRAME_1, kFrameReg); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, 0ull); + gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, 0x30000ull); + gs.writeRegister(GS_REG_ALPHA_1, 0ull); + gs.writeRegister(GS_REG_TEX0_1, kTex0); + gs.writeRegister(GS_REG_TEXA, kTexa); + gs.writeRegister(GS_REG_PRIM, kPrim); + gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); + gs.writeRegister(GS_REG_UV, 0ull); + gs.writeRegister(GS_REG_XYZ2, 0ull); + gs.writeRegister(GS_REG_UV, 0ull); + gs.writeRegister(GS_REG_XYZ2, 0ull); + + uint32_t pixel = 0u; + std::memcpy(&pixel, vram.data(), sizeof(pixel)); + t.Equals(pixel, kExpectedColor, + "CT16 CSM1 should retain CSA[4] instead of aliasing the upper palette onto the lower one"); + }); + + tc.Run("GS TEX0 dimensions saturate at 1024 pixels", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + GSRasterizer rasterizer; + + constexpr uint32_t kTexTbp = 64u; + constexpr uint64_t kTex0 = + (static_cast(kTexTbp) << 0) | + (16ull << 14) | + (static_cast(GS_PSM_CT32) << 20) | + (15ull << 26) | + (15ull << 30) | + (1ull << 34) | + (1ull << 35); + constexpr uint64_t kPrim = + static_cast(GS_PRIM_TRIANGLE) | + (1ull << 4); + constexpr uint32_t kExpectedColor = 0xFF3366CCu; + constexpr uint32_t kUnsaturatedColor = 0xFF00FF00u; + + gs.WriteVram(GS_PSM_CT32, kTexTbp, 16u, 1u, 0u, kExpectedColor); + gs.WriteVram(GS_PSM_CT32, kTexTbp, 16u, 32u, 0u, kUnsaturatedColor); + gs.writeRegister(GS_REG_TEX0_1, kTex0); + gs.writeRegister(GS_REG_PRIM, kPrim); + + const uint32_t sampled = + rasterizer.sampleTexture(&gs, 1.0f / 1024.0f, 0.0f, 1.0f, 0u, 0u); + t.Equals(sampled, kExpectedColor, + "TW/TH values above 10 should address a 1024-pixel texture instead of growing beyond GS limits"); + }); + tc.Run("GS TEX2 updates CLUT state independently from TEX0", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); @@ -2975,97 +3316,312 @@ 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 CLAMP modes transform texture coordinates before sampling", [](TestCase &t) { - std::vector vram(PS2_GS_VRAM_SIZE, 0u); - GS gs; - gs.init(vram.data(), static_cast(vram.size()), nullptr); + auto renderConstantUv = [](uint64_t clampReg, + uint16_t fixedU, + uint16_t fixedV) -> uint32_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 = - 1ull | // ATE - (5ull << 1) | // ATST = GEQUAL - (0x80ull << 4) | // AREF - (1ull << 12) | // AFAIL = FB_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 kTexTbp = 64u; + constexpr uint32_t kTexel0 = 0x800000FFu; + constexpr uint32_t kTexel1 = 0x8000FF00u; + constexpr uint32_t kTexel2 = 0x80FF0000u; + constexpr uint32_t kTexel3 = 0x80FFFFFFu; + constexpr uint32_t kTexelV3 = 0x80FFFF00u; + constexpr uint64_t kFrame = + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + constexpr uint64_t kZbuf = (1ull << 32); + constexpr uint64_t kTex0 = + (static_cast(kTexTbp) << 0) | + (1ull << 14) | + (static_cast(GS_PSM_CT32) << 20) | + (2ull << 26) | + (2ull << 30) | + (1ull << 34) | + (1ull << 35); + constexpr uint64_t kPrim = + static_cast(GS_PRIM_TRIANGLE) | + (1ull << 4) | + (1ull << 8); + constexpr uint64_t kRgbaq = 0x3F80000080808080ull; - 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); + writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 0u, 0u, kTexel0); + writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 1u, 0u, kTexel1); + writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 2u, 0u, kTexel2); + writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 3u, 0u, kTexel3); + writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 0u, 3u, kTexelV3); - 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"); + gs.writeRegister(GS_REG_FRAME_1, kFrame); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, (3ull << 16) | (3ull << 48)); + gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, 0x30000ull); + gs.writeRegister(GS_REG_TEX0_1, kTex0); + gs.writeRegister(GS_REG_CLAMP_1, clampReg); + gs.writeRegister(GS_REG_PRIM, kPrim); + gs.writeRegister(GS_REG_RGBAQ, kRgbaq); + + const uint64_t uv = + static_cast(fixedU) | + (static_cast(fixedV) << 16); + gs.writeRegister(GS_REG_UV, uv); + gs.writeRegister(GS_REG_XYZ2, 0ull); + gs.writeRegister(GS_REG_UV, uv); + gs.writeRegister(GS_REG_XYZ2, 32ull); + gs.writeRegister(GS_REG_UV, uv); + gs.writeRegister(GS_REG_XYZ2, (32ull << 16)); + + return readReferencePSMCT32Pixel(vram, 0u, 1u, 0u, 0u); + }; + + constexpr uint64_t kClamp = 1ull; + constexpr uint64_t kRegionClamp = + 2ull | + (1ull << 4) | + (2ull << 14); + constexpr uint64_t kRegionRepeat = + 3ull | + (1ull << 4) | + (2ull << 14); + + t.Equals(renderConstantUv(0ull, 4u * 16u, 0u), 0x800000FFu, + "REPEAT should wrap texel 4 to texel 0 for a four-wide texture"); + t.Equals(renderConstantUv(0ull, 0u, 4u * 16u), 0x800000FFu, + "REPEAT should wrap texel row 4 to row 0 for a four-high texture"); + t.Equals(renderConstantUv(kClamp, 4u * 16u, 0u), 0x80FFFFFFu, + "CLAMP should hold texel 4 at the last texel"); + t.Equals(renderConstantUv(kRegionClamp, 3u * 16u, 0u), 0x80FF0000u, + "REGION_CLAMP should hold texel 3 at MAXU=2"); + t.Equals(renderConstantUv(kRegionRepeat, 4u * 16u, 0u), 0x80FF0000u, + "REGION_REPEAT should calculate (U & UMSK) | UFIX"); }); - tc.Run("GS alpha test AFAIL RGB-only preserves destination alpha", [](TestCase &t) + tc.Run("GS STQ triangle interpolation divides homogeneous coordinates after DDA", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; gs.init(vram.data(), static_cast(vram.size()), nullptr); + constexpr uint32_t kTexTbp = 64u; 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 = - 1ull | // ATE - (5ull << 1) | // ATST = GEQUAL - (0x80ull << 4) | // AREF - (3ull << 12) | // AFAIL = RGB_ONLY - (1ull << 17); // ZTST = ALWAYS + constexpr uint64_t kTex0 = + (static_cast(kTexTbp) << 0) | + (1ull << 14) | + (static_cast(GS_PSM_CT32) << 20) | + (2ull << 26) | + (1ull << 34) | + (1ull << 35); 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; + static_cast(GS_PRIM_TRIANGLE) | + (1ull << 4); + constexpr uint32_t kAffineTexel = 0x800000FFu; + constexpr uint32_t kHomogeneousTexel = 0x8000FF00u; - std::memcpy(vram.data(), &kExisting, sizeof(kExisting)); + auto packFloat = [](float value) -> uint32_t + { + uint32_t bits = 0u; + std::memcpy(&bits, &value, sizeof(bits)); + return bits; + }; + auto packSt = [&](float s, float tVal) -> uint64_t + { + return static_cast(packFloat(s)) | + (static_cast(packFloat(tVal)) << 32); + }; + auto packRgbaq = [&](float q) -> uint64_t + { + return 0x80808080ull | + (static_cast(packFloat(q)) << 32); + }; + + writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 1u, 0u, kAffineTexel); + writeReferencePSMCT32Pixel(vram, kTexTbp, 1u, 2u, 0u, kHomogeneousTexel); 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_SCISSOR_1, (4ull << 16) | (4ull << 48)); + gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, 0x30000ull); + gs.writeRegister(GS_REG_TEX0_1, kTex0); + gs.writeRegister(GS_REG_CLAMP_1, 1ull); 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, 0xAB563412u, - "AFAIL=RGB_ONLY should update RGB while preserving destination alpha"); + gs.writeRegister(GS_REG_ST, packSt(0.0f, 0.0f)); + gs.writeRegister(GS_REG_RGBAQ, packRgbaq(1.0f)); + gs.writeRegister(GS_REG_XYZ2, 0ull); + gs.writeRegister(GS_REG_ST, packSt(2.0f, 0.0f)); + gs.writeRegister(GS_REG_RGBAQ, packRgbaq(2.0f)); + gs.writeRegister(GS_REG_XYZ2, 64ull); + gs.writeRegister(GS_REG_ST, packSt(0.0f, 0.0f)); + gs.writeRegister(GS_REG_RGBAQ, packRgbaq(1.0f)); + gs.writeRegister(GS_REG_XYZ2, (64ull << 16)); + + const uint32_t pixel = + readReferencePSMCT32Pixel(vram, 0u, 1u, 1u, 1u); + t.Equals(pixel, kHomogeneousTexel, + "the DDA should interpolate S=0.75 and Q=1.375, selecting texel 2 after S/Q"); + }); + + tc.Run("GS alpha-test AFAIL independently masks framebuffer and depth", [](TestCase &t) + { + constexpr uint32_t kInitialFramebuffer = 0xAB030201u; + constexpr uint32_t kInitialDepth = 0x11111111u; + constexpr uint64_t kTestBase = + 1ull | // ATE + (5ull << 1) | // ATST = GEQUAL + (0x80ull << 4) | // AREF + (1ull << 16) | // ZTE + (1ull << 17); // ZTST = ALWAYS + + 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 RGB_ONLY falls back to FB_ONLY outside CT32", [](TestCase &t) + { + constexpr uint32_t kInitialDepth = 0x11111111u; + constexpr uint64_t kTest = + 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"); + }); + + 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"); + }); + + 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 59340ba..d75b213 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 3980bde..4dce622 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 c31b4d4..e480e8d 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -82,12 +82,38 @@ namespace 0x28u; } + uint32_t makeVuIaddiu(uint8_t it, uint8_t is, int16_t immediate) + { + return (0x08u << 25) | + (static_cast(it & 0xFu) << 16) | + (static_cast(is & 0xFu) << 11) | + (static_cast(immediate) & 0x7FFu); + } + + uint32_t makeVuLowerSpecial(uint8_t specialOp, uint8_t is, + uint8_t it = 0u, uint8_t dest = 0u) + { + return (0x40u << 25) | + (static_cast(dest & 0xFu) << 21) | + (static_cast(it & 0x1Fu) << 16) | + (static_cast(is & 0x1Fu) << 11) | + (static_cast(specialOp & 0x7Cu) << 4) | + static_cast(specialOp & 0x3u) | + 0x3Cu; + } + void writeVuInstructionPair(uint8_t *code, 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)); } + uint64_t packVuInstructionPair(uint32_t lower, uint32_t upper) + { + return static_cast(lower) | + (static_cast(upper) << 32); + } + bool hasSignedRdWrite(const std::string &generated, uint8_t rd) { if (rd == 0u) @@ -182,7 +208,7 @@ namespace } bool shouldPreempt = false; - for (int attempt = 0; attempt < 256 && + for (int attempt = 0; attempt < 2048 && !shouldPreempt; ++attempt) { @@ -571,6 +597,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; @@ -1693,7 +1743,7 @@ void register_ps2_runtime_expansion_tests() writeVuInstructionPair(code, 8u, 0u, makeVuAdd(0xFu, 2u, 1u, 1u)); writeVuInstructionPair(code, 16u, makeVuSq(0xFu, 2u, 0u, 1), kVuEndNop); - R5900Context ctx; + R5900Context ctx{}; runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u); float output[4]{}; @@ -1709,6 +1759,102 @@ void register_ps2_runtime_expansion_tests() t.Equals(static_cast(ctx.vi[0]), 0u, "VU0 VI0 should remain zero"); }); + tc.Run("VU0 microprogram preserves the architectural RNG state", [](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(); + std::memset(code, 0, PS2_VU0_CODE_SIZE); + constexpr uint32_t kVuUpperNop = 0x000002FFu; + constexpr uint32_t kVuUpperEndNop = 0x400002FFu; + writeVuInstructionPair( + code, 0u, + makeVuLowerSpecial(0x40u, 0u, 1u, 0x8u), + kVuUpperEndNop); // RNEXT.x vf1 + writeVuInstructionPair(code, 8u, 0u, kVuUpperNop); + + constexpr uint32_t seed = 0x3FC00000u; + const uint32_t x = (seed >> 4) & 1u; + const uint32_t y = (seed >> 22) & 1u; + const uint32_t expected = + (((seed << 1) ^ x ^ y) & 0x007FFFFFu) | 0x3F800000u; + R5900Context ctx{}; + ctx.vu0_r = _mm_castsi128_ps( + _mm_set1_epi32(static_cast(seed))); + runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u); + + alignas(16) uint32_t rWords[4]{}; + _mm_storeu_si128(reinterpret_cast<__m128i *>(rWords), + _mm_castps_si128(ctx.vu0_r)); + t.Equals(rWords[0], expected, "VU0 micro RNG should advance the imported R seed"); + t.Equals(rWords[1], expected, "VU0 R should remain replicated for macro-mode access"); + alignas(16) uint32_t vf1Words[4]{}; + _mm_storeu_si128(reinterpret_cast<__m128i *>(vf1Words), + _mm_castps_si128(ctx.vu0_vf[1])); + 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) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed"); + t.IsTrue(runtime.syncCoreSubsystems(), "runtime core subsystems should bind"); + + constexpr uint32_t kVuUpperNop = 0x000002FFu; + constexpr uint32_t kVuUpperEndNop = 0x400002FFu; + runtime.memory().write64( + PS2_VU0_CODE_BASE, + packVuInstructionPair(makeVuIaddiu(1u, 0u, 1), kVuUpperEndNop)); + runtime.memory().write64( + PS2_VU0_CODE_BASE + 8u, + packVuInstructionPair(0u, kVuUpperNop)); + + R5900Context first{}; + runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &first, 0u); + t.Equals(static_cast(first.vi[1]), 1u, + "first cached VU0 microprogram should execute"); + + runtime.memory().write64( + PS2_VU0_CODE_BASE, + packVuInstructionPair(makeVuIaddiu(1u, 0u, 2), kVuUpperEndNop)); + R5900Context second{}; + runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &second, 0u); + t.Equals(static_cast(second.vi[1]), 2u, + "VU0 cache should rebuild after a direct MicroMem write"); + }); + + tc.Run("VU0 FBRST TE gates a T-bit microprogram stop", [](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(); + std::memset(code, 0, PS2_VU0_CODE_SIZE); + constexpr uint32_t kVuUpperNop = 0x000002FFu; + writeVuInstructionPair( + code, 0u, makeVuIaddiu(1u, 0u, 7), + kVuUpperNop | 0x08000000u); + writeVuInstructionPair( + code, 8u, makeVuIaddiu(2u, 0u, 9), + kVuUpperNop); + + R5900Context ctx{}; + ctx.vu0_fbrst = 1u << 3; // TE0 + runtime.executeVU0Microprogram(runtime.memory().getRDRAM(), &ctx, 0u); + + t.Equals(static_cast(ctx.vi[1]), 7u, + "the T-marked instruction should execute"); + t.Equals(static_cast(ctx.vi[2]), 0u, + "TE0 should stop VU0 before the following instruction"); + t.IsTrue((ctx.vu0_vpu_stat & (1u << 2)) != 0u, + "VPU-STAT should report a VU0 T-bit stop"); + t.Equals(ctx.vu0_tpc, 8u, + "TPC should point at the first instruction not executed"); + }); + tc.Run("GS sprite draw applies XYOFFSET and fully-outside scissor should not render", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index 7a4371f..a4a1514 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 a66da89..8c0ff0d 100644 --- a/ps2xTest/src/ps2_vu1_tests.cpp +++ b/ps2xTest/src/ps2_vu1_tests.cpp @@ -5,13 +5,15 @@ #include "runtime/ps2_memory.h" #include "runtime/ps2_vu1.h" +#include #include #include +#include #include namespace { - constexpr uint32_t kVuUpperNop = 0u; + constexpr uint32_t kVuUpperNop = 0x000002FFu; struct Vu1Fixture { @@ -81,6 +83,31 @@ namespace static_cast(op & 0x3Fu); } + uint32_t makeVuUpperSpecial(uint8_t specialOp, uint8_t dest, uint8_t ft, uint8_t fs) + { + return (static_cast(dest & 0xFu) << 21) | + (static_cast(ft & 0x1Fu) << 16) | + (static_cast(fs & 0x1Fu) << 11) | + (static_cast(specialOp & 0x7Cu) << 4) | + static_cast(specialOp & 0x3u) | + 0x3Cu; + } + + 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) | @@ -111,6 +138,29 @@ namespace return (0x20u << 25) | (static_cast(imm) & 0x7FFu); } + uint32_t makeVuJr(uint8_t is) + { + return (0x24u << 25) | + (static_cast(is & 0xFu) << 11); + } + + uint32_t makeVuIbne(uint8_t is, uint8_t it, int16_t imm) + { + return (0x29u << 25) | + (static_cast(it & 0xFu) << 16) | + (static_cast(is & 0xFu) << 11) | + (static_cast(imm) & 0x7FFu); + } + + uint32_t makeVuIlw(uint8_t dest, uint8_t targetVi, uint8_t baseVi, int16_t imm) + { + return (0x04u << 25) | + (static_cast(dest & 0xFu) << 21) | + (static_cast(targetVi & 0xFu) << 16) | + (static_cast(baseVi & 0xFu) << 11) | + (static_cast(imm) & 0x7FFu); + } + uint32_t makeVuDiv(uint8_t fs, uint8_t ft, uint8_t fsf, uint8_t ftf) { return makeVuLowerSpecial(0x38u, fs, ft, 0u, static_cast(((ftf & 0x3u) << 2) | (fsf & 0x3u))); @@ -132,6 +182,11 @@ namespace return static_cast(lower) | (static_cast(upper) << 32); } + void writeTrackedVuInstructionPair(Vu1Fixture &fx, uint32_t pc, uint32_t lower, uint32_t upper) + { + fx.mem.write64(PS2_VU1_CODE_BASE + pc, packVuInstructionPair(lower, upper)); + } + void appendU32(std::vector &bytes, uint32_t value) { const uint8_t *src = reinterpret_cast(&value); @@ -185,6 +240,10 @@ void register_ps2_vu1_tests() 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().vf[3][0], -1.0f, + "FMAC destination must remain hidden before its four-cycle writeback"); + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, + fx.gs, &fx.mem, 0u, 0u, 3u); t.Equals(vu1.state().vf[3][0], 11.0f, "ADD.x should write x"); t.Equals(vu1.state().vf[3][1], -2.0f, "ADD.xz should preserve y"); t.Equals(vu1.state().vf[3][2], 33.0f, "ADD.xz should write z"); @@ -211,6 +270,12 @@ void register_ps2_vu1_tests() 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().vf[2][0], 0.0f, + "ADDi result should remain in the FMAC pipeline"); + t.Equals(vu1.state().i, 7.0f, + "LOI should become visible after the upper from the same pair"); + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, + fx.gs, &fx.mem, 0u, 0u, 3u); t.Equals(vu1.state().vf[2][0], 3.0f, "ADDi should use old I for x"); t.Equals(vu1.state().vf[2][1], 4.0f, "ADDi should use old I for y"); t.Equals(vu1.state().vf[2][2], 5.0f, "ADDi should use old I for z"); @@ -218,6 +283,65 @@ void register_ps2_vu1_tests() t.Equals(vu1.state().i, 7.0f, "LOI should commit lower immediate into I after upper execution"); }); + tc.Run("ITOF converts the raw signed integer bits without float normalization", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, 0u, + makeVuUpperSpecial(0x10u, 0xFu, 2u, 1u)); + + VU1Interpreter 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, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 4u); + + t.Equals(vu1.state().vf[2][0], 1.0f, + "ITOF0 must not flush an integer bit pattern that resembles a denormal"); + t.Equals(vu1.state().vf[2][1], -16.0f, + "ITOF0 must preserve negative integer bit patterns"); + t.Equals(vu1.state().vf[2][2], 4096.0f, + "ITOF0 should convert positive fixed-point source bits"); + t.Equals(vu1.state().vf[2][3], -32768.0f, + "ITOF0 should convert negative fixed-point source bits"); + }); + + tc.Run("MTIR decodes fsf as a component selector", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + for (uint32_t component = 0; component < 4u; ++component) + { + writeVuInstructionPair( + fx.code, component * 8u, + makeVuLowerSpecial(0x3Cu, 1u, + static_cast(component + 2u), + 0u, + static_cast(component)), + kVuUpperNop); + } + + VU1Interpreter 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, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 4u); + + t.Equals(vu1.state().vi[2], 0x1111, + "MTIR fsf=x should read VF.x"); + t.Equals(vu1.state().vi[3], 0x2222, + "MTIR fsf=y should read VF.y"); + t.Equals(vu1.state().vi[4], 0x3333, + "MTIR fsf=z should read VF.z"); + t.Equals(vu1.state().vi[5], 0x4444, + "MTIR fsf=w should read VF.w"); + }); + tc.Run("LQ and SQ use VI qword addressing and destination masks", [](TestCase &t) { Vu1Fixture fx; @@ -238,8 +362,12 @@ void register_ps2_vu1_tests() vu1.state().vf[4][2] = 300.0f; vu1.state().vf[4][3] = 400.0f; - vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 2u); + 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().vf[4][1], 200.0f, + "LQ result should remain hidden before its fourth cycle"); + 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().vf[4][0], 100.0f, "LQ.yw should preserve x"); t.Equals(vu1.state().vf[4][1], 20.0f, "LQ.yw should load y"); t.Equals(vu1.state().vf[4][2], 300.0f, "LQ.yw should preserve z"); @@ -251,6 +379,8 @@ void register_ps2_vu1_tests() t.Equals(stored[1], -2.0f, "SQ.xz should preserve y"); t.Equals(stored[2], 300.0f, "SQ.xz should store z"); t.Equals(stored[3], -4.0f, "SQ.xz should preserve w"); + t.Equals(vu1.state().cycles, static_cast(4u), + "disjoint LQ/SQ lanes should issue without delaying LQ writeback"); }); tc.Run("integer lower ops keep VI0 hardwired to zero", [](TestCase &t) @@ -331,7 +461,7 @@ void register_ps2_vu1_tests() vu1.state().vf[3][2] = 300.0f; vu1.state().vf[3][3] = 400.0f; - vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 2u); float stored[4] = {}; readVuQword(fx.data, 6u, stored); @@ -339,30 +469,625 @@ void register_ps2_vu1_tests() t.Equals(stored[1], 2.0f, "SQ should observe old VF value for y"); t.Equals(stored[2], 3.0f, "SQ should observe old VF value for z"); t.Equals(stored[3], 4.0f, "SQ should observe old VF value for w"); + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, + fx.gs, &fx.mem, 0u, 0u, 2u); t.Equals(vu1.state().vf[1][0], 110.0f, "upper ADD should write x after lower read"); t.Equals(vu1.state().vf[1][1], 220.0f, "upper ADD should write y after lower read"); t.Equals(vu1.state().vf[1][2], 330.0f, "upper ADD should write z after lower read"); t.Equals(vu1.state().vf[1][3], 440.0f, "upper ADD should write w after lower read"); }); + tc.Run("upper suppresses only the colliding lower VF write", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + const float loaded[4] = {90.0f, 91.0f, 92.0f, 93.0f}; + writeVuQword(fx.data, 5u, loaded); + writeVuInstructionPair( + fx.code, + 0u, + makeVuLowerSpecial(0x34u, 1u, 1u, 0u, 0xFu), + makeVuUpper(0x28u, 0x8u, 3u, 2u, 1u)); + + VU1Interpreter vu1; + vu1.state().vi[1] = 5; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][1] = 2.0f; + vu1.state().vf[1][2] = 3.0f; + vu1.state().vf[1][3] = 4.0f; + vu1.state().vf[2][0] = 10.0f; + vu1.state().vf[3][0] = 100.0f; + + 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().vf[1][0], 1.0f, + "upper write should remain pending until the FMAC writeback cycle"); + t.Equals(vu1.state().vi[1], 6, + "LQI post-increment should commit after one cycle"); + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 3u); + t.Equals(vu1.state().vf[1][0], 110.0f, + "upper x should be written"); + t.Equals(vu1.state().vf[1][1], 2.0f, + "discarded lower must not leak y into the upper result"); + t.Equals(vu1.state().vf[1][2], 3.0f, + "discarded lower must not leak z into the upper result"); + t.Equals(vu1.state().vf[1][3], 4.0f, + "discarded lower must not leak w into the upper result"); + t.Equals(vu1.state().vi[1], 6, + "LQI post-increment must survive suppression of its colliding VF write"); + }); + + tc.Run("upper and lower both read the pre-pair VF state", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, + makeVuLowerSpecial(0x30u, 1u, 2u, 0u, 0x8u), + makeVuUpper(0x28u, 0x8u, 3u, 2u, 1u)); + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 10.0f; + vu1.state().vf[2][0] = 20.0f; + vu1.state().vf[3][0] = 1.0f; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 1u); + + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 3u); + t.Equals(vu1.state().vf[1][0], 21.0f, + "upper must read vf2 before lower MOVE overwrites it"); + t.Equals(vu1.state().vf[2][0], 10.0f, + "lower must read vf1 before upper ADD overwrites it"); + }); + + tc.Run("FMAC dependency stalls only the lanes that are read", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, 0u, + makeVuUpper(0x28u, 0x8u, 2u, 1u, 3u)); + writeVuInstructionPair( + fx.code, 8u, 0u, + makeVuUpper(0x28u, 0x8u, 2u, 3u, 4u)); + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[2][0] = 2.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().vf[4][0], 0.0f, + "the dependent result should remain pending until its own writeback"); + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 3u); + t.Equals(vu1.state().vf[4][0], 5.0f, + "dependent ADD should consume the completed x lane"); + t.Equals(vu1.state().cycles, static_cast(8u), + "dependency stall and the dependent FMAC writeback must both consume cycles"); + }); + + tc.Run("ACC forwarding feeds the next upper instruction without a dependency stall", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, 0u, + makeVuUpperSpecial(0x28u, 0x8u, 2u, 1u)); // ADDA.x acc, vf1, vf2 + writeVuInstructionPair( + fx.code, 8u, 0u, + makeVuUpper(0x29u, 0x8u, 4u, 3u, 5u)); // MADD.x vf5, vf3, vf4 + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[2][0] = 10.0f; + vu1.state().vf[3][0] = 2.0f; + vu1.state().vf[4][0] = 3.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().acc[0], 11.0f, + "ADDA should forward ACC to the following upper instruction"); + t.Equals(vu1.state().vf[5][0], 17.0f, + "MADD should consume the forwarded ACC value without stalling"); + t.Equals(vu1.state().cycles, static_cast(5u), + "ACC forwarding must not introduce a four-cycle dependency stall"); + }); + + tc.Run("ILW result becomes visible after four cycles before IALU consumes it", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + 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); + writeVuInstructionPair( + fx.code, 8u, makeVuIaddiu(3u, 2u, 1), kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vi[1] = 2; + 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().vi[2], 0x1234, + "ILW should commit the selected word after four cycles"); + t.Equals(vu1.state().vi[3], 0x1235, + "IADDIU should wait for and consume the ILW result"); + t.Equals(vu1.state().cycles, static_cast(5u), + "ILW-to-IALU dependency should account for all stalled cycles"); + }); + tc.Run("DIV and SQRT update the Q register from selected vector components", [](TestCase &t) { 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, makeVuSqrt(3u, 3u), kVuUpperNop); // Q = sqrt(abs(vf3.w)) + 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 VU1Interpreter vu1; vu1.state().vf[1][1] = 18.0f; vu1.state().vf[2][2] = 3.0f; vu1.state().vf[3][3] = 25.0f; - 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().q, 6.0f, "DIV should divide selected FS and FT components into Q"); + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 8u); + t.Equals(vu1.state().q, 6.0f, "WAITQ should expose DIV after its seven-cycle latency"); + t.Equals(vu1.state().cycles, static_cast(8u), + "maxCycles should count FDIV stalls as elapsed VU cycles"); - 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().q, 5.0f, "SQRT should write square root of selected FT component into Q"); + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 8u); + t.Equals(vu1.state().q, 5.0f, "WAITQ should expose SQRT after its seven-cycle latency"); + }); + + tc.Run("FDIV resource serializes back-to-back scalar operations", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, + makeVuDiv(1u, 2u, 0u, 0u), + kVuUpperNop); // Q = vf1.x / vf2.x + writeVuInstructionPair( + fx.code, 8u, + makeVuSqrt(3u, 0u), + kVuUpperNop); // Must wait for the shared FDIV unit. + writeVuInstructionPair( + fx.code, 16u, + makeVuLowerSpecial(0x3Bu, 0u), + kVuUpperNop); // WAITQ + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 18.0f; + vu1.state().vf[2][0] = 3.0f; + vu1.state().vf[3][0] = 25.0f; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 15u); + + t.Equals(vu1.state().q, 5.0f, + "the second FDIV operation should commit the final Q value"); + t.Equals(vu1.state().cycles, static_cast(15u), + "back-to-back FDIV operations should include the resource stall"); + }); + + tc.Run("FDIV commits current and sticky divide-invalid status", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, makeVuDiv(1u, 2u, 0u, 0u), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 8u, makeVuLowerSpecial(0x3Bu, 0u), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 0.0f; + vu1.state().vf[2][0] = 0.0f; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 8u); + t.Equals(vu1.state().status, 0x410u, + "zero divided by zero should set current and sticky I"); + + vu1.reset(); + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[2][0] = 0.0f; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 8u); + t.Equals(vu1.state().status, 0x820u, + "a nonzero numerator divided by zero should set current and sticky D"); + }); + + tc.Run("EFU WAITP and RNG execute with architectural latency and state", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, + makeVuLowerSpecial(0x70u, 1u), + kVuUpperNop); // ESADD P, vf1 + writeVuInstructionPair( + fx.code, 8u, + makeVuLowerSpecial(0x7Bu, 0u), + kVuUpperNop); // WAITP + writeVuInstructionPair( + fx.code, 16u, + makeVuLowerSpecial(0x42u, 2u, 0u, 0u, 0x8u), + kVuUpperNop); // RINIT R, vf2.x + writeVuInstructionPair( + fx.code, 24u, + makeVuLowerSpecial(0x40u, 0u, 3u, 0u, 0x8u), + kVuUpperNop); // RNEXT.x vf3 + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][1] = 2.0f; + vu1.state().vf[1][2] = 3.0f; + vu1.state().vf[2][0] = 1.5f; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 14u); + + t.Equals(vu1.state().p, 14.0f, + "WAITP should expose ESADD after eleven cycles"); + t.Equals(vu1.state().vf[3][0], 0.0f, + "RNEXT vector result should respect FMAC writeback latency"); + vu1.resume(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 3u); + t.IsTrue(vu1.state().vf[3][0] >= 1.0f && + vu1.state().vf[3][0] < 2.0f && + vu1.state().vf[3][0] != 1.5f, + "RNEXT should advance the 23-bit R LFSR and write a 1.x value"); + }); + + tc.Run("EFU resource observes throughput separately from P visibility", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, + makeVuLowerSpecial(0x70u, 1u), + kVuUpperNop); // ESADD: result at cycle 11, resource free at 10. + writeVuInstructionPair( + fx.code, 8u, + makeVuLowerSpecial(0x72u, 1u), + kVuUpperNop); // ELENG: must issue at cycle 10. + writeVuInstructionPair( + fx.code, 16u, + makeVuLowerSpecial(0x7Bu, 0u), + kVuUpperNop); // WAITP waits for ELENG at cycle 28. + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 1.0f; + vu1.state().vf[1][1] = 2.0f; + vu1.state().vf[1][2] = 3.0f; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 29u); + + t.IsTrue(vu1.state().p > 3.7f && vu1.state().p < 3.8f, + "WAITP should expose the second EFU result"); + t.Equals(vu1.state().cycles, static_cast(29u), + "EFU scheduling should use opcode throughput and result latency"); + }); + + tc.Run("all EFU opcodes produce P at their architectural latency", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + struct EfuCase + { + uint8_t opcode; + uint32_t latency; + }; + constexpr EfuCase cases[] = { + {0x70u, 11u}, {0x71u, 18u}, {0x72u, 18u}, {0x73u, 24u}, + {0x74u, 54u}, {0x75u, 54u}, {0x76u, 12u}, {0x77u, 18u}, + {0x78u, 12u}, {0x79u, 29u}, {0x7Au, 12u}, {0x7Cu, 54u}, + {0x7Du, 44u}}; + + for (const EfuCase &efu : cases) + { + std::memset(fx.code, 0, PS2_VU1_CODE_SIZE); + writeVuInstructionPair( + fx.code, 0u, + makeVuLowerSpecial(efu.opcode, 1u, 0u, 0u, 0u), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 8u, + makeVuLowerSpecial(0x7Bu, 0u), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 0.25f; + vu1.state().vf[1][1] = 0.5f; + vu1.state().vf[1][2] = 0.75f; + vu1.state().vf[1][3] = 1.0f; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, efu.latency + 1u); + + t.Equals(vu1.state().cycles, + static_cast(efu.latency + 1u), + "WAITP should count every EFU stall as an elapsed VU cycle"); + t.IsTrue(std::isfinite(vu1.state().p), + "architected EFU opcode should commit a finite P result"); + } + }); + + tc.Run("E and enabled D/T stop with their architectural delay rules", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, 0u, + kVuUpperNop | 0x40000000u); + writeVuInstructionPair( + fx.code, 8u, makeVuIaddiu(1u, 0u, 7), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 16u, makeVuIaddiu(2u, 0u, 9), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 32u); + t.Equals(vu1.state().vi[1], 7, + "E should execute exactly one sequential delay slot"); + t.Equals(vu1.state().vi[2], 0, + "E should stop before the instruction after its delay slot"); + + vu1.reset(); + writeTrackedVuInstructionPair( + fx, 0u, makeVuIaddiu(1u, 0u, 3), + kVuUpperNop | 0x08000000u); + writeTrackedVuInstructionPair( + fx, 8u, makeVuIaddiu(2u, 0u, 5), + kVuUpperNop); + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 2u); + t.Equals(vu1.state().vi[2], 5, + "T must be ignored while TE is disabled"); + + vu1.reset(); + vu1.state().tBitEnabled = true; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 32u); + t.Equals(vu1.state().vi[1], 3, + "T instruction itself should complete"); + t.Equals(vu1.state().vi[2], 0, + "enabled T should stop without an ordinary delay slot"); + t.IsTrue(vu1.state().stoppedByT, + "the stop reason should identify T"); + + vu1.reset(); + vu1.state().dBitEnabled = true; + writeTrackedVuInstructionPair( + fx, 0u, makeVuIaddiu(1u, 0u, 4), + kVuUpperNop | 0x10000000u); + writeTrackedVuInstructionPair( + fx, 8u, makeVuIaddiu(2u, 0u, 6), + kVuUpperNop); + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 32u); + t.Equals(vu1.state().vi[1], 4, + "D instruction itself should complete"); + t.Equals(vu1.state().vi[2], 0, + "enabled D should stop without an ordinary delay slot"); + t.IsTrue(vu1.state().stoppedByD, + "the stop reason should identify D"); + + 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().vi[2], 6, + "MSCNT-style resume should continue at the stopped TPC"); + t.IsTrue(!vu1.state().stoppedByD && !vu1.state().stoppedByT, + "resuming should clear the previous D/T stop reason"); + + vu1.reset(); + vu1.state().tBitEnabled = true; + writeTrackedVuInstructionPair( + fx, 0u, makeVuBranch(1), + kVuUpperNop | 0x08000000u); + writeTrackedVuInstructionPair( + fx, 8u, makeVuIaddiu(2u, 0u, 11), + kVuUpperNop); + writeTrackedVuInstructionPair( + fx, 16u, makeVuIaddiu(3u, 0u, 13), + kVuUpperNop); + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 32u); + t.Equals(vu1.state().vi[2], 11, + "T on a branch should still execute its branch delay slot"); + t.Equals(vu1.state().vi[3], 0, + "T on a branch should stop before executing the branch target"); + t.Equals(vu1.state().pc, 16u, + "the stopped TPC should be the branch destination"); + }); + + tc.Run("conditional branch sees the previous VI value for one instruction", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, makeVuIaddiu(1u, 0u, 1), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 8u, makeVuIbne(1u, 0u, 2), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 16u, makeVuIaddiu(2u, 0u, 2), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 24u, makeVuIaddiu(3u, 0u, 3), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 32u, makeVuIaddiu(4u, 0u, 4), + kVuUpperNop); + + VU1Interpreter vu1; + 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().vi[2], 2, + "the instruction after a conditional branch remains its delay slot"); + t.Equals(vu1.state().vi[3], 3, + "an immediately following branch should see the pre-write VI value"); + + vu1.reset(); + writeTrackedVuInstructionPair(fx, 8u, 0u, kVuUpperNop); + writeTrackedVuInstructionPair( + fx, 16u, makeVuIbne(1u, 0u, 2), + kVuUpperNop); + writeTrackedVuInstructionPair( + fx, 40u, makeVuIaddiu(5u, 0u, 5), + kVuUpperNop); + 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().vi[3], 3, + "taken branch should still execute its delay slot"); + t.Equals(vu1.state().vi[4], 0, + "after one intervening instruction the branch should observe and branch on the new VI value"); + t.Equals(vu1.state().vi[5], 5, + "taken branch should arrive at its target after the delay slot"); + + vu1.reset(); + writeTrackedVuInstructionPair( + fx, 0u, makeVuIaddiu(1u, 0u, 1), + makeVuUpper(0x28u, 0x8u, 2u, 1u, 3u)); + writeTrackedVuInstructionPair( + fx, 8u, makeVuIbne(1u, 0u, 2), + makeVuUpper(0x28u, 0x8u, 0u, 3u, 4u)); + writeTrackedVuInstructionPair( + fx, 16u, makeVuIaddiu(2u, 0u, 2), + kVuUpperNop); + writeTrackedVuInstructionPair( + fx, 24u, makeVuIaddiu(3u, 0u, 3), + kVuUpperNop); + writeTrackedVuInstructionPair( + fx, 32u, makeVuIaddiu(4u, 0u, 4), + kVuUpperNop); + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 7u); + t.Equals(vu1.state().vi[2], 2, + "a stalled conditional branch should retain its delay slot"); + t.Equals(vu1.state().vi[3], 3, + "the VI bypass must survive VF hazard stalls before the next pair issues"); + t.Equals(vu1.state().vi[4], 0, + "elapsed stall cycles must not expire the one-instruction VI bypass"); + }); + + tc.Run("flag checks are visible to an immediately following branch", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, + makeVuFlagImmediate(0x16u, 1u, 0x001u), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 8u, makeVuIbne(1u, 0u, 2), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 16u, makeVuIaddiu(2u, 0u, 2), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 24u, makeVuIaddiu(3u, 0u, 3), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 32u, makeVuIaddiu(4u, 0u, 4), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().status = 0x001u; + 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().vi[1], 1, + "FSAND should publish its result after one cycle"); + t.Equals(vu1.state().vi[2], 2, + "the taken branch should still execute its delay slot"); + t.Equals(vu1.state().vi[3], 0, + "the immediately following branch must consume the flag-check result"); + t.Equals(vu1.state().vi[4], 4, + "the flag-driven branch should arrive at its target"); + }); + + tc.Run("JR shares the one-instruction VI branch visibility rule", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, makeVuIaddiu(1u, 0u, 2), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 8u, makeVuJr(1u), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 16u, makeVuIaddiu(2u, 0u, 2), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 24u, makeVuIaddiu(3u, 0u, 3), + kVuUpperNop); + writeVuInstructionPair( + fx.code, 32u, makeVuIaddiu(4u, 0u, 4), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vi[1] = 4; + 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().vi[1], 2, + "the pending IALU write should still commit normally"); + t.Equals(vu1.state().vi[2], 2, + "JR should execute exactly one delay-slot pair"); + t.Equals(vu1.state().vi[3], 0, + "JR should skip the sequential instruction after its delay slot"); + t.Equals(vu1.state().vi[4], 4, + "JR immediately after a VI write should branch using the previous VI value"); }); tc.Run("MPG upload invalidates cached VU1 decode before MSCAL", [](TestCase &t) @@ -458,7 +1183,7 @@ void register_ps2_vu1_tests() 0u, 0u, 0u, - 1u); + 3u); t.Equals(captured.size(), static_cast(1u), "XGKICK should emit one wrapped GIF packet"); if (!captured.empty()) @@ -477,6 +1202,176 @@ void register_ps2_vu1_tests() } }); + tc.Run("XGKICK observes stores committed before a future PATH1 qword", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + std::vector> captured; + mem.setGifPacketCallback([&](const uint8_t *packet, uint32_t sizeBytes) + { + captured.emplace_back(packet, packet + sizeBytes); + }); + + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + 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, + makeVuLowerSpecial(0x6Cu, 1u), + kVuUpperNop); + writeVuInstructionPair( + code, 8u, + makeVuSq(0xFu, 4u, 2u, 0), + kVuUpperNop); + writeVuInstructionPair(code, 16u, 0u, kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vi[1] = 0; + vu1.state().vi[2] = 1; + const float replacement[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + std::memcpy(vu1.state().vf[4], replacement, sizeof(replacement)); + vu1.execute(code, PS2_VU1_CODE_SIZE, + data, PS2_VU1_DATA_SIZE, gs, &mem, + 0u, 0u, 0u, 3u); + + t.Equals(captured.size(), static_cast(1u), + "PATH1 should finish after consuming the updated payload"); + if (!captured.empty()) + { + t.IsTrue(captured[0].size() >= 32u && + std::memcmp(captured[0].data() + 16u, + replacement, + sizeof(replacement)) == 0, + "XGKICK must read each qword in its transfer cycle"); + } + }); + + tc.Run("a second XGKICK stalls until the active PATH1 transfer completes", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + std::vector> captured; + mem.setGifPacketCallback([&](const uint8_t *packet, uint32_t sizeBytes) + { + captured.emplace_back(packet, packet + sizeBytes); + }); + + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + 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 + 0u, &imageTag, sizeof(imageTag)); + std::memcpy(data + 32u, &imageTag, sizeof(imageTag)); + std::memset(data + 16u, 0x11, 16u); + std::memset(data + 48u, 0x22, 16u); + writeVuInstructionPair( + code, 0u, + makeVuLowerSpecial(0x6Cu, 1u), + kVuUpperNop); + writeVuInstructionPair( + code, 8u, + makeVuLowerSpecial(0x6Cu, 2u), + kVuUpperNop); + writeVuInstructionPair(code, 16u, 0u, kVuUpperNop); + writeVuInstructionPair(code, 24u, 0u, kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vi[1] = 0; + vu1.state().vi[2] = 2; + vu1.execute(code, PS2_VU1_CODE_SIZE, + data, PS2_VU1_DATA_SIZE, gs, &mem, + 0u, 0u, 0u, 6u); + + t.Equals(captured.size(), static_cast(2u), + "both PATH1 transfers should complete in issue order"); + if (captured.size() == 2u) + { + t.IsTrue(captured[0].size() >= 32u && captured[0][16u] == 0x11u, + "the first XGKICK payload should be delivered first"); + t.IsTrue(captured[1].size() >= 32u && captured[1][16u] == 0x22u, + "the stalled XGKICK should retain its own source address"); + } + t.Equals(vu1.state().cycles, static_cast(6u), + "XGKICK resource stalls should consume VU cycles"); + }); + + tc.Run("synthetic Code Veronica text packet preserves black-frame PATH1 data", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + std::vector> captured; + mem.setGifPacketCallback([&](const uint8_t *data, uint32_t sizeBytes) + { + captured.emplace_back(data, data + sizeBytes); + }); + + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + 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, + makeVuUpper(0x28u, 0xFu, 3u, 2u, 4u)); + writeVuInstructionPair( + code, 8u, + makeVuSq(0xFu, 4u, 2u, 0), + kVuUpperNop); + writeVuInstructionPair( + code, 16u, + makeVuLowerSpecial(0x6Cu, 1u), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vi[1] = 0; + vu1.state().vi[2] = 1; + const float a[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + const float b[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + std::memcpy(vu1.state().vf[2], a, sizeof(a)); + std::memcpy(vu1.state().vf[3], b, sizeof(b)); + + vu1.execute(code, PS2_VU1_CODE_SIZE, + data, PS2_VU1_DATA_SIZE, gs, &mem, + 0u, 0u, 0u, 10u); + + t.Equals(captured.size(), static_cast(1u), + "the synthetic scene should emit exactly one PATH1 packet"); + if (!captured.empty()) + { + const float expected[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + t.Equals(captured[0].size(), static_cast(32u), + "text regression packet should contain one image qword"); + t.IsTrue(captured[0].size() >= 32u && + std::memcmp(captured[0].data() + 16u, + expected, + sizeof(expected)) == 0, + "PATH1 must observe the post-FMAC store, not stale blue/magenta data"); + } + }); + tc.Run("MSCAL can start a VU1 XGKICK program and update GS VRAM", [](TestCase &t) { PS2Memory mem; @@ -533,7 +1428,7 @@ void register_ps2_vu1_tests() startPC, top, itop, - 1u); + 3u); }); const uint32_t mscalCmd = makeVifCmd(0x14u, 0u, 0u); @@ -555,5 +1450,267 @@ 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, 3u); + 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 has one flag pipeline and FSSET wins a same-pair conflict", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 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); + + 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.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, 0x28u, + "same-pair FSSET should suppress only STATUS, not the upper MAC result"); + t.Equals(vu1.state().status, 0xA80u, + "the single runtime pipeline should commit FSSET sticky 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(0x18u, 6u, 7u), + kVuUpperNop); + writeVuInstructionPair(fx.code, 16u, + 0u, + kVuUpperNop); + writeVuInstructionPair(fx.code, 24u, + 0u, + kVuUpperNop); + writeVuInstructionPair(fx.code, 32u, + makeVuFlagRegister(0x1Au, 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, 3u); + + t.Equals(vu1.state().mac, 0u, + "FMAC flags should remain hidden before four cycles elapse"); + 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, 2u); + + 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 CLIP and FCGET share the four-cycle flag timeline", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, 0u, + makeVuUpperSpecial(0x1Fu, 0u, 2u, 1u)); + writeVuInstructionPair( + fx.code, 8u, + makeVuFlagRegister(0x1Cu, 3u, 0u), + kVuUpperNop); + writeVuInstructionPair(fx.code, 16u, 0u, kVuUpperNop); + writeVuInstructionPair(fx.code, 24u, 0u, kVuUpperNop); + writeVuInstructionPair( + fx.code, 32u, + makeVuFlagRegister(0x1Cu, 4u, 0u), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.state().vf[1][0] = 2.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().vi[3], 0, + "FCGET before cycle four should observe the previous CLIP value"); + t.Equals(vu1.state().vi[4], 1, + "FCGET on cycle four should observe the committed +X CLIP bit"); + t.Equals(vu1.state().clip, 1u, + "CLIP should shift and commit the six new comparison bits"); + }); + + tc.Run("VU1 FMAC normalizes overflow and underflow before writing results", [](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::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] = 0.5f; + 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 clamp to the largest finite VU value"); + t.Equals(vu1.state().vf[3][1], 0.0f, + "underflow should flush to signed zero before writeback"); + }); + + tc.Run("FMAC product contributes Z/S/U/O sticky flags", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair( + fx.code, 0u, 0u, + makeVuUpper(0x29u, 0x8u, 2u, 1u, 3u)); // MADD.x + + VU1Interpreter vu1; + vu1.state().acc[0] = 1.0f; + vu1.state().vf[1][0] = std::numeric_limits::min(); + vu1.state().vf[2][0] = 0.5f; + 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().vf[3][0], 1.0f, + "the accumulated FMAC result should remain normal"); + t.Equals(vu1.state().mac, 0u, + "MAC flags should describe the final accumulated value"); + t.Equals(vu1.state().status, 0x140u, + "the underflowing product should set sticky Z and U"); + }); + + 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.code, 8u, makeVuIaddiu(1u, 0u, 7), + kVuUpperNop); + + VU1Interpreter vu1; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, + fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, + 0u, 0u, 0u, 8u); + + t.Equals(vu1.state().cycles, static_cast(0u), + "reserved opcode should stop before consuming its issue cycle"); + t.Equals(vu1.state().pc, 0u, + "reserved opcode should retain the diagnostic PC"); + t.Equals(vu1.state().vi[1], 0, + "instruction following a reserved opcode must not execute"); + }); }); }