diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index 43dc416..8921b5d 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -358,7 +358,9 @@ add_library(ps2_runtime STATIC src/lib/ps2_pad.cpp src/lib/ps2_runtime.cpp src/lib/ps2_vif1_interpreter.cpp - src/lib/ps2_vu1.cpp + src/lib/vu/ps2_vu1_core.cpp + src/lib/vu/ps2_vu1_upper.cpp + src/lib/vu/ps2_vu1_lower.cpp src/lib/games_database.cpp ) diff --git a/ps2xRuntime/include/runtime/ps2_memory.h b/ps2xRuntime/include/runtime/ps2_memory.h index e6eefde..30fc455 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 getVU1CodeGeneration() const { return m_vu1CodeGeneration.load(std::memory_order_relaxed); } // Read/write memory uint8_t read8(uint32_t address); @@ -371,6 +372,7 @@ public: std::atomic m_gifCopyCount{0}; std::atomic m_gsWriteCount{0}; std::atomic m_vifWriteCount{0}; + std::atomic m_vu1CodeGeneration{0}; // I/O registers std::unordered_map m_ioRegisters; @@ -429,6 +431,7 @@ public: bool isAddressInRegion(uint32_t address, const CodeRegion ®ion); void markModified(uint32_t address, uint32_t size); + 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); const uint8_t *mapVuMemory(uint32_t physAddr, uint32_t size, uint32_t &offset, uint32_t &limit) const; diff --git a/ps2xRuntime/include/runtime/ps2_vu1.h b/ps2xRuntime/include/runtime/ps2_vu1.h index 102bc9e..cdbdec9 100644 --- a/ps2xRuntime/include/runtime/ps2_vu1.h +++ b/ps2xRuntime/include/runtime/ps2_vu1.h @@ -2,6 +2,7 @@ #define PS2_VU1_H #include +#include class GS; class PS2Memory; @@ -19,8 +20,8 @@ struct VU1State uint32_t clip; uint32_t status; bool ebit; - uint32_t top; // VIF1 TOP visible to VU1 XTOP - uint32_t itop; // VIF1 ITOP visible to VU1 XITOP + uint32_t top; // VIF1 TOP visible to VU1 XTOP + uint32_t itop; // VIF1 ITOP visible to VU1 XITOP bool branchPending; uint32_t branchTarget; @@ -49,12 +50,33 @@ public: const VU1State &state() const { return m_state; } private: + struct DecodedInstructionPair + { + uint32_t lower = 0; + uint32_t upper = 0; + bool iBit = false; + bool eBit = false; + bool lowerBeforeUpper = false; + }; + VU1State m_state; + std::vector 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; void run(uint8_t *vuCode, uint32_t codeSize, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t maxCycles); + DecodedInstructionPair decodeInstructionPair(const uint8_t *vuCode, uint32_t pc) const; + DecodedInstructionPair getDecodedInstructionPairForPc(const uint8_t *vuCode, uint32_t codeSize, + PS2Memory *memory, uint32_t pc); + void rebuildDecodedCodeCache(const uint8_t *vuCode, uint32_t codeSize, + const PS2Memory *memory, uint64_t generation); + void execUpper(uint32_t instr); void execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr); diff --git a/ps2xRuntime/src/lib/ps2_memory.cpp b/ps2xRuntime/src/lib/ps2_memory.cpp index bfd2163..3d59328 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); + markVU1CodeModified(); // Initialize VIF registers memset(&vif0_regs, 0, sizeof(vif0_regs)); @@ -764,6 +765,8 @@ void PS2Memory::write8(uint32_t address, uint8_t value) { (void)vuLimit; vuMem[vuOffset] = value; + if (vuMem == m_vu1Code) + markVU1CodeModified(); return; } } @@ -803,6 +806,8 @@ 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) + markVU1CodeModified(); return; } } @@ -863,6 +868,8 @@ 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) + markVU1CodeModified(); return; } } @@ -914,6 +921,8 @@ 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) + markVU1CodeModified(); return; } } @@ -953,6 +962,8 @@ 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) + markVU1CodeModified(); return; } } @@ -1208,9 +1219,9 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) auto appendCompactVif1TagData = [&](uint32_t localTagAddr, uint32_t qwCount) { uint32_t tagPhys = 0u; - const bool tagScratch = isScratchpad(localTagAddr); + const bool tagScratch = isScratchpad(localTagAddr); tagPhys = translateAddress(localTagAddr); - + const uint8_t *localBase = tagScratch ? m_scratchpad : m_rdram; const uint32_t localMax = tagScratch ? PS2_SCRATCHPAD_SIZE : PS2_RAM_SIZE; if (tagPhys + 16u > localMax) @@ -1718,11 +1729,11 @@ void PS2Memory::processGIFPacket(uint32_t srcPhysAddr, uint32_t qwCount) chunk = PS2_RAM_SIZE - srcPhysAddr; if (chunk == 0) break; - + m_seenGifCopy = true; m_gifCopyCount.fetch_add(1, std::memory_order_relaxed); submitGifPacket(GifPathId::Path3, m_rdram + srcPhysAddr, chunk); - + bytesLeft -= chunk; srcPhysAddr += chunk; } diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index 5fefa7f..c0af2a3 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -445,7 +445,10 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) if (destAddr + copyBytes > PS2_VU1_CODE_SIZE) copyBytes = PS2_VU1_CODE_SIZE - destAddr; if (pos + copyBytes <= sizeBytes) + { std::memcpy(m_vu1Code + destAddr, data + pos, copyBytes); + markVU1CodeModified(); + } } pos += mpgBytes; if (pos > sizeBytes) diff --git a/ps2xRuntime/src/lib/ps2_vu1.cpp b/ps2xRuntime/src/lib/ps2_vu1.cpp index 7c3b281..2c22cc2 100644 --- a/ps2xRuntime/src/lib/ps2_vu1.cpp +++ b/ps2xRuntime/src/lib/ps2_vu1.cpp @@ -1,1464 +1 @@ -#include "runtime/ps2_vu1.h" -#include "runtime/ps2_gs_gpu.h" -#include "runtime/ps2_gif_arbiter.h" -#include "runtime/ps2_memory.h" -#include -#include -#include -#include -#include - -// Instruction field extraction helpers -static inline uint8_t DEST(uint32_t i) { return (uint8_t)((i >> 21) & 0xF); } -static inline uint8_t FT(uint32_t i) { return (uint8_t)((i >> 16) & 0x1F); } -static inline uint8_t FS(uint32_t i) { return (uint8_t)((i >> 11) & 0x1F); } -static inline uint8_t FD(uint32_t i) { return (uint8_t)((i >> 6) & 0x1F); } -static inline uint8_t BC(uint32_t i) { return (uint8_t)(i & 0x3); } - -// Lower instruction field helpers -static inline uint8_t LIT(uint32_t i) { return (uint8_t)((i >> 16) & 0x1F); } -static inline uint8_t LIS(uint32_t i) { return (uint8_t)((i >> 11) & 0x1F); } -static inline uint8_t LID(uint32_t i) { return (uint8_t)((i >> 6) & 0x1F); } -static inline uint8_t VIT(uint32_t i) { return (uint8_t)((i >> 16) & 0xF); } -static inline uint8_t VIS(uint32_t i) { return (uint8_t)((i >> 11) & 0xF); } -static inline uint8_t VID(uint32_t i) { return (uint8_t)((i >> 6) & 0xF); } -static inline int16_t IMM11(uint32_t i) { return (int16_t)(int32_t)((int32_t)(i << 21) >> 21); } -static inline int16_t IMM15(uint32_t i) -{ - uint32_t lo11 = i & 0x7FF; - uint32_t hi4 = (i >> 21) & 0xF; - uint32_t raw = (hi4 << 11) | lo11; - return (int16_t)(int32_t)((int32_t)(raw << 17) >> 17); -} - - -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; -} - - -VU1Interpreter::VU1Interpreter() -{ - reset(); -} - -void VU1Interpreter::reset() -{ - std::memset(&m_state, 0, sizeof(m_state)); - m_state.vf[0][3] = 1.0f; // VF0.w = 1.0 - m_state.q = 1.0f; -} - -float VU1Interpreter::broadcast(const float *vf, uint8_t bc) -{ - return vf[bc & 3]; -} - -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 -} - -void VU1Interpreter::applyDestAcc(const float *result, uint8_t dest) -{ - applyDest(m_state.acc, result, dest); -} - -void VU1Interpreter::execute(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory, - uint32_t startPC, uint32_t top, uint32_t itop, - uint32_t maxCycles) -{ - m_state.pc = startPC & 0x3FFFu; - m_state.ebit = false; - m_state.top = top; - m_state.itop = itop; - m_state.branchPending = false; - m_state.branchTarget = 0; - m_state.branchDelay = 0; - m_state.vf[0][0] = 0.0f; - m_state.vf[0][1] = 0.0f; - m_state.vf[0][2] = 0.0f; - m_state.vf[0][3] = 1.0f; - run(vuCode, codeSize, vuData, dataSize, gs, memory, maxCycles); -} - -void VU1Interpreter::resume(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory, - uint32_t top, uint32_t itop, uint32_t maxCycles) -{ - m_state.ebit = false; - m_state.top = top; - m_state.itop = itop; - run(vuCode, codeSize, vuData, dataSize, gs, memory, maxCycles); -} - -void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, - uint8_t *vuData, uint32_t dataSize, - GS &gs, PS2Memory *memory, uint32_t maxCycles) -{ - for (uint32_t cycle = 0; cycle < maxCycles; ++cycle) - { - if (m_state.pc + 8 > codeSize) - break; - - uint32_t lower, upper; - std::memcpy(&lower, vuCode + m_state.pc, 4); - std::memcpy(&upper, vuCode + m_state.pc + 4, 4); - - const bool iBit = ((upper >> 31) & 1u) != 0u; - const bool eBit = ((upper >> 30) & 1u) != 0u; - const bool mBit = ((upper >> 29) & 1u) != 0u; - (void)mBit; - - // 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. - const bool loi = iBit; - if (loi) - { - // LOI is special: the upper instruction sees the old I value, then LOI loads I. - execUpper(upper); - std::memcpy(&m_state.i, &lower, 4); - } - else if (vuLowerShouldRunBeforeUpper(upper, lower)) - { - // 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(lower, vuData, dataSize, gs, memory, upper); - execUpper(upper); - } - else - { - execUpper(upper); - execLower(lower, vuData, dataSize, gs, memory, upper); - } - - // Enforce VF0 invariant - 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; - - // 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) - { - m_state.pc = m_state.branchTarget & 0x3FFFu; - m_state.branchPending = false; - } - else - { - --m_state.branchDelay; - } - } - - if (m_state.ebit) - break; - - if (eBit) - m_state.ebit = true; - } -} - -// ============================================================================ -// Upper instructions (FMAC pipeline) -// ============================================================================ -void VU1Interpreter::execUpper(uint32_t instr) -{ - uint8_t dest = DEST(instr); - uint8_t ft = FT(instr); - uint8_t fs = FS(instr); - uint8_t fd = FD(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 result[4]; - - // Upper opcode decoding (bits 5:0 of upper word) - switch (op) - { - case 0x00: - case 0x01: - case 0x02: - case 0x03: // ADDbc - { - float bc = broadcast(vt, op & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] + bc; - applyDest(vd, result, dest); - return; - } - case 0x04: - case 0x05: - case 0x06: - case 0x07: // SUBbc - { - float bc = broadcast(vt, op & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] - bc; - applyDest(vd, result, dest); - return; - } - case 0x08: - case 0x09: - case 0x0A: - case 0x0B: // MADDbc - { - 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); - return; - } - case 0x0C: - case 0x0D: - case 0x0E: - case 0x0F: // MSUBbc - { - 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); - return; - } - case 0x10: - case 0x11: - case 0x12: - case 0x13: // MAXbc - { - float bc = broadcast(vt, op & 3); - for (int c = 0; c < 4; c++) - result[c] = (vs[c] > bc) ? vs[c] : bc; - applyDest(vd, result, dest); - return; - } - case 0x14: - case 0x15: - case 0x16: - case 0x17: // MINIbc - { - float bc = broadcast(vt, op & 3); - for (int c = 0; c < 4; c++) - result[c] = (vs[c] < bc) ? vs[c] : bc; - applyDest(vd, result, dest); - return; - } - case 0x18: - case 0x19: - case 0x1A: - case 0x1B: // MULbc - { - float bc = broadcast(vt, op & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] * bc; - applyDest(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); - return; - case 0x1D: // MAXi - for (int c = 0; c < 4; c++) - result[c] = (vs[c] > m_state.i) ? vs[c] : m_state.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); - return; - case 0x1F: // MINIi - for (int c = 0; c < 4; c++) - result[c] = (vs[c] < m_state.i) ? vs[c] : m_state.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); - 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); - return; - case 0x22: // ADDi - for (int c = 0; c < 4; c++) - result[c] = vs[c] + m_state.i; - applyDest(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); - return; - case 0x24: // SUBq - for (int c = 0; c < 4; c++) - result[c] = vs[c] - m_state.q; - applyDest(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); - return; - case 0x26: // SUBi - for (int c = 0; c < 4; c++) - result[c] = vs[c] - m_state.i; - applyDest(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); - return; - case 0x28: // ADD - for (int c = 0; c < 4; c++) - result[c] = vs[c] + vt[c]; - applyDest(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); - return; - case 0x2A: // MUL - for (int c = 0; c < 4; c++) - result[c] = vs[c] * vt[c]; - applyDest(vd, result, dest); - return; - case 0x2B: // MAX - for (int c = 0; c < 4; c++) - result[c] = (vs[c] > vt[c]) ? vs[c] : vt[c]; - applyDest(vd, result, dest); - return; - case 0x2C: // SUB - for (int c = 0; c < 4; c++) - result[c] = vs[c] - vt[c]; - applyDest(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); - return; - case 0x2E: // OPMSUB - result[0] = m_state.acc[0] - vs[1] * vt[2]; - result[1] = m_state.acc[1] - vs[2] * vt[0]; - result[2] = m_state.acc[2] - vs[0] * vt[1]; - result[3] = 0.0f; - applyDest(vd, result, dest); - return; - case 0x2F: // MINI - for (int c = 0; c < 4; c++) - result[c] = (vs[c] < vt[c]) ? vs[c] : vt[c]; - applyDest(vd, result, dest); - return; - - // Upper special group (low op 0x3C..0x3F). - // Like lower1 special, the real selector is not just bits 5:0. Dobie decodes: - // op = (instr & 0x3) | ((instr >> 4) & 0x7C) - // Several instructions in this group also use FT as the destination, not FD. - case 0x3C: - case 0x3D: - case 0x3E: - case 0x3F: - { - const uint8_t specialOp = static_cast((instr & 0x3u) | ((instr >> 4) & 0x7Cu)); - float *vtDest = m_state.vf[ft]; - - switch (specialOp) - { - case 0x00: - case 0x01: - case 0x02: - case 0x03: // ADDAbc - { - float bc = broadcast(vt, specialOp & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] + bc; - applyDestAcc(result, dest); - return; - } - case 0x04: - case 0x05: - case 0x06: - case 0x07: // SUBAbc - { - float bc = broadcast(vt, specialOp & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] - bc; - applyDestAcc(result, dest); - return; - } - case 0x08: - case 0x09: - case 0x0A: - case 0x0B: // MADDAbc - { - 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); - return; - } - case 0x0C: - case 0x0D: - case 0x0E: - case 0x0F: // MSUBAbc - { - 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); - return; - } - case 0x10: // ITOF0 - for (int c = 0; c < 4; c++) - { - int32_t iv; - std::memcpy(&iv, &vs[c], 4); - result[c] = static_cast(iv); - } - applyDest(vtDest, result, dest); - return; - case 0x11: // ITOF4 - for (int c = 0; c < 4; c++) - { - int32_t iv; - std::memcpy(&iv, &vs[c], 4); - result[c] = static_cast(iv) / 16.0f; - } - applyDest(vtDest, result, dest); - return; - case 0x12: // ITOF12 - for (int c = 0; c < 4; c++) - { - int32_t iv; - std::memcpy(&iv, &vs[c], 4); - result[c] = static_cast(iv) / 4096.0f; - } - applyDest(vtDest, result, dest); - return; - case 0x13: // ITOF15 - for (int c = 0; c < 4; c++) - { - int32_t iv; - std::memcpy(&iv, &vs[c], 4); - result[c] = static_cast(iv) / 32768.0f; - } - applyDest(vtDest, result, dest); - return; - case 0x14: // FTOI0 - for (int c = 0; c < 4; c++) - { - int32_t iv = static_cast(vs[c]); - std::memcpy(&result[c], &iv, 4); - } - applyDest(vtDest, result, dest); - return; - case 0x15: // FTOI4 - for (int c = 0; c < 4; c++) - { - int32_t iv = static_cast(vs[c] * 16.0f); - std::memcpy(&result[c], &iv, 4); - } - applyDest(vtDest, result, dest); - return; - case 0x16: // FTOI12 - for (int c = 0; c < 4; c++) - { - int32_t iv = static_cast(vs[c] * 4096.0f); - std::memcpy(&result[c], &iv, 4); - } - applyDest(vtDest, result, dest); - return; - case 0x17: // FTOI15 - for (int c = 0; c < 4; c++) - { - int32_t iv = static_cast(vs[c] * 32768.0f); - std::memcpy(&result[c], &iv, 4); - } - applyDest(vtDest, result, dest); - return; - case 0x18: - case 0x19: - case 0x1A: - case 0x1B: // MULAbc - { - float bc = broadcast(vt, specialOp & 3); - for (int c = 0; c < 4; c++) - result[c] = vs[c] * bc; - applyDestAcc(result, dest); - return; - } - case 0x1C: // MULAq - for (int c = 0; c < 4; c++) - result[c] = vs[c] * m_state.q; - applyDestAcc(result, dest); - return; - case 0x1D: // ABS - for (int c = 0; c < 4; c++) - result[c] = std::fabs(vs[c]); - applyDest(vtDest, result, dest); - return; - case 0x1E: // MULAi - for (int c = 0; c < 4; c++) - result[c] = vs[c] * m_state.i; - applyDestAcc(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; - return; - } - case 0x20: // ADDAq - for (int c = 0; c < 4; c++) - result[c] = vs[c] + m_state.q; - applyDestAcc(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); - return; - case 0x22: // ADDAi - for (int c = 0; c < 4; c++) - result[c] = vs[c] + m_state.i; - applyDestAcc(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); - return; - case 0x24: // SUBAq - for (int c = 0; c < 4; c++) - result[c] = vs[c] - m_state.q; - applyDestAcc(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); - return; - case 0x26: // SUBAi - for (int c = 0; c < 4; c++) - result[c] = vs[c] - m_state.i; - applyDestAcc(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); - return; - case 0x28: // ADDA - for (int c = 0; c < 4; c++) - result[c] = vs[c] + vt[c]; - applyDestAcc(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); - return; - case 0x2A: // MULA - for (int c = 0; c < 4; c++) - result[c] = vs[c] * vt[c]; - applyDestAcc(result, dest); - return; - case 0x2C: // SUBA - for (int c = 0; c < 4; c++) - result[c] = vs[c] - vt[c]; - applyDestAcc(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); - 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); - return; - case 0x2F: - case 0x30: // NOP - return; - default: - return; - } - } - - case 0x30: - case 0x31: - case 0x32: - case 0x33: - default: - return; - } -} - -// ============================================================================ -// Lower instructions -// ============================================================================ -void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr) -{ - (void)upperInstr; - if (instr == 0x00000000 || instr == 0x8000033C) // NOP - return; - - uint8_t opHi = (instr >> 25) & 0x7F; - - // 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 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) - { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - applyDest(m_state.vf[it], tmp, dest); - } - return; - } - 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 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); - } - 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 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) - { - int comp = 0; - if (dest & 0x8) - comp = 0; - else if (dest & 0x4) - comp = 1; - else if (dest & 0x2) - comp = 2; - else - comp = 3; - uint32_t v; - std::memcpy(&v, vuData + addr + comp * 4, 4); - if (it != 0) - m_state.vi[it] = (int32_t)(int16_t)(v & 0xFFFF); - } - return; - } - 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 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); - } - return; - } - case 0x08: // IADDIU - { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); - int16_t imm = (int16_t)(instr & 0x7FF) | ((instr >> 10) & 0x7800); - if (it != 0) - m_state.vi[it] = (int16_t)(m_state.vi[is] + imm); - return; - } - case 0x09: // ISUBIU - { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); - int16_t imm = (int16_t)(instr & 0x7FF) | ((instr >> 10) & 0x7800); - if (it != 0) - m_state.vi[it] = (int16_t)(m_state.vi[is] - imm); - return; - } - case 0x10: // FCEQ - { - uint32_t imm24 = instr & 0xFFFFFF; - if (1 != 0) - m_state.vi[1] = ((m_state.clip & 0xFFFFFF) == imm24) ? 1 : 0; - return; - } - case 0x11: // FCSET - { - m_state.clip = instr & 0xFFFFFF; - return; - } - case 0x12: // FCAND - { - uint32_t imm24 = instr & 0xFFFFFF; - if (1 != 0) - m_state.vi[1] = ((m_state.clip & imm24) != 0) ? 1 : 0; - return; - } - case 0x13: // FCOR - { - uint32_t imm24 = instr & 0xFFFFFF; - if (1 != 0) - m_state.vi[1] = ((m_state.clip | imm24) == 0xFFFFFF) ? 1 : 0; - return; - } - case 0x14: // FSEQ - { - uint16_t imm12 = instr & 0xFFF; - if (1 != 0) - m_state.vi[1] = ((m_state.status & 0xFFF) == imm12) ? 1 : 0; - return; - } - case 0x15: // FSSET - { - m_state.status = (instr >> 6) & 0xFC0; - return; - } - case 0x16: // FSAND - { - uint16_t imm12 = instr & 0xFFF; - if (1 != 0) - m_state.vi[1] = (int32_t)(m_state.status & 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); - if (it != 0) - m_state.vi[it] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]); - return; - } - case 0x1A: // FMEQ - { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); - if (it != 0) - m_state.vi[it] = ((m_state.mac & 0xFFFF) == (uint32_t)(uint16_t)m_state.vi[is]) ? 1 : 0; - return; - } - case 0x1C: // FMOR - { - 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 0x20: // B (unconditional branch) - { - int16_t imm = IMM11(instr); - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - return; - } - case 0x21: // BAL (Branch and link) - { - uint8_t it = VIT(instr); - int16_t imm = IMM11(instr); - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - if (it != 0) - m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - return; - } - case 0x24: // JR - { - uint8_t is = VIS(instr); - uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF; - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - return; - } - case 0x25: // JALR - { - uint8_t it = VIT(instr); - uint8_t is = VIS(instr); - uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF; - if (it != 0) - m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - return; - } - case 0x28: // IBEQ - { - 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]) - { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - } - return; - } - case 0x29: // IBNE - { - 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]) - { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - } - return; - } - case 0x2C: // IBLTZ - { - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] < 0) - { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - } - return; - } - case 0x2D: // IBGTZ - { - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] > 0) - { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - } - return; - } - case 0x2E: // IBLEZ - { - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] <= 0) - { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - } - return; - } - case 0x2F: // IBGEZ - { - uint8_t is = VIS(instr); - int16_t imm = IMM11(instr); - if ((int16_t)m_state.vi[is] >= 0) - { - uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; - m_state.branchPending = true; - m_state.branchTarget = target; - m_state.branchDelay = 1; - } - return; - } - - case 0x40: // Lower1 / lower special. Bit31 set; low 6 bits select integer or special op. - { - const uint8_t funct = instr & 0x3Fu; - const uint8_t vfT = FT(instr); - const uint8_t vfS = FS(instr); - const uint8_t viT = VIT(instr); - const uint8_t viS = VIS(instr); - const uint8_t viD = VID(instr); - const uint8_t 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 - if (viD != 0) - m_state.vi[viD] = (int16_t)(m_state.vi[viS] + m_state.vi[viT]); - return; - case 0x31: // ISUB - if (viD != 0) - m_state.vi[viD] = (int16_t)(m_state.vi[viS] - m_state.vi[viT]); - return; - case 0x32: // IADDI - { - int16_t imm5 = (int16_t)((int32_t)((instr >> 6) & 0x1F) << 27 >> 27); - if (viT != 0) - m_state.vi[viT] = (int16_t)(m_state.vi[viS] + imm5); - return; - } - case 0x34: // IAND - if (viD != 0) - m_state.vi[viD] = m_state.vi[viS] & m_state.vi[viT]; - return; - case 0x35: // IOR - if (viD != 0) - m_state.vi[viD] = m_state.vi[viS] | m_state.vi[viT]; - return; - - case 0x3C: - case 0x3D: - case 0x3E: - case 0x3F: // Lower1 special. Dobie decodes this as (instr & 3) | ((instr >> 4) & 0x7C). - { - const uint8_t funct2 = (uint8_t)((instr & 0x3u) | ((instr >> 4) & 0x7Cu)); - switch (funct2) - { - case 0x30: // MOVE - { - float tmp[4]; - std::memcpy(tmp, m_state.vf[vfS], 16); - applyDest(m_state.vf[vfT], tmp, dest); - return; - } - case 0x31: // MR32 (rotate right by 32 bits = shift xyzw -> yzwx) - { - float tmp[4] = {m_state.vf[vfS][1], m_state.vf[vfS][2], m_state.vf[vfS][3], m_state.vf[vfS][0]}; - applyDest(m_state.vf[vfT], tmp, dest); - return; - } - case 0x34: // LQI (Load Quadword, post-increment) - { - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) - { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - applyDest(m_state.vf[vfT], tmp, dest); - } - if (viS != 0) - m_state.vi[viS] = (int16_t)(m_state.vi[viS] + 1); - return; - } - case 0x35: // SQI (Store Quadword, post-increment) - { - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viT]) * 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[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); - } - if (viT != 0) - m_state.vi[viT] = (int16_t)(m_state.vi[viT] + 1); - return; - } - case 0x36: // LQD (Load Quadword, pre-decrement) - { - if (viS != 0) - m_state.vi[viS] = (int16_t)(m_state.vi[viS] - 1); - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) - { - float tmp[4]; - std::memcpy(tmp, vuData + addr, 16); - applyDest(m_state.vf[vfT], tmp, dest); - } - return; - } - case 0x37: // SQD (Store Quadword, pre-decrement) - { - if (viT != 0) - m_state.vi[viT] = (int16_t)(m_state.vi[viT] - 1); - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viT]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) - { - 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); - } - return; - } - case 0x38: // DIV - { - 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; - else - m_state.q = (num >= 0.0f) ? std::numeric_limits::max() : -std::numeric_limits::max(); - return; - } - case 0x39: // SQRT - { - int ftf = (instr >> 23) & 0x3; - float val = m_state.vf[vfT][ftf]; - m_state.q = std::sqrt(std::fabs(val)); - 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])); - if (den != 0.0f) - m_state.q = num / den; - else - m_state.q = std::numeric_limits::max(); - 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; - uint32_t fval; - std::memcpy(&fval, &m_state.vf[vfS][comp], 4); - if (viT != 0) - m_state.vi[viT] = (int32_t)(int16_t)(fval & 0xFFFF); - return; - } - case 0x3D: // MFIR (Move From Integer Register) - { - float result[4]; - int32_t val = (int32_t)(int16_t)(m_state.vi[viS] & 0xFFFF); - std::memcpy(&result[0], &val, 4); - result[1] = result[0]; - result[2] = result[0]; - result[3] = result[0]; - applyDest(m_state.vf[vfT], result, dest); - return; - } - case 0x3E: // ILWR - integer load word from address in VI[is] - { - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - addr &= (dataSize - 1); - if (addr + 16 <= dataSize) - { - int comp = 0; - if (dest & 0x8) - comp = 0; - else if (dest & 0x4) - comp = 1; - else if (dest & 0x2) - comp = 2; - else - comp = 3; - uint32_t v; - std::memcpy(&v, vuData + addr + comp * 4, 4); - if (viT != 0) - m_state.vi[viT] = (int32_t)(int16_t)(v & 0xFFFF); - } - return; - } - case 0x3F: // ISWR - integer store word to address in VI[is] - { - uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; - 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); - } - return; - } - case 0x40: // RNEXT - return; - case 0x41: // RGET - return; - case 0x42: // RINIT - return; - case 0x43: // RXOR - return; - case 0x64: // MFP (Move From P register) - { - float result[4] = {m_state.p, m_state.p, m_state.p, m_state.p}; - applyDest(m_state.vf[vfT], result, dest); - return; - } - case 0x68: // XTOP - move current VIF1 TOP into VI register - { - if (viT != 0) - m_state.vi[viT] = (int32_t)(m_state.top & 0x3FFu); - return; - } - case 0x69: // XITOP - move current VIF1 ITOP into VI register - { - if (viT != 0) - m_state.vi[viT] = (int32_t)(m_state.itop & 0x3FFu); - return; - } - case 0x6C: // XGKICK - send GIF packet from VU1 data memory - doXgkick(); - return; - case 0x70: // ESADD - return; - case 0x71: // ERSADD - 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); - 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(); - 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(); - return; - } - case 0x7B: // WAITP - return; - case 0x7D: // EATAN / EATANxy / EATANxz placeholder - return; - default: - return; - } - } - default: - return; - } - } - default: - break; - } -} +// VU1 interpreter implementation has been split into src/lib/vu/*.cpp. diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp new file mode 100644 index 0000000..6f24894 --- /dev/null +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp @@ -0,0 +1,201 @@ +#include "runtime/ps2_vu1.h" +#include "runtime/ps2_memory.h" +#include "ps2_vu1_detail.h" + +#include + +VU1Interpreter::VU1Interpreter() +{ + reset(); +} + +void VU1Interpreter::reset() +{ + std::memset(&m_state, 0, sizeof(m_state)); + m_state.vf[0][3] = 1.0f; // VF0.w = 1.0 + m_state.q = 1.0f; +} + +float VU1Interpreter::broadcast(const float *vf, uint8_t bc) +{ + return vf[bc & 3]; +} + +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 +} + +void VU1Interpreter::applyDestAcc(const float *result, uint8_t dest) +{ + applyDest(m_state.acc, result, dest); +} + +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 >> 31) & 1u) != 0u; + decoded.eBit = ((decoded.upper >> 30) & 1u) != 0u; + decoded.lowerBeforeUpper = !decoded.iBit && vuLowerShouldRunBeforeUpper(decoded.upper, decoded.lower); + 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); + for (uint32_t i = 0; i < pairCount; ++i) + { + m_decodedCodeCache[i] = decodeInstructionPair(vuCode, i * 8u); + } + + m_cachedVuCode = vuCode; + m_cachedMemory = memory; + m_cachedCodeSize = codeSize; + m_cachedCodeGeneration = generation; + m_decodedCodeCacheValid = true; +} + +VU1Interpreter::DecodedInstructionPair VU1Interpreter::getDecodedInstructionPairForPc(const uint8_t *vuCode, + uint32_t codeSize, + PS2Memory *memory, + uint32_t pc) +{ + // 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(); + if (!trackedVu1Code) + { + return decodeInstructionPair(vuCode, pc); + } + + const uint64_t generation = memory->getVU1CodeGeneration(); + const bool rebuild = + !m_decodedCodeCacheValid || + m_cachedVuCode != vuCode || + m_cachedMemory != memory || + m_cachedCodeSize != codeSize || + m_cachedCodeGeneration != generation; + + if (rebuild) + { + rebuildDecodedCodeCache(vuCode, codeSize, memory, generation); + } + + return m_decodedCodeCache[pc / 8u]; +} + +void VU1Interpreter::execute(uint8_t *vuCode, uint32_t codeSize, + uint8_t *vuData, uint32_t dataSize, + GS &gs, PS2Memory *memory, + uint32_t startPC, uint32_t top, uint32_t itop, + uint32_t maxCycles) +{ + m_state.pc = startPC & 0x3FFFu; + m_state.ebit = false; + m_state.top = top; + m_state.itop = itop; + m_state.branchPending = false; + m_state.branchTarget = 0; + m_state.branchDelay = 0; + m_state.vf[0][0] = 0.0f; + m_state.vf[0][1] = 0.0f; + m_state.vf[0][2] = 0.0f; + m_state.vf[0][3] = 1.0f; + run(vuCode, codeSize, vuData, dataSize, gs, memory, maxCycles); +} + +void VU1Interpreter::resume(uint8_t *vuCode, uint32_t codeSize, + uint8_t *vuData, uint32_t dataSize, + GS &gs, PS2Memory *memory, + uint32_t top, uint32_t itop, uint32_t maxCycles) +{ + m_state.ebit = false; + m_state.top = top; + m_state.itop = itop; + run(vuCode, codeSize, vuData, dataSize, gs, memory, maxCycles); +} + +void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize, + uint8_t *vuData, uint32_t dataSize, + GS &gs, PS2Memory *memory, uint32_t maxCycles) +{ + for (uint32_t cycle = 0; cycle < maxCycles; ++cycle) + { + if (m_state.pc + 8 > codeSize) + break; + + const DecodedInstructionPair decoded = getDecodedInstructionPairForPc(vuCode, codeSize, memory, m_state.pc); + + // LOI is controlled by the upper I-bit. The lower word is the float immediate. + // 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)); + } + else if (decoded.lowerBeforeUpper) + { + // 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); + execUpper(decoded.upper); + } + else + { + execUpper(decoded.upper); + execLower(decoded.lower, vuData, dataSize, gs, memory, decoded.upper); + } + + // Enforce VF0 invariant + 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; + + // 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) + { + m_state.pc = m_state.branchTarget & 0x3FFFu; + m_state.branchPending = false; + } + else + { + --m_state.branchDelay; + } + } + + if (m_state.ebit) + break; + + if (decoded.eBit) + m_state.ebit = true; + } +} diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h b/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h new file mode 100644 index 0000000..40acd76 --- /dev/null +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_detail.h @@ -0,0 +1,157 @@ +#ifndef PS2_VU1_DETAIL_H +#define PS2_VU1_DETAIL_H + +#include + +// Instruction field extraction helpers +static inline uint8_t DEST(uint32_t i) { return (uint8_t)((i >> 21) & 0xF); } +static inline uint8_t FT(uint32_t i) { return (uint8_t)((i >> 16) & 0x1F); } +static inline uint8_t FS(uint32_t i) { return (uint8_t)((i >> 11) & 0x1F); } +static inline uint8_t FD(uint32_t i) { return (uint8_t)((i >> 6) & 0x1F); } +static inline uint8_t BC(uint32_t i) { return (uint8_t)(i & 0x3); } + +// Lower instruction field helpers +static inline uint8_t LIT(uint32_t i) { return (uint8_t)((i >> 16) & 0x1F); } +static inline uint8_t LIS(uint32_t i) { return (uint8_t)((i >> 11) & 0x1F); } +static inline uint8_t LID(uint32_t i) { return (uint8_t)((i >> 6) & 0x1F); } +static inline uint8_t VIT(uint32_t i) { return (uint8_t)((i >> 16) & 0xF); } +static inline uint8_t VIS(uint32_t i) { return (uint8_t)((i >> 11) & 0xF); } +static inline uint8_t VID(uint32_t i) { return (uint8_t)((i >> 6) & 0xF); } +static inline int16_t IMM11(uint32_t i) { return (int16_t)(int32_t)((int32_t)(i << 21) >> 21); } +static inline int16_t IMM15(uint32_t i) +{ + uint32_t lo11 = i & 0x7FF; + uint32_t hi4 = (i >> 21) & 0xF; + uint32_t raw = (hi4 << 11) | lo11; + return (int16_t)(int32_t)((int32_t)(raw << 17) >> 17); +} + + +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 diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp new file mode 100644 index 0000000..f89ba7b --- /dev/null +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_lower.cpp @@ -0,0 +1,718 @@ +#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 + +// ============================================================================ +// Lower instructions +// ============================================================================ +void VU1Interpreter::execLower(uint32_t instr, uint8_t *vuData, uint32_t dataSize, GS &gs, PS2Memory *memory, uint32_t upperInstr) +{ + (void)upperInstr; + if (instr == 0x00000000 || instr == 0x8000033C) // NOP + return; + + uint8_t opHi = (instr >> 25) & 0x7F; + + // 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 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) + { + float tmp[4]; + std::memcpy(tmp, vuData + addr, 16); + applyDest(m_state.vf[it], tmp, dest); + } + return; + } + 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 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); + } + 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 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) + { + int comp = 0; + if (dest & 0x8) + comp = 0; + else if (dest & 0x4) + comp = 1; + else if (dest & 0x2) + comp = 2; + else + comp = 3; + uint32_t v; + std::memcpy(&v, vuData + addr + comp * 4, 4); + if (it != 0) + m_state.vi[it] = (int32_t)(int16_t)(v & 0xFFFF); + } + return; + } + 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 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); + } + return; + } + case 0x08: // IADDIU + { + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); + int16_t imm = (int16_t)(instr & 0x7FF) | ((instr >> 10) & 0x7800); + if (it != 0) + m_state.vi[it] = (int16_t)(m_state.vi[is] + imm); + return; + } + case 0x09: // ISUBIU + { + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); + int16_t imm = (int16_t)(instr & 0x7FF) | ((instr >> 10) & 0x7800); + if (it != 0) + m_state.vi[it] = (int16_t)(m_state.vi[is] - imm); + return; + } + case 0x10: // FCEQ + { + uint32_t imm24 = instr & 0xFFFFFF; + if (1 != 0) + m_state.vi[1] = ((m_state.clip & 0xFFFFFF) == imm24) ? 1 : 0; + return; + } + case 0x11: // FCSET + { + m_state.clip = instr & 0xFFFFFF; + return; + } + case 0x12: // FCAND + { + uint32_t imm24 = instr & 0xFFFFFF; + if (1 != 0) + m_state.vi[1] = ((m_state.clip & imm24) != 0) ? 1 : 0; + return; + } + case 0x13: // FCOR + { + uint32_t imm24 = instr & 0xFFFFFF; + if (1 != 0) + m_state.vi[1] = ((m_state.clip | imm24) == 0xFFFFFF) ? 1 : 0; + return; + } + case 0x14: // FSEQ + { + uint16_t imm12 = instr & 0xFFF; + if (1 != 0) + m_state.vi[1] = ((m_state.status & 0xFFF) == imm12) ? 1 : 0; + return; + } + case 0x15: // FSSET + { + m_state.status = (instr >> 6) & 0xFC0; + return; + } + case 0x16: // FSAND + { + uint16_t imm12 = instr & 0xFFF; + if (1 != 0) + m_state.vi[1] = (int32_t)(m_state.status & 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); + if (it != 0) + m_state.vi[it] = (int32_t)(m_state.mac & (uint32_t)(uint16_t)m_state.vi[is]); + return; + } + case 0x1A: // FMEQ + { + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); + if (it != 0) + m_state.vi[it] = ((m_state.mac & 0xFFFF) == (uint32_t)(uint16_t)m_state.vi[is]) ? 1 : 0; + return; + } + case 0x1C: // FMOR + { + 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 0x20: // B (unconditional branch) + { + int16_t imm = IMM11(instr); + uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + return; + } + case 0x21: // BAL (Branch and link) + { + uint8_t it = VIT(instr); + int16_t imm = IMM11(instr); + uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + if (it != 0) + m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + return; + } + case 0x24: // JR + { + uint8_t is = VIS(instr); + uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + return; + } + case 0x25: // JALR + { + uint8_t it = VIT(instr); + uint8_t is = VIS(instr); + uint32_t target = ((uint32_t)(uint16_t)m_state.vi[is] * 8u) & 0x3FFF; + if (it != 0) + m_state.vi[it] = (int32_t)((m_state.pc + 16) / 8); + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + return; + } + case 0x28: // IBEQ + { + 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]) + { + uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + } + return; + } + case 0x29: // IBNE + { + 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]) + { + uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + } + return; + } + case 0x2C: // IBLTZ + { + uint8_t is = VIS(instr); + int16_t imm = IMM11(instr); + if ((int16_t)m_state.vi[is] < 0) + { + uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + } + return; + } + case 0x2D: // IBGTZ + { + uint8_t is = VIS(instr); + int16_t imm = IMM11(instr); + if ((int16_t)m_state.vi[is] > 0) + { + uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + } + return; + } + case 0x2E: // IBLEZ + { + uint8_t is = VIS(instr); + int16_t imm = IMM11(instr); + if ((int16_t)m_state.vi[is] <= 0) + { + uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + } + return; + } + case 0x2F: // IBGEZ + { + uint8_t is = VIS(instr); + int16_t imm = IMM11(instr); + if ((int16_t)m_state.vi[is] >= 0) + { + uint32_t target = (m_state.pc + 8 + imm * 8) & 0x3FFF; + m_state.branchPending = true; + m_state.branchTarget = target; + m_state.branchDelay = 1; + } + return; + } + + case 0x40: // Lower1 / lower special. Bit31 set; low 6 bits select integer or special op. + { + const uint8_t funct = instr & 0x3Fu; + const uint8_t vfT = FT(instr); + const uint8_t vfS = FS(instr); + const uint8_t viT = VIT(instr); + const uint8_t viS = VIS(instr); + const uint8_t viD = VID(instr); + const uint8_t 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 + if (viD != 0) + m_state.vi[viD] = (int16_t)(m_state.vi[viS] + m_state.vi[viT]); + return; + case 0x31: // ISUB + if (viD != 0) + m_state.vi[viD] = (int16_t)(m_state.vi[viS] - m_state.vi[viT]); + return; + case 0x32: // IADDI + { + int16_t imm5 = (int16_t)((int32_t)((instr >> 6) & 0x1F) << 27 >> 27); + if (viT != 0) + m_state.vi[viT] = (int16_t)(m_state.vi[viS] + imm5); + return; + } + case 0x34: // IAND + if (viD != 0) + m_state.vi[viD] = m_state.vi[viS] & m_state.vi[viT]; + return; + case 0x35: // IOR + if (viD != 0) + m_state.vi[viD] = m_state.vi[viS] | m_state.vi[viT]; + return; + + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: // Lower1 special. Dobie decodes this as (instr & 3) | ((instr >> 4) & 0x7C). + { + const uint8_t funct2 = (uint8_t)((instr & 0x3u) | ((instr >> 4) & 0x7Cu)); + switch (funct2) + { + case 0x30: // MOVE + { + float tmp[4]; + std::memcpy(tmp, m_state.vf[vfS], 16); + applyDest(m_state.vf[vfT], tmp, dest); + return; + } + case 0x31: // MR32 (rotate right by 32 bits = shift xyzw -> yzwx) + { + float tmp[4] = {m_state.vf[vfS][1], m_state.vf[vfS][2], m_state.vf[vfS][3], m_state.vf[vfS][0]}; + applyDest(m_state.vf[vfT], tmp, dest); + return; + } + case 0x34: // LQI (Load Quadword, post-increment) + { + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + float tmp[4]; + std::memcpy(tmp, vuData + addr, 16); + applyDest(m_state.vf[vfT], tmp, dest); + } + if (viS != 0) + m_state.vi[viS] = (int16_t)(m_state.vi[viS] + 1); + return; + } + case 0x35: // SQI (Store Quadword, post-increment) + { + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viT]) * 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[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); + } + if (viT != 0) + m_state.vi[viT] = (int16_t)(m_state.vi[viT] + 1); + return; + } + case 0x36: // LQD (Load Quadword, pre-decrement) + { + if (viS != 0) + m_state.vi[viS] = (int16_t)(m_state.vi[viS] - 1); + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + float tmp[4]; + std::memcpy(tmp, vuData + addr, 16); + applyDest(m_state.vf[vfT], tmp, dest); + } + return; + } + case 0x37: // SQD (Store Quadword, pre-decrement) + { + if (viT != 0) + m_state.vi[viT] = (int16_t)(m_state.vi[viT] - 1); + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viT]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + 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); + } + return; + } + case 0x38: // DIV + { + 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; + else + m_state.q = (num >= 0.0f) ? std::numeric_limits::max() : -std::numeric_limits::max(); + return; + } + case 0x39: // SQRT + { + int ftf = (instr >> 23) & 0x3; + float val = m_state.vf[vfT][ftf]; + m_state.q = std::sqrt(std::fabs(val)); + 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])); + if (den != 0.0f) + m_state.q = num / den; + else + m_state.q = std::numeric_limits::max(); + 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; + uint32_t fval; + std::memcpy(&fval, &m_state.vf[vfS][comp], 4); + if (viT != 0) + m_state.vi[viT] = (int32_t)(int16_t)(fval & 0xFFFF); + return; + } + case 0x3D: // MFIR (Move From Integer Register) + { + float result[4]; + int32_t val = (int32_t)(int16_t)(m_state.vi[viS] & 0xFFFF); + std::memcpy(&result[0], &val, 4); + result[1] = result[0]; + result[2] = result[0]; + result[3] = result[0]; + applyDest(m_state.vf[vfT], result, dest); + return; + } + case 0x3E: // ILWR - integer load word from address in VI[is] + { + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; + addr &= (dataSize - 1); + if (addr + 16 <= dataSize) + { + int comp = 0; + if (dest & 0x8) + comp = 0; + else if (dest & 0x4) + comp = 1; + else if (dest & 0x2) + comp = 2; + else + comp = 3; + uint32_t v; + std::memcpy(&v, vuData + addr + comp * 4, 4); + if (viT != 0) + m_state.vi[viT] = (int32_t)(int16_t)(v & 0xFFFF); + } + return; + } + case 0x3F: // ISWR - integer store word to address in VI[is] + { + uint32_t addr = ((uint32_t)(uint16_t)m_state.vi[viS]) * 16u; + 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); + } + return; + } + case 0x40: // RNEXT + return; + case 0x41: // RGET + return; + case 0x42: // RINIT + return; + case 0x43: // RXOR + return; + case 0x64: // MFP (Move From P register) + { + float result[4] = {m_state.p, m_state.p, m_state.p, m_state.p}; + applyDest(m_state.vf[vfT], result, dest); + return; + } + case 0x68: // XTOP - move current VIF1 TOP into VI register + { + if (viT != 0) + m_state.vi[viT] = (int32_t)(m_state.top & 0x3FFu); + return; + } + case 0x69: // XITOP - move current VIF1 ITOP into VI register + { + if (viT != 0) + m_state.vi[viT] = (int32_t)(m_state.itop & 0x3FFu); + return; + } + case 0x6C: // XGKICK - send GIF packet from VU1 data memory + doXgkick(); + return; + case 0x70: // ESADD + return; + case 0x71: // ERSADD + 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); + 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(); + 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(); + return; + } + case 0x7B: // WAITP + return; + case 0x7D: // EATAN / EATANxy / EATANxz placeholder + return; + default: + return; + } + } + default: + return; + } + } + default: + break; + } +} diff --git a/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp b/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp new file mode 100644 index 0000000..27dde2d --- /dev/null +++ b/ps2xRuntime/src/lib/vu/ps2_vu1_upper.cpp @@ -0,0 +1,458 @@ +#include "runtime/ps2_vu1.h" +#include "ps2_vu1_detail.h" + +#include +#include + +// ============================================================================ +// Upper instructions (FMAC pipeline) +// ============================================================================ +void VU1Interpreter::execUpper(uint32_t instr) +{ + uint8_t dest = DEST(instr); + uint8_t ft = FT(instr); + uint8_t fs = FS(instr); + uint8_t fd = FD(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 result[4]; + + // Upper opcode decoding (bits 5:0 of upper word) + switch (op) + { + case 0x00: + case 0x01: + case 0x02: + case 0x03: // ADDbc + { + float bc = broadcast(vt, op & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] + bc; + applyDest(vd, result, dest); + return; + } + case 0x04: + case 0x05: + case 0x06: + case 0x07: // SUBbc + { + float bc = broadcast(vt, op & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] - bc; + applyDest(vd, result, dest); + return; + } + case 0x08: + case 0x09: + case 0x0A: + case 0x0B: // MADDbc + { + 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); + return; + } + case 0x0C: + case 0x0D: + case 0x0E: + case 0x0F: // MSUBbc + { + 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); + return; + } + case 0x10: + case 0x11: + case 0x12: + case 0x13: // MAXbc + { + float bc = broadcast(vt, op & 3); + for (int c = 0; c < 4; c++) + result[c] = (vs[c] > bc) ? vs[c] : bc; + applyDest(vd, result, dest); + return; + } + case 0x14: + case 0x15: + case 0x16: + case 0x17: // MINIbc + { + float bc = broadcast(vt, op & 3); + for (int c = 0; c < 4; c++) + result[c] = (vs[c] < bc) ? vs[c] : bc; + applyDest(vd, result, dest); + return; + } + case 0x18: + case 0x19: + case 0x1A: + case 0x1B: // MULbc + { + float bc = broadcast(vt, op & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] * bc; + applyDest(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); + return; + case 0x1D: // MAXi + for (int c = 0; c < 4; c++) + result[c] = (vs[c] > m_state.i) ? vs[c] : m_state.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); + return; + case 0x1F: // MINIi + for (int c = 0; c < 4; c++) + result[c] = (vs[c] < m_state.i) ? vs[c] : m_state.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); + 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); + return; + case 0x22: // ADDi + for (int c = 0; c < 4; c++) + result[c] = vs[c] + m_state.i; + applyDest(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); + return; + case 0x24: // SUBq + for (int c = 0; c < 4; c++) + result[c] = vs[c] - m_state.q; + applyDest(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); + return; + case 0x26: // SUBi + for (int c = 0; c < 4; c++) + result[c] = vs[c] - m_state.i; + applyDest(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); + return; + case 0x28: // ADD + for (int c = 0; c < 4; c++) + result[c] = vs[c] + vt[c]; + applyDest(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); + return; + case 0x2A: // MUL + for (int c = 0; c < 4; c++) + result[c] = vs[c] * vt[c]; + applyDest(vd, result, dest); + return; + case 0x2B: // MAX + for (int c = 0; c < 4; c++) + result[c] = (vs[c] > vt[c]) ? vs[c] : vt[c]; + applyDest(vd, result, dest); + return; + case 0x2C: // SUB + for (int c = 0; c < 4; c++) + result[c] = vs[c] - vt[c]; + applyDest(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); + return; + case 0x2E: // OPMSUB + result[0] = m_state.acc[0] - vs[1] * vt[2]; + result[1] = m_state.acc[1] - vs[2] * vt[0]; + result[2] = m_state.acc[2] - vs[0] * vt[1]; + result[3] = 0.0f; + applyDest(vd, result, dest); + return; + case 0x2F: // MINI + for (int c = 0; c < 4; c++) + result[c] = (vs[c] < vt[c]) ? vs[c] : vt[c]; + applyDest(vd, result, dest); + return; + + // Upper special group (low op 0x3C..0x3F). + // Like lower1 special, the real selector is not just bits 5:0. Dobie decodes: + // op = (instr & 0x3) | ((instr >> 4) & 0x7C) + // Several instructions in this group also use FT as the destination, not FD. + case 0x3C: + case 0x3D: + case 0x3E: + case 0x3F: + { + const uint8_t specialOp = static_cast((instr & 0x3u) | ((instr >> 4) & 0x7Cu)); + float *vtDest = m_state.vf[ft]; + + switch (specialOp) + { + case 0x00: + case 0x01: + case 0x02: + case 0x03: // ADDAbc + { + float bc = broadcast(vt, specialOp & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] + bc; + applyDestAcc(result, dest); + return; + } + case 0x04: + case 0x05: + case 0x06: + case 0x07: // SUBAbc + { + float bc = broadcast(vt, specialOp & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] - bc; + applyDestAcc(result, dest); + return; + } + case 0x08: + case 0x09: + case 0x0A: + case 0x0B: // MADDAbc + { + 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); + return; + } + case 0x0C: + case 0x0D: + case 0x0E: + case 0x0F: // MSUBAbc + { + 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); + return; + } + case 0x10: // ITOF0 + for (int c = 0; c < 4; c++) + { + int32_t iv; + std::memcpy(&iv, &vs[c], 4); + result[c] = static_cast(iv); + } + applyDest(vtDest, result, dest); + return; + case 0x11: // ITOF4 + for (int c = 0; c < 4; c++) + { + int32_t iv; + std::memcpy(&iv, &vs[c], 4); + result[c] = static_cast(iv) / 16.0f; + } + applyDest(vtDest, result, dest); + return; + case 0x12: // ITOF12 + for (int c = 0; c < 4; c++) + { + int32_t iv; + std::memcpy(&iv, &vs[c], 4); + result[c] = static_cast(iv) / 4096.0f; + } + applyDest(vtDest, result, dest); + return; + case 0x13: // ITOF15 + for (int c = 0; c < 4; c++) + { + int32_t iv; + std::memcpy(&iv, &vs[c], 4); + result[c] = static_cast(iv) / 32768.0f; + } + applyDest(vtDest, result, dest); + return; + case 0x14: // FTOI0 + for (int c = 0; c < 4; c++) + { + int32_t iv = static_cast(vs[c]); + std::memcpy(&result[c], &iv, 4); + } + applyDest(vtDest, result, dest); + return; + case 0x15: // FTOI4 + for (int c = 0; c < 4; c++) + { + int32_t iv = static_cast(vs[c] * 16.0f); + std::memcpy(&result[c], &iv, 4); + } + applyDest(vtDest, result, dest); + return; + case 0x16: // FTOI12 + for (int c = 0; c < 4; c++) + { + int32_t iv = static_cast(vs[c] * 4096.0f); + std::memcpy(&result[c], &iv, 4); + } + applyDest(vtDest, result, dest); + return; + case 0x17: // FTOI15 + for (int c = 0; c < 4; c++) + { + int32_t iv = static_cast(vs[c] * 32768.0f); + std::memcpy(&result[c], &iv, 4); + } + applyDest(vtDest, result, dest); + return; + case 0x18: + case 0x19: + case 0x1A: + case 0x1B: // MULAbc + { + float bc = broadcast(vt, specialOp & 3); + for (int c = 0; c < 4; c++) + result[c] = vs[c] * bc; + applyDestAcc(result, dest); + return; + } + case 0x1C: // MULAq + for (int c = 0; c < 4; c++) + result[c] = vs[c] * m_state.q; + applyDestAcc(result, dest); + return; + case 0x1D: // ABS + for (int c = 0; c < 4; c++) + result[c] = std::fabs(vs[c]); + applyDest(vtDest, result, dest); + return; + case 0x1E: // MULAi + for (int c = 0; c < 4; c++) + result[c] = vs[c] * m_state.i; + applyDestAcc(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; + return; + } + case 0x20: // ADDAq + for (int c = 0; c < 4; c++) + result[c] = vs[c] + m_state.q; + applyDestAcc(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); + return; + case 0x22: // ADDAi + for (int c = 0; c < 4; c++) + result[c] = vs[c] + m_state.i; + applyDestAcc(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); + return; + case 0x24: // SUBAq + for (int c = 0; c < 4; c++) + result[c] = vs[c] - m_state.q; + applyDestAcc(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); + return; + case 0x26: // SUBAi + for (int c = 0; c < 4; c++) + result[c] = vs[c] - m_state.i; + applyDestAcc(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); + return; + case 0x28: // ADDA + for (int c = 0; c < 4; c++) + result[c] = vs[c] + vt[c]; + applyDestAcc(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); + return; + case 0x2A: // MULA + for (int c = 0; c < 4; c++) + result[c] = vs[c] * vt[c]; + applyDestAcc(result, dest); + return; + case 0x2C: // SUBA + for (int c = 0; c < 4; c++) + result[c] = vs[c] - vt[c]; + applyDestAcc(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); + 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); + return; + case 0x2F: + case 0x30: // NOP + return; + default: + return; + } + } + + case 0x30: + case 0x31: + case 0x32: + case 0x33: + default: + return; + } +} diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 5ab3921..917df6a 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -23,6 +23,7 @@ add_library(ps2_test_lib STATIC src/ps2_runtime_kernel_tests.cpp src/ps2_runtime_interrupt_tests.cpp src/ps2_memory_tests.cpp + src/ps2_vu1_tests.cpp src/ps2_gs_tests.cpp src/ps2_sif_rpc_tests.cpp src/ps2_sif_dma_tests.cpp diff --git a/ps2xTest/src/main.cpp b/ps2xTest/src/main.cpp index 0ed5e9b..7e8c3c3 100644 --- a/ps2xTest/src/main.cpp +++ b/ps2xTest/src/main.cpp @@ -10,6 +10,7 @@ void register_ps2_runtime_io_tests(); void register_ps2_runtime_kernel_tests(); void register_ps2_runtime_interrupt_tests(); void register_ps2_memory_tests(); +void register_ps2_vu1_tests(); void register_ps2_gs_tests(); void register_ps2_sif_rpc_tests(); void register_ps2_sif_dma_tests(); @@ -29,6 +30,7 @@ int main() register_ps2_runtime_kernel_tests(); register_ps2_runtime_interrupt_tests(); register_ps2_memory_tests(); + register_ps2_vu1_tests(); register_ps2_gs_tests(); register_ps2_sif_rpc_tests(); register_ps2_sif_dma_tests(); diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 0eab823..3bcb7a5 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -2,7 +2,6 @@ #include "runtime/ps2_memory.h" #include "runtime/ps2_gs_gpu.h" #include "runtime/ps2_gs_psmct32.h" -#include "runtime/ps2_vu1.h" #include "ps2_runtime.h" #include "ps2_runtime_macros.h" #include "Stubs/DMA.h" @@ -156,17 +155,6 @@ namespace appendU64(packet, 0u); } - uint32_t makeVuLowerSpecial(uint8_t specialOp, uint8_t is, uint8_t it = 0u, uint8_t id = 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(id & 0x1Fu) << 6) | - (static_cast(specialOp & 0x7Cu) << 4) | - static_cast(specialOp & 0x3u) | - 0x3Cu; - } } void register_ps2_memory_tests() @@ -1732,72 +1720,6 @@ void register_ps2_memory_tests() t.Equals(mem.readIORegister(kDstadr), 0u, "sceDmaReset should clear D_STADR"); }); - tc.Run("VU1 XGKICK wraps packet payload across VU1 memory boundary", [](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 *vuCode = mem.getVU1Code(); - uint8_t *vuData = mem.getVU1Data(); - std::memset(vuCode, 0, PS2_VU1_CODE_SIZE); - std::memset(vuData, 0, PS2_VU1_DATA_SIZE); - - constexpr uint32_t kLastQw = (PS2_VU1_DATA_SIZE / 16u) - 1u; - const uint32_t tagOffset = kLastQw * 16u; - - const uint64_t imageTag = makeGifTag(1u, GIF_FMT_IMAGE, 0u, true); - std::memcpy(vuData + tagOffset, &imageTag, sizeof(imageTag)); - - for (uint32_t i = 0; i < 16u; ++i) - { - vuData[i] = static_cast(0xC0u + i); - } - - const uint32_t lower = makeVuLowerSpecial(0x6Cu, 1u); - std::memcpy(vuCode + 0u, &lower, sizeof(lower)); - const uint32_t upper = 0u; - std::memcpy(vuCode + 4u, &upper, sizeof(upper)); - - VU1Interpreter vu1; - vu1.state().vi[1] = static_cast(kLastQw); - vu1.execute(vuCode, - PS2_VU1_CODE_SIZE, - vuData, - PS2_VU1_DATA_SIZE, - gs, - &mem, - 0u, - 0u, - 0u, - 1u); - - t.Equals(captured.size(), static_cast(1u), "XGKICK should emit one wrapped GIF packet"); - if (!captured.empty()) - { - t.Equals(captured[0].size(), static_cast(32u), "wrapped packet should include tag plus one qword payload"); - bool payloadOk = true; - for (uint32_t i = 0; i < 16u; ++i) - { - if (captured[0].size() < 32u || captured[0][16u + i] != static_cast(0xC0u + i)) - { - payloadOk = false; - break; - } - } - t.IsTrue(payloadOk, "wrapped payload should be copied from start of VU1 memory"); - } - }); - tc.Run("VIF1 DMA DIRECT image packet reaches GS through arbiter", [](TestCase &t) { PS2Memory mem; @@ -1919,85 +1841,6 @@ void register_ps2_memory_tests() t.IsTrue(imageOk, "raw qwords after a DIRECT image tag should continue the PATH2 image upload"); }); - tc.Run("VIF MSCAL callback can execute XGKICK and update GS VRAM", [](TestCase &t) - { - PS2Memory mem; - t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); - - GS gs; - gs.init(mem.getGSVRAM(), static_cast(PS2_GS_VRAM_SIZE), &mem.gs()); - GifArbiter arbiter([&](const uint8_t *data, uint32_t sizeBytes) - { - gs.processGIFPacket(data, sizeBytes); - }); - mem.setGifArbiter(&arbiter); - - const uint64_t bitblt = - (static_cast(0u) << 0) | - (static_cast(1u) << 16) | - (static_cast(0u) << 24) | - (static_cast(0u) << 32) | - (static_cast(1u) << 48) | - (static_cast(0u) << 56); - gs.writeRegister(GS_REG_BITBLTBUF, bitblt); - gs.writeRegister(GS_REG_TRXPOS, 0ull); - gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); - gs.writeRegister(GS_REG_TRXDIR, 0ull); - - uint8_t *vuCode = mem.getVU1Code(); - uint8_t *vuData = mem.getVU1Data(); - std::memset(vuCode, 0, PS2_VU1_CODE_SIZE); - std::memset(vuData, 0, PS2_VU1_DATA_SIZE); - - const uint32_t lower = makeVuLowerSpecial(0x6Cu, 0u); - std::memcpy(vuCode + 0u, &lower, sizeof(lower)); - const uint32_t upper = 0u; - std::memcpy(vuCode + 4u, &upper, sizeof(upper)); - - const uint64_t gifTag = makeGifTag(1u, GIF_FMT_IMAGE, 0u, true); - std::memcpy(vuData + 0u, &gifTag, sizeof(gifTag)); - const uint64_t tagHi = 0u; - std::memcpy(vuData + 8u, &tagHi, sizeof(tagHi)); - for (uint32_t i = 0; i < 16u; ++i) - { - vuData[16u + i] = static_cast(0x90u + i); - } - - VU1Interpreter vu1; - mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t top, uint32_t itop) - { - vu1.execute(vuCode, - PS2_VU1_CODE_SIZE, - vuData, - PS2_VU1_DATA_SIZE, - gs, - &mem, - startPC, - top, - itop, - 1u); - }); - - const uint32_t mscalCmd = makeVifCmd(0x14u, 0u, 0u); - mem.processVIF1Data(reinterpret_cast(&mscalCmd), sizeof(mscalCmd)); - - const uint8_t *vramOut = mem.getGSVRAM(); - bool imageOk = true; - for (uint32_t x = 0; x < 4u && imageOk; ++x) - { - const uint32_t off = GSPSMCT32::addrPSMCT32(0u, 1u, x, 0u); - for (uint32_t c = 0; c < 4u; ++c) - { - if (vramOut[off + c] != static_cast(0x90u + x * 4u + c)) - { - imageOk = false; - break; - } - } - } - t.IsTrue(imageOk, "MSCAL-triggered XGKICK should route PATH1 packet into GS VRAM"); - }); - tc.Run("unaligned accesses throw", [](TestCase &t) { PS2Memory mem; diff --git a/ps2xTest/src/ps2_vu1_tests.cpp b/ps2xTest/src/ps2_vu1_tests.cpp new file mode 100644 index 0000000..a66da89 --- /dev/null +++ b/ps2xTest/src/ps2_vu1_tests.cpp @@ -0,0 +1,559 @@ +#include "MiniTest.h" +#include "runtime/ps2_gif_arbiter.h" +#include "runtime/ps2_gs_gpu.h" +#include "runtime/ps2_gs_psmct32.h" +#include "runtime/ps2_memory.h" +#include "runtime/ps2_vu1.h" + +#include +#include +#include + +namespace +{ + constexpr uint32_t kVuUpperNop = 0u; + + struct Vu1Fixture + { + PS2Memory mem; + GS gs; + uint8_t *code = nullptr; + uint8_t *data = nullptr; + + bool initialize() + { + if (!mem.initialize()) + return false; + gs.init(mem.getGSVRAM(), static_cast(PS2_GS_VRAM_SIZE), &mem.gs()); + code = mem.getVU1Code(); + data = mem.getVU1Data(); + std::memset(code, 0, PS2_VU1_CODE_SIZE); + std::memset(data, 0, PS2_VU1_DATA_SIZE); + return code != nullptr && data != nullptr; + } + }; + + uint32_t makeVifCmd(uint8_t opcode, uint8_t num, uint16_t imm) + { + return (static_cast(opcode) << 24) | + (static_cast(num) << 16) | + static_cast(imm); + } + + uint64_t makeGifTag(uint16_t nloop, uint8_t flg, uint8_t nreg, bool eop = true) + { + uint64_t tag = static_cast(nloop & 0x7FFFu); + if (eop) + tag |= (1ull << 15); + tag |= (static_cast(flg & 0x3u) << 58); + tag |= (static_cast(nreg & 0xFu) << 60); + return tag; + } + + uint32_t makeVuLowerSpecial(uint8_t specialOp, uint8_t is, uint8_t it = 0u, uint8_t id = 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(id & 0x1Fu) << 6) | + (static_cast(specialOp & 0x7Cu) << 4) | + static_cast(specialOp & 0x3u) | + 0x3Cu; + } + + uint32_t makeVuLowerDirect(uint8_t funct, uint8_t is, uint8_t it = 0u, uint8_t id = 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(id & 0x1Fu) << 6) | + static_cast(funct & 0x3Fu); + } + + uint32_t makeVuUpper(uint8_t op, uint8_t dest, uint8_t ft, uint8_t fs, uint8_t fd) + { + return (static_cast(dest & 0xFu) << 21) | + (static_cast(ft & 0x1Fu) << 16) | + (static_cast(fs & 0x1Fu) << 11) | + (static_cast(fd & 0x1Fu) << 6) | + static_cast(op & 0x3Fu); + } + + uint32_t makeVuLq(uint8_t dest, uint8_t targetVf, uint8_t baseVi, int16_t imm) + { + return (static_cast(dest & 0xFu) << 21) | + (static_cast(targetVf & 0x1Fu) << 16) | + (static_cast(baseVi & 0xFu) << 11) | + (static_cast(imm) & 0x7FFu); + } + + uint32_t makeVuSq(uint8_t dest, uint8_t sourceVf, uint8_t baseVi, int16_t imm) + { + return (0x01u << 25) | + (static_cast(dest & 0xFu) << 21) | + (static_cast(baseVi & 0xFu) << 16) | + (static_cast(sourceVf & 0x1Fu) << 11) | + (static_cast(imm) & 0x7FFu); + } + + uint32_t makeVuIaddiu(uint8_t it, uint8_t is, int16_t imm) + { + return (0x08u << 25) | + (static_cast(it & 0xFu) << 16) | + (static_cast(is & 0xFu) << 11) | + (static_cast(imm) & 0x7FFu); + } + + uint32_t makeVuBranch(int16_t imm) + { + return (0x20u << 25) | (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))); + } + + uint32_t makeVuSqrt(uint8_t ft, uint8_t ftf) + { + return makeVuLowerSpecial(0x39u, 0u, ft, 0u, static_cast((ftf & 0x3u) << 2)); + } + + void writeVuInstructionPair(uint8_t *code, uint32_t pc, uint32_t lower, uint32_t upper) + { + 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); + } + + void appendU32(std::vector &bytes, uint32_t value) + { + const uint8_t *src = reinterpret_cast(&value); + bytes.insert(bytes.end(), src, src + sizeof(value)); + } + + void uploadVu1Mpg(PS2Memory &mem, uint16_t instructionAddress, uint32_t lower, uint32_t upper) + { + std::vector packet; + appendU32(packet, makeVifCmd(0x4Au, 1u, instructionAddress)); + appendU32(packet, lower); + appendU32(packet, upper); + mem.processVIF1Data(packet.data(), static_cast(packet.size())); + } + + void writeVuQword(uint8_t *data, uint32_t qwordIndex, const float values[4]) + { + std::memcpy(data + qwordIndex * 16u, values, sizeof(float) * 4u); + } + + void readVuQword(const uint8_t *data, uint32_t qwordIndex, float values[4]) + { + std::memcpy(values, data + qwordIndex * 16u, sizeof(float) * 4u); + } +} + +void register_ps2_vu1_tests() +{ + MiniTest::Case("PS2VU1", [](TestCase &tc) + { + tc.Run("upper ADD applies the destination mask", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 0u, 0u, makeVuUpper(0x28u, 0xAu, 2u, 1u, 3u)); // ADD.xz vf3, vf1, vf2 + + 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[1][3] = 4.0f; + vu1.state().vf[2][0] = 10.0f; + vu1.state().vf[2][1] = 20.0f; + vu1.state().vf[2][2] = 30.0f; + vu1.state().vf[2][3] = 40.0f; + vu1.state().vf[3][0] = -1.0f; + vu1.state().vf[3][1] = -2.0f; + vu1.state().vf[3][2] = -3.0f; + vu1.state().vf[3][3] = -4.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[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"); + t.Equals(vu1.state().vf[3][3], -4.0f, "ADD.xz should preserve w"); + }); + + tc.Run("LOI commits the lower immediate after the upper instruction", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + const float newI = 7.0f; + uint32_t lowerImmediate = 0u; + std::memcpy(&lowerImmediate, &newI, sizeof(newI)); + const uint32_t upperAddiWithIBit = makeVuUpper(0x22u, 0xFu, 0u, 1u, 2u) | 0x80000000u; // ADDi.xyzw vf2, vf1 + writeVuInstructionPair(fx.code, 0u, lowerImmediate, upperAddiWithIBit); + + VU1Interpreter vu1; + vu1.state().i = 2.0f; + 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.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], 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"); + t.Equals(vu1.state().vf[2][3], 6.0f, "ADDi should use old I for w"); + t.Equals(vu1.state().i, 7.0f, "LOI should commit lower immediate into I after upper execution"); + }); + + tc.Run("LQ and SQ use VI qword addressing and destination masks", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + const float sourceQw[4] = {10.0f, 20.0f, 30.0f, 40.0f}; + const float destQw[4] = {-1.0f, -2.0f, -3.0f, -4.0f}; + writeVuQword(fx.data, 3u, sourceQw); + writeVuQword(fx.data, 5u, destQw); + writeVuInstructionPair(fx.code, 0u, makeVuLq(0x5u, 4u, 1u, 1), kVuUpperNop); // LQ.yw vf4, 1(vi1) + writeVuInstructionPair(fx.code, 8u, makeVuSq(0xAu, 4u, 2u, 1), kVuUpperNop); // SQ.xz vf4, 1(vi2) + + VU1Interpreter vu1; + vu1.state().vi[1] = 2; + vu1.state().vi[2] = 4; + vu1.state().vf[4][0] = 100.0f; + vu1.state().vf[4][1] = 200.0f; + 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); + + 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"); + t.Equals(vu1.state().vf[4][3], 40.0f, "LQ.yw should load w"); + + float stored[4] = {}; + readVuQword(fx.data, 5u, stored); + t.Equals(stored[0], 100.0f, "SQ.xz should store x"); + 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"); + }); + + tc.Run("integer lower ops keep VI0 hardwired to zero", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 0u, makeVuIaddiu(2u, 1u, 5), kVuUpperNop); // IADDIU vi2, vi1, 5 + writeVuInstructionPair(fx.code, 8u, makeVuIaddiu(0u, 2u, 7), kVuUpperNop); // IADDIU vi0, vi2, 7 + writeVuInstructionPair(fx.code, 16u, makeVuLowerDirect(0x30u, 2u, 1u, 3u), kVuUpperNop); // IADD vi3, vi2, vi1 + + VU1Interpreter vu1; + vu1.state().vi[0] = 99; + vu1.state().vi[1] = 10; + + 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[2], 15, "IADDIU should add signed immediate to VI source"); + t.Equals(vu1.state().vi[3], 25, "IADD should add VI source registers"); + t.Equals(vu1.state().vi[0], 0, "VI0 should remain hardwired to zero"); + }); + + tc.Run("XTOP and XITOP expose VIF TOP values to VI registers", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 0u, makeVuLowerSpecial(0x68u, 0u, 2u), kVuUpperNop); // XTOP vi2 + writeVuInstructionPair(fx.code, 8u, makeVuLowerSpecial(0x69u, 0u, 3u), kVuUpperNop); // XITOP vi3 + + VU1Interpreter vu1; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0x123u, 0x2ABu, 2u); + + t.Equals(vu1.state().vi[2], 0x123, "XTOP should move TOP into the target VI register"); + t.Equals(vu1.state().vi[3], 0x2AB, "XITOP should move ITOP into the target VI register"); + }); + + tc.Run("lower branch commits after one delay-slot instruction", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, 0u, makeVuBranch(2), kVuUpperNop); // target pc = 24 + writeVuInstructionPair(fx.code, 8u, makeVuIaddiu(1u, 0u, 1), kVuUpperNop); // delay slot + writeVuInstructionPair(fx.code, 16u, makeVuIaddiu(2u, 0u, 99), kVuUpperNop); // skipped + writeVuInstructionPair(fx.code, 24u, makeVuIaddiu(3u, 0u, 7), kVuUpperNop); // branch target + + VU1Interpreter vu1; + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 3u); + + t.Equals(vu1.state().vi[1], 1, "branch delay slot should execute"); + t.Equals(vu1.state().vi[2], 0, "instruction between delay slot and target should be skipped"); + t.Equals(vu1.state().vi[3], 7, "branch target should execute after the delay slot"); + }); + + tc.Run("lower side sees old VF value when upper writes the same register", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + writeVuInstructionPair(fx.code, + 0u, + makeVuSq(0xFu, 1u, 1u, 0), // SQ.xyzw vf1, 0(vi1) + makeVuUpper(0x28u, 0xFu, 3u, 2u, 1u)); // ADD.xyzw vf1, vf2, vf3 + + VU1Interpreter vu1; + vu1.state().vi[1] = 6; + 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[2][1] = 20.0f; + vu1.state().vf[2][2] = 30.0f; + vu1.state().vf[2][3] = 40.0f; + vu1.state().vf[3][0] = 100.0f; + vu1.state().vf[3][1] = 200.0f; + 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); + + float stored[4] = {}; + readVuQword(fx.data, 6u, stored); + t.Equals(stored[0], 1.0f, "SQ should observe old VF value for x"); + 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"); + 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("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)) + + 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.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"); + }); + + tc.Run("MPG upload invalidates cached VU1 decode before MSCAL", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + VU1Interpreter vu1; + fx.mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t top, uint32_t itop) + { + vu1.execute(fx.code, + PS2_VU1_CODE_SIZE, + fx.data, + PS2_VU1_DATA_SIZE, + fx.gs, + &fx.mem, + startPC, + top, + itop, + 1u); + }); + + uploadVu1Mpg(fx.mem, 0u, makeVuIaddiu(1u, 0u, 1), kVuUpperNop); + const uint32_t firstMscal = makeVifCmd(0x14u, 0u, 0u); + fx.mem.processVIF1Data(reinterpret_cast(&firstMscal), sizeof(firstMscal)); + t.Equals(vu1.state().vi[1], 1, "first MSCAL should execute the first uploaded program"); + + uploadVu1Mpg(fx.mem, 0u, makeVuIaddiu(1u, 0u, 2), kVuUpperNop); + const uint32_t secondMscal = makeVifCmd(0x14u, 0u, 0u); + fx.mem.processVIF1Data(reinterpret_cast(&secondMscal), sizeof(secondMscal)); + t.Equals(vu1.state().vi[1], 2, "second MSCAL should see the MPG-updated instruction"); + }); + + tc.Run("direct VU1 code writes invalidate cached decode", [](TestCase &t) + { + Vu1Fixture fx; + t.IsTrue(fx.initialize(), "VU1 fixture should initialize"); + + VU1Interpreter vu1; + fx.mem.write64(PS2_VU1_CODE_BASE, packVuInstructionPair(makeVuIaddiu(1u, 0u, 1), kVuUpperNop)); + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + t.Equals(vu1.state().vi[1], 1, "first execution should use the original direct write"); + + fx.mem.write64(PS2_VU1_CODE_BASE, packVuInstructionPair(makeVuIaddiu(1u, 0u, 2), kVuUpperNop)); + vu1.execute(fx.code, PS2_VU1_CODE_SIZE, fx.data, PS2_VU1_DATA_SIZE, fx.gs, &fx.mem, 0u, 0u, 0u, 1u); + t.Equals(vu1.state().vi[1], 2, "second execution should rebuild decode after the direct write"); + }); + + tc.Run("XGKICK sends a VU memory GIF packet through PATH1", [](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 *vuCode = mem.getVU1Code(); + uint8_t *vuData = mem.getVU1Data(); + std::memset(vuCode, 0, PS2_VU1_CODE_SIZE); + std::memset(vuData, 0, PS2_VU1_DATA_SIZE); + + constexpr uint32_t kLastQw = (PS2_VU1_DATA_SIZE / 16u) - 1u; + const uint32_t tagOffset = kLastQw * 16u; + + const uint64_t imageTag = makeGifTag(1u, GIF_FMT_IMAGE, 0u, true); + std::memcpy(vuData + tagOffset, &imageTag, sizeof(imageTag)); + + for (uint32_t i = 0; i < 16u; ++i) + { + vuData[i] = static_cast(0xC0u + i); + } + + const uint32_t lower = makeVuLowerSpecial(0x6Cu, 1u); + std::memcpy(vuCode + 0u, &lower, sizeof(lower)); + const uint32_t upper = 0u; + std::memcpy(vuCode + 4u, &upper, sizeof(upper)); + + VU1Interpreter vu1; + vu1.state().vi[1] = static_cast(kLastQw); + vu1.execute(vuCode, + PS2_VU1_CODE_SIZE, + vuData, + PS2_VU1_DATA_SIZE, + gs, + &mem, + 0u, + 0u, + 0u, + 1u); + + t.Equals(captured.size(), static_cast(1u), "XGKICK should emit one wrapped GIF packet"); + if (!captured.empty()) + { + t.Equals(captured[0].size(), static_cast(32u), "wrapped packet should include tag plus one qword payload"); + bool payloadOk = true; + for (uint32_t i = 0; i < 16u; ++i) + { + if (captured[0].size() < 32u || captured[0][16u + i] != static_cast(0xC0u + i)) + { + payloadOk = false; + break; + } + } + t.IsTrue(payloadOk, "wrapped payload should be copied from start of VU1 memory"); + } + }); + + tc.Run("MSCAL can start a VU1 XGKICK program and update GS VRAM", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + GS gs; + gs.init(mem.getGSVRAM(), static_cast(PS2_GS_VRAM_SIZE), &mem.gs()); + GifArbiter arbiter([&](const uint8_t *data, uint32_t sizeBytes) + { + gs.processGIFPacket(data, sizeBytes); + }); + mem.setGifArbiter(&arbiter); + + const uint64_t bitblt = + (static_cast(0u) << 0) | + (static_cast(1u) << 16) | + (static_cast(0u) << 24) | + (static_cast(0u) << 32) | + (static_cast(1u) << 48) | + (static_cast(0u) << 56); + gs.writeRegister(GS_REG_BITBLTBUF, bitblt); + gs.writeRegister(GS_REG_TRXPOS, 0ull); + gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); + gs.writeRegister(GS_REG_TRXDIR, 0ull); + + uint8_t *vuCode = mem.getVU1Code(); + uint8_t *vuData = mem.getVU1Data(); + std::memset(vuCode, 0, PS2_VU1_CODE_SIZE); + std::memset(vuData, 0, PS2_VU1_DATA_SIZE); + + const uint32_t lower = makeVuLowerSpecial(0x6Cu, 0u); + std::memcpy(vuCode + 0u, &lower, sizeof(lower)); + const uint32_t upper = 0u; + std::memcpy(vuCode + 4u, &upper, sizeof(upper)); + + const uint64_t gifTag = makeGifTag(1u, GIF_FMT_IMAGE, 0u, true); + std::memcpy(vuData + 0u, &gifTag, sizeof(gifTag)); + const uint64_t tagHi = 0u; + std::memcpy(vuData + 8u, &tagHi, sizeof(tagHi)); + for (uint32_t i = 0; i < 16u; ++i) + { + vuData[16u + i] = static_cast(0x90u + i); + } + + VU1Interpreter vu1; + mem.setVu1MscalCallback([&](uint32_t startPC, uint32_t top, uint32_t itop) + { + vu1.execute(vuCode, + PS2_VU1_CODE_SIZE, + vuData, + PS2_VU1_DATA_SIZE, + gs, + &mem, + startPC, + top, + itop, + 1u); + }); + + const uint32_t mscalCmd = makeVifCmd(0x14u, 0u, 0u); + mem.processVIF1Data(reinterpret_cast(&mscalCmd), sizeof(mscalCmd)); + + const uint8_t *vramOut = mem.getGSVRAM(); + bool imageOk = true; + for (uint32_t x = 0; x < 4u && imageOk; ++x) + { + const uint32_t off = GSPSMCT32::addrPSMCT32(0u, 1u, x, 0u); + for (uint32_t c = 0; c < 4u; ++c) + { + if (vramOut[off + c] != static_cast(0x90u + x * 4u + c)) + { + imageOk = false; + break; + } + } + } + t.IsTrue(imageOk, "MSCAL-triggered XGKICK should route PATH1 packet into GS VRAM"); + }); + }); +}