Feature vu1 cache (#158)

* feat: explode vu1 in files
feat: added way  more tests for vu1

* feat: added VU1 cache
This commit is contained in:
Ranieri
2026-07-07 21:48:26 -03:00
committed by GitHub
parent 52edf07657
commit ecc86f4b5d
14 changed files with 2145 additions and 1628 deletions
+3 -1
View File
@@ -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
)
+3
View File
@@ -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<uint64_t> m_gifCopyCount{0};
std::atomic<uint64_t> m_gsWriteCount{0};
std::atomic<uint64_t> m_vifWriteCount{0};
std::atomic<uint64_t> m_vu1CodeGeneration{0};
// I/O registers
std::unordered_map<uint32_t, uint32_t> m_ioRegisters;
@@ -429,6 +431,7 @@ public:
bool isAddressInRegion(uint32_t address, const CodeRegion &region);
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;
+24 -2
View File
@@ -2,6 +2,7 @@
#define PS2_VU1_H
#include <cstdint>
#include <vector>
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<DecodedInstructionPair> 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);
+15 -4
View File
@@ -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<uint16_t>(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<uint32_t>(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<uint64_t>(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;
}
@@ -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)
File diff suppressed because it is too large Load Diff
+201
View File
@@ -0,0 +1,201 @@
#include "runtime/ps2_vu1.h"
#include "runtime/ps2_memory.h"
#include "ps2_vu1_detail.h"
#include <cstring>
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;
}
}
+157
View File
@@ -0,0 +1,157 @@
#ifndef PS2_VU1_DETAIL_H
#define PS2_VU1_DETAIL_H
#include <cstdint>
// 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<uint8_t>((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<uint8_t>((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<uint8_t>((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
+718
View File
@@ -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 <cmath>
#include <cstring>
#include <limits>
#include <vector>
// ============================================================================
// 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<uint8_t> 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<float>::max() : -std::numeric_limits<float>::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<float>::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<float>::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<float>::max();
return;
}
case 0x7B: // WAITP
return;
case 0x7D: // EATAN / EATANxy / EATANxz placeholder
return;
default:
return;
}
}
default:
return;
}
}
default:
break;
}
}
+458
View File
@@ -0,0 +1,458 @@
#include "runtime/ps2_vu1.h"
#include "ps2_vu1_detail.h"
#include <cmath>
#include <cstring>
// ============================================================================
// 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<uint8_t>((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<float>(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<float>(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<float>(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<float>(iv) / 32768.0f;
}
applyDest(vtDest, result, dest);
return;
case 0x14: // FTOI0
for (int c = 0; c < 4; c++)
{
int32_t iv = static_cast<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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;
}
}
+1
View File
@@ -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
+2
View File
@@ -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();
-157
View File
@@ -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<uint32_t>(dest & 0xFu) << 21) |
(static_cast<uint32_t>(it & 0x1Fu) << 16) |
(static_cast<uint32_t>(is & 0x1Fu) << 11) |
(static_cast<uint32_t>(id & 0x1Fu) << 6) |
(static_cast<uint32_t>(specialOp & 0x7Cu) << 4) |
static_cast<uint32_t>(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<std::vector<uint8_t>> captured;
mem.setGifPacketCallback([&](const uint8_t *data, uint32_t sizeBytes)
{
captured.emplace_back(data, data + sizeBytes);
});
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(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<uint8_t>(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<int32_t>(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<size_t>(1u), "XGKICK should emit one wrapped GIF packet");
if (!captured.empty())
{
t.Equals(captured[0].size(), static_cast<size_t>(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<uint8_t>(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<uint32_t>(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<uint64_t>(0u) << 0) |
(static_cast<uint64_t>(1u) << 16) |
(static_cast<uint64_t>(0u) << 24) |
(static_cast<uint64_t>(0u) << 32) |
(static_cast<uint64_t>(1u) << 48) |
(static_cast<uint64_t>(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<uint8_t>(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<const uint8_t *>(&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<uint8_t>(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;
+559
View File
@@ -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 <cstdint>
#include <cstring>
#include <vector>
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<uint32_t>(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<uint32_t>(opcode) << 24) |
(static_cast<uint32_t>(num) << 16) |
static_cast<uint32_t>(imm);
}
uint64_t makeGifTag(uint16_t nloop, uint8_t flg, uint8_t nreg, bool eop = true)
{
uint64_t tag = static_cast<uint64_t>(nloop & 0x7FFFu);
if (eop)
tag |= (1ull << 15);
tag |= (static_cast<uint64_t>(flg & 0x3u) << 58);
tag |= (static_cast<uint64_t>(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<uint32_t>(dest & 0xFu) << 21) |
(static_cast<uint32_t>(it & 0x1Fu) << 16) |
(static_cast<uint32_t>(is & 0x1Fu) << 11) |
(static_cast<uint32_t>(id & 0x1Fu) << 6) |
(static_cast<uint32_t>(specialOp & 0x7Cu) << 4) |
static_cast<uint32_t>(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<uint32_t>(dest & 0xFu) << 21) |
(static_cast<uint32_t>(it & 0x1Fu) << 16) |
(static_cast<uint32_t>(is & 0x1Fu) << 11) |
(static_cast<uint32_t>(id & 0x1Fu) << 6) |
static_cast<uint32_t>(funct & 0x3Fu);
}
uint32_t makeVuUpper(uint8_t op, uint8_t dest, uint8_t ft, uint8_t fs, uint8_t fd)
{
return (static_cast<uint32_t>(dest & 0xFu) << 21) |
(static_cast<uint32_t>(ft & 0x1Fu) << 16) |
(static_cast<uint32_t>(fs & 0x1Fu) << 11) |
(static_cast<uint32_t>(fd & 0x1Fu) << 6) |
static_cast<uint32_t>(op & 0x3Fu);
}
uint32_t makeVuLq(uint8_t dest, uint8_t targetVf, uint8_t baseVi, int16_t imm)
{
return (static_cast<uint32_t>(dest & 0xFu) << 21) |
(static_cast<uint32_t>(targetVf & 0x1Fu) << 16) |
(static_cast<uint32_t>(baseVi & 0xFu) << 11) |
(static_cast<uint32_t>(imm) & 0x7FFu);
}
uint32_t makeVuSq(uint8_t dest, uint8_t sourceVf, uint8_t baseVi, int16_t imm)
{
return (0x01u << 25) |
(static_cast<uint32_t>(dest & 0xFu) << 21) |
(static_cast<uint32_t>(baseVi & 0xFu) << 16) |
(static_cast<uint32_t>(sourceVf & 0x1Fu) << 11) |
(static_cast<uint32_t>(imm) & 0x7FFu);
}
uint32_t makeVuIaddiu(uint8_t it, uint8_t is, int16_t imm)
{
return (0x08u << 25) |
(static_cast<uint32_t>(it & 0xFu) << 16) |
(static_cast<uint32_t>(is & 0xFu) << 11) |
(static_cast<uint32_t>(imm) & 0x7FFu);
}
uint32_t makeVuBranch(int16_t imm)
{
return (0x20u << 25) | (static_cast<uint32_t>(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<uint8_t>(((ftf & 0x3u) << 2) | (fsf & 0x3u)));
}
uint32_t makeVuSqrt(uint8_t ft, uint8_t ftf)
{
return makeVuLowerSpecial(0x39u, 0u, ft, 0u, static_cast<uint8_t>((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<uint64_t>(lower) | (static_cast<uint64_t>(upper) << 32);
}
void appendU32(std::vector<uint8_t> &bytes, uint32_t value)
{
const uint8_t *src = reinterpret_cast<const uint8_t *>(&value);
bytes.insert(bytes.end(), src, src + sizeof(value));
}
void uploadVu1Mpg(PS2Memory &mem, uint16_t instructionAddress, uint32_t lower, uint32_t upper)
{
std::vector<uint8_t> packet;
appendU32(packet, makeVifCmd(0x4Au, 1u, instructionAddress));
appendU32(packet, lower);
appendU32(packet, upper);
mem.processVIF1Data(packet.data(), static_cast<uint32_t>(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<const uint8_t *>(&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<const uint8_t *>(&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<std::vector<uint8_t>> captured;
mem.setGifPacketCallback([&](const uint8_t *data, uint32_t sizeBytes)
{
captured.emplace_back(data, data + sizeBytes);
});
std::vector<uint8_t> vram(PS2_GS_VRAM_SIZE, 0u);
GS gs;
gs.init(vram.data(), static_cast<uint32_t>(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<uint8_t>(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<int32_t>(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<size_t>(1u), "XGKICK should emit one wrapped GIF packet");
if (!captured.empty())
{
t.Equals(captured[0].size(), static_cast<size_t>(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<uint8_t>(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<uint32_t>(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<uint64_t>(0u) << 0) |
(static_cast<uint64_t>(1u) << 16) |
(static_cast<uint64_t>(0u) << 24) |
(static_cast<uint64_t>(0u) << 32) |
(static_cast<uint64_t>(1u) << 48) |
(static_cast<uint64_t>(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<uint8_t>(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<const uint8_t *>(&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<uint8_t>(0x90u + x * 4u + c))
{
imageOk = false;
break;
}
}
}
t.IsTrue(imageOk, "MSCAL-triggered XGKICK should route PATH1 packet into GS VRAM");
});
});
}