mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
Merge origin/master into feat/vsync_hotfix.
Keep vsync/pacer XOR and skip the full-period wait while manual fast-forward already paces at the turbo multiplier. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+101
@@ -766,3 +766,104 @@ defect, which is the whole finding of G1.1–G1.8.
|
||||
|
||||
The withdrawn row, the full post-mortem, the mechanism that would fix it, and the
|
||||
re-test protocol are preserved on `park/pgxp-geometry-correction` (see its G1.9).
|
||||
|
||||
### G1.10 — PGXP value-propagation engine LANDED, phases 0+1 (2026-08-15)
|
||||
|
||||
The G1.2/G1.3 mechanism — precision travelling WITH the data — now exists
|
||||
(`feat/pgxp-dataflow`, tracked as `beads-eio.3.48`). **Clean-room**: the
|
||||
vendored `duckstation/` tree is CC BY-NC-ND (NoDerivatives) and `beetle-psx/`
|
||||
is GPL, both incompatible with this project's PolyForm Noncommercial license,
|
||||
so NO code was ported from either; they remain black-box behavioral oracles
|
||||
only. The engine implements the publicly documented technique from psx-spx +
|
||||
our own G1 analysis.
|
||||
|
||||
**What exists now:**
|
||||
|
||||
- `runtime/src/pgxp.cpp` + `runtime/include/pgxp.h`: per-word RAM+scratchpad
|
||||
shadows (~10 MB, lazily allocated, fail-closed), per-GPR (+HI/LO) and
|
||||
per-GTE-data-reg shadows. Each `PGXPValue` records the sub-pixel 16.16
|
||||
screen X/Y, the projected SZ depth, per-half validity flags, and the exact
|
||||
guest word it describes. **The one safety invariant: a shadow is only
|
||||
believed after validating that word against reality.** Overwrite-and-
|
||||
validate only — no side-effect modelling, so DMA/memcpy/untracked writers
|
||||
need no hooks at all (their stale shadows fail validation at use). The
|
||||
plain-store `gte_precision_invalidate_word` calls in memory.c are removed
|
||||
as redundant under this model (a small store-path win).
|
||||
- Hook funnel (`runtime/include/pgxp_hooks.h`): 5 entry points
|
||||
(`psx_pgxp_load/store/alu/muldiv/cop2`) taking the raw instruction word.
|
||||
Generated code will call them via `PGXP_*()` macros that expand to nothing
|
||||
unless compiled with `-DPSX_PGXP=1` — the G1.3 codegen-time gate, as a
|
||||
preprocessor define over ONE generated tree (no flavour drift, base objects
|
||||
byte-identical). `OverlayCallbacks` gained an appended-last `PGXPHooks*`
|
||||
table (NULL-tolerant); the ABI bump arrives with the Phase-2 emitter change.
|
||||
- Both interpreters (`dirty_ram_interp.c`, `psx_interpreter.c`) call the
|
||||
hooks directly on loads/stores/COP2 transfers and the precision-carrying
|
||||
arithmetic (moves, add/sub, or-merge, 16-bit shifts, LUI, HI/LO). Ops that
|
||||
merely destroy precision are deliberately NOT hooked — validation already
|
||||
drops their stale shadows.
|
||||
- `pgxp_cpu_mode` (tier-2 arithmetic propagation, default off, matching the
|
||||
references' default) and `pgxp_tolerance` (sub-pixel clamp, default off)
|
||||
are new `[video]` keys and live-tunable via the TCP `pgxp` verb, which also
|
||||
live-toggles `geometry`/`texture` for same-scene A/B (the G1.6 method rule
|
||||
no longer needs a restart between arms).
|
||||
- GPU consumption is now PER-VERTEX (`pgxp_get_precise_vertex`): dataflow
|
||||
shadow first (validated against the actual packet word), the G1.4 exact
|
||||
ambiguity-gated position cache second (never carries depth), native
|
||||
integers last. Two safeguards on every precise vertex: truncation agreement
|
||||
(integer part must equal the native 11-bit parse) and the tolerance clamp.
|
||||
Mixed precise/native triangles are correct — a native vertex sits exactly
|
||||
where the uncorrected pipeline put it, bounding any seam to < 1px of
|
||||
sub-pixel fraction (the G1.1 cracks came from wrong-vertex substitution and
|
||||
all-or-nothing triangles, both gone). Geometry and perspective UVs now
|
||||
share one provenance source and compose per triangle.
|
||||
- Census: `geom_correction` grew a `pgxp` object (dataflow_hit /
|
||||
fallback_hit / native / value_mismatch / trunc_reject / tolerance_reject /
|
||||
w_valid). The G1.9 gate now reads: dataflow-hit share must be dramatically
|
||||
above the 5.2% position-cache ceiling before any launcher surface returns.
|
||||
- Tests: `pgxp_test` (roundtrips, half-word semantics, validate-on-read,
|
||||
DMA-overwrite rejection, repack arithmetic, suppression bracket,
|
||||
safeguards) + `gte_register_access_test` updated to the precise
|
||||
invalidation contract. 33/33 enabled runtime tests green.
|
||||
|
||||
**Not yet (Phase 2+):** emitter macros at the ~47 sites, the `-DPSX_PGXP`
|
||||
dual link targets + flavor-namespaced shard caches + ABI bump, BIOS regen,
|
||||
and the Ape/Crash/Tomba2 census + `screenshot_hires` A/B campaign. Until the
|
||||
emitter lands, compiled code feeds the engine only through the v14 swc2
|
||||
sites, so dataflow coverage on a running title comes from interpreted code
|
||||
paths; the full-coverage census gate is a Phase-2/3 measurement.
|
||||
|
||||
Incidental fixes while landing this: the per-target `psx_game_version.txt`
|
||||
`file(GENERATE)` collision that broke every fresh single-config configure
|
||||
(runtime.cmake once-only stamp), and `overlay_capture_retry_test` not
|
||||
compiling under the SDL3 default (SDL_SetMainReady gone + inverted SDL_Init
|
||||
check — it now uses `psx_sdl_init`).
|
||||
|
||||
**First real-title census (Ape Escape, 2026-08-15) — the G1.9 gate is
|
||||
PASSED.** Worktree build against this branch (`_wt-ape-pgxp`,
|
||||
`-DPSX_PGXP_VARIANT=ON -DPSX_DEBUG_TOOLS=ON`, game C regenerated), memory-mode
|
||||
only (`pgxp_cpu_mode` off), boot → title → attract, per-10s windows:
|
||||
**79–90% dataflow hits** (vs the 5.2% position-cache ceiling of G1.4),
|
||||
fallback ≤0.4%, `value_mismatch` 0, `trunc_reject` ~0, and every dataflow hit
|
||||
carried a usable depth. The residual native share is 2D HUD/sprite content,
|
||||
which is exactly what must stay native. Still to run before any ship surface:
|
||||
the windowed `screenshot_hires` off/on A/B (headless has NO hi-res mirror —
|
||||
`scale:1` fallback, the same blind-instrument trap G1.5 documented), on Ape +
|
||||
Crash with Tomba 2 as the 2D negative control; the hook-overhead FPS measure
|
||||
(pgxp build, feature off, vs base); and pgxp-flavour (`_f2`) overlay-shard
|
||||
autocompile validation. Validation-build notes: title Release builds default
|
||||
`PSX_DEBUG_TOOLS=OFF` (no TCP), and a title launched without `--game` binds
|
||||
the framework default port 4370.
|
||||
|
||||
**Interactive visual isolation, USER-VALIDATED (Ape Escape, in-game,
|
||||
2026-08-15).** Same scene, one toggle at a time, player observing live:
|
||||
OFF = baseline vertex jiggle; geometry ON unclamped = jiggle largely gone but
|
||||
sparse hairline background-bleed seams (a <1px disagreement along a long
|
||||
shared edge between a corrected triangle and an uncorrected neighbour — the
|
||||
THPS2 line class, NOT wild vertices; both reported artifacts vanished with
|
||||
the toggle off); geometry ON with `pgxp_tolerance = 0.5` = **seams gone**,
|
||||
only sub-half-pixel misalignment remains. 0.5 is therefore the shipped
|
||||
default (config_loader.h), live-tunable via the `pgxp` TCP verb. Known
|
||||
tooling defect found on the way: `screenshot_hires` produces a tiled/black
|
||||
PNG at 768x480 scale-2 windowed (row-pitch bug in the hires readback) —
|
||||
the census + the player's own captures carried the session; fix it before
|
||||
the formal Crash/Tomba2 A/B.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
format_version = 5
|
||||
id = "psx.enhancement.pgxp"
|
||||
version = "1.0.0"
|
||||
name = "PGXP Precision (geometry + textures)"
|
||||
author = "psxrecomp"
|
||||
description = "Sub-pixel vertex precision and perspective-correct texturing (the PGXP technique): the runtime shadows the GTE's full-precision projections and follows them to the GPU, so 3D polygons stop wobbling between whole pixels and large floor/wall textures stop warping. Visual-only: the game's own math, culling and readback see exactly what real hardware produces."
|
||||
resolver = "declarative"
|
||||
save_compatibility = "shared"
|
||||
|
||||
# Sub-pixel precision is a property of the emulated GTE/GPU pair, not of any
|
||||
# particular disc, so this ships with the framework and applies to every game.
|
||||
[[target]]
|
||||
game_id = "*"
|
||||
|
||||
[[feature]]
|
||||
id = "pgxp"
|
||||
name = "PGXP Precision"
|
||||
description = "Disabled by default (the faithful floor). Corrections are validated per vertex against the exact packet words the game drew with and clamped to half a pixel, so 2D sprites and UI are never touched and a vertex the tracker cannot prove simply draws the faithful way. Most effective on a build compiled with the PGXP hook variant; on other builds coverage is reduced but never harmful. Needs supersampling >= 2 to be visible."
|
||||
group = "Visual"
|
||||
default_enabled = false
|
||||
|
||||
[[option]]
|
||||
feature = "pgxp"
|
||||
id = "cpu_mode"
|
||||
label = "CPU-mode propagation"
|
||||
description = "Also track precision through CPU arithmetic (register repacking math). Off matches the reference implementations' default; on can raise coverage in games that rebuild vertices with integer math. Always safe either way - validation rejects anything stale."
|
||||
group = "Visual"
|
||||
type = "boolean"
|
||||
default = "false"
|
||||
|
||||
[[plugin]]
|
||||
feature = "pgxp"
|
||||
id = "psx.pgxp"
|
||||
+546
-537
File diff suppressed because it is too large
Load Diff
@@ -146,6 +146,14 @@ struct CodeGenConfig {
|
||||
// (INT32_MAX while revealed, vanilla at 4:3); empty by default.
|
||||
std::set<uint32_t> ws_cull_xclip_load_sites;
|
||||
|
||||
// Exact `bltz MAC0, reject`-style NCLIP/backface rejects to suppress while
|
||||
// widescreen reveals extra world. 4:3 keeps the original branch predicate.
|
||||
std::set<uint32_t> ws_cull_nclip_keep_sites;
|
||||
|
||||
// Exact branch PCs whose reject target is suppressed while widescreen
|
||||
// reveals extra world. 4:3 keeps the original branch predicate.
|
||||
std::set<uint32_t> ws_cull_branch_keep_sites;
|
||||
|
||||
// Exact, full-word-guarded comparison sites whose result is forced only
|
||||
// while widescreen reveals extra world. 4:3 evaluates the original compare.
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep_sites;
|
||||
|
||||
@@ -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"
|
||||
@@ -737,6 +738,12 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t rs = get_rs(instr);
|
||||
uint32_t rt = get_rt(instr);
|
||||
auto keep_branch_if_wide = [&](std::string cond) {
|
||||
if (!config_.ws_cull_branch_keep_sites.count(addr))
|
||||
return cond;
|
||||
return fmt::format("psx_ws_x_margin() > 0 ? 0 : ({}) /* ws branch keep */",
|
||||
cond);
|
||||
};
|
||||
|
||||
// REGIMM branches. R3000A hardware decodes EVERY rt value here, not just
|
||||
// the four assembler mnemonics: the branch sense is rt bit 0 (0 = bltz,
|
||||
@@ -758,9 +765,14 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
(ws_cull_bltz_pcs_.count(addr) || config_.ws_cull_bltz_sites.count(addr)))
|
||||
return fmt::format("psx_ws_cull_bltz({}) /* ws cull (left edge) */",
|
||||
reg_name(rs));
|
||||
return fmt::format("(int32_t){} < 0", reg_name(rs));
|
||||
if (regimm_op == 0x00 && config_.ws_cull_nclip_keep_sites.count(addr))
|
||||
return fmt::format("psx_ws_x_margin() > 0 ? 0 : ((int32_t){} < 0) /* ws nclip keep */",
|
||||
reg_name(rs));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("(int32_t){} < 0", reg_name(rs)));
|
||||
} else { // bgez family (incl. bgezal + undefined mirrors)
|
||||
return fmt::format("(int32_t){} >= 0", reg_name(rs));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("(int32_t){} >= 0", reg_name(rs)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,22 +780,26 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
switch (opcode) {
|
||||
case 0x04: // beq
|
||||
case 0x14: // beql
|
||||
return fmt::format("{} == {}", reg_name(rs), reg_name(rt));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("{} == {}", reg_name(rs), reg_name(rt)));
|
||||
|
||||
case 0x05: // bne
|
||||
case 0x15: // bnel
|
||||
return fmt::format("{} != {}", reg_name(rs), reg_name(rt));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("{} != {}", reg_name(rs), reg_name(rt)));
|
||||
|
||||
case 0x06: // blez
|
||||
case 0x16: // blezl
|
||||
return fmt::format("(int32_t){} <= 0", reg_name(rs));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("(int32_t){} <= 0", reg_name(rs)));
|
||||
|
||||
case 0x07: // bgtz
|
||||
case 0x17: // bgtzl
|
||||
return fmt::format("(int32_t){} > 0", reg_name(rs));
|
||||
return keep_branch_if_wide(
|
||||
fmt::format("(int32_t){} > 0", reg_name(rs)));
|
||||
}
|
||||
|
||||
return "0 /* unknown branch condition: defaults to not-taken */";
|
||||
return keep_branch_if_wide("0 /* unknown branch condition: defaults to not-taken */");
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
|
||||
@@ -1500,17 +1516,21 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
const std::string gte_read = fmt::format(
|
||||
"\n#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_gte_read(cpu, {});\n#endif\n ", rt);
|
||||
if (cop_op == 0x00) { // MFC2 - move from COP2 data
|
||||
if (PSXRecompGTERegisters::data_read_needs_helper(static_cast<uint8_t>(rd))) {
|
||||
code = gte_read + fmt::format("{} = gte_read_data(cpu, {}); /* mfc2 */", reg_name(rt), rd);
|
||||
} else {
|
||||
code = gte_read + fmt::format("{} = cpu->gte_data[{}]; /* mfc2 */", reg_name(rt), rd);
|
||||
}
|
||||
const std::string value =
|
||||
PSXRecompGTERegisters::data_read_needs_helper(static_cast<uint8_t>(rd))
|
||||
? fmt::format("gte_read_data(cpu, {})", rd)
|
||||
: fmt::format("cpu->gte_data[{}]", rd);
|
||||
code = gte_read + (rt == 0
|
||||
? fmt::format("(void){}; /* mfc2 to $zero: read preserved, write discarded */", value)
|
||||
: fmt::format("{} = {}; /* mfc2 */", reg_name(rt), value));
|
||||
} else if (cop_op == 0x02) { // CFC2 - move from COP2 control
|
||||
if (PSXRecompGTERegisters::ctrl_read_needs_helper(static_cast<uint8_t>(rd))) {
|
||||
code = gte_read + fmt::format("{} = gte_read_ctrl(cpu, {}); /* cfc2 */", reg_name(rt), rd);
|
||||
} else {
|
||||
code = gte_read + fmt::format("{} = cpu->gte_ctrl[{}]; /* cfc2 */", reg_name(rt), rd);
|
||||
}
|
||||
const std::string value =
|
||||
PSXRecompGTERegisters::ctrl_read_needs_helper(static_cast<uint8_t>(rd))
|
||||
? fmt::format("gte_read_ctrl(cpu, {})", rd)
|
||||
: fmt::format("cpu->gte_ctrl[{}]", rd);
|
||||
code = gte_read + (rt == 0
|
||||
? fmt::format("(void){}; /* cfc2 to $zero: read preserved, write discarded */", value)
|
||||
: fmt::format("{} = {}; /* cfc2 */", reg_name(rt), value));
|
||||
} else if (cop_op == 0x04) { // MTC2 - move to COP2 data
|
||||
if (PSXRecompGTERegisters::data_write_needs_helper(static_cast<uint8_t>(rd))) {
|
||||
code = gte_stall + fmt::format("gte_write_data(cpu, {}, {}); /* mtc2 */", rd, reg_name(rt));
|
||||
@@ -1568,12 +1588,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;
|
||||
@@ -1589,16 +1616,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:
|
||||
@@ -1606,6 +1632,7 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
}
|
||||
}
|
||||
|
||||
PSXRecomp::append_pgxp_hooks(instr, code);
|
||||
return config_.indent + code + comment;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,8 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
|
||||
h.words("cull_depth", c.ws_cull_depth_sites);
|
||||
h.words("cull_plane_nx", c.ws_cull_plane_nx_sites);
|
||||
h.words("cull_xclip_load", c.ws_cull_xclip_load_sites);
|
||||
h.words("cull_nclip_keep", c.ws_cull_nclip_keep_sites);
|
||||
h.words("cull_branch_keep", c.ws_cull_branch_keep_sites);
|
||||
h.words("cull_w_imms", c.ws_cull_w_imms);
|
||||
h.words("cull_h_imms", c.ws_cull_h_imms);
|
||||
h.words("backdrop_x", c.ws_backdrop_x_sites);
|
||||
@@ -327,10 +329,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cfg.contains("runtime")) return rt;
|
||||
const toml::value& runtime = toml::find(cfg, "runtime");
|
||||
if (runtime.contains("language")) // [runtime].language convenience alias
|
||||
rt.language = toml::find<std::string>(runtime, "language");
|
||||
if (cfg.contains("runtime")) {
|
||||
const toml::value& runtime = toml::find(cfg, "runtime");
|
||||
if (runtime.contains("language")) // [runtime].language convenience alias
|
||||
rt.language = toml::find<std::string>(runtime, "language");
|
||||
|
||||
if (runtime.contains("debug_port")) {
|
||||
const auto port = toml::find<int64_t>(runtime, "debug_port");
|
||||
@@ -502,9 +504,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
if (runtime.contains("overlay_backend")) {
|
||||
rt.overlay_backend = toml::find<std::string>(runtime, "overlay_backend");
|
||||
}
|
||||
if (runtime.contains("overlay_native_block")) {
|
||||
for (const auto& a : toml::find<std::vector<std::string>>(runtime, "overlay_native_block")) {
|
||||
rt.overlay_native_block.push_back(parse_hex(a, "runtime.overlay_native_block"));
|
||||
if (runtime.contains("overlay_native_block")) {
|
||||
for (const auto& a : toml::find<std::vector<std::string>>(runtime, "overlay_native_block")) {
|
||||
rt.overlay_native_block.push_back(parse_hex(a, "runtime.overlay_native_block"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,6 +574,13 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
rt.video_perspective_texturing =
|
||||
toml::find<bool>(video, "perspective_texturing");
|
||||
}
|
||||
if (video.contains("pgxp_cpu_mode")) {
|
||||
rt.video_pgxp_cpu_mode = toml::find<bool>(video, "pgxp_cpu_mode");
|
||||
}
|
||||
if (video.contains("pgxp_tolerance")) {
|
||||
rt.video_pgxp_tolerance =
|
||||
toml::find<double>(video, "pgxp_tolerance");
|
||||
}
|
||||
if (video.contains("crt_filter")) {
|
||||
const auto mode = toml::find<std::string>(video, "crt_filter");
|
||||
if (mode == "raw") rt.video_screen_kind = 0;
|
||||
@@ -702,6 +712,16 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
toml::find<std::string>(ct, "p2_mode"), PAD_MODE_ANALOG);
|
||||
rt.has_default_mode = true;
|
||||
}
|
||||
if (ct.contains("p1_device")) {
|
||||
rt.default_p1_device = toml::find<std::string>(ct, "p1_device");
|
||||
if (!rt.default_p1_device.empty())
|
||||
rt.has_default_p1_device = true;
|
||||
}
|
||||
if (ct.contains("p2_device")) {
|
||||
rt.default_p2_device = toml::find<std::string>(ct, "p2_device");
|
||||
if (!rt.default_p2_device.empty())
|
||||
rt.has_default_p2_device = true;
|
||||
}
|
||||
if (ct.contains("lock_mode")) {
|
||||
rt.controller_lock_mode = toml::find<bool>(ct, "lock_mode");
|
||||
}
|
||||
@@ -1180,6 +1200,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
bool has_netplay_required_leadout = false;
|
||||
uint32_t netplay_required_leadout_lba = 0;
|
||||
std::string netplay_required_disc_fp;
|
||||
std::string netplay_local_viewport;
|
||||
std::string netplay_local_viewport_aspect;
|
||||
if (cfg.contains("netplay")) {
|
||||
const toml::value& np = toml::find(cfg, "netplay");
|
||||
if (np.contains("require_cue"))
|
||||
@@ -1196,6 +1218,37 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
for (char& c : netplay_required_disc_fp)
|
||||
c = (char)std::tolower((unsigned char)c);
|
||||
}
|
||||
if (np.contains("local_viewport")) {
|
||||
netplay_local_viewport = toml::find<std::string>(np, "local_viewport");
|
||||
for (char& c : netplay_local_viewport)
|
||||
c = (char)std::tolower((unsigned char)c);
|
||||
if (!netplay_local_viewport.empty() &&
|
||||
netplay_local_viewport != "vertical_split") {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"[netplay] local_viewport must be \"vertical_split\", got '{}'",
|
||||
netplay_local_viewport));
|
||||
}
|
||||
}
|
||||
if (np.contains("local_viewport_aspect")) {
|
||||
netplay_local_viewport_aspect =
|
||||
toml::find<std::string>(np, "local_viewport_aspect");
|
||||
for (char& c : netplay_local_viewport_aspect)
|
||||
c = (char)std::tolower((unsigned char)c);
|
||||
if (!netplay_local_viewport_aspect.empty() &&
|
||||
netplay_local_viewport_aspect != "16:9" &&
|
||||
netplay_local_viewport_aspect != "21:9" &&
|
||||
netplay_local_viewport_aspect != "adaptive") {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"[netplay] local_viewport_aspect must be \"16:9\", "
|
||||
"\"21:9\", or \"adaptive\", got '{}'",
|
||||
netplay_local_viewport_aspect));
|
||||
}
|
||||
if (!netplay_local_viewport_aspect.empty() &&
|
||||
netplay_local_viewport.empty()) {
|
||||
throw std::runtime_error(
|
||||
"[netplay] local_viewport_aspect requires local_viewport");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [recompiler]
|
||||
@@ -1307,6 +1360,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
bool ws_auto_ui_squash = false;
|
||||
bool ws_full_2d = false;
|
||||
bool ws_gte_game_mode = false;
|
||||
bool ws_precise_nclip = false;
|
||||
uint32_t ws_gameplay_state_addr = 0;
|
||||
std::vector<uint32_t> ws_gameplay_state_values;
|
||||
bool ws_native_wide = true;
|
||||
@@ -1454,6 +1508,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
ws_full_2d = toml::find<bool>(ws, "full_2d");
|
||||
if (ws.contains("gte_game_mode"))
|
||||
ws_gte_game_mode = toml::find<bool>(ws, "gte_game_mode");
|
||||
if (ws.contains("precise_nclip"))
|
||||
ws_precise_nclip = toml::find<bool>(ws, "precise_nclip");
|
||||
const bool has_gameplay_state_addr = ws.contains("gameplay_state_addr");
|
||||
const bool has_gameplay_state_values = ws.contains("gameplay_state_values");
|
||||
if (has_gameplay_state_addr != has_gameplay_state_values)
|
||||
@@ -1556,6 +1612,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
std::vector<uint32_t> ws_cull_depth_sites;
|
||||
std::vector<uint32_t> ws_cull_plane_nx_sites;
|
||||
std::vector<uint32_t> ws_cull_xclip_load_sites;
|
||||
std::vector<uint32_t> ws_cull_nclip_keep_sites;
|
||||
std::vector<uint32_t> ws_cull_branch_keep_sites;
|
||||
std::vector<WidescreenCullKeepSite> ws_cull_keep_sites;
|
||||
std::vector<WidescreenAngleSite> ws_cull_angle_sites;
|
||||
WidescreenAspectConeConfig ws_aspect_cone;
|
||||
@@ -1589,6 +1647,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
load_sites("depth_sites", ws_cull_depth_sites);
|
||||
load_sites("plane_nx_sites", ws_cull_plane_nx_sites);
|
||||
load_sites("xclip_load_sites", ws_cull_xclip_load_sites);
|
||||
load_sites("nclip_keep_sites", ws_cull_nclip_keep_sites);
|
||||
load_sites("branch_keep_sites", ws_cull_branch_keep_sites);
|
||||
if (cull.contains("keep")) {
|
||||
std::set<uint32_t> seen;
|
||||
for (const auto& item : toml::find<toml::array>(cull, "keep")) {
|
||||
@@ -1975,6 +2035,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*has_netplay_required_leadout*/ has_netplay_required_leadout,
|
||||
/*netplay_required_leadout_lba*/ netplay_required_leadout_lba,
|
||||
/*netplay_required_disc_fp*/ netplay_required_disc_fp,
|
||||
/*netplay_local_viewport*/ netplay_local_viewport,
|
||||
/*netplay_local_viewport_aspect*/ netplay_local_viewport_aspect,
|
||||
/*seeds_path*/ seeds_path,
|
||||
/*bios_thunks_path*/ bios_thunks_path,
|
||||
/*bios_config_path*/ bios_config_path,
|
||||
@@ -2013,6 +2075,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*ws_cull_depth_sites*/ ws_cull_depth_sites,
|
||||
/*ws_cull_plane_nx_sites*/ ws_cull_plane_nx_sites,
|
||||
/*ws_cull_xclip_load_sites*/ ws_cull_xclip_load_sites,
|
||||
/*ws_cull_nclip_keep_sites*/ ws_cull_nclip_keep_sites,
|
||||
/*ws_cull_branch_keep_sites*/ ws_cull_branch_keep_sites,
|
||||
/*ws_cull_keep_sites*/ ws_cull_keep_sites,
|
||||
/*ws_cull_angle_sites*/ ws_cull_angle_sites,
|
||||
/*ws_aspect_cone*/ ws_aspect_cone,
|
||||
@@ -2027,6 +2091,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*ws_auto_backdrop_preload*/ ws_auto_backdrop_preload,
|
||||
/*ws_full_2d*/ ws_full_2d,
|
||||
/*ws_gte_game_mode*/ ws_gte_game_mode,
|
||||
/*ws_precise_nclip*/ ws_precise_nclip,
|
||||
/*ws_gameplay_state_addr*/ ws_gameplay_state_addr,
|
||||
/*ws_gameplay_state_values*/ ws_gameplay_state_values,
|
||||
/*ws_native_wide*/ ws_native_wide,
|
||||
@@ -2103,6 +2168,10 @@ GameOptions load_game_options(const fs::path& path) {
|
||||
|
||||
// ---- UserSettings (settings.toml) — launcher-written override layer ----
|
||||
|
||||
static bool valid_user_audio_freq(int n) {
|
||||
return n == 32040 || n == 32000 || n == 44100 || n == 48000;
|
||||
}
|
||||
|
||||
UserSettings load_user_settings(const fs::path& path) {
|
||||
UserSettings s;
|
||||
std::error_code ec;
|
||||
@@ -2245,6 +2314,13 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
}
|
||||
if (doc.contains("audio")) {
|
||||
const toml::value& a = toml::find(doc, "audio");
|
||||
if (a.contains("frequency")) try_get([&]{
|
||||
const auto n = toml::find<int64_t>(a, "frequency");
|
||||
if (valid_user_audio_freq((int)n)) {
|
||||
s.audio_freq = (int)n;
|
||||
s.has_audio_freq = true;
|
||||
}
|
||||
});
|
||||
if (a.contains("spu_hq")) try_get([&]{
|
||||
s.spu_hq = toml::find<bool>(a, "spu_hq"); s.has_spu_hq = true;
|
||||
});
|
||||
@@ -2487,6 +2563,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
if (s.has_rewind_interval)
|
||||
f << "rewind_interval = " << s.rewind_interval << "\n";
|
||||
f << "\n[audio]\n";
|
||||
if (s.has_audio_freq)
|
||||
f << "frequency = " << s.audio_freq << "\n";
|
||||
if (s.has_spu_hq)
|
||||
f << "spu_hq = " << (s.spu_hq ? "true" : "false") << "\n";
|
||||
if (s.has_hotkey_pad_rewind || s.has_hotkey_pad_save_state_menu) {
|
||||
|
||||
@@ -387,6 +387,24 @@ struct RuntimeConfig {
|
||||
// [video] perspective_texturing = true.
|
||||
bool video_perspective_texturing = false;
|
||||
|
||||
// pgxp_cpu_mode: propagate sub-pixel precision through CPU arithmetic as
|
||||
// well as memory moves (the PGXP engine's tier-2 hooks). Off by default —
|
||||
// the same default as the reference implementations — because some games
|
||||
// deliberately rely on integer truncation in their own math; value
|
||||
// validation keeps it SAFE either way, this only trades coverage.
|
||||
// Meaningful only in a pgxp-flavour build; live-tunable over TCP.
|
||||
bool video_pgxp_cpu_mode = false;
|
||||
|
||||
// pgxp_tolerance: reject a corrected vertex whose sub-pixel offset from
|
||||
// the native integer position exceeds this many pixels (the truncation-
|
||||
// agreement check already bounds offsets to < 1px; this narrows them
|
||||
// further). Default 0.5 — user-validated on Ape Escape (2026-08-15):
|
||||
// unclamped, sparse hairline background-bleed seams appear where a
|
||||
// corrected triangle borders an uncorrected one; at 0.5 the seams are
|
||||
// gone and only sub-half-pixel misalignment remains. Negative disables
|
||||
// the clamp. Live-tunable over TCP (pgxp verb).
|
||||
double video_pgxp_tolerance = 0.5;
|
||||
|
||||
// offer_vulkan: expose the experimental Vulkan renderer in the launcher.
|
||||
// Defaults false even for Vulkan-enabled builds; developers must opt in per
|
||||
// game once visuals are validated.
|
||||
@@ -497,6 +515,15 @@ struct RuntimeConfig {
|
||||
int default_p1_mode = PAD_MODE_ANALOG;
|
||||
int default_p2_mode = PAD_MODE_ANALOG;
|
||||
|
||||
// p1_device / p2_device: optional per-game default input sources for
|
||||
// fresh settings and launcher-less boots. Same vocabulary as settings.toml:
|
||||
// "none", "keyboard", "auto"/"gamepad"/"controller", or an SDL GUID.
|
||||
// Per-install settings.toml still overrides these defaults.
|
||||
bool has_default_p1_device = false;
|
||||
bool has_default_p2_device = false;
|
||||
std::string default_p1_device;
|
||||
std::string default_p2_device;
|
||||
|
||||
// lock_mode: when true the launcher HIDES the whole pad-mode selector
|
||||
// (Hybrid | Analog | D-Pad) and forces every port to default_p1_mode. For a
|
||||
// game that supports exactly one pad type — e.g. Tomba 2, whose driver only
|
||||
@@ -676,6 +703,15 @@ struct GameConfig {
|
||||
bool has_netplay_required_leadout = false;
|
||||
uint32_t netplay_required_leadout_lba = 0;
|
||||
std::string netplay_required_disc_fp; // lowercase hex SHA-256
|
||||
// local_viewport = "vertical_split": while real netplay is active, crop
|
||||
// presentation to this peer's left/right split-screen half. This is a
|
||||
// presentation-only helper for titles that still render native split-screen
|
||||
// in netplay; unset keeps every peer seeing the full framebuffer.
|
||||
std::string netplay_local_viewport;
|
||||
// Optional display aspect to use with local_viewport. Accepted values:
|
||||
// "16:9", "21:9", or "adaptive" (initial 16:9, live-window capped 21:9).
|
||||
// Unset keeps netplay at the title's normal mod-cleared aspect.
|
||||
std::string netplay_local_viewport_aspect;
|
||||
|
||||
// [recompiler] block
|
||||
std::filesystem::path seeds_path; // absolute path to seeds (text or json)
|
||||
@@ -812,6 +848,14 @@ struct GameConfig {
|
||||
// scissor clips the overflow and wrapped off-left coords pass); the
|
||||
// vanilla loaded value at 4:3. Empty by default; regen required.
|
||||
std::vector<uint32_t> ws_cull_xclip_load_sites;
|
||||
// Exact `bltz MAC0, reject`-style NCLIP/backface rejects that are forced
|
||||
// not-taken only while widescreen reveals extra world. This is deliberately
|
||||
// separate from bltz_sites, whose helper adjusts screen-X edge thresholds.
|
||||
std::vector<uint32_t> ws_cull_nclip_keep_sites;
|
||||
// Exact branch PCs whose reject path is forced not-taken only while
|
||||
// widescreen reveals extra world. Use only after screenshot-validated
|
||||
// evidence that the target is a visibility reject.
|
||||
std::vector<uint32_t> ws_cull_branch_keep_sites;
|
||||
// Exact comparison sites whose result is forced only while widescreen
|
||||
// reveals extra world. Used for proven object/model participation gates
|
||||
// where maximal overdraw is preferable to range guessing. Each entry is
|
||||
@@ -903,6 +947,14 @@ struct GameConfig {
|
||||
// (native-wide engages); genuine full-2D screens (save/options) still
|
||||
// pillarbox 4:3. Runtime-only — no regen required. Off by default.
|
||||
bool ws_gte_game_mode = false;
|
||||
|
||||
// [widescreen] precise_nclip — use the runtime's unsaturated GTE projection
|
||||
// provenance for NCLIP/backface tests while classic adaptive widescreen is
|
||||
// active. This is for 3D titles whose wide side geometry otherwise hits the
|
||||
// PS1 SXY +/-1024 clamp and then disappears from game-side visibility tests.
|
||||
// Runtime-only; off by default.
|
||||
bool ws_precise_nclip = false;
|
||||
|
||||
// Optional authoritative game-state gate for titles whose menus also
|
||||
// render enough 3D geometry to fool gte_game_mode. When configured,
|
||||
// native-wide is active only while the guest word matches one listed
|
||||
@@ -1115,6 +1167,7 @@ struct UserSettings {
|
||||
bool has_hotkey_pad_save_state_menu = false; int hotkey_pad_save_state_menu = 2040; /* select+r1 */
|
||||
// [audio]
|
||||
bool has_spu_hq = false; bool spu_hq = false;
|
||||
bool has_audio_freq = false; int audio_freq = 44100;
|
||||
// [bios] / [disc] / [memcard]
|
||||
bool has_bios_path = false; std::filesystem::path bios_path;
|
||||
bool has_disc_path = false; std::filesystem::path disc_path;
|
||||
|
||||
@@ -209,6 +209,8 @@ int main(int argc, char** argv) {
|
||||
std::set<uint32_t> ws_cull_depth; // [widescreen.cull] depth_sites
|
||||
std::set<uint32_t> ws_cull_plane_nx; // [widescreen.cull] plane_nx_sites
|
||||
std::set<uint32_t> ws_cull_xclip_load; // [widescreen.cull] xclip_load_sites
|
||||
std::set<uint32_t> ws_cull_nclip_keep; // [widescreen.cull] nclip_keep_sites
|
||||
std::set<uint32_t> ws_cull_branch_keep; // [widescreen.cull] branch_keep_sites
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
|
||||
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle;
|
||||
PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone;
|
||||
@@ -270,6 +272,8 @@ int main(int argc, char** argv) {
|
||||
ws_cull_depth.insert(cfg.ws_cull_depth_sites.begin(), cfg.ws_cull_depth_sites.end());
|
||||
ws_cull_plane_nx.insert(cfg.ws_cull_plane_nx_sites.begin(), cfg.ws_cull_plane_nx_sites.end());
|
||||
ws_cull_xclip_load.insert(cfg.ws_cull_xclip_load_sites.begin(), cfg.ws_cull_xclip_load_sites.end());
|
||||
ws_cull_nclip_keep.insert(cfg.ws_cull_nclip_keep_sites.begin(), cfg.ws_cull_nclip_keep_sites.end());
|
||||
ws_cull_branch_keep.insert(cfg.ws_cull_branch_keep_sites.begin(), cfg.ws_cull_branch_keep_sites.end());
|
||||
ws_cull_keep = cfg.ws_cull_keep_sites;
|
||||
ws_cull_angle = cfg.ws_cull_angle_sites;
|
||||
ws_aspect_cone = cfg.ws_aspect_cone;
|
||||
@@ -363,6 +367,8 @@ int main(int argc, char** argv) {
|
||||
ws_cull_depth.insert(wscfg.ws_cull_depth_sites.begin(), wscfg.ws_cull_depth_sites.end());
|
||||
ws_cull_plane_nx.insert(wscfg.ws_cull_plane_nx_sites.begin(), wscfg.ws_cull_plane_nx_sites.end());
|
||||
ws_cull_xclip_load.insert(wscfg.ws_cull_xclip_load_sites.begin(), wscfg.ws_cull_xclip_load_sites.end());
|
||||
ws_cull_nclip_keep.insert(wscfg.ws_cull_nclip_keep_sites.begin(), wscfg.ws_cull_nclip_keep_sites.end());
|
||||
ws_cull_branch_keep.insert(wscfg.ws_cull_branch_keep_sites.begin(), wscfg.ws_cull_branch_keep_sites.end());
|
||||
if (ws_cull_keep.empty()) ws_cull_keep = wscfg.ws_cull_keep_sites;
|
||||
if (ws_cull_angle.empty()) ws_cull_angle = wscfg.ws_cull_angle_sites;
|
||||
if (ws_aspect_cone.sites.empty())
|
||||
@@ -1225,6 +1231,8 @@ int main(int argc, char** argv) {
|
||||
codegen_config.ws_cull_depth_sites = ws_cull_depth;
|
||||
codegen_config.ws_cull_plane_nx_sites = ws_cull_plane_nx;
|
||||
codegen_config.ws_cull_xclip_load_sites = ws_cull_xclip_load;
|
||||
codegen_config.ws_cull_nclip_keep_sites = ws_cull_nclip_keep;
|
||||
codegen_config.ws_cull_branch_keep_sites = ws_cull_branch_keep;
|
||||
codegen_config.ws_cull_keep_sites = ws_cull_keep;
|
||||
codegen_config.ws_cull_angle_sites = ws_cull_angle;
|
||||
codegen_config.ws_aspect_cone = ws_aspect_cone;
|
||||
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -142,6 +142,36 @@ buffer_ms = 60
|
||||
check(audio_config.runtime.audio_buffer_ms == 60,
|
||||
"parser preserves per-game audio buffer target");
|
||||
|
||||
const auto controller_defaults = write_config(root, "controller-defaults", R"toml(
|
||||
[controller]
|
||||
p1_device = "keyboard"
|
||||
p2_device = "gamepad"
|
||||
p2_mode = "digital"
|
||||
)toml");
|
||||
const auto controller_defaults_config =
|
||||
PSXRecompV4::load_game_config(controller_defaults);
|
||||
check(controller_defaults_config.runtime.has_default_p1_device &&
|
||||
controller_defaults_config.runtime.default_p1_device == "keyboard" &&
|
||||
controller_defaults_config.runtime.has_default_p2_device &&
|
||||
controller_defaults_config.runtime.default_p2_device == "gamepad" &&
|
||||
controller_defaults_config.runtime.default_p2_mode ==
|
||||
PSXRecompV4::PAD_MODE_DIGITAL,
|
||||
"parser preserves per-game controller device defaults");
|
||||
|
||||
const auto netplay_viewport = write_config(root, "netplay-viewport", R"toml(
|
||||
[runtime]
|
||||
|
||||
[netplay]
|
||||
local_viewport = "vertical_split"
|
||||
local_viewport_aspect = "adaptive"
|
||||
)toml");
|
||||
const auto netplay_viewport_config =
|
||||
PSXRecompV4::load_game_config(netplay_viewport);
|
||||
check(netplay_viewport_config.netplay_local_viewport == "vertical_split",
|
||||
"parser preserves netplay local viewport mode");
|
||||
check(netplay_viewport_config.netplay_local_viewport_aspect == "adaptive",
|
||||
"parser preserves netplay local viewport aspect");
|
||||
|
||||
const auto bad_audio_buffer = write_config(root, "bad-audio-buffer", R"toml(
|
||||
[runtime]
|
||||
|
||||
@@ -206,6 +236,15 @@ negsub_sites = ["0x80012340"]
|
||||
std::vector<uint32_t>{0x80012340u},
|
||||
"parser preserves negsub cull sites");
|
||||
|
||||
const auto branch_keep = write_config(root, "branch-keep", R"toml(
|
||||
[widescreen.cull]
|
||||
branch_keep_sites = ["0x80012340"]
|
||||
)toml");
|
||||
const auto branch_keep_config = PSXRecompV4::load_game_config(branch_keep);
|
||||
check(branch_keep_config.ws_cull_branch_keep_sites ==
|
||||
std::vector<uint32_t>{0x80012340u},
|
||||
"parser preserves branch keep sites");
|
||||
|
||||
const auto vxrange = write_config(root, "vxrange", R"toml(
|
||||
[widescreen.cull]
|
||||
vxrange_sites = ["0x80012340"]
|
||||
@@ -680,6 +719,16 @@ void codegen_tests() {
|
||||
std::string::npos,
|
||||
"activation bias gains the same isolated resident-object lead");
|
||||
|
||||
PSXRecomp::CodeGenConfig branch_keep_config;
|
||||
branch_keep_config.ws_cull_branch_keep_sites.insert(0x80010000u);
|
||||
const std::string branch_keep = generate_first_instruction(
|
||||
0x14400002u, {}, false, branch_keep_config); // bne v0,zero,+2
|
||||
check(branch_keep.find(
|
||||
"psx_ws_x_margin() > 0 ? 0 : (cpu->gpr[2] != cpu->gpr[0])") !=
|
||||
std::string::npos &&
|
||||
branch_keep.find("ws branch keep") != std::string::npos,
|
||||
"codegen emits guarded branch keep predicate");
|
||||
|
||||
PSXRecomp::CodeGenConfig plane_nx_config;
|
||||
plane_nx_config.ws_cull_plane_nx_sites.insert(0x80010000u);
|
||||
const std::string plane_nx = generate_first_instruction(
|
||||
@@ -814,6 +863,19 @@ void gte_codegen_classification_tests() {
|
||||
all_ok = false;
|
||||
}
|
||||
};
|
||||
const auto expect_zero_destination = [&](uint32_t word,
|
||||
const std::string& read,
|
||||
const char *kind) {
|
||||
const std::string code = generate_first_instruction(word, {}, false);
|
||||
if (code.find("cpu->gpr[0] =") != std::string::npos ||
|
||||
code.find(read) == std::string::npos ||
|
||||
code.find("psx_gte_read(cpu, 0)") == std::string::npos) {
|
||||
fmt::print(stderr,
|
||||
"FAIL full-game GTE {} reg {} writes $zero or drops read/timing\n",
|
||||
kind, reg);
|
||||
all_ok = false;
|
||||
}
|
||||
};
|
||||
|
||||
const uint32_t cop2 = 0x12u << 26;
|
||||
expect(cop2 | (0x00u << 21) | (2u << 16) | (uint32_t(reg) << 11),
|
||||
@@ -822,6 +884,18 @@ void gte_codegen_classification_tests() {
|
||||
expect(cop2 | (0x02u << 21) | (2u << 16) | (uint32_t(reg) << 11),
|
||||
fmt::format("gte_read_ctrl(cpu, {})", reg),
|
||||
PSXRecompGTERegisters::ctrl_read_needs_helper(reg), "CFC2");
|
||||
expect_zero_destination(
|
||||
cop2 | (0x00u << 21) | (0u << 16) | (uint32_t(reg) << 11),
|
||||
PSXRecompGTERegisters::data_read_needs_helper(reg)
|
||||
? fmt::format("gte_read_data(cpu, {})", reg)
|
||||
: fmt::format("cpu->gte_data[{}]", reg),
|
||||
"MFC2");
|
||||
expect_zero_destination(
|
||||
cop2 | (0x02u << 21) | (0u << 16) | (uint32_t(reg) << 11),
|
||||
PSXRecompGTERegisters::ctrl_read_needs_helper(reg)
|
||||
? fmt::format("gte_read_ctrl(cpu, {})", reg)
|
||||
: fmt::format("cpu->gte_ctrl[{}]", reg),
|
||||
"CFC2");
|
||||
expect(cop2 | (0x04u << 21) | (2u << 16) | (uint32_t(reg) << 11),
|
||||
fmt::format("gte_write_data(cpu, {}", reg),
|
||||
PSXRecompGTERegisters::data_write_needs_helper(reg), "MTC2");
|
||||
|
||||
@@ -122,13 +122,17 @@ static void test_user_settings_read() {
|
||||
fs::path p = write_temp("psxrecomp_pgxp_settings.toml",
|
||||
"[video]\n"
|
||||
"geometry_correction = true\n"
|
||||
"perspective_texturing = false\n");
|
||||
"perspective_texturing = false\n"
|
||||
"[audio]\n"
|
||||
"frequency = 48000\n");
|
||||
auto us = PSXRecompV4::load_user_settings(p);
|
||||
check(!us.parse_error, "settings.toml parses");
|
||||
check(us.has_geometry_correction && us.geometry_correction,
|
||||
"settings.toml geometry_correction = true read");
|
||||
check(us.has_perspective_texturing && !us.perspective_texturing,
|
||||
"settings.toml perspective_texturing = false read (explicit off)");
|
||||
check(us.has_audio_freq && us.audio_freq == 48000,
|
||||
"settings.toml audio frequency read");
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
@@ -150,6 +154,7 @@ static void test_user_settings_round_trip() {
|
||||
PSXRecompV4::UserSettings out;
|
||||
out.geometry_correction = true; out.has_geometry_correction = true;
|
||||
out.perspective_texturing = true; out.has_perspective_texturing = true;
|
||||
out.audio_freq = 48000; out.has_audio_freq = true;
|
||||
|
||||
fs::path p = fs::temp_directory_path() / "psxrecomp_pgxp_roundtrip.toml";
|
||||
check(PSXRecompV4::save_user_settings(p, out), "save_user_settings writes");
|
||||
@@ -160,6 +165,8 @@ static void test_user_settings_round_trip() {
|
||||
"geometry_correction survives a save/load round trip");
|
||||
check(back.has_perspective_texturing && back.perspective_texturing,
|
||||
"perspective_texturing survives a save/load round trip");
|
||||
check(back.has_audio_freq && back.audio_freq == 48000,
|
||||
"audio frequency survives a save/load round trip");
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,11 @@ psxrecomp_add_runtime_target(psx-runtime
|
||||
EXE_NAME "PSXRecomp"
|
||||
)
|
||||
|
||||
# With -DPSX_PGXP_VARIANT=ON, psxrecomp_add_runtime_target auto-clones a
|
||||
# psx-runtime-pgxp sibling (PSXRecomp_pgxp.exe) compiled with -DPSX_PGXP=1 —
|
||||
# the framework-dev validation surface for the PGXP value-propagation engine
|
||||
# (ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(launcher_device_roundtrip_test
|
||||
tests/test_launcher_device_roundtrip.cpp)
|
||||
@@ -52,6 +57,17 @@ if(BUILD_TESTING)
|
||||
PSX_OVERLAY_DLL_BUILD=1)
|
||||
add_test(NAME psx_icache_fastpath_test COMMAND psx_icache_fastpath_test)
|
||||
|
||||
add_executable(host_keymap_test
|
||||
tests/test_host_keymap.c
|
||||
src/host_keymap.c)
|
||||
target_include_directories(host_keymap_test PRIVATE
|
||||
include ${PSX_SDL_INCLUDE_DIRS})
|
||||
target_compile_definitions(host_keymap_test PRIVATE
|
||||
SDL_MAIN_HANDLED
|
||||
$<$<BOOL:${PSX_SDL3}>:PSX_SDL3=1>)
|
||||
target_link_libraries(host_keymap_test PRIVATE ${PSX_SDL_LIBRARIES})
|
||||
add_test(NAME host_keymap_test COMMAND host_keymap_test)
|
||||
|
||||
# Registered since before this change, but it did NOT LINK on origin/master:
|
||||
# sio.c had grown psx_read_word / psx_get_cycle_count / i_mask /
|
||||
# g_debug_last_store_pc references that this test's stub block never gained,
|
||||
@@ -72,6 +88,7 @@ if(BUILD_TESTING)
|
||||
add_executable(gte_register_access_test
|
||||
tests/test_gte_register_access.cpp
|
||||
src/gte.cpp
|
||||
src/pgxp.cpp
|
||||
)
|
||||
target_include_directories(gte_register_access_test PRIVATE include)
|
||||
target_compile_definitions(gte_register_access_test PRIVATE
|
||||
@@ -80,6 +97,16 @@ if(BUILD_TESTING)
|
||||
PSX_ENABLE_BLOCK_CYCLES=1)
|
||||
add_test(NAME gte_register_access_test COMMAND gte_register_access_test)
|
||||
|
||||
# PGXP value-propagation engine (ENHANCEMENTS.md G1.2/G1.3): shadow
|
||||
# load/store/COP2 roundtrips, half-word semantics, validate-on-read
|
||||
# (foreign overwrite + DMA-overwrite rejection), MOVE/repack arithmetic,
|
||||
# suppression bracket, invalidate-all, and GetPreciseVertex safeguards
|
||||
# (truncation agreement + tolerance clamp). White-box: includes src/pgxp.cpp
|
||||
# and stubs the gte.cpp fallback-cache extern.
|
||||
add_executable(pgxp_test tests/test_pgxp.cpp src/pgxp.cpp)
|
||||
target_include_directories(pgxp_test PRIVATE include)
|
||||
add_test(NAME pgxp_test COMMAND pgxp_test)
|
||||
|
||||
add_executable(psx_instr_cost_test tests/test_psx_instr_cost.cpp)
|
||||
target_include_directories(psx_instr_cost_test PRIVATE include)
|
||||
add_test(NAME psx_instr_cost_test COMMAND psx_instr_cost_test)
|
||||
@@ -178,6 +205,14 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(iso_reader_cdda_test PRIVATE chdr-static)
|
||||
add_test(NAME iso_reader_cdda_test COMMAND iso_reader_cdda_test)
|
||||
|
||||
add_executable(iso_reader_sbi_test
|
||||
tests/test_iso_reader_sbi.cpp
|
||||
src/iso_reader.cpp
|
||||
src/cue_sheet.cpp)
|
||||
target_include_directories(iso_reader_sbi_test PRIVATE include)
|
||||
target_link_libraries(iso_reader_sbi_test PRIVATE chdr-static)
|
||||
add_test(NAME iso_reader_sbi_test COMMAND iso_reader_sbi_test)
|
||||
|
||||
add_executable(mod_packages_test
|
||||
tests/test_mod_packages.cpp
|
||||
src/mod_packages.cpp
|
||||
|
||||
@@ -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
|
||||
@@ -44,6 +46,7 @@ set(PSXRECOMP_CODEGEN_HASH_SRCS
|
||||
# .inc is the shim every shard links against.
|
||||
${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/overlay_api.h
|
||||
${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/cpu_state.h
|
||||
${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/pgxp_hooks.h
|
||||
${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/psx_cyc.h
|
||||
${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/psx_cycles.h
|
||||
${PSXRECOMP_CODEGEN_HASH_ROOT}/runtime/include/overlay_dispatch_preamble.c.inc)
|
||||
|
||||
@@ -38,10 +38,11 @@ extern "C" {
|
||||
#define BOOT_STATE_MAGIC 0x50535842u /* "PSXB" */
|
||||
/* v1 = incomplete RAM-only; v2 = full machine but host-struct memcpy (padding);
|
||||
* v3 = little-endian field wire (portable Win/Linux/macOS ARM);
|
||||
* v4 = v3 + optional zlib on large sections (section pad bit0 = compressed). */
|
||||
#define BOOT_STATE_VERSION 4u
|
||||
/* Older readers reject v4; load still accepts v3 uncompressed blobs. */
|
||||
#define BOOT_STATE_VERSION_MIN_READ 3u
|
||||
* v4 = v3 + optional zlib on large sections (section pad bit0 = compressed);
|
||||
* v5 = v4 + CD-ROM Sub-Q replacement state. */
|
||||
#define BOOT_STATE_VERSION 5u
|
||||
/* v5 intentionally breaks older savestates after the CD-ROM wire grew. */
|
||||
#define BOOT_STATE_VERSION_MIN_READ 5u
|
||||
/* Section pad bit0: payload is u32 LE uncompressed_len + zlib deflate bytes. */
|
||||
#define BOOT_STATE_SEC_ZLIB 1u
|
||||
|
||||
|
||||
@@ -205,6 +205,7 @@ extern void gte_precision_timeline_invalidate(void);
|
||||
extern void gte_precision_speculative_begin(void);
|
||||
extern void gte_precision_speculative_end(void);
|
||||
extern void gte_precision_store_word(uint32_t addr, uint8_t reg);
|
||||
extern void gte_precision_tracking_set(int enabled);
|
||||
/* Sub-pixel vertex precision ([video] geometry_correction). Enables the side
|
||||
* cache that retains the 16.16 projection fraction the GTE discards when it
|
||||
* saturates SXY to integer screen pixels. Guest-visible GTE state is
|
||||
@@ -220,6 +221,12 @@ extern void gte_geometry_correction_stats(uint32_t *lookups, uint32_t *hits,
|
||||
uint32_t *miss_unrecorded,
|
||||
uint32_t *miss_ambiguous);
|
||||
|
||||
/* PGXP dataflow-shadowing hook macros (PGXP_LOAD/STORE/ALU/MULDIV/COP2).
|
||||
* The emitter writes them unconditionally; they expand to real calls only
|
||||
* under -DPSX_PGXP=1 (the pgxp build variant) and to ((void)0) otherwise.
|
||||
* Pulled in here so every generated translation unit sees them. */
|
||||
#include "pgxp_hooks.h"
|
||||
|
||||
/* ============================================================================
|
||||
* Dispatch call contract (Bug D / wild-return family fix)
|
||||
* ----------------------------------------------------------------------------
|
||||
|
||||
@@ -75,6 +75,11 @@ typedef struct {
|
||||
int32_t offset_x, offset_y;
|
||||
} GpuDrawArea;
|
||||
void gpu_get_draw_area(GpuDrawArea* out);
|
||||
/* Non-zero when the frame that just reached vblank used two side-by-side draw
|
||||
* areas. Presentation-only helpers use this to expand a local split-screen
|
||||
* viewport without changing the underlying framebuffer or netplay hashes. */
|
||||
int gpu_last_frame_vertical_split_screen(void);
|
||||
void gpu_vertical_split_debug(int *active, int *left_age, int *right_age);
|
||||
uint16_t gpu_vram_peek(int x, int y);
|
||||
|
||||
/* Shaded quad vertex capture (Phase 4.5 debug). */
|
||||
@@ -207,6 +212,10 @@ void psx_ws_sprite_tag(struct CPUState* cpu);
|
||||
* by this; 0 when native-wide is inactive). */
|
||||
int ws_native_wide_active(void);
|
||||
int ws_nw_extra(void);
|
||||
int ws_nw_present_width(void);
|
||||
void gpu_ws_set_netplay_local_viewport(int enabled, int slot);
|
||||
int gpu_ws_netplay_local_viewport_base_x(void);
|
||||
int gpu_ws_netplay_local_viewport_width(void);
|
||||
/* True when the current frame must present at native 4:3 (FMV video or a
|
||||
* full-2D menu/title screen), so the squash is suppressed and content drawn
|
||||
* pixel-native. The present path uses the same predicate to pillarbox. */
|
||||
@@ -310,12 +319,14 @@ int psx_ws_auto_cull_on(void);
|
||||
* tracking it needs, so a polygon only qualifies when every position word in
|
||||
* its DMA packet was written by a GTE projection store. Default off. */
|
||||
void gpu_texture_correction_set(int enabled);
|
||||
int gpu_texture_correction_enabled(void);
|
||||
/* Triangles drawn with perspective-correct UVs since startup. */
|
||||
uint32_t gpu_texture_correction_hits(void);
|
||||
/* GTE-activity gameplay detector ([widescreen] gte_game_mode) for 3D titles
|
||||
* with no sprite-tag helper: gte.cpp notes every RTPS/RTPT projection; a frame
|
||||
* that projects enough vertices is stamped as gameplay. */
|
||||
void gpu_ws_set_gte_game_mode(int on);
|
||||
void gpu_ws_set_precise_nclip(int on);
|
||||
void psx_ws_note_gte_project(int nverts);
|
||||
/* Optional authoritative gameplay-state gate. When configured, it replaces
|
||||
* heuristic gameplay classification for native-wide presentation. */
|
||||
|
||||
@@ -10,13 +10,15 @@ extern "C" {
|
||||
|
||||
/*
|
||||
* Host hotkeys from recomp-ui config.ini [KeyMap] (SDL key names + Ctrl+/Alt+/
|
||||
* Shift+ prefixes). VolumeUp / VolumeDown are honored by the runtime; missing
|
||||
* lines keep the historic keypad +/- defaults.
|
||||
* Shift+ prefixes). Missing lines keep the historic runtime defaults.
|
||||
*/
|
||||
|
||||
typedef enum HostKeymapAction {
|
||||
HOST_KEYMAP_VOLUME_UP = 0,
|
||||
HOST_KEYMAP_FULLSCREEN = 0,
|
||||
HOST_KEYMAP_TURBO,
|
||||
HOST_KEYMAP_VOLUME_UP,
|
||||
HOST_KEYMAP_VOLUME_DOWN,
|
||||
HOST_KEYMAP_DISPLAY_PERF,
|
||||
HOST_KEYMAP_REWIND, /* default F8 */
|
||||
HOST_KEYMAP_SAVE_STATE_MENU, /* default F7 */
|
||||
HOST_KEYMAP_ACTION_COUNT
|
||||
@@ -28,6 +30,9 @@ void host_keymap_load(const char *config_ini_path);
|
||||
/* 1 if (keycode, mod) matches a binding for `action`. */
|
||||
int host_keymap_match(HostKeymapAction action, int keycode, int mod);
|
||||
|
||||
/* 1 if any configured bind for `action` is currently held. */
|
||||
int host_keymap_down(HostKeymapAction action, const uint8_t *keys, int mod);
|
||||
|
||||
/* Format the primary bind for `action` into out (e.g. "F8", "Ctrl+R",
|
||||
* "GRAVE"). Punctuation key names are tokenized for the rewind overlay font.
|
||||
* Returns out, or "F8" / "" on empty action. Always NUL-terminates. */
|
||||
|
||||
@@ -19,6 +19,9 @@ extern "C" {
|
||||
/* Top-left text toast. duration_ms <= 0 uses 2000. */
|
||||
void host_osd_push(const char *msg, int duration_ms);
|
||||
|
||||
/* Persistent top-left status text. Passing NULL/empty clears it. */
|
||||
void host_osd_set_status(const char *msg);
|
||||
|
||||
/* Right-side vertical volume bar (percent clamped 0..100). */
|
||||
void host_osd_show_volume(int percent, int duration_ms);
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
@@ -108,7 +110,15 @@ public:
|
||||
* @param buffer Buffer to read into (must be at least 2352 bytes)
|
||||
* @return true if the image stores raw sectors and read succeeded
|
||||
*/
|
||||
bool ReadRawSector(uint32_t lba, uint8_t* buffer);
|
||||
bool ReadRawSector(uint32_t lba, uint8_t* buffer);
|
||||
|
||||
/**
|
||||
* Read generated or SBI-replacement subchannel Q for a disc LBA.
|
||||
* SBI records intentionally carry invalid CRC data; valid is false for
|
||||
* those sectors so the CD controller can retain its last valid Q packet.
|
||||
*/
|
||||
bool ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) const;
|
||||
bool HasSubChannelReplacements() const { return !subq_replacements_.empty(); }
|
||||
|
||||
/**
|
||||
* Check if a file is currently open
|
||||
@@ -213,7 +223,9 @@ private:
|
||||
* Helper: find the BinSegment containing a disc-relative LBA.
|
||||
* @return segment pointer, or nullptr when lba is past the disc end
|
||||
*/
|
||||
BinSegment* SegmentForLBA(uint32_t lba);
|
||||
BinSegment* SegmentForLBA(uint32_t lba);
|
||||
|
||||
bool LoadSBICompanion(const std::string& image_path);
|
||||
|
||||
bool is_open_;
|
||||
std::string volume_id_;
|
||||
@@ -222,6 +234,7 @@ private:
|
||||
std::vector<CDTrack> tracks_; // from the .cue TOC; >=1 entry after Open()
|
||||
std::vector<BinSegment> segments_; // cue FILE entries in disc order; >=1 after Open()
|
||||
std::unique_ptr<CHDState> chd_; // present only for a directly mounted CHD
|
||||
std::unordered_map<uint32_t, std::array<uint8_t, 12>> subq_replacements_;
|
||||
};
|
||||
|
||||
} // namespace PS1
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "pgxp_hooks.h"
|
||||
#include <stdint.h>
|
||||
|
||||
/* ABI version exported by every overlay DLL as `overlay_abi()`. The loader
|
||||
@@ -85,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
|
||||
@@ -112,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)))
|
||||
@@ -138,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>/)
|
||||
@@ -291,6 +308,15 @@ typedef struct {
|
||||
|
||||
/* Aspect-scaled terrain-frustum angle helper (ABI v21). */
|
||||
uint32_t (*ws_angle_widen)(uint32_t vanilla);
|
||||
|
||||
/* PGXP dataflow-shadowing hook table (pgxp_hooks.h). Only pgxp-flavour
|
||||
* shards (PSX_OVERLAY_FLAVOR pgxp bit, compiled with -DPSX_PGXP=1) emit
|
||||
* calls to psx_pgxp_*; their preamble forwards through this table. Base-
|
||||
* flavour shards never reference it. Appended LAST; NULL on an older host
|
||||
* no-ops every hook — precision shadowing is visual-only, never
|
||||
* load-bearing. The ABI version bump that arms this ships with the
|
||||
* emitter change (Phase 2 of ENHANCEMENTS.md G1 value propagation). */
|
||||
const PGXPHooks *pgxp;
|
||||
} OverlayCallbacks;
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -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 */
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#ifndef PGXP_H
|
||||
#define PGXP_H
|
||||
|
||||
/* PGXP value-propagation engine — runtime-internal API (pgxp.cpp).
|
||||
*
|
||||
* The engine shadows every 32-bit word of guest RAM/scratchpad, every GPR,
|
||||
* and every GTE data register with the sub-pixel projection it carries
|
||||
* (screen X/Y in 16.16 plus the projected SZ depth). The GTE fills shadows at
|
||||
* RTPS/RTPT; the psx_pgxp_* hooks (pgxp_hooks.h) move them along with the
|
||||
* data; the GPU asks for the precise position of each GP0 vertex word by the
|
||||
* packet's RAM address, validated against the actual word — never guessed
|
||||
* from the rounded position (the measured G1.4 dead end).
|
||||
*
|
||||
* Guest-visible state is NEVER touched: shadows are host-only, dropped on
|
||||
* savestate/rewind, and suppressed during speculative validation passes.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* --- lifecycle / configuration ------------------------------------------ */
|
||||
|
||||
/* Master arm. Allocates the RAM shadow lazily; fails closed (stays disabled)
|
||||
* if the allocation fails. Idempotent. */
|
||||
void pgxp_set_enabled(int enabled);
|
||||
int pgxp_enabled(void);
|
||||
|
||||
/* Tier-2 propagation through CPU arithmetic (default off, like the reference
|
||||
* implementations' default). Off is SAFE — value validation already stops
|
||||
* stale shadows — it only bounds coverage. Live-tunable (TCP). */
|
||||
void pgxp_set_cpu_mode(int enabled);
|
||||
int pgxp_cpu_mode(void);
|
||||
|
||||
/* Reject a precise position whose sub-pixel offset from the native integer
|
||||
* position exceeds this many pixels. < 0 disables the clamp (default). */
|
||||
void pgxp_set_tolerance(float pixels);
|
||||
float pgxp_tolerance(void);
|
||||
|
||||
/* Drop all shadows (savestate load, raw RAM restore, timeline breaks).
|
||||
* O(1) via generation bump. Deferred while suppressed. */
|
||||
void pgxp_invalidate_all(void);
|
||||
|
||||
/* Counted suppression bracket for speculative native-validation passes and
|
||||
* the GTE replay sandbox: hooks and producers no-op inside it. */
|
||||
void pgxp_suppress_begin(void);
|
||||
void pgxp_suppress_end(void);
|
||||
|
||||
/* --- GTE producer (gte.cpp) ---------------------------------------------- */
|
||||
|
||||
/* Called at the RTPS/RTPT projection with the pre-truncation 16.16 screen
|
||||
* coordinates, the projected depth (SZ3), and the packed SXY word the guest
|
||||
* sees. Shifts the shadow FIFO exactly like push_sxy (regs 12..15). */
|
||||
void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3, uint32_t packed);
|
||||
|
||||
/* Current SXY FIFO shadow (index 0..3 selects GTE data regs 12..15).
|
||||
* Returns nonzero when the shadow is live and carries X/Y precision. */
|
||||
int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16);
|
||||
|
||||
/* Guest write to a GTE data register outside gte_execute (MTC2/CTC2 handled
|
||||
* by psx_pgxp_cop2; this is for direct gte_write_data paths): reg 15 performs
|
||||
* the SXYP FIFO push on the shadows, others just invalidate/overwrite. */
|
||||
void pgxp_gte_reg_written(int reg, uint32_t value);
|
||||
|
||||
/* --- GPU consumer (gpu.c) ------------------------------------------------ */
|
||||
|
||||
enum {
|
||||
PGXP_SRC_NATIVE = 0, /* no shadow — caller uses the parsed integers */
|
||||
PGXP_SRC_FALLBACK = 1, /* position-cache fallback (never carries depth)*/
|
||||
PGXP_SRC_DATAFLOW = 2, /* address-keyed shadow, value-validated */
|
||||
};
|
||||
|
||||
/* Precise position for one GP0 vertex word.
|
||||
* addr guest address the word was DMA'd from, or 0xFFFFFFFF if
|
||||
* unknown (immediate GP0 writes) — skips the dataflow tier.
|
||||
* packet_word the actual word (validation key).
|
||||
* int_x/int_y the natively parsed 11-bit positions (pre-draw-offset).
|
||||
* On DATAFLOW/FALLBACK, *x16/*y16 hold the sub-pixel position (16.16, same
|
||||
* coordinate space as the packet halves); *sz is the projected depth or 0.
|
||||
* Safeguards applied here: the integer part must match the native parse
|
||||
* (truncation agreement) and the tolerance clamp. */
|
||||
int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y,
|
||||
int32_t *x16, int32_t *y16, uint16_t *sz);
|
||||
|
||||
/* --- observability -------------------------------------------------------- */
|
||||
|
||||
typedef struct PGXPStats {
|
||||
uint64_t lookups; /* pgxp_get_precise_vertex calls */
|
||||
uint64_t dataflow_hit;
|
||||
uint64_t fallback_hit;
|
||||
uint64_t native;
|
||||
uint64_t value_mismatch; /* shadow present but wrong word (stale) */
|
||||
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);
|
||||
|
||||
/* --- gte.cpp forwarding surface (v14 ABI compat) -------------------------- */
|
||||
|
||||
/* SWC2 site: copy the GTE register shadow (regs 12..15) to the RAM shadow. */
|
||||
void pgxp_store_gte_reg(uint32_t addr, uint8_t reg);
|
||||
|
||||
/* Address-keyed precise-word lookup (perspective texturing path). Returns
|
||||
* nonzero when the tracked word matches `packed` AND carries a depth. */
|
||||
int pgxp_load_precise_word(uint32_t addr, uint32_t packed,
|
||||
int32_t *x16, int32_t *y16, uint16_t *z);
|
||||
|
||||
/* --- test accessors (always compiled; trivial) ---------------------------- */
|
||||
|
||||
/* index 0..3 selects the SXY0..SXYP register shadow (GTE data regs 12..15). */
|
||||
void pgxp_test_seed_gte_sxy(uint32_t index, uint32_t packed,
|
||||
int32_t x16, int32_t y16, uint16_t z, int valid);
|
||||
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
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PGXP_H */
|
||||
@@ -0,0 +1,93 @@
|
||||
#ifndef PGXP_HOOKS_H
|
||||
#define PGXP_HOOKS_H
|
||||
|
||||
/* PGXP dataflow-shadowing hook surface (ENHANCEMENTS.md G1.2/G1.3).
|
||||
*
|
||||
* The runtime keeps a host-only "shadow" of sub-pixel GTE projection results
|
||||
* and follows them BY PROVENANCE as the game moves them through registers and
|
||||
* RAM into a GP0 packet (pgxp.cpp). Generated code participates by calling
|
||||
* these hooks after the guest operation completes.
|
||||
*
|
||||
* Gating model (the G1.3 decision): the emitter writes PGXP_*() macro
|
||||
* invocations unconditionally; this header expands them to real calls only
|
||||
* when the translation unit is compiled with -DPSX_PGXP=1 (the pgxp build
|
||||
* variant). In the base variant the macros expand to ((void)0), so base
|
||||
* objects compile to exactly the pre-feature code — the feature costs nothing
|
||||
* unless a title links the pgxp target.
|
||||
*
|
||||
* RULES for emit sites:
|
||||
* - Macro arguments MUST be side-effect free (they vanish in base builds).
|
||||
* - Hooks run AFTER the guest architectural effect (value = what actually
|
||||
* loaded/stored/resulted), so the shadow can validate against reality.
|
||||
* - `cpu` must be in scope at every emit site (it always is in generated C).
|
||||
*
|
||||
* The interpreters (dirty_ram_interp.c, psx_interpreter.c) call the
|
||||
* psx_pgxp_* functions directly — they are runtime code compiled once, and
|
||||
* every hook early-outs on a single global when the feature is off.
|
||||
*
|
||||
* Overlay DLLs cannot link runtime symbols directly; they forward through the
|
||||
* PGXPHooks table below (OverlayCallbacks.pgxp, overlay_api.h). A NULL table
|
||||
* no-ops every hook — precision shadowing is a visual enhancement, never
|
||||
* load-bearing.
|
||||
*
|
||||
* LICENSING: this is a clean-room implementation of the publicly documented
|
||||
* PGXP technique (psx-spx, public design write-ups). Do NOT port code from
|
||||
* the vendored duckstation/ (CC BY-NC-ND) or beetle-psx/ (GPL) trees; they
|
||||
* are black-box behavioral oracles only. See ENHANCEMENTS.md G1.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct CPUState;
|
||||
|
||||
/* Funnel entry points (implemented in runtime/src/pgxp.cpp). The raw
|
||||
* instruction word carries the op/rs/rt/rd/imm decode so the emitted call is
|
||||
* uniform per class; `value` is the architectural result of the operation.
|
||||
*
|
||||
* load: rt <- [addr]; value = loaded word/half/byte (post-extension)
|
||||
* store: [addr] <- rt; value = the stored word/half/byte
|
||||
* alu: rd/rt <- op(s1, s2); result = the written value (also LUI, HILO
|
||||
* moves and MOVE idioms — the body decodes the class from instr)
|
||||
* muldiv: HI/LO <- op(s1, s2)
|
||||
* cop2: MFC2/CFC2/MTC2/CTC2 (addr = 0) and LWC2/SWC2 (addr = guest addr);
|
||||
* value = the transferred word
|
||||
*/
|
||||
void psx_pgxp_load (struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value);
|
||||
void psx_pgxp_store (struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value);
|
||||
void psx_pgxp_alu (struct CPUState *cpu, uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2);
|
||||
void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr, uint32_t hi, uint32_t lo, uint32_t s1, uint32_t s2);
|
||||
void psx_pgxp_cop2 (struct CPUState *cpu, uint32_t instr, uint32_t value, uint32_t addr);
|
||||
|
||||
/* Forwarder table for overlay DLLs (OverlayCallbacks.pgxp). Appended-last
|
||||
* member semantics apply: a NULL pointer (older host) means "no shadowing". */
|
||||
typedef struct PGXPHooks {
|
||||
void (*load) (struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value);
|
||||
void (*store) (struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value);
|
||||
void (*alu) (struct CPUState *cpu, uint32_t instr, uint32_t result, uint32_t s1, uint32_t s2);
|
||||
void (*muldiv)(struct CPUState *cpu, uint32_t instr, uint32_t hi, uint32_t lo, uint32_t s1, uint32_t s2);
|
||||
void (*cop2) (struct CPUState *cpu, uint32_t instr, uint32_t value, uint32_t addr);
|
||||
} PGXPHooks;
|
||||
|
||||
#if defined(PSX_PGXP) && PSX_PGXP
|
||||
#define PGXP_LOAD(instr, addr, val) psx_pgxp_load(cpu, (instr), (addr), (val))
|
||||
#define PGXP_STORE(instr, addr, val) psx_pgxp_store(cpu, (instr), (addr), (val))
|
||||
#define PGXP_ALU(instr, res, s1, s2) psx_pgxp_alu(cpu, (instr), (res), (s1), (s2))
|
||||
#define PGXP_MULDIV(instr, hi, lo, s1, s2) psx_pgxp_muldiv(cpu, (instr), (hi), (lo), (s1), (s2))
|
||||
#define PGXP_COP2(instr, val, addr) psx_pgxp_cop2(cpu, (instr), (val), (addr))
|
||||
#else
|
||||
#define PGXP_LOAD(instr, addr, val) ((void)0)
|
||||
#define PGXP_STORE(instr, addr, val) ((void)0)
|
||||
#define PGXP_ALU(instr, res, s1, s2) ((void)0)
|
||||
#define PGXP_MULDIV(instr, hi, lo, s1, s2) ((void)0)
|
||||
#define PGXP_COP2(instr, val, addr) ((void)0)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PGXP_HOOKS_H */
|
||||
@@ -8,7 +8,7 @@
|
||||
* g_snaps. Disabled while psx_netplay_active().
|
||||
*
|
||||
* Keyboard: F8 (config.ini [KeyMap] Rewind). Controller: View/Back or L3.
|
||||
* While open: D-pad / left-stick Left/Right, Cross/A load, Circle/B/Back close.
|
||||
* While open: D-pad / left-stick Left/Right, Cross load, Circle/Back close.
|
||||
*
|
||||
* Env: PSX_REWIND=0 disables; PSX_REWIND_INTERVAL (1/4/8/12/15, default 15);
|
||||
* PSX_REWIND_FMV_INTERVAL (frames while depth24/MDEC/XA, default 4);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#define SDL_ENABLE_OLD_NAMES
|
||||
#define SDL_FUNCTION_POINTER_IS_VOID_POINTER
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
|
||||
#ifndef SDL_WINDOW_SHOWN
|
||||
#define SDL_WINDOW_SHOWN 0
|
||||
|
||||
+99
-25
@@ -299,6 +299,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/crash_trace.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/freeze_heartbeat.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gte.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/pgxp.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/nd_intro_ot.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/crc32.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_sha256.c
|
||||
@@ -331,6 +332,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/event_ring.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/game_options.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_speed.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_pgxp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_keybinds.c
|
||||
@@ -441,18 +443,34 @@ endif()
|
||||
|
||||
# Local rewind: full rbengine when netplay is on; otherwise compile snap_ring.c
|
||||
# only (no recomp-net / sched / hash_confirm). Never both — duplicate symbols.
|
||||
# Defaults ON because local rewind is useful for every PSX title. If a title
|
||||
# deliberately opts out with -DPSX_REWIND=OFF, recomp-ui hides the Rewind
|
||||
# controls instead of exposing dead hotkeys.
|
||||
option(PSX_REWIND "Build and expose local rewind support" ON)
|
||||
set(PSXRECOMP_HAS_RBENGINE_SNAP FALSE)
|
||||
set(PSXRECOMP_RBENGINE_SNAP_INCLUDE "")
|
||||
if(PSXRECOMP_HAS_RBENGINE)
|
||||
set(PSXRECOMP_HAS_RBENGINE_SNAP TRUE)
|
||||
elseif(RECOMP_RBENGINE_ROOT
|
||||
AND EXISTS "${RECOMP_RBENGINE_ROOT}/src/snap/rbe_snap_ring.c"
|
||||
AND EXISTS "${RECOMP_RBENGINE_ROOT}/include/retcomm_rbengine/snap_ring.h")
|
||||
list(APPEND PSXRECOMP_RUNTIME_SOURCES
|
||||
${RECOMP_RBENGINE_ROOT}/src/snap/rbe_snap_ring.c)
|
||||
set(PSXRECOMP_HAS_RBENGINE_SNAP TRUE)
|
||||
set(PSXRECOMP_RBENGINE_SNAP_INCLUDE "${RECOMP_RBENGINE_ROOT}/include")
|
||||
message(STATUS "psxrecomp: rewind snap_ring (${RECOMP_RBENGINE_ROOT})")
|
||||
if(PSX_REWIND)
|
||||
if(PSXRECOMP_HAS_RBENGINE)
|
||||
set(PSXRECOMP_HAS_RBENGINE_SNAP TRUE)
|
||||
elseif(RECOMP_RBENGINE_ROOT
|
||||
AND EXISTS "${RECOMP_RBENGINE_ROOT}/src/snap/rbe_snap_ring.c"
|
||||
AND EXISTS "${RECOMP_RBENGINE_ROOT}/include/retcomm_rbengine/snap_ring.h")
|
||||
list(APPEND PSXRECOMP_RUNTIME_SOURCES
|
||||
${RECOMP_RBENGINE_ROOT}/src/snap/rbe_snap_ring.c)
|
||||
set(PSXRECOMP_HAS_RBENGINE_SNAP TRUE)
|
||||
set(PSXRECOMP_RBENGINE_SNAP_INCLUDE "${RECOMP_RBENGINE_ROOT}/include")
|
||||
message(STATUS "psxrecomp: rewind snap_ring (${RECOMP_RBENGINE_ROOT})")
|
||||
endif()
|
||||
if(NOT PSXRECOMP_HAS_RBENGINE_SNAP)
|
||||
message(FATAL_ERROR
|
||||
"psxrecomp: PSX_REWIND=ON exposes the Rewind launcher controls "
|
||||
"but no retcomm-rbengine snap-ring backend was found.\n"
|
||||
" git submodule update --init lib/retcomm-rbengine\n"
|
||||
" or -DRECOMP_RBENGINE_ROOT=/path/to/retcomm-rbengine\n"
|
||||
" or configure with -DPSX_REWIND=OFF to hide Rewind.")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "psxrecomp: local rewind disabled (PSX_REWIND=OFF)")
|
||||
endif()
|
||||
|
||||
# Lobby WebSocket client helpers are vendored under runtime/src/lobby_ws/
|
||||
@@ -744,7 +762,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
|
||||
@@ -900,6 +924,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 ---------------------------------------
|
||||
@@ -1153,16 +1186,27 @@ function(psxrecomp_add_runtime_target target)
|
||||
# second GENERATE to the same path is rejected ("Files to be generated by
|
||||
# multiple different commands"). Multi-config (VS): exe lives in Release/,
|
||||
# so also drop a copy at the build root for packager lookup.
|
||||
file(GENERATE
|
||||
OUTPUT "$<TARGET_FILE_DIR:${target}>/psx_game_version.txt"
|
||||
CONTENT "${PSXRT_GAME_VERSION}\n"
|
||||
)
|
||||
get_property(_psxrt_ver_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
if(_psxrt_ver_multi)
|
||||
# Once per build tree, not per target: with several runtime targets
|
||||
# (psx-runtime + psx-oracle + psx-beetle) each TARGET_FILE_DIR genex is a
|
||||
# DIFFERENT command producing the SAME path on single-config generators,
|
||||
# which CMake rejects even when the content is identical (the ae9e6a4e
|
||||
# hotfix fixed the same-target duplicate but not the multi-target one).
|
||||
# All runtime targets in one tree share one PSXRT_GAME_VERSION, so a
|
||||
# single stamp is the correct semantics anyway.
|
||||
get_property(_psxrt_ver_stamped GLOBAL PROPERTY PSXRT_GAME_VERSION_STAMPED)
|
||||
if(NOT _psxrt_ver_stamped)
|
||||
set_property(GLOBAL PROPERTY PSXRT_GAME_VERSION_STAMPED 1)
|
||||
file(GENERATE
|
||||
OUTPUT "${CMAKE_BINARY_DIR}/psx_game_version.txt"
|
||||
OUTPUT "$<TARGET_FILE_DIR:${target}>/psx_game_version.txt"
|
||||
CONTENT "${PSXRT_GAME_VERSION}\n"
|
||||
)
|
||||
get_property(_psxrt_ver_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
if(_psxrt_ver_multi)
|
||||
file(GENERATE
|
||||
OUTPUT "${CMAKE_BINARY_DIR}/psx_game_version.txt"
|
||||
CONTENT "${PSXRT_GAME_VERSION}\n"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# OpenBIOS is part of the native runtime product, not a developer-machine
|
||||
@@ -1358,6 +1402,8 @@ function(psxrecomp_add_runtime_target target)
|
||||
list(APPEND _psx_recomp_ui_args BRAND "${PSXRT_LAUNCHER_BRAND}")
|
||||
endif()
|
||||
recomp_target_launcher_ui(${target} ${_psx_recomp_ui_args})
|
||||
target_compile_definitions(${target} PRIVATE
|
||||
RECOMP_UI_PSX_HAS_REWIND=$<BOOL:${PSXRECOMP_HAS_RBENGINE_SNAP}>)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR MINGW)
|
||||
@@ -1540,6 +1586,22 @@ function(psxrecomp_add_runtime_target target)
|
||||
COMMENT "Staging trusted xdelta3 decoder for derived-disc mods"
|
||||
VERBATIM)
|
||||
endif()
|
||||
|
||||
# PGXP variant auto-clone (ENHANCEMENTS.md G1.10): with
|
||||
# -DPSX_PGXP_VARIANT=ON, every primary runtime target grows an
|
||||
# <exe>_pgxp sibling — the SAME arguments (same generated C, extras,
|
||||
# ports) compiled with -DPSX_PGXP=1 (see the PGXP option above). Done
|
||||
# HERE, at the end, by replaying ARGN recursively, so it works for
|
||||
# every caller — titles that call this function directly (Ape) and
|
||||
# the psxrecomp_add_game_runtime wrapper alike. Oracle/cosim targets
|
||||
# and the clone itself are excluded.
|
||||
if(NOT PSXRT_PGXP AND NOT PSXRT_ORACLE AND NOT PSXRT_COSIM)
|
||||
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 ${ARGN})
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Compatibility for early v4 game projects that used the longer helper name.
|
||||
@@ -1760,20 +1822,30 @@ function(psxrecomp_add_game_runtime target)
|
||||
GAME_GENERATED_DISPATCH_C "${_psxg_marker}"
|
||||
${_psxg_rt_args}
|
||||
)
|
||||
# psxrecomp_add_runtime_target auto-clones a ${target}-pgxp sibling
|
||||
# when PSX_PGXP_VARIANT is ON; fold it into the tail configuration
|
||||
# below so it gets the same game-codegen defines and includes.
|
||||
set(_psxg_targets ${target})
|
||||
if(TARGET ${target}-pgxp)
|
||||
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
|
||||
@@ -1786,5 +1858,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()
|
||||
|
||||
@@ -66,13 +66,17 @@ void main() {
|
||||
} else {
|
||||
/* Bilinear, Beetle-PSX formulation: the NEAREST texel is the base
|
||||
* (cutout + STP authority), the neighbours lie toward the sub-texel
|
||||
* offset and clamp to v_limits, and each texel's weight is gated by its
|
||||
* opacity with the colour renormalised. */
|
||||
* offset and clamp to v_limits. Transparent neighbours are ignored and
|
||||
* the colour is renormalised, but a transparent base stays cut out.
|
||||
* v_uv is aligned for PS1 top-left point sampling; recenter the
|
||||
* bilinear footprint so 1x rect tiles sample their own texels. */
|
||||
uv += vec2(pc.u_shift);
|
||||
int iu = int(floor(uv.x)), iv = int(floor(uv.y));
|
||||
float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;
|
||||
int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;
|
||||
fx = abs(fx); fy = abs(fy);
|
||||
int c00 = fetch_texel(iu, iv);
|
||||
if (c00 == 0) discard;
|
||||
int c10 = fetch_texel(iu + sx, iv);
|
||||
int c01 = fetch_texel(iu, iv + sy);
|
||||
int c11 = fetch_texel(iu + sx, iv + sy);
|
||||
@@ -81,7 +85,6 @@ void main() {
|
||||
float w01 = (c01 == 0 ? 0.0 : 1.0) * (1.0 - fx) * fy;
|
||||
float w11 = (c11 == 0 ? 0.0 : 1.0) * fx * fy;
|
||||
float opac = w00 + w10 + w01 + w11;
|
||||
if (opac < 0.5) discard;
|
||||
rgb = (col5(c00) * w00 + col5(c10) * w10 + col5(c01) * w01 + col5(c11) * w11) / opac;
|
||||
float stpf = (float((c00 >> 15) & 1) * w00 + float((c10 >> 15) & 1) * w10
|
||||
+ float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;
|
||||
|
||||
+38
-12
@@ -36,6 +36,9 @@
|
||||
extern void* iso_open(const char* path);
|
||||
extern int iso_read_sector(void* handle, uint32_t lba, uint8_t* buffer, int size);
|
||||
extern int iso_read_raw_sector(void* handle, uint32_t lba, uint8_t* buffer, int size);
|
||||
extern int iso_read_subq(void* handle, uint32_t lba, uint8_t* buffer, int size,
|
||||
int* valid);
|
||||
extern int iso_has_subq_replacements(void* handle);
|
||||
extern uint32_t iso_sector_count(void* handle);
|
||||
extern void iso_close(void* handle);
|
||||
/* Multi-track TOC accessors (CD-DA / multi-track discs). track is 1-based. */
|
||||
@@ -684,6 +687,18 @@ static void exec_command(uint8_t cmd);
|
||||
|
||||
/* ISO reader */
|
||||
static void* iso_handle = NULL;
|
||||
static uint8_t last_valid_subq[12];
|
||||
static int last_valid_subq_available;
|
||||
static int subq_replacements_active;
|
||||
|
||||
static void update_last_valid_subq(uint32_t lba) {
|
||||
uint8_t subq[12];
|
||||
int valid = 0;
|
||||
if (iso_read_subq(iso_handle, lba, subq, sizeof(subq), &valid) && valid) {
|
||||
memcpy(last_valid_subq, subq, sizeof(last_valid_subq));
|
||||
last_valid_subq_available = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static CDROMTraceEntry cdrom_trace[CDROM_TRACE_CAP];
|
||||
static uint64_t cdrom_trace_seq;
|
||||
@@ -1316,6 +1331,7 @@ static int read_sector_at(int min, int sec, int sect) {
|
||||
} else if (!iso_read_sector(iso_handle, lba, user_data, SECTOR_SIZE)) {
|
||||
memset(user_data, 0, sizeof(user_data));
|
||||
}
|
||||
if (subq_replacements_active) update_last_valid_subq((uint32_t)lba);
|
||||
} else {
|
||||
memset(user_data, 0, sizeof(user_data));
|
||||
}
|
||||
@@ -2013,18 +2029,23 @@ static void exec_command(uint8_t cmd) {
|
||||
} else {
|
||||
lba = msf_to_lba(seek_min, seek_sec, seek_sect);
|
||||
}
|
||||
int rm, rs, rf;
|
||||
int am, as, af;
|
||||
lba_to_msf(lba - track_lba, 0, &rm, &rs, &rf);
|
||||
lba_to_msf(lba, 150, &am, &as, &af);
|
||||
response_push(bin_to_bcd(track));
|
||||
response_push(0x01);
|
||||
response_push(bin_to_bcd(rm));
|
||||
response_push(bin_to_bcd(rs));
|
||||
response_push(bin_to_bcd(rf));
|
||||
response_push(bin_to_bcd(am));
|
||||
response_push(bin_to_bcd(as));
|
||||
response_push(bin_to_bcd(af));
|
||||
if (subq_replacements_active) update_last_valid_subq((uint32_t)lba);
|
||||
if (subq_replacements_active && last_valid_subq_available) {
|
||||
for (int i = 1; i <= 8; ++i) response_push(last_valid_subq[i]);
|
||||
} else {
|
||||
int rm, rs, rf;
|
||||
int am, as, af;
|
||||
lba_to_msf(lba - track_lba, 0, &rm, &rs, &rf);
|
||||
lba_to_msf(lba, 150, &am, &as, &af);
|
||||
response_push(bin_to_bcd(track));
|
||||
response_push(0x01);
|
||||
response_push(bin_to_bcd(rm));
|
||||
response_push(bin_to_bcd(rs));
|
||||
response_push(bin_to_bcd(rf));
|
||||
response_push(bin_to_bcd(am));
|
||||
response_push(bin_to_bcd(as));
|
||||
response_push(bin_to_bcd(af));
|
||||
}
|
||||
set_irq(CDIRQ_ACK);
|
||||
break;
|
||||
}
|
||||
@@ -2478,6 +2499,9 @@ void cdrom_init(const char* cue_path) {
|
||||
iso_handle = NULL;
|
||||
}
|
||||
iso_handle = iso_open(cue_path);
|
||||
last_valid_subq_available = 0;
|
||||
subq_replacements_active = iso_has_subq_replacements(iso_handle);
|
||||
if (subq_replacements_active) update_last_valid_subq(0);
|
||||
}
|
||||
|
||||
stat_reg = has_disc() ? CDSTAT_MOTOR : CDSTAT_SHELL;
|
||||
@@ -2924,6 +2948,7 @@ static int cdrom_snap_emit(PstW *w) {
|
||||
WB(response_fifo); WI(response_read); WI(response_count);
|
||||
WB(sector_buffer); WI(sector_read_pos); WI(sector_available); WI(sector_size);
|
||||
WB(last_sector_buffer); WI(last_sector_lba); WI(last_sector_size);
|
||||
WB(last_valid_subq); WI(last_valid_subq_available);
|
||||
/* last_sector_frame is host s_frame_count — zero on the wire so netplay
|
||||
* digests/pins are not forked by present-skip / FPS skew. */
|
||||
WU(0u); W8(last_sector_mode); W8(last_sector_have_raw);
|
||||
@@ -2971,6 +2996,7 @@ static int cdrom_snap_parse(PstR *r) {
|
||||
RB(response_fifo); RI(response_read); RI(response_count);
|
||||
RB(sector_buffer); RI(sector_read_pos); RI(sector_available); RI(sector_size);
|
||||
RB(last_sector_buffer); RI(last_sector_lba); RI(last_sector_size);
|
||||
RB(last_valid_subq); RI(last_valid_subq_available);
|
||||
RU(last_sector_frame); R8(last_sector_mode); R8(last_sector_have_raw);
|
||||
R8(last_sector_raw_mode); R8(last_sector_xa_file); R8(last_sector_xa_channel);
|
||||
R8(last_sector_xa_submode); R8(last_sector_xa_coding);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "code_provider.h"
|
||||
#include "overlay_backend.h"
|
||||
#include "cpu_state.h"
|
||||
#include "pgxp.h"
|
||||
#include "dma.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_render.h" /* gr_scale + gr_render_display_hires (screenshot_hires) */
|
||||
@@ -5196,16 +5197,75 @@ static void handle_geom_correction(int id, const char *json)
|
||||
* distinct vertices are landing on the same pixel. */
|
||||
uint32_t lookups = 0, hits = 0, unrec = 0, ambig = 0;
|
||||
gte_geometry_correction_stats(&lookups, &hits, &unrec, &ambig);
|
||||
/* PGXP dataflow census (per-vertex): the primary provenance source.
|
||||
* dataflow_hit is the number that had to move — the G1.9 gate is a
|
||||
* dataflow-hit share dramatically above the 5.2% the position table
|
||||
* measured on its own. value_mismatch counts shadows that were present
|
||||
* but described a different word (stale = provenance hole to hunt). */
|
||||
PGXPStats ps;
|
||||
pgxp_get_stats(&ps);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||||
"\"geometry_correction\":%d,"
|
||||
"\"geometry_vertex_hits\":%u,"
|
||||
"\"perspective_triangles\":%u,"
|
||||
"\"lookups\":%u,\"miss_unrecorded\":%u,\"miss_ambiguous\":%u}",
|
||||
"\"lookups\":%u,\"miss_unrecorded\":%u,\"miss_ambiguous\":%u,"
|
||||
"\"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,"
|
||||
"\"produced\":%llu,\"swc2_stores\":%llu}}",
|
||||
id,
|
||||
gte_geometry_correction_enabled(),
|
||||
(unsigned)hits,
|
||||
(unsigned)gpu_texture_correction_hits(),
|
||||
(unsigned)lookups, (unsigned)unrec, (unsigned)ambig);
|
||||
(unsigned)lookups, (unsigned)unrec, (unsigned)ambig,
|
||||
pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(),
|
||||
(unsigned long long)ps.lookups,
|
||||
(unsigned long long)ps.dataflow_hit,
|
||||
(unsigned long long)ps.fallback_hit,
|
||||
(unsigned long long)ps.native,
|
||||
(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.produced,
|
||||
(unsigned long long)ps.swc2_stores);
|
||||
}
|
||||
|
||||
/* pgxp — live-tune the value-propagation engine for one-toggle isolation runs
|
||||
* without a rebuild: {"cmd":"pgxp","cpu_mode":0|1,"tolerance":F}. Fields are
|
||||
* optional; the reply echoes the resulting state (same shape as
|
||||
* geom_correction's "pgxp" object, flattened). */
|
||||
static void handle_pgxp(int id, const char *json)
|
||||
{
|
||||
/* Live toggles for the one-toggle-at-a-time A/B protocol (ENHANCEMENTS.md
|
||||
* G1.6 method rule): same scene, flip one knob, screenshot_hires. */
|
||||
int geom = json_get_int(json, "geometry", -1);
|
||||
if (geom >= 0)
|
||||
gte_geometry_correction_set(geom != 0);
|
||||
int tex = json_get_int(json, "texture", -1);
|
||||
if (tex >= 0)
|
||||
gpu_texture_correction_set(tex != 0);
|
||||
if (geom >= 0 && tex < 0) {
|
||||
/* keep the engine armed consistently with both flags */
|
||||
gpu_texture_correction_set(gpu_texture_correction_enabled());
|
||||
}
|
||||
int cm = json_get_int(json, "cpu_mode", -1);
|
||||
if (cm >= 0)
|
||||
pgxp_set_cpu_mode(cm != 0);
|
||||
/* tolerance is fractional (sub-pixel), so scan it directly — json_get_int
|
||||
* would truncate 0.5 to 0. */
|
||||
const char *p = strstr(json, "\"tolerance\"");
|
||||
if (p) {
|
||||
p += 11;
|
||||
while (*p == ' ' || *p == ':' || *p == '"') p++;
|
||||
if (*p == '-' || (*p >= '0' && *p <= '9') || *p == '.')
|
||||
pgxp_set_tolerance((float)strtod(p, NULL));
|
||||
}
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"cpu_mode\":%d,"
|
||||
"\"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)
|
||||
@@ -5221,6 +5281,8 @@ static void handle_gpu_state(int id, const char *json)
|
||||
gpu_get_draw_area(&da);
|
||||
uint64_t nop, fill, draw, env, copy;
|
||||
gpu_get_gp0_stats(&nop, &fill, &draw, &env, ©);
|
||||
int split_active = 0, split_left_age = 0, split_right_age = 0;
|
||||
gpu_vertical_split_debug(&split_active, &split_left_age, &split_right_age);
|
||||
GpuWsDebug ws;
|
||||
gpu_ws_get_debug(&ws);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||||
@@ -5235,6 +5297,7 @@ static void handle_gpu_state(int id, const char *json)
|
||||
"\"gp0_nop\":%llu,\"gp0_fill\":%llu,\"gp0_draw\":%llu,\"gp0_env\":%llu,\"gp0_copy\":%llu,"
|
||||
"\"draw_area\":[%u,%u,%u,%u],"
|
||||
"\"draw_offset\":[%d,%d],"
|
||||
"\"vertical_split\":{\"active\":%d,\"left_age\":%d,\"right_age\":%d},"
|
||||
"\"ws\":{\"configured\":%d,\"active\":%d,\"game_mode\":%d,"
|
||||
"\"present_native_43\":%d,\"x_margin\":%d,"
|
||||
"\"activation_margin\":%d,\"squash\":[%d,%d],"
|
||||
@@ -5263,6 +5326,7 @@ static void handle_gpu_state(int id, const char *json)
|
||||
(unsigned long long)copy,
|
||||
da.left, da.top, da.right, da.bottom,
|
||||
da.offset_x, da.offset_y,
|
||||
split_active, split_left_age, split_right_age,
|
||||
ws.configured, ws.active, ws.game_mode,
|
||||
ws.present_native_43, ws.x_margin, ws.activation_margin,
|
||||
ws.xnum, ws.xden,
|
||||
@@ -13163,6 +13227,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "write_ram", handle_write_ram },
|
||||
{ "gpu_state", handle_gpu_state },
|
||||
{ "geom_correction", handle_geom_correction },
|
||||
{ "pgxp", handle_pgxp },
|
||||
{ "ws_aspect_cone_site", handle_ws_aspect_cone_site },
|
||||
{ "ws_margin", handle_ws_margin },
|
||||
{ "ws_hud_mode", handle_ws_hud_mode },
|
||||
|
||||
@@ -1512,30 +1512,48 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
switch (opc) {
|
||||
case 0x00: /* SPECIAL */
|
||||
switch (fnt) {
|
||||
case 0x00: /* SLL rd, rt, sh (also nop when all fields are 0) */
|
||||
cpu->gpr[rd] = cpu->gpr[rt] << sh;
|
||||
case 0x00: { /* SLL rd, rt, sh (also nop when all fields are 0) */
|
||||
uint32_t a = cpu->gpr[rt];
|
||||
cpu->gpr[rd] = a << sh;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, sh);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x02: /* SRL */
|
||||
cpu->gpr[rd] = cpu->gpr[rt] >> sh;
|
||||
}
|
||||
case 0x02: { /* SRL */
|
||||
uint32_t a = cpu->gpr[rt];
|
||||
cpu->gpr[rd] = a >> sh;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, sh);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x03: /* SRA */
|
||||
cpu->gpr[rd] = (uint32_t)((int32_t)cpu->gpr[rt] >> sh);
|
||||
}
|
||||
case 0x03: { /* SRA */
|
||||
uint32_t a = cpu->gpr[rt];
|
||||
cpu->gpr[rd] = (uint32_t)((int32_t)a >> sh);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, sh);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x04: /* SLLV */
|
||||
cpu->gpr[rd] = cpu->gpr[rt] << (cpu->gpr[rs] & 31);
|
||||
}
|
||||
case 0x04: { /* SLLV */
|
||||
uint32_t a = cpu->gpr[rt], b = cpu->gpr[rs];
|
||||
cpu->gpr[rd] = a << (b & 31);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x06: /* SRLV */
|
||||
cpu->gpr[rd] = cpu->gpr[rt] >> (cpu->gpr[rs] & 31);
|
||||
}
|
||||
case 0x06: { /* SRLV */
|
||||
uint32_t a = cpu->gpr[rt], b = cpu->gpr[rs];
|
||||
cpu->gpr[rd] = a >> (b & 31);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x07: /* SRAV */
|
||||
cpu->gpr[rd] = (uint32_t)((int32_t)cpu->gpr[rt] >> (cpu->gpr[rs] & 31));
|
||||
}
|
||||
case 0x07: { /* SRAV */
|
||||
uint32_t a = cpu->gpr[rt], b = cpu->gpr[rs];
|
||||
cpu->gpr[rd] = (uint32_t)((int32_t)a >> (b & 31));
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x08: { /* JR rs */
|
||||
uint32_t target = cpu->gpr[rs];
|
||||
if (target & 3) return interp_exception(cpu, 4, target, pc); /* LoadAddressError */
|
||||
@@ -1615,25 +1633,30 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
psx_muldiv_stall(cpu); /* stall to mult/div completion (faithful) */
|
||||
#endif
|
||||
cpu->gpr[rd] = cpu->hi;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], cpu->hi, 0);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x11: /* MTHI */
|
||||
cpu->hi = cpu->gpr[rs];
|
||||
psx_pgxp_alu(cpu, insn, cpu->hi, cpu->hi, 0);
|
||||
return 0;
|
||||
case 0x12: /* MFLO */
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_muldiv_stall(cpu); /* stall to mult/div completion (faithful) */
|
||||
#endif
|
||||
cpu->gpr[rd] = cpu->lo;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], cpu->lo, 0);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x13: /* MTLO */
|
||||
cpu->lo = cpu->gpr[rs];
|
||||
psx_pgxp_alu(cpu, insn, cpu->lo, cpu->lo, 0);
|
||||
return 0;
|
||||
case 0x18: { /* MULT */
|
||||
int64_t r = (int64_t)(int32_t)cpu->gpr[rs] * (int64_t)(int32_t)cpu->gpr[rt];
|
||||
cpu->lo = (uint32_t)r;
|
||||
cpu->hi = (uint32_t)((uint64_t)r >> 32);
|
||||
psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, cpu->gpr[rs], cpu->gpr[rt]);
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_muldiv_set(cpu, psx_mult_latency_s(cpu->gpr[rs])); /* completion deadline */
|
||||
#endif
|
||||
@@ -1643,6 +1666,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
uint64_t r = (uint64_t)cpu->gpr[rs] * (uint64_t)cpu->gpr[rt];
|
||||
cpu->lo = (uint32_t)r;
|
||||
cpu->hi = (uint32_t)(r >> 32);
|
||||
psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, cpu->gpr[rs], cpu->gpr[rt]);
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_muldiv_set(cpu, psx_mult_latency_u(cpu->gpr[rs])); /* completion deadline */
|
||||
#endif
|
||||
@@ -1661,6 +1685,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->lo = (uint32_t)(a / b);
|
||||
cpu->hi = (uint32_t)(a % b);
|
||||
}
|
||||
psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, cpu->gpr[rs], cpu->gpr[rt]);
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_muldiv_set(cpu, 37u); /* DIV completion deadline (fixed) */
|
||||
#endif
|
||||
@@ -1674,31 +1699,41 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->lo = cpu->gpr[rs] / cpu->gpr[rt];
|
||||
cpu->hi = cpu->gpr[rs] % cpu->gpr[rt];
|
||||
}
|
||||
psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, cpu->gpr[rs], cpu->gpr[rt]);
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_muldiv_set(cpu, 37u); /* DIVU completion deadline (fixed) */
|
||||
#endif
|
||||
return 0;
|
||||
case 0x20: /* ADD - overflow traps are delegated if they occur. */
|
||||
case 0x21: /* ADDU rd, rs, rt */
|
||||
cpu->gpr[rd] = cpu->gpr[rs] + cpu->gpr[rt];
|
||||
case 0x21: { /* ADDU rd, rs, rt */
|
||||
uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt];
|
||||
cpu->gpr[rd] = a + b;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x22: /* SUB - overflow traps are delegated if they occur. */
|
||||
case 0x23: /* SUBU */
|
||||
case 0x23: { /* SUBU */
|
||||
uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt];
|
||||
if (rs == 0 && psx_ws_is_cull_negsub_site(pc))
|
||||
cpu->gpr[rd] = 0u - cpu->gpr[rt] - (uint32_t)psx_ws_x_margin();
|
||||
cpu->gpr[rd] = 0u - b - (uint32_t)psx_ws_x_margin();
|
||||
else
|
||||
cpu->gpr[rd] = cpu->gpr[rs] - cpu->gpr[rt];
|
||||
cpu->gpr[rd] = a - b;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x24: /* AND */
|
||||
cpu->gpr[rd] = cpu->gpr[rs] & cpu->gpr[rt];
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x25: /* OR */
|
||||
cpu->gpr[rd] = cpu->gpr[rs] | cpu->gpr[rt];
|
||||
case 0x25: { /* OR */
|
||||
uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt];
|
||||
cpu->gpr[rd] = a | b;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x26: /* XOR */
|
||||
cpu->gpr[rd] = cpu->gpr[rs] ^ cpu->gpr[rt];
|
||||
cpu->gpr[0] = 0;
|
||||
@@ -1852,24 +1887,28 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
case 0x08: /* ADDI rt, rs, simm — same as ADDIU, sans overflow trap (we don't model traps here) */
|
||||
{
|
||||
uint32_t widened = 0;
|
||||
uint32_t a = cpu->gpr[rs];
|
||||
if (psx_ws_angle_site(pc, insn, &widened))
|
||||
cpu->gpr[rt] = widened;
|
||||
else
|
||||
cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm
|
||||
cpu->gpr[rt] = a + (uint32_t)simm
|
||||
+ (psx_ws_is_cull_bias_site(pc)
|
||||
? (uint32_t)psx_ws_activation_margin() : 0u);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, (uint32_t)simm);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x09: /* ADDIU rt, rs, simm */
|
||||
{
|
||||
uint32_t widened = 0;
|
||||
uint32_t a = cpu->gpr[rs];
|
||||
if (psx_ws_angle_site(pc, insn, &widened))
|
||||
cpu->gpr[rt] = widened;
|
||||
else
|
||||
cpu->gpr[rt] = cpu->gpr[rs] + (uint32_t)simm
|
||||
cpu->gpr[rt] = a + (uint32_t)simm
|
||||
+ (psx_ws_is_cull_bias_site(pc)
|
||||
? (uint32_t)psx_ws_activation_margin() : 0u);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, (uint32_t)simm);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -1930,10 +1969,13 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->gpr[rt] = cpu->gpr[rs] & imm;
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x0D: /* ORI */
|
||||
cpu->gpr[rt] = cpu->gpr[rs] | imm;
|
||||
case 0x0D: { /* ORI */
|
||||
uint32_t a = cpu->gpr[rs];
|
||||
cpu->gpr[rt] = a | imm;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, imm);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x0E: /* XORI */
|
||||
cpu->gpr[rt] = cpu->gpr[rs] ^ imm;
|
||||
cpu->gpr[0] = 0;
|
||||
@@ -1943,6 +1985,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->gpr[rt] = (uint32_t)psx_ws_player_x_bound((int32_t)(imm << 16));
|
||||
else
|
||||
cpu->gpr[rt] = imm << 16;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rt], 0, 0);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
case 0x10: { /* COP0 */
|
||||
@@ -2033,6 +2076,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
psx_gte_read(cpu, rt);
|
||||
#endif
|
||||
cpu->gpr[rt] = gte_read_data(cpu, (uint8_t)rd);
|
||||
psx_pgxp_cop2(cpu, insn, cpu->gpr[rt], 0);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2041,6 +2085,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
psx_gte_read(cpu, rt);
|
||||
#endif
|
||||
cpu->gpr[rt] = gte_read_ctrl(cpu, (uint8_t)rd);
|
||||
psx_pgxp_cop2(cpu, insn, cpu->gpr[rt], 0);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2049,6 +2094,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
psx_gte_stall(cpu);
|
||||
#endif
|
||||
gte_write_data(cpu, (uint8_t)rd, cpu->gpr[rt]);
|
||||
psx_pgxp_cop2(cpu, insn, cpu->gpr[rt], 0);
|
||||
return 0;
|
||||
}
|
||||
if (cop_op == 0x06) { /* CTC2 */
|
||||
@@ -2072,6 +2118,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
case 0x20: { /* LB rt, simm(rs) */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = (uint32_t)(int32_t)(int8_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs);
|
||||
psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2079,6 +2126,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
if (addr & 1) return interp_exception(cpu, 4, addr, pc); /* LoadAddressError */
|
||||
cpu->gpr[rt] = (uint32_t)(int32_t)(int16_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs);
|
||||
psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2086,6 +2134,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs);
|
||||
cpu->gpr[rt] = lwl_merge(addr, word, cpu->gpr[rt]);
|
||||
psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2101,12 +2150,14 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->gpr[rt] = psx_ws_xclip_bound(psx_cyc_load_word(cpu, addr, rt, 1u << rs));
|
||||
else
|
||||
cpu->gpr[rt] = psx_cyc_load_word(cpu, addr, rt, 1u << rs);
|
||||
psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
case 0x24: { /* LBU */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
cpu->gpr[rt] = (uint32_t)psx_cyc_load_byte(cpu, addr, rt, 1u << rs);
|
||||
psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2114,6 +2165,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
if (addr & 1) return interp_exception(cpu, 4, addr, pc); /* LoadAddressError */
|
||||
cpu->gpr[rt] = (uint32_t)psx_cyc_load_half(cpu, addr, rt, 1u << rs);
|
||||
psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2121,6 +2173,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
uint32_t word = psx_cyc_load_word(cpu, addr & ~3u, rt, 1u << rs);
|
||||
cpu->gpr[rt] = lwr_merge(addr, word, cpu->gpr[rt]);
|
||||
psx_pgxp_load(cpu, insn, addr, cpu->gpr[rt]);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2128,6 +2181,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
g_debug_last_store_pc = pc;
|
||||
cpu->write_byte(addr, (uint8_t)cpu->gpr[rt]);
|
||||
psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt] & 0xFFu);
|
||||
return 0;
|
||||
}
|
||||
case 0x29: { /* SH */
|
||||
@@ -2143,12 +2197,14 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
val = (uint16_t)psx_ws_backdrop_x((int16_t)val);
|
||||
g_debug_last_store_pc = pc;
|
||||
cpu->write_half(addr, val);
|
||||
psx_pgxp_store(cpu, insn, addr, val);
|
||||
return 0;
|
||||
}
|
||||
case 0x2A: { /* SWL */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
g_debug_last_store_pc = pc;
|
||||
interp_swl(cpu, addr, cpu->gpr[rt]);
|
||||
psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt]);
|
||||
return 0;
|
||||
}
|
||||
case 0x2B: { /* SW */
|
||||
@@ -2156,12 +2212,14 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
if (addr & 3) return interp_exception(cpu, 5, addr, pc); /* StoreAddressError */
|
||||
g_debug_last_store_pc = pc;
|
||||
cpu->write_word(addr, cpu->gpr[rt]);
|
||||
psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt]);
|
||||
return 0;
|
||||
}
|
||||
case 0x2E: { /* SWR */
|
||||
uint32_t addr = cpu->gpr[rs] + (uint32_t)simm;
|
||||
g_debug_last_store_pc = pc;
|
||||
interp_swr(cpu, addr, cpu->gpr[rt]);
|
||||
psx_pgxp_store(cpu, insn, addr, cpu->gpr[rt]);
|
||||
return 0;
|
||||
}
|
||||
case 0x32: { /* LWC2 — §1+DO_LDS charged by exec_one's top step (mask 0) */
|
||||
@@ -2169,7 +2227,11 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
psx_gte_stall(cpu); /* COP2 reg write stalls to GTE completion */
|
||||
#endif
|
||||
gte_write_data(cpu, (uint8_t)rt, psx_cyc_lwc2_read(cpu, addr));
|
||||
{
|
||||
uint32_t lw2 = psx_cyc_lwc2_read(cpu, addr);
|
||||
gte_write_data(cpu, (uint8_t)rt, lw2);
|
||||
psx_pgxp_cop2(cpu, insn, lw2, addr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
case 0x3A: { /* SWC2 */
|
||||
@@ -2178,8 +2240,12 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
psx_gte_stall(cpu); /* COP2 reg read stalls to GTE completion */
|
||||
#endif
|
||||
g_debug_last_store_pc = pc;
|
||||
cpu->write_word(addr, gte_read_data(cpu, (uint8_t)rt));
|
||||
gte_precision_store_word(addr, (uint8_t)rt);
|
||||
{
|
||||
uint32_t sw2 = gte_read_data(cpu, (uint8_t)rt);
|
||||
cpu->write_word(addr, sw2);
|
||||
gte_precision_store_word(addr, (uint8_t)rt);
|
||||
psx_pgxp_cop2(cpu, insn, sw2, addr);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
|
||||
+321
-67
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
#include "gpu.h"
|
||||
#include "pgxp.h"
|
||||
#include "mod_memory.h"
|
||||
#include "gpu_primitive_reject.h"
|
||||
#include "gpu_sw_renderer.h"
|
||||
@@ -86,6 +87,7 @@ static uint32_t ws_anchor_addr = 0; /* scratchpad addr of anchor SXY */
|
||||
static int ws_hud_sprt = 0; /* edge-anchor untagged HUD SPRTs */
|
||||
static int ws_auto_ui_squash;
|
||||
static int ws_auto_ui_dense;
|
||||
static int ws_active(void);
|
||||
static uint64_t ws_auto_ui_candidate_count;
|
||||
static uint64_t ws_auto_ui_transform_count;
|
||||
#define WS_UI_PREPASS_MAX 2048u
|
||||
@@ -126,6 +128,7 @@ static int ws_engaged(void) { return ws_mode != 0; }
|
||||
|
||||
/* Forward decls: defined later but used by psx_ws_backdrop_x above them. */
|
||||
static int32_t ws_scale_about(int32_t x, int32_t ax);
|
||||
static int32_t ws_disp_x(void);
|
||||
static int32_t ws_disp_w(void);
|
||||
static void ws_clear_all_reveal_margins(void);
|
||||
|
||||
@@ -166,6 +169,7 @@ static int ws_gte_game_mode_cfg = 0;
|
||||
static uint32_t ws_gte_frame = (uint32_t)-1;
|
||||
static uint32_t ws_gte_count = 0;
|
||||
static uint32_t ws_last_gte_stamp = (uint32_t)-1000;
|
||||
static int ws_precise_nclip_cfg = 0;
|
||||
#define WS_GAMEPLAY_STATE_VALUES_MAX 16
|
||||
static uint32_t ws_gameplay_state_addr = 0;
|
||||
static uint32_t ws_gameplay_state_values[WS_GAMEPLAY_STATE_VALUES_MAX];
|
||||
@@ -180,6 +184,8 @@ static int ws_gameplay_state_value_count = 0;
|
||||
* this many consecutive frames (save/options/memory-card) — reverts to 4:3. */
|
||||
#define WS_GTE_GAME_MODE_HYSTERESIS 45u
|
||||
void gpu_ws_set_gte_game_mode(int on) { ws_gte_game_mode_cfg = on ? 1 : 0; }
|
||||
void gpu_ws_set_precise_nclip(int on) { ws_precise_nclip_cfg = on ? 1 : 0; }
|
||||
int gpu_ws_precise_nclip_enabled(void) { return ws_precise_nclip_cfg && ws_active(); }
|
||||
void gpu_ws_set_gameplay_state_gate(uint32_t addr,
|
||||
const uint32_t *values, int nvalues) {
|
||||
if (nvalues < 0) nvalues = 0;
|
||||
@@ -344,8 +350,48 @@ int ws_native_wide_active(void) {
|
||||
static int ws_native_wide_configured(void) {
|
||||
return ws_mode == 2 && ws_cfg_num * 3 > ws_cfg_den * 4;
|
||||
}
|
||||
|
||||
static int ws_local_viewport_cfg = 0;
|
||||
static int ws_local_viewport_slot = 0;
|
||||
static int ws_local_viewport_draw_target(int *base_x);
|
||||
static int ws_vertical_split_active(void);
|
||||
void gpu_ws_set_netplay_local_viewport(int enabled, int slot) {
|
||||
ws_local_viewport_cfg = enabled ? 1 : 0;
|
||||
ws_local_viewport_slot = slot == 1 ? 1 : 0;
|
||||
ws_nw_sync_target();
|
||||
}
|
||||
|
||||
static int ws_local_viewport_layout(int *base_x, int *source_w,
|
||||
int *wide_w, int *offset) {
|
||||
if (!ws_local_viewport_cfg || !ws_native_wide_configured() ||
|
||||
!ws_vertical_split_active())
|
||||
return 0;
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (di.disabled || di.depth24 || di.width < 2 || di.height == 0)
|
||||
return 0;
|
||||
if ((di.width & 1u) != 0)
|
||||
return 0;
|
||||
|
||||
int src_w = (int)di.width / 2;
|
||||
int target_w = ((int)di.height * ws_cfg_num + ws_cfg_den / 2) / ws_cfg_den;
|
||||
if (target_w < src_w)
|
||||
target_w = src_w;
|
||||
int off = (target_w - src_w) / 2;
|
||||
int final_w = src_w + off * 2;
|
||||
|
||||
if (base_x) *base_x = (int)di.display_x + (ws_local_viewport_slot ? src_w : 0);
|
||||
if (source_w) *source_w = src_w;
|
||||
if (wide_w) *wide_w = final_w;
|
||||
if (offset) *offset = off;
|
||||
return final_w > src_w && off > 0;
|
||||
}
|
||||
|
||||
static int ws_nw_configured_offset(void) {
|
||||
if (!ws_native_wide_configured()) return 0;
|
||||
int local_offset = 0;
|
||||
if (ws_local_viewport_layout(NULL, NULL, NULL, &local_offset))
|
||||
return local_offset;
|
||||
int numr = 3 * ws_cfg_num - 4 * ws_cfg_den;
|
||||
int w = (int)ws_disp_w();
|
||||
return (w * numr + 4 * ws_cfg_den) / (8 * ws_cfg_den);
|
||||
@@ -356,6 +402,23 @@ static int ws_nw_offset(void) {
|
||||
}
|
||||
int ws_nw_extra(void) { return 2 * ws_nw_offset(); }
|
||||
|
||||
int ws_nw_present_width(void) {
|
||||
int wide_w = 0;
|
||||
if (ws_local_viewport_layout(NULL, NULL, &wide_w, NULL))
|
||||
return wide_w;
|
||||
return (int)ws_disp_w() + ws_nw_extra();
|
||||
}
|
||||
|
||||
int gpu_ws_netplay_local_viewport_base_x(void) {
|
||||
int base = 0;
|
||||
return ws_local_viewport_layout(&base, NULL, NULL, NULL) ? base : 0;
|
||||
}
|
||||
|
||||
int gpu_ws_netplay_local_viewport_width(void) {
|
||||
int wide_w = 0;
|
||||
return ws_local_viewport_layout(NULL, NULL, &wide_w, NULL) ? wide_w : 0;
|
||||
}
|
||||
|
||||
/* Per-side X cull margin in screen/world units (the game's draw classifier
|
||||
* works in objX-camX where 1 unit ~= 1 native-4:3 screen pixel). The squash
|
||||
* shows a half-view of 160/s pixels (s = squash factor = ws_xnum/ws_xden), so
|
||||
@@ -1408,14 +1471,15 @@ int psx_ws_cull_bltz_at(const uint32_t *words, int n, int idx) {
|
||||
* x is the int16 screenX the handler was about to store. */
|
||||
int psx_ws_backdrop_x(int x) {
|
||||
if (ws_native_wide_active()) {
|
||||
int32_t X = ws_disp_x();
|
||||
int32_t W = (int32_t)ws_disp_w();
|
||||
int32_t extra = ws_nw_extra();
|
||||
int32_t cx = W / 2;
|
||||
int32_t d = (int16_t)x - cx;
|
||||
return (int)(cx + (d * (W + extra) + (d >= 0 ? W / 2 : -W / 2)) / W);
|
||||
int32_t d = (int16_t)x - X - cx;
|
||||
return (int)(X + cx + (d * (W + extra) + (d >= 0 ? W / 2 : -W / 2)) / W);
|
||||
}
|
||||
if (!ws_active()) return (int16_t)x;
|
||||
int32_t cx = ws_disp_w() / 2; /* screen centre (=160 @ 320) */
|
||||
int32_t cx = ws_disp_x() + ws_disp_w() / 2; /* screen centre (=160 @ 320) */
|
||||
return ws_scale_about((int16_t)x, cx);
|
||||
}
|
||||
|
||||
@@ -1823,8 +1887,20 @@ static int32_t ws_scale_len(int32_t w) {
|
||||
return s < 1 ? 1 : s;
|
||||
}
|
||||
|
||||
/* Horizontal display width in drawing space (e.g. 320). */
|
||||
/* Horizontal display viewport in drawing space (e.g. 320). For a selected
|
||||
* netplay-local split viewport this is that player's authored half, not the
|
||||
* whole composed display. */
|
||||
static int32_t ws_disp_x(void) {
|
||||
int base = 0;
|
||||
if (ws_local_viewport_layout(&base, NULL, NULL, NULL))
|
||||
return (int32_t)base;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int32_t ws_disp_w(void) {
|
||||
int src_w = 0;
|
||||
if (ws_local_viewport_layout(NULL, &src_w, NULL, NULL))
|
||||
return (int32_t)src_w;
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
return di.width ? (int32_t)di.width : 320;
|
||||
@@ -1841,8 +1917,8 @@ static int32_t ws_disp_h(void) {
|
||||
* display; ordinary world-space rectangles remain untouched. */
|
||||
static void ws_expand_fullscreen_rect(int32_t *x, int32_t y, int *w, int h) {
|
||||
if (!ws_native_wide_active()) return;
|
||||
int W = (int)ws_disp_w(), H = (int)ws_disp_h();
|
||||
if (*x <= 0 && *x + *w >= W && y <= 0 && y + h >= H) {
|
||||
int X = (int)ws_disp_x(), W = (int)ws_disp_w(), H = (int)ws_disp_h();
|
||||
if (*x <= X && *x + *w >= X + W && y <= 0 && y + h >= H) {
|
||||
int off = ws_nw_offset();
|
||||
*x -= off;
|
||||
*w += 2 * off;
|
||||
@@ -1857,11 +1933,12 @@ static void ws_expand_fullscreen_rect(int32_t *x, int32_t y, int *w, int h) {
|
||||
* one zone, so its pieces share a pivot and stay aligned. (Full-2D menu
|
||||
* screens never reach here — they get zero squash + 4:3 pillarbox instead.) */
|
||||
static int32_t ws_hud_pivot(int32_t x, int32_t w) {
|
||||
int32_t X = ws_disp_x();
|
||||
int32_t W = ws_disp_w();
|
||||
int32_t cx = 2 * x + w; /* 2*centre, avoids losing the half */
|
||||
if (3 * cx < 2 * W) return 0;
|
||||
if (3 * cx > 4 * W) return W;
|
||||
return W / 2;
|
||||
int32_t cx = 2 * (x - X) + w; /* 2*centre, avoids losing the half */
|
||||
if (3 * cx < 2 * W) return X;
|
||||
if (3 * cx > 4 * W) return X + W;
|
||||
return X + W / 2;
|
||||
}
|
||||
|
||||
/* Automatic UI correction is deliberately tied to draw provenance, not to
|
||||
@@ -1933,8 +2010,8 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) {
|
||||
if (vy[i] > max_y) max_y = vy[i];
|
||||
}
|
||||
int32_t width = max_x - min_x, height = max_y - min_y;
|
||||
int32_t W = ws_disp_w(), H = ws_disp_h();
|
||||
if ((min_x <= 0 && max_x >= W && min_y <= 0 && max_y >= H) ||
|
||||
int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h();
|
||||
if ((min_x <= X && max_x >= X + W && min_y <= 0 && max_y >= H) ||
|
||||
(width > W / 2 && height > H / 4))
|
||||
return 0;
|
||||
|
||||
@@ -1948,8 +2025,8 @@ static int ws_auto_ui_transform_quad(int32_t vx[4], const int32_t vy[4]) {
|
||||
static int ws_auto_ui_transform_rect(int32_t *x, int32_t y, int *w, int h) {
|
||||
if (!x || !w || *w <= 0 || psx_ws_prim_is_tagged())
|
||||
return 0;
|
||||
int32_t W = ws_disp_w(), H = ws_disp_h();
|
||||
if ((*x <= 0 && *x + *w >= W && y <= 0 && y + h >= H) ||
|
||||
int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h();
|
||||
if ((*x <= X && *x + *w >= X + W && y <= 0 && y + h >= H) ||
|
||||
(*w > W / 2 && h > H / 4))
|
||||
return 0;
|
||||
int32_t anchor;
|
||||
@@ -2026,8 +2103,9 @@ static int32_t ws_nw_hud_shift(int32_t x, int32_t w) {
|
||||
* for live A/B: some HUD composites (Tomba's AP counter) render through
|
||||
* the tagged sprite funnel and stay inset without it. */
|
||||
if (ws_anchor_addr && !ws_nw_hud_tag_rects && psx_ws_prim_is_tagged()) return 0;
|
||||
int32_t X = ws_disp_x();
|
||||
int32_t W = ws_disp_w();
|
||||
int32_t cx = 2 * x + w; /* 2*centre, avoids losing the half */
|
||||
int32_t cx = 2 * (x - X) + w; /* 2*centre, avoids losing the half */
|
||||
if (3 * cx < 2 * W) return -off; /* left third -> pull to left edge */
|
||||
if (3 * cx > 4 * W) return off; /* right third -> push to right edge */
|
||||
return 0; /* middle third -> stay centred */
|
||||
@@ -2072,6 +2150,7 @@ static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) {
|
||||
if (!ws_nw_backdrop || !ws_native_wide_active()) return 0;
|
||||
int32_t extra = ws_nw_extra();
|
||||
if (extra <= 0) return 0;
|
||||
int32_t X = ws_disp_x();
|
||||
int32_t W = ws_disp_w();
|
||||
const int32_t EDGE = 24; /* slack for "touches the frame edge" */
|
||||
int32_t minx = vx[0], maxx = vx[0], miny = vy[0], maxy = vy[0];
|
||||
@@ -2082,7 +2161,7 @@ static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) {
|
||||
if (vy[i] > maxy) maxy = vy[i];
|
||||
}
|
||||
/* Must span the full display width and a real vertical extent. */
|
||||
if (minx > EDGE || maxx < W - EDGE || (maxy - miny) < 64) return 0;
|
||||
if (minx > X + EDGE || maxx < X + W - EDGE || (maxy - miny) < 64) return 0;
|
||||
/* Axis-aligned: every vertex X sits at either the min or the max edge, and
|
||||
* every Y at the top or bottom edge (a true screen-space rectangle). */
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -2092,7 +2171,7 @@ static int ws_nw_backdrop_stretch_quad(int32_t *vx, const int32_t *vy) {
|
||||
}
|
||||
/* Stretch X about the display centre by (W+extra)/W so [0,W] -> [-off, W+off],
|
||||
* which the wide compositor (+off) maps onto the full [0, W+extra] surface. */
|
||||
int32_t cx = W / 2;
|
||||
int32_t cx = X + W / 2;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int32_t d = vx[i] - cx;
|
||||
vx[i] = cx + (d * (W + extra) + (d >= 0 ? W / 2 : -W / 2)) / W;
|
||||
@@ -2165,6 +2244,59 @@ static uint32_t texture_disable; /* bit 15 */
|
||||
static uint32_t draw_area_left, draw_area_top;
|
||||
static uint32_t draw_area_right, draw_area_bottom;
|
||||
|
||||
static int draw_area_intersects_rect(int x, int y, int w, int h) {
|
||||
if (w <= 0 || h <= 0)
|
||||
return 0;
|
||||
int r = x + w - 1;
|
||||
int b = y + h - 1;
|
||||
return !((int)draw_area_right < x ||
|
||||
(int)draw_area_left > r ||
|
||||
(int)draw_area_bottom < y ||
|
||||
(int)draw_area_top > b);
|
||||
}
|
||||
|
||||
static int ws_local_viewport_draw_target(int *base_x) {
|
||||
int base = 0, src_w = 0;
|
||||
if (!ws_local_viewport_layout(&base, &src_w, NULL, NULL))
|
||||
return 0;
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (draw_area_intersects_rect(base, (int)di.display_y,
|
||||
src_w, (int)di.height)) {
|
||||
if (base_x) *base_x = base;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int ws_display_viewport_draw_target(int *base_x) {
|
||||
if (!ws_native_wide_active())
|
||||
return 0;
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (di.disabled || di.depth24 || di.width == 0 || di.height == 0)
|
||||
return 0;
|
||||
if (!draw_area_intersects_rect((int)di.display_x, (int)di.display_y,
|
||||
(int)di.width, (int)di.height))
|
||||
return 0;
|
||||
if (base_x) *base_x = (int)di.display_x;
|
||||
return 1;
|
||||
}
|
||||
|
||||
typedef struct GpuVerticalSplitTrace {
|
||||
uint8_t left_seen;
|
||||
uint8_t right_seen;
|
||||
uint16_t display_w;
|
||||
uint16_t display_h;
|
||||
} GpuVerticalSplitTrace;
|
||||
|
||||
static GpuVerticalSplitTrace split_trace_this;
|
||||
static GpuVerticalSplitTrace split_trace_last;
|
||||
static uint8_t split_recent_left_age = 255;
|
||||
static uint8_t split_recent_right_age = 255;
|
||||
static uint16_t split_recent_display_w = 0;
|
||||
static uint16_t split_recent_display_h = 0;
|
||||
|
||||
/* Draw offset (set by GP0(E5h)) */
|
||||
static int32_t draw_offset_x, draw_offset_y;
|
||||
/* Instrumentation: per-vblank range/count of GP0(E5) draw-offset-Y sets. If a
|
||||
@@ -2231,12 +2363,80 @@ static int ws_is_fb_base(uint32_t bx) {
|
||||
for (int i = 0; i < ws_fb_n; i++) if (ws_fb_base[i] == bx) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void split_trace_reset(GpuVerticalSplitTrace *trace) {
|
||||
memset(trace, 0, sizeof(*trace));
|
||||
}
|
||||
|
||||
static void split_trace_note_draw_area(void) {
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (di.disabled || di.depth24 || di.width < 256 || di.height < 128)
|
||||
return;
|
||||
if ((di.width & 1u) != 0)
|
||||
return;
|
||||
if (di.display_x + di.width > 1024u || di.display_y + di.height > 512u)
|
||||
return;
|
||||
if (draw_area_right <= draw_area_left || draw_area_bottom <= draw_area_top)
|
||||
return;
|
||||
|
||||
const int disp_l = (int)di.display_x;
|
||||
const int disp_r = (int)(di.display_x + di.width - 1u);
|
||||
const int mid = disp_l + (int)(di.width / 2u);
|
||||
const int tol = (int)di.width / 32 > 8 ? (int)di.width / 32 : 8;
|
||||
|
||||
const int area_l = (int)draw_area_left;
|
||||
const int area_r = (int)draw_area_right;
|
||||
|
||||
if (split_trace_this.display_w != 0 &&
|
||||
(split_trace_this.display_w != (uint16_t)di.width ||
|
||||
split_trace_this.display_h != (uint16_t)di.height)) {
|
||||
split_trace_reset(&split_trace_this);
|
||||
}
|
||||
split_trace_this.display_w = (uint16_t)di.width;
|
||||
split_trace_this.display_h = (uint16_t)di.height;
|
||||
|
||||
if (area_l <= disp_l + tol &&
|
||||
area_r >= mid - 1 - tol && area_r <= mid - 1 + tol) {
|
||||
split_trace_this.left_seen = 1;
|
||||
}
|
||||
if (area_l >= mid - tol && area_l <= mid + tol &&
|
||||
area_r >= disp_r - tol) {
|
||||
split_trace_this.right_seen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static int ws_vertical_split_active(void) {
|
||||
return split_recent_left_age <= 8 && split_recent_right_age <= 8;
|
||||
}
|
||||
|
||||
int gpu_last_frame_vertical_split_screen(void) {
|
||||
return ws_vertical_split_active();
|
||||
}
|
||||
|
||||
void gpu_vertical_split_debug(int *active, int *left_age, int *right_age) {
|
||||
if (active) *active = gpu_last_frame_vertical_split_screen();
|
||||
if (left_age) *left_age = split_recent_left_age;
|
||||
if (right_age) *right_age = split_recent_right_age;
|
||||
}
|
||||
|
||||
/* Point the renderer's wide mirror at the current back buffer (draw_area_left)
|
||||
* when native-wide is active and that buffer is a known display buffer; else
|
||||
* disable mirroring for this draw. Called when the draw env changes. */
|
||||
static void ws_nw_sync_target(void) {
|
||||
if (!ws_native_wide_active()) { gr_wide_disable_target(); return; }
|
||||
gr_wide_configure((int)ws_disp_w() + ws_nw_extra(), ws_nw_offset());
|
||||
gr_wide_configure(ws_nw_present_width(), ws_nw_offset());
|
||||
int local_base = 0;
|
||||
if (ws_local_viewport_draw_target(&local_base)) {
|
||||
gr_wide_set_target(local_base);
|
||||
return;
|
||||
}
|
||||
if (ws_local_viewport_cfg) { gr_wide_disable_target(); return; }
|
||||
int display_base = 0;
|
||||
if (ws_display_viewport_draw_target(&display_base)) {
|
||||
gr_wide_set_target(display_base);
|
||||
return;
|
||||
}
|
||||
uint32_t base = draw_area_left;
|
||||
if (ws_is_fb_base(base)) gr_wide_set_target((int)base);
|
||||
else gr_wide_disable_target();
|
||||
@@ -2362,6 +2562,12 @@ static void gpu_reset_state(int clear_vram) {
|
||||
draw_area_top = 0;
|
||||
draw_area_right = 0;
|
||||
draw_area_bottom = 0;
|
||||
split_trace_reset(&split_trace_this);
|
||||
split_trace_reset(&split_trace_last);
|
||||
split_recent_left_age = 255;
|
||||
split_recent_right_age = 255;
|
||||
split_recent_display_w = 0;
|
||||
split_recent_display_h = 0;
|
||||
draw_offset_x = 0;
|
||||
draw_offset_y = 0;
|
||||
texture_window_value = 0;
|
||||
@@ -2654,6 +2860,26 @@ void gpu_vblank_tick(void) {
|
||||
g_doff_cnt_last = g_doff_cnt_this;
|
||||
}
|
||||
g_doff_min_this = 0x7fffffff; g_doff_max_this = -0x7fffffff; g_doff_cnt_this = 0;
|
||||
split_trace_last = split_trace_this;
|
||||
if (split_trace_this.display_w != 0 &&
|
||||
(split_recent_display_w != split_trace_this.display_w ||
|
||||
split_recent_display_h != split_trace_this.display_h)) {
|
||||
split_recent_left_age = 255;
|
||||
split_recent_right_age = 255;
|
||||
split_recent_display_w = split_trace_this.display_w;
|
||||
split_recent_display_h = split_trace_this.display_h;
|
||||
}
|
||||
if (split_trace_this.left_seen) {
|
||||
split_recent_left_age = 0;
|
||||
} else if (split_recent_left_age < 255) {
|
||||
split_recent_left_age++;
|
||||
}
|
||||
if (split_trace_this.right_seen) {
|
||||
split_recent_right_age = 0;
|
||||
} else if (split_recent_right_age < 255) {
|
||||
split_recent_right_age++;
|
||||
}
|
||||
split_trace_reset(&split_trace_this);
|
||||
gpustat_poll_count = 0;
|
||||
/* Trusted package-selected plugins run on guest VBlank, independent of
|
||||
* host presentation, pacing, turbo, or skipped frames. */
|
||||
@@ -2956,48 +3182,64 @@ static void parse_vertex(uint32_t word, int32_t* x, int32_t* y) {
|
||||
*y = sign_extend((word >> 16) & 0x7FFu, 11);
|
||||
}
|
||||
|
||||
extern int gte_geometry_correction_lookup(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16);
|
||||
extern int gte_geometry_correction_enabled(void);
|
||||
static int s_texture_correction_enabled = 0;
|
||||
extern void gte_precision_tracking_set(int enabled);
|
||||
extern int gte_precision_load_word(uint32_t addr, uint32_t packed,
|
||||
int32_t *x16, int32_t *y16, uint16_t *z);
|
||||
|
||||
void gpu_texture_correction_set(int enabled) {
|
||||
s_texture_correction_enabled = enabled ? 1 : 0;
|
||||
gte_precision_tracking_set(enabled);
|
||||
/* The PGXP dataflow engine feeds BOTH corrections; arm it while either
|
||||
* is on (geometry correction is toggled in gte.cpp, so re-derive here). */
|
||||
pgxp_set_enabled(s_texture_correction_enabled ||
|
||||
gte_geometry_correction_enabled());
|
||||
}
|
||||
|
||||
int gpu_texture_correction_enabled(void) {
|
||||
return s_texture_correction_enabled;
|
||||
}
|
||||
|
||||
uint32_t gpu_texture_correction_hits(void) {
|
||||
return sw_perspective_triangle_count();
|
||||
}
|
||||
|
||||
/* Match all three GP0 positions to recent GTE projections. Requiring a full
|
||||
* triangle match prevents screen-space HUD/sprites that happen to share one
|
||||
* coordinate from receiving world-geometry correction. The integer delta
|
||||
* folds in draw offsets and any widescreen adjustment already applied. */
|
||||
static void prepare_precise_triangle(uint32_t w0, uint32_t w1, uint32_t w2,
|
||||
/* Per-vertex precise positions (PGXP, ENHANCEMENTS.md G1). Each of the three
|
||||
* packet words is resolved independently: the address-keyed dataflow shadow
|
||||
* first (validated against the actual word — exact provenance, survives
|
||||
* ordering-table reordering), the ambiguity-gated position cache second, the
|
||||
* parsed integers last. Mixing precise and native vertices in one triangle
|
||||
* is correct — a native vertex is exactly where the uncorrected pipeline put
|
||||
* it, so shared edges between neighbouring triangles cannot disagree by more
|
||||
* than the sub-pixel fraction. The integer delta folds in draw offsets and
|
||||
* any widescreen adjustment already applied by the caller. */
|
||||
static void prepare_precise_triangle(int i0, int i1, int i2,
|
||||
const int32_t vx[3], const int32_t vy[3]) {
|
||||
uint32_t words[3] = { w0, w1, w2 };
|
||||
int32_t fx[3], fy[3];
|
||||
const int geometry_enabled = gte_geometry_correction_enabled();
|
||||
gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
|
||||
if (!geometry_enabled) {
|
||||
if (!gte_geometry_correction_enabled()) {
|
||||
gr_set_precise_triangle(0, 0,0, 0,0, 0,0);
|
||||
return;
|
||||
}
|
||||
const int idx[3] = { i0, i1, i2 };
|
||||
int32_t fx[3], fy[3];
|
||||
int any_precise = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint32_t word = gp0_cmd_buf[idx[i]];
|
||||
int32_t raw_x, raw_y;
|
||||
parse_vertex(words[i], &raw_x, &raw_y);
|
||||
if (!gte_geometry_correction_lookup(words[i], &fx[i], &fy[i])) {
|
||||
gr_set_precise_triangle(0, 0,0, 0,0, 0,0);
|
||||
return;
|
||||
}
|
||||
fx[i] = (int32_t)((int64_t)fx[i] +
|
||||
(int64_t)(vx[i] - raw_x) * 65536);
|
||||
fy[i] = (int32_t)((int64_t)fy[i] +
|
||||
(int64_t)(vy[i] - raw_y) * 65536);
|
||||
parse_vertex(word, &raw_x, &raw_y);
|
||||
uint32_t addr = (gp0_cmd_source_addr == 0xFFFFFFFFu)
|
||||
? 0xFFFFFFFFu
|
||||
: gp0_cmd_source_addr + (uint32_t)idx[i] * 4u;
|
||||
int32_t px, py;
|
||||
uint16_t sz;
|
||||
if (pgxp_get_precise_vertex(addr, word, raw_x, raw_y,
|
||||
&px, &py, &sz) != PGXP_SRC_NATIVE)
|
||||
any_precise = 1;
|
||||
fx[i] = (int32_t)((int64_t)px + (int64_t)(vx[i] - raw_x) * 65536);
|
||||
fy[i] = (int32_t)((int64_t)py + (int64_t)(vy[i] - raw_y) * 65536);
|
||||
}
|
||||
if (!any_precise) {
|
||||
gr_set_precise_triangle(0, 0,0, 0,0, 0,0);
|
||||
return;
|
||||
}
|
||||
gr_set_precise_triangle(1, fx[0],fy[0], fx[1],fy[1], fx[2],fy[2]);
|
||||
}
|
||||
@@ -3061,7 +3303,11 @@ static void raster_pixel(int32_t x, int32_t y, uint16_t color) {
|
||||
* https://github.com/mstan/psxrecomp/pull/73
|
||||
* Keep that credit with this guarded framebuffer-target variant. */
|
||||
static inline int32_t draw_area_wide_x_margin(void) {
|
||||
if (!ws_native_wide_active() || !ws_is_fb_base(draw_area_left)) return 0;
|
||||
int local_base = 0;
|
||||
if (!ws_native_wide_active()) return 0;
|
||||
if (ws_local_viewport_draw_target(&local_base)) return (int32_t)ws_nw_offset();
|
||||
if (ws_display_viewport_draw_target(NULL)) return (int32_t)ws_nw_offset();
|
||||
if (!ws_is_fb_base(draw_area_left)) return 0;
|
||||
return (int32_t)ws_nw_offset();
|
||||
}
|
||||
|
||||
@@ -3209,7 +3455,7 @@ static void gp0_exec_mono_tri(void) {
|
||||
}
|
||||
if (draw_area_out_bbox(vx, vy, 3)) return;
|
||||
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
|
||||
prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[2], gp0_cmd_buf[3],
|
||||
prepare_precise_triangle(1, 2, 3,
|
||||
vx, vy);
|
||||
gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color);
|
||||
}
|
||||
@@ -3275,15 +3521,13 @@ static void gp0_exec_mono_quad(void) {
|
||||
if (!rej_a) {
|
||||
int32_t tx[3] = { vx[0], vx[1], vx[2] };
|
||||
int32_t ty[3] = { vy[0], vy[1], vy[2] };
|
||||
prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[2],
|
||||
gp0_cmd_buf[3], tx, ty);
|
||||
prepare_precise_triangle(1, 2, 3, tx, ty);
|
||||
gr_draw_flat_triangle(vx[0], vy[0], vx[1], vy[1], vx[2], vy[2], color);
|
||||
}
|
||||
if (!rej_b) {
|
||||
int32_t tx[3] = { vx[2], vx[1], vx[3] };
|
||||
int32_t ty[3] = { vy[2], vy[1], vy[3] };
|
||||
prepare_precise_triangle(gp0_cmd_buf[3], gp0_cmd_buf[2],
|
||||
gp0_cmd_buf[4], tx, ty);
|
||||
prepare_precise_triangle(3, 2, 4, tx, ty);
|
||||
gr_draw_flat_triangle(vx[2], vy[2], vx[1], vy[1], vx[3], vy[3], color);
|
||||
}
|
||||
}
|
||||
@@ -3306,7 +3550,7 @@ static void gp0_exec_shaded_tri(void) {
|
||||
}
|
||||
if (draw_area_out_bbox(vx, vy, 3)) return;
|
||||
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
|
||||
prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], gp0_cmd_buf[5],
|
||||
prepare_precise_triangle(1, 3, 5,
|
||||
vx, vy);
|
||||
gr_draw_gouraud_triangle(vx[0], vy[0], c[0],
|
||||
vx[1], vy[1], c[1],
|
||||
@@ -3352,8 +3596,7 @@ static void gp0_exec_shaded_quad(void) {
|
||||
if (!rej_a) {
|
||||
int32_t tx[3] = { vx[0], vx[1], vx[2] };
|
||||
int32_t ty[3] = { vy[0], vy[1], vy[2] };
|
||||
prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3],
|
||||
gp0_cmd_buf[5], tx, ty);
|
||||
prepare_precise_triangle(1, 3, 5, tx, ty);
|
||||
gr_draw_gouraud_triangle(vx[0], vy[0], c[0],
|
||||
vx[1], vy[1], c[1],
|
||||
vx[2], vy[2], c[2]);
|
||||
@@ -3361,8 +3604,7 @@ static void gp0_exec_shaded_quad(void) {
|
||||
if (!rej_b) {
|
||||
int32_t tx[3] = { vx[2], vx[1], vx[3] };
|
||||
int32_t ty[3] = { vy[2], vy[1], vy[3] };
|
||||
prepare_precise_triangle(gp0_cmd_buf[5], gp0_cmd_buf[3],
|
||||
gp0_cmd_buf[7], tx, ty);
|
||||
prepare_precise_triangle(5, 3, 7, tx, ty);
|
||||
gr_draw_gouraud_triangle(vx[2], vy[2], c[2],
|
||||
vx[1], vy[1], c[1],
|
||||
vx[3], vy[3], c[3]);
|
||||
@@ -3433,7 +3675,7 @@ static void gp0_exec_textured_tri(void) {
|
||||
if (draw_area_out_bbox(vx, vy, 3)) return;
|
||||
|
||||
setup_textured_draw(color24, semi_trans, raw_texture);
|
||||
prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3], gp0_cmd_buf[5],
|
||||
prepare_precise_triangle(1, 3, 5,
|
||||
vx, vy);
|
||||
prepare_texture_triangle(1, 3, 5);
|
||||
gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0],
|
||||
@@ -3515,8 +3757,7 @@ static void gp0_exec_textured_quad(void) {
|
||||
if (!rej_a) {
|
||||
int32_t tx[3] = { vx[0], vx[1], vx[2] };
|
||||
int32_t ty[3] = { vy[0], vy[1], vy[2] };
|
||||
prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[3],
|
||||
gp0_cmd_buf[5], tx, ty);
|
||||
prepare_precise_triangle(1, 3, 5, tx, ty);
|
||||
prepare_texture_triangle(1, 3, 5);
|
||||
gr_draw_textured_triangle(vx[0], vy[0], u[0], v[0],
|
||||
vx[1], vy[1], u[1], v[1],
|
||||
@@ -3526,8 +3767,7 @@ static void gp0_exec_textured_quad(void) {
|
||||
if (!rej_b) {
|
||||
int32_t tx[3] = { vx[2], vx[1], vx[3] };
|
||||
int32_t ty[3] = { vy[2], vy[1], vy[3] };
|
||||
prepare_precise_triangle(gp0_cmd_buf[5], gp0_cmd_buf[3],
|
||||
gp0_cmd_buf[7], tx, ty);
|
||||
prepare_precise_triangle(5, 3, 7, tx, ty);
|
||||
prepare_texture_triangle(5, 3, 7);
|
||||
gr_draw_textured_triangle(vx[2], vy[2], u[2], v[2],
|
||||
vx[1], vy[1], u[1], v[1],
|
||||
@@ -3569,7 +3809,7 @@ static void gp0_exec_shaded_textured_tri(void) {
|
||||
if (draw_area_out_bbox(vx, vy, 3)) return;
|
||||
|
||||
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
|
||||
prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[4], gp0_cmd_buf[7],
|
||||
prepare_precise_triangle(1, 4, 7,
|
||||
vx, vy);
|
||||
prepare_texture_triangle(1, 4, 7);
|
||||
gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0],
|
||||
@@ -3620,8 +3860,7 @@ static void gp0_exec_shaded_textured_quad(void) {
|
||||
if (!rej_a) {
|
||||
int32_t tx[3] = { vx[0], vx[1], vx[2] };
|
||||
int32_t ty[3] = { vy[0], vy[1], vy[2] };
|
||||
prepare_precise_triangle(gp0_cmd_buf[1], gp0_cmd_buf[4],
|
||||
gp0_cmd_buf[7], tx, ty);
|
||||
prepare_precise_triangle(1, 4, 7, tx, ty);
|
||||
prepare_texture_triangle(1, 4, 7);
|
||||
gr_draw_shaded_textured_triangle(vx[0], vy[0], u[0], v[0], c[0],
|
||||
vx[1], vy[1], u[1], v[1], c[1],
|
||||
@@ -3631,8 +3870,7 @@ static void gp0_exec_shaded_textured_quad(void) {
|
||||
if (!rej_b) {
|
||||
int32_t tx[3] = { vx[2], vx[1], vx[3] };
|
||||
int32_t ty[3] = { vy[2], vy[1], vy[3] };
|
||||
prepare_precise_triangle(gp0_cmd_buf[7], gp0_cmd_buf[4],
|
||||
gp0_cmd_buf[10], tx, ty);
|
||||
prepare_precise_triangle(7, 4, 10, tx, ty);
|
||||
prepare_texture_triangle(7, 4, 10);
|
||||
gr_draw_shaded_textured_triangle(vx[2], vy[2], u[2], v[2], c[2],
|
||||
vx[1], vy[1], u[1], v[1], c[1],
|
||||
@@ -3865,7 +4103,11 @@ static void gp0_exec_fill_rect(void) {
|
||||
/* Native-wide: when the game clears a display buffer, clear the full width
|
||||
* of that buffer's wide surface over the same rows — refreshing the centred
|
||||
* content region and keeping the revealed margins clean. */
|
||||
if (ws_native_wide_active() && ws_is_fb_base(dst_x)) {
|
||||
int local_base = 0;
|
||||
if (ws_native_wide_active() &&
|
||||
(ws_is_fb_base(dst_x) ||
|
||||
(ws_local_viewport_draw_target(&local_base) &&
|
||||
dst_x == (uint32_t)local_base))) {
|
||||
gr_wide_clear((int)dst_x, (int)dst_y, (int)height, color16);
|
||||
}
|
||||
}
|
||||
@@ -3907,6 +4149,7 @@ static void gp0_exec_draw_area_tl(void) {
|
||||
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
|
||||
draw_area_left = param & 0x3FF;
|
||||
draw_area_top = (param >> 10) & 0x3FF;
|
||||
split_trace_note_draw_area();
|
||||
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
|
||||
(int)draw_area_right, (int)draw_area_bottom);
|
||||
ws_nw_sync_target(); /* back buffer (draw_area_left) → wide mirror surface */
|
||||
@@ -3919,6 +4162,7 @@ static void gp0_exec_draw_area_br(void) {
|
||||
uint32_t param = gp0_cmd_buf[0] & 0x00FFFFFFu;
|
||||
draw_area_right = param & 0x3FF;
|
||||
draw_area_bottom = (param >> 10) & 0x3FF;
|
||||
split_trace_note_draw_area();
|
||||
gr_set_draw_area((int)draw_area_left, (int)draw_area_top,
|
||||
(int)draw_area_right, (int)draw_area_bottom);
|
||||
ws_nw_sync_target(); /* back buffer (draw_area_left) → wide mirror surface */
|
||||
@@ -4254,15 +4498,15 @@ static void ws_ui_prepass_add(const uint32_t *words, uint32_t source_addr,
|
||||
}
|
||||
|
||||
int32_t width = max_x - min_x, height = max_y - min_y;
|
||||
int32_t W = ws_disp_w(), H = ws_disp_h();
|
||||
if ((min_x <= 0 && max_x >= W && min_y <= 0 && max_y >= H) ||
|
||||
int32_t X = ws_disp_x(), W = ws_disp_w(), H = ws_disp_h();
|
||||
if ((min_x <= X && max_x >= X + W && min_y <= 0 && max_y >= H) ||
|
||||
(width > W / 2 && height > H / 4))
|
||||
return;
|
||||
|
||||
WsUiPrepassItem *item = &ws_ui_prepass[ws_ui_prepass_count++];
|
||||
item->group.key =
|
||||
ws_auto_ui_group_key_words(words, op, min_y, height);
|
||||
item->group.x = min_x;
|
||||
item->group.x = min_x - X;
|
||||
item->group.width = width;
|
||||
item->group.anchor = 0;
|
||||
item->src_addr = source_addr & 0x1FFFFCu;
|
||||
@@ -4343,16 +4587,26 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) {
|
||||
ws_ui_prepass[out++] = ws_ui_prepass[i];
|
||||
}
|
||||
ws_ui_prepass_count = out;
|
||||
ws_auto_ui_dense = ws_ui_prepass_count >= 32u;
|
||||
/*
|
||||
* A high final-layer primitive count is a good "dense 2D menu" signal for
|
||||
* titles without an explicit gameplay detector, where grouping everything
|
||||
* around the centre avoids tearing text grids apart. For GTE-gated 3D
|
||||
* titles, though, reaching this path already means a gameplay frame is
|
||||
* being stretched. WipEout 3's race HUD is dense enough to trip the old
|
||||
* threshold, which pinned every HUD group to the 4:3 centre. Keep edge
|
||||
* groups edge-anchored in those frames so the HUD adapts to the wide view.
|
||||
*/
|
||||
ws_auto_ui_dense = ws_ui_prepass_count >= 32u && !ws_gte_game_mode_cfg;
|
||||
if (ws_ui_prepass_count == 0) return;
|
||||
|
||||
const int32_t group_origin = ws_disp_x();
|
||||
WsUiGroupItem groups[WS_UI_PREPASS_MAX];
|
||||
for (uint32_t i = 0; i < ws_ui_prepass_count; i++)
|
||||
groups[i] = ws_ui_prepass[i].group;
|
||||
ws_ui_group_assign(groups, ws_ui_prepass_count, ws_disp_w(),
|
||||
ws_auto_ui_dense);
|
||||
for (uint32_t i = 0; i < ws_ui_prepass_count; i++)
|
||||
ws_ui_prepass[i].group.anchor = groups[i].anchor;
|
||||
ws_ui_prepass[i].group.anchor = group_origin + groups[i].anchor;
|
||||
}
|
||||
|
||||
/* Per-opcode execution counters (exposed via gpu_get_opcode_stats) */
|
||||
|
||||
@@ -983,10 +983,11 @@ static const char *TEX_FS =
|
||||
"uniform usampler2D u_vram;\n"
|
||||
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
|
||||
"uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n"
|
||||
"uniform ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n"
|
||||
"uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n"
|
||||
"uniform int u_filter; /* 1 = bilinear */\n"
|
||||
"int vram_at(int x, int y){\n"
|
||||
"uniform ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n"
|
||||
"uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n"
|
||||
"uniform int u_filter; /* 1 = bilinear */\n"
|
||||
"uniform float u_shift;\n"
|
||||
"int vram_at(int x, int y){\n"
|
||||
" return int(texelFetch(u_vram, ivec2(x & 1023, y & 511), 0).r);\n"
|
||||
"}\n"
|
||||
"int fetch_texel(int u, int v){\n"
|
||||
@@ -1027,12 +1028,15 @@ static const char *TEX_FS =
|
||||
" * offset and clamp to u_limits, and each texel's weight is gated by\n"
|
||||
" * its opacity with the colour renormalised — so prim edges and\n"
|
||||
" * cutout borders keep their colour instead of dissolving into the\n"
|
||||
" * transparent (black) neighbour and discarding whole edge columns. */\n"
|
||||
" * transparent (black) neighbour and discarding whole edge columns.\n"
|
||||
" * Recenter from PS1 top-left point sampling to the bilinear footprint. */\n"
|
||||
" uv += vec2(u_shift);\n"
|
||||
" int iu = int(floor(uv.x)), iv = int(floor(uv.y));\n"
|
||||
" float fx = uv.x - float(iu) - 0.5, fy = uv.y - float(iv) - 0.5;\n"
|
||||
" int sx = fx < 0.0 ? -1 : 1, sy = fy < 0.0 ? -1 : 1;\n"
|
||||
" fx = abs(fx); fy = abs(fy);\n"
|
||||
" int c00 = fetch_texel(iu, iv);\n"
|
||||
" if (c00 == 0) discard;\n"
|
||||
" int c10 = fetch_texel(iu + sx, iv);\n"
|
||||
" int c01 = fetch_texel(iu, iv + sy);\n"
|
||||
" int c11 = fetch_texel(iu + sx, iv + sy);\n"
|
||||
@@ -1041,7 +1045,6 @@ static const char *TEX_FS =
|
||||
" float w01 = (c01 == 0 ? 0.0 : 1.0) * (1.0 - fx) * fy;\n"
|
||||
" float w11 = (c11 == 0 ? 0.0 : 1.0) * fx * fy;\n"
|
||||
" float opac = w00 + w10 + w01 + w11;\n"
|
||||
" if (opac < 0.5) discard;\n"
|
||||
" rgb = (col5(c00)*w00 + col5(c10)*w10 + col5(c01)*w01 + col5(c11)*w11) / opac;\n"
|
||||
" float stpf = (float((c00 >> 15) & 1) * w00 + float((c10 >> 15) & 1) * w10\n"
|
||||
" + float((c01 >> 15) & 1) * w01 + float((c11 >> 15) & 1) * w11) / opac;\n"
|
||||
|
||||
@@ -408,6 +408,7 @@ static uint16_t texel_fetch_bilinear(float fu, float fv, uint16_t texpage,
|
||||
if (fy > 128) fy = 128;
|
||||
|
||||
uint16_t c00 = bl_fetch(iu, iv, texpage, clut_x, clut_y);
|
||||
if (c00 == 0) return 0x0000;
|
||||
uint16_t c10 = bl_fetch(iu + su, iv, texpage, clut_x, clut_y);
|
||||
uint16_t c01 = bl_fetch(iu, iv + sv, texpage, clut_x, clut_y);
|
||||
uint16_t c11 = bl_fetch(iu + su, iv + sv, texpage, clut_x, clut_y);
|
||||
@@ -417,7 +418,6 @@ static uint16_t texel_fetch_bilinear(float fu, float fv, uint16_t texpage,
|
||||
int w01 = (c01 ? 1 : 0) * (256 - fx) * fy;
|
||||
int w11 = (c11 ? 1 : 0) * fx * fy;
|
||||
int opac = w00 + w10 + w01 + w11;
|
||||
if (opac < 32768) return 0x0000; /* opacity < 0.5: transparent */
|
||||
|
||||
int r = ((c00 & 0x1F) * w00 + (c10 & 0x1F) * w10
|
||||
+ (c01 & 0x1F) * w01 + (c11 & 0x1F) * w11) / opac;
|
||||
@@ -431,6 +431,11 @@ static uint16_t texel_fetch_bilinear(float fu, float fv, uint16_t texpage,
|
||||
return (uint16_t)(r | (g << 5) | (b << 10) | (stp ? 0x8000 : 0));
|
||||
}
|
||||
|
||||
static inline float bilinear_center_shift_for_target(const RTarget *t) {
|
||||
int s = (t && t->s > 0) ? t->s : 1;
|
||||
return 0.5f / (float)s - 1.0f / 64.0f;
|
||||
}
|
||||
|
||||
/* uv sampling bounds (see g_uv_lim): the shared PS1 uv model in gpu_uv.h.
|
||||
* The software DDA truncates exactly like the PS1, so unlike the GL/VK
|
||||
* backends NO mirrored-2D compensation is applied here — these bounds only
|
||||
@@ -982,10 +987,16 @@ static void raster_textured_triangle(const RTarget *t,
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t texel = g_texture_filter
|
||||
? texel_fetch_bilinear(fu, fv, texpage, clut_x, clut_y)
|
||||
: texel_fetch((int)fu & 0xFF, (int)fv & 0xFF, texpage, clut_x, clut_y);
|
||||
put_textured(t, x, y, texel, g_mod_r, g_mod_g, g_mod_b, g_raw_texture);
|
||||
if (g_texture_filter) {
|
||||
float sh = bilinear_center_shift_for_target(t);
|
||||
uint16_t texel = texel_fetch_bilinear(fu + sh, fv + sh,
|
||||
texpage, clut_x, clut_y);
|
||||
put_textured(t, x, y, texel, g_mod_r, g_mod_g, g_mod_b, g_raw_texture);
|
||||
} else {
|
||||
uint16_t texel = texel_fetch((int)fu & 0xFF, (int)fv & 0xFF,
|
||||
texpage, clut_x, clut_y);
|
||||
put_textured(t, x, y, texel, g_mod_r, g_mod_g, g_mod_b, g_raw_texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1161,10 +1172,16 @@ static void raster_shaded_textured_triangle(const RTarget *t,
|
||||
if (mg < 0) mg = 0; if (mg > 31) mg = 31;
|
||||
if (mb < 0) mb = 0; if (mb > 31) mb = 31;
|
||||
|
||||
uint16_t texel = g_texture_filter
|
||||
? texel_fetch_bilinear(fu, fv, texpage, clut_x, clut_y)
|
||||
: texel_fetch((int)fu & 0xFF, (int)fv & 0xFF, texpage, clut_x, clut_y);
|
||||
put_textured(t, x, y, texel, mr, mg, mb, raw_texture);
|
||||
if (g_texture_filter) {
|
||||
float sh = bilinear_center_shift_for_target(t);
|
||||
uint16_t texel = texel_fetch_bilinear(fu + sh, fv + sh,
|
||||
texpage, clut_x, clut_y);
|
||||
put_textured(t, x, y, texel, mr, mg, mb, raw_texture);
|
||||
} else {
|
||||
uint16_t texel = texel_fetch((int)fu & 0xFF, (int)fv & 0xFF,
|
||||
texpage, clut_x, clut_y);
|
||||
put_textured(t, x, y, texel, mr, mg, mb, raw_texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1300,7 +1317,8 @@ static void raster_textured_rect(const RTarget *t, int x, int y, int w, int h,
|
||||
uint16_t texel;
|
||||
if (g_texture_filter) {
|
||||
float fu = (float)u + (float)col / (float)s;
|
||||
texel = texel_fetch_bilinear(fu, fv, texpage, clut_x, clut_y);
|
||||
float sh = bilinear_center_shift_for_target(t);
|
||||
texel = texel_fetch_bilinear(fu + sh, fv + sh, texpage, clut_x, clut_y);
|
||||
} else {
|
||||
int tu = (u + col / s) & 0xFF;
|
||||
texel = texel_fetch(tu, tv, texpage, clut_x, clut_y);
|
||||
@@ -1391,7 +1409,8 @@ static void raster_textured_rect_scaled(const RTarget *t, int x, int y,
|
||||
uint16_t texel;
|
||||
if (g_texture_filter) {
|
||||
float fu = (float)u0 + (float)du * (float)col / (float)w;
|
||||
texel = texel_fetch_bilinear(fu, fv, texpage, clut_x, clut_y);
|
||||
float sh = bilinear_center_shift_for_target(t);
|
||||
texel = texel_fetch_bilinear(fu + sh, fv + sh, texpage, clut_x, clut_y);
|
||||
} else {
|
||||
int tu = (int)(u0 + ((int64_t)du * col) / w) & 0xFF;
|
||||
texel = texel_fetch(tu, tv, texpage, clut_x, clut_y);
|
||||
|
||||
+77
-123
@@ -1,12 +1,14 @@
|
||||
#include "gte.h"
|
||||
#include "cpu_state.h"
|
||||
#include "nd_intro_ot.h"
|
||||
#include "pgxp.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
extern "C" uint32_t psx_read_word(uint32_t addr);
|
||||
extern "C" int gpu_ws_precise_nclip_enabled(void);
|
||||
|
||||
namespace PSXRecomp {
|
||||
namespace GTE {
|
||||
@@ -222,38 +224,16 @@ static uint32_t s_geom_lookups = 0; /* lookups attempted *
|
||||
static uint32_t s_geom_miss_unrec = 0; /* nothing was ever recorded here */
|
||||
static uint32_t s_geom_miss_ambig = 0; /* recorded, but not unambiguously */
|
||||
|
||||
/* Exact GTE projection provenance for perspective texture correction. The
|
||||
* recompiler/interpreters call gte_precision_store after SWC2 writes an SXY
|
||||
* register into guest RAM. GPU DMA later supplies that exact RAM address, so
|
||||
* depth lookup follows the packet rather than guessing from rounded X/Y. */
|
||||
struct PreciseProjection {
|
||||
uint32_t packed;
|
||||
int32_t x16, y16;
|
||||
uint16_t z;
|
||||
uint8_t valid;
|
||||
};
|
||||
static PreciseProjection s_precise_sxy[4] = {};
|
||||
|
||||
#define PRECISION_STORE_SIZE 65536u
|
||||
struct PrecisionStoreEntry {
|
||||
uint32_t addr;
|
||||
PreciseProjection projection;
|
||||
uint32_t generation;
|
||||
};
|
||||
static PrecisionStoreEntry s_precision_store[PRECISION_STORE_SIZE];
|
||||
static uint32_t s_precision_generation = 1;
|
||||
static int s_precision_tracking = 0;
|
||||
static PreciseProjection s_speculative_saved_sxy[4];
|
||||
/* Exact GTE projection provenance now lives in the PGXP value-propagation
|
||||
* engine (pgxp.cpp): per-word RAM/scratchpad shadows plus per-GPR and per-GTE-
|
||||
* register shadows, validated on read (ENHANCEMENTS.md G1.2/G1.3). The
|
||||
* gte_precision_* entry points below are kept as the stable emit/ABI surface
|
||||
* (v14 swc2 sites, tests) and forward into that engine. The hashed
|
||||
* s_precision_store table and the s_precise_sxy FIFO it fed are retired —
|
||||
* the GTE register shadows ARE the FIFO now. */
|
||||
static uint32_t s_speculative_depth = 0;
|
||||
static int s_speculative_timeline_invalidated = 0;
|
||||
|
||||
static void gte_precision_generation_advance(void) {
|
||||
if (++s_precision_generation == 0) {
|
||||
std::memset(s_precision_store, 0, sizeof(s_precision_store));
|
||||
s_precision_generation = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void gte_geom_generation_advance(void) {
|
||||
if (++s_geom_generation == 0) {
|
||||
if (s_geom_cache)
|
||||
@@ -263,95 +243,63 @@ static void gte_geom_generation_advance(void) {
|
||||
}
|
||||
|
||||
extern "C" void gte_precision_timeline_invalidate(void) {
|
||||
for (int i = 0; i < 4; ++i) s_precise_sxy[i].valid = 0;
|
||||
/* Raw machine-state restore is authoritative even if host polling reached
|
||||
* it during a speculative validation pass. Defer the generation advance
|
||||
* until the outer transaction ends so old provenance cannot be restored. */
|
||||
* until the outer transaction ends so old provenance cannot be restored.
|
||||
* (pgxp_invalidate_all defers the same way behind its suppress bracket.) */
|
||||
pgxp_invalidate_all();
|
||||
if (s_speculative_depth != 0) {
|
||||
s_speculative_timeline_invalidated = 1;
|
||||
return;
|
||||
}
|
||||
gte_precision_generation_advance();
|
||||
gte_geom_generation_advance();
|
||||
}
|
||||
|
||||
extern "C" void gte_precision_speculative_begin(void) {
|
||||
if (s_speculative_depth++ == 0) {
|
||||
std::memcpy(s_speculative_saved_sxy, s_precise_sxy,
|
||||
sizeof(s_precise_sxy));
|
||||
if (s_speculative_depth++ == 0)
|
||||
s_speculative_timeline_invalidated = 0;
|
||||
}
|
||||
/* The speculative pass runs real GTE commands and hooks on state that is
|
||||
* rolled back afterwards: suppress all shadow recording so the shadows
|
||||
* still describe the restored timeline when the bracket closes. */
|
||||
pgxp_suppress_begin();
|
||||
}
|
||||
|
||||
extern "C" void gte_precision_speculative_end(void) {
|
||||
if (s_speculative_depth == 0) return;
|
||||
pgxp_suppress_end();
|
||||
if (--s_speculative_depth == 0) {
|
||||
if (s_speculative_timeline_invalidated) {
|
||||
gte_precision_generation_advance();
|
||||
gte_geom_generation_advance();
|
||||
s_speculative_timeline_invalidated = 0;
|
||||
} else {
|
||||
std::memcpy(s_precise_sxy, s_speculative_saved_sxy,
|
||||
sizeof(s_precise_sxy));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static inline uint32_t precision_hash(uint32_t addr) {
|
||||
addr >>= 2;
|
||||
addr ^= addr >> 11;
|
||||
return (addr * 2654435761u) & (PRECISION_STORE_SIZE - 1u);
|
||||
}
|
||||
|
||||
static inline int precision_ram_address(uint32_t addr, uint32_t *physical) {
|
||||
uint32_t mapped = addr & 0x1FFFFFFFu;
|
||||
if (mapped >= 0x00800000u) return 0;
|
||||
*physical = mapped & 0x1FFFFCu;
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" void gte_precision_tracking_set(int enabled) {
|
||||
s_precision_tracking = enabled ? 1 : 0;
|
||||
gte_precision_generation_advance();
|
||||
pgxp_set_enabled(enabled);
|
||||
}
|
||||
|
||||
/* v14 ABI emit surface: every swc2 site (all backends, overlay DLLs) calls
|
||||
* this. The pgxp engine copies the GTE register shadow to the RAM shadow;
|
||||
* the GPU-side lookup validates against the actual packet word, so a stale
|
||||
* register shadow can never be believed. */
|
||||
extern "C" void gte_precision_store_word(uint32_t addr, uint8_t reg) {
|
||||
if (s_speculative_depth != 0 || s_gte_replay_sandbox || !s_precision_tracking ||
|
||||
reg < 12 || reg > 15) return;
|
||||
int index = reg == 15 ? 2 : (int)reg - 12;
|
||||
const PreciseProjection &projection = s_precise_sxy[index];
|
||||
if (!projection.valid) return;
|
||||
uint32_t physical;
|
||||
if (!precision_ram_address(addr, &physical)) return;
|
||||
PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)];
|
||||
entry.addr = physical;
|
||||
entry.projection = projection;
|
||||
entry.generation = s_precision_generation;
|
||||
if (s_gte_replay_sandbox || reg < 12 || reg > 15) return;
|
||||
pgxp_store_gte_reg(addr, reg);
|
||||
}
|
||||
|
||||
/* Retired: plain-store invalidation is unnecessary under validate-on-read
|
||||
* (the GPU compares the tracked word against the actual packet word). Kept
|
||||
* as a no-op for link compatibility. */
|
||||
extern "C" void gte_precision_invalidate_word(uint32_t addr) {
|
||||
if (s_speculative_depth != 0 || s_gte_replay_sandbox || !s_precision_tracking) return;
|
||||
uint32_t physical;
|
||||
if (!precision_ram_address(addr, &physical)) return;
|
||||
PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)];
|
||||
if (entry.generation == s_precision_generation && entry.addr == physical)
|
||||
entry.generation = 0;
|
||||
(void)addr;
|
||||
}
|
||||
|
||||
extern "C" int gte_precision_load_word(uint32_t addr, uint32_t packed,
|
||||
int32_t *x16, int32_t *y16,
|
||||
uint16_t *z) {
|
||||
if (s_speculative_depth != 0 || !s_precision_tracking) return 0;
|
||||
uint32_t physical;
|
||||
if (!precision_ram_address(addr, &physical)) return 0;
|
||||
const PrecisionStoreEntry &entry = s_precision_store[precision_hash(physical)];
|
||||
if (entry.generation != s_precision_generation || entry.addr != physical ||
|
||||
!entry.projection.valid || entry.projection.packed != packed)
|
||||
return 0;
|
||||
if (x16) *x16 = entry.projection.x16;
|
||||
if (y16) *y16 = entry.projection.y16;
|
||||
if (z) *z = entry.projection.z;
|
||||
return entry.projection.z != 0;
|
||||
if (s_speculative_depth != 0) return 0;
|
||||
return pgxp_load_precise_word(addr, packed, x16, y16, z);
|
||||
}
|
||||
|
||||
/* Exact slot for a packed SXY pair, or -1 if it is outside the representable
|
||||
@@ -940,16 +888,9 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
int64_t sx = sx16 >> 16;
|
||||
int64_t sy = sy16 >> 16;
|
||||
gte->push_sxy(sx, sy);
|
||||
if (!s_gte_replay_sandbox) {
|
||||
s_precise_sxy[0] = s_precise_sxy[1];
|
||||
s_precise_sxy[1] = s_precise_sxy[2];
|
||||
s_precise_sxy[2].packed = (uint32_t)gte->SXY[2];
|
||||
s_precise_sxy[2].x16 = (int32_t)sx16;
|
||||
s_precise_sxy[2].y16 = (int32_t)sy16;
|
||||
s_precise_sxy[2].z = gte->SZ[3];
|
||||
s_precise_sxy[2].valid = gte->SZ[3] != 0;
|
||||
s_precise_sxy[3] = s_precise_sxy[2];
|
||||
}
|
||||
if (!s_gte_replay_sandbox)
|
||||
pgxp_gte_push_sxy((int32_t)sx16, (int32_t)sy16, gte->SZ[3],
|
||||
(uint32_t)gte->SXY[2]);
|
||||
geom_note((uint32_t)gte->SXY[2], sx16, sy16);
|
||||
|
||||
// Step 5: Depth cueing (MAC0/IR0) — only for last vertex of RTPT or RTPS
|
||||
@@ -982,12 +923,24 @@ void gte_rtpt(GTEState* gte, uint32_t instr) {
|
||||
// ---------------------------------------------------------------------------
|
||||
void gte_nclip(GTEState* gte, uint32_t instr) {
|
||||
gte->FLAG = 0;
|
||||
int16_t sx0 = static_cast<int16_t>(gte->SXY[0] & 0xFFFF);
|
||||
int16_t sy0 = static_cast<int16_t>(gte->SXY[0] >> 16);
|
||||
int16_t sx1 = static_cast<int16_t>(gte->SXY[1] & 0xFFFF);
|
||||
int16_t sy1 = static_cast<int16_t>(gte->SXY[1] >> 16);
|
||||
int16_t sx2 = static_cast<int16_t>(gte->SXY[2] & 0xFFFF);
|
||||
int16_t sy2 = static_cast<int16_t>(gte->SXY[2] >> 16);
|
||||
int32_t sx0 = static_cast<int16_t>(gte->SXY[0] & 0xFFFF);
|
||||
int32_t sy0 = static_cast<int16_t>(gte->SXY[0] >> 16);
|
||||
int32_t sx1 = static_cast<int16_t>(gte->SXY[1] & 0xFFFF);
|
||||
int32_t sy1 = static_cast<int16_t>(gte->SXY[1] >> 16);
|
||||
int32_t sx2 = static_cast<int16_t>(gte->SXY[2] & 0xFFFF);
|
||||
int32_t sy2 = static_cast<int16_t>(gte->SXY[2] >> 16);
|
||||
int32_t px0 = 0, py0 = 0, px1 = 0, py1 = 0, px2 = 0, py2 = 0;
|
||||
if (gpu_ws_precise_nclip_enabled() &&
|
||||
pgxp_get_gte_sxy(0, &px0, &py0) &&
|
||||
pgxp_get_gte_sxy(1, &px1, &py1) &&
|
||||
pgxp_get_gte_sxy(2, &px2, &py2)) {
|
||||
sx0 = px0 >> 16;
|
||||
sy0 = py0 >> 16;
|
||||
sx1 = px1 >> 16;
|
||||
sy1 = py1 >> 16;
|
||||
sx2 = px2 >> 16;
|
||||
sy2 = py2 >> 16;
|
||||
}
|
||||
int64_t mac0 = (int64_t)sx0 * (sy1 - sy2) +
|
||||
(int64_t)sx1 * (sy2 - sy0) +
|
||||
(int64_t)sx2 * (sy0 - sy1);
|
||||
@@ -2054,24 +2007,32 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) {
|
||||
cpu->gte_data[29] = packed;
|
||||
}
|
||||
break;
|
||||
/* Guest writes to the SXY FIFO drop the affected register shadows:
|
||||
* we never model FIFO side effects on shadows — the shifted regs 12/13
|
||||
* now describe words their shadows no longer match, and validation
|
||||
* drops those on next use. */
|
||||
case 12: case 13:
|
||||
cpu->gte_data[reg] = val;
|
||||
if (!PSXRecomp::GTE::s_gte_replay_sandbox)
|
||||
for (int i = 0; i < 4; ++i) PSXRecomp::GTE::s_precise_sxy[i].valid = 0;
|
||||
pgxp_gte_reg_written(reg, val);
|
||||
break;
|
||||
case 14:
|
||||
cpu->gte_data[14] = val;
|
||||
cpu->gte_data[15] = val;
|
||||
if (!PSXRecomp::GTE::s_gte_replay_sandbox)
|
||||
for (int i = 0; i < 4; ++i) PSXRecomp::GTE::s_precise_sxy[i].valid = 0;
|
||||
if (!PSXRecomp::GTE::s_gte_replay_sandbox) {
|
||||
pgxp_gte_reg_written(14, val);
|
||||
pgxp_gte_reg_written(15, val);
|
||||
}
|
||||
break;
|
||||
case 15:
|
||||
cpu->gte_data[12] = cpu->gte_data[13];
|
||||
cpu->gte_data[13] = cpu->gte_data[14];
|
||||
cpu->gte_data[14] = val;
|
||||
cpu->gte_data[15] = val;
|
||||
if (!PSXRecomp::GTE::s_gte_replay_sandbox)
|
||||
for (int i = 0; i < 4; ++i) PSXRecomp::GTE::s_precise_sxy[i].valid = 0;
|
||||
if (!PSXRecomp::GTE::s_gte_replay_sandbox) {
|
||||
pgxp_gte_reg_written(14, val);
|
||||
pgxp_gte_reg_written(15, val);
|
||||
}
|
||||
break;
|
||||
case 23:
|
||||
cpu->gte_data[23] = 0;
|
||||
@@ -2134,26 +2095,31 @@ extern "C" void gte_canonicalize_cpu_state(CPUState* cpu) {
|
||||
}
|
||||
|
||||
#ifdef PSX_GTE_REGISTER_TEST
|
||||
/* The precise-projection FIFO is the pgxp GTE register shadow (regs 12..15);
|
||||
* these keep the historical test entry points working over it. */
|
||||
extern "C" void gte_test_set_precise_valid_mask(uint32_t mask) {
|
||||
for (uint32_t i = 0; i < 4; ++i)
|
||||
PSXRecomp::GTE::s_precise_sxy[i].valid = (mask >> i) & 1u;
|
||||
pgxp_test_seed_gte_sxy(i, 0, 0, 0, 1, (mask >> i) & 1u);
|
||||
}
|
||||
|
||||
extern "C" uint32_t gte_test_get_precise_valid_mask(void) {
|
||||
uint32_t mask = 0;
|
||||
for (uint32_t i = 0; i < 4; ++i)
|
||||
mask |= (PSXRecomp::GTE::s_precise_sxy[i].valid ? 1u : 0u) << i;
|
||||
for (uint32_t i = 0; i < 4; ++i) {
|
||||
uint8_t valid = 0;
|
||||
pgxp_test_get_gte_sxy(i, nullptr, nullptr, nullptr, nullptr, &valid);
|
||||
mask |= (valid ? 1u : 0u) << i;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
extern "C" void gte_test_set_timeline_generations(uint32_t precision,
|
||||
uint32_t geometry) {
|
||||
PSXRecomp::GTE::s_precision_generation = precision;
|
||||
pgxp_test_set_generation(precision);
|
||||
PSXRecomp::GTE::s_geom_generation = geometry;
|
||||
}
|
||||
|
||||
extern "C" uint32_t gte_test_get_precision_generation(void) {
|
||||
return PSXRecomp::GTE::s_precision_generation;
|
||||
return pgxp_test_generation();
|
||||
}
|
||||
|
||||
extern "C" uint32_t gte_test_get_geometry_generation(void) {
|
||||
@@ -2165,13 +2131,7 @@ extern "C" void gte_test_seed_precise_projection(uint32_t index,
|
||||
int32_t x16,
|
||||
int32_t y16,
|
||||
uint16_t z) {
|
||||
if (index >= 4) return;
|
||||
auto &p = PSXRecomp::GTE::s_precise_sxy[index];
|
||||
p.packed = packed;
|
||||
p.x16 = x16;
|
||||
p.y16 = y16;
|
||||
p.z = z;
|
||||
p.valid = 1;
|
||||
pgxp_test_seed_gte_sxy(index, packed, x16, y16, z, 1);
|
||||
}
|
||||
|
||||
extern "C" void gte_test_get_precise_projection(uint32_t index,
|
||||
@@ -2180,13 +2140,7 @@ extern "C" void gte_test_get_precise_projection(uint32_t index,
|
||||
int32_t* y16,
|
||||
uint16_t* z,
|
||||
uint8_t* valid) {
|
||||
if (index >= 4) return;
|
||||
const auto &p = PSXRecomp::GTE::s_precise_sxy[index];
|
||||
if (packed) *packed = p.packed;
|
||||
if (x16) *x16 = p.x16;
|
||||
if (y16) *y16 = p.y16;
|
||||
if (z) *z = p.z;
|
||||
if (valid) *valid = p.valid;
|
||||
pgxp_test_get_gte_sxy(index, packed, x16, y16, z, valid);
|
||||
}
|
||||
|
||||
extern "C" void gte_test_seed_geometry(uint32_t packed, int32_t x16,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
typedef struct HostKeyBind {
|
||||
int keycode;
|
||||
int scancode;
|
||||
int mods; /* KMOD_CTRL | KMOD_ALT | KMOD_SHIFT subset */
|
||||
} HostKeyBind;
|
||||
|
||||
@@ -65,32 +66,44 @@ static void clear_all(void) {
|
||||
memset(s_actions, 0, sizeof(s_actions));
|
||||
}
|
||||
|
||||
static void add_bind(HostKeymapAction action, int keycode, int mods) {
|
||||
static void add_bind(HostKeymapAction action, int keycode, int scancode, int mods) {
|
||||
HostKeyAction *a;
|
||||
if (action < 0 || action >= HOST_KEYMAP_ACTION_COUNT) return;
|
||||
if (keycode == 0 || keycode == SDLK_UNKNOWN) return;
|
||||
a = &s_actions[action];
|
||||
if (a->count >= HOST_KEYMAP_MAX_BINDS) return;
|
||||
a->binds[a->count].keycode = keycode;
|
||||
a->binds[a->count].scancode = scancode;
|
||||
a->binds[a->count].mods = mods;
|
||||
a->count++;
|
||||
}
|
||||
|
||||
static void apply_defaults(void) {
|
||||
if (s_actions[HOST_KEYMAP_FULLSCREEN].count == 0) {
|
||||
add_bind(HOST_KEYMAP_FULLSCREEN, (int)SDLK_RETURN, (int)SDL_SCANCODE_RETURN, KMOD_ALT);
|
||||
add_bind(HOST_KEYMAP_FULLSCREEN, (int)SDLK_f, (int)SDL_SCANCODE_F, KMOD_CTRL);
|
||||
}
|
||||
if (s_actions[HOST_KEYMAP_TURBO].count == 0)
|
||||
add_bind(HOST_KEYMAP_TURBO, (int)SDLK_TAB, (int)SDL_SCANCODE_TAB, 0);
|
||||
if (s_actions[HOST_KEYMAP_VOLUME_UP].count == 0)
|
||||
add_bind(HOST_KEYMAP_VOLUME_UP, (int)SDLK_KP_PLUS, 0);
|
||||
add_bind(HOST_KEYMAP_VOLUME_UP, (int)SDLK_KP_PLUS, (int)SDL_SCANCODE_KP_PLUS, 0);
|
||||
if (s_actions[HOST_KEYMAP_VOLUME_DOWN].count == 0)
|
||||
add_bind(HOST_KEYMAP_VOLUME_DOWN, (int)SDLK_KP_MINUS, 0);
|
||||
add_bind(HOST_KEYMAP_VOLUME_DOWN, (int)SDLK_KP_MINUS, (int)SDL_SCANCODE_KP_MINUS, 0);
|
||||
if (s_actions[HOST_KEYMAP_DISPLAY_PERF].count == 0)
|
||||
add_bind(HOST_KEYMAP_DISPLAY_PERF, (int)SDLK_f, (int)SDL_SCANCODE_F, 0);
|
||||
#if defined(PSX_HAS_RBENGINE_SNAP)
|
||||
if (s_actions[HOST_KEYMAP_REWIND].count == 0)
|
||||
add_bind(HOST_KEYMAP_REWIND, (int)SDLK_F8, 0);
|
||||
add_bind(HOST_KEYMAP_REWIND, (int)SDLK_F8, (int)SDL_SCANCODE_F8, 0);
|
||||
#endif
|
||||
if (s_actions[HOST_KEYMAP_SAVE_STATE_MENU].count == 0)
|
||||
add_bind(HOST_KEYMAP_SAVE_STATE_MENU, (int)SDLK_F7, 0);
|
||||
add_bind(HOST_KEYMAP_SAVE_STATE_MENU, (int)SDLK_F7, (int)SDL_SCANCODE_F7, 0);
|
||||
}
|
||||
|
||||
/* Parse one "Ctrl+Alt+PageUp" token into key+mods. */
|
||||
static void parse_one_token(HostKeymapAction action, char *tok) {
|
||||
int mods = 0;
|
||||
SDL_Keycode key;
|
||||
SDL_Scancode sc;
|
||||
trim_inplace(tok);
|
||||
if (!tok[0] || ieq(tok, "(unbound)") || ieq(tok, "None")) return;
|
||||
for (;;) {
|
||||
@@ -114,7 +127,8 @@ static void parse_one_token(HostKeymapAction action, char *tok) {
|
||||
fprintf(stderr, "host_keymap: unknown key '%s'\n", tok);
|
||||
return;
|
||||
}
|
||||
add_bind(action, (int)key, mods);
|
||||
sc = SDL_GetScancodeFromName(tok);
|
||||
add_bind(action, (int)key, (int)sc, mods);
|
||||
}
|
||||
|
||||
static void parse_value(HostKeymapAction action, const char *value) {
|
||||
@@ -133,8 +147,11 @@ static void parse_value(HostKeymapAction action, const char *value) {
|
||||
}
|
||||
|
||||
static HostKeymapAction action_for_key(const char *name) {
|
||||
if (ieq(name, "Fullscreen")) return HOST_KEYMAP_FULLSCREEN;
|
||||
if (ieq(name, "Turbo")) return HOST_KEYMAP_TURBO;
|
||||
if (ieq(name, "VolumeUp")) return HOST_KEYMAP_VOLUME_UP;
|
||||
if (ieq(name, "VolumeDown")) return HOST_KEYMAP_VOLUME_DOWN;
|
||||
if (ieq(name, "DisplayPerf")) return HOST_KEYMAP_DISPLAY_PERF;
|
||||
if (ieq(name, "Rewind")) return HOST_KEYMAP_REWIND;
|
||||
if (ieq(name, "SaveStateMenu")) return HOST_KEYMAP_SAVE_STATE_MENU;
|
||||
return HOST_KEYMAP_ACTION_COUNT;
|
||||
@@ -201,6 +218,21 @@ int host_keymap_match(HostKeymapAction action, int keycode, int mod) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int host_keymap_down(HostKeymapAction action, const uint8_t *keys, int mod) {
|
||||
const HostKeyAction *a;
|
||||
const int relevant = (int)(KMOD_CTRL | KMOD_ALT | KMOD_SHIFT);
|
||||
int i;
|
||||
if (!keys || action < 0 || action >= HOST_KEYMAP_ACTION_COUNT) return 0;
|
||||
a = &s_actions[action];
|
||||
for (i = 0; i < a->count; i++) {
|
||||
const int sc = a->binds[i].scancode;
|
||||
if (sc <= 0 || sc >= SDL_NUM_SCANCODES) continue;
|
||||
if (!keys[sc]) continue;
|
||||
if ((mod & relevant) == a->binds[i].mods) return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Rewind overlay FONT8 only draws ASCII 32..90 (space..Z); lowercase is
|
||||
* uppercased by the drawer. Map punctuation / odd SDL single-glyph names to
|
||||
* short tokens so binds like backtick don't render as "?". */
|
||||
@@ -257,9 +289,12 @@ const char *host_keymap_label(HostKeymapAction action, char *out, size_t cap) {
|
||||
if (action < 0 || action >= HOST_KEYMAP_ACTION_COUNT) return out;
|
||||
a = &s_actions[action];
|
||||
if (a->count <= 0) {
|
||||
#if defined(PSX_HAS_RBENGINE_SNAP)
|
||||
if (action == HOST_KEYMAP_REWIND)
|
||||
snprintf(out, cap, "F8");
|
||||
else if (action == HOST_KEYMAP_SAVE_STATE_MENU)
|
||||
else
|
||||
#endif
|
||||
if (action == HOST_KEYMAP_SAVE_STATE_MENU)
|
||||
snprintf(out, cap, "F7");
|
||||
return out;
|
||||
}
|
||||
|
||||
+38
-7
@@ -130,6 +130,10 @@ static Uint32 s_expire_ms;
|
||||
static int s_active;
|
||||
static int s_img_dirty = 1;
|
||||
|
||||
static char s_status_msg[OSD_MAX_CHARS];
|
||||
static int s_status_active;
|
||||
static int s_status_dirty = 1;
|
||||
|
||||
static int s_volume = 100; /* host master 0..100 */
|
||||
static int s_vol_pct = 100; /* last shown bar fill */
|
||||
static Uint32 s_vol_expire_ms;
|
||||
@@ -197,8 +201,8 @@ static int vol_visible(void) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void rasterize_text(void) {
|
||||
int n = (int)strlen(s_msg);
|
||||
static void rasterize_text(const char *msg) {
|
||||
int n = (int)strlen(msg);
|
||||
if (n > OSD_MAX_CHARS) n = OSD_MAX_CHARS;
|
||||
s_img_w = (OSD_PAD_X * 2 + n * OSD_GLYPH_W) * OSD_SCALE;
|
||||
s_img_h = (OSD_PAD_Y * 2 + OSD_GLYPH_H) * OSD_SCALE;
|
||||
@@ -211,7 +215,7 @@ static void rasterize_text(void) {
|
||||
s_img[i] = 0xFF202020u;
|
||||
|
||||
for (int ci = 0; ci < n; ci++) {
|
||||
unsigned char ch = (unsigned char)s_msg[ci];
|
||||
unsigned char ch = (unsigned char)msg[ci];
|
||||
if (ch < 32 || ch > 126) ch = '?';
|
||||
const uint8_t *g = FONT8X8[ch - 32];
|
||||
for (int row = 0; row < OSD_GLYPH_H; row++) {
|
||||
@@ -338,6 +342,25 @@ void host_osd_push(const char *msg, int duration_ms) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void host_osd_set_status(const char *msg) {
|
||||
#if !HOST_OSD_VISUAL
|
||||
(void)msg;
|
||||
return;
|
||||
#else
|
||||
if (!msg || !msg[0]) {
|
||||
if (s_status_active) s_needs_clear = 1;
|
||||
s_status_msg[0] = '\0';
|
||||
s_status_active = 0;
|
||||
s_status_dirty = 1;
|
||||
return;
|
||||
}
|
||||
snprintf(s_status_msg, sizeof(s_status_msg), "%s", msg);
|
||||
s_status_active = 1;
|
||||
s_status_dirty = 1;
|
||||
s_needs_clear = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void host_osd_show_volume(int percent, int duration_ms) {
|
||||
#if !HOST_OSD_VISUAL
|
||||
(void)percent;
|
||||
@@ -375,7 +398,7 @@ int host_osd_needs_present(void) {
|
||||
#if !HOST_OSD_VISUAL
|
||||
return 0;
|
||||
#else
|
||||
if (msg_visible() || vol_visible()) return 1;
|
||||
if (msg_visible() || s_status_active || vol_visible()) return 1;
|
||||
return s_needs_clear;
|
||||
#endif
|
||||
}
|
||||
@@ -387,13 +410,21 @@ int host_osd_image(const uint32_t **pixels, int *w, int *h) {
|
||||
if (h) *h = 0;
|
||||
return 0;
|
||||
#else
|
||||
if (!msg_visible()) {
|
||||
const int show_msg = msg_visible();
|
||||
if (!show_msg && !s_status_active) {
|
||||
if (pixels) *pixels = NULL;
|
||||
if (w) *w = 0;
|
||||
if (h) *h = 0;
|
||||
return 0;
|
||||
}
|
||||
if (s_img_dirty) rasterize_text();
|
||||
if (show_msg) {
|
||||
if (s_img_dirty) rasterize_text(s_msg);
|
||||
} else {
|
||||
if (s_status_dirty || s_img_dirty) {
|
||||
rasterize_text(s_status_msg);
|
||||
s_status_dirty = 0;
|
||||
}
|
||||
}
|
||||
if (pixels) *pixels = s_img;
|
||||
if (w) *w = s_img_w;
|
||||
if (h) *h = s_img_h;
|
||||
@@ -426,7 +457,7 @@ void host_osd_present_done(void) {
|
||||
#if !HOST_OSD_VISUAL
|
||||
return;
|
||||
#else
|
||||
if (!s_active && !s_vol_active) s_needs_clear = 0;
|
||||
if (!s_active && !s_status_active && !s_vol_active) s_needs_clear = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+117
-7
@@ -22,6 +22,33 @@ constexpr size_t RAW_DATA_OFFSET = 24;
|
||||
// Primary Volume Descriptor location
|
||||
constexpr uint32_t PVD_SECTOR = 16;
|
||||
|
||||
constexpr uint32_t CD_FRAMES_PER_SECOND = 75;
|
||||
constexpr uint32_t CD_FRAMES_PER_MINUTE = 60 * CD_FRAMES_PER_SECOND;
|
||||
constexpr uint32_t CD_LEAD_IN_FRAMES = 2 * CD_FRAMES_PER_SECOND;
|
||||
|
||||
static bool valid_bcd(uint8_t value) {
|
||||
return (value & 0x0f) <= 9 && (value >> 4) <= 9;
|
||||
}
|
||||
|
||||
static uint32_t bcd_to_binary(uint8_t value) {
|
||||
return (value >> 4) * 10u + (value & 0x0f);
|
||||
}
|
||||
|
||||
static uint8_t binary_to_bcd(uint32_t value) {
|
||||
return static_cast<uint8_t>(((value / 10u) << 4) | (value % 10u));
|
||||
}
|
||||
|
||||
static uint16_t subq_crc(const uint8_t* data) {
|
||||
uint16_t crc = 0;
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
crc ^= static_cast<uint16_t>(data[i]) << 8;
|
||||
for (int bit = 0; bit < 8; ++bit)
|
||||
crc = (crc & 0x8000) ? static_cast<uint16_t>((crc << 1) ^ 0x1021)
|
||||
: static_cast<uint16_t>(crc << 1);
|
||||
}
|
||||
return static_cast<uint16_t>(~crc);
|
||||
}
|
||||
|
||||
enum class CHDTrackMode {
|
||||
Mode1,
|
||||
Mode1Raw,
|
||||
@@ -236,6 +263,10 @@ bool ISOReader::Open(const std::string& filename) {
|
||||
root_dir_.lba = 0;
|
||||
root_dir_.size = 0;
|
||||
}
|
||||
if (!LoadSBICompanion(filename)) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -331,14 +362,19 @@ bool ISOReader::Open(const std::string& filename) {
|
||||
// Parse the volume descriptor to extract filesystem metadata when
|
||||
// present. Runtime CD-ROM access only needs sector reads, so keep the
|
||||
// image mounted even if an ISO9660 header is missing or nonstandard.
|
||||
if (!ParseVolumeDescriptor()) {
|
||||
if (!ParseVolumeDescriptor()) {
|
||||
volume_id_.clear();
|
||||
root_dir_.lba = 0;
|
||||
root_dir_.size = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!LoadSBICompanion(filename)) {
|
||||
Close();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ISOReader::Close() {
|
||||
chd_.reset();
|
||||
@@ -348,13 +384,87 @@ void ISOReader::Close() {
|
||||
}
|
||||
}
|
||||
segments_.clear();
|
||||
tracks_.clear();
|
||||
tracks_.clear();
|
||||
subq_replacements_.clear();
|
||||
bin_path_.clear();
|
||||
volume_id_.clear();
|
||||
root_dir_.lba = 0;
|
||||
root_dir_.size = 0;
|
||||
is_open_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ISOReader::LoadSBICompanion(const std::string& image_path) {
|
||||
std::filesystem::path path(image_path);
|
||||
path.replace_extension(".sbi");
|
||||
if (!std::filesystem::exists(path)) return true;
|
||||
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file.is_open()) return false;
|
||||
char header[4] = {};
|
||||
file.read(header, sizeof(header));
|
||||
if (file.gcount() != sizeof(header) ||
|
||||
std::memcmp(header, "SBI\0", sizeof(header)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
uint8_t record[14] = {};
|
||||
file.read(reinterpret_cast<char*>(record), sizeof(record));
|
||||
if (file.gcount() == 0 && file.eof()) break;
|
||||
if (file.gcount() != sizeof(record) || record[3] != 1 ||
|
||||
!valid_bcd(record[0]) || !valid_bcd(record[1]) ||
|
||||
!valid_bcd(record[2])) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t absolute_frame =
|
||||
bcd_to_binary(record[0]) * CD_FRAMES_PER_MINUTE +
|
||||
bcd_to_binary(record[1]) * CD_FRAMES_PER_SECOND +
|
||||
bcd_to_binary(record[2]);
|
||||
if (absolute_frame < CD_LEAD_IN_FRAMES) return false;
|
||||
const uint32_t lba = absolute_frame - CD_LEAD_IN_FRAMES;
|
||||
std::array<uint8_t, 12> subq = {};
|
||||
std::memcpy(subq.data(), record + 4, 10);
|
||||
const uint16_t invalid_crc = static_cast<uint16_t>(subq_crc(subq.data()) ^ 0xffff);
|
||||
subq[10] = static_cast<uint8_t>(invalid_crc);
|
||||
subq[11] = static_cast<uint8_t>(invalid_crc >> 8);
|
||||
if (!subq_replacements_.emplace(lba, subq).second) return false;
|
||||
}
|
||||
return !subq_replacements_.empty();
|
||||
}
|
||||
|
||||
bool ISOReader::ReadSubChannelQ(uint32_t lba, uint8_t* buffer, bool* valid) const {
|
||||
if (!buffer || !valid || !is_open_) return false;
|
||||
const auto replacement = subq_replacements_.find(lba);
|
||||
if (replacement != subq_replacements_.end()) {
|
||||
std::memcpy(buffer, replacement->second.data(), replacement->second.size());
|
||||
*valid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
int track = 1;
|
||||
for (const CDTrack& candidate : tracks_) {
|
||||
if (candidate.start_lba > lba) break;
|
||||
track = candidate.number;
|
||||
}
|
||||
const uint32_t track_lba = TrackStartLBA(track);
|
||||
const uint32_t relative = lba >= track_lba ? lba - track_lba : 0;
|
||||
const uint32_t absolute = lba + 150;
|
||||
std::memset(buffer, 0, 12);
|
||||
buffer[0] = TrackIsAudio(track) ? 0x01 : 0x41;
|
||||
buffer[1] = binary_to_bcd(static_cast<uint32_t>(track));
|
||||
buffer[2] = 0x01;
|
||||
buffer[3] = binary_to_bcd(relative / CD_FRAMES_PER_MINUTE);
|
||||
buffer[4] = binary_to_bcd((relative / CD_FRAMES_PER_SECOND) % 60);
|
||||
buffer[5] = binary_to_bcd(relative % CD_FRAMES_PER_SECOND);
|
||||
buffer[6] = binary_to_bcd(absolute / CD_FRAMES_PER_MINUTE);
|
||||
buffer[7] = binary_to_bcd((absolute / CD_FRAMES_PER_SECOND) % 60);
|
||||
buffer[8] = binary_to_bcd(absolute % CD_FRAMES_PER_SECOND);
|
||||
const uint16_t crc = subq_crc(buffer);
|
||||
buffer[10] = static_cast<uint8_t>(crc);
|
||||
buffer[11] = static_cast<uint8_t>(crc >> 8);
|
||||
*valid = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
BinSegment* ISOReader::SegmentForLBA(uint32_t lba) {
|
||||
// Linear scan: images carry a handful of segments (one per track file).
|
||||
|
||||
@@ -36,6 +36,20 @@ int iso_read_raw_sector(void* handle, uint32_t lba, uint8_t* buffer, int size) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int iso_read_subq(void* handle, uint32_t lba, uint8_t* buffer, int size,
|
||||
int* valid) {
|
||||
if (!handle || !buffer || size < 12 || !valid) return 0;
|
||||
bool crc_valid = false;
|
||||
if (!static_cast<PS1::ISOReader*>(handle)->ReadSubChannelQ(
|
||||
lba, buffer, &crc_valid)) return 0;
|
||||
*valid = crc_valid ? 1 : 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int iso_has_subq_replacements(void* handle) {
|
||||
return handle && static_cast<PS1::ISOReader*>(handle)->HasSubChannelReplacements();
|
||||
}
|
||||
|
||||
uint32_t iso_sector_count(void* handle) {
|
||||
if (!handle) return 0;
|
||||
auto* reader = static_cast<PS1::ISOReader*>(handle);
|
||||
|
||||
+363
-57
@@ -32,6 +32,7 @@
|
||||
extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "overlay_backend.h"
|
||||
#include "gpu.h"
|
||||
#include "pgxp.h"
|
||||
#include "interrupts.h"
|
||||
#include "present_ring.h"
|
||||
#include "load_transition_ring.h"
|
||||
@@ -89,6 +90,14 @@ extern "C" void psx_game_codegen_forward_if_built(int argc, char** argv);
|
||||
#endif
|
||||
#endif
|
||||
#include "psx_sdl.h"
|
||||
#if defined(PSX_SDL3)
|
||||
/*
|
||||
* SDL_main.h is a single-header implementation in SDL3. Keep it in the one
|
||||
* translation unit that defines main(); including it through psx_sdl.h makes
|
||||
* every SDL-using source emit WinMain under MinGW.
|
||||
*/
|
||||
#include <SDL3/SDL_main.h>
|
||||
#endif
|
||||
#include "psx_sdl_audio.h"
|
||||
#if defined(PSX_WEB)
|
||||
#include <emscripten/emscripten.h>
|
||||
@@ -465,6 +474,7 @@ static int post_load_probe_env_on(void) {
|
||||
static Uint64 s_fps_last_time = 0;
|
||||
static uint64_t s_fps_last_frame = 0;
|
||||
static std::string s_fps_base_title;
|
||||
static int s_fps_telemetry_enabled = -1; /* -1 = unread env */
|
||||
static FramePacer s_frame_pacer = { 0 };
|
||||
static int s_turbo_present_skip = 0;
|
||||
static int s_fmv_skip_present_skip = 0;
|
||||
@@ -517,6 +527,46 @@ static void present_session_reset(void) {
|
||||
smooth_60_reset();
|
||||
}
|
||||
|
||||
static int fps_telemetry_enabled(void) {
|
||||
if (s_fps_telemetry_enabled < 0) {
|
||||
const char *e = std::getenv("PSX_FPS_TELEMETRY");
|
||||
s_fps_telemetry_enabled = (e && e[0] && e[0] != '0') ? 1 : 0;
|
||||
}
|
||||
return s_fps_telemetry_enabled;
|
||||
}
|
||||
|
||||
static void fps_telemetry_toggle(void) {
|
||||
const int enabled = fps_telemetry_enabled() ? 0 : 1;
|
||||
s_fps_telemetry_enabled = enabled;
|
||||
s_fps_last_time = 0;
|
||||
s_fps_last_frame = 0;
|
||||
if (!enabled && sdl_window && !s_fps_base_title.empty())
|
||||
SDL_SetWindowTitle(sdl_window, s_fps_base_title.c_str());
|
||||
if (!enabled)
|
||||
host_osd_set_status(NULL);
|
||||
s_fps_base_title.clear();
|
||||
host_osd_push(enabled ? "FPS readout on" : "FPS readout off", 1500);
|
||||
}
|
||||
|
||||
static int manual_fast_forward_multiplier(void) {
|
||||
static int value = -2; /* -2 = unread, -1 = max/unbounded */
|
||||
if (value == -2) {
|
||||
const char *e = std::getenv("PSX_FAST_FORWARD_SPEED");
|
||||
value = 4;
|
||||
if (e && e[0]) {
|
||||
if (std::strcmp(e, "max") == 0 || std::strcmp(e, "MAX") == 0 ||
|
||||
std::strcmp(e, "0") == 0) {
|
||||
value = -1;
|
||||
} else {
|
||||
int parsed = std::atoi(e);
|
||||
if (parsed >= 2 && parsed <= 16)
|
||||
value = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static void post_load_probe_stall_pc_note(uint32_t pc) {
|
||||
if (!pc) return;
|
||||
for (int i = 0; i < s_stall_pc_used; i++) {
|
||||
@@ -1059,12 +1109,15 @@ static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */
|
||||
* SXY readback are untouched. Default off = the faithful floor. */
|
||||
static int g_video_geometry_correction = 0;
|
||||
static int g_video_perspective_texturing = 0;
|
||||
static int g_video_pgxp_cpu_mode = 0;
|
||||
static float g_video_pgxp_tolerance = 0.5f;
|
||||
static int g_video_renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER;
|
||||
static int g_fullscreen = 0; /* tri-state: 0 windowed, 1 borderless (desktop)
|
||||
* fullscreen, 2 exclusive fullscreen */
|
||||
static int g_video_screen = 0; /* 0=raw,1=crt,2=composite,3=trinitron */
|
||||
static int g_video_win_w = 1280; /* window width (height follows aspect) */
|
||||
static bool g_audio_spu_hq = false; /* SPU float-shadow (env overrides) */
|
||||
static int g_audio_freq = 44100; /* host device request */
|
||||
static int g_auto_skip_fmv = 0; /* skip FMVs the instant they're detected */
|
||||
static int g_rewind_depth = 50; /* local rewind snap count (50/100/150/200) */
|
||||
static int g_rewind_interval = 15; /* frames between snaps (1/4/8/12/15) */
|
||||
@@ -1172,6 +1225,13 @@ static int g_video_aspect_den = 3;
|
||||
static bool g_ws_adaptive_view = false;
|
||||
static int g_ws_adaptive_max_num = 16;
|
||||
static int g_ws_adaptive_max_den = 9;
|
||||
/* game.toml [netplay] local_viewport = "vertical_split": during real netplay,
|
||||
* present only this peer's native split-screen half and stretch it to the
|
||||
* window. Presentation-only; the guest still renders the original framebuffer. */
|
||||
static int g_netplay_local_viewport = 0; /* 0 off, 1 vertical split */
|
||||
/* Optional aspect for netplay local-view extraction. Mirrors trusted mod aspect
|
||||
* activation, but remains game.toml opt-in so normal netplay stays vanilla. */
|
||||
static int g_netplay_local_viewport_aspect = 0; /* 0 off, 1 16:9, 2 21:9, 3 adaptive */
|
||||
|
||||
extern "C" int psx_mod_set_fixed_display_aspect(
|
||||
uint32_t numerator, uint32_t denominator) {
|
||||
@@ -1374,6 +1434,10 @@ static bool g_ws_hud_sprt = false;
|
||||
/* Runtime-only transition cleanup; kept out of gpu.h because generated game
|
||||
* units include that ABI header and do not need this frontend-only setter. */
|
||||
extern "C" void gpu_ws_set_clear_reveal(int on);
|
||||
extern "C" void gpu_ws_set_precise_nclip(int on);
|
||||
extern "C" void gpu_ws_set_netplay_local_viewport(int enabled, int slot);
|
||||
extern "C" int gpu_ws_netplay_local_viewport_base_x(void);
|
||||
extern "C" int gpu_ws_netplay_local_viewport_width(void);
|
||||
extern "C" void gpu_ws_set_nw_textured_edges(int on, int scale_pct);
|
||||
extern "C" void gpu_ws_set_signed_x_bound_sites(const uint32_t*, const uint32_t*, int);
|
||||
/* Widescreen engages at game entry (fntrace_is_game_started): the BIOS boot
|
||||
@@ -1409,6 +1473,87 @@ static int aspect_gcd(int a, int b) {
|
||||
return a > 0 ? a : 1;
|
||||
}
|
||||
|
||||
static int64_t aspect_gcd64(int64_t a, int64_t b) {
|
||||
if (a < 0) a = -a;
|
||||
if (b < 0) b = -b;
|
||||
while (b) { int64_t t = a % b; a = b; b = t; }
|
||||
return a > 0 ? a : 1;
|
||||
}
|
||||
|
||||
static void netplay_local_viewport_projection_aspect(
|
||||
int present_num, int present_den, int* proj_num, int* proj_den) {
|
||||
if (!proj_num || !proj_den) return;
|
||||
*proj_num = present_num;
|
||||
*proj_den = present_den;
|
||||
|
||||
if (g_netplay_local_viewport != 1 ||
|
||||
!psx_netplay_active() ||
|
||||
!gpu_last_frame_vertical_split_screen() ||
|
||||
present_num * 3 <= present_den * 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
GpuDisplayInfo di;
|
||||
gpu_get_display_info(&di);
|
||||
if (di.disabled || di.depth24 || di.width < 2 || di.height == 0)
|
||||
return;
|
||||
|
||||
/* The normal widescreen squash assumes a 4:3 source. A split-screen peer
|
||||
* source is only half the display width, so derive the equivalent aspect
|
||||
* that produces source_aspect / target_aspect as the X squash factor:
|
||||
*
|
||||
* source = (display_w / 2) / display_h
|
||||
* effective = (4:3) * target / source
|
||||
*/
|
||||
int64_t n = (int64_t)present_num * 8 * (int64_t)di.height;
|
||||
int64_t d = (int64_t)present_den * 3 * (int64_t)di.width;
|
||||
if (n <= 0 || d <= 0) return;
|
||||
int64_t gcd = aspect_gcd64(n, d);
|
||||
n /= gcd;
|
||||
d /= gcd;
|
||||
if (n <= 0 || d <= 0 || n > INT32_MAX || d > INT32_MAX)
|
||||
return;
|
||||
|
||||
*proj_num = (int)n;
|
||||
*proj_den = (int)d;
|
||||
}
|
||||
|
||||
static int g_ws_projection_num = 4;
|
||||
static int g_ws_projection_den = 3;
|
||||
static int g_ws_projection_mode = -1;
|
||||
static void refresh_widescreen_projection() {
|
||||
if (!g_ws_engaged) return;
|
||||
|
||||
const bool wide = g_video_aspect_num * 3 != g_video_aspect_den * 4;
|
||||
const bool local_native_wide =
|
||||
g_netplay_local_viewport == 1 && psx_netplay_active() &&
|
||||
gpu_last_frame_vertical_split_screen();
|
||||
const bool native_wide = (g_netplay_local_viewport == 1)
|
||||
? local_native_wide
|
||||
: (g_ws_native_wide != 0);
|
||||
const int mode = wide ? (native_wide ? 2 : 1) : 0;
|
||||
int proj_num = g_video_aspect_num;
|
||||
int proj_den = g_video_aspect_den;
|
||||
if (mode == 1) {
|
||||
netplay_local_viewport_projection_aspect(
|
||||
g_video_aspect_num, g_video_aspect_den, &proj_num, &proj_den);
|
||||
}
|
||||
|
||||
if (mode == g_ws_projection_mode &&
|
||||
proj_num == g_ws_projection_num &&
|
||||
proj_den == g_ws_projection_den) {
|
||||
return;
|
||||
}
|
||||
|
||||
g_ws_projection_mode = mode;
|
||||
g_ws_projection_num = proj_num;
|
||||
g_ws_projection_den = proj_den;
|
||||
gte_set_display_aspect(mode == 1 ? proj_num : 4,
|
||||
mode == 1 ? proj_den : 3);
|
||||
gpu_ws_configure(proj_num, proj_den, g_ws_anchor_addr,
|
||||
g_ws_hud_sprt ? 1 : 0, mode);
|
||||
}
|
||||
|
||||
/* Follow the host window without feeding its absolute pixel size into guest
|
||||
* rendering. Only the ratio matters: gpu_ws_configure derives the PSX-native
|
||||
* sidecar width from it, just as the fixed 16:9/21:9 modes do. */
|
||||
@@ -1441,14 +1586,7 @@ static void update_adaptive_widescreen() {
|
||||
SDL_RenderSetLogicalSize(sdl_renderer, g_logical_w, 480 * g_video_scale);
|
||||
}
|
||||
|
||||
if (g_ws_engaged) {
|
||||
const bool wide = num * 3 != den * 4;
|
||||
const int mode = wide ? (g_ws_native_wide ? 2 : 1) : 0;
|
||||
gte_set_display_aspect(mode == 1 ? num : 4,
|
||||
mode == 1 ? den : 3);
|
||||
gpu_ws_configure(num, den, g_ws_anchor_addr,
|
||||
g_ws_hud_sprt ? 1 : 0, mode);
|
||||
}
|
||||
refresh_widescreen_projection();
|
||||
}
|
||||
|
||||
/* SDL GL attributes are global inputs to the next context creation. Set the
|
||||
@@ -1466,13 +1604,8 @@ static void configure_core_gl_context_attributes() {
|
||||
* Re-engages with the chosen mode in place if widescreen is already running. */
|
||||
extern "C" void psx_ws_set_native_wide(int on) {
|
||||
g_ws_native_wide = on ? 1 : 0;
|
||||
if (g_ws_engaged && g_video_aspect_num * 3 != g_video_aspect_den * 4) {
|
||||
int mode = g_ws_native_wide ? 2 : 1;
|
||||
gte_set_display_aspect(mode == 1 ? g_video_aspect_num : 4,
|
||||
mode == 1 ? g_video_aspect_den : 3);
|
||||
gpu_ws_configure(g_video_aspect_num, g_video_aspect_den,
|
||||
g_ws_anchor_addr, g_ws_hud_sprt ? 1 : 0, mode);
|
||||
}
|
||||
g_ws_projection_mode = -1;
|
||||
refresh_widescreen_projection();
|
||||
}
|
||||
extern "C" int psx_ws_get_native_wide(void) { return g_ws_native_wide; }
|
||||
|
||||
@@ -2362,6 +2495,31 @@ static void shutdown_runtime(void);
|
||||
static int g_netplay_from_lobby = 0;
|
||||
static int g_netplay_vsync_forced_off = 0;
|
||||
|
||||
static void apply_netplay_local_viewport_aspect(bool netplay_enabled) {
|
||||
if (!netplay_enabled ||
|
||||
g_netplay_local_viewport != 1 ||
|
||||
g_netplay_local_viewport_aspect == 0) {
|
||||
gpu_ws_set_netplay_local_viewport(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
gpu_ws_set_netplay_local_viewport(1, psx_netplay_local_slot());
|
||||
switch (g_netplay_local_viewport_aspect) {
|
||||
case 1:
|
||||
(void)psx_mod_set_fixed_display_aspect(16u, 9u);
|
||||
break;
|
||||
case 2:
|
||||
(void)psx_mod_set_fixed_display_aspect(21u, 9u);
|
||||
break;
|
||||
case 3:
|
||||
(void)psx_mod_set_fixed_display_aspect(16u, 9u);
|
||||
(void)psx_mod_set_adaptive_display_aspect(21u, 9u);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Host-only: lockstep already couples peers. Driver vsync on top of the
|
||||
* wall-clock pacer double-blocks the vblank callback (present is before the
|
||||
* guest resumes), which shows up as MotK FMV ~30–40 FPS in netplay vs ~50+
|
||||
@@ -4659,6 +4817,36 @@ static void netplay_note_present(void) {
|
||||
s_present_last_ms = now ? now : 1ull;
|
||||
}
|
||||
|
||||
static int netplay_local_viewport_slot(void) {
|
||||
if (g_netplay_local_viewport != 1 || !psx_netplay_active())
|
||||
return -1;
|
||||
if (!gpu_last_frame_vertical_split_screen())
|
||||
return -1;
|
||||
int slot = psx_netplay_local_slot();
|
||||
return (slot == 0 || slot == 1) ? slot : -1;
|
||||
}
|
||||
|
||||
static int crop_present_to_netplay_local_viewport(uint32_t* pixels,
|
||||
int* width,
|
||||
int height) {
|
||||
if (!pixels || !width || *width < 2 || height <= 0)
|
||||
return 0;
|
||||
const int slot = netplay_local_viewport_slot();
|
||||
if (slot < 0)
|
||||
return 0;
|
||||
|
||||
const int src_w = *width;
|
||||
const int crop_w = src_w / 2;
|
||||
const int crop_x = slot == 0 ? 0 : (src_w - crop_w);
|
||||
for (int y = 0; y < height; ++y) {
|
||||
uint32_t* dst = pixels + (size_t)y * (size_t)crop_w;
|
||||
uint32_t* src = pixels + (size_t)y * (size_t)src_w + crop_x;
|
||||
memmove(dst, src, (size_t)crop_w * sizeof(uint32_t));
|
||||
}
|
||||
*width = crop_w;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void netplay_present_gap_stats(uint32_t *p95_out, uint32_t *max_out) {
|
||||
unsigned n = s_present_gaps_n;
|
||||
unsigned idx;
|
||||
@@ -5373,20 +5561,19 @@ static void savestate_menu_move(int delta) {
|
||||
savestate_menu_sync_overlay();
|
||||
}
|
||||
|
||||
static void savestate_menu_submit(int save) {
|
||||
const int slot = savestate_menu_slot;
|
||||
static int savestate_submit_slot(int slot, int save) {
|
||||
if (!save && !savestate_slot_exists(slot)) {
|
||||
char msg[32];
|
||||
snprintf(msg, sizeof(msg), "Slot %d is empty", slot + 1);
|
||||
host_osd_push(msg, 1200);
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
if (!save)
|
||||
savestate_input_guard_arm();
|
||||
if (psx_netplay_active()) {
|
||||
if (!psx_netplay_is_host()) {
|
||||
host_osd_push("Save states are host-only in netplay", 1500);
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
if (save)
|
||||
(void)psx_netplay_request_save(slot);
|
||||
@@ -5397,8 +5584,14 @@ static void savestate_menu_submit(int save) {
|
||||
} else {
|
||||
(void)savestate_request_load(slot);
|
||||
}
|
||||
savestate_menu_open = 0;
|
||||
savestate_menu_sync_overlay();
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void savestate_menu_submit(int save) {
|
||||
if (savestate_submit_slot(savestate_menu_slot, save) && savestate_menu_open) {
|
||||
savestate_menu_open = 0;
|
||||
savestate_menu_sync_overlay();
|
||||
}
|
||||
}
|
||||
|
||||
static int savestate_menu_slot_from_key(SDL_Keycode key) {
|
||||
@@ -5713,14 +5906,14 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* than presents so turbo and skipped-frame modes still report game speed.
|
||||
* Skip during netplay post-load barrier — admit is stalled and the window
|
||||
* is not updating, so a climbing FPS line is misleading. */
|
||||
if (!psx_netplay_in_load_barrier()) {
|
||||
if (fps_telemetry_enabled() && !psx_netplay_in_load_barrier()) {
|
||||
extern uint64_t s_frame_count;
|
||||
const Uint64 now = SDL_GetPerformanceCounter();
|
||||
const Uint64 frequency = SDL_GetPerformanceFrequency();
|
||||
if (!s_fps_last_time) {
|
||||
s_fps_last_time = now;
|
||||
s_fps_last_frame = s_frame_count;
|
||||
if (sdl_window) {
|
||||
if (sdl_window && s_fps_base_title.empty()) {
|
||||
const char *title = SDL_GetWindowTitle(sdl_window);
|
||||
if (title) s_fps_base_title = title;
|
||||
}
|
||||
@@ -5728,12 +5921,36 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
const double seconds = (double)(now - s_fps_last_time) / (double)frequency;
|
||||
const double fps = (double)(s_frame_count - s_fps_last_frame) / seconds;
|
||||
const double speed = fps / 59.94;
|
||||
double display_fps = 0.0;
|
||||
if (g_frame_interpolation && g_gl_active) {
|
||||
display_fps = g_frame_interpolation_fps > 0
|
||||
? (double)g_frame_interpolation_fps
|
||||
: g_host_refresh_hz;
|
||||
}
|
||||
if (!g_headless && sdl_window) {
|
||||
char title[256];
|
||||
snprintf(title, sizeof(title), "%s [%.0f fps %.2fx]",
|
||||
s_fps_base_title.c_str(), fps, speed);
|
||||
if (display_fps > 0.0) {
|
||||
snprintf(title, sizeof(title),
|
||||
"%s [Game %.0f fps %.2fx | Display %.0f fps]",
|
||||
s_fps_base_title.c_str(), fps, speed, display_fps);
|
||||
} else {
|
||||
snprintf(title, sizeof(title), "%s [Game %.0f fps %.2fx]",
|
||||
s_fps_base_title.c_str(), fps, speed);
|
||||
}
|
||||
SDL_SetWindowTitle(sdl_window, title);
|
||||
}
|
||||
if (!g_headless) {
|
||||
char osd[96];
|
||||
if (display_fps > 0.0) {
|
||||
snprintf(osd, sizeof(osd),
|
||||
"Game %.0f FPS %.2fx | Display %.0f FPS",
|
||||
fps, speed, display_fps);
|
||||
} else {
|
||||
snprintf(osd, sizeof(osd), "Game %.0f FPS %.2fx",
|
||||
fps, speed);
|
||||
}
|
||||
host_osd_set_status(osd);
|
||||
}
|
||||
if (netplay_timing_on() && s_np_timing_frames > 0) {
|
||||
const double invf = 1000.0 / (double)frequency;
|
||||
const double admit_ms =
|
||||
@@ -5851,6 +6068,11 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
debug_force_cd_reinsert();
|
||||
host_osd_push("CD reinsert", 1500);
|
||||
}
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match(HOST_KEYMAP_DISPLAY_PERF, (int)key,
|
||||
(int)mod)) {
|
||||
fps_telemetry_toggle();
|
||||
}
|
||||
/* Host volume: config.ini [KeyMap] VolumeUp/VolumeDown
|
||||
* (defaults: keypad +/-). 5% steps; shows right-side bar. */
|
||||
else if (host_keymap_match(HOST_KEYMAP_VOLUME_UP, (int)key,
|
||||
@@ -5868,8 +6090,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* set in both SDL_WINDOW_FULLSCREEN and
|
||||
* SDL_WINDOW_FULLSCREEN_DESKTOP, so testing just that bit
|
||||
* detects "currently fullscreen, either mode". */
|
||||
else if ((key == SDLK_RETURN && (mod & KMOD_ALT)) ||
|
||||
(key == SDLK_f && (mod & (KMOD_GUI | KMOD_CTRL)))) {
|
||||
else if (!key_repeat &&
|
||||
host_keymap_match(HOST_KEYMAP_FULLSCREEN, (int)key,
|
||||
(int)mod)) {
|
||||
Uint32 is_fs = SDL_GetWindowFlags(sdl_window) &
|
||||
SDL_WINDOW_FULLSCREEN;
|
||||
if (is_fs) {
|
||||
@@ -6073,23 +6296,45 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Turbo mode: while TAB is held, skip both VRAM->ARGB conversion and
|
||||
* SDL_RenderPresent. The recompiled BIOS still advances simulated
|
||||
* cycles every vblank, so the BIOS proceeds at whatever rate the host
|
||||
* CPU sustains without graphics-driver vsync overhead. Present once
|
||||
* every TURBO_PRESENT_EVERY frames so the user sees visual progress. */
|
||||
bool manual_turbo_active = false;
|
||||
bool turbo_load_paced = false;
|
||||
|
||||
/* Manual fast-forward: bounded by default so the game visibly advances and
|
||||
* audio is less hostile. PSX_FAST_FORWARD_SPEED=2..16 changes the cap;
|
||||
* PSX_FAST_FORWARD_SPEED=max restores the old unbounded simulation rate. */
|
||||
{
|
||||
const Uint8* keys = SDL_GetKeyboardState(NULL);
|
||||
static int turbo_skip = 0;
|
||||
const int TURBO_PRESENT_EVERY = 30;
|
||||
if (keys[SDL_SCANCODE_TAB]) {
|
||||
turbo_skip = (turbo_skip + 1) % TURBO_PRESENT_EVERY;
|
||||
static int turbo_was_down = 0;
|
||||
if (host_keymap_down(HOST_KEYMAP_TURBO, keys, (int)SDL_GetModState())) {
|
||||
const int mult = manual_fast_forward_multiplier();
|
||||
const int present_every = (mult < 0) ? 4 : (mult <= 4 ? 2 : 4);
|
||||
manual_turbo_active = true;
|
||||
if (!turbo_was_down) {
|
||||
char msg[40];
|
||||
if (mult < 0)
|
||||
snprintf(msg, sizeof(msg), "Fast forward: max");
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "Fast forward: %dx", mult);
|
||||
host_osd_push(msg, 900);
|
||||
}
|
||||
turbo_was_down = 1;
|
||||
if (mult >= 2 && g_frame_period_ms > 0.0) {
|
||||
uint64_t perf_start = runtime_perf_section_begin();
|
||||
frame_pacer_wait(&s_frame_pacer,
|
||||
g_frame_period_ms / (double)mult);
|
||||
runtime_perf_section_end(perf_start,
|
||||
&g_runtime_perf.pacer_ticks);
|
||||
latency_ring_mark(LAT_PACED);
|
||||
}
|
||||
turbo_skip = (turbo_skip + 1) % present_every;
|
||||
if (turbo_skip != 0) {
|
||||
ep.skip_pace = 1;
|
||||
return ep; /* skip render this frame */
|
||||
}
|
||||
} else {
|
||||
turbo_skip = 0;
|
||||
turbo_was_down = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6101,7 +6346,6 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* real time (2.2-4.8 sectors/frame against a 32-256 IRQ budget), so
|
||||
* host-speed execution is the lever that compresses load wall-time.
|
||||
* Presents 1-in-30 so visual progress stays visible. */
|
||||
bool turbo_load_paced = false;
|
||||
if (turbo_loads_active) {
|
||||
g_turbo_loads_frames++;
|
||||
/* A mod may request a bounded wall-clock multiplier instead of the
|
||||
@@ -6176,7 +6420,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* resim. */
|
||||
if (!psx_netplay_active() && !psx_selfcheck_resim_active()) {
|
||||
uint64_t perf_start = runtime_perf_section_begin();
|
||||
if (!turbo_load_paced && present_should_wall_pace())
|
||||
if (!manual_turbo_active && !turbo_load_paced && present_should_wall_pace())
|
||||
frame_pacer_wait(&s_frame_pacer, g_frame_period_ms);
|
||||
runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks);
|
||||
latency_ring_mark(LAT_PACED);
|
||||
@@ -6214,14 +6458,11 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
extern int fntrace_is_game_started(void);
|
||||
if (fntrace_is_game_started()) {
|
||||
g_ws_engaged = true;
|
||||
int mode = g_ws_native_wide ? 2 : 1;
|
||||
/* Native-wide: GTE drawn un-squashed — feed it the 4:3 ratio
|
||||
* (identity squash). Squash mode: feed the real wide aspect. */
|
||||
gte_set_display_aspect(mode == 1 ? g_video_aspect_num : 4,
|
||||
mode == 1 ? g_video_aspect_den : 3);
|
||||
gpu_ws_configure(g_video_aspect_num, g_video_aspect_den,
|
||||
g_ws_anchor_addr, g_ws_hud_sprt ? 1 : 0, mode);
|
||||
g_ws_projection_mode = -1;
|
||||
refresh_widescreen_projection();
|
||||
}
|
||||
} else {
|
||||
refresh_widescreen_projection();
|
||||
}
|
||||
|
||||
/* Rollback resim (§33/§47): short catch-up keeps hold-last; long catch-up
|
||||
@@ -6276,6 +6517,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
bool pin_43 = false; /* pillarbox this present (FMV, or a native-wide
|
||||
game frame that could not present wide) */
|
||||
bool depth24_frame = false;
|
||||
bool local_viewport_crop_applied = false;
|
||||
if (s_force_present_after_load && g_gl_active)
|
||||
gl_renderer_flush_cpu_uploads();
|
||||
{
|
||||
@@ -6330,6 +6572,11 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
if (g_gl_active)
|
||||
gl_renderer_set_interpolation_suspended(
|
||||
fmv_frame || mdec_recently_active(2));
|
||||
const int local_viewport_slot = netplay_local_viewport_slot();
|
||||
const bool local_viewport_crop = local_viewport_slot >= 0;
|
||||
bool local_viewport_wide =
|
||||
local_viewport_crop && g_ws_engaged && ws_native_wide_active() &&
|
||||
gr_wide_supported() && gpu_ws_netplay_local_viewport_width() > 0;
|
||||
|
||||
/* Canonical present width. Native-wide does NOT widen the canonical read
|
||||
* (that bled across adjacent framebuffers); it composites into a separate
|
||||
@@ -6340,8 +6587,13 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* wide compositor, present the wider surface (canonical width + EXTRA)
|
||||
* from the displayed buffer's surface. FMV/menu frames stay 4:3. */
|
||||
bool wide_present = (!fmv_frame && !di.depth24 && g_ws_engaged &&
|
||||
ws_native_wide_active() && gr_wide_supported());
|
||||
if (wide_present) present_w = w + (uint32_t)ws_nw_extra();
|
||||
ws_native_wide_active() && gr_wide_supported() &&
|
||||
(!local_viewport_crop || local_viewport_wide));
|
||||
if (wide_present) {
|
||||
present_w = local_viewport_wide
|
||||
? (uint32_t)gpu_ws_netplay_local_viewport_width()
|
||||
: (w + (uint32_t)ws_nw_extra());
|
||||
}
|
||||
|
||||
/* Native-wide invariant: canonical (320-wide) content is NEVER
|
||||
* stretched across the wide window — a game frame that cannot present
|
||||
@@ -6366,7 +6618,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* do NOT flush_cpu_uploads (MDEC already wrote the CPU mirror; forcing
|
||||
* FBO uploads every frame cut MotK intro from ~50 to ~30 FPS). */
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
if (g_gl_active && g_gl_fbo_present && !di.depth24) {
|
||||
if (g_gl_active && g_gl_fbo_present && !di.depth24 &&
|
||||
!local_viewport_crop) {
|
||||
if (wide_present) {
|
||||
/* GPU-direct native-wide present: blit the displayed buffer's
|
||||
* wide FBO straight to the window (GPU-side, like the canonical
|
||||
@@ -6393,7 +6646,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* VRAM image (deterministic blit, no readback), mirroring the GL path;
|
||||
* 24-bit (FMV) frames go through the CPU present (Phase 3). The Vulkan
|
||||
* window has no SDL_Renderer, so we must never fall through below. */
|
||||
if (g_vk_active) {
|
||||
if (g_vk_active && !local_viewport_crop) {
|
||||
if (di.depth24) {
|
||||
/* 24-bit (FMV): packed RGB lives in the CPU mirror — do NOT
|
||||
* sync_cpu (FBO readback clobbers RGB888). Batch per-scanline
|
||||
@@ -6443,12 +6696,16 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* hires path if the displayed buffer has no surface yet. */
|
||||
int s = gr_scale();
|
||||
int sw = (int)present_w * s;
|
||||
int base_x = local_viewport_wide
|
||||
? gpu_ws_netplay_local_viewport_base_x()
|
||||
: (int)di.display_x;
|
||||
int n = gr_render_wide_display(sdl_pixel_buf, (int)(sw * sizeof(uint32_t)),
|
||||
(int)di.display_x, (int)di.display_y, (int)h);
|
||||
base_x, (int)di.display_y, (int)h);
|
||||
if (n > 0) {
|
||||
active_scale = s;
|
||||
} else {
|
||||
wide_present = false;
|
||||
local_viewport_wide = false;
|
||||
present_w = w;
|
||||
pres_entry->path = PRES_PATH_CANONICAL;
|
||||
pres_entry->wide_fellback = 1;
|
||||
@@ -6489,9 +6746,19 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
depth24_fix_trailing_margin(sdl_pixel_buf, present_w, h,
|
||||
di.display_x);
|
||||
|
||||
int present_px_w = (int)present_w * active_scale;
|
||||
int present_px_h = (int)h * active_scale;
|
||||
if (!local_viewport_wide &&
|
||||
crop_present_to_netplay_local_viewport(sdl_pixel_buf,
|
||||
&present_px_w,
|
||||
present_px_h)) {
|
||||
pin_43 = false;
|
||||
local_viewport_crop_applied = true;
|
||||
}
|
||||
|
||||
smooth_60_present(sdl_pixel_buf,
|
||||
present_w * (uint32_t)active_scale,
|
||||
h * (uint32_t)active_scale,
|
||||
(uint32_t)present_px_w,
|
||||
(uint32_t)present_px_h,
|
||||
!g_gl_active && !g_vk_active && !di.depth24 && !fmv_frame);
|
||||
|
||||
/* Frame blending (CRT-persistence masker for 30fps double-buffered
|
||||
@@ -6516,7 +6783,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
!g_smooth_60fps.load(std::memory_order_acquire)) {
|
||||
static uint32_t prev_buf[640 * 512];
|
||||
static uint32_t prev_px = 0;
|
||||
const uint32_t npx = present_w * h;
|
||||
const uint32_t npx = local_viewport_crop_applied
|
||||
? (uint32_t)(present_px_w * present_px_h)
|
||||
: present_w * h;
|
||||
if (npx <= (uint32_t)(640 * 512)) {
|
||||
if (prev_px == npx) {
|
||||
for (uint32_t i = 0; i < npx; i++) {
|
||||
@@ -6544,6 +6813,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
int src_w = (int)present_w * active_scale;
|
||||
int src_h = (int)h * active_scale;
|
||||
if (local_viewport_crop_applied && src_w >= 2)
|
||||
src_w /= 2;
|
||||
if (g_gl_active) {
|
||||
/* OpenGL present: upload the active display rect and draw a full-screen
|
||||
* quad. Either SwapWindow vsync OR the wall-clock pacer owns timing,
|
||||
@@ -10298,6 +10569,12 @@ int main(int argc, char** argv) {
|
||||
g_netplay_disc_expect.required_leadout_lba =
|
||||
gc.netplay_required_leadout_lba;
|
||||
g_netplay_disc_expect.required_disc_fp = gc.netplay_required_disc_fp;
|
||||
g_netplay_local_viewport =
|
||||
(gc.netplay_local_viewport == "vertical_split") ? 1 : 0;
|
||||
g_netplay_local_viewport_aspect =
|
||||
(gc.netplay_local_viewport_aspect == "16:9") ? 1 :
|
||||
(gc.netplay_local_viewport_aspect == "21:9") ? 2 :
|
||||
(gc.netplay_local_viewport_aspect == "adaptive") ? 3 : 0;
|
||||
if (!gc.discs.empty()) resolved_disc = gc.discs.front();
|
||||
if (gc.runtime.has_memcard_dir) memcard_dir = gc.runtime.memcard_dir;
|
||||
if (gc.runtime.has_window_title) window_title = gc.runtime.window_title;
|
||||
@@ -10358,6 +10635,8 @@ int main(int argc, char** argv) {
|
||||
gc.runtime.video_geometry_correction ? 1 : 0;
|
||||
g_video_perspective_texturing =
|
||||
gc.runtime.video_perspective_texturing ? 1 : 0;
|
||||
g_video_pgxp_cpu_mode = gc.runtime.video_pgxp_cpu_mode ? 1 : 0;
|
||||
g_video_pgxp_tolerance = (float)gc.runtime.video_pgxp_tolerance;
|
||||
g_video_renderer = gc.runtime.video_renderer;
|
||||
g_video_screen = gc.runtime.video_screen_kind;
|
||||
g_video_aspect_num = gc.runtime.video_aspect_num;
|
||||
@@ -10391,6 +10670,7 @@ int main(int argc, char** argv) {
|
||||
gc.ws_bg2d_packet_cap);
|
||||
/* [widescreen] gte_game_mode — 3D-title gameplay detector (Ape). */
|
||||
gpu_ws_set_gte_game_mode(gc.ws_gte_game_mode ? 1 : 0);
|
||||
gpu_ws_set_precise_nclip(gc.ws_precise_nclip ? 1 : 0);
|
||||
gpu_ws_set_gameplay_state_gate(
|
||||
gc.ws_gameplay_state_addr,
|
||||
gc.ws_gameplay_state_values.data(),
|
||||
@@ -10548,6 +10828,10 @@ int main(int argc, char** argv) {
|
||||
if (i >= 2) player_mode[i] = gc.runtime.default_p1_mode;
|
||||
}
|
||||
}
|
||||
if (gc.runtime.has_default_p1_device)
|
||||
player_device[0] = gc.runtime.default_p1_device;
|
||||
if (PSX_MAX_PLAYERS >= 2 && gc.runtime.has_default_p2_device)
|
||||
player_device[1] = gc.runtime.default_p2_device;
|
||||
for (int i = 0; i < PSX_MAX_PLAYERS; ++i)
|
||||
ctrl_locked_mode[i] = player_mode[i];
|
||||
ctrl_lock_mode = gc.runtime.controller_lock_mode;
|
||||
@@ -10737,6 +11021,7 @@ int main(int argc, char** argv) {
|
||||
g_video_aspect_num = us.aspect_num;
|
||||
g_video_aspect_den = us.aspect_den;
|
||||
}
|
||||
if (us.has_audio_freq) g_audio_freq = us.audio_freq;
|
||||
if (us.has_spu_hq) g_audio_spu_hq = us.spu_hq;
|
||||
if (us.has_rewind_depth) g_rewind_depth = us.rewind_depth;
|
||||
if (us.has_rewind_interval) g_rewind_interval = us.rewind_interval;
|
||||
@@ -11154,6 +11439,7 @@ int main(int argc, char** argv) {
|
||||
seed.has_frame_interpolation_fps = true;
|
||||
seed.aspect_num = g_video_aspect_num;
|
||||
seed.aspect_den = g_video_aspect_den; seed.has_aspect_ratio = true;
|
||||
seed.audio_freq = g_audio_freq; seed.has_audio_freq = true;
|
||||
seed.spu_hq = g_audio_spu_hq; seed.has_spu_hq = true;
|
||||
seed.rewind_depth = g_rewind_depth; seed.has_rewind_depth = true;
|
||||
seed.rewind_interval = g_rewind_interval; seed.has_rewind_interval = true;
|
||||
@@ -11259,7 +11545,7 @@ int main(int argc, char** argv) {
|
||||
ls.widescreen = (seed.aspect_num == 16 && seed.aspect_den == 9) ? 1 : 0;
|
||||
ls.widescreen_hud = ls.widescreen;
|
||||
ls.enable_audio = 1;
|
||||
ls.audio_freq = 44100;
|
||||
ls.audio_freq = seed.audio_freq;
|
||||
ls.volume = host_volume_get();
|
||||
{
|
||||
const int n = std::min(PSX_MAX_PLAYERS, RECOMP_LAUNCHER_MAX_PLAYERS);
|
||||
@@ -11547,6 +11833,7 @@ int main(int argc, char** argv) {
|
||||
seed.screen_kind = ls.screen_kind; seed.has_screen_kind = true;
|
||||
seed.frame_interpolation = ls.frame_interp != 0; seed.has_frame_interpolation = true;
|
||||
seed.frame_interpolation_fps = ls.frame_interp_fps; seed.has_frame_interpolation_fps = true;
|
||||
seed.audio_freq = ls.audio_freq; seed.has_audio_freq = true;
|
||||
seed.spu_hq = ls.spu_hq != 0; seed.has_spu_hq = true;
|
||||
seed.rewind_depth = ls.rewind_depth > 0 ? ls.rewind_depth : 50;
|
||||
seed.has_rewind_depth = true;
|
||||
@@ -11758,6 +12045,7 @@ int main(int argc, char** argv) {
|
||||
g_frame_interpolation_fps = seed.frame_interpolation_fps;
|
||||
g_video_aspect_num = seed.aspect_num;
|
||||
g_video_aspect_den = seed.aspect_den;
|
||||
g_audio_freq = seed.audio_freq;
|
||||
g_audio_spu_hq = seed.spu_hq;
|
||||
g_rewind_depth = seed.has_rewind_depth && seed.rewind_depth > 0
|
||||
? seed.rewind_depth : 50;
|
||||
@@ -11875,6 +12163,7 @@ int main(int argc, char** argv) {
|
||||
g_turbo_loads_enabled = 0;
|
||||
g_frame_interpolation_blend = g_frame_interpolation_blend_default;
|
||||
mod_runtime_activate_plugins();
|
||||
apply_netplay_local_viewport_aspect(net_cfg.enabled);
|
||||
if (g_mod_controller_mode_override[0] >= 0)
|
||||
player_mode[0] = g_mod_controller_mode_override[0];
|
||||
if (g_mod_controller_mode_override[1] >= 0)
|
||||
@@ -12089,8 +12378,20 @@ 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);
|
||||
pgxp_set_tolerance(g_video_pgxp_tolerance);
|
||||
if (g_video_geometry_correction || g_video_perspective_texturing) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: geometry correction %s, perspective texturing %s%s\n",
|
||||
@@ -12331,7 +12632,7 @@ session_reboot:
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO) == 0) {
|
||||
PsxSdlAudioSpec want = {};
|
||||
PsxSdlAudioSpec have = {};
|
||||
want.freq = 44100;
|
||||
want.freq = g_audio_freq;
|
||||
want.format = AUDIO_S16SYS;
|
||||
want.channels = 2;
|
||||
want.samples = 1024;
|
||||
@@ -12633,6 +12934,7 @@ session_reboot:
|
||||
nrc, net_cfg.local_slot, net_cfg.bind_hostport, net_cfg.peer_hostport);
|
||||
return 1;
|
||||
}
|
||||
apply_netplay_local_viewport_aspect(net_cfg.enabled);
|
||||
std::printf("psxrecomp: netplay transport=%s slot=%d input_player=%d delay=%d "
|
||||
"force_turn=%d bind=%s peer=%s session=%u\n",
|
||||
psx_netplay_transport_name(),
|
||||
@@ -12757,14 +13059,14 @@ session_reboot:
|
||||
psx_rewind_set_depth((uint32_t)g_rewind_depth);
|
||||
psx_rewind_set_interval((uint32_t)g_rewind_interval);
|
||||
psx_rewind_configure(memory_get_bios_checksum(), game_entry_pc);
|
||||
/* Headless / agent load: PSX_LOAD_SLOT=N stages F1..F12 load (0..11)
|
||||
/* Headless / agent load: PSX_LOAD_SLOT=N stages slot load (0..11)
|
||||
* at the next safe block boundary after boot. */
|
||||
if (const char *ls = std::getenv("PSX_LOAD_SLOT")) {
|
||||
int slot = atoi(ls);
|
||||
if (slot >= 0 && slot < 12) {
|
||||
if (psx_netplay_active()) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: PSX_LOAD_SLOT=%d ignored (use host F1–F12 after netplay starts)\n",
|
||||
"psxrecomp: PSX_LOAD_SLOT=%d ignored (use the save-state menu after netplay starts)\n",
|
||||
slot);
|
||||
} else if (savestate_request_load(slot)) {
|
||||
std::fprintf(stdout, "psxrecomp: PSX_LOAD_SLOT=%d staged\n", slot);
|
||||
@@ -13087,7 +13389,7 @@ soft_return_lobby:
|
||||
(g_video_aspect_num == 16 && g_video_aspect_den == 9) ? 1 : 0;
|
||||
ls.widescreen_hud = ls.widescreen;
|
||||
ls.enable_audio = 1;
|
||||
ls.audio_freq = 44100;
|
||||
ls.audio_freq = g_audio_freq;
|
||||
ls.volume = host_volume_get();
|
||||
ls.window_width = g_video_win_w;
|
||||
ls.renderer = g_video_renderer;
|
||||
@@ -13358,6 +13660,8 @@ soft_return_lobby:
|
||||
us.has_frame_interpolation = true;
|
||||
us.frame_interpolation_fps = ls.frame_interp_fps;
|
||||
us.has_frame_interpolation_fps = true;
|
||||
us.audio_freq = ls.audio_freq;
|
||||
us.has_audio_freq = true;
|
||||
us.spu_hq = ls.spu_hq != 0;
|
||||
us.has_spu_hq = true;
|
||||
us.rewind_depth = ls.rewind_depth > 0 ? ls.rewind_depth : 50;
|
||||
@@ -13416,6 +13720,7 @@ soft_return_lobby:
|
||||
g_fullscreen = ls.fullscreen != 0;
|
||||
g_frame_interpolation = ls.frame_interp ? 1 : 0;
|
||||
g_frame_interpolation_fps = ls.frame_interp_fps;
|
||||
g_audio_freq = ls.audio_freq;
|
||||
g_audio_spu_hq = ls.spu_hq != 0;
|
||||
if (ls.rewind_depth > 0) {
|
||||
g_rewind_depth = ls.rewind_depth;
|
||||
@@ -13502,6 +13807,7 @@ soft_return_lobby:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
apply_netplay_local_viewport_aspect(net_cfg.enabled);
|
||||
std::printf("psxrecomp: rematch from lobby (netplay=%d)\n",
|
||||
net_cfg.enabled ? 1 : 0);
|
||||
std::fflush(stdout);
|
||||
|
||||
@@ -1555,7 +1555,6 @@ static inline void d44_note(uint32_t phys, uint32_t old, uint32_t val) {
|
||||
}
|
||||
|
||||
static void psx_write_word_raw(uint32_t addr, uint32_t val);
|
||||
extern void gte_precision_invalidate_word(uint32_t addr);
|
||||
void psx_write_word(uint32_t addr, uint32_t val) {
|
||||
extern void (*g_overlay_flush_pending_cycles)(void);
|
||||
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
|
||||
@@ -1572,7 +1571,9 @@ void psx_write_word(uint32_t addr, uint32_t val) {
|
||||
}
|
||||
static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
g_guest_store_count++;
|
||||
gte_precision_invalidate_word(addr);
|
||||
/* (pgxp) plain-store shadow invalidation retired: the PGXP engine
|
||||
* validates tracked words against the actual packet word on read, so an
|
||||
* overwritten word can never be believed (ENHANCEMENTS.md G1). */
|
||||
/* KSEG2 cache control — before physical translation. */
|
||||
if (addr == 0xFFFE0130u) { cache_ctrl = val; return; }
|
||||
/* KSEG2 guard — see psx_read_word_raw. */
|
||||
@@ -1772,7 +1773,6 @@ void psx_write_half(uint32_t addr, uint16_t val) {
|
||||
}
|
||||
static void psx_write_half_raw(uint32_t addr, uint16_t val) {
|
||||
g_guest_store_count++;
|
||||
gte_precision_invalidate_word(addr);
|
||||
if (sr_ptr && (*sr_ptr & 0x10000u)) return;
|
||||
|
||||
/* KSEG2 guard — see psx_read_word_raw. */
|
||||
@@ -2108,7 +2108,6 @@ void psx_write_byte(uint32_t addr, uint8_t val) {
|
||||
}
|
||||
static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
|
||||
g_guest_store_count++;
|
||||
gte_precision_invalidate_word(addr);
|
||||
if (sr_ptr && (*sr_ptr & 0x10000u)) return;
|
||||
|
||||
/* KSEG2 guard — see psx_read_word_raw. */
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Framework-owned PGXP precision mod, available to every game.
|
||||
*
|
||||
* Sub-pixel vertex precision + perspective-correct texturing is a property of
|
||||
* the emulated GTE/GPU pair, not of any particular disc, so it ships here and
|
||||
* mods/builtin/packages/psx.enhancement.pgxp targets game_id "*". Default off
|
||||
* (the faithful floor, ENHANCEMENTS.md G1.9); enabling arms the value-
|
||||
* propagation engine (pgxp.cpp) with the user-validated defaults — tolerance
|
||||
* clamp 0.5px, cpu-mode per the mod option.
|
||||
*
|
||||
* Coverage note: the engine reaches near-total dataflow coverage on a binary
|
||||
* compiled with the PGXP hook variant (-DPSX_PGXP=1, PSX_PGXP_VARIANT); on a
|
||||
* base-flavour binary only the always-emitted swc2 tier and the interpreters
|
||||
* feed it, so the correction is weaker there but never harmful — every vertex
|
||||
* is validated against the exact packet word before it is believed.
|
||||
*/
|
||||
#include "mod_plugins.h"
|
||||
#include "pgxp.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define PKG_PGXP "psx.enhancement.pgxp"
|
||||
|
||||
/* gte.cpp / gpu.c — the two correction toggles this mod arms. */
|
||||
extern void gte_geometry_correction_set(int enabled);
|
||||
extern void gpu_texture_correction_set(int enabled);
|
||||
|
||||
static int pgxp_option_flag(const char* feature, const char* id) {
|
||||
char text[16] = "";
|
||||
return psx_mod_option_value(PKG_PGXP, feature, id, text, sizeof text) &&
|
||||
strcmp(text, "true") == 0;
|
||||
}
|
||||
|
||||
static void builtin_pgxp_activate(void) {
|
||||
gte_geometry_correction_set(1);
|
||||
gpu_texture_correction_set(1); /* also arms the shadow engine */
|
||||
pgxp_set_cpu_mode(pgxp_option_flag("pgxp", "cpu_mode"));
|
||||
}
|
||||
|
||||
PSX_MOD_CONSTRUCTOR(psx_register_builtin_pgxp_plugin) {
|
||||
(void)psx_mod_register_activation_plugin("psx.pgxp",
|
||||
builtin_pgxp_activate);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,803 @@
|
||||
/* pgxp.cpp — PGXP value-propagation engine (ENHANCEMENTS.md G1.2/G1.3).
|
||||
*
|
||||
* CLEAN-ROOM implementation of the publicly documented PGXP technique
|
||||
* (psx-spx GTE docs + public design write-ups + our own G1 measurements).
|
||||
* The vendored duckstation/ (CC BY-NC-ND) and beetle-psx/ (GPL) trees are
|
||||
* black-box behavioral oracles only — no code from them appears here.
|
||||
*
|
||||
* Model
|
||||
* -----
|
||||
* Every 32-bit word of guest RAM/scratchpad, every GPR (plus HI/LO), and
|
||||
* every GTE data register owns a shadow slot recording the sub-pixel screen
|
||||
* position that word carries (16.16 X/Y + projected SZ depth), the exact
|
||||
* guest word it describes (`value`), and per-half validity flags. RTPS/RTPT
|
||||
* fill the SXY shadow FIFO with the pre-truncation projection; the
|
||||
* psx_pgxp_* hooks copy shadows along with the data (loads, stores, COP2
|
||||
* transfers, and — in cpu-mode — the arithmetic games use to repack vertex
|
||||
* halves); the GPU asks for the precise position of a GP0 vertex word by the
|
||||
* packet's RAM address.
|
||||
*
|
||||
* The single safety invariant: a shadow is only ever BELIEVED after
|
||||
* validation against the actual guest word it claims to describe. Anything
|
||||
* that writes guest state without a hook (DMA, memcpy loaders, un-hooked
|
||||
* instructions) simply leaves a stale shadow behind, and the next validation
|
||||
* drops it. We never model side effects — overwrite and validate only. The
|
||||
* one accepted hole (shared with the reference implementations): an untracked
|
||||
* writer storing the byte-identical word keeps the shadow alive, which is
|
||||
* harmless because the position it describes is still that word.
|
||||
*
|
||||
* Everything here is host-only and visual-only: guest-visible state is never
|
||||
* read back from shadows, shadows are dropped on savestate/rewind, and the
|
||||
* speculative native-validation bracket suppresses all recording.
|
||||
*/
|
||||
|
||||
#include "pgxp.h"
|
||||
#include "pgxp_hooks.h"
|
||||
#include "cpu_state.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
/* gte.cpp — position-cache fallback tier (ambiguity-gated, G1.4 exact table) */
|
||||
extern "C" int gte_geometry_correction_lookup(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16);
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Shadow storage */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
enum {
|
||||
PGXP_F_VX = 1u << 0, /* low half (screen X) tracked */
|
||||
PGXP_F_VY = 1u << 1, /* high half (screen Y) tracked */
|
||||
PGXP_F_VZ = 1u << 2, /* projected depth rode along intact */
|
||||
PGXP_F_VXY = PGXP_F_VX | PGXP_F_VY,
|
||||
};
|
||||
|
||||
struct PGXPValue {
|
||||
int32_t x16, y16; /* sub-pixel screen coords, 16.16 */
|
||||
uint16_t z; /* projected SZ depth (perspective source), 0 = none */
|
||||
uint16_t flags;
|
||||
uint32_t value; /* the guest word this shadow describes */
|
||||
uint32_t gen; /* valid iff == s_gen (O(1) invalidate-all) */
|
||||
};
|
||||
|
||||
#define PGXP_RAM_WORDS (0x200000u >> 2) /* 2 MB RAM */
|
||||
#define PGXP_SCRATCH_WORDS (0x400u >> 2) /* 1 KB scratchpad */
|
||||
#define PGXP_REG_HI 32
|
||||
#define PGXP_REG_LO 33
|
||||
|
||||
static PGXPValue *s_ram = nullptr; /* lazily allocated, ~10 MB */
|
||||
static PGXPValue s_scratch[PGXP_SCRATCH_WORDS];
|
||||
static PGXPValue s_gpr[34]; /* 32 GPRs + HI + LO */
|
||||
static PGXPValue s_gte[32]; /* GTE data registers */
|
||||
|
||||
static uint32_t s_gen = 1;
|
||||
static int s_enabled = 0;
|
||||
static int s_cpu_mode = 0;
|
||||
static float s_tolerance = 0.5f; /* user-validated seam clamp (G1.10) */
|
||||
static uint32_t s_suppress = 0;
|
||||
static int s_deferred_invalidate = 0;
|
||||
|
||||
/* Single hot-path gate for every hook. */
|
||||
static int g_pgxp_active = 0;
|
||||
static inline void recompute_active(void) {
|
||||
g_pgxp_active = (s_enabled && s_suppress == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
static PGXPStats s_stats;
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Lifecycle */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
extern "C" void pgxp_invalidate_all(void) {
|
||||
if (s_suppress != 0) { s_deferred_invalidate = 1; return; }
|
||||
if (++s_gen == 0) {
|
||||
/* generation wrapped: physically clear so stale slots can't revive */
|
||||
if (s_ram) std::memset(s_ram, 0, PGXP_RAM_WORDS * sizeof(PGXPValue));
|
||||
std::memset(s_scratch, 0, sizeof(s_scratch));
|
||||
std::memset(s_gpr, 0, sizeof(s_gpr));
|
||||
std::memset(s_gte, 0, sizeof(s_gte));
|
||||
s_gen = 1;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void pgxp_set_enabled(int enabled) {
|
||||
if (enabled && !s_ram) {
|
||||
s_ram = (PGXPValue *)std::calloc(PGXP_RAM_WORDS, sizeof(PGXPValue));
|
||||
if (!s_ram) enabled = 0; /* fail closed: stay faithful */
|
||||
}
|
||||
s_enabled = enabled ? 1 : 0;
|
||||
pgxp_invalidate_all();
|
||||
recompute_active();
|
||||
}
|
||||
|
||||
extern "C" int pgxp_enabled(void) { return s_enabled; }
|
||||
|
||||
extern "C" void pgxp_set_cpu_mode(int enabled) { s_cpu_mode = enabled ? 1 : 0; }
|
||||
extern "C" int pgxp_cpu_mode(void) { return s_cpu_mode; }
|
||||
|
||||
extern "C" void pgxp_set_tolerance(float pixels) { s_tolerance = pixels; }
|
||||
extern "C" float pgxp_tolerance(void) { return s_tolerance; }
|
||||
|
||||
extern "C" void pgxp_suppress_begin(void) {
|
||||
++s_suppress;
|
||||
recompute_active();
|
||||
}
|
||||
|
||||
extern "C" void pgxp_suppress_end(void) {
|
||||
if (s_suppress != 0 && --s_suppress == 0) {
|
||||
if (s_deferred_invalidate) {
|
||||
s_deferred_invalidate = 0;
|
||||
pgxp_invalidate_all();
|
||||
}
|
||||
recompute_active();
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void pgxp_get_stats(PGXPStats *out) {
|
||||
if (out) *out = s_stats;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Address mapping + validation */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Guest address -> shadow slot, or NULL for BIOS/MMIO/KSEG2 (untrackable). */
|
||||
static inline PGXPValue *pgxp_ptr(uint32_t addr) {
|
||||
uint32_t m = addr & 0x1FFFFFFFu;
|
||||
if (m < 0x00800000u) /* RAM + its mirrors */
|
||||
return s_ram ? &s_ram[(m & 0x1FFFFCu) >> 2] : nullptr;
|
||||
if ((m & 0xFFFFFC00u) == 0x1F800000u) /* scratchpad */
|
||||
return &s_scratch[(m & 0x3FCu) >> 2];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static inline int pv_live(const PGXPValue *pv) {
|
||||
return pv && pv->gen == s_gen && (pv->flags & PGXP_F_VXY) != 0;
|
||||
}
|
||||
|
||||
/* Drop whichever tracked halves no longer match the actual guest word; the
|
||||
* depth belongs to the whole vertex, so any half going stale kills it too. */
|
||||
static inline void pv_validate(PGXPValue *pv, uint32_t actual) {
|
||||
if (pv->gen != s_gen) return;
|
||||
uint32_t diff = pv->value ^ actual;
|
||||
if ((pv->flags & PGXP_F_VX) && (diff & 0x0000FFFFu))
|
||||
pv->flags &= (uint16_t)~(PGXP_F_VX | PGXP_F_VZ);
|
||||
if ((pv->flags & PGXP_F_VY) && (diff & 0xFFFF0000u))
|
||||
pv->flags &= (uint16_t)~(PGXP_F_VY | PGXP_F_VZ);
|
||||
pv->value = actual;
|
||||
}
|
||||
|
||||
/* Mark a slot as "known word, no precision" — keeps `value` current so later
|
||||
* half-merges stay keyed correctly. */
|
||||
static inline void pv_reset(PGXPValue *pv, uint32_t value) {
|
||||
pv->x16 = 0; pv->y16 = 0; pv->z = 0;
|
||||
pv->flags = 0;
|
||||
pv->value = value;
|
||||
pv->gen = s_gen;
|
||||
}
|
||||
|
||||
static inline void pv_kill(PGXPValue *pv) { pv->gen = 0; }
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Instruction field helpers */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static inline uint32_t f_op(uint32_t i) { return i >> 26; }
|
||||
static inline uint32_t f_rs(uint32_t i) { return (i >> 21) & 31u; }
|
||||
static inline uint32_t f_rt(uint32_t i) { return (i >> 16) & 31u; }
|
||||
static inline uint32_t f_rd(uint32_t i) { return (i >> 11) & 31u; }
|
||||
static inline uint32_t f_shamt(uint32_t i) { return (i >> 6) & 31u; }
|
||||
static inline uint32_t f_funct(uint32_t i) { return i & 63u; }
|
||||
static inline int32_t f_simm(uint32_t i) { return (int32_t)(int16_t)(i & 0xFFFFu); }
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Memory-mode hooks: loads / stores */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
extern "C" void psx_pgxp_load(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t addr, uint32_t value) {
|
||||
(void)cpu;
|
||||
if (!g_pgxp_active) return;
|
||||
uint32_t rt = f_rt(instr);
|
||||
if (rt == 0) return;
|
||||
PGXPValue *dst = &s_gpr[rt];
|
||||
|
||||
switch (f_op(instr)) {
|
||||
case 0x23: { /* LW */
|
||||
PGXPValue *src = pgxp_ptr(addr);
|
||||
if (src && src->gen == s_gen) {
|
||||
pv_validate(src, value);
|
||||
*dst = *src;
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x21: /* LH */
|
||||
case 0x25: { /* LHU */
|
||||
PGXPValue *src = pgxp_ptr(addr);
|
||||
int hi_half = (addr >> 1) & 1;
|
||||
pv_reset(dst, value);
|
||||
/* The high half of the extended GPR is a known-exact constant
|
||||
* (0/-1 for LH, 0 for LHU): track it so re-packing via sll/or in
|
||||
* cpu-mode keeps working. */
|
||||
dst->y16 = ((int32_t)value >> 16) << 16;
|
||||
dst->flags = PGXP_F_VY;
|
||||
if (src && src->gen == s_gen) {
|
||||
uint32_t actual_half = (value & 0xFFFFu) << (hi_half ? 16 : 0);
|
||||
uint32_t mask = hi_half ? 0xFFFF0000u : 0x0000FFFFu;
|
||||
uint16_t want = hi_half ? PGXP_F_VY : PGXP_F_VX;
|
||||
if ((src->flags & want) && ((src->value ^ actual_half) & mask) == 0) {
|
||||
dst->x16 = hi_half ? src->y16 : src->x16;
|
||||
dst->flags |= PGXP_F_VX;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
default: /* LB/LBU/LWL/LWR: untrackable */
|
||||
pv_reset(dst, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t addr, uint32_t value) {
|
||||
(void)cpu;
|
||||
if (!g_pgxp_active) return;
|
||||
PGXPValue *dst = pgxp_ptr(addr);
|
||||
if (!dst) return;
|
||||
uint32_t rt = f_rt(instr);
|
||||
PGXPValue *src = (rt != 0) ? &s_gpr[rt] : nullptr;
|
||||
|
||||
switch (f_op(instr)) {
|
||||
case 0x2B: { /* SW */
|
||||
if (src && src->gen == s_gen) {
|
||||
pv_validate(src, value);
|
||||
*dst = *src;
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x29: { /* SH */
|
||||
int hi_half = (addr >> 1) & 1;
|
||||
uint32_t half = value & 0xFFFFu;
|
||||
if (dst->gen != s_gen) pv_reset(dst, half << (hi_half ? 16 : 0));
|
||||
/* Patch the stored half into the tracked word; the other half's
|
||||
* validity (if any) survives untouched. Depth never survives a
|
||||
* half-write — the vertex it described no longer exists whole. */
|
||||
if (hi_half) dst->value = (dst->value & 0x0000FFFFu) | (half << 16);
|
||||
else dst->value = (dst->value & 0xFFFF0000u) | half;
|
||||
dst->flags &= (uint16_t)~((hi_half ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ);
|
||||
dst->z = 0;
|
||||
if (src && src->gen == s_gen && (src->flags & PGXP_F_VX) &&
|
||||
((src->value ^ value) & 0xFFFFu) == 0) {
|
||||
if (hi_half) { dst->y16 = src->x16; dst->flags |= PGXP_F_VY; }
|
||||
else { dst->x16 = src->x16; dst->flags |= PGXP_F_VX; }
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x28: { /* SB */
|
||||
if (dst->gen != s_gen) return; /* nothing tracked: stay dead */
|
||||
uint32_t shift = (addr & 3u) * 8u;
|
||||
dst->value = (dst->value & ~(0xFFu << shift)) |
|
||||
((value & 0xFFu) << shift);
|
||||
dst->flags &= (uint16_t)~(((addr & 2u) ? PGXP_F_VY : PGXP_F_VX) | PGXP_F_VZ);
|
||||
dst->z = 0;
|
||||
return;
|
||||
}
|
||||
default: /* SWL/SWR: forget the word */
|
||||
pv_kill(dst);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Memory-mode hooks: COP2 transfers */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t value, uint32_t addr) {
|
||||
(void)cpu;
|
||||
if (!g_pgxp_active) return;
|
||||
|
||||
switch (f_op(instr)) {
|
||||
case 0x32: { /* LWC2: gte[rt] <- [addr] */
|
||||
PGXPValue *src = pgxp_ptr(addr);
|
||||
PGXPValue *dst = &s_gte[f_rt(instr)];
|
||||
if (src && src->gen == s_gen) {
|
||||
pv_validate(src, value);
|
||||
*dst = *src;
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x3A: { /* SWC2: [addr] <- gte[rt] */
|
||||
PGXPValue *dst = pgxp_ptr(addr);
|
||||
if (!dst) return;
|
||||
PGXPValue *src = &s_gte[f_rt(instr)];
|
||||
if (src->gen == s_gen) {
|
||||
pv_validate(src, value);
|
||||
*dst = *src;
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x12: { /* COP2 register transfers */
|
||||
switch (f_rs(instr)) {
|
||||
case 0x00: { /* MFC2: gpr[rt] <- gte[rd] */
|
||||
uint32_t rt = f_rt(instr);
|
||||
if (rt == 0) return;
|
||||
PGXPValue *src = &s_gte[f_rd(instr)];
|
||||
if (src->gen == s_gen) {
|
||||
pv_validate(src, value);
|
||||
s_gpr[rt] = *src;
|
||||
} else {
|
||||
pv_reset(&s_gpr[rt], value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x04: { /* MTC2: gte[rd] <- gpr[rt] */
|
||||
uint32_t rt = f_rt(instr);
|
||||
PGXPValue *dst = &s_gte[f_rd(instr)];
|
||||
if (rt != 0 && s_gpr[rt].gen == s_gen) {
|
||||
pv_validate(&s_gpr[rt], value);
|
||||
*dst = s_gpr[rt];
|
||||
} else {
|
||||
pv_reset(dst, value);
|
||||
}
|
||||
/* An SXYP write (rd==15) shifts the hardware FIFO; we do not
|
||||
* model it — regs 12..14 now describe words they no longer
|
||||
* match, and validation drops them on next use. */
|
||||
return;
|
||||
}
|
||||
case 0x02: { /* CFC2: control regs carry no
|
||||
positions */
|
||||
uint32_t rt = f_rt(instr);
|
||||
if (rt != 0) pv_reset(&s_gpr[rt], value);
|
||||
return;
|
||||
}
|
||||
default: /* CTC2: nothing shadowed */
|
||||
return;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* CPU-mode hooks: arithmetic (tier 2, default off) */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* One 16-bit operand component in 16.16: a tracked half contributes its
|
||||
* sub-pixel value; an untracked half contributes its exact integer value
|
||||
* (adding an exact offset to a tracked coordinate keeps the fraction). */
|
||||
static inline int32_t comp16(const PGXPValue *pv, uint32_t value, int hi_half) {
|
||||
if (pv && pv->gen == s_gen) {
|
||||
if (!hi_half && (pv->flags & PGXP_F_VX) &&
|
||||
((pv->value ^ value) & 0x0000FFFFu) == 0)
|
||||
return pv->x16;
|
||||
if (hi_half && (pv->flags & PGXP_F_VY) &&
|
||||
((pv->value ^ value) & 0xFFFF0000u) == 0)
|
||||
return pv->y16;
|
||||
}
|
||||
return ((int32_t)(int16_t)(hi_half ? (value >> 16) : value)) << 16;
|
||||
}
|
||||
|
||||
static inline int comp_tracked(const PGXPValue *pv, uint32_t value, int hi_half) {
|
||||
if (!pv || pv->gen != s_gen) return 0;
|
||||
if (!hi_half)
|
||||
return (pv->flags & PGXP_F_VX) &&
|
||||
((pv->value ^ value) & 0x0000FFFFu) == 0;
|
||||
return (pv->flags & PGXP_F_VY) &&
|
||||
((pv->value ^ value) & 0xFFFF0000u) == 0;
|
||||
}
|
||||
|
||||
/* Half-wise add/sub is only meaningful when the guest result shows no carry
|
||||
* crossed the half boundary; otherwise the halves did not combine
|
||||
* independently and the shadow must not pretend they did. */
|
||||
static inline int halves_independent(uint32_t a, uint32_t b, uint32_t r, int sub) {
|
||||
uint32_t lo = sub ? ((a & 0xFFFFu) - (b & 0xFFFFu))
|
||||
: ((a & 0xFFFFu) + (b & 0xFFFFu));
|
||||
return ((lo ^ r) & 0xFFFFu) == 0 &&
|
||||
((((sub ? a - b : a + b)) ^ r) == 0) &&
|
||||
((lo >> 16) == 0); /* no carry/borrow out of low */
|
||||
}
|
||||
|
||||
extern "C" void psx_pgxp_alu(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t result, uint32_t s1, uint32_t s2) {
|
||||
(void)cpu;
|
||||
if (!g_pgxp_active) return;
|
||||
|
||||
uint32_t op = f_op(instr);
|
||||
uint32_t dst_reg;
|
||||
PGXPValue *a = nullptr, *b = nullptr; /* source shadows (may be 0) */
|
||||
|
||||
if (op == 0) { /* SPECIAL */
|
||||
dst_reg = f_rd(instr);
|
||||
uint32_t rs = f_rs(instr), rt = f_rt(instr);
|
||||
uint32_t funct = f_funct(instr);
|
||||
if (funct == 0x11 || funct == 0x13) { /* MTHI / MTLO */
|
||||
PGXPValue *hl = &s_gpr[funct == 0x11 ? PGXP_REG_HI : PGXP_REG_LO];
|
||||
if (s_cpu_mode && rs != 0 && s_gpr[rs].gen == s_gen) {
|
||||
pv_validate(&s_gpr[rs], result);
|
||||
*hl = s_gpr[rs];
|
||||
} else pv_reset(hl, result);
|
||||
return;
|
||||
}
|
||||
if (dst_reg == 0) return;
|
||||
PGXPValue *dst = &s_gpr[dst_reg];
|
||||
|
||||
switch (funct) {
|
||||
case 0x10: /* MFHI */
|
||||
if (s_cpu_mode && s_gpr[PGXP_REG_HI].gen == s_gen) {
|
||||
pv_validate(&s_gpr[PGXP_REG_HI], result);
|
||||
*dst = s_gpr[PGXP_REG_HI];
|
||||
} else pv_reset(dst, result);
|
||||
return;
|
||||
case 0x12: /* MFLO */
|
||||
if (s_cpu_mode && s_gpr[PGXP_REG_LO].gen == s_gen) {
|
||||
pv_validate(&s_gpr[PGXP_REG_LO], result);
|
||||
*dst = s_gpr[PGXP_REG_LO];
|
||||
} else pv_reset(dst, result);
|
||||
return;
|
||||
case 0x00: case 0x02: case 0x03: /* SLL / SRL / SRA */
|
||||
case 0x04: case 0x06: case 0x07: { /* SLLV / SRLV / SRAV */
|
||||
uint32_t sh = (funct < 4) ? f_shamt(instr) : (s2 & 31u);
|
||||
uint32_t src_reg = rt;
|
||||
PGXPValue *src = (src_reg != 0) ? &s_gpr[src_reg] : nullptr;
|
||||
int right = (funct & 3u) != 0;
|
||||
if (!s_cpu_mode || sh != 16 || !src || src->gen != s_gen) {
|
||||
pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
pv_validate(src, s1);
|
||||
pv_reset(dst, result);
|
||||
if (!right) { /* << 16: low comp -> Y */
|
||||
if (src->flags & PGXP_F_VX) {
|
||||
dst->y16 = src->x16;
|
||||
dst->flags |= PGXP_F_VY;
|
||||
}
|
||||
dst->x16 = 0;
|
||||
dst->flags |= PGXP_F_VX; /* low half exactly zero */
|
||||
} else { /* >> 16: high comp -> X */
|
||||
if (src->flags & PGXP_F_VY) {
|
||||
dst->x16 = src->y16;
|
||||
dst->flags |= PGXP_F_VX;
|
||||
}
|
||||
dst->y16 = ((int32_t)result >> 16) << 16; /* 0 or sign fill */
|
||||
dst->flags |= PGXP_F_VY;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x20: case 0x21: /* ADD / ADDU */
|
||||
case 0x22: case 0x23: /* SUB / SUBU */
|
||||
case 0x25: { /* OR */
|
||||
/* MOVE idioms first — they matter even without full cpu-mode
|
||||
* arithmetic, and they are exact. */
|
||||
int is_or = (funct == 0x25);
|
||||
int is_sub = (funct == 0x22 || funct == 0x23);
|
||||
if ((is_or || !is_sub) && rt == 0) { /* op rd, rs, $zero */
|
||||
if (rs != 0 && s_gpr[rs].gen == s_gen) {
|
||||
pv_validate(&s_gpr[rs], result);
|
||||
*dst = s_gpr[rs];
|
||||
} else pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
if ((is_or || !is_sub) && rs == 0) { /* op rd, $zero, rt */
|
||||
if (rt != 0 && s_gpr[rt].gen == s_gen) {
|
||||
pv_validate(&s_gpr[rt], result);
|
||||
*dst = s_gpr[rt];
|
||||
} else pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
if (!s_cpu_mode) { pv_reset(dst, result); return; }
|
||||
a = (rs != 0) ? &s_gpr[rs] : nullptr;
|
||||
b = (rt != 0) ? &s_gpr[rt] : nullptr;
|
||||
if (is_or) {
|
||||
/* Merge disjoint halves (the lhu/sll/or repack pattern). */
|
||||
if ((s1 & s2) != 0) { pv_reset(dst, result); return; }
|
||||
pv_reset(dst, result);
|
||||
const PGXPValue *xsrc; uint32_t xval;
|
||||
if ((s1 & 0xFFFFu) != 0) { xsrc = a; xval = s1; }
|
||||
else { xsrc = b; xval = s2; }
|
||||
const PGXPValue *ysrc; uint32_t yval;
|
||||
if ((s1 & 0xFFFF0000u) != 0) { ysrc = a; yval = s1; }
|
||||
else { ysrc = b; yval = s2; }
|
||||
if (comp_tracked(xsrc, xval, 0)) {
|
||||
dst->x16 = xsrc->x16; dst->flags |= PGXP_F_VX;
|
||||
} else if ((result & 0xFFFFu) == 0) {
|
||||
dst->x16 = 0; dst->flags |= PGXP_F_VX;
|
||||
}
|
||||
if (comp_tracked(ysrc, yval, 1)) {
|
||||
dst->y16 = ysrc->y16; dst->flags |= PGXP_F_VY;
|
||||
} else if ((result & 0xFFFF0000u) == 0) {
|
||||
dst->y16 = 0; dst->flags |= PGXP_F_VY;
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* add/sub: require at least one tracked component and halves
|
||||
* that combined independently (no cross-half carry). */
|
||||
if (!halves_independent(s1, s2, result, is_sub) ||
|
||||
(!comp_tracked(a, s1, 0) && !comp_tracked(a, s1, 1) &&
|
||||
!comp_tracked(b, s2, 0) && !comp_tracked(b, s2, 1))) {
|
||||
pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
pv_reset(dst, result);
|
||||
dst->x16 = is_sub ? comp16(a, s1, 0) - comp16(b, s2, 0)
|
||||
: comp16(a, s1, 0) + comp16(b, s2, 0);
|
||||
dst->y16 = is_sub ? comp16(a, s1, 1) - comp16(b, s2, 1)
|
||||
: comp16(a, s1, 1) + comp16(b, s2, 1);
|
||||
dst->flags = PGXP_F_VXY;
|
||||
return;
|
||||
}
|
||||
default:
|
||||
pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* immediates */
|
||||
dst_reg = f_rt(instr);
|
||||
if (dst_reg == 0) return;
|
||||
PGXPValue *dst = &s_gpr[dst_reg];
|
||||
uint32_t rs = f_rs(instr);
|
||||
|
||||
switch (op) {
|
||||
case 0x0F: { /* LUI: both halves exact */
|
||||
pv_reset(dst, result);
|
||||
dst->x16 = 0;
|
||||
dst->y16 = ((int32_t)result >> 16) << 16;
|
||||
dst->flags = PGXP_F_VXY;
|
||||
return;
|
||||
}
|
||||
case 0x08: case 0x09: { /* ADDI / ADDIU */
|
||||
int32_t imm = f_simm(instr);
|
||||
if (imm == 0) { /* MOVE idiom */
|
||||
if (rs != 0 && s_gpr[rs].gen == s_gen) {
|
||||
pv_validate(&s_gpr[rs], result);
|
||||
*dst = s_gpr[rs];
|
||||
} else pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
a = (rs != 0) ? &s_gpr[rs] : nullptr;
|
||||
/* A negative immediate is a subtraction of its magnitude: checking
|
||||
* carry on the sign-extended form would reject nearly every -N. */
|
||||
{
|
||||
int neg = imm < 0;
|
||||
uint32_t mag = (uint32_t)(neg ? -imm : imm);
|
||||
if (!s_cpu_mode ||
|
||||
!halves_independent(s1, mag, result, neg) ||
|
||||
(!comp_tracked(a, s1, 0) && !comp_tracked(a, s1, 1))) {
|
||||
pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
int32_t dx = (int32_t)(mag & 0xFFFFu) << 16;
|
||||
pv_reset(dst, result);
|
||||
dst->x16 = comp16(a, s1, 0) + (neg ? -dx : dx);
|
||||
dst->y16 = comp16(a, s1, 1); /* no carry crossed: Y untouched */
|
||||
dst->flags = PGXP_F_VXY;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case 0x0D: { /* ORI */
|
||||
uint32_t imm = instr & 0xFFFFu;
|
||||
if (imm == 0 || (s_cpu_mode && (s1 & 0xFFFFu) == 0)) {
|
||||
a = (rs != 0) ? &s_gpr[rs] : nullptr;
|
||||
pv_reset(dst, result);
|
||||
if (comp_tracked(a, s1, 1)) {
|
||||
dst->y16 = a->y16; dst->flags |= PGXP_F_VY;
|
||||
}
|
||||
if (imm == 0) {
|
||||
if (comp_tracked(a, s1, 0)) {
|
||||
dst->x16 = a->x16; dst->flags |= PGXP_F_VX;
|
||||
}
|
||||
} else { /* exact constant low half */
|
||||
dst->x16 = ((int32_t)(int16_t)imm) << 16;
|
||||
dst->flags |= PGXP_F_VX;
|
||||
}
|
||||
return;
|
||||
}
|
||||
pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
default: /* ANDI/XORI/SLTI/... */
|
||||
pv_reset(dst, result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr,
|
||||
uint32_t hi, uint32_t lo,
|
||||
uint32_t s1, uint32_t s2) {
|
||||
(void)cpu; (void)instr; (void)s1; (void)s2;
|
||||
if (!g_pgxp_active) return;
|
||||
/* Products/quotients of screen coordinates are not screen coordinates:
|
||||
* record the results as known-but-imprecise so MFHI/MFLO stay honest. */
|
||||
pv_reset(&s_gpr[PGXP_REG_HI], hi);
|
||||
pv_reset(&s_gpr[PGXP_REG_LO], lo);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* GTE producer */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
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];
|
||||
pv->x16 = x16;
|
||||
pv->y16 = y16;
|
||||
pv->z = sz3;
|
||||
pv->flags = (uint16_t)(PGXP_F_VXY | (sz3 != 0 ? PGXP_F_VZ : 0));
|
||||
pv->value = packed;
|
||||
pv->gen = s_gen;
|
||||
s_gte[15] = *pv; /* SXYP mirrors SXY2 */
|
||||
}
|
||||
|
||||
extern "C" int pgxp_get_gte_sxy(uint32_t index, int32_t *x16, int32_t *y16) {
|
||||
if (index >= 4) return 0;
|
||||
const PGXPValue *pv = &s_gte[12 + index];
|
||||
if (!pv_live(pv) || (pv->flags & PGXP_F_VXY) != PGXP_F_VXY)
|
||||
return 0;
|
||||
if (x16) *x16 = pv->x16;
|
||||
if (y16) *y16 = pv->y16;
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) {
|
||||
/* Invalidation-class bookkeeping: runs even with the engine disarmed so
|
||||
* seeded/leftover shadows can never outlive a guest register write. Only
|
||||
* the suppression bracket skips it (the speculative pass rolls the
|
||||
* machine state back, so its writes must not stick to the shadows). */
|
||||
if (s_suppress != 0) return;
|
||||
if (reg < 0 || reg > 31) return;
|
||||
pv_kill(&s_gte[reg]);
|
||||
pv_reset(&s_gte[reg], value);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* GPU consumer */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y,
|
||||
int32_t *x16, int32_t *y16,
|
||||
uint16_t *sz) {
|
||||
s_stats.lookups++;
|
||||
|
||||
int32_t px = 0, py = 0;
|
||||
uint16_t pz = 0;
|
||||
int have = 0;
|
||||
|
||||
if (s_enabled && addr != 0xFFFFFFFFu) {
|
||||
PGXPValue *pv = pgxp_ptr(addr);
|
||||
if (pv && pv->gen == s_gen && (pv->flags & PGXP_F_VXY) != 0) {
|
||||
if (pv->value == packet_word &&
|
||||
(pv->flags & PGXP_F_VXY) == PGXP_F_VXY) {
|
||||
px = pv->x16;
|
||||
py = pv->y16;
|
||||
pz = (pv->flags & PGXP_F_VZ) ? pv->z : 0;
|
||||
have = PGXP_SRC_DATAFLOW;
|
||||
} else {
|
||||
s_stats.value_mismatch++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!have && gte_geometry_correction_lookup(packet_word, &px, &py)) {
|
||||
pz = 0; /* fallback never carries depth */
|
||||
have = PGXP_SRC_FALLBACK;
|
||||
}
|
||||
|
||||
if (have) {
|
||||
/* Truncation agreement: the GPU parsed 11-bit integers out of the
|
||||
* packet; a precise position whose integer part disagrees (a
|
||||
* wrapped/CPU-modified coordinate) must not be believed. */
|
||||
if ((px >> 16) != int_x || (py >> 16) != int_y) {
|
||||
s_stats.trunc_reject++;
|
||||
have = 0;
|
||||
} else if (s_tolerance >= 0.0f) {
|
||||
float dx = (float)(px - (int_x << 16)) * (1.0f / 65536.0f);
|
||||
float dy = (float)(py - (int_y << 16)) * (1.0f / 65536.0f);
|
||||
if (dx > s_tolerance || dy > s_tolerance) {
|
||||
s_stats.tolerance_reject++;
|
||||
have = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!have) {
|
||||
s_stats.native++;
|
||||
*x16 = int_x << 16;
|
||||
*y16 = int_y << 16;
|
||||
*sz = 0;
|
||||
return PGXP_SRC_NATIVE;
|
||||
}
|
||||
|
||||
if (have == PGXP_SRC_DATAFLOW) s_stats.dataflow_hit++;
|
||||
else s_stats.fallback_hit++;
|
||||
if (pz != 0) s_stats.w_valid++;
|
||||
*x16 = px;
|
||||
*y16 = py;
|
||||
*sz = pz;
|
||||
return have;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Legacy v14 SWC2 tracker — kept as the base-flavour feed */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
/* Emitted at every swc2 since ABI v14 (all flavours, all backends, overlay
|
||||
* DLLs). In the pgxp flavour psx_pgxp_cop2 supersedes it for the same swc2 —
|
||||
* the double write is idempotent (same source shadow, same destination). */
|
||||
extern "C" void pgxp_store_gte_reg(uint32_t addr, uint8_t reg) {
|
||||
if (!g_pgxp_active) return;
|
||||
PGXPValue *dst = pgxp_ptr(addr);
|
||||
if (!dst) return;
|
||||
const PGXPValue *src = &s_gte[reg & 31u];
|
||||
if (src->gen != s_gen) return;
|
||||
s_stats.swc2_stores++;
|
||||
*dst = *src;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Test accessors */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
extern "C" void pgxp_test_seed_gte_sxy(uint32_t index, uint32_t packed,
|
||||
int32_t x16, int32_t y16, uint16_t z,
|
||||
int valid) {
|
||||
if (index >= 4) return;
|
||||
PGXPValue *pv = &s_gte[12 + index];
|
||||
pv->x16 = x16;
|
||||
pv->y16 = y16;
|
||||
pv->z = z;
|
||||
pv->value = packed;
|
||||
pv->flags = (uint16_t)(PGXP_F_VXY | (z != 0 ? PGXP_F_VZ : 0));
|
||||
pv->gen = valid ? s_gen : 0;
|
||||
}
|
||||
|
||||
extern "C" void pgxp_test_get_gte_sxy(uint32_t index, uint32_t *packed,
|
||||
int32_t *x16, int32_t *y16, uint16_t *z,
|
||||
uint8_t *valid) {
|
||||
if (index >= 4) return;
|
||||
const PGXPValue *pv = &s_gte[12 + index];
|
||||
if (packed) *packed = pv->value;
|
||||
if (x16) *x16 = pv->x16;
|
||||
if (y16) *y16 = pv->y16;
|
||||
if (z) *z = pv->z;
|
||||
if (valid) *valid = pv_live(pv) ? 1 : 0;
|
||||
}
|
||||
|
||||
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
|
||||
* (gpu.c prepare_texture_triangle). Same contract as the retired hashed
|
||||
* table: hit only when the tracked word matches the packet word exactly. */
|
||||
extern "C" int pgxp_load_precise_word(uint32_t addr, uint32_t packed,
|
||||
int32_t *x16, int32_t *y16, uint16_t *z) {
|
||||
if (!s_enabled) return 0;
|
||||
PGXPValue *pv = pgxp_ptr(addr);
|
||||
if (!pv || pv->gen != s_gen || pv->value != packed ||
|
||||
(pv->flags & PGXP_F_VXY) != PGXP_F_VXY)
|
||||
return 0;
|
||||
if (x16) *x16 = pv->x16;
|
||||
if (y16) *y16 = pv->y16;
|
||||
if (z) *z = (pv->flags & PGXP_F_VZ) ? pv->z : 0;
|
||||
return (pv->flags & PGXP_F_VZ) && pv->z != 0;
|
||||
}
|
||||
@@ -211,12 +211,18 @@ static void exec_one(CPUState* cpu) {
|
||||
uint32_t rd = RD(insn);
|
||||
uint32_t sa = SA(insn);
|
||||
switch (func) {
|
||||
case 0x00: set_reg(cpu, rd, rt_val << sa); break; /* SLL */
|
||||
case 0x02: set_reg(cpu, rd, rt_val >> sa); break; /* SRL */
|
||||
case 0x03: set_reg(cpu, rd, (uint32_t)((int32_t)rt_val >> sa)); break; /* SRA */
|
||||
case 0x04: set_reg(cpu, rd, rt_val << (rs_val & 31)); break; /* SLLV */
|
||||
case 0x06: set_reg(cpu, rd, rt_val >> (rs_val & 31)); break; /* SRLV */
|
||||
case 0x07: set_reg(cpu, rd, (uint32_t)((int32_t)rt_val >> (rs_val & 31))); break; /* SRAV */
|
||||
case 0x00: set_reg(cpu, rd, rt_val << sa);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, sa); break; /* SLL */
|
||||
case 0x02: set_reg(cpu, rd, rt_val >> sa);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, sa); break; /* SRL */
|
||||
case 0x03: set_reg(cpu, rd, (uint32_t)((int32_t)rt_val >> sa));
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, sa); break; /* SRA */
|
||||
case 0x04: set_reg(cpu, rd, rt_val << (rs_val & 31));
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, rs_val); break; /* SLLV */
|
||||
case 0x06: set_reg(cpu, rd, rt_val >> (rs_val & 31));
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, rs_val); break; /* SRLV */
|
||||
case 0x07: set_reg(cpu, rd, (uint32_t)((int32_t)rt_val >> (rs_val & 31)));
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rt_val, rs_val); break; /* SRAV */
|
||||
case 0x08: { /* JR */
|
||||
uint32_t target = rs_val;
|
||||
exec_one(cpu); /* delay slot */
|
||||
@@ -232,41 +238,54 @@ static void exec_one(CPUState* cpu) {
|
||||
}
|
||||
case 0x0C: raise_exception(cpu, 8, 0); return; /* SYSCALL */
|
||||
case 0x0D: raise_exception(cpu, 9, 0); return; /* BREAK */
|
||||
case 0x10: set_reg(cpu, rd, cpu->hi); break; /* MFHI */
|
||||
case 0x11: cpu->hi = rs_val; break; /* MTHI */
|
||||
case 0x12: set_reg(cpu, rd, cpu->lo); break; /* MFLO */
|
||||
case 0x13: cpu->lo = rs_val; break; /* MTLO */
|
||||
case 0x10: set_reg(cpu, rd, cpu->hi);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], cpu->hi, 0); break; /* MFHI */
|
||||
case 0x11: cpu->hi = rs_val;
|
||||
psx_pgxp_alu(cpu, insn, cpu->hi, rs_val, 0); break; /* MTHI */
|
||||
case 0x12: set_reg(cpu, rd, cpu->lo);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], cpu->lo, 0); break; /* MFLO */
|
||||
case 0x13: cpu->lo = rs_val;
|
||||
psx_pgxp_alu(cpu, insn, cpu->lo, rs_val, 0); break; /* MTLO */
|
||||
case 0x18: { /* MULT */
|
||||
int64_t r = (int64_t)(int32_t)rs_val * (int64_t)(int32_t)rt_val;
|
||||
cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32); break;
|
||||
cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32);
|
||||
psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, rs_val, rt_val); break;
|
||||
}
|
||||
case 0x19: { /* MULTU */
|
||||
uint64_t r = (uint64_t)rs_val * (uint64_t)rt_val;
|
||||
cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32); break;
|
||||
cpu->lo = (uint32_t)r; cpu->hi = (uint32_t)(r >> 32);
|
||||
psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, rs_val, rt_val); break;
|
||||
}
|
||||
case 0x1A: /* DIV */
|
||||
if (rt_val == 0) { cpu->lo = (rs_val & 0x80000000u) ? 1 : 0xFFFFFFFFu; cpu->hi = rs_val; }
|
||||
else if (rs_val == 0x80000000u && rt_val == 0xFFFFFFFFu) { cpu->lo = 0x80000000u; cpu->hi = 0; }
|
||||
else { cpu->lo = (uint32_t)((int32_t)rs_val / (int32_t)rt_val); cpu->hi = (uint32_t)((int32_t)rs_val % (int32_t)rt_val); }
|
||||
psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, rs_val, rt_val);
|
||||
break;
|
||||
case 0x1B: /* DIVU */
|
||||
if (rt_val == 0) { cpu->lo = 0xFFFFFFFFu; cpu->hi = rs_val; }
|
||||
else { cpu->lo = rs_val / rt_val; cpu->hi = rs_val % rt_val; }
|
||||
psx_pgxp_muldiv(cpu, insn, cpu->hi, cpu->lo, rs_val, rt_val);
|
||||
break;
|
||||
case 0x20: { /* ADD (overflow trap) */
|
||||
int64_t r = (int64_t)(int32_t)rs_val + (int64_t)(int32_t)rt_val;
|
||||
if (r < (int64_t)INT32_MIN || r > (int64_t)INT32_MAX) { raise_exception(cpu, 12, 0); return; }
|
||||
set_reg(cpu, rd, (uint32_t)(int32_t)r); break;
|
||||
set_reg(cpu, rd, (uint32_t)(int32_t)r);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break;
|
||||
}
|
||||
case 0x21: set_reg(cpu, rd, rs_val + rt_val); break; /* ADDU */
|
||||
case 0x21: set_reg(cpu, rd, rs_val + rt_val);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break; /* ADDU */
|
||||
case 0x22: { /* SUB (overflow trap) */
|
||||
int64_t r = (int64_t)(int32_t)rs_val - (int64_t)(int32_t)rt_val;
|
||||
if (r < (int64_t)INT32_MIN || r > (int64_t)INT32_MAX) { raise_exception(cpu, 12, 0); return; }
|
||||
set_reg(cpu, rd, (uint32_t)(int32_t)r); break;
|
||||
set_reg(cpu, rd, (uint32_t)(int32_t)r);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break;
|
||||
}
|
||||
case 0x23: set_reg(cpu, rd, rs_val - rt_val); break; /* SUBU */
|
||||
case 0x23: set_reg(cpu, rd, rs_val - rt_val);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break; /* SUBU */
|
||||
case 0x24: set_reg(cpu, rd, rs_val & rt_val); break; /* AND */
|
||||
case 0x25: set_reg(cpu, rd, rs_val | rt_val); break; /* OR */
|
||||
case 0x25: set_reg(cpu, rd, rs_val | rt_val);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], rs_val, rt_val); break; /* OR */
|
||||
case 0x26: set_reg(cpu, rd, rs_val ^ rt_val); break; /* XOR */
|
||||
case 0x27: set_reg(cpu, rd, ~(rs_val | rt_val)); break; /* NOR */
|
||||
case 0x2A: set_reg(cpu, rd, (int32_t)rs_val < (int32_t)rt_val ? 1 : 0); break; /* SLT */
|
||||
@@ -339,15 +358,19 @@ static void exec_one(CPUState* cpu) {
|
||||
case 0x08: { /* ADDI (overflow trap) */
|
||||
int64_t r = (int64_t)(int32_t)rs_val + (int64_t)SIMM(insn);
|
||||
if (r < (int64_t)INT32_MIN || r > (int64_t)INT32_MAX) { raise_exception(cpu, 12, 0); return; }
|
||||
set_reg(cpu, RT(insn), (uint32_t)(int32_t)r); break;
|
||||
set_reg(cpu, RT(insn), (uint32_t)(int32_t)r);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[RT(insn)], rs_val, (uint32_t)SIMM(insn)); break;
|
||||
}
|
||||
case 0x09: set_reg(cpu, RT(insn), rs_val + (uint32_t)SIMM(insn)); break; /* ADDIU */
|
||||
case 0x09: set_reg(cpu, RT(insn), rs_val + (uint32_t)SIMM(insn));
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[RT(insn)], rs_val, (uint32_t)SIMM(insn)); break; /* ADDIU */
|
||||
case 0x0A: set_reg(cpu, RT(insn), (int32_t)rs_val < SIMM(insn) ? 1 : 0); break; /* SLTI */
|
||||
case 0x0B: set_reg(cpu, RT(insn), rs_val < (uint32_t)SIMM(insn) ? 1 : 0); break; /* SLTIU */
|
||||
case 0x0C: set_reg(cpu, RT(insn), rs_val & IMM16(insn)); break; /* ANDI */
|
||||
case 0x0D: set_reg(cpu, RT(insn), rs_val | IMM16(insn)); break; /* ORI */
|
||||
case 0x0D: set_reg(cpu, RT(insn), rs_val | IMM16(insn));
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[RT(insn)], rs_val, IMM16(insn)); break; /* ORI */
|
||||
case 0x0E: set_reg(cpu, RT(insn), rs_val ^ IMM16(insn)); break; /* XORI */
|
||||
case 0x0F: set_reg(cpu, RT(insn), (uint32_t)IMM16(insn) << 16); break; /* LUI */
|
||||
case 0x0F: set_reg(cpu, RT(insn), (uint32_t)IMM16(insn) << 16);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[RT(insn)], 0, 0); break; /* LUI */
|
||||
|
||||
/* --- COP0 --- */
|
||||
case 0x10: {
|
||||
@@ -376,14 +399,21 @@ static void exec_one(CPUState* cpu) {
|
||||
case 0x12: {
|
||||
uint32_t cop_op = RS(insn);
|
||||
switch (cop_op) {
|
||||
case 0x00: /* MFC2 */
|
||||
set_load_delay(RT(insn), gte_read_data(cpu, RD(insn)));
|
||||
case 0x00: { /* MFC2 */
|
||||
uint32_t v = gte_read_data(cpu, RD(insn));
|
||||
set_load_delay(RT(insn), v);
|
||||
psx_pgxp_cop2(cpu, insn, v, 0);
|
||||
break;
|
||||
case 0x02: /* CFC2 */
|
||||
set_load_delay(RT(insn), gte_read_ctrl(cpu, RD(insn)));
|
||||
}
|
||||
case 0x02: { /* CFC2 */
|
||||
uint32_t v = gte_read_ctrl(cpu, RD(insn));
|
||||
set_load_delay(RT(insn), v);
|
||||
psx_pgxp_cop2(cpu, insn, v, 0);
|
||||
break;
|
||||
}
|
||||
case 0x04: /* MTC2 */
|
||||
gte_write_data(cpu, RD(insn), rt_val);
|
||||
psx_pgxp_cop2(cpu, insn, rt_val, 0);
|
||||
break;
|
||||
case 0x06: /* CTC2 */
|
||||
gte_write_ctrl(cpu, RD(insn), rt_val);
|
||||
@@ -403,12 +433,16 @@ static void exec_one(CPUState* cpu) {
|
||||
/* --- Loads --- */
|
||||
case 0x20: { /* LB */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
set_load_delay(RT(insn), (uint32_t)(int32_t)(int8_t)cpu->read_byte(addr));
|
||||
uint32_t v = (uint32_t)(int32_t)(int8_t)cpu->read_byte(addr);
|
||||
set_load_delay(RT(insn), v);
|
||||
psx_pgxp_load(cpu, insn, addr, v);
|
||||
break;
|
||||
}
|
||||
case 0x21: { /* LH */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
set_load_delay(RT(insn), (uint32_t)(int32_t)(int16_t)cpu->read_half(addr));
|
||||
uint32_t v = (uint32_t)(int32_t)(int16_t)cpu->read_half(addr);
|
||||
set_load_delay(RT(insn), v);
|
||||
psx_pgxp_load(cpu, insn, addr, v);
|
||||
break;
|
||||
}
|
||||
case 0x22: { /* LWL */
|
||||
@@ -423,21 +457,28 @@ static void exec_one(CPUState* cpu) {
|
||||
case 3: cur = word; break;
|
||||
}
|
||||
set_reg(cpu, RT(insn), cur);
|
||||
psx_pgxp_load(cpu, insn, addr, cur);
|
||||
break;
|
||||
}
|
||||
case 0x23: { /* LW */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
set_load_delay(RT(insn), cpu->read_word(addr));
|
||||
uint32_t v = cpu->read_word(addr);
|
||||
set_load_delay(RT(insn), v);
|
||||
psx_pgxp_load(cpu, insn, addr, v);
|
||||
break;
|
||||
}
|
||||
case 0x24: { /* LBU */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
set_load_delay(RT(insn), (uint32_t)cpu->read_byte(addr));
|
||||
uint32_t v = (uint32_t)cpu->read_byte(addr);
|
||||
set_load_delay(RT(insn), v);
|
||||
psx_pgxp_load(cpu, insn, addr, v);
|
||||
break;
|
||||
}
|
||||
case 0x25: { /* LHU */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
set_load_delay(RT(insn), (uint32_t)cpu->read_half(addr));
|
||||
uint32_t v = (uint32_t)cpu->read_half(addr);
|
||||
set_load_delay(RT(insn), v);
|
||||
psx_pgxp_load(cpu, insn, addr, v);
|
||||
break;
|
||||
}
|
||||
case 0x26: { /* LWR */
|
||||
@@ -452,6 +493,7 @@ static void exec_one(CPUState* cpu) {
|
||||
case 3: cur = (cur & 0xFFFFFF00u) | (word >> 24); break;
|
||||
}
|
||||
set_reg(cpu, RT(insn), cur);
|
||||
psx_pgxp_load(cpu, insn, addr, cur);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -459,11 +501,13 @@ static void exec_one(CPUState* cpu) {
|
||||
case 0x28: { /* SB */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
cpu->write_byte(addr, (uint8_t)(rt_val & 0xFF));
|
||||
psx_pgxp_store(cpu, insn, addr, rt_val & 0xFFu);
|
||||
break;
|
||||
}
|
||||
case 0x29: { /* SH */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
cpu->write_half(addr, (uint16_t)(rt_val & 0xFFFF));
|
||||
psx_pgxp_store(cpu, insn, addr, rt_val & 0xFFFFu);
|
||||
break;
|
||||
}
|
||||
case 0x2A: { /* SWL */
|
||||
@@ -477,11 +521,13 @@ static void exec_one(CPUState* cpu) {
|
||||
case 3: cur = rt_val; break;
|
||||
}
|
||||
cpu->write_word(aligned, cur);
|
||||
psx_pgxp_store(cpu, insn, addr, rt_val);
|
||||
break;
|
||||
}
|
||||
case 0x2B: { /* SW */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
cpu->write_word(addr, rt_val);
|
||||
psx_pgxp_store(cpu, insn, addr, rt_val);
|
||||
break;
|
||||
}
|
||||
case 0x2E: { /* SWR */
|
||||
@@ -495,19 +541,24 @@ static void exec_one(CPUState* cpu) {
|
||||
case 3: cur = (cur & 0x00FFFFFFu) | (rt_val << 24); break;
|
||||
}
|
||||
cpu->write_word(aligned, cur);
|
||||
psx_pgxp_store(cpu, insn, addr, rt_val);
|
||||
break;
|
||||
}
|
||||
|
||||
/* --- COP2 load/store (LWC2/SWC2) --- */
|
||||
case 0x32: { /* LWC2 */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
gte_write_data(cpu, RT(insn), cpu->read_word(addr));
|
||||
uint32_t v = cpu->read_word(addr);
|
||||
gte_write_data(cpu, RT(insn), v);
|
||||
psx_pgxp_cop2(cpu, insn, v, addr);
|
||||
break;
|
||||
}
|
||||
case 0x3A: { /* SWC2 */
|
||||
uint32_t addr = rs_val + (uint32_t)SIMM(insn);
|
||||
cpu->write_word(addr, gte_read_data(cpu, RT(insn)));
|
||||
uint32_t v = gte_read_data(cpu, RT(insn));
|
||||
cpu->write_word(addr, v);
|
||||
gte_precision_store_word(addr, RT(insn));
|
||||
psx_pgxp_cop2(cpu, insn, v, addr);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -702,6 +702,80 @@ static void blit_thumb(uint32_t *dst, int dw, int x0, int y0, int tw, int th,
|
||||
}
|
||||
}
|
||||
|
||||
static void glyph_fill_rect(uint32_t *dst, int dw, int x0, int y0,
|
||||
int w, int h, uint32_t col)
|
||||
{
|
||||
int x, y;
|
||||
if (x0 < 0) { w += x0; x0 = 0; }
|
||||
if (y0 < 0) { h += y0; y0 = 0; }
|
||||
if (x0 + w > dw) w = dw - x0;
|
||||
if (y0 + h > RW_PANEL_H) h = RW_PANEL_H - y0;
|
||||
if (w <= 0 || h <= 0) return;
|
||||
for (y = y0; y < y0 + h; y++)
|
||||
for (x = x0; x < x0 + w; x++)
|
||||
dst[y * dw + x] = col;
|
||||
}
|
||||
|
||||
static void glyph_fill_disc(uint32_t *dst, int dw, int cx, int cy,
|
||||
int r, uint32_t col)
|
||||
{
|
||||
int x, y;
|
||||
const int rr = r * r;
|
||||
for (y = -r; y <= r; y++) {
|
||||
for (x = -r; x <= r; x++) {
|
||||
if (x * x + y * y > rr) continue;
|
||||
glyph_fill_rect(dst, dw, cx + x, cy + y, 1, 1, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void glyph_line(uint32_t *dst, int dw, int x0, int y0, int x1, int y1,
|
||||
int thickness, uint32_t col)
|
||||
{
|
||||
int dx = x1 > x0 ? x1 - x0 : x0 - x1;
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int dy = y1 > y0 ? y0 - y1 : y1 - y0;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
int err = dx + dy;
|
||||
int inset = thickness / 2;
|
||||
|
||||
for (;;) {
|
||||
glyph_fill_rect(dst, dw, x0 - inset, y0 - inset, thickness, thickness,
|
||||
col);
|
||||
if (x0 == x1 && y0 == y1)
|
||||
break;
|
||||
{
|
||||
int e2 = err * 2;
|
||||
if (e2 >= dy) {
|
||||
err += dy;
|
||||
x0 += sx;
|
||||
}
|
||||
if (e2 <= dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_psx_button(uint32_t *dst, int dw, int x, int y, char kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case 'x':
|
||||
glyph_line(dst, dw, x + 4, y + 4, x + 14, y + 14, 2, 0xFF5FA8FFu);
|
||||
glyph_line(dst, dw, x + 14, y + 4, x + 4, y + 14, 2, 0xFF5FA8FFu);
|
||||
break;
|
||||
case 'o':
|
||||
glyph_fill_disc(dst, dw, x + 9, y + 9, 8, 0xFFFF6B6Bu);
|
||||
glyph_fill_disc(dst, dw, x + 9, y + 9, 5, 0xE0101218u);
|
||||
break;
|
||||
default:
|
||||
glyph_fill_rect(dst, dw, x + 7, y + 2, 5, 15, 0xFFD7DCE6u);
|
||||
glyph_fill_rect(dst, dw, x + 2, y + 7, 15, 5, 0xFFD7DCE6u);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_char(uint32_t *dst, int dw, int x0, int y0, char c, uint32_t col)
|
||||
{
|
||||
const uint8_t *g;
|
||||
@@ -745,6 +819,7 @@ static void rasterize_panel(void)
|
||||
{
|
||||
uint32_t *d = s_panel;
|
||||
int i, n, card_w, card_h, gap, x, y, sel_w, sel_h;
|
||||
int fy;
|
||||
int center_x, origin;
|
||||
char buf[96];
|
||||
char hk[32];
|
||||
@@ -762,10 +837,15 @@ static void rasterize_panel(void)
|
||||
draw_text(d, RW_PANEL_W, 120, 8, "NO SNAPSHOTS YET", 0xFF888888u);
|
||||
}
|
||||
host_keymap_label(HOST_KEYMAP_REWIND, hk, sizeof(hk));
|
||||
/* Overlay font is uppercase-only; keep PS labels dual for pad + KB users. */
|
||||
snprintf(buf, sizeof(buf),
|
||||
"LEFT/RIGHT A/CROSS LOAD B/CIRCLE CLOSE %s", hk);
|
||||
draw_text(d, RW_PANEL_W, 12, RW_PANEL_H - 16, buf, 0xFFAAAAAAu);
|
||||
fy = RW_PANEL_H - 22;
|
||||
draw_psx_button(d, RW_PANEL_W, 12, fy, 'd');
|
||||
draw_text(d, RW_PANEL_W, 36, fy + 5, "SEEK", 0xFFE2E5EBu);
|
||||
draw_psx_button(d, RW_PANEL_W, 116, fy, 'x');
|
||||
draw_text(d, RW_PANEL_W, 140, fy + 5, "LOAD", 0xFFE2E5EBu);
|
||||
draw_psx_button(d, RW_PANEL_W, 216, fy, 'o');
|
||||
draw_text(d, RW_PANEL_W, 240, fy + 5, "CLOSE", 0xFFE2E5EBu);
|
||||
snprintf(buf, sizeof(buf), "%s MENU", hk[0] ? hk : "F8");
|
||||
draw_text(d, RW_PANEL_W, 512, fy + 5, buf, 0xFFAAAAAAu);
|
||||
|
||||
n = (int)s_count;
|
||||
if (n <= 0) {
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
|
||||
#define SSM_W 640
|
||||
#define SSM_H 480
|
||||
#define SSM_ROW_H 132
|
||||
#define SSM_ROW_GAP 8
|
||||
#define SSM_ROWS_Y 56
|
||||
#define SSM_ROW_H 108
|
||||
#define SSM_ROW_GAP 6
|
||||
#define SSM_ROWS_Y 58
|
||||
#define SSM_ROWS_X 28
|
||||
#define SSM_ROWS_W 584
|
||||
#define SSM_VISIBLE_ROWS 3
|
||||
#define SSM_THUMB_W 168
|
||||
#define SSM_THUMB_H 126
|
||||
#define SSM_THUMB_W 136
|
||||
#define SSM_THUMB_H 102
|
||||
|
||||
/* Public-domain 8x8 ASCII 32..90 subset from font8x8_basic. */
|
||||
static const uint8_t FONT8[59][8] = {
|
||||
@@ -82,6 +82,68 @@ static void stroke_rect(uint32_t *dst, int x, int y, int w, int h, uint32_t col)
|
||||
fill_rect(dst, x + w - 1, y, 1, h, col);
|
||||
}
|
||||
|
||||
static void fill_disc(uint32_t *dst, int cx, int cy, int r, uint32_t col)
|
||||
{
|
||||
int x, y;
|
||||
const int rr = r * r;
|
||||
for (y = -r; y <= r; y++) {
|
||||
for (x = -r; x <= r; x++) {
|
||||
if (x * x + y * y > rr) continue;
|
||||
fill_rect(dst, cx + x, cy + y, 1, 1, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_line(uint32_t *dst, int x0, int y0, int x1, int y1,
|
||||
int thickness, uint32_t col)
|
||||
{
|
||||
int dx = x1 > x0 ? x1 - x0 : x0 - x1;
|
||||
int sx = x0 < x1 ? 1 : -1;
|
||||
int dy = y1 > y0 ? y0 - y1 : y1 - y0;
|
||||
int sy = y0 < y1 ? 1 : -1;
|
||||
int err = dx + dy;
|
||||
int inset = thickness / 2;
|
||||
|
||||
for (;;) {
|
||||
fill_rect(dst, x0 - inset, y0 - inset, thickness, thickness, col);
|
||||
if (x0 == x1 && y0 == y1)
|
||||
break;
|
||||
{
|
||||
int e2 = err * 2;
|
||||
if (e2 >= dy) {
|
||||
err += dy;
|
||||
x0 += sx;
|
||||
}
|
||||
if (e2 <= dx) {
|
||||
err += dx;
|
||||
y0 += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_psx_button(uint32_t *dst, int x, int y, char kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case 'x':
|
||||
draw_line(dst, x + 4, y + 4, x + 14, y + 14, 2, 0xFF5FA8FFu);
|
||||
draw_line(dst, x + 14, y + 4, x + 4, y + 14, 2, 0xFF5FA8FFu);
|
||||
break;
|
||||
case 's':
|
||||
stroke_rect(dst, x + 3, y + 3, 13, 13, 0xFFFF7EB6u);
|
||||
stroke_rect(dst, x + 4, y + 4, 11, 11, 0xFFFF7EB6u);
|
||||
break;
|
||||
case 'o':
|
||||
fill_disc(dst, x + 9, y + 9, 8, 0xFFFF6B6Bu);
|
||||
fill_disc(dst, x + 9, y + 9, 5, 0xFF171B25u);
|
||||
break;
|
||||
default:
|
||||
fill_rect(dst, x + 7, y + 2, 5, 15, 0xFFD7DCE6u);
|
||||
fill_rect(dst, x + 2, y + 7, 15, 5, 0xFFD7DCE6u);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_char(uint32_t *dst, int x0, int y0, char c,
|
||||
uint32_t col, int scale)
|
||||
{
|
||||
@@ -203,15 +265,27 @@ static void rasterize_panel(void)
|
||||
SSM_THUMB_W, SSM_THUMB_H, 0xFF242A35u);
|
||||
stroke_rect(s_panel, SSM_ROWS_X + 118, y + 3,
|
||||
SSM_THUMB_W, SSM_THUMB_H, 0xFF3A4352u);
|
||||
draw_text(s_panel, SSM_ROWS_X + 180, y + 58, "NEW",
|
||||
draw_text(s_panel, SSM_ROWS_X + 169, y + 46, "NEW",
|
||||
0xFF707887u, 1);
|
||||
}
|
||||
format_slot_status(i, buf, sizeof(buf));
|
||||
draw_text(s_panel, SSM_ROWS_X + 314, y + 50, buf, sub, 1);
|
||||
draw_text(s_panel, SSM_ROWS_X + 278, y + 42, buf, sub, 1);
|
||||
if (sel)
|
||||
draw_text(s_panel, SSM_ROWS_X + SSM_ROWS_W - 88, y + 100,
|
||||
draw_text(s_panel, SSM_ROWS_X + SSM_ROWS_W - 88, y + 82,
|
||||
"SELECT", 0xFFFFD24Du, 1);
|
||||
}
|
||||
|
||||
fill_rect(s_panel, 0, 410, SSM_W, 70, 0xFF171B25u);
|
||||
draw_psx_button(s_panel, 32, 424, 'd');
|
||||
draw_text(s_panel, 56, 428, "SLOT", 0xFFE2E5EBu, 1);
|
||||
draw_psx_button(s_panel, 132, 424, 'x');
|
||||
draw_text(s_panel, 156, 428, "LOAD", 0xFFE2E5EBu, 1);
|
||||
draw_psx_button(s_panel, 230, 424, 's');
|
||||
draw_text(s_panel, 254, 428, "SAVE", 0xFFE2E5EBu, 1);
|
||||
draw_psx_button(s_panel, 328, 424, 'o');
|
||||
draw_text(s_panel, 352, 428, "BACK", 0xFFE2E5EBu, 1);
|
||||
draw_text(s_panel, 32, 454, "KEYS: ARROWS SLOT ENTER/L LOAD SHIFT+ENTER/S SAVE ESC BACK",
|
||||
0xFFB8BDC8u, 1);
|
||||
s_dirty = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Keep CPU text writes separate from explicit overlay admission."""
|
||||
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
def function_body(source: str, signature: str) -> str:
|
||||
start = source.index(signature)
|
||||
brace = source.index("{", start)
|
||||
depth = 0
|
||||
for pos in range(brace, len(source)):
|
||||
if source[pos] == "{":
|
||||
depth += 1
|
||||
elif source[pos] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace : pos + 1]
|
||||
raise AssertionError(f"unterminated function: {signature}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--root", type=Path,
|
||||
default=Path(__file__).resolve().parents[2])
|
||||
root = parser.parse_args().root.resolve()
|
||||
memory = (root / "runtime/src/memory.c").read_text(encoding="utf-8")
|
||||
dma = (root / "runtime/src/dma.c").read_text(encoding="utf-8")
|
||||
|
||||
text_write = function_body(memory, "static inline void text_guard_note_write(")
|
||||
if "text_modified_bitmap" not in text_write:
|
||||
raise AssertionError("CPU text divergence is no longer recorded")
|
||||
for admission in ("dirty_ram_mark_page(", "dirty_ram_mark_executable_range("):
|
||||
if admission in text_write:
|
||||
raise AssertionError(
|
||||
"an ordinary CPU write inside game text automatically admits "
|
||||
f"executable overlay code via {admission}"
|
||||
)
|
||||
|
||||
cd_slice = function_body(dma, "void dma_advance(")
|
||||
read = cd_slice.index("uint32_t word = cdrom_dma_read();")
|
||||
store = cd_slice.index("psx_write_word(addr, word);", read)
|
||||
admit = cd_slice.index("dirty_ram_mark_executable_range(addr, 4);", store)
|
||||
advance = cd_slice.index("addr = (addr + addr_step)", admit)
|
||||
if not read < store < admit < advance:
|
||||
raise AssertionError("CD DMA overlay admission is not coupled to each RAM word")
|
||||
|
||||
mark_range = function_body(memory, "void dirty_ram_mark_executable_range(")
|
||||
for fragment in (
|
||||
"dirty_ram_bitmap[page >> 5] |= (1u << (page & 31u));",
|
||||
"g_dirty_ram_code_gen++;",
|
||||
):
|
||||
if fragment not in mark_range:
|
||||
raise AssertionError(f"explicit executable-range admission lost: {fragment}")
|
||||
|
||||
print("dirty-text admission guards: ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -12,6 +12,8 @@ required = (
|
||||
'std::getenv("PSX_FPS_TELEMETRY")',
|
||||
"fps_telemetry_enabled() && !psx_netplay_in_load_barrier()",
|
||||
"sdl_window && s_fps_base_title.empty()",
|
||||
"Game %.0f fps %.2fx | Display %.0f fps",
|
||||
"Game %.0f FPS %.2fx | Display %.0f FPS",
|
||||
)
|
||||
for needle in required:
|
||||
if needle not in main_cpp:
|
||||
|
||||
@@ -488,7 +488,15 @@ int test_precise_sxy_invalidation() {
|
||||
for (uint8_t reg = 0; reg < 32; ++reg) {
|
||||
gte_test_set_precise_valid_mask(0xFu);
|
||||
gte_write_data(&cpu, reg, 0x12345678u);
|
||||
const uint32_t expected = (reg >= 12 && reg <= 15) ? 0u : 0xFu;
|
||||
/* The PGXP shadow drops exactly the register(s) the guest wrote:
|
||||
* SXY0/SXY1 clear their own slot; SXY2 and SXYP clear both mirrors
|
||||
* (regs 14+15). The untouched slots stay live — their register
|
||||
* values did not change (SXYP's hardware FIFO shift makes 12/13
|
||||
* stale in VALUE, which validate-on-read handles at use; liveness
|
||||
* alone is not a correctness claim). */
|
||||
uint32_t expected = 0xFu;
|
||||
if (reg == 12 || reg == 13) expected &= ~(1u << (reg - 12));
|
||||
else if (reg == 14 || reg == 15) expected &= ~0xCu;
|
||||
const uint32_t actual = gte_test_get_precise_valid_mask();
|
||||
if (actual != expected)
|
||||
return fail_value("precise SXY invalidation", 0, reg,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#include "host_keymap.h"
|
||||
#include "psx_sdl.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
static void check(int cond, const char *what) {
|
||||
if (!cond) {
|
||||
fprintf(stderr, "FAIL: %s\n", what);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
static int mod_ctrl(void) { return (int)KMOD_CTRL; }
|
||||
static int mod_alt(void) { return (int)KMOD_ALT; }
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
uint8_t keys[SDL_NUM_SCANCODES];
|
||||
const char *cfg = "host_keymap_test.ini";
|
||||
FILE *f;
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
memset(keys, 0, sizeof(keys));
|
||||
host_keymap_load(NULL);
|
||||
check(host_keymap_match(HOST_KEYMAP_FULLSCREEN, (int)SDLK_RETURN, mod_alt()),
|
||||
"default fullscreen includes Alt+Return");
|
||||
check(host_keymap_match(HOST_KEYMAP_FULLSCREEN, (int)SDLK_f, mod_ctrl()),
|
||||
"default fullscreen includes Ctrl+F");
|
||||
keys[SDL_SCANCODE_TAB] = 1;
|
||||
check(host_keymap_down(HOST_KEYMAP_TURBO, keys, 0),
|
||||
"default turbo is held by Tab");
|
||||
keys[SDL_SCANCODE_TAB] = 0;
|
||||
check(host_keymap_match(HOST_KEYMAP_VOLUME_UP, (int)SDLK_KP_PLUS, 0),
|
||||
"default volume up is keypad plus");
|
||||
check(host_keymap_match(HOST_KEYMAP_VOLUME_DOWN, (int)SDLK_KP_MINUS, 0),
|
||||
"default volume down is keypad minus");
|
||||
check(host_keymap_match(HOST_KEYMAP_DISPLAY_PERF, (int)SDLK_f, 0),
|
||||
"default display perf is F");
|
||||
|
||||
f = fopen(cfg, "wb");
|
||||
check(f != NULL, "create temporary config.ini");
|
||||
if (!f) return 1;
|
||||
fputs("[KeyMap]\n"
|
||||
"Fullscreen = F11\n"
|
||||
"Turbo = Q\n"
|
||||
"VolumeUp = Up\n"
|
||||
"VolumeDown = Down\n"
|
||||
"DisplayPerf = F10\n",
|
||||
f);
|
||||
fclose(f);
|
||||
|
||||
host_keymap_load(cfg);
|
||||
check(!host_keymap_match(HOST_KEYMAP_FULLSCREEN, (int)SDLK_f, mod_ctrl()),
|
||||
"fullscreen rebind disables Ctrl+F fallback");
|
||||
check(host_keymap_match(HOST_KEYMAP_FULLSCREEN, (int)SDLK_F11, 0),
|
||||
"fullscreen rebind uses F11");
|
||||
memset(keys, 0, sizeof(keys));
|
||||
keys[SDL_SCANCODE_TAB] = 1;
|
||||
check(!host_keymap_down(HOST_KEYMAP_TURBO, keys, 0),
|
||||
"turbo rebind disables Tab fallback");
|
||||
keys[SDL_SCANCODE_TAB] = 0;
|
||||
keys[SDL_SCANCODE_Q] = 1;
|
||||
check(host_keymap_down(HOST_KEYMAP_TURBO, keys, 0),
|
||||
"turbo rebind uses Q");
|
||||
check(host_keymap_match(HOST_KEYMAP_VOLUME_UP, (int)SDLK_UP, 0),
|
||||
"volume up rebind uses Up");
|
||||
check(host_keymap_match(HOST_KEYMAP_VOLUME_DOWN, (int)SDLK_DOWN, 0),
|
||||
"volume down rebind uses Down");
|
||||
check(!host_keymap_match(HOST_KEYMAP_DISPLAY_PERF, (int)SDLK_f, 0),
|
||||
"display perf rebind disables F fallback");
|
||||
check(host_keymap_match(HOST_KEYMAP_DISPLAY_PERF, (int)SDLK_F10, 0),
|
||||
"display perf rebind uses F10");
|
||||
|
||||
remove(cfg);
|
||||
if (failures) {
|
||||
fprintf(stderr, "host_keymap_test: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
printf("PASS: host keymap defaults and rebinds\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "iso_reader.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
static int failures;
|
||||
|
||||
static void check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
static void write_file(const std::filesystem::path& path,
|
||||
const std::vector<uint8_t>& bytes) {
|
||||
std::ofstream out(path, std::ios::binary);
|
||||
out.write(reinterpret_cast<const char*>(bytes.data()),
|
||||
static_cast<std::streamsize>(bytes.size()));
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto root = std::filesystem::current_path() / "iso_reader_sbi_scratch";
|
||||
std::filesystem::remove_all(root);
|
||||
std::filesystem::create_directories(root);
|
||||
const auto image = root / "protected.bin";
|
||||
write_file(image, std::vector<uint8_t>(2352 * 20));
|
||||
|
||||
const std::array<uint8_t, 10> replacement = {
|
||||
0x41, 0x01, 0x01, 0x23, 0x06, 0x05, 0x00, 0x03, 0x08, 0x01};
|
||||
// SBI positions are absolute MSF. 00:02:00 is disc LBA zero after the
|
||||
// standard 150-frame lead-in.
|
||||
std::vector<uint8_t> sbi = {'S', 'B', 'I', 0, 0x00, 0x02, 0x00, 0x01};
|
||||
sbi.insert(sbi.end(), replacement.begin(), replacement.end());
|
||||
write_file(root / "protected.sbi", sbi);
|
||||
|
||||
PS1::ISOReader reader;
|
||||
check(reader.Open(image.string()), "valid same-basename SBI mounts");
|
||||
std::array<uint8_t, 12> subq = {};
|
||||
bool valid = true;
|
||||
check(reader.ReadSubChannelQ(0, subq.data(), &valid),
|
||||
"replacement sub-Q can be read");
|
||||
check(!valid, "SBI replacement is reported with intentionally invalid CRC");
|
||||
check(std::equal(replacement.begin(), replacement.end(), subq.begin()),
|
||||
"SBI replacement payload is preserved");
|
||||
|
||||
check(reader.ReadSubChannelQ(1, subq.data(), &valid),
|
||||
"ordinary sub-Q can be generated");
|
||||
check(valid, "ordinary generated sub-Q has valid CRC");
|
||||
check(subq[0] == 0x41 && subq[1] == 0x01 && subq[2] == 0x01,
|
||||
"generated data-track identity is correct");
|
||||
check(subq[6] == 0x00 && subq[7] == 0x02 && subq[8] == 0x01,
|
||||
"generated absolute position includes the 150-sector lead-in");
|
||||
reader.Close();
|
||||
|
||||
write_file(root / "protected.sbi", {'B', 'A', 'D'});
|
||||
check(!reader.Open(image.string()), "malformed SBI fails the mount closed");
|
||||
|
||||
std::filesystem::remove(root / "protected.sbi");
|
||||
check(reader.Open(image.string()), "image without SBI still mounts");
|
||||
check(!reader.HasSubChannelReplacements(),
|
||||
"image without SBI keeps the compatibility path inactive");
|
||||
check(reader.ReadSubChannelQ(0, subq.data(), &valid) && valid,
|
||||
"image without SBI generates valid sub-Q");
|
||||
reader.Close();
|
||||
std::filesystem::remove_all(root);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -124,8 +124,13 @@ extern "C" uint32_t crc32_compute(const uint8_t *data, size_t size) {
|
||||
}
|
||||
|
||||
int main() {
|
||||
SDL_SetMainReady();
|
||||
if (SDL_Init(0) != 0) {
|
||||
/* psx_sdl_init: 0 on success under BOTH backends. SDL3's SDL.h no longer
|
||||
* pulls SDL_main.h, and its SDL_Init returns bool (true = success), so
|
||||
* the previous SDL_SetMainReady() + `SDL_Init(0) != 0` rotted twice over
|
||||
* when the default backend moved to SDL3: it did not compile, and the
|
||||
* check was inverted. SDL_SetMainReady is unnecessary here — this test
|
||||
* never uses SDL_main. */
|
||||
if (psx_sdl_init(0) != 0) {
|
||||
std::fprintf(stderr, "FAIL: SDL_Init: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/* test_pgxp.cpp — PGXP value-propagation engine unit tests (ENHANCEMENTS.md
|
||||
* G1.2/G1.3). White-box over runtime/src/pgxp.cpp with the gte.cpp fallback
|
||||
* cache stubbed, exercising exactly the properties the engine's safety rests
|
||||
* on: provenance roundtrips, validate-on-read, half-word semantics, the
|
||||
* repack arithmetic, the suppression bracket, and the GPU-side safeguards. */
|
||||
|
||||
#include "pgxp.h"
|
||||
#include "pgxp_hooks.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \
|
||||
#cond); \
|
||||
g_failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* ---- gte.cpp fallback-cache stub ----------------------------------------- */
|
||||
|
||||
static uint32_t g_fb_packed = 0;
|
||||
static int32_t g_fb_x16 = 0, g_fb_y16 = 0;
|
||||
static int g_fb_valid = 0;
|
||||
|
||||
extern "C" int gte_geometry_correction_lookup(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16) {
|
||||
if (!g_fb_valid || packed != g_fb_packed) return 0;
|
||||
if (x16) *x16 = g_fb_x16;
|
||||
if (y16) *y16 = g_fb_y16;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- MIPS encodings ------------------------------------------------------ */
|
||||
|
||||
static uint32_t enc_i(uint32_t op, uint32_t rs, uint32_t rt, uint16_t imm) {
|
||||
return (op << 26) | (rs << 21) | (rt << 16) | imm;
|
||||
}
|
||||
static uint32_t enc_r(uint32_t rs, uint32_t rt, uint32_t rd, uint32_t sh,
|
||||
uint32_t funct) {
|
||||
return (rs << 21) | (rt << 16) | (rd << 11) | (sh << 6) | funct;
|
||||
}
|
||||
static uint32_t enc_cop2(uint32_t sub, uint32_t rt, uint32_t rd) {
|
||||
return (0x12u << 26) | (sub << 21) | (rt << 16) | (rd << 11);
|
||||
}
|
||||
|
||||
#define LW(rs, rt) enc_i(0x23, rs, rt, 0)
|
||||
#define SW(rs, rt) enc_i(0x2B, rs, rt, 0)
|
||||
#define LH(rs, rt) enc_i(0x21, rs, rt, 0)
|
||||
#define LHU(rs, rt) enc_i(0x25, rs, rt, 0)
|
||||
#define SH(rs, rt) enc_i(0x29, rs, rt, 0)
|
||||
#define SB(rs, rt) enc_i(0x28, rs, rt, 0)
|
||||
#define LWC2(rt) enc_i(0x32, 1, rt, 0)
|
||||
#define SWC2(rt) enc_i(0x3A, 1, rt, 0)
|
||||
#define MFC2(rt, rd) enc_cop2(0x00, rt, rd)
|
||||
#define MTC2(rt, rd) enc_cop2(0x04, rt, rd)
|
||||
#define ADDIU(rs, rt, imm) enc_i(0x09, rs, rt, (uint16_t)(imm))
|
||||
#define LUI(rt, imm) enc_i(0x0F, 0, rt, (uint16_t)(imm))
|
||||
#define SLL(rt, rd, sh) enc_r(0, rt, rd, sh, 0x00)
|
||||
#define SRA(rt, rd, sh) enc_r(0, rt, rd, sh, 0x03)
|
||||
#define OR(rs, rt, rd) enc_r(rs, rt, rd, 0, 0x25)
|
||||
#define ADDU(rs, rt, rd) enc_r(rs, rt, rd, 0, 0x21)
|
||||
|
||||
/* One projected vertex: x = 160.5, y = 80.25 -> packed integer word. */
|
||||
static const uint32_t PACKED = (80u << 16) | 160u;
|
||||
static const int32_t X16 = (160 << 16) | 0x8000; /* 160.5 */
|
||||
static const int32_t Y16 = (80 << 16) | 0x4000; /* 80.25 */
|
||||
static const uint16_t SZ3 = 100;
|
||||
|
||||
static const uint32_t ADDR_A = 0x80100000u; /* packet slot A (KSEG0) */
|
||||
static const uint32_t ADDR_B = 0x00100040u; /* packet slot B (KUSEG) */
|
||||
|
||||
static void produce_at(uint32_t addr) {
|
||||
pgxp_gte_push_sxy(X16, Y16, SZ3, PACKED);
|
||||
psx_pgxp_cop2(nullptr, SWC2(14), PACKED, addr);
|
||||
}
|
||||
|
||||
static int lookup(uint32_t addr, uint32_t word, int32_t ix, int32_t iy,
|
||||
int32_t *x, int32_t *y, uint16_t *z) {
|
||||
int32_t lx, ly; uint16_t lz;
|
||||
int r = pgxp_get_precise_vertex(addr, word, ix, iy, &lx, &ly, &lz);
|
||||
if (x) *x = lx;
|
||||
if (y) *y = ly;
|
||||
if (z) *z = lz;
|
||||
return r;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
pgxp_set_enabled(1);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
pgxp_set_cpu_mode(0);
|
||||
|
||||
/* --- SWC2 produce -> GPU consume (the perspective-texturing spine) --- */
|
||||
produce_at(ADDR_A);
|
||||
{
|
||||
int32_t x, y; uint16_t z;
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
||||
CHECK(x == X16 && y == Y16 && z == SZ3);
|
||||
/* mirrors resolve to the same shadow word */
|
||||
CHECK(lookup(0xA0100000u, PACKED, 160, 80, &x, &y, &z) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
}
|
||||
|
||||
/* --- DMA/untracked overwrite: value validation rejects the shadow --- */
|
||||
{
|
||||
int32_t x, y; uint16_t z;
|
||||
uint32_t other = (81u << 16) | 161u;
|
||||
CHECK(lookup(ADDR_A, other, 161, 81, &x, &y, &z) == PGXP_SRC_NATIVE);
|
||||
CHECK(x == (161 << 16) && y == (81 << 16) && z == 0);
|
||||
}
|
||||
|
||||
/* --- LW/SW roundtrip: packet copied by the CPU keeps provenance --- */
|
||||
produce_at(ADDR_A);
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
psx_pgxp_store(nullptr, SW(1, 8), ADDR_B, PACKED);
|
||||
{
|
||||
int32_t x, y; uint16_t z;
|
||||
CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
||||
CHECK(x == X16 && y == Y16 && z == SZ3);
|
||||
}
|
||||
|
||||
/* --- stale GPR: register changed between load and store --- */
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
psx_pgxp_store(nullptr, SW(1, 8), ADDR_B, 0xDEADBEEFu); /* r8 mutated */
|
||||
CHECK(lookup(ADDR_B, 0xDEADBEEFu, 0, 0, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
|
||||
/* --- MOVE idiom (memory mode, no cpu_mode needed) --- */
|
||||
produce_at(ADDR_A);
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
psx_pgxp_alu(nullptr, ADDU(8, 0, 10), PACKED, PACKED, 0);
|
||||
psx_pgxp_store(nullptr, SW(1, 10), ADDR_B, PACKED);
|
||||
CHECK(lookup(ADDR_B, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
|
||||
/* --- MFC2 -> SW (register transfer path) --- */
|
||||
pgxp_gte_push_sxy(X16, Y16, SZ3, PACKED);
|
||||
psx_pgxp_cop2(nullptr, MFC2(9, 14), PACKED, 0);
|
||||
psx_pgxp_store(nullptr, SW(1, 9), ADDR_B, PACKED);
|
||||
CHECK(lookup(ADDR_B, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
|
||||
/* --- LH/SH: halves travel independently, depth does not survive --- */
|
||||
produce_at(ADDR_A);
|
||||
psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16); /* Y */
|
||||
psx_pgxp_store(nullptr, SH(1, 8), ADDR_B + 2u, PACKED >> 16);
|
||||
psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A, PACKED & 0xFFFFu); /* X */
|
||||
psx_pgxp_store(nullptr, SH(1, 8), ADDR_B, PACKED & 0xFFFFu);
|
||||
{
|
||||
int32_t x, y; uint16_t z;
|
||||
CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
||||
CHECK(x == X16 && y == Y16);
|
||||
CHECK(z == 0); /* SH killed the vertex depth */
|
||||
}
|
||||
|
||||
/* --- SB destroys the touched half only --- */
|
||||
produce_at(ADDR_B);
|
||||
psx_pgxp_store(nullptr, SB(1, 8), ADDR_B, PACKED & 0xFFu); /* same byte */
|
||||
{
|
||||
int32_t x, y; uint16_t z;
|
||||
/* low half invalidated -> not a full XY hit anymore */
|
||||
CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, &z) != PGXP_SRC_DATAFLOW);
|
||||
}
|
||||
|
||||
/* --- cpu-mode repack: lhu / sll 16 / or (the classic vertex build) --- */
|
||||
pgxp_set_cpu_mode(1);
|
||||
produce_at(ADDR_A);
|
||||
psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16); /* Y */
|
||||
psx_pgxp_alu(nullptr, SLL(8, 9, 16), (PACKED >> 16) << 16,
|
||||
PACKED >> 16, 16);
|
||||
psx_pgxp_load(nullptr, LHU(1, 10), ADDR_A, PACKED & 0xFFFFu); /* X */
|
||||
psx_pgxp_alu(nullptr, OR(9, 10, 11), PACKED,
|
||||
(PACKED >> 16) << 16, PACKED & 0xFFFFu);
|
||||
psx_pgxp_store(nullptr, SW(1, 11), ADDR_B, PACKED);
|
||||
{
|
||||
int32_t x, y;
|
||||
CHECK(lookup(ADDR_B, PACKED, 160, 80, &x, &y, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
CHECK(x == X16 && y == Y16);
|
||||
}
|
||||
|
||||
/* --- cpu-mode addiu: fraction rides an integer offset (incl. -N) --- */
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
psx_pgxp_alu(nullptr, ADDIU(8, 12, 4), PACKED + 4u, PACKED, 4u);
|
||||
psx_pgxp_store(nullptr, SW(1, 12), ADDR_B, PACKED + 4u);
|
||||
{
|
||||
int32_t x, y;
|
||||
CHECK(lookup(ADDR_B, PACKED + 4u, 164, 80, &x, &y, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
CHECK(x == X16 + (4 << 16) && y == Y16);
|
||||
}
|
||||
psx_pgxp_load(nullptr, LW(1, 8), ADDR_A, PACKED);
|
||||
psx_pgxp_alu(nullptr, ADDIU(8, 12, (uint16_t)-4), PACKED - 4u, PACKED,
|
||||
(uint32_t)(int32_t)-4);
|
||||
psx_pgxp_store(nullptr, SW(1, 12), ADDR_B, PACKED - 4u);
|
||||
{
|
||||
int32_t x;
|
||||
CHECK(lookup(ADDR_B, PACKED - 4u, 156, 80, &x, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
CHECK(x == X16 - (4 << 16));
|
||||
}
|
||||
pgxp_set_cpu_mode(0);
|
||||
|
||||
/* --- cpu-mode OFF: the same repack must degrade to native, cleanly --- */
|
||||
produce_at(ADDR_A);
|
||||
psx_pgxp_load(nullptr, LHU(1, 8), ADDR_A + 2u, PACKED >> 16);
|
||||
psx_pgxp_alu(nullptr, SLL(8, 9, 16), (PACKED >> 16) << 16,
|
||||
PACKED >> 16, 16);
|
||||
psx_pgxp_store(nullptr, SW(1, 9), ADDR_B, (PACKED >> 16) << 16);
|
||||
CHECK(lookup(ADDR_B, (PACKED >> 16) << 16, 0, 80, nullptr, nullptr,
|
||||
nullptr) == PGXP_SRC_NATIVE);
|
||||
|
||||
/* --- truncation agreement: integer part must match the native parse --- */
|
||||
produce_at(ADDR_A);
|
||||
CHECK(lookup(ADDR_A, PACKED, 161, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
|
||||
/* --- tolerance clamp --- */
|
||||
produce_at(ADDR_A);
|
||||
pgxp_set_tolerance(0.25f); /* fraction is 0.5 -> reject */
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
pgxp_set_tolerance(0.75f); /* 0.5 <= 0.75 -> accept */
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
|
||||
/* --- fallback tier: no address -> position cache, never a depth --- */
|
||||
g_fb_valid = 1; g_fb_packed = PACKED; g_fb_x16 = X16; g_fb_y16 = Y16;
|
||||
{
|
||||
int32_t x, y; uint16_t z;
|
||||
CHECK(lookup(0xFFFFFFFFu, PACKED, 160, 80, &x, &y, &z) ==
|
||||
PGXP_SRC_FALLBACK);
|
||||
CHECK(x == X16 && y == Y16 && z == 0);
|
||||
}
|
||||
g_fb_valid = 0;
|
||||
|
||||
/* --- suppression bracket: nothing records inside it --- */
|
||||
pgxp_invalidate_all();
|
||||
pgxp_suppress_begin();
|
||||
produce_at(ADDR_A);
|
||||
pgxp_suppress_end();
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
|
||||
/* --- deferred invalidate inside the bracket --- */
|
||||
produce_at(ADDR_A);
|
||||
pgxp_suppress_begin();
|
||||
pgxp_invalidate_all(); /* deferred */
|
||||
pgxp_suppress_end(); /* applies here */
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
|
||||
/* --- invalidate-all + generation wrap --- */
|
||||
produce_at(ADDR_A);
|
||||
pgxp_invalidate_all();
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
pgxp_test_set_generation(0xFFFFFFFFu);
|
||||
pgxp_invalidate_all();
|
||||
CHECK(pgxp_test_generation() == 1u);
|
||||
|
||||
/* --- test accessors mirror the SXY FIFO shadows --- */
|
||||
pgxp_test_seed_gte_sxy(2, PACKED, X16, Y16, SZ3, 1);
|
||||
{
|
||||
uint32_t packed; int32_t x, y; uint16_t z; uint8_t valid;
|
||||
pgxp_test_get_gte_sxy(2, &packed, &x, &y, &z, &valid);
|
||||
CHECK(valid && packed == PACKED && x == X16 && y == Y16 && z == SZ3);
|
||||
pgxp_test_seed_gte_sxy(2, 0, 0, 0, 0, 0);
|
||||
pgxp_test_get_gte_sxy(2, &packed, &x, &y, &z, &valid);
|
||||
CHECK(!valid);
|
||||
}
|
||||
|
||||
/* --- stats sanity: dataflow hits were counted --- */
|
||||
{
|
||||
PGXPStats st;
|
||||
pgxp_get_stats(&st);
|
||||
CHECK(st.lookups > 0);
|
||||
CHECK(st.dataflow_hit > 0);
|
||||
CHECK(st.native > 0);
|
||||
CHECK(st.fallback_hit > 0);
|
||||
CHECK(st.value_mismatch > 0);
|
||||
}
|
||||
|
||||
if (g_failures) {
|
||||
std::fprintf(stderr, "test_pgxp: %d FAILURES\n", g_failures);
|
||||
return 1;
|
||||
}
|
||||
std::printf("test_pgxp: all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -25,6 +25,8 @@ assert (
|
||||
) not in MAIN
|
||||
assert "HOST_KEYMAP_SAVE_STATE_MENU" in MAIN
|
||||
assert "key >= SDLK_F1 && key <= SDLK_F12" not in MAIN
|
||||
assert "savestate_slot_from_function_key" not in MAIN
|
||||
assert "savestate_submit_slot(f_slot" not in MAIN
|
||||
assert "savestate_input_guard_arm();" in MAIN
|
||||
assert "savestate_input_guard_active()" in MAIN
|
||||
assert "out->buttons = 0xFFFFu;" in MAIN
|
||||
@@ -33,5 +35,9 @@ assert "savestate_menu_ignore_toggle_release" in MAIN
|
||||
assert "savestate_menu_open_key" in MAIN
|
||||
assert "savestate_menu_toggle(key)" in MAIN
|
||||
assert "savestate_menu_toggle(0)" in MAIN
|
||||
assert 'std::getenv("PSX_FAST_FORWARD_SPEED")' in MAIN
|
||||
assert "bool manual_turbo_active = false;" in MAIN
|
||||
assert "Fast forward: %dx" in MAIN
|
||||
assert "g_frame_period_ms / (double)mult" in MAIN
|
||||
|
||||
print("host shortcut guard passed")
|
||||
|
||||
@@ -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})')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# New Project Layout scaffolding (Windows).
|
||||
#
|
||||
# Path params (pass on the CLI — tab-complete in your shell):
|
||||
# Path params (pass on the CLI -- tab-complete in your shell):
|
||||
# -Disc <cue> REQUIRED legal Redump .cue (kept on your dump drive)
|
||||
# -Dir <parent> Parent directory (default: .)
|
||||
# -Bios <SCPH1001.BIN> Optional retail BIOS for Generate
|
||||
@@ -129,7 +129,7 @@ if ($Players -eq 0) {
|
||||
}
|
||||
}
|
||||
if ($Players -lt 1 -or $Players -gt 8) {
|
||||
throw "Players must be 1–8 (got: $Players)"
|
||||
throw "Players must be 1-8 (got: $Players)"
|
||||
}
|
||||
|
||||
$env:PYTHONPATH = "$ScriptDir" + $(if ($env:PYTHONPATH) { ";$env:PYTHONPATH" } else { "" })
|
||||
@@ -171,21 +171,21 @@ $useWizard = $false
|
||||
$useNetplay = $false
|
||||
if (-not $useRecompUi) {
|
||||
if ($EnableWizard -or $EnableNetplay) {
|
||||
Write-Warning "Wizard/netplay require recomp-ui — forcing both off."
|
||||
Write-Warning "Wizard/netplay require recomp-ui -- forcing both off."
|
||||
}
|
||||
Write-Host " (recomp-ui declined — skipping wizard/netplay; PSX_RECOMP_UI=OFF)"
|
||||
Write-Host " (recomp-ui declined -- skipping wizard/netplay; PSX_RECOMP_UI=OFF)"
|
||||
} else {
|
||||
# Default ON (interactive + non-interactive): setup-host CI needs the wizard.
|
||||
$useWizard = Resolve-BoolOpt -Enable:$EnableWizard -No:$NoWizard -PromptDefault:$true `
|
||||
-Question "Enable first-run setup wizard + Generate & rebuild?"
|
||||
if (-not $interactive -and -not $NoWizard) { $useWizard = $true }
|
||||
# 1-player titles cannot use multiplayer netplay — skip prompts entirely.
|
||||
# 1-player titles cannot use multiplayer netplay -- skip prompts entirely.
|
||||
if ($Players -eq 1) {
|
||||
if ($EnableNetplay) {
|
||||
Write-Warning "-EnableNetplay ignored for 1-player title."
|
||||
}
|
||||
$useNetplay = $false
|
||||
Write-Host " (1-player title — netplay skipped)"
|
||||
Write-Host " (1-player title -- netplay skipped)"
|
||||
} else {
|
||||
$useNetplay = Resolve-BoolOpt -Enable:$EnableNetplay -No:$NoNetplay -PromptDefault:$false `
|
||||
-Question "Enable netplay UI (needs nested recomp-net)?"
|
||||
@@ -196,7 +196,7 @@ $NetplayLobbyWs = ""
|
||||
if ($useNetplay) {
|
||||
if (-not $LobbyUrl) {
|
||||
if ($interactive) {
|
||||
$LobbyUrl = Prompt-Line "Netplay lobby server URL (host or ws://…)" $DefaultLobbyHost
|
||||
$LobbyUrl = Prompt-Line "Netplay lobby server URL (host or ws://...)" $DefaultLobbyHost
|
||||
} else {
|
||||
$LobbyUrl = $DefaultLobbyHost
|
||||
}
|
||||
@@ -216,7 +216,7 @@ if ($doGenerate) {
|
||||
$doBuild = Resolve-BoolOpt -Enable:$EnableBuild -No:$NoBuild -PromptDefault:$true `
|
||||
-Question "Configure & build psx-runtime after Generate?"
|
||||
} elseif ($EnableBuild) {
|
||||
Write-Warning "-EnableBuild requires Generate — enabling Generate"
|
||||
Write-Warning "-EnableBuild requires Generate -- enabling Generate"
|
||||
$doGenerate = $true
|
||||
$doBuild = $true
|
||||
}
|
||||
@@ -242,8 +242,8 @@ $GameName = $GameName.Trim()
|
||||
if (-not $GameName) { $GameName = $Name }
|
||||
$DiscHint = "your legally owned $GameName disc"
|
||||
$EntryPc = "0x80010000"
|
||||
$PublisherDisp = if ($Publisher) { $Publisher } else { "—" }
|
||||
$YearDisp = if ($Year) { $Year } else { "—" }
|
||||
$PublisherDisp = if ($Publisher) { $Publisher } else { "-" }
|
||||
$YearDisp = if ($Year) { $Year } else { "-" }
|
||||
$DescriptionMd = if ($Description) { $Description } else { "_Add a short pitch in catalog_identity.json / README._" }
|
||||
|
||||
$Root = Join-Path (Resolve-Path $Dir) $Name
|
||||
@@ -263,7 +263,7 @@ if ($useRecompUi) {
|
||||
set(PSX_RECOMP_UI OFF CACHE BOOL
|
||||
"No recomp-ui submodule in this scaffold" FORCE)
|
||||
"@
|
||||
Set-Content -Encoding UTF8 -Path $CodegenArgFile -Value " # CODEGEN_SETUP_SOURCES — add with recomp-ui / wizard later"
|
||||
Set-Content -Encoding UTF8 -Path $CodegenArgFile -Value " # CODEGEN_SETUP_SOURCES -- add with recomp-ui / wizard later"
|
||||
}
|
||||
|
||||
if ($useNetplay) {
|
||||
@@ -279,9 +279,9 @@ endif()
|
||||
$NetplayLobbyUrlArg = " NETPLAY_LOBBY_URL `"$NetplayLobbyWs`""
|
||||
} else {
|
||||
Set-Content -Encoding UTF8 -Path $NetplayBlockFile -Value @"
|
||||
# Full netplay UI — enable after adding recomp-ui + testing:
|
||||
# Full netplay UI -- enable after adding recomp-ui + testing:
|
||||
# if(EXISTS "`${PSXRECOMP_ROOT}/lib/recomp-net/CMakeLists.txt")
|
||||
# set(PSX_NETPLAY ON CACHE BOOL "…" FORCE)
|
||||
# set(PSX_NETPLAY ON CACHE BOOL "..." FORCE)
|
||||
# endif()
|
||||
"@
|
||||
$NetplayRuntimeArg = " # ENABLE_NETPLAY_IF_PRESENT"
|
||||
@@ -296,8 +296,8 @@ set(PSX_SETUP_WIZARD ON CACHE BOOL
|
||||
$WizardRuntimeArg = " ENABLE_SETUP_WIZARD"
|
||||
} else {
|
||||
Set-Content -Encoding UTF8 -Path $WizardBlockFile -Value @"
|
||||
# First-run setup wizard — enable after testing:
|
||||
# set(PSX_SETUP_WIZARD ON CACHE BOOL "…" FORCE)
|
||||
# First-run setup wizard -- enable after testing:
|
||||
# set(PSX_SETUP_WIZARD ON CACHE BOOL "..." FORCE)
|
||||
"@
|
||||
$WizardRuntimeArg = " # ENABLE_SETUP_WIZARD"
|
||||
}
|
||||
@@ -370,7 +370,7 @@ New-Item -ItemType Directory -Force -Path (Join-Path $Root "scripts") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root "tools") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root "mods\preloaded\packages") | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Root "assets") | Out-Null
|
||||
# Empty mod catalog tree (runtime copies mods/preloaded → beside the exe as mods/).
|
||||
# Empty mod catalog tree (runtime copies mods/preloaded -> beside the exe as mods/).
|
||||
@'
|
||||
# Preloaded mods
|
||||
|
||||
@@ -379,7 +379,7 @@ Ship reviewed, default-disabled packages here:
|
||||
```text
|
||||
packages/<package-id>/<version>/
|
||||
manifest.toml
|
||||
…
|
||||
...
|
||||
```
|
||||
|
||||
Build wiring copies `mods/preloaded` next to the game executable as `mods/`.
|
||||
@@ -456,7 +456,7 @@ if ($useCi) {
|
||||
--set "WINDOW_TITLE=$WindowTitle"
|
||||
Write-Host " wrote .github/workflows/release.yml"
|
||||
} else {
|
||||
Write-Warning "Missing $WfSrc — skipped CI workflow copy"
|
||||
Write-Warning "Missing $WfSrc -- skipped CI workflow copy"
|
||||
}
|
||||
} else {
|
||||
Write-Host "== CI workflow skipped =="
|
||||
@@ -494,7 +494,7 @@ for m in re.finditer(r'FILE\s+"([^"]+)"', text, re.I):
|
||||
print(f" copied track: {src.name}")
|
||||
else:
|
||||
print(f" warning: cue FILE missing: {src}", file=sys.stderr)
|
||||
print(" disc staged under disc/ (gitignored — never commit dumps)")
|
||||
print(" disc staged under disc/ (gitignored -- never commit dumps)")
|
||||
'@
|
||||
$pyFile = Join-Path $env:TEMP "psxrecomp_stage_disc.py"
|
||||
Set-Content -Encoding UTF8 -Path $pyFile -Value $py
|
||||
@@ -546,19 +546,19 @@ print(m.group(1) if m else '')
|
||||
$BootExe = $BootFromToml
|
||||
Fill-Template (Join-Path $TemplateDir "CMakeLists.txt.in") (Join-Path $Root "CMakeLists.txt")
|
||||
Fill-Template (Join-Path $TemplateDir "codegen_setup.c.in") (Join-Path $Root "codegen_setup.c")
|
||||
Write-Host " synced boot EXE → $BootExe (CMake / codegen)"
|
||||
Write-Host " synced boot EXE -> $BootExe (CMake / codegen)"
|
||||
}
|
||||
if ($EntryFromToml) {
|
||||
$EntryPc = $EntryFromToml
|
||||
Fill-Template (Join-Path $TemplateDir "symbols.toml.in") (Join-Path $Root "symbols.toml")
|
||||
Write-Host " symbols.toml BootEntry → $EntryPc"
|
||||
Write-Host " symbols.toml BootEntry -> $EntryPc"
|
||||
}
|
||||
if (-not $StageDisc) {
|
||||
Write-Host " game.toml disc → $DiscTomlPath"
|
||||
Write-Host " (local absolute path — do not commit machine-specific dumps)"
|
||||
Write-Host " game.toml disc -> $DiscTomlPath"
|
||||
Write-Host " (local absolute path -- do not commit machine-specific dumps)"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Disc probe failed — left template game.toml; fill by hand."
|
||||
Write-Warning "Disc probe failed -- left template game.toml; fill by hand."
|
||||
}
|
||||
} else {
|
||||
Write-Warning "-Disc is not a .cue; skipped probe autofill."
|
||||
@@ -589,17 +589,17 @@ try {
|
||||
commit -q -m "Initial New Project Layout scaffold"
|
||||
$committed = $true
|
||||
} catch {
|
||||
Write-Host " (skipped initial commit — commit manually when ready)"
|
||||
Write-Host " (skipped initial commit -- commit manually when ready)"
|
||||
}
|
||||
|
||||
# Create the GitHub repo + origin now; push deferred until after generate/build.
|
||||
$githubCreated = $false
|
||||
if ($createGithub) {
|
||||
Write-Host "== GitHub repo (gh, create only — push deferred) =="
|
||||
Write-Host "== GitHub repo (gh, create only -- push deferred) =="
|
||||
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
|
||||
Write-Warning "gh not installed — skip create; push manually later."
|
||||
Write-Warning "gh not installed -- skip create; push manually later."
|
||||
} elseif (-not $committed) {
|
||||
Write-Warning "no initial commit — skip gh repo create."
|
||||
Write-Warning "no initial commit -- skip gh repo create."
|
||||
} else {
|
||||
$vis = switch ($GithubVisibility) {
|
||||
"public" { "--public" }
|
||||
@@ -611,13 +611,13 @@ if ($createGithub) {
|
||||
Write-Host " created origin ($GithubVisibility); push deferred until end"
|
||||
$githubCreated = $true
|
||||
} catch {
|
||||
Write-Warning "gh repo create failed — if the repo already exists, add origin and push at the end."
|
||||
Write-Warning "gh repo create failed -- if the repo already exists, add origin and push at the end."
|
||||
if (-not (git remote get-url origin 2>$null)) {
|
||||
try {
|
||||
$ghUrl = (gh repo view $Name --json url -q .url 2>$null)
|
||||
if ($ghUrl) {
|
||||
git remote add origin $ghUrl
|
||||
Write-Host " attached existing origin → $ghUrl"
|
||||
Write-Host " attached existing origin -> $ghUrl"
|
||||
$githubCreated = $true
|
||||
}
|
||||
} catch {}
|
||||
@@ -647,9 +647,9 @@ if ($doGenerate) {
|
||||
}
|
||||
python @genArgs
|
||||
$GeneratedOk = $true
|
||||
Write-Host " generate OK (generated/ is gitignored — not committed)"
|
||||
Write-Host " generate OK (generated/ is gitignored -- not committed)"
|
||||
} catch {
|
||||
Write-Warning "Generate failed — fix seeds/disc and re-run generate by hand."
|
||||
Write-Warning "Generate failed -- fix seeds/disc and re-run generate by hand."
|
||||
$doBuild = $false
|
||||
} finally {
|
||||
Pop-Location
|
||||
@@ -663,9 +663,9 @@ if ($doBuild -and $GeneratedOk) {
|
||||
cmake -S . -B build-release -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build-release --target psx-runtime
|
||||
$BuildOk = $true
|
||||
Write-Host " build OK → build-release/ (gitignored)"
|
||||
Write-Host " build OK -> build-release/ (gitignored)"
|
||||
} catch {
|
||||
Write-Warning "Build failed — fix and re-run cmake by hand."
|
||||
Write-Warning "Build failed -- fix and re-run cmake by hand."
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -700,7 +700,7 @@ function Ensure-ActionsRegistersReleaseYml([string]$OwnerRepo) {
|
||||
if ($i -lt 4) { Start-Sleep -Seconds 2 }
|
||||
}
|
||||
|
||||
Write-Host " Actions has not listed release.yml yet — nudging with a follow-up commit…"
|
||||
Write-Host " Actions has not listed release.yml yet -- nudging with a follow-up commit..."
|
||||
$stamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mmZ")
|
||||
Add-Content -Encoding utf8 -Path $wfPath -Value "`n# Actions registration nudge ($stamp UTC)"
|
||||
git add $wfPath 2>$null
|
||||
@@ -732,7 +732,7 @@ if ($createGithub -and (git remote get-url origin 2>$null)) {
|
||||
try {
|
||||
git push -u origin HEAD
|
||||
$githubPushed = $true
|
||||
Write-Host " pushed $(git rev-parse --abbrev-ref HEAD) → origin"
|
||||
Write-Host " pushed $(git rev-parse --abbrev-ref HEAD) -> origin"
|
||||
if ($useCi -and (Test-Path (Join-Path $Root ".github\workflows\release.yml"))) {
|
||||
try {
|
||||
$ownerRepo = (gh repo view --json nameWithOwner -q .nameWithOwner 2>$null)
|
||||
@@ -740,7 +740,7 @@ if ($createGithub -and (git remote get-url origin 2>$null)) {
|
||||
} catch {}
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "git push failed — fetch/rebase or force-push manually if origin already has a different initial commit."
|
||||
Write-Warning "git push failed -- fetch/rebase or force-push manually if origin already has a different initial commit."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -751,7 +751,7 @@ $Step1 = if ($Probed) {
|
||||
"Edit game.toml / re-run probe_disc.py --write-seeds."
|
||||
}
|
||||
$Step2 = if ($BuildOk) {
|
||||
"Runtime already built under build-release/ — soak offline boot."
|
||||
"Runtime already built under build-release/ -- soak offline boot."
|
||||
} elseif ($GeneratedOk) {
|
||||
@"
|
||||
Build playable runtime:
|
||||
@@ -771,15 +771,15 @@ Build emitters, Generate, then playable runtime:
|
||||
}
|
||||
$CiNote = if ($useCi) {
|
||||
$n = "CI: .github/workflows/release.yml ready (zip prefix=$ZipPrefix; submodule gitlinks pin the build)."
|
||||
if ($githubPushed) { $n += " Pushed — open Actions → Release builds (workflow_dispatch)." }
|
||||
if ($githubPushed) { $n += " Pushed -- open Actions -> Release builds (workflow_dispatch)." }
|
||||
$n
|
||||
} else {
|
||||
"CI workflow not installed (declined)."
|
||||
}
|
||||
$GhNote = if ($createGithub) {
|
||||
if ($githubPushed) { "GitHub: created/attached origin and pushed ($GithubVisibility)." }
|
||||
elseif ($githubCreated) { "GitHub: origin ready but push failed — see warnings above." }
|
||||
else { "GitHub: create/push did not complete — check gh auth / remote." }
|
||||
elseif ($githubCreated) { "GitHub: origin ready but push failed -- see warnings above." }
|
||||
else { "GitHub: create/push did not complete -- check gh auth / remote." }
|
||||
} else {
|
||||
"GitHub remote not created (declined)."
|
||||
}
|
||||
@@ -791,11 +791,11 @@ Write-Host @"
|
||||
Next steps:
|
||||
1. $Step1
|
||||
2. $Step2
|
||||
3. Label symbols in symbols.toml → python tools/sync_symbols.py
|
||||
4. Soak offline boot → netplay QA (if enabled) → tag vX.Y.Z.
|
||||
3. Label symbols in symbols.toml -> python tools/sync_symbols.py
|
||||
4. Soak offline boot -> netplay QA (if enabled) -> tag vX.Y.Z.
|
||||
5. $CiNote
|
||||
Pins: submodule gitlinks (framework_pins.txt is an optional snapshot)
|
||||
6. $GhNote
|
||||
|
||||
Docs: psxrecomp\docs\GAME_PROJECT_SETUP.md · psxrecomp\docs\SYMBOLS.md
|
||||
Docs: psxrecomp\docs\GAME_PROJECT_SETUP.md ; psxrecomp\docs\SYMBOLS.md
|
||||
"@
|
||||
|
||||
Reference in New Issue
Block a user