pgxp: emit hook macros + dual build targets + flavor-namespaced caches (phase 2)

Emitter side of the value-propagation engine (ENHANCEMENTS.md G1.10):

- recompiler/src/pgxp_hook_emitter.{h,cpp} (new, in the codegen-hash
  surface): ONE hook grammar shared by BOTH emitters, wrapping loads/
  stores/COP2 transfers and precision-carrying ALU ops (moves, add/sub,
  or-merge, shifts, LUI, HI/LO, muldiv) with PGXP_*() macros. Clobberable
  sources are captured into locals before the statement. Precision-
  destroying ops (AND/XOR/SLT/...) are deliberately unhooked - validate-
  on-read drops their stale shadows for free.
- code_generator.cpp: central append in translate_instruction; LWC2/SWC2
  emissions capture the RAW loaded/stored word (a masked GTE register
  write must validate against the word as loaded).
- strict_translator.cpp: translate() wraps translate_impl()'s c_code with
  the shared grammar; deferred load-delay variants hook the psx_ldd temp.
- BIOS regenerated, fingerprints verified CHANGED (SCPH1001 + OpenBIOS;
  42.6k/12.2k macro sites). Base runtime + 33/33 tests green against it.
- Gating: macros expand only under -DPSX_PGXP=1. runtime.cmake grows a
  PGXP option on psxrecomp_add_runtime_target (defines PSX_PGXP=1 +
  PSX_OVERLAY_FLAVOR=2, _pgxp exe suffix) and PSX_PGXP_VARIANT to build
  the sibling target from the same generated tree (framework runtime +
  game wrapper). No ABI version bump needed: the appended PGXPHooks
  table is NULL-tolerant and the flavor half of the ABI tag rejects any
  host/DLL flavor mix.
- Shard caches: flavor folded into the cache tag (cg<V>_<hash>_gc<hash>
  _f<flavor>) in overlay_loader.c + compile_overlays.py so base and pgxp
  caches never share a directory; compile_overlays.py passes
  -DPSX_PGXP=1 when flavor bit 2 is set; overlay preamble forwards
  psx_pgxp_* through the table. PSX_OVERLAY_CODEGEN_VER 9 -> 10.
- Env overrides PSX_GEOMETRY_CORRECTION / PSX_PERSPECTIVE_TEXTURING /
  PSX_PGXP_CPU_MODE (validation path: free-running headless boots arm
  from the first vertex); census grew produced/swc2_stores and the pgxp
  verb reports suppress/active gate state.

Validated: PSXRecomp_pgxp.exe boots the recompiled BIOS with hooks armed
(engine active, per-vertex lookups flowing; the BIOS shell has no RTPS
content so produced=0 there is correct - coverage measurement moves to a
3D title in phase 3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-08-15 19:03:36 -07:00
parent 2ba74f3b4b
commit 38bdeeaade
17 changed files with 941 additions and 587 deletions
+539 -534
View File
File diff suppressed because it is too large Load Diff
+22 -14
View File
@@ -1,4 +1,5 @@
#include "code_generator.h"
#include "pgxp_hook_emitter.h"
#include "control_flow.h"
#include "gte_register_classification.h"
#include "../src/bios_address_model.h"
@@ -1572,12 +1573,19 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
? reg_name(rs)
: fmt::format("{} + {}", reg_name(rs), offset);
bool special = PSXRecompGTERegisters::data_write_needs_helper(static_cast<uint8_t>(rt));
/* The raw loaded word is captured for the PGXP hook: the
* register may hold a MASKED value (write helper), and
* the shadow must validate against the word as loaded. */
if (special) {
code = gte_stall + fmt::format("gte_write_data(cpu, {}, psx_cyc_lwc2_read(cpu, {})); /* lwc2 gte[{}] */",
rt, addr, rt);
code = gte_stall + fmt::format(
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); "
"gte_write_data(cpu, {}, _pgxv); PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }} /* lwc2 gte[{}] */",
addr, rt, instr, rt);
} else {
code = gte_stall + fmt::format("cpu->gte_data[{}] = psx_cyc_lwc2_read(cpu, {}); /* lwc2 gte[{}], ({}) */",
rt, addr, rt, addr);
code = gte_stall + fmt::format(
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); "
"cpu->gte_data[{}] = _pgxv; PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }} /* lwc2 gte[{}], ({}) */",
addr, rt, instr, rt, addr);
}
}
break;
@@ -1593,16 +1601,15 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
? fmt::format("gte_read_data(cpu, {})", rt)
: fmt::format("cpu->gte_data[{}]", rt);
std::string swc2_store_pc = fmt::format("g_debug_last_store_pc = 0x{:08X}u; ", addr);
if (offset == 0) {
code = gte_stall + swc2_store_pc + fmt::format(
"psx_store_cycle_barrier(); cpu->write_word({}, {}); gte_precision_store_word({}, {}); /* swc2 gte[{}], ({}) */",
reg_name(rs), value, reg_name(rs), rt, rt, reg_name(rs));
} else {
code = gte_stall + swc2_store_pc + fmt::format(
"psx_store_cycle_barrier(); cpu->write_word({} + {}, {}); gte_precision_store_word({} + {}, {}); /* swc2 gte[{}], {}({}) */",
reg_name(rs), offset, value, reg_name(rs), offset, rt,
rt, offset, reg_name(rs));
}
std::string swc2_addr = (offset == 0)
? reg_name(rs)
: fmt::format("{} + {}", reg_name(rs), offset);
code = gte_stall + swc2_store_pc + fmt::format(
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = {}; "
"psx_store_cycle_barrier(); cpu->write_word(_pgxa, _pgxv); "
"gte_precision_store_word(_pgxa, {}); "
"PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }} /* swc2 gte[{}], {}({}) */",
swc2_addr, value, rt, instr, rt, offset, reg_name(rs));
}
break;
default:
@@ -1610,6 +1617,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
}
}
PSXRecomp::append_pgxp_hooks(instr, code);
return config_.indent + code + comment;
}
+134
View File
@@ -0,0 +1,134 @@
#include "pgxp_hook_emitter.h"
#include <fmt/format.h>
namespace PSXRecomp {
namespace {
/* Same register naming both emitters use ("cpu->gpr[n]"); kept local so this
* TU links into psxrecomp-bios without pulling the game CodeGenerator. */
inline std::string reg_name(uint32_t n) { return fmt::format("cpu->gpr[{}]", n); }
inline uint32_t get_rs(uint32_t i) { return (i >> 21) & 0x1F; }
inline uint32_t get_rt(uint32_t i) { return (i >> 16) & 0x1F; }
inline uint32_t get_rd(uint32_t i) { return (i >> 11) & 0x1F; }
inline int16_t get_imm16(uint32_t i) { return (int16_t)(i & 0xFFFF); }
}
/* PGXP dataflow-shadowing hook emission (ENHANCEMENTS.md G1.10; pgxp_hooks.h).
*
* Wraps the translated statement with a PGXP_*() macro carrying the raw
* instruction word plus the operand values the runtime engine needs. Source
* operands that the statement may clobber (a load/ALU destination aliasing a
* source, a load overwriting its own base register) are captured into locals
* BEFORE the statement; everything else is read after it. In the base build
* the macros preprocess to ((void)0) and the optimizer erases the dead
* capture locals, so base objects are unchanged; only a -DPSX_PGXP=1 TU pays.
*
* Deliberately unhooked: AND/XOR/NOR/SLT-family and the exotic immediates —
* they only ever DESTROY precision, and the engine's validate-on-read drops
* their stale shadows without help. The configured widescreen special sites
* return early above translate_instruction's main dispatch and are likewise
* unhooked (they are cull compares, not vertex moves; validation covers). */
void append_pgxp_hooks(uint32_t instr, std::string& code) {
const uint32_t opcode = (instr >> 26) & 0x3F;
const uint32_t rs = get_rs(instr);
const uint32_t rt = get_rt(instr);
const uint32_t rd = get_rd(instr);
const int16_t offset = get_imm16(instr);
const auto addr_expr = [&]() {
return (offset == 0) ? reg_name(rs)
: fmt::format("{} + {}", reg_name(rs), offset);
};
switch (opcode) {
case 0x00: { /* SPECIAL */
const uint32_t funct = instr & 0x3F;
switch (funct) {
case 0x00: case 0x02: case 0x03: /* SLL / SRL / SRA */
if (rd == 0 || instr == 0) return; /* skip plain nop */
code = fmt::format(
"{{ uint32_t _pgx1 = {}; {} PGXP_ALU(0x{:08X}u, {}, _pgx1, {}u); }}",
reg_name(rt), code, instr, reg_name(rd), (instr >> 6) & 31u);
return;
case 0x04: case 0x06: case 0x07: /* SLLV / SRLV / SRAV */
if (rd == 0) return;
code = fmt::format(
"{{ uint32_t _pgx1 = {}; uint32_t _pgx2 = {}; {} "
"PGXP_ALU(0x{:08X}u, {}, _pgx1, _pgx2); }}",
reg_name(rt), reg_name(rs), code, instr, reg_name(rd));
return;
case 0x10: case 0x12: /* MFHI / MFLO */
if (rd == 0) return;
code = fmt::format("{} PGXP_ALU(0x{:08X}u, {}, {}, 0u);",
code, instr, reg_name(rd),
funct == 0x10 ? "cpu->hi" : "cpu->lo");
return;
case 0x11: case 0x13: /* MTHI / MTLO */
code = fmt::format("{} PGXP_ALU(0x{:08X}u, {}, {}, 0u);",
code, instr,
funct == 0x11 ? "cpu->hi" : "cpu->lo",
funct == 0x11 ? "cpu->hi" : "cpu->lo");
return;
case 0x18: case 0x19: case 0x1A: case 0x1B: /* MULT(U)/DIV(U) */
code = fmt::format(
"{} PGXP_MULDIV(0x{:08X}u, cpu->hi, cpu->lo, {}, {});",
code, instr, reg_name(rs), reg_name(rt));
return;
case 0x20: case 0x21: case 0x22: case 0x23: /* ADD(U)/SUB(U) */
case 0x25: /* OR */
if (rd == 0) return;
code = fmt::format(
"{{ uint32_t _pgx1 = {}; uint32_t _pgx2 = {}; {} "
"PGXP_ALU(0x{:08X}u, {}, _pgx1, _pgx2); }}",
reg_name(rs), reg_name(rt), code, instr, reg_name(rd));
return;
default:
return;
}
}
case 0x08: case 0x09: /* ADDI / ADDIU */
if (rt == 0) return;
code = fmt::format(
"{{ uint32_t _pgx1 = {}; {} PGXP_ALU(0x{:08X}u, {}, _pgx1, 0x{:08X}u); }}",
reg_name(rs), code, instr, reg_name(rt),
(uint32_t)(int32_t)offset);
return;
case 0x0D: /* ORI */
if (rt == 0) return;
code = fmt::format(
"{{ uint32_t _pgx1 = {}; {} PGXP_ALU(0x{:08X}u, {}, _pgx1, 0x{:04X}u); }}",
reg_name(rs), code, instr, reg_name(rt),
(uint32_t)(uint16_t)offset);
return;
case 0x0F: /* LUI */
if (rt == 0) return;
code = fmt::format("{} PGXP_ALU(0x{:08X}u, {}, 0u, 0u);",
code, instr, reg_name(rt));
return;
case 0x12: { /* COP2 register transfers */
const uint32_t cop_op = (instr >> 21) & 0x1F;
if ((cop_op == 0x00 && rt != 0) || cop_op == 0x04) /* MFC2 / MTC2 */
code = fmt::format("{} PGXP_COP2(0x{:08X}u, {}, 0u);",
code, instr, reg_name(rt));
return;
}
case 0x20: case 0x21: case 0x22: case 0x23:
case 0x24: case 0x25: case 0x26: /* loads */
if (rt == 0) return;
code = fmt::format(
"{{ uint32_t _pgxa = {}; {} PGXP_LOAD(0x{:08X}u, _pgxa, {}); }}",
addr_expr(), code, instr, reg_name(rt));
return;
case 0x28: case 0x29: case 0x2A: case 0x2B:
case 0x2E: /* stores */
code = fmt::format(
"{{ uint32_t _pgxa = {}; {} PGXP_STORE(0x{:08X}u, _pgxa, {}); }}",
addr_expr(), code, instr, reg_name(rt));
return;
default:
return;
}
}
} // namespace PSXRecomp
+31
View File
@@ -0,0 +1,31 @@
#ifndef PGXP_HOOK_EMITTER_H
#define PGXP_HOOK_EMITTER_H
#include <cstdint>
#include <string>
namespace PSXRecomp {
/* PGXP dataflow-shadowing hook emission (ENHANCEMENTS.md G1.10; runtime
* pgxp_hooks.h). Shared by BOTH emitters — CodeGenerator (game) and
* StrictTranslator (BIOS) — so the hook grammar can never drift between them.
*
* Wraps the translated statement `code` with a PGXP_*() macro invocation for
* the instruction classes that move sub-pixel projection provenance. Source
* operands the statement may clobber are captured into locals BEFORE it;
* everything else is read after it. In the base build the macros preprocess
* to ((void)0) and the optimizer erases the dead captures, so base objects
* are unchanged; only a -DPSX_PGXP=1 TU pays.
*
* NOT applied to LWC2/SWC2 — those emissions capture the raw loaded/stored
* word themselves (a masked GTE register write must validate against the
* word as loaded, not the register's masked value).
*
* Deliberately unhooked: AND/XOR/NOR/SLT-family and the exotic immediates —
* they only ever DESTROY precision, and the engine's validate-on-read drops
* their stale shadows without help. */
void append_pgxp_hooks(uint32_t instr, std::string& code);
} // namespace PSXRecomp
#endif /* PGXP_HOOK_EMITTER_H */
+43 -16
View File
@@ -7,6 +7,7 @@
// records this substitution explicitly.
#include "strict_translator.h"
#include "pgxp_hook_emitter.h" /* shared PGXP hook grammar */
#include "gte_register_classification.h"
#include <cstdint>
@@ -63,6 +64,30 @@ TranslateResult unsupported(const PSXRecomp::DecodedInstruction& d, const std::s
} // namespace
TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction& d) {
TranslateResult r = translate_impl(d);
if (!r.supported) return r;
const uint32_t opcode = (d.raw >> 26) & 0x3F;
/* Shared PGXP hook grammar (ENHANCEMENTS.md G1.10). LWC2/SWC2 capture
* their raw word inside translate_impl and are skipped here. */
if (opcode != 0x32 && opcode != 0x3A && !r.c_code.empty())
PSXRecomp::append_pgxp_hooks(d.raw, r.c_code);
/* The deferred load-delay variant assigns into the function-scope
* psx_ldd_<addr> temp instead of the GPR; hook it with that temp as the
* loaded value (the shadow keys on the value, not on where it lands —
* the emitter's later writeback makes the GPR match). */
if (r.load_dest >= 0 && !r.c_code_deferred.empty()) {
const uint32_t rs = (d.raw >> 21) & 0x1F;
const int16_t offset = static_cast<int16_t>(d.raw & 0xFFFF);
r.c_code_deferred = fmt::format(
"{{ uint32_t _pgxa = cpu->gpr[{}] + {}; {} "
"PGXP_LOAD(0x{:08X}u, _pgxa, psx_ldd_{:08X}); }}",
static_cast<int>(rs), static_cast<int>(offset),
r.c_code_deferred, d.raw, d.address);
}
return r;
}
TranslateResult StrictTranslator::translate_impl(const PSXRecomp::DecodedInstruction& d) {
TranslateResult r;
// NOP (raw == 0, i.e. SLL $zero,$zero,0) — emit as a comment with no semantics.
@@ -1405,14 +1430,18 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
? fmt::format("cpu->gpr[{}]", static_cast<int>(rs))
: fmt::format("(uint32_t)((int32_t)cpu->gpr[{}] + ({}))", static_cast<int>(rs), static_cast<int>(offset));
bool special = PSXRecompGTERegisters::data_write_needs_helper(rt);
/* The raw loaded word is captured for the PGXP hook: a masked
* register write must validate against the word as loaded. */
if (special) {
r.c_code = gte_stall + fmt::format(
"gte_write_data(cpu, {}, psx_cyc_lwc2_read(cpu, {}));",
static_cast<int>(rt), addr);
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); "
"gte_write_data(cpu, {}, _pgxv); PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}",
addr, static_cast<int>(rt), d.raw);
} else {
r.c_code = gte_stall + fmt::format(
"cpu->gte_data[{}] = psx_cyc_lwc2_read(cpu, {});",
static_cast<int>(rt), addr);
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = psx_cyc_lwc2_read(cpu, _pgxa); "
"cpu->gte_data[{}] = _pgxv; PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}",
addr, static_cast<int>(rt), d.raw);
}
r.comment = fmt::format("lwc2 gte[{}], {}({})",
rt, static_cast<int>(offset), gpr_name(rs));
@@ -1431,20 +1460,18 @@ TranslateResult StrictTranslator::translate(const PSXRecomp::DecodedInstruction&
std::string value = PSXRecompGTERegisters::data_read_needs_helper(rt)
? fmt::format("gte_read_data(cpu, {})", static_cast<int>(rt))
: fmt::format("cpu->gte_data[{}]", static_cast<int>(rt));
if (offset == 0) {
{
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 = gte_stall + fmt::format(
"psx_store_cycle_barrier(); g_debug_last_store_pc = 0x{:08X}u; "
"cpu->write_word(cpu->gpr[{}], {}); "
"gte_precision_store_word(cpu->gpr[{}], {});",
d.address, static_cast<int>(rs), value,
static_cast<int>(rs), static_cast<int>(rt));
} else {
r.c_code = gte_stall + fmt::format(
"psx_store_cycle_barrier(); g_debug_last_store_pc = 0x{:08X}u; "
"cpu->write_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})), {}); "
"gte_precision_store_word((uint32_t)((int32_t)cpu->gpr[{}] + ({})), {});",
d.address, static_cast<int>(rs), static_cast<int>(offset), value,
static_cast<int>(rs), static_cast<int>(offset), static_cast<int>(rt));
"{{ uint32_t _pgxa = {}; uint32_t _pgxv = {}; "
"cpu->write_word(_pgxa, _pgxv); "
"gte_precision_store_word(_pgxa, {}); "
"PGXP_COP2(0x{:08X}u, _pgxv, _pgxa); }}",
d.address, addr, value, static_cast<int>(rt), d.raw);
}
r.comment = fmt::format("swc2 gte[{}], {}({})",
rt, static_cast<int>(offset), gpr_name(rs));
+7
View File
@@ -63,6 +63,13 @@ class StrictTranslator {
public:
// Translate a single decoded instruction. Pure function — no state.
static TranslateResult translate(const PSXRecomp::DecodedInstruction& d);
private:
// The per-opcode translation body; translate() wraps its c_code with the
// shared PGXP hook grammar (CodeGenerator::append_pgxp_hooks).
static TranslateResult translate_impl(const PSXRecomp::DecodedInstruction& d);
public:
};
} // namespace PSXRecompV4
+17
View File
@@ -17,6 +17,23 @@ psxrecomp_add_runtime_target(psx-runtime
EXE_NAME "PSXRecomp"
)
# PGXP variant of the framework runtime (ENHANCEMENTS.md G1.10): the same
# recompiled BIOS compiled with -DPSX_PGXP=1 so the emitted PGXP_*() hook
# macros arm the value-propagation engine. Framework-dev validation surface
# (LLE boot logo is real GTE content); titles get theirs via
# PSX_PGXP_VARIANT in psxrecomp_add_game_runtime.
option(PSX_PGXP_VARIANT
"Also build the _pgxp PGXP precision-shadowing variant" OFF)
if(PSX_PGXP_VARIANT)
psxrecomp_add_runtime_target(psx-runtime-pgxp
PGXP
DEBUG_PORT 4370
WINDOW_TITLE "psxrecomp - BIOS"
DEFAULT_BIOS_PATH "${PSXRECOMP_ROOT}/bios/SCPH1001.BIN"
EXE_NAME "PSXRecomp"
)
endif()
if(BUILD_TESTING)
add_executable(launcher_device_roundtrip_test
tests/test_launcher_device_roundtrip.cpp)
+2
View File
@@ -28,6 +28,8 @@ set(PSXRECOMP_CODEGEN_HASH_SRCS
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/full_function_emitter.h
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/strict_translator.cpp
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/strict_translator.h
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/pgxp_hook_emitter.cpp
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/pgxp_hook_emitter.h
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/function_discovery.cpp
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/src/function_discovery.h
${PSXRECOMP_CODEGEN_HASH_ROOT}/recompiler/include/gte_register_classification.h
+18 -2
View File
@@ -86,6 +86,13 @@
* model participation with queue-safe guard/hysteresis policy. */
/* v21: ws_angle_widen forwarder for exact, aspect-scaled 12-bit terrain
* frustum half-angle constants. */
/* (PGXP, no version bump): OverlayCallbacks grew an appended-last PGXPHooks
* table pointer (pgxp_hooks.h). Only pgxp-flavour shards (compiled with
* -DPSX_PGXP=1, PSX_OVERLAY_FLAVOR_PGXP set) reference the psx_pgxp_*
* forwarders, and the flavor half of the ABI tag already rejects any
* host/DLL flavor mix — base-flavour DLLs and hosts are untouched, so
* the version stays. The emit-content change (PGXP_*() macros in all
* generated C) is covered by the codegen hash + CODEGEN_VER below. */
#define PSX_OVERLAY_ABI_VERSION 21
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
@@ -113,6 +120,12 @@
#define PSX_OVERLAY_FLAVOR 0
#endif
/* Flavor bits (compose bitwise; base = 0):
* 1 — widescreen variant codegen
* 2 — PGXP precision shadowing (-DPSX_PGXP=1 objects; pgxp_hooks.h) */
#define PSX_OVERLAY_FLAVOR_WIDESCREEN 1
#define PSX_OVERLAY_FLAVOR_PGXP 2
/* Combined tag exported by overlay_abi() and checked by the loader. */
#define PSX_OVERLAY_ABI_TAG \
((int)((PSX_OVERLAY_ABI_VERSION & 0xFFFF) | ((PSX_OVERLAY_FLAVOR & 0xFFFF) << 16)))
@@ -139,8 +152,11 @@
* masked-16-bit screen-X, and depth cull emits.
* 7: + configured side-plane normal-X (plane_nx) cull emit.
* 8: + configured per-primitive bound-load (xclip) cull emit.
* 9: + full-word-guarded object/model participation compare emit. */
#define PSX_OVERLAY_CODEGEN_VER 9
* 9: + full-word-guarded object/model participation compare emit.
* 10: + PGXP dataflow-shadowing PGXP_*() macro emission at loads/stores/
* COP2 transfers/precision-carrying ALU ops (pgxp_hooks.h; no-ops in
* base-flavour objects, real hooks under -DPSX_PGXP=1). */
#define PSX_OVERLAY_CODEGEN_VER 10
/* Auto codegen hash (generated by hash_codegen.cmake from the recompiler codegen
* sources). Folded into the cache PATH next to cg<N> (gcc/<arch-abi>/cg<N>_<hash>/)
@@ -265,4 +265,27 @@ uint32_t psx_ws_angle_widen(uint32_t vanilla) {
void gte_precision_store_word(uint32_t addr, uint8_t reg) {
if (g_cbs.gte_precision_store_word) g_cbs.gte_precision_store_word(addr, reg);
}
#if defined(PSX_PGXP) && PSX_PGXP
/* PGXP dataflow-shadowing hooks (pgxp_hooks.h; ENHANCEMENTS.md G1.10). Only a
* pgxp-flavour shard (compiled -DPSX_PGXP=1, flavor bit set) references these
* — base-flavour shards preprocess the PGXP_*() macros away, so this block is
* compiled out of them entirely. Forwards to the host engine through the
* appended-last table; every hook no-ops on a host that predates it
* (precision shadowing is visual-only, never load-bearing). */
void psx_pgxp_load(struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value) {
if (g_cbs.pgxp && g_cbs.pgxp->load) g_cbs.pgxp->load(cpu, instr, addr, value);
}
void psx_pgxp_store(struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value) {
if (g_cbs.pgxp && g_cbs.pgxp->store) g_cbs.pgxp->store(cpu, instr, addr, value);
}
void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2) {
if (g_cbs.pgxp && g_cbs.pgxp->alu) g_cbs.pgxp->alu(cpu, instr, result, s1, s2);
}
void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr, uint32_t hi, uint32_t lo, uint32_t s1, uint32_t s2) {
if (g_cbs.pgxp && g_cbs.pgxp->muldiv) g_cbs.pgxp->muldiv(cpu, instr, hi, lo, s1, s2);
}
void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr, uint32_t value, uint32_t addr) {
if (g_cbs.pgxp && g_cbs.pgxp->cop2) g_cbs.pgxp->cop2(cpu, instr, value, addr);
}
#endif /* PSX_PGXP */
/* ----------------------------------------------------------------------- */
+4
View File
@@ -92,6 +92,8 @@ typedef struct PGXPStats {
uint64_t trunc_reject; /* integer part disagreed with native parse */
uint64_t tolerance_reject;
uint64_t w_valid; /* lookups that also carried a usable depth */
uint64_t produced; /* RTPS/RTPT projections pushed into shadows */
uint64_t swc2_stores; /* GTE reg shadows copied to RAM shadows */
} PGXPStats;
void pgxp_get_stats(PGXPStats *out);
@@ -115,6 +117,8 @@ void pgxp_test_get_gte_sxy(uint32_t index, uint32_t *packed,
int32_t *x16, int32_t *y16, uint16_t *z,
uint8_t *valid);
uint32_t pgxp_test_generation(void);
uint32_t pgxp_test_suppress_depth(void);
int pgxp_test_active(void);
void pgxp_test_set_generation(uint32_t gen);
#ifdef __cplusplus
+46 -8
View File
@@ -761,7 +761,13 @@ function(psxrecomp_ensure_zlib)
endfunction()
function(psxrecomp_add_runtime_target target)
set(options ORACLE COSIM)
# PGXP: build this target's objects with -DPSX_PGXP=1 so the PGXP_*()
# hook macros the emitter writes into ALL generated C become real calls
# into the value-propagation engine (pgxp_hooks.h; ENHANCEMENTS.md G1.10),
# and stamp the pgxp overlay flavor so the shard cache and the ABI gate
# keep pgxp and base DLLs fully separate. The base target is untouched —
# the macros preprocess away without the define.
set(options ORACLE COSIM PGXP)
set(oneValueArgs
GAME_GENERATED_DISPATCH_C
GAME_OVERLAY_STATIC_C
@@ -917,6 +923,15 @@ function(psxrecomp_add_runtime_target target)
if(PSXRT_ORACLE)
set(_psxrt_exe_name "${_psxrt_exe_name}_oracle")
endif()
if(PSXRT_PGXP)
# Distinct binary beside the base one; the launcher (or the player)
# picks the variant. Same debug port as the base build — run one at a
# time (the A/B protocol is one-toggle-per-run anyway).
set(_psxrt_exe_name "${_psxrt_exe_name}_pgxp")
target_compile_definitions(${target} PRIVATE
PSX_PGXP=1
PSX_OVERLAY_FLAVOR=2) # PSX_OVERLAY_FLAVOR_PGXP (overlay_api.h)
endif()
set_target_properties(${target} PROPERTIES OUTPUT_NAME "${_psxrt_exe_name}")
# ---- Windows / desktop app icon ---------------------------------------
@@ -1790,20 +1805,41 @@ function(psxrecomp_add_game_runtime target)
GAME_GENERATED_DISPATCH_C "${_psxg_marker}"
${_psxg_rt_args}
)
set(_psxg_targets ${target})
# PGXP variant (ENHANCEMENTS.md G1.10): the SAME generated sources
# compiled a second time with -DPSX_PGXP=1 into <exe>_pgxp.exe. The
# emitted PGXP_*() macros become real hook calls, arming true
# geometry correction; the base target is byte-for-byte the
# pre-feature build. Opt-in per configure — it roughly doubles the
# generated-C compile time.
option(PSX_PGXP_VARIANT
"Also build the <exe>_pgxp PGXP precision-shadowing variant" OFF)
if(PSX_PGXP_VARIANT)
psxrecomp_add_runtime_target(${target}-pgxp
PGXP
GAME_GENERATED_FULL_C ${_psxg_full_list}
GAME_GENERATED_DISPATCH_C "${_psxg_marker}"
${_psxg_rt_args}
)
list(APPEND _psxg_targets ${target}-pgxp)
endif()
else()
message(STATUS
"psxrecomp: setup host (no game C, no BIOS backends) — "
"first-run Generate & rebuild")
psxrecomp_add_runtime_target(${target} ${_psxg_rt_args})
set(_psxg_targets ${target})
endif()
target_compile_definitions(${target} PRIVATE PSX_HAS_GAME_CODEGEN=1)
foreach(_psxg_t IN LISTS _psxg_targets)
target_compile_definitions(${_psxg_t} PRIVATE PSX_HAS_GAME_CODEGEN=1)
if(PSX_NET_LOBBY_DEFAULT_URL)
# Stringify for C: PSX_NET_LOBBY_DEFAULT_URL="ws://..."
target_compile_definitions(${target} PRIVATE
"PSX_NET_LOBBY_DEFAULT_URL=\"${PSX_NET_LOBBY_DEFAULT_URL}\"")
endif()
if(PSX_NET_LOBBY_DEFAULT_URL)
# Stringify for C: PSX_NET_LOBBY_DEFAULT_URL="ws://..."
target_compile_definitions(${_psxg_t} PRIVATE
"PSX_NET_LOBBY_DEFAULT_URL=\"${PSX_NET_LOBBY_DEFAULT_URL}\"")
endif()
endforeach()
# Include the portable codegen host. Do NOT add CMAKE_CURRENT_SOURCE_DIR
# wholesale to -I: on case-insensitive macOS, #include <version> can pick
@@ -1816,5 +1852,7 @@ function(psxrecomp_add_game_runtime target)
if(RECOMP_UI_ROOT)
list(APPEND _psxg_inc "${RECOMP_UI_ROOT}/src")
endif()
target_include_directories(${target} PRIVATE ${_psxg_inc})
foreach(_psxg_t IN LISTS _psxg_targets)
target_include_directories(${_psxg_t} PRIVATE ${_psxg_inc})
endforeach()
endfunction()
+8 -4
View File
@@ -5212,7 +5212,8 @@ static void handle_geom_correction(int id, const char *json)
"\"pgxp\":{\"enabled\":%d,\"cpu_mode\":%d,\"tolerance\":%.3f,"
"\"lookups\":%llu,\"dataflow_hit\":%llu,\"fallback_hit\":%llu,"
"\"native\":%llu,\"value_mismatch\":%llu,\"trunc_reject\":%llu,"
"\"tolerance_reject\":%llu,\"w_valid\":%llu}}",
"\"tolerance_reject\":%llu,\"w_valid\":%llu,"
"\"produced\":%llu,\"swc2_stores\":%llu}}",
id,
gte_geometry_correction_enabled(),
(unsigned)hits,
@@ -5226,7 +5227,9 @@ static void handle_geom_correction(int id, const char *json)
(unsigned long long)ps.value_mismatch,
(unsigned long long)ps.trunc_reject,
(unsigned long long)ps.tolerance_reject,
(unsigned long long)ps.w_valid);
(unsigned long long)ps.w_valid,
(unsigned long long)ps.produced,
(unsigned long long)ps.swc2_stores);
}
/* pgxp — live-tune the value-propagation engine for one-toggle isolation runs
@@ -5260,8 +5263,9 @@ static void handle_pgxp(int id, const char *json)
pgxp_set_tolerance((float)strtod(p, NULL));
}
send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"cpu_mode\":%d,"
"\"tolerance\":%.3f}",
id, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance());
"\"tolerance\":%.3f,\"suppress\":%u,\"active\":%d}",
id, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(),
(unsigned)pgxp_test_suppress_depth(), pgxp_test_active());
}
static void handle_gpu_state(int id, const char *json)
+10
View File
@@ -12123,6 +12123,16 @@ session_reboot:
/* Sub-pixel vertex precision + perspective-correct UVs. Both default off;
* with both off every setter below leaves the tracking caches disabled and
* the draw path is the faithful integer one, unchanged. */
/* Env overrides (debug/validation path, like PSX_BIOS_HLE): arm the
* corrections from process start so free-running (headless) boots can be
* measured from the first projected vertex — a TCP toggle always arrives
* after the interesting window. '0' = off, anything else = on. */
if (const char* e = std::getenv("PSX_GEOMETRY_CORRECTION"))
g_video_geometry_correction = (*e && *e != '0') ? 1 : 0;
if (const char* e = std::getenv("PSX_PERSPECTIVE_TEXTURING"))
g_video_perspective_texturing = (*e && *e != '0') ? 1 : 0;
if (const char* e = std::getenv("PSX_PGXP_CPU_MODE"))
g_video_pgxp_cpu_mode = (*e && *e != '0') ? 1 : 0;
gte_geometry_correction_set(g_video_geometry_correction);
gpu_texture_correction_set(g_video_perspective_texturing);
pgxp_set_cpu_mode(g_video_pgxp_cpu_mode);
+25 -8
View File
@@ -1727,9 +1727,9 @@ static void warn_on_cgtag_mismatch(const char *tier) {
snprintf(base, sizeof base, "%s/%s/%s/%s",
s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI);
char expect[64];
snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x",
snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x_f%u",
PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH,
(unsigned)s_config_hash);
(unsigned)s_config_hash, (unsigned)PSX_OVERLAY_FLAVOR);
snprintf(pattern, sizeof pattern, "%s/cg*", base);
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA(pattern, &fd);
@@ -1755,9 +1755,9 @@ static void warn_on_cgtag_mismatch(const char *tier) {
char base[768], expect[64], found[256];
snprintf(base, sizeof base, "%s/%s/%s/%s",
s_cache_dir, s_game_id, tier, PSX_OVERLAY_ARCH_ABI);
snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x",
snprintf(expect, sizeof expect, "cg%d_%08x_gc%08x_f%u",
PSX_OVERLAY_CODEGEN_VER, (unsigned)PSX_OVERLAY_CODEGEN_HASH,
(unsigned)s_config_hash);
(unsigned)s_config_hash, (unsigned)PSX_OVERLAY_FLAVOR);
if (psx_overlay_posix_find_other_cache_tag(base, expect, found,
sizeof(found))) {
loader_log("*** OVERLAY CACHE HASH MISMATCH: this build reads %s/%s but "
@@ -1906,14 +1906,16 @@ static void scan_cache_dir(void) {
/* Index both tiers and every immutable artifact. Runtime selection prefers
* usable GCC over TCC; an invalid GCC artifact cannot suppress a valid TCC
* fallback merely because its filename was enumerated first. */
snprintf(dir, sizeof(dir), "%s/%s/gcc/%s/cg%d_%08x_gc%08x",
snprintf(dir, sizeof(dir), "%s/%s/gcc/%s/cg%d_%08x_gc%08x_f%u",
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
(unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash);
(unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash,
(unsigned)PSX_OVERLAY_FLAVOR);
scan_one_cache_dir(dir, CACHE_TIER_GCC);
abi_preflight_sweep(dir);
snprintf(dir, sizeof(dir), "%s/%s/tcc/%s/cg%d_%08x_gc%08x",
snprintf(dir, sizeof(dir), "%s/%s/tcc/%s/cg%d_%08x_gc%08x_f%u",
s_cache_dir, s_game_id, PSX_OVERLAY_ARCH_ABI, PSX_OVERLAY_CODEGEN_VER,
(unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash);
(unsigned)PSX_OVERLAY_CODEGEN_HASH, (unsigned)s_config_hash,
(unsigned)PSX_OVERLAY_FLAVOR);
scan_one_cache_dir(dir, CACHE_TIER_TCC);
abi_preflight_sweep(dir);
@@ -2355,6 +2357,21 @@ static void init_callbacks(void) {
extern uint32_t psx_ws_angle_widen(uint32_t vanilla);
s_callbacks.ws_angle_widen = psx_ws_angle_widen;
}
/* PGXP dataflow-shadowing hook table (pgxp_hooks.h, appended last).
* Referenced only by pgxp-flavour shards; the flavor half of the ABI
* tag already rejects any host/DLL flavor mix, and a NULL table on an
* older host makes every hook a no-op (visual-only, never
* load-bearing). */
{
static const PGXPHooks pgxp_hooks_table = {
psx_pgxp_load,
psx_pgxp_store,
psx_pgxp_alu,
psx_pgxp_muldiv,
psx_pgxp_cop2,
};
s_callbacks.pgxp = &pgxp_hooks_table;
}
}
}
+5
View File
@@ -631,6 +631,7 @@ extern "C" void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr,
extern "C" void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3,
uint32_t packed) {
if (!g_pgxp_active) return;
s_stats.produced++;
s_gte[12] = s_gte[13];
s_gte[13] = s_gte[14];
PGXPValue *pv = &s_gte[14];
@@ -735,6 +736,7 @@ extern "C" void pgxp_store_gte_reg(uint32_t addr, uint8_t reg) {
if (!dst) return;
const PGXPValue *src = &s_gte[reg & 31u];
if (src->gen != s_gen) return;
s_stats.swc2_stores++;
*dst = *src;
}
@@ -769,6 +771,9 @@ extern "C" void pgxp_test_get_gte_sxy(uint32_t index, uint32_t *packed,
extern "C" uint32_t pgxp_test_generation(void) { return s_gen; }
extern "C" uint32_t pgxp_test_suppress_depth(void) { return s_suppress; }
extern "C" int pgxp_test_active(void) { return g_pgxp_active; }
extern "C" void pgxp_test_set_generation(uint32_t gen) { s_gen = gen; }
/* Address-keyed depth lookup for the shipped perspective-texturing path
+7 -1
View File
@@ -4068,6 +4068,9 @@ def _compile_dll_tcc(c_path: str, out_dll: str, include_dirs, flavor: int,
'-DPSX_NO_DEBUG_TOOLS',
'-DPSX_ENABLE_BLOCK_CYCLES=1',
f'-DPSX_OVERLAY_FLAVOR={int(flavor)}',
# PGXP flavor bit (overlay_api.h PSX_OVERLAY_FLAVOR_PGXP): arm the
# PGXP_*() hook macros the emitter writes into every overlay C.
*(['-DPSX_PGXP=1'] if int(flavor) & 2 else []),
native_path(c_path), '-o', native_path(out_dll)]
for d in include_dirs:
cmd.append('-I' + native_path(_bom_free_incdir(d)))
@@ -4116,6 +4119,9 @@ def _compile_dll_direct(c_path: str, out_dll: str, include_dirs: list[str],
# cache and a base cache can never cross-contaminate even if they share
# a directory (they key by guest-bytes CRC, which is flavor-blind).
f'-DPSX_OVERLAY_FLAVOR={int(flavor)}',
# PGXP flavor bit (overlay_api.h PSX_OVERLAY_FLAVOR_PGXP): arm the
# PGXP_*() hook macros the emitter writes into every overlay C.
*(['-DPSX_PGXP=1'] if int(flavor) & 2 else []),
c_path,
'-o', out_dll,
*includes,
@@ -5303,7 +5309,7 @@ def main():
ch = codegen_hash(args.runtime_include)
gh = overlay_config_hash(args.recompiler, args.game_toml)
cache_dir = os.path.join(args.out_dir, game_id, args.compiler, cache_arch_abi(),
f'cg{cg}_{ch:08x}_gc{gh:08x}')
f'cg{cg}_{ch:08x}_gc{gh:08x}_f{int(args.flavor)}')
os.makedirs(cache_dir, exist_ok=True)
print(f'Cache dir: {cache_dir} '
f'(codegen ver {cg}, emitter {ch:08x}, config {gh:08x})')