Support native-wide presentation diagnostics and verified external mod media

Checkpoint exact NCLIP and cull-scale code generation, overlay ABI 25, draw-area clipping, timeline and presented-image rings, mod counters, hotkey matching and savestate messages. Add canonical external ROM/disc verification with immutable plugin-scoped snapshots. Focused recompiler/runtime validation: 16 tests passed.
This commit is contained in:
Matthew Stanley
2026-09-29 20:57:11 -07:00
parent 7b2962efb5
commit b210446460
50 changed files with 2243 additions and 125 deletions
+51
View File
@@ -742,6 +742,57 @@ they are not copied into the package. Optional resources with no selected path
are omitted from the committed plan. Required resources reject launch while the
feature is enabled and unset.
Format 8 adds engine-verified donor media to the same picker and state format:
```toml
[[resource]]
feature = "arena"
id = "donor-rom"
label = "Source ROM"
format = "n64-rom"
file_patterns = "*.z64,*.v64,*.n64"
required = true
size = 8388608
sha256 = "<64 lowercase hexadecimal digits from the canonical image>"
```
`size` and `sha256` must both be present. Verification runs only for enabled
features. Missing required media, a removed selected file, an incorrect size,
or a hash mismatch rejects the launch plan before plugin activation. Disabling
the feature restores the stock launch; the selected path is preserved.
Canonical identity domains:
| `format` | Size and SHA-256 domain |
|---|---|
| `file` with identity fields | Exact file bytes, at most 512 MiB |
| `n64-rom` | Big-endian `.z64` bytes; `.v64` halfword swaps and `.n64` word swaps normalize first; 64 bytes to 64 MiB, word aligned |
| `psx-disc` | First data track's 2048-byte sector payloads, at most 512 MiB; CUE/BIN/ISO/CHD use the shared disc reader; audio tracks are excluded |
For `psx-disc`, Mode 1 payloads start at raw-sector offset 16 and Mode 2
payloads at 24. This domain retains the first 2048 bytes of Form 2 sectors;
it is intended for asset data, and is not a full XA/CD-audio identity. Mixed-mode
retail ISO headers can declare a volume size extending into audio tracks; the
reader uses the actual first-track boundary from the TOC or single-track image
length. An audio-first disc or a nonzero first-track start is rejected. Use a
CUE for a raw image containing multiple tracks. Declared volume metadata must
have matching byte orders and 2048-byte logical sectors.
The resolved plan owns an immutable canonical snapshot. During its callbacks,
a trusted plugin calls `psx_mod_current_resource_bytes(id, &bytes, &size)` to
obtain a read-only view scoped to its own package and feature. Ordinary
unverified resources cannot supply bytes through this API. The pointer remains
valid until the committed plan is replaced or cleared. Decode into host or
enhancement memory and rebuild derived data when activation changes; never
reopen the owner path as a substitute for the verified snapshot.
Verified resource fingerprints include format, canonical size and SHA-256,
rather than the selected path, so moving identical media or changing N64 byte
order does not change save compatibility. Donor bytes are never package files,
and donor executable code is not dispatched by this API. N64 ZIPs must be
extracted before selecting the ROM. Format 8 prevents older runtimes from
silently accepting manifests whose identity checks they cannot enforce.
`resolver = "builtin:<id>"` selects a resolver statically registered by the
game. Format-5 plugin ids likewise select only statically registered
implementations. Packages cannot load arbitrary native code or select
+10
View File
@@ -213,6 +213,16 @@ on a fixed region -> next.
## 5. Status / Log (update every session)
- **2026-09-29 (Phase 5 enhancement media):** Added format-8 verified donor
resources (exact canonical size/SHA-256, immutable snapshots scoped to the
owning plugin). N64 byte orders normalize before hashing; PSX identity covers
the first data track using the existing disc reader. Mixed-mode retail volume
sizes include audio and are not data-track lengths. Framework media/package/
runtime tests pass; V8's isolated headless importer validates Super Dreamland
64 from owner media. No CPU execution or timing changes. Arena conversion and
gameplay integration remain open. Local uncommitted work tracked by
`beads-09ww` and `beads-ip30`; content epic `beads-eio.5.13` remains open.
- **2026-09-13 (SIO card hack removal — branch-only review checkpoint):**
Reproduced fixed-Ape-RAM IRQ7/mask injection after an absent-card probe,
plus SELECT-time ACK fabrication and INTC-pending ACK requeueing with the
+11
View File
@@ -0,0 +1,11 @@
import json,sys
d=json.load(open(sys.argv[1]))
addrs=[int(x,16) for x in sys.argv[2:]]
funcs=d['functions']
funcs.sort(key=lambda f:f['addr'])
import bisect
starts=[f['addr'] for f in funcs]
for a in addrs:
i=bisect.bisect_right(starts,a)-1
f=funcs[i]
print(hex(a), '->', f['name'], hex(f['addr']), hex(f['end']), f.get('confidence'))
+2 -1
View File
@@ -840,7 +840,8 @@ if(BUILD_TESTING)
vk_mailbox_policy
vk_present_timing
vk_present_wait_stage
vk_staging_cache)
vk_staging_cache
wide_overlay_band_scissor)
add_test(
NAME ${_t}
COMMAND ${Python3_EXECUTABLE}
+1
View File
@@ -161,6 +161,7 @@ struct CodeGenConfig {
// 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;
std::vector<PSXRecompV4::WidescreenCullScaleSite> ws_cull_scale_sites;
// Exact `addi[u] rt,zero,imm` 12-bit angular half-extents. The runtime
// scales tan(angle) by the current horizontal reveal factor.
+59 -6
View File
@@ -771,6 +771,20 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
return fmt::format("psx_ws_x_margin() > 0 ? 0 : ({}) /* ws branch keep */",
cond);
};
// NCLIP/backface sign branches (bltz/bgez/blez/bgtz on a MAC0 copy).
// nclip_keep: reject forced not-taken while wide. nclip_exact: test the
// exact tracked sign while wide. Every sign-test shape is supported: the
// title's emitter decides which one its backface reject uses. Returns ""
// when addr is not a configured NCLIP site.
auto nclip_sign_branch = [&](const char *cmp) -> std::string {
if (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), cmp);
if (config_.ws_cull_nclip_exact_sites.count(addr))
return fmt::format("psx_ws_x_margin() > 0 ? (gte_nclip_exact_sign((int32_t){}, 0x{:08X}u) {} 0) : ((int32_t){} {} 0) /* ws exact nclip */",
reg_name(rs), addr, cmp, reg_name(rs), cmp);
return "";
};
// REGIMM branches. R3000A hardware decodes EVERY rt value here, not just
// the four assembler mnemonics: the branch sense is rt bit 0 (0 = bltz,
@@ -792,15 +806,17 @@ 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));
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));
if (regimm_op == 0x00 && config_.ws_cull_nclip_exact_sites.count(addr))
return fmt::format("psx_ws_x_margin() > 0 ? gte_nclip_precise_bltz((int32_t){}) : ((int32_t){} < 0) /* ws exact nclip */",
reg_name(rs), reg_name(rs));
if (regimm_op == 0x00) {
const std::string n = nclip_sign_branch("<");
if (!n.empty()) return n;
}
return keep_branch_if_wide(
fmt::format("(int32_t){} < 0", reg_name(rs)));
} else { // bgez family (incl. bgezal + undefined mirrors)
if (regimm_op == 0x01) {
const std::string n = nclip_sign_branch(">=");
if (!n.empty()) return n;
}
return keep_branch_if_wide(
fmt::format("(int32_t){} >= 0", reg_name(rs)));
}
@@ -820,11 +836,15 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
case 0x06: // blez
case 0x16: // blezl
if (const std::string n = nclip_sign_branch("<="); !n.empty())
return n;
return keep_branch_if_wide(
fmt::format("(int32_t){} <= 0", reg_name(rs)));
case 0x07: // bgtz
case 0x17: // bgtzl
if (const std::string n = nclip_sign_branch(">"); !n.empty())
return n;
return keep_branch_if_wide(
fmt::format("(int32_t){} > 0", reg_name(rs)));
}
@@ -1001,6 +1021,38 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
reg_name(dst), vanilla, site.result, comment);
}
// Register-register frustum-edge compares ([[widescreen.cull.scale]]):
// the configured operand is a +/-half_extent*z bound computed at runtime,
// so it is scaled by the live reveal instead of rewriting an immediate.
// psx_ws_cull_scale is identity at 4:3. Overlay variants with another
// instruction at the same VA are left untouched.
for (const auto& site : config_.ws_cull_scale_sites) {
if ((site.address & 0x1FFFFFFFu) != (addr & 0x1FFFFFFFu)) continue;
if (instr != site.expected) {
if (config_.overlay_mode) continue;
fmt::print(stderr,
"ERROR: cull scale expected 0x{:08X} at 0x{:08X}, found 0x{:08X}\n",
site.expected, addr, instr);
std::exit(1);
}
const uint32_t rs = get_rs(instr), rt = get_rt(instr), rd = get_rd(instr);
const std::string a = site.operand == 0
? fmt::format("(uint32_t)psx_ws_cull_scale((int32_t){}, {})",
reg_name(rs), site.half_extent)
: std::string(reg_name(rs));
const std::string b = site.operand == 1
? fmt::format("(uint32_t)psx_ws_cull_scale((int32_t){}, {})",
reg_name(rt), site.half_extent)
: std::string(reg_name(rt));
if (funct == 0x2A)
return fmt::format("{} = ((int32_t){} < (int32_t){}) ? 1u : 0u;"
" /* ws cull scale (bound x (half+margin)/half) */{}",
reg_name(rd), a, b, comment);
return fmt::format("{} = ({} < {}) ? 1u : 0u;"
" /* ws cull scale (bound x (half+margin)/half) */{}",
reg_name(rd), a, b, comment);
}
// Widescreen automatic far-backdrop column PRELOAD ([widescreen.cull]
// auto_backdrop). At a detected window's START/END finalize, route the value
// through psx_ws_backdrop_value(orig, is_end): identity at 4:3, but in
@@ -3336,6 +3388,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
ss << "extern int32_t psx_ws_plane_nx(int32_t nx); /* ws side-plane normal-X scale (gpu.c) */\n";
ss << "extern uint32_t psx_ws_xclip_bound(uint32_t vanilla); /* ws per-prim X reject bound load (gpu.c) */\n";
ss << "extern uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced); /* ws guarded keep compare (gpu.c) */\n";
ss << "extern int32_t psx_ws_cull_scale(int32_t bound, int32_t half_extent); /* ws frustum-edge bound scale (gpu.c) */\n";
ss << "extern uint32_t psx_ws_aspect_cone_result(uint32_t site, uint32_t vanilla, uint32_t object, int32_t x, int32_t z, int32_t y); /* aspect-aware participation cone */\n";
ss << "extern uint32_t psx_ws_angle_widen(uint32_t vanilla); /* aspect-scaled 12-bit terrain-frustum half-angle */\n";
ss << "extern int psx_ws_backdrop_x(int x); /* widescreen backdrop screenX squash (gpu.c) */\n";
+46
View File
@@ -118,6 +118,18 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
h.u32(site.result);
}
std::vector<WidescreenCullScaleSite> scale_sites = c.ws_cull_scale_sites;
std::sort(scale_sites.begin(), scale_sites.end(),
[](const auto& a, const auto& b) { return a.address < b.address; });
h.tag("cull_scale");
h.u32((uint32_t)scale_sites.size());
for (const auto& site : scale_sites) {
h.u32(site.address);
h.u32(site.expected);
h.u32(site.operand);
h.u32(site.half_extent);
}
std::vector<WidescreenAngleSite> angle_sites = c.ws_cull_angle_sites;
std::sort(angle_sites.begin(), angle_sites.end(),
[](const auto& a, const auto& b) {
@@ -1723,6 +1735,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
std::vector<uint32_t> ws_cull_nclip_exact_sites;
std::vector<uint32_t> ws_cull_branch_keep_sites;
std::vector<WidescreenCullKeepSite> ws_cull_keep_sites;
std::vector<WidescreenCullScaleSite> ws_cull_scale_sites;
std::vector<WidescreenAngleSite> ws_cull_angle_sites;
WidescreenAspectConeConfig ws_aspect_cone;
int ws_cull_guard_pixels = 0;
@@ -1789,6 +1802,38 @@ GameConfig load_game_config(const fs::path& config_path_in) {
ws_cull_keep_sites.push_back(site);
}
}
if (cull.contains("scale")) {
std::set<uint32_t> seen;
for (const auto& item : toml::find<toml::array>(cull, "scale")) {
WidescreenCullScaleSite site;
site.address = parse_hex(toml::find<std::string>(item, "address"),
"widescreen.cull.scale.address");
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
"widescreen.cull.scale.expected");
const std::string operand = toml::find<std::string>(item, "operand");
if (operand == "rs") site.operand = 0;
else if (operand == "rt") site.operand = 1;
else throw std::runtime_error(fmt::format(
"{}: [[widescreen.cull.scale]] operand must be rs or rt",
config_path.string()));
const int half = toml::find<int>(item, "half_extent");
if (half <= 0 || half > 1024) throw std::runtime_error(fmt::format(
"{}: [[widescreen.cull.scale]] half_extent must be 1..1024",
config_path.string()));
site.half_extent = (uint32_t)half;
const uint32_t op = site.expected >> 26;
const uint32_t fn = site.expected & 0x3Fu;
if (!(op == 0u && (fn == 0x2Au || fn == 0x2Bu)))
throw std::runtime_error(fmt::format(
"{}: [[widescreen.cull.scale]] expected must be SLT/SLTU",
config_path.string()));
if (!seen.insert(site.address & 0x1FFFFFFFu).second)
throw std::runtime_error(fmt::format(
"{}: duplicate [[widescreen.cull.scale]] address 0x{:08X}",
config_path.string(), site.address));
ws_cull_scale_sites.push_back(site);
}
}
if (cull.contains("angle")) {
std::set<uint32_t> seen;
for (const auto& item :
@@ -2190,6 +2235,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_cull_nclip_exact_sites*/ ws_cull_nclip_exact_sites,
/*ws_cull_branch_keep_sites*/ ws_cull_branch_keep_sites,
/*ws_cull_keep_sites*/ ws_cull_keep_sites,
/*ws_cull_scale_sites*/ ws_cull_scale_sites,
/*ws_cull_angle_sites*/ ws_cull_angle_sites,
/*ws_aspect_cone*/ ws_aspect_cone,
/*ws_cull_guard_pixels*/ ws_cull_guard_pixels,
+15
View File
@@ -103,6 +103,18 @@ struct WidescreenCullKeepSite {
// constant with `addi[u] rt,zero,imm`; the runtime widens tan(angle) by the
// live horizontal reveal factor. Full-word guards prevent overlay-address
// aliases from changing unrelated immediates.
// One register-register view-bound compare whose bound operand is widened
// with the live reveal: `slt rd, rs, rt` where the configured operand holds
// +/-half_extent*z (a camera-space frustum edge computed at runtime, so there
// is no immediate to rewrite). The operand is scaled by
// (half_extent + margin) / half_extent; identity at 4:3. Full-word guarded.
struct WidescreenCullScaleSite {
uint32_t address = 0;
uint32_t expected = 0; // guarded SLT/SLTU instruction
uint32_t operand = 1; // 0 = scale rs, 1 = scale rt
uint32_t half_extent = 0; // authored screen half-extent (e.g. 160)
};
struct WidescreenAngleSite {
uint32_t address = 0;
uint32_t expected = 0; // guarded ADDI/ADDIU with rs == zero
@@ -956,6 +968,9 @@ struct GameConfig {
// where maximal overdraw is preferable to range guessing. Each entry is
// guarded by the complete MIPS word; 4:3 executes the vanilla comparison.
std::vector<WidescreenCullKeepSite> ws_cull_keep_sites;
// Register-register frustum-edge compares whose bound operand scales
// with the live reveal ([[widescreen.cull.scale]]).
std::vector<WidescreenCullScaleSite> ws_cull_scale_sites;
// Exact 12-bit angular half-extents used by terrain-cell frusta.
std::vector<WidescreenAngleSite> ws_cull_angle_sites;
// Full-word-guarded model-participation cosine compares widened only in
+4
View File
@@ -213,6 +213,7 @@ int main(int argc, char** argv) {
std::set<uint32_t> ws_cull_nclip_exact; // [widescreen.cull] nclip_exact_sites
std::set<uint32_t> ws_cull_branch_keep; // [widescreen.cull] branch_keep_sites
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
std::vector<PSXRecompV4::WidescreenCullScaleSite> ws_cull_scale;
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle;
PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone;
int ws_cull_activation_guard_pixels = 0;
@@ -277,6 +278,7 @@ int main(int argc, char** argv) {
ws_cull_nclip_exact.insert(cfg.ws_cull_nclip_exact_sites.begin(), cfg.ws_cull_nclip_exact_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_scale = cfg.ws_cull_scale_sites;
ws_cull_angle = cfg.ws_cull_angle_sites;
ws_aspect_cone = cfg.ws_aspect_cone;
ws_cull_activation_guard_pixels =
@@ -373,6 +375,7 @@ int main(int argc, char** argv) {
ws_cull_nclip_exact.insert(wscfg.ws_cull_nclip_exact_sites.begin(), wscfg.ws_cull_nclip_exact_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_scale.empty()) ws_cull_scale = wscfg.ws_cull_scale_sites;
if (ws_cull_angle.empty()) ws_cull_angle = wscfg.ws_cull_angle_sites;
if (ws_aspect_cone.sites.empty())
ws_aspect_cone = wscfg.ws_aspect_cone;
@@ -1245,6 +1248,7 @@ int main(int argc, char** argv) {
codegen_config.ws_cull_nclip_exact_sites = ws_cull_nclip_exact;
codegen_config.ws_cull_branch_keep_sites = ws_cull_branch_keep;
codegen_config.ws_cull_keep_sites = ws_cull_keep;
codegen_config.ws_cull_scale_sites = ws_cull_scale;
codegen_config.ws_cull_angle_sites = ws_cull_angle;
codegen_config.ws_aspect_cone = ws_aspect_cone;
codegen_config.ws_cull_w_imms = ws_cull_w_imms;
+65 -1
View File
@@ -311,6 +311,28 @@ result = 0
keep_config.ws_cull_keep_sites[1].result == 0u,
"parser preserves full-word-guarded maximal-participation sites");
const auto scale = write_config(root, "cull-scale", R"toml(
[[widescreen.cull.scale]]
address = "0x8002C734"
expected = "0x0062182A"
operand = "rt"
half_extent = 160
[[widescreen.cull.scale]]
address = "0x8002C758"
expected = "0x0043102A"
operand = "rs"
half_extent = 160
)toml");
const auto scale_config = PSXRecompV4::load_game_config(scale);
check(scale_config.ws_cull_scale_sites.size() == 2 &&
scale_config.ws_cull_scale_sites[0].address == 0x8002C734u &&
scale_config.ws_cull_scale_sites[0].expected == 0x0062182Au &&
scale_config.ws_cull_scale_sites[0].operand == 1u &&
scale_config.ws_cull_scale_sites[0].half_extent == 160u &&
scale_config.ws_cull_scale_sites[1].operand == 0u,
"parser preserves register-register frustum-edge scale sites");
const auto signed_bound = write_config(root, "signed-x-bound", R"toml(
[[widescreen.signed_x_bound]]
address = "0x8002D290"
@@ -789,10 +811,31 @@ void codegen_tests() {
const std::string nclip_exact = generate_first_instruction(
0x04400002u, {}, false, nclip_exact_config); // bltz v0,+2
check(nclip_exact.find(
"gte_nclip_precise_bltz((int32_t)cpu->gpr[2])") !=
"gte_nclip_exact_sign((int32_t)cpu->gpr[2], 0x80010000u) < 0") !=
std::string::npos &&
nclip_exact.find("ws exact nclip") != std::string::npos,
"codegen emits title-scoped exact NCLIP predicate");
// Every sign-branch shape a backface reject can use.
const struct { uint32_t word; const char *cmp; const char *name; } nclip_shapes[] = {
{0x18400002u, "<= 0", "blez"}, {0x1C400002u, "> 0", "bgtz"},
{0x04410002u, ">= 0", "bgez"},
};
for (const auto &shape : nclip_shapes) {
const std::string exact = generate_first_instruction(
shape.word, {}, false, nclip_exact_config);
check(exact.find(std::string("gte_nclip_exact_sign((int32_t)cpu->gpr[2], 0x80010000u) ") +
shape.cmp) != std::string::npos &&
exact.find(std::string("((int32_t)cpu->gpr[2] ") + shape.cmp + ")") !=
std::string::npos,
(std::string("codegen exact NCLIP covers ") + shape.name).c_str());
}
PSXRecomp::CodeGenConfig nclip_keep_config;
nclip_keep_config.ws_cull_nclip_keep_sites.insert(0x80010000u);
const std::string nclip_keep_blez = generate_first_instruction(
0x18400002u, {}, false, nclip_keep_config);
check(nclip_keep_blez.find("psx_ws_x_margin() > 0 ? 0 : ((int32_t)cpu->gpr[2] <= 0)") !=
std::string::npos,
"codegen NCLIP keep covers blez");
PSXRecomp::CodeGenConfig plane_nx_config;
plane_nx_config.ws_cull_plane_nx_sites.insert(0x80010000u);
@@ -842,6 +885,27 @@ void codegen_tests() {
std::string::npos,
"overlay full-word mismatch leaves keep site unchanged");
PSXRecomp::CodeGenConfig scale_config_gen;
scale_config_gen.ws_cull_scale_sites.push_back(
{0x80010000u, 0x0062182Au, 1u, 160u}); // slt v1,v1,v0: scale rt
const std::string scale_rt = generate_first_instruction(
0x0062182Au, {}, false, scale_config_gen);
check(scale_rt.find("psx_ws_cull_scale((int32_t)") != std::string::npos &&
scale_rt.find(", 160)") != std::string::npos &&
scale_rt.find("(int32_t)cpu->gpr[3] < (int32_t)(uint32_t)psx_ws_cull_scale") !=
std::string::npos,
"codegen scales the configured rt bound of a signed frustum edge");
scale_config_gen.ws_cull_scale_sites[0] = {0x80010000u, 0x0043102Au, 0u, 160u};
const std::string scale_rs = generate_first_instruction(
0x0043102Au, {}, false, scale_config_gen);
check(scale_rs.find("(int32_t)(uint32_t)psx_ws_cull_scale((int32_t)cpu->gpr[2], 160) < (int32_t)cpu->gpr[3]") !=
std::string::npos,
"codegen scales the configured rs bound of the opposite edge");
const std::string scale_overlay_mismatch = generate_first_instruction(
0x0043102Bu, {}, true, scale_config_gen); // sltu at same overlay VA
check(scale_overlay_mismatch.find("psx_ws_cull_scale") == std::string::npos,
"overlay full-word mismatch leaves scale site unchanged");
PSXRecomp::CodeGenConfig angle_config;
angle_config.ws_cull_angle_sites.push_back(
{0x80010000u, 0x24020155u});
+15
View File
@@ -161,6 +161,7 @@ if(BUILD_TESTING)
tests/psx_ram_runtime_map_stubs.c
src/memory.c
src/dma.c
src/gpu_timeline.c
src/boot_state.c
src/psx_ram_geometry.c)
target_include_directories(psx_ram_runtime_map_test PRIVATE include src)
@@ -444,25 +445,38 @@ if(BUILD_TESTING)
add_executable(mod_packages_test
tests/test_mod_packages.cpp
src/mod_packages.cpp
src/mod_media.cpp src/iso_reader.cpp src/cue_sheet.cpp
src/crc32.c
src/psx_sha256.c)
target_include_directories(mod_packages_test PRIVATE
include
../recompiler/include
../recompiler/lib/toml11)
target_link_libraries(mod_packages_test PRIVATE chdr-static)
add_test(NAME mod_packages_test COMMAND mod_packages_test)
add_executable(builtin_mod_manifests_test
tests/test_builtin_mod_manifests.cpp
src/mod_packages.cpp
src/mod_media.cpp src/iso_reader.cpp src/cue_sheet.cpp
src/crc32.c
src/psx_sha256.c)
target_include_directories(builtin_mod_manifests_test PRIVATE
include
../recompiler/include
../recompiler/lib/toml11)
target_link_libraries(builtin_mod_manifests_test PRIVATE chdr-static)
add_test(NAME builtin_mod_manifests_test
COMMAND builtin_mod_manifests_test
"${CMAKE_CURRENT_SOURCE_DIR}/../mods/builtin/packages")
add_executable(mod_media_test
tests/test_mod_media.cpp src/mod_media.cpp src/iso_reader.cpp
src/cue_sheet.cpp src/psx_sha256.c)
target_include_directories(mod_media_test PRIVATE include ../recompiler/include)
target_link_libraries(mod_media_test PRIVATE chdr-static)
add_test(NAME mod_media_test COMMAND mod_media_test)
add_executable(text_xlate_fix_only_test
tests/test_text_xlate_fix_only.cpp
src/text_xlate.cpp
@@ -476,6 +490,7 @@ if(BUILD_TESTING)
tests/test_mod_runtime.cpp
src/mod_runtime.cpp
src/mod_packages.cpp
src/mod_media.cpp
src/disc_path.cpp
src/cue_sheet.cpp
src/iso_reader.cpp
+6 -4
View File
@@ -235,10 +235,12 @@ extern void gte_geometry_correction_stats(uint32_t *lookups, uint32_t *hits,
* where sub-pixel and guest-visible integer signs differ. MAC0 stays native. */
extern void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks,
uint64_t *disagreements);
/* Title-scoped widescreen cull consumer. Returns the exact tracked NCLIP sign
* only when it belongs to the supplied native MAC0; otherwise preserves the
* native comparison. This does not change guest-visible GTE state. */
extern int gte_nclip_precise_bltz(int32_t native_mac0);
/* Title-scoped widescreen cull consumer ([widescreen.cull] nclip_exact_sites).
* Returns the sign (-1/0/1) the branch at `pc` should test: the exact tracked
* NCLIP sign only when it belongs to the supplied native MAC0, otherwise the
* native sign. Counts per-site use (gte_nclip_stats.h). Does not change
* guest-visible GTE state. */
extern int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc);
/* PGXP dataflow-shadowing hook macros (PGXP_LOAD/STORE/ALU/MULDIV/COP2).
* The emitter writes them unconditionally; they expand to real calls only
+4
View File
@@ -108,6 +108,10 @@ uint32_t debug_server_get_tcp_drops(void);
/* Record the current frame's state into the ring buffer.
* Call after vblank processing. */
void debug_server_record_frame(void);
/* Operator capture mark (hotkey, present thread): freeze the always-on display
* and GP0 rings at this instant and save the presented native-wide frame, so a
* transient glitch can be read back afterwards over TCP (capture_mark). */
void debug_server_capture_mark(void);
/* Block while paused, polling TCP + SDL events.
* Call from vblank callback before frame processing. */
+29
View File
@@ -256,6 +256,12 @@ void gpu_ws_set_cull_keep_sites(const uint32_t *addresses,
const uint32_t *results, int nsites);
void gpu_ws_set_angle_sites(const uint32_t *addresses,
const uint32_t *expected, int nsites);
/* [[widescreen.cull.scale]]: full-word-guarded SLT/SLTU sites whose rs (0) or
* rt (1) operand is a runtime +/-half_extent*z frustum bound. */
void gpu_ws_set_cull_scale_sites(const uint32_t *addresses,
const uint32_t *expected,
const uint32_t *operands,
const uint32_t *half_extents, int nsites);
void gpu_ws_set_aspect_cone(const uint32_t *addresses,
const uint32_t *expected,
const uint32_t *cosine_thresholds,
@@ -289,6 +295,12 @@ int psx_ws_cull_keep_site(uint32_t pc, uint32_t instr, uint32_t vanilla,
uint32_t *out);
uint32_t psx_ws_angle_widen(uint32_t vanilla);
int psx_ws_angle_site(uint32_t pc, uint32_t instr, uint32_t *out);
/* bound * (half_extent + margin) / half_extent; identity at 4:3. */
int32_t psx_ws_cull_scale(int32_t bound, int32_t half_extent);
/* Interpreter path for a configured scale site: returns 1 and the widened
* compare verdict in *out when (pc, instr) is a configured site. */
int psx_ws_cull_scale_site(uint32_t pc, uint32_t instr, uint32_t rs_value,
uint32_t rt_value, uint32_t *out);
uint32_t psx_ws_aspect_cone_result(uint32_t site, uint32_t vanilla,
uint32_t object, int32_t x, int32_t z,
int32_t y);
@@ -366,6 +378,23 @@ void gpu_ws_tag_hud_prim(uint32_t prim, int anchor);
* Tag the complete background composite before submitting its ordering table.
* The caller proves background identity/coverage; no guest data is modified. */
void gpu_ws_tag_background_prim(uint32_t prim);
/* Always-on counters for the explicit HUD-anchor and background tag tables:
* how many tags a title plugin offered, how many were rejected at insert
* (native-wide not configured / unreadable or unsupported command), and why
* executing primitives that matched a tagged address were or were not
* honoured. TCP: {"cmd":"ws_tag_stats"}. */
typedef struct GpuWsTagStats {
uint64_t tag_calls;
uint64_t tag_rejected;
uint64_t hit;
uint64_t stale;
uint64_t guard_mismatch;
uint32_t last_tag_frame;
uint32_t last_hit_frame;
} GpuWsTagStats;
void gpu_ws_get_tag_stats(GpuWsTagStats *hud, GpuWsTagStats *background);
/* Freeze/unfreeze the always-on GP0 command ring (capture mark). */
void gpu_gp0_ring_set_frozen(int frozen);
/* Fresh packet tags are eligible for background classification. */
int gpu_ws_background_stretch_active(void);
/* A tagged background can remain in a wide surface after its packet expires.
+52
View File
@@ -0,0 +1,52 @@
#ifndef PSXRECOMP_GPU_TIMELINE_H
#define PSXRECOMP_GPU_TIMELINE_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* GPU frame timeline: an always-on, guest-cycle-stamped record of the events
* that decide WHAT a present shows — vblank, display-start flips, draw-area
* changes, GPU linked-list DMA start/end, and each present (swapped or
* skipped). The GP0, display and presented-image rings are all tagged by
* frame number only, which cannot order a present against a draw inside the
* same frame; this ring can. Frozen by the capture mark. */
#define GPU_TIMELINE_CAP 32768u /* power of two; ~40 s at typical rates */
typedef enum {
GTL_VBLANK = 1, /* a = lcf after toggle */
GTL_DISP_START = 2, /* a = x, b = y (GP1 05h) */
GTL_DRAW_AREA = 3, /* a = left, b = top (GP0 E3h) */
GTL_LL_START = 4, /* a = OT start address */
GTL_LL_END = 5, /* a = nodes, b = words */
GTL_PRESENT = 6, /* a = path, b = disp_x | disp_y << 16 */
GTL_PRESENT_SKIP = 7, /* a = path, b = disp_x | disp_y << 16 (no swap) */
GTL_DISP_CAPTURE = 8, /* display-ring capture; b = disp_x | disp_y << 16 */
GTL_FILL = 9, /* a = x | y << 16, b = w | h << 16 (GP0 02h) */
} GpuTimelineKind;
/* Present paths recorded in GTL_PRESENT/GTL_PRESENT_SKIP `a`. */
enum { GTL_PATH_WIDE_FBO = 1, GTL_PATH_VRAM_FBO = 2, GTL_PATH_CPU = 3,
GTL_PATH_HOLD = 4 };
typedef struct {
uint64_t cyc; /* psx_cycle_count */
uint32_t frame; /* s_frame_count */
uint32_t a, b;
uint8_t kind;
uint8_t pad[3];
} GpuTimelineEntry;
void gpu_timeline_note(uint8_t kind, uint32_t a, uint32_t b);
void gpu_timeline_set_frozen(int frozen);
uint64_t gpu_timeline_total(void);
int gpu_timeline_get(uint64_t seq, GpuTimelineEntry *out);
const char *gpu_timeline_kind_name(uint8_t kind);
#ifdef __cplusplus
}
#endif
#endif
+60
View File
@@ -0,0 +1,60 @@
#ifndef PSXRECOMP_GTE_NCLIP_STATS_H
#define PSXRECOMP_GTE_NCLIP_STATS_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Precise-NCLIP observability (widescreen). While [widescreen] precise_nclip
* is active, every NCLIP also computes the backface sign from the UNCLAMPED
* projection (the PGXP sub-pixel shadow, validated against each SXY word;
* precise_nclip arms that transport by itself, independent of the visual
* corrections). Guest MAC0 stays native; only configured branch sites
* ([widescreen.cull] nclip_exact_sites) consume the precise sign.
*
* Always-on tables, keyed by the last guest store PC before each NCLIP (a
* producer locator inside the issuing emitter) and by branch PC (exact-site
* consumers), so a title can find which emitters
* lose triangles to the +/-1024 screen clamp before listing any site. */
typedef struct {
uint32_t func; /* locator: last guest store PC before the NCLIP */
uint32_t nclips; /* NCLIPs with a precise sign available */
uint32_t disagree; /* precise sign != native MAC0 sign */
uint32_t saturated; /* any of the three SXY entries hit the clamp */
uint32_t last_frame;
} GteNclipFuncStat;
typedef struct {
uint32_t pc; /* configured exact-NCLIP branch PC */
uint32_t evals;
uint32_t flips; /* branch decision changed by the precise sign */
uint32_t fallbacks; /* no precise sign for this MAC0 -> native */
uint32_t last_frame;
} GteNclipSiteStat;
#define GTE_NCLIP_STAT_CAP 256
void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks,
uint64_t *disagreements);
int gte_nclip_func_stats(GteNclipFuncStat *out, int max);
int gte_nclip_site_stats(GteNclipSiteStat *out, int max);
void gte_nclip_stats_reset(void);
/* Branch consumer for exact-NCLIP sites: the sign (-1/0/1) the branch should
* test. The precise sign when it belongs to `native_mac0` (the value the
* guest just read from MAC0), else the native sign. */
int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc);
/* Kill switch for the exact-site consumer (default on): off makes every exact
* site test the native sign, for same-build/same-state A/B and diagnosis. */
void gte_nclip_exact_set_enabled(int on);
int gte_nclip_exact_enabled(void);
#ifdef __cplusplus
}
#endif
#endif
+1
View File
@@ -23,6 +23,7 @@ typedef enum HostKeymapAction {
HOST_KEYMAP_SAVE_STATE_MENU, /* default F7 */
HOST_KEYMAP_SCANLINES, /* default F6 */
HOST_KEYMAP_TURBO_TOGGLE, /* default F9; latches Turbo until pressed again */
HOST_KEYMAP_CAPTURE_MARK, /* default Ctrl+M; freezes the debug capture rings */
HOST_KEYMAP_ACTION_COUNT
} HostKeymapAction;
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <cstdint>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
namespace PSXRecompV4 {
/* Canonical bytes are an immutable, engine-owned snapshot. Plugins never
* reopen the selected file after verification. The PSX domain contains the
* first data track's 2048-byte sector payloads, excluding CD audio. */
bool load_mod_media(const std::filesystem::path& path,
const std::string& format, uint64_t expected_size,
const std::string& expected_sha256,
std::shared_ptr<const std::vector<uint8_t>>& bytes,
std::string* error = nullptr);
} // namespace PSXRecompV4
+27
View File
@@ -6,6 +6,7 @@
#include <filesystem>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>
@@ -224,6 +225,8 @@ struct ModResource {
std::string file_description;
std::string format = "file";
bool required = false;
uint64_t size = 0;
std::string sha256;
};
struct ModDerivedDisc {
@@ -352,6 +355,9 @@ struct ModResolution {
std::string feature_id;
std::string id;
std::filesystem::path path;
std::string format;
std::string sha256;
std::shared_ptr<const std::vector<uint8_t>> bytes;
};
std::vector<Resource> resources;
struct Diagnostic {
@@ -535,4 +541,25 @@ struct ModFunctionEntryHook {
std::vector<ModFunctionEntryHook> mod_function_entry_hooks(const std::string& id);
void mod_clear_plugins_for_tests();
/* Every id a trusted implementation registered (activation, vblank or
* function-entry), sorted. */
std::vector<std::string> mod_registered_plugin_ids();
/* Registered implementations that no manifest under `manifest_roots` declares
* as a [[plugin]] id. The resolved plan activates plugins by manifest id only,
* so such an implementation can never run: a hook registered under a sub-id
* ("x.widescreen.hud") is silently dead while its package ("x.widescreen")
* resolves cleanly. Roots are searched recursively for manifest.toml; pass the
* AUTHORED trees (title mods/preloaded, framework mods/builtin) so developer
* packages a release catalog strips still count as declarations. Manifests
* that fail to parse are reported through `errors`. */
struct ModPluginAudit {
std::vector<std::string> undeclared; /* registered, never declared */
std::vector<std::string> declared; /* every manifest [[plugin]] id */
std::vector<std::string> errors;
size_t manifests = 0;
};
ModPluginAudit mod_audit_registered_plugins(
const std::vector<std::filesystem::path>& manifest_roots);
} // namespace PSXRecompV4
+18
View File
@@ -32,6 +32,13 @@ void psx_mod_function_entry(struct CPUState* cpu, uint32_t address);
* call, so a run without an active hook pays one load per interpreted entry. */
extern uint32_t g_psx_mod_function_entry_hooks;
/* Always-on named event counters for trusted plugins (observability, not
* logging): e.g. how often each guard in a hook rejected. `name` should be a
* string literal of the form "<plugin>.<event>"; up to 128 distinct names are
* kept, further names are counted in an overflow bucket. Emulation-thread only.
* TCP: {"cmd":"mod_counters"} lists every counter with its last frame. */
void psx_mod_counter_add(const char* name, uint32_t delta);
/* Narrow guest services available to trusted plugin callbacks. */
int psx_mod_game_started(void);
/* Read an original mounted-disc file without changing guest CD state/timing.
@@ -194,6 +201,17 @@ int psx_mod_option_value(const char* package_id, const char* feature_id,
*/
int psx_mod_current_resource_path(const char* resource_id,
char* out, uint32_t out_size);
/* Read-only canonical media verified by the engine for this plugin's owning
* package/feature. The pointer lives until the committed plan is replaced or
* cleared. Available only during that plugin's callbacks; returns 0 for an
* ordinary unverified resource, an inactive feature, or a different owner. */
int psx_mod_current_resource_bytes(const char* resource_id,
const uint8_t** bytes, uint64_t* size);
/* Display aspects have no framework ceiling: the native-wide surfaces size
* themselves from the live width, and each title caps its own view at what
* it has validated (fixed ratio, or the adaptive maximum below). Requests
* must be at least native 4:3, with numerator and denominator in 1..99. */
/*
* Request a fixed host display aspect before renderer/window initialization.
+15 -2
View File
@@ -106,7 +106,13 @@
* the version stays. The emit-content change (PGXP_*() macros in all
* generated C) is covered by the codegen hash + CODEGEN_VER below. */
/* v23: native overlay forwarding for signed screen bounds and mod entry hooks. */
#define PSX_OVERLAY_ABI_VERSION 23
/* v24: ws_cull_scale forwarder for register-register frustum-edge compares
* whose runtime bound operand scales with the live reveal. Appended
* last; NULL preserves the vanilla bound. */
/* v25: gte_nclip_exact_sign forwarder for [widescreen.cull] nclip_exact_sites
* in overlay code (all sign-branch shapes). Appended last; NULL keeps the
* native sign. */
#define PSX_OVERLAY_ABI_VERSION 25
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
* record consumes one slot, even when another DLL carries an identical
@@ -173,7 +179,10 @@
* so packaged pairs cannot be hidden by an older user cache on first boot. */
/* 13: merge adaptive backdrop v12: independent quotient consumers, proven
* copied/biased camera inputs, and two biased bounds for terrain strips. */
#define PSX_OVERLAY_CODEGEN_VER 13
/* 14: + [[widescreen.cull.scale]] register-register frustum-edge emit. */
/* 15: exact/keep NCLIP sites emit for every sign-branch shape through
* gte_nclip_exact_sign(mac0, pc). */
#define PSX_OVERLAY_CODEGEN_VER 15
/* 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>/)
@@ -342,6 +351,10 @@ typedef struct {
const PGXPHooks *pgxp;
int32_t (*ws_screen_x_bound)(int32_t vanilla);
void (*mod_function_entry)(CPUState *cpu, uint32_t address);
/* Frustum-edge bound scale helper (ABI v24). */
int32_t (*ws_cull_scale)(int32_t bound, int32_t half_extent);
/* Exact-NCLIP branch sign (ABI v25). */
int32_t (*nclip_exact_sign)(int32_t native_mac0, uint32_t pc);
} OverlayCallbacks;
#ifdef __cplusplus
@@ -263,6 +263,16 @@ uint32_t psx_ws_angle_widen(uint32_t vanilla) {
? g_cbs.ws_angle_widen(vanilla)
: vanilla;
}
int32_t psx_ws_cull_scale(int32_t bound, int32_t half_extent) {
return g_cbs.ws_cull_scale
? g_cbs.ws_cull_scale(bound, half_extent)
: bound;
}
int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc) {
return g_cbs.nclip_exact_sign
? g_cbs.nclip_exact_sign(native_mac0, pc)
: (native_mac0 < 0 ? -1 : (native_mac0 > 0 ? 1 : 0));
}
/* Sub-pixel GTE precision-store tracker (ABI v14). The emitter emits a direct
* gte_precision_store_word() call for every swc2 (GTE store-word), including in
* overlay-resident code, so the DLL must define it or the shard fails to link
+55
View File
@@ -0,0 +1,55 @@
#ifndef PSXRECOMP_PRESENT_IMAGE_RING_H
#define PSXRECOMP_PRESENT_IMAGE_RING_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Presented-image ring: a small copy of EVERY frame actually shown in the
* window (after compositing, letterbox included, before host OSD), for about
* ten seconds. Complements the display ring (canonical VRAM, ~1 s) for
* glitches that live only in presentation — native-wide compositing, frame
* interpolation, held frames — and that last a frame or two, so a single
* screenshot cannot catch them. Always-on in debug-tools builds
* (PSX_PRESENT_IMAGE_RING=0 disables); frozen by the capture mark.
*
* Entries are RGB565, PRESENT_IMAGE_RING_H rows tall, width following the
* window aspect (capped at PRESENT_IMAGE_RING_MAX_W). */
#define PRESENT_IMAGE_RING_CAP 600
#define PRESENT_IMAGE_RING_H 120
#define PRESENT_IMAGE_RING_MAX_W 720
/* Thumbnail width for a window of ww x wh pixels. */
int present_image_ring_thumb_w(int ww, int wh);
/* 1 while capture is enabled and not frozen (callers skip the downscale). */
int present_image_ring_accepting(void);
/* Record one presented frame. `rgba` is 8-bit R,G,B,(A) per pixel, `pitch`
* bytes per row; `bottom_up` = rows stored bottom-first (GL readback). The
* image must already be w x PRESENT_IMAGE_RING_H. */
void present_image_ring_push(uint32_t frame, const uint8_t *rgba, int w,
int pitch, int bottom_up);
/* Box-downscale an ARGB8888 CPU present buffer (0xAARRGGBB, sw x sh) into a
* thumbnail and record it. Software / CPU-present path. */
void present_image_ring_push_argb(uint32_t frame, const uint32_t *argb,
int sw, int sh, int src_pitch_px);
void present_image_ring_set_frozen(int frozen);
int present_image_ring_frozen(void);
/* Oldest/newest recorded frame numbers and how many entries are valid. */
void present_image_ring_span(uint32_t *oldest, uint32_t *newest, int *count);
/* Copy entry for `frame` as RGB888 top-down into a malloc'd buffer the
* caller frees. Returns 0 if the frame is not in the ring. */
int present_image_ring_get_rgb(uint32_t frame, uint8_t **rgb, int *w, int *h);
#ifdef __cplusplus
}
#endif
#endif
+3
View File
@@ -140,6 +140,9 @@ void psx_frontend_on_rb_snap_loaded(void);
/* Frontend hook (main.cpp): host OSD toast after a user save/load settles.
* is_load: 0 = save, 1 = load. slot is 0-based. ok: 1 on success. */
void psx_frontend_on_savestate_notify(int is_load, int slot, int ok);
/* Frontend hook (main.cpp): a user save/load was refused before it was staged
* (so no notify follows). `reason` is short, plain text for the OSD. */
void psx_frontend_on_savestate_refused(int is_load, int slot, const char *reason);
/* Called every block from psx_check_interrupts (in_exception == 0). If a save is
* pending, serialize with cpu->pc = resume_pc; if a load is pending, restore and
+33 -12
View File
@@ -76,14 +76,23 @@ static inline void ws_hud_anchor_insert(WsHudAnchorTag *tags, uint32_t count,
tags[victim].guard = *guard;
}
static inline int ws_hud_anchor_lookup(const WsHudAnchorTag *tags,
uint32_t count,
uint32_t command_addr,
const uint32_t *words,
uint32_t word_count,
uint32_t frame,
int *out_anchor) {
if (!tags || !count || !words) return 0;
/* Lookup outcome, so callers can keep always-on counters that say WHY a tagged
* primitive was or was not honoured (absent, expired, or packet rewritten). */
enum {
WS_HUD_ANCHOR_HIT = 1,
WS_HUD_ANCHOR_ABSENT = 0,
WS_HUD_ANCHOR_STALE = -1,
WS_HUD_ANCHOR_GUARD_MISMATCH = -2
};
static inline int ws_hud_anchor_lookup_result(const WsHudAnchorTag *tags,
uint32_t count,
uint32_t command_addr,
const uint32_t *words,
uint32_t word_count,
uint32_t frame,
int *out_anchor) {
if (!tags || !count || !words) return WS_HUD_ANCHOR_ABSENT;
uint32_t idx = ws_hud_anchor_slot(command_addr) & (count - 1u);
for (uint32_t i = 0; i < WS_HUD_ANCHOR_PROBES && i < count; i++) {
const WsHudAnchorTag *tag = &tags[(idx + i) & (count - 1u)];
@@ -91,13 +100,25 @@ static inline int ws_hud_anchor_lookup(const WsHudAnchorTag *tags,
continue;
if (tag->frame > frame ||
frame - tag->frame > WS_HUD_ANCHOR_FRESH_FRAMES)
return 0;
return WS_HUD_ANCHOR_STALE;
if (!ws_prepass_packet_matches(&tag->guard, words, word_count))
return 0;
return WS_HUD_ANCHOR_GUARD_MISMATCH;
if (out_anchor) *out_anchor = tag->anchor;
return 1;
return WS_HUD_ANCHOR_HIT;
}
return 0;
return WS_HUD_ANCHOR_ABSENT;
}
static inline int ws_hud_anchor_lookup(const WsHudAnchorTag *tags,
uint32_t count,
uint32_t command_addr,
const uint32_t *words,
uint32_t word_count,
uint32_t frame,
int *out_anchor) {
return ws_hud_anchor_lookup_result(tags, count, command_addr, words,
word_count, frame, out_anchor) ==
WS_HUD_ANCHOR_HIT;
}
#ifdef __cplusplus
+19
View File
@@ -402,6 +402,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/psx_icache.c
${PSXRECOMP_ROOT}/runtime/src/starvation_ring.c
${PSXRECOMP_ROOT}/runtime/src/latency_ring.c
${PSXRECOMP_ROOT}/runtime/src/present_image_ring.c
${PSXRECOMP_ROOT}/runtime/src/gpu_timeline.c
${PSXRECOMP_ROOT}/runtime/src/data_shards.c
${PSXRECOMP_ROOT}/runtime/src/load_accel.c
${PSXRECOMP_ROOT}/runtime/src/card_read_summary.c
@@ -420,6 +422,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_bezel.c
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_ram.c
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_media.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
${PSXRECOMP_ROOT}/runtime/src/mod_texture_banks.c
${PSXRECOMP_ROOT}/runtime/src/psx_keybinds.c
@@ -1249,6 +1252,22 @@ function(_psxrt_stage_mod_catalog target preloaded_dir)
endforeach()
_psxrt_write_if_changed("${_excluded_manifest}" "${_excluded_text}")
# Every trusted plugin the executable registers must be selectable by an
# authored manifest [[plugin]] id; the resolved plan activates plugins by
# manifest id only, so an undeclared id (typically a hook registered under
# a sub-id of its package's plugin) is dead code that fails silently.
# Audits the AUTHORED trees, so developer packages a release catalog
# strips still count as declarations.
if(BUILD_TESTING)
set(_audit_roots "${PSXRECOMP_ROOT}/mods/builtin")
if(NOT preloaded_dir STREQUAL "" AND NOT preloaded_dir STREQUAL "NONE"
AND IS_DIRECTORY "${preloaded_dir}")
list(APPEND _audit_roots "${preloaded_dir}")
endif()
add_test(NAME psx_mod_plugin_audit_${target}
COMMAND $<TARGET_FILE:${target}> --audit-mod-plugins ${_audit_roots})
endif()
set_property(GLOBAL APPEND PROPERTY PSXRECOMP_MOD_CATALOG_TARGETS "${target}")
set_property(GLOBAL APPEND PROPERTY PSXRECOMP_MOD_CATALOG_MANIFESTS "${_manifest}")
set_property(GLOBAL APPEND PROPERTY PSXRECOMP_MOD_CATALOG_EXCLUDED "${_excluded_manifest}")
+278 -16
View File
@@ -31,6 +31,9 @@
#include "gpu.h"
#include "gpu_render.h" /* gr_scale + gr_render_display_hires (screenshot_hires) */
#include "present_ring.h"
#include "gpu_timeline.h"
#include "gte_nclip_stats.h"
#include "present_image_ring.h"
#include "load_transition_ring.h"
#include "cdrom.h"
#include "sio.h"
@@ -7810,6 +7813,72 @@ static void handle_ws_hud_mode(int id, const char *json)
send_fmt("{\"id\":%d,\"ok\":true,\"tag_rects\":%d}", id, v ? 1 : 0);
}
/* Named plugin counters (psx_mod_counter_add), always on:
* {"cmd":"mod_counters"}
* -> counters: [{name, count, last_frame}], overflow. */
static void handle_mod_counters(int id, const char *json)
{
extern int psx_mod_counters_snapshot(const char **names, uint64_t *counts,
uint64_t *last_frames, int cap,
uint64_t *overflow);
(void)json;
enum { CAP = 128 };
const char *names[CAP];
uint64_t counts[CAP], last[CAP], overflow = 0;
int n = psx_mod_counters_snapshot(names, counts, last, CAP, &overflow);
if (n > CAP) n = CAP;
size_t cap = 256u + (size_t)n * 160u;
char *buf = (char *)malloc(cap);
if (!buf) { send_err(id, "alloc failed"); return; }
size_t off = 0;
off += (size_t)snprintf(buf + off, cap - off,
"{\"id\":%d,\"ok\":true,\"frame\":%llu,\"counters\":[",
id, (unsigned long long)s_frame_count);
for (int i = 0; i < n && off < cap; i++) {
char esc[96];
size_t e = 0;
for (const char *p = names[i]; *p && e + 2 < sizeof esc; p++)
if (*p != '"' && *p != '\\' && (unsigned char)*p >= 0x20) esc[e++] = *p;
esc[e] = '\0';
off += (size_t)snprintf(buf + off, cap - off,
"%s{\"name\":\"%s\",\"count\":%llu,\"last_frame\":%llu}",
i ? "," : "", esc, (unsigned long long)counts[i],
(unsigned long long)last[i]);
}
if (off < cap)
snprintf(buf + off, cap - off, "],\"overflow\":%llu}",
(unsigned long long)overflow);
debug_server_send_line(buf);
free(buf);
}
/* Explicit HUD-anchor / background tag pipeline counters (always on):
* {"cmd":"ws_tag_stats"}
* -> hud / background: tag_calls, tag_rejected, hit, stale, guard_mismatch,
* last_tag_frame, last_hit_frame. A title plugin that tags but never hits
* is either tagging the wrong address or rewriting packets after tagging. */
static void handle_ws_tag_stats(int id, const char *json)
{
(void)json;
GpuWsTagStats hud, bg;
gpu_ws_get_tag_stats(&hud, &bg);
const GpuWsTagStats *s[2] = { &hud, &bg };
char part[2][256];
for (int i = 0; i < 2; i++)
snprintf(part[i], sizeof part[i],
"{\"tag_calls\":%llu,\"tag_rejected\":%llu,\"hit\":%llu,"
"\"stale\":%llu,\"guard_mismatch\":%llu,"
"\"last_tag_frame\":%u,\"last_hit_frame\":%u}",
(unsigned long long)s[i]->tag_calls,
(unsigned long long)s[i]->tag_rejected,
(unsigned long long)s[i]->hit,
(unsigned long long)s[i]->stale,
(unsigned long long)s[i]->guard_mismatch,
s[i]->last_tag_frame, s[i]->last_hit_frame);
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%llu,\"hud\":%s,\"background\":%s}",
id, (unsigned long long)s_frame_count, part[0], part[1]);
}
/* Kernel-image bless state: {"cmd":"kernel_bless"} ->
* entries/clean/mismatch/native_hits/verifies/invalidations, plus the
* declared kernel patch ranges and the segments they made the verifier
@@ -7973,7 +8042,7 @@ static void handle_display_aspect(int id, const char *json) {
int num=json_get_int(json,"num",-1), den=json_get_int(json,"den",-1);
int adaptive=json_get_int(json,"adaptive",0);
if (!psx_debug_display_aspect(num,den,adaptive)) {
send_err(id,"invalid display aspect (4:3 through 32:9)");return;
send_err(id,"invalid display aspect (4:3 or wider, terms 1..99)");return;
}
send_fmt("{\"id\":%d,\"ok\":true,\"num\":%d,\"den\":%d,\"adaptive\":%d}",id,num,den,adaptive!=0);
}
@@ -8689,6 +8758,12 @@ typedef struct {
uint16_t *vram; /* full 1024x512 */
} DispRingEntry;
static PSX_BSS DispRingEntry s_disp_ring[DISP_RING_CAP];
/* Capture mark state (debug_server_capture_mark / capture_mark). */
static int s_capture_frozen = 0;
static uint64_t s_capture_mark_frame = 0;
static uint64_t s_capture_mark_count = 0;
static char s_capture_wide_path[512];
static int s_capture_wide_ok = 0;
static uint16_t *s_disp_ring_px = NULL; /* one block for all entries */
static void disp_ring_capture(void)
@@ -8702,7 +8777,7 @@ static void disp_ring_capture(void)
const char *e = getenv("PSX_DISPLAY_RING");
enabled = (!e || !*e || *e != '0') ? 1 : 0;
}
if (!enabled) return;
if (!enabled || s_capture_frozen) return;
if (!s_disp_ring_px) {
size_t per = (size_t)DISP_RING_MAX_W * DISP_RING_MAX_H;
size_t vram = (size_t)1024 * 512;
@@ -8720,6 +8795,7 @@ static void disp_ring_capture(void)
GpuDisplayInfo di;
gpu_get_display_info(&di);
if (di.disabled || di.width == 0 || di.height == 0) return;
gpu_timeline_note(GTL_DISP_CAPTURE, 0, di.display_x | (di.display_y << 16));
uint32_t w = di.width, h = di.height;
if (w > DISP_RING_MAX_W) w = DISP_RING_MAX_W;
if (h > DISP_RING_MAX_H) h = DISP_RING_MAX_H;
@@ -9055,7 +9131,10 @@ static void handle_dump_buffer(int id, const char *json)
* present path uses (gr_render_wide_display into a scratch buffer), so the dump
* reflects the composited wide frame 1:1, orientation included. Errors if the
* active backend has no wide compositor or native-wide isn't engaged. */
static void handle_wide_shot(int id, const char *json)
/* Write the presented native-wide frame to `path`. Returns NULL on success
* (and the pixel size), or a static error string. Shared by wide_shot and
* the capture mark. */
static const char *write_wide_png(const char *path, int *out_w, int *out_h)
{
extern int gr_wide_supported(void);
extern int gr_scale(void);
@@ -9064,33 +9143,28 @@ static void handle_wide_shot(int id, const char *json)
extern void gl_renderer_sync_cpu(void);
gl_renderer_sync_cpu(); /* no-op on SW / when no GL frame pending */
if (!gr_wide_supported()) { send_err(id, "active backend has no wide compositor"); return; }
if (!gr_wide_supported()) return "active backend has no wide compositor";
int extra = ws_nw_extra();
if (extra <= 0) { send_err(id, "native-wide not engaged (extra=0; need a wide game frame)"); return; }
if (extra <= 0) return "native-wide not engaged (extra=0; need a wide game frame)";
GpuDisplayInfo di;
gpu_get_display_info(&di);
if (di.disabled || di.width == 0 || di.height == 0) { send_err(id, "display disabled"); return; }
if (di.disabled || di.width == 0 || di.height == 0) return "display disabled";
int scale = gr_scale(); if (scale < 1) scale = 1;
int present_w = (int)di.width + extra;
int W = present_w * scale, H = (int)di.height * scale;
char path[512];
if (!json_get_str(json, "path", path, sizeof(path)))
strncpy(path, "psx_wide.png", sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
uint32_t *buf = (uint32_t *)malloc((size_t)W * H * sizeof(uint32_t));
if (!buf) { send_err(id, "alloc failed"); return; }
if (!buf) return "alloc failed";
int n = gr_render_wide_display(buf, W * (int)sizeof(uint32_t),
(int)di.display_x, (int)di.display_y, (int)di.height);
if (n <= 0) { free(buf); send_err(id, "no wide surface for displayed buffer"); return; }
if (n <= 0) { free(buf); return "no wide surface for displayed buffer"; }
/* ARGB8888 (0xAARRGGBB) -> RGB, written in the buffer's row order (so the
* PNG shows exactly the present orientation — the point of this probe). */
uint8_t *rgb = (uint8_t *)malloc((size_t)W * H * 3);
if (!rgb) { free(buf); send_err(id, "alloc failed"); return; }
if (!rgb) { free(buf); return "alloc failed"; }
for (int y = 0; y < H; y++) {
for (int x = 0; x < W; x++) {
uint32_t px = buf[(size_t)y * W + x];
@@ -9102,14 +9176,114 @@ static void handle_wide_shot(int id, const char *json)
}
free(buf);
FILE *f = fopen(path, "wb");
if (!f) { free(rgb); send_err(id, "cannot open file"); return; }
if (!f) { free(rgb); return "cannot open file"; }
int ok = png_write_rgb(f, rgb, (uint32_t)W, (uint32_t)H);
free(rgb); fclose(f);
if (!ok) { send_err(id, "png encode failed"); return; }
if (!ok) return "png encode failed";
if (out_w) *out_w = W;
if (out_h) *out_h = H;
return NULL;
}
static void handle_wide_shot(int id, const char *json)
{
char path[512];
if (!json_get_str(json, "path", path, sizeof(path)))
strncpy(path, "psx_wide.png", sizeof(path) - 1);
path[sizeof(path) - 1] = '\0';
int W = 0, H = 0;
const char *err = write_wide_png(path, &W, &H);
if (err) { send_err(id, err); return; }
send_fmt("{\"id\":%d,\"ok\":true,\"path\":\"%s\",\"width\":%d,\"height\":%d}",
id, path, W, H);
}
/* Operator capture mark. Runs on the present thread (hotkey handler), where
* the renderer's context is current. Freezes the display and GP0 rings so the
* frames and draw lists leading up to the mark survive until read, and saves
* the presented native-wide frame (the only record of the compositor output).
* A second mark while frozen is ignored; release with capture_mark op=release. */
void debug_server_capture_mark(void)
{
if (s_capture_frozen) return;
s_capture_mark_frame = s_frame_count;
s_capture_mark_count++;
snprintf(s_capture_wide_path, sizeof s_capture_wide_path,
"capture_mark_%llu_wide.png", (unsigned long long)s_frame_count);
s_capture_wide_ok = write_wide_png(s_capture_wide_path, NULL, NULL) == NULL;
s_capture_frozen = 1;
gpu_gp0_ring_set_frozen(1);
present_image_ring_set_frozen(1);
gpu_timeline_set_frozen(1);
}
/* Presented-image ring (see present_image_ring.h):
* {"cmd":"present_image_ring_stats"} -> oldest, newest, count, frozen
* {"cmd":"present_image_ring_get","frame":N,"path":"x.png"} */
static void handle_present_image_ring_stats(int id, const char *json)
{
(void)json;
uint32_t lo = 0, hi = 0; int n = 0;
present_image_ring_span(&lo, &hi, &n);
send_fmt("{\"id\":%d,\"ok\":true,\"oldest\":%u,\"newest\":%u,\"count\":%d,"
"\"frozen\":%d,\"capacity\":%d,\"height\":%d}",
id, lo, hi, n, present_image_ring_frozen(),
PRESENT_IMAGE_RING_CAP, PRESENT_IMAGE_RING_H);
}
static void handle_present_image_ring_get(int id, const char *json)
{
int f = json_get_int(json, "frame", -1);
char path[512];
if (f < 0 || !json_get_str(json, "path", path, sizeof path)) {
send_err(id, "need frame and path"); return;
}
uint8_t *rgb = NULL; int w = 0, h = 0;
if (!present_image_ring_get_rgb((uint32_t)f, &rgb, &w, &h)) {
send_err(id, "frame not in presented-image ring"); return;
}
FILE *fp = fopen(path, "wb");
int ok = fp && png_write_rgb(fp, rgb, (uint32_t)w, (uint32_t)h);
if (fp) fclose(fp);
free(rgb);
if (!ok) { send_err(id, "png write failed"); return; }
send_fmt("{\"id\":%d,\"ok\":true,\"frame\":%d,\"width\":%d,\"height\":%d}",
id, f, w, h);
}
/* {"cmd":"capture_mark","op":"status"|"mark"|"release"}
* status -> frozen, mark_frame, count, wide_path/wide_ok, and the frozen
* display-ring / GP0-ring frame spans to query.
* mark -> same as the hotkey (use only when nobody is at the window).
* release -> resume continuous capture. */
static void handle_capture_mark(int id, const char *json)
{
char op[16] = "status";
(void)json_get_str(json, "op", op, sizeof op);
if (!strcmp(op, "mark")) debug_server_capture_mark();
else if (!strcmp(op, "release")) {
s_capture_frozen = 0;
gpu_gp0_ring_set_frozen(0);
present_image_ring_set_frozen(0);
gpu_timeline_set_frozen(0);
} else if (strcmp(op, "status")) { send_err(id, "op must be status|mark|release"); return; }
uint32_t oldest = 0, newest = 0;
gpu_gp0_ring_frame_span(&oldest, &newest);
uint32_t d_old = UINT32_MAX, d_new = 0;
for (int i = 0; i < DISP_RING_CAP; i++) {
if (!s_disp_ring[i].valid) continue;
if (s_disp_ring[i].frame < d_old) d_old = s_disp_ring[i].frame;
if (s_disp_ring[i].frame > d_new) d_new = s_disp_ring[i].frame;
}
if (d_old == UINT32_MAX) d_old = 0;
send_fmt("{\"id\":%d,\"ok\":true,\"frozen\":%d,\"mark_frame\":%llu,"
"\"count\":%llu,\"wide_path\":\"%s\",\"wide_ok\":%d,"
"\"display_ring\":[%u,%u],\"gp0_ring\":[%u,%u],\"frame\":%llu}",
id, s_capture_frozen, (unsigned long long)s_capture_mark_frame,
(unsigned long long)s_capture_mark_count, s_capture_wide_path,
s_capture_wide_ok, d_old, d_new, oldest, newest,
(unsigned long long)s_frame_count);
}
/* screenshot: capture what the player is actually being shown. Native-wide
* presentation is wider than the canonical PSX display rectangle, so routing
* this command unconditionally through handle_screenshot_file silently omits
@@ -9392,6 +9566,87 @@ static void handle_present_ring(int id, const char *json)
free(buf);
}
/* Precise-NCLIP attribution (gte_nclip_stats.h).
* {"cmd":"nclip_stats"} totals + per-function + per-site tables
* {"cmd":"nclip_stats","reset":1} clear the tables after reading
* {"cmd":"nclip_stats","exact":0|1} switch exact-site consumers off/on
* funcs: [near_store_pc, nclips, disagree, saturated, last_frame] (1 = unknown)
* sites: [pc, evals, flips, fallbacks, last_frame] */
static void handle_nclip_stats(int id, const char *json)
{
extern int gpu_ws_precise_nclip_enabled(void);
static GteNclipFuncStat funcs[GTE_NCLIP_STAT_CAP];
static GteNclipSiteStat sites[GTE_NCLIP_STAT_CAP];
uint64_t hits = 0, fallbacks = 0, disagree = 0;
gte_nclip_precise_stats(&hits, &fallbacks, &disagree);
const int nf = gte_nclip_func_stats(funcs, GTE_NCLIP_STAT_CAP);
const int ns = gte_nclip_site_stats(sites, GTE_NCLIP_STAT_CAP);
const int bufsz = 256 + (nf + ns) * 72;
char *buf = (char *)malloc((size_t)bufsz);
if (!buf) { send_err(id, "alloc failed"); return; }
int pos = snprintf(buf, bufsz,
"{\"id\":%d,\"ok\":true,\"enabled\":%d,\"exact\":%d,\"hits\":%llu,\"fallbacks\":%llu,"
"\"disagreements\":%llu,\"funcs\":[",
id, gpu_ws_precise_nclip_enabled(), gte_nclip_exact_enabled(),
(unsigned long long)hits,
(unsigned long long)fallbacks, (unsigned long long)disagree);
for (int i = 0; i < nf; i++)
pos += snprintf(buf + pos, bufsz - pos, "%s[\"0x%08X\",%u,%u,%u,%u]",
i ? "," : "", funcs[i].func, funcs[i].nclips,
funcs[i].disagree, funcs[i].saturated, funcs[i].last_frame);
pos += snprintf(buf + pos, bufsz - pos, "],\"sites\":[");
for (int i = 0; i < ns; i++)
pos += snprintf(buf + pos, bufsz - pos, "%s[\"0x%08X\",%u,%u,%u,%u]",
i ? "," : "", sites[i].pc, sites[i].evals,
sites[i].flips, sites[i].fallbacks, sites[i].last_frame);
snprintf(buf + pos, bufsz - pos, "]}");
if (json_get_int(json, "reset", 0)) gte_nclip_stats_reset();
{
const int exact = json_get_int(json, "exact", -1);
if (exact >= 0) gte_nclip_exact_set_enabled(exact);
}
send_fmt("%s", buf);
free(buf);
}
/* GPU frame timeline (gpu_timeline.h): cycle-ordered vblank / flip /
* draw-area / linked-list DMA / present events.
* {"cmd":"gpu_timeline","from":F,"to":T} frames [F,T] (default: last 8)
* events: [seq, cycle, frame, kind, a, b] */
static void handle_gpu_timeline(int id, const char *json)
{
uint64_t total = gpu_timeline_total();
uint64_t oldest = total > GPU_TIMELINE_CAP ? total - GPU_TIMELINE_CAP : 0;
GpuTimelineEntry last;
uint32_t newest_frame = (total && gpu_timeline_get(total - 1, &last)) ? last.frame : 0;
int to = json_get_int(json, "to", (int)newest_frame);
int from = json_get_int(json, "from", to - 7);
if (from < 0) from = 0;
if (to < from) { send_err(id, "to < from"); return; }
int bufsz = 256 + 4096 * 80;
char *buf = (char *)malloc((size_t)bufsz);
if (!buf) { send_err(id, "alloc failed"); return; }
GpuTimelineEntry first_e;
uint32_t oldest_frame = (total && gpu_timeline_get(oldest, &first_e)) ? first_e.frame : 0;
int pos = snprintf(buf, bufsz,
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"frames\":[%u,%u],\"events\":[",
id, (unsigned long long)total, oldest_frame, newest_frame);
int first = 1, n = 0;
for (uint64_t s = oldest; s < total && pos < bufsz - 128 && n < 4096; s++) {
GpuTimelineEntry e;
if (!gpu_timeline_get(s, &e)) continue;
if ((int)e.frame < from || (int)e.frame > to) continue;
pos += snprintf(buf + pos, bufsz - pos, "%s[%llu,%llu,%u,\"%s\",%u,%u]",
first ? "" : ",", (unsigned long long)s,
(unsigned long long)e.cyc, e.frame,
gpu_timeline_kind_name(e.kind), e.a, e.b);
first = 0; n++;
}
pos += snprintf(buf + pos, bufsz - pos, "],\"truncated\":%s}", n >= 4096 ? "true" : "false");
send_fmt("%s", buf);
free(buf);
}
/* ---- Write trace: hook + handlers (Tier 1 reverse debugger) ---- */
extern CPUState *debug_cpu_ptr;
@@ -13831,6 +14086,11 @@ static const CmdEntry s_commands[] = {
{ "ws_aspect_cone_site", handle_ws_aspect_cone_site },
{ "ws_margin", handle_ws_margin },
{ "ws_hud_mode", handle_ws_hud_mode },
{ "ws_tag_stats", handle_ws_tag_stats },
{ "mod_counters", handle_mod_counters },
{ "capture_mark", handle_capture_mark },
{ "present_image_ring_stats", handle_present_image_ring_stats },
{ "present_image_ring_get", handle_present_image_ring_get },
{ "kernel_bless", handle_kernel_bless },
{ "ws_aspect", handle_ws_aspect },
{ "display_aspect", handle_display_aspect },
@@ -13851,6 +14111,8 @@ static const CmdEntry s_commands[] = {
{ "gl_coh_ring", handle_gl_coh_ring },
{ "gl_present_ring", handle_gl_present_ring },
{ "present_ring", handle_present_ring },
{ "gpu_timeline", handle_gpu_timeline },
{ "nclip_stats", handle_nclip_stats },
{ "frame_perf", handle_frame_perf },
{ "gl_ws_ablate", handle_gl_ws_ablate },
{ "gl_interp", handle_gl_interp },
+4 -2
View File
@@ -1848,7 +1848,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
uint32_t vanilla =
((int32_t)cpu->gpr[rs] < (int32_t)cpu->gpr[rt]) ? 1u : 0u;
uint32_t kept = vanilla;
if (!psx_ws_aspect_cone_site(cpu, pc, insn, vanilla, &kept))
if (!psx_ws_cull_scale_site(pc, insn, cpu->gpr[rs], cpu->gpr[rt], &kept) &&
!psx_ws_aspect_cone_site(cpu, pc, insn, vanilla, &kept))
(void)psx_ws_cull_keep_site(pc, insn, vanilla, &kept);
cpu->gpr[rd] = kept;
cpu->gpr[0] = 0;
@@ -1858,7 +1859,8 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
{
uint32_t vanilla = (cpu->gpr[rs] < cpu->gpr[rt]) ? 1u : 0u;
uint32_t kept = vanilla;
(void)psx_ws_cull_keep_site(pc, insn, vanilla, &kept);
if (!psx_ws_cull_scale_site(pc, insn, cpu->gpr[rs], cpu->gpr[rt], &kept))
(void)psx_ws_cull_keep_site(pc, insn, vanilla, &kept);
cpu->gpr[rd] = kept;
cpu->gpr[0] = 0;
return 0;
+4
View File
@@ -30,6 +30,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gpu_timeline.h"
/* Word-aligned main-RAM offset of a DMA address (MADR / stepped cursor)
* through the live geometry: retail folds to 2 MiB exactly as the DMAC's
@@ -727,6 +728,8 @@ static void gpu_ll_complete(void *opaque, int hit_limit) {
(void)opaque;
gpu_ot_stats.completes++;
gpu_ot_record_walk_stats(psx_cycle_count - gpu_ot_start_cycle);
gpu_timeline_note(GTL_LL_END, gpu_linked_list.nodes_processed,
gpu_linked_list.total_words);
channels[2].madr = hit_limit ? gpu_linked_list.current_addr
: 0x00FFFFFFu;
gpu_ws_end_linked_list();
@@ -754,6 +757,7 @@ static void start_async_gpu_linked_list(void) {
* header and payload read at its consumption boundary. */
gpu_ws_prepass_linked_list(start_addr);
dma_gpu_ll_start(&gpu_linked_list, start_addr, 0x40000u);
gpu_timeline_note(GTL_LL_START, start_addr, 0);
event_ring_record_aux(EV_DMA_SCHED, 2u, channels[2].chcr);
gpu_ot_stats.starts++;
gpu_ot_start_cycle = psx_cycle_count;
+120 -15
View File
@@ -45,6 +45,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gpu_timeline.h"
/* Word-aligned main-RAM key for a primitive/OT address through the live
* geometry (retail: the DMAC's 0x1FFFFC fold). */
@@ -262,7 +263,12 @@ void gpu_ws_set_precise_nclip(int on) {
* PGXP correction is disabled. Re-derive the internal transport arm. */
gpu_pgxp_rederive_enable();
}
int gpu_ws_precise_nclip_enabled(void) { return ws_precise_nclip_cfg && ws_active(); }
/* Same "reveals extra world" test the exact-NCLIP branch consumers use
* (psx_ws_x_margin() > 0): squash AND native-wide. ws_active() alone is the
* squash factor, which native-wide never sets, so the producer was dead there. */
int gpu_ws_precise_nclip_enabled(void) {
return ws_precise_nclip_cfg && psx_ws_x_margin() > 0;
}
void gpu_ws_set_gameplay_state_gate(uint32_t addr,
const uint32_t *values, int nvalues) {
if (nvalues < 0) nvalues = 0;
@@ -742,6 +748,59 @@ int psx_ws_cull_keep_site(uint32_t pc, uint32_t instr, uint32_t vanilla,
return 0;
}
typedef struct {
uint32_t address;
uint32_t expected;
uint32_t operand; /* 0 = rs, 1 = rt */
int32_t half_extent;
} WsCullScaleSite;
static WsCullScaleSite ws_cull_scale_sites[WS_EXPLICIT_CULL_SITES_MAX];
static int ws_cull_scale_n = 0;
void gpu_ws_set_cull_scale_sites(const uint32_t *addresses,
const uint32_t *expected,
const uint32_t *operands,
const uint32_t *half_extents, int nsites) {
if (nsites < 0) nsites = 0;
if (nsites > WS_EXPLICIT_CULL_SITES_MAX) nsites = WS_EXPLICIT_CULL_SITES_MAX;
ws_cull_scale_n = nsites;
for (int i = 0; i < nsites; i++) {
ws_cull_scale_sites[i].address = addresses[i] & 0x1FFFFFFFu;
ws_cull_scale_sites[i].expected = expected[i];
ws_cull_scale_sites[i].operand = operands[i] ? 1u : 0u;
ws_cull_scale_sites[i].half_extent = (int32_t)half_extents[i];
}
}
/* A camera-space frustum edge authored for a half_extent-pixel half-screen
* (x < half*z/H) widens to the live view by scaling the bound by
* (half + margin) / half. 64-bit intermediate; saturates to int32. */
int32_t psx_ws_cull_scale(int32_t bound, int32_t half_extent) {
const int32_t margin = psx_ws_x_margin();
if (margin <= 0 || half_extent <= 0) return bound;
int64_t v = (int64_t)bound * (int64_t)(half_extent + margin) / half_extent;
if (v > INT32_MAX) v = INT32_MAX;
if (v < INT32_MIN) v = INT32_MIN;
return (int32_t)v;
}
int psx_ws_cull_scale_site(uint32_t pc, uint32_t instr, uint32_t rs_value,
uint32_t rt_value, uint32_t *out) {
const uint32_t phys = pc & 0x1FFFFFFFu;
for (int i = 0; i < ws_cull_scale_n; i++) {
const WsCullScaleSite *site = &ws_cull_scale_sites[i];
if (site->address != phys || site->expected != instr) continue;
if (site->operand == 0)
rs_value = (uint32_t)psx_ws_cull_scale((int32_t)rs_value, site->half_extent);
else
rt_value = (uint32_t)psx_ws_cull_scale((int32_t)rt_value, site->half_extent);
const uint32_t funct = instr & 0x3Fu;
if (out)
*out = funct == 0x2Au
? (((int32_t)rs_value < (int32_t)rt_value) ? 1u : 0u)
: ((rs_value < rt_value) ? 1u : 0u);
return 1;
}
return 0;
}
typedef struct {
uint32_t address;
uint32_t expected;
@@ -1929,35 +1988,67 @@ static int ws_hud_command_words(uint32_t command_addr, uint32_t *words,
return 1;
}
static GpuWsTagStats ws_hud_tag_stats;
static GpuWsTagStats ws_background_tag_stats;
void gpu_ws_get_tag_stats(GpuWsTagStats *hud, GpuWsTagStats *background) {
if (hud) *hud = ws_hud_tag_stats;
if (background) *background = ws_background_tag_stats;
}
static void ws_tag_stats_note_lookup(GpuWsTagStats *stats, int result) {
switch (result) {
case WS_HUD_ANCHOR_HIT:
stats->hit++;
stats->last_hit_frame = (uint32_t)s_frame_count;
break;
case WS_HUD_ANCHOR_STALE: stats->stale++; break;
case WS_HUD_ANCHOR_GUARD_MISMATCH: stats->guard_mismatch++; break;
default: break;
}
}
void gpu_ws_tag_hud_prim(uint32_t prim, int anchor) {
if (!ws_native_wide_configured()) return;
if ((prim & 3u) != 0 || prim > UINT32_MAX - 4u) return;
ws_hud_tag_stats.tag_calls++;
uint32_t command_addr = prim + 4u;
uint32_t words[12];
uint32_t word_count = 0;
if (!ws_hud_command_words(command_addr, words, &word_count))
if (!ws_native_wide_configured() ||
(prim & 3u) != 0 || prim > UINT32_MAX - 4u ||
!ws_hud_command_words(command_addr, words, &word_count)) {
ws_hud_tag_stats.tag_rejected++;
return;
}
WsPrepassPacketGuard guard =
ws_prepass_packet_guard(words, word_count);
ws_hud_anchor_insert(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE,
GPU_RAM_KEY(command_addr), anchor, &guard,
(uint32_t)s_frame_count);
ws_hud_tag_stats.last_tag_frame = (uint32_t)s_frame_count;
}
void gpu_ws_tag_background_prim(uint32_t prim) {
if (!ws_native_wide_configured() || (prim & 3u) ||
prim > UINT32_MAX - 4u) return;
ws_background_tag_stats.tag_calls++;
uint32_t words[12], count = 0;
if (!ws_hud_command_words(prim + 4u, words, &count)) return;
if (!ws_native_wide_configured() || (prim & 3u) ||
prim > UINT32_MAX - 4u ||
!ws_hud_command_words(prim + 4u, words, &count)) {
ws_background_tag_stats.tag_rejected++;
return;
}
uint32_t op = words[0] >> 24;
if (op < 0x20u || op > 0x3Fu || (op & 2u)) return;
if (op < 0x20u || op > 0x3Fu || (op & 2u)) {
ws_background_tag_stats.tag_rejected++;
return;
}
WsPrepassPacketGuard guard = ws_prepass_packet_guard(words, count);
ws_hud_anchor_insert(ws_background_tags, WS_HUD_ANCHOR_TABLE_SIZE,
(prim + 4u) & 0x1FFFFCu, 0, &guard,
(uint32_t)s_frame_count);
ws_background_tags_used = 1;
ws_background_tag_frame = (uint32_t)s_frame_count;
ws_background_tag_stats.last_tag_frame = (uint32_t)s_frame_count;
}
int gpu_ws_background_stretch_active(void) {
@@ -1982,9 +2073,11 @@ static int ws_nw_explicit_background(void) {
gp0_words_needed <= 0 || gp0_words_needed > 12) return 0;
uint32_t addr = gp0_cmd_source_addr & 0x1FFFFFFFu;
if (addr >= 0x00200000u || (addr & 3u)) return 0;
return ws_hud_anchor_lookup(ws_background_tags, WS_HUD_ANCHOR_TABLE_SIZE,
addr, gp0_cmd_buf, (uint32_t)gp0_words_needed,
(uint32_t)s_frame_count, NULL);
int result = ws_hud_anchor_lookup_result(
ws_background_tags, WS_HUD_ANCHOR_TABLE_SIZE, addr, gp0_cmd_buf,
(uint32_t)gp0_words_needed, (uint32_t)s_frame_count, NULL);
ws_tag_stats_note_lookup(&ws_background_tag_stats, result);
return result == WS_HUD_ANCHOR_HIT;
}
void gpu_ws_tag_black_reveal_rect(uint32_t prim) {
@@ -2024,10 +2117,12 @@ static int ws_nw_explicit_hud_delta(int32_t *out_delta) {
uint32_t command_addr = gp0_cmd_source_addr & 0x1FFFFFFFu;
if (command_addr >= psx_ram_live_bytes() || (command_addr & 3u)) return 0;
int anchor = 0;
if (!ws_hud_anchor_lookup(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE,
command_addr, gp0_cmd_buf,
(uint32_t)gp0_words_needed,
(uint32_t)s_frame_count, &anchor))
int result = ws_hud_anchor_lookup_result(
ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE, command_addr,
gp0_cmd_buf, (uint32_t)gp0_words_needed, (uint32_t)s_frame_count,
&anchor);
ws_tag_stats_note_lookup(&ws_hud_tag_stats, result);
if (result != WS_HUD_ANCHOR_HIT)
return 0;
int32_t off = ws_nw_offset();
if (out_delta)
@@ -3257,6 +3352,7 @@ void gpu_vblank_flush_present(void) {
void gpu_vblank_tick(void) {
lcf ^= 1;
gpu_timeline_note(GTL_VBLANK, lcf, 0);
/* GPUSTAT.13 (interlace FIELD): on real hardware this alternates per
* field while GP1(08h) vertical interlace is on, in antiphase with the
* even/odd-lines bit 31 during active display. Modeled here at vblank
@@ -4652,6 +4748,7 @@ static void gp0_exec_fill_rect(void) {
* VRAM. Routed through the renderer so it also fills the hi-res
* supersampling mirror (no-op cost when supersampling is off). */
gr_fill_rect((int)dst_x, (int)dst_y, (int)width, (int)height, color16);
gpu_timeline_note(GTL_FILL, dst_x | (dst_y << 16), width | (height << 16));
/* 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
@@ -4706,6 +4803,7 @@ static void gp0_exec_draw_area_tl(void) {
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 */
gpu_timeline_note(GTL_DRAW_AREA, draw_area_left, draw_area_top);
}
static void gp0_exec_draw_area_br(void) {
@@ -5331,7 +5429,13 @@ static GpuGp0RingEntry *gp0_ring = NULL; /* lazy-alloc on first record */
static uint64_t gp0_ring_seq = 0; /* total commands recorded */
static uint32_t gp0_ring_head = 0; /* next write slot */
/* Capture mark (debug_server_capture_mark): stop overwriting so the window
* that led up to an operator-reported glitch survives until it is read. */
static int gp0_ring_frozen = 0;
void gpu_gp0_ring_set_frozen(int frozen) { gp0_ring_frozen = frozen ? 1 : 0; }
static void gp0_ring_record(const uint32_t *words, int n) {
if (gp0_ring_frozen) return;
if (!gp0_ring) {
gp0_ring = (GpuGp0RingEntry *)calloc(GP0_RING_CAP, sizeof(*gp0_ring));
if (!gp0_ring) return; /* OOM — capture disabled, no other effect */
@@ -5996,6 +6100,7 @@ static void gp1_display_area_start(uint32_t val) {
display_area_x = val & 0x3FF;
display_area_y = (val >> 10) & 0x1FF;
ws_note_display_base(display_area_x); /* learn the display buffer set (native-wide) */
gpu_timeline_note(GTL_DISP_START, display_area_x, display_area_y);
}
static void gp1_h_display_range(uint32_t val) {
+124 -13
View File
@@ -73,6 +73,7 @@
#include "psx_savestate_menu.h"
#include "host_time.h"
#include "latency_ring.h"
#include "gpu_timeline.h"
#include "frame_pacing.h"
#include "psx_rewind.h"
@@ -182,6 +183,9 @@ typedef void (APIENTRY *PFN_glEndQuery)(GLenum);
typedef void (APIENTRY *PFN_glGetQueryObjectui64v)(GLuint, GLenum, GLuint64 *);
typedef void (APIENTRY *PFN_glGetQueryObjectiv)(GLuint, GLenum, GLint *);
typedef void (APIENTRY *PFN_glQueryCounter)(GLuint, GLenum);
/* Buffer mapping for asynchronous pixel-pack readback (presented-image ring). */
typedef void * (APIENTRY *PFN_glMapBuffer)(GLenum, GLenum);
typedef GLboolean (APIENTRY *PFN_glUnmapBuffer)(GLenum);
#ifndef GL_TIME_ELAPSED
#define GL_TIME_ELAPSED 0x88BF
#endif
@@ -195,6 +199,8 @@ typedef void (APIENTRY *PFN_glQueryCounter)(GLuint, GLenum);
#define GL_QUERY_RESULT_AVAILABLE 0x8867
#endif
static PFN_glMapBuffer p_glMapBuffer;
static PFN_glUnmapBuffer p_glUnmapBuffer;
static PFN_glCreateShader p_glCreateShader;
static PFN_glShaderSource p_glShaderSource;
static PFN_glCompileShader p_glCompileShader;
@@ -297,6 +303,9 @@ static int load_modern_gl(void) {
p_glGetQueryObjectui64v = (void *)SDL_GL_GetProcAddress("glGetQueryObjectui64v");
p_glGetQueryObjectiv = (void *)SDL_GL_GetProcAddress("glGetQueryObjectiv");
p_glQueryCounter = (void *)SDL_GL_GetProcAddress("glQueryCounter");
/* Optional: only the debug presented-image ring maps pack buffers. */
p_glMapBuffer = (void *)SDL_GL_GetProcAddress("glMapBuffer");
p_glUnmapBuffer = (void *)SDL_GL_GetProcAddress("glUnmapBuffer");
#undef LOAD
return ok;
}
@@ -1635,18 +1644,21 @@ static void mark_prim_dirty(const int *xs, const int *ys, int n, int textured) {
* boundary (MMX6: draw area alternates y=0/y=240, both bands in ONE wide
* surface) bleed into the OTHER band's rows — presented one frame later as
* top/bottom edge flicker (16:9 GL only). */
/* Wide-surface scissor: full wide width, draw-area rows (see above). Every
* draw into a wide surface uses it, including the full-screen-overlay pass. */
static void wide_band_scissor(void) {
int sy = s_area_y1, sh = s_area_y2 - s_area_y1 + 1;
if (sy < 0) { sh += sy; sy = 0; }
if (sy + sh > VRAM_H) sh = VRAM_H - sy;
if (sh < 0) sh = 0;
glEnable(GL_SCISSOR_TEST);
glScissor(0, sy * s_scale, g_wide_w * s_scale, sh * s_scale);
}
static void wide_target_begin(int dx, GLint uXoff, GLint uXhalf) {
if (s_ws_ablate != 3) /* ablate 3: no FBO rebind (draws land in hr — perf probe) */
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur);
glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale);
glEnable(GL_SCISSOR_TEST);
{
int sy = s_area_y1, sh = s_area_y2 - s_area_y1 + 1;
if (sy < 0) { sh += sy; sy = 0; }
if (sy + sh > VRAM_H) sh = VRAM_H - sy;
if (sh < 0) sh = 0;
glScissor(0, sy * s_scale, g_wide_w * s_scale, sh * s_scale);
}
wide_band_scissor();
p_glUniform1f(uXoff, (float)dx);
p_glUniform1f(uXhalf, (float)g_wide_w / 2.0f);
}
@@ -2159,7 +2171,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
/* Draw a flat-colored rect (GEO program) DIRECTLY into the active wide surface
* at wide-space coords [wx, wx+ww) × [y, y+h). Used only by the full-screen-
* overlay path; positions are already in wide space so u_xoff stays 0. Mirrors
* raster_flat_rect(&wt, ...) in sw_draw_flat_rect. Caller stays inside
* raster_flat_rect(&wt, ...) in sw_draw_flat_rect, including its draw-area row
* clip. Caller stays inside
* hr_begin/hr_end. */
static void wide_flat_rect_direct(int wx, int y, int ww, int h, uint16_t c, int semi) {
if (ww <= 0 || h <= 0) return;
@@ -2172,12 +2185,13 @@ static void wide_flat_rect_direct(int wx, int y, int ww, int h, uint16_t c, int
fx0,fy0,r,g,b,a, fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a,
fx1,fy0,r,g,b,a, fx0,fy1,r,g,b,a, fx1,fy1,r,g,b,a,
};
/* Wide target: positions already wide-space so u_xoff = 0; full-width
* scissor; u_xhalf = g_wide_w/2. */
/* Wide target: positions already wide-space so u_xoff = 0; full-width,
* draw-area-rows scissor like rt_wide() — a full-height scissor let an
* overlay-width rect that canonically clips at a vertical double-buffer
* band boundary paint the OTHER band's rows; u_xhalf = g_wide_w/2. */
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, g_wide_cur);
glViewport(0, 0, g_wide_w * s_scale, VRAM_H * s_scale);
glEnable(GL_SCISSOR_TEST);
glScissor(0, 0, g_wide_w * s_scale, VRAM_H * s_scale); /* full surface (rt_wide) */
wide_band_scissor();
if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND);
mask_stencil(s_mask_set);
p_glUseProgram(s_geo_prog);
@@ -4227,7 +4241,95 @@ static void gl_draw_osd_image(const uint32_t *px, int ow, int oh,
}
/* Composite host toast + volume bar into the default framebuffer, then swap. */
/* ---- presented-image ring feed (debug tools) ------------------------------
* Every present, before the host OSD is drawn: blit the composed default
* framebuffer (what the window shows, letterbox included) down to a
* PRESENT_IMAGE_RING_H-tall thumbnail FBO, and read it back through a pair of
* pixel-pack buffers so the GPU->CPU copy of frame N completes while frame
* N+1 is presented (no pipeline stall). Tagged with the frame number at issue
* time. Absent buffer-mapping entry points disable only this feed. */
#ifndef PSX_NO_DEBUG_TOOLS
#include "present_image_ring.h"
extern uint64_t s_frame_count;
#define PSXGL_PIXEL_PACK_BUFFER 0x88EB
#define PSXGL_STREAM_READ 0x88E1
#define PSXGL_READ_ONLY 0x88B8
static GLuint s_pir_fbo, s_pir_tex, s_pir_pbo[2];
static int s_pir_w, s_pir_pending[2], s_pir_pending_w[2], s_pir_idx;
static uint32_t s_pir_frame[2];
static int s_pir_disabled;
static void present_image_ring_capture_gl(void) {
if (s_pir_disabled || !p_glMapBuffer || !p_glUnmapBuffer) return;
if (!present_image_ring_accepting()) return;
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
const int tw = present_image_ring_thumb_w(ww, wh);
const int th = PRESENT_IMAGE_RING_H;
if (tw <= 0) return;
if (!s_pir_fbo) {
glGenTextures(1, &s_pir_tex);
glBindTexture(GL_TEXTURE_2D, s_pir_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, PRESENT_IMAGE_RING_MAX_W, th, 0,
GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
p_glGenFramebuffers(1, &s_pir_fbo);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_pir_fbo);
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, s_pir_tex, 0);
const GLenum st = p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER);
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
if (st != PSXGL_FRAMEBUFFER_COMPLETE) { s_pir_disabled = 1; return; }
p_glGenBuffers(2, s_pir_pbo);
for (int i = 0; i < 2; i++) {
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, s_pir_pbo[i]);
p_glBufferData(PSXGL_PIXEL_PACK_BUFFER,
(ptrdiff_t)PRESENT_IMAGE_RING_MAX_W * th * 4, NULL,
PSXGL_STREAM_READ);
}
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, 0);
}
/* Downscale the presented image (default framebuffer back buffer). */
glDisable(GL_SCISSOR_TEST);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_pir_fbo);
p_glBlitFramebuffer(0, 0, ww, wh, 0, 0, tw, th, GL_COLOR_BUFFER_BIT, GL_LINEAR);
/* Queue this frame's readback into the current pack buffer. */
const int cur = s_pir_idx;
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, s_pir_fbo);
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, s_pir_pbo[cur]);
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glReadPixels(0, 0, tw, th, GL_RGBA, GL_UNSIGNED_BYTE, (void *)0);
s_pir_pending[cur] = 1;
s_pir_pending_w[cur] = tw;
s_pir_frame[cur] = (uint32_t)s_frame_count;
/* Harvest the previous frame's buffer (its copy finished during this one). */
const int prev = cur ^ 1;
if (s_pir_pending[prev]) {
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, s_pir_pbo[prev]);
const uint8_t *px = (const uint8_t *)p_glMapBuffer(PSXGL_PIXEL_PACK_BUFFER,
PSXGL_READ_ONLY);
if (px) {
present_image_ring_push(s_pir_frame[prev], px, s_pir_pending_w[prev],
s_pir_pending_w[prev] * 4, 1 /* GL rows bottom-up */);
p_glUnmapBuffer(PSXGL_PIXEL_PACK_BUFFER);
}
s_pir_pending[prev] = 0;
}
p_glBindBuffer(PSXGL_PIXEL_PACK_BUFFER, 0);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
s_pir_idx = prev;
s_pir_w = tw;
}
#endif
static void gl_swap_with_osd(void) {
#ifndef PSX_NO_DEBUG_TOOLS
if (s_ctx) present_image_ring_capture_gl();
#endif
if (s_present_prog && s_ctx) {
int ww = 0, wh = 0;
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
@@ -4448,6 +4550,7 @@ int gl_renderer_present_hold_last(void) {
* full-window image (GL without interp). */
hold_capture_drawable();
}
gpu_timeline_note(GTL_PRESENT, GTL_PATH_HOLD, 0);
latency_ring_mark(LAT_SWAP_BEGIN);
gl_swap_with_osd();
latency_ring_mark(LAT_SWAP_END);
@@ -4470,6 +4573,8 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
!psx_present_vsync_owns_cadence() &&
!gl_renderer_interpolation_owns_cadence()) {
s_probe_skip++;
gpu_timeline_note(GTL_PRESENT_SKIP, GTL_PATH_VRAM_FBO,
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
gl_perf_present_enter();
gl_perf_present_exit(0);
return;
@@ -4509,6 +4614,8 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
present_target_quad(s_hr_tex, VRAM_W, VRAM_H,
disp_x, disp_y, w, h, linear, lx, ly, lw, lh, 1, 1);
pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh);
gpu_timeline_note(GTL_PRESENT, GTL_PATH_VRAM_FBO,
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
hold_capture_drawable();
latency_ring_mark(LAT_SWAP_BEGIN);
gl_swap_with_osd();
@@ -4581,6 +4688,8 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
!psx_present_vsync_owns_cadence() &&
!gl_renderer_interpolation_owns_cadence()) {
s_probe_skip++;
gpu_timeline_note(GTL_PRESENT_SKIP, GTL_PATH_WIDE_FBO,
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
gl_perf_present_enter();
gl_perf_present_exit(1);
return 1;
@@ -4613,6 +4722,8 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
present_target_quad(tex, g_wide_w, VRAM_H,
0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh, 1, 1);
pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh);
gpu_timeline_note(GTL_PRESENT, GTL_PATH_WIDE_FBO,
(uint32_t)disp_x | ((uint32_t)disp_y << 16));
hold_capture_drawable();
latency_ring_mark(LAT_SWAP_BEGIN);
gl_swap_with_osd();
+45
View File
@@ -0,0 +1,45 @@
/* GPU frame timeline ring — see gpu_timeline.h. */
#include "gpu_timeline.h"
#include "psx_cycles.h"
extern uint64_t s_frame_count; /* debug_server.c */
static GpuTimelineEntry s_ring[GPU_TIMELINE_CAP];
static uint64_t s_total = 0;
static int s_frozen = 0;
void gpu_timeline_note(uint8_t kind, uint32_t a, uint32_t b) {
if (s_frozen) return;
GpuTimelineEntry *e = &s_ring[s_total & (GPU_TIMELINE_CAP - 1u)];
e->cyc = psx_cycle_count;
e->frame = (uint32_t)s_frame_count;
e->a = a;
e->b = b;
e->kind = kind;
s_total++;
}
void gpu_timeline_set_frozen(int frozen) { s_frozen = frozen ? 1 : 0; }
uint64_t gpu_timeline_total(void) { return s_total; }
int gpu_timeline_get(uint64_t seq, GpuTimelineEntry *out) {
if (seq >= s_total || s_total - seq > GPU_TIMELINE_CAP) return 0;
*out = s_ring[seq & (GPU_TIMELINE_CAP - 1u)];
return 1;
}
const char *gpu_timeline_kind_name(uint8_t kind) {
switch (kind) {
case GTL_VBLANK: return "vblank";
case GTL_DISP_START: return "disp_start";
case GTL_DRAW_AREA: return "draw_area";
case GTL_LL_START: return "ll_start";
case GTL_LL_END: return "ll_end";
case GTL_PRESENT: return "present";
case GTL_PRESENT_SKIP: return "present_skip";
case GTL_DISP_CAPTURE: return "disp_capture";
case GTL_FILL: return "fill";
default: return "?";
}
}
+5 -7
View File
@@ -2800,8 +2800,10 @@ static void vkb_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,ui
/* Full-screen-overlay wide pass (pause gray-filter / load fade): draw a flat
* rect covering the FULL wide width [0, wide_w) x [y, y+h) directly into the
* active wide surface (positions already wide-space, u_xoff = 0). Mirrors GL's
* wide_flat_rect_direct: full-surface scissor (the overlay must dim the
* revealed margins too). The 6 verts are consumed privately (s_vbase bumped)
* wide_flat_rect_direct: full wide width (the overlay must dim the revealed
* margins too) but only the draw-area rows, like SW rt_wide() — a full-height
* scissor let a rect that canonically clips at a vertical double-buffer band
* boundary paint the OTHER band's rows. The 6 verts are consumed privately (s_vbase bumped)
* so they never join a canonical batch. */
static void wide_overlay_rect(int y, int h, uint16_t c, int semi) {
if (s_wide_cur < 0 || !s_ready) return;
@@ -2813,12 +2815,8 @@ static void wide_overlay_rect(int y, int h, uint16_t c, int semi) {
push_vert(x0, fy0, col); push_vert(x1, fy1, col); push_vert(x0, fy1, col);
s_vbase += s_vcount; s_vcount = 0; /* consume privately */
int blend = (semi < 0) ? 0 : (semi + 1);
int S = s_scale;
VkCommandBuffer cb = begin_oneshot();
wide_pass_begin(cb);
/* Overlay covers the whole surface: override the band scissor. */
VkRect2D sc = { { 0, 0 }, { (uint32_t)(s_wide_w * S), (uint32_t)(VRAM_H * S) } };
p_vkCmdSetScissor(cb, 0, 1, &sc);
wide_pass_begin(cb); /* band scissor: full wide width, draw-area rows */
bind_masked(cb, 0, 0, blend, s_mask_check, s_mask_set);
GeoPush gp = { px_shift(), 0.0f, (float)s_wide_w / 2.0f };
p_vkCmdPushConstants(cb, s_pl_geo, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof gp, &gp);
+78 -5
View File
@@ -2,6 +2,7 @@
#include "cpu_state.h"
#include "nd_intro_ot.h"
#include "pgxp.h"
#include "gte_nclip_stats.h"
#include <algorithm>
#include <cstdlib>
#include <cstdio>
@@ -9,6 +10,8 @@
extern "C" uint32_t psx_read_word(uint32_t addr);
extern "C" int gpu_ws_precise_nclip_enabled(void);
extern "C" uint64_t s_frame_count; /* debug_server.c */
extern "C" uint32_t g_debug_last_store_pc; /* debug_server.c */
namespace PSXRecomp {
namespace GTE {
@@ -946,10 +949,63 @@ extern "C" void gte_nclip_precise_stats(uint64_t *hits, uint64_t *fallbacks,
if (fallbacks) *fallbacks = s_nclip_fallbacks;
if (disagreements) *disagreements = s_nclip_disagreements;
}
extern "C" int gte_nclip_precise_bltz(int32_t native_mac0) {
if (!s_nclip_last_precise_valid || native_mac0 != s_nclip_last_native)
return native_mac0 < 0;
return s_nclip_last_precise_sign < 0;
/* Always-on attribution tables (gte_nclip_stats.h): producers by guest
* function, consumers by branch PC. Open addressing; a full table drops new
* keys (counted in s_nclip_*_dropped) rather than evicting. */
static GteNclipFuncStat s_nclip_func[GTE_NCLIP_STAT_CAP]; /* key 1 = unknown */
static GteNclipSiteStat s_nclip_site[GTE_NCLIP_STAT_CAP];
static uint32_t s_nclip_func_dropped = 0, s_nclip_site_dropped = 0;
static inline uint32_t nclip_slot(uint32_t key) {
return (key * 2654435761u) >> 24; /* GTE_NCLIP_STAT_CAP == 256 */
}
template <typename T>
static T *nclip_find(T *table, uint32_t key, uint32_t T::*field,
uint32_t *dropped) {
for (uint32_t i = 0, s = nclip_slot(key); i < GTE_NCLIP_STAT_CAP;
++i, s = (s + 1) & (GTE_NCLIP_STAT_CAP - 1)) {
T *e = &table[s];
if (e->*field == key) return e;
if (e->*field == 0) { e->*field = key; return e; }
}
++*dropped;
return nullptr;
}
extern "C" int gte_nclip_func_stats(GteNclipFuncStat *out, int max) {
int n = 0;
for (int i = 0; i < GTE_NCLIP_STAT_CAP && n < max; ++i)
if (s_nclip_func[i].func) out[n++] = s_nclip_func[i];
return n;
}
extern "C" int gte_nclip_site_stats(GteNclipSiteStat *out, int max) {
int n = 0;
for (int i = 0; i < GTE_NCLIP_STAT_CAP && n < max; ++i)
if (s_nclip_site[i].pc) out[n++] = s_nclip_site[i];
return n;
}
extern "C" void gte_nclip_stats_reset(void) {
memset(s_nclip_func, 0, sizeof s_nclip_func);
memset(s_nclip_site, 0, sizeof s_nclip_site);
s_nclip_func_dropped = s_nclip_site_dropped = 0;
}
static int s_nclip_exact_enabled = 1;
extern "C" void gte_nclip_exact_set_enabled(int on) { s_nclip_exact_enabled = on ? 1 : 0; }
extern "C" int gte_nclip_exact_enabled(void) { return s_nclip_exact_enabled; }
extern "C" int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc) {
const int32_t native = native_mac0 < 0 ? -1 : (native_mac0 > 0 ? 1 : 0);
const bool usable = s_nclip_exact_enabled && s_nclip_last_precise_valid &&
native_mac0 == s_nclip_last_native;
const int32_t sign = usable ? s_nclip_last_precise_sign : native;
if (!s_gte_replay_sandbox && s_speculative_depth == 0) {
if (GteNclipSiteStat *e = nclip_find(s_nclip_site, pc,
&GteNclipSiteStat::pc,
&s_nclip_site_dropped)) {
e->evals++;
if (!usable) e->fallbacks++;
else if (sign != native) e->flips++;
e->last_frame = (uint32_t)s_frame_count;
}
}
return sign;
}
void gte_nclip(GTEState* gte, uint32_t instr) {
@@ -983,8 +1039,25 @@ void gte_nclip(GTEState* gte, uint32_t instr) {
const int64_t cross = dx10 * dy20 - dy10 * dx20;
s_nclip_last_precise_sign = cross < 0 ? -1 : (cross > 0 ? 1 : 0);
s_nclip_last_precise_valid = true;
if ((cross > 0 && out <= 0) || (cross < 0 && out >= 0))
const int32_t native_sign = out < 0 ? -1 : (out > 0 ? 1 : 0);
const bool disagree = native_sign != s_nclip_last_precise_sign;
if (disagree)
s_nclip_disagreements++;
/* Last guest store before this NCLIP: codegen stamps it on every
* store, so it lands inside the emitter that issued the NCLIP (the
* function stamp is not maintained for static code). */
const uint32_t key = g_debug_last_store_pc ? g_debug_last_store_pc : 1u;
if (GteNclipFuncStat *e = nclip_find(s_nclip_func, key,
&GteNclipFuncStat::func,
&s_nclip_func_dropped)) {
e->nclips++;
if (disagree) e->disagree++;
if (sx0 == -0x400 || sx0 == 0x3FF || sy0 == -0x400 || sy0 == 0x3FF ||
sx1 == -0x400 || sx1 == 0x3FF || sy1 == -0x400 || sy1 == 0x3FF ||
sx2 == -0x400 || sx2 == 0x3FF || sy2 == -0x400 || sy2 == 0x3FF)
e->saturated++;
e->last_frame = (uint32_t)s_frame_count;
}
} else if (gpu_ws_precise_nclip_enabled() &&
!s_gte_replay_sandbox && s_speculative_depth == 0) {
s_nclip_fallbacks++;
+16 -2
View File
@@ -110,6 +110,8 @@ static void apply_defaults(void) {
add_bind(HOST_KEYMAP_SCANLINES, (int)SDLK_F6, (int)SDL_SCANCODE_F6, 0);
if (want_default(HOST_KEYMAP_TURBO_TOGGLE))
add_bind(HOST_KEYMAP_TURBO_TOGGLE, (int)SDLK_F9, (int)SDL_SCANCODE_F9, 0);
if (want_default(HOST_KEYMAP_CAPTURE_MARK))
add_bind(HOST_KEYMAP_CAPTURE_MARK, (int)SDLK_m, (int)SDL_SCANCODE_M, KMOD_CTRL);
}
/* Parse one "Ctrl+Alt+PageUp" token into key+mods. */
@@ -169,6 +171,7 @@ static HostKeymapAction action_for_key(const char *name) {
if (ieq(name, "SaveStateMenu")) return HOST_KEYMAP_SAVE_STATE_MENU;
if (ieq(name, "Scanlines")) return HOST_KEYMAP_SCANLINES;
if (ieq(name, "TurboToggle")) return HOST_KEYMAP_TURBO_TOGGLE;
if (ieq(name, "CaptureMark")) return HOST_KEYMAP_CAPTURE_MARK;
return HOST_KEYMAP_ACTION_COUNT;
}
@@ -223,6 +226,17 @@ void host_keymap_load(const char *config_ini_path) {
apply_defaults();
}
/* A physical key event reports which side's modifier is down (KMOD_LCTRL or
* KMOD_RCTRL), while a binding means "either Ctrl". Compare on the side-less
* Ctrl/Alt/Shift set so Ctrl+X binds match a real left- or right-Ctrl press. */
static int canonical_mods(int mod) {
int m = 0;
if (mod & KMOD_CTRL) m |= KMOD_CTRL;
if (mod & KMOD_ALT) m |= KMOD_ALT;
if (mod & KMOD_SHIFT) m |= KMOD_SHIFT;
return m;
}
int host_keymap_match_event(HostKeymapAction action, int keycode,
int scancode, int mod) {
const HostKeyAction *a;
@@ -234,7 +248,7 @@ int host_keymap_match_event(HostKeymapAction action, int keycode,
if (a->binds[i].keycode != keycode &&
(scancode <= 0 || a->binds[i].scancode != scancode))
continue;
if ((mod & relevant) == a->binds[i].mods) return 1;
if (canonical_mods(mod & relevant) == canonical_mods(a->binds[i].mods)) return 1;
}
return 0;
}
@@ -253,7 +267,7 @@ int host_keymap_down(HostKeymapAction action, const uint8_t *keys, int mod) {
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;
if (canonical_mods(mod & relevant) == canonical_mods(a->binds[i].mods)) return 1;
}
return 0;
}
+158 -31
View File
@@ -85,6 +85,9 @@ extern "C" void psx_event_step_conservative_env_init(void);
#include "game_options.h"
#include "mod_plugins.h"
#include "mod_runtime.h"
#include "mod_packages.h"
#include "present_image_ring.h"
#include "gpu_timeline.h"
#include "crc32.h"
#include "disc_identity.h"
#include "sbi_setup.h"
@@ -1012,6 +1015,12 @@ extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok)
host_osd_push(buf, 2000);
}
extern "C" void psx_frontend_on_savestate_refused(int is_load, int slot,
const char *reason) {
(void)is_load; (void)slot;
host_osd_push(reason, 3000);
}
extern "C" void psx_frontend_on_savestate_loaded(void) {
mod_runtime_on_savestate_loaded();
s_disabled_frame_presented = false;
@@ -1354,8 +1363,7 @@ extern "C" int psx_mod_set_fixed_display_aspect(
uint32_t numerator, uint32_t denominator) {
if (numerator == 0 || denominator == 0 ||
numerator > 99 || denominator > 99 ||
3u * numerator < 4u * denominator ||
9u * numerator > 32u * denominator) {
3u * numerator < 4u * denominator) {
std::fprintf(stderr,
"psxrecomp: mod rejected invalid display aspect %u:%u\n",
(unsigned)numerator, (unsigned)denominator);
@@ -1374,8 +1382,7 @@ extern "C" int psx_mod_set_adaptive_display_aspect(
const bool uncapped = max_numerator == 0 && max_denominator == 0;
if (!uncapped && (max_numerator == 0 || max_denominator == 0 ||
max_numerator > 99 || max_denominator > 99 ||
3u * max_numerator < 4u * max_denominator ||
9u * max_numerator > 32u * max_denominator)) {
3u * max_numerator < 4u * max_denominator)) {
std::fprintf(stderr,
"psxrecomp: mod rejected invalid adaptive display aspect %u:%u\n",
(unsigned)max_numerator, (unsigned)max_denominator);
@@ -6534,6 +6541,47 @@ struct NetplayVblankEpilogue {
};
/* Called from gpu_vblank_tick() at each simulated vblank. */
#ifndef PSX_NO_DEBUG_TOOLS
/* Headless has no present surface, but automated A/B runs still need the
* frame-indexed record of what a windowed build would show: the displayed
* band of the native-wide surface on a wide game frame, otherwise the 4:3
* display area. Same classification as the windowed present below. */
static void headless_present_image_ring_capture(void) {
if (!present_image_ring_accepting()) return;
GpuDisplayInfo di;
gpu_get_display_info(&di);
if (di.disabled || di.width == 0 || di.height == 0 || di.depth24) return;
const bool fmv_frame = !g_ws_engaged || gpu_ws_present_native_43() != 0;
static std::vector<uint32_t> buf;
int w = 0, h = 0;
if (!fmv_frame && ws_native_wide_active() && gr_wide_supported()) {
buf.resize((size_t)1024 * 4 * 512 * 4);
if (gr_wide_dump_full(buf.data(), (int)buf.size(), &w, &h,
(int)di.display_x) > 0 && h >= 512) {
const int s = h / 512;
const int y0 = (int)di.display_y * s, rows = (int)di.height * s;
if (y0 + rows <= h) {
present_image_ring_push_argb((uint32_t)s_frame_count,
buf.data() + (size_t)y0 * w,
w, rows, w);
return;
}
}
}
w = (int)di.width; h = (int)di.height;
buf.resize((size_t)w * h);
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++) {
const uint16_t p = gpu_vram_peek((int)(di.display_x + x),
(int)(di.display_y + y));
buf[(size_t)y * w + x] = 0xFF000000u | ((uint32_t)(p & 31) << 19) |
((uint32_t)((p >> 5) & 31) << 11) |
((uint32_t)((p >> 10) & 31) << 3);
}
present_image_ring_push_argb((uint32_t)s_frame_count, buf.data(), w, h, w);
}
#endif
static NetplayVblankEpilogue sdl_vblank_present_body(void) {
NetplayVblankEpilogue ep{};
/* Guest quantum for this vblank is complete. Drop top-level-resume armed
@@ -6753,6 +6801,17 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
(int)mod)) {
fast_forward_toggle_flip();
}
else if (!key_repeat &&
host_keymap_match_event(HOST_KEYMAP_CAPTURE_MARK,
(int)key, (int)scancode,
(int)mod)) {
#ifndef PSX_NO_DEBUG_TOOLS
debug_server_capture_mark();
host_osd_push("Capture marked (debug rings frozen)", 2000);
#else
host_osd_push("Capture mark needs a debug-tools build", 2000);
#endif
}
else if (!key_repeat &&
host_keymap_match_event(HOST_KEYMAP_DISPLAY_PERF,
(int)key, (int)scancode,
@@ -6981,7 +7040,34 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
}
#endif
/* Guest-affecting per-frame services run on EVERY guest frame, before any
* presentation early-out (headless, TCP turbo, load/FMV present skips).
* Widescreen engagement changes GTE projection and cull margins, and mod
* frame hooks may touch guest state, so neither may depend on whether this
* frame is presented — otherwise headless validation runs a different game
* than the windowed build. Hooks still run after all normal input sampling. */
mod_call_frame_hooks();
/* Resize-driven mode updates the pending aspect during BIOS boot too, but
* the actual wide compositor remains disengaged until game entry. No-op
* without a window. */
update_adaptive_widescreen();
/* Engage widescreen at game entry: BIOS boot stays authentic 4:3. */
if (!g_ws_engaged) {
if (fntrace_is_game_started()) {
g_ws_engaged = true;
g_ws_projection_mode = -1;
refresh_widescreen_projection();
}
} else {
refresh_widescreen_projection();
}
if (g_headless) {
#ifndef PSX_NO_DEBUG_TOOLS
headless_present_image_ring_capture();
#endif
ep.skip_pace = 1;
return ep;
}
@@ -7150,13 +7236,6 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
}
}
/* Mod hooks. Run after all normal input sampling. */
mod_call_frame_hooks();
/* Resize-driven mode updates the pending aspect during BIOS boot too, but
* the actual wide compositor remains disengaged until game entry. */
update_adaptive_widescreen();
/* Depth24 GP1(07h) retarget (MotK intro→crawl): keep the prior Swap for a
* few vblanks so stale trailing VRAM never flashes on the right edge.
* Must tick every present — gpu.c arms s_d24_present_hold and also
@@ -7164,16 +7243,6 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
* sticks and depth24_fix_trailing_margin blanks the whole FMV forever. */
if (gpu_depth24_present_hold_tick())
return ep;
/* Engage widescreen at game entry: BIOS boot stays authentic 4:3. */
if (!g_ws_engaged) {
if (fntrace_is_game_started()) {
g_ws_engaged = true;
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
* periodically presents live Replay VRAM so the display shows progress
@@ -7539,6 +7608,17 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
SDL_Rect src = { 0, 0, src_w, src_h };
SDL_UpdateTexture(sdl_texture, &src, sdl_pixel_buf,
(int)(src_w * sizeof(uint32_t)));
#ifndef PSX_NO_DEBUG_TOOLS
/* Presented-image ring (software/CPU present): the frame being shown. */
present_image_ring_push_argb((uint32_t)s_frame_count, sdl_pixel_buf,
(int)src_w, (int)src_h, (int)src_w);
#endif
{
GpuDisplayInfo tdi;
gpu_get_display_info(&tdi);
gpu_timeline_note(GTL_PRESENT, GTL_PATH_CPU,
tdi.display_x | ((uint32_t)tdi.display_y << 16));
}
/* Short FMV bands only update [0..src_h). Linear sampling at the bottom
* edge blends with uninitialized texels below (X11 SDL backends often
* show that as a thin white strip; Wayland may not). Pad one black row
@@ -12445,6 +12525,32 @@ int main(int argc, char** argv) {
launcher_boot_timing_mark("host:main_enter");
#endif
/* --audit-mod-plugins <manifest-root>...: build-time check, no emulation.
* Every trusted implementation this executable registered must be named
* by some authored manifest [[plugin]] id, or the plan can never activate
* it. Exit 0 when all are declared, 1 otherwise (ctest per title). */
for (int i = 1; i < argc; i++) {
if (std::strcmp(argv[i], "--audit-mod-plugins") != 0) continue;
std::vector<std::filesystem::path> roots;
for (int j = i + 1; j < argc && std::strncmp(argv[j], "--", 2) != 0; j++)
roots.emplace_back(argv[j]);
const PSXRecompV4::ModPluginAudit audit =
PSXRecompV4::mod_audit_registered_plugins(roots);
const std::vector<std::string> registered =
PSXRecompV4::mod_registered_plugin_ids();
std::fprintf(stdout, "mod plugin audit: %zu manifest(s), %zu declared id(s), "
"%zu registered id(s)\n", audit.manifests,
audit.declared.size(), registered.size());
for (const std::string& e : audit.errors)
std::fprintf(stdout, " manifest error: %s\n", e.c_str());
for (const std::string& id : audit.undeclared)
std::fprintf(stdout, " UNDECLARED (can never run): %s\n", id.c_str());
const bool ok = roots.size() > 0 && audit.errors.empty() &&
audit.undeclared.empty();
std::fprintf(stdout, "mod plugin audit: %s\n", ok ? "PASS" : "FAIL");
return ok ? 0 : 1;
}
/* Setup-host zip-root exe: after Generate & rebuild, hand off to the
* product binary under build-release/ (bios/, mods/, assets/, settings). */
#if defined(PSX_HAS_CODEGEN_SETUP_HOST)
@@ -12938,6 +13044,18 @@ int main(int argc, char** argv) {
addresses.data(), expected.data(), results.data(),
(int)addresses.size());
}
{
std::vector<uint32_t> addresses, expected, operands, halves;
for (const auto& site : gc.ws_cull_scale_sites) {
addresses.push_back(site.address);
expected.push_back(site.expected);
operands.push_back(site.operand);
halves.push_back(site.half_extent);
}
gpu_ws_set_cull_scale_sites(
addresses.data(), expected.data(), operands.data(),
halves.data(), (int)addresses.size());
}
{
std::vector<uint32_t> addresses, expected;
addresses.reserve(gc.ws_cull_angle_sites.size());
@@ -14824,13 +14942,20 @@ session_reboot:
} else {
/* Select the renderer backend BEFORE gpu_init() (which runs gr_init ->
* the backend's init on the VRAM buffer). Software is the default and
* the fallback; an unavailable OpenGL backend reverts to software. */
gr_set_backend(g_video_renderer == 2 ? GR_BACKEND_VULKAN :
g_video_renderer == 1 ? GR_BACKEND_OPENGL :
GR_BACKEND_SOFTWARE);
std::fprintf(stdout, "psxrecomp: renderer backend requested: %s\n",
g_video_renderer == 2 ? "vulkan" :
g_video_renderer == 1 ? "opengl" : "software");
* the fallback; an unavailable OpenGL backend reverts to software.
* Headless never creates a window or GPU context, so a GPU backend
* would only rasterize through its software fallback while refusing
* the context-bound services (native-wide compositing, hi-res
* readback). Select the software backend it would really run, so
* headless validation exercises the same wide compositor as a window. */
const int backend_renderer = g_headless ? 0 : g_video_renderer;
gr_set_backend(backend_renderer == 2 ? GR_BACKEND_VULKAN :
backend_renderer == 1 ? GR_BACKEND_OPENGL :
GR_BACKEND_SOFTWARE);
std::fprintf(stdout, "psxrecomp: renderer backend requested: %s%s\n",
backend_renderer == 2 ? "vulkan" :
backend_renderer == 1 ? "opengl" : "software",
g_headless && g_video_renderer != 0 ? " (headless)" : "");
}
gpu_init();
/* Internal-resolution supersampling (SSAA). Must follow gpu_init.
@@ -14921,10 +15046,12 @@ session_reboot:
/* Present-time screen-colour model (verified-enhancement LUT). Default raw
* is byte-identical; PSX_SCREEN env overrides this at scanout. */
gpu_set_screen_kind(g_video_screen);
if (g_video_scale > 1 || g_video_texfilter)
/* Report the REQUESTED scale: the GL backend's effective scale is only
* known once its context exists (it logs "internal scale Nx" then). */
if (requested_scale > 1 || g_video_texfilter)
std::fprintf(stdout,
"psxrecomp: supersampling %dx (antialiasing %s, texture filter %s)\n",
g_video_scale, g_video_aa ? "on" : "off",
"psxrecomp: supersampling %dx requested (antialiasing %s, texture filter %s)\n",
requested_scale, g_video_aa ? "on" : "off",
g_video_texfilter ? "bilinear" : "nearest");
if (g_video_screen != 0)
std::fprintf(stdout, "psxrecomp: screen-colour model %s\n",
+118
View File
@@ -0,0 +1,118 @@
#include "mod_media.h"
#include "iso_reader.h"
#include "psx_sha256.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <fstream>
#include <stdexcept>
namespace PSXRecompV4 {
namespace {
constexpr uint64_t kMaxMediaBytes = 512ull * 1024 * 1024;
uint32_t le32(const uint8_t* p) {
return uint32_t(p[0]) | (uint32_t(p[1]) << 8) |
(uint32_t(p[2]) << 16) | (uint32_t(p[3]) << 24);
}
uint32_t be32(const uint8_t* p) {
return uint32_t(p[3]) | (uint32_t(p[2]) << 8) |
(uint32_t(p[1]) << 16) | (uint32_t(p[0]) << 24);
}
void read_disc_sector(PS1::ISOReader& reader, uint32_t lba, uint8_t* out) {
std::array<uint8_t, 2352> raw{};
if (reader.ReadRawSector(lba, raw.data())) {
if (raw[15] != 1 && raw[15] != 2)
throw std::runtime_error("donor volume contains a non-data sector");
std::memcpy(out, raw.data() + (raw[15] == 1 ? 16 : 24), 2048);
} else if (!reader.ReadSector(lba, out)) {
throw std::runtime_error("cannot read donor volume sector");
}
}
} // namespace
bool load_mod_media(const std::filesystem::path& path,
const std::string& format, uint64_t expected_size,
const std::string& expected_sha256,
std::shared_ptr<const std::vector<uint8_t>>& bytes,
std::string* error) {
bytes.reset();
if (error) error->clear();
try {
if (!expected_size || expected_size > kMaxMediaBytes ||
expected_sha256.size() != 64 ||
!std::all_of(expected_sha256.begin(), expected_sha256.end(),
[](char c) { return (c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f'); }))
throw std::runtime_error("invalid donor size or SHA-256");
auto data = std::make_shared<std::vector<uint8_t>>();
if (format == "psx-disc") {
PS1::ISOReader reader;
if (!reader.Open(path.string()))
throw std::runtime_error("cannot open donor disc");
std::array<uint8_t, 2048> pvd{};
read_disc_sector(reader, 16, pvd.data());
if (pvd[0] != 1 || std::memcmp(pvd.data() + 1, "CD001", 5) ||
pvd[6] != 1 || pvd[128] != 0 || pvd[129] != 8 ||
pvd[130] != 8 || pvd[131] != 0)
throw std::runtime_error("donor disc is not a 2048-byte ISO9660 volume");
const uint32_t declared_count = le32(pvd.data() + 80);
/* Retail mixed-mode discs may declare the whole disc, including
* audio, as their ISO volume size. The actual data-track boundary
* comes from the TOC or the single-file image length. */
const uint32_t count = reader.TrackCount() > 1
? reader.TrackPregapLBA(2) : reader.GetSectorCount();
if (reader.TrackIsAudio(1) || reader.TrackStartLBA(1) != 0 ||
declared_count != be32(pvd.data() + 84) || declared_count < count ||
count <= 16 || uint64_t(count) * 2048 != expected_size)
throw std::runtime_error("donor data-track size does not match");
data->resize(static_cast<size_t>(expected_size));
for (uint32_t i = 0; i < count; ++i)
read_disc_sector(reader, i, data->data() + size_t(i) * 2048);
} else if (format == "file" || format == "n64-rom") {
if (std::filesystem::file_size(path) != expected_size)
throw std::runtime_error("donor file size does not match");
if (format == "n64-rom" &&
(expected_size < 64 || expected_size % 4 || expected_size > 64ull * 1024 * 1024))
throw std::runtime_error("invalid N64 ROM size");
std::ifstream input(path, std::ios::binary);
data->resize(static_cast<size_t>(expected_size));
if (!input.read(reinterpret_cast<char*>(data->data()),
static_cast<std::streamsize>(expected_size)) ||
input.peek() != std::char_traits<char>::eof())
throw std::runtime_error("cannot read complete donor file");
if (format == "n64-rom") {
const uint32_t magic = be32(data->data());
if (magic == 0x37804012) {
for (size_t i = 0; i < data->size(); i += 2)
std::swap((*data)[i], (*data)[i + 1]);
} else if (magic == 0x40123780) {
for (size_t i = 0; i < data->size(); i += 4)
std::reverse(data->begin() + i, data->begin() + i + 4);
} else if (magic != 0x80371240) {
throw std::runtime_error("unrecognized N64 ROM byte order");
}
}
} else {
throw std::runtime_error("unsupported donor media format");
}
uint8_t digest[32];
psx_sha256_compute(data->data(), data->size(), digest);
static const char hex[] = "0123456789abcdef";
std::string actual(64, '0');
for (size_t i = 0; i < 32; ++i) {
actual[i * 2] = hex[digest[i] >> 4];
actual[i * 2 + 1] = hex[digest[i] & 15];
}
if (actual != expected_sha256)
throw std::runtime_error("donor SHA-256 does not match");
bytes = std::move(data);
return true;
} catch (const std::exception& e) {
if (error) *error = e.what();
return false;
}
}
} // namespace PSXRecompV4
+78 -4
View File
@@ -2,6 +2,7 @@
#include "crc32.h"
#include "mod_plugins.h"
#include "mod_media.h"
#include "psx_sha256.h"
#include "toml.hpp"
@@ -23,7 +24,7 @@ namespace PSXRecompV4 {
namespace {
constexpr uint32_t kMinFormatVersion = 1;
constexpr uint32_t kMaxFormatVersion = 7;
constexpr uint32_t kMaxFormatVersion = 8;
constexpr uint64_t kMaxArchiveBytes = 256ull * 1024ull * 1024ull;
constexpr uint32_t kMaxArchiveFiles = 4096;
@@ -760,7 +761,13 @@ std::string canonical_resolution(const std::vector<const ModPackage*>& ordered,
}
for (const ModResolution::Resource& resource : resources) {
out << "resource:" << resource.package_id << ':' << resource.feature_id
<< ':' << resource.id << '=' << resource.path.string() << '\n';
<< ':' << resource.id << '=';
if (resource.bytes)
out << resource.format << ':' << resource.bytes->size() << ':'
<< resource.sha256;
else
out << resource.path.string();
out << '\n';
}
return out.str();
}
@@ -1496,6 +1503,47 @@ void mod_clear_plugins_for_tests() {
registered_plugins().clear();
}
std::vector<std::string> mod_registered_plugin_ids() {
std::vector<std::string> ids;
for (const auto& entry : registered_plugins())
if (entry.second.activation || entry.second.vblank ||
!entry.second.function_entries.empty())
ids.push_back(entry.first);
return ids; /* std::map keeps them sorted */
}
ModPluginAudit mod_audit_registered_plugins(
const std::vector<fs::path>& manifest_roots) {
ModPluginAudit audit;
std::set<std::string> declared;
for (const fs::path& root : manifest_roots) {
std::error_code ec;
if (!fs::is_directory(root, ec)) {
audit.errors.push_back(root.string() + ": not a directory");
continue;
}
for (fs::recursive_directory_iterator it(root, ec), end;
!ec && it != end; it.increment(ec)) {
if (!it->is_regular_file(ec) || it->path().filename() != "manifest.toml")
continue;
ModPackage package;
std::string error;
if (!ModPackageManager::read_manifest(it->path(), package, &error)) {
audit.errors.push_back(it->path().string() + ": " + error);
continue;
}
++audit.manifests;
for (const ModPlugin& plugin : package.plugins)
declared.insert(plugin.id);
}
if (ec) audit.errors.push_back(root.string() + ": " + ec.message());
}
audit.declared.assign(declared.begin(), declared.end());
for (const std::string& id : mod_registered_plugin_ids())
if (!declared.count(id)) audit.undeclared.push_back(id);
return audit;
}
const char* mod_channel_name(ModChannel channel) {
switch (channel) {
case ModChannel::Experimental: return "experimental";
@@ -1795,6 +1843,19 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
toml::find_or<std::string>(v, "format", "file");
resource.required =
toml::find_or<bool>(v, "required", false);
if (v.contains("size") || v.contains("sha256") ||
resource.format == "n64-rom" || resource.format == "psx-disc") {
if (out.format_version < 8)
throw std::runtime_error("verified media requires format_version 8");
const int64_t size = toml::find<int64_t>(v, "size");
resource.sha256 = toml::find<std::string>(v, "sha256");
if (size <= 0 || size > 512ll * 1024 * 1024 ||
!valid_sha256(resource.sha256) ||
(resource.format != "file" && resource.format != "n64-rom" &&
resource.format != "psx-disc"))
throw std::runtime_error("invalid verified media identity or format");
resource.size = static_cast<uint64_t>(size);
}
if (!find_feature(out, resource.feature_id))
throw std::runtime_error(
"resource references unknown feature");
@@ -1807,9 +1868,11 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
throw std::runtime_error("resource label is empty");
if (resource.format != "file" &&
resource.format != "directory" &&
resource.format != "folder")
resource.format != "folder" &&
resource.format != "n64-rom" &&
resource.format != "psx-disc")
throw std::runtime_error(
"resource format must be file or directory");
"unsupported resource format");
out.resources.push_back(std::move(resource));
}
}
@@ -3692,6 +3755,17 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
resolved.feature_id = resource.feature_id;
resolved.id = resource.id;
resolved.path = path;
resolved.format = resource.format;
resolved.sha256 = resource.sha256;
if (!resource.sha256.empty()) {
std::string media_error;
if (!load_mod_media(path, resource.format, resource.size,
resource.sha256, resolved.bytes, &media_error)) {
result.errors.push_back(package->id + "/" + resource.feature_id +
": " + resource.id + ": " + media_error);
continue;
}
}
result.resources.push_back(std::move(resolved));
}
}
+71
View File
@@ -53,6 +53,8 @@ extern "C" int fntrace_is_game_started(void);
/* Declared in mod_plugins.h; see active_function_entry_hooks() below. */
uint32_t g_psx_mod_function_entry_hooks = 0;
/* Guest vblanks seen by the mod runtime: the plugin counters' time base. */
static uint64_t g_mod_vblanks = 0;
namespace PSXRecompV4 {
namespace {
@@ -1422,6 +1424,7 @@ extern "C" void mod_runtime_activate_plugins(void) {
extern "C" void mod_runtime_on_vblank(void) {
using namespace PSXRecompV4;
++g_mod_vblanks;
RuntimeMods& s = state();
if (!s.initialized || !s.plan.ok) return;
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
@@ -1484,6 +1487,27 @@ extern "C" uint8_t psx_mod_read_byte(uint32_t address) {
return psx_read_byte(address);
}
extern "C" int psx_mod_current_resource_bytes(const char* resource_id,
const uint8_t** bytes,
uint64_t* size) {
using namespace PSXRecompV4;
if (bytes) *bytes = nullptr;
if (size) *size = 0;
if (!resource_id || !*resource_id || !bytes || !size) return 0;
const auto& s = state();
if (!s.initialized || !s.plan.ok || !s.current_plugin) return 0;
for (const auto& resource : s.plan.resources) {
if (resource.package_id == s.current_plugin->package_id &&
resource.feature_id == s.current_plugin->feature_id &&
resource.id == resource_id && resource.bytes) {
*bytes = resource.bytes->data();
*size = resource.bytes->size();
return 1;
}
}
return 0;
}
extern "C" void psx_mod_write_byte(uint32_t address, uint8_t value) {
psx_write_byte(address, value);
}
@@ -1519,6 +1543,53 @@ extern "C" uint32_t psx_mod_alloc_gpu_dma_memory(uint32_t size,
return psx_mod_gpu_dma_memory_alloc(size, alignment);
}
namespace {
struct ModCounter {
const char* name = nullptr; /* caller literal; compared by content */
uint64_t count = 0;
uint64_t last_frame = 0;
};
constexpr size_t kModCounterCap = 128;
ModCounter g_mod_counters[kModCounterCap];
size_t g_mod_counter_count = 0;
uint64_t g_mod_counter_overflow = 0;
} // namespace
extern "C" void psx_mod_counter_add(const char* name, uint32_t delta) {
if (!name || !name[0]) return;
for (size_t i = 0; i < g_mod_counter_count; i++) {
ModCounter& c = g_mod_counters[i];
if (c.name == name || std::strcmp(c.name, name) == 0) {
c.count += delta;
c.last_frame = g_mod_vblanks;
return;
}
}
if (g_mod_counter_count == kModCounterCap) {
g_mod_counter_overflow += delta;
return;
}
ModCounter& c = g_mod_counters[g_mod_counter_count++];
c.name = name;
c.count = delta;
c.last_frame = g_mod_vblanks;
}
/* Debug-server accessor: copies up to `cap` entries; returns the total count
* of distinct counters and writes the overflow bucket. */
extern "C" int psx_mod_counters_snapshot(const char** names, uint64_t* counts,
uint64_t* last_frames, int cap,
uint64_t* overflow) {
const int n = (int)g_mod_counter_count;
for (int i = 0; i < n && i < cap; i++) {
names[i] = g_mod_counters[i].name;
counts[i] = g_mod_counters[i].count;
last_frames[i] = g_mod_counters[i].last_frame;
}
if (overflow) *overflow = g_mod_counter_overflow;
return n;
}
extern "C" int32_t psx_mod_widescreen_x_margin(void) {
return (int32_t)psx_ws_x_margin();
}
+8
View File
@@ -2511,6 +2511,14 @@ static void init_callbacks(void) {
extern uint32_t psx_ws_angle_widen(uint32_t vanilla);
s_callbacks.ws_angle_widen = psx_ws_angle_widen;
}
{
extern int32_t psx_ws_cull_scale(int32_t bound, int32_t half_extent);
s_callbacks.ws_cull_scale = psx_ws_cull_scale;
}
{
extern int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc);
s_callbacks.nclip_exact_sign = gte_nclip_exact_sign;
}
/* 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
+137
View File
@@ -0,0 +1,137 @@
#include "present_image_ring.h"
#include <stdlib.h>
#include <string.h>
typedef struct {
uint32_t frame;
uint16_t w;
uint8_t valid;
uint16_t *px; /* PRESENT_IMAGE_RING_MAX_W * PRESENT_IMAGE_RING_H, RGB565 */
} PresentImage;
static PresentImage s_ring[PRESENT_IMAGE_RING_CAP];
static uint16_t *s_pixels = NULL;
static uint32_t s_head = 0;
static int s_enabled = -1;
static int s_frozen = 0;
static int ring_ready(void) {
if (s_enabled < 0) {
const char *e = getenv("PSX_PRESENT_IMAGE_RING");
s_enabled = (!e || !*e || *e != '0') ? 1 : 0;
}
if (!s_enabled) return 0;
if (!s_pixels) {
const size_t per = (size_t)PRESENT_IMAGE_RING_MAX_W * PRESENT_IMAGE_RING_H;
s_pixels = (uint16_t *)calloc((size_t)PRESENT_IMAGE_RING_CAP * per, sizeof(uint16_t));
if (!s_pixels) { s_enabled = 0; return 0; }
for (int i = 0; i < PRESENT_IMAGE_RING_CAP; i++)
s_ring[i].px = s_pixels + (size_t)i * per;
}
return 1;
}
int present_image_ring_thumb_w(int ww, int wh) {
if (ww <= 0 || wh <= 0) return 0;
long w = ((long)ww * PRESENT_IMAGE_RING_H + wh / 2) / wh;
if (w < 1) w = 1;
if (w > PRESENT_IMAGE_RING_MAX_W) w = PRESENT_IMAGE_RING_MAX_W;
return (int)w;
}
int present_image_ring_accepting(void) {
return !s_frozen && ring_ready();
}
static PresentImage *next_slot(uint32_t frame, int w) {
PresentImage *e = &s_ring[s_head];
s_head = (s_head + 1) % PRESENT_IMAGE_RING_CAP;
e->frame = frame;
e->w = (uint16_t)w;
e->valid = 1;
return e;
}
static uint16_t pack565(unsigned r, unsigned g, unsigned b) {
return (uint16_t)(((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3));
}
void present_image_ring_push(uint32_t frame, const uint8_t *rgba, int w,
int pitch, int bottom_up) {
if (!rgba || w <= 0 || w > PRESENT_IMAGE_RING_MAX_W) return;
if (!present_image_ring_accepting()) return;
PresentImage *e = next_slot(frame, w);
for (int y = 0; y < PRESENT_IMAGE_RING_H; y++) {
const int sy = bottom_up ? (PRESENT_IMAGE_RING_H - 1 - y) : y;
const uint8_t *src = rgba + (size_t)sy * pitch;
uint16_t *dst = e->px + (size_t)y * w;
for (int x = 0; x < w; x++)
dst[x] = pack565(src[4 * x], src[4 * x + 1], src[4 * x + 2]);
}
}
void present_image_ring_push_argb(uint32_t frame, const uint32_t *argb,
int sw, int sh, int src_pitch_px) {
if (!argb || sw <= 0 || sh <= 0) return;
if (!present_image_ring_accepting()) return;
const int w = present_image_ring_thumb_w(sw, sh);
PresentImage *e = next_slot(frame, w);
for (int y = 0; y < PRESENT_IMAGE_RING_H; y++) {
const int y0 = (int)((long)y * sh / PRESENT_IMAGE_RING_H);
int y1 = (int)((long)(y + 1) * sh / PRESENT_IMAGE_RING_H);
if (y1 <= y0) y1 = y0 + 1;
for (int x = 0; x < w; x++) {
const int x0 = (int)((long)x * sw / w);
int x1 = (int)((long)(x + 1) * sw / w);
if (x1 <= x0) x1 = x0 + 1;
unsigned r = 0, g = 0, b = 0, n = 0;
for (int yy = y0; yy < y1 && yy < sh; yy++)
for (int xx = x0; xx < x1 && xx < sw; xx++) {
const uint32_t p = argb[(size_t)yy * src_pitch_px + xx];
r += (p >> 16) & 0xFF; g += (p >> 8) & 0xFF; b += p & 0xFF; n++;
}
if (!n) n = 1;
e->px[(size_t)y * w + x] = pack565(r / n, g / n, b / n);
}
}
}
void present_image_ring_set_frozen(int frozen) { s_frozen = frozen ? 1 : 0; }
int present_image_ring_frozen(void) { return s_frozen; }
void present_image_ring_span(uint32_t *oldest, uint32_t *newest, int *count) {
uint32_t lo = UINT32_MAX, hi = 0;
int n = 0;
for (int i = 0; i < PRESENT_IMAGE_RING_CAP; i++) {
if (!s_ring[i].valid) continue;
n++;
if (s_ring[i].frame < lo) lo = s_ring[i].frame;
if (s_ring[i].frame > hi) hi = s_ring[i].frame;
}
if (oldest) *oldest = n ? lo : 0;
if (newest) *newest = hi;
if (count) *count = n;
}
int present_image_ring_get_rgb(uint32_t frame, uint8_t **rgb, int *w, int *h) {
if (!rgb || !s_pixels) return 0;
for (int i = 0; i < PRESENT_IMAGE_RING_CAP; i++) {
const PresentImage *e = &s_ring[i];
if (!e->valid || e->frame != frame) continue;
const size_t n = (size_t)e->w * PRESENT_IMAGE_RING_H;
uint8_t *out = (uint8_t *)malloc(n * 3);
if (!out) return 0;
for (size_t k = 0; k < n; k++) {
const uint16_t p = e->px[k];
out[3 * k] = (uint8_t)(((p >> 11) & 31) << 3);
out[3 * k + 1] = (uint8_t)(((p >> 5) & 63) << 2);
out[3 * k + 2] = (uint8_t)((p & 31) << 3);
}
*rgb = out;
if (w) *w = e->w;
if (h) *h = PRESENT_IMAGE_RING_H;
return 1;
}
return 0;
}
+12 -2
View File
@@ -649,7 +649,11 @@ static int netplay_user_blocked(void) {
}
static int request_save_inner(int slot) {
if (!s_configured) { fprintf(stderr, "savestate: not configured\n"); return 0; }
if (!s_configured) {
fprintf(stderr, "savestate: not configured\n");
psx_frontend_on_savestate_refused(0, slot, "Save states are not available yet");
return 0;
}
if (slot < 0 || slot >= SAVESTATE_SLOTS) return 0;
s_save_failed = 0;
s_last_save_pc = 0; /* block netplay transfer until this write stamps a PC */
@@ -662,13 +666,19 @@ static int request_save_inner(int slot) {
}
static int request_load_inner(int slot) {
if (!s_configured) { fprintf(stderr, "savestate: not configured\n"); return 0; }
if (!s_configured) {
fprintf(stderr, "savestate: not configured\n");
psx_frontend_on_savestate_refused(1, slot, "Save states are not available yet");
return 0;
}
if (slot < 0 || slot >= SAVESTATE_SLOTS) return 0;
if (!psx_hle_scheduler_enabled()) {
/* LLE (host-fiber) mode: the restore longjmp target lives on the
* scheduler fiber; cross-fiber unwind is unsafe. HLE is the default. */
fprintf(stderr, "savestate: load requires the HLE scheduler (default); "
"PSX_HLE_SCHEDULER=0 run cannot load states.\n");
psx_frontend_on_savestate_refused(1, slot,
"Cannot load: this run uses the legacy thread scheduler");
return 0;
}
s_load_failed = 0;
@@ -181,6 +181,13 @@ void psx_mod_function_entry(CPUState *cpu, uint32_t address) {
int psx_netplay_is_resimulating(void) { return 0; }
int psx_game_text_native_ok(uint32_t address) { (void)address; return 1; }
uint32_t psx_ws_angle_widen(uint32_t vanilla) { return vanilla; }
int32_t psx_ws_cull_scale(int32_t bound, int32_t half_extent) {
(void)half_extent; return bound;
}
int32_t gte_nclip_exact_sign(int32_t native_mac0, uint32_t pc) {
(void)pc;
return native_mac0 < 0 ? -1 : (native_mac0 > 0 ? 1 : 0);
}
uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced) {
(void)forced; return vanilla;
}
+28
View File
@@ -1,6 +1,10 @@
/* Original source-owned GL readback-coherence regression. No retail payload. */
#include "gpu_gl_renderer.c"
#include "mod_texture_banks.c"
#include "gpu_timeline.c"
uint64_t psx_cycle_count=0;
static int test_full_composite;
int gpu_ws_background_requires_full_composite(void){return test_full_composite;}
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
static uint16_t image[1024*512], oracle[1024*512];
@@ -60,6 +64,28 @@ static void verify_bank_batching(void) {
psx_mod_set_texture_bank_batching(0);s_tex_filter=0;
glb_set_mask_bits(0,0);glb_set_semi_transparency(0,0);
}
/* Native-wide, vertically double-buffered (both bands in ONE wide surface):
* a full-width flat rect takes the full-screen-overlay wide pass. It spans
* above its draw area; canonical VRAM clips it at the band top, and the wide
* surface must clip it there too instead of painting the other band. */
static void verify_wide_overlay_band(void) {
const int S=s_scale,W=426*S,H=512*S;
uint32_t *wide=(uint32_t*)malloc((size_t)W*H*sizeof(uint32_t));
int ow=0,oh=0;
check(wide!=NULL,"wide dump alloc");if(!wide)return;
glb_set_draw_area(0,0,1023,511);
glb_wide_configure(426,53);glb_wide_set_target(0);
glb_wide_clear(0,0,512,0x0421);
glb_set_draw_area(0,240,319,479);
glb_draw_flat_rect(0,158,320,322,0x001f);flush_flat_batch();
check(glb_wide_dump_full(wide,W*H,&ow,&oh,0)>0&&ow==W&&oh==H,"wide surface dump");
const uint32_t other=wide[(200*S)*W+5*S],edge=wide[(239*S+S-1)*W+420*S],drawn=wide[(240*S)*W+5*S];
check(((other>>16)&0xff)<0x20,"overlay rect leaves the other band's margin");
check(((edge>>16)&0xff)<0x20,"overlay rect stops at the draw-area top row");
check(((drawn>>16)&0xff)>=0xf0,"overlay rect covers its own band's margin");
free(wide);
glb_set_draw_area(0,0,1023,511);glb_wide_disable_target();glb_wide_configure(0,0);
}
int main(int argc,char **argv){
int scale=argc>1?atoi(argv[1]):1;
if(SDL_Init(SDL_INIT_VIDEO)!=0)return 2;
@@ -127,6 +153,8 @@ int main(int argc,char **argv){
check(glb_vram_read(302,252)==0x001f,"retained 16-bit texel");
verify("retained banks and original VRAM ordered together");
verify_bank_batching();
for(test_full_composite=0;test_full_composite<2;test_full_composite++)verify_wide_overlay_band();
test_full_composite=0;
printf("checks=%d failures=%d\n",checks,failures);
gl_renderer_shutdown();SDL_DestroyWindow(win);SDL_Quit();return failures?1:0;
}
+31 -2
View File
@@ -1,5 +1,6 @@
#include "cpu_state.h"
#include "gte.h"
#include "gte_nclip_stats.h"
#include <array>
#include <cstdint>
@@ -52,6 +53,7 @@ extern "C" void gpu_pgxp_rederive_enable(void) {}
extern "C" void psx_ws_note_gte_project(int) {}
extern "C" {
uint64_t s_frame_count = 0;
uint32_t g_debug_last_store_pc = 0;
}
uint64_t g_test_cycle = 0;
@@ -596,6 +598,8 @@ int test_precise_nclip_is_title_scoped() {
cpu.gte_data[12 + i] = packed[i];
gte_test_seed_precise_projection(i, packed[i], x16[i], y16[i], 100);
}
gte_nclip_stats_reset();
g_debug_last_store_pc = 0x80012340u;
uint64_t hit0 = 0, fallback0 = 0, disagree0 = 0;
gte_nclip_precise_stats(&hit0, &fallback0, &disagree0);
gte_execute(&cpu, 0x06u);
@@ -605,9 +609,34 @@ int test_precise_nclip_is_title_scoped() {
fallback1 != fallback0 || disagree1 != disagree0 + 1u)
return fail_value("precise NCLIP preserves guest MAC0", 0, 0x06u,
0, 1u, cpu.gte_data[24]);
if (!gte_nclip_precise_bltz(1) || gte_nclip_precise_bltz(2))
/* The branch consumer sees the precise (negative) sign only for the MAC0
* the NCLIP produced; any other value keeps its native sign. Every branch
* shape tests this one sign. */
if (gte_nclip_exact_sign(1, 0x80011000u) != -1 ||
gte_nclip_exact_sign(2, 0x80011000u) != 1 ||
gte_nclip_exact_sign(0, 0x80011004u) != 0)
return fail_value("title-scoped precise NCLIP predicate", 0, 0x06u,
0, 1u, 0u);
{
GteNclipSiteStat sites[GTE_NCLIP_STAT_CAP];
const int n = gte_nclip_site_stats(sites, GTE_NCLIP_STAT_CAP);
uint32_t evals = 0, flips = 0, falls = 0;
for (int i = 0; i < n; ++i)
if (sites[i].pc == 0x80011000u) {
evals = sites[i].evals; flips = sites[i].flips;
falls = sites[i].fallbacks;
}
if (evals != 2u || flips != 1u || falls != 1u)
return fail_value("exact NCLIP site attribution", 0, 0x06u,
0, 2u, evals);
GteNclipFuncStat funcs[GTE_NCLIP_STAT_CAP];
const int nf = gte_nclip_func_stats(funcs, GTE_NCLIP_STAT_CAP);
uint32_t fn = 0, fd = 0;
for (int i = 0; i < nf; ++i)
if (funcs[i].func == 0x80012340u) { fn = funcs[i].nclips; fd = funcs[i].disagree; }
if (fn != 1u || fd != 1u)
return fail_value("NCLIP producer attribution", 0, 0x06u, 0, 1u, fn);
}
/* A stale packed-word shadow must fail closed to the native sign and count
* as a fallback, never as a precise hit. */
@@ -619,7 +648,7 @@ int test_precise_nclip_is_title_scoped() {
g_test_precise_nclip_enabled = 0;
if (cpu.gte_data[24] != 1u || hit2 != hit1 ||
fallback2 != fallback1 + 1u || disagree2 != disagree1 ||
gte_nclip_precise_bltz(1))
gte_nclip_exact_sign(1, 0x80011000u) != 1)
return fail_value("stale precise NCLIP falls back natively", 0, 0x06u,
0, 1u, cpu.gte_data[24]);
return 0;
+9
View File
@@ -30,6 +30,15 @@ int main(int argc, char **argv) {
"default fullscreen includes Alt+Return");
check(host_keymap_match(HOST_KEYMAP_FULLSCREEN, (int)SDLK_f, mod_ctrl()),
"default fullscreen includes Ctrl+F");
/* A real key event carries only one side's modifier bit. */
check(host_keymap_match(HOST_KEYMAP_FULLSCREEN, (int)SDLK_f, (int)KMOD_LCTRL),
"Ctrl+F matches a left-Ctrl-only press");
check(host_keymap_match(HOST_KEYMAP_FULLSCREEN, (int)SDLK_RETURN, (int)KMOD_RALT),
"Alt+Return matches a right-Alt-only press");
check(host_keymap_match(HOST_KEYMAP_CAPTURE_MARK, (int)SDLK_m, (int)KMOD_LCTRL),
"default capture mark is Ctrl+M");
check(!host_keymap_match(HOST_KEYMAP_CAPTURE_MARK, (int)SDLK_m, 0),
"plain M does not trigger the capture mark");
keys[SDL_SCANCODE_TAB] = 1;
check(host_keymap_down(HOST_KEYMAP_TURBO, keys, 0),
"default turbo is held by Tab");
+116
View File
@@ -0,0 +1,116 @@
#include "mod_media.h"
#include "psx_sha256.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace fs = std::filesystem;
using namespace PSXRecompV4;
static int failures;
static void check(bool ok, const char* text) {
if (!ok) { std::cerr << "FAIL: " << text << '\n'; ++failures; }
}
static void write(const fs::path& path, const std::vector<uint8_t>& bytes) {
std::ofstream out(path, std::ios::binary);
out.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
}
static std::string hash(const std::vector<uint8_t>& bytes) {
uint8_t digest[32];
psx_sha256_compute(bytes.data(), bytes.size(), digest);
std::string result;
for (uint8_t byte : digest) {
result += "0123456789abcdef"[byte >> 4];
result += "0123456789abcdef"[byte & 15];
}
return result;
}
int main(int argc, char** argv) {
std::shared_ptr<const std::vector<uint8_t>> bytes;
std::string error;
if (argc == 5) {
const bool ok = load_mod_media(argv[1], argv[2], std::stoull(argv[3]), argv[4], bytes, &error);
if (!ok) { std::cerr << error << '\n'; return 1; }
std::cout << "verified bytes=" << bytes->size() << " sha256=" << hash(*bytes) << '\n';
return 0;
}
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-media-test";
fs::create_directories(root);
std::vector<uint8_t> rom(64);
for (size_t i = 0; i < rom.size(); ++i) rom[i] = static_cast<uint8_t>(i);
rom[0] = 0x80; rom[1] = 0x37; rom[2] = 0x12; rom[3] = 0x40;
const auto rom_hash = hash(rom);
const auto path = root / "rom.z64";
write(path, rom);
check(load_mod_media(path, "n64-rom", rom.size(), rom_hash, bytes, &error) && *bytes == rom,
"canonical N64 bytes");
auto snapshot = bytes;
auto swapped = rom;
for (size_t i = 0; i < swapped.size(); i += 2) std::swap(swapped[i], swapped[i + 1]);
write(path, swapped);
check(load_mod_media(path, "n64-rom", rom.size(), rom_hash, bytes, &error) && *bytes == rom,
"halfword swapped N64 identity");
swapped = rom;
for (size_t i = 0; i < swapped.size(); i += 4)
std::reverse(swapped.begin() + i, swapped.begin() + i + 4);
write(path, swapped);
check(load_mod_media(path, "n64-rom", rom.size(), rom_hash, bytes, &error) && *bytes == rom,
"word swapped N64 identity");
swapped[32] ^= 1;
write(path, swapped);
check(!load_mod_media(path, "n64-rom", rom.size(), rom_hash, bytes, &error) && !bytes,
"same-size wrong media rejected without exposing bytes");
check(*snapshot == rom, "snapshot survives on-disk changes");
check(!load_mod_media(path, "n64-rom", rom.size() + 4, rom_hash, bytes, &error), "size guard");
check(!load_mod_media(path, "n64-rom", rom.size(), std::string(64, 'z'), bytes, &error), "hash syntax");
check(!load_mod_media(root / "absent", "n64-rom", 64, rom_hash, bytes, &error), "missing file");
check(!load_mod_media(path, "file", 513ull * 1024 * 1024, rom_hash, bytes, &error), "bounded allocation");
std::vector<uint8_t> iso(24 * 2048);
size_t pvd = 16 * 2048;
iso[pvd] = 1;
std::copy_n("CD001", 5, iso.begin() + pvd + 1);
iso[pvd + 6] = 1; iso[pvd + 80] = 24; iso[pvd + 87] = 24;
iso[pvd + 129] = 8; iso[pvd + 130] = 8;
const auto iso_hash = hash(iso);
const auto disc = root / "disc.iso";
write(disc, iso);
check(load_mod_media(disc, "psx-disc", iso.size(), iso_hash, bytes, &error) && *bytes == iso,
"cooked ISO volume");
for (unsigned mode : {1u, 2u}) {
std::vector<uint8_t> raw(24 * 2352);
for (unsigned i = 0; i < 24; ++i) {
std::fill(raw.begin() + i * 2352 + 1, raw.begin() + i * 2352 + 11, 0xff);
raw[i * 2352 + 15] = static_cast<uint8_t>(mode);
std::copy_n(iso.begin() + i * 2048, 2048,
raw.begin() + i * 2352 + (mode == 1 ? 16 : 24));
}
write(root / "disc.bin", raw);
check(load_mod_media(root / "disc.bin", "psx-disc", iso.size(), iso_hash, bytes, &error) && *bytes == iso,
"Mode 1/2 raw BIN canonical identity");
write(root / "audio.bin", std::vector<uint8_t>(2352, 0x5a));
const auto cue = root / "disc.cue";
{
std::ofstream out(cue);
out << "FILE \"disc.bin\" BINARY\n TRACK 01 MODE" << mode << "/2352\n INDEX 01 00:00:00\n"
"FILE \"audio.bin\" BINARY\n TRACK 02 AUDIO\n INDEX 01 00:00:00\n";
}
check(load_mod_media(cue, "psx-disc", iso.size(), iso_hash, bytes, &error) && *bytes == iso,
"CUE identity excludes audio tracks");
}
iso[pvd + 87] = 23;
write(disc, iso);
check(!load_mod_media(disc, "psx-disc", iso.size(), hash(iso), bytes, &error), "ISO endian count guard");
iso[pvd + 87] = 24; iso[pvd + 1] = 'X';
write(disc, iso);
check(!load_mod_media(disc, "psx-disc", iso.size(), hash(iso), bytes, &error), "ISO PVD guard");
iso[pvd + 1] = 'C'; iso[pvd + 80] = 40; iso[pvd + 87] = 40;
write(disc, iso);
check(load_mod_media(disc, "psx-disc", iso.size(), hash(iso), bytes, &error) && *bytes == iso,
"mixed-mode volume size may extend beyond data track");
for (const char* file : {"rom.z64", "disc.iso", "disc.bin", "disc.cue", "audio.bin"})
fs::remove(root / file);
fs::remove(root);
return failures ? 1 : 0;
}
+80
View File
@@ -410,6 +410,37 @@ int main() {
"an implementation exposes exactly the hooks it registered");
check(mod_function_entry_hooks("test.missing").empty(),
"an unregistered implementation exposes no hooks");
/* Audit: a hook registered under a sub-id of the package's declared
* plugin can never be activated by a plan and must be reported. */
check(mod_register_function_entry_plugin("test.entry.hud", 0x80020000u, entry_hook),
"sub-id function-entry plugin must register");
{
const fs::path audit_root = root / "audit-authored";
write_text(audit_root / "entry.mod/1.0.0/manifest.toml",
"format_version = 5\n"
"id = \"entry.mod\"\n"
"version = \"1.0.0\"\n"
"name = \"Entry Mod\"\n"
"resolver = \"declarative\"\n"
"[[target]]\n"
"game_id = \"SLUS-TEST\"\n"
"[[feature]]\n"
"id = \"entry\"\n"
"name = \"Entry Plugin\"\n"
"[[plugin]]\n"
"feature = \"entry\"\n"
"id = \"test.entry\"\n");
const ModPluginAudit audit = mod_audit_registered_plugins({audit_root});
check(audit.errors.empty() && audit.manifests == 1,
"audit must read every authored manifest");
check(std::find(audit.declared.begin(), audit.declared.end(),
"test.entry") != audit.declared.end(),
"audit must collect declared [[plugin]] ids");
check(audit.undeclared.size() == 1 && audit.undeclared[0] == "test.entry.hud",
"audit must report exactly the registered-but-undeclared id");
const ModPluginAudit missing = mod_audit_registered_plugins({root / "absent"});
check(!missing.errors.empty(), "audit must report an unreadable root");
}
mod_clear_plugins_for_tests();
ModResolution entry_unavailable = reload.resolve("SLUS-TEST");
check(!entry_unavailable.ok,
@@ -2109,6 +2140,55 @@ int main() {
mod_clear_plugins_for_tests();
}
{
const auto media_root = root / "media-catalog";
const auto manifest_path = media_root / "packages/media.mod/1.0.0/manifest.toml";
std::vector<uint8_t> rom(64);
rom[0] = 0x80; rom[1] = 0x37; rom[2] = 0x12; rom[3] = 0x40;
const auto owner = media_root / "owner.z64";
const auto moved = media_root / "moved.v64";
write_bytes(owner, rom);
const std::string declaration =
"id = \"media.mod\"\nversion = \"1.0.0\"\nname = \"Media\"\n"
"[[target]]\ngame_id = \"SLUS-TEST\"\n"
"[[feature]]\nid = \"arena\"\nname = \"Arena\"\n"
"[[resource]]\nfeature = \"arena\"\nid = \"rom\"\nlabel = \"ROM\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n";
write_text(manifest_path, "format_version = 8\n" + declaration);
ModPackageManager media(media_root);
check(media.scan(&error) && media.scan_errors().empty(), "verified-media manifest parses");
const auto stock_plan = media.resolve("SLUS-TEST");
check(stock_plan.ok && stock_plan.resources.empty(), "disabled donor mod needs no media");
check(media.set_feature_enabled("media.mod", "arena", true, &error), error.c_str());
check(!media.resolve("SLUS-TEST").ok, "enabled donor mod without media cannot launch");
check(media.set_feature_resource_path("media.mod", "arena", "rom", owner, &error), error.c_str());
auto pinned = media.resolve("SLUS-TEST");
check(pinned.ok && pinned.resources.size() == 1 && pinned.resources[0].bytes &&
*pinned.resources[0].bytes == rom, "plan contains verified canonical bytes");
auto swapped = rom;
for (size_t i = 0; i < swapped.size(); i += 2) std::swap(swapped[i], swapped[i + 1]);
write_bytes(moved, swapped);
check(media.set_feature_resource_path("media.mod", "arena", "rom", moved, &error), error.c_str());
check(media.resolve("SLUS-TEST").fingerprint == pinned.fingerprint,
"donor fingerprint depends on canonical identity rather than path or byte order");
swapped[24] ^= 1;
write_bytes(moved, swapped);
check(!media.resolve("SLUS-TEST").ok, "wrong same-size media blocks plan");
fs::remove(moved);
check(!media.resolve("SLUS-TEST").ok, "removed media blocks a new plan");
check(media.set_feature_enabled("media.mod", "arena", false, &error), error.c_str());
const auto restored = media.resolve("SLUS-TEST");
check(restored.ok && restored.resources.empty() && restored.fingerprint == stock_plan.fingerprint,
"disabling donor mod restores the exact stock plan");
write_text(manifest_path, "format_version = 7\n" + declaration);
ModPackage invalid;
check(!ModPackageManager::read_manifest(manifest_path, invalid, &error),
"old manifest version cannot silently ignore donor verification");
auto negative_size = declaration;
negative_size.replace(negative_size.find("size = 64"), 9, "size = -1");
write_text(manifest_path, "format_version = 8\n" + negative_size);
check(!ModPackageManager::read_manifest(manifest_path, invalid, &error), "negative media size rejected");
}
fs::remove_all(root, ec);
if (failures) {
std::cerr << failures << " mod package test(s) failed\n";
+46
View File
@@ -102,6 +102,16 @@ static void test_activation_plugin(void) {
activation_calls++;
}
static const uint8_t* media_snapshot;
static uint64_t media_size;
static int media_access, foreign_media_access;
static void test_media_plugin(void) {
media_access = psx_mod_current_resource_bytes("rom", &media_snapshot, &media_size);
const uint8_t* foreign = nullptr;
uint64_t size = 0;
foreign_media_access = psx_mod_current_resource_bytes("private", &foreign, &size);
}
static int big_ram_activations;
static void test_big_ram_activation(void) {
big_ram_activations++;
@@ -720,6 +730,42 @@ int main() {
check(PSXRecompV4::mod_runtime_commit(raw_path,&error),"reader mount raw disc");
check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) &&
std::equal(result.begin(),result.end(),iso.begin()+22*2048),"raw and ISO reads identical");
/* Verified bytes are scoped to the committed feature, and do not depend
* on reopening an owner file after launch. No launcher is needed here. */
const auto media_root = root / "verified-media";
std::vector<uint8_t> rom(64, 0);
rom[0] = 0x80; rom[1] = 0x37; rom[2] = 0x12; rom[3] = 0x40;
rom[32] = 0x5a;
const auto rom_path = media_root / "owner.z64";
write_bytes(rom_path, rom);
write_text(media_root / "packages/media.test/1.0.0/manifest.toml",
"format_version = 8\nid = \"media.test\"\nversion = \"1.0.0\"\nname = \"Media Test\"\n"
"[[target]]\ngame_id = \"READER\"\n"
"[[feature]]\nid = \"active\"\nname = \"Active\"\ndefault_enabled = true\n"
"[[feature]]\nid = \"private\"\nname = \"Private\"\n"
"[[resource]]\nfeature = \"active\"\nid = \"rom\"\nlabel = \"ROM\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[resource]]\nfeature = \"private\"\nid = \"private\"\nlabel = \"Private\"\n"
"format = \"n64-rom\"\nrequired = true\nsize = 64\nsha256 = \"" + sha256_hex(rom) + "\"\n"
"[[plugin]]\nfeature = \"active\"\nid = \"media.test.plugin\"\n");
write_text(media_root / "state.toml",
"format_version = 2\n[[feature]]\npackage_id = \"media.test\"\nid = \"active\"\nenabled = true\n"
"[feature.resources]\nrom = \"" + rom_path.generic_string() + "\"\n");
check(psx_mod_register_activation_plugin("media.test.plugin", test_media_plugin), "register media consumer");
check(PSXRecompV4::mod_runtime_initialize(media_root, "READER", 0, {}, &error), "media initialize");
check(PSXRecompV4::mod_runtime_commit({}, &error), "verify and commit owner media");
const uint8_t* unavailable = nullptr;
uint64_t unavailable_size = 0;
check(!psx_mod_current_resource_bytes("rom", &unavailable, &unavailable_size), "bytes unavailable outside plugin callback");
mod_runtime_activate_plugins();
check(media_access && media_size == rom.size() &&
std::equal(rom.begin(), rom.end(), media_snapshot), "plugin receives exact verified bytes");
check(!foreign_media_access, "inactive feature cannot supply bytes to another feature");
rom[32] ^= 1;
write_bytes(rom_path, rom);
check(media_snapshot && media_snapshot[32] == 0x5a, "committed bytes survive owner file changes");
check(!PSXRecompV4::mod_runtime_commit({}, &error), "new commit rejects modified media");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), "netplay clears donor plan");
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";
@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""The full-screen-overlay native-wide pass must clip to the draw-area rows.
Vertically double-buffered titles keep both display bands in ONE wide surface.
A full-width rect that extends above its draw area is clipped at the band top
in canonical VRAM; the wide pass has to clip it there too (the software
reference rt_wide() does), or it paints the other band's rows and the next
present shows a flat band over that buffer. The GL pixel check lives in
test_gl_readback_region.c; this guard covers both GPU backends' source.
"""
from pathlib import Path
import re
src = Path(__file__).parents[1] / "src"
def body(text, signature):
m = re.search(re.escape(signature) + r".*?\n\}", text, flags=re.DOTALL)
assert m, f"{signature} not found"
return m.group(0)
gl = (src / "gpu_gl_renderer.c").read_text(encoding="utf-8")
band = body(gl, "static void wide_band_scissor(void) {")
assert "s_area_y1" in band and "s_area_y2" in band, "GL band scissor ignores the draw area"
flat = body(gl, "static void wide_flat_rect_direct(")
assert "wide_band_scissor();" in flat, "GL overlay pass does not use the band scissor"
assert "VRAM_H * s_scale); /* full surface" not in flat, "GL overlay pass uses a full-height scissor"
target = body(gl, "static void wide_target_begin(")
assert "wide_band_scissor();" in target, "GL wide mirror does not use the band scissor"
vk = (src / "gpu_vk_renderer.c").read_text(encoding="utf-8")
begin = body(vk, "static void wide_pass_begin(VkCommandBuffer cb) {")
assert "s_da_y1" in begin and "s_da_y2" in begin, "Vulkan wide pass ignores the draw area"
overlay = body(vk, "static void wide_overlay_rect(")
assert "wide_pass_begin(cb);" in overlay, "Vulkan overlay pass does not open a wide pass"
assert "vkCmdSetScissor" not in overlay, "Vulkan overlay pass overrides the band scissor"
print("wide overlay band scissor test passed")