timing: faithful GTE (COP2) per-command completion-stall

Models the GTE command latency + stall-on-COP2-access, the next Stage-2 cycle
component. Mirrors the mult/div completion-stall mechanism:

- cpu->gte_ts_done (new CPUState field, appended): a GTE command sets it to
  psx_cycle_count + (cost-1); back-to-back commands serialize (psx_gte_set
  stalls to the prior deadline first). Any COP2 register access
  (MFC2/CFC2/MTC2/CTC2/LWC2/SWC2) calls psx_gte_stall, advancing guest cycles
  to the deadline if the op is still in flight.
- Cost table (psx_cycles.c, psx_gte_cmd_latency): per-op cost-1, transcribed +
  verified from beetle-psx gte.cpp op returns (RTPS15 NCLIP8 MVMVA8 NCDS19
  NCDT44 RTPT23 NCCT39 ... AVSZ4=5 not 6). cost-1 because the COP2 instruction's
  own +1 base is charged separately (Beetle GTE_Instruction returns ret-1).
- Set armed in the shared gte_execute() (one site for both backends); stall
  emitted before every COP2 reg access in both emitters (code_generator,
  strict_translator) and the dirty interp. The compiled "+1 at top" vs interp
  "base after exec_one" offset cancels (same as muldiv).

Validated EXACT on ruler #2 (new gte_rtps + gte_nclip loops): native +15/+8 ==
Beetle +15/+8. All other components unchanged (no regression); Tomba2 boots to
FMV. Requires regen (emitters changed); runtime+BIOS+game regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-27 12:41:22 -07:00
parent 184d8fee3c
commit ec1fd76b56
8 changed files with 169 additions and 22 deletions
+22 -11
View File
@@ -1100,22 +1100,27 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
uint32_t cop_op = (instr >> 21) & 0x1F;
uint32_t rt = get_rt(instr);
uint32_t rd = get_rd(instr);
// Faithful GTE: any COP2 register access stalls to the pending
// command completion deadline (gte_execute arms it). Emitted
// before the access; cost-free unless an op is still in flight.
const char* gte_stall =
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
if (cop_op == 0x00) { // MFC2 - move from COP2 data
if ((rd >= 8 && rd <= 11) || rd == 15 || rd == 28 || rd == 29 || rd == 31) {
code = fmt::format("{} = gte_read_data(cpu, {}); /* mfc2 */", reg_name(rt), rd);
code = gte_stall + fmt::format("{} = gte_read_data(cpu, {}); /* mfc2 */", reg_name(rt), rd);
} else {
code = fmt::format("{} = cpu->gte_data[{}]; /* mfc2 */", reg_name(rt), rd);
code = gte_stall + fmt::format("{} = cpu->gte_data[{}]; /* mfc2 */", reg_name(rt), rd);
}
} else if (cop_op == 0x02) { // CFC2 - move from COP2 control
code = fmt::format("{} = cpu->gte_ctrl[{}]; /* cfc2 */", reg_name(rt), rd);
code = gte_stall + fmt::format("{} = cpu->gte_ctrl[{}]; /* cfc2 */", reg_name(rt), rd);
} else if (cop_op == 0x04) { // MTC2 - move to COP2 data
if (rd == 7 || (rd >= 8 && rd <= 11) || rd == 14 || rd == 15 || rd == 28 || rd == 30) {
code = fmt::format("gte_write_data(cpu, {}, {}); /* mtc2 */", rd, reg_name(rt));
code = gte_stall + fmt::format("gte_write_data(cpu, {}, {}); /* mtc2 */", rd, reg_name(rt));
} else {
code = fmt::format("cpu->gte_data[{}] = {}; /* mtc2 */", rd, reg_name(rt));
code = gte_stall + fmt::format("cpu->gte_data[{}] = {}; /* mtc2 */", rd, reg_name(rt));
}
} else if (cop_op == 0x06) { // CTC2 - move to COP2 control
code = fmt::format("cpu->gte_ctrl[{}] = {}; /* ctc2 */", rd, reg_name(rt));
code = gte_stall + fmt::format("cpu->gte_ctrl[{}] = {}; /* ctc2 */", rd, reg_name(rt));
} else if ((cop_op & 0x10) != 0) { // GTE command (bit 25 set)
uint32_t gte_cmd = instr & 0x1FFFFFF;
// Route ALL GTE commands through gte_execute() for correct behavior
@@ -1145,18 +1150,21 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
// Faithful GTE: COP2 reg write stalls to the command deadline.
const char* gte_stall =
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
bool special = (rt == 7 || (rt >= 8 && rt <= 11) || rt == 14 || rt == 15 || rt == 28 || rt == 30);
if (special) {
std::string addr = (offset == 0)
? reg_name(rs)
: fmt::format("{} + {}", reg_name(rs), offset);
code = fmt::format("gte_write_data(cpu, {}, cpu->read_word({})); /* lwc2 gte[{}] */",
code = gte_stall + fmt::format("gte_write_data(cpu, {}, cpu->read_word({})); /* lwc2 gte[{}] */",
rt, addr, rt);
} else if (offset == 0) {
code = fmt::format("cpu->gte_data[{}] = cpu->read_word({}); /* lwc2 gte[{}], ({}) */",
code = gte_stall + fmt::format("cpu->gte_data[{}] = cpu->read_word({}); /* lwc2 gte[{}], ({}) */",
rt, reg_name(rs), rt, reg_name(rs));
} else {
code = fmt::format("cpu->gte_data[{}] = cpu->read_word({} + {}); /* lwc2 gte[{}], {}({}) */",
code = gte_stall + fmt::format("cpu->gte_data[{}] = cpu->read_word({} + {}); /* lwc2 gte[{}], {}({}) */",
rt, reg_name(rs), offset, rt, offset, reg_name(rs));
}
}
@@ -1166,14 +1174,17 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
uint32_t rs = get_rs(instr);
uint32_t rt = get_rt(instr);
int16_t offset = get_imm16(instr);
// Faithful GTE: COP2 reg read stalls to the command deadline.
const char* gte_stall =
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
std::string value = ((rt >= 8 && rt <= 11) || rt == 15 || rt == 28 || rt == 29 || rt == 31)
? fmt::format("gte_read_data(cpu, {})", rt)
: fmt::format("cpu->gte_data[{}]", rt);
if (offset == 0) {
code = fmt::format("cpu->write_word({}, {}); /* swc2 gte[{}], ({}) */",
code = gte_stall + fmt::format("cpu->write_word({}, {}); /* swc2 gte[{}], ({}) */",
reg_name(rs), value, rt, reg_name(rs));
} else {
code = fmt::format("cpu->write_word({} + {}, {}); /* swc2 gte[{}], {}({}) */",
code = gte_stall + fmt::format("cpu->write_word({} + {}, {}); /* swc2 gte[{}], {}({}) */",
reg_name(rs), offset, value, rt, offset, reg_name(rs));
}
}
+22 -11
View File
@@ -1242,15 +1242,20 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
const uint8_t rt = (d.raw >> 16) & 0x1F;
const uint8_t rd = (d.raw >> 11) & 0x1F;
// Faithful GTE: any COP2 register access stalls to the pending command
// completion deadline (gte_execute arms it). Cost-free unless in flight.
const std::string gte_stall =
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
if (cop_op == 0x00) { // MFC2 — move from COP2 data register
r.supported = true;
// Computed/sign-extended data registers must route through the
// helper instead of reading the backing array directly.
if ((rd >= 8 && rd <= 11) || rd == 15 || rd == 28 || rd == 29 || rd == 31) {
r.c_code = emit_gpr_write(rt,
r.c_code = gte_stall + emit_gpr_write(rt,
fmt::format("gte_read_data(cpu, {})", static_cast<int>(rd)));
} else {
r.c_code = emit_gpr_write(rt,
r.c_code = gte_stall + emit_gpr_write(rt,
fmt::format("cpu->gte_data[{}]", static_cast<int>(rd)));
}
r.comment = fmt::format("mfc2 {}, gte_data[{}]", gpr_name(rt), rd);
@@ -1258,7 +1263,7 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
}
if (cop_op == 0x02) { // CFC2 — move from COP2 control register
r.supported = true;
r.c_code = emit_gpr_write(rt,
r.c_code = gte_stall + emit_gpr_write(rt,
fmt::format("cpu->gte_ctrl[{}]", static_cast<int>(rd)));
r.comment = fmt::format("cfc2 {}, gte_ctrl[{}]", gpr_name(rt), rd);
return r;
@@ -1268,11 +1273,11 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
// Route registers with side effects or canonical masking through
// gte_write_data(): OTZ/IR/SXY2/SXYP/IRGB/LZCS.
if (rd == 7 || (rd >= 8 && rd <= 11) || rd == 14 || rd == 15 || rd == 28 || rd == 30) {
r.c_code = fmt::format(
r.c_code = gte_stall + fmt::format(
"gte_write_data(cpu, {}, cpu->gpr[{}]);",
static_cast<int>(rd), static_cast<int>(rt));
} else {
r.c_code = fmt::format(
r.c_code = gte_stall + fmt::format(
"cpu->gte_data[{}] = cpu->gpr[{}];",
static_cast<int>(rd), static_cast<int>(rt));
}
@@ -1281,7 +1286,7 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
}
if (cop_op == 0x06) { // CTC2 — move to COP2 control register
r.supported = true;
r.c_code = fmt::format(
r.c_code = gte_stall + fmt::format(
"cpu->gte_ctrl[{}] = cpu->gpr[{}];",
static_cast<int>(rd), static_cast<int>(rt));
r.comment = fmt::format("ctc2 {}, gte_ctrl[{}]", gpr_name(rt), rd);
@@ -1307,20 +1312,23 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
const uint8_t rt = (d.raw >> 16) & 0x1F;
const int16_t offset = static_cast<int16_t>(d.raw & 0xFFFF);
r.supported = true;
// Faithful GTE: COP2 reg write stalls to the command deadline.
const std::string gte_stall =
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
bool special = (rt == 7 || (rt >= 8 && rt <= 11) || rt == 14 || rt == 15 || rt == 28 || rt == 30);
if (special) {
std::string addr = offset == 0
? fmt::format("cpu->gpr[{}]", static_cast<int>(rs))
: fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), static_cast<int>(offset));
r.c_code = fmt::format(
r.c_code = gte_stall + fmt::format(
"gte_write_data(cpu, {}, cpu->read_word({}));",
static_cast<int>(rt), addr);
} else if (offset == 0) {
r.c_code = fmt::format(
r.c_code = gte_stall + fmt::format(
"cpu->gte_data[{}] = cpu->read_word(cpu->gpr[{}]);",
static_cast<int>(rt), static_cast<int>(rs));
} else {
r.c_code = fmt::format(
r.c_code = gte_stall + fmt::format(
"cpu->gte_data[{}] = cpu->read_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})));",
static_cast<int>(rt), static_cast<int>(rs), static_cast<int>(offset));
}
@@ -1335,16 +1343,19 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
const uint8_t rt = (d.raw >> 16) & 0x1F;
const int16_t offset = static_cast<int16_t>(d.raw & 0xFFFF);
r.supported = true;
// Faithful GTE: COP2 reg read stalls to the command deadline.
const std::string gte_stall =
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_stall(cpu);\n#endif\n ";
std::string value = ((rt >= 8 && rt <= 11) || rt == 15 || rt == 28 || rt == 29 || rt == 31)
? fmt::format("gte_read_data(cpu, {})", static_cast<int>(rt))
: fmt::format("cpu->gte_data[{}]", static_cast<int>(rt));
if (offset == 0) {
r.c_code = fmt::format(
r.c_code = gte_stall + fmt::format(
"g_debug_last_store_pc = 0x{:08X}u; "
"cpu->write_word(cpu->gpr[{}], {});",
d.address, static_cast<int>(rs), value);
} else {
r.c_code = fmt::format(
r.c_code = gte_stall + fmt::format(
"g_debug_last_store_pc = 0x{:08X}u; "
"cpu->write_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})), {});",
d.address, static_cast<int>(rs), static_cast<int>(offset), value);
+19
View File
@@ -61,6 +61,15 @@ typedef struct CPUState {
* cycles) until this deadline — faithful R3000A behavior (Beetle
* muldiv_ts_done). Appended at END so prior field offsets are unchanged. */
uint64_t muldiv_ts_done;
/* GTE (COP2) command completion deadline (absolute guest cycle). A GTE
* command sets this to psx_cycle_count()+(cost-1) (cost from gte.cpp per-op
* returns; -1 because the COP2 instruction's own +1 base is charged
* separately). Back-to-back commands serialize (set stalls to the prior
* deadline first); any COP2 register access (MFC2/CFC2/MTC2/CTC2/LWC2/SWC2)
* stalls until it — faithful R3000A behavior (Beetle gte_ts_done).
* Appended at END so prior field offsets are unchanged. */
uint64_t gte_ts_done;
} CPUState;
/* Trap trampolines — defined in runtime/src/traps.c */
@@ -82,6 +91,16 @@ extern void psx_muldiv_stall(CPUState* cpu);
extern uint32_t psx_mult_latency_s(uint32_t rs); /* MULT (signed) */
extern uint32_t psx_mult_latency_u(uint32_t rs); /* MULTU (unsigned) */
/* GTE (COP2) per-command completion-stall timing (psx_cycles.c). gte_execute()
* calls psx_gte_set with the command's added latency (cost-1, from
* psx_gte_cmd_latency); it serializes back-to-back ops by stalling to the prior
* deadline first. Every COP2 register access calls psx_gte_stall, which advances
* guest cycles to the deadline if the op is still in flight. Faithful only with
* per-instruction cycle charging. */
extern void psx_gte_set(CPUState* cpu, uint32_t latency);
extern void psx_gte_stall(CPUState* cpu);
extern uint32_t psx_gte_cmd_latency(uint32_t cmd); /* cost-1 for the 6-bit op */
extern void psx_unaligned_access(CPUState* cpu, uint32_t addr, uint32_t pc);
extern void psx_break(CPUState* cpu, uint32_t code, uint32_t pc);
/* Fail-closed native entry guard: a function's CPS entry-switch calls this when
+20
View File
@@ -1335,21 +1335,35 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
}
case 0x12: { /* COP2 / GTE */
uint32_t cop_op = rs;
/* Faithful GTE: any COP2 register access stalls to the pending command
* completion deadline (gte_execute armed it via psx_gte_set). */
if (cop_op == 0x00) { /* MFC2 */
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_gte_stall(cpu);
#endif
cpu->gpr[rt] = gte_read_data(cpu, (uint8_t)rd);
cpu->gpr[0] = 0;
return 0;
}
if (cop_op == 0x02) { /* CFC2 */
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_gte_stall(cpu);
#endif
cpu->gpr[rt] = gte_read_ctrl(cpu, (uint8_t)rd);
cpu->gpr[0] = 0;
return 0;
}
if (cop_op == 0x04) { /* MTC2 */
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_gte_stall(cpu);
#endif
gte_write_data(cpu, (uint8_t)rd, cpu->gpr[rt]);
return 0;
}
if (cop_op == 0x06) { /* CTC2 */
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_gte_stall(cpu);
#endif
gte_write_ctrl(cpu, (uint8_t)rd, cpu->gpr[rt]);
return 0;
}
@@ -1436,11 +1450,17 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
}
case 0x32: { /* LWC2 */
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_gte_stall(cpu); /* COP2 reg write stalls to GTE completion */
#endif
gte_write_data(cpu, (uint8_t)rt, cpu->read_word(addr));
return 0;
}
case 0x3A: { /* SWC2 */
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_gte_stall(cpu); /* COP2 reg read stalls to GTE completion */
#endif
cpu->write_word(addr, gte_read_data(cpu, (uint8_t)rt));
return 0;
}
+8
View File
@@ -883,6 +883,14 @@ extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) {
for (int i = 0; i < 32; i++) cpu->gte_data[i] = gte_mfc2(&gte, i);
for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(&gte, i);
#ifdef PSX_ENABLE_BLOCK_CYCLES
/* Faithful GTE command completion-stall: arm the per-command deadline
* (serializing back-to-back ops). Any later COP2 register access stalls to
* it. Single shared site for BOTH backends (compiled + dirty interp both
* route GTE commands through gte_execute). */
psx_gte_set(cpu, psx_gte_cmd_latency(cmd));
#endif
}
/* C-callable wrappers for GTE register transfers */
+60
View File
@@ -112,3 +112,63 @@ void psx_muldiv_stall(CPUState* cpu) {
psx_advance_cycles((uint32_t)(cpu->muldiv_ts_done - psx_cycle_count));
}
}
/* ---- GTE (COP2) per-command completion-stall timing ----
*
* Faithful R3000A/GTE model (Beetle cpu.cpp:1410-1412 + gte.cpp GTE_Instruction
* return(ret-1)). Each GTE command takes `cost` cycles (gte.cpp per-op returns,
* verified from source); the COP2 instruction's own +1 base is charged
* separately by per-instruction charging, so the *added* deadline latency is
* cost-1. A later COP2 register access (MFC2/CFC2/MTC2/CTC2/LWC2/SWC2) stalls
* until the deadline. Back-to-back commands serialize: psx_gte_set stalls to the
* prior deadline before arming the next (Beetle stalls timestamp to gte_ts_done
* at the command site before computing the new gte_ts_done).
*
* Cost table = (cost-1), indexed by the 6-bit GTE command (instr & 0x3F).
* cost values transcribed + verified from beetle-psx/mednafen/psx/gte.cpp op
* returns: RTPS15 RTPT23 MVMVA8 SQR5 OP6 AVSZ3/4=5 NCLIP8 NCDS19 NCDT44 NCCS17
* NCCT39 NCS14 NCT30 CC11 CDP13 DPCS8 DPCT17 DCPL8 INTPL8 GPF5 GPL5. Unknown/
* undefined commands = 1 cycle (Beetle default ret=1) -> 0 added. */
static const uint8_t PSX_GTE_LAT_M1[64] = {
[0x00] = 14, [0x01] = 14, /* RTPS */
[0x06] = 7, /* NCLIP */
[0x0C] = 5, /* OP */
[0x10] = 7, /* DPCS */
[0x11] = 7, /* INTPL */
[0x12] = 7, /* MVMVA */
[0x13] = 18, /* NCDS */
[0x14] = 12, /* CDP */
[0x16] = 43, /* NCDT */
[0x1A] = 7, /* DCPL (alt of 0x29) */
[0x1B] = 16, /* NCCS */
[0x1C] = 10, /* CC */
[0x1E] = 13, /* NCS */
[0x20] = 29, /* NCT */
[0x28] = 4, /* SQR */
[0x29] = 7, /* DCPL */
[0x2A] = 16, /* DPCT */
[0x2D] = 4, /* AVSZ3 */
[0x2E] = 4, /* AVSZ4 */
[0x30] = 22, /* RTPT */
[0x3D] = 4, /* GPF */
[0x3E] = 4, /* GPL */
[0x3F] = 38, /* NCCT */
};
uint32_t psx_gte_cmd_latency(uint32_t cmd) {
return (uint32_t)PSX_GTE_LAT_M1[cmd & 0x3Fu];
}
void psx_gte_set(CPUState* cpu, uint32_t latency) {
/* Back-to-back GTE ops serialize: finish the prior op first. */
if (cpu->gte_ts_done > psx_cycle_count) {
psx_advance_cycles((uint32_t)(cpu->gte_ts_done - psx_cycle_count));
}
cpu->gte_ts_done = psx_cycle_count + (uint64_t)latency;
}
void psx_gte_stall(CPUState* cpu) {
if (cpu->gte_ts_done > psx_cycle_count) {
psx_advance_cycles((uint32_t)(cpu->gte_ts_done - psx_cycle_count));
}
}
+10
View File
@@ -25,6 +25,8 @@ Loops (each adds ONLY its op(s) over the shared loop overhead):
- `div` — + divu, mflo (div latency + stall-on-read, worst case)
- `div_spaced` — + divu, 2 filler addu, mflo (stall partly absorbed)
- `mult` — + multu, mflo (mult latency)
- `gte_rtps` — + RTPS cmd, mfc2 (GTE per-command stall; cost 15)
- `gte_nclip` — + NCLIP cmd, mfc2 (GTE per-command stall; cost 8)
**Isolation by baseline subtraction:** (component_per_iter − baseline_per_iter)
= the component's cost; loop overhead AND instruction fetch (both fully
@@ -91,6 +93,14 @@ Per-iteration cycle delta, and component cost (minus baseline=3):
| div | 41 | +38 | divu+mflo base 2 + ~36 div stall |
| div_spaced | 41 | +38 | divu+2addu+mflo — fillers ABSORBED by stall |
| mult | 18 | +15 | multu+mflo base 2 + ~13 mult stall |
| gte_rtps | 18 | +15 | RTPS cmd + mfc2 stall (gte.cpp RTPS=15) |
| gte_nclip | 11 | +8 | NCLIP cmd + mfc2 stall (gte.cpp NCLIP=8) |
**GTE per-command stall — VALIDATED EXACT 2026-06-27:** native (compiled path)
gte_rtps +15 == Beetle +15, gte_nclip +8 == Beetle +8. Modeled via
`psx_gte_set`/`psx_gte_stall` (cpu->gte_ts_done), cost table in `psx_cycles.c`
(verified from gte.cpp op returns). All other components unchanged (load2 +10 vs
Beetle +11 = the remaining ReadFudge gap).
Key targets for the native cost model: **div stall ~36, mult stall ~13** (native
charges 0). Load ~5/6 (native memory.c flat +6 — close). **`div_spaced`==`div`
+8
View File
@@ -64,6 +64,8 @@ def mflo(rd): return R(0x12, 0, 0, rd)
def mfhi(rd): return R(0x10, 0, 0, rd)
def lw(rt, off, base): return I(0x23, base, rt, off)
def sw(rt, off, base): return I(0x2B, base, rt, off)
def cop2cmd(cmd): return (0x12 << 26) | (1 << 25) | (cmd & 0x1FFFFFF) # GTE command
def mfc2(rt, rd): return (0x12 << 26) | (0x00 << 21) | (_r(rt) << 16) | ((rd & 0x1F) << 11) # MFC2 rt,gte[rd]
def bne(rs, rt, off): return I(0x05, rs, rt, off) # off in words, signed
def beq(rs, rt, off): return I(0x04, rs, rt, off)
def jr(rs): return R(0x08, rs)
@@ -160,6 +162,12 @@ def build():
mflo("$t4")])
# mult: multu + mflo (isolates mult latency)
inner_loop("mult", [multu("$t2", "$t3"), mflo("$t4")])
# gte_rtps: GTE RTPS command (0x01) + mfc2 result read. Isolates the GTE
# per-command completion-stall: the COP2 read stalls to the command deadline.
# Cost is operand-independent (gte.cpp RTPS returns 15), so delta == 15.
inner_loop("gte_rtps", [cop2cmd(0x01), mfc2("$t4", 14)])
# gte_nclip: NCLIP command (0x06, cost 8) + mfc2 MAC0 (reg 24) read.
inner_loop("gte_nclip", [cop2cmd(0x06), mfc2("$t4", 24)])
# loop outer forever
o = a.labels["outer"]