Add guarded native draw replay, motion interpolation and stable world filtering

This commit is contained in:
Matthew Stanley
2026-10-03 23:35:11 -07:00
parent 030cb00c45
commit d2c8f03e76
66 changed files with 3454 additions and 76 deletions
+61
View File
@@ -860,6 +860,50 @@ 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.
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
@@ -870,6 +914,23 @@ rejects encrypted entries and unsafe or absolute paths, limits archives to 4096
files and 256 MiB expanded size, stages extraction, validates the manifest, and
publishes the version atomically.
### Guarded instruction callbacks
Trusted plugins can register
`psx_mod_register_instruction_plugin(id, address, expected_word, callback)`.
List native code locations in `[recompiler] mod_instruction_sites` and regenerate.
Callbacks run immediately before the instruction; the registered full word,
executing word and current RAM word must agree. Segment aliases share a site.
Only active resolved plugins run, with their feature's resource context.
The dirty-RAM backend uses the same checks, including nested delay slots.
Overlay ABI v27 carries the forwarder; site metadata changes the codegen hash.
Callbacks may change registers and data. They must preserve PC and must not
re-enter guest execution, because pending branch/load state is live.
`psx_mod_finish_function` is unavailable in this callback scope. Inactive plans
do not change guest behavior. Function entry replacement remains the separate
`psx_mod_register_function_entry_plugin` interface.
### Presentation bezel packages
The framework registers `psx.bezel`, a trusted presentation plugin for OpenGL
+119
View File
@@ -73,6 +73,125 @@ back to `HOLD` when the status is `READY` again. A backend mode that cannot
host passes (for example a renderer path whose VRAM is not one texture) should
report `BACKEND` from `gl_renderer_pass_unavailable()`.
## Flip modes
`psx_mod_set_render_pass_flip()` says how the game flips relative to the pass
point (default `PENDING`, reset at every session start):
- `PENDING`: frame N is drawn and waits; the next flip shows the pass rect.
This is the PsyQ `VSync(0)`-then-`PutDispEnv` loop, and also any game that
flips only after the next frame is submitted (Vigilante 8: 2nd Offense
flips from a VBlank callback armed by the next submit). Phase 0 is
re-captured when the flip is seen, so a game that adds a last layer to the
shown buffer after the pass point (V8:2 draws its HUD at the next submit)
shows its own final image at phase 0.
- `SHOWN`: the game flips each frame as soon as it is drawn, so at the pass
point frame N is already on screen. The rect is that on-screen rect; frame
N and its in-betweens are shown from the next flip, one game frame later
than stock (the latency of any interpolator with one frame of history).
Decide by decoding the game's DISPENV / DRAWENV tables and where it calls
`PutDispEnv` and `DrawOTag`, not by assumption: in V8:2 `DISPENV[i]` shows
the buffer `DRAWENV[1-i]` draws.
## Replaying a span of frame code
Re-implementing a game's per-frame draw call sequence in a plugin copies one
path through it (HUD, messages, pause menu, replay mode each branch
differently) and drifts from it. Instead, a plugin records the registers at
the start of the game's draw section during the real frame (an instruction
hook, `mod_instruction_sites`) and, inside a pass, replays that section:
```c
int psx_mod_run_guest_span(struct CPUState *cpu, uint32_t start_pc, uint32_t stop_pc);
```
Every PC in `[start_pc, stop_pc)` is interpreted from its RAM bytes, even
where a compiled body or a compiled resume point (the return address of a
call) begins, so the span may start in the middle of a compiled function.
Calls the span makes leave the range as plain transfers and run with the
full dispatch trampoline back to their return address (compiled code is CPS:
a callee's own calls and returns go through the trampoline). A call may be
the span's last instruction. Anything else that leaves the range ends the
span with failure. Only inside a pass: the sandbox rolls a failed or runaway
span back, and the watchdog landing closes an open span. The span is a
replay of the game's own instructions, never a substitute for code the
recompiler could not translate; it stays out of capture, alias seeding and
per-PC entry statistics, and fires instruction hooks as compiled code does.
`render_pass_stats` counts `spans` / `span_failures` and keeps the last
failure (`span_fail`: reason, exit PC, `$ra`, post-call PC).
## Blending game state (render_pass_motion.h)
What makes a pass an in-between image is the state the draw code reads,
placed part of the way between two logic ticks. `render_pass_motion.h` holds
that for any title:
```c
psx_motion_begin(set, tick); /* each frame, capture point */
psx_motion_track(set, PSX_MOTION_MATRIX, obj + 0x20, psx_motion_identity(type, fn, mesh));
psx_motion_prepare(set, &limits, &stats); /* pair with the previous frame */
/* in each pass */
psx_motion_apply(set, alpha); /* write blends to guest RAM */
psx_motion_blend_at(set, kind, addr, alpha, &out); /* a value a hook hands on */
```
Kinds: PsyQ `MATRIX` (slerped rotation, linear translation), its translation
only (billboards), `VECTOR`, `SVECTOR`, 12-bit `ANGLES` (shorter arc) and a
`SCALAR`. Values pair by address and identity, so a heap block reused by
another object never blends between the two. `limits.move_per_tick` (game
units) and `limits.turn_radians` separate motion from placement: a value
past them was respawned, teleported or cut to and keeps its current value.
Measure the game's real motion first; V8:2's world spans about 2^27 units
per axis and moves up to 2^16 per frame.
## Per-frame driver
```c
int psx_mod_activate_render_pass_rate(const char *pkg, const char *feature,
const char *option, uint32_t flip_mode);
uint32_t psx_mod_render_pass_frame(struct CPUState *cpu,
const PSXModRenderPassFrame *frame,
PSXModRenderPassFn fn, void *user);
```
The first reads a choice option (`display` or a rate) and selects the FLIP
source, HOLD blend, the flip mode and the rate. The second plans one game
frame, runs `fn` at each phase into the frame's rect, and switches the
presenter to a crossfade while passes are unavailable for a lasting reason
(back to HOLD when they return).
## Porting a title
1. Find the main loop's draw section: the range from camera setup through
OT build and HUD to the submit call, and every call in it.
2. Decode DISPENV/DRAWENV and the submit/flip path: which buffer the next
flip shows at the pass point, and in what order the game draws into it
(immediate `DrawOTag`, VBlank callback). Pick the flip mode.
3. Find the pass point (a function entry after the section's OT build, before
submit) and the logic tick counter.
4. Find what the draw code reads that moves: the camera input (often the
argument of a set-camera function: track it with `blend_at` from an entry
hook), object transforms and their identity, particles.
5. Measure motion per frame with histograms (mod counters) before setting
limits.
6. Plugin: instruction hook at the section start (registers), capture at the
pass point, `psx_mod_render_pass_frame` with a pass function that applies
the blend, replays the span and submits as the screen gets it.
7. Verify in `--hidden-window` (below): `render_pass_stats` passes with no
span failures, `render_pass_dump` images between their neighbours,
`PSX_RENDER_PASS_VERIFY=1` with `verify_mismatch` 0.
`Vigilante82PSXRecomp/src/mods/v82_frame_rate_plugin.cpp` is the worked
example.
## Testing without a window
`--hidden-window` runs the full windowed runtime (presenter, interpolation,
render passes) in a window that is never shown and never opens a modal or a
file picker. `--headless` skips the presenter, so plans there are always
empty. Drive it over TCP; set `SDL_AUDIODRIVER=dummy` to keep it silent.
## What a pass may and may not do
While `fn` runs (`g_psx_render_pass_active`):
+39
View File
@@ -0,0 +1,39 @@
# Stable world texture filtering
Trusted title plugins can call `psx_mod_set_texture_filter(2)` to enable stable
minification on OpenGL. Modes 0 and 1 select nearest and bilinear. A mod-session
reset restores the player's Display setting; the override is not saved there.
The stable shader uses screen-space UV derivatives and at most sixteen
bilinear samples over a footprint bounded to eight texels per axis. It decodes
4-bit/8-bit palettes or direct 15-bit colors before averaging. Each sample reads
the current CLUT, honors texture windows, and clamps primitive bounds before
UV wrapping. This avoids stale palette mipmaps and sampling across atlas edges.
The center texel owns the cutout and semi-transparency classification. Neighbors
of a different STP class cannot contribute, and surviving sample weights are
renormalized. Only polygons with proven projected geometry receive the stable
filter. Sprites and untracked UI packets retain nearest sampling. Other backends
fall back to their existing bilinear mode.
This is a bounded minification filter, not an asset mipmap or texture replacement
system. Extreme minification can still alias; filtering also adds texture fetch
cost. The title catalog offers nearest, bilinear and stable choices.
`gl_texture_filter_test` runs a hidden real GL context over source-owned fixtures:
325 checks cover all three texture depths, minification, live palettes, cutouts,
STP separation, sharp UI, texture windows and wrap/atlas edges. On the checkerboard
fixture, red/blue chroma variation falls from 1116 to 22 for every depth.
For a running debug build, compare the same scene and resolution with:
```text
python tools/measure_render_quality.py --port 4370 --counter-prefix medievil.fr --output measurement.json
```
The resulting JSON records guest cadence, original and extra draw counters,
renderer settings, timing and statistical execution phases. The phases are host
wall-time samples, including waits and render replay; they are not a percentage
of guest instructions compiled. Extra draw counts also do not establish how
many unique images reached the monitor. Keep workload and other system activity
consistent, and report the measured rates separately from the selected target.
+67
View File
@@ -213,6 +213,13 @@ on a fixed region -> next.
## 5. Status / Log (update every session)
- **2026-10-03 - Render replay validation and texture streaming.** Increased the bounded small-transfer VRAM journal from 16 to 64 entries; Jersey Devil streams 31 texture/CLUT uploads in a draw. The 32-transfer rollback regression compares all VRAM bytes. Jersey Devil configures the SDK draw environment before replay packet allocation, preventing its command storage from overwriting a world quad. Final live runs completed at least 1,040 Jersey Devil and 1,088 A Bug's Life extra passes with zero state-verification mismatches, VRAM leaks, watchdogs or span failures. Changes remain local beyond base 9c1e716a.
- **2026-10-03 - Ape Escape wall-flicker regression.** Dense frame/VRAM capture isolated world GT4s incorrectly promoted to UI behind a front-layer fade. UI grouping now selects the actual populated drawing layer, retaining empty trailing OT behavior. Captured-packet tests fail on the prior core and pass on the fix; software and 4K OpenGL/PGXP captures show no gaps in the affected camera interval. No guest timing or draw replay changes.
- **2026-10-02 - Native high-rate rendering for Jersey Devil, A Bug's Life, and Ape Escape.** Added final RTPS/RTPT transform capture and guarded replay through the original draw sections. Projection overrides restore rotation/translation registers after each command. Camera cuts, unmatched geometry, stale captures, overflow, and aborted spans retain current poses or skip extra frames. Authoritative CPU/RAM/VRAM and guest timing remain isolated; live verification found zero mismatches in the sampled Ape Escape and A Bug's Life routes. GTE register and projection replay regressions pass. Ape Escape's card-menu scene predicate now applies in squash projection mode as well as native-wide; session resets clear it. Keyboard routing preserves analog mode on analog-locked titles. Changes are local beyond base 9c1e716a.
- **2026-10-01 — MMX6 adaptive framework integration.** The opt-in function
filters from the MMX6 branch are being reconciled with the active-plan
function-entry registry and master overlay store-PC forwarding. A handled
@@ -222,6 +229,66 @@ on a fixed region -> next.
view, host tile packets, retained texture banks and screen masks remain
presentation-only enhancements; faithful guest timing is unchanged.
- **2026-09-30 (Phase 5 owner-requested source checkpoint):** Preserve the
existing V8:2 combined-content title and generic callback/disc-extent support
on local `checkpoint/v82-combined-20260930` branches. Source, tests and
attribution only; donor media, generated outputs and playtest evidence stay
external. Owner explicitly paused further implementation, including
widescreen, original music and netplay, pending a check-in. No publication.
- **2026-09-30 (Phase 5 Dreamland mushroom freeze):** `beads-eio.12.5`
identified an imported projectile flag write altering its callback, followed
by a skipped prologue and invalid return into the BIOS exception loop. The
title converter now tracks the local projectile factory and target pointers.
Added 4163 relocation/flag property cases and an opt-in native mushroom-event
stress driver; the old overlay fails and the repaired overlay completes both
families. All 16 title checks pass. No framework behavior changes.
- **2026-09-30 (Phase 5 Dreamland effect cleanup):** `beads-eio.12.2` traced
the owner's frozen V8:2 match to an imported projectile effect writing its
matrix over the native child link. The title's input-overlay converter now
recognizes both effect-factory object returns. Five corrected field accesses,
owner-media regression checks, complete overlay code generation and a live
explosion allocation/free check validate the repair. No framework execution,
timing or generated-source changes; owner handoff remains at 1x rendering.
- **2026-09-30 (Phase 5 instruction callbacks):** `beads-kbj0` adds plan-owned,
exact-word-guarded callbacks before opt-in game instructions, including branch
and delay slots. Native/overlay metadata participates in the codegen hash;
dirty-RAM execution routes through the same guard after load retirement.
Overlay ABI v27 forwards the callback. Runtime, parser/emitter and interpreter
routing checks pass. V8:2 uses seven seams for independent vehicle stats,
upgrades, transformation parts and special dependencies. Chassey's live match,
input/pause and savestate roundtrip were observed after correcting the title
allocator to retain shrinking allocations, matching its source implementation.
- **2026-09-30 (Phase 5 mod-defined callbacks):** `beads-eio.3.212` adds
trusted mod-defined guest functions in an unused address range, separate
from original function-entry hooks. Active plans own availability and
callback resource context. Generated BIOS dispatch handles these through
its normal return/stack contract, with no synthetic guest instructions.
Runtime tests cover aliases, collisions, activation/clearing and nested
callback scopes; emitter tests cover routing before hardware dispatch.
OpenBIOS/SCPH1001 regenerated; V8:2 title rebuilt. Imported content still
needs its title-side runtime/selection adapters (`beads-eio.12.1`).
- **2026-09-30 (Phase 5 combined V8 project):** Owner selected USA 2nd Offense
as the game-code base while retaining psxrecomp as the runtime/recompiler.
New local `Vigilante82PSXRecomp` builds against `071847ab`, reaches a stock
match, responds to driving input and reloads a savestate in an isolated run.
The reference's thirteen original-car conversions roundtrip successfully;
imported runtime/selector adapters remain title work (`beads-eio.12.1`).
No generic execution or hardware change was needed for this bring-up.
- **2026-09-30 (Phase 5 callback reentrancy):** Plugin activation, VBlank and
function-entry callbacks now use the same scoped resource/completion context.
A native call inside an entry hook can deliver VBlank; previously those
callbacks cleared its plugin, making explicit completion fail and executing
the original function again. A regression fails before the fix and passes
after it, also refusing VBlank completion of the interrupted function. The
runtime test and seven V8 checks pass; isolated six-contact construction now
produces one chassis and survives drive/save/load. Tracked by `beads-i3ow`.
- **2026-09-30 — Original-disc overlay discovery (beads-eio.3.215).**
Ape Escape Primordial Ooze exposed return-adjacent functions omitted by the
archive scanner: a scheduled LUI/load before stack setup and an RA save beyond
+1 -1
View File
@@ -1 +1 @@
ae1e806fb3be31443f462df2f81314fb3288ee16bfd212bdafdbb2a03820497b
8b10cee63c750fee313839c132742e52441938a70c5736576c140383240036df
+6 -1
View File
@@ -33,6 +33,9 @@ extern void debug_server_trace_dispatch(uint32_t func_addr);
* the tier. */
extern int (*g_psx_bios_hle_hook)(CPUState* cpu, uint32_t phys);
extern uint32_t g_psx_mod_guest_functions;
extern int psx_mod_dispatch_guest_function(CPUState*, uint32_t);
#ifdef PSX_HAS_GAME_DISPATCH
extern int psx_game_address_in_text(uint32_t addr);
#endif
@@ -9090,6 +9093,8 @@ static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr)
cpu->pc = 0;
int lo = 0, hi = 3990 - 1;
int found = 0;
if (g_psx_mod_guest_functions)
found = psx_mod_dispatch_guest_function(cpu, addr);
/* BIOS HLE tier: consult the hook FIRST, on the pre-normalize
* physical address. It must see (a) the A0/B0/C0 service vectors
* (game thunks jr there with the function number in $t1) and (b)
@@ -9098,7 +9103,7 @@ static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr)
* backends. Handled (rc 1) ⇒ the service completed against guest
* state and the guest resumes at $ra via the trampoline's normal
* return/tail contract below. rc 0 ⇒ pure LLE fall-through. */
if (g_psx_bios_hle_hook &&
if (!found && g_psx_bios_hle_hook &&
g_psx_bios_hle_hook(cpu, addr & 0x1FFFFFFFu)) {
cpu->pc = cpu->gpr[31];
found = 1;
+1 -1
View File
@@ -1 +1 @@
530a85d6f2c9c6ab3927f8071f9494799755017fd44f0f2fdb8f1cddc1a86833
3e8f77cfe0b52d865e06c2375cac130df6e7a50dc3516fae30c945c44a9b2341
+6 -1
View File
@@ -33,6 +33,9 @@ extern void debug_server_trace_dispatch(uint32_t func_addr);
* the tier. */
extern int (*g_psx_bios_hle_hook)(CPUState* cpu, uint32_t phys);
extern uint32_t g_psx_mod_guest_functions;
extern int psx_mod_dispatch_guest_function(CPUState*, uint32_t);
#ifdef PSX_HAS_GAME_DISPATCH
extern int psx_game_address_in_text(uint32_t addr);
#endif
@@ -27537,6 +27540,8 @@ static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr)
cpu->pc = 0;
int lo = 0, hi = 13012 - 1;
int found = 0;
if (g_psx_mod_guest_functions)
found = psx_mod_dispatch_guest_function(cpu, addr);
/* BIOS HLE tier: consult the hook FIRST, on the pre-normalize
* physical address. It must see (a) the A0/B0/C0 service vectors
* (game thunks jr there with the function number in $t1) and (b)
@@ -27545,7 +27550,7 @@ static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr)
* backends. Handled (rc 1) ⇒ the service completed against guest
* state and the guest resumes at $ra via the trampoline's normal
* return/tail contract below. rc 0 ⇒ pure LLE fall-through. */
if (g_psx_bios_hle_hook &&
if (!found && g_psx_bios_hle_hook &&
g_psx_bios_hle_hook(cpu, addr & 0x1FFFFFFFu)) {
cpu->pc = cpu->gpr[31];
found = 1;
+2
View File
@@ -66,6 +66,7 @@ struct CodeGenConfig {
// Trusted game-mod entry hooks ([recompiler] mod_function_entry_funcs).
// Only explicitly listed guest functions call the runtime dispatcher.
std::set<uint32_t> mod_function_entry_funcs;
std::set<uint32_t> mod_instruction_sites;
// [recompiler] hot_funcs: emit __attribute__((hot)) on these guest
// addresses (MotK VLC leaves, etc.). Host locality hint only.
@@ -490,6 +491,7 @@ private:
std::string translate_instruction_core(uint32_t addr, uint32_t instr);
// The [[draw_distance.clamp]] statement for (addr, instr), or "".
std::string draw_distance_clamp_prefix(uint32_t addr, uint32_t instr) const;
std::string mod_instruction_call(uint32_t addr, uint32_t instr) const;
// Register name mapping
static std::string reg_name(int reg_num);
+13 -3
View File
@@ -895,6 +895,13 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
return keep_branch_if_wide("0 /* unknown branch condition: defaults to not-taken */");
}
std::string CodeGenerator::mod_instruction_call(uint32_t addr, uint32_t instr) const {
const bool selected = std::any_of(config_.mod_instruction_sites.begin(),
config_.mod_instruction_sites.end(), [=](uint32_t site) {
return (site & 0x1FFFFFFFu) == (addr & 0x1FFFFFFFu);
});
return selected ? fmt::format("psx_mod_instruction(cpu, 0x{:08X}u, 0x{:08X}u);\n", addr, instr) : "";
}
// [[draw_distance.clamp]]: a main-EXE ordering-table range guard. While a
// mod has the clamps on, the site's register is clamped to `max` before the
// instruction runs, so the guard keeps a far primitive in the last OT slot
@@ -935,9 +942,10 @@ std::string CodeGenerator::draw_distance_clamp_prefix(uint32_t addr,
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
std::string clamp = draw_distance_clamp_prefix(addr, instr);
if (clamp.empty()) return translate_instruction_core(addr, instr);
const std::string hook = mod_instruction_call(addr, instr);
if (clamp.empty()) return hook + translate_instruction_core(addr, instr);
// The core translation carries its own indent; give the clamp the same.
return config_.indent + clamp + "\n" + translate_instruction_core(addr, instr);
return hook + config_.indent + clamp + "\n" + translate_instruction_core(addr, instr);
}
std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t instr) {
@@ -1673,7 +1681,7 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in
case 0x2A: code = translate_slt(instr); break; // slt
case 0x2B: code = translate_sltu(instr); break; // sltu
default:
code = fmt::format("/* TODO: SPECIAL funct=0x{:02X} */", funct);
code = fmt::format("/* undefined SPECIAL funct=0x{:02X}: R3000A-compatible no-op */", funct);
}
}
// Immediate and load/store instructions
@@ -2204,6 +2212,7 @@ std::string CodeGenerator::translate_basic_block(
// Jumps (j/jal/jr/jalr) and reserved words never reach
// generate_branch_condition; check them here.
check_explicit_branch_site(addr, block.exit_instr.instruction);
ss << config_.indent << mod_instruction_call(addr, instr);
std::string delay_saved_cond; // branch condition captured before delay
std::string delay_saved_target; // JR/JALR target captured before delay
@@ -3518,6 +3527,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
ss << "#endif\n";
ss << "extern int psx_game_text_native_ok(uint32_t addr); /* stale-static guard (dispatch shard) */\n";
ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n";
ss << "extern void psx_mod_instruction(CPUState*, uint32_t, uint32_t);\n";
ss << "extern int psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
ss << "extern int psx_ws_masked_reject(uint32_t flags, uint32_t mask); /* guarded packed-coordinate reject */\n";
ss << "extern int psx_ws_nclip_branch(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);\n";
+10
View File
@@ -76,6 +76,7 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
h.u32(site.reject_mask);
}
}
h.words("mod_instruction_sites", c.mod_instruction_sites);
h.words("cull_bias", c.ws_cull_bias_sites);
if (!c.ws_cull_bias_lower_sites.empty())
h.words("cull_bias_lower", c.ws_cull_bias_lower_sites);
@@ -1668,6 +1669,14 @@ GameConfig load_game_config(const fs::path& config_path_in) {
}
// Optional [recompiler] hot_funcs — __attribute__((hot)) on emitted C.
// Optional trusted, statically linked game-mod entry hooks.
std::vector<uint32_t> mod_instruction_sites;
if (recomp.contains("mod_instruction_sites"))
for (const auto& a : toml::find<std::vector<std::string>>(recomp, "mod_instruction_sites")) {
const auto pc = parse_hex(a, "recompiler.mod_instruction_sites");
if ((pc & 3u) || pc >= 0xC0000000u || (pc & 0x1FFFFFFFu) >= 0x00800000u)
throw std::runtime_error("mod instruction site must be aligned main RAM");
mod_instruction_sites.push_back(pc);
}
std::vector<uint32_t> mod_function_entry_funcs;
if (recomp.contains("mod_function_entry_funcs")) {
const auto& arr = toml::find<std::vector<std::string>>(
@@ -2396,6 +2405,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_auto_ui_in_place*/ ws_auto_ui_in_place,
/*data_shard_funcs*/ data_shard_funcs,
/*mod_function_entry_funcs*/ mod_function_entry_funcs,
/*mod_instruction_sites*/ mod_instruction_sites,
/*hot_funcs*/ hot_funcs,
/*load_charge_batch*/ load_charge_batch,
/*load_charge_batch_funcs*/ load_charge_batch_funcs,
+2
View File
@@ -954,6 +954,8 @@ struct GameConfig {
// entries that dispatch trusted, statically linked mod callbacks. Empty by
// default, so projects that do not opt in emit no callback overhead.
std::vector<uint32_t> mod_function_entry_funcs;
// Opt-in pre-instruction callbacks; plugins supply exact-word guards.
std::vector<uint32_t> mod_instruction_sites;
// [recompiler] hot_funcs: guest addresses that get __attribute__((hot))
// on their generated C bodies (profile/host locality; no guest semantics).
+5 -1
View File
@@ -1837,6 +1837,8 @@ void FullFunctionEmitter::emit_dispatch(
out += " * NULL (the default) = pure LLE, dispatch identical to a build without\n";
out += " * the tier. */\n";
out += "extern int (*g_psx_bios_hle_hook)(CPUState* cpu, uint32_t phys);\n\n";
out += "extern uint32_t g_psx_mod_guest_functions;\n";
out += "extern int psx_mod_dispatch_guest_function(CPUState*, uint32_t);\n\n";
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
out += "extern int psx_game_address_in_text(uint32_t addr);\n";
out += "#endif\n\n";
@@ -2265,6 +2267,8 @@ void FullFunctionEmitter::emit_dispatch(
out += " cpu->pc = 0;\n";
out += fmt::format(" int lo = 0, hi = {} - 1;\n", total_entries);
out += " int found = 0;\n";
out += " if (g_psx_mod_guest_functions)\n";
out += " found = psx_mod_dispatch_guest_function(cpu, addr);\n";
out += " /* BIOS HLE tier: consult the hook FIRST, on the pre-normalize\n";
out += " * physical address. It must see (a) the A0/B0/C0 service vectors\n";
out += " * (game thunks jr there with the function number in $t1) and (b)\n";
@@ -2273,7 +2277,7 @@ void FullFunctionEmitter::emit_dispatch(
out += " * backends. Handled (rc 1) \xE2\x87\x92 the service completed against guest\n";
out += " * state and the guest resumes at $ra via the trampoline's normal\n";
out += " * return/tail contract below. rc 0 \xE2\x87\x92 pure LLE fall-through. */\n";
out += " if (g_psx_bios_hle_hook &&\n";
out += " if (!found && g_psx_bios_hle_hook &&\n";
out += " g_psx_bios_hle_hook(cpu, addr & 0x1FFFFFFFu)) {\n";
out += " cpu->pc = cpu->gpr[31];\n";
out += " found = 1;\n";
+4
View File
@@ -219,6 +219,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
bool reachable_discovery = false;
std::set<uint32_t> ws_tag_funcs; // [widescreen] sprite_tag_funcs
std::set<uint32_t> ds_funcs; // [data_shards] funcs
std::set<uint32_t> mod_instruction_sites;
std::set<uint32_t> mod_entry_funcs; // trusted game-mod entry hooks
std::vector<PSXRecompV4::WidescreenMaskedRejectSite> ws_cull_masked_reject;
std::set<uint32_t> hot_funcs; // [recompiler] hot_funcs
@@ -285,6 +286,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
ds_funcs.insert(cfg.data_shard_funcs.begin(), cfg.data_shard_funcs.end());
mod_entry_funcs.insert(cfg.mod_function_entry_funcs.begin(),
cfg.mod_function_entry_funcs.end());
mod_instruction_sites.insert(cfg.mod_instruction_sites.begin(), cfg.mod_instruction_sites.end());
hot_funcs.insert(cfg.hot_funcs.begin(), cfg.hot_funcs.end());
if (cfg.load_charge_batch) {
load_charge_batch_funcs.insert(cfg.load_charge_batch_funcs.begin(),
@@ -400,6 +402,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
ws_tag_funcs.insert(wscfg.ws_sprite_tag_funcs.begin(), wscfg.ws_sprite_tag_funcs.end());
mod_entry_funcs.insert(wscfg.mod_function_entry_funcs.begin(),
wscfg.mod_function_entry_funcs.end());
mod_instruction_sites.insert(wscfg.mod_instruction_sites.begin(), wscfg.mod_instruction_sites.end());
ws_cull_bias.insert(wscfg.ws_cull_bias_sites.begin(), wscfg.ws_cull_bias_sites.end());
ws_cull_bias_lower.insert(wscfg.ws_cull_bias_lower_sites.begin(), wscfg.ws_cull_bias_lower_sites.end());
ws_cull_range.insert(wscfg.ws_cull_range_sites.begin(), wscfg.ws_cull_range_sites.end());
@@ -1464,6 +1467,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
codegen_config.data_shard_funcs = ds_funcs;
codegen_config.mod_function_entry_funcs = mod_entry_funcs;
codegen_config.ws_cull_masked_reject_sites = ws_cull_masked_reject;
codegen_config.mod_instruction_sites = mod_instruction_sites;
codegen_config.hot_funcs = hot_funcs;
codegen_config.load_charge_batch_funcs = load_charge_batch_funcs;
codegen_config.vsync_query_hle_funcs = vsync_query_hle_funcs;
@@ -260,6 +260,19 @@ void patch_range_delay_boundaries(BiosConfig config) {
}
}
void mod_guest_dispatch_precedes_hardware_routes() {
const auto result = run_case("mod-guest-dispatch", {0x03E00008u, 0u},
{function_at(kBase, kBase + 4u, {kBase})});
const auto dispatch = result.dispatch.find("static void psx_dispatch_impl(");
const auto hook = result.dispatch.find("found = psx_mod_dispatch_guest_function(cpu, addr)", dispatch);
const auto hle = result.dispatch.find("if (!found && g_psx_bios_hle_hook", dispatch);
const auto hardware = result.dispatch.find("if (!found && psx_bios_try_native_call_stub", dispatch);
expect(dispatch != std::string::npos && hook > dispatch && hook < hle && hle < hardware,
"mod-defined guest calls are handled before address normalization and hardware dispatch");
expect(result.dispatch.find("if (g_psx_mod_guest_functions)", dispatch) < hook,
"no active mod guest functions keeps the dispatch path dormant");
}
int main() {
BiosConfig config{};
config.config_path = "test://full-function-emitter";
@@ -282,6 +295,7 @@ int main() {
fragment_split_load_falls_back();
noncomplementary_lwl_falls_back();
complementary_lwl_lwr_stays_native();
mod_guest_dispatch_precedes_hardware_routes();
patch_range_guards(config);
patch_range_delay_boundaries(config);
@@ -1160,6 +1160,47 @@ void cfg_codegen_load_delay_test() {
"CFG codegen preserves MIPS-I dependent load-delay value semantics");
}
void mod_function_completion_codegen_test() {
PSXRecomp::CodeGenConfig config{};
config.mod_function_entry_funcs.insert(0x80010000u);
for (bool overlay : {false, true}) {
const auto code = generate_first_instruction(0x2402002Au, {}, overlay, config);
check(code.find("if (psx_mod_function_entry(cpu, 0x80010000u)) return;") != std::string::npos,
overlay ? "overlay body honors trusted mod completion" : "main body honors trusted mod completion");
}
const auto stock = generate_first_instruction(0x2402002Au, {}, false);
check(stock.find("psx_mod_function_entry(cpu,") == std::string::npos,
"unconfigured functions retain the stock entry path");
}
void mod_instruction_codegen_test(const fs::path& root) {
const auto path = write_config(root, "instruction-hooks",
"mod_instruction_sites = [\"0x80010000\", \"0x80010004\"]\n");
const auto parsed = PSXRecompV4::load_game_config(path);
check(parsed.mod_instruction_sites.size() == 2, "instruction sites parse");
auto changed = parsed;
changed.mod_instruction_sites.clear();
check(PSXRecompV4::overlay_codegen_config_hash(parsed) !=
PSXRecompV4::overlay_codegen_config_hash(changed), "instruction sites invalidate cached code");
const auto invalid = write_config(root, "instruction-unaligned",
"mod_instruction_sites = [\"0x80010001\"]\n");
check_throws([&] { PSXRecompV4::load_game_config(invalid); }, "aligned main RAM",
"instruction sites reject invalid addresses");
PSXRecomp::CodeGenConfig config{};
config.mod_instruction_sites = {0x80010000u, 0x80010004u};
for (bool overlay : {false, true}) {
for (uint32_t word : {0x2402002Au, 0x10400002u}) {
const auto code = generate_first_instruction(word, {}, overlay, config);
check(code.find(fmt::format("psx_mod_instruction(cpu, 0x80010000u, 0x{:08X}u);", word)) != std::string::npos,
"native/overlay emits instruction guard for ALU and branch");
check(code.find("psx_mod_instruction(cpu, 0x80010004u, 0x00000000u);") != std::string::npos,
"native/overlay includes sequential and branch delay-slot hooks");
}
}
check(generate_first_instruction(0x2402002Au, {}, false).find("psx_mod_instruction(cpu,") == std::string::npos,
"unconfigured code emits no instruction callbacks");
}
void cfg_fallthrough_reachability_test() {
constexpr uint32_t base = 0x80010000u;
PSXRecomp::PS1Executable exe{};
@@ -1209,6 +1250,8 @@ int main() {
gte_codegen_classification_tests();
jump_table_producer_codegen_test();
cfg_codegen_load_delay_test();
mod_function_completion_codegen_test();
mod_instruction_codegen_test(root);
cfg_fallthrough_reachability_test();
} catch (const std::exception& e) {
fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what());
+43
View File
@@ -72,6 +72,14 @@ if(BUILD_TESTING)
target_compile_options(ws_world_scene_test PRIVATE -O2 -flto -ffunction-sections -fdata-sections)
target_link_options(ws_world_scene_test PRIVATE -flto -Wl,--gc-sections)
add_test(NAME ws_world_scene_test COMMAND ws_world_scene_test)
add_executable(boot_state_handoff_test tests/test_boot_state_handoff.c)
target_include_directories(boot_state_handoff_test PRIVATE include src)
target_compile_definitions(boot_state_handoff_test PRIVATE PSX_COSIM=1)
target_compile_options(boot_state_handoff_test PRIVATE -O2 -flto -ffunction-sections -fdata-sections -UNDEBUG)
target_link_options(boot_state_handoff_test PRIVATE -flto -Wl,--gc-sections)
target_link_libraries(boot_state_handoff_test PRIVATE ZLIB::ZLIB)
add_test(NAME boot_state_handoff_test COMMAND boot_state_handoff_test)
add_executable(mod_memory_snapshot_test tests/test_mod_memory_snapshot.c
src/psx_ram_geometry.c)
target_include_directories(mod_memory_snapshot_test PRIVATE include src)
@@ -95,6 +103,8 @@ if(BUILD_TESTING)
set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME counted_mips_relocations_test COMMAND "${Python3_EXECUTABLE}"
"${PSXRECOMP_ROOT}/tools/tests/test_counted_mips_relocations.py")
add_test(NAME interp_mod_entry_guards_test COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_interp_mod_entry_guards.py")
add_test(NAME mirror_pc_preserved_guards_test COMMAND "${Python3_EXECUTABLE}"
@@ -151,6 +161,17 @@ if(BUILD_TESTING)
else()
message(STATUS "gl_scale_invariance_test: not registered (needs a non-Windows host, Python 3 and SDL3)")
endif()
if(Python3_Interpreter_FOUND AND TARGET SDL3::SDL3-static)
add_test(NAME gl_texture_filter_test
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_texture_filter.py"
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_texture_filter.c"
--cc "${CMAKE_C_COMPILER}"
--sdl-include "$<TARGET_PROPERTY:SDL3::Headers,INTERFACE_INCLUDE_DIRECTORIES>"
--sdl-library "$<TARGET_FILE:SDL3::SDL3-static>"
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-filter-results")
set_tests_properties(gl_texture_filter_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 180)
endif()
add_executable(launcher_device_roundtrip_test
tests/test_launcher_device_roundtrip.cpp)
target_include_directories(launcher_device_roundtrip_test PRIVATE include)
@@ -706,6 +727,7 @@ if(BUILD_TESTING)
add_executable(mod_runtime_test
tests/test_mod_runtime.cpp
src/iso_reader_c.cpp
src/mod_runtime.cpp
src/mod_packages.cpp
src/disc_path.cpp
@@ -841,6 +863,27 @@ if(BUILD_TESTING)
endif()
add_test(NAME render_pass_plan_test COMMAND render_pass_plan_test)
# Motion kit and per-frame driver for render-pass plugins: blending,
# identity matching, limits and the crossfade fallback, on mock RAM.
add_executable(render_pass_motion_test
tests/test_render_pass_motion.c
src/render_pass_motion.c
src/render_pass_frame.c)
target_include_directories(render_pass_motion_test PRIVATE include)
if(NOT MSVC)
target_link_libraries(render_pass_motion_test PRIVATE m)
endif()
add_test(NAME render_pass_motion_test COMMAND render_pass_motion_test)
add_executable(render_pass_projection_test
tests/test_render_pass_projection.cpp
src/render_pass_projection.cpp
src/render_pass_motion.c)
target_include_directories(render_pass_projection_test PRIVATE include)
if(NOT MSVC)
target_link_libraries(render_pass_projection_test PRIVATE m)
endif()
add_test(NAME render_pass_projection_test COMMAND render_pass_projection_test)
# Render-pass time freeze against mock devices (psx_cycles.c only).
add_executable(render_pass_freeze_test
tests/test_render_pass_freeze.c
+5 -2
View File
@@ -42,9 +42,11 @@ extern "C" {
* v5 = v4 + CD-ROM Sub-Q replacement state;
* v6 = v5 + per-word GPU DMA2 linked-list progress;
* v7 = v6 + pending XA DATA_END IRQ state. */
#define BOOT_STATE_VERSION 8u
/* v8 adds allocated enhancement memory. Vanilla writers remain v7; older
#define BOOT_STATE_VERSION 9u
/* v8 added allocated enhancement memory (vanilla v8-era writers kept v7); older
* runtimes must not accept enhanced snapshots while ignoring their arenas. */
/* v9 persists the BIOS-to-game handoff latch. Engine overlays may have
* replaced the original entry code by the time a gameplay state is loaded. */
/* v7 intentionally breaks older savestates after the CD-ROM wire grew. Reject
* them at the header before any section changes the live machine. */
#define BOOT_STATE_VERSION_MIN_READ 7u
@@ -84,6 +86,7 @@ typedef struct {
* never allowed) -> normal boot + recapture.
*/
enum {
BS_SEC_GAME_START = 0x12, /* u32 LE boolean: BIOS-to-game handoff completed */
BS_SEC_MODMEM = 0x11, /* allocated opt-in CPU/GPU enhancement arenas */
BS_SEC_CPU = 0x01, /* CPUState: gpr/pc/hi/lo/cop0/gte_data/gte_ctrl */
BS_SEC_RAM = 0x02, /* 2 MB main RAM */
+28
View File
@@ -42,6 +42,34 @@ extern "C" {
* corruption, 2026-06-10). */
int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr);
/* Replay [start_pc, stop_pc) of already-loaded guest code from the CPU state
* given: every PC in the range is interpreted from its live RAM bytes, even
* where a compiled body or a compiled resume point (the return address of a
* call) begins, so a span can start in the middle of a compiled function.
* Calls the span makes run on their normal backend and return into it.
* Returns 1 when control reaches stop_pc, 0 when it leaves the range any other
* way (a return, a jump out, a nested span) or runs past max_insns
* interpreted instructions. The range is half-open, CODE identity, one span
* at a time. A span is a replay, not a function entry: it fires no
* function-entry hooks at its own PCs and stays out of capture, alias seeding
* and per-PC entry statistics. Instruction hooks fire as in compiled code.
* Mods reach it through psx_mod_run_guest_span (render passes only). */
int dirty_ram_run_span(CPUState* cpu, uint32_t start_pc, uint32_t stop_pc,
uint64_t max_insns);
/* Why the last span that failed stopped: 1 returned (pc 0), 2 left the range,
* 3 call-bail unwind, 4 instruction budget, 5 the interpreter declined the PC,
* 6 bad arguments or a span already open, 7 a call did not return to the
* span (`after`). `pc` is where control was. */
typedef struct DirtyRamSpanFailure {
uint32_t reason, pc, start_pc, stop_pc;
uint32_t ra, after; /* $ra at the exit; pc after a call that did not return */
uint64_t insns;
} DirtyRamSpanFailure;
void dirty_ram_span_last_failure(DirtyRamSpanFailure *out);
/* The open span (0,0 = none), for landings that longjmp past its exit. */
void dirty_ram_span_get(uint32_t *lo, uint32_t *hi);
void dirty_ram_span_set(uint32_t lo, uint32_t hi);
/* Retire a deferred R3000A load-delay writeback (see dirty_ram_interp.c). The
* interpreter defers a load's destination-register write past the delay-slot
* instruction, as hardware does; call this anywhere control leaves the
+3
View File
@@ -89,6 +89,9 @@ void fntrace_record(CPUState* cpu, uint32_t target);
* First dispatch into [lo, hi) calls cdrom_notify_game_started(). */
void fntrace_set_game_range(uint32_t lo, uint32_t hi);
int fntrace_is_game_started(void);
/* Snapshot restore only: the handoff side effects are already in the saved
* machine. Do not clear restored RAM or capture another boot snapshot. */
void fntrace_restore_game_started(int started);
/* One-shot game-start handoff. Idempotent — safe to call from any path
* (compiled dispatch, dirty-RAM interpreter, generated entry function).
* Performs dirty-image baseline clear, low-boot scratch clear, CD speed
+3
View File
@@ -66,6 +66,9 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
* rect at the same scale. Returns 0 when refused (nothing changed). */
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
uint32_t period_vblanks, int reuse_backup);
/* Generations opened from now on are for a frame already on screen (1) or
* for the next flip (0, default); render_pass_gen_flip_matches. */
void gl_renderer_pass_set_flip_shown(int shown);
/* Capture the drawn rect at alpha_q16 (keep) and roll the rect back. */
void gl_renderer_pass_end(uint32_t alpha_q16, int keep);
uint32_t gl_renderer_pass_leaks(void);
+10
View File
@@ -525,6 +525,8 @@ bool mod_register_function_entry_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback);
bool mod_register_function_filter_plugin(const std::string& id, uint32_t address,
PSXModFunctionFilterCallback callback);
bool mod_register_guest_function_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback);
bool mod_plugin_registered(const std::string& id);
void mod_invoke_activation_plugin(const std::string& id);
void mod_invoke_vblank_plugin(const std::string& id);
@@ -536,6 +538,14 @@ struct ModFunctionEntryHook {
/* Hooks one implementation registered, in registration order. mod_runtime
* flattens these into an address table when the plan's plugins activate. */
std::vector<ModFunctionEntryHook> mod_function_entry_hooks(const std::string& id);
std::vector<ModFunctionEntryHook> mod_guest_functions(const std::string& id);
struct ModInstructionHook {
uint32_t address, expected;
PSXModFunctionEntryCallback callback;
};
bool mod_register_instruction_plugin(const std::string& id, uint32_t address,
uint32_t expected, PSXModFunctionEntryCallback callback);
std::vector<ModInstructionHook> mod_instruction_hooks(const std::string& id);
void mod_clear_plugins_for_tests();
} // namespace PSXRecompV4
+102
View File
@@ -27,6 +27,26 @@ int psx_mod_register_vblank_plugin(const char* id,
PSXModVBlankCallback callback);
int psx_mod_register_function_entry_plugin(
const char* id, uint32_t address, PSXModFunctionEntryCallback callback);
/* Mod-defined guest functions have no original machine-code body. Addresses
* must be aligned and in physical 0x0F000000..0x0FFFFFFF (an unused bus range),
* never hardware/BIOS/game text. Only statically linked code can register one;
* the active package plan owns its availability and resource context. Address
* aliases share one globally unique registration. The callback supplies the
* full function behavior; return publishes $ra through normal dispatch. */
int psx_mod_register_guest_function_plugin(
const char* id, uint32_t address, PSXModFunctionEntryCallback callback);
int psx_mod_dispatch_guest_function(struct CPUState* cpu, uint32_t address);
extern uint32_t g_psx_mod_guest_functions;
/* Run immediately before a configured instruction, including delay slots.
* The complete instruction word must match both registration and live RAM.
* Callbacks may update registers/data but cannot redirect PC, finish a guest
* function, or re-enter guest execution (pending load/branch state is live).
* Native emits opt in with [recompiler] mod_instruction_sites; dirty-RAM
* execution uses the same guarded table. Inactive plans are inert. */
int psx_mod_register_instruction_plugin(const char* id, uint32_t address,
uint32_t expected, PSXModFunctionEntryCallback callback);
void psx_mod_instruction(struct CPUState* cpu, uint32_t address, uint32_t instruction);
extern uint32_t g_psx_mod_instruction_hooks;
/* Called from generated functions listed by the game config and from every
* interpreted entry, so the hook contract does not depend on the backend.
* Hooks match by code address (segment bits ignored) and run only for plugins
@@ -34,9 +54,26 @@ int psx_mod_register_function_entry_plugin(
int psx_mod_register_function_filter_plugin(
const char* id, uint32_t address, PSXModFunctionFilterCallback callback);
int psx_mod_function_entry(struct CPUState* cpu, uint32_t address);
/* Complete a guest function from its trusted entry callback after supplying
* its full result. Valid only for that callback's CPU. Publishes pc=$ra and
* prevents the original body from executing. Nested callbacks have separate
* completion scopes; requests outside an entry callback return zero. */
int psx_mod_finish_function(struct CPUState* cpu);
/* Active function-entry hook count (0 = none). Hot callers test it before the
* 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);
/* Presentation-only filtering: 0 nearest, 1 bilinear, 2 stable minification.
* Mode 2 uses a bounded palette-aware footprint for proven 3D polygons on
* OpenGL; untracked UI stays nearest. Other backends use bilinear. A session
* reset restores the player's configured filter. */
void psx_mod_set_texture_filter(int mode);
/* Entry callbacks can make nested guest calls while retaining host registers.
* Save/load and rewind must wait until that host context has returned. */
int psx_mod_function_entry_active(void);
@@ -227,6 +264,10 @@ 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);
/* 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.
@@ -372,6 +413,67 @@ enum {
PSX_MOD_RENDER_PASS_BUSY = 6
};
uint32_t psx_mod_render_pass_status(void);
/*
* When the game flips relative to the pass point. PENDING (default, reset at
* every session start): the pass rect is the one the next flip will show --
* the PsyQ VSync(0)-then-PutDispEnv loop, whose frame N is drawn and waits.
* SHOWN: the game flips each frame as soon as it is drawn (for example
* PutDispEnv in a VBlank callback armed by the DrawSync callback), so by the
* time frame N+1's logic is done frame N is already on screen. The pass rect
* is then that on-screen rect, and frame N's own image and its in-between
* images are shown from the game's next flip on: one game frame later than
* the game shows them, as a host interpolator with one frame of history
* would. Set it from activation, before the first plan.
*/
enum {
PSX_MOD_RENDER_PASS_FLIP_PENDING = 0,
PSX_MOD_RENDER_PASS_FLIP_SHOWN = 1
};
int psx_mod_set_render_pass_flip(uint32_t mode);
/*
* The per-frame plumbing every frame-rate plugin repeats (render_pass_frame.c).
* Blending the game state itself: render_pass_motion.h.
*
* Activation: read a choice option holding "display" (follow the measured
* display refresh) or a frame rate, and select the FLIP source, HOLD blend,
* `flip_mode` and that rate. Returns 0 if any setting was refused.
*/
int psx_mod_activate_render_pass_rate(const char* package, const char* feature,
const char* option, uint32_t flip_mode);
typedef struct PSXModRenderPassFrame {
uint32_t struct_size; /* sizeof(PSXModRenderPassFrame) */
uint32_t period_vblanks; /* VBlanks this game frame stays on screen */
uint32_t shown_after_vblanks; /* VBlank presents before it is shown */
uint16_t x, y, w, h; /* VRAM rect the passes draw */
} PSXModRenderPassFrame;
/*
* Plan this game frame's passes and run `fn` at each phase. While passes are
* unavailable for a lasting reason (BACKEND, DISABLED) the presenter is
* switched to a motion-adaptive crossfade, and back to HOLD once they return.
* Returns how many passes produced an image.
*/
uint32_t psx_mod_render_pass_frame(struct CPUState* cpu,
const PSXModRenderPassFrame* frame,
PSXModRenderPassFn fn, void* user);
/*
* Replay part of an already-loaded guest function inside a render pass:
* run from start_pc with the CPU state given until control reaches stop_pc.
* Every PC in [start_pc, stop_pc) is interpreted from its RAM bytes, so the
* span may start anywhere in a compiled function, including just after one
* of its calls; calls the span makes run normally and return into it.
* A plugin uses this to redraw with the game's own frame code, branches and
* all, from registers it captured at start_pc during the real frame (an
* instruction hook), instead of re-implementing that code's call sequence.
* Returns 1 when stop_pc is reached; 0 when control leaves the range another
* way (a return or jump out, a nested span), after 1M interpreted
* instructions, or outside a pass. The pass restores the machine either way.
* Both PCs are 4-aligned, in one segment, start_pc < stop_pc.
*/
int psx_mod_run_guest_span(struct CPUState* cpu, uint32_t start_pc,
uint32_t stop_pc);
int psx_mod_set_auto_skip_fmv(int enabled);
/*
* Draw still artwork behind the game image in OpenGL letterbox/pillarbox
+9 -2
View File
@@ -117,7 +117,8 @@
/* v26: guarded packed-coordinate rejection callback, preserving the selected
* rejection axes while the native-wide renderer clips horizontal margins. */
/* v27: full-word-bound native-wide winding branch callback. */
#define PSX_OVERLAY_ABI_VERSION 27
/* v28: guarded instruction callbacks and scoped guest-function completion. */
#define PSX_OVERLAY_ABI_VERSION 28
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
* record consumes one slot, even when another DLL carries an identical
@@ -185,7 +186,7 @@
/* 13: merge adaptive backdrop v12: independent quotient consumers, proven
* copied/biased camera inputs, and two biased bounds for terrain strips. */
/* 14: guarded saturation-aware NCLIP branch consumers and ABI v27 forwarding. */
#define PSX_OVERLAY_CODEGEN_VER 14
#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>/)
@@ -363,6 +364,12 @@ typedef struct OverlayCallbacks {
uint32_t *last_store_pc;
int (*ws_masked_reject)(uint32_t flags, uint32_t mask);
int (*ws_nclip_branch)(uint32_t pc, uint32_t instr, int32_t mac0, int vanilla);
/* 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);
/* Guarded pre-instruction mod callback (ABI v27). */
void (*mod_instruction)(CPUState*, uint32_t, uint32_t);
} OverlayCallbacks;
#ifdef __cplusplus
@@ -256,6 +256,10 @@ int32_t psx_ws_player_x_bound(int32_t vanilla) {
int32_t psx_ws_screen_x_bound(int32_t vanilla) {
return g_cbs.ws_screen_x_bound ? g_cbs.ws_screen_x_bound(vanilla) : vanilla;
}
void psx_mod_instruction(CPUState *cpu, uint32_t address, uint32_t word) {
overlay_flush_cycles();
if (g_cbs.mod_instruction) g_cbs.mod_instruction(cpu, address, word);
}
int psx_mod_function_entry(CPUState *cpu, uint32_t address) {
overlay_flush_cycles();
return g_cbs.mod_function_entry ? g_cbs.mod_function_entry(cpu, address) : 0;
+6
View File
@@ -266,6 +266,12 @@ void pgxp_store_gte_reg(uint32_t addr, uint8_t reg);
int pgxp_load_precise_word(uint32_t addr, uint32_t packed,
int32_t *x16, int32_t *y16, uint16_t *z);
/* Render-pass checkpoint: journal every shadow a sandboxed pass mutates and
* put the shadows back when the sandbox restores the machine. Use through
* gte_precision_checkpoint_begin / _rollback (which also cover gte.cpp). */
void pgxp_checkpoint_begin(void);
void pgxp_checkpoint_rollback(void);
/* --- test accessors (always compiled; trivial) ---------------------------- */
/* index 0..3 selects the SXY0..SXYP register shadow (GTE data regs 12..15). */
+2
View File
@@ -38,6 +38,8 @@ typedef struct RenderPassStats {
uint64_t nesting_repairs; /* aborts whose skipped exits the restore undid */
uint64_t verify_checks; /* PSX_RENDER_PASS_VERIFY comparisons */
uint64_t verify_mismatch; /* ... that found a difference */
uint64_t spans; /* psx_mod_run_guest_span calls that reached stop */
uint64_t span_failures; /* ... that left the span or overran */
uint64_t dropped[RENDER_PASS_DROP_CLASSES];
double last_pass_ms; /* host time of the last pass (sandbox + draw) */
double avg_pass_ms; /* smoothed */
+123
View File
@@ -0,0 +1,123 @@
#ifndef PSX_RENDER_PASS_MOTION_H
#define PSX_RENDER_PASS_MOTION_H
/*
* Motion kit for render-pass plugins (docs/RENDER_PASSES.md, "Blending game
* state"). A pass redraws the scene with the game's own code between two
* logic ticks; what makes the image an in-between one is the game state the
* draw code reads, placed part of the way from the previous tick to the
* current one. This kit owns that part for any title:
*
* per game frame, at the plugin's capture point
* psx_motion_begin(set, tick) the game's logic tick counter
* psx_motion_track(set, kind, addr, identity) for each value the draw
* code reads (object matrices,
* positions, angles, the camera)
* psx_motion_prepare(set, &limits, &stats) pair with the previous frame
* in each pass
* psx_motion_apply(set, alpha) write the blends into guest RAM
* psx_motion_blend_at(set, addr, ...) or blend one value a hook hands on
*
* Values are matched by address AND identity: game objects are heap blocks,
* and a freed address reused by another object must never blend between the
* two. A value that moved further than the limits allow in the ticks between
* the frames was placed (respawn, teleport, camera cut), not moved; it keeps
* its current value, as do values without history. Nothing is written
* outside psx_motion_apply, which a pass's sandbox rolls back.
*
* Guest RAM is read and written through psx_mod_read_* / psx_mod_write_*.
*/
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* What lives at a tracked address (guest RAM layout). */
enum {
/* PsyQ MATRIX: 3x3 s16 rotation in 4.12 at +0x00..+0x11, s32 t[3] at
* +0x14. Rotation slerps when both rows are orthonormal, otherwise it
* blends elementwise; translation blends linearly. */
PSX_MOTION_MATRIX = 1,
/* Only the translation of a MATRIX (+0x14..+0x1F): for matrices whose
* rotation the draw code rebuilds itself (billboards). */
PSX_MOTION_MATRIX_TRANSLATION = 2,
PSX_MOTION_VECTOR = 3, /* s32 x, y, z */
PSX_MOTION_SVECTOR = 4, /* s16 x, y, z */
/* s16 x, y, z angles, 4096 per turn: each blends along the shorter arc. */
PSX_MOTION_ANGLES = 5,
PSX_MOTION_SCALAR = 6, /* s32 */
/* A bare 3x3 s16 rotation in 4.12 (18 bytes), blended like a MATRIX's;
* pair it with a VECTOR / SVECTOR kind wherever its translation lives
* (skeleton pose records, matrices stored apart from their position). */
PSX_MOTION_ROTATION = 7
};
typedef struct PSXMotionLimits {
/* Distance one value may move per logic tick and still blend
* (translation, VECTOR, SVECTOR, SCALAR, in the game's units). */
double move_per_tick;
/* Rotation between the two frames beyond which a value snaps
* (MATRIX rotation, ANGLES). 0 means pi (any turn up to a half turn). */
double turn_radians;
} PSXMotionLimits;
typedef struct PSXMotionStats {
uint32_t tracked; /* values captured this frame */
uint32_t blended; /* ... that will blend */
uint32_t placed; /* ... that moved or turned past the limits */
uint32_t unmatched; /* ... new, or a different identity at the address */
uint32_t ticks; /* logic ticks between the two frames (0 = no history) */
} PSXMotionStats;
typedef struct PSXMotionMatrix {
int16_t r[9];
int32_t t[3];
} PSXMotionMatrix;
typedef struct PSXMotionSet PSXMotionSet;
PSXMotionSet* psx_motion_set_create(uint32_t capacity);
void psx_motion_set_destroy(PSXMotionSet* set);
/* Start this frame's capture. `tick` is the game's logic tick counter at
* this frame; the previous capture becomes the history. */
void psx_motion_begin(PSXMotionSet* set, uint32_t tick);
/* Record the value of `kind` at `addr` as the draw code will read it.
* `identity` names what lives there (psx_motion_identity of its type,
* handler, model...). One value per (addr, kind) per frame; a repeat
* replaces the earlier one. Returns 0 when the set is full. */
int psx_motion_track(PSXMotionSet* set, uint32_t kind, uint32_t addr,
uint32_t identity);
/* Pair this capture with the previous one. Returns how many values will
* blend. History counts only when it is the capture just before this one
* with 1..8 ticks between them; otherwise nothing blends. */
uint32_t psx_motion_prepare(PSXMotionSet* set, const PSXMotionLimits* limits,
PSXMotionStats* stats);
/* Write every prepared value at t (0 = previous frame, 1 = this frame). */
void psx_motion_apply(const PSXMotionSet* set, double t);
/* Blend one prepared value at t into `out` without writing guest RAM:
* a PSXMotionMatrix for the MATRIX kinds and ROTATION (translation 0), int32_t[3] for VECTOR / SVECTOR /
* ANGLES, int32_t for SCALAR. Returns 0 when that value does not blend this
* frame (out then holds its current value) or was not tracked (out
* untouched, returns -1). */
int psx_motion_blend_at(const PSXMotionSet* set, uint32_t kind, uint32_t addr,
double t, void* out);
/* Forget all history (scene change, load, a frame the plugin skipped). */
void psx_motion_invalidate(PSXMotionSet* set);
/* Mix up to three words into an identity tag. */
uint32_t psx_motion_identity(uint32_t a, uint32_t b, uint32_t c);
/* The math, for plugins that blend values the set does not hold. Returns 0
* when the rotation turned past turn_radians (out = b). */
int psx_motion_blend_matrix(const PSXMotionMatrix* a, const PSXMotionMatrix* b,
double t, double turn_radians,
PSXMotionMatrix* out);
void psx_motion_read_matrix(uint32_t addr, PSXMotionMatrix* out);
void psx_motion_write_matrix(uint32_t addr, const PSXMotionMatrix* m,
int rotation);
#ifdef __cplusplus
}
#endif
#endif
+15 -1
View File
@@ -65,6 +65,18 @@ int render_pass_select(const uint32_t *phases, uint32_t n, double p,
int render_pass_gen_select(const uint32_t *phases, uint32_t n, double p,
uint32_t *lo, uint32_t *hi, float *t);
/* Is the flip the FLIP source just saw the one a pending generation waits
* for? A generation built for the next flip (`shown` 0, the canonical PsyQ
* VSync-then-PutDispEnv point) waits for the display to flip to its own rect.
* One built for a frame already on screen (`shown` 1: the game flips each
* frame as soon as it is drawn) waits for the next flip, to any other rect;
* its images then show one game frame late. Either way the presented
* geometry must still be the one the generation was captured at. */
int render_pass_gen_flip_matches(int shown, int gen_x, int gen_y,
int gen_source, int gen_w, int gen_h,
int flip_x, int flip_y, int flip_source,
int flip_w, int flip_h);
/* Exponential moving average used for the per-pass host cost. */
double render_pass_ema(double current, double sample);
@@ -149,7 +161,9 @@ int render_pass_store_to(const RenderPassStoreTarget *t, uint32_t addr,
/* VRAM journal for a pass's writes outside its display rect
* (gpu_gl_renderer.c pass_refuse_write): the CPU-side policy and the CPU VRAM
* rows. The renderer keeps the matching GPU copies per entry. */
#define RENDER_PASS_JOURNAL_MAX 16
/* Jersey Devil streams more than 16 small texture/CLUT rectangles in a draw.
* Keep the transaction bounded, but allow the complete set to roll back. */
#define RENDER_PASS_JOURNAL_MAX 64
enum {
RENDER_PASS_VRAM_ALLOW = 0, /* inside the rect, empty, or already journaled */
RENDER_PASS_VRAM_JOURNAL = 1, /* outside: back up, then allow */
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct CPUState;
typedef struct PSXProjectionHistory PSXProjectionHistory;
typedef struct PSXProjectionStats {
uint32_t captured, matched, changed, unmatched, discontinuities, overflow, replayed;
} PSXProjectionStats;
/* Capture the final model/view transforms that the game actually projects.
* Replay uses the same guest draw code, with interpolated transforms only for
* identified vertices. Unmatched vertices and camera cuts keep current state.
* A history belongs to one game drawing section; emulation thread only. */
PSXProjectionHistory* psx_projection_create(uint32_t capacity);
void psx_projection_destroy(PSXProjectionHistory* history);
void psx_projection_invalidate(PSXProjectionHistory* history);
void psx_projection_reset_session(void);
void psx_projection_capture_begin(PSXProjectionHistory* history);
void psx_projection_capture_end(PSXProjectionHistory* history, uint32_t ticks,
double max_translation_per_tick);
void psx_projection_replay_begin(PSXProjectionHistory* history, uint32_t alpha_q16);
void psx_projection_replay_end(PSXProjectionHistory* history);
void psx_projection_stats(const PSXProjectionHistory* history, PSXProjectionStats* out);
/* GTE bridge: before RTPS/RTPT imports its registers. Returns nonzero only
* when a render pass temporarily replaced rotation/translation. The bridge
* restores those eight control registers after exporting command results. */
int psx_projection_command(struct CPUState* cpu, uint32_t command);
extern int (*g_psx_projection_command)(struct CPUState*, uint32_t);
#ifdef __cplusplus
}
#endif
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include "cpu_state.h"
#include "mod_plugins.h"
#include "psx_cycle_freeze.h"
#include "render_pass_projection.h"
#include "pgxp.h"
#include <cstring>
#include <vector>
extern "C" uint8_t* memory_get_ram_ptr(void);
extern "C" uint8_t* memory_get_scratchpad_ptr(void);
extern "C" uint32_t memory_get_ram_bytes(void);
/* A draw section may calculate temporary matrices, consume random seeds or
* update effect caches. Start every replay from the original section's RAM,
* rather than applying those calculations twice to the completed frame. Only
* restore inside the framework's machine/VRAM sandbox. */
class PSXDrawReplay {
CPUState entry{};
std::vector<uint8_t> ram;
uint8_t scratch[1024]{};
uint32_t start = 0, stop = 0, previous_tick = 0;
bool captured = false, ready = false, have_tick = false;
PSXProjectionHistory* history = nullptr;
public:
PSXProjectionStats stats{};
uint32_t ticks = 0;
~PSXDrawReplay() { psx_projection_destroy(history); }
void invalidate() {
captured = ready = have_tick = false; ticks = 0;
psx_projection_invalidate(history);
}
void capture(CPUState* cpu, uint32_t first, uint32_t last) {
if (g_psx_render_pass_active) return;
if (captured) invalidate(); // previous drawing section never completed
if (!history) history = psx_projection_create(65536);
if (!history || first >= last || ((first | last) & 3)) return;
entry = *cpu; start = first; stop = last;
ram.resize(memory_get_ram_bytes());
std::memcpy(ram.data(), memory_get_ram_ptr(), ram.size());
std::memcpy(scratch, memory_get_scratchpad_ptr(), sizeof scratch);
captured = true; ready = false;
psx_projection_capture_begin(history);
}
bool prepare(uint32_t tick) {
if (!captured) { invalidate(); return false; }
captured = false;
ticks = have_tick ? tick - previous_tick : 0;
previous_tick = tick; have_tick = true;
psx_projection_capture_end(history, ticks, 512.0);
psx_projection_stats(history, &stats);
ready = ticks && ticks <= 8 && stats.changed && !stats.overflow;
return ready;
}
bool restore(CPUState* cpu, uint32_t alpha) {
if (!ready || !g_psx_render_pass_active || ram.size() != memory_get_ram_bytes()) return false;
std::memcpy(memory_get_ram_ptr(), ram.data(), ram.size());
std::memcpy(memory_get_scratchpad_ptr(), scratch, sizeof scratch);
// Raw RAM is now from section entry. Its current-frame precision
// shadows are stale; the pass checkpoint restores them afterwards.
pgxp_invalidate_all();
*cpu = entry;
psx_projection_replay_begin(history, alpha);
return true;
}
bool draw(CPUState* cpu) {
const bool reached = psx_mod_run_guest_span(cpu, start, stop) != 0;
psx_projection_replay_end(history);
psx_projection_stats(history, &stats);
return reached && stats.replayed != 0;
}
static uint32_t call(CPUState* cpu, uint32_t function, uint32_t a0=0, uint32_t a1=0) {
const CPUState saved = *cpu;
cpu->pc = 0; cpu->gpr[4] = a0; cpu->gpr[5] = a1;
psx_dispatch_call(cpu, function, saved.gpr[31]);
const uint32_t result = cpu->gpr[2]; *cpu = saved; return result;
}
static void rate(uint32_t fps, uint32_t flip) {
psx_mod_set_frame_interpolation_source(PSX_MOD_FRAME_SOURCE_FLIP);
psx_mod_set_render_pass_flip(flip);
psx_mod_set_frame_interpolation_blend(PSX_MOD_FRAME_INTERPOLATION_HOLD);
psx_mod_set_frame_interpolation(fps);
}
};
+3
View File
@@ -362,6 +362,9 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/frame_interpolation.c
${PSXRECOMP_ROOT}/runtime/src/render_pass.c
${PSXRECOMP_ROOT}/runtime/src/render_pass_plan.c
${PSXRECOMP_ROOT}/runtime/src/render_pass_motion.c
${PSXRECOMP_ROOT}/runtime/src/render_pass_projection.cpp
${PSXRECOMP_ROOT}/runtime/src/render_pass_frame.c
${PSXRECOMP_ROOT}/runtime/src/host_time.c
${PSXRECOMP_ROOT}/runtime/src/psx_fiber.c
${PSXRECOMP_ROOT}/runtime/src/sio.c
+20 -2
View File
@@ -1,4 +1,5 @@
#include "boot_state.h"
#include "fntrace.h"
#include "mod_memory.h"
#include "overlay_api.h" /* PSX_OVERLAY_CODEGEN_HASH / _ABI_TAG / _CODEGEN_VER */
#include "dirty_ram_interp.h"
@@ -383,14 +384,14 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
int ok;
memset(&h, 0, sizeof h);
h.magic = BOOT_STATE_MAGIC;
h.version = psx_mod_memory_snapshot_bytes() ? BOOT_STATE_VERSION : 7u;
h.version = BOOT_STATE_VERSION;
h.reserved = boot_state_layout_cookie();
h.bios_checksum = bios_checksum;
h.entry_pc = entry_pc;
h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH;
h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG;
h.codegen_ver = (uint32_t)PSX_OVERLAY_CODEGEN_VER;
h.section_count = 16 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u);
h.section_count = 17 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u);
ok = write_header_le(o, &h);
@@ -481,6 +482,13 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
free(db);
}
}
if (ok) {
uint8_t handoff[4];
PstW w;
pst_w_init(&w, handoff, sizeof handoff);
ok = pst_w_u32(&w, fntrace_is_game_started() ? 1u : 0u) &&
write_section(o, BS_SEC_GAME_START, handoff, sizeof handoff);
}
return ok;
}
@@ -693,6 +701,9 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
}
case BS_SEC_MODMEM:
return psx_mod_memory_snapshot_read(p, len);
case BS_SEC_GAME_START:
fntrace_restore_game_started(p[0]);
return 1;
case BS_SEC_ICACHE: {
PstR r;
if (len != 1024u * 4u) return 0;
@@ -749,6 +760,8 @@ static int validate_section(uint32_t tag, const uint8_t* p, uint32_t len) {
case BS_SEC_DIRTY: return len % 4u == 0u &&
len / 4u <= dirty_ram_get_bitmap_word_count();
case BS_SEC_MODMEM: return psx_mod_memory_snapshot_validate(p, len);
case BS_SEC_GAME_START:
return len == 4u && p[0] <= 1u && !p[1] && !p[2] && !p[3];
case BS_SEC_ICACHE: return len == 1024u * 4u;
default: return 1;
}
@@ -970,10 +983,15 @@ int boot_state_load_buffer(const uint8_t* file, size_t file_len,
}
s->tag = tag;
if (!validate_section(tag, s->p, s->len)) { ok = 0; break; }
if (tag == BS_SEC_GAME_START && (seen & (1u << tag))) {
ok = 0; break;
}
if (tag < 32) seen |= (1u << tag);
}
if (ok && (seen & required) != required)
ok = 0;
if (ok && h.version >= 9u && !(seen & (1u << BS_SEC_GAME_START)))
ok = 0;
/* PASS 2 -- apply. Every section passed validate_section, whose checks
* are exactly the preconditions apply_section can fail on, so this pass
+53 -4
View File
@@ -7833,6 +7833,45 @@ static void handle_input_route_status(int id, const char *json)
* {"cmd":"ws_hud_mode","tag_rects":0|1}
* tag_rects=1 lets TAGGED rect-family prims re-anchor too (Tomba's AP
* counter renders through the tagged sprite funnel). */
/* 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);
}
static void handle_ws_hud_mode(int id, const char *json)
{
int v = json_get_int(json, "tag_rects", -1);
@@ -7991,7 +8030,9 @@ static void handle_render_pass_stats(int id, const char *json)
RenderPassStats st;
uint64_t gd[10], image_bytes = 0;
uint32_t image_textures;
DirtyRamSpanFailure sf;
render_pass_get_stats(&st);
dirty_ram_span_last_failure(&sf);
gl_renderer_pass_diag(gd);
image_textures = gl_renderer_pass_image_textures(&image_bytes);
send_fmt("{\"id\":%d,\"ok\":true,\"plans\":%llu,\"planned\":%llu,"
@@ -8011,7 +8052,10 @@ static void handle_render_pass_stats(int id, const char *json)
"\"cost_us\":%llu,\"cost_rewarms\":%llu,\"frame_images\":%llu,"
"\"journaled\":%llu,"
"\"image_textures\":%u,\"image_bytes\":%llu,\"status\":%u,"
"\"backups_reused\":%llu}",
"\"backups_reused\":%llu,\"spans\":%llu,\"span_failures\":%llu,"
"\"span_fail\":{\"reason\":%u,\"pc\":\"0x%08X\",\"start\":\"0x%08X\","
"\"stop\":\"0x%08X\",\"ra\":\"0x%08X\",\"after\":\"0x%08X\","
"\"insns\":%llu}}",
id, (unsigned long long)st.plans, (unsigned long long)st.planned,
(unsigned long long)st.wanted, (unsigned long long)st.refused,
(unsigned long long)st.passes, (unsigned long long)st.aborted,
@@ -8037,7 +8081,11 @@ static void handle_render_pass_stats(int id, const char *json)
(unsigned long long)gl_renderer_pass_journaled(),
(unsigned)image_textures, (unsigned long long)image_bytes,
(unsigned)psx_mod_render_pass_status(),
(unsigned long long)gl_renderer_pass_backups_reused());
(unsigned long long)gl_renderer_pass_backups_reused(),
(unsigned long long)st.spans, (unsigned long long)st.span_failures,
(unsigned)sf.reason, (unsigned)sf.pc, (unsigned)sf.start_pc,
(unsigned)sf.stop_pc, (unsigned)sf.ra, (unsigned)sf.after,
(unsigned long long)sf.insns);
}
/* render_pass_refuse on=<0|1>: make the OpenGL backend decline render passes
@@ -9133,7 +9181,7 @@ static void handle_video_info(int id, const char *json)
gpu_get_display_info(&di);
int eff = gr_scale();
send_fmt("{\"id\":%d,\"ok\":true,\"backend\":\"%s\",\"preset\":%d,"
"\"reference_lines\":%d,\"requested_scale\":%d,\"effective_scale\":%d,"
"\"reference_lines\":%d,\"requested_scale\":%d,\"effective_scale\":%d,\"texture_filter\":%d,"
"\"internal_lines\":%u,\"gl\":%d,\"gl_max_dim\":%d,\"gl_max_scale\":%d,"
"\"gl_clamp_reason\":%d,\"gl_alloc_retries\":%d,\"gl_budget_mib\":%d,"
"\"fbo_w\":%d,\"fbo_h\":%d,\"hidpi_window\":%d,\"window_w\":%d,"
@@ -9145,7 +9193,7 @@ static void handle_video_info(int id, const char *json)
"\"hires_window_mib\":%d}",
id, gr_backend() == GR_BACKEND_OPENGL ? "opengl"
: gr_backend() == GR_BACKEND_VULKAN ? "vulkan" : "software",
preset, ref, req, eff, di.height * (unsigned)(eff > 0 ? eff : 1), gl,
preset, ref, req, eff, gr_texture_filter(), di.height * (unsigned)(eff > 0 ? eff : 1), gl,
si.max_dim, si.max_scale, si.clamp_reason, si.alloc_retries, si.budget_mib,
si.fbo_w, si.fbo_h, hidpi, ww, wh, pw, ph, di.display_x, di.display_y,
di.width, di.height,
@@ -14147,6 +14195,7 @@ 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 },
{ "mod_counters", handle_mod_counters },
{ "kernel_bless", handle_kernel_bless },
{ "ws_aspect", handle_ws_aspect },
{ "display_aspect", handle_display_aspect },
+106 -14
View File
@@ -526,6 +526,17 @@ static int interp_refused_game_text(uint32_t pc) {
#endif
}
/* Guest span replay (dirty_ram_run_span, psx_mod_run_guest_span): while a span
* is open, every PC in [s_span_lo, s_span_hi) (CODE identity) is interpreted
* from the live RAM bytes, even where a compiled body or one of its resume
* points starts there. Calls the span makes leave the range and run on their
* normal backend. A span is a replay, not an entry: it stays out of capture,
* seeding and entry statistics. */
static uint32_t s_span_lo, s_span_hi;
static inline int span_forced(uint32_t phys) {
return phys >= s_span_lo && phys < s_span_hi;
}
/* Forward decls from memory.c — used to read instruction bytes. */
extern uint8_t *memory_get_ram_ptr(void);
extern void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);
@@ -1510,6 +1521,10 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,
}
}
extern uint32_t g_psx_mod_instruction_hooks;
extern void psx_mod_instruction(CPUState*, uint32_t, uint32_t);
if (g_psx_mod_instruction_hooks) psx_mod_instruction(cpu, pc, insn);
/* op 0x20..0x26 = LB/LH/LWL/LW/LBU/LHU/LWR. LWC2 (GTE, 0x32) targets a COP2
* register, not a GPR, so it needs no deferral here. */
const uint32_t ld_op = op_field(insn);
@@ -1751,7 +1766,9 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
#endif
uint32_t _cr = callret_begin(cpu, pc, target); /* call-resolution ring */
#define CRET(code, rv) do { callret_end(_cr, cpu, (code)); return (rv); } while (0)
if (g_precise_mode || g_ls_replay_active) { cpu->pc = target; CRET(CRES_PLAIN, 1); } /* slice / lockstep-replay: plain transfer, never execute the callee */
/* slice / lockstep-replay / guest span: plain transfer, never execute the
* callee (a span's runner calls it with the full trampoline). */
if (g_precise_mode || g_ls_replay_active || span_forced(pc & 0x1FFFFFFFu)) { cpu->pc = target; CRET(CRES_PLAIN, 1); }
if (rd != 31) {
/* No architectural $ra contract: preserve the transfer as a
* pc-chain and let the callee's eventual JR choose the real
@@ -1941,7 +1958,9 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
return 0;
}
default:
return abort_unsupported(pc, insn, "SPECIAL funct");
/* R3000A ignores undefined SPECIAL encodings; psx_interpreter.c
* already models this, so the dirty interpreter must agree. */
return 0;
}
break;
@@ -1969,7 +1988,9 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
#else
#define XRES(code) do { (void)(code); } while (0)
#endif
if (g_precise_mode || g_ls_replay_active) { cpu->pc = target; return 1; } /* slice / lockstep-replay: plain transfer, never execute the callee */
/* slice / lockstep-replay / guest span: plain transfer, never execute the
* callee (a span's runner calls it with the full trampoline). */
if (g_precise_mode || g_ls_replay_active || span_forced(pc & 0x1FFFFFFFu)) { cpu->pc = target; return 1; }
#ifdef PSX_HAS_GAME_DISPATCH
cpu->pc = 0;
if (interp_enter_compiled(cpu, target)) {
@@ -2459,6 +2480,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_addr);
/* Public entry point. Caller (psx_dispatch) has translated `addr` to a
* KSEG-stripped form already in some cases, so accept any address and
* mask. Returns 1 if interpretation handled the basic block; 0 if the
@@ -2598,6 +2620,74 @@ int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {
return r;
}
void dirty_ram_span_get(uint32_t *lo, uint32_t *hi) {
if (lo) *lo = s_span_lo;
if (hi) *hi = s_span_hi;
}
void dirty_ram_span_set(uint32_t lo, uint32_t hi) {
s_span_lo = lo;
s_span_hi = hi;
}
static DirtyRamSpanFailure s_span_failure;
void dirty_ram_span_last_failure(DirtyRamSpanFailure *out) {
if (out) *out = s_span_failure;
}
int dirty_ram_run_span(CPUState* cpu, uint32_t start_pc, uint32_t stop_pc,
uint64_t max_insns) {
const uint32_t lo = start_pc & 0x1FFFFFFFu, hi = stop_pc & 0x1FFFFFFFu;
DirtyRamSpanFailure f = {0u, start_pc, start_pc, stop_pc, 0u, 0u, 0u};
if (!cpu || s_span_hi || ((start_pc | stop_pc) & 3u) || lo >= hi ||
(start_pc ^ stop_pc) & 0xE0000000u) {
f.reason = 6u;
s_span_failure = f;
return 0;
}
const uint64_t first = g_dirty_ram_insns_run;
s_span_lo = lo;
s_span_hi = hi;
cpu->pc = start_pc;
for (;;) {
const uint32_t pc = cpu->pc;
f.pc = pc;
f.ra = cpu->gpr[31];
if (pc == stop_pc) break;
if (!pc) { f.reason = 1u; break; }
if (g_psx_call_bail) { f.reason = 3u; break; }
if (!span_forced(pc & 0x1FFFFFFFu)) {
/* The interpreter hands a call out of clean code back as a tail
* transfer (pc = callee, $ra = the return address). A call made
* by the span returns into it: run it as the caller would. A
* transfer out that is not a call ends the span. */
const uint32_t ra = cpu->gpr[31];
const uint32_t site = ra - 8u;
/* The span's last instruction may be a call: it returns to stop. */
const uint32_t w = span_forced(site & 0x1FFFFFFFu) &&
(span_forced(ra & 0x1FFFFFFFu) || ra == stop_pc)
? fetch_word(site & 0x1FFFFFFFu) : 0u;
const int is_call = (w >> 26) == 0x03u || /* jal */
((w >> 26) == 0u && (w & 0x3Fu) == 0x09u); /* jalr */
if (!is_call) { f.reason = 2u; break; }
cpu->pc = 0;
psx_dispatch_call(cpu, pc, ra);
if (g_psx_call_bail) { f.reason = 3u; break; }
if (cpu->pc != 0u && cpu->pc != ra) { f.after = cpu->pc; f.reason = 7u; break; }
cpu->pc = ra;
continue;
}
if (g_dirty_ram_insns_run - first > max_insns) { f.reason = 4u; break; }
if (!dirty_ram_dispatch(cpu, pc, stop_pc)) { f.reason = 5u; break; }
}
s_span_lo = s_span_hi = 0;
dirty_ram_ld_delay_flush(cpu);
f.insns = g_dirty_ram_insns_run - first;
if (f.reason) s_span_failure = f;
return f.reason == 0u;
}
/* ===== Cycle-budgeted precise event slicing (PRECISE_IRQ_SLICE.md) ===== */
/* Is a hardware interrupt deliverable to the guest at this exact instruction
@@ -2865,7 +2955,8 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
* They differ only for a PC in a retail RAM mirror. */
uint32_t phys = addr & 0x1FFFFFFFu;
const uint32_t ram_phys = psx_ram_map_read(phys);
int clean_game_text_miss = 0;
const int forced = span_forced(phys);
int clean_game_text_miss = forced;
if (addr == 0x80000048u) {
g_sentinel_reach_dirty++;
@@ -2893,7 +2984,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
/* Run the statically-compiled game function only while the target is still
* native-safe. Dirty overlay pages and pages whose text bytes diverged from
* the original EXE image fall through to interpret the live RAM bytes. */
if (psx_game_text_native_ok(addr)) {
if (!forced && psx_game_text_native_ok(addr)) {
g_mixed_depth++;
{
ls_func_enter(addr, cpu);
@@ -2906,7 +2997,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
if (_gc) return 1;
}
clean_game_text_miss = psx_game_address_in_text(addr) ? 1 : 0;
} else if (psx_game_address_in_text(addr)) {
} else if (!forced && psx_game_address_in_text(addr)) {
/* RAM at a game-text address diverged from the static EXE image
* (runtime-relocated / overlaid / self-modified code the compiled
* static function no longer reflects). The live RAM is the truth:
@@ -2924,7 +3015,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
* another segment misses above and is interpreted here like any clean
* text miss, never run through the other segment's body. Record it with
* the full PC; the fix is a segment-qualified seed and regeneration. */
if (clean_game_text_miss)
if (clean_game_text_miss && !forced)
(void)psx_segment_miss_note(addr, psx_game_is_function_entry,
cpu->gpr[31], cpu->gpr[29],
(uint32_t)s_frame_count);
@@ -2937,7 +3028,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
extern int psx_overlay_dispatch(CPUState *cpu, uint32_t addr);
int previous_phase = g_exec_phase;
g_exec_phase = 3;
int handled = psx_overlay_dispatch(cpu, addr);
int handled = !forced && psx_overlay_dispatch(cpu, addr);
g_exec_phase = previous_phase;
if (handled) return 1;
}
@@ -2954,7 +3045,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
extern void overlay_regs_snap(uint32_t out[34], const CPUState *cpu);
extern void overlay_fp_log(uint32_t addr, const uint32_t *in_regs,
const CPUState *cpu, int native);
int _ovfp = overlay_fp_enabled() &&
int _ovfp = !forced && overlay_fp_enabled() &&
overlay_cache_window_contains(phys) &&
overlay_loader_is_candidate(phys);
uint32_t _in_regs[34];
@@ -2969,7 +3060,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
{
extern int overlay_loader_dispatch(CPUState *cpu, uint32_t addr);
if (overlay_loader_dispatch(cpu, addr)) {
if (!forced && overlay_loader_dispatch(cpu, addr)) {
if (_ovfp) overlay_fp_log(addr, _in_regs, cpu, 1);
return 1;
}
@@ -3004,7 +3095,7 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
* counts dispatches the interpreter actually handles inside a capture
* window. The autocapture tick reads-and-resets this to decide whether
* an unseen region variant is being interp-executed right now. */
if (overlay_cache_window_contains(phys)) g_dirty_window_dispatches++;
if (!forced && overlay_cache_window_contains(phys)) g_dirty_window_dispatches++;
/* Reset soft-fail state at block entry. */
g_unsupported_seen = 0;
@@ -3022,13 +3113,14 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
{
extern uint32_t g_psx_mod_function_entry_hooks;
extern int psx_mod_function_entry(CPUState *cpu, uint32_t address);
if (g_psx_mod_function_entry_hooks && psx_mod_function_entry(cpu, addr)) return 1;
if (!forced && g_psx_mod_function_entry_hooks && psx_mod_function_entry(cpu, addr))
return 1;
}
/* Per-PC entry counter (visible via dirty_ram_stats). */
DirtyRamPcEntry *pc_entry = pc_table_get_or_insert(phys);
DirtyRamPcEntry *pc_entry = forced ? NULL : pc_table_get_or_insert(phys);
if (pc_entry) pc_entry->hits++;
{
if (!forced) {
uint32_t word = phys >> 2;
g_dirty_ram_dispatch_pc_bitmap[word >> 5] |= 1u << (word & 31u);
/* ...and the segment it entered through, from the full PC
+1
View File
@@ -46,6 +46,7 @@ void fntrace_set_game_range(uint32_t lo, uint32_t hi) {
}
int fntrace_is_game_started(void) { return s_game_started; }
void fntrace_restore_game_started(int started) { s_game_started = started != 0; }
/* Centralised game-start transition. Idempotent — safe to call from both
* the dispatcher (fntrace_record) and the generated entry-point function.
+16 -9
View File
@@ -445,7 +445,7 @@ void gpu_ws_set_native_scene_predicate(int (*predicate)(void)) {
int gpu_ws_present_native_43(void) {
if (!ws_engaged()) return 0;
if (ws_mode == 2 && s_ws_native_scene_predicate && s_ws_native_scene_predicate())
if (s_ws_native_scene_predicate && s_ws_native_scene_predicate())
return 1;
int game_mode = ws_game_mode();
if (!game_mode) ws_scene_latch.confirmed = 0;
@@ -5679,6 +5679,7 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) {
uint32_t addr = psx_mod_gpu_dma_resolve_address(start_addr);
uint32_t safety = 0;
uint16_t rank = 0xFFFFu;
uint16_t max_rank = 0xFFFFu;
const uint32_t max_nodes = 0x40000u;
int area_left = (int)draw_area_left, area_right = (int)draw_area_right;
GpuDisplayInfo di;
@@ -5727,6 +5728,13 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) {
/* CPU->VRAM data follows its 3-word header and is not a command
* stream. Such transfers are not UI draws; stop this node. */
if (op >= 0xA0u && op <= 0xBFu) break;
/* Select the frontmost populated drawing layer, including
* full-screen fades and non-rectangular world primitives.
* Choosing only among UI-shaped candidates promotes a deeper
* wall quad to HUD when the real front layer has no widgets. */
if (op >= 0x20u && op <= 0x7Fu && rank != 0xFFFFu &&
(max_rank == 0xFFFFu || rank > max_rank))
max_rank = rank;
uint32_t words[12] = {0};
for (int i = 0; i < count && i < 12; i++) {
words[i] = psx_read_word(
@@ -5752,14 +5760,13 @@ void gpu_ws_prepass_linked_list(uint32_t start_addr) {
return;
}
/* Empty ordering-table buckets can trail the actual frontmost layer.
* Selecting the last empty bucket made the memory-card glyph layer (rank
* 4095 followed by an empty rank 4096) disappear from the correction
* pass. Pick the highest rank that contains an eligible UI primitive. */
uint16_t max_rank = ws_ui_prepass[0].ot_rank;
for (uint32_t i = 1; i < ws_ui_prepass_count; i++) {
if (ws_ui_prepass[i].ot_rank > max_rank)
max_rank = ws_ui_prepass[i].ot_rank;
/* Empty trailing OT buckets do not pick the layer. A populated front
* layer without eligible widgets also must not fall back to a deeper
* UI-shaped world surface (Ape Escape's intro walls). */
if (max_rank == 0xFFFFu) {
ws_ui_reject.rank = ws_ui_prepass_count;
ws_ui_prepass_count = 0;
return;
}
ws_ui_prepass_rank = max_rank;
+59 -8
View File
@@ -1328,7 +1328,7 @@ static const char *TEX_FS =
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
"uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n"
"uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n"
"uniform int u_filter; /* 1 = bilinear */\n"
"uniform int u_filter; /* 0 nearest, 1 bilinear, 2 stable */\n"
"uniform float u_shift;\n"
"int vram_at(int x, int y){\n"
" ivec2 p = ivec2(x & 1023, y & 511);\n"
@@ -1364,17 +1364,46 @@ static const char *TEX_FS =
"vec3 col5(int raw){\n"
" return vec3(float(raw & 31), float((raw >> 5) & 31), float((raw >> 10) & 31)) / 31.0;\n"
"}\n"
"int stable_texel(ivec2 p){\n"
" /* Clamp before wrap, so a footprint left of u=0 cannot pick up u=255. */\n"
" if ((u_twin.x | u_twin.y) == 0) p=clamp(p,v_limits.xy,v_limits.zw);\n"
" return fetch_texel(p.x,p.y);\n"
"}\n"
"vec4 stable_bilinear(vec2 uv,int stp){\n"
" vec2 p=uv-vec2(0.5), f=fract(p); ivec2 b=ivec2(floor(p));\n"
" vec4 sum=vec4(0.0);\n"
" for(int y=0;y<2;++y) for(int x=0;x<2;++x){\n"
" int raw=stable_texel(b+ivec2(x,y));\n"
" float w=(x==0 ? 1.0-f.x : f.x)*(y==0 ? 1.0-f.y : f.y);\n"
" if(raw!=0 && ((raw>>15)&1)==stp) sum+=vec4(col5(raw),1.0)*w;\n"
" }\n"
" return sum;\n"
"}\n"
"void main(){\n"
" int stp; vec3 rgb;\n"
" /* v_persp is 0 for every prim unless [video] perspective_texturing is on\n"
" * AND this prim's packet carried full GTE projection provenance, so the\n"
" * default is the PS1's affine (noperspective) mapping. */\n"
" vec2 uv = (v_persp != 0) ? v_uv_p : v_uv;\n"
" vec2 dx=dFdx(uv), dy=dFdy(uv);\n"
" if (u_filter == 0) {\n"
" int raw = fetch_texel(int(floor(uv.x)), int(floor(uv.y)));\n"
" if (raw == 0) discard;\n"
" rgb = col5(raw);\n"
" stp = (raw >> 15) & 1;\n"
" } else if (u_filter == 2) {\n"
" uv+=vec2(u_shift); int raw=stable_texel(ivec2(floor(uv)));\n"
" if(raw==0) discard; stp=(raw>>15)&1;\n"
" float lx=length(dx), ly=length(dy);\n"
" dx*=min(1.0,8.0/max(lx,0.001)); dy*=min(1.0,8.0/max(ly,0.001));\n"
" int nx=clamp(int(ceil(lx)),1,4), ny=clamp(int(ceil(ly)),1,4);\n"
" vec4 sum=vec4(0.0);\n"
" for(int y=0;y<ny;++y) for(int x=0;x<nx;++x){\n"
" vec2 offset=dx*((float(x)+0.5)/float(nx)-0.5)\n"
" +dy*((float(y)+0.5)/float(ny)-0.5);\n"
" sum+=stable_bilinear(uv+offset,stp);\n"
" }\n"
" rgb=sum.a>0.00001 ? sum.rgb/sum.a : col5(raw);\n"
" } else {\n"
" /* Bilinear, Beetle-PSX formulation: the NEAREST texel is the base\n"
" * (cutout + STP authority), the neighbours lie toward the sub-texel\n"
@@ -3256,6 +3285,9 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
const float *col, uint16_t texpage,
uint16_t clut_x, uint16_t clut_y, int rawtex,
int semi, const int *lim) {
// Mode 2 needs proven world geometry. Sprites, HUD and untracked packets
// retain point sampling; their cutout pixels must stay sharp.
const int filter=s_tex_filter==2 && (lim || (!s_pc_valid && !s_pq_valid)) ? 0 : s_tex_filter;
int lim_buf[4];
int uv_buf[6];
if (!lim) {
@@ -3323,7 +3355,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
else if (isolate) reason = 0;
else if (batch_semi != s_tb_semi) reason = 1;
else if (s_mask_set != s_tb_mask) reason = 2;
else if (s_tex_filter != s_tb_filter) reason = 3;
else if (filter != s_tb_filter) reason = 3;
else if (gate != s_tb_gate) reason = 4;
else if ((!s_twin_batching || s_mask_check) &&
(twx != s_tb_twin[0] || twy != s_tb_twin[1] ||
@@ -3335,7 +3367,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
}
if (s_tb_n + 3 > TEXBATCH_MAXV) { s_batch_reason[6]++; flush_tex_batch(); }
if (s_tb_n == 0) { /* opening a batch: capture its keyed state */
s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = s_tex_filter; s_tb_gate = gate;
s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = filter; s_tb_gate = gate;
s_tb_bank_tex = s_selected_bank_tex;
s_tb_bank_live_clut = s_selected_bank_live_clut;
s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy;
@@ -3595,7 +3627,7 @@ static int glb_scale(void) { return s_out_scale; } /* real internal SSAA scal
native-wide CPU present path + gr_scale() callers
need the true scale — the FBO-direct present is
unaffected since it never reads gr_scale()) */
static void glb_set_texture_filter(int b) { s_tex_filter = b ? 1 : 0; sw_set_texture_filter(b); }
static void glb_set_texture_filter(int b) { s_tex_filter = b >= 0 && b <= 2 ? b : 0; sw_set_texture_filter(b != 0); }
static int glb_texture_filter(void) { return s_tex_filter; }
static void glb_set_semi_transparency(int e, int m) { s_semi_en = e; s_semi_mode = m & 3; sw_set_semi_transparency(e, m); }
@@ -5810,8 +5842,10 @@ typedef struct PassGen {
uint32_t n; /* images: [0] = the game's own */
uint32_t phase[PASS_SLOTS]; /* Q16, ascending */
uint32_t period; /* guest VBlanks the frame stays on screen */
int shown; /* built for a frame already on screen */
double t_start, t_len; /* host ticks, set on promotion */
} PassGen;
static int s_pass_flip_shown = 0;
static PassGen s_pgen[2];
/* Slot textures are made as slots fill: [0, s_pgen_alloc_n) exist, all at
* s_pgen_alloc_w x h. A generation never fills past pass_slot_cap, so two
@@ -6224,6 +6258,7 @@ int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
g->tex_w = tw; g->tex_h = th;
g->source_path = wide ? GL_PRES_WIDE : GL_PRES_VRAM;
g->period = period_vblanks ? period_vblanks : 1u;
g->shown = s_pass_flip_shown;
pass_capture_into(s_pgen_tex[gi][0], g); /* the game's own image */
g->phase[0] = 0;
g->n = 1;
@@ -6449,14 +6484,17 @@ static uint64_t s_present_ticks_accum_fwd(uint64_t add) {
return s_present_ticks_accum;
}
void gl_renderer_pass_set_flip_shown(int shown) { s_pass_flip_shown = shown ? 1 : 0; }
/* FLIP source saw a new frame: it is the flip a pending generation was built
* for (same rect, same presented geometry) or a frame without passes. */
* for (render_pass_gen_flip_matches) or a frame without passes. */
static void pass_note_new_frame(int origin_x, int origin_y, int source_path,
int pw, int ph) {
PassGen *pend = &s_pgen[1 - s_pgen_cur];
if (pend->valid && !pend->promoted && pend->x == origin_x &&
pend->y == origin_y && pend->source_path == source_path &&
pend->tex_w == pw && pend->tex_h == ph) {
if (pend->valid && !pend->promoted &&
render_pass_gen_flip_matches(pend->shown, pend->x, pend->y,
pend->source_path, pend->tex_w, pend->tex_h,
origin_x, origin_y, source_path, pw, ph)) {
s_pgen_promote = 1;
} else {
s_pgen_promote = 0;
@@ -6512,6 +6550,19 @@ static void pass_apply_promotion(void) {
g->promoted = 1;
g->t_start = s_interp_schedule.frame_start;
g->t_len = (double)g->period * sp;
/* Phase 0 is the image the game actually flipped to. The capture at
* the pass point is the same for a game that draws nothing more into
* the rect before its flip; a game that finishes the frame later (V8:2
* draws its HUD at the next submit, onto the rect about to be shown)
* would otherwise show its own frame without that last layer. The
* flip was matched to this generation's rect and presented geometry,
* and no guest code ran since it, so the rect holds that image. A
* FLIP_SHOWN generation's rect is not the one flipped to: it keeps
* the image taken when it opened. */
if (!g->shown) {
pass_capture_into(s_pgen_tex[s_pgen_cur][0], g);
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
}
s_pgen_promotions++;
if (s_pdump_left > 0) pass_dump_generation(s_pgen_cur);
}
+79
View File
@@ -2,6 +2,7 @@
#include "cpu_state.h"
#include "nd_intro_ot.h"
#include "pgxp.h"
#include "render_pass_projection.h"
#include <algorithm>
#include <cstdlib>
#include <cstdio>
@@ -12,6 +13,7 @@ extern "C" int gpu_ws_precise_nclip_enabled(void);
/* overlay_loader.c: nonzero while an overlay shadow diff (interpreter record
* plus native replay of one candidate) is running. */
extern "C" int psx_overlay_shadow_diff_active(void);
extern "C" { int (*g_psx_projection_command)(CPUState*, uint32_t) = nullptr; }
namespace PSXRecomp {
namespace GTE {
@@ -372,6 +374,76 @@ extern "C" int gte_geometry_correction_lookup(uint32_t packed,
return 1;
}
/* Render-pass checkpoint for the gte.cpp side of precision tracking: the
* position cache entries a pass writes are journaled and put back, and the
* speculative bracket depth is restored (a watchdog abort can leave one open).
* The PGXP shadows roll back through pgxp_checkpoint_*. */
struct GeomJournalEntry { GeomVertex *slot; GeomVertex old; };
static uint32_t s_gck_depth = 0;
static GeomJournalEntry *s_gck_log = nullptr;
static size_t s_gck_n = 0, s_gck_cap = 0;
static int s_gck_lossy = 0;
static uint32_t s_gck_generation = 0;
static GeomVertex *s_gck_cache = nullptr;
static uint32_t s_gck_spec_depth = 0;
static int s_gck_spec_invalidated = 0;
static uint8_t *s_gck_bits = nullptr; /* one bit per cache slot */
static inline void geom_ck_note(GeomVertex *slot) {
if (s_gck_depth == 0) return;
if (s_geom_cache != s_gck_cache || !s_gck_bits) { s_gck_lossy = 1; return; }
size_t i = (size_t)(slot - s_geom_cache);
uint8_t bit = (uint8_t)(1u << (i & 7u));
if (s_gck_bits[i >> 3] & bit) return; /* journaled this pass */
if (s_gck_n == s_gck_cap) {
size_t cap = s_gck_cap ? s_gck_cap * 2 : 16384;
GeomJournalEntry *p = (GeomJournalEntry *)std::realloc(
s_gck_log, cap * sizeof *p);
if (!p) { s_gck_lossy = 1; return; }
s_gck_log = p;
s_gck_cap = cap;
}
s_gck_bits[i >> 3] |= bit;
s_gck_log[s_gck_n].slot = slot;
s_gck_log[s_gck_n].old = *slot;
s_gck_n++;
}
extern "C" void gte_precision_checkpoint_begin(void) {
pgxp_checkpoint_begin();
if (s_gck_depth++ != 0) return;
if (s_geom_cache && !s_gck_bits)
s_gck_bits = (uint8_t *)std::calloc(GEOM_CACHE_SIZE / 8u, 1);
s_gck_n = 0;
s_gck_generation = s_geom_generation;
s_gck_cache = s_geom_cache;
s_gck_spec_depth = s_speculative_depth;
s_gck_spec_invalidated = s_speculative_timeline_invalidated;
}
extern "C" void gte_precision_checkpoint_rollback(void) {
if (s_gck_depth != 0 && --s_gck_depth == 0) {
for (size_t i = s_gck_n; i-- > 0;) {
GeomVertex *slot = s_gck_log[i].slot;
*slot = s_gck_log[i].old;
size_t k = (size_t)(slot - s_gck_cache);
s_gck_bits[k >> 3] &= (uint8_t)~(1u << (k & 7u));
}
s_gck_n = 0;
s_speculative_depth = s_gck_spec_depth;
s_speculative_timeline_invalidated = s_gck_spec_invalidated;
if (s_gck_lossy || s_geom_cache != s_gck_cache ||
s_geom_generation < s_gck_generation) {
s_gck_lossy = 0;
gte_geom_generation_advance(); /* fail closed */
} else {
s_geom_generation = s_gck_generation;
}
}
pgxp_checkpoint_rollback();
}
static inline void geom_note(uint32_t packed, int64_t x16, int64_t y16) {
if (s_speculative_depth != 0 || s_gte_replay_sandbox || !s_geom_enabled ||
!s_geom_cache) return;
@@ -384,6 +456,7 @@ static inline void geom_note(uint32_t packed, int64_t x16, int64_t y16) {
int64_t slot = geom_slot(packed);
if (slot < 0) return;
GeomVertex &entry = s_geom_cache[slot];
geom_ck_note(&entry);
if (entry.generation == s_geom_generation) {
/* Already occupied this generation: only flag ambiguity if the stored
* sub-pixel position actually DIFFERS — re-projecting the same vertex
@@ -1897,6 +1970,11 @@ static void gte_run_command(PSXRecomp::GTE::GTEState* gte, uint32_t cmd) {
extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) {
using namespace PSXRecomp::GTE;
uint32_t original_transform[8];
const bool observe_projection = g_psx_projection_command &&
((cmd & 63) == 1 || (cmd & 63) == 0x30);
if (observe_projection) std::memcpy(original_transform, cpu->gte_ctrl, sizeof original_transform);
const bool intermediate = observe_projection && g_psx_projection_command(cpu, cmd);
#ifndef PSX_NO_DEBUG_TOOLS
if (!s_gte_replay_sandbox) s_gte_exec_count++;
s_gte_caller_ra = cpu->gpr[31]; /* dome-locate probe: game fn that issued this projection */
@@ -1930,6 +2008,7 @@ extern "C" void gte_execute(CPUState* cpu, uint32_t cmd) {
#endif
gte_export_cpu_state(cpu, &gte);
if (intermediate) std::memcpy(cpu->gte_ctrl, original_transform, sizeof original_transform);
#ifndef PSX_NO_DEBUG_TOOLS
/* ND digit-rain: NdIntroSiblingFaceLoop (jal 0x80069CC4; $ra stays 69C3C..
+21 -6
View File
@@ -14,6 +14,7 @@
#include "psx_interpreter.h"
#include "cdrom.h"
#include "fntrace.h"
#include "render_pass_projection.h"
#include "text_xlate.h"
#include "boot_state.h"
#include "bios_hle.h"
@@ -1040,6 +1041,7 @@ extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok)
}
extern "C" void psx_frontend_on_savestate_loaded(void) {
psx_projection_reset_session();
mod_runtime_on_savestate_loaded();
#ifndef PSX_NO_DEBUG_TOOLS
debug_server_note_savestate_loaded();
@@ -1230,6 +1232,12 @@ static inline int cfg_fmv_filter_to_launcher(int cfg_value) {
return cfg_value + 1;
}
static int g_video_texfilter = 0; /* 0=nearest, 1=bilinear */
static int g_mod_texfilter = -1;
extern "C" void psx_mod_set_texture_filter(int mode) {
if(mode < 0 || mode > 2) return;
g_mod_texfilter = mode;
gr_set_texture_filter(mode);
}
/* Sub-pixel vertex precision + perspective-correct UVs (PGXP-style). Visual
* only: the PS1-visible GTE SXY FIFO stays integer, so guest-side culling and
* SXY readback are untouched. Default off = the faithful floor. */
@@ -1531,6 +1539,8 @@ extern "C" int psx_mod_set_adaptive_display_aspect(
static PSXModSessionBaseline g_mod_owned_baseline;
static void reset_mod_owned_presentation(void) {
g_mod_texfilter = -1;
gr_set_texture_filter(g_video_texfilter);
PSXModSessionScalars live;
live.video_vsync = g_video_vsync;
live.frame_interpolation = g_frame_interpolation;
@@ -1561,6 +1571,7 @@ static void reset_mod_owned_presentation(void) {
g_ws_adaptive_max_num = 16;
g_ws_adaptive_max_den = 9;
psx_mod_set_world_scene_predicate(nullptr);
gpu_ws_set_native_scene_predicate(nullptr);
psx_mod_set_retained_scene_predicate(nullptr);
psx_mod_set_adaptive_backdrop_preload(0);
(void)psx_mod_set_draw_distance_clamp(0);
@@ -1895,6 +1906,12 @@ static int g_ws_projection_num = 4;
static int g_ws_projection_den = 3;
static int g_ws_projection_mode = -1;
static void refresh_widescreen_projection() {
if (g_ws_engaged && !fntrace_is_game_started()) {
g_ws_engaged = false;
g_ws_projection_mode = -1;
gte_set_display_aspect(4, 3);
gpu_ws_configure(4, 3, g_ws_anchor_addr, 0, 0);
}
if (!g_ws_engaged) return;
const bool wide = g_video_aspect_num * 3 != g_video_aspect_den * 4;
@@ -4960,12 +4977,10 @@ static int pad_mode_boot_analog(int mode) {
return mode == PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
}
/* Keyboard has no DualShock sticks/config handshake — always present as a
* plain digital pad (SCPH-1080). Leaving keyboard slots in ANALOG/policy mode made
* P2–P5 show as connected while games that expect digital multitap pads never
* saw usable button input. */
/* Keyboard mappings can drive both DualShock sticks. Honor the configured
* mode, including analog-only title locks; multitap policy belongs to the
* SIO seat below and applies equally to keyboards and physical controllers. */
static int effective_player_mode(const PlayerInput& p) {
if (p.kind == 1) return (int)PSXRecompV4::PAD_MODE_DIGITAL;
return p.mode;
}
@@ -16013,7 +16028,7 @@ session_reboot:
(net_cfg.enabled && s_netplay_gl_present &&
gl_renderer_cpu_auth_dual());
g_video_scale = gr_scale(); /* reflect any clamp / alloc fallback */
gr_set_texture_filter(g_video_texfilter);
gr_set_texture_filter(g_mod_texfilter < 0 ? g_video_texfilter : g_mod_texfilter);
/* Sub-pixel vertex precision + perspective-correct UVs. Both default off;
* with both off every setter below leaves the tracking caches disabled and
* the draw path is the faithful integer one, unchanged. */
+79
View File
@@ -1806,9 +1806,88 @@ static void render_pass_mmio_write(uint32_t phys, uint32_t val,
/* The policy is render_pass_store_to (render_pass_plan.c, unit-tested by
* render_pass_sandbox_test); this only wires it to memory.c's arrays. */
/* Mod arenas (Expansion-1 mod memory, the GPU-DMA aperture) are guest
* memory a pass may draw into: a title that moves its primitive buffers there
* (an extended draw distance) has its passes build their packets there. Each
* arena page a pass writes is backed up on its first write and put back by
* render_pass_mod_arenas_rollback(), with the rest of the pass restore. */
#define RP_MOD_PAGE 4096u
#define RP_MOD_PAGES ((MOD_MEMORY_SIZE + PSX_MOD_GPU_DMA_APERTURE_SIZE) / RP_MOD_PAGE)
typedef struct { uint8_t *page; uint32_t index; uint8_t data[RP_MOD_PAGE]; } RpModPage;
static uint8_t s_rp_mod_bits[RP_MOD_PAGES / 8u];
static RpModPage *s_rp_mod_log = NULL;
static uint32_t s_rp_mod_n = 0, s_rp_mod_cap = 0;
static int s_rp_mod_lossy = 0;
uint64_t g_render_pass_mod_pages = 0; /* pages journaled, total */
static int rp_mod_journal(uint8_t *base, uint32_t page_base, uint32_t off,
uint32_t width) {
for (uint32_t pg = off / RP_MOD_PAGE; pg <= (off + width - 1u) / RP_MOD_PAGE; pg++) {
const uint32_t index = page_base + pg;
const uint8_t bit = (uint8_t)(1u << (index & 7u));
if (s_rp_mod_bits[index >> 3] & bit) continue;
if (s_rp_mod_n == s_rp_mod_cap) {
uint32_t cap = s_rp_mod_cap ? s_rp_mod_cap * 2u : 64u;
RpModPage *p = (RpModPage *)realloc(s_rp_mod_log, cap * sizeof *p);
if (!p) { s_rp_mod_lossy = 1; return 0; }
s_rp_mod_log = p;
s_rp_mod_cap = cap;
}
s_rp_mod_bits[index >> 3] |= bit;
s_rp_mod_log[s_rp_mod_n].page = base + pg * RP_MOD_PAGE;
s_rp_mod_log[s_rp_mod_n].index = index;
memcpy(s_rp_mod_log[s_rp_mod_n].data, base + pg * RP_MOD_PAGE, RP_MOD_PAGE);
s_rp_mod_n++;
g_render_pass_mod_pages++;
}
return 1;
}
/* 1 when the store landed in a mod arena (journaled and written). */
static int render_pass_mod_store(uint32_t addr, uint32_t val, uint32_t width) {
uint32_t phys, off;
uint8_t *base = NULL;
uint32_t page_base = 0;
if (addr >= 0xC0000000u) return 0;
phys = addr & 0x1FFFFFFFu;
if (phys < psx_ram_live_bytes()) return 0;
if (phys < 0x00800000u && psx_ram_live_bytes() < 0x00800000u) return 0; /* mirrors */
if (mod_gpu_dma_memory_offset(phys, width, &off)) {
base = mod_gpu_dma_memory;
page_base = MOD_MEMORY_SIZE / RP_MOD_PAGE;
} else if (mod_memory_offset(phys, width, &off)) {
base = mod_memory;
} else {
return 0;
}
if (!rp_mod_journal(base, page_base, off, width)) return 1; /* lossy: drop */
for (uint32_t i = 0; i < width; i++) base[off + i] = (uint8_t)(val >> (8u * i));
return 1;
}
void render_pass_mod_arenas_rollback(void) {
for (uint32_t i = s_rp_mod_n; i-- > 0;) {
memcpy(s_rp_mod_log[i].page, s_rp_mod_log[i].data, RP_MOD_PAGE);
s_rp_mod_bits[s_rp_mod_log[i].index >> 3] &=
(uint8_t)~(1u << (s_rp_mod_log[i].index & 7u));
}
s_rp_mod_n = 0;
s_rp_mod_lossy = 0;
}
/* FNV-1a over the used part of both arenas (PSX_RENDER_PASS_VERIFY). */
uint64_t render_pass_mod_arenas_hash(void) {
uint64_t h = 1469598103934665603ULL;
for (uint32_t i = 0; i < mod_memory_used; i++) h = (h ^ mod_memory[i]) * 1099511628211ULL;
for (uint32_t i = 0; i < mod_gpu_dma_memory_used; i++)
h = (h ^ mod_gpu_dma_memory[i]) * 1099511628211ULL;
return h;
}
static void render_pass_store(uint32_t addr, uint32_t val, uint32_t width) {
RenderPassStoreTarget t;
int cls;
if (render_pass_mod_store(addr, val, width)) return;
t.ram = ram;
t.ram_size = psx_ram_live_bytes(); /* live geometry, as psx_ram_map_write */
t.scratchpad = scratchpad;
+41 -1
View File
@@ -38,6 +38,8 @@ struct RegisteredPlugin {
/* Generated-function entry hooks, keyed by guest address. One id may
* observe several functions. */
std::vector<ModFunctionEntryHook> function_entries;
std::vector<ModFunctionEntryHook> guest_functions;
std::vector<ModInstructionHook> instructions;
};
std::map<std::string, RegisteredPlugin>& registered_plugins() {
@@ -1478,7 +1480,45 @@ bool mod_plugin_registered(const std::string& id) {
const auto found = registered_plugins().find(id);
return found != registered_plugins().end() &&
(found->second.activation || found->second.vblank ||
!found->second.function_entries.empty());
!found->second.function_entries.empty() || !found->second.guest_functions.empty() ||
!found->second.instructions.empty());
}
bool mod_register_guest_function_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback) {
const uint32_t key = address & 0x1FFFFFFFu;
if (!valid_id(id) || !callback || (address & 3u) ||
key < 0x0F000000u || key >= 0x10000000u || address >= 0xC0000000u)
return false;
for (const auto& plugin : registered_plugins())
for (const auto& function : plugin.second.guest_functions)
if (function.address == key) return false;
registered_plugins()[id].guest_functions.push_back({key, callback});
return true;
}
std::vector<ModFunctionEntryHook> mod_guest_functions(const std::string& id) {
const auto found = registered_plugins().find(id);
return found == registered_plugins().end()
? std::vector<ModFunctionEntryHook>{} : found->second.guest_functions;
}
bool mod_register_instruction_plugin(const std::string& id, uint32_t address,
uint32_t expected, PSXModFunctionEntryCallback callback) {
if (!valid_id(id) || !callback || (address & 3u) || address >= 0xC0000000u ||
(address & 0x1FFFFFFFu) >= 0x00800000u) return false;
auto& hooks = registered_plugins()[id].instructions;
const uint32_t key = address & 0x1FFFFFFFu;
for (const auto& hook : hooks)
if (hook.address == key) return false;
hooks.push_back({key, expected, callback});
return true;
}
std::vector<ModInstructionHook> mod_instruction_hooks(const std::string& id) {
const auto found = registered_plugins().find(id);
return found == registered_plugins().end()
? std::vector<ModInstructionHook>{} : found->second.instructions;
}
void mod_invoke_activation_plugin(const std::string& id) {
+172 -9
View File
@@ -7,7 +7,9 @@
#include "mod_plugins.h"
#include "gpu.h"
#include "psx_memory.h"
#include "render_pass_projection.h"
#include "psx_sha256.h"
#include "cpu_state.h"
#if defined(RECOMP_LAUNCHER)
#include "recomp_launcher.h"
@@ -54,6 +56,10 @@ 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;
uint32_t g_psx_mod_guest_functions = 0;
uint32_t g_psx_mod_instruction_hooks = 0;
/* Guest vblanks seen by the mod runtime: the plugin counters' time base. */
static uint64_t g_mod_vblanks = 0;
namespace PSXRecompV4 {
namespace {
@@ -78,6 +84,8 @@ struct RuntimeMods {
bool disc_enabled = false;
bool disc_guard_failed = false;
const ModResolution::Plugin* current_plugin = nullptr;
CPUState* current_function_cpu = nullptr;
bool current_function_finished = false;
};
RuntimeMods& state() {
@@ -85,6 +93,33 @@ RuntimeMods& state() {
return value;
}
/* Guest calls made from an entry hook can deliver VBlank callbacks before
* returning. Every callback owns its resource/completion context; restoring
* only entry hooks would let VBlank erase the interrupted plugin or complete
* a function that belongs to another callback. */
class PluginCallbackScope {
RuntimeMods& runtime;
const ModResolution::Plugin* previous_plugin;
CPUState* previous_cpu;
bool previous_finished;
public:
PluginCallbackScope(RuntimeMods& s, const ModResolution::Plugin* plugin,
CPUState* cpu = nullptr)
: runtime(s), previous_plugin(s.current_plugin),
previous_cpu(s.current_function_cpu), previous_finished(s.current_function_finished) {
s.current_plugin = plugin;
s.current_function_cpu = cpu;
s.current_function_finished = false;
}
~PluginCallbackScope() {
runtime.current_plugin = previous_plugin;
runtime.current_function_cpu = previous_cpu;
runtime.current_function_finished = previous_finished;
}
PluginCallbackScope(const PluginCallbackScope&) = delete;
PluginCallbackScope& operator=(const PluginCallbackScope&) = delete;
};
/* Function-entry hooks of the ACTIVE plan, flattened at plugin activation into
* one table sorted by code key (address with the segment bits stripped, so a
* KUSEG or KSEG1 PC reaches the same hook as KSEG0). The interpreter consults
@@ -105,6 +140,21 @@ std::vector<ActiveFunctionEntryHook>& active_function_entry_hooks() {
}
unsigned function_entry_depth;
std::vector<ActiveFunctionEntryHook>& active_guest_functions() {
static std::vector<ActiveFunctionEntryHook> value;
return value;
}
struct ActiveInstructionHook {
uint32_t key, expected;
PSXModFunctionEntryCallback callback;
const ModResolution::Plugin* plugin;
};
std::vector<ActiveInstructionHook>& active_instruction_hooks() {
static std::vector<ActiveInstructionHook> value;
return value;
}
inline uint32_t function_entry_key(uint32_t address) {
return address & 0x1FFFFFFFu;
}
@@ -112,6 +162,10 @@ inline uint32_t function_entry_key(uint32_t address) {
void clear_function_entry_hooks() {
active_function_entry_hooks().clear();
g_psx_mod_function_entry_hooks = 0;
active_guest_functions().clear();
g_psx_mod_guest_functions = 0;
active_instruction_hooks().clear();
g_psx_mod_instruction_hooks = 0;
}
void build_function_entry_hooks(const RuntimeMods& s) {
@@ -127,6 +181,20 @@ void build_function_entry_hooks(const RuntimeMods& s) {
[](const ActiveFunctionEntryHook& a,
const ActiveFunctionEntryHook& b) { return a.key < b.key; });
g_psx_mod_function_entry_hooks = (uint32_t)table.size();
auto& functions = active_guest_functions();
for (const auto& plugin : s.plan.plugins)
for (const auto& function : mod_guest_functions(plugin.id))
functions.push_back({function.address, function.callback, nullptr, &plugin});
std::sort(functions.begin(), functions.end(),
[](const auto& a, const auto& b) { return a.key < b.key; });
g_psx_mod_guest_functions = (uint32_t)functions.size();
auto& instructions = active_instruction_hooks();
for (const auto& plugin : s.plan.plugins)
for (const auto& hook : mod_instruction_hooks(plugin.id))
instructions.push_back({hook.address, hook.expected, hook.callback, &plugin});
std::stable_sort(instructions.begin(), instructions.end(),
[](const auto& a, const auto& b) { return a.key < b.key; });
g_psx_mod_instruction_hooks = (uint32_t)instructions.size();
}
const ModPackage* selected_package(const std::string& id) {
@@ -1414,24 +1482,25 @@ extern "C" int psx_mod_read_disc_file(const char* path, void* buffer,
extern "C" void mod_runtime_activate_plugins(void) {
using namespace PSXRecompV4;
RuntimeMods& s = state();
psx_projection_reset_session();
gpu_ws_set_native_scene_predicate(nullptr);
psx_ram_reset_size_request();
if (!s.initialized || !s.plan.ok) return;
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
s.current_plugin = &plugin;
PluginCallbackScope scope(s, &plugin);
mod_invoke_activation_plugin(plugin.id);
s.current_plugin = nullptr;
}
build_function_entry_hooks(s);
}
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) {
s.current_plugin = &plugin;
PluginCallbackScope scope(s, &plugin);
mod_invoke_vblank_plugin(plugin.id);
s.current_plugin = nullptr;
}
}
@@ -1523,6 +1592,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();
}
@@ -1586,6 +1702,55 @@ extern "C" int psx_mod_register_function_filter_plugin(
return mod_register_function_filter_plugin(id, address, callback) ? 1 : 0;
}
extern "C" int psx_mod_finish_function(CPUState* cpu) {
using namespace PSXRecompV4;
RuntimeMods& s = state();
if (!cpu || s.current_function_cpu != cpu || !s.current_plugin) return 0;
s.current_function_finished = true;
return 1;
}
extern "C" int psx_mod_register_guest_function_plugin(
const char* id, uint32_t address, PSXModFunctionEntryCallback callback) {
return id && PSXRecompV4::mod_register_guest_function_plugin(id, address, callback);
}
extern "C" int psx_mod_dispatch_guest_function(CPUState* cpu, uint32_t address) {
using namespace PSXRecompV4;
if (!g_psx_mod_guest_functions || !cpu || address >= 0xC0000000u) return 0;
const auto& functions = active_guest_functions();
const uint32_t key = function_entry_key(address);
auto it = std::lower_bound(functions.begin(), functions.end(), key,
[](const auto& function, uint32_t k) { return function.key < k; });
if (it == functions.end() || it->key != key) return 0;
PluginCallbackScope scope(state(), it->plugin, cpu);
it->callback(cpu, address);
cpu->pc = cpu->gpr[31];
return 1;
}
extern "C" int psx_mod_register_instruction_plugin(const char* id, uint32_t address,
uint32_t expected, PSXModFunctionEntryCallback callback) {
return id && PSXRecompV4::mod_register_instruction_plugin(id, address, expected, callback);
}
extern "C" void psx_mod_instruction(CPUState* cpu, uint32_t address, uint32_t instruction) {
using namespace PSXRecompV4;
if (!g_psx_mod_instruction_hooks || !cpu || address >= 0xC0000000u) return;
const auto& hooks = active_instruction_hooks();
const auto key = function_entry_key(address);
auto it = std::lower_bound(hooks.begin(), hooks.end(), key,
[](const auto& h, uint32_t k) { return h.key < k; });
for (; it != hooks.end() && it->key == key; ++it) {
if (it->expected != instruction || psx_mod_read_word(address) != instruction) continue;
PluginCallbackScope scope(state(), it->plugin, nullptr);
const uint32_t pc = cpu->pc;
it->callback(cpu, address);
if (cpu->pc != pc) std::abort();
cpu->gpr[0] = 0;
}
}
extern "C" int psx_mod_function_entry(CPUState* cpu, uint32_t address) {
using namespace PSXRecompV4;
if (!g_psx_mod_function_entry_hooks || !cpu) return 0;
@@ -1596,14 +1761,12 @@ extern "C" int psx_mod_function_entry(CPUState* cpu, uint32_t address) {
table.begin(), table.end(), key,
[](const ActiveFunctionEntryHook& hook, uint32_t k) { return hook.key < k; });
for (; it != table.end() && it->key == key; ++it) {
const ModResolution::Plugin* previous = s.current_plugin;
s.current_plugin = it->plugin;
PluginCallbackScope scope(s, it->plugin, cpu);
++function_entry_depth;
if (it->callback) it->callback(cpu, address);
const int handled = it->filter && it->filter(cpu, address);
const bool finished = s.current_function_finished || (it->filter && it->filter(cpu, address));
--function_entry_depth;
s.current_plugin = previous;
if (handled) {
if (finished) {
cpu->pc = cpu->gpr[31];
return 1;
}
+2
View File
@@ -2605,6 +2605,8 @@ static void init_callbacks(void) {
extern int psx_mod_function_entry(CPUState *cpu, uint32_t address);
s_callbacks.ws_screen_x_bound = psx_ws_screen_x_bound;
s_callbacks.mod_function_entry = psx_mod_function_entry;
extern void psx_mod_instruction(CPUState*, uint32_t, uint32_t);
s_callbacks.mod_instruction = psx_mod_instruction;
}
/* ABI v14: GTE precision-store tracker — the emitter emits a direct
* gte_precision_store_word() call for every swc2 (GTE store-word),
+117 -5
View File
@@ -97,9 +97,12 @@ static PGXPStats s_stats;
/* Lifecycle */
/* ------------------------------------------------------------------------- */
static void ck_wrapped(void);
extern "C" void pgxp_invalidate_all(void) {
if (s_suppress != 0) { s_deferred_invalidate = 1; return; }
if (++s_gen == 0) {
ck_wrapped();
/* generation wrapped: physically clear so stale slots can't revive */
if (s_ram) std::memset(s_ram, 0, PGXP_RAM_WORDS * sizeof(PGXPValue));
std::memset(s_scratch, 0, sizeof(s_scratch));
@@ -219,6 +222,115 @@ static inline PGXPValue *pgxp_ptr(uint32_t addr) {
return nullptr;
}
/* ------------------------------------------------------------------------- */
/* Checkpoint (render passes) */
/* ------------------------------------------------------------------------- */
/* A render pass (render_pass.c) runs guest draw code on a sandboxed machine
* and puts RAM, scratchpad and registers back with raw copies afterwards. The
* shadows must roll back with them: otherwise they describe the words the
* pass wrote (interpolated vertices), and the live frame's packets - consumed
* after the passes - fail validation. Copying the shadow arrays whole per
* pass would cost tens of MB; instead every RAM / scratchpad slot a pass
* mutates is journaled on its first write, and register shadows (small) are
* copied whole. A journal that cannot grow, or a generation wrap during the
* pass, fails closed: rollback invalidates everything. */
struct PGXPJournalEntry {
PGXPValue *slot;
PGXPValue old;
};
static uint32_t s_ck_depth = 0;
static uint8_t *s_ck_bits = nullptr; /* one bit per shadow slot */
static PGXPValue *s_ck_ram = nullptr; /* s_ram the bits index */
static PGXPJournalEntry *s_ck_log = nullptr;
static size_t s_ck_n = 0, s_ck_cap = 0;
static int s_ck_lossy = 0;
static PGXPValue s_ck_gpr[34], s_ck_gte[32];
static uint32_t s_ck_gen = 0, s_ck_suppress = 0;
static int s_ck_deferred = 0;
static inline size_t ck_index(const PGXPValue *pv) {
if (pv >= s_scratch && pv < s_scratch + PGXP_SCRATCH_WORDS)
return (size_t)PGXP_RAM_WORDS + (size_t)(pv - s_scratch);
return (size_t)(pv - s_ram);
}
/* Journal a RAM / scratchpad slot before its first mutation in a pass. */
static inline void ck_note(PGXPValue *pv) {
if (s_ck_depth == 0 || !pv) return;
if (s_ram != s_ck_ram && !(pv >= s_scratch &&
pv < s_scratch + PGXP_SCRATCH_WORDS)) {
s_ck_lossy = 1; /* shadow RAM appeared mid-pass */
return;
}
size_t i = ck_index(pv);
uint8_t bit = (uint8_t)(1u << (i & 7u));
if (s_ck_bits[i >> 3] & bit) return;
if (s_ck_n == s_ck_cap) {
size_t cap = s_ck_cap ? s_ck_cap * 2 : 65536;
PGXPJournalEntry *p = (PGXPJournalEntry *)std::realloc(
s_ck_log, cap * sizeof *p);
if (!p) { s_ck_lossy = 1; return; }
s_ck_log = p;
s_ck_cap = cap;
}
s_ck_bits[i >> 3] |= bit;
s_ck_log[s_ck_n].slot = pv;
s_ck_log[s_ck_n].old = *pv;
s_ck_n++;
}
static void ck_wrapped(void) {
if (s_ck_depth != 0) s_ck_lossy = 1; /* shadows were cleared */
}
static inline PGXPValue *pgxp_ptr_w(uint32_t addr) {
PGXPValue *pv = pgxp_ptr(addr);
ck_note(pv);
return pv;
}
extern "C" void pgxp_checkpoint_begin(void) {
if (s_ck_depth++ != 0) return; /* the outermost pass journals */
if (!s_ck_bits) {
s_ck_bits = (uint8_t *)std::calloc(
((size_t)PGXP_RAM_WORDS + PGXP_SCRATCH_WORDS + 7u) / 8u, 1);
if (!s_ck_bits) s_ck_lossy = 1;
}
s_ck_ram = s_ram;
s_ck_n = 0;
std::memcpy(s_ck_gpr, s_gpr, sizeof s_gpr);
std::memcpy(s_ck_gte, s_gte, sizeof s_gte);
s_ck_gen = s_gen;
s_ck_suppress = s_suppress;
s_ck_deferred = s_deferred_invalidate;
}
extern "C" void pgxp_checkpoint_rollback(void) {
if (s_ck_depth == 0) return;
if (--s_ck_depth != 0) return;
for (size_t i = s_ck_n; i-- > 0;) {
PGXPJournalEntry *e = &s_ck_log[i];
*e->slot = e->old;
size_t k = ck_index(e->slot);
s_ck_bits[k >> 3] &= (uint8_t)~(1u << (k & 7u));
}
s_ck_n = 0;
std::memcpy(s_gpr, s_ck_gpr, sizeof s_gpr);
std::memcpy(s_gte, s_ck_gte, sizeof s_gte);
s_gen = s_ck_gen;
/* A watchdog abort can leave a suppress bracket open: the machine it
* interrupted is gone, so its bracket is too. */
s_suppress = s_ck_suppress;
s_deferred_invalidate = s_ck_deferred;
recompute_active();
if (s_ck_lossy || s_ram != s_ck_ram) {
s_ck_lossy = 0;
pgxp_invalidate_all();
}
}
static inline int pv_live(const PGXPValue *pv) {
return pv && pv->gen == s_gen && (pv->flags & PGXP_F_VXY) != 0;
}
@@ -272,7 +384,7 @@ extern "C" void psx_pgxp_load(struct CPUState *cpu, uint32_t instr,
switch (f_op(instr)) {
case 0x23: { /* LW */
PGXPValue *src = pgxp_ptr(addr);
PGXPValue *src = pgxp_ptr_w(addr); /* validation may mutate it */
if (src && src->gen == s_gen) {
pv_validate(src, value);
*dst = *src;
@@ -312,7 +424,7 @@ extern "C" void psx_pgxp_store(struct CPUState *cpu, uint32_t instr,
uint32_t addr, uint32_t value) {
(void)cpu;
if (!g_pgxp_active) return;
PGXPValue *dst = pgxp_ptr(addr);
PGXPValue *dst = pgxp_ptr_w(addr);
if (!dst) return;
uint32_t rt = f_rt(instr);
PGXPValue *src = (rt != 0) ? &s_gpr[rt] : nullptr;
@@ -371,7 +483,7 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr,
switch (f_op(instr)) {
case 0x32: { /* LWC2: gte[rt] <- [addr] */
PGXPValue *src = pgxp_ptr(addr);
PGXPValue *src = pgxp_ptr_w(addr); /* validation may mutate it */
PGXPValue *dst = &s_gte[f_rt(instr)];
if (src && src->gen == s_gen) {
pv_validate(src, value);
@@ -382,7 +494,7 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr,
return;
}
case 0x3A: { /* SWC2: [addr] <- gte[rt] */
PGXPValue *dst = pgxp_ptr(addr);
PGXPValue *dst = pgxp_ptr_w(addr);
if (!dst) return;
PGXPValue *src = &s_gte[f_rt(instr)];
if (src->gen == s_gen) {
@@ -936,7 +1048,7 @@ extern "C" int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
* the double write is idempotent (same source shadow, same destination). */
extern "C" void pgxp_store_gte_reg(uint32_t addr, uint8_t reg) {
if (!g_pgxp_active) return;
PGXPValue *dst = pgxp_ptr(addr);
PGXPValue *dst = pgxp_ptr_w(addr);
if (!dst) return;
const PGXPValue *src = &s_gte[reg & 31u];
if (src->gen != s_gen) return;
+47 -1
View File
@@ -40,6 +40,13 @@ extern uint8_t *memory_get_scratchpad_ptr(void);
extern uint32_t memory_get_ram_bytes(void);
extern uint32_t i_stat;
extern uint32_t i_mask;
/* gte.cpp: precision-tracking shadows (PGXP + the position cache) roll back
* with the machine, or they describe the words a pass wrote. */
extern void gte_precision_checkpoint_begin(void);
extern void gte_precision_checkpoint_rollback(void);
/* memory.c: mod arena pages a pass wrote, and their hash for verify mode. */
extern void render_pass_mod_arenas_rollback(void);
extern uint64_t render_pass_mod_arenas_hash(void);
extern uint32_t dma_snapshot_bytes(void);
extern void dma_snapshot_write(uint8_t *p);
extern int dma_snapshot_read(const uint8_t *p, uint32_t len);
@@ -108,6 +115,7 @@ typedef struct RenderPassNesting {
int ov_active_depth;
uint32_t ov_inprogress;
void (*ov_flush)(void);
uint32_t span_lo, span_hi; /* open guest span (dirty_ram_run_span) */
} RenderPassNesting;
static void nesting_save(RenderPassNesting *n) {
@@ -125,6 +133,7 @@ static void nesting_save(RenderPassNesting *n) {
dirty_ram_ld_delay_save(&n->ld_delay);
overlay_loader_native_nesting(&n->ov_active_depth, &n->ov_inprogress);
n->ov_flush = g_overlay_flush_pending_cycles;
dirty_ram_span_get(&n->span_lo, &n->span_hi);
}
static void nesting_restore(const RenderPassNesting *n) {
@@ -142,6 +151,7 @@ static void nesting_restore(const RenderPassNesting *n) {
dirty_ram_ld_delay_restore(&n->ld_delay);
overlay_loader_set_native_nesting(n->ov_active_depth, n->ov_inprogress);
g_overlay_flush_pending_cycles = n->ov_flush;
dirty_ram_span_set(n->span_lo, n->span_hi);
}
/* PSX_RENDER_PASS_VERIFY: a pass that returned normally must leave the
@@ -158,7 +168,8 @@ static int nesting_balanced(const RenderPassNesting *n) {
now.exec_phase == n->exec_phase &&
now.precise_mode == n->precise_mode &&
now.ov_active_depth == n->ov_active_depth &&
now.ov_flush == n->ov_flush;
now.ov_flush == n->ov_flush &&
now.span_lo == n->span_lo && now.span_hi == n->span_hi;
}
/* After a watchdog abort, before the restore: which exits the longjmp
@@ -193,6 +204,7 @@ static int nesting_describe(const RenderPassNesting *n, int bb_defer_ck,
RP_NOTE(now.exec_phase != n->exec_phase, "exec phase %d->%d",
now.exec_phase, n->exec_phase);
RP_NOTE(now.ld_delay.armed != n->ld_delay.armed, "pending load");
RP_NOTE(now.span_hi != n->span_hi, "guest span");
#undef RP_NOTE
return count;
}
@@ -220,6 +232,7 @@ static RenderPassCheckpoint s_ck;
static PsxCycleFreeze s_freeze;
static jmp_buf s_abort_jmp;
static volatile int s_abort_armed;
static const CPUState* s_pass_cpu;
static int s_nesting;
static RenderPassStats s_stats;
@@ -270,6 +283,15 @@ void render_pass_reset_session(void) {
memset(g_render_pass_dropped_writes, 0, sizeof g_render_pass_dropped_writes);
s_open_generation = 0;
s_restored_valid = 0;
gl_renderer_pass_set_flip_shown(0);
}
int psx_mod_set_render_pass_flip(uint32_t mode) {
if (mode != PSX_MOD_RENDER_PASS_FLIP_PENDING &&
mode != PSX_MOD_RENDER_PASS_FLIP_SHOWN)
return 0;
gl_renderer_pass_set_flip_shown(mode == PSX_MOD_RENDER_PASS_FLIP_SHOWN);
return 1;
}
/* Everything that must hold before guest code may run frozen, as a
@@ -298,6 +320,16 @@ uint32_t psx_mod_render_pass_status(void) {
return pass_status();
}
int psx_mod_run_guest_span(CPUState *cpu, uint32_t start_pc, uint32_t stop_pc) {
if (!g_psx_render_pass_active || !cpu) return 0;
if (dirty_ram_run_span(cpu, start_pc, stop_pc, 1000000u)) {
s_stats.spans++;
return 1;
}
s_stats.span_failures++;
return 0;
}
uint32_t psx_mod_render_pass_plan(uint32_t period_vblanks,
uint32_t shown_after_vblanks,
uint32_t *alpha_q16, uint32_t max) {
@@ -354,6 +386,10 @@ static uint64_t state_hash(const CPUState *cpu) {
h = fnv(h, cpu->gte_ctrl, sizeof cpu->gte_ctrl);
h = fnv(h, memory_get_ram_ptr(), memory_get_ram_bytes());
h = fnv(h, memory_get_scratchpad_ptr(), RP_SPAD_SIZE);
{
const uint64_t mh = render_pass_mod_arenas_hash();
h = fnv(h, &mh, sizeof mh);
}
h = fnv(h, g_psx_icache_tv, sizeof g_psx_icache_tv);
h = fnv(h, &i_stat, sizeof i_stat);
h = fnv(h, &i_mask, sizeof i_mask);
@@ -426,6 +462,7 @@ static int checkpoint_save(const CPUState *cpu) {
s_ck.call_bail = g_psx_call_bail;
memset(&s_ck.nest, 0, sizeof s_ck.nest);
nesting_save(&s_ck.nest);
gte_precision_checkpoint_begin();
return 1;
}
@@ -449,6 +486,8 @@ static void checkpoint_restore(CPUState *cpu) {
g_psx_dispatch_depth = s_ck.dispatch_depth;
g_psx_call_bail = s_ck.call_bail;
nesting_restore(&s_ck.nest);
render_pass_mod_arenas_rollback();
gte_precision_checkpoint_rollback();
}
static double s_ms_per_tick = 0.0;
@@ -460,6 +499,11 @@ static double ema_ms(double cur, uint64_t ticks) {
static void watchdog_overrun(void) {
s_stats.watchdog++;
s_stats.watchdog_flag = 1;
if (s_stats.watchdog <= 4 && s_pass_cpu)
fprintf(stderr, "psxrecomp: render pass watchdog pc=0x%08X "
"function=0x%08X ra=0x%08X last_store=0x%08X\n",
s_pass_cpu->pc, g_debug_current_func_addr,
s_pass_cpu->gpr[31], g_debug_last_store_pc);
if (s_abort_armed) {
s_abort_armed = 0;
longjmp(s_abort_jmp, 1);
@@ -531,6 +575,7 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
(void)psx_cycle_freeze_begin(&s_freeze, watchdog_cycles(),
watchdog_overrun);
if (setjmp(s_abort_jmp) == 0) {
s_pass_cpu = cpu;
s_abort_armed = 1;
ok = fn(cpu, user, pass->alpha_q16) ? 1 : 0;
s_abort_armed = 0;
@@ -554,6 +599,7 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
}
s_stats.guest_cycles_last = psx_cycle_count - cycles_before;
tg = gl_renderer_perf_ticks();
s_pass_cpu = NULL;
leaks = gl_renderer_pass_leaks() - s_leaks_before;
if (leaks) {
+58
View File
@@ -0,0 +1,58 @@
/* Per-frame driver and activation for render-pass frame-rate plugins: the
* parts every title repeats around psx_mod_render_pass_plan / _pass. */
#include "mod_plugins.h"
#include <stdlib.h>
#include <string.h>
#define FRAME_MAX_PHASES 16u
static int s_crossfade; /* passes lastingly unavailable: presenter crossfades */
int psx_mod_activate_render_pass_rate(const char* package, const char* feature,
const char* option, uint32_t flip_mode) {
char value[16];
unsigned long fps = 0; /* 0 follows the measured display refresh */
if (package && feature && option &&
psx_mod_option_value(package, feature, option, value, sizeof value) &&
strcmp(value, "display") != 0) {
char* end = value;
const unsigned long parsed = strtoul(value, &end, 10);
if (end != value && *end == '\0') fps = parsed;
}
s_crossfade = 0;
if (!psx_mod_set_frame_interpolation_source(PSX_MOD_FRAME_SOURCE_FLIP) ||
!psx_mod_set_render_pass_flip(flip_mode) ||
!psx_mod_set_frame_interpolation_blend(PSX_MOD_FRAME_INTERPOLATION_HOLD))
return 0;
return psx_mod_set_frame_interpolation((uint32_t)fps);
}
uint32_t psx_mod_render_pass_frame(struct CPUState* cpu,
const PSXModRenderPassFrame* frame,
PSXModRenderPassFn fn, void* user) {
uint32_t phases[FRAME_MAX_PHASES], n, kept = 0;
const uint32_t status = psx_mod_render_pass_status();
const int lasting = status == PSX_MOD_RENDER_PASS_BACKEND ||
status == PSX_MOD_RENDER_PASS_DISABLED;
PSXModRenderPass pass;
if (lasting != s_crossfade) {
s_crossfade = lasting;
psx_mod_set_frame_interpolation_blend(
lasting ? PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE
: PSX_MOD_FRAME_INTERPOLATION_HOLD);
}
if (!cpu || !frame || !fn || frame->struct_size < sizeof *frame ||
frame->period_vblanks == 0 || frame->w == 0 || frame->h == 0)
return 0;
n = psx_mod_render_pass_plan(frame->period_vblanks, frame->shown_after_vblanks,
phases, FRAME_MAX_PHASES);
memset(&pass, 0, sizeof pass);
pass.struct_size = sizeof pass;
pass.x = frame->x; pass.y = frame->y; pass.w = frame->w; pass.h = frame->h;
for (uint32_t i = 0; i < n; i++) {
pass.alpha_q16 = phases[i];
if (psx_mod_render_pass(cpu, &pass, fn, user)) kept++;
}
return kept;
}
+456
View File
@@ -0,0 +1,456 @@
/* Motion kit for render-pass plugins: capture, pair and blend the game state
* a pass redraws from. See include/render_pass_motion.h. */
#include "render_pass_motion.h"
#include "mod_plugins.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
#define MOTION_MAX_TICKS 8u
#define MOTION_PI 3.14159265358979323846
typedef struct MotionEntry {
uint32_t addr, identity, seq;
uint8_t kind;
int32_t v[12]; /* MATRIX: r[0..8], t at [9..11]; others from v[0] */
} MotionEntry;
typedef struct MotionPair { uint32_t prev, cur; } MotionPair;
struct PSXMotionSet {
uint32_t cap;
MotionEntry* cap_buf[2];
uint32_t n[2];
uint32_t tick[2];
int began[2]; /* a capture was made into this buffer */
int sorted[2];
int cur; /* buffer the current capture writes */
int history; /* the other buffer holds the capture just before */
MotionPair* pairs;
uint32_t npairs;
uint32_t ticks;
};
uint32_t psx_motion_identity(uint32_t a, uint32_t b, uint32_t c) {
uint32_t h = 2166136261u;
const uint32_t w[3] = {a, b, c};
for (int i = 0; i < 3; i++)
for (int k = 0; k < 4; k++) h = (h ^ ((w[i] >> (8 * k)) & 0xFFu)) * 16777619u;
return h;
}
PSXMotionSet* psx_motion_set_create(uint32_t capacity) {
PSXMotionSet* s;
if (capacity == 0) return NULL;
s = (PSXMotionSet*)calloc(1, sizeof *s);
if (!s) return NULL;
s->cap = capacity;
s->cap_buf[0] = (MotionEntry*)calloc(capacity, sizeof(MotionEntry));
s->cap_buf[1] = (MotionEntry*)calloc(capacity, sizeof(MotionEntry));
s->pairs = (MotionPair*)calloc(capacity, sizeof(MotionPair));
if (!s->cap_buf[0] || !s->cap_buf[1] || !s->pairs) {
psx_motion_set_destroy(s);
return NULL;
}
return s;
}
void psx_motion_set_destroy(PSXMotionSet* s) {
if (!s) return;
free(s->cap_buf[0]);
free(s->cap_buf[1]);
free(s->pairs);
free(s);
}
void psx_motion_invalidate(PSXMotionSet* s) {
if (!s) return;
s->history = 0;
s->began[0] = s->began[1] = 0;
s->n[0] = s->n[1] = 0;
s->npairs = 0;
s->ticks = 0;
}
void psx_motion_begin(PSXMotionSet* s, uint32_t tick) {
if (!s) return;
s->history = s->began[s->cur];
s->cur ^= 1;
s->n[s->cur] = 0;
s->tick[s->cur] = tick;
s->began[s->cur] = 1;
s->sorted[s->cur] = 0;
s->npairs = 0;
s->ticks = 0;
}
/* ---- guest RAM --------------------------------------------------------- */
void psx_motion_read_matrix(uint32_t addr, PSXMotionMatrix* m) {
for (int i = 0; i < 9; i++) m->r[i] = (int16_t)psx_mod_read_half(addr + 2u * (uint32_t)i);
for (int i = 0; i < 3; i++) m->t[i] = (int32_t)psx_mod_read_word(addr + 0x14u + 4u * (uint32_t)i);
}
void psx_motion_write_matrix(uint32_t addr, const PSXMotionMatrix* m, int rotation) {
if (rotation)
for (int i = 0; i < 9; i++) psx_mod_write_half(addr + 2u * (uint32_t)i, (uint16_t)m->r[i]);
for (int i = 0; i < 3; i++) psx_mod_write_word(addr + 0x14u + 4u * (uint32_t)i, (uint32_t)m->t[i]);
}
static void read_entry(MotionEntry* e) {
switch (e->kind) {
case PSX_MOTION_MATRIX:
case PSX_MOTION_MATRIX_TRANSLATION: {
PSXMotionMatrix m;
psx_motion_read_matrix(e->addr, &m);
for (int i = 0; i < 9; i++) e->v[i] = m.r[i];
for (int i = 0; i < 3; i++) e->v[9 + i] = m.t[i];
break;
}
case PSX_MOTION_ROTATION:
for (int i = 0; i < 9; i++) e->v[i] = (int16_t)psx_mod_read_half(e->addr + 2u * (uint32_t)i);
break;
case PSX_MOTION_VECTOR:
for (int i = 0; i < 3; i++) e->v[i] = (int32_t)psx_mod_read_word(e->addr + 4u * (uint32_t)i);
break;
case PSX_MOTION_SVECTOR:
case PSX_MOTION_ANGLES:
for (int i = 0; i < 3; i++) e->v[i] = (int16_t)psx_mod_read_half(e->addr + 2u * (uint32_t)i);
break;
case PSX_MOTION_SCALAR:
e->v[0] = (int32_t)psx_mod_read_word(e->addr);
break;
}
}
int psx_motion_track(PSXMotionSet* s, uint32_t kind, uint32_t addr, uint32_t identity) {
MotionEntry* e;
if (!s || kind < PSX_MOTION_MATRIX || kind > PSX_MOTION_ROTATION) return 0;
if (!s->began[s->cur] || s->n[s->cur] >= s->cap) return 0;
e = &s->cap_buf[s->cur][s->n[s->cur]];
memset(e, 0, sizeof *e);
e->addr = addr;
e->kind = (uint8_t)kind;
e->identity = identity;
e->seq = s->n[s->cur]++;
s->sorted[s->cur] = 0;
read_entry(e);
return 1;
}
/* ---- math ---------------------------------------------------------------- */
static int orthonormal(const int16_t* r) {
for (int i = 0; i < 3; i++) {
double len = 0;
for (int j = 0; j < 3; j++) len += (double)r[3 * i + j] * r[3 * i + j];
if (fabs(sqrt(len) - 4096.0) > 4096.0 * 0.04) return 0;
for (int k = i + 1; k < 3; k++) {
double dot = 0;
for (int j = 0; j < 3; j++) dot += (double)r[3 * i + j] * r[3 * k + j];
if (fabs(dot) > 4096.0 * 4096.0 * 0.04) return 0;
}
}
return 1;
}
static void to_quat(const int16_t* r, double q[4]) {
double a[9], n;
for (int i = 0; i < 9; i++) a[i] = r[i] / 4096.0;
const double tr = a[0] + a[4] + a[8];
if (tr > 0) {
double s = sqrt(tr + 1.0) * 2;
q[0] = 0.25 * s; q[1] = (a[7] - a[5]) / s; q[2] = (a[2] - a[6]) / s; q[3] = (a[3] - a[1]) / s;
} else if (a[0] > a[4] && a[0] > a[8]) {
double s = sqrt(1.0 + a[0] - a[4] - a[8]) * 2;
q[0] = (a[7] - a[5]) / s; q[1] = 0.25 * s; q[2] = (a[1] + a[3]) / s; q[3] = (a[2] + a[6]) / s;
} else if (a[4] > a[8]) {
double s = sqrt(1.0 + a[4] - a[0] - a[8]) * 2;
q[0] = (a[2] - a[6]) / s; q[1] = (a[1] + a[3]) / s; q[2] = 0.25 * s; q[3] = (a[5] + a[7]) / s;
} else {
double s = sqrt(1.0 + a[8] - a[0] - a[4]) * 2;
q[0] = (a[3] - a[1]) / s; q[1] = (a[2] + a[6]) / s; q[2] = (a[5] + a[7]) / s; q[3] = 0.25 * s;
}
n = sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]);
for (int i = 0; i < 4; i++) q[i] /= n;
}
static int16_t fixed12(double v) {
double x = v * 4096.0;
if (x > 32767.0) x = 32767.0;
if (x < -32768.0) x = -32768.0;
return (int16_t)lround(x);
}
static double turn_limit(double turn) { return turn > 0 ? turn : MOTION_PI; }
/* Angle between two orientations, or -1 when either is not a rotation. */
static double rotation_angle(const int16_t* a, const int16_t* b) {
double p[4], q[4], d;
if (!orthonormal(a) || !orthonormal(b)) return -1;
to_quat(a, p);
to_quat(b, q);
d = fabs(p[0] * q[0] + p[1] * q[1] + p[2] * q[2] + p[3] * q[3]);
if (d > 1.0) d = 1.0;
return 2.0 * acos(d);
}
static int blend_rotation(const int16_t* a, const int16_t* b, double t, double turn, int16_t* out) {
if (orthonormal(a) && orthonormal(b)) {
double p[4], q[4], d, angle, wa, wb, r[4];
to_quat(a, p);
to_quat(b, q);
d = p[0] * q[0] + p[1] * q[1] + p[2] * q[2] + p[3] * q[3];
if (d < 0) { for (int i = 0; i < 4; i++) q[i] = -q[i]; d = -d; }
if (d > 1.0) d = 1.0;
angle = acos(d);
if (2.0 * angle > turn_limit(turn)) { memcpy(out, b, 9 * sizeof *out); return 0; }
wa = 1 - t; wb = t;
if (angle > 1e-6) { wa = sin((1 - t) * angle) / sin(angle); wb = sin(t * angle) / sin(angle); }
for (int i = 0; i < 4; i++) r[i] = wa * p[i] + wb * q[i];
{
const double w = r[0], x = r[1], y = r[2], z = r[3];
const double m[9] = {1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w),
2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w),
2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)};
for (int i = 0; i < 9; i++) out[i] = fixed12(m[i]);
}
return 1;
}
/* Scaled or sheared: elementwise, refusing jumps no motion produces. */
for (int i = 0; i < 9; i++)
if (abs((int)a[i] - (int)b[i]) > 4096) { memcpy(out, b, 9 * sizeof *out); return 0; }
for (int i = 0; i < 9; i++) out[i] = (int16_t)lround(a[i] + (b[i] - a[i]) * t);
return 1;
}
int psx_motion_blend_matrix(const PSXMotionMatrix* a, const PSXMotionMatrix* b,
double t, double turn, PSXMotionMatrix* out) {
const int ok = blend_rotation(a->r, b->r, t, turn, out->r);
for (int i = 0; i < 3; i++)
out->t[i] = ok ? (int32_t)llround(a->t[i] + ((double)b->t[i] - a->t[i]) * t) : b->t[i];
return ok;
}
static double dist3(const int32_t* a, const int32_t* b) {
double s = 0;
for (int i = 0; i < 3; i++) { const double d = (double)b[i] - a[i]; s += d * d; }
return sqrt(s);
}
static int32_t arc12(int32_t from, int32_t to) { /* shortest signed delta */
int32_t d = (to - from) & 4095;
return d > 2048 ? d - 4096 : d; /* an exact half turn goes forward */
}
/* ---- pairing ------------------------------------------------------------- */
static int entry_cmp(const void* x, const void* y) {
const MotionEntry* a = (const MotionEntry*)x;
const MotionEntry* b = (const MotionEntry*)y;
if (a->addr != b->addr) return a->addr < b->addr ? -1 : 1;
if (a->kind != b->kind) return a->kind < b->kind ? -1 : 1;
return a->seq < b->seq ? -1 : a->seq > b->seq;
}
/* Sort by (addr, kind); of repeats the last tracked wins. */
static void sort_capture(PSXMotionSet* s, int b) {
MotionEntry* e = s->cap_buf[b];
uint32_t n = s->n[b], w = 0;
if (s->sorted[b]) return;
qsort(e, n, sizeof *e, entry_cmp);
for (uint32_t i = 0; i < n; i++) {
if (w && e[w - 1].addr == e[i].addr && e[w - 1].kind == e[i].kind) e[w - 1] = e[i];
else e[w++] = e[i];
}
s->n[b] = w;
s->sorted[b] = 1;
}
/* 1 when the pair moved within the limits over `ticks`. */
static int within_limits(const MotionEntry* a, const MotionEntry* b,
const PSXMotionLimits* lim, uint32_t ticks) {
const double move = lim->move_per_tick > 0 ? lim->move_per_tick * ticks : HUGE_VAL;
switch (a->kind) {
case PSX_MOTION_MATRIX:
case PSX_MOTION_ROTATION: {
double ang;
int16_t ra[9], rb[9];
if (a->kind == PSX_MOTION_MATRIX && dist3(&a->v[9], &b->v[9]) > move) return 0;
for (int i = 0; i < 9; i++) { ra[i] = (int16_t)a->v[i]; rb[i] = (int16_t)b->v[i]; }
ang = rotation_angle(ra, rb);
if (ang >= 0) return ang <= turn_limit(lim->turn_radians);
for (int i = 0; i < 9; i++) if (abs(a->v[i] - b->v[i]) > 4096) return 0;
return 1;
}
case PSX_MOTION_MATRIX_TRANSLATION:
return dist3(&a->v[9], &b->v[9]) <= move;
case PSX_MOTION_VECTOR:
case PSX_MOTION_SVECTOR:
return dist3(a->v, b->v) <= move;
case PSX_MOTION_ANGLES: {
const double lim12 = turn_limit(lim->turn_radians) * 4096.0 / (2.0 * MOTION_PI);
for (int i = 0; i < 3; i++) if (fabs((double)arc12(a->v[i], b->v[i])) > lim12) return 0;
return 1;
}
case PSX_MOTION_SCALAR:
return fabs((double)b->v[0] - a->v[0]) <= move;
}
return 0;
}
uint32_t psx_motion_prepare(PSXMotionSet* s, const PSXMotionLimits* lim,
PSXMotionStats* stats) {
PSXMotionStats st;
const PSXMotionLimits none = {0, 0};
const int c = s ? s->cur : 0, h = c ^ 1;
memset(&st, 0, sizeof st);
if (!lim) lim = &none;
if (!s || !s->began[c]) { if (stats) *stats = st; return 0; }
sort_capture(s, c);
st.tracked = s->n[c];
s->npairs = 0;
s->ticks = 0;
if (s->history && s->began[h]) {
const uint32_t ticks = s->tick[c] - s->tick[h];
if (ticks >= 1 && ticks <= MOTION_MAX_TICKS) s->ticks = ticks;
}
if (!s->ticks) {
st.unmatched = st.tracked;
} else {
const MotionEntry* pe = s->cap_buf[h];
const MotionEntry* ce = s->cap_buf[c];
uint32_t i = 0;
sort_capture(s, h);
for (uint32_t j = 0; j < s->n[c]; j++) {
while (i < s->n[h] && (pe[i].addr < ce[j].addr ||
(pe[i].addr == ce[j].addr && pe[i].kind < ce[j].kind)))
i++;
if (i >= s->n[h] || pe[i].addr != ce[j].addr || pe[i].kind != ce[j].kind ||
pe[i].identity != ce[j].identity) {
st.unmatched++;
} else if (!within_limits(&pe[i], &ce[j], lim, s->ticks)) {
st.placed++;
} else {
s->pairs[s->npairs].prev = i;
s->pairs[s->npairs].cur = j;
s->npairs++;
}
}
}
st.blended = s->npairs;
st.ticks = s->ticks;
if (stats) *stats = st;
return s->npairs;
}
/* Blend one pair into `v` (entry layout). */
static void blend_entry(const MotionEntry* a, const MotionEntry* b, double t, int32_t* v) {
switch (a->kind) {
case PSX_MOTION_MATRIX:
case PSX_MOTION_ROTATION: {
int16_t ra[9], rb[9], ro[9];
for (int i = 0; i < 9; i++) { ra[i] = (int16_t)a->v[i]; rb[i] = (int16_t)b->v[i]; }
blend_rotation(ra, rb, t, MOTION_PI * 2, ro); /* limits were applied at prepare */
for (int i = 0; i < 9; i++) v[i] = ro[i];
for (int i = 9; i < 12; i++) v[i] = (int32_t)llround(a->v[i] + ((double)b->v[i] - a->v[i]) * t);
break;
}
case PSX_MOTION_MATRIX_TRANSLATION:
for (int i = 0; i < 9; i++) v[i] = b->v[i];
for (int i = 9; i < 12; i++) v[i] = (int32_t)llround(a->v[i] + ((double)b->v[i] - a->v[i]) * t);
break;
case PSX_MOTION_ANGLES:
for (int i = 0; i < 3; i++)
v[i] = (int32_t)lround(a->v[i] + arc12(a->v[i], b->v[i]) * t);
break;
default:
for (int i = 0; i < 3; i++) v[i] = (int32_t)llround(a->v[i] + ((double)b->v[i] - a->v[i]) * t);
break;
}
}
static void write_entry(uint8_t kind, uint32_t addr, const int32_t* v) {
switch (kind) {
case PSX_MOTION_MATRIX:
case PSX_MOTION_MATRIX_TRANSLATION: {
PSXMotionMatrix m;
for (int i = 0; i < 9; i++) m.r[i] = (int16_t)v[i];
for (int i = 0; i < 3; i++) m.t[i] = v[9 + i];
psx_motion_write_matrix(addr, &m, kind == PSX_MOTION_MATRIX);
break;
}
case PSX_MOTION_VECTOR:
for (int i = 0; i < 3; i++) psx_mod_write_word(addr + 4u * (uint32_t)i, (uint32_t)v[i]);
break;
case PSX_MOTION_SVECTOR:
for (int i = 0; i < 3; i++) psx_mod_write_half(addr + 2u * (uint32_t)i, (uint16_t)v[i]);
break;
case PSX_MOTION_ANGLES:
for (int i = 0; i < 3; i++) psx_mod_write_half(addr + 2u * (uint32_t)i, (uint16_t)(v[i] & 4095));
break;
case PSX_MOTION_SCALAR:
psx_mod_write_word(addr, (uint32_t)v[0]);
break;
case PSX_MOTION_ROTATION:
for (int i = 0; i < 9; i++) psx_mod_write_half(addr + 2u * (uint32_t)i, (uint16_t)v[i]);
break;
}
}
void psx_motion_apply(const PSXMotionSet* s, double t) {
if (!s || !s->npairs) return;
const MotionEntry* pe = s->cap_buf[s->cur ^ 1];
const MotionEntry* ce = s->cap_buf[s->cur];
for (uint32_t k = 0; k < s->npairs; k++) {
int32_t v[12];
const MotionEntry* a = &pe[s->pairs[k].prev];
const MotionEntry* b = &ce[s->pairs[k].cur];
blend_entry(a, b, t, v);
write_entry(b->kind, b->addr, v);
}
}
static void export_value(uint8_t kind, const int32_t* v, void* out) {
if (kind == PSX_MOTION_MATRIX || kind == PSX_MOTION_MATRIX_TRANSLATION ||
kind == PSX_MOTION_ROTATION) {
PSXMotionMatrix* m = (PSXMotionMatrix*)out;
for (int i = 0; i < 9; i++) m->r[i] = (int16_t)v[i];
for (int i = 0; i < 3; i++) m->t[i] = v[9 + i];
} else if (kind == PSX_MOTION_SCALAR) {
*(int32_t*)out = v[0];
} else {
memcpy(out, v, 3 * sizeof(int32_t));
}
}
int psx_motion_blend_at(const PSXMotionSet* s, uint32_t kind, uint32_t addr,
double t, void* out) {
const MotionEntry* ce;
uint32_t lo = 0, hi;
if (!s || !out || !s->began[s->cur]) return -1;
ce = s->cap_buf[s->cur];
hi = s->n[s->cur];
if (!s->sorted[s->cur]) {
for (uint32_t j = hi; j-- > 0;) /* unprepared: newest wins */
if (ce[j].addr == addr && ce[j].kind == kind) { export_value(ce[j].kind, ce[j].v, out); return 0; }
return -1;
}
while (lo < hi) {
const uint32_t mid = (lo + hi) / 2;
if (ce[mid].addr < addr || (ce[mid].addr == addr && ce[mid].kind < kind)) lo = mid + 1;
else hi = mid;
}
if (lo >= s->n[s->cur] || ce[lo].addr != addr || ce[lo].kind != kind) return -1;
for (uint32_t k = 0; k < s->npairs; k++) {
if (s->pairs[k].cur == lo) {
int32_t v[12];
blend_entry(&s->cap_buf[s->cur ^ 1][s->pairs[k].prev], &ce[lo], t, v);
export_value(ce[lo].kind, v, out);
return 1;
}
}
export_value(ce[lo].kind, ce[lo].v, out);
return 0;
}
+13 -2
View File
@@ -111,6 +111,16 @@ int render_pass_select(const uint32_t *phases, uint32_t n, double p,
return 1;
}
int render_pass_gen_flip_matches(int shown, int gen_x, int gen_y,
int gen_source, int gen_w, int gen_h,
int flip_x, int flip_y, int flip_source,
int flip_w, int flip_h) {
const int same_rect = gen_x == flip_x && gen_y == flip_y;
if (gen_source != flip_source || gen_w != flip_w || gen_h != flip_h)
return 0;
return shown ? !same_rect : same_rect;
}
int render_pass_gen_select(const uint32_t *phases, uint32_t n, double p,
uint32_t *lo, uint32_t *hi, float *t) {
if (!(p <= RENDER_PASS_GEN_HOLD_MAX)) return 0;
@@ -240,8 +250,9 @@ int render_pass_store_to(const RenderPassStoreTarget *t, uint32_t addr,
t->mmio_write(phys, val, width);
return -1;
}
/* Mod memory, expansion, ROM, unmapped: never written by a pass (mod
* arenas are not part of the pass restore). */
/* Expansion, ROM, unmapped: never written by a pass. (Mod arenas are
* written and journaled before this policy: memory.c
* render_pass_mod_store.) */
return RENDER_PASS_DROP_OTHER;
}
+198
View File
@@ -0,0 +1,198 @@
#include "render_pass_projection.h"
#include "render_pass_motion.h"
#include "cpu_state.h"
#include "psx_cycle_freeze.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <new>
#include <unordered_map>
#include <vector>
namespace {
struct Projection {
uint64_t key;
uint32_t vertices[6];
uint32_t caller, command;
PSXMotionMatrix current{}, previous{};
bool blend = false;
};
PSXMotionMatrix matrix(const CPUState* cpu) {
PSXMotionMatrix m{};
for (unsigned i = 0; i < 9; ++i)
m.r[i] = int16_t(cpu->gte_ctrl[i / 2] >> (16 * (i % 2)));
for (unsigned i = 0; i < 3; ++i) m.t[i] = int32_t(cpu->gte_ctrl[5 + i]);
return m;
}
void set_matrix(CPUState* cpu, const PSXMotionMatrix& m) {
for (unsigned i = 0; i < 4; ++i)
cpu->gte_ctrl[i] = uint16_t(m.r[2*i]) | (uint32_t(uint16_t(m.r[2*i+1])) << 16);
cpu->gte_ctrl[4] = uint32_t(int32_t(m.r[8]));
for (unsigned i = 0; i < 3; ++i) cpu->gte_ctrl[5+i] = uint32_t(m.t[i]);
}
Projection projection(const CPUState* cpu, uint32_t command) {
Projection p{};
p.current = matrix(cpu);
p.command = command;
p.caller = cpu->gpr[31] & 0x1FFFFFFFu;
const unsigned count = (command & 63) == 0x30 ? 6 : 2;
p.key = 1469598103934665603ull;
auto hash = [&](uint32_t v) { p.key = (p.key ^ v) * 1099511628211ull; };
hash(p.caller); hash(command);
for (unsigned i = 0; i < count; ++i) {
// VZ is signed 16-bit; ignore unused high halves in backing registers.
p.vertices[i] = (i & 1) ? uint16_t(cpu->gte_data[i]) : cpu->gte_data[i];
hash(p.vertices[i]);
}
return p;
}
bool same_vertices(const Projection& a, const Projection& b) {
return a.caller == b.caller && a.command == b.command &&
std::memcmp(a.vertices, b.vertices, sizeof a.vertices) == 0;
}
double distance(const PSXMotionMatrix& a, const PSXMotionMatrix& b) {
double d = 0;
for (unsigned i=0; i<3; ++i) { double v=double(a.t[i])-b.t[i]; d += v*v; }
// Distinguish coincident instances with different rotations.
for (unsigned i=0; i<9; ++i) { double v=(double(a.r[i])-b.r[i])/16; d += v*v; }
return d;
}
bool same_matrix(const PSXMotionMatrix& a, const PSXMotionMatrix& b) {
return std::memcmp(a.r, b.r, sizeof a.r) == 0 &&
std::memcmp(a.t, b.t, sizeof a.t) == 0;
}
}
struct PSXProjectionHistory {
PSXProjectionHistory* next = nullptr;
uint32_t capacity;
std::vector<Projection> current, previous;
std::unordered_map<uint64_t, std::vector<uint32_t>> index;
std::vector<uint8_t> used;
PSXProjectionStats stats{};
bool capturing = false, replaying = false, history_valid = false;
double alpha = 1;
};
static PSXProjectionHistory* active = nullptr;
// Intrusive list has no global destructor: title replay objects may release
// their histories from static destructors, in either link initialization order.
static PSXProjectionHistory* histories = nullptr;
extern "C" void psx_projection_reset_session(void) {
// A load normally arrives between frames, when active is null. Invalidate
// idle histories too so a new timeline cannot blend against the old one.
for (auto* h = histories; h; h = h->next) psx_projection_invalidate(h);
g_psx_projection_command = nullptr;
}
extern "C" PSXProjectionHistory* psx_projection_create(uint32_t capacity) {
if (!capacity || capacity > 262144) return nullptr;
auto* h = new (std::nothrow) PSXProjectionHistory;
if (h) {
h->capacity = capacity; h->current.reserve(capacity); h->previous.reserve(capacity);
h->next = histories; histories = h;
}
return h;
}
extern "C" void psx_projection_invalidate(PSXProjectionHistory* h) {
if (!h) return;
if (active == h) { active = nullptr; g_psx_projection_command = nullptr; }
h->current.clear(); h->previous.clear(); h->index.clear(); h->used.clear();
h->stats = {}; h->capturing = h->replaying = h->history_valid = false;
}
extern "C" void psx_projection_destroy(PSXProjectionHistory* h) {
psx_projection_invalidate(h);
for (auto** link = &histories; *link; link = &(*link)->next) {
if (*link == h) { *link = h->next; break; }
}
delete h;
}
extern "C" void psx_projection_capture_begin(PSXProjectionHistory* h) {
if (!h || g_psx_render_pass_active) return;
h->previous.swap(h->current); h->current.clear();
h->stats = {}; h->capturing = true; h->replaying = false;
active = h; g_psx_projection_command = psx_projection_command;
}
extern "C" void psx_projection_capture_end(PSXProjectionHistory* h, uint32_t ticks,
double max_move) {
if (!h) return;
h->capturing = false;
if (active == h) { active = nullptr; g_psx_projection_command = nullptr; }
h->stats.captured = uint32_t(h->current.size());
h->index.clear();
if (h->history_valid && ticks && ticks <= 8 && !h->stats.overflow) {
for (uint32_t i=0; i<h->previous.size(); ++i) h->index[h->previous[i].key].push_back(i);
h->used.assign(h->previous.size(), 0);
const double limit = max_move * ticks;
for (auto& p : h->current) {
auto it = h->index.find(p.key);
uint32_t best = UINT32_MAX;
double best_d = INFINITY;
if (it != h->index.end()) for (uint32_t i : it->second) {
if (h->used[i] || !same_vertices(p, h->previous[i])) continue;
const double d = distance(p.current, h->previous[i].current);
if (d < best_d) { best = i; best_d = d; }
}
if (best == UINT32_MAX) { ++h->stats.unmatched; continue; }
h->used[best] = 1;
p.previous = h->previous[best].current;
bool placed = false;
for (unsigned i=0; i<3; ++i)
placed |= std::abs(double(p.current.t[i])-p.previous.t[i]) > limit;
PSXMotionMatrix midpoint;
p.blend = !placed && psx_motion_blend_matrix(&p.previous, &p.current, .5, 1.2, &midpoint);
if (p.blend) {
++h->stats.matched;
if (!same_matrix(p.previous,p.current)) ++h->stats.changed;
} else ++h->stats.discontinuities;
}
} else h->stats.unmatched = h->stats.captured;
h->history_valid = !h->stats.overflow && !h->current.empty();
// Index the current frame for replay; visibility/subdivision may reorder
// projections. Never blend a different vertex because an ordinal shifted.
h->index.clear();
for (uint32_t i=0; i<h->current.size(); ++i) h->index[h->current[i].key].push_back(i);
}
extern "C" void psx_projection_replay_begin(PSXProjectionHistory* h, uint32_t alpha) {
if (!h || !g_psx_render_pass_active) return;
h->capturing = false; h->replaying = true;
h->used.assign(h->current.size(), 0);
h->alpha = std::min(alpha, 65536u) / 65536.0;
h->stats.replayed = 0; active = h; g_psx_projection_command = psx_projection_command;
}
extern "C" void psx_projection_replay_end(PSXProjectionHistory* h) {
if (!h) return;
h->replaying = false;
if (active == h) { active = nullptr; g_psx_projection_command = nullptr; }
}
extern "C" void psx_projection_stats(const PSXProjectionHistory* h, PSXProjectionStats* out) {
if (out) *out = h ? h->stats : PSXProjectionStats{};
}
extern "C" int psx_projection_command(CPUState* cpu, uint32_t command) {
auto* h = active;
if (!h || ((command & 63) != 1 && (command & 63) != 0x30)) return 0;
// A watchdog may unwind a replay callback without its normal end call.
// The sandbox flag, rather than a plugin-local flag, owns replay lifetime.
if (h->replaying && !g_psx_render_pass_active) {
active = nullptr; g_psx_projection_command = nullptr; h->replaying = false; return 0;
}
Projection p = projection(cpu, command);
if (h->capturing && !g_psx_render_pass_active) {
if (h->current.size() == h->capacity) ++h->stats.overflow;
else h->current.push_back(p);
return 0;
}
if (!h->replaying || !g_psx_render_pass_active) return 0;
auto it = h->index.find(p.key);
if (it == h->index.end()) return 0;
for (uint32_t i : it->second) {
const auto& c = h->current[i];
if (h->used[i] || !same_vertices(c,p) || !same_matrix(c.current,p.current)) continue;
h->used[i] = 1;
if (!c.blend || same_matrix(c.previous,c.current)) return 0;
PSXMotionMatrix intermediate;
psx_motion_blend_matrix(&c.previous, &c.current, h->alpha, 1.2, &intermediate);
set_matrix(cpu, intermediate); ++h->stats.replayed;
return 1;
}
return 0;
}
+3
View File
@@ -282,6 +282,9 @@ void gr_draw_shaded_textured_triangle(int x0, int y0, int u0, int v0,
uint32_t color2, uint16_t clut_x,
uint16_t clut_y, uint16_t texpage,
int raw_texture) {
#ifdef GPU_EXEC_RECORD_SHADED_TEXTURED
gpu_exec_record_triangle(x0, y0, x1, y1, x2, y2);
#endif
(void)x0; (void)y0; (void)u0; (void)v0; (void)color0; (void)x1;
(void)y1; (void)u1; (void)v1; (void)color1; (void)x2; (void)y2;
(void)u2; (void)v2; (void)color2; (void)clut_x; (void)clut_y;
+54
View File
@@ -0,0 +1,54 @@
"""Compile and check palette-aware minification on a hidden real GL context."""
import argparse
import json
from pathlib import Path
import platform
import subprocess
import sys
def main():
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--cc',required=True)
parser.add_argument('--sdl-include',required=True)
parser.add_argument('--sdl-library',required=True)
parser.add_argument('--output',required=True)
parser.add_argument('--fixture',type=Path)
args=parser.parse_args()
fw=Path(__file__).resolve().parents[2]
out=Path(args.output).resolve(); out.mkdir(parents=True,exist_ok=True)
receipt=[]
def run(command):
command=[str(x) for x in command]
r=subprocess.run(command,cwd=out,capture_output=True,text=True,encoding='utf-8',errors='replace')
receipt.append(dict(cmd=command,exit=r.returncode,stdout=r.stdout,stderr=r.stderr))
(out/'receipt.json').write_text(json.dumps(receipt,indent=2)+'\n',encoding='utf-8')
print(r.stdout,r.stderr[-4000:],flush=True)
if r.returncode: raise subprocess.CalledProcessError(r.returncode,command)
flags=['-std=gnu11','-O1','-DPSX_SDL3=1','-DPSX_NO_DEBUG_TOOLS=1',
'-ffunction-sections','-fdata-sections','-I',fw/'runtime/include','-I',fw/'runtime/src']
for include in args.sdl_include.split(';'):
if include: flags+=['-I',include]
sources=[('probe',args.fixture or fw/'runtime/tests/test_gl_texture_filter.c'),
('sw',fw/'runtime/src/gpu_sw_renderer.c'),('fi',fw/'runtime/src/frame_interpolation.c'),
('rp',fw/'runtime/src/render_pass_plan.c')]
objects=[]
for name,source in sources:
obj=out/(name+'.o'); run([args.cc,*flags,'-c',source,'-o',obj]); objects.append(obj)
exe=out/('filter.exe' if platform.system()=='Windows' else 'filter')
libraries=['-lm']
if platform.system()=='Windows':
libraries+=['-static','-lopengl32']+['-l'+x for x in ('kernel32','user32','gdi32','winmm','imm32',
'ole32','oleaut32','version','uuid','advapi32','setupapi','shell32','dinput8')]
elif platform.system()=='Darwin':
from run_gl_scale_invariance import MAC_FRAMEWORKS
for framework in MAC_FRAMEWORKS: libraries+=['-framework',framework]
libraries+=['-liconv','-Wl,-dead_strip']
else: libraries+=['-lGL','-ldl','-lpthread']
if platform.system()!='Darwin': libraries+=['-Wl,--gc-sections']
run([args.cc,*objects,args.sdl_library,'-o',exe,*libraries])
run([exe])
return 0
if __name__=='__main__':
try: sys.exit(main())
except subprocess.CalledProcessError as exc: sys.exit(exc.returncode)
+85
View File
@@ -0,0 +1,85 @@
/* Exercise the real snapshot writer/loader and handoff latch with hardware
* doubles. In particular, restore must never run the destructive boot hook. */
#include "boot_state.c"
#include "fntrace.c"
#include <assert.h>
static uint8_t ram[2*1024*1024], spad[1024], spuram[512*1024];
static uint16_t vram[1024*512];
static int boot_clears, cd_handoffs;
uint32_t i_stat, i_mask, g_psx_icache_tv[1024];
uint64_t psx_cycle_count;
uint8_t* memory_get_ram_ptr(void) { return ram; }
uint32_t memory_get_ram_bytes(void) { return sizeof ram; }
uint8_t* memory_get_scratchpad_ptr(void) { return spad; }
int psx_ram_8mb_active(void) { return 0; }
void memory_clear_low_boot_scratch(void) { ++boot_clears; memset(ram,0,sizeof ram); }
void dirty_ram_clear_image_baseline(void) { ++boot_clears; }
void cdrom_notify_game_started(void) { ++cd_handoffs; }
uint32_t dirty_ram_get_bitmap_word_count(void) { return 16; }
uint32_t dirty_ram_get_bitmap_word(uint32_t i) { (void)i; return 0; }
void dirty_ram_set_bitmap_words(const uint32_t* p,uint32_t n) { (void)p; (void)n; }
void overlay_watch_invalidate_after_ram_restore(void) {}
void psx_kernel_bless_note_range(uint32_t p,uint32_t n) { (void)p; (void)n; }
void gte_canonicalize_cpu_state(CPUState* cpu) { cpu->gpr[0]=0; }
uint32_t interrupts_get_cycles_since_vblank(void) { return 42; }
void interrupts_set_cycles_since_vblank(uint32_t n) { (void)n; }
void timers_get_snapshot(uint16_t c[3],uint32_t m[3],uint16_t t[3],int32_t irq[3],uint32_t f[3]) {
memset(c,0,6); memset(m,0,12); memset(t,0,6); memset(irq,0,12); memset(f,0,12);
}
void timers_set_snapshot(const uint16_t c[3],const uint32_t m[3],const uint16_t t[3],const int32_t irq[3],const uint32_t f[3]) {
(void)c;(void)m;(void)t;(void)irq;(void)f;
}
#define MODULE(name) uint32_t name##_snapshot_bytes(void){return 4;} \
void name##_snapshot_write(uint8_t* p){memset(p,0,4);} \
int name##_snapshot_read(const uint8_t* p,uint32_t n){(void)p;return n==4;}
MODULE(gpu) MODULE(spu) MODULE(cdrom) MODULE(dma) MODULE(sio) MODULE(mdec)
int sio_snapshot_validate(const uint8_t* p,uint32_t n){(void)p;return n==4;}
int mdec_snapshot_validate(const uint8_t* p,uint32_t n){(void)p;return n==4;}
uint8_t* spu_get_ram_ptr(void){return spuram;}
uint32_t spu_get_ram_bytes(void){return sizeof spuram;}
uint32_t psx_mod_memory_snapshot_bytes(void){return 0;}
uint32_t psx_mod_memory_layout_cookie(void){return 0;}
void psx_mod_memory_snapshot_write(uint8_t* p){(void)p;}
int psx_mod_memory_snapshot_read(const uint8_t* p,uint32_t n){(void)p;return n==0;}
int psx_mod_memory_snapshot_validate(const uint8_t* p,uint32_t n){(void)p;return n==0;}
int gpu_vram_dirty_tracking(void){return 0;}
uint32_t gpu_vram_dirty_row_count(void){return 0;}
const uint64_t* gpu_vram_dirty_mask(void){static const uint64_t mask[8]={0};return mask;}
void gpu_vram_dirty_clear(void){}
void gr_vram_transfer_in(int x,int y,int w,int h,const uint16_t* p){(void)x;(void)y;memcpy(vram,p,(size_t)w*h*2);}
void gr_vram_transfer_out(int x,int y,int w,int h,uint16_t* p){(void)x;(void)y;memcpy(p,vram,(size_t)w*h*2);}
static void u32(uint8_t* p,uint32_t n){PstW w;pst_w_init(&w,p,4);assert(pst_w_u32(&w,n));}
int main(void) {
CPUState cpu={0};
fntrace_set_game_range(0x801b0000,0);
for(int compressed=0;compressed<2;++compressed) for(int started=0;started<2;++started) {
uint8_t* saved=NULL; size_t len=0;
cpu.pc=0x80050044; cpu.gpr[4]=0x12345678; ram[0x100]=0xab;
fntrace_restore_game_started(started);
assert(compressed ? boot_state_save_buffer(&cpu,123,0x801b0000,&saved,&len)
: boot_state_save_buffer_raw(&cpu,123,0x801b0000,&saved,&len));
assert(saved[4]==9 && saved[len-4]==started); /* handoff is last */
fntrace_restore_game_started(!started); ram[0x100]=0; cpu.pc=0;
assert(boot_state_load_buffer(saved,len,123,0x801b0000,&cpu));
assert(fntrace_is_game_started()==started && ram[0x100]==0xab && cpu.pc==0x80050044);
assert(!boot_clears && !cd_handoffs);
/* Malformed, missing and duplicate phase sections must reject before
* changing CPU, RAM or the previously active handoff latch. */
cpu.pc=0xdeadbeef; ram[0x100]=0x55; fntrace_restore_game_started(!started);
saved[len-4]=2;
assert(!boot_state_load_buffer(saved,len,123,0x801b0000,&cpu));
saved[len-4]=started;
u32(saved+28,16); /* omit the last, required phase section */
assert(!boot_state_load_buffer(saved,len-20,123,0x801b0000,&cpu));
u32(saved+28,18);
uint8_t* duplicate=malloc(len+20); assert(duplicate);
memcpy(duplicate,saved,len); memcpy(duplicate+len,saved+len-20,20);
assert(!boot_state_load_buffer(duplicate,len+20,123,0x801b0000,&cpu));
assert(cpu.pc==0xdeadbeef && ram[0x100]==0x55 && fntrace_is_game_started()==!started);
free(duplicate); free(saved);
}
puts("snapshot handoff: cold/warm, BIOS/game, compressed/raw and atomic rejection passed");
return 0;
}
+128
View File
@@ -0,0 +1,128 @@
/* Real GL fixture, sharing the source-owned renderer stubs and setup helpers. */
#define main scale_fixture_main
#include "test_gl_scale_invariance.c"
#undef main
void present_shot_done(int ok) { (void)ok; }
int host_osd_needs_present(void) { return 0; }
int psx_present_vsync_owns_cadence(void) { return 0; }
void latency_ring_mark(LatencyStage stage) { (void)stage; }
void psx_host_sleep_ms(unsigned ms) { SDL_Delay(ms); }
int present_shot_take(char* out,int n) { (void)out; (void)n; return 0; }
int host_osd_image(const uint32_t** p,int* w,int* h) { (void)p; (void)w; (void)h; return 0; }
int host_osd_volume_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
void host_osd_present_done(void) {}
int psx_rewind_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
float psx_rewind_slide(void) { return 0; }
int psx_savestate_menu_overlay_image(const uint32_t** p,int* w,int* h) { return host_osd_image(p,w,h); }
static void upload_checker(int depth,int stp) {
uint16_t page[128*128], palette[256];
for(int i=0;i<256;++i) palette[i]=0x03e0;
palette[1]=0x001f; palette[2]=0x7c00 | (stp?0x8000:0);
const int pitch=depth==0?32:depth==1?64:128;
for(int y=0;y<128;++y) for(int x=0;x<pitch;++x) {
uint16_t p=0;
if(depth==2) p=((x+y)&1)?palette[1]:palette[2];
else {
int bits=depth==0?4:8, count=16/bits;
for(int n=0;n<count;++n) p|=(uint16_t)((((x*count+n+y)&1)?1:2)<<(n*bits));
}
page[y*pitch+x]=p;
}
glb_vram_transfer_in(512,0,pitch,128,page);
glb_vram_transfer_in(512,256,256,1,palette);
}
static void triangle(int mode,int depth,int shift,int tracked) {
glb_set_texture_filter(mode);
glb_set_draw_area(0,0,319,239); glb_set_draw_offset(0,0);
glb_set_mask_bits(0,0); glb_set_semi_transparency(0,0);
glb_set_color_modulation(128,128,128,1);
glb_fill_rect(0,0,64,64,0x03e0);
glb_set_precise_triangle(tracked,20<<16,20<<16,44<<16,20<<16,20<<16,44<<16);
glb_draw_textured_triangle(20,20,shift,0,44,20,127+shift,0,20,44,shift,127,512,256,(uint16_t)(8|(depth<<7)));
gl_renderer_sync_cpu();
check(gl_renderer_fbo_peek(0,0,64,64,peek),"filter readback");
}
static int chroma(void) {
int total=0;
for(int y=23;y<29;++y) for(int x=23;x<29;++x) {
uint16_t c=peek[y*64+x]; total+=abs((c&31)-((c>>10)&31));
check(((c>>5)&31)==0,"palette decode does not pick unrelated green entries");
}
return total;
}
static void atlas_edge(int depth) {
uint16_t page[256*256], palette[256];
const int word_pixels=depth==0?4:depth==1?2:1, pitch=256/word_pixels;
for(int i=0;i<256;++i) palette[i]=0x03e0;
palette[2]=0x7c00;
for(int y=0;y<256;++y) for(int x=0;x<pitch;++x) {
const int inside=y<127 && x<128/word_pixels;
page[y*pitch+x]=depth==0?(inside?0x2222:0x1111):depth==1?(inside?0x0202:0x0101):(inside?0x7c00:0x03e0);
}
glb_vram_transfer_in(512,0,pitch,256,page);
glb_vram_transfer_in(512,256,256,1,palette);
glb_set_texture_filter(2); glb_fill_rect(0,0,64,64,0x001f);
// The first covered pixel lies barely inside the triangle. Its footprint
// extends below UV zero; wrapping before clamping would sample green at 255.
const int fraction=64881; // 0.99 pixels; renderer adds half a pixel
glb_set_precise_triangle(1,(20<<16)+fraction,(20<<16)+fraction,
(44<<16)+fraction,(20<<16)+fraction,(20<<16)+fraction,(44<<16)+fraction);
glb_draw_textured_triangle(20,20,0,0,44,20,127,0,20,44,0,127,512,256,(uint16_t)(8|(depth<<7)));
gl_renderer_sync_cpu();
check(gl_renderer_fbo_peek(0,0,64,64,peek),"atlas-edge readback");
check((peek[21*64+21]&0x7fff)==0x7c00,"footprint clamps before wrapping into far atlas row");
}
int main(void) {
if(SDL_Init(SDL_INIT_VIDEO)!=0) return 2;
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION,3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION,3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,SDL_GL_CONTEXT_PROFILE_CORE);
SDL_Window* win=SDL_CreateWindow("Filtering hidden test",0,0,128,128,SDL_WINDOW_OPENGL|SDL_WINDOW_HIDDEN);
if(!win) return 2;
glb_init(vram); glb_set_scale(1); gl_renderer_set_swap_interval(0);
if(!gl_renderer_init_context(win)) return 2;
for(int depth=0;depth<3;++depth) {
upload_checker(depth,0);
triangle(0,depth,0,1); int nearest=chroma();
triangle(2,depth,0,1); int stable=chroma();
printf("depth=%d nearest_chroma=%d stable_chroma=%d\n",depth,nearest,stable);
check(stable<nearest/2,"minified checkerboard aliases less");
triangle(0,depth,0,0); uint64_t hud=fnv(peek,64*64*2,0xcbf29ce484222325ull);
triangle(2,depth,0,0);
check(hud==fnv(peek,64*64*2,0xcbf29ce484222325ull),"untracked UI remains nearest");
// A texture window redirects every tap into an 8x8 region. Poison
// surrounding palette entries so filtering across its edge is visible.
upload_checker(depth,0);
const int word_pixels=depth==0?4:depth==1?2:1;
const int width=8/word_pixels;
uint16_t patch[64];
for(int i=0;i<width*8;++i) patch[i]=depth==0?0x2222:depth==1?0x0202:0x7c00;
glb_vram_transfer_in(512+32/word_pixels,32,width,8,patch);
glb_set_texture_window(31u | (31u<<5) | (4u<<10) | (4u<<15));
triangle(2,depth,0,1);
for(int y=21;y<26;++y) for(int x=21;x<26;++x)
check((peek[y*64+x]&0x7fff)==0x7c00,"all filter taps respect texture window");
glb_set_texture_window(0);
atlas_edge(depth);
}
// A different CLUT after a prior draw must immediately change the filtered
// result. No stale mip chain or palette-index averaging may survive it.
upload_checker(0,0);
uint16_t blue[]={0,0x7c00,0x7c00};
glb_vram_transfer_in(512,256,3,1,blue);
triangle(2,0,0,1);
check((peek[24*64+24]&0x7fff)==0x7c00,"live palette update changes filtered color");
// Cutouts and STP remain the center texel's classification.
uint16_t transparent[32*128]={0};
glb_vram_transfer_in(512,0,32,128,transparent);
triangle(2,0,0,1);
check(peek[24*64+24]==0x03e0,"zero texel remains a cutout");
upload_checker(2,1); triangle(2,2,0,1);
check((peek[24*64+24]&0x7fff)==0x001f || (peek[24*64+24]&0xffff)==0xfc00,
"filter never averages colors across opaque and STP classes");
check(glGetError()==GL_NO_ERROR,"GL errors");
printf("checks=%d failures=%d\n",checks,failures);
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
return failures?1:0;
}
@@ -1,6 +1,7 @@
#include "cpu_state.h"
#include "gte.h"
#include "pgxp.h"
#include "render_pass_projection.h"
#include <array>
#include <cmath>
@@ -409,6 +410,57 @@ int test_sequence_fuzz() {
return 0;
}
static unsigned projection_callback_calls;
static int substitute_projection(CPUState *cpu, uint32_t) {
++projection_callback_calls;
cpu->gte_ctrl[0] = 0x1000u;
cpu->gte_ctrl[1] = 0u;
cpu->gte_ctrl[2] = 0x1000u;
cpu->gte_ctrl[3] = 0u;
cpu->gte_ctrl[4] = 0x1000u;
cpu->gte_ctrl[5] = 32u;
cpu->gte_ctrl[6] = 16u;
cpu->gte_ctrl[7] = 1024u;
return 1;
}
int test_projection_override_restores_transform() {
for (uint32_t command : {0x80001u, 0x80030u}) {
CPUState actual{};
actual.gte_ctrl[0] = 0x800u;
actual.gte_ctrl[2] = 0x800u;
actual.gte_ctrl[4] = 0x800u;
actual.gte_ctrl[5] = 80u;
actual.gte_ctrl[6] = 40u;
actual.gte_ctrl[7] = 2048u;
actual.gte_ctrl[26] = 256u;
actual.gte_data[0] = 0x00100020u;
actual.gte_data[1] = 64u;
actual.gte_data[2] = 0x00300040u;
actual.gte_data[3] = 96u;
actual.gte_data[4] = 0x00500060u;
actual.gte_data[5] = 128u;
CPUState original = actual, expected = actual;
substitute_projection(&expected, command);
gte_test_execute_reference(&expected, command);
projection_callback_calls = 0;
g_psx_projection_command = substitute_projection;
gte_execute(&actual, command);
g_psx_projection_command = nullptr;
if (projection_callback_calls != 1 ||
std::memcmp(actual.gte_ctrl, original.gte_ctrl, 8 * sizeof(uint32_t)) ||
std::memcmp(actual.gte_data, expected.gte_data, sizeof(actual.gte_data))) {
std::fprintf(stderr, "FAIL: projection override leaked transform or changed output (%x)\n", command);
return 1;
}
g_psx_projection_command = substitute_projection;
gte_execute(&actual, 0x06u);
g_psx_projection_command = nullptr;
if (projection_callback_calls != 1) return 1;
}
return 0;
}
int test_command_marshaling() {
constexpr std::array<uint8_t, 22> kFunctions = {
0x01, 0x06, 0x0C, 0x10, 0x11, 0x12, 0x13, 0x14,
@@ -1022,6 +1074,7 @@ int main() {
if (int rc = test_writes()) return rc;
if (int rc = test_sequence_fuzz()) return rc;
if (int rc = test_command_marshaling()) return rc;
if (int rc = test_projection_override_restores_transform()) return rc;
if (int rc = test_command_timing_hook()) return rc;
if (int rc = test_precise_sxy_invalidation()) return rc;
if (int rc = test_precise_nclip_is_title_scoped()) return rc;
@@ -6,6 +6,11 @@ from pathlib import Path
def main():
source = (Path(__file__).resolve().parents[1] /
'src/dirty_ram_interp.c').read_text(encoding='utf-8')
fetched = source[source.index('static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,\n uint32_t *next_pc_out) {'):]
fetched = fetched[:fetched.index('const uint32_t ld_op')]
assert fetched.count('psx_mod_instruction(cpu, pc, insn)') == 1
assert fetched.index('if (s_ld_pend_armed') < fetched.index('psx_mod_instruction(cpu, pc, insn)')
assert 'if (g_psx_mod_instruction_hooks) psx_mod_instruction(cpu, pc, insn);' in fetched
start = source.index('static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {')
source = source[start:]
static_dispatch = source[source.index('/* B-2: statically-compiled'):]
+21
View File
@@ -25,5 +25,26 @@ int main(void){
check(psx_mod_memory_layout_cookie()==cookie,"stable layout identity");
check(psx_mod_gpu_dma_memory_alloc(16,16)!=0 && psx_mod_memory_layout_cookie()!=cookie,"layout change detected");
check(!psx_mod_memory_snapshot_read(saved,size),"old layout rejected");
/* Render passes write mod arenas through a first-write page journal and
* put them back with the rest of the pass restore. */
{
const uint64_t before = render_pass_mod_arenas_hash();
check(render_pass_mod_store(PSX_MOD_GPU_DMA_GUEST_BASE + 8u, 0xAABBCCDDu, 4) == 1 &&
mod_gpu_dma_memory[8] == 0xDD && mod_gpu_dma_memory[11] == 0xAA,
"pass store reaches the GPU-DMA arena");
check(render_pass_mod_store(PSX_MOD_GPU_DMA_GUEST_BASE + 4096u - 2u, 0x1234u, 2) == 1,
"pass store at a page edge");
check(render_pass_mod_store(0x9f000004u, 0x77u, 1) == 1 && mod_memory[4] == 0x77,
"pass store reaches mod memory");
check(render_pass_mod_store(0x80001000u, 1u, 4) == 0, "main RAM left to the RAM path");
check(render_pass_mod_arenas_hash() != before, "hash sees the pass writes");
render_pass_mod_arenas_rollback();
check(mod_gpu_dma_memory[8] == 0x34 && mod_gpu_dma_memory[4094] == 0x34 &&
mod_memory[4] == 0x12 && render_pass_mod_arenas_hash() == before,
"rollback restores every journaled page");
check(render_pass_mod_store(PSX_MOD_GPU_DMA_GUEST_BASE + 8u, 0x1u, 4) == 1 &&
(render_pass_mod_arenas_rollback(), mod_gpu_dma_memory[8] == 0x34),
"a second pass journals again");
}
free(saved);puts("enhancement snapshot checks passed");return 0;
}
+134 -1
View File
@@ -76,6 +76,8 @@ extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) {
return 0;
}
extern "C" void psx_ram_reset_size_request(void) {}
extern "C" void psx_projection_reset_session(void) {}
extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {}
extern "C" int psx_ws_x_margin(void) { return 0; }
/* Stand-in for the GPU's display geometry. psx_mod_display_width/height must
@@ -93,9 +95,15 @@ extern "C" int fntrace_is_game_started(void) { return 1; }
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
extern "C" int gpu_ws_configured_x_reveal(void) { return 0; }
extern "C" void gpu_ws_tag_hud_prim(uint32_t, int) {}
extern "C" void gpu_ws_tag_screen_mask_quad(uint32_t) {}
extern "C" void gpu_ws_tag_radial_screen_mask_quad(uint32_t, float) {}
static CPUState* interrupted_entry_cpu;
static void test_vblank_plugin(void) {
plugin_calls++;
if (interrupted_entry_cpu && psx_mod_finish_function(interrupted_entry_cpu)) failures++;
}
/* Function-entry hooks: one owned by the plan's active plugin, one by a
@@ -104,9 +112,32 @@ static int active_entry_hits;
static int disabled_entry_hits;
static int unselected_entry_hits;
static uint32_t active_entry_last;
static void test_active_entry(CPUState*, uint32_t address) {
static int entry_test_mode, nested_result;
static uint32_t nested_pc;
static void test_active_entry(CPUState* cpu, uint32_t address) {
active_entry_hits++;
active_entry_last = address;
if (entry_test_mode == 1) {
CPUState unrelated{};
if (psx_mod_finish_function(&unrelated) || !psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = 0x12345678u;
} else if (entry_test_mode == 2 || entry_test_mode == 3) {
const int mode = entry_test_mode;
if (mode == 3 && !psx_mod_finish_function(cpu)) failures++;
CPUState nested{}; nested.gpr[31] = 0x80004000u;
entry_test_mode = mode == 2 ? 1 : 0;
nested_result = psx_mod_function_entry(&nested, address);
nested_pc = nested.pc;
entry_test_mode = mode;
} else if (entry_test_mode == 4) {
/* A native call made by a hook can cross a guest VBlank. Its
* callbacks must neither inherit nor discard the interrupted hook. */
interrupted_entry_cpu = cpu;
mod_runtime_on_vblank();
interrupted_entry_cpu = nullptr;
if (!psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = 0x87654321u;
}
}
static void test_disabled_entry(CPUState*, uint32_t) { disabled_entry_hits++; }
static void test_unselected_entry(CPUState*, uint32_t) { unselected_entry_hits++; }
@@ -129,6 +160,30 @@ static int test_inactive_filter(CPUState*, uint32_t) {
return 1;
}
static int guest_function_hits;
static int instruction_hits;
static void test_instruction(CPUState* cpu, uint32_t) {
instruction_hits++;
cpu->gpr[5] = 0x80301008u;
if (psx_mod_finish_function(cpu)) failures++;
}
static void test_guest_function(CPUState* cpu, uint32_t) {
guest_function_hits++;
const uint32_t ra = cpu->gpr[31];
CPUState nested{};
nested.gpr[31] = 0x80007000u;
const int mode = entry_test_mode;
entry_test_mode = 1;
if (!psx_mod_function_entry(&nested, 0x80003000u) || nested.pc != nested.gpr[31]) failures++;
entry_test_mode = mode;
interrupted_entry_cpu = cpu;
mod_runtime_on_vblank();
interrupted_entry_cpu = nullptr;
if (!psx_mod_finish_function(cpu)) failures++;
cpu->gpr[2] = cpu->gpr[4] + 7u;
if (cpu->gpr[31] != ra) failures++;
}
static void test_activation_plugin(void) {
activation_calls++;
}
@@ -380,18 +435,52 @@ int main() {
"runtime.unselected-entry", 0x80003000u, test_unselected_entry) == 1,
"unselected function-entry hook must register");
CPUState entry_cpu{};
check(psx_mod_register_instruction_plugin("runtime.test-vblank", 0x80003004u,
0x90A30014u, test_instruction), "register guarded instruction callback");
check(!psx_mod_register_instruction_plugin("runtime.test-vblank", 0xA0003004u,
0x90A30014u, test_instruction) &&
!psx_mod_register_instruction_plugin("runtime.other", 0x80003005u,
0x90A30014u, test_instruction), "reject aliased duplicate and unaligned sites");
psx_mod_write_word(0x80003004u, 0x90A30014u);
check(psx_mod_register_guest_function_plugin(
"runtime.test-vblank", 0x8FFF0000u, test_guest_function),
"trusted callback registers outside the hardware map");
check(!psx_mod_register_guest_function_plugin(
"runtime.other", 0xAFFF0000u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0x80003000u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0x8FFF0001u, test_guest_function) &&
!psx_mod_register_guest_function_plugin("runtime.other", 0xCFFF0000u, test_guest_function),
"guest callbacks refuse alias collisions, game addresses, misalignment and KSEG2");
check(psx_mod_register_guest_function_plugin(
"runtime.unselected-entry", 0x8FFF0004u, test_unselected_entry),
"unselected callback implementation can register");
check(PSXRecompV4::mod_runtime_initialize(
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
error.c_str());
check(PSXRecompV4::mod_runtime_commit(cue_path, &error),
"CUE and its data-track BIN must have the same mod target identity");
/* Committed but not yet activated: no hook may run. */
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 0 && !g_psx_mod_instruction_hooks, "instruction hooks await activation");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
g_psx_mod_guest_functions == 0,
"committed guest functions remain unavailable until activation");
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 0,
"function-entry hooks must not run before plugin activation");
mod_runtime_activate_plugins();
check(activation_calls == 1,
"resolved trusted plugin must activate before runtime startup");
entry_cpu.pc = 0x80004000u;
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
psx_mod_instruction(&entry_cpu, 0xA0003004u, 0x90A30014u);
check(instruction_hits == 2 && entry_cpu.gpr[5] == 0x80301008u &&
entry_cpu.pc == 0x80004000u, "instruction aliases preserve control flow and edit registers");
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30015u);
psx_mod_write_word(0x80003004u, 0x90A30015u);
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 2, "fetched and live instruction guards reject changed code");
psx_mod_write_word(0x80003004u, 0x90A30014u);
mod_runtime_on_vblank();
check(plugin_calls == 1,
"resolved trusted plugin must run on guest VBlank");
@@ -412,6 +501,11 @@ int main() {
"hooks of plugins the plan does not activate must never run");
/* A replaced plan drops every hook until its own activation. */
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u);
check(instruction_hits == 2 && !g_psx_mod_instruction_hooks, "clearing removes instruction hooks");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
g_psx_mod_guest_functions == 0,
"clearing the plan drops guest callback availability");
psx_mod_function_entry(&entry_cpu, 0x80003000u);
check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 3,
"clearing the plan must drop its function-entry hooks");
@@ -482,6 +576,45 @@ int main() {
check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str());
mod_runtime_activate_plugins();
entry_cpu.gpr[31] = 0x80005000u;
check(!psx_mod_finish_function(&entry_cpu) && !psx_mod_function_entry(nullptr, 0x80003000u),
"completion outside an entry callback or with no CPU is refused");
entry_test_mode = 1;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && entry_cpu.gpr[2] == 0x12345678u,
"explicit completion returns handled and publishes guest return PC/results");
entry_test_mode = 2; entry_cpu.pc = 0;
check(!psx_mod_function_entry(&entry_cpu, 0x80003000u) && entry_cpu.pc == 0 &&
nested_result == 1 && nested_pc == 0x80004000u,
"inner completion cannot finish an ordinary outer callback");
entry_test_mode = 3;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && nested_result == 0,
"outer completion survives a nested ordinary callback");
entry_test_mode = 4;
check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 &&
entry_cpu.pc == 0x80005000u && entry_cpu.gpr[2] == 0x87654321u,
"entry completion survives VBlank callbacks during a native call");
entry_test_mode = 0;
entry_cpu.gpr[4] = 35u;
const uint32_t guest_sp = entry_cpu.gpr[29];
check(g_psx_mod_guest_functions == 1 &&
psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) &&
entry_cpu.gpr[2] == 42u && entry_cpu.pc == 0x80005000u &&
entry_cpu.gpr[29] == guest_sp,
"active callback returns its result through normal guest PC/stack state");
check(psx_mod_dispatch_guest_function(&entry_cpu, 0x0FFF0000u) &&
psx_mod_dispatch_guest_function(&entry_cpu, 0xAFFF0000u) &&
guest_function_hits == 3,
"guest callback aliases retain nested entry and VBlank scope");
check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0004u) &&
!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0001u) &&
!psx_mod_dispatch_guest_function(&entry_cpu, 0xCFFF0000u) &&
!psx_mod_dispatch_guest_function(nullptr, 0x8FFF0000u) &&
unselected_entry_hits == 0,
"unselected, unknown, misaligned and invalid callback requests fall through");
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
ram[0x1100] = 0; ram[0x1101] = 0;
ram[0x1102] = 1; ram[0x1103] = 0;
+64 -1
View File
@@ -136,6 +136,15 @@ void overlay_loader_set_native_nesting(int d, uint32_t ip) {
s_ov_active_depth = d; s_ov_inprogress = ip;
}
/* Precision-tracking checkpoint (gte.cpp): every pass opens one and every
* restore - including the watchdog landing - closes it. */
static int s_prec_open, s_prec_begins;
void gte_precision_checkpoint_begin(void) { s_prec_open++; s_prec_begins++; }
void gte_precision_checkpoint_rollback(void) { s_prec_open--; }
/* memory.c mod arenas (render_pass_mod_store): nothing journaled here. */
void render_pass_mod_arenas_rollback(void) {}
uint64_t render_pass_mod_arenas_hash(void) { return 0; }
/* GPU and presenter. */
uint64_t gpu_pass_state_hash(void) { return 42; }
int gpu_pass_checkpoint_save(void) { return 1; }
@@ -152,6 +161,7 @@ uint32_t gl_renderer_pass_plan(uint32_t p, uint32_t s, uint32_t *a,
return 0;
}
static int s_open_passes, s_kept;
void gl_renderer_pass_set_flip_shown(int shown) { (void)shown; }
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
uint32_t period, int reuse_backup) {
(void)x; (void)y; (void)w; (void)h; (void)open_gen; (void)period;
@@ -236,6 +246,29 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) {
g_call_unit_depth = prev_unit;
}
/* The interpreter's guest-span state (dirty_ram_run_span): a span replays
* guest frames like any other code, so a runaway inside one leaves it open
* when the watchdog longjmps out. */
static uint32_t s_span_lo, s_span_hi;
static const Frames *s_span_frames;
void dirty_ram_span_get(uint32_t *lo, uint32_t *hi) { *lo = s_span_lo; *hi = s_span_hi; }
void dirty_ram_span_set(uint32_t lo, uint32_t hi) { s_span_lo = lo; s_span_hi = hi; }
int dirty_ram_run_span(CPUState *cpu, uint32_t start, uint32_t stop, uint64_t max) {
(void)max;
if (s_span_hi) return 0;
s_span_lo = start & 0x1FFFFFFFu;
s_span_hi = stop & 0x1FFFFFFFu;
guest_frame(cpu, s_span_frames, 0);
s_span_lo = s_span_hi = 0;
return 1;
}
static int span_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
(void)alpha_q16;
s_span_frames = (const Frames *)user;
return psx_mod_run_guest_span(cpu, 0x800143FCu, 0x8001473Cu);
}
static int pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
(void)alpha_q16;
guest_frame(cpu, (const Frames *)user, 0);
@@ -251,7 +284,7 @@ static int leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
typedef struct Live {
int bb_defer, unit, dma, host, dma_ch, active, phase, precise, dispatch;
int ov_depth;
uint32_t ov_ip, batch, resume;
uint32_t ov_ip, batch, resume, span_lo, span_hi;
void (*flush)(void);
DirtyRamLoadDelay ld;
uint64_t cycles, next_service, dev_cycles;
@@ -273,6 +306,8 @@ static void snap(Live *l) {
l->batch = g_psx_cyc_batch;
l->resume = g_dirty_safe_resume_pc;
l->flush = g_overlay_flush_pending_cycles;
l->span_lo = s_span_lo;
l->span_hi = s_span_hi;
l->ld = s_ld;
l->cycles = psx_cycle_count;
l->next_service = psx_next_service_cycle;
@@ -300,6 +335,8 @@ static void check_live(const Live *a, const char *when) {
SAME(batch, "pending cycle batch");
SAME(resume, "interpreter resume latch");
SAME(flush, "shard cycle-flush hook");
SAME(span_lo, "guest span start");
SAME(span_hi, "guest span end");
SAME(ld.armed, "pending load (armed)");
SAME(ld.val, "pending load (value)");
SAME(cycles, "guest clock");
@@ -383,6 +420,32 @@ int main(void) {
CHECK(st.verify_mismatch == 1, "an unbalanced pass is reported in verify mode");
check_live(&live, "after an unbalanced pass");
/* 4. A guest span runs only inside a pass. */
CHECK(psx_mod_run_guest_span(&cpu, 0x800143FCu, 0x8001473Cu) == 0,
"a guest span outside a pass is refused");
render_pass_get_stats(&st);
CHECK(st.spans == 0 && st.span_failures == 0, "a refused span counts nothing");
/* 5. A span that completes inside a pass is counted and closed. */
snap(&live);
CHECK(psx_mod_render_pass(&cpu, &pass, span_pass_fn, &clean) == 1,
"a pass that replays a span is presented");
render_pass_get_stats(&st);
CHECK(st.spans == 1, "the span reached its stop");
check_live(&live, "after a pass with a span");
/* 6. A span that runs away: the watchdog's landing closes it, so later
* dispatches of those PCs take their compiled bodies again. */
snap(&live);
CHECK(psx_mod_render_pass(&cpu, &pass, span_pass_fn, &runaway) == 0,
"a runaway span is rolled back");
render_pass_get_stats(&st);
CHECK(st.watchdog == 2, "the watchdog cut the span off");
CHECK(s_span_hi == 0 && s_span_lo == 0, "the abort closed the open span");
check_live(&live, "after a span watchdog abort");
CHECK(s_prec_begins > 0 && s_prec_open == 0,
"precision shadows were checkpointed and rolled back on every pass, aborts included");
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
+3
View File
@@ -54,6 +54,9 @@ void psx_spu_sample_event_service(void) {}
int psx_get_in_exception(void) { return 0; }
void starvation_watchdog_check(void) {}
void starvation_ring_pc_sample(void) {}
/* memory.c mod arenas (render_pass_mod_store): nothing journaled here. */
void render_pass_mod_arenas_rollback(void) {}
uint64_t render_pass_mod_arenas_hash(void) { return 0; }
int psx_netplay_active(void) { return 0; }
int psx_selfcheck_enabled(void) { return 0; }
void dirty_ram_ld_delay_discard(void) {}
+237
View File
@@ -0,0 +1,237 @@
/* render_pass_motion.c and render_pass_frame.c against a mock guest RAM and
* mock render-pass / mod APIs. */
#include "mod_plugins.h"
#include "render_pass_motion.h"
#include <math.h>
#include <stdio.h>
#include <string.h>
static int failures;
#define CHECK(cond, msg) do { if (!(cond)) { failures++; printf("FAIL: %s\n", msg); } } while (0)
/* ---- mock guest RAM ------------------------------------------------------ */
static uint8_t s_ram[0x10000];
static uint32_t off(uint32_t a) { return a & 0xFFFFu; }
uint8_t psx_mod_read_byte(uint32_t a) { return s_ram[off(a)]; }
uint16_t psx_mod_read_half(uint32_t a) { uint16_t v; memcpy(&v, s_ram + off(a), 2); return v; }
uint32_t psx_mod_read_word(uint32_t a) { uint32_t v; memcpy(&v, s_ram + off(a), 4); return v; }
void psx_mod_write_byte(uint32_t a, uint8_t v) { s_ram[off(a)] = v; }
void psx_mod_write_half(uint32_t a, uint16_t v) { memcpy(s_ram + off(a), &v, 2); }
void psx_mod_write_word(uint32_t a, uint32_t v) { memcpy(s_ram + off(a), &v, 4); }
/* ---- mock mod / pass APIs (render_pass_frame.c) --------------------------- */
static char s_option[16] = "display";
static uint32_t s_fps = 99, s_source = 99, s_blend = 99, s_flip = 99, s_blend_calls;
static uint32_t s_status, s_phases, s_period, s_shown, s_passes_run;
int psx_mod_option_value(const char* p, const char* f, const char* o, char* out, uint32_t n) {
(void)p; (void)f; (void)o;
snprintf(out, n, "%s", s_option);
return 1;
}
int psx_mod_set_frame_interpolation(uint32_t fps) { s_fps = fps; return 1; }
int psx_mod_set_frame_interpolation_source(uint32_t s) { s_source = s; return 1; }
int psx_mod_set_frame_interpolation_blend(uint32_t b) { s_blend = b; s_blend_calls++; return 1; }
int psx_mod_set_render_pass_flip(uint32_t m) { s_flip = m; return 1; }
uint32_t psx_mod_render_pass_status(void) { return s_status; }
uint32_t psx_mod_render_pass_plan(uint32_t period, uint32_t shown, uint32_t* a, uint32_t max) {
s_period = period; s_shown = shown;
if (s_status != PSX_MOD_RENDER_PASS_READY) return 0;
for (uint32_t i = 0; i < s_phases && i < max; i++) a[i] = (i + 1) * 65536u / (s_phases + 1);
return s_phases < max ? s_phases : max;
}
int psx_mod_render_pass(struct CPUState* cpu, const PSXModRenderPass* p,
PSXModRenderPassFn fn, void* user) {
s_passes_run++;
return fn(cpu, user, p->alpha_q16);
}
/* ---- helpers --------------------------------------------------------------- */
static void put_matrix(uint32_t a, double yaw, int32_t x, int32_t y, int32_t z) {
PSXMotionMatrix m;
const double c = cos(yaw), s = sin(yaw);
const double r[9] = {c, 0, s, 0, 1, 0, -s, 0, c};
for (int i = 0; i < 9; i++) m.r[i] = (int16_t)lround(r[i] * 4096.0);
m.t[0] = x; m.t[1] = y; m.t[2] = z;
psx_motion_write_matrix(a, &m, 1);
}
static double yaw_of(uint32_t a) {
PSXMotionMatrix m;
psx_motion_read_matrix(a, &m);
return atan2(m.r[2] / 4096.0, m.r[0] / 4096.0);
}
static void test_matrix_slerp(void) {
PSXMotionSet* s = psx_motion_set_create(16);
PSXMotionLimits lim = {1000.0, 0};
PSXMotionStats st;
PSXMotionMatrix m;
put_matrix(0x100, 0.0, 0, 0, 0);
psx_motion_begin(s, 10);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x100, 7);
CHECK(psx_motion_prepare(s, &lim, &st) == 0 && st.ticks == 0, "first frame has no history");
put_matrix(0x100, 1.2, 800, 0, -400);
psx_motion_begin(s, 12);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x100, 7);
CHECK(psx_motion_prepare(s, &lim, &st) == 1 && st.ticks == 2 && st.blended == 1,
"the same object two ticks later blends");
psx_motion_apply(s, 0.5);
psx_motion_read_matrix(0x100, &m);
CHECK(fabs(yaw_of(0x100) - 0.6) < 0.002, "rotation slerps to the half-way angle");
CHECK(m.t[0] == 400 && m.t[2] == -200, "translation blends linearly");
{
double len = 0;
for (int j = 0; j < 3; j++) len += (double)m.r[j] * m.r[j];
CHECK(fabs(sqrt(len) - 4096.0) < 3.0, "the blended rotation stays a rotation");
}
CHECK(psx_motion_blend_at(s, PSX_MOTION_MATRIX, 0x100, 1.0, &m) == 1 &&
m.t[0] == 800, "blend_at at t=1 is the current value");
psx_motion_set_destroy(s);
}
static void test_identity_and_limits(void) {
PSXMotionSet* s = psx_motion_set_create(16);
PSXMotionLimits lim = {1000.0, 1.0};
PSXMotionStats st;
int32_t v[3];
put_matrix(0x200, 0.0, 0, 0, 0); /* moves within limits */
put_matrix(0x300, 0.0, 0, 0, 0); /* reused by another object */
put_matrix(0x400, 0.0, 0, 0, 0); /* teleports */
put_matrix(0x500, 0.0, 0, 0, 0); /* spins past the turn limit */
psx_motion_begin(s, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x200, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x300, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x400, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x500, 1);
psx_motion_prepare(s, &lim, &st);
put_matrix(0x200, 0.0, 1500, 0, 0);
put_matrix(0x300, 0.0, 10, 0, 0);
put_matrix(0x400, 0.0, 5000, 0, 0);
put_matrix(0x500, 2.0, 0, 0, 0);
psx_motion_begin(s, 3); /* two ticks: 2000 units allowed */
psx_motion_track(s, PSX_MOTION_MATRIX, 0x200, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x300, 2);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x400, 1);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x500, 1);
psx_motion_track(s, PSX_MOTION_VECTOR, 0x600, 1); /* new this frame */
CHECK(psx_motion_prepare(s, &lim, &st) == 1, "only the moving object blends");
CHECK(st.placed == 2 && st.unmatched == 2 && st.tracked == 5,
"teleport and spin are placed; reuse and new are unmatched");
psx_motion_apply(s, 0.5);
CHECK((int32_t)psx_mod_read_word(0x300 + 0x14) == 10, "a reused address keeps its new object");
CHECK((int32_t)psx_mod_read_word(0x400 + 0x14) == 5000, "a teleport is drawn where it landed");
CHECK((int32_t)psx_mod_read_word(0x200 + 0x14) == 750, "the mover is half-way");
CHECK(psx_motion_blend_at(s, PSX_MOTION_VECTOR, 0x600, 0.5, v) == 0, "no history: current value");
CHECK(psx_motion_blend_at(s, PSX_MOTION_VECTOR, 0x700, 0.5, v) == -1, "untracked: refused");
/* A gap (a frame not captured) or too many ticks: nothing blends. */
psx_motion_begin(s, 20);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x200, 1);
CHECK(psx_motion_prepare(s, &lim, &st) == 0 && st.ticks == 0, "17 ticks apart is not one motion");
psx_motion_invalidate(s);
psx_motion_begin(s, 21);
psx_motion_track(s, PSX_MOTION_MATRIX, 0x200, 1);
CHECK(psx_motion_prepare(s, &lim, &st) == 0, "after invalidate there is no history");
psx_motion_set_destroy(s);
}
static void test_angles_vectors_scalars(void) {
PSXMotionSet* s = psx_motion_set_create(16);
PSXMotionLimits lim = {100.0, 0};
PSXMotionStats st;
psx_mod_write_half(0x800, 4000); psx_mod_write_half(0x802, 100); psx_mod_write_half(0x804, 0);
psx_mod_write_half(0x810, (uint16_t)-50); psx_mod_write_half(0x812, 0); psx_mod_write_half(0x814, 0);
psx_mod_write_word(0x820, 1000);
psx_motion_begin(s, 5);
psx_motion_track(s, PSX_MOTION_ANGLES, 0x800, 1);
psx_motion_track(s, PSX_MOTION_SVECTOR, 0x810, 1);
psx_motion_track(s, PSX_MOTION_SCALAR, 0x820, 1);
psx_motion_prepare(s, &lim, &st);
psx_mod_write_half(0x800, 96); psx_mod_write_half(0x802, 300); psx_mod_write_half(0x804, 2048);
psx_mod_write_half(0x810, 50);
psx_mod_write_word(0x820, 1100);
psx_motion_begin(s, 6);
psx_motion_track(s, PSX_MOTION_ANGLES, 0x800, 1);
psx_motion_track(s, PSX_MOTION_SVECTOR, 0x810, 1);
psx_motion_track(s, PSX_MOTION_SCALAR, 0x820, 1);
CHECK(psx_motion_prepare(s, &lim, &st) == 3, "angles, svector and scalar blend");
psx_motion_apply(s, 0.5);
CHECK(psx_mod_read_half(0x800) == 0, "4000 -> 96 crosses 4096 the short way (0)");
CHECK(psx_mod_read_half(0x802) == 200, "100 -> 300 is 200");
CHECK(psx_mod_read_half(0x804) == 1024, "0 -> 2048 half turn blends to 1024");
CHECK((int16_t)psx_mod_read_half(0x810) == 0, "signed svector -50 -> 50 is 0");
CHECK(psx_mod_read_word(0x820) == 1050, "scalar blends");
psx_motion_set_destroy(s);
}
static void test_rotation_only(void) {
PSXMotionSet* s = psx_motion_set_create(4);
PSXMotionLimits lim = {100.0, 0};
PSXMotionStats st;
/* A bare rotation (as in a pose record) with other data right after it. */
put_matrix(0xA00, 0.0, 0, 0, 0);
psx_mod_write_word(0xA00 + 0x12, 0xDEADBEEFu);
psx_motion_begin(s, 1);
psx_motion_track(s, PSX_MOTION_ROTATION, 0xA00, 3);
psx_motion_prepare(s, &lim, &st);
put_matrix(0xA00, 1.0, 0, 0, 0);
psx_mod_write_word(0xA00 + 0x12, 0xDEADBEEFu);
psx_motion_begin(s, 2);
psx_motion_track(s, PSX_MOTION_ROTATION, 0xA00, 3);
CHECK(psx_motion_prepare(s, &lim, &st) == 1, "a bare rotation blends");
psx_motion_apply(s, 0.5);
CHECK(fabs(yaw_of(0xA00) - 0.5) < 0.002, "rotation-only slerps");
CHECK(psx_mod_read_word(0xA00 + 0x12) == 0xDEADBEEFu, "nothing past the 18 bytes is written");
psx_motion_set_destroy(s);
}
static int s_fn_calls;
static double s_fn_last;
static int frame_fn(struct CPUState* cpu, void* user, uint32_t alpha) {
(void)cpu; (void)user;
s_fn_calls++;
s_fn_last = alpha / 65536.0;
return 1;
}
static void test_frame_driver(void) {
PSXModRenderPassFrame f;
char cpu_storage[4096];
struct CPUState* cpu = (struct CPUState*)cpu_storage;
strcpy(s_option, "165");
CHECK(psx_mod_activate_render_pass_rate("p", "f", "rate", PSX_MOD_RENDER_PASS_FLIP_PENDING) &&
s_fps == 165 && s_source == PSX_MOD_FRAME_SOURCE_FLIP &&
s_blend == PSX_MOD_FRAME_INTERPOLATION_HOLD &&
s_flip == PSX_MOD_RENDER_PASS_FLIP_PENDING, "activation selects rate, FLIP, HOLD, flip mode");
strcpy(s_option, "display");
psx_mod_activate_render_pass_rate("p", "f", "rate", PSX_MOD_RENDER_PASS_FLIP_SHOWN);
CHECK(s_fps == 0 && s_flip == PSX_MOD_RENDER_PASS_FLIP_SHOWN, "display refresh is rate 0");
memset(&f, 0, sizeof f);
f.struct_size = sizeof f;
f.period_vblanks = 2; f.w = 320; f.h = 240;
s_status = PSX_MOD_RENDER_PASS_READY;
s_phases = 3;
s_blend_calls = 0;
CHECK(psx_mod_render_pass_frame(cpu, &f, frame_fn, NULL) == 3 && s_fn_calls == 3 &&
s_period == 2 && s_fn_last > 0.7, "every planned phase runs");
CHECK(s_blend_calls == 0, "READY keeps HOLD without re-setting it");
s_status = PSX_MOD_RENDER_PASS_BACKEND;
CHECK(psx_mod_render_pass_frame(cpu, &f, frame_fn, NULL) == 0 &&
s_blend == PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE, "lasting refusal crossfades");
s_status = PSX_MOD_RENDER_PASS_SESSION;
psx_mod_render_pass_frame(cpu, &f, frame_fn, NULL);
CHECK(s_blend == PSX_MOD_FRAME_INTERPOLATION_HOLD, "a transient reason returns to HOLD");
f.struct_size = 4;
CHECK(psx_mod_render_pass_frame(cpu, &f, frame_fn, NULL) == 0, "a short struct is refused");
}
int main(void) {
test_matrix_slerp();
test_identity_and_limits();
test_angles_vectors_scalars();
test_rotation_only();
test_frame_driver();
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
+21
View File
@@ -149,6 +149,26 @@ static void test_gen_select_late(void) {
CHECK(!render_pass_gen_select(ph, 0, 0.5, &lo, &hi, &t), "no items, no pick");
}
/* Promotion: a generation for the next flip waits for its own rect; one for
* a frame already on screen waits for the flip after it, to another rect. */
static void test_gen_flip_matches(void) {
/* PsyQ VSync-then-PutDispEnv: built for the rect the flip shows. */
CHECK(render_pass_gen_flip_matches(0, 0, 240, 1, 320, 240, 0, 240, 1, 320, 240),
"pending: the flip to the generation's rect promotes it");
CHECK(!render_pass_gen_flip_matches(0, 0, 240, 1, 320, 240, 0, 0, 1, 320, 240),
"pending: a flip to the other buffer does not");
/* Flip-when-drawn (V8:2): built for the rect already on screen. */
CHECK(render_pass_gen_flip_matches(1, 0, 240, 1, 320, 240, 0, 0, 1, 320, 240),
"shown: the next flip, to the other buffer, promotes it");
CHECK(!render_pass_gen_flip_matches(1, 0, 240, 1, 320, 240, 0, 240, 1, 320, 240),
"shown: a redraw of the rect on screen is not the next flip");
/* Geometry the images were captured at must still be presented. */
CHECK(!render_pass_gen_flip_matches(1, 0, 240, 1, 320, 240, 0, 0, 0, 320, 240),
"a different presented source never promotes");
CHECK(!render_pass_gen_flip_matches(0, 0, 0, 1, 320, 240, 0, 0, 1, 640, 480),
"a different presented size never promotes");
}
static void test_budget_and_ema(void) {
CHECK(fabs(render_pass_budget(0, 0, 100.0, 0.5) - 50.0) < 1e-9,
"no history spends the share of the frame");
@@ -301,6 +321,7 @@ int main(void) {
test_shedding();
test_select();
test_gen_select_late();
test_gen_flip_matches();
test_budget_and_ema();
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
@@ -0,0 +1,86 @@
#include "cpu_state.h"
#include "render_pass_projection.h"
#include <cmath>
#include <cstdio>
extern "C" {
int g_psx_render_pass_active=0;
int (*g_psx_projection_command)(CPUState*,uint32_t)=nullptr;
uint16_t psx_mod_read_half(uint32_t) { return 0; }
uint32_t psx_mod_read_word(uint32_t) { return 0; }
void psx_mod_write_half(uint32_t,uint16_t) {}
void psx_mod_write_word(uint32_t,uint32_t) {}
}
static int failures=0;
#define CHECK(v) do { if(!(v)) { std::printf("FAIL line %d: %s\n",__LINE__,#v); ++failures; } } while(0)
static CPUState pose(int x,int vertex=7,double yaw=0) {
CPUState c{}; c.gpr[31]=0x80012340;
int16_t r[9]={(int16_t)std::lround(4096*std::cos(yaw)),0,(int16_t)std::lround(4096*std::sin(yaw)),
0,4096,0,(int16_t)std::lround(-4096*std::sin(yaw)),0,(int16_t)std::lround(4096*std::cos(yaw))};
for(int i=0;i<9;++i) c.gte_ctrl[i/2]|=uint32_t(uint16_t(r[i]))<<(16*(i%2));
c.gte_ctrl[5]=x; c.gte_ctrl[7]=1000;
c.gte_data[0]=vertex; c.gte_data[1]=10;
return c;
}
static void capture(PSXProjectionHistory* h,CPUState* list,int count,uint32_t ticks=2) {
g_psx_render_pass_active=0;
psx_projection_capture_begin(h);
for(int i=0;i<count;++i) CHECK(!psx_projection_command(list+i,1));
psx_projection_capture_end(h,ticks,512);
}
static void test_matching() {
auto* h=psx_projection_create(16);
CPUState first[]={pose(0),pose(1000),pose(0,9)};
capture(h,first,3);
PSXProjectionStats st{}; psx_projection_stats(h,&st);
CHECK(st.captured==3 && st.matched==0);
// Same geometry on two objects, reversed order; one disappears and a new
// vertex enters. A shifted ordinal must not reuse the disappeared object.
CPUState current[]={pose(1100),pose(100),pose(50,11)};
capture(h,current,3); psx_projection_stats(h,&st);
CHECK(st.matched==2 && st.changed==2 && st.unmatched==1);
g_psx_render_pass_active=1; psx_projection_replay_begin(h,32768);
CPUState c=current[1]; CHECK(psx_projection_command(&c,1)); CHECK(c.gte_ctrl[5]==50);
c=current[0]; CHECK(psx_projection_command(&c,1)); CHECK(c.gte_ctrl[5]==1050);
c=current[2]; CHECK(!psx_projection_command(&c,1)); CHECK(c.gte_ctrl[5]==50);
c=current[0]; CHECK(!psx_projection_command(&c,1)); // each occurrence once
psx_projection_replay_end(h); CHECK(!g_psx_projection_command);
psx_projection_destroy(h);
}
static void test_cuts_and_rotation() {
auto* h=psx_projection_create(16);
CPUState a[]={pose(0,1),pose(0,2),pose(0,3)};
CPUState b[]={pose(2048,1),pose(0,2,2.0),pose(100,3,.6)};
capture(h,a,3); capture(h,b,3);
PSXProjectionStats st{}; psx_projection_stats(h,&st);
CHECK(st.discontinuities==2 && st.matched==1);
g_psx_render_pass_active=1; psx_projection_replay_begin(h,32768);
CPUState c=b[0]; CHECK(!psx_projection_command(&c,1)); CHECK(c.gte_ctrl[5]==2048);
c=b[1]; CHECK(!psx_projection_command(&c,1));
c=b[2]; CHECK(psx_projection_command(&c,1)); CHECK(c.gte_ctrl[5]==50);
CHECK(std::abs(std::atan2(int16_t(c.gte_ctrl[1]),int16_t(c.gte_ctrl[0]))-.3)<.002);
// If an abort unwinds the callback, a later architectural GTE command is
// never interpolated using the abandoned pass's active history.
g_psx_render_pass_active=0; c=b[2]; CHECK(!psx_projection_command(&c,1));
CHECK(!g_psx_projection_command);
psx_projection_destroy(h);
}
static void test_overflow_and_reset() {
auto* h=psx_projection_create(1);
CPUState a[]={pose(0),pose(10)};
capture(h,a,2);
PSXProjectionStats st{}; psx_projection_stats(h,&st); CHECK(st.overflow==1);
capture(h,a,1); psx_projection_stats(h,&st); CHECK(!st.matched);
capture(h,a,1); psx_projection_stats(h,&st); CHECK(st.matched==1);
// Savestate loads occur between frames, with no active capture/replay.
psx_projection_reset_session();
capture(h,a,1); psx_projection_stats(h,&st); CHECK(!st.matched);
psx_projection_capture_begin(h); psx_projection_reset_session();
CHECK(!g_psx_projection_command);
psx_projection_stats(h,&st); CHECK(!st.captured);
psx_projection_destroy(h);
}
int main() {
test_matching(); test_cuts_and_rotation(); test_overflow_and_reset();
std::printf("projection history failures=%d\n",failures); return failures?1:0;
}
+37
View File
@@ -176,8 +176,21 @@ static DirtyRamLoadDelay s_ld;
void dirty_ram_ld_delay_discard(void) { memset(&s_ld, 0, sizeof s_ld); }
void dirty_ram_ld_delay_save(DirtyRamLoadDelay *o) { *o = s_ld; }
void dirty_ram_ld_delay_restore(const DirtyRamLoadDelay *i) { s_ld = *i; }
static uint32_t s_span_lo, s_span_hi;
void dirty_ram_span_get(uint32_t *lo, uint32_t *hi) { *lo = s_span_lo; *hi = s_span_hi; }
void dirty_ram_span_set(uint32_t lo, uint32_t hi) { s_span_lo = lo; s_span_hi = hi; }
int dirty_ram_run_span(CPUState *cpu, uint32_t start, uint32_t stop, uint64_t max) {
(void)cpu; (void)start; (void)stop; (void)max;
return 0;
}
void overlay_loader_native_nesting(int *d, uint32_t *ip) { *d = 0; *ip = 0; }
void overlay_loader_set_native_nesting(int d, uint32_t ip) { (void)d; (void)ip; }
/* memory.c mod arenas (render_pass_mod_store): nothing journaled here. */
void render_pass_mod_arenas_rollback(void) {}
uint64_t render_pass_mod_arenas_hash(void) { return 0; }
static int s_prec_open;
void gte_precision_checkpoint_begin(void) { s_prec_open++; }
void gte_precision_checkpoint_rollback(void) { s_prec_open--; }
/* GPU and presenter. */
uint64_t gpu_pass_state_hash(void) { return 42; }
@@ -195,6 +208,7 @@ uint32_t gl_renderer_pass_plan(uint32_t p, uint32_t s, uint32_t *a,
return 0;
}
static int s_open_passes, s_kept;
void gl_renderer_pass_set_flip_shown(int shown) { (void)shown; }
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
uint32_t period, int reuse_backup) {
(void)x; (void)y; (void)w; (void)h; (void)open_gen; (void)period;
@@ -558,11 +572,34 @@ static void test_journal(void) {
#undef POLICY
}
static void test_texture_stream_journal(void) {
RenderPassJournal j = {0};
memcpy(s_vram, s_vram0, sizeof s_vram);
/* The Jersey draw OT alternates animated texture strips and CLUT rows.
* All 32 destinations must be backed up before the replay writes them. */
for (int i = 0; i < 32; ++i) {
int x = (i / 2) * 16, y = (i & 1) ? 492 : 64;
int w = (i & 1) ? 16 : 4, h = (i & 1) ? 1 : 64;
CHECK(render_pass_vram_policy(&j, 512, 0, 512, 240, VW, VH,
1, &x, &y, &w, &h) == RENDER_PASS_VRAM_JOURNAL,
"texture stream fits the bounded journal");
CHECK(render_pass_journal_add(&j, s_vram, VW, x, y, w, h) == i,
"texture destination backed up");
paint(x, y, w, h, (uint16_t)(0x9000 + i));
}
render_pass_journal_rollback(&j, s_vram, VW);
CHECK(!memcmp(s_vram, s_vram0, sizeof s_vram),
"texture stream and palettes restored exactly");
render_pass_journal_free(&j);
}
int main(void) {
test_store_policy();
test_pass();
test_ram_8mb();
test_journal();
test_texture_stream_journal();
CHECK(s_prec_open == 0, "precision checkpoints balanced");
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
@@ -17,6 +17,7 @@
#include <string.h>
#define GPU_EXEC_REAL_UI_GROUP
#define GPU_EXEC_RECORD_SHADED_TEXTURED
#include "../src/gpu.c"
#include "gpu_exec_stubs.inc"
@@ -152,9 +153,59 @@ static void run_fills(int *gouraud_min, int *gouraud_max,
*flat_max = gpu_exec_triangles.max_x;
}
/* Captured Ape intro GT4: world rank 3105, then a full-screen fade at 4095.
* Relative ranks suffice here. The wall becomes exactly rectangular for a
* few frames, but it must never become UI just because the fade is excluded. */
static void test_wall_behind_front_layer(int triangle) {
reset_state(0);
hres1 = 0; hres2 = 1; /* Ape's 384x240 display */
ws_cfg_num = 16; ws_cfg_den = 9; ws_xnum = 3; ws_xden = 4;
memset(test_ram, 0, sizeof(test_ram));
const uint32_t wall[12] = {
0x3C00FEFEu, 0x001C00C4u, 0x3ED8593Fu,
0x0000FEFEu, 0x004100C4u, 0x0089653Fu,
0x003FBEBEu, 0x001C008Bu, 0x006F591Fu,
0x003FBEBEu, 0x0041008Bu, 0x0042651Fu,
};
const uint32_t fade[3] = {
0x63000000u, pack_vertex(0, 0), 0x00F00180u,
};
const uint32_t front_triangle[4] = {
0x20808080u, pack_vertex(1, 1), pack_vertex(8, 1), pack_vertex(1, 8),
};
test_ram[OT_HEAD / 4u] = NODE_RING;
put_node(NODE_RING, NODE_GAP, wall, 12);
test_ram[NODE_GAP / 4u] = NODE_FILL;
put_node(NODE_FILL, NODE_FLAT, triangle ? front_triangle : fade,
triangle ? 4 : 3);
test_ram[NODE_FLAT / 4u] = 0xFFFFFFu; /* trailing empty rank */
gpu_ws_prepass_linked_list(OT_HEAD);
assert(ws_ui_prepass_count == 0);
gpu_exec_reset_triangles();
load_packet(NODE_RING, 12);
gp0_exec_shaded_textured_quad();
assert(gpu_exec_triangles.calls == 2);
assert(gpu_exec_triangles.min_x == 139);
assert(gpu_exec_triangles.max_x == 196);
assert(ws_auto_ui_transform_count == 0);
}
int main(void) {
int gmin, gmax, fmin, fmax;
test_wall_behind_front_layer(0);
test_wall_behind_front_layer(1);
/* A real HUD remains eligible when its last drawing layer is followed
* by an empty OT bucket, as in Ape's memory-card menu. */
reset_state(1);
build_hud(60, 128, 64, 120);
test_ram[NODE_FLAT / 4u] = (5u << 24) | NODE_PANEL;
test_ram[NODE_PANEL / 4u] = 0xFFFFFFu;
run_fills(&gmin, &gmax, &fmin, &fmax);
assert(gmin == ws_scale_about(64, 94));
assert(gmax == ws_scale_about(120, 94));
/* in_place: the fill and its frame share the run's own centre. */
reset_state(1);
build_hud(60, 128, 64, 120);
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Measure a running debug build's cadence, redraws and execution phases.
Use the same scene/settings for comparisons. Phase samples measure host wall
time (including waits), not the percentage of guest instructions compiled.
This tool neither changes settings nor supplies input to the game.
"""
import argparse
import json
import math
from pathlib import Path
import time
from debug_client import query
def measure(host, port, seconds, counter_prefix):
def ask(cmd, **values):
result = query(host, port, dict(cmd=cmd, **values), timeout=30)
if not result.get('ok'):
raise RuntimeError(f'{cmd}: {result.get("err", "request failed")}')
return result
def sample():
return {cmd: ask(cmd) for cmd in ('get_registers', 'mod_counters', 'render_pass_stats')}
def frame_count(sample):
return next((entry['count'] for entry in sample['mod_counters']['counters']
if entry['name'] == counter_prefix + '.frames'), None)
before = sample()
start = time.monotonic()
time.sleep(seconds)
after = sample()
elapsed = time.monotonic() - start
guest_hz = (after['get_registers']['frame'] - before['get_registers']['frame']) / elapsed
redraw_hz = (after['render_pass_stats']['passes'] - before['render_pass_stats']['passes']) / elapsed
native_before, native_after = frame_count(before), frame_count(after)
return dict(
elapsed_seconds=elapsed, guest_vblanks_per_second=guest_hz,
extra_geometry_renders_per_second=redraw_hz,
original_draws_per_second=(native_after-native_before)/elapsed
if native_after is not None and native_before is not None else None,
game_phase=ask('turbo_loads'), video=ask('video_info'),
execution_phase_samples=ask('phase_profile', window=min(60, math.ceil(seconds))),
timing=ask('frame_perf'), presenter=ask('gl_interp'), before=before, after=after,
method='Host wall-time phase samples and event-counter deltas; neither instruction coverage nor unique displayed FPS.')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--host', default='127.0.0.1')
parser.add_argument('--port', type=int, default=4370)
parser.add_argument('--seconds', type=float, default=10)
parser.add_argument('--counter-prefix', default='', help='For example medievil.fr or medievil2.fr')
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
if not math.isfinite(args.seconds) or not 1 <= args.seconds <= 60:
parser.error('--seconds must be between 1 and 60')
result = measure(args.host, args.port, args.seconds, args.counter_prefix)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, indent=2)+'\n', encoding='utf-8')
print(f'Guest VBlanks: {result["guest_vblanks_per_second"]:.2f}/s; '
f'extra geometry renders: {result["extra_geometry_renders_per_second"]:.2f}/s')
print(f'Evidence: {args.output}')
if __name__ == '__main__':
main()