mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
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:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(>e, i);
|
||||
for (int i = 0; i < 32; i++) cpu->gte_ctrl[i] = gte_cfc2(>e, 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 */
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user