mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Add guarded native draw replay, motion interpolation and stable world filtering
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`):
|
||||
|
||||
@@ -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.
|
||||
@@ -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 @@
|
||||
ae1e806fb3be31443f462df2f81314fb3288ee16bfd212bdafdbb2a03820497b
|
||||
8b10cee63c750fee313839c132742e52441938a70c5736576c140383240036df
|
||||
|
||||
@@ -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 @@
|
||||
530a85d6f2c9c6ab3927f8071f9494799755017fd44f0f2fdb8f1cddc1a86833
|
||||
3e8f77cfe0b52d865e06c2375cac130df6e7a50dc3516fae30c945c44a9b2341
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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, >e);
|
||||
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
@@ -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. */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'):]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user