Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions ps2xRuntime/src/lib/vu/ps2_vu1_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,49 @@ void VU1Interpreter::run(uint8_t *vuCode, uint32_t codeSize,
}
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.
// Both halves of a pair are issued concurrently, so both observe the
// pre-pair VF register file and the pre-pair Q, and a VF write
// collision resolves in favour of the upper half for the whole
// register - not just the fields it names. Running the lower half
// first gives it the pre-pair values; the snapshots below give the
// upper half the same guarantee for its own VF operands and for Q,
// and enforce the VF write priority afterwards. No upper
// instruction writes Q, so Q is restored unconditionally and needs
// no priority arbitration. Inter-pair Q and P visibility latency is
// a separate concern and is not modelled here.
const uint32_t lowerWrites = vuLowerVfWriteMask(decoded.lower);
const uint8_t upperWriteReg = vuUpperVfWriteReg(decoded.upper);

uint8_t guarded[3];
uint8_t guardedCount = 0u;
vuAddGuardedReg(guarded, guardedCount, FS(decoded.upper), lowerWrites);
vuAddGuardedReg(guarded, guardedCount, FT(decoded.upper), lowerWrites);
vuAddGuardedReg(guarded, guardedCount, upperWriteReg, lowerWrites);

float prePair[3][4];
float postLower[3][4];
for (uint8_t i = 0u; i < guardedCount; ++i)
std::memcpy(prePair[i], m_state.vf[guarded[i]], sizeof(prePair[i]));
const float prePairQ = m_state.q;

execLower(decoded.lower, vuData, dataSize, gs, memory, decoded.upper);

for (uint8_t i = 0u; i < guardedCount; ++i)
{
std::memcpy(postLower[i], m_state.vf[guarded[i]], sizeof(postLower[i]));
std::memcpy(m_state.vf[guarded[i]], prePair[i], sizeof(prePair[i]));
}
const float postLowerQ = m_state.q;
m_state.q = prePairQ;

execUpper(decoded.upper);
m_state.q = postLowerQ;

for (uint8_t i = 0u; i < guardedCount; ++i)
{
if (guarded[i] != upperWriteReg)
std::memcpy(m_state.vf[guarded[i]], postLower[i], sizeof(postLower[i]));
}
}
else
{
Expand Down
65 changes: 65 additions & 0 deletions ps2xRuntime/src/lib/vu/ps2_vu1_detail.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,44 @@ static inline void vuLowerVfReadWriteMasks(uint32_t lower, uint32_t &readMask, u
case 0x64: // MFP
vuSetRegBit(writeMask, it);
return;
// R-register block. RNEXT and RGET store the random number into
// VF[ft]; RINIT and RXOR take VF[fs]fsf as their only vector
// operand. RGET's instruction page states the rule this table
// exists to serve: "When an Upper instruction in the same cycle
// writes data to the VF[ft] register, the result of this
// instruction is discarded with priority given to the Upper
// instruction, regardless of whether the data is written to the
// same field or not."
case 0x40: // RNEXT
case 0x41: // RGET
vuSetRegBit(writeMask, it);
return;
case 0x42: // RINIT
case 0x43: // RXOR
vuSetRegBit(readMask, is);
return;
// EFU block. Every EFU operation takes VF[is] as its only vector
// operand and writes the P register; see the VU User's Manual EFU
// instruction pages ("ESADD P, VF[fs]" through "EEXP P, VF[fs]fsf").
// The read is reported whether or not the execution switch gives the
// opcode a body today, so implementing one later cannot silently
// reopen the hazard. WAITP (0x7B) has no vector operand and is
// deliberately absent, as are the unallocated slots 0x77 and 0x7F.
case 0x70: // ESADD
case 0x71: // ERSADD
case 0x72: // ELENG
case 0x73: // ERLENG
case 0x74: // EATANxy
case 0x75: // EATANxz
case 0x76: // ESUM
case 0x78: // ESQRT
case 0x79: // ERSQRT
case 0x7A: // ERCPR
case 0x7C: // ESIN
case 0x7D: // EATAN
case 0x7E: // EEXP
vuSetRegBit(readMask, is);
return;
default:
return;
}
Expand All @@ -140,6 +178,33 @@ static inline void vuLowerVfReadWriteMasks(uint32_t lower, uint32_t &readMask, u
}
}

static inline uint32_t vuLowerVfWriteMask(uint32_t lower)
{
uint32_t reads = 0u;
uint32_t writes = 0u;
vuLowerVfReadWriteMasks(lower, reads, writes);
return writes;
}

// Collects the VF registers a reordered pair has to protect: the upper half's
// source registers, whose pre-pair values it must still observe, and the
// upper half's destination register, whose lower-half write hardware discards.
// A register is collected at most once, and that is required rather than tidy:
// the caller captures and rolls back each entry in turn, so a second entry for
// an already-rolled-back register would capture the pre-pair value and replay
// it over the lower half's write.
static inline void vuAddGuardedReg(uint8_t *regs, uint8_t &count, uint8_t reg, uint32_t lowerWrites)
{
if (reg == 0u || ((lowerWrites >> reg) & 1u) == 0u)
return;
for (uint8_t i = 0u; i < count; ++i)
{
if (regs[i] == reg)
return;
}
regs[count++] = reg;
}

static inline bool vuLowerShouldRunBeforeUpper(uint32_t upper, uint32_t lower)
{
const uint8_t upperWrite = vuUpperVfWriteReg(upper);
Expand Down
Loading