mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 02:32:02 -04:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
@@ -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}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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 "?";
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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++;
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user