mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Narrow render-pass PR to HD and local-view transaction
Merge current upstream and retain only the focused HD permission, local-view scope, and transaction qualification changes. Independent replay, scheduling, PGXP, and tracing work belongs in separate topics. Co-Authored-By: GPT-6.1-Sol <noreply@openai.com>
This commit is contained in:
@@ -538,6 +538,22 @@ frames. Trusted callbacks receive only the narrow C services exposed by
|
||||
`runtime/include/mod_plugins.h`. Games should continue to use declarative
|
||||
patches and overlays when those operations are sufficient.
|
||||
|
||||
An implementation may also register **function overrides** under a plugin id
|
||||
(`psx_mod_register_function_override`): hand-written C that replaces or wraps
|
||||
a guest function at a given address, with an optional prologue-word residency
|
||||
guard. Registration only queues the override; it is ARMED into the dispatcher
|
||||
tier when the resolved plan selects that plugin — the same gating as the other
|
||||
callback kinds, so an override-only plugin id counts as available to the
|
||||
resolver. Every registration states a required guest-cycle `credit` (a fixed
|
||||
per-handled-call charge, `0` for a mod with no hardware analog, or
|
||||
`FO_CREDIT_SELF` when the body — or a wrapped original — accounts for its own
|
||||
time). The full execution contract (guest ABI, decline semantics,
|
||||
`func_override_call_original` wrap primitive, `func_override_guest_call`,
|
||||
determinism and cycle-accounting policy) is documented in
|
||||
`runtime/include/func_override.h`. Overrides registered directly through
|
||||
`func_override_add` (game `EXTRAS_SOURCES` constructors, the progressive-
|
||||
decompilation idiom) bypass package gating and are always active; packages
|
||||
are the right home for anything a player should be able to toggle.
|
||||
### Session starts and mod-owned state
|
||||
|
||||
What a plugin sets up should last only for a session whose resolved plan
|
||||
@@ -1166,3 +1182,10 @@ existing post-commit path, not the preparation worker.
|
||||
The `prepare_resources` method is separate from read-only `resolve`, so editing
|
||||
launcher settings does not trigger conversions. Runtime activation still sees
|
||||
only fully verified immutable resources.
|
||||
|
||||
Function-override package arming is transactional and runs before activation
|
||||
callbacks. Address collisions or capacity failures reject the launch through
|
||||
`mod_runtime_activation_error()` and remove all package entries, retaining
|
||||
always-on direct implementations. Every session rearms its selected plan.
|
||||
Replacement overrides cannot be selected as netplay local-view plugins: they
|
||||
change shared execution rather than only sandboxed presentation.
|
||||
|
||||
@@ -92,6 +92,19 @@ Bloom uses the mask at both ends: only lit pixels feed its bright pass, and
|
||||
only lit pixels take the glow back. HUD pixels are therefore never changed,
|
||||
even next to a lamp.
|
||||
|
||||
## Depth, history, time and live uniforms
|
||||
|
||||
- `input <sampler> depth`: view depth `sz` (GTE screen z) of the presented frame, R32F at window
|
||||
size, rebuilt from the surfaces' PGXP depth (`zn = 1 - 512/(sz + 256)`); 0 where nothing 3D drew.
|
||||
Only built while a loaded chain reads it.
|
||||
- `input <sampler> history`: this pass's own target from the last real frame (trails, temporal
|
||||
filters), including the final window pass. Generated frames do not update it. History starts empty after loading or resizing.
|
||||
- `u_time` (host seconds since the chain first drew, reset on reload) and `u_proj` (latest GTE projection: `240/H`, screen
|
||||
centre in uv, aspect) are declared in every pass.
|
||||
- Live uniforms: a title sets a preset `uniform` from game state with
|
||||
`psx_host_set_post_uniform(name, x, y, z, w)` (`host_overlay.h`), e.g. a light source projected
|
||||
to the screen; the preset value is the default until the first call.
|
||||
|
||||
## Tests
|
||||
|
||||
- `postfx_chain_test`: the format, built-in presets, errors and file import.
|
||||
@@ -102,3 +115,7 @@ even next to a lamp.
|
||||
thread and on the render thread.
|
||||
- `gl_post_aa_test`: FXAA on the pipeline. Its output is byte-identical to
|
||||
the former fixed pass.
|
||||
|
||||
The projection is a coherent latest CPU value. It is not associated with a
|
||||
queued frame or an individual replay view; shaders requiring an exact camera
|
||||
for a delayed or stereo image need corresponding frame/view metadata.
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# Production input replay
|
||||
|
||||
`PSX_INPUT_REPLAY=/absolute/path/route.txt` supplies recorded active-low digital
|
||||
pad words in every build, including `PSX_DEBUG_TOOLS=OFF`. Each non-comment
|
||||
line is `frame_count hexadecimal_buttons`, for example `60 bfff`. Counts must
|
||||
be positive; pad words must fit 16 bits. The route starts at the first VBlank
|
||||
after a successful disk savestate load, advances by guest frames, and returns
|
||||
neutral input after its end. A failed requested state load is fatal. Netplay
|
||||
and file replay cannot run together.
|
||||
|
||||
Use the exact state paired with the recording. This is a host input source;
|
||||
it does not bypass savestate compatibility checks or change guest timing.
|
||||
|
||||
Optional bounded benchmark controls:
|
||||
|
||||
- `PSX_INPUT_REPLAY_STOP=2700`: request normal shutdown ten frames after this
|
||||
zero-based route offset.
|
||||
- `PSX_INPUT_REPLAY_SECONDS=60`: instead of STOP, run a wall-time window of
|
||||
1..60 seconds beginning at route offset 900. Stop at the first guest frame
|
||||
after its deadline, then request shutdown ten frames later.
|
||||
- `PSX_INPUT_REPLAY_REPORT=/absolute/path/frames.csv`: export frame stamps and
|
||||
cumulative guest, pacer and render-queue times at shutdown. Enable existing
|
||||
`PSX_RUNTIME_PERF_DIAG=1` for guest/pacer counters. Times use microseconds;
|
||||
differences between adjacent rows describe the intervening guest interval.
|
||||
A companion `.spans.csv` exports the bounded host attribution ring.
|
||||
`.render.csv` exports GL work counts. With `PSX_GL_CPU_PERF=1` it also
|
||||
reports inclusive CPU wall spans (calls, total and maximum) for depth
|
||||
restoration, queue replay, VRAM copies, interpolation captures/intervals and
|
||||
centre splices. These spans overlap; do not sum them as exclusive costs.
|
||||
Collection is off by default and its queue drain/export happens after the
|
||||
measured route window. Use separate profiling and acceptance runs.
|
||||
`PSX_GL_CPU_PERF=2` additionally finishes queued GPU work before and after
|
||||
queue replay, VRAM copies, captures and centre splices. These spans include
|
||||
GPU completion; `_prior_drain` rows measure earlier queued work separately.
|
||||
This serializes the pipeline and is attribution only. Totals cover the
|
||||
whole replay, including warmup. `.rth.json` records queue waits and recent
|
||||
context-acquisition reasons before the report's own drain.
|
||||
`PSX_GL_CPU_PERF=3` instead samples one in 128 high-resolution textured
|
||||
depth batches: `colour_gpu_sample` and `depth_gpu_sample` finish GPU work
|
||||
around the same batch's colour and depth draws. Sampling perturbs cadence;
|
||||
these completion spans are not acceptance frame times.
|
||||
`PSX_GL_PERF=1` additionally enables GPU queries in production and exports
|
||||
rolling GPU aggregates to `.gpu.csv`; use this for separate attribution
|
||||
trials, since queries can perturb the measured workload.
|
||||
- `PSX_INPUT_REPLAY_SHOTS=/absolute/path/prefix`: request screenshots at route
|
||||
offsets 800 and STOP+2, outside the usual measured interval 900..2700.
|
||||
|
||||
The caller chooses new output paths and provides a process timeout. Reports
|
||||
measure guest VBlanks, not distinct rendered images or display scanout.
|
||||
@@ -196,18 +196,6 @@ example.
|
||||
|
||||
## Testing without a window
|
||||
|
||||
`render_pass_stats.timing` contains cumulative milliseconds and a transaction
|
||||
count. Subtract two receipts and divide each time delta by the count delta for
|
||||
window means; `avg_*` fields are exponential estimates, not window averages.
|
||||
`gl_begin_ms` includes GPU backup and optional VRAM verification readback;
|
||||
`checkpoint_ms` is the CPU/device checkpoint; `verify_before_ms` is the optional
|
||||
pre-draw state hash. `gl_end_ms` includes GPU flush, image capture, restore and
|
||||
optional VRAM readback; `restore_ms` restores the CPU/device checkpoint;
|
||||
`verify_after_ms` includes the post-restore state hash. All buckets include host
|
||||
waits and scheduling, so they are wall time, not GPU execution time. Measure
|
||||
production cost with `PSX_RENDER_PASS_VERIFY=0`, and test restoration separately
|
||||
with verification enabled. Counts include discarded and aborted transactions.
|
||||
|
||||
`--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
|
||||
|
||||
@@ -184,8 +184,8 @@ resolution (below), not of this layer.
|
||||
|---|---|
|
||||
| savestates / rewind | load and save are sync points; the frame stays synchronous, the next frame boundary releases |
|
||||
| netplay / rollback resimulation | `psx_netplay_active()` or dual-raster makes every frame ineligible: the emulation thread holds the context (synchronous path). Not started when netplay is configured at boot |
|
||||
| render passes | a transaction is a sync point; the rest of that frame is synchronous. An allocation-mode refusal needs no queue drain |
|
||||
| frame interpolation | eligible; temporal composition uses the renderer's existing context. The guest keeps its own deadline cap while the context is on the render thread |
|
||||
| render passes | first `gl_renderer_pass_*` call in a frame is a sync point; the rest of that frame is synchronous. Recommended: leave `render_thread` off with the frame-rate mod |
|
||||
| frame interpolation | ineligible (held) while enabled; not started when it is on at boot |
|
||||
| HD texture replacements / texture dumping | eligible; held only from a GP0(A0) header to its commit |
|
||||
| OpenXR | ineligible while a session is active |
|
||||
| Smooth motion (frame generation) | `[video] frame_generation`: in-between frames drawn by the render thread from recorded lists, docs/FRAME_GENERATION.md |
|
||||
@@ -405,13 +405,6 @@ means), `guest_bound`, the step counters and `up_blocked`.
|
||||
|
||||
## Debug server
|
||||
|
||||
Drawable pixel dimensions are queried on the window's owning thread and
|
||||
published as one atomic width/height pair before each frame handoff. Render
|
||||
and present workers read that pair; they do not call SDL3's main-thread-only
|
||||
`SDL_GetWindowSizeInPixels`. Synchronous rendering still refreshes directly.
|
||||
The hardware fixture verifies two resizes from a worker as well as image,
|
||||
guest-readback and stencil equivalence with temporal presentation enabled.
|
||||
|
||||
`{"cmd":"render_thread"}` — active, held, max_frames, records, bytes,
|
||||
frames produced/consumed, presents and `presents_stale` (skipped), acquires
|
||||
(sync points) with the 16 most recent reasons and wait times,
|
||||
|
||||
@@ -493,9 +493,9 @@ between Play and TCP availability.
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**386 commands registered** — 373 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**387 commands registered** — 374 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
85 of 386 have prose above; **301 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
85 of 387 have prose above; **302 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -630,6 +630,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `frame_trace` | ✓ | | |
|
||||
| `frame_wall_ring` | ✓ | | |
|
||||
| `freeze_check` | ✓ | | |
|
||||
| `func_override` | ✓ | | |
|
||||
| `game_options` | ✓ | | |
|
||||
| `gcp_ring` | ✓ | | ✓ |
|
||||
| `geom_correction` | ✓ | | ✓ |
|
||||
@@ -736,6 +737,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `pc_probe_arm` | ✓ | | |
|
||||
| `pc_probe_clear` | ✓ | | |
|
||||
| `pc_probe_dump` | ✓ | | |
|
||||
| `perf` | ✓ | | |
|
||||
| `pgxp` | ✓ | | ✓ |
|
||||
| `pgxp_miss_ring` | ✓ | | |
|
||||
| `pgxp_shadow` | ✓ | | |
|
||||
|
||||
+50
-3
@@ -299,13 +299,14 @@ are non-empty, so titles that do not use them keep their caches.
|
||||
|
||||
`screen_x_sites` is the explicit form of the `auto_screen_x` compare, for a
|
||||
title whose width is not in `screen_w_imms` or whose function has no paired
|
||||
height compare. It takes four instructions:
|
||||
height compare. It takes five instructions:
|
||||
|
||||
| Instruction at the site | While widened |
|
||||
|---|---|
|
||||
| `sltiu rt, SX, W` | `rt = -m <= SX < W + m` |
|
||||
| `lui rt, W` (a screen edge kept as `W << 16`) | `rt = (W + m) << 16`; a negative `W` moves left instead |
|
||||
| `bltz x, outside` where `x` is `SX << 16` | taken while `SX < -m` |
|
||||
| `slti rt, x, 0` where `x` is `SX << 16` | `rt = SX < -m` (the `bltz` test as a flag) |
|
||||
| `sltu rd, SX, W` with `W` in a register | `rd = -4m <= (int16)SX < W` |
|
||||
|
||||
The `sltu` form covers a renderer that compares a 16-bit screen X
|
||||
@@ -343,11 +344,57 @@ screen_x_sites = [
|
||||
`screen_x_sites` is applied when code is generated (native code and overlay
|
||||
shards) and, at runtime, by the dirty-RAM interpreter: code that runs from
|
||||
RAM through the interpreter (including under `PSX_FORCE_INTERP=1`) widens the
|
||||
same four instruction forms at the listed addresses, with the same helpers and
|
||||
same five instruction forms at the listed addresses, with the same helpers and
|
||||
the same identity at 4:3. A listed `bltz` follows the precedence `bltz_sites`,
|
||||
then `screen_x_sites`, then the `auto_screen_x` classifier. In the main EXE
|
||||
a listed address holding another non-branch instruction is a hard build error;
|
||||
a branch other than `bltz` at a listed address is left as it is.
|
||||
a branch other than `bltz` at a listed address is left as it is, and a
|
||||
`slti` with a nonzero immediate is a build error (it is not this form).
|
||||
|
||||
### Packed-SXY all-vertices reject (`[[widescreen.cull.sxy]]`)
|
||||
|
||||
Some renderers trivially reject a whole triangle or quad by ANDing the packed
|
||||
GTE `SXY` words of its vertices (Y in the high half, X in the low half) with a
|
||||
sign mask and branching on the result:
|
||||
|
||||
```
|
||||
and at, t0, s1 ; s1 = 0x80008000
|
||||
and at, s4, at
|
||||
and at, t3, at ; (and at, t1, at for a quad)
|
||||
bne at, zero, reject ; bit 31: all above, bit 15: all left of the screen
|
||||
```
|
||||
|
||||
`screen_x_sites`, `bltz_sites` and the packed-coordinate helpers test one
|
||||
value at a time and cannot widen this fold. Listing it widens only the left
|
||||
reject: each AND of the fold becomes `psx_ws_cull_sxy_and(acc, sxy)`, the
|
||||
vanilla AND except that bit 15 also needs this vertex's `X < -m`. Bit 31 (the
|
||||
vertical reject) and every other bit are the vanilla AND, the guest's own
|
||||
branch decides, and right/bottom rejects (a separate chain) are untouched.
|
||||
At 4:3 (`m = 0`) every AND produces the vanilla word.
|
||||
|
||||
```toml
|
||||
[[widescreen.cull.sxy]]
|
||||
kind = "tri" # or "quad" (4 vertices)
|
||||
vertex_regs = [8, 20, 11] # the SXY operand of each AND
|
||||
fold_addresses = ["0x8001A7F0", "0x8001A7F8", "0x8001A800"]
|
||||
fold_expected = ["0x01110824", "0x02810824", "0x01610824"]
|
||||
consumer_address = "0x8001A808"
|
||||
consumer_expected = "0x14200034" # bne at, zero, reject
|
||||
```
|
||||
|
||||
The loader derives the shape from the words and refuses anything else: one
|
||||
AND per vertex, in increasing address order within 16 instructions, all into
|
||||
one register `r`; the first combines `vertex_regs[0]` with a separate mask
|
||||
register, each later one combines its vertex with `r`; the consumer is
|
||||
`beq`/`bne r, zero` within four instructions after the last AND. Every word
|
||||
of the fold and the consumer is part of the identity: the main-EXE build
|
||||
fails on any mismatch (checked against the image up front and again when the
|
||||
AND is emitted), and a captured overlay or interpreted variant with any other
|
||||
word in the fold is left vanilla. Native code, overlay shards and the
|
||||
dirty-RAM interpreter use the same helper and the same whole-fold check.
|
||||
Changing a site changes the overlay-cache identity; regenerate after a change.
|
||||
Wild 9 (SLUS-00425) is the first user: its triangle and quad rejects use this
|
||||
shape, and its shifted-X lower edge is the `slti` form of `screen_x_sites`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -262,6 +262,29 @@ result = 1
|
||||
semantics.
|
||||
- Regenerate main/overlay native code after changing the list.
|
||||
|
||||
### Packed-SXY all-vertices rejects
|
||||
|
||||
```toml
|
||||
[[widescreen.cull.sxy]]
|
||||
kind = "quad"
|
||||
vertex_regs = [8, 20, 11, 9]
|
||||
fold_addresses = ["0x8001A9B4", "0x8001A9B8", "0x8001A9BC", "0x8001A9C0"]
|
||||
fold_expected = ["0x01110824", "0x02810824", "0x01610824", "0x01210824"]
|
||||
consumer_address = "0x8001A9C4"
|
||||
consumer_expected = "0x14200049"
|
||||
```
|
||||
|
||||
- `kind` is `"tri"` (3 vertices) or `"quad"` (4); `vertex_regs`,
|
||||
`fold_addresses` and `fold_expected` have one entry per vertex.
|
||||
- Every `fold_expected` is `and r, ...` into the same `r != $zero`; the first
|
||||
reads `vertex_regs[0]` and a separate mask register, each later one reads
|
||||
its vertex and `r`. `consumer_expected` is `beq`/`bne r, zero`.
|
||||
- Addresses are word-aligned, in one segment, strictly increasing, the fold
|
||||
within 16 instructions and the consumer within 4 after it; no address is
|
||||
shared with another sxy site. At most 64 sites.
|
||||
- Identity at 4:3; while widened only the X-sign bit of each AND changes. See
|
||||
`docs/WIDESCREEN.md`.
|
||||
|
||||
Prefer an aspect-derived cone over `keep` when the original predicate is a
|
||||
camera-frustum test. `keep` has no queue policy and is appropriate only for a
|
||||
separately proven binary verdict.
|
||||
|
||||
@@ -213,58 +213,16 @@ on a fixed region -> next.
|
||||
|
||||
## 5. Status / Log (update every session)
|
||||
|
||||
- **2026-10-21 (queued vertex uploads):** Pack replay vertices at offsets
|
||||
aligned to each layout and upload once per used VBO per flush, reusing the
|
||||
data across canonical tiles and wide mirrors. Native/readback/hires/wide
|
||||
and stencil hashes match the per-batch upload control, 1x/4x, both allocation
|
||||
modes and renderer thread off/on. R4's 30-second Ultra probe improves to
|
||||
55.684 guest Hz / p99 busy 63.3%, still below the owner's acceptance bar.
|
||||
Broader bloom fusion passes the same matrix for mask-set and translucent
|
||||
sources; production timing is pending.
|
||||
|
||||
- **2026-10-21 (render coverage and driver waits):** Enabled temporal
|
||||
presentation on the existing renderer context/thread. Queued wide mirrors
|
||||
retain bloom classification; eligible opaque marking is fused with colour,
|
||||
colourless passes omit RGB-only filtering, and depth restoration derives
|
||||
the preceding draw's enable state instead of querying the threaded driver.
|
||||
Final-flush centre shading is elided only before the authoritative splice.
|
||||
Native/readback/hires/wide and both stencil-bit hashes match all independent
|
||||
controls at 1x/4x with threads off/on, native and forced-window hires.
|
||||
Default-Ultra production acceptance remains open; R4's slowdown report
|
||||
records failed route trials and profiling limitations. No guest time changes.
|
||||
|
||||
- **2026-10-21 (Windows sleep concurrency):** Concurrent guest/render pacers
|
||||
exposed a shared auto-reset waitable timer: one caller cancels/consumes the
|
||||
other's wake. Replaced it with FLS-owned timers and synchronized QPC setup.
|
||||
Eight-sleeper regression fails old source at 10 s and passes the fix in
|
||||
0.49 s, including timer-handle cleanup. No guest time model changed.
|
||||
|
||||
- **2026-10-21 (production R4 follow-up):** Removed debug-only PGXP receipt
|
||||
writes from no-debug-tools builds; numeric PGXP and word-shadow suites pass.
|
||||
Added disk-state-anchored applied-input replay and bounded frame/work/span
|
||||
exports for production measurements. Five focused framework CTests and 26
|
||||
R4 checks pass on Windows. The owner's stricter default-Ultra acceptance bar
|
||||
remains open; evidence is in R4 `analysis/render-60fps/SLOWDOWNS.md`. No guest
|
||||
timing or projection arithmetic changed.
|
||||
|
||||
- **2026-10-21 (Windows continuation, `feat/render-60fps`):** Added cumulative
|
||||
render-transaction wall-time buckets, separating checkpoint copying from
|
||||
verification hashing. After the owner's moving-course correction, R4
|
||||
same-state headless/turbo off/on: 2,701 guest fingerprints match (no temporal
|
||||
passes in headless). Recorded driving route, 30-second 60 Hz windows: 51
|
||||
normal passes average 11.060 ms (max 16.202); 17 verification passes average
|
||||
18.227 ms, zero restoration mismatches. Earlier straight-acceleration timing
|
||||
is excluded. Pass admission remains sparse; sustained 60 fps is unproven.
|
||||
Windows build, nine R4 CTests, sandbox and
|
||||
watchdog/abort tests pass. No faithful timing behavior changed.
|
||||
|
||||
- **2026-10-21 (`feat/render-60fps`):** Admit opt-in render passes with HD
|
||||
CPU-authoritative textures and give opted-in netplay temporal passes the
|
||||
restored own-view scope. Sandbox tests and hidden GL pass fixture pass.
|
||||
R4 delay-netplay: 120 common core digests identical with frame-rate on only
|
||||
on one peer; 14 temporal passes, no restore mismatches or dispatch misses.
|
||||
R4 performance/visual acceptance remains open; see its
|
||||
`analysis/render-60fps/REPORT.md`.
|
||||
- **2026-10-24 — scoped render-pass qualification.** The sandbox now compares
|
||||
shared RAM/register/clock/interrupt digests before and after an actual
|
||||
opted-in local temporal pass, discard, device-read abort, rollback refusal,
|
||||
and a nonparticipating peer. The HD GL fixture separately opens a pass under
|
||||
native authority. Consuming-title and matched-performance evidence remains
|
||||
required for upstream promotion.
|
||||
- **2026-10-24 — render-pass HD/local-view scope.** Re-scope the fork's
|
||||
permission change onto current master without replay controls or host tracing.
|
||||
Title-level positive HD/local-view digest and abort qualification remains
|
||||
required before upstream promotion.
|
||||
- **2026-10-23 — VR upstream split.** Precise projective replay reconstructed on
|
||||
current master; six focused runtime regressions and the GPU-history guard pass.
|
||||
Prior fork/headset acceptance remains historical. Other VR features are
|
||||
@@ -2044,30 +2002,3 @@ Guest-frame lookup merged multiple extra presentations and could not verify
|
||||
HUD stability. Added duplicate-frame/freeze/eviction regression coverage and
|
||||
retained legacy frame lookup. Six focused framework checks and all 21 title
|
||||
checks pass on Windows and Linux, and both runtime builds link successfully.
|
||||
|
||||
### 2026-10-21: Production indexed texture decode reuse
|
||||
|
||||
Cache native 1555 indexed-page/CLUT decode results with raw-VRAM tile
|
||||
generations and deferred-draw layer pinning. No filtering or guest behavior
|
||||
changes. Hardware fixtures compare five hashes with cache off and forced
|
||||
eviction, normal/HiW allocation, 1x/4x, render thread off/on; all pass.
|
||||
R4 recorded-route production probe improves 53.371 to 57.472 guest Hz;
|
||||
zero-gap acceptance remains open. Matched GPU completion probes also reject
|
||||
a separate depth shader (no improvement); that experiment was removed.
|
||||
See title analysis/render-60fps/SLOWDOWNS.md for receipts and limitations.
|
||||
|
||||
|
||||
### 2026-10-21: Bounded asynchronous presentation pacing
|
||||
|
||||
Publish drawable sizes from SDL3 pixel-size events instead of querying window
|
||||
properties per frame. Batch SPSC queue consumption/accounting on separated
|
||||
producer/consumer cache lines. For HiW hold output at guest rate, move deadline
|
||||
waits to the existing presenter; its bounded queue paces the guest without a
|
||||
second independent timer. Queued scheduling preserves early producer anchors.
|
||||
Guest device timing and quality settings are unchanged. Hardware image/mask,
|
||||
resize and scheduling controls pass. R4 Release default Ultra recorded drives
|
||||
cover 60 seconds at 60.000 guest Hz, p99 emulation busy 52.3% idle / 56.4% with
|
||||
23.61% total host CPU background load; zero presented gaps >25 ms, maxima
|
||||
17.286/17.567 ms. Internal callback/completion gaps are separately reported and
|
||||
are not claimed to pass that limit. See title SLOWDOWNS.md for attribution,
|
||||
receipts, and clock definitions. No release or main-branch landing.
|
||||
|
||||
@@ -1 +1 @@
|
||||
86f7329c2692e76133c40fa11cc8cfac214302d8238e05369dc8b263f4dc76fd
|
||||
d887bd825dee12170e8bf22577543114787a3c7f5f2c9bf5e62b14c990561564
|
||||
|
||||
@@ -36,6 +36,13 @@ 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);
|
||||
|
||||
/* Game function-override tier (runtime/include/func_override.h):
|
||||
* hand-written C registered against a guest address. NULL (the
|
||||
* default) = no overrides, dispatch identical to a build without
|
||||
* the tier. Returns 1 when the override handled the call; the
|
||||
* guest resumes at $ra exactly as if the original ran jr $ra. */
|
||||
extern int (*g_psx_func_override_hook)(CPUState* cpu, uint32_t phys);
|
||||
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
extern int psx_game_address_in_text(uint32_t addr);
|
||||
#endif
|
||||
@@ -9111,6 +9118,18 @@ static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr)
|
||||
/* Byte-guarded A0/B0/C0 call-vector tail stubs. */
|
||||
if (!found && psx_bios_try_native_call_stub(cpu, addr))
|
||||
found = 1;
|
||||
/* Function-override tier: AFTER the BIOS tiers (an override
|
||||
* can never shadow a kernel service vector), BEFORE every
|
||||
* game code backend, so one address-keyed hook covers the
|
||||
* static EXE, runtime-loaded overlays and dirty RAM alike.
|
||||
* Handled (rc 1) => the override completed against guest
|
||||
* state; resume at $ra via the trampoline's normal
|
||||
* return/tail contract. rc 0 => fall through untouched. */
|
||||
if (!found && g_psx_func_override_hook &&
|
||||
g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu)) {
|
||||
cpu->pc = cpu->gpr[31];
|
||||
found = 1;
|
||||
}
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
/* Game EXEs can overlap the BIOS shell copy window at
|
||||
* physical 0x30000-0x5AFFF. If the target belongs to the
|
||||
|
||||
@@ -1477,6 +1477,11 @@ if(BUILD_TESTING AND Python3_Interpreter_FOUND AND CMAKE_C_COMPILER_ID STREQUAL
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_syscall_change_thread_self.py"
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
# CloseTh(current); ChangeTh(other) still saves the outgoing registers.
|
||||
add_test(NAME syscall_closed_thread_context
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_syscall_closed_thread_context.py"
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
endif()
|
||||
|
||||
# Presentation capture ring: distinct-presentation dedupe and bounded slots.
|
||||
|
||||
@@ -177,6 +177,8 @@ struct CodeGenConfig {
|
||||
// Exact, full-word-guarded comparison sites whose result is forced only
|
||||
// while widescreen reveals extra world. 4:3 evaluates the original compare.
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep_sites;
|
||||
// [[widescreen.cull.sxy]] packed-SXY AND folds (left edge widened).
|
||||
std::vector<PSXRecompV4::WidescreenSxyCullSite> ws_cull_sxy_sites;
|
||||
std::vector<PSXRecompV4::WidescreenMaskedRejectSite> ws_cull_masked_reject_sites;
|
||||
std::vector<PSXRecompV4::WidescreenCullScaleSite> ws_cull_scale_sites;
|
||||
|
||||
|
||||
@@ -1143,6 +1143,55 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in
|
||||
site.y_reg, config_.ws_aspect_cone.y_reg)), comment);
|
||||
}
|
||||
|
||||
// [[widescreen.cull.sxy]]: one AND of a packed-SXY all-vertices reject
|
||||
// fold. Emitted only when every AND word of the fold and the consumer
|
||||
// branch word read back exactly from this image (in this instruction's
|
||||
// segment); the AND keeps its vanilla value except that the X-sign bit
|
||||
// also needs this vertex left of the revealed edge (identity at 4:3).
|
||||
// An overlay variant with other words anywhere in the fold is untouched.
|
||||
if (opcode == 0x00 && funct == 0x24 && !config_.ws_cull_sxy_sites.empty()) {
|
||||
for (const auto& site : config_.ws_cull_sxy_sites) {
|
||||
size_t step = site.vertices;
|
||||
for (size_t i = 0; i < site.vertices; i++)
|
||||
if ((site.fold_addresses[i] & 0x1FFFFFFFu) == (addr & 0x1FFFFFFFu))
|
||||
step = i;
|
||||
if (step == site.vertices) continue;
|
||||
const uint32_t segment = addr & ~0x1FFFFFFFu;
|
||||
uint32_t bad_addr = 0, bad_want = 0, bad_got = 0;
|
||||
bool ok = true;
|
||||
auto verify = [&](uint32_t site_addr, uint32_t want) {
|
||||
if (!ok) return;
|
||||
const uint32_t a = segment | (site_addr & 0x1FFFFFFFu);
|
||||
uint32_t got = ~want;
|
||||
if (a == addr) got = instr;
|
||||
else if (const auto w = exe_.read_word(a)) got = *w;
|
||||
if (got != want) {
|
||||
ok = false;
|
||||
bad_addr = a; bad_want = want; bad_got = got;
|
||||
}
|
||||
};
|
||||
for (size_t i = 0; i < site.vertices; i++)
|
||||
verify(site.fold_addresses[i], site.fold_expected[i]);
|
||||
verify(site.consumer_address, site.consumer_expected);
|
||||
if (!ok) {
|
||||
if (config_.overlay_mode) continue;
|
||||
fmt::print(stderr,
|
||||
"ERROR: [[widescreen.cull.sxy]] expected 0x{:08X} at "
|
||||
"0x{:08X}, found 0x{:08X}\n",
|
||||
bad_want, bad_addr, bad_got);
|
||||
std::exit(1);
|
||||
}
|
||||
const uint32_t vertex = site.vertex_regs[step];
|
||||
const uint32_t other =
|
||||
get_rs(instr) == vertex ? get_rt(instr) : get_rs(instr);
|
||||
return fmt::format(
|
||||
"{} = psx_ws_cull_sxy_and({}, {});"
|
||||
" /* ws SXY fold (left edge widened) */{}",
|
||||
reg_name(get_rd(instr)), reg_name(other), reg_name(vertex),
|
||||
comment);
|
||||
}
|
||||
}
|
||||
|
||||
// Full-word-guarded object/model participation compares. At 4:3 the
|
||||
// helper returns the comparison's vanilla value; when widescreen reveals
|
||||
// extra world it returns the configured keep verdict. Overlay variants
|
||||
@@ -1507,9 +1556,17 @@ std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t in
|
||||
return fmt::format("{} = psx_ws_cull_lui_hi(0x{:04X}u);"
|
||||
" /* ws explicit screen-x edge (high half) */{}",
|
||||
reg_name(rt), imm, comment);
|
||||
} else if (opcode == 0x0A && get_imm16_u(instr) == 0 && get_rt(instr) != 0) {
|
||||
// slti rt,x,0 on `SX << 16`: the bltz sign test as a flag. Set
|
||||
// only while SX < -margin, exactly like the bltz form.
|
||||
return fmt::format("{} = (uint32_t)psx_ws_cull_bltz_hi({});"
|
||||
" /* ws explicit screen-x left edge (high half) */{}",
|
||||
reg_name(get_rt(instr)), reg_name(get_rs(instr)),
|
||||
comment);
|
||||
} else if (!config_.overlay_mode) {
|
||||
fmt::print(stderr, "ERROR: [widescreen.cull] screen_x site 0x{:08X} "
|
||||
"is not sltiu, sltu, lui or bltz (0x{:08X})\n", addr, instr);
|
||||
"is not sltiu, sltu, lui, slti rt,rs,0 or bltz (0x{:08X})\n",
|
||||
addr, instr);
|
||||
std::exit(1);
|
||||
}
|
||||
// Overlay variant: the configured main-EXE PC holds different code.
|
||||
@@ -3702,6 +3759,9 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
|
||||
ss << "extern int psx_ws_cull_bltz_hi(uint32_t v); /* ws cull left edge, X in the high half (gpu.c) */\n";
|
||||
ss << "extern int psx_ws_cull_sltu_band(uint32_t sx, uint32_t bound); /* ws screen-x band, W in a register (gpu.c) */\n";
|
||||
}
|
||||
// Only for a config that lists [[widescreen.cull.sxy]] sites.
|
||||
if (!config_.ws_cull_sxy_sites.empty())
|
||||
ss << "extern uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy); /* ws SXY reject fold, left edge (gpu.c) */\n";
|
||||
ss << "extern int psx_ws_cull_vxrange(uint32_t x, uint32_t imm); /* ws masked-u16 X window */\n";
|
||||
ss << "extern int32_t psx_ws_depth_bound(int32_t imm); /* ws aspect-scaled far bound */\n";
|
||||
ss << "extern int32_t psx_ws_plane_nx(int32_t nx); /* ws side-plane normal-X scale (gpu.c) */\n";
|
||||
|
||||
@@ -166,6 +166,27 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
|
||||
h.u32(site.result);
|
||||
}
|
||||
|
||||
// Appended only when used, so every other title's hash is unchanged.
|
||||
if (!c.ws_cull_sxy_sites.empty()) {
|
||||
std::vector<WidescreenSxyCullSite> sxy_sites = c.ws_cull_sxy_sites;
|
||||
std::sort(sxy_sites.begin(), sxy_sites.end(),
|
||||
[](const auto& a, const auto& b) {
|
||||
return a.fold_addresses.front() < b.fold_addresses.front();
|
||||
});
|
||||
h.tag("cull_sxy");
|
||||
h.u32((uint32_t)sxy_sites.size());
|
||||
for (const auto& site : sxy_sites) {
|
||||
h.u32(site.vertices);
|
||||
for (size_t i = 0; i < site.vertices; i++) {
|
||||
h.u32(site.fold_addresses[i]);
|
||||
h.u32(site.fold_expected[i]);
|
||||
h.u32(site.vertex_regs[i]);
|
||||
}
|
||||
h.u32(site.consumer_address);
|
||||
h.u32(site.consumer_expected);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<WidescreenCullScaleSite> scale_sites = c.ws_cull_scale_sites;
|
||||
std::sort(scale_sites.begin(), scale_sites.end(),
|
||||
[](const auto& a, const auto& b) { return a.address < b.address; });
|
||||
@@ -313,6 +334,110 @@ static uint32_t parse_hex(const std::string& s, const std::string& field) {
|
||||
}
|
||||
}
|
||||
|
||||
// One [[widescreen.cull.sxy]] entry. The fold and its consumer are identified
|
||||
// by their exact instruction words; everything the runtime relies on is
|
||||
// derived from those words and must agree with the declared registers:
|
||||
// fold[0] = and r, v0, mask (operands in either order)
|
||||
// fold[k] = and r, vk, r (k >= 1, operands in either order)
|
||||
// consumer = beq/bne r, zero (within 4 instructions after the last AND)
|
||||
// `seen` holds the physical addresses claimed by earlier entries.
|
||||
static WidescreenSxyCullSite parse_sxy_cull_site(const toml::value& item,
|
||||
const std::string& path,
|
||||
std::set<uint32_t>& seen) {
|
||||
auto fail = [&](const std::string& why) {
|
||||
return std::runtime_error(fmt::format(
|
||||
"{}: [[widescreen.cull.sxy]] {}", path, why));
|
||||
};
|
||||
WidescreenSxyCullSite site;
|
||||
const std::string kind = toml::find<std::string>(item, "kind");
|
||||
if (kind == "tri") site.vertices = 3;
|
||||
else if (kind == "quad") site.vertices = 4;
|
||||
else throw fail("kind must be \"tri\" or \"quad\"");
|
||||
|
||||
for (int reg : toml::find<std::vector<int>>(item, "vertex_regs")) {
|
||||
if (reg < 1 || reg > 31)
|
||||
throw fail(fmt::format("vertex_regs: {} is not a GPR 1..31", reg));
|
||||
site.vertex_regs.push_back((uint32_t)reg);
|
||||
}
|
||||
for (const auto& a : toml::find<std::vector<std::string>>(item, "fold_addresses"))
|
||||
site.fold_addresses.push_back(parse_hex(a, "widescreen.cull.sxy.fold_addresses"));
|
||||
for (const auto& e : toml::find<std::vector<std::string>>(item, "fold_expected"))
|
||||
site.fold_expected.push_back(parse_hex(e, "widescreen.cull.sxy.fold_expected"));
|
||||
site.consumer_address = parse_hex(toml::find<std::string>(item, "consumer_address"),
|
||||
"widescreen.cull.sxy.consumer_address");
|
||||
site.consumer_expected = parse_hex(toml::find<std::string>(item, "consumer_expected"),
|
||||
"widescreen.cull.sxy.consumer_expected");
|
||||
|
||||
const size_t n = site.vertices;
|
||||
if (site.vertex_regs.size() != n || site.fold_addresses.size() != n ||
|
||||
site.fold_expected.size() != n)
|
||||
throw fail(fmt::format("{} needs exactly {} vertex_regs, fold_addresses "
|
||||
"and fold_expected", kind, n));
|
||||
for (size_t i = 0; i < n; i++)
|
||||
for (size_t j = i + 1; j < n; j++)
|
||||
if (site.vertex_regs[i] == site.vertex_regs[j])
|
||||
throw fail("vertex_regs must be distinct");
|
||||
|
||||
// Addresses: aligned, one segment, strictly increasing, and short.
|
||||
const uint32_t segment = site.fold_addresses[0] & 0xE0000000u;
|
||||
uint32_t prev = 0;
|
||||
for (size_t i = 0; i <= n; i++) {
|
||||
const uint32_t a = i < n ? site.fold_addresses[i] : site.consumer_address;
|
||||
if ((a & 3u) != 0 || (a & 0xE0000000u) != segment)
|
||||
throw fail(fmt::format("0x{:08X} must be word-aligned and in the "
|
||||
"segment of the first AND", a));
|
||||
if (i > 0 && a <= prev)
|
||||
throw fail(fmt::format("0x{:08X} must follow 0x{:08X}", a, prev));
|
||||
if (!seen.insert(a & 0x1FFFFFFFu).second)
|
||||
throw fail(fmt::format("address 0x{:08X} is already used by "
|
||||
"another sxy site", a));
|
||||
prev = a;
|
||||
}
|
||||
if (site.fold_addresses[n - 1] - site.fold_addresses[0] > 4u * 15u)
|
||||
throw fail("the AND fold spans more than 16 instructions");
|
||||
if (site.consumer_address - site.fold_addresses[n - 1] > 16u)
|
||||
throw fail("consumer_address must be within 4 instructions of the last AND");
|
||||
|
||||
// The words: one AND chain into a single register r.
|
||||
auto rs_of = [](uint32_t w) { return (w >> 21) & 31u; };
|
||||
auto rt_of = [](uint32_t w) { return (w >> 16) & 31u; };
|
||||
const uint32_t r = (site.fold_expected[0] >> 11) & 31u;
|
||||
if (r == 0) throw fail("the AND fold must not write $zero");
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const uint32_t w = site.fold_expected[i];
|
||||
const uint32_t v = site.vertex_regs[i];
|
||||
if ((w >> 26) != 0u || (w & 0x7FFu) != 0x24u || ((w >> 11) & 31u) != r)
|
||||
throw fail(fmt::format("fold_expected 0x{:08X} must be `and r, ...` "
|
||||
"with the same destination as the first AND", w));
|
||||
if (v == r)
|
||||
throw fail(fmt::format("vertex register {} is the fold register", v));
|
||||
const uint32_t other = rs_of(w) == v ? rt_of(w)
|
||||
: rt_of(w) == v ? rs_of(w) : 32u;
|
||||
if (other == 32u)
|
||||
throw fail(fmt::format("fold_expected 0x{:08X} does not read "
|
||||
"vertex register {}", w, v));
|
||||
if (i == 0) {
|
||||
// The seed operand is the sign mask: a register of its own.
|
||||
if (other == 0 || other == r ||
|
||||
std::find(site.vertex_regs.begin(), site.vertex_regs.end(),
|
||||
other) != site.vertex_regs.end())
|
||||
throw fail(fmt::format("first AND 0x{:08X} must combine vertex "
|
||||
"register {} with a separate mask "
|
||||
"register", w, v));
|
||||
} else if (other != r) {
|
||||
throw fail(fmt::format("fold_expected 0x{:08X} must combine vertex "
|
||||
"register {} with the fold register {}", w, v, r));
|
||||
}
|
||||
}
|
||||
const uint32_t c = site.consumer_expected;
|
||||
const uint32_t c_op = c >> 26;
|
||||
if ((c_op != 0x04u && c_op != 0x05u) ||
|
||||
!((rs_of(c) == r && rt_of(c) == 0) || (rs_of(c) == 0 && rt_of(c) == r)))
|
||||
throw fail(fmt::format("consumer_expected 0x{:08X} must be beq/bne of the "
|
||||
"fold register {} against $zero", c, r));
|
||||
return site;
|
||||
}
|
||||
|
||||
// Parse a display aspect "W:H" string (e.g. "4:3", "16:9", "21:9"). Accepts
|
||||
// only aspects between native 4:3 and 32:9 — narrower than 4:3 has no
|
||||
// widescreen meaning and ultra-extreme values would squash the GTE X axis
|
||||
@@ -2362,6 +2487,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
std::vector<uint32_t> ws_cull_clip_edge_x_load_sites;
|
||||
uint32_t ws_cull_clip_edge_width = 0;
|
||||
std::vector<WidescreenCullKeepSite> ws_cull_keep_sites;
|
||||
std::vector<WidescreenSxyCullSite> ws_cull_sxy_sites;
|
||||
std::vector<WidescreenMaskedRejectSite> ws_cull_masked_reject_sites;
|
||||
std::vector<WidescreenCullScaleSite> ws_cull_scale_sites;
|
||||
std::vector<WidescreenAngleSite> ws_cull_angle_sites;
|
||||
@@ -2462,6 +2588,17 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
ws_cull_keep_sites.push_back(site);
|
||||
}
|
||||
}
|
||||
if (cull.contains("sxy")) {
|
||||
std::set<uint32_t> seen;
|
||||
for (const auto& item : toml::find<toml::array>(cull, "sxy")) {
|
||||
ws_cull_sxy_sites.push_back(parse_sxy_cull_site(
|
||||
item, config_path.string(), seen));
|
||||
if (ws_cull_sxy_sites.size() > 64)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: [[widescreen.cull.sxy]] supports at most 64 sites",
|
||||
config_path.string()));
|
||||
}
|
||||
}
|
||||
if (cull.contains("scale")) {
|
||||
std::set<uint32_t> seen;
|
||||
for (const auto& item : toml::find<toml::array>(cull, "scale")) {
|
||||
@@ -2904,6 +3041,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*ws_cull_nclip_exact_sites*/ ws_cull_nclip_exact_sites,
|
||||
/*ws_cull_branch_keep_sites*/ ws_cull_branch_keep_sites,
|
||||
/*ws_cull_keep_sites*/ ws_cull_keep_sites,
|
||||
/*ws_cull_sxy_sites*/ ws_cull_sxy_sites,
|
||||
/*ws_cull_scale_sites*/ ws_cull_scale_sites,
|
||||
/*ws_cull_angle_sites*/ ws_cull_angle_sites,
|
||||
/*ws_aspect_cone*/ ws_aspect_cone,
|
||||
|
||||
@@ -99,6 +99,21 @@ struct WidescreenCullKeepSite {
|
||||
uint32_t result = 0; // forced comparison result (0 or 1)
|
||||
};
|
||||
|
||||
// One packed-SXY all-vertices reject ([[widescreen.cull.sxy]]): an AND fold
|
||||
// over the projected SXY of every vertex of a triangle or quad, seeded with a
|
||||
// sign mask and consumed by `beq/bne r, zero`. Each AND is widened only in
|
||||
// its X-sign bit (ws_cull_edge.h psx_ws_sxy_and_value); the guest's own
|
||||
// branch still decides. Every word of the fold and of the consumer is part of
|
||||
// the identity: an overlay variant whose words differ anywhere is untouched.
|
||||
struct WidescreenSxyCullSite {
|
||||
uint32_t vertices = 0; // 3 (kind = "tri") or 4 ("quad")
|
||||
std::vector<uint32_t> fold_addresses; // one AND per vertex, chain order
|
||||
std::vector<uint32_t> fold_expected; // the exact AND words
|
||||
std::vector<uint32_t> vertex_regs; // the SXY operand of each AND
|
||||
uint32_t consumer_address = 0; // beq/bne r, zero after the fold
|
||||
uint32_t consumer_expected = 0;
|
||||
};
|
||||
|
||||
// Aspect-scaled 12-bit angular half-extent. These sites load a positive angle
|
||||
// constant with `addi[u] rt,zero,imm`; the runtime widens tan(angle) by the
|
||||
// live horizontal reveal factor. Full-word guards prevent overlay-address
|
||||
@@ -1276,6 +1291,8 @@ struct GameConfig {
|
||||
// where maximal overdraw is preferable to range guessing. Each entry is
|
||||
// guarded by the complete MIPS word; 4:3 executes the vanilla comparison.
|
||||
std::vector<WidescreenCullKeepSite> ws_cull_keep_sites;
|
||||
// Packed-SXY all-vertices rejects widened only on the left edge.
|
||||
std::vector<WidescreenSxyCullSite> ws_cull_sxy_sites;
|
||||
// Register-register frustum-edge compares whose bound operand scales
|
||||
// with the live reveal ([[widescreen.cull.scale]]).
|
||||
std::vector<WidescreenCullScaleSite> ws_cull_scale_sites;
|
||||
|
||||
@@ -1839,6 +1839,12 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
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 += "/* Game function-override tier (runtime/include/func_override.h):\n";
|
||||
out += " * hand-written C registered against a guest address. NULL (the\n";
|
||||
out += " * default) = no overrides, dispatch identical to a build without\n";
|
||||
out += " * the tier. Returns 1 when the override handled the call; the\n";
|
||||
out += " * guest resumes at $ra exactly as if the original ran jr $ra. */\n";
|
||||
out += "extern int (*g_psx_func_override_hook)(CPUState* cpu, uint32_t phys);\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";
|
||||
@@ -2285,6 +2291,18 @@ void FullFunctionEmitter::emit_dispatch(
|
||||
out += " /* Byte-guarded A0/B0/C0 call-vector tail stubs. */\n";
|
||||
out += " if (!found && psx_bios_try_native_call_stub(cpu, addr))\n";
|
||||
out += " found = 1;\n";
|
||||
out += " /* Function-override tier: AFTER the BIOS tiers (an override\n";
|
||||
out += " * can never shadow a kernel service vector), BEFORE every\n";
|
||||
out += " * game code backend, so one address-keyed hook covers the\n";
|
||||
out += " * static EXE, runtime-loaded overlays and dirty RAM alike.\n";
|
||||
out += " * Handled (rc 1) => the override completed against guest\n";
|
||||
out += " * state; resume at $ra via the trampoline's normal\n";
|
||||
out += " * return/tail contract. rc 0 => fall through untouched. */\n";
|
||||
out += " if (!found && g_psx_func_override_hook &&\n";
|
||||
out += " g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu)) {\n";
|
||||
out += " cpu->pc = cpu->gpr[31];\n";
|
||||
out += " found = 1;\n";
|
||||
out += " }\n";
|
||||
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
|
||||
out += " /* Game EXEs can overlap the BIOS shell copy window at\n";
|
||||
out += " * physical 0x30000-0x5AFFF. If the target belongs to the\n";
|
||||
|
||||
@@ -245,6 +245,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
std::set<uint32_t> ws_cull_clip_edge_x_load; // [widescreen.cull] clip_edge_x_load_sites
|
||||
uint32_t ws_cull_clip_edge_width = 0; // [widescreen.cull] clip_edge_width
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
|
||||
std::vector<PSXRecompV4::WidescreenSxyCullSite> ws_cull_sxy;
|
||||
std::vector<PSXRecompV4::WidescreenCullScaleSite> ws_cull_scale;
|
||||
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle;
|
||||
std::vector<PSXRecompV4::DrawDistanceClampSite> draw_distance_clamps; // [[draw_distance.clamp]]
|
||||
@@ -324,6 +325,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
if (!cfg.ws_cull_clip_edge_x_load_sites.empty())
|
||||
ws_cull_clip_edge_width = PSXRecompV4::ws_cull_clip_edge_width(cfg);
|
||||
ws_cull_keep = cfg.ws_cull_keep_sites;
|
||||
ws_cull_sxy = cfg.ws_cull_sxy_sites;
|
||||
ws_cull_masked_reject = cfg.ws_cull_masked_reject_sites;
|
||||
ws_cull_scale = cfg.ws_cull_scale_sites;
|
||||
ws_cull_angle = cfg.ws_cull_angle_sites;
|
||||
@@ -442,6 +444,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
if (!wscfg.ws_cull_clip_edge_x_load_sites.empty() && !ws_cull_clip_edge_width)
|
||||
ws_cull_clip_edge_width = PSXRecompV4::ws_cull_clip_edge_width(wscfg);
|
||||
if (ws_cull_keep.empty()) ws_cull_keep = wscfg.ws_cull_keep_sites;
|
||||
if (ws_cull_sxy.empty()) ws_cull_sxy = wscfg.ws_cull_sxy_sites;
|
||||
if (ws_cull_masked_reject.empty()) ws_cull_masked_reject = wscfg.ws_cull_masked_reject_sites;
|
||||
if (ws_cull_scale.empty()) ws_cull_scale = wscfg.ws_cull_scale_sites;
|
||||
if (ws_cull_angle.empty()) ws_cull_angle = wscfg.ws_cull_angle_sites;
|
||||
@@ -672,6 +675,30 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
check("widescreen.bg2d.bufbase_site", ws_bg2d_bufbase_site);
|
||||
check("widescreen.bg2d.cap_site", ws_bg2d_cap_site);
|
||||
check("widescreen.bg2d.init_func", ws_bg2d_init_func);
|
||||
// [[widescreen.cull.sxy]] sites are matched physically, and every
|
||||
// word of each fold and its consumer is part of the identity: check
|
||||
// them all against the image up front, so a stale address fails the
|
||||
// build even when no discovered function reaches the fold.
|
||||
for (const auto& site : ws_cull_sxy) {
|
||||
for (size_t i = 0; i <= site.vertices; i++) {
|
||||
const bool consumer = i == site.vertices;
|
||||
const uint32_t phys = (consumer ? site.consumer_address
|
||||
: site.fold_addresses[i]) &
|
||||
PSXRecomp::kPhysMask;
|
||||
const uint32_t want = consumer ? site.consumer_expected
|
||||
: site.fold_expected[i];
|
||||
const uint32_t a = exe->link_segment() | phys;
|
||||
if (!exe->contains_phys(a)) continue;
|
||||
const auto got = exe->read_word(a);
|
||||
if (!got || *got != want) {
|
||||
fmt::print(stderr,
|
||||
"ERROR: [[widescreen.cull.sxy]] expected 0x{:08X} at "
|
||||
"0x{:08X}, found 0x{:08X}\n",
|
||||
want, a, got ? *got : 0u);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!foreign.empty()) {
|
||||
fmt::print(stderr,
|
||||
"ERROR: {} config code site(s) name this image's bytes in another "
|
||||
@@ -1550,6 +1577,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
else if (!ws_cull_w_imms.empty())
|
||||
codegen_config.ws_cull_clip_edge_width = ws_cull_w_imms.front();
|
||||
codegen_config.ws_cull_keep_sites = ws_cull_keep;
|
||||
codegen_config.ws_cull_sxy_sites = ws_cull_sxy;
|
||||
codegen_config.ws_cull_scale_sites = ws_cull_scale;
|
||||
codegen_config.ws_cull_angle_sites = ws_cull_angle;
|
||||
codegen_config.draw_distance_clamp_sites = draw_distance_clamps;
|
||||
|
||||
@@ -190,8 +190,8 @@ CodeGenConfig move_exact_config_sites(const CodeGenConfig& cfg, At at) {
|
||||
v.ws_bg2d_bufbase_site = at(v.ws_bg2d_bufbase_site);
|
||||
v.ws_bg2d_cap_site = at(v.ws_bg2d_cap_site);
|
||||
// Physically matched, kept as written: ws_signed_x_bound_sites,
|
||||
// ws_cull_keep_sites, ws_cull_angle_sites, ws_aspect_cone.sites,
|
||||
// draw_distance_clamp_sites.
|
||||
// ws_cull_keep_sites, ws_cull_sxy_sites, ws_cull_angle_sites,
|
||||
// ws_aspect_cone.sites, draw_distance_clamp_sites.
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
@@ -415,6 +415,278 @@ reject_mask = "0xFFFF0000"
|
||||
"overlay with another instruction stays vanilla");
|
||||
}
|
||||
|
||||
// ---- [[widescreen.cull.sxy]] ----------------------------------------------
|
||||
// The Wild 9 (SLUS-00425) shapes, relocated to kBase. Tri: the fold has GTE
|
||||
// reads and stores between its ANDs and one instruction before the consumer.
|
||||
// +00 and at,t0,s1 +04 mfc2 t3,$14 +08 and at,s4,at +0C sw t0,8(a0)
|
||||
// +10 and at,t3,at +14 sub t0,s0,t0 +18 bne at,zero,+1 ; nop ; jr ra ; nop
|
||||
// Quad: four contiguous ANDs, then the consumer.
|
||||
const std::vector<uint32_t> kTriWords = {
|
||||
0x01110824u, 0x480B7000u, 0x02810824u, 0xAC880008u, 0x01610824u,
|
||||
0x02084022u, 0x14200001u, 0x00000000u, 0x03E00008u, 0x00000000u};
|
||||
const std::vector<uint32_t> kQuadWords = {
|
||||
0x01110824u, 0x02810824u, 0x01610824u, 0x01210824u, 0x14200001u,
|
||||
0x00000000u, 0x03E00008u, 0x00000000u};
|
||||
|
||||
PSXRecomp::GeneratedFunction generate_words(const std::vector<uint32_t>& words,
|
||||
const PSXRecomp::CodeGenConfig& config) {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
exe.header.initial_pc = kBase;
|
||||
exe.header.file_size = (uint32_t)(4 * words.size());
|
||||
for (uint32_t w : words) append_word(exe.code_data, w);
|
||||
PSXRecomp::Function function{};
|
||||
function.start_addr = kBase;
|
||||
function.end_addr = kBase + 4u * (uint32_t)words.size();
|
||||
function.size = 4u * (uint32_t)words.size();
|
||||
function.name = "ws_cull_sxy_test";
|
||||
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
||||
const auto cfg = analyzer.analyze_function(function);
|
||||
PSXRecomp::CodeGenerator generator(exe, config);
|
||||
return generator.generate_function(function, cfg);
|
||||
}
|
||||
|
||||
struct SxyToml {
|
||||
std::string kind = "tri";
|
||||
std::string vertex_regs = "[8, 20, 11]";
|
||||
std::string fold_addresses = R"(["0x80010000", "0x80010008", "0x80010010"])";
|
||||
std::string fold_expected = R"(["0x01110824", "0x02810824", "0x01610824"])";
|
||||
std::string consumer_address = "0x80010018";
|
||||
std::string consumer_expected = "0x14200001";
|
||||
std::string str() const {
|
||||
return "\n[[widescreen.cull.sxy]]\nkind = \"" + kind + "\"\n" +
|
||||
"vertex_regs = " + vertex_regs + "\n" +
|
||||
"fold_addresses = " + fold_addresses + "\n" +
|
||||
"fold_expected = " + fold_expected + "\n" +
|
||||
"consumer_address = \"" + consumer_address + "\"\n" +
|
||||
"consumer_expected = \"" + consumer_expected + "\"\n";
|
||||
}
|
||||
};
|
||||
|
||||
SxyToml quad_toml() {
|
||||
SxyToml q;
|
||||
q.kind = "quad";
|
||||
q.vertex_regs = "[8, 20, 11, 9]";
|
||||
q.fold_addresses = R"(["0x80010000", "0x80010004", "0x80010008", "0x8001000C"])";
|
||||
q.fold_expected = R"(["0x01110824", "0x02810824", "0x01610824", "0x01210824"])";
|
||||
q.consumer_address = "0x80010010";
|
||||
return q;
|
||||
}
|
||||
|
||||
void sxy_loader() {
|
||||
const auto tri = load(base_config() + SxyToml{}.str());
|
||||
check(tri.ws_cull_sxy_sites.size() == 1, "loader reads a tri sxy site");
|
||||
const auto& t = tri.ws_cull_sxy_sites.at(0);
|
||||
check(t.vertices == 3 && t.vertex_regs == std::vector<uint32_t>({8, 20, 11}) &&
|
||||
t.fold_addresses == std::vector<uint32_t>({0x80010000u, 0x80010008u, 0x80010010u}) &&
|
||||
t.fold_expected == std::vector<uint32_t>({0x01110824u, 0x02810824u, 0x01610824u}) &&
|
||||
t.consumer_address == 0x80010018u && t.consumer_expected == 0x14200001u,
|
||||
"tri sxy site fields");
|
||||
const auto quad = load(base_config() + quad_toml().str());
|
||||
check(quad.ws_cull_sxy_sites.size() == 1 && quad.ws_cull_sxy_sites[0].vertices == 4,
|
||||
"loader reads a quad sxy site");
|
||||
check(load(base_config()).ws_cull_sxy_sites.empty(), "sxy defaults to empty");
|
||||
|
||||
// Operands may be written in either order; beq is a consumer as well.
|
||||
SxyToml swapped;
|
||||
swapped.fold_expected = R"(["0x02280824", "0x00340824", "0x002B0824"])";
|
||||
swapped.consumer_expected = "0x10010001";
|
||||
check(load(base_config() + swapped.str()).ws_cull_sxy_sites.size() == 1,
|
||||
"swapped AND operands and a beq zero,at consumer are accepted");
|
||||
|
||||
auto bad = [&](SxyToml site, const char* needle, const char* what) {
|
||||
check(load_throws_with(base_config() + site.str(), needle), what);
|
||||
};
|
||||
SxyToml s;
|
||||
s = {}; s.kind = "pent";
|
||||
bad(s, "kind must be", "unknown kind");
|
||||
s = {}; s.vertex_regs = "[8, 20]";
|
||||
bad(s, "needs exactly 3", "tri with two vertex_regs");
|
||||
s = quad_toml(); s.fold_expected = R"(["0x01110824", "0x02810824", "0x01610824"])";
|
||||
bad(s, "needs exactly 4", "quad with three AND words");
|
||||
s = {}; s.vertex_regs = "[8, 8, 11]";
|
||||
bad(s, "must be distinct", "repeated vertex register");
|
||||
s = {}; s.vertex_regs = "[8, 20, 0]";
|
||||
bad(s, "not a GPR", "$zero vertex register");
|
||||
s = {}; s.vertex_regs = "[8, 20, 1]";
|
||||
bad(s, "is the fold register", "vertex register equal to the fold register");
|
||||
s = {}; s.vertex_regs = "[8, 20, 12]";
|
||||
bad(s, "does not read vertex register 12", "AND does not read its vertex");
|
||||
s = {}; s.vertex_regs = "[8, 20, 17]";
|
||||
s.fold_expected = R"(["0x01140824", "0x02810824", "0x02210824"])";
|
||||
bad(s, "separate mask register", "first AND combines two vertices");
|
||||
s = {}; s.fold_expected = R"(["0x01110824", "0x02811024", "0x01610824"])";
|
||||
bad(s, "same destination", "AND into another register");
|
||||
s = {}; s.fold_expected = R"(["0x01110825", "0x02810824", "0x01610824"])";
|
||||
bad(s, "must be `and r", "OR in the fold");
|
||||
s = {}; s.fold_expected = R"(["0x01110824", "0x02910824", "0x01610824"])";
|
||||
bad(s, "with the fold register", "later AND not chained through the fold register");
|
||||
s = {}; s.fold_expected = R"(["0x01100024", "0x02800024", "0x01600024"])";
|
||||
bad(s, "must not write $zero", "fold into $zero");
|
||||
s = {}; s.consumer_expected = "0x14220001";
|
||||
bad(s, "beq/bne of the fold register", "consumer compares with another register");
|
||||
s = {}; s.consumer_expected = "0x04200001";
|
||||
bad(s, "beq/bne of the fold register", "consumer is bltz");
|
||||
s = {}; s.consumer_address = "0x8001002C";
|
||||
bad(s, "within 4 instructions", "consumer too far from the fold");
|
||||
s = {}; s.fold_addresses = R"(["0x80010008", "0x80010000", "0x80010010"])";
|
||||
bad(s, "must follow", "fold addresses out of order");
|
||||
s = {}; s.consumer_address = "0x80010010";
|
||||
bad(s, "must follow", "consumer on the last AND");
|
||||
s = {}; s.fold_addresses = R"(["0x80010000", "0xA0010008", "0x80010010"])";
|
||||
bad(s, "segment of the first AND", "fold crossing segments");
|
||||
s = {}; s.fold_addresses = R"(["0x80010000", "0x8001000A", "0x80010010"])";
|
||||
bad(s, "word-aligned", "misaligned AND");
|
||||
check(load_throws_with(base_config() + SxyToml{}.str() + quad_toml().str(),
|
||||
"already used by another sxy site"),
|
||||
"two sites sharing a physical address");
|
||||
}
|
||||
|
||||
void sxy_hash_identity() {
|
||||
const auto plain = load(base_config());
|
||||
const auto with = load(base_config() + SxyToml{}.str());
|
||||
check(PSXRecompV4::overlay_codegen_config_hash(plain) !=
|
||||
PSXRecompV4::overlay_codegen_config_hash(with),
|
||||
"an sxy site changes the overlay-cache identity");
|
||||
auto changed = with;
|
||||
changed.ws_cull_sxy_sites[0].consumer_expected = 0x10200001u;
|
||||
check(PSXRecompV4::overlay_codegen_config_hash(changed) !=
|
||||
PSXRecompV4::overlay_codegen_config_hash(with),
|
||||
"the consumer word is part of the overlay-cache identity");
|
||||
changed = with;
|
||||
changed.ws_cull_sxy_sites[0].vertex_regs[2] = 12u;
|
||||
check(PSXRecompV4::overlay_codegen_config_hash(changed) !=
|
||||
PSXRecompV4::overlay_codegen_config_hash(with),
|
||||
"the vertex registers are part of the overlay-cache identity");
|
||||
}
|
||||
|
||||
bool has_text(const std::string& code, const char* needle) {
|
||||
return code.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
void sxy_codegen() {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
const auto vanilla_tri = generate_words(kTriWords, config).full_code;
|
||||
check(!has_text(vanilla_tri, "psx_ws_cull_sxy_and"), "no sxy site: vanilla ANDs");
|
||||
|
||||
config.ws_cull_sxy_sites = load(base_config() + SxyToml{}.str()).ws_cull_sxy_sites;
|
||||
const auto tri = generate_words(kTriWords, config).full_code;
|
||||
check(has_text(tri, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[17], cpu->gpr[8]);"),
|
||||
"tri AND 0 seeds from the mask with vertex t0");
|
||||
check(has_text(tri, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[20]);"),
|
||||
"tri AND 1 folds vertex s4");
|
||||
check(has_text(tri, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[11]);"),
|
||||
"tri AND 2 folds vertex t3");
|
||||
check(!has_text(tri, "cpu->gpr[1] = cpu->gpr[8] & cpu->gpr[17];"),
|
||||
"tri vanilla AND is replaced");
|
||||
check(!exits_with_failure([&] { generate_words(kTriWords, config); }),
|
||||
"a matching tri fold generates");
|
||||
|
||||
PSXRecomp::CodeGenConfig qconfig{};
|
||||
qconfig.ws_cull_sxy_sites = load(base_config() + quad_toml().str()).ws_cull_sxy_sites;
|
||||
const auto quad = generate_words(kQuadWords, qconfig).full_code;
|
||||
check(has_text(quad, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[17], cpu->gpr[8]);") &&
|
||||
has_text(quad, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[20]);") &&
|
||||
has_text(quad, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[11]);") &&
|
||||
has_text(quad, "cpu->gpr[1] = psx_ws_cull_sxy_and(cpu->gpr[1], cpu->gpr[9]);"),
|
||||
"quad folds all four vertices");
|
||||
|
||||
// The guest's own consumer branch is untouched.
|
||||
check(has_text(quad, "cpu->gpr[1] != cpu->gpr[0]") || has_text(quad, "cpu->gpr[1] != 0"),
|
||||
"the consumer bne is the vanilla branch");
|
||||
|
||||
// Any word of the fold or the consumer that differs: main EXE fails,
|
||||
// an overlay variant keeps every AND vanilla.
|
||||
for (size_t i : {size_t(1), size_t(2), size_t(6)}) {
|
||||
auto words = kTriWords;
|
||||
if (i == 1) words[2] = 0x02810825u; // the second AND is an OR
|
||||
if (i == 2) words[4] = 0x01710824u; // the last AND reads s1
|
||||
if (i == 6) words[6] = 0x10200001u; // beq instead of bne
|
||||
check(exits_with_failure([&] { generate_words(words, config); }),
|
||||
"a mismatching tri fold fails main-EXE generation");
|
||||
auto overlay = config;
|
||||
overlay.overlay_mode = true;
|
||||
const auto code = generate_words(words, overlay).full_code;
|
||||
check(!has_text(code, "psx_ws_cull_sxy_and"),
|
||||
"an overlay variant with any mismatching word stays vanilla");
|
||||
}
|
||||
auto overlay = config;
|
||||
overlay.overlay_mode = true;
|
||||
check(has_text(generate_words(kTriWords, overlay).full_code,
|
||||
"psx_ws_cull_sxy_and(cpu->gpr[17], cpu->gpr[8])"),
|
||||
"an overlay with the exact fold emits the helper");
|
||||
|
||||
// Declared only for a config that lists sxy sites.
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
std::vector<PSXRecomp::GeneratedFunction> functions;
|
||||
check(!has_text(PSXRecomp::CodeGenerator(exe).build_shared_decls_header(functions),
|
||||
"psx_ws_cull_sxy_and"),
|
||||
"a config without sxy sites does not declare the helper");
|
||||
check(has_text(PSXRecomp::CodeGenerator(exe, config).build_shared_decls_header(functions),
|
||||
"extern uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy);"),
|
||||
"a config with sxy sites declares the helper");
|
||||
}
|
||||
|
||||
uint32_t pack_sxy(int x, int y) {
|
||||
return ((uint32_t)(uint16_t)(int16_t)y << 16) | (uint16_t)(int16_t)x;
|
||||
}
|
||||
|
||||
uint32_t sxy_fold(const uint32_t* v, int n, uint32_t mask, int32_t m) {
|
||||
uint32_t r = psx_ws_sxy_and_value(mask, v[0], m);
|
||||
for (int k = 1; k < n; k++) r = psx_ws_sxy_and_value(r, v[k], m);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Every tri (and a quad sweep) over the edge values: at margin 0 the fold is
|
||||
// the vanilla AND word for any mask; widened, it differs from vanilla only in
|
||||
// bit 15, which then means "every X < -m". Bit 31 (all Y above the screen)
|
||||
// and the right/bottom side (a separate chain in the guest) are unchanged.
|
||||
void sxy_runtime_math() {
|
||||
const int xs[] = {-1024, -200, -54, -53, -52, -1, 0, 1, 160, 319, 320, 1023};
|
||||
const int ys[] = {-1024, -1, 0, 1, 239, 240};
|
||||
std::vector<uint32_t> pts;
|
||||
for (int x : xs) for (int y : ys) pts.push_back(pack_sxy(x, y));
|
||||
const uint32_t masks[] = {0x80008000u, 0xFFFFFFFFu, 0x00008000u, 0x80000000u};
|
||||
int bad0 = 0, badm = 0;
|
||||
for (uint32_t mask : masks)
|
||||
for (uint32_t a : pts) for (uint32_t b : pts) for (uint32_t c : pts) {
|
||||
const uint32_t v[3] = {a, b, c};
|
||||
const uint32_t vanilla = mask & a & b & c;
|
||||
if (sxy_fold(v, 3, mask, 0) != vanilla) bad0++;
|
||||
for (int32_t m : {53, 192}) {
|
||||
bool all_left = true;
|
||||
for (uint32_t p : v) all_left &= (int16_t)(p & 0xFFFFu) < -m;
|
||||
const uint32_t want = (vanilla & ~0x8000u) |
|
||||
(all_left ? (vanilla & 0x8000u) : 0u);
|
||||
if (sxy_fold(v, 3, mask, m) != want) badm++;
|
||||
}
|
||||
}
|
||||
for (uint32_t a : pts) for (uint32_t d : pts) {
|
||||
const uint32_t v[4] = {a, pack_sxy(-300, 10), pack_sxy(-60, -5), d};
|
||||
const uint32_t vanilla = 0x80008000u & v[0] & v[1] & v[2] & v[3];
|
||||
if (sxy_fold(v, 4, 0x80008000u, 0) != vanilla) bad0++;
|
||||
bool all_left = true;
|
||||
for (uint32_t p : v) all_left &= (int16_t)(p & 0xFFFFu) < -53;
|
||||
const uint32_t want = (vanilla & ~0x8000u) | (all_left ? (vanilla & 0x8000u) : 0u);
|
||||
if (sxy_fold(v, 4, 0x80008000u, 53) != want) badm++;
|
||||
}
|
||||
check(bad0 == 0, "the sxy fold is the vanilla AND at margin 0");
|
||||
check(badm == 0, "widened, the sxy fold differs only in bit 15 = every X < -m");
|
||||
|
||||
// Spot checks at 16:9 on 320 wide (margin 53), mask 0x80008000.
|
||||
const uint32_t mask = 0x80008000u;
|
||||
uint32_t v[3] = {pack_sxy(-60, 10), pack_sxy(-54, 20), pack_sxy(-100, 30)};
|
||||
check(sxy_fold(v, 3, mask, 53) == 0x8000u, "all vertices left of -m: rejected");
|
||||
v[1] = pack_sxy(-53, 20);
|
||||
check(sxy_fold(v, 3, mask, 53) == 0u, "one vertex in the revealed band: kept");
|
||||
check(sxy_fold(v, 3, mask, 0) == 0x8000u, "the same triangle is rejected at 4:3");
|
||||
uint32_t up[3] = {pack_sxy(-10, -1), pack_sxy(100, -5), pack_sxy(5, -9)};
|
||||
check(sxy_fold(up, 3, mask, 53) == 0x80000000u, "all above the screen: still rejected");
|
||||
check(psx_ws_sxy_and_value(0x8000u, pack_sxy(-5, 0), -7) == 0x8000u,
|
||||
"a negative margin is treated as 0");
|
||||
}
|
||||
|
||||
void runtime_math() {
|
||||
// Identity at 4:3 (margin 0) for every input.
|
||||
for (int32_t v : {-1000, -54, -53, -1, 0, 1, 98, 222, 319, 320, 321, 1000}) {
|
||||
@@ -460,6 +732,10 @@ int main(int argc, char** argv) {
|
||||
shared_decls_include_new_helpers();
|
||||
runtime_math();
|
||||
masked_reject_sites();
|
||||
sxy_loader();
|
||||
sxy_hash_identity();
|
||||
sxy_codegen();
|
||||
sxy_runtime_math();
|
||||
|
||||
if (g_exit_probe >= 0) return 3; // no call had that probe index
|
||||
if (failures != 0) {
|
||||
|
||||
@@ -117,6 +117,8 @@ constexpr uint32_t kBltzT0 = 0x05000002u; // bltz t0, +2
|
||||
constexpr uint32_t kBgezT0 = 0x05010002u; // bgez t0, +2
|
||||
constexpr uint32_t kBltzalT0 = 0x05100002u; // bltzal t0, +2
|
||||
constexpr uint32_t kAddiuV0 = 0x24820070u; // addiu v0, a0, 0x70
|
||||
constexpr uint32_t kSltiAtS6 = 0x2AC10000u; // slti at, s6, 0 (s6 = SX << 16)
|
||||
constexpr uint32_t kSltiAtS6Imm = 0x2AC10010u; // slti at, s6, 0x10
|
||||
|
||||
const char* const kLuiHelper =
|
||||
"cpu->gpr[22] = psx_ws_cull_lui_hi(0x0200u); "
|
||||
@@ -182,6 +184,33 @@ void listed_sltu_emits_the_band_helper() {
|
||||
"a screen_x site on sltu generates");
|
||||
}
|
||||
|
||||
// slti rt,x,0 on `SX << 16` is the bltz sign test as a flag: set only while
|
||||
// SX < -margin through the same helper. Any other slti immediate is not this
|
||||
// form and fails the main-EXE build; an overlay variant keeps its code.
|
||||
void listed_slti_zero_emits_the_high_half_test() {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
config.emit_comments = true;
|
||||
const auto plain = generate_first_instruction(kSltiAtS6, config);
|
||||
check(!has(plain, "psx_ws_cull_bltz_hi"), "an unlisted slti stays vanilla");
|
||||
config.ws_cull_screen_x_sites.insert(kBase);
|
||||
check(has(generate_first_instruction(kSltiAtS6, config),
|
||||
"cpu->gpr[1] = (uint32_t)psx_ws_cull_bltz_hi(cpu->gpr[22]); "
|
||||
"/* ws explicit screen-x left edge (high half) */"),
|
||||
"a screen_x site on slti rt,rs,0 routes through psx_ws_cull_bltz_hi");
|
||||
check(!exits_with_failure([&] { generate_first_instruction(kSltiAtS6, config); }),
|
||||
"a screen_x site on slti rt,rs,0 generates");
|
||||
check(exits_with_failure([&] { generate_first_instruction(kSltiAtS6Imm, config); }),
|
||||
"a screen_x site on slti with a nonzero immediate fails main-EXE generation");
|
||||
config.overlay_mode = true;
|
||||
check(!has(generate_first_instruction(kSltiAtS6Imm, config), "psx_ws_cull_"),
|
||||
"an overlay slti with a nonzero immediate at a screen_x site stays vanilla");
|
||||
for (int32_t x : {-1000, -54, -53, -1, 0, 1, 319}) {
|
||||
const uint32_t v = (uint32_t)x << 16;
|
||||
check(psx_ws_cull_bltz_hi_value(v, 0) == (((int32_t)v < 0) ? 1 : 0),
|
||||
"the slti form is the vanilla slti at margin 0");
|
||||
}
|
||||
}
|
||||
|
||||
// bltz_sites is the low-half form and keeps its meaning when an address is in
|
||||
// both lists.
|
||||
void bltz_sites_wins_over_screen_x_sites() {
|
||||
@@ -301,6 +330,7 @@ int main(int argc, char** argv) {
|
||||
unlisted_instructions_stay_vanilla();
|
||||
listed_sites_emit_the_helpers();
|
||||
listed_sltu_emits_the_band_helper();
|
||||
listed_slti_zero_emits_the_high_half_test();
|
||||
bltz_sites_wins_over_screen_x_sites();
|
||||
main_exe_guard_and_overlay_variants();
|
||||
declarations_only_when_the_key_is_used();
|
||||
|
||||
+16
-36
@@ -23,14 +23,16 @@ psxrecomp_add_runtime_target(psx-runtime
|
||||
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(postfx_publication_test tests/test_postfx_publication.cpp)
|
||||
target_include_directories(postfx_publication_test PRIVATE include)
|
||||
add_test(NAME postfx_publication_test COMMAND postfx_publication_test)
|
||||
# Windows UTF-8 text boundary (issue #371): the helpers legacy Windows
|
||||
# relies on, and — with the shipped manifest — a UTF-8 process code page.
|
||||
if(WIN32)
|
||||
add_executable(host_time_threads_test tests/test_host_time_threads.c src/host_time.c)
|
||||
target_include_directories(host_time_threads_test PRIVATE include)
|
||||
add_test(NAME host_time_threads_test COMMAND host_time_threads_test)
|
||||
# Four waves each have their own 10-second lost-wakeup watchdog.
|
||||
set_tests_properties(host_time_threads_test PROPERTIES TIMEOUT 45)
|
||||
set_tests_properties(host_time_threads_test PROPERTIES TIMEOUT 15)
|
||||
add_executable(win_utf8_helpers_test tests/test_win_utf8.cpp)
|
||||
target_include_directories(win_utf8_helpers_test PRIVATE include)
|
||||
add_test(NAME win_utf8_helpers_test COMMAND win_utf8_helpers_test)
|
||||
@@ -366,30 +368,6 @@ if(BUILD_TESTING)
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-render-thread-results")
|
||||
set_tests_properties(gl_render_thread_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
foreach(_interp_mode normal hiw wide_quality hiw_wide_quality hiw_wide_quality_present)
|
||||
set(_interp_args --interpolation)
|
||||
if(_interp_mode MATCHES "wide_quality")
|
||||
list(APPEND _interp_args --wide-present --quality)
|
||||
endif()
|
||||
add_test(NAME gl_render_thread_interpolation_${_interp_mode}_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
${_interp_args}
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-rth-interp-${_interp_mode}")
|
||||
set_tests_properties(gl_render_thread_interpolation_${_interp_mode}_test
|
||||
PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
if(_interp_mode MATCHES "hiw")
|
||||
set_tests_properties(gl_render_thread_interpolation_${_interp_mode}_test
|
||||
PROPERTIES ENVIRONMENT "PSX_GL_HIRES_WINDOW=1")
|
||||
endif()
|
||||
if(_interp_mode MATCHES "_present$")
|
||||
set_tests_properties(gl_render_thread_interpolation_${_interp_mode}_test
|
||||
PROPERTIES ENVIRONMENT "PSX_GL_HIRES_WINDOW=1;PSX_GL_TEST_PRESENT_THREAD=1")
|
||||
endif()
|
||||
endforeach()
|
||||
# Frame generation ([video] frame_generation): the real presented
|
||||
# images are identical with it off and on (VRAM and native-wide
|
||||
# presents), generated frames are presented, and an in-between frame
|
||||
@@ -966,11 +944,6 @@ if(BUILD_TESTING)
|
||||
target_include_directories(pgxp_test PRIVATE include)
|
||||
add_test(NAME pgxp_test COMMAND pgxp_test)
|
||||
|
||||
add_executable(pgxp_production_test tests/test_pgxp.cpp src/pgxp.cpp src/psx_ram_geometry.c)
|
||||
target_include_directories(pgxp_production_test PRIVATE include)
|
||||
target_compile_definitions(pgxp_production_test PRIVATE PSX_NO_DEBUG_TOOLS)
|
||||
add_test(NAME pgxp_production_test COMMAND pgxp_production_test)
|
||||
|
||||
add_executable(pgxp_word_shadow_test tests/test_pgxp_word_shadow.cpp
|
||||
src/pgxp.cpp src/psx_ram_geometry.c)
|
||||
target_include_directories(pgxp_word_shadow_test PRIVATE include)
|
||||
@@ -1021,7 +994,7 @@ if(BUILD_TESTING)
|
||||
endforeach()
|
||||
|
||||
foreach(_pgxp_isolated_test
|
||||
gte_register_access_test gte_hle_contract_test pgxp_test pgxp_production_test pgxp_session_test pgxp_session_hooks_test)
|
||||
gte_register_access_test gte_hle_contract_test pgxp_test pgxp_session_test pgxp_session_hooks_test)
|
||||
target_sources(${_pgxp_isolated_test} PRIVATE tests/pgxp_test_mod_memory.cpp)
|
||||
endforeach()
|
||||
|
||||
@@ -1312,6 +1285,16 @@ if(BUILD_TESTING)
|
||||
target_include_directories(bios_hle_plan_test PRIVATE include)
|
||||
add_test(NAME bios_hle_plan_test COMMAND bios_hle_plan_test)
|
||||
|
||||
# Every func_override failure mode is silent at runtime: a registration
|
||||
# that is never consulted, a package override that survives clear-mods
|
||||
# (a netplay divergence), a residency guard that corrupts instead of
|
||||
# declining. Drives the real g_psx_func_override_hook pointer.
|
||||
add_executable(func_override_test
|
||||
tests/test_func_override.c
|
||||
src/func_override.c)
|
||||
target_include_directories(func_override_test PRIVATE include)
|
||||
add_test(NAME func_override_test COMMAND func_override_test)
|
||||
|
||||
# A cue, conventional raw image, or Steam .car image is a valid pick; each
|
||||
# must mount the same disc and identify off the same data track.
|
||||
add_executable(sbi_setup_test tests/test_sbi_setup.cpp src/psx_sha256.c)
|
||||
@@ -1441,6 +1424,7 @@ if(BUILD_TESTING)
|
||||
src/mod_immediate.c
|
||||
src/host_overlay.c
|
||||
tests/test_mod_runtime.cpp
|
||||
src/func_override.c
|
||||
src/iso_reader_c.cpp
|
||||
src/host_span_ring.c
|
||||
src/mod_runtime.cpp
|
||||
@@ -1727,10 +1711,6 @@ if(BUILD_TESTING)
|
||||
add_test(NAME frame_interpolation_schedule_test
|
||||
COMMAND frame_interpolation_schedule_test)
|
||||
|
||||
add_executable(input_replay_test tests/test_input_replay.c src/input_replay.c)
|
||||
target_include_directories(input_replay_test PRIVATE include)
|
||||
add_test(NAME input_replay_test COMMAND input_replay_test "${CMAKE_CURRENT_BINARY_DIR}/input_replay_fixture.txt")
|
||||
|
||||
add_executable(render_pass_plan_test
|
||||
tests/test_render_pass_plan.c
|
||||
src/render_pass_plan.c)
|
||||
|
||||
@@ -170,6 +170,12 @@ int duck_texture_pack_match_parts(DuckTexturePack* pack,
|
||||
* Release the lease even after pack destruction. */
|
||||
void duck_texture_pack_set_decode_budget(DuckTexturePack* pack, size_t bytes);
|
||||
int duck_texture_pack_request_decode(DuckTexturePack* pack, uint64_t entry_id);
|
||||
/* Elective full preload with the hd_texture_pack_preload contract: bounded
|
||||
* aggregate decoded bytes and concurrency, pinned images outside the decode
|
||||
* budget, everything not preloaded left to the asynchronous decode path.
|
||||
* Returns 1 complete, 0 partial (see out), -1 on error. */
|
||||
int duck_texture_pack_preload(DuckTexturePack* pack, size_t max_bytes,
|
||||
unsigned max_threads, HdTexturePreloadResult* out);
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* pack, uint64_t entry_id,
|
||||
DuckTexturePixels* out_pixels);
|
||||
void duck_texture_pixels_release(DuckTexturePixels* pixels);
|
||||
|
||||
@@ -17,9 +17,6 @@ typedef struct FrameInterpolationSchedule {
|
||||
* plans the whole 0..1 window, which is the historical behaviour. */
|
||||
double phase_lo;
|
||||
double phase_hi;
|
||||
/* Bounded asynchronous producer: preparing several frames ahead is normal,
|
||||
* so an early arrival must not reset the presentation clock. */
|
||||
int queued;
|
||||
} FrameInterpolationSchedule;
|
||||
|
||||
/* Plan one stock guest-frame interval. Presentation deadlines remain anchored
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/* func_override.h — replace a guest function with hand-written C.
|
||||
*
|
||||
* WHY THIS EXISTS
|
||||
*
|
||||
* A recomp turns the game into C, but that C is a build artifact: regenerated
|
||||
* from the player's own disc, never hand-edited (CLAUDE.md rule 4). So there
|
||||
* is no supported way to say "this function should do something else" — which
|
||||
* is the single most important thing a port needs to grow beyond a faithful
|
||||
* replay of the original.
|
||||
*
|
||||
* This is that mechanism. Register hand-written C against a guest address and
|
||||
* the dispatcher calls yours instead of the recompiled original.
|
||||
*
|
||||
* NO SIZE LIMIT. This is not a ROM patch; there is no slot to fit inside, no
|
||||
* code cave, nothing to shift. Your replacement is native code in a native
|
||||
* binary — five instructions or five thousand lines. That freedom is the
|
||||
* entire point of recompiling rather than hacking bytes.
|
||||
*
|
||||
* WHERE IT HOOKS, AND WHY IT MATTERS
|
||||
*
|
||||
* The hook is consulted in psx_dispatch_impl after the BIOS tiers (so an
|
||||
* override can never shadow a kernel service vector) and BEFORE every game
|
||||
* code backend. That placement is deliberate and load-bearing: one
|
||||
* address-keyed hook covers the STATIC EXE, runtime-loaded OVERLAYS and
|
||||
* DIRTY RAM alike. The interpreter's JAL/JALR call-resolution tiers consult
|
||||
* it on the SAME terms — before enter-compiled, before overlay-native and
|
||||
* before the local pc-chain (the reserved CRES_OVERRIDE slot) — so no call
|
||||
* the interpreter resolves itself can bypass an override. Ordering there is
|
||||
* not cosmetic: interp_enter_compiled calls psx_dispatch_game_compiled ->
|
||||
* entry->fn(cpu) directly and never re-enters psx_dispatch_impl, so a hook
|
||||
* placed after it is unreachable for any override on a statically-compiled
|
||||
* function reached from interpreted code, while registration and the
|
||||
* func_override inventory both still look healthy.
|
||||
*
|
||||
* SCOPE: overrides are CALL-site keyed. A guest tail transfer (j / jr) into
|
||||
* an overridden address from the dirty-RAM block loop is a TRANSFER, not a
|
||||
* call, and does not consult the hook — see dirty_ram_interp.c's compiled
|
||||
* handoff sites. Registering on a function only ever entered by tail
|
||||
* transfer will show calls == 0.
|
||||
*
|
||||
* THE CONTRACT
|
||||
*
|
||||
* Your function receives the CPUState and must obey the guest ABI:
|
||||
*
|
||||
* int my_impl(CPUState *cpu) {
|
||||
* uint32_t a0 = cpu->gpr[4], a1 = cpu->gpr[5]; // arguments
|
||||
* ...
|
||||
* cpu->gpr[2] = result; // $v0 = return value
|
||||
* return 1;
|
||||
* }
|
||||
*
|
||||
* Return 1 and the guest resumes at $ra exactly as if the original had run
|
||||
* `jr $ra`. Return 0 and the original recompiled or interpreted code runs
|
||||
* untouched, so an override can decline case by case (a conditional
|
||||
* pre-hook: mutate state, return 0, original runs).
|
||||
*
|
||||
* POINTERS MUST BE GUEST ADDRESSES. The caller is recompiled too: when it
|
||||
* dereferences your return value it performs a guest load. A host pointer
|
||||
* will read garbage. If you need to hand back a buffer, write it into guest
|
||||
* RAM (psx_write_byte and friends, or psx_mod_alloc_guest_memory) and return
|
||||
* that address.
|
||||
*
|
||||
* DETERMINISM. Keep ALL mutable state in guest RAM. Rollback/rewind and
|
||||
* netplay snapshot guest state only; host-side statics in an override
|
||||
* desync replays. Reading host config that never changes mid-session is
|
||||
* fine.
|
||||
*
|
||||
* CYCLE ACCOUNTING — REQUIRED AT REGISTRATION.
|
||||
* On hardware the replaced function took guest time, and every device in
|
||||
* the machine schedules against that clock; a replacement that takes zero
|
||||
* cycles shifts IRQ phase for the rest of the session. So every
|
||||
* registration MUST state its timing intent via the `credit` argument —
|
||||
* there is no default to inherit. Two forms:
|
||||
*
|
||||
* credit >= 0 The tier charges exactly `credit` guest cycles
|
||||
* (psx_advance_cycles) on every HANDLED call, after
|
||||
* the body runs. Declines charge nothing — the
|
||||
* original runs and accrues its own cycles. 0 is
|
||||
* legal and means "deliberately free": right for a
|
||||
* player-toggled mod whose behavior has no hardware
|
||||
* analog, indefensible for an always-on
|
||||
* reimplementation claiming to be indistinguishable.
|
||||
*
|
||||
* FO_CREDIT_SELF The tier charges nothing; the BODY owns its timing
|
||||
* and calls psx_advance_cycles() itself. Use this
|
||||
* when the original's cost is data-dependent
|
||||
* (charge base + per_iteration * n, the bios_hle.c
|
||||
* TIMING NOTE pattern), and ALWAYS for wraps: the
|
||||
* original re-dispatched by func_override_call_original
|
||||
* accrues its exact cycles by executing, so a fixed
|
||||
* nonzero credit on a wrap double-counts. Guest code
|
||||
* invoked via func_override_guest_call likewise
|
||||
* self-charges — credit only the code you REPLACED.
|
||||
*
|
||||
* Credits are approximations of the original's dynamic instruction count
|
||||
* unless measured; LLE remains the timing oracle (run with the override
|
||||
* declined and sample the cycle delta across the call boundary — the
|
||||
* callret ring — to measure a real distribution). See FAITHFUL_TIMING_PLAN.
|
||||
*
|
||||
* WHAT IT DOES NOT DO
|
||||
*
|
||||
* It does not lift the game's own assumptions. If the caller copies your
|
||||
* result into a fixed-size field, that field is still fixed. The override
|
||||
* removes the code limit, not every limit.
|
||||
*/
|
||||
|
||||
#ifndef PSXRECOMP_FUNC_OVERRIDE_H
|
||||
#define PSXRECOMP_FUNC_OVERRIDE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct CPUState;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define FO_MAX_OVERRIDES 128
|
||||
#define FO_MAX_ID 64
|
||||
#define FO_MAX_GUARD_WORDS 4
|
||||
|
||||
/* `credit` value: the override body states its own timing by calling
|
||||
* psx_advance_cycles() itself (mandatory for wraps — see CYCLE ACCOUNTING
|
||||
* above). Any other negative value is FO_ERR_ARGS. */
|
||||
#define FO_CREDIT_SELF (-1)
|
||||
|
||||
/* func_override_add return codes. */
|
||||
#define FO_OK 0
|
||||
#define FO_ERR_FULL 1
|
||||
#define FO_ERR_ARGS 2
|
||||
#define FO_ERR_DUPLICATE 3
|
||||
|
||||
/* An override implementation. Reads arguments from cpu->gpr[4..7], writes
|
||||
* the return value to cpu->gpr[2]. Returning nonzero means "handled";
|
||||
* returning 0 declines this call and the original code runs instead. */
|
||||
typedef int (*FuncOverrideFn)(struct CPUState *cpu);
|
||||
|
||||
/* Register `fn` for guest address `addr` (KSEG or physical — normalised).
|
||||
* `id` is copied, used for diagnostics, and may be NULL. Registering the
|
||||
* same address twice is an error rather than a silent last-wins, because a
|
||||
* silent shadow is impossible to debug. `credit` is the guest-cycle policy
|
||||
* (CYCLE ACCOUNTING above): >= 0 charged per handled call, or
|
||||
* FO_CREDIT_SELF when the body charges its own. */
|
||||
int func_override_add(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
int32_t credit);
|
||||
|
||||
/* Like func_override_add, plus a residency guard: before each consult the
|
||||
* first `n_words` guest words at `addr` are compared against
|
||||
* `expected_words`; on mismatch the override silently declines (the guard
|
||||
* miss is counted, see func_override_get_ex). Use this when the address
|
||||
* belongs to overlay/dirty RAM that other code may occupy at other times —
|
||||
* the guard makes "wrong code resident" a decline instead of a corruption.
|
||||
* n_words must be 1..FO_MAX_GUARD_WORDS. */
|
||||
int func_override_add_guarded(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words,
|
||||
int32_t credit);
|
||||
|
||||
/* Call a guest function from inside an override (or any native code on the
|
||||
* dispatch thread): args already placed in cpu->gpr[4..7], returns after the
|
||||
* callee completes; result in cpu->gpr[2]. `site_ra` is the return address
|
||||
* the callee should observe in $ra — pass the original call site's return
|
||||
* address when re-creating a call the original code would have made, so
|
||||
* callees, fntrace and crash forensics see authentic values. $ra is
|
||||
* restored around the call. */
|
||||
void func_override_guest_call(struct CPUState *cpu, uint32_t target,
|
||||
uint32_t site_ra);
|
||||
|
||||
/* Run the ORIGINAL code of the function currently being overridden — the
|
||||
* wrap primitive. Callable only from inside an executing override; arms a
|
||||
* one-shot bypass for this address, re-dispatches it, and returns after the
|
||||
* original completes (result in cpu->gpr[2]). Typical post-hook:
|
||||
*
|
||||
* int wrap(CPUState *cpu) {
|
||||
* func_override_call_original(cpu); // original runs fully
|
||||
* ...inspect / adjust results...
|
||||
* return 1; // we handled the call
|
||||
* }
|
||||
*
|
||||
* The bypass is one-shot per invocation: if the original recursively calls
|
||||
* itself, the recursive calls consult the override again (matching what a
|
||||
* guest-level wrap would observe). */
|
||||
void func_override_call_original(struct CPUState *cpu);
|
||||
|
||||
/* Register on behalf of a mod package plan. Identical to the two calls above
|
||||
* (pass n_words 0 for unguarded) except the entry is tagged package-armed, so
|
||||
* func_override_reset_package_armed can drop it when the plan is cleared.
|
||||
* The mod runtime uses this; game code registering its own always-on
|
||||
* reimplementations should use func_override_add/_add_guarded. */
|
||||
int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words,
|
||||
int32_t credit);
|
||||
/* Scoped package variant. The owner supplies a stable plan context and
|
||||
* invokes fn inside that plugin's resource/completion context. The plan must
|
||||
* disarm package entries before replacing that context. */
|
||||
typedef int (*FuncOverrideInvoker)(struct CPUState *cpu, FuncOverrideFn fn,
|
||||
const void *context);
|
||||
int func_override_add_package_scoped(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words, int32_t credit,
|
||||
FuncOverrideInvoker invoke, const void *context);
|
||||
/* Nonzero while a native override stack is live, including nested guest
|
||||
* calls. Save/load and rewind must defer until this returns to zero. */
|
||||
int func_override_active(void);
|
||||
|
||||
/* Install the dispatcher hook. Call once at startup AFTER registering (the
|
||||
* mod runtime calls it again after arming package-gated overrides — safe).
|
||||
* Cheap with nothing registered — it leaves the hook NULL, so dispatch is
|
||||
* byte-identical to a build without the tier. */
|
||||
void func_override_install(void);
|
||||
|
||||
/* Drop every package-armed override, keep direct registrations, re-install
|
||||
* (so the hook goes back to NULL if the table empties). Returns how many were
|
||||
* dropped. THIS IS REQUIRED FOR CORRECTNESS, not a convenience: a cleared mod
|
||||
* plan stops activation/vblank callbacks simply by no longer being iterated,
|
||||
* but an armed override lives in this module's own table, so without an
|
||||
* explicit reset it keeps firing after the plan is gone — including after
|
||||
* netplay's clear-mods announces a vanilla session, which is a peer
|
||||
* divergence. Direct registrations are deliberately kept: they are the game's
|
||||
* own always-on faithful reimplementations, present identically in both
|
||||
* peers' builds and not player-selectable. */
|
||||
int func_override_reset_package_armed(void);
|
||||
|
||||
/* Introspection, surfaced by the `func_override` TCP command. An override
|
||||
* whose `calls` stays 0 was never reached: wrong address, or that code path
|
||||
* never ran. `guard_misses` counts consults declined by the residency
|
||||
* guard. `credit_out` reports the declared cycle policy (FO_CREDIT_SELF or
|
||||
* the fixed per-call charge), so every armed override's timing intent is
|
||||
* inspectable, not just documented. */
|
||||
int func_override_count(void);
|
||||
int func_override_get(int index, char *id_out, size_t id_cap,
|
||||
uint32_t *addr_out, uint64_t *calls_out);
|
||||
int func_override_get_ex(int index, char *id_out, size_t id_cap,
|
||||
uint32_t *addr_out, uint64_t *calls_out,
|
||||
uint64_t *guard_misses_out, int *guarded_out,
|
||||
int32_t *credit_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PSXRECOMP_FUNC_OVERRIDE_H */
|
||||
@@ -346,6 +346,17 @@ void gpu_ws_set_clip_edge_x_load_sites(const uint32_t *sites, int nsites,
|
||||
void gpu_ws_set_cull_keep_sites(const uint32_t *addresses,
|
||||
const uint32_t *expected,
|
||||
const uint32_t *results, int nsites);
|
||||
/* [[widescreen.cull.sxy]]: packed-SXY all-vertices reject folds. Per site i,
|
||||
* vertices[i] (3 or 4) ANDs at fold_addresses[i*4 + k] with words
|
||||
* fold_expected[i*4 + k] and SXY operand vertex_regs[i*4 + k], then the
|
||||
* beq/bne consumer. The recompiler config loader has validated the shape. */
|
||||
#define PSX_WS_SXY_CULL_SITES_MAX 64
|
||||
void gpu_ws_set_sxy_cull_sites(const uint32_t *vertices,
|
||||
const uint32_t *fold_addresses,
|
||||
const uint32_t *fold_expected,
|
||||
const uint32_t *vertex_regs,
|
||||
const uint32_t *consumer_addresses,
|
||||
const uint32_t *consumer_expected, int nsites);
|
||||
void gpu_ws_set_angle_sites(const uint32_t *addresses,
|
||||
const uint32_t *expected, int nsites);
|
||||
/* [[widescreen.cull.scale]]: full-word-guarded SLT/SLTU sites whose rs (0) or
|
||||
@@ -399,6 +410,14 @@ uint32_t psx_ws_clip_edge_width(void);
|
||||
uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced);
|
||||
int psx_ws_cull_keep_site(uint32_t pc, uint32_t instr, uint32_t vanilla,
|
||||
uint32_t *out);
|
||||
/* One AND of a [[widescreen.cull.sxy]] fold (ws_cull_edge.h); identity at 4:3. */
|
||||
uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy);
|
||||
/* Interpreter lookup: the SXY operand register (1..31) when `instr` at `pc`
|
||||
* is an AND of a configured fold and every fold and consumer word, read
|
||||
* through `read_word` (physical address), matches; else 0. */
|
||||
extern int g_psx_ws_sxy_cull_n;
|
||||
uint32_t psx_ws_sxy_cull_vertex_reg(uint32_t pc, uint32_t instr,
|
||||
uint32_t (*read_word)(uint32_t phys));
|
||||
uint32_t psx_ws_angle_widen(uint32_t vanilla);
|
||||
int psx_ws_angle_site(uint32_t pc, uint32_t instr, uint32_t *out);
|
||||
/* bound * (half_extent + margin) / half_extent; identity at 4:3. */
|
||||
|
||||
@@ -157,8 +157,6 @@ void gl_renderer_stereo_diag(GLRenderStereoDiag *out);
|
||||
uint64_t gl_renderer_perf_ticks(void);
|
||||
uint64_t gl_renderer_perf_frequency(void);
|
||||
int gl_renderer_interpolation_owns_cadence(void);
|
||||
/* Nonblocking, atomically published readiness for bounded queued pacing. */
|
||||
int gl_renderer_interpolation_async_cadence(void);
|
||||
void gl_renderer_interpolation_diag(int *enabled, int *suspended,
|
||||
int *history_frames,
|
||||
double *host_hz, double *target_hz,
|
||||
@@ -201,6 +199,8 @@ float gl_renderer_get_bloom(void);
|
||||
* compile is logged and skipped. */
|
||||
void gl_renderer_set_post_shader(const char *path);
|
||||
int gl_renderer_post_shader_loaded(void);
|
||||
/* Debug: path, last load outcome and live uniforms of the user chain (JSON members). */
|
||||
int gl_renderer_post_shader_debug(char *out, int cap);
|
||||
typedef struct {
|
||||
float strength, threshold;
|
||||
uint64_t passes, us_n;
|
||||
@@ -570,20 +570,6 @@ int gl_renderer_pres_get(uint64_t seq, GlPresEvent *out);
|
||||
* sets/frame avg, [17]=wide FBO creations/frame avg. GPU phases are true
|
||||
* GL_TIME_ELAPSED times (CPU-overhead independent). Returns the count. */
|
||||
int gl_renderer_perf_aggregate(int wide_filter, double out[18]);
|
||||
/* Bounded benchmark attribution. CPU spans require PSX_GL_CPU_PERF=1;
|
||||
* reading drains the render queue, so read outside the measured interval. */
|
||||
typedef struct {
|
||||
uint64_t counts[10];
|
||||
uint64_t calls[8], ticks[8], max_ticks[8], frequency;
|
||||
uint64_t prior_ticks[8], prior_max_ticks[8];
|
||||
} GlRenderWorkStats;
|
||||
void gl_renderer_work_stats(GlRenderWorkStats *out);
|
||||
/* Opt-in replay receipt: actual SwapWindow return times, process-clock us.
|
||||
* Query outside measurement: drains the render/present queues. */
|
||||
#define GL_SWAP_TIMING_CAP 65536
|
||||
typedef uint64_t (*GlSwapClockFn)(void);
|
||||
void gl_renderer_swap_timing_start(GlSwapClockFn clock);
|
||||
int gl_renderer_swap_timestamps(uint64_t *out, int cap);
|
||||
|
||||
/* Native-wide mirror ablation (perf attribution, debug cmd gl_ws_ablate):
|
||||
* 0 = normal, 1 = skip the whole mirror pass (incl. wide_clear), 2 = full mirror
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef struct GtePostfxProjection { int32_t h, ofx, ofy; } GtePostfxProjection;
|
||||
/* Coherent latest CPU projection, not a frame/view identity. One CPU producer. */
|
||||
void gte_postfx_projection_enable(int enabled);
|
||||
int gte_postfx_projection_read(GtePostfxProjection *out);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "gte_postfx_projection.h"
|
||||
#include <atomic>
|
||||
/* CPU owns cached fields; renderer owns enable(). Atomic payloads plus an SC
|
||||
* version guard make concurrent reads defined and reject mixed tuples. */
|
||||
class GteProjectionPublication {
|
||||
std::atomic<uint32_t> control{0}, version{0}, published{0};
|
||||
std::atomic<int32_t> h{0}, x{0}, y{0};
|
||||
uint32_t seen=0; GtePostfxProjection cached{};
|
||||
public:
|
||||
void enable(bool on) {
|
||||
const auto c=control.load();
|
||||
if(bool(c&1)!=on) control.store(((c+2)&~1u)|uint32_t(on));
|
||||
}
|
||||
void publish(int32_t ph,int32_t px,int32_t py) {
|
||||
const auto c=control.load(std::memory_order_relaxed);
|
||||
if(!(c&1))return;
|
||||
if(c==seen && ph==cached.h && px==cached.ofx && py==cached.ofy)return;
|
||||
version.fetch_add(1); h.store(ph); x.store(px); y.store(py); published.store(c);
|
||||
version.fetch_add(1); seen=c; cached={ph,px,py};
|
||||
}
|
||||
bool read(GtePostfxProjection& out) const {
|
||||
const auto c=control.load(); if(!(c&1))return false;
|
||||
for(unsigned i=0;i<3;++i) {
|
||||
const auto v=version.load(); if(v&1)continue;
|
||||
const auto p=published.load(); GtePostfxProjection value{h.load(),x.load(),y.load()};
|
||||
if(v==version.load() && p==c && c==control.load()) {out=value;return true;}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -25,6 +25,13 @@ enum HdTextureLookupStatus {
|
||||
HD_TEXTURE_LOOKUP_AMBIGUOUS = 2
|
||||
};
|
||||
|
||||
/* Palette identity of an indexed replacement, named TTTTTTTT-index.png: its
|
||||
* red channel holds CLUT indices (alpha 0 = transparent) and the renderer
|
||||
* resolves them through the live CLUT of each draw, so one image serves every
|
||||
* palette the game draws the texture with (highlights, fades). An exact
|
||||
* TTTTTTTT-PPPPPPPP image still wins for its own palette. */
|
||||
#define HD_TEXTURE_PALETTE_INDEXED 0xFFFFFFFFu
|
||||
|
||||
typedef struct HdTexturePackInfo {
|
||||
const char* asset_root; /* valid until hd_texture_pack_destroy */
|
||||
const char* replacement_root; /* valid until hd_texture_pack_destroy */
|
||||
@@ -109,6 +116,8 @@ uint32_t hd_texture_hash_clut(const uint16_t* vram,
|
||||
/* Upload bytes are row-major logical upload words, independent of destination
|
||||
* wrapping. Tracking invalidates/splits every intersected older residency.
|
||||
* Width <= 1024 and height <= 512; X/Y may wrap. */
|
||||
int hd_texture_pack_debug_uploads_at(const HdTexturePack* pack, unsigned x, unsigned y,
|
||||
char* out, size_t cap);
|
||||
int hd_texture_pack_track_upload(HdTexturePack* pack,
|
||||
uint16_t x,
|
||||
uint16_t y,
|
||||
@@ -180,6 +189,27 @@ typedef struct HdTexturePixels {
|
||||
|
||||
void hd_texture_pack_set_decode_budget(HdTexturePack* pack,
|
||||
size_t budget_bytes);
|
||||
/* Elective full preload: decode unambiguous replacements up front and pin
|
||||
* them, bounded by max_bytes of decoded RGBA in aggregate and by max_threads
|
||||
* concurrent decodes (the caller's thread included). Pinned images are kept
|
||||
* outside the on-demand decode budget, which stays as configured. Images not
|
||||
* preloaded (over the cap, unreadable, malformed, a worker failure) are left
|
||||
* to the normal asynchronous, budgeted decode path; nothing is marked failed
|
||||
* by the preload. Returns 1 when every image was preloaded, 0 for a partial
|
||||
* preload (see out), -1 on error. Repeated calls count already pinned images
|
||||
* without reading/decoding them again; max_bytes caps the pack's total pinned
|
||||
* residency. A cap smaller than existing residency is rejected with -1 and
|
||||
* the existing residency reported; it does not discard live pinned images. */
|
||||
typedef struct HdTexturePreloadResult {
|
||||
int requested; /* unambiguous replacements considered */
|
||||
int loaded; /* resident and pinned, including earlier calls */
|
||||
int over_budget; /* skipped: the aggregate max_bytes was reached */
|
||||
int failed; /* unreadable, malformed, too large, or worker error */
|
||||
int worker_errors; /* of failed: a decode that threw (caught) */
|
||||
size_t bytes; /* total decoded RGBA pinned by this pack */
|
||||
} HdTexturePreloadResult;
|
||||
int hd_texture_pack_preload(HdTexturePack* pack, size_t max_bytes,
|
||||
unsigned max_threads, HdTexturePreloadResult* out);
|
||||
int hd_texture_pack_request_decode(HdTexturePack* pack,
|
||||
uint32_t texture_hash,
|
||||
uint32_t palette_hash);
|
||||
|
||||
@@ -56,6 +56,13 @@ void psx_host_overlay_draw(int width, int height); /* renderer side */
|
||||
void psx_host_set_hud_draw_cb(PsxHostOverlayDrawFn fn, void *ctx);
|
||||
void psx_host_hud_draw(int width, int height); /* renderer side */
|
||||
|
||||
/* ---- Post-effect live uniforms ------------------------------------------
|
||||
* Sets the value of a `uniform <name> ...` line of the loaded post-effect chain
|
||||
* ([video] post_shader, postfx_chain.h) from game state, e.g. a light source
|
||||
* projected to the screen. Any thread; effective at the next present; the
|
||||
* preset default applies until the first call. Up to 16 names. */
|
||||
void psx_host_set_post_uniform(const char *name, float x, float y, float z, float w);
|
||||
|
||||
/* ---- P3: input sink -----------------------------------------------------
|
||||
* The sink sees every host SDL event first (main thread), in the normal event
|
||||
* drain and in the host pause loop. Returning non-zero consumes the event.
|
||||
|
||||
@@ -45,10 +45,6 @@ typedef enum HostSpanKind {
|
||||
HSPAN_MOD_WORK, /* emu: resident pack / mod host work */
|
||||
HSPAN_FRAME_PRESENT, /* emu: frontend present for the frame */
|
||||
HSPAN_DISC_READ, /* emu: one sector read from the disc image (arg lba) */
|
||||
HSPAN_HOST_EVENTS, /* emu: pump and dispatch host window events */
|
||||
HSPAN_REWIND_CAPTURE, /* emu: snapshot, store and thumbnail */
|
||||
HSPAN_DISPLAY_PROBE, /* emu: host refresh/mode query */
|
||||
HSPAN_AUDIO_UPDATE, /* emu: host audio pump */
|
||||
HSPAN_KIND_COUNT
|
||||
} HostSpanKind;
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#ifndef PSX_INPUT_REPLAY_H
|
||||
#define PSX_INPUT_REPLAY_H
|
||||
#include <stdint.h>
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Optional host-input replay in every build. Text rows: frame count, hex pad
|
||||
* word (active low). Starts at the first VBlank after a disk state load.
|
||||
* No guest state is stored here; repeated reads of a frame are idempotent. */
|
||||
int input_replay_load(const char *path);
|
||||
void input_replay_on_load(uint64_t next_frame);
|
||||
int input_replay_buttons(uint64_t frame);
|
||||
uint64_t input_replay_frame(uint64_t frame); /* UINT64_MAX while inactive */
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -627,6 +627,7 @@ bool mod_register_function_filter_plugin(const std::string& id, uint32_t address
|
||||
bool mod_register_guest_function_plugin(const std::string& id, uint32_t address,
|
||||
PSXModFunctionEntryCallback callback);
|
||||
bool mod_plugin_registered(const std::string& id);
|
||||
bool mod_register_function_override_marker(const std::string& id);
|
||||
void mod_invoke_activation_plugin(const std::string& id);
|
||||
void mod_invoke_vblank_plugin(const std::string& id);
|
||||
struct ModFunctionEntryHook {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "func_override.h" /* FO_CREDIT_SELF for the credit argument below */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
@@ -135,6 +137,37 @@ int psx_mod_function_entry_active(void);
|
||||
void psx_mod_set_guest_cycle_scale_gate(int open);
|
||||
/* Live scale: the configured one while every gate is open, else 1. */
|
||||
uint32_t psx_mod_guest_cycle_scale(void);
|
||||
|
||||
/*
|
||||
* Package-gated function override (see runtime/include/func_override.h for
|
||||
* the full contract). Registration queues the override under the plugin id;
|
||||
* it is ARMED into the dispatcher tier only when a resolved package plan
|
||||
* selects that plugin — the same gating as vblank/activation callbacks. The
|
||||
* callback obeys the guest ABI (args cpu->gpr[4..7], result cpu->gpr[2]);
|
||||
* return 1 = handled (guest resumes at $ra), 0 = decline (original runs).
|
||||
* The owning plugin resource context is active throughout the callback and
|
||||
* restored across nested callbacks. Snapshots wait until its native stack
|
||||
* unwinds. Wrap semantics via func_override_call_original(); optional residency
|
||||
* guard via expected_words (NULL/0 = unguarded).
|
||||
*
|
||||
* `id` may carry an optional ":label" suffix ("pkg.feature:aim") that names
|
||||
* the override in the `func_override` TCP inventory; gating and manifest
|
||||
* matching use only the part before the ':', so several overrides can sit
|
||||
* under one [[plugin]] entry and still read apart in diagnostics.
|
||||
*
|
||||
* `credit` is the REQUIRED guest-cycle policy (func_override.h, CYCLE
|
||||
* ACCOUNTING): >= 0 cycles charged per handled call, or FO_CREDIT_SELF when
|
||||
* the body charges its own. For a mod whose behavior has no hardware analog
|
||||
* an explicit 0 is the honest value; a wrap that runs the original via
|
||||
* func_override_call_original must pass FO_CREDIT_SELF (the original
|
||||
* self-charges by executing — a fixed credit would double-count).
|
||||
*/
|
||||
typedef int (*PSXModFunctionOverrideFn)(struct CPUState* cpu);
|
||||
int psx_mod_register_function_override(const char* id, uint32_t address,
|
||||
PSXModFunctionOverrideFn fn,
|
||||
const uint32_t* expected_words,
|
||||
int n_words, int32_t credit);
|
||||
|
||||
/* Narrow guest services available to trusted plugin callbacks. */
|
||||
int psx_mod_game_started(void);
|
||||
/* Read an original mounted-disc file without changing guest CD state/timing.
|
||||
|
||||
@@ -113,6 +113,8 @@ void mod_runtime_set_progress_hook(void (*hook)(void));
|
||||
/* Invokes activation callbacks for the committed plan. Call after the final
|
||||
* launcher commit and before renderer/window initialization. */
|
||||
void mod_runtime_activate_plugins(void);
|
||||
/* NULL on success; a non-NULL arming failure requires refusing this launch. */
|
||||
const char* mod_runtime_activation_error(void);
|
||||
void mod_runtime_on_vblank(void);
|
||||
/* Host VBlank query for the active title's optional disc-request provider. */
|
||||
int mod_runtime_disc_request(int *ready);
|
||||
|
||||
@@ -293,6 +293,10 @@ int32_t psx_ws_plane_nx(int32_t nx) {
|
||||
uint32_t psx_ws_xclip_bound(uint32_t vanilla) {
|
||||
return g_cbs.ws_xclip_bound ? g_cbs.ws_xclip_bound(vanilla) : vanilla;
|
||||
}
|
||||
/* [[widescreen.cull.sxy]] fold AND (ws_cull_edge.h). MUST match gpu.c. */
|
||||
uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy) {
|
||||
return psx_ws_sxy_and_value(acc, sxy, psx_ws_x_margin());
|
||||
}
|
||||
uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced) {
|
||||
return g_cbs.ws_cull_keep_result
|
||||
? g_cbs.ws_cull_keep_result(vanilla, forced)
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
* pfx 1 # format version (required first)
|
||||
* pass <name> # starts a pass; the last pass writes the window
|
||||
* shader <file.glsl|builtin:name> # fragment body (see below)
|
||||
* input <sampler> <source|source_down|mask|previous|<pass name>>
|
||||
* input <sampler> <source|source_down|mask|depth|history|previous|<pass name>>
|
||||
* # depth: PGXP view depth sz (GTE screen z, R32F;
|
||||
* # 0 where nothing 3D drew); history: this pass's
|
||||
* # own target from the last real frame (not last pass)
|
||||
* # source_down: the image scaled to this pass's
|
||||
* # target (one bilinear blit, no full-size copy)
|
||||
* scale <f> # target size, fraction of the game rect (default 1)
|
||||
@@ -27,6 +30,7 @@
|
||||
* uniform vec2 u_rcp; // 1 / this pass's target size in px
|
||||
* uniform vec2 u_size; // this pass's target size in px
|
||||
* uniform ivec2 u_org; // window origin of the game rect (gl_FragCoord - u_org = px in it)
|
||||
* uniform float u_time; // seconds since the chain loaded (real and generated frames)
|
||||
* plus one `uniform sampler2D <sampler>;` per input. v_uv is 0..1 over the
|
||||
* game rect, bottom-up like the window. */
|
||||
#ifndef PSX_POSTFX_CHAIN_H
|
||||
@@ -38,7 +42,8 @@ extern "C" {
|
||||
|
||||
enum { PFX_MAX_PASSES = 16, PFX_MAX_INPUTS = 4, PFX_MAX_UNIFORMS = 8, PFX_NAME = 32 };
|
||||
/* Input sources; a value >= 0 is an earlier pass's index. */
|
||||
enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3 };
|
||||
enum { PFX_IN_SOURCE = -1, PFX_IN_MASK = -2, PFX_IN_SOURCE_DOWN = -3, PFX_IN_DEPTH = -4,
|
||||
PFX_IN_HISTORY = -5 };
|
||||
enum { PFX_BLEND_REPLACE = 0, PFX_BLEND_SCREEN = 1 };
|
||||
enum { PFX_FMT_RGBA8 = 0, PFX_FMT_RGBA16F = 1 };
|
||||
|
||||
@@ -69,6 +74,7 @@ typedef struct {
|
||||
char name[PFX_NAME];
|
||||
int npasses;
|
||||
int uses_mask; /* some pass reads `mask` */
|
||||
int uses_depth; /* some pass reads `depth` */
|
||||
PfxPass pass[PFX_MAX_PASSES];
|
||||
} PfxChain;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
typedef struct PfxDebugLive { char name[32]; float v[4]; } PfxDebugLive;
|
||||
typedef struct PfxJsonWriter { char *out; size_t cap,used; int ok; } PfxJsonWriter;
|
||||
static inline void pfx_json_raw(PfxJsonWriter *w,const char *s) {
|
||||
const size_t n=strlen(s);
|
||||
if(!w->ok || n>=w->cap-w->used){w->ok=0;return;}
|
||||
memcpy(w->out+w->used,s,n+1);w->used+=n;
|
||||
}
|
||||
static inline void pfx_json_quote(PfxJsonWriter *w,const char *s) {
|
||||
pfx_json_raw(w,"\"");
|
||||
for(const unsigned char *p=(const unsigned char*)s;*p;++p) {
|
||||
char b[7];
|
||||
if(*p=='"'||*p=='\\'){b[0]='\\';b[1]=(char)*p;b[2]=0;}
|
||||
else if(*p<32)snprintf(b,sizeof b,"\\u%04x",(unsigned)*p);
|
||||
else {b[0]=(char)*p;b[1]=0;}
|
||||
pfx_json_raw(w,b);
|
||||
}
|
||||
pfx_json_raw(w,"\"");
|
||||
}
|
||||
/* Returns 0 with an empty buffer on insufficient space, never partial JSON. */
|
||||
static inline int pfx_debug_json(char *out,int cap,const char *path,const char *status,
|
||||
const PfxDebugLive *live,int count) {
|
||||
if(!out||cap<=0)return 0;
|
||||
out[0]=0;PfxJsonWriter w={out,(size_t)cap,0,1};
|
||||
pfx_json_raw(&w,"\"path\":");pfx_json_quote(&w,path);
|
||||
pfx_json_raw(&w,",\"status\":");pfx_json_quote(&w,status);
|
||||
pfx_json_raw(&w,",\"uniforms\":{");
|
||||
for(int i=0;i<count;++i) {
|
||||
if(i)pfx_json_raw(&w,",");
|
||||
pfx_json_quote(&w,live[i].name);pfx_json_raw(&w,":[");
|
||||
for(int j=0;j<4;++j) {
|
||||
if(j)pfx_json_raw(&w,",");
|
||||
if(!isfinite(live[i].v[j])){w.ok=0;break;}
|
||||
char number[48];snprintf(number,sizeof number,"%.9g",(double)live[i].v[j]);pfx_json_raw(&w,number);
|
||||
}
|
||||
pfx_json_raw(&w,"]");
|
||||
}
|
||||
pfx_json_raw(&w,"}");
|
||||
if(!w.ok){out[0]=0;return 0;}return (int)w.used;
|
||||
}
|
||||
@@ -69,13 +69,6 @@ typedef struct RenderPassStats {
|
||||
double avg_guest_ms; /* smoothed: the plugin's guest draw code */
|
||||
double avg_end_ms; /* smoothed: GPU flush, capture, VRAM restore */
|
||||
double avg_restore_ms; /* smoothed: machine state restore */
|
||||
/* Cumulative wall-time receipts, including discarded/aborted transactions.
|
||||
* Difference two samples for actual window means (the avg_* fields above
|
||||
* are exponential estimates). Verification is kept separate from copying. */
|
||||
uint64_t timed_passes;
|
||||
double total_gl_begin_ms, total_checkpoint_ms, total_verify_before_ms;
|
||||
double total_guest_ms, total_gl_end_ms, total_restore_ms, total_verify_after_ms;
|
||||
double total_pass_ms, max_pass_ms;
|
||||
uint64_t guest_cycles_last; /* guest cycles the last pass executed */
|
||||
int disabled; /* sticky: passes disabled after repeated faults */
|
||||
int watchdog_flag; /* internal: the current pass overran */
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifndef PSX_TEXTURE_PACK_PRELOAD_HPP
|
||||
#define PSX_TEXTURE_PACK_PRELOAD_HPP
|
||||
|
||||
/* Shared machinery for the elective full texture-pack preload
|
||||
* (hd_texture_pack_preload, duck_texture_pack_preload): an aggregate
|
||||
* decoded-byte reservation and a bounded worker pool that can neither leak a
|
||||
* joinable std::thread nor let an exception escape a thread entry. */
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace psx_texpreload {
|
||||
|
||||
/* Test-only fault injection. 1: the first item's decode throws
|
||||
* std::bad_alloc. 2: creating the second pool thread throws std::system_error.
|
||||
* 3: publication fails after the first pinned image. */
|
||||
inline std::atomic<int>& fault_mode() {
|
||||
static std::atomic<int> mode{0};
|
||||
return mode;
|
||||
}
|
||||
inline void after_publish(size_t count) {
|
||||
if (count == 1 && fault_mode().load() == 3) throw std::bad_alloc();
|
||||
}
|
||||
|
||||
/* Aggregate decoded bytes, reserved before an image is decoded. */
|
||||
class Budget {
|
||||
public:
|
||||
explicit Budget(size_t cap) : cap_(cap) {}
|
||||
bool reserve(size_t bytes) {
|
||||
size_t cur = used_.load();
|
||||
do {
|
||||
if (bytes > cap_ || cur > cap_ - bytes) return false;
|
||||
} while (!used_.compare_exchange_weak(cur, cur + bytes));
|
||||
return true;
|
||||
}
|
||||
void release(size_t bytes) { used_.fetch_sub(bytes); }
|
||||
private:
|
||||
size_t cap_;
|
||||
std::atomic<size_t> used_{0};
|
||||
};
|
||||
|
||||
/* Per-item outcome, written by exactly one worker. */
|
||||
enum ItemStatus : unsigned char { kPending = 0, kLoaded, kFailed, kOverBudget };
|
||||
|
||||
/* Joins every started thread when the pool goes out of scope, including while
|
||||
* unwinding from a failed std::thread construction. */
|
||||
struct Joiner {
|
||||
std::vector<std::thread>& threads;
|
||||
~Joiner() {
|
||||
for (auto& t : threads)
|
||||
if (t.joinable()) t.join();
|
||||
}
|
||||
};
|
||||
|
||||
/* Runs item(k) for every k < n on at most max_threads threads, the caller
|
||||
* being one of them. An exception from item(k) is caught and counted, never
|
||||
* propagated out of a thread entry. A failure to start a thread only lowers
|
||||
* concurrency. Returns the number of items whose item(k) threw. */
|
||||
template <class F>
|
||||
size_t run(size_t n, unsigned max_threads, F&& item) {
|
||||
std::atomic<size_t> next{0}, errors{0};
|
||||
auto work = [&]() noexcept {
|
||||
for (size_t k; (k = next.fetch_add(1)) < n;) {
|
||||
try {
|
||||
if (k == 0 && fault_mode().load() == 1) throw std::bad_alloc();
|
||||
item(k);
|
||||
} catch (...) {
|
||||
errors.fetch_add(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
unsigned hw = std::thread::hardware_concurrency();
|
||||
if (hw == 0) hw = 1;
|
||||
const unsigned threads = std::max(1u, std::min(max_threads ? max_threads : 1u, hw));
|
||||
std::vector<std::thread> pool;
|
||||
{
|
||||
Joiner joiner{pool};
|
||||
for (unsigned t = 1; t < threads && t < n; ++t) {
|
||||
try {
|
||||
if (t == 1 && fault_mode().load() == 2)
|
||||
throw std::system_error(
|
||||
std::make_error_code(std::errc::resource_unavailable_try_again));
|
||||
pool.emplace_back(work);
|
||||
} catch (...) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
work();
|
||||
}
|
||||
return errors.load();
|
||||
}
|
||||
|
||||
} // namespace psx_texpreload
|
||||
|
||||
#endif
|
||||
@@ -30,6 +30,18 @@ void psx_video_telemetry_present(int result);
|
||||
* Completion/error counters remain available in that case. */
|
||||
void psx_video_telemetry_snapshot(PsxVideoTelemetry *out);
|
||||
|
||||
typedef struct PsxPresentCadence {
|
||||
int valid;
|
||||
uint32_t count, mean_us, p50_us, p95_us, p99_us, max_us, hitches;
|
||||
double span_ms, fps;
|
||||
} PsxPresentCadence;
|
||||
/* Host API completion intervals, shared by all backends; no monitor-display
|
||||
* guarantee. Bounded passive snapshot. reset is consumed by the presenter. */
|
||||
void psx_video_telemetry_cadence(int window_ms, int reset, PsxPresentCadence *out);
|
||||
int psx_video_telemetry_cadence_json(int window_ms, int reset, char *out, int capacity);
|
||||
/* Explicit monotonic host microseconds for adapters and deterministic tests. */
|
||||
void psx_video_telemetry_present_at(int result, uint64_t now_us);
|
||||
|
||||
/* Emulation-owner settings after runtime mod overrides, sampled every VBlank.
|
||||
* Actual allocated scale/dimensions come from the separate backend snapshot. */
|
||||
typedef struct PsxVideoConfigTelemetry {
|
||||
|
||||
@@ -47,6 +47,26 @@
|
||||
* The `lui` moves to (W + m) << 16 and the `bltz` is taken only while
|
||||
* SX < -m. bltz_sites does not fit this `bltz`: it compares the whole
|
||||
* register with -m, and every negative `SX << 16` is far below that.
|
||||
* `slti rt, x, 0` on the same `SX << 16` value is the same sign test as a
|
||||
* flag instead of a branch, and screen_x_sites widens it identically.
|
||||
*
|
||||
* [[widescreen.cull.sxy]] -- the all-vertices trivial reject over packed GTE
|
||||
* SXY words (Y in the high half, X in the low half), folded with AND:
|
||||
*
|
||||
* and r, v0, mask ; mask holds the sign bits 0x80008000
|
||||
* and r, v1, r
|
||||
* and r, v2, r ; (and r, v3, r for a quad)
|
||||
* bne r, zero, reject
|
||||
*
|
||||
* Bit 31 of r survives only if every Y is negative (all above the screen)
|
||||
* and bit 15 only if every X is negative (all left of it). Each AND of the
|
||||
* chain becomes psx_ws_sxy_and_value(acc, v, m): the vanilla AND, except that
|
||||
* bit 15 also needs this vertex's X < -m. The fold then rejects to the left
|
||||
* only when every vertex is left of the revealed edge. Every other bit,
|
||||
* including the vertical bit 31, is the vanilla AND, and at m == 0 the whole
|
||||
* word is the vanilla AND. Each step reads its operands when the guest's
|
||||
* own AND does, so the chain stays exact whatever the guest does between
|
||||
* steps.
|
||||
* ========================================================================== */
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -92,6 +112,17 @@ static inline int psx_ws_cull_bltz_hi_value(uint32_t v, int32_t m)
|
||||
return (int32_t)v < -((m > 0 ? m : 0) * 65536);
|
||||
}
|
||||
|
||||
/* One AND of a [[widescreen.cull.sxy]] fold: `acc & sxy`, keeping bit 15 (X
|
||||
* sign) only while this vertex's X (low half of `sxy`) is left of -m. */
|
||||
static inline uint32_t psx_ws_sxy_and_value(uint32_t acc, uint32_t sxy,
|
||||
int32_t m)
|
||||
{
|
||||
const uint32_t r = acc & sxy;
|
||||
if (m <= 0 || !(r & 0x8000u)) return r;
|
||||
if ((int32_t)(int16_t)(uint16_t)sxy < -m) return r;
|
||||
return r & ~0x8000u;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -418,7 +418,6 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/render_pass_frame.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dynamic_resolution.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/host_time.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/input_replay.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/quality_tier.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/quality_probe.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/quality_presets.cpp
|
||||
@@ -503,6 +502,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/autocompile.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/code_provider.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/event_ring.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/func_override.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/game_options.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_speed.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_builtin_pgxp.c
|
||||
|
||||
@@ -8778,6 +8778,19 @@ static void handle_bloom(int id, const char *json)
|
||||
bi.gen_us_mean, (unsigned long long)bi.reused);
|
||||
}
|
||||
|
||||
/* post_shader: {"cmd":"post_shader"} reads the user chain (path, load outcome, live
|
||||
* uniforms); "path" sets it, "" turns it off, the same path reloads it (shader edits). */
|
||||
static void handle_post_shader(int id, const char *json)
|
||||
{
|
||||
char path[512];
|
||||
if (json_get_str(json, "path", path, sizeof path)) gl_renderer_set_post_shader(path);
|
||||
char info[1536];
|
||||
if (!gl_renderer_post_shader_debug(info, (int)sizeof info)) {
|
||||
send_err(id, "post shader diagnostics exceed response capacity"); return;
|
||||
}
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"loaded\":%d,%s}", id, gl_renderer_post_shader_loaded(), info);
|
||||
}
|
||||
|
||||
static void handle_post_aa(int id, const char *json)
|
||||
{
|
||||
int mode = json_get_int(json, "mode", -1);
|
||||
@@ -9013,9 +9026,6 @@ static void handle_render_pass_stats(int id, const char *json)
|
||||
"\"last_pass_ms\":%.3f,\"avg_pass_ms\":%.3f,"
|
||||
"\"avg_begin_ms\":%.3f,\"avg_guest_ms\":%.3f,"
|
||||
"\"avg_end_ms\":%.3f,\"avg_restore_ms\":%.3f,"
|
||||
"\"timing\":{\"count\":%llu,\"gl_begin_ms\":%.3f,\"checkpoint_ms\":%.3f,"
|
||||
"\"verify_before_ms\":%.3f,\"guest_ms\":%.3f,\"gl_end_ms\":%.3f,"
|
||||
"\"restore_ms\":%.3f,\"verify_after_ms\":%.3f,\"total_ms\":%.3f,\"max_ms\":%.3f},"
|
||||
"\"guest_cycles_last\":%llu,\"disabled\":%d,"
|
||||
"\"promotions\":%llu,\"pass_presents\":%llu,"
|
||||
"\"blended_presents\":%llu,\"expired\":%llu,\"late_presents\":%llu,"
|
||||
@@ -9049,10 +9059,6 @@ static void handle_render_pass_stats(int id, const char *json)
|
||||
(unsigned long long)st.dropped[RENDER_PASS_DROP_OTHER],
|
||||
st.last_pass_ms, st.avg_pass_ms,
|
||||
st.avg_begin_ms, st.avg_guest_ms, st.avg_end_ms, st.avg_restore_ms,
|
||||
(unsigned long long)st.timed_passes, st.total_gl_begin_ms,
|
||||
st.total_checkpoint_ms, st.total_verify_before_ms, st.total_guest_ms,
|
||||
st.total_gl_end_ms, st.total_restore_ms, st.total_verify_after_ms,
|
||||
st.total_pass_ms, st.max_pass_ms,
|
||||
(unsigned long long)st.guest_cycles_last, st.disabled,
|
||||
(unsigned long long)gd[0], (unsigned long long)gd[1],
|
||||
(unsigned long long)gd[2], (unsigned long long)gd[3],
|
||||
@@ -9454,6 +9460,25 @@ static void handle_ws_aspect(int id, const char *json)
|
||||
}
|
||||
|
||||
extern int psx_debug_display_aspect(int num, int den, int adaptive);
|
||||
/* perf: host API completion cadence across the active presentation backend. */
|
||||
extern int psx_video_telemetry_cadence_json(int window_ms, int reset, char *out, int cap);
|
||||
static void handle_perf(int id, const char *json) {
|
||||
char buf[512];
|
||||
psx_video_telemetry_cadence_json(json_get_int(json, "window_ms", 0), json_get_int(json, "reset", 0),
|
||||
buf, sizeof buf);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,%s}", id, buf);
|
||||
}
|
||||
|
||||
/* hd_uploads x=<vram x> y=<vram y> -- texture uploads covering that VRAM
|
||||
* point (newest first): hash, size, fragment, whether the pack replaces it. */
|
||||
extern int gpu_hd_textures_debug_uploads_at(int x, int y, char *out, size_t cap);
|
||||
static void handle_hd_uploads(int id, const char *json) {
|
||||
static char buf[16384];
|
||||
int n = gpu_hd_textures_debug_uploads_at(json_get_int(json, "x", 0), json_get_int(json, "y", 0),
|
||||
buf, sizeof buf);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"count\":%d,\"uploads\":[%s]}", id, n, buf);
|
||||
}
|
||||
|
||||
static void handle_display_aspect(int id, const char *json) {
|
||||
int num=json_get_int(json,"num",-1), den=json_get_int(json,"den",-1);
|
||||
int adaptive=json_get_int(json,"adaptive",0);
|
||||
@@ -11223,6 +11248,61 @@ static void handle_wide_full(int id, const char *json)
|
||||
id, path, W, H);
|
||||
}
|
||||
|
||||
/* Function-override tier introspection (func_override.h): count + per-entry
|
||||
* {id, addr, calls, guard_misses}. calls == 0 = never reached (wrong address
|
||||
* or path never ran); guard_misses = consults declined by the residency
|
||||
* guard. */
|
||||
static void handle_func_override(int id, const char *json)
|
||||
{
|
||||
extern int func_override_count(void);
|
||||
extern int func_override_get_ex(int index, char *id_out, size_t id_cap,
|
||||
uint32_t *addr_out, uint64_t *calls_out,
|
||||
uint64_t *guard_misses_out,
|
||||
int *guarded_out, int32_t *credit_out);
|
||||
(void)json;
|
||||
char buf[16 * 1024];
|
||||
int n = snprintf(buf, sizeof(buf),
|
||||
"{\"id\":%d,\"ok\":true,\"count\":%d,\"overrides\":[",
|
||||
id, func_override_count());
|
||||
for (int i = 0; i < func_override_count(); i++) {
|
||||
char oid[64];
|
||||
uint32_t addr = 0;
|
||||
uint64_t calls = 0, misses = 0;
|
||||
int guarded = 0;
|
||||
int32_t credit = 0;
|
||||
char creditstr[16];
|
||||
if (!func_override_get_ex(i, oid, sizeof(oid), &addr, &calls,
|
||||
&misses, &guarded, &credit))
|
||||
break;
|
||||
/* credit: the declared cycle policy — a number (fixed per-call
|
||||
* charge) or the string "self" (the body charges its own). */
|
||||
if (credit < 0)
|
||||
snprintf(creditstr, sizeof(creditstr), "\"self\"");
|
||||
else
|
||||
snprintf(creditstr, sizeof(creditstr), "%d", credit);
|
||||
/* Clamp before advancing: snprintf returns the length it WOULD have
|
||||
* written, so on truncation an unclamped n exceeds sizeof(buf) and
|
||||
* the next sizeof(buf) - n underflows to a huge size_t. The 256-byte
|
||||
* headroom check below keeps that unreachable at today's caps, but
|
||||
* raising FO_MAX_ID or FO_MAX_OVERRIDES must not silently turn this
|
||||
* into an overflow. */
|
||||
const int w = snprintf(buf + n, sizeof(buf) - (size_t)n,
|
||||
"%s{\"id\":\"%s\",\"addr\":\"0x%08X\",\"calls\":%llu,"
|
||||
"\"guard_misses\":%llu,\"guarded\":%d,\"credit\":%s}",
|
||||
i ? "," : "", oid, addr,
|
||||
(unsigned long long)calls, (unsigned long long)misses,
|
||||
guarded, creditstr);
|
||||
if (w < 0) break;
|
||||
n += w;
|
||||
if ((size_t)n >= sizeof(buf) - 256) {
|
||||
n = (int)(sizeof(buf) - 256);
|
||||
break;
|
||||
}
|
||||
}
|
||||
snprintf(buf + n, sizeof(buf) - (size_t)n, "]}");
|
||||
send_fmt("%s", buf);
|
||||
}
|
||||
|
||||
static void handle_vram_peek(int id, const char *json)
|
||||
{
|
||||
int x = json_get_int(json, "x", 0);
|
||||
@@ -16322,6 +16402,8 @@ static const CmdEntry s_commands[] = {
|
||||
{ "kernel_bless", handle_kernel_bless },
|
||||
{ "ws_aspect", handle_ws_aspect },
|
||||
{ "display_aspect", handle_display_aspect },
|
||||
{ "perf", handle_perf },
|
||||
{ "hd_uploads", handle_hd_uploads },
|
||||
{ "window_size", handle_window_size },
|
||||
{ "ws_nw", handle_ws_nw },
|
||||
{ "debug_key", handle_debug_key },
|
||||
@@ -16346,6 +16428,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "mmx6_freshfix", handle_mmx6_freshfix },
|
||||
{ "mem_words", handle_mem_words },
|
||||
{ "vram_peek", handle_vram_peek },
|
||||
{ "func_override", handle_func_override },
|
||||
{ "gl_coh_ring", handle_gl_coh_ring },
|
||||
{ "gl_present_ring", handle_gl_present_ring },
|
||||
{ "present_ring", handle_present_ring },
|
||||
@@ -16354,6 +16437,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "frame_perf", handle_frame_perf },
|
||||
{ "batch_sites", handle_batch_sites },
|
||||
{ "post_aa", handle_post_aa },
|
||||
{ "post_shader", handle_post_shader },
|
||||
{ "fmv_chroma", handle_fmv_chroma },
|
||||
{ "texture_lod", handle_texture_lod },
|
||||
{ "accurate_blending", handle_accurate_blending },
|
||||
|
||||
@@ -576,6 +576,12 @@ static inline uint32_t fetch_word(uint32_t phys) {
|
||||
| ((uint32_t)ram[phys + 3] << 24);
|
||||
}
|
||||
|
||||
/* Word reader for the [[widescreen.cull.sxy]] whole-fold check: the fold
|
||||
* words around an interpreted PC are in main RAM like the PC itself. */
|
||||
static uint32_t sxy_fold_word(uint32_t phys) {
|
||||
return phys < 0x00800000u ? fetch_word(phys) : 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
/* Widescreen render-funnel cull detection for the interpreter ([widescreen.cull]
|
||||
* auto_screen_x). A `sltiu …,0x140/0x141` is widened ONLY when its enclosing
|
||||
* render function also carries a `sltiu …,0xE0/0xF1` (the GTE per-vertex trivial-
|
||||
@@ -1137,7 +1143,13 @@ enum { XSITE_INTERP = 0, XSITE_DD = 1 };
|
||||
/* XOP_RES path codes (in the `site` field). */
|
||||
enum { XRES_EC_BAIL = 2, XRES_EC_PC = 3, XRES_EC_CONTRACT = 4, XRES_EC_RET = 5,
|
||||
XRES_OV_BAIL = 6, XRES_OV_PC = 7, XRES_OV_CONTRACT = 8, XRES_OV_RET = 9,
|
||||
XRES_NONLOCAL = 10, XRES_PCCHAIN = 11, XRES_UNDECODABLE = 12 };
|
||||
XRES_NONLOCAL = 10, XRES_PCCHAIN = 11, XRES_UNDECODABLE = 12,
|
||||
/* Appended, NOT slotted into consult order: unlike CRES (which had a
|
||||
* reserved gap at 6) this enum is dense, so inserting would renumber
|
||||
* the codes above and invalidate every previously captured xprobe
|
||||
* trace. The override tier is consulted FIRST at both call sites; the
|
||||
* wire value carries no ordering meaning. */
|
||||
XRES_OVERRIDE = 13 };
|
||||
|
||||
int g_mixed_depth = 0; /* best-effort interp→compiled nesting depth; reset per frame */
|
||||
|
||||
@@ -1786,6 +1798,37 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->pc = target;
|
||||
CRET(CRES_PCCHAIN, 1);
|
||||
}
|
||||
/* Function-override tier (func_override.h): consulted BEFORE
|
||||
* every game code backend, matching psx_dispatch_impl. It must
|
||||
* precede interp_enter_compiled, not follow it: that tier calls
|
||||
* psx_dispatch_game_compiled -> entry->fn(cpu) directly and
|
||||
* never re-enters psx_dispatch_impl, so a hook consulted after
|
||||
* it is unreachable for any override on a statically-compiled
|
||||
* function called from interpreted code. Registration would
|
||||
* still succeed and the func_override inventory would still
|
||||
* list the entry, so the miss is invisible — the same
|
||||
* registration-is-not-coverage failure the overlay-native and
|
||||
* pc-chain tiers below already had. Handled => the override
|
||||
* completed against guest state; same call contract as a
|
||||
* compiled callee. */
|
||||
{
|
||||
extern int (*g_psx_func_override_hook)(CPUState *cpu,
|
||||
uint32_t phys);
|
||||
if (g_psx_func_override_hook) {
|
||||
cpu->pc = 0;
|
||||
if (g_psx_func_override_hook(cpu, target & 0x1FFFFFFFu)) {
|
||||
if (g_psx_call_bail) CRET(CRES_OVERRIDE, 1);
|
||||
if (cpu->pc != 0) CRET(CRES_OVERRIDE, 1);
|
||||
if (rd == 0 || rd == 31) {
|
||||
if (psx_call_contract(cpu, return_pc, site_sp))
|
||||
CRET(CRES_OVERRIDE, 1);
|
||||
}
|
||||
{ int _r = dirty_ram_finish_call_return(cpu, return_pc,
|
||||
next_pc_out);
|
||||
CRET(CRES_OVERRIDE | (_r ? 0x100u : 0u), _r); }
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
cpu->pc = 0;
|
||||
if (interp_enter_compiled(cpu, target)) {
|
||||
@@ -1930,7 +1973,15 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
}
|
||||
case 0x24: { /* AND */
|
||||
uint32_t a = cpu->gpr[rs], b = cpu->gpr[rt];
|
||||
cpu->gpr[rd] = a & b;
|
||||
/* [[widescreen.cull.sxy]] fold AND: the same helper and the same
|
||||
* whole-fold word check as the recompiled emit. */
|
||||
const uint32_t sxy_v = g_psx_ws_sxy_cull_n > 0
|
||||
? psx_ws_sxy_cull_vertex_reg(pc, insn, sxy_fold_word) : 0u;
|
||||
if (sxy_v != 0u)
|
||||
cpu->gpr[rd] = psx_ws_cull_sxy_and(rs == sxy_v ? b : a,
|
||||
cpu->gpr[sxy_v]);
|
||||
else
|
||||
cpu->gpr[rd] = a & b;
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rd], a, b);
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
@@ -2017,6 +2068,27 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
/* 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; }
|
||||
/* Function-override tier: same placement and contract as the JALR
|
||||
* site above — BEFORE interp_enter_compiled, so an override on a
|
||||
* statically-compiled callee is reachable from interpreted code
|
||||
* (reserved CRES_OVERRIDE slot). */
|
||||
{
|
||||
extern int (*g_psx_func_override_hook)(CPUState *cpu,
|
||||
uint32_t phys);
|
||||
if (g_psx_func_override_hook) {
|
||||
cpu->pc = 0;
|
||||
if (g_psx_func_override_hook(cpu, target & 0x1FFFFFFFu)) {
|
||||
if (g_psx_call_bail) { XRES(XRES_OVERRIDE); return 1; }
|
||||
if (cpu->pc != 0) { XRES(XRES_OVERRIDE); return 1; }
|
||||
if (psx_call_contract(cpu, return_pc, site_sp)) {
|
||||
XRES(XRES_OVERRIDE); return 1;
|
||||
}
|
||||
XRES(XRES_OVERRIDE);
|
||||
return dirty_ram_finish_call_return(cpu, return_pc,
|
||||
next_pc_out);
|
||||
}
|
||||
}
|
||||
}
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
cpu->pc = 0;
|
||||
if (interp_enter_compiled(cpu, target)) {
|
||||
@@ -2169,6 +2241,10 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->gpr[rt] = kept;
|
||||
else if (psx_ws_is_cull_depth_site(pc))
|
||||
cpu->gpr[rt] = ((int32_t)cpu->gpr[rs] < psx_ws_depth_bound(simm)) ? 1u : 0u;
|
||||
/* screen_x_sites on `slti rt, x, 0` with `x = SX << 16`: the bltz
|
||||
* sign test as a flag (same order as the recompiled emit). */
|
||||
else if (imm == 0u && psx_ws_is_cull_screen_x_site(pc))
|
||||
cpu->gpr[rt] = (uint32_t)psx_ws_cull_bltz_hi(cpu->gpr[rs]);
|
||||
/* Widescreen render-funnel RIGHT-edge widen (auto_screen_x) for the
|
||||
* signed min/max funnel idiom (`slti v, minSX, W`) — the paired left
|
||||
* edge is the bltz above. Identity at 4:3 (margin 0). */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "texture_image_decode.h"
|
||||
#include "texture_pack_preload.hpp"
|
||||
#define XXH_INLINE_ALL
|
||||
#include "../third_party/xxhash.h"
|
||||
#include "png_write.h"
|
||||
@@ -183,6 +184,7 @@ struct Decode {
|
||||
std::string path;
|
||||
std::shared_ptr<Image> image;
|
||||
uint64_t use = 0;
|
||||
bool pinned = false; /* preloaded: never evicted */
|
||||
};
|
||||
struct Dump { fs::path path; std::shared_ptr<Image> image; };
|
||||
struct DumpHistory { std::unordered_set<std::string> names; std::deque<std::string> order; };
|
||||
@@ -231,6 +233,7 @@ struct Worker {
|
||||
size_t dump_bytes = 0;
|
||||
uint64_t tick = 0;
|
||||
uint64_t decoded_images = 0, decode_evictions = 0;
|
||||
size_t pinned_count = 0; /* preloaded entries: outside the 512-entry cap */
|
||||
bool stop = false;
|
||||
bool dump_busy = false;
|
||||
std::string dump_error;
|
||||
@@ -243,7 +246,7 @@ struct Worker {
|
||||
while (used + incoming > budget) {
|
||||
auto candidate = decoded.end();
|
||||
for (auto it = decoded.begin(); it != decoded.end(); ++it)
|
||||
if (it->second.status == DecodeStatus::Ready &&
|
||||
if (it->second.status == DecodeStatus::Ready && !it->second.pinned &&
|
||||
(candidate == decoded.end() || it->second.use < candidate->second.use)) candidate = it;
|
||||
if (candidate == decoded.end()) break;
|
||||
used -= candidate->second.image->rgba.size();
|
||||
@@ -827,6 +830,29 @@ void duck_texture_pack_invalidate(DuckTexturePack* p, uint16_t x, uint16_t y, ui
|
||||
const unsigned w0 = std::min(unsigned(w), 1024 - px), h0 = std::min(unsigned(h), 512 - py);
|
||||
const std::array<Rect, 4> regions{{{px,py,w0,h0},{0,py,unsigned(w)-w0,h0},
|
||||
{px,0,w0,unsigned(h)-h0},{0,0,unsigned(w)-w0,unsigned(h)-h0}}};
|
||||
/* Under HD authority every native draw lands here, almost always on the
|
||||
* frame buffer, which no tracked texture overlaps: leave without touching
|
||||
* (or allocating) anything when no residency or cache entry intersects. */
|
||||
const auto touches = [&](const Rect& x) {
|
||||
for (const auto& r : regions) if (r.w && r.h && intersects(x,r)) return true;
|
||||
return false;
|
||||
};
|
||||
bool hit = false;
|
||||
for (const auto& upload : p->uploads) if (touches(upload.active)) { hit = true; break; }
|
||||
if (!hit) for (const auto& page : p->pages) {
|
||||
if (touches(page.source->rect)) { hit = true; break; }
|
||||
for (const auto& record : page.source->records)
|
||||
if (record.depth < 2 && touches({record.clut_x,record.clut_y,record.palette_size,1})) { hit = true; break; }
|
||||
if (hit) break;
|
||||
}
|
||||
if (!hit) for (const auto& v : p->rect_cache) if (touches(v.rect)) { hit = true; break; }
|
||||
if (!hit) for (const auto& c : p->match_cache) {
|
||||
const auto& q = c.query;
|
||||
const Rect page{q.page_x,q.page_y,256 / expansion(q.depth),256};
|
||||
const Rect clut{q.clut_x,q.clut_y,std::min(q.depth == 0 ? 16u : 256u,1024u-q.clut_x),1};
|
||||
if (touches(page) || (q.depth < 2 && touches(clut))) { hit = true; break; }
|
||||
}
|
||||
if (!hit) return;
|
||||
++p->revision;
|
||||
bool released = false; /* a residency record dropped or split */
|
||||
for (const auto& r : regions) if (r.w && r.h) {
|
||||
@@ -1118,11 +1144,11 @@ int duck_texture_pack_request_decode(DuckTexturePack* p, uint64_t id) {
|
||||
}
|
||||
if (!w.budget) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
if (w.decode_queue.size() >= kMaxDecodeQueue) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (w.decoded.size() >= 512) {
|
||||
if (w.decoded.size() - w.pinned_count >= 512) {
|
||||
auto victim = w.decoded.end();
|
||||
for (auto item = w.decoded.begin(); item != w.decoded.end(); ++item)
|
||||
if ((item->second.status == DecodeStatus::Ready || item->second.status == DecodeStatus::Failed) &&
|
||||
(victim == w.decoded.end() || item->second.use < victim->second.use)) victim = item;
|
||||
!item->second.pinned && (victim == w.decoded.end() || item->second.use < victim->second.use)) victim = item;
|
||||
if (victim == w.decoded.end()) return HD_TEXTURE_LOOKUP_NONE;
|
||||
if (victim->second.image) w.used -= victim->second.image->rgba.size();
|
||||
w.decoded.erase(victim);
|
||||
@@ -1132,6 +1158,105 @@ int duck_texture_pack_request_decode(DuckTexturePack* p, uint64_t id) {
|
||||
w.wake.notify_one(); return HD_TEXTURE_LOOKUP_NONE;
|
||||
} catch (...) { return HD_TEXTURE_LOOKUP_ERROR; }
|
||||
}
|
||||
int duck_texture_pack_preload(DuckTexturePack* p, size_t max_bytes, unsigned max_threads,
|
||||
HdTexturePreloadResult* out) {
|
||||
HdTexturePreloadResult r{};
|
||||
if (out) *out = r;
|
||||
if (!p) return -1;
|
||||
try {
|
||||
namespace tp = psx_texpreload;
|
||||
Worker& w = p->worker;
|
||||
std::vector<uint64_t> ids;
|
||||
size_t per_image = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(w.mutex);
|
||||
per_image = w.budget;
|
||||
/* The cap and result describe the pack's total pinned residency,
|
||||
* including earlier successful calls or publication prefixes. */
|
||||
for (const auto& item : w.decoded)
|
||||
if (item.second.pinned && item.second.image)
|
||||
r.bytes += item.second.image->rgba.size();
|
||||
for (size_t i = 0; i < p->entries.size(); ++i) {
|
||||
if (p->entries[i].ambiguous) continue;
|
||||
++r.requested;
|
||||
const auto it = w.decoded.find(i + 1);
|
||||
if (it != w.decoded.end() && it->second.pinned) ++r.loaded;
|
||||
}
|
||||
if (r.bytes > max_bytes) {
|
||||
r.failed = r.requested - r.loaded;
|
||||
if (out) *out = r;
|
||||
return -1; /* A smaller cap cannot discard live pinned data. */
|
||||
}
|
||||
for (size_t i = 0; i < p->entries.size(); ++i) {
|
||||
if (p->entries[i].ambiguous) continue;
|
||||
const auto it = w.decoded.find(i + 1);
|
||||
if (it == w.decoded.end() || !it->second.pinned) ids.push_back(i + 1);
|
||||
}
|
||||
}
|
||||
std::vector<std::shared_ptr<Image>> images(ids.size());
|
||||
std::vector<unsigned char> status(ids.size(), tp::kPending);
|
||||
tp::Budget budget(max_bytes - r.bytes);
|
||||
r.worker_errors = static_cast<int>(tp::run(ids.size(), max_threads, [&](size_t k) {
|
||||
status[k] = tp::kFailed;
|
||||
const std::string& path = p->entries[ids[k] - 1].path;
|
||||
std::ifstream input(fs::u8path(path), std::ios::binary | std::ios::ate);
|
||||
const auto length = input ? input.tellg() : std::streampos(-1);
|
||||
if (length <= 0 || static_cast<uint64_t>(length) > kMaxEncoded) return;
|
||||
std::vector<uint8_t> encoded(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
if (!input.read(reinterpret_cast<char*>(encoded.data()), length)) return;
|
||||
TextureImageProbe probe; std::string error;
|
||||
if (!texture_image_probe(encoded.data(), encoded.size(), per_image, probe, error)) return;
|
||||
const size_t need = static_cast<size_t>(probe.width) * probe.height * 4u;
|
||||
if (!budget.reserve(need)) { status[k] = tp::kOverBudget; return; }
|
||||
struct Reservation {
|
||||
tp::Budget& b; size_t n; bool keep = false;
|
||||
~Reservation() { if (!keep) b.release(n); }
|
||||
} reservation{budget, need};
|
||||
DecodedTextureImage decoded;
|
||||
if (!texture_image_decode(encoded.data(), encoded.size(), need, decoded, error) ||
|
||||
decoded.rgba.size() != need) return;
|
||||
auto image = std::make_shared<Image>(); image->w = decoded.width; image->h = decoded.height;
|
||||
image->rgba = std::move(decoded.rgba);
|
||||
images[k] = std::move(image);
|
||||
reservation.keep = true;
|
||||
status[k] = tp::kLoaded;
|
||||
}));
|
||||
std::lock_guard<std::mutex> lock(w.mutex);
|
||||
for (size_t k = 0; k < ids.size(); ++k) {
|
||||
if (status[k] == tp::kOverBudget) { ++r.over_budget; continue; }
|
||||
if (status[k] != tp::kLoaded) continue;
|
||||
auto it = w.decoded.find(ids[k]);
|
||||
if (it != w.decoded.end()) {
|
||||
/* Already pinned, or an on-demand decode in flight: leave it. */
|
||||
if (it->second.pinned || it->second.status == DecodeStatus::Queued ||
|
||||
it->second.status == DecodeStatus::Loading) continue;
|
||||
}
|
||||
const size_t previous_bytes = it != w.decoded.end() && it->second.image
|
||||
? it->second.image->rgba.size() : 0;
|
||||
Decode entry;
|
||||
/* Prepare throwing metadata before touching an existing entry. */
|
||||
entry.path = p->entries[ids[k] - 1].path;
|
||||
entry.image = std::move(images[k]); entry.status = DecodeStatus::Ready;
|
||||
entry.pinned = true; entry.use = ++w.tick;
|
||||
const size_t bytes = entry.image->rgba.size();
|
||||
if (it == w.decoded.end()) w.decoded.emplace(ids[k], std::move(entry));
|
||||
else it->second = std::move(entry);
|
||||
w.used -= previous_bytes;
|
||||
r.bytes += bytes;
|
||||
++w.pinned_count; ++r.loaded;
|
||||
++w.decoded_images;
|
||||
tp::after_publish(r.loaded);
|
||||
}
|
||||
r.failed = r.requested - r.loaded - r.over_budget;
|
||||
if (out) *out = r;
|
||||
return r.loaded == r.requested ? 1 : 0;
|
||||
} catch (...) {
|
||||
r.failed = r.requested - r.loaded - r.over_budget;
|
||||
if (out) *out = r; /* Account the pinned prefix even on publication failure. */
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
int duck_texture_pack_acquire_decoded(DuckTexturePack* p, uint64_t id, DuckTexturePixels* out) {
|
||||
if (out) *out = {};
|
||||
if (!p || !out) return HD_TEXTURE_LOOKUP_ERROR;
|
||||
|
||||
@@ -45,7 +45,7 @@ int frame_interpolation_schedule_begin_phase(FrameInterpolationSchedule *schedul
|
||||
* Normal guest work lands after the prior deadline but before the next one;
|
||||
* retaining the old anchor makes that work consume the pacing budget. */
|
||||
if (schedule->source_deadline <= 0.0 ||
|
||||
(!schedule->queued && now_d + source_period < schedule->source_deadline) ||
|
||||
now_d + source_period < schedule->source_deadline ||
|
||||
now_d > schedule->source_deadline + source_period * 4.0) {
|
||||
schedule->frame_start = now_d;
|
||||
schedule->source_deadline = now_d + source_period;
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
/* func_override.c — replace a guest function with hand-written C.
|
||||
* See runtime/include/func_override.h.
|
||||
*
|
||||
* CLAUDE.md rule 3 (no printf, no logs): this module prints nothing. State
|
||||
* is exposed through the accessors and surfaced by the `func_override` TCP
|
||||
* command. Registration errors are returned as codes for the caller to
|
||||
* report.
|
||||
*/
|
||||
|
||||
#include "func_override.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "psx_cycles.h"
|
||||
|
||||
/* Residency-guard reads go through the UNTRACED peek, never psx_read_word.
|
||||
* psx_read_word is a traced accessor (memory.c): under lockstep it feeds
|
||||
* ls_read_hook, under lockstep replay it RETURNS the replayed value instead
|
||||
* of RAM, and under DuckStation recording it calls ds_note_read. The guard
|
||||
* is a host-side residency check, not a guest memory access — routing it
|
||||
* through the traced path injects phantom reads the oracle side never
|
||||
* performs (dirtying every divergence comparison) and makes the guard
|
||||
* compare replayed data rather than resident bytes. The peek keeps the full
|
||||
* address decode, so a guard on a non-RAM address stays correct. */
|
||||
extern uint32_t psx_peek_word_untraced(uint32_t addr);
|
||||
|
||||
/* Set by us, read at the top of every psx_dispatch_impl (emitted by
|
||||
* recompiler/src/full_function_emitter.cpp) and at the interpreter's
|
||||
* JAL/JALR call-resolution tiers (dirty_ram_interp.c). NULL until install,
|
||||
* so a build with no overrides dispatches exactly as before. */
|
||||
int (*g_psx_func_override_hook)(CPUState *cpu, uint32_t phys) = NULL;
|
||||
|
||||
typedef struct {
|
||||
char id[FO_MAX_ID];
|
||||
uint32_t phys; /* normalised: KSEG bits masked off */
|
||||
FuncOverrideFn fn;
|
||||
FuncOverrideInvoker invoke;
|
||||
const void *context;
|
||||
uint64_t calls;
|
||||
uint64_t guard_misses;
|
||||
uint32_t guard[FO_MAX_GUARD_WORDS];
|
||||
int n_guard; /* 0 = unguarded */
|
||||
int package; /* 1 = armed by a mod package plan */
|
||||
int32_t credit; /* >= 0 charged per handled call;
|
||||
FO_CREDIT_SELF = body self-charges */
|
||||
} Entry;
|
||||
|
||||
static Entry s_entries[FO_MAX_OVERRIDES];
|
||||
static int s_count = 0;
|
||||
|
||||
/* call_original support: the address whose hook is currently executing
|
||||
* (for redial) and a one-shot bypass armed by func_override_call_original.
|
||||
* Dispatch is single-threaded; a nested override (an override whose
|
||||
* guest_call reaches another overridden function) saves/restores around
|
||||
* the inner consult, so plain statics are correct here. */
|
||||
static uint32_t s_active_phys = 0;
|
||||
static uint32_t s_bypass_phys = 0;
|
||||
|
||||
static uint32_t normalise(uint32_t addr) { return addr & 0x1FFFFFFFu; }
|
||||
|
||||
static Entry *find(uint32_t phys)
|
||||
{
|
||||
for (int i = 0; i < s_count; i++)
|
||||
if (s_entries[i].phys == phys)
|
||||
return &s_entries[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int add_common(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *guard, int n_guard, int package,
|
||||
int32_t credit)
|
||||
{
|
||||
if (!fn) return FO_ERR_ARGS;
|
||||
if (n_guard < 0 || n_guard > FO_MAX_GUARD_WORDS) return FO_ERR_ARGS;
|
||||
if (n_guard > 0 && !guard) return FO_ERR_ARGS;
|
||||
/* The credit is a required statement of timing intent (header, CYCLE
|
||||
* ACCOUNTING): a fixed per-call charge, or FO_CREDIT_SELF. Any other
|
||||
* negative value is a typo, not a policy. */
|
||||
if (credit < FO_CREDIT_SELF) return FO_ERR_ARGS;
|
||||
if (s_count >= FO_MAX_OVERRIDES) return FO_ERR_FULL;
|
||||
|
||||
const uint32_t phys = normalise(addr);
|
||||
/* phys 0 is the "not inside an override" sentinel for s_active_phys and
|
||||
* the "no bypass armed" sentinel for s_bypass_phys, so an override there
|
||||
* would silently break func_override_call_original. No real function
|
||||
* lives at guest 0 anyway. */
|
||||
if (phys == 0) return FO_ERR_ARGS;
|
||||
/* Two overrides on one address is always a mistake, and a silent
|
||||
* last-wins would be untraceable. Refuse it. */
|
||||
if (find(phys)) return FO_ERR_DUPLICATE;
|
||||
|
||||
Entry *e = &s_entries[s_count];
|
||||
memset(e, 0, sizeof(*e));
|
||||
if (id) {
|
||||
strncpy(e->id, id, FO_MAX_ID - 1);
|
||||
e->id[FO_MAX_ID - 1] = '\0';
|
||||
}
|
||||
e->phys = phys;
|
||||
e->fn = fn;
|
||||
for (int i = 0; i < n_guard; i++) e->guard[i] = guard[i];
|
||||
e->n_guard = n_guard;
|
||||
e->package = package ? 1 : 0;
|
||||
e->credit = credit;
|
||||
s_count++;
|
||||
return FO_OK;
|
||||
}
|
||||
|
||||
int func_override_add(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
int32_t credit)
|
||||
{
|
||||
return add_common(id, addr, fn, NULL, 0, 0, credit);
|
||||
}
|
||||
|
||||
int func_override_add_guarded(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words,
|
||||
int32_t credit)
|
||||
{
|
||||
if (n_words < 1) return FO_ERR_ARGS;
|
||||
return add_common(id, addr, fn, expected_words, n_words, 0, credit);
|
||||
}
|
||||
|
||||
int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words,
|
||||
int32_t credit)
|
||||
{
|
||||
if (n_words < 0 || n_words > FO_MAX_GUARD_WORDS) return FO_ERR_ARGS;
|
||||
return add_common(id, addr, fn, n_words ? expected_words : NULL, n_words,
|
||||
1, credit);
|
||||
}
|
||||
|
||||
int func_override_add_package_scoped(const char *id, uint32_t addr, FuncOverrideFn fn,
|
||||
const uint32_t *expected_words, int n_words, int32_t credit,
|
||||
FuncOverrideInvoker invoke, const void *context)
|
||||
{
|
||||
if (!invoke || !context) return FO_ERR_ARGS;
|
||||
const int rc = func_override_add_package(id,addr,fn,expected_words,n_words,credit);
|
||||
if (rc == FO_OK) {
|
||||
Entry *entry = &s_entries[s_count-1];
|
||||
entry->invoke = invoke;
|
||||
entry->context = context;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
int func_override_active(void) { return s_active_phys != 0; }
|
||||
|
||||
/* The hook. Runs on EVERY dispatch, so the common path — nothing registered
|
||||
* for this address — must stay cheap. A linear scan over a handful of
|
||||
* entries beats a hash for realistic counts and keeps the code honest;
|
||||
* revisit if a game ever registers hundreds. */
|
||||
static int hook(CPUState *cpu, uint32_t phys)
|
||||
{
|
||||
for (int i = 0; i < s_count; i++) {
|
||||
Entry *e = &s_entries[i];
|
||||
if (e->phys != phys) continue;
|
||||
/* One-shot bypass: func_override_call_original re-dispatched this
|
||||
* address; let the original backend take it exactly once. */
|
||||
if (s_bypass_phys == phys) {
|
||||
s_bypass_phys = 0;
|
||||
return 0;
|
||||
}
|
||||
/* Residency guard: wrong bytes at the address (overlay swapped,
|
||||
* page reused) means the function this override targets is not
|
||||
* resident — decline, never corrupt. */
|
||||
if (e->n_guard) {
|
||||
const uint32_t base = 0x80000000u | phys;
|
||||
int miss = 0;
|
||||
for (int w = 0; w < e->n_guard; w++)
|
||||
if (psx_peek_word_untraced(base + (uint32_t)(w * 4)) !=
|
||||
e->guard[w]) {
|
||||
miss = 1;
|
||||
break;
|
||||
}
|
||||
if (miss) {
|
||||
e->guard_misses++;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
/* Count the CONSULT, not just the handled case: an override that
|
||||
* declines still proves the address was reached through a hooked
|
||||
* path, which is exactly what a diagnostic probe needs (calls == 0
|
||||
* means "this call never crosses a hook", the bypass class of bug
|
||||
* that hid the interp local-chain gap). */
|
||||
e->calls++;
|
||||
{
|
||||
const uint32_t saved_active = s_active_phys;
|
||||
s_active_phys = phys;
|
||||
const int handled = (e->invoke ? e->invoke(cpu,e->fn,e->context) : e->fn(cpu)) ? 1 : 0;
|
||||
s_active_phys = saved_active;
|
||||
/* Charge the declared credit only when HANDLED: a decline runs
|
||||
* the original, which accrues its exact cycles by executing.
|
||||
* FO_CREDIT_SELF (< 0) and 0 charge nothing here — SELF bodies
|
||||
* called psx_advance_cycles themselves, and guest work the body
|
||||
* ran via func_override_guest_call / call_original has already
|
||||
* self-charged through the normal backends. */
|
||||
if (handled && e->credit > 0)
|
||||
psx_advance_cycles((uint32_t)e->credit);
|
||||
return handled;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void func_override_guest_call(CPUState *cpu, uint32_t target, uint32_t site_ra)
|
||||
{
|
||||
/* Spoof $ra to the original call site so callees, fntrace and crash
|
||||
* forensics see authentic values, then restore. psx_dispatch_call runs
|
||||
* the callee to completion (pc == site_ra with the caller's $sp). */
|
||||
extern void psx_dispatch_call(CPUState *cpu, uint32_t addr,
|
||||
uint32_t return_addr);
|
||||
const uint32_t saved_ra = cpu->gpr[31];
|
||||
cpu->gpr[31] = site_ra;
|
||||
psx_dispatch_call(cpu, target, site_ra);
|
||||
cpu->gpr[31] = saved_ra;
|
||||
}
|
||||
|
||||
void func_override_call_original(CPUState *cpu)
|
||||
{
|
||||
if (!s_active_phys) return; /* not inside an override — nothing to do */
|
||||
const uint32_t phys = s_active_phys;
|
||||
const uint32_t saved = s_bypass_phys;
|
||||
s_bypass_phys = phys;
|
||||
/* KSEG0 form: game text dispatches with the 0x8000_0000 view; the hook
|
||||
* itself keys on the normalised address either way. Return to the
|
||||
* override's caller frame: the original must observe the same $ra the
|
||||
* override was entered with. */
|
||||
func_override_guest_call(cpu, 0x80000000u | phys, cpu->gpr[31]);
|
||||
s_bypass_phys = saved;
|
||||
}
|
||||
|
||||
void func_override_install(void)
|
||||
{
|
||||
/* Leave the hook NULL when empty: dispatch then matches a build without
|
||||
* the tier exactly, which keeps "overrides cost nothing" literally
|
||||
* true. */
|
||||
g_psx_func_override_hook = (s_count > 0) ? hook : NULL;
|
||||
}
|
||||
|
||||
int func_override_reset_package_armed(void)
|
||||
{
|
||||
/* Drop every package-armed entry and compact, keeping direct
|
||||
* registrations. Direct ones are the game's own always-on faithful
|
||||
* reimplementations: not player-toggleable, identical in both peers'
|
||||
* builds, so netplay has no reason to disarm them. Package-armed ones
|
||||
* ARE player-selected, so a cleared plan must leave no trace of them —
|
||||
* without this the entry survives in the table with the hook still
|
||||
* installed and keeps firing after the vanilla-session banner prints.
|
||||
*
|
||||
* Any in-flight bypass/active marker refers to an entry that may be
|
||||
* disappearing, so clear both rather than leave a dangling address. */
|
||||
int removed = 0;
|
||||
int w = 0;
|
||||
for (int i = 0; i < s_count; i++) {
|
||||
if (s_entries[i].package) { removed++; continue; }
|
||||
if (w != i) s_entries[w] = s_entries[i];
|
||||
w++;
|
||||
}
|
||||
s_count = w;
|
||||
if (removed) {
|
||||
s_active_phys = 0;
|
||||
s_bypass_phys = 0;
|
||||
func_override_install(); /* follows s_count back to NULL if empty */
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
int func_override_count(void) { return s_count; }
|
||||
|
||||
int func_override_get(int index, char *id_out, size_t id_cap,
|
||||
uint32_t *addr_out, uint64_t *calls_out)
|
||||
{
|
||||
return func_override_get_ex(index, id_out, id_cap, addr_out, calls_out,
|
||||
NULL, NULL, NULL);
|
||||
}
|
||||
|
||||
int func_override_get_ex(int index, char *id_out, size_t id_cap,
|
||||
uint32_t *addr_out, uint64_t *calls_out,
|
||||
uint64_t *guard_misses_out, int *guarded_out,
|
||||
int32_t *credit_out)
|
||||
{
|
||||
if (index < 0 || index >= s_count) return 0;
|
||||
const Entry *e = &s_entries[index];
|
||||
/* Bounded copy: the caller states its buffer size rather than being
|
||||
* silently required to supply FO_MAX_ID bytes. Always NUL-terminated. */
|
||||
if (id_out && id_cap) {
|
||||
strncpy(id_out, e->id, id_cap - 1);
|
||||
id_out[id_cap - 1] = '\0';
|
||||
}
|
||||
if (addr_out) *addr_out = e->phys;
|
||||
if (calls_out) *calls_out = e->calls;
|
||||
if (guard_misses_out) *guard_misses_out = e->guard_misses;
|
||||
if (guarded_out) *guarded_out = e->n_guard;
|
||||
if (credit_out) *credit_out = e->credit;
|
||||
return 1;
|
||||
}
|
||||
+224
-11
@@ -1,3 +1,8 @@
|
||||
#include "gte_postfx_projection.h"
|
||||
#include "postfx_debug_json.h"
|
||||
#ifndef PFX_NOW_TICKS
|
||||
#define PFX_NOW_TICKS() SDL_GetPerformanceCounter()
|
||||
#endif
|
||||
/* ---- Post-effect pipeline (docs/POST_EFFECTS.md) ---------------------------
|
||||
* One executor for every post effect on the composed game image: chains of
|
||||
* full-screen fragment passes (postfx_chain.h). Three stages, in order, each
|
||||
@@ -36,16 +41,20 @@ typedef struct {
|
||||
GLint fixed[PFX_LOC_FIXED];
|
||||
GLint in[PFX_MAX_INPUTS];
|
||||
GLint u[PFX_MAX_UNIFORMS];
|
||||
GLint time, proj;
|
||||
} PfxLocs;
|
||||
typedef struct {
|
||||
PfxChain chain;
|
||||
int loaded, failed;
|
||||
uint64_t started_at;
|
||||
GLuint prog[PFX_MAX_PASSES];
|
||||
PfxLocs loc[PFX_MAX_PASSES];
|
||||
GLuint tex[PFX_MAX_PASSES], fbo[PFX_MAX_PASSES];
|
||||
GLuint dtex[PFX_MAX_PASSES], dfbo[PFX_MAX_PASSES]; /* source_down copies */
|
||||
int dw[PFX_MAX_PASSES], dh[PFX_MAX_PASSES];
|
||||
int tw[PFX_MAX_PASSES], th[PFX_MAX_PASSES];
|
||||
GLuint htex[PFX_MAX_PASSES], hfbo[PFX_MAX_PASSES]; /* `history` copies */
|
||||
int hw[PFX_MAX_PASSES], hh[PFX_MAX_PASSES], hist[PFX_MAX_PASSES];
|
||||
} PfxStage;
|
||||
enum { PFX_STAGE_BLOOM, PFX_STAGE_USER, PFX_STAGE_AA, PFX_STAGE_HQ, PFX_NSTAGES };
|
||||
static PfxStage s_pfx[PFX_NSTAGES];
|
||||
@@ -78,6 +87,49 @@ static GLint s_mmap_uUv = -1, s_mmap_uS = -1, s_mmap_uSrc = -1, s_mmap_uDst = -
|
||||
* depth-stencil blit is needed (one costs the CPU milliseconds on macOS). */
|
||||
typedef struct { GLuint rb, tex, fbo; int w, h; } PfxSurfMask;
|
||||
static PfxSurfMask s_smask[2];
|
||||
/* `depth`: each surface's depth-stencil renderbuffer is blitted into a depth
|
||||
* texture of its size (dtex/dfbo), then mapped to the window as view depth sz
|
||||
* (GTE screen z; PGXP writes zn = 1 - 512/(sz + 256)) in an R32F texture. */
|
||||
typedef struct { GLuint rb, dtex, dfbo; int w, h; } PfxSurfDepth;
|
||||
static PfxSurfDepth s_sdepth[2];
|
||||
static GLuint s_depth_tex = 0, s_depth_fbo = 0, s_depth_map_prog = 0;
|
||||
static GLint s_dmap_uUv = -1, s_dmap_uS = -1, s_dmap_uSrc = -1, s_dmap_uDst = -1;
|
||||
static int s_depth_w = 0, s_depth_h = 0, s_depth_ok = 0;
|
||||
static int s_pfx_depth_on = 0; /* a loaded chain reads `depth` */
|
||||
/* Live values for preset `uniform` lines, set by the title from game state
|
||||
* (psx_host_set_post_uniform, host_overlay.h): a live value replaces the
|
||||
* preset default of the same name in every pass that declares it. */
|
||||
enum { PFX_LIVE_MAX = 16 };
|
||||
typedef PfxDebugLive PfxLive;
|
||||
static PfxLive s_pfx_live[PFX_LIVE_MAX];
|
||||
static int s_pfx_nlive = 0;
|
||||
static SDL_SpinLock s_pfx_live_lock = 0;
|
||||
void psx_host_set_post_uniform(const char *name, float x, float y, float z, float w) {
|
||||
if (!name || !name[0] || strlen(name) >= sizeof s_pfx_live[0].name ||
|
||||
!isfinite(x) || !isfinite(y) || !isfinite(z) || !isfinite(w)) return;
|
||||
SDL_LockSpinlock(&s_pfx_live_lock);
|
||||
int i = 0;
|
||||
while (i < s_pfx_nlive && strcmp(s_pfx_live[i].name, name)) i++;
|
||||
if (i == s_pfx_nlive && s_pfx_nlive < PFX_LIVE_MAX) {
|
||||
snprintf(s_pfx_live[i].name, sizeof s_pfx_live[i].name, "%s", name);
|
||||
s_pfx_nlive++;
|
||||
}
|
||||
if (i < s_pfx_nlive) {
|
||||
s_pfx_live[i].v[0] = x; s_pfx_live[i].v[1] = y; s_pfx_live[i].v[2] = z; s_pfx_live[i].v[3] = w;
|
||||
}
|
||||
SDL_UnlockSpinlock(&s_pfx_live_lock);
|
||||
}
|
||||
/* Last load outcome of the user chain, for the debug server. */
|
||||
static char s_pfx_user_status[192] = "";
|
||||
static int pfx_live_get(const char *name, float out[4]) {
|
||||
int found = 0;
|
||||
SDL_LockSpinlock(&s_pfx_live_lock);
|
||||
for (int i = 0; i < s_pfx_nlive; i++)
|
||||
if (!strcmp(s_pfx_live[i].name, name)) { memcpy(out, s_pfx_live[i].v, sizeof s_pfx_live[i].v); found = 1; break; }
|
||||
SDL_UnlockSpinlock(&s_pfx_live_lock);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int s_mask_w = 0, s_mask_h = 0;
|
||||
|
||||
static int pfx_stage_on(int i) {
|
||||
@@ -102,22 +154,30 @@ static void pfx_stage_unload(PfxStage *st) {
|
||||
if (st->tex[i]) glDeleteTextures(1, &st->tex[i]);
|
||||
if (st->dfbo[i]) p_glDeleteFramebuffers(1, &st->dfbo[i]);
|
||||
if (st->dtex[i]) glDeleteTextures(1, &st->dtex[i]);
|
||||
if (st->hfbo[i]) p_glDeleteFramebuffers(1, &st->hfbo[i]);
|
||||
if (st->htex[i]) glDeleteTextures(1, &st->htex[i]);
|
||||
}
|
||||
st->htex[i] = st->hfbo[i] = 0; st->hw[i] = st->hh[i] = 0; st->hist[i] = 0;
|
||||
st->prog[i] = st->tex[i] = st->fbo[i] = 0; st->tw[i] = st->th[i] = 0;
|
||||
st->dtex[i] = st->dfbo[i] = 0; st->dw[i] = st->dh[i] = 0;
|
||||
}
|
||||
pfx_chain_free(&st->chain);
|
||||
st->loaded = st->failed = 0;
|
||||
st->started_at = 0;
|
||||
}
|
||||
|
||||
/* Which stages read the mask decides whether draws keep it. */
|
||||
static void pfx_mask_wanted_update(void) {
|
||||
gte_postfx_projection_enable(postfx_active());
|
||||
int want = 0;
|
||||
for (int i = 0; i < PFX_NSTAGES; i++) {
|
||||
if (!pfx_stage_on(i)) continue;
|
||||
if (i == PFX_STAGE_BLOOM) want = 1; /* the bloom preset reads it */
|
||||
else if (s_pfx[i].loaded && s_pfx[i].chain.uses_mask) want = 1;
|
||||
}
|
||||
s_pfx_depth_on = 0;
|
||||
for (int i = 0; i < PFX_NSTAGES; i++)
|
||||
if (pfx_stage_on(i) && s_pfx[i].loaded && s_pfx[i].chain.uses_depth) s_pfx_depth_on = 1;
|
||||
if (want != s_bcls_on) { s_bcls_on = want; pgxp_render_wanted_update(); }
|
||||
}
|
||||
|
||||
@@ -135,26 +195,41 @@ static int pfx_stage_load(int i) {
|
||||
if (!ok) {
|
||||
fprintf(stdout, "psxrecomp: post effect '%s' not loaded: %s\n",
|
||||
i == PFX_STAGE_USER ? s_pfx_user_path : "built-in", err);
|
||||
if (i == PFX_STAGE_USER) snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "not loaded: %s", err);
|
||||
st->failed = 1;
|
||||
return 0;
|
||||
}
|
||||
st->loaded = 1;
|
||||
if (i == PFX_STAGE_USER)
|
||||
for (int p = 0; p < st->chain.npasses; p++)
|
||||
for (int k = 0; k < st->chain.pass[p].ninputs; k++)
|
||||
if (st->chain.pass[p].in[k].from == PFX_IN_HISTORY) st->hist[p] = 1;
|
||||
if (i == PFX_STAGE_USER) {
|
||||
fprintf(stdout, "psxrecomp: post shader '%s' (%d passes%s)\n", s_pfx_user_path,
|
||||
st->chain.npasses, st->chain.uses_mask ? ", mask" : "");
|
||||
snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "parsed: %d passes", st->chain.npasses);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static uint64_t s_pfx_builds = 0; /* program builds (tests) */
|
||||
static GLuint pfx_build_pass(const PfxPass *ps) {
|
||||
s_pfx_builds++;
|
||||
size_t n = strlen(ps->shader) + 512 + (size_t)ps->ninputs * 64;
|
||||
size_t n = strlen(ps->shader) + 512 + (size_t)ps->ninputs * 64 + (size_t)ps->nuniforms * 64;
|
||||
char *src = (char *)malloc(n);
|
||||
if (!src) return 0;
|
||||
int k = snprintf(src, n, "#version 330\nin vec2 v_uv; out vec4 o;\n"
|
||||
"uniform vec2 u_rcp; uniform vec2 u_size; uniform ivec2 u_org;\n");
|
||||
"uniform vec2 u_rcp; uniform vec2 u_size; uniform ivec2 u_org;\n"
|
||||
"uniform float u_time; uniform vec4 u_proj;\n");
|
||||
for (int i = 0; i < ps->ninputs; i++)
|
||||
k += snprintf(src + k, n - (size_t)k, "uniform sampler2D %s;\n", ps->in[i].sampler);
|
||||
/* The preset's `uniform` lines are declared here unless the body declares
|
||||
* them itself (the built-in shaders do). */
|
||||
static const char *const kTypes[] = { "float", "vec2", "vec3", "vec4" };
|
||||
for (int i = 0; i < ps->nuniforms && (size_t)k + 96 < n; i++) {
|
||||
char decl[PFX_NAME + 16];
|
||||
snprintf(decl, sizeof decl, "%s %s;", kTypes[ps->u[i].count - 1], ps->u[i].name);
|
||||
if (!strstr(ps->shader, decl)) k += snprintf(src + k, n - (size_t)k, "uniform %s\n", decl);
|
||||
}
|
||||
snprintf(src + k, n - (size_t)k, "%s", ps->shader);
|
||||
GLuint prog = build_program(PRESENT_VS, src);
|
||||
free(src);
|
||||
@@ -188,7 +263,7 @@ static void pfx_rect_px(GLuint tex, float tex_w, float tex_h, int x, int y, int
|
||||
static void postfx_note_center(GLuint fbo, GLuint tex, float tex_w, float tex_h,
|
||||
int x, int y, int w, int h, int off, int wide_w) {
|
||||
s_mask_ctr_valid = 0;
|
||||
if (!s_bcls_on || !fbo || !tex || wide_w <= 0) return;
|
||||
if ((!s_bcls_on && !s_pfx_depth_on) || !fbo || !tex || wide_w <= 0) return;
|
||||
s_mask_ctr_rb = pfx_surface_rb(fbo);
|
||||
if (!s_mask_ctr_rb) return;
|
||||
pfx_rect_px(tex, tex_w, tex_h, x, y, w, h, s_mask_ctr, &s_mask_ctr_tw, &s_mask_ctr_th);
|
||||
@@ -204,7 +279,7 @@ static void postfx_note_source(GLuint fbo, GLuint tex, float tex_w, float tex_h,
|
||||
int x, int y, int w, int h) {
|
||||
s_mask_src_valid = 0;
|
||||
s_mask_ctr_valid = 0;
|
||||
if (!s_bcls_on || !fbo || !tex || tex_w <= 0.0f || tex_h <= 0.0f) return;
|
||||
if ((!s_bcls_on && !s_pfx_depth_on) || !fbo || !tex || tex_w <= 0.0f || tex_h <= 0.0f) return;
|
||||
s_mask_src_rb = pfx_surface_rb(fbo);
|
||||
if (!s_mask_src_rb) return;
|
||||
pfx_rect_px(tex, tex_w, tex_h, x, y, w, h, s_mask_src, &s_mask_src_tw, &s_mask_src_th);
|
||||
@@ -217,6 +292,7 @@ static void pfx_tex_alloc(GLuint *t, int w, int h, int fmt, int nearest) {
|
||||
glBindTexture(GL_TEXTURE_2D, *t);
|
||||
if (fmt == PFX_FMT_RGBA16F) glTexImage2D(GL_TEXTURE_2D, 0, 0x881A /*RGBA16F*/, w, h, 0, GL_RGBA, GL_FLOAT, NULL);
|
||||
else if (fmt == -1) glTexImage2D(GL_TEXTURE_2D, 0, 0x8229 /*R8*/, w, h, 0, 0x1903 /*RED*/, GL_UNSIGNED_BYTE, NULL);
|
||||
else if (fmt == -2) glTexImage2D(GL_TEXTURE_2D, 0, 0x822E /*R32F*/, w, h, 0, 0x1903 /*RED*/, GL_FLOAT, NULL);
|
||||
else glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
@@ -309,6 +385,81 @@ static int pfx_mask_build(int fresh, int lw, int lh) {
|
||||
return s_mask_ok;
|
||||
}
|
||||
|
||||
/* Slot k: surface rb (tw x th) blitted into its depth texture. */
|
||||
static int pfx_surf_depth(int k, GLuint fbo, GLuint rb, int tw, int th) {
|
||||
PfxSurfDepth *d = &s_sdepth[k];
|
||||
if (d->rb != rb || d->w != tw || d->h != th) {
|
||||
if (!d->dtex) glGenTextures(1, &d->dtex);
|
||||
glBindTexture(GL_TEXTURE_2D, d->dtex);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, PSXGL_DEPTH24_STENCIL8, tw, th, 0,
|
||||
0x84F9 /*DEPTH_STENCIL*/, 0x84FA /*UNSIGNED_INT_24_8*/, NULL);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (!d->dfbo) p_glGenFramebuffers(1, &d->dfbo);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, d->dfbo);
|
||||
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, d->dtex, 0);
|
||||
d->rb = rb; d->w = tw; d->h = th;
|
||||
}
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbo);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, d->dfbo);
|
||||
p_glBlitFramebuffer(0, 0, tw, th, 0, 0, tw, th, GL_DEPTH_BUFFER_BIT, GL_NEAREST);
|
||||
return 1;
|
||||
}
|
||||
static void pfx_depth_map(int k, const int r[4], int x0, int x1, int lh) {
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_depth_fbo);
|
||||
glViewport(x0, 0, x1 - x0, lh);
|
||||
p_glUseProgram(s_depth_map_prog);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_sdepth[k].dtex);
|
||||
p_glUniform1i(s_dmap_uS, 0);
|
||||
p_glUniform4i(s_dmap_uSrc, r[0], r[1], r[2], r[3]);
|
||||
p_glUniform4f(s_dmap_uDst, (float)x0, (float)(x1 - x0), (float)lh, 0.0f);
|
||||
post_fullscreen(s_depth_map_prog, s_dmap_uUv);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
/* Window-sized view depth from the presented surface (fresh) or the last. */
|
||||
static int pfx_depth_build(int fresh, int lw, int lh) {
|
||||
if (!s_depth_map_prog) {
|
||||
s_depth_map_prog = build_program(PRESENT_VS,
|
||||
"#version 330\nin vec2 v_uv; out vec4 o; uniform sampler2D u_s;\n"
|
||||
"uniform ivec4 u_src; uniform vec4 u_dst;\n"
|
||||
"void main(){\n"
|
||||
" vec2 f = gl_FragCoord.xy;\n"
|
||||
" int sx = u_src.x + int(floor((f.x - u_dst.x) * float(u_src.z - u_src.x) / u_dst.y));\n"
|
||||
" int sy = u_src.y + int(floor((u_dst.z - f.y) * float(u_src.w - u_src.y) / u_dst.z));\n"
|
||||
" float d = texelFetch(u_s, ivec2(sx, sy), 0).r;\n"
|
||||
" /* zn = 2d - 1 = 1 - 512/(sz + 256); d = 0.5 is the zn = 0 of 2D draws */\n"
|
||||
" float sz = (d >= 0.999999 || abs(d - 0.5) < 1e-6) ? 0.0 : 256.0 / max(1.0 - d, 1e-6) - 256.0;\n"
|
||||
" o = vec4(sz);\n"
|
||||
"}\n");
|
||||
if (!s_depth_map_prog) return 0;
|
||||
s_dmap_uUv = p_glGetUniformLocation(s_depth_map_prog, "u_uv_rect");
|
||||
s_dmap_uS = p_glGetUniformLocation(s_depth_map_prog, "u_s");
|
||||
s_dmap_uSrc = p_glGetUniformLocation(s_depth_map_prog, "u_src");
|
||||
s_dmap_uDst = p_glGetUniformLocation(s_depth_map_prog, "u_dst");
|
||||
}
|
||||
if (s_depth_w != lw || s_depth_h != lh) {
|
||||
pfx_tex_alloc(&s_depth_tex, lw, lh, -2, 1);
|
||||
pfx_fbo_attach(&s_depth_fbo, s_depth_tex);
|
||||
s_depth_w = lw; s_depth_h = lh; s_depth_ok = 0;
|
||||
}
|
||||
if (fresh && s_mask_src_valid) {
|
||||
pfx_surf_depth(0, s_mask_src_fbo, s_mask_src_rb, s_mask_src_tw, s_mask_src_th);
|
||||
if (s_mask_ctr_valid)
|
||||
pfx_surf_depth(1, s_mask_ctr_fbo, s_mask_ctr_rb, s_mask_ctr_tw, s_mask_ctr_th);
|
||||
pfx_depth_map(0, s_mask_src, 0, lw, lh);
|
||||
if (s_mask_ctr_valid) {
|
||||
const int c0 = (int)lrintf(s_mask_ctr_x0 * (float)lw), c1 = (int)lrintf(s_mask_ctr_x1 * (float)lw);
|
||||
if (c1 > c0) pfx_depth_map(1, s_mask_ctr, c0, c1, lh);
|
||||
}
|
||||
s_depth_ok = 1;
|
||||
}
|
||||
return s_depth_ok;
|
||||
}
|
||||
|
||||
/* Run one stage's chain over the window rect. 0 when it could not run.
|
||||
* generated: a Smooth motion in-between frame; passes marked `generated
|
||||
* reuse` keep the target the last real frame left (when it is there at this
|
||||
@@ -318,6 +469,7 @@ static int pfx_mask_build(int fresh, int lw, int lh) {
|
||||
static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, int lh) {
|
||||
PfxChain *c = &st->chain;
|
||||
int copied = 0;
|
||||
if (!st->started_at) st->started_at = PFX_NOW_TICKS();
|
||||
for (int i = 0; i < c->npasses; i++) {
|
||||
const PfxPass *ps = &c->pass[i];
|
||||
const int last = i == c->npasses - 1;
|
||||
@@ -330,12 +482,16 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
L->fixed[PFX_LOC_SIZE] = p_glGetUniformLocation(pg, "u_size");
|
||||
L->fixed[PFX_LOC_ORG] = p_glGetUniformLocation(pg, "u_org");
|
||||
L->fixed[PFX_LOC_UV] = p_glGetUniformLocation(pg, "u_uv_rect");
|
||||
L->time = p_glGetUniformLocation(pg, "u_time");
|
||||
L->proj = p_glGetUniformLocation(pg, "u_proj");
|
||||
for (int k = 0; k < ps->ninputs; k++) L->in[k] = p_glGetUniformLocation(pg, ps->in[k].sampler);
|
||||
for (int k = 0; k < ps->nuniforms; k++) L->u[k] = p_glGetUniformLocation(pg, ps->u[k].name);
|
||||
}
|
||||
if (!st->prog[i]) {
|
||||
fprintf(stdout, "psxrecomp: post effect '%s' pass '%s' did not compile; effect off\n",
|
||||
c->name, ps->name);
|
||||
if (st == &s_pfx[PFX_STAGE_USER])
|
||||
snprintf(s_pfx_user_status, sizeof s_pfx_user_status, "pass '%s' did not compile", ps->name);
|
||||
st->failed = 1;
|
||||
return 0;
|
||||
}
|
||||
@@ -372,6 +528,17 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
p_glBlitFramebuffer(lx, ly, lx + lw, ly + lh, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
}
|
||||
}
|
||||
/* Allocate/reset before sampling, including the final window pass.
|
||||
* A resize starts an empty history rather than sampling the old size. */
|
||||
if (st->hist[i] && (st->hw[i] != w || st->hh[i] != h)) {
|
||||
pfx_tex_alloc(&st->htex[i], w, h, ps->format, ps->nearest);
|
||||
pfx_fbo_attach(&st->hfbo[i], st->htex[i]);
|
||||
st->hw[i] = w; st->hh[i] = h;
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, st->hfbo[i]);
|
||||
GLfloat clear[4]; glGetFloatv(GL_COLOR_CLEAR_VALUE, clear);
|
||||
glClearColor(0,0,0,0); glClear(GL_COLOR_BUFFER_BIT);
|
||||
glClearColor(clear[0],clear[1],clear[2],clear[3]);
|
||||
}
|
||||
if (last) { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0); glViewport(lx, ly, lw, lh); }
|
||||
else { p_glBindFramebuffer(PSXGL_FRAMEBUFFER, st->fbo[i]); glViewport(0, 0, w, h); }
|
||||
const GLuint prog = st->prog[i];
|
||||
@@ -381,7 +548,9 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
const int from = ps->in[k].from;
|
||||
const GLuint t = from == PFX_IN_SOURCE ? s_post_copy_tex
|
||||
: from == PFX_IN_SOURCE_DOWN ? st->dtex[i]
|
||||
: from == PFX_IN_MASK ? s_mask_tex : st->tex[from];
|
||||
: from == PFX_IN_MASK ? s_mask_tex
|
||||
: from == PFX_IN_DEPTH ? s_depth_tex
|
||||
: from == PFX_IN_HISTORY ? st->htex[i] : st->tex[from];
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + (GLenum)k);
|
||||
glBindTexture(GL_TEXTURE_2D, t);
|
||||
p_glUniform1i(L->in[k], k);
|
||||
@@ -390,15 +559,31 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
p_glUniform2f(L->fixed[PFX_LOC_RCP], 1.0f / (float)w, 1.0f / (float)h);
|
||||
p_glUniform2f(L->fixed[PFX_LOC_SIZE], (float)w, (float)h);
|
||||
p_glUniform2i(L->fixed[PFX_LOC_ORG], last ? lx : 0, last ? ly : 0);
|
||||
if (L->proj >= 0) {
|
||||
/* u_proj: x/z = (uv.x - y) * w * x, y/z = (uv.y - z) * x for a view-space
|
||||
* point at depth sz (240-line reference, same scale on both axes). */
|
||||
const float aspect = (float)lw / (float)(lh > 0 ? lh : 1);
|
||||
GtePostfxProjection projection = {256, 160 * 65536, 120 * 65536};
|
||||
(void)gte_postfx_projection_read(&projection);
|
||||
const float H = projection.h > 0 ? (float)projection.h : 256.0f;
|
||||
const float ofx = (float)projection.ofx / 65536.0f, ofy = (float)projection.ofy / 65536.0f;
|
||||
p_glUniform4f(L->proj, 240.0f / H, 0.5f + (ofx - 160.0f) / (240.0f * aspect),
|
||||
1.0f - (ofy > 0.0f ? ofy : 120.0f) / 240.0f, aspect);
|
||||
}
|
||||
if (L->time >= 0)
|
||||
p_glUniform1f(L->time, (float)((double)(PFX_NOW_TICKS() - st->started_at) /
|
||||
(double)SDL_GetPerformanceFrequency()));
|
||||
for (int k = 0; k < ps->nuniforms; k++) {
|
||||
const PfxUniform *u = &ps->u[k];
|
||||
const GLint loc = L->u[k];
|
||||
if (loc < 0) continue;
|
||||
float live[4];
|
||||
const float *v = pfx_live_get(u->name, live) ? live : u->v;
|
||||
switch (u->count) {
|
||||
case 1: p_glUniform1f(loc, u->v[0]); break;
|
||||
case 2: p_glUniform2f(loc, u->v[0], u->v[1]); break;
|
||||
case 3: p_glUniform3f(loc, u->v[0], u->v[1], u->v[2]); break;
|
||||
default: p_glUniform4f(loc, u->v[0], u->v[1], u->v[2], u->v[3]); break;
|
||||
case 1: p_glUniform1f(loc, v[0]); break;
|
||||
case 2: p_glUniform2f(loc, v[0], v[1]); break;
|
||||
case 3: p_glUniform3f(loc, v[0], v[1], v[2]); break;
|
||||
default: p_glUniform4f(loc, v[0], v[1], v[2], v[3]); break;
|
||||
}
|
||||
}
|
||||
if (ps->blend == PFX_BLEND_SCREEN) {
|
||||
@@ -408,6 +593,13 @@ static int pfx_stage_run(PfxStage *st, int generated, int lx, int ly, int lw, in
|
||||
}
|
||||
post_fullscreen(prog, L->fixed[PFX_LOC_UV]);
|
||||
if (ps->blend != PFX_BLEND_REPLACE) glDisable(GL_BLEND);
|
||||
if (!generated && st->hist[i]) {
|
||||
/* Capture the final window target as well as intermediate FBOs. */
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, last ? 0 : st->fbo[i]);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, st->hfbo[i]);
|
||||
const int x = last ? lx : 0, y = last ? ly : 0;
|
||||
p_glBlitFramebuffer(x, y, x+w, y+h, 0, 0, w, h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
}
|
||||
}
|
||||
for (int k = PFX_MAX_INPUTS - 1; k >= 0; k--) {
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + (GLenum)k);
|
||||
@@ -453,7 +645,7 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l
|
||||
PfxGlState saved;
|
||||
pfx_state_save(&saved);
|
||||
glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_SCISSOR_TEST); glDisable(GL_STENCIL_TEST);
|
||||
int mask_done = 0, mask_ok = 0;
|
||||
int mask_done = 0, mask_ok = 0, depth_done = 0, depth_ok = 0;
|
||||
for (int i = 0; i < PFX_NSTAGES; i++) {
|
||||
if (!pfx_stage_on(i)) continue;
|
||||
if ((i == PFX_STAGE_AA || i == PFX_STAGE_HQ) && !with_aa) continue;
|
||||
@@ -463,6 +655,10 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l
|
||||
if (!mask_done) { mask_ok = pfx_mask_build(src_fresh, lw, lh); mask_done = 1; }
|
||||
if (!mask_ok) continue; /* no marks yet */
|
||||
}
|
||||
if (st->chain.uses_depth) {
|
||||
if (!depth_done) { depth_ok = pfx_depth_build(src_fresh, lw, lh); depth_done = 1; }
|
||||
if (!depth_ok) continue; /* no depth yet */
|
||||
}
|
||||
if (i == PFX_STAGE_BLOOM) {
|
||||
const float k = s_bloom;
|
||||
pfx_chain_set_uniform(&st->chain, "u_strength", 1, &k);
|
||||
@@ -505,6 +701,7 @@ static void postfx_present(int fresh, int with_aa, int lx, int ly, int lw, int l
|
||||
|
||||
/* Objects died with the context. */
|
||||
static void postfx_release(void) {
|
||||
gte_postfx_projection_enable(0);
|
||||
for (int i = 0; i < PFX_NSTAGES; i++) {
|
||||
for (int k = 0; k < PFX_MAX_PASSES; k++) {
|
||||
s_pfx[i].prog[k] = s_pfx[i].tex[k] = s_pfx[i].fbo[k] = 0;
|
||||
@@ -515,6 +712,12 @@ static void postfx_release(void) {
|
||||
}
|
||||
s_mask_tex = s_mask_fbo = s_mask_prog = s_mask_map_prog = 0;
|
||||
memset(s_smask, 0, sizeof s_smask);
|
||||
s_depth_tex = s_depth_fbo = s_depth_map_prog = 0; s_depth_w = s_depth_h = 0; s_depth_ok = 0;
|
||||
memset(s_sdepth, 0, sizeof s_sdepth);
|
||||
for (int i = 0; i < PFX_NSTAGES; i++)
|
||||
for (int k = 0; k < PFX_MAX_PASSES; k++) {
|
||||
s_pfx[i].htex[k] = s_pfx[i].hfbo[k] = 0; s_pfx[i].hw[k] = s_pfx[i].hh[k] = 0;
|
||||
}
|
||||
s_mask_w = s_mask_h = 0; s_mask_ok = 0; s_mask_src_valid = 0;
|
||||
}
|
||||
|
||||
@@ -538,6 +741,16 @@ void gl_renderer_set_post_shader(const char *path) {
|
||||
}
|
||||
pfx_mask_wanted_update();
|
||||
}
|
||||
/* Debug: the user chain's last load outcome and the live uniforms, as JSON members. */
|
||||
int gl_renderer_post_shader_debug(char *out, int cap) {
|
||||
GL_RT_SYNC("post_shader_debug");
|
||||
PfxLive live[PFX_LIVE_MAX]; int count;
|
||||
SDL_LockSpinlock(&s_pfx_live_lock);
|
||||
count = s_pfx_nlive; memcpy(live,s_pfx_live,sizeof live);
|
||||
SDL_UnlockSpinlock(&s_pfx_live_lock);
|
||||
return pfx_debug_json(out,cap,s_pfx_user_path,s_pfx_user_status,live,count);
|
||||
}
|
||||
|
||||
int gl_renderer_post_shader_loaded(void) {
|
||||
GL_RT_SYNC("post_shader");
|
||||
return s_pfx[PFX_STAGE_USER].loaded && !s_pfx[PFX_STAGE_USER].failed;
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
/* Exact indexed-page decode cache. Included after TEX_FS and batch helpers.
|
||||
* Array layers contain native 1555 words, not filtered/display colours. */
|
||||
#ifndef GL_TEXTURE_2D_ARRAY
|
||||
#define GL_TEXTURE_2D_ARRAY 0x8C1A
|
||||
#endif
|
||||
typedef void (APIENTRY *TcImage3D)(GLenum, GLint, GLint, GLsizei, GLsizei, GLsizei,
|
||||
GLint, GLenum, GLenum, const void *);
|
||||
typedef void (APIENTRY *TcAttachLayer)(GLenum, GLenum, GLuint, GLint, GLint);
|
||||
#define TC_CAP 256
|
||||
typedef struct {
|
||||
GLuint source, palette;
|
||||
int x, y, depth, cx, cy, valid, pinned;
|
||||
uint64_t stamp, used, checked_epoch;
|
||||
int next;
|
||||
} TexelCacheEntry;
|
||||
static TexelCacheEntry s_tc[TC_CAP];
|
||||
static uint64_t s_tc_tiles[32][64], s_tc_epoch, s_tc_clock;
|
||||
static uint64_t s_tc_hits, s_tc_misses, s_tc_evictions;
|
||||
static GLuint s_tc_array, s_tc_fbo, s_tc_prog;
|
||||
static GLint s_tc_page, s_tc_clut, s_tc_depth;
|
||||
static int s_tc_ready = -1, s_tc_capacity = TC_CAP;
|
||||
static TcAttachLayer s_tc_attach;
|
||||
static int s_tc_pins[TC_CAP], s_tc_pin_n;
|
||||
static int s_tc_heads[1024], s_tc_last = -1;
|
||||
static int texel_cache_lookup_fast(void) {
|
||||
static int enabled = -1;
|
||||
if (enabled < 0) {
|
||||
const char *e = getenv("PSX_GL_TEXEL_LOOKUP_FAST");
|
||||
enabled = !(e && e[0] == '0');
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
static unsigned texel_cache_hash(GLuint source, GLuint palette, int x, int y, int depth, int cx, int cy) {
|
||||
uint32_t h = source * 0x9e3779b9u ^ palette * 0x85ebca6bu;
|
||||
h ^= (uint32_t)x * 73856093u ^ (uint32_t)y * 19349663u;
|
||||
h ^= (uint32_t)cx * 83492791u ^ (uint32_t)cy * 2654435761u ^ (uint32_t)depth;
|
||||
h ^= h >> 16;
|
||||
return h & 1023u;
|
||||
}
|
||||
static int texel_cache_matches(const TexelCacheEntry *c, GLuint source, GLuint palette,
|
||||
int x, int y, int depth, int cx, int cy) {
|
||||
return c->valid && c->source==source && c->palette==palette && c->x==x && c->y==y &&
|
||||
c->depth==depth && c->cx==cx && c->cy==cy;
|
||||
}
|
||||
static void texel_cache_pin(int slot) {
|
||||
if (!s_tc[slot].pinned) { s_tc[slot].pinned=1; s_tc_pins[s_tc_pin_n++]=slot; }
|
||||
}
|
||||
static void texel_cache_unpin(void) {
|
||||
for (int i=0; i<s_tc_pin_n; ++i) s_tc[s_tc_pins[i]].pinned=0;
|
||||
s_tc_pin_n=0;
|
||||
}
|
||||
|
||||
static void texel_cache_dirty(int x0, int y0, int x1, int y1) {
|
||||
if (s_tc_ready != 1) return;
|
||||
if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0;
|
||||
if (x1 > 1023) x1 = 1023; if (y1 > 511) y1 = 511;
|
||||
if (x1 < x0 || y1 < y0) return;
|
||||
uint64_t stamp = ++s_tc_epoch;
|
||||
for (int y=y0/16; y<=y1/16; ++y)
|
||||
for (int x=x0/16; x<=x1/16; ++x) s_tc_tiles[y][x] = stamp;
|
||||
}
|
||||
static uint64_t texel_cache_region(int x, int y, int w, int h) {
|
||||
uint64_t stamp = 0;
|
||||
for (int ty=y/16; ty<=(y+h-1)/16; ++ty)
|
||||
for (int tx=x/16; tx<=(x+w-1)/16; ++tx) {
|
||||
uint64_t v = s_tc_tiles[ty & 31][tx & 63];
|
||||
if (v > stamp) stamp = v;
|
||||
}
|
||||
return stamp;
|
||||
}
|
||||
static void texel_cache_reset(void) {
|
||||
memset(s_tc, 0, sizeof s_tc);
|
||||
memset(s_tc_heads, 0, sizeof s_tc_heads);
|
||||
s_tc_last = -1;
|
||||
memset(s_tc_tiles, 0, sizeof s_tc_tiles);
|
||||
s_tc_epoch = s_tc_clock = s_tc_hits = s_tc_misses = s_tc_evictions = 0;
|
||||
s_tc_array = s_tc_fbo = s_tc_prog = 0;
|
||||
s_tc_ready = -1;
|
||||
s_tc_pin_n = 0;
|
||||
}
|
||||
static int texel_cache_init(void) {
|
||||
if (s_tc_ready >= 0) return s_tc_ready;
|
||||
s_tc_ready = 0;
|
||||
const char *e = getenv("PSX_GL_TEXEL_CACHE");
|
||||
if (e && e[0] == '0') return 0;
|
||||
e = getenv("PSX_GL_TEXEL_CACHE_LAYERS");
|
||||
s_tc_capacity = e ? atoi(e) : TC_CAP;
|
||||
if (s_tc_capacity < 4) s_tc_capacity = 4;
|
||||
if (s_tc_capacity > TC_CAP) s_tc_capacity = TC_CAP;
|
||||
TcImage3D image = (TcImage3D)SDL_GL_GetProcAddress("glTexImage3D");
|
||||
s_tc_attach = (TcAttachLayer)SDL_GL_GetProcAddress("glFramebufferTextureLayer");
|
||||
if (!image || !s_tc_attach) return 0;
|
||||
/* Reuse the literal native address/decode functions from TEX_FS. The
|
||||
* decoder has no window or UV bounds: fetch_texel applies those before
|
||||
* it samples the decoded layer. This avoids a second decode definition. */
|
||||
const char *helpers = strstr(TEX_FS, "int vram_at(");
|
||||
const char *fetch = strstr(TEX_FS, "int fetch_texel(");
|
||||
const char *native = strstr(TEX_FS, " if (v_depth == 0)");
|
||||
const char *end = strstr(TEX_FS, "vec3 col5(");
|
||||
if (!helpers || !fetch || !native || !end) return 0;
|
||||
char *fs = malloc(strlen(TEX_FS) + 512);
|
||||
if (!fs) return 0;
|
||||
snprintf(fs, strlen(TEX_FS) + 512,
|
||||
"#version 330\nuniform usampler2D u_vram, u_palette;\n"
|
||||
"uniform ivec2 v_tpage, v_clut; uniform int v_depth;\n"
|
||||
"layout(location=0) out uint decoded_word;\n%.*s"
|
||||
"int decode_native(int u,int v){\n%.*s"
|
||||
"void main(){decoded_word=uint(decode_native(int(gl_FragCoord.x),int(gl_FragCoord.y)));}\n",
|
||||
(int)(fetch-helpers), helpers, (int)(end-native), native);
|
||||
s_tc_prog = build_program(PRESENT_VS, fs);
|
||||
free(fs);
|
||||
if (!s_tc_prog) return 0;
|
||||
glGenTextures(1, &s_tc_array);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 3);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, s_tc_array);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
p_glBindBuffer(PSXGL_PIXEL_UNPACK_BUFFER, 0);
|
||||
image(GL_TEXTURE_2D_ARRAY, 0, PSXGL_R16UI, 256, 256, s_tc_capacity, 0,
|
||||
PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT, NULL);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
if (glGetError() != GL_NO_ERROR) return 0;
|
||||
p_glGenFramebuffers(1, &s_tc_fbo);
|
||||
s_tc_page = p_glGetUniformLocation(s_tc_prog, "v_tpage");
|
||||
s_tc_clut = p_glGetUniformLocation(s_tc_prog, "v_clut");
|
||||
s_tc_depth = p_glGetUniformLocation(s_tc_prog, "v_depth");
|
||||
p_glUseProgram(s_tc_prog);
|
||||
p_glUniform1i(p_glGetUniformLocation(s_tc_prog, "u_vram"), 0);
|
||||
p_glUniform1i(p_glGetUniformLocation(s_tc_prog, "u_palette"), 1);
|
||||
s_tc_ready = 1;
|
||||
return 1;
|
||||
}
|
||||
static void texel_cache_bind(void) {
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 3);
|
||||
glBindTexture(GL_TEXTURE_2D_ARRAY, s_tc_array);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
}
|
||||
static int texel_cache_get(GLuint source, GLuint palette, int x, int y, int depth, int cx, int cy) {
|
||||
if (depth >= 2 || !texel_cache_init()) return -1;
|
||||
const int fast = texel_cache_lookup_fast();
|
||||
const unsigned bucket = texel_cache_hash(source,palette,x,y,depth,cx,cy);
|
||||
if (fast) {
|
||||
int hit = s_tc_last;
|
||||
if (hit < 0 || !texel_cache_matches(&s_tc[hit],source,palette,x,y,depth,cx,cy)) {
|
||||
hit = s_tc_heads[bucket] - 1;
|
||||
while (hit >= 0 && !texel_cache_matches(&s_tc[hit],source,palette,x,y,depth,cx,cy))
|
||||
hit = s_tc[hit].next - 1;
|
||||
}
|
||||
if (hit >= 0) {
|
||||
TexelCacheEntry *c = &s_tc[hit];
|
||||
if (c->checked_epoch != s_tc_epoch) {
|
||||
uint64_t stamp = source == s_raw_tex ? texel_cache_region(x,y,depth ? 128 : 64,256) : 0;
|
||||
uint64_t ps = palette == s_raw_tex ? texel_cache_region(cx,cy,depth ? 256 : 16,1) : 0;
|
||||
if (ps > stamp) stamp = ps;
|
||||
if (c->stamp == stamp) c->checked_epoch = s_tc_epoch;
|
||||
}
|
||||
if (c->checked_epoch == s_tc_epoch) {
|
||||
c->used=++s_tc_clock; texel_cache_pin(hit); s_tc_last=hit; s_tc_hits++; return hit;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint64_t stamp = source == s_raw_tex ? texel_cache_region(x,y,depth ? 128 : 64,256) : 0;
|
||||
uint64_t ps = palette == s_raw_tex ? texel_cache_region(cx,cy,depth ? 256 : 16,1) : 0;
|
||||
if (ps > stamp) stamp = ps;
|
||||
int slot = -1;
|
||||
uint64_t oldest = UINT64_MAX;
|
||||
for (int i=0; i<s_tc_capacity; ++i) {
|
||||
TexelCacheEntry *c = &s_tc[i];
|
||||
if (!fast && texel_cache_matches(c,source,palette,x,y,depth,cx,cy)) {
|
||||
if (c->stamp == stamp) { c->used=++s_tc_clock; texel_cache_pin(i); s_tc_hits++; return i; }
|
||||
}
|
||||
if (!c->valid) { if (oldest != 0) { slot=i; oldest=0; } }
|
||||
else if (!c->pinned && c->used < oldest) { oldest=c->used; slot=i; }
|
||||
}
|
||||
s_tc_misses++;
|
||||
if (slot < 0) return -1; /* All layers pinned: preserve the original batch. */
|
||||
if (s_tc[slot].valid) {
|
||||
/* Only layers without deferred CPU draw references can be reused.
|
||||
* GPU readers were already submitted, so command ordering protects
|
||||
* them. Never split a mask-checked batch to make cache space. */
|
||||
s_tc_evictions++;
|
||||
}
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_tc_fbo);
|
||||
s_tc_attach(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0, s_tc_array, 0, slot);
|
||||
if (p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER) != PSXGL_FRAMEBUFFER_COMPLETE) return -1;
|
||||
glViewport(0,0,256,256);
|
||||
glDisable(GL_SCISSOR_TEST); glDisable(GL_BLEND); glDisable(GL_DEPTH_TEST); glDisable(GL_STENCIL_TEST);
|
||||
glColorMask(GL_TRUE,GL_TRUE,GL_TRUE,GL_TRUE);
|
||||
p_glUseProgram(s_tc_prog);
|
||||
p_glUniform2i(s_tc_page,x,y); p_glUniform2i(s_tc_clut,cx,cy); p_glUniform1i(s_tc_depth,depth);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0); glBindTexture(GL_TEXTURE_2D,source);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0+1); glBindTexture(GL_TEXTURE_2D,palette);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0+3); glBindTexture(GL_TEXTURE_2D_ARRAY,0);
|
||||
p_glBindVertexArray(s_empty_vao); glDrawArrays(GL_TRIANGLES,0,3);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER,s_hr_fbo);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
if (s_tc[slot].valid) {
|
||||
TexelCacheEntry *c = &s_tc[slot];
|
||||
unsigned old = texel_cache_hash(c->source,c->palette,c->x,c->y,c->depth,c->cx,c->cy);
|
||||
int *link = &s_tc_heads[old];
|
||||
while (*link && *link != slot+1) link = &s_tc[*link-1].next;
|
||||
if (*link) *link = c->next;
|
||||
}
|
||||
s_tc[slot]=(TexelCacheEntry){.source=source,.palette=palette,.x=x,.y=y,.depth=depth,
|
||||
.cx=cx,.cy=cy,.valid=1,.stamp=stamp,.used=++s_tc_clock,
|
||||
.checked_epoch=s_tc_epoch,.next=s_tc_heads[bucket]};
|
||||
s_tc_heads[bucket]=slot+1;
|
||||
s_tc_last=slot;
|
||||
texel_cache_pin(slot);
|
||||
return slot;
|
||||
}
|
||||
@@ -1086,6 +1086,63 @@ int psx_ws_cull_keep_site(uint32_t pc, uint32_t instr, uint32_t vanilla,
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint32_t vertices;
|
||||
uint32_t fold_address[4]; /* physical */
|
||||
uint32_t fold_expected[4];
|
||||
uint32_t vertex_reg[4];
|
||||
uint32_t consumer_address; /* physical */
|
||||
uint32_t consumer_expected;
|
||||
} WsSxyCullSite;
|
||||
static WsSxyCullSite ws_sxy_cull_sites[PSX_WS_SXY_CULL_SITES_MAX];
|
||||
int g_psx_ws_sxy_cull_n = 0;
|
||||
void gpu_ws_set_sxy_cull_sites(const uint32_t *vertices,
|
||||
const uint32_t *fold_addresses,
|
||||
const uint32_t *fold_expected,
|
||||
const uint32_t *vertex_regs,
|
||||
const uint32_t *consumer_addresses,
|
||||
const uint32_t *consumer_expected, int nsites) {
|
||||
int n = 0;
|
||||
if (nsites > PSX_WS_SXY_CULL_SITES_MAX) nsites = PSX_WS_SXY_CULL_SITES_MAX;
|
||||
for (int i = 0; i < nsites; i++) {
|
||||
WsSxyCullSite *site = &ws_sxy_cull_sites[n];
|
||||
if (vertices[i] != 3u && vertices[i] != 4u) continue;
|
||||
site->vertices = vertices[i];
|
||||
for (uint32_t k = 0; k < site->vertices; k++) {
|
||||
site->fold_address[k] = fold_addresses[i * 4 + k] & 0x1FFFFFFFu;
|
||||
site->fold_expected[k] = fold_expected[i * 4 + k];
|
||||
site->vertex_reg[k] = vertex_regs[i * 4 + k] & 31u;
|
||||
}
|
||||
site->consumer_address = consumer_addresses[i] & 0x1FFFFFFFu;
|
||||
site->consumer_expected = consumer_expected[i];
|
||||
n++;
|
||||
}
|
||||
g_psx_ws_sxy_cull_n = n;
|
||||
}
|
||||
uint32_t psx_ws_cull_sxy_and(uint32_t acc, uint32_t sxy) {
|
||||
return psx_ws_sxy_and_value(acc, sxy, psx_ws_x_margin());
|
||||
}
|
||||
uint32_t psx_ws_sxy_cull_vertex_reg(uint32_t pc, uint32_t instr,
|
||||
uint32_t (*read_word)(uint32_t phys)) {
|
||||
const uint32_t phys = pc & 0x1FFFFFFFu;
|
||||
for (int i = 0; i < g_psx_ws_sxy_cull_n; i++) {
|
||||
const WsSxyCullSite *site = &ws_sxy_cull_sites[i];
|
||||
uint32_t step = site->vertices;
|
||||
for (uint32_t k = 0; k < site->vertices; k++)
|
||||
if (site->fold_address[k] == phys && site->fold_expected[k] == instr)
|
||||
step = k;
|
||||
if (step == site->vertices) continue;
|
||||
/* The rest of the fold and the consumer are part of the identity,
|
||||
* exactly as the recompiler checks them before emitting the helper. */
|
||||
int ok = read_word(site->consumer_address) == site->consumer_expected;
|
||||
for (uint32_t k = 0; ok && k < site->vertices; k++)
|
||||
if (k != step && read_word(site->fold_address[k]) != site->fold_expected[k])
|
||||
ok = 0;
|
||||
if (ok) return site->vertex_reg[step];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
uint32_t address;
|
||||
uint32_t expected;
|
||||
|
||||
+189
-595
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -25,6 +26,11 @@ constexpr size_t kCompositionBudget = 64u * 1024u * 1024u;
|
||||
constexpr size_t kCompositionCount = 256;
|
||||
constexpr uint32_t kCompositionDimension = 8192;
|
||||
constexpr size_t kMaxParts = 64;
|
||||
/* Elective full preload of an installed pack: at most this much decoded RGBA
|
||||
* pinned across both formats, decoded by at most this many threads. Anything
|
||||
* beyond decodes on demand under the normal budget. */
|
||||
constexpr size_t kPreloadMaxBytes = size_t{512} * 1024u * 1024u;
|
||||
constexpr unsigned kPreloadThreads = 4;
|
||||
struct Composition {
|
||||
std::vector<uint64_t> signature;
|
||||
std::vector<uint8_t> rgba;
|
||||
@@ -289,6 +295,32 @@ extern "C" int gpu_hd_textures_configure(const char* root, int replacements,
|
||||
std::filesystem::create_directories(directory / "dumps");
|
||||
}
|
||||
if (!duck_texture_pack_create(root, &next->duck, error, capacity)) return 0;
|
||||
if (next->replacements) {
|
||||
/* Replacements decoded and resident before the game draws, within
|
||||
* kPreloadMaxBytes; the rest stays on the budgeted async path. */
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
HdTexturePreloadResult dr{}, br{};
|
||||
const int dstatus = duck_texture_pack_preload(next->duck, kPreloadMaxBytes,
|
||||
kPreloadThreads, &dr);
|
||||
int bstatus = 1;
|
||||
if (next->beetle) {
|
||||
const size_t left = dr.bytes < kPreloadMaxBytes ? kPreloadMaxBytes - dr.bytes : 0;
|
||||
bstatus = hd_texture_pack_preload(next->beetle, left, kPreloadThreads, &br);
|
||||
}
|
||||
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - t0).count();
|
||||
const int requested = dr.requested + br.requested;
|
||||
if (requested > 0 || dstatus < 0 || bstatus < 0)
|
||||
std::fprintf(stdout, "psxrecomp: texture pack: preloaded %d/%d images (%.1f MiB) in %lld ms"
|
||||
"%s; %d over the %u MiB preload cap, %d failed (%d worker errors);"
|
||||
" the rest decode on demand\n",
|
||||
dr.loaded + br.loaded, requested, (dr.bytes + br.bytes) / 1048576.0,
|
||||
static_cast<long long>(ms),
|
||||
(dstatus < 0 || bstatus < 0) ? " (preload error)" :
|
||||
(dstatus == 1 && bstatus == 1) ? " (complete)" : " (partial)",
|
||||
dr.over_budget + br.over_budget, static_cast<unsigned>(kPreloadMaxBytes / (1024u * 1024u)),
|
||||
dr.failed + br.failed, dr.worker_errors + br.worker_errors);
|
||||
}
|
||||
/* Quiesce renderer users (GL render thread: dump_draw appends to the
|
||||
* old session's sources) before flushing its dumps or reading its
|
||||
* residency; the cache is rebuilt from the new session. */
|
||||
@@ -396,6 +428,10 @@ extern "C" void gpu_hd_textures_get_diag(GpuHdTextureDiag* out) {
|
||||
session->diagnostics.push_back(std::move(diagnostic));
|
||||
out->diagnostic = session->diagnostics.back().c_str();
|
||||
}
|
||||
extern "C" int gpu_hd_textures_debug_uploads_at(int x, int y, char* out, size_t cap) {
|
||||
if (out && cap) out[0] = '\0';
|
||||
return session ? hd_texture_pack_debug_uploads_at(session->beetle, (unsigned)x, (unsigned)y, out, cap) : 0;
|
||||
}
|
||||
extern "C" void gpu_hd_textures_note_applied(void) { if (session) ++session->diag.applied_draws; }
|
||||
extern "C" void gpu_hd_textures_note_gl_cache(int eviction) {
|
||||
if (session) {
|
||||
@@ -557,7 +593,7 @@ static int acquire_draw(uint16_t tp, uint16_t cx, uint16_t cy,
|
||||
image->origin_u = query.u_first - (match.source_word_x * pixels_per_word + (query.u_first % pixels_per_word));
|
||||
image->origin_v = query.v_first - match.source_y;
|
||||
image->cache_key = (static_cast<uint64_t>(match.entry.texture_hash) << 32) | match.entry.palette_hash;
|
||||
image->alpha_mode = 1;
|
||||
image->alpha_mode = match.entry.palette_hash == HD_TEXTURE_PALETTE_INDEXED ? 5 : 1;
|
||||
} else {
|
||||
try {
|
||||
std::array<DuckTextureMatch,kMaxParts> matches{}; size_t count = 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <cmath>
|
||||
#include "gte.h"
|
||||
#include "gte_projection_publication.hpp"
|
||||
#include "gte_view.h"
|
||||
#include "cpu_state.h"
|
||||
#include "execution_profile.h"
|
||||
@@ -20,6 +21,12 @@ extern "C" int gpu_ws_precise_nclip_enabled(void);
|
||||
* plus native replay of one candidate) is running. */
|
||||
extern "C" int psx_overlay_shadow_diff_active(void);
|
||||
extern "C" uint64_t s_frame_count; /* debug_server.c */
|
||||
/* Latest projection (H, OFX, OFY in 16.16), read by the post-effect executor. */
|
||||
static GteProjectionPublication s_postfx_projection;
|
||||
extern "C" void gte_postfx_projection_enable(int enabled) { s_postfx_projection.enable(enabled!=0); }
|
||||
extern "C" int gte_postfx_projection_read(GtePostfxProjection* out) {
|
||||
return out && s_postfx_projection.read(*out);
|
||||
}
|
||||
extern "C" uint32_t g_debug_last_store_pc; /* debug_server.c */
|
||||
extern "C" { int (*g_psx_projection_command)(CPUState*, uint32_t) = nullptr; }
|
||||
|
||||
@@ -1206,6 +1213,9 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
}
|
||||
}
|
||||
dome_probe_note(gte->SZ[3]); /* locate the dome draw fn (far-vertex tally) */
|
||||
if (!s_gte_replay_sandbox) { /* projection of the latest 3D draw, for post effects (u_proj) */
|
||||
s_postfx_projection.publish(gte->H,gte->OFX,gte->OFY);
|
||||
}
|
||||
int64_t sx16 = gte->OFX + xterm;
|
||||
int64_t sy16 = gte->OFY + (int64_t)gte->IR2 * h_div_sz;
|
||||
/* Render-view projection override (psx_mod_render_view, projection=1).
|
||||
|
||||
+191
-31
@@ -1,10 +1,12 @@
|
||||
#include "hd_texture_pack.h"
|
||||
#include "texture_pack_preload.hpp"
|
||||
|
||||
#ifndef HD_TEXTURE_PACK_DISABLE_PNG_DECODE
|
||||
#include "../third_party/stb_image.h" /* declarations only; implementation is shared */
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <array>
|
||||
#include <condition_variable>
|
||||
#include <cctype>
|
||||
@@ -113,6 +115,10 @@ bool parse_key_text(const std::string& text,
|
||||
const size_t dash = text.find('-');
|
||||
if (dash == std::string::npos || text.find('-', dash + 1) != std::string::npos)
|
||||
return false;
|
||||
if (ascii_lower(text.substr(dash + 1)) == "index") {
|
||||
*palette_hash = HD_TEXTURE_PALETTE_INDEXED;
|
||||
return parse_hex_component(text.substr(0, dash), texture_hash);
|
||||
}
|
||||
return parse_hex_component(text.substr(0, dash), texture_hash) &&
|
||||
parse_hex_component(text.substr(dash + 1), palette_hash);
|
||||
}
|
||||
@@ -448,6 +454,7 @@ struct DecodeItem {
|
||||
std::shared_ptr<DecodedImage> image;
|
||||
size_t bytes = 0;
|
||||
uint64_t last_used = 0;
|
||||
bool pinned = false; /* preloaded: never evicted */
|
||||
};
|
||||
|
||||
struct DecodeCache {
|
||||
@@ -480,7 +487,7 @@ void evict_decode_cache(DecodeCache& cache, size_t incoming = 0) {
|
||||
while (cache.used + incoming > cache.budget) {
|
||||
auto victim = cache.items.end();
|
||||
for (auto it = cache.items.begin(); it != cache.items.end(); ++it) {
|
||||
if (it->second.state != DecodeState::Ready) continue;
|
||||
if (it->second.state != DecodeState::Ready || it->second.pinned) continue;
|
||||
if (victim == cache.items.end() ||
|
||||
it->second.last_used < victim->second.last_used)
|
||||
victim = it;
|
||||
@@ -511,38 +518,46 @@ void decode_worker(DecodeCache* cache) {
|
||||
path = it->second.path;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> encoded;
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary | std::ios::ate);
|
||||
std::streamoff length = -1;
|
||||
if (input) length = static_cast<std::streamoff>(input.tellg());
|
||||
if (length > 0 && static_cast<uint64_t>(length) <= kMaxPngFileBytes &&
|
||||
static_cast<uint64_t>(length) <=
|
||||
static_cast<uint64_t>(std::numeric_limits<int>::max())) {
|
||||
encoded.resize(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
if (!input.read(reinterpret_cast<char*>(encoded.data()), length))
|
||||
encoded.clear();
|
||||
}
|
||||
}
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int components = 0;
|
||||
stbi_uc* loaded = encoded.empty() ? nullptr : stbi_load_from_memory(
|
||||
encoded.data(), static_cast<int>(encoded.size()),
|
||||
&width, &height, &components, 4);
|
||||
std::shared_ptr<DecodedImage> decoded;
|
||||
size_t bytes = 0;
|
||||
if (loaded && width > 0 && height > 0 &&
|
||||
static_cast<uint64_t>(width) * static_cast<uint64_t>(height) <=
|
||||
std::numeric_limits<size_t>::max() / 4) {
|
||||
bytes = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
|
||||
decoded = std::make_shared<DecodedImage>();
|
||||
decoded->width = static_cast<uint32_t>(width);
|
||||
decoded->height = static_cast<uint32_t>(height);
|
||||
decoded->rgba.assign(loaded, loaded + bytes);
|
||||
/* Allocation failure (encoded buffer, RGBA copy) must not escape the
|
||||
* thread entry: the item fails like an undecodable image. */
|
||||
try {
|
||||
std::vector<uint8_t> encoded;
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary | std::ios::ate);
|
||||
std::streamoff length = -1;
|
||||
if (input) length = static_cast<std::streamoff>(input.tellg());
|
||||
if (length > 0 && static_cast<uint64_t>(length) <= kMaxPngFileBytes &&
|
||||
static_cast<uint64_t>(length) <=
|
||||
static_cast<uint64_t>(std::numeric_limits<int>::max())) {
|
||||
encoded.resize(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
if (!input.read(reinterpret_cast<char*>(encoded.data()), length))
|
||||
encoded.clear();
|
||||
}
|
||||
}
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int components = 0;
|
||||
std::unique_ptr<stbi_uc, void (*)(void*)> loaded(
|
||||
encoded.empty() ? nullptr : stbi_load_from_memory(
|
||||
encoded.data(), static_cast<int>(encoded.size()),
|
||||
&width, &height, &components, 4),
|
||||
stbi_image_free);
|
||||
if (loaded && width > 0 && height > 0 &&
|
||||
static_cast<uint64_t>(width) * static_cast<uint64_t>(height) <=
|
||||
std::numeric_limits<size_t>::max() / 4) {
|
||||
bytes = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
|
||||
decoded = std::make_shared<DecodedImage>();
|
||||
decoded->width = static_cast<uint32_t>(width);
|
||||
decoded->height = static_cast<uint32_t>(height);
|
||||
decoded->rgba.assign(loaded.get(), loaded.get() + bytes);
|
||||
}
|
||||
} catch (...) {
|
||||
decoded.reset();
|
||||
bytes = 0;
|
||||
}
|
||||
if (loaded) stbi_image_free(loaded);
|
||||
|
||||
std::lock_guard<std::mutex> lock(cache->mutex);
|
||||
auto it = cache->items.find(key);
|
||||
@@ -1070,6 +1085,35 @@ int hd_texture_pack_track_upload(HdTexturePack* pack,
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Debug: uploads whose live fragments cover VRAM (x, y), newest first, as
|
||||
* JSON objects: serial, texture hash, size, fragment rect and source offset,
|
||||
* and whether the pack holds any replacement for that hash. */
|
||||
int hd_texture_pack_debug_uploads_at(const HdTexturePack* pack, unsigned x, unsigned y,
|
||||
char* out, size_t cap) {
|
||||
if (!out || cap == 0) return 0;
|
||||
size_t pos = 0;
|
||||
int n = 0;
|
||||
out[0] = '\0';
|
||||
if (!pack) return 0;
|
||||
for (auto it = pack->uploads.rbegin(); it != pack->uploads.rend(); ++it) {
|
||||
for (const Fragment& f : it->fragments) {
|
||||
if (x < f.rect.x || x >= f.rect.x + f.rect.width ||
|
||||
y < f.rect.y || y >= f.rect.y + f.rect.height) continue;
|
||||
const int w = std::snprintf(out + pos, cap - pos,
|
||||
"%s{\"serial\":%llu,\"hash\":\"%08x\",\"w\":%u,\"h\":%u,"
|
||||
"\"rect\":[%u,%u,%u,%u],\"src\":[%u,%u],\"in_pack\":%d}",
|
||||
n ? "," : "", (unsigned long long)it->serial, it->hash,
|
||||
it->width, it->height, f.rect.x, f.rect.y, f.rect.width, f.rect.height,
|
||||
f.source_x, f.source_y, pack->texture_hashes.count(it->hash) ? 1 : 0);
|
||||
if (w < 0 || (size_t)w >= cap - pos) return n;
|
||||
pos += (size_t)w;
|
||||
++n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void hd_texture_pack_reset_tracking(HdTexturePack* pack) {
|
||||
if (pack) {
|
||||
pack->uploads.clear();
|
||||
@@ -1212,7 +1256,9 @@ int hd_texture_pack_match(HdTexturePack* pack,
|
||||
if (indexed == pack->upload_by_serial.end() || !indexed->second)
|
||||
continue;
|
||||
const Upload& upload = *indexed->second;
|
||||
const auto record = pack->entries.find(make_key(upload.hash, palette_hash));
|
||||
auto record = pack->entries.find(make_key(upload.hash, palette_hash));
|
||||
if (record == pack->entries.end() && query->depth != HD_TEXTURE_DEPTH_16BPP)
|
||||
record = pack->entries.find(make_key(upload.hash, HD_TEXTURE_PALETTE_INDEXED));
|
||||
if (record == pack->entries.end()) continue;
|
||||
if (!covered_by_upload(upload, wanted)) continue;
|
||||
if (record->second.ambiguous) return HD_TEXTURE_LOOKUP_AMBIGUOUS;
|
||||
@@ -1282,6 +1328,120 @@ void hd_texture_pack_set_decode_budget(HdTexturePack* pack,
|
||||
evict_decode_cache(pack->decode);
|
||||
}
|
||||
|
||||
int hd_texture_pack_preload(HdTexturePack* pack, size_t max_bytes, unsigned max_threads,
|
||||
HdTexturePreloadResult* out) {
|
||||
HdTexturePreloadResult r{};
|
||||
if (out) *out = r;
|
||||
#ifdef HD_TEXTURE_PACK_DISABLE_PNG_DECODE
|
||||
(void)pack; (void)max_bytes; (void)max_threads;
|
||||
return -1;
|
||||
#else
|
||||
if (!pack) return -1;
|
||||
try {
|
||||
namespace tp = psx_texpreload;
|
||||
std::vector<std::pair<uint64_t, std::string>> work_items;
|
||||
size_t per_image = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pack->decode.mutex);
|
||||
per_image = pack->decode.budget;
|
||||
for (const auto& item : pack->decode.items)
|
||||
if (item.second.pinned) r.bytes += item.second.bytes;
|
||||
for (const auto& entry : pack->entries) {
|
||||
if (entry.second.ambiguous) continue;
|
||||
++r.requested;
|
||||
const auto it = pack->decode.items.find(entry.first);
|
||||
if (it != pack->decode.items.end() && it->second.pinned) ++r.loaded;
|
||||
}
|
||||
if (r.bytes > max_bytes) {
|
||||
r.failed = r.requested - r.loaded;
|
||||
if (out) *out = r;
|
||||
return -1;
|
||||
}
|
||||
for (const auto& entry : pack->entries) {
|
||||
if (entry.second.ambiguous) continue;
|
||||
const auto it = pack->decode.items.find(entry.first);
|
||||
if (it == pack->decode.items.end() || !it->second.pinned)
|
||||
work_items.emplace_back(entry.first, entry.second.replacement_path);
|
||||
}
|
||||
}
|
||||
std::vector<std::shared_ptr<DecodedImage>> images(work_items.size());
|
||||
std::vector<unsigned char> status(work_items.size(), tp::kPending);
|
||||
tp::Budget budget(max_bytes - r.bytes);
|
||||
r.worker_errors = static_cast<int>(tp::run(work_items.size(), max_threads, [&](size_t k) {
|
||||
status[k] = tp::kFailed;
|
||||
std::ifstream input(work_items[k].second, std::ios::binary | std::ios::ate);
|
||||
const std::streamoff length = input ? static_cast<std::streamoff>(input.tellg()) : -1;
|
||||
if (length <= 0 || static_cast<uint64_t>(length) > kMaxPngFileBytes ||
|
||||
static_cast<uint64_t>(length) > static_cast<uint64_t>(std::numeric_limits<int>::max()))
|
||||
return;
|
||||
std::vector<uint8_t> encoded(static_cast<size_t>(length));
|
||||
input.seekg(0);
|
||||
if (!input.read(reinterpret_cast<char*>(encoded.data()), length)) return;
|
||||
int width = 0, height = 0, components = 0;
|
||||
if (!stbi_info_from_memory(encoded.data(), static_cast<int>(encoded.size()),
|
||||
&width, &height, &components) ||
|
||||
width <= 0 || height <= 0 ||
|
||||
static_cast<uint64_t>(width) * static_cast<uint64_t>(height) >
|
||||
static_cast<uint64_t>(per_image) / 4)
|
||||
return;
|
||||
const size_t need = static_cast<size_t>(width) * static_cast<size_t>(height) * 4;
|
||||
if (!budget.reserve(need)) { status[k] = tp::kOverBudget; return; }
|
||||
struct Reservation {
|
||||
tp::Budget& b; size_t n; bool keep = false;
|
||||
~Reservation() { if (!keep) b.release(n); }
|
||||
} reservation{budget, need};
|
||||
int lw = 0, lh = 0, lc = 0;
|
||||
std::unique_ptr<stbi_uc, void (*)(void*)> loaded(
|
||||
stbi_load_from_memory(encoded.data(), static_cast<int>(encoded.size()),
|
||||
&lw, &lh, &lc, 4),
|
||||
stbi_image_free);
|
||||
if (!loaded || lw != width || lh != height) return;
|
||||
auto image = std::make_shared<DecodedImage>();
|
||||
image->width = static_cast<uint32_t>(width);
|
||||
image->height = static_cast<uint32_t>(height);
|
||||
image->rgba.assign(loaded.get(), loaded.get() + need);
|
||||
images[k] = std::move(image);
|
||||
reservation.keep = true;
|
||||
status[k] = tp::kLoaded;
|
||||
}));
|
||||
std::lock_guard<std::mutex> lock(pack->decode.mutex);
|
||||
for (size_t k = 0; k < work_items.size(); ++k) {
|
||||
if (status[k] == tp::kOverBudget) { ++r.over_budget; continue; }
|
||||
if (status[k] != tp::kLoaded) continue;
|
||||
auto it = pack->decode.items.find(work_items[k].first);
|
||||
if (it != pack->decode.items.end()) {
|
||||
/* Already pinned, or an on-demand decode in flight: leave it. */
|
||||
if (it->second.pinned || it->second.state == DecodeState::Queued ||
|
||||
it->second.state == DecodeState::Loading) continue;
|
||||
}
|
||||
const size_t previous_bytes = it != pack->decode.items.end() &&
|
||||
it->second.state == DecodeState::Ready ? it->second.bytes : 0;
|
||||
DecodeItem item;
|
||||
/* Prepare throwing metadata before touching an existing entry. */
|
||||
item.path = work_items[k].second;
|
||||
item.bytes = images[k]->rgba.size();
|
||||
item.image = std::move(images[k]);
|
||||
item.state = DecodeState::Ready; item.pinned = true;
|
||||
item.last_used = ++pack->decode.tick;
|
||||
const size_t bytes = item.bytes;
|
||||
if (it == pack->decode.items.end())
|
||||
pack->decode.items.emplace(work_items[k].first, std::move(item));
|
||||
else it->second = std::move(item);
|
||||
pack->decode.used -= previous_bytes;
|
||||
r.bytes += bytes; ++r.loaded;
|
||||
tp::after_publish(r.loaded);
|
||||
}
|
||||
r.failed = r.requested - r.loaded - r.over_budget;
|
||||
if (out) *out = r;
|
||||
return r.loaded == r.requested ? 1 : 0;
|
||||
} catch (...) {
|
||||
r.failed = r.requested - r.loaded - r.over_budget;
|
||||
if (out) *out = r; /* Account the pinned prefix even on publication failure. */
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
int hd_texture_pack_request_decode(HdTexturePack* pack,
|
||||
uint32_t texture_hash,
|
||||
uint32_t palette_hash) {
|
||||
|
||||
@@ -142,8 +142,6 @@ const char *host_span_kind_name(unsigned kind) {
|
||||
"none", "autocapture_tick", "provider_poll", "publish_commit", "loader_rescan",
|
||||
"dll_register", "lazy_load", "capture_on_dma", "capture_before_dma", "ac_request", "ac_done_join",
|
||||
"ac_prepare_image", "ac_child", "overlay_dispatch", "mod_work", "frame_present", "disc_read",
|
||||
"host_events", "rewind_capture",
|
||||
"display_probe", "audio_update",
|
||||
};
|
||||
return kind < HSPAN_KIND_COUNT ? names[kind] : "?";
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
#include "input_replay.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
|
||||
typedef struct ReplayStep { uint64_t end; unsigned buttons; } ReplayStep;
|
||||
static ReplayStep *s_steps;
|
||||
static size_t s_count, s_index;
|
||||
static uint64_t s_base;
|
||||
static int s_armed;
|
||||
|
||||
int input_replay_load(const char *path) {
|
||||
FILE *f = fopen(path, "r");
|
||||
ReplayStep *steps = NULL;
|
||||
size_t count = 0;
|
||||
uint64_t end = 0;
|
||||
char line[256];
|
||||
int ok = f != NULL;
|
||||
if (!f) return 0;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
char *p = line;
|
||||
unsigned long long frames, buttons;
|
||||
char *endp;
|
||||
while (isspace((unsigned char)*p)) p++;
|
||||
if (!*p || *p == '#') continue;
|
||||
if (*p == '-' || *p == '+') { ok = 0; break; }
|
||||
errno = 0; frames = strtoull(p, &endp, 10);
|
||||
if (endp == p || errno || !isspace((unsigned char)*endp) ||
|
||||
!frames || frames > UINT32_MAX) { ok = 0; break; }
|
||||
p = endp;
|
||||
while (isspace((unsigned char)*p)) p++;
|
||||
if (*p == '-' || *p == '+') { ok = 0; break; }
|
||||
errno = 0; buttons = strtoull(p, &endp, 16);
|
||||
if (endp == p || errno || buttons > 65535) { ok = 0; break; }
|
||||
while (isspace((unsigned char)*endp)) endp++;
|
||||
if (*endp || count >= 65536 || end + frames > UINT32_MAX) { ok = 0; break; }
|
||||
ReplayStep *next = realloc(steps, (count+1)*sizeof *steps);
|
||||
if (!next) { ok = 0; break; }
|
||||
steps = next;
|
||||
end += frames;
|
||||
steps[count++] = (ReplayStep){end, buttons};
|
||||
}
|
||||
if (ferror(f)) ok = 0;
|
||||
fclose(f);
|
||||
if (!ok || !count) { free(steps); return 0; }
|
||||
free(s_steps);
|
||||
s_steps = steps; s_count = count; s_index = 0; s_armed = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void input_replay_on_load(uint64_t next_frame) {
|
||||
s_base = next_frame; s_index = 0; s_armed = s_count != 0;
|
||||
}
|
||||
|
||||
uint64_t input_replay_frame(uint64_t frame) {
|
||||
return s_armed && frame >= s_base ? frame-s_base : UINT64_MAX;
|
||||
}
|
||||
|
||||
int input_replay_buttons(uint64_t frame) {
|
||||
uint64_t at = input_replay_frame(frame);
|
||||
if (at == UINT64_MAX) return -1;
|
||||
if (s_index && at < s_steps[s_index-1].end) s_index = 0;
|
||||
while (s_index < s_count && at >= s_steps[s_index].end) s_index++;
|
||||
return s_index < s_count ? (int)s_steps[s_index].buttons : 65535;
|
||||
}
|
||||
+41
-200
@@ -49,7 +49,6 @@
|
||||
#include "autocompile.h"
|
||||
#include "code_provider.h"
|
||||
#include "host_span_ring.h"
|
||||
#include "input_replay.h"
|
||||
#ifdef PSX_HAS_OVERLAY_DISPATCH
|
||||
extern "C" void psx_overlay_static_get_stats(uint64_t *, uint64_t *, uint64_t *, uint64_t *);
|
||||
#endif
|
||||
@@ -134,6 +133,7 @@ extern "C" uint32_t g_guest_last_store_addr, g_guest_last_store_val;
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_local_input_policy.h"
|
||||
#include "mod_session_baseline.h"
|
||||
#include "func_override.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "video_texture_filter.h"
|
||||
#include "video_telemetry.h"
|
||||
@@ -1082,10 +1082,6 @@ static void post_load_probe_on_vblank(int turbo_active, int present_reached) {
|
||||
* restored VRAM — including a blank if display was disabled in the snapshot. */
|
||||
static void savestate_input_guard_arm(void);
|
||||
extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok) {
|
||||
if (is_load && !ok && std::getenv("PSX_INPUT_REPLAY")) {
|
||||
std::puts("psxrecomp: requested input replay savestate failed to load");
|
||||
std::exit(1);
|
||||
}
|
||||
char buf[64];
|
||||
const int disp = slot + 1;
|
||||
if (!is_load && ok)
|
||||
@@ -1108,16 +1104,11 @@ extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok)
|
||||
|
||||
extern "C" void psx_frontend_on_savestate_refused(int is_load, int slot,
|
||||
const char *reason) {
|
||||
if (is_load && std::getenv("PSX_INPUT_REPLAY")) {
|
||||
std::puts(reason);
|
||||
std::exit(1);
|
||||
}
|
||||
(void)is_load; (void)slot;
|
||||
host_osd_push(reason, 3000);
|
||||
}
|
||||
|
||||
extern "C" void psx_frontend_on_savestate_loaded(void) {
|
||||
input_replay_on_load(s_frame_count + 1);
|
||||
psx_projection_reset_session();
|
||||
psx_local_mouse_reset();
|
||||
mod_runtime_on_savestate_loaded();
|
||||
@@ -3889,10 +3880,6 @@ static void refresh_host_display_cadence(int force_log, int force_probe) {
|
||||
return;
|
||||
}
|
||||
g_host_refresh_last_probe_ms = now_ms ? now_ms : 1ull;
|
||||
struct ProbeSpan {
|
||||
uint64_t start = host_span_begin();
|
||||
~ProbeSpan() { host_span_end(HSPAN_DISPLAY_PROBE, start, 0); }
|
||||
} probe_span;
|
||||
|
||||
double host_hz = 0.0, max_hz = 0.0;
|
||||
if (disp_idx >= 0) {
|
||||
@@ -4070,22 +4057,9 @@ static int present_effective_swap_interval(void) {
|
||||
}
|
||||
|
||||
static int present_should_wall_pace(void) {
|
||||
static const bool queued_pace = [] {
|
||||
const char *e = std::getenv("PSX_GL_ASYNC_GUEST_PACE");
|
||||
return !(e && e[0] == '0');
|
||||
}();
|
||||
/* The presenter explicitly waits for each guest-rate deadline. Bounded
|
||||
* render/present queues carry that clock back to the guest, independently
|
||||
* of driver vsync; a second guest timer only serializes the pipeline. */
|
||||
if (queued_pace && g_frame_interpolation && g_gl_active &&
|
||||
gl_renderer_render_thread_active() && !rt_held() &&
|
||||
gl_renderer_interpolation_async_cadence())
|
||||
return 0;
|
||||
/* While this thread owns the GL context, temporal presentation subdivides
|
||||
* this same interval. When rendering is asynchronous, cap the guest here:
|
||||
* the renderer's presentation deadline belongs to its own thread. */
|
||||
/* The single-context temporal-blend path subdivides this same stock frame
|
||||
* interval. Do not run the outer pacer as well or guest timing doubles. */
|
||||
if (g_frame_interpolation && g_gl_active &&
|
||||
(!gl_renderer_render_thread_active() || rt_held()) &&
|
||||
gl_renderer_interpolation_owns_cadence())
|
||||
return 0;
|
||||
/* Run the pacer even when driver vsync is SUPPOSED to own the cadence.
|
||||
@@ -4830,11 +4804,7 @@ static void runtime_perf_frame_end() {
|
||||
|
||||
class RuntimePerfFrameScope {
|
||||
public:
|
||||
uint64_t start = host_span_begin();
|
||||
~RuntimePerfFrameScope() {
|
||||
runtime_perf_frame_end();
|
||||
host_span_end(HSPAN_FRAME_PRESENT, start, 0);
|
||||
}
|
||||
~RuntimePerfFrameScope() { runtime_perf_frame_end(); }
|
||||
};
|
||||
|
||||
static uint64_t runtime_perf_section_begin() {
|
||||
@@ -5000,142 +4970,6 @@ static void openxr_ready_report_tick(uint64_t frames) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Explicit, file-driven production benchmark. No debug server, sampler or
|
||||
* per-instruction tracing is needed. Capture outside the measured interval;
|
||||
* export the existing frame ring before requesting the normal quit path. */
|
||||
struct ReplayWorkSample { uint64_t frame, guest, pacer, queue, completed; };
|
||||
static std::vector<ReplayWorkSample> g_replay_work;
|
||||
static void input_replay_frame_completed(uint64_t frame) {
|
||||
if (g_replay_work.empty()) return;
|
||||
auto &sample = g_replay_work[frame % HOST_FRAME_STAMP_CAP];
|
||||
if (sample.frame == frame) sample.completed = host_clock_us();
|
||||
}
|
||||
static void input_replay_frontend_tick(uint64_t frame) {
|
||||
uint64_t relative = input_replay_frame(frame);
|
||||
if (relative == UINT64_MAX) return;
|
||||
static uint64_t stop = 0, seconds = 0, deadline_us = 0;
|
||||
static const char *report = nullptr, *shots = nullptr;
|
||||
static bool initialized = false, finished = false;
|
||||
auto &work = g_replay_work;
|
||||
if (!initialized) {
|
||||
if (const char *e = std::getenv("PSX_INPUT_REPLAY_STOP")) stop = std::strtoull(e, nullptr, 10);
|
||||
if (const char *e = std::getenv("PSX_INPUT_REPLAY_SECONDS")) {
|
||||
char *end = nullptr;
|
||||
seconds = std::strtoull(e, &end, 10);
|
||||
if (!seconds || seconds > 60 || !end || *end || stop)
|
||||
throw std::runtime_error("replay seconds must be 1..60, without replay stop");
|
||||
}
|
||||
report = std::getenv("PSX_INPUT_REPLAY_REPORT");
|
||||
shots = std::getenv("PSX_INPUT_REPLAY_SHOTS");
|
||||
if (report && *report) {
|
||||
work.resize(HOST_FRAME_STAMP_CAP);
|
||||
if (g_gl_active) gl_renderer_swap_timing_start(host_clock_us);
|
||||
}
|
||||
initialized = true;
|
||||
}
|
||||
if (finished) return;
|
||||
if (seconds && relative >= 900 && !deadline_us)
|
||||
deadline_us = host_clock_us() + seconds * 1000000;
|
||||
if (deadline_us && !stop && host_clock_us() >= deadline_us) stop = relative;
|
||||
if (report && *report) {
|
||||
RtStats rs{};
|
||||
rt_get_stats(&rs);
|
||||
const uint64_t hz = SDL_GetPerformanceFrequency();
|
||||
work[frame % HOST_FRAME_STAMP_CAP] = {frame, g_runtime_perf.guest_work_ticks * 1000000 / hz,
|
||||
g_runtime_perf.pacer_ticks * 1000000 / hz,
|
||||
(rs.backpressure_ns + rs.ring_full_ns + rs.acquire_ns) / 1000, 0};
|
||||
}
|
||||
if (shots && (relative == 800 || (stop > 0 && relative == stop+2))) {
|
||||
char path[512];
|
||||
std::snprintf(path, sizeof path, "%s-%llu.png", shots, (unsigned long long)relative);
|
||||
present_shot_request(path);
|
||||
}
|
||||
if (!stop || relative < stop+10) return;
|
||||
finished = true;
|
||||
if (report && *report) {
|
||||
FILE *f = std::fopen(report, "w");
|
||||
if (!f) throw std::runtime_error("cannot write input replay frame report");
|
||||
std::fputs("frame,relative,us,interp,checks,faults,io_kb,guest_us,pacer_us,queue_us,completed_us\n", f);
|
||||
uint64_t total = host_frame_stamp_total();
|
||||
uint64_t first = total > HOST_FRAME_STAMP_CAP ? total-HOST_FRAME_STAMP_CAP : 0;
|
||||
for (uint64_t i=first; i<total; i++) {
|
||||
uint32_t fr, ii, ck, pf, io; uint64_t us;
|
||||
if (!host_frame_stamp_get(i, &fr, &us, &ii, &ck, &pf, &io)) continue;
|
||||
uint64_t rel = input_replay_frame(fr);
|
||||
if (rel == UINT64_MAX) continue;
|
||||
const auto &w = work[fr % HOST_FRAME_STAMP_CAP];
|
||||
if (w.frame != fr) continue;
|
||||
std::fprintf(f, "%u,%llu,%llu,%u,%u,%u,%u,%llu,%llu,%llu,%llu\n", fr,
|
||||
(unsigned long long)rel, (unsigned long long)us, ii, ck, pf, io,
|
||||
(unsigned long long)w.guest, (unsigned long long)w.pacer,
|
||||
(unsigned long long)w.queue, (unsigned long long)w.completed);
|
||||
}
|
||||
if (std::fclose(f) != 0) throw std::runtime_error("cannot finish input replay frame report");
|
||||
std::string span_path = std::string(report) + ".spans.csv";
|
||||
f = std::fopen(span_path.c_str(), "w");
|
||||
if (!f) throw std::runtime_error("cannot write input replay span report");
|
||||
std::fputs("start_us,dur_us,frame,kind,emu,arg\n", f);
|
||||
total = host_span_total();
|
||||
first = total > HOST_SPAN_RING_CAP ? total-HOST_SPAN_RING_CAP : 0;
|
||||
for (uint64_t i=first; i<total; ++i) {
|
||||
HostSpan s;
|
||||
if (host_span_get(i, &s))
|
||||
std::fprintf(f, "%llu,%u,%u,%s,%u,%u\n", (unsigned long long)s.t0_us,
|
||||
s.dur_us, s.frame, host_span_kind_name(s.kind), s.emu, s.arg);
|
||||
}
|
||||
if (std::fclose(f) != 0) throw std::runtime_error("cannot finish input replay span report");
|
||||
if (g_gl_active) {
|
||||
char thread_json[8192];
|
||||
int length = gl_renderer_render_thread_json(thread_json, sizeof thread_json);
|
||||
if (length < 0 || length >= (int)sizeof thread_json)
|
||||
throw std::runtime_error("render thread report exceeds its buffer");
|
||||
std::string thread_path = std::string(report) + ".rth.json";
|
||||
f = std::fopen(thread_path.c_str(), "w");
|
||||
if (!f) throw std::runtime_error("cannot write input replay render thread report");
|
||||
std::fprintf(f, "{%.*s}\n", length, thread_json);
|
||||
if (std::fclose(f) != 0) throw std::runtime_error("cannot finish input replay render thread report");
|
||||
GlRenderWorkStats stats{};
|
||||
gl_renderer_work_stats(&stats);
|
||||
std::vector<uint64_t> swaps(GL_SWAP_TIMING_CAP);
|
||||
const int swap_count = gl_renderer_swap_timestamps(swaps.data(), (int)swaps.size());
|
||||
std::string swap_path = std::string(report) + ".present.csv";
|
||||
f = std::fopen(swap_path.c_str(), "w");
|
||||
if (!f) throw std::runtime_error("cannot write input replay presentation report");
|
||||
std::fputs("us\n", f);
|
||||
for (int i=0; i<swap_count; ++i) std::fprintf(f,"%llu\n",(unsigned long long)swaps[i]);
|
||||
if (std::fclose(f) != 0) throw std::runtime_error("cannot finish input replay presentation report");
|
||||
std::string path = std::string(report) + ".render.csv";
|
||||
f = std::fopen(path.c_str(), "w");
|
||||
if (!f) throw std::runtime_error("cannot write input replay render report");
|
||||
std::fputs("stat,count,total_us,max_us\n", f);
|
||||
const char *counts[10] = {"queue_uploads", "replay_draws", "bloom_fused", "coverage_depth", "centre_pruned", "texel_cache_hits", "texel_cache_misses", "texel_cache_evictions", "hiw_copy_blits", "identity_copies"};
|
||||
for (int i=0; i<10; ++i) std::fprintf(f, "%s,%llu,0,0\n", counts[i], (unsigned long long)stats.counts[i]);
|
||||
const char *spans[8] = {"depth_restore", "queue_replay", "vram_copy", "interp_capture", "interp_interval", "centre_copy", "depth_gpu_sample", "colour_gpu_sample"};
|
||||
double us = stats.frequency ? 1e6 / stats.frequency : 0;
|
||||
for (int i=0; i<8; ++i) std::fprintf(f, "%s,%llu,%.3f,%.3f\n", spans[i],
|
||||
(unsigned long long)stats.calls[i], stats.ticks[i]*us, stats.max_ticks[i]*us);
|
||||
for (int i=0; i<8; ++i) std::fprintf(f, "%s_prior_drain,%llu,%.3f,%.3f\n", spans[i],
|
||||
(unsigned long long)stats.calls[i], stats.prior_ticks[i]*us, stats.prior_max_ticks[i]*us);
|
||||
if (std::fclose(f) != 0) throw std::runtime_error("cannot finish input replay render report");
|
||||
}
|
||||
double gpu[18];
|
||||
if (g_gl_active && gl_renderer_perf_aggregate(-1, gpu) > 0) {
|
||||
std::string path = std::string(report) + ".gpu.csv";
|
||||
f = std::fopen(path.c_str(), "w");
|
||||
if (!f) throw std::runtime_error("cannot write input replay GPU report");
|
||||
const char *names[18] = {"samples", "total_ms", "total_max_ms", "cpu_ms",
|
||||
"present_wall_ms", "scene_gpu_ms", "scene_gpu_max_ms", "present_gpu_ms",
|
||||
"present_gpu_max_ms", "primitives", "mirror_gpu_ms", "mirror_gpu_max_ms",
|
||||
"mirror_passes", "cpu_flush_ms", "cpu_wide_ms", "batches", "wide_sets", "fbo_creations"};
|
||||
std::fputs("stat,value\n", f);
|
||||
for (int i=0; i<18; ++i) std::fprintf(f, "%s,%.6f\n", names[i], gpu[i]);
|
||||
if (std::fclose(f) != 0) throw std::runtime_error("cannot finish input replay GPU report");
|
||||
}
|
||||
}
|
||||
SDL_Event event{}; event.type = SDL_QUIT; SDL_PushEvent(&event);
|
||||
}
|
||||
|
||||
static void frame_report_tick(uint64_t frames) {
|
||||
static int interval_ms = -1;
|
||||
static uint64_t first_ticks = 0, last_ticks = 0, last_frames = 0;
|
||||
@@ -5350,10 +5184,6 @@ static void sdl_audio_pump_midframe(void) {
|
||||
}
|
||||
|
||||
static void sdl_audio_update(int hard_mute_active, int turbo_sink_active) {
|
||||
struct AudioSpan {
|
||||
uint64_t start = host_span_begin();
|
||||
~AudioSpan() { host_span_end(HSPAN_AUDIO_UPDATE, start, 0); }
|
||||
} audio_span;
|
||||
{ /* Tag audio events with the vblank frame counter. */
|
||||
extern uint64_t s_frame_count;
|
||||
audio_trace_note_frame((uint32_t)s_frame_count);
|
||||
@@ -9181,10 +9011,6 @@ static int debug_toggles_on(void);
|
||||
static int debug_toggle_key(int key, char *out, int cap);
|
||||
#endif
|
||||
static bool drain_host_events() {
|
||||
struct EventSpan {
|
||||
uint64_t start = host_span_begin();
|
||||
~EventSpan() { host_span_end(HSPAN_HOST_EVENTS, start, 0); }
|
||||
} event_span;
|
||||
SDL_Event ev;
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (psx_local_mouse_event(ev)) continue;
|
||||
@@ -9379,8 +9205,6 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
int override = -1;
|
||||
int override_p2 = -1;
|
||||
#endif
|
||||
int replay_override = input_replay_buttons(s_frame_count);
|
||||
if (replay_override >= 0) override = replay_override;
|
||||
/* Always-on wall-time stamp for this guest frame (host_span_ring.h). */
|
||||
extern uint64_t s_frame_count;
|
||||
uint32_t stamp_static_checks = 0;
|
||||
@@ -9402,7 +9226,6 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
}
|
||||
runtime_perf_frame_begin();
|
||||
RuntimePerfFrameScope runtime_perf_frame_scope;
|
||||
input_replay_frontend_tick(s_frame_count);
|
||||
runtime_perf_diag_tick();
|
||||
|
||||
/* Lightweight frontend telemetry from PR #13. Count simulated vblanks rather
|
||||
@@ -9440,6 +9263,15 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
snprintf(title, sizeof(title), "%s [Game %.0f fps %.2fx]",
|
||||
s_fps_base_title.c_str(), fps, speed);
|
||||
}
|
||||
{
|
||||
PsxPresentCadence cadence{};
|
||||
psx_video_telemetry_cadence(1000, 0, &cadence);
|
||||
const double sfps = cadence.fps;
|
||||
const unsigned hitch = cadence.hitches, worst = cadence.max_us / 1000;
|
||||
size_t l = strlen(title);
|
||||
snprintf(title + l, sizeof(title) - l, " [Present %.1f fps, %u hitch, max %u ms]",
|
||||
sfps, hitch, worst);
|
||||
}
|
||||
SDL_SetWindowTitle(sdl_window, title);
|
||||
}
|
||||
if (!g_headless) {
|
||||
@@ -9751,11 +9583,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* frame hooks may touch guest state, so neither may depend on whether this
|
||||
* frame is presented — otherwise headless validation runs a different game
|
||||
* than the windowed build. Hooks still run after all normal input sampling. */
|
||||
{
|
||||
const uint64_t start = host_span_begin();
|
||||
mod_call_frame_hooks();
|
||||
host_span_end(HSPAN_MOD_WORK, start, 0);
|
||||
}
|
||||
mod_call_frame_hooks();
|
||||
|
||||
/* Resize-driven mode updates the pending aspect during BIOS boot too, but
|
||||
* the actual wide compositor remains disengaged until game entry. No-op
|
||||
@@ -10524,7 +10352,8 @@ static void render_thread_vblank(void) {
|
||||
}
|
||||
} else {
|
||||
std::fprintf(stdout, "psxrecomp: render thread requested but not started "
|
||||
"(needs the OpenGL backend without netplay or a 24-bit display)\n");
|
||||
"(needs the OpenGL backend without netplay, frame interpolation "
|
||||
"or a 24-bit display)\n");
|
||||
if (g_frame_generation)
|
||||
std::fprintf(stdout, "psxrecomp: Smooth motion (frame generation) needs the render thread; off\n");
|
||||
}
|
||||
@@ -11279,13 +11108,6 @@ extern "C" int psx_dynres_status_json(char *out, int cap) {
|
||||
|
||||
|
||||
static void sdl_vblank_present(void) {
|
||||
extern uint64_t s_frame_count;
|
||||
struct ReplayCompletion {
|
||||
~ReplayCompletion() {
|
||||
extern uint64_t s_frame_count;
|
||||
input_replay_frame_completed(s_frame_count);
|
||||
}
|
||||
} completion;
|
||||
sync_guest_cadence_to_video_standard();
|
||||
NetplayVblankEpilogue ep = sdl_vblank_present_body();
|
||||
render_thread_vblank();
|
||||
@@ -18541,6 +18363,26 @@ int main(int argc, char** argv) {
|
||||
addresses.data(), expected.data(), results.data(),
|
||||
(int)addresses.size());
|
||||
}
|
||||
{
|
||||
// [[widescreen.cull.sxy]]: four slots per site.
|
||||
std::vector<uint32_t> vertices, fold_addresses, fold_expected,
|
||||
vertex_regs, consumer_addresses, consumer_expected;
|
||||
for (const auto& site : gc.ws_cull_sxy_sites) {
|
||||
vertices.push_back(site.vertices);
|
||||
for (size_t k = 0; k < 4; k++) {
|
||||
const bool used = k < site.vertices;
|
||||
fold_addresses.push_back(used ? site.fold_addresses[k] : 0u);
|
||||
fold_expected.push_back(used ? site.fold_expected[k] : 0u);
|
||||
vertex_regs.push_back(used ? site.vertex_regs[k] : 0u);
|
||||
}
|
||||
consumer_addresses.push_back(site.consumer_address);
|
||||
consumer_expected.push_back(site.consumer_expected);
|
||||
}
|
||||
gpu_ws_set_sxy_cull_sites(
|
||||
vertices.data(), fold_addresses.data(), fold_expected.data(),
|
||||
vertex_regs.data(), consumer_addresses.data(),
|
||||
consumer_expected.data(), (int)vertices.size());
|
||||
}
|
||||
{
|
||||
std::vector<uint32_t> addresses, expected, masks;
|
||||
for (const auto& site : gc.ws_cull_masked_reject_sites) {
|
||||
@@ -20599,6 +20441,11 @@ int main(int argc, char** argv) {
|
||||
netplay && PSXRecompV4::mod_runtime_netplay_view_active() ? 1 : 0);
|
||||
psx_mod_immediate_begin_session(netplay ? 1 : 0);
|
||||
mod_runtime_activate_plugins();
|
||||
if (const char* error = mod_runtime_activation_error()) {
|
||||
std::fprintf(stderr,"psxrecomp: %s\n",error);
|
||||
return false;
|
||||
}
|
||||
func_override_install();
|
||||
if (!psx_mod_immediate_session_valid()) {
|
||||
std::fprintf(stderr, "psxrecomp: immediate-selector enhancements are offline-only\n");
|
||||
return false;
|
||||
@@ -21253,12 +21100,6 @@ session_reboot:
|
||||
memcard_init_slots(memcard_dir_str.c_str(), slots);
|
||||
}
|
||||
if (!rematch_session) {
|
||||
if (const char *route = std::getenv("PSX_INPUT_REPLAY")) {
|
||||
if (psx_netplay_active() || !input_replay_load(route)) {
|
||||
std::puts("psxrecomp: input replay requires a valid route and an offline session");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
std::atexit(memcard_flush_all);
|
||||
/* Persist the game's native OPTION settings on any exit path (belt-and-
|
||||
* suspenders; save-on-change already persists them live). No-op if not yet
|
||||
|
||||
@@ -1687,6 +1687,16 @@ uint32_t psx_read_word(uint32_t addr) {
|
||||
if (!psx_get_in_exception()) ls_read_hook(addr, 4, v);
|
||||
return v;
|
||||
}
|
||||
/* Untraced word peek for HOST-SIDE inspection: same address decode as a guest
|
||||
* read, but no lockstep record, no lockstep-replay substitution, and no
|
||||
* data-shard/DuckStation capture. Use this for anything the guest did not
|
||||
* actually execute — residency guards, host-side checks, diagnostics — so a
|
||||
* host inspection can never appear in a guest read stream (which would both
|
||||
* dirty every divergence comparison and, under replay, return replayed data
|
||||
* instead of resident bytes). Never use it to service a guest load. */
|
||||
uint32_t psx_peek_word_untraced(uint32_t addr) {
|
||||
return psx_read_word_raw(addr);
|
||||
}
|
||||
/* Physical address of a CPU/DMA main-RAM access. Fold KUSEG/KSEG0/KSEG1 first
|
||||
* (0x1FFFFFFF), then fold the 2nd-4th main-RAM mirrors: real hardware mirrors the
|
||||
* 2 MB DRAM across the WHOLE 0..0x7FFFFF physical window (PSX-SPX memorymap,
|
||||
|
||||
@@ -50,6 +50,10 @@ struct RegisteredPlugin {
|
||||
std::vector<ModFunctionEntryHook> function_entries;
|
||||
std::vector<ModFunctionEntryHook> guest_functions;
|
||||
std::vector<ModInstructionHook> instructions;
|
||||
/* Set when a function override is queued under this id (the callback
|
||||
* itself lives in mod_runtime's pending list) — participates in
|
||||
* availability so a manifest can gate an override-only plugin. */
|
||||
bool function_override = false;
|
||||
};
|
||||
|
||||
std::map<std::string, RegisteredPlugin>& registered_plugins() {
|
||||
@@ -1535,12 +1539,20 @@ bool mod_register_function_filter_plugin(const std::string& id, uint32_t address
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mod_register_function_override_marker(const std::string& id) {
|
||||
if (!valid_id(id)) return false;
|
||||
RegisteredPlugin& plugin = registered_plugins()[id];
|
||||
if (plugin.function_override) return false;
|
||||
plugin.function_override = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
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.session_reset || found->second.vblank || found->second.savestate ||
|
||||
!found->second.function_entries.empty() || !found->second.guest_functions.empty() ||
|
||||
!found->second.instructions.empty());
|
||||
!found->second.instructions.empty() || found->second.function_override);
|
||||
}
|
||||
|
||||
bool mod_register_guest_function_plugin(const std::string& id, uint32_t address,
|
||||
|
||||
+112
-1
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "disc_path.h"
|
||||
#include "host_file_identity.h"
|
||||
#include "func_override.h"
|
||||
#include "iso_reader.h"
|
||||
#include "mod_packages.h"
|
||||
#include "mod_plugins.h"
|
||||
@@ -368,6 +369,27 @@ void build_function_entry_hooks(const RuntimeMods& s) {
|
||||
g_psx_mod_instruction_hooks = (uint32_t)instructions.size();
|
||||
}
|
||||
|
||||
/* Function overrides queue here at constructor time and are ARMED into the
|
||||
* func_override tier only for plugins the resolved package plan selects —
|
||||
* the same gating as vblank/activation callbacks. `id` is the full
|
||||
* registered name (may carry a ":label" suffix, kept for diagnostics);
|
||||
* `plugin` is the manifest-facing part gating matches on. */
|
||||
struct FunctionOverridePlugin {
|
||||
std::string id;
|
||||
std::string plugin;
|
||||
uint32_t address = 0;
|
||||
PSXModFunctionOverrideFn fn = nullptr;
|
||||
uint32_t guard[FO_MAX_GUARD_WORDS] = {0, 0, 0, 0};
|
||||
int n_guard = 0;
|
||||
int32_t credit = 0;
|
||||
bool armed = false;
|
||||
};
|
||||
|
||||
std::vector<FunctionOverridePlugin>& function_override_plugins() {
|
||||
static std::vector<FunctionOverridePlugin> value;
|
||||
return value;
|
||||
}
|
||||
|
||||
const ModPackage* selected_package(const std::string& id) {
|
||||
return state().manager.selected_package(id);
|
||||
}
|
||||
@@ -1663,6 +1685,20 @@ bool mod_runtime_clear_for_netplay(std::string* error) {
|
||||
s.disc_guard_failed = false;
|
||||
s.netplay_view_plan = false;
|
||||
s.error.clear();
|
||||
/* Clearing s.plan is enough for activation/vblank callbacks — they only
|
||||
* run while something iterates the plan. Function overrides are armed
|
||||
* into func_override.c's own table with the dispatcher hook installed,
|
||||
* so they survive a cleared plan and keep firing unless explicitly
|
||||
* disarmed. That matters most on the rematch path (main.cpp jumps to
|
||||
* session_reboot, which is PAST mod_runtime_activate_plugins and
|
||||
* func_override_install), where a modded session entering netplay would
|
||||
* otherwise print the vanilla banner and still run its overrides —
|
||||
* diverging from a peer without the mod. Also re-arm-able: dropping the
|
||||
* armed flag lets a later plan register the same override again. */
|
||||
const int disarmed = func_override_reset_package_armed();
|
||||
(void)disarmed;
|
||||
for (FunctionOverridePlugin& pending : function_override_plugins())
|
||||
pending.armed = false;
|
||||
if (error) error->clear();
|
||||
std::fprintf(stdout, "psxrecomp: mods cleared for netplay (vanilla session)\n");
|
||||
return true;
|
||||
@@ -2962,6 +2998,17 @@ bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_by
|
||||
}
|
||||
} // namespace PSXRecompV4
|
||||
|
||||
static std::string s_override_activation_error;
|
||||
extern "C" const char* mod_runtime_activation_error(void) {
|
||||
return s_override_activation_error.empty() ? nullptr : s_override_activation_error.c_str();
|
||||
}
|
||||
|
||||
static int invoke_package_override(CPUState* cpu, FuncOverrideFn fn, const void* context) {
|
||||
using namespace PSXRecompV4;
|
||||
PluginCallbackScope scope(state(),static_cast<const ModResolution::Plugin*>(context),cpu);
|
||||
return fn(cpu);
|
||||
}
|
||||
|
||||
extern "C" void mod_runtime_activate_plugins(void) {
|
||||
using namespace PSXRecompV4;
|
||||
HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION);
|
||||
@@ -2969,6 +3016,9 @@ extern "C" void mod_runtime_activate_plugins(void) {
|
||||
psx_host_set_hud_draw_cb(nullptr, nullptr);
|
||||
mod_reset_plugin_sessions();
|
||||
RuntimeMods& s = state();
|
||||
s_override_activation_error.clear();
|
||||
func_override_reset_package_armed();
|
||||
for (auto& pending : function_override_plugins()) pending.armed = false;
|
||||
s.ticket_ready = false;
|
||||
psx_mod_set_disc_request_provider(nullptr);
|
||||
psx_projection_reset_session();
|
||||
@@ -2979,6 +3029,28 @@ extern "C" void mod_runtime_activate_plugins(void) {
|
||||
s.disc_extents.clear();
|
||||
s.audio_tracks.clear();
|
||||
s.extent_start = 0;
|
||||
/* Arm the entire tier before activation callbacks change host state. */
|
||||
for (auto& pending : function_override_plugins()) {
|
||||
const auto owner = std::find_if(s.plan.plugins.begin(),s.plan.plugins.end(),
|
||||
[&](const ModResolution::Plugin& p) { return p.id == pending.plugin; });
|
||||
if (owner == s.plan.plugins.end()) continue;
|
||||
const int rc = s.netplay_view_plan ? FO_ERR_ARGS : func_override_add_package_scoped(
|
||||
pending.id.c_str(),pending.address,pending.fn,
|
||||
pending.n_guard ? pending.guard : nullptr,pending.n_guard,pending.credit,
|
||||
invoke_package_override,&*owner);
|
||||
if (rc != FO_OK) {
|
||||
s_override_activation_error = "Cannot arm function override " + pending.id +
|
||||
" (registration error " + std::to_string(rc) + ")";
|
||||
s.error = s_override_activation_error;
|
||||
s.plan.ok = false;
|
||||
func_override_reset_package_armed();
|
||||
for (auto& item : function_override_plugins()) item.armed = false;
|
||||
clear_function_entry_hooks();
|
||||
return;
|
||||
}
|
||||
pending.armed = true;
|
||||
}
|
||||
func_override_install();
|
||||
s.activating = true;
|
||||
psx_mod_immediate_begin_activation();
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
|
||||
@@ -3471,6 +3543,45 @@ static int mod_plan_hooks_parked(void) {
|
||||
return PSXRecompV4::state().netplay_view_plan && !psx_mod_local_view_scope();
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_register_function_override(
|
||||
const char* id, uint32_t address, PSXModFunctionOverrideFn fn,
|
||||
const uint32_t* expected_words, int n_words, int32_t credit) {
|
||||
using namespace PSXRecompV4;
|
||||
if (!id || !*id || !(address & 0x1fffffffu) || (address & 3u) || !fn) return 0;
|
||||
if (n_words < 0 || n_words > FO_MAX_GUARD_WORDS) return 0;
|
||||
if (n_words > 0 && !expected_words) return 0;
|
||||
if (credit < FO_CREDIT_SELF) return 0;
|
||||
/* An optional ":label" suffix names this override in diagnostics (the
|
||||
* `func_override` TCP command) without multiplying manifest plugin ids —
|
||||
* gating and resolver availability use only the part before the ':'.
|
||||
* "pkg.feature:aim" and "pkg.feature:fire" are two overrides under the
|
||||
* one manifest plugin "pkg.feature". */
|
||||
const char* colon = strchr(id, ':');
|
||||
const std::string plugin_id = colon ? std::string(id, colon - id)
|
||||
: std::string(id);
|
||||
if (plugin_id.empty() || (colon && !colon[1])) return 0;
|
||||
auto& plugins = function_override_plugins();
|
||||
const auto duplicate = std::find_if(
|
||||
plugins.begin(), plugins.end(),
|
||||
[&](const FunctionOverridePlugin& item) {
|
||||
return item.id == id && item.address == address;
|
||||
});
|
||||
if (duplicate != plugins.end()) return 0;
|
||||
FunctionOverridePlugin plugin;
|
||||
plugin.id = id;
|
||||
plugin.plugin = plugin_id;
|
||||
plugin.address = address;
|
||||
plugin.fn = fn;
|
||||
for (int i = 0; i < n_words; ++i) plugin.guard[i] = expected_words[i];
|
||||
plugin.n_guard = n_words;
|
||||
plugin.credit = credit;
|
||||
plugins.push_back(plugin);
|
||||
/* Mark the plugin id available to the package resolver so a manifest can
|
||||
* gate an override-only plugin (multiple overrides may share one). */
|
||||
mod_register_function_override_marker(plugin_id);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -3673,7 +3784,7 @@ static int game_netplay_function_entry(CPUState* cpu, uint32_t address) {
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_function_entry_active(void) {
|
||||
return PSXRecompV4::function_entry_depth != 0;
|
||||
return PSXRecompV4::function_entry_depth != 0 || func_override_active();
|
||||
}
|
||||
|
||||
extern "C" void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) {
|
||||
|
||||
@@ -161,14 +161,9 @@ extern "C" void pgxp_invalidate_all(void) {
|
||||
|
||||
extern "C" void pgxp_set_enabled(int enabled) {
|
||||
int resized = 0;
|
||||
/* Writer/triangle/miss receipts are consumed only by debug commands.
|
||||
* Production still propagates every precision shadow and keeps summary
|
||||
* counters, but does not copy a diagnostic record for each guest store. */
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
if (enabled && !s_store_ring)
|
||||
s_store_ring = (PGXPStoreRecord *)std::calloc(PGXP_STORE_RING_CAP,
|
||||
sizeof(PGXPStoreRecord));
|
||||
#endif
|
||||
if (enabled && !s_ram) {
|
||||
s_ram = (PGXPValue *)std::calloc(PGXP_SHADOW_WORDS, sizeof(PGXPValue));
|
||||
if (s_ram)
|
||||
@@ -276,14 +271,10 @@ static uint64_t s_tri_seq = 0;
|
||||
static int ck_in_pass(void); /* render-pass checkpoint open */
|
||||
|
||||
extern "C" void pgxp_note_triangle_detail(const PGXPTriRecord *rec) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
PGXPTriRecord *r = &s_tri_ring[s_tri_seq % PGXP_TRI_RING_CAP];
|
||||
*r = *rec;
|
||||
r->seq = s_tri_seq++;
|
||||
r->pass = ck_in_pass() ? 1u : 0u;
|
||||
#else
|
||||
(void)rec;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" uint64_t pgxp_tri_ring(const PGXPTriRecord **ring, uint32_t *cap) {
|
||||
@@ -307,7 +298,6 @@ extern "C" void pgxp_set_trace_sources(const uint32_t *store_pc,
|
||||
|
||||
static void note_store(uint32_t op, uint32_t reg, const PGXPValue *src,
|
||||
uint32_t addr, uint32_t value, const PGXPValue *dst) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
if (!s_store_ring) return;
|
||||
PGXPStoreRecord *r = &s_store_ring[s_store_seq % PGXP_STORE_RING_CAP];
|
||||
r->seq = s_store_seq++;
|
||||
@@ -323,9 +313,6 @@ static void note_store(uint32_t op, uint32_t reg, const PGXPValue *src,
|
||||
r->dst_flags = (dst && dst->gen == s_gen) ? dst->flags : 0u;
|
||||
r->dst_x16 = dst ? dst->x16 : 0;
|
||||
r->dst_y16 = dst ? dst->y16 : 0;
|
||||
#else
|
||||
(void)op; (void)reg; (void)src; (void)addr; (void)value; (void)dst;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" uint64_t pgxp_store_ring(const PGXPStoreRecord **ring, uint32_t *cap) {
|
||||
@@ -1998,7 +1985,6 @@ static uint64_t s_miss_seq = 0;
|
||||
|
||||
static void note_word_miss(uint32_t addr, uint32_t packed, const PGXPValue *pv,
|
||||
uint8_t reason) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
PGXPWordMiss *m = &s_miss_ring[s_miss_seq % PGXP_MISS_RING_CAP];
|
||||
m->seq = s_miss_seq++;
|
||||
m->addr = addr;
|
||||
@@ -2007,9 +1993,6 @@ static void note_word_miss(uint32_t addr, uint32_t packed, const PGXPValue *pv,
|
||||
m->shadow_flags = pv ? (uint8_t)pv->flags : 0;
|
||||
m->live = (pv && pv->gen == s_gen) ? 1 : 0;
|
||||
m->reason = reason;
|
||||
#else
|
||||
(void)addr; (void)packed; (void)pv; (void)reason;
|
||||
#endif
|
||||
}
|
||||
|
||||
extern "C" uint64_t pgxp_word_miss_ring(const PGXPWordMiss **ring,
|
||||
|
||||
@@ -238,13 +238,15 @@ int pfx_chain_parse(const char *text, const char *name, PfxShaderLoader load, vo
|
||||
if (!strcmp(f, "source")) from = PFX_IN_SOURCE;
|
||||
else if (!strcmp(f, "source_down")) from = PFX_IN_SOURCE_DOWN;
|
||||
else if (!strcmp(f, "mask")) { from = PFX_IN_MASK; out->uses_mask = 1; }
|
||||
else if (!strcmp(f, "depth")) { from = PFX_IN_DEPTH; out->uses_depth = 1; }
|
||||
else if (!strcmp(f, "history")) from = PFX_IN_HISTORY;
|
||||
else if (!strcmp(f, "previous")) {
|
||||
if (self == 0) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "the first pass has no previous"); }
|
||||
from = self - 1;
|
||||
} else {
|
||||
from = -100;
|
||||
for (int i = 0; i < self; i++) if (!strcmp(out->pass[i].name, f)) from = i;
|
||||
if (from == -100) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "unknown input '%s' (source, mask, previous or an earlier pass)", f); }
|
||||
if (from == -100) { pfx_chain_free(out); return pfx_fail(err, errlen, line, "unknown input '%s' (source, mask, depth, history, previous or an earlier pass)", f); }
|
||||
}
|
||||
snprintf(cur->in[cur->ninputs].sampler, PFX_NAME, "%s", s);
|
||||
cur->in[cur->ninputs++].from = from;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include "gpu.h"
|
||||
#include "host_keymap.h"
|
||||
#include "host_osd.h"
|
||||
#include "host_span_ring.h"
|
||||
#include "interrupts.h"
|
||||
#include "mdec.h"
|
||||
#include "psx_cycles.h"
|
||||
@@ -584,11 +583,8 @@ void psx_rewind_poll(CPUState *cpu, uint32_t resume_pc)
|
||||
(void)do_load(cpu, tick);
|
||||
return;
|
||||
}
|
||||
if (s_capture_due && !s_open && !psx_netplay_active() && !s_title_blocked) {
|
||||
const uint64_t start = host_span_begin();
|
||||
const int ok = do_capture(cpu, resume_pc);
|
||||
host_span_end(HSPAN_REWIND_CAPTURE, start, (uint32_t)ok);
|
||||
}
|
||||
if (s_capture_due && !s_open && !psx_netplay_active() && !s_title_blocked)
|
||||
(void)do_capture(cpu, resume_pc);
|
||||
}
|
||||
|
||||
int psx_rewind_toggle(void)
|
||||
|
||||
@@ -839,7 +839,7 @@ static int transaction_end(int eye, uint32_t alpha, int keep) {
|
||||
|
||||
static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
PSXModRenderPassFn fn, void *user, int eye) {
|
||||
uint64_t t0, t1, tv, tc, tb, tg, te, tr, hash_before = 0, hash_after = 0;
|
||||
uint64_t t0, t1, tb, tg, te, tr, hash_before = 0, hash_after = 0;
|
||||
uint64_t cycles_before;
|
||||
uint32_t leaks;
|
||||
int ok = 0, open, reuse;
|
||||
@@ -916,7 +916,6 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass
|
||||
}
|
||||
s_open_generation = 0;
|
||||
s_leaks_before = gl_renderer_pass_leaks();
|
||||
tv = gl_renderer_perf_ticks();
|
||||
|
||||
/* A stereo eye, or the second matte of a capture, whose machine state still
|
||||
* equals the baseline captured for it skips re-capturing that state. */
|
||||
@@ -939,7 +938,6 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass
|
||||
}
|
||||
if (reuse_baseline) s_stereo.checkpoint_reuses++;
|
||||
else { s_stereo.checkpoint_saves++; s_stereo.checkpoint_ram_bytes += s_ck.ram_bytes; }
|
||||
tc = gl_renderer_perf_ticks();
|
||||
if (verify_on()) hash_before = state_hash(cpu);
|
||||
|
||||
tb = gl_renderer_perf_ticks();
|
||||
@@ -1081,16 +1079,6 @@ static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass
|
||||
double ms = (double)(t1 - t0) * 1000.0 /
|
||||
(double)gl_renderer_perf_frequency();
|
||||
s_stats.last_pass_ms = ms;
|
||||
s_stats.timed_passes++;
|
||||
s_stats.total_gl_begin_ms += (tv - t0) * s_ms_per_tick;
|
||||
s_stats.total_checkpoint_ms += (tc - tv) * s_ms_per_tick;
|
||||
s_stats.total_verify_before_ms += (tb - tc) * s_ms_per_tick;
|
||||
s_stats.total_guest_ms += (tg - tb) * s_ms_per_tick;
|
||||
s_stats.total_gl_end_ms += (te - tg) * s_ms_per_tick;
|
||||
s_stats.total_restore_ms += (tr - te) * s_ms_per_tick;
|
||||
s_stats.total_verify_after_ms += (t1 - tr) * s_ms_per_tick;
|
||||
s_stats.total_pass_ms += ms;
|
||||
if (ms > s_stats.max_pass_ms) s_stats.max_pass_ms = ms;
|
||||
s_stats.avg_pass_ms = s_stats.avg_pass_ms > 0.0
|
||||
? s_stats.avg_pass_ms * 0.9 + ms * 0.1 : ms;
|
||||
}
|
||||
|
||||
+17
-29
@@ -102,11 +102,8 @@ static struct {
|
||||
uint64_t tick_at; /* consumer: next tick deadline (0 = none) */
|
||||
|
||||
/* shared positions / counters */
|
||||
/* Single-writer publications must not invalidate each other's cache line. */
|
||||
_Alignas(128) _Atomic uint64_t wpos;
|
||||
_Atomic uint64_t frames_produced;
|
||||
_Alignas(128) _Atomic uint64_t rpos;
|
||||
_Atomic uint64_t frames_consumed;
|
||||
_Atomic uint64_t wpos, rpos;
|
||||
_Atomic uint64_t frames_produced, frames_consumed;
|
||||
_Atomic int render_sleeping, emu_sleeping;
|
||||
_Atomic int running;
|
||||
|
||||
@@ -117,7 +114,7 @@ static struct {
|
||||
_Atomic int released; /* read unlocked by emu_wait's first check */
|
||||
|
||||
/* producer-only */
|
||||
_Alignas(128) uint64_t pend_start, pend_size, pend_payload_off;
|
||||
uint64_t pend_start, pend_size, pend_payload_off;
|
||||
uint16_t pend_op, pend_flags;
|
||||
uint32_t pend_payload;
|
||||
int pending;
|
||||
@@ -128,8 +125,8 @@ static struct {
|
||||
/* stats (producer unless noted) */
|
||||
uint64_t records, bytes, acquires, releases, oversize;
|
||||
uint64_t bp_waits, full_waits, bp_ns, full_ns, acq_ns, high_water;
|
||||
_Alignas(128) _Atomic uint64_t busy_ns, idle_ns; /* consumer */
|
||||
_Alignas(128) _Atomic uint64_t bp_events; /* producer writes, any thread reads */
|
||||
_Atomic uint64_t busy_ns, idle_ns; /* consumer */
|
||||
_Atomic uint64_t bp_events; /* producer writes, any thread reads */
|
||||
RtAcquireEvent acq_ring[ACQ_RING];
|
||||
uint64_t acq_n;
|
||||
|
||||
@@ -210,8 +207,6 @@ static void *render_main(void *arg)
|
||||
R.ctx(R.user, 1);
|
||||
uint64_t r = atomic_load(&R.rpos);
|
||||
int have_ctx = 1;
|
||||
const char *batch_env = getenv("PSX_RENDER_QUEUE_BATCH");
|
||||
const unsigned batch_limit = batch_env && batch_env[0] == '0' ? 1 : WAKE_RECORDS;
|
||||
for (;;) {
|
||||
run_tick();
|
||||
uint64_t w = atomic_load_explicit(&R.wpos, memory_order_acquire);
|
||||
@@ -247,23 +242,16 @@ static void *render_main(void *arg)
|
||||
continue;
|
||||
}
|
||||
uint64_t t0 = now_ns();
|
||||
unsigned count = 0;
|
||||
/* Time and publish a bounded ready prefix, not every state command.
|
||||
* Stop at frame markers so producer backpressure is released at the
|
||||
* same boundary. The acquire above covers every payload in this prefix. */
|
||||
do {
|
||||
const RtCmd *c = (const RtCmd *)(R.ring + (r & R.mask));
|
||||
const int frame = c->op == OP_PAD && (c->flags & F_FRAME);
|
||||
if (c->op == OP_PAD) {
|
||||
if (frame) atomic_fetch_add_explicit(&R.frames_consumed, 1, memory_order_relaxed);
|
||||
} else {
|
||||
R.exec(R.user, c, (const uint8_t *)c + sizeof(RtCmd));
|
||||
}
|
||||
r += c->size;
|
||||
if (frame) break;
|
||||
} while (r != w && ++count < batch_limit);
|
||||
atomic_store_explicit(&R.rpos, r, memory_order_release);
|
||||
atomic_fetch_add_explicit(&R.busy_ns, now_ns() - t0, memory_order_relaxed);
|
||||
const RtCmd *c = (const RtCmd *)(R.ring + (r & R.mask));
|
||||
if (c->op == OP_PAD) {
|
||||
if (c->flags & F_FRAME)
|
||||
atomic_fetch_add(&R.frames_consumed, 1);
|
||||
} else {
|
||||
R.exec(R.user, c, (const uint8_t *)c + sizeof(RtCmd));
|
||||
}
|
||||
r += c->size;
|
||||
atomic_store(&R.rpos, r);
|
||||
atomic_fetch_add(&R.busy_ns, now_ns() - t0);
|
||||
wake_emu();
|
||||
}
|
||||
if (have_ctx) R.ctx(R.user, 0);
|
||||
@@ -345,7 +333,7 @@ void rt_cmd_commit(void) {
|
||||
if (!R.pending) return;
|
||||
R.pending = 0;
|
||||
uint64_t end = R.pend_start + R.pend_size;
|
||||
atomic_store_explicit(&R.wpos, end, memory_order_release);
|
||||
atomic_store(&R.wpos, end);
|
||||
R.records++;
|
||||
R.bytes += R.pend_size;
|
||||
uint64_t depth = end - atomic_load(&R.rpos);
|
||||
@@ -362,7 +350,7 @@ static void commit_marker(uint16_t flags) {
|
||||
if (!pred_space(w + size)) emu_wait(pred_space, w + size);
|
||||
RtCmd *c = (RtCmd *)(R.ring + off); /* 16 bytes always fit before the end */
|
||||
c->size = (uint32_t)size; c->op = OP_PAD; c->flags = flags; c->payload = 0; c->seq = R.seq++;
|
||||
atomic_store_explicit(&R.wpos, w + size, memory_order_release);
|
||||
atomic_store(&R.wpos, w + size);
|
||||
}
|
||||
|
||||
void rt_frame_end(void) {
|
||||
|
||||
+15
-4
@@ -572,8 +572,15 @@ static int psx_change_thread_fiber(CPUState* cpu, uint32_t target_tcb)
|
||||
|
||||
HostThreadFiber* current = psx_bind_current_host_thread(cpu, current_tcb);
|
||||
int saved_current_context = 0;
|
||||
if (psx_tcb_state(cpu, current_tcb) == 0x4000u) {
|
||||
/* SYS03 saves the outgoing register context before changing the PCB,
|
||||
* including CloseTh(current); ChangeTh(other): CloseTh only marks the TCB
|
||||
* free and does not suppress the exception handler's register stores.
|
||||
* OpenTh initializes PC/SP/FP/GP but keeps the other saved registers when
|
||||
* it reuses the TCB. Saving never reopens the closed TCB. */
|
||||
const uint32_t outgoing_state = psx_tcb_state(cpu, current_tcb);
|
||||
if (outgoing_state == 0x4000u || outgoing_state == 0x1000u)
|
||||
psx_save_context_to_tcb(cpu, current_tcb, cpu->gpr[31]);
|
||||
if (outgoing_state == 0x4000u) {
|
||||
saved_current_context = 1;
|
||||
} else {
|
||||
/* 0x1000 is the BIOS closed/free state and must retire the host fiber.
|
||||
@@ -681,9 +688,13 @@ static int psx_request_thread_switch(CPUState* cpu, uint32_t target_tcb)
|
||||
}
|
||||
/* Commit the yielding thread's context so it can be re-dispatched later.
|
||||
* resume PC = its return address (resume right after the ChangeThread call).
|
||||
* Only a still-running (0x4000) thread keeps a live context; a closed/free
|
||||
* (0x1000) thread is being torn down and must not be saved. */
|
||||
if (psx_tcb_state(cpu, current_tcb) == 0x4000u) {
|
||||
* A closed/free (0x1000) thread is still the current thread until SYS03
|
||||
* switches the PCB, and the BIOS exception handler stores its registers
|
||||
* regardless: CloseTh(current); ChangeTh(other) keeps those stores. OpenTh
|
||||
* initializes PC/SP/FP/GP but keeps the other saved registers when it
|
||||
* reuses the TCB. Saving never reopens the closed TCB. */
|
||||
const uint32_t outgoing_state = psx_tcb_state(cpu, current_tcb);
|
||||
if (outgoing_state == 0x4000u || outgoing_state == 0x1000u) {
|
||||
psx_save_context_to_tcb(cpu, current_tcb, cpu->gpr[31]);
|
||||
}
|
||||
psx_set_current_tcb(cpu, target_tcb);
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
#include "video_telemetry.h"
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
|
||||
namespace {
|
||||
std::atomic<uint64_t> presents{0}, errors{0};
|
||||
std::atomic<int> status_available{0};
|
||||
std::atomic<unsigned> generation{0};
|
||||
std::atomic<int> fields[8]{};
|
||||
constexpr unsigned cadence_capacity = 4096;
|
||||
std::atomic<uint32_t> intervals[cadence_capacity]{};
|
||||
std::atomic<uint64_t> cadence_version{0}, cadence_count{0};
|
||||
std::atomic<bool> cadence_reset{false};
|
||||
// Only the current present owner touches these. Readers request reset rather
|
||||
// than changing the producer's cursor or timestamp.
|
||||
uint64_t last_present_us;
|
||||
bool have_last_present;
|
||||
}
|
||||
|
||||
extern "C" void psx_video_telemetry_compose(int renderer, int scale, int sw, int sh,
|
||||
@@ -20,8 +32,75 @@ extern "C" void psx_video_telemetry_compose(int renderer, int scale, int sw, int
|
||||
}
|
||||
|
||||
extern "C" void psx_video_telemetry_present(int result) {
|
||||
const auto now = std::chrono::steady_clock::now().time_since_epoch();
|
||||
psx_video_telemetry_present_at(result,
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(now).count());
|
||||
}
|
||||
|
||||
extern "C" void psx_video_telemetry_present_at(int result, uint64_t now_us) {
|
||||
status_available.store(result >= 0, std::memory_order_relaxed);
|
||||
(result != 0 ? presents : errors).fetch_add(1, std::memory_order_relaxed);
|
||||
if (result == 0) return;
|
||||
cadence_version.fetch_add(1);
|
||||
if (cadence_reset.exchange(false)) {
|
||||
cadence_count.store(0);
|
||||
have_last_present = false;
|
||||
}
|
||||
if (have_last_present && now_us > last_present_us) {
|
||||
const uint64_t count = cadence_count.load();
|
||||
const auto delta = std::min<uint64_t>(now_us - last_present_us, UINT32_MAX);
|
||||
intervals[count % cadence_capacity].store(static_cast<uint32_t>(delta));
|
||||
cadence_count.store(count + 1);
|
||||
}
|
||||
last_present_us = now_us;
|
||||
have_last_present = true;
|
||||
cadence_version.fetch_add(1);
|
||||
}
|
||||
|
||||
extern "C" void psx_video_telemetry_cadence(int window_ms, int reset, PsxPresentCadence *out) {
|
||||
if (!out) return;
|
||||
*out = {};
|
||||
uint32_t values[cadence_capacity]; // each reader owns its sorting storage
|
||||
uint64_t sum = 0;
|
||||
for (unsigned attempt = 0; attempt < 3; ++attempt) {
|
||||
out->count = 0; sum = 0;
|
||||
const uint64_t before = cadence_version.load();
|
||||
if (before & 1u) continue;
|
||||
if (cadence_reset.load()) { out->valid = 1; break; }
|
||||
const uint64_t count = cadence_count.load();
|
||||
const unsigned n = static_cast<unsigned>(std::min<uint64_t>(count, cadence_capacity));
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
const uint32_t value = intervals[(count - 1 - i) % cadence_capacity].load();
|
||||
if (window_ms > 0 && sum + value > static_cast<uint64_t>(window_ms) * 1000) break;
|
||||
sum += value; values[out->count++] = value;
|
||||
}
|
||||
if (before == cadence_version.load()) { out->valid = 1; break; }
|
||||
}
|
||||
if (reset) cadence_reset.store(true);
|
||||
if (!out->valid) { *out = {}; return; }
|
||||
if (!out->count) return;
|
||||
std::sort(values, values + out->count);
|
||||
const auto k = out->count;
|
||||
out->mean_us = static_cast<uint32_t>(sum / k);
|
||||
out->p50_us = values[k / 2];
|
||||
out->p95_us = values[(k - 1) * 95 / 100];
|
||||
out->p99_us = values[(k - 1) * 99 / 100];
|
||||
out->max_us = values[k - 1];
|
||||
for (unsigned i = 0; i < k; ++i)
|
||||
if (static_cast<uint64_t>(values[i]) * 2 > static_cast<uint64_t>(out->p50_us) * 3)
|
||||
++out->hitches;
|
||||
out->span_ms = sum / 1000.0;
|
||||
out->fps = sum ? k * 1e6 / static_cast<double>(sum) : 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_video_telemetry_cadence_json(int window_ms, int reset, char *out, int capacity) {
|
||||
PsxPresentCadence r{};
|
||||
psx_video_telemetry_cadence(window_ms, reset, &r);
|
||||
return std::snprintf(out, capacity,
|
||||
"\"valid\":%s,\"measurement\":\"host_api_completion\",\"swaps\":%u,\"span_ms\":%.1f,"
|
||||
"\"fps\":%.2f,\"mean_us\":%u,\"p50_us\":%u,\"p95_us\":%u,\"p99_us\":%u,\"max_us\":%u,\"hitches\":%u",
|
||||
r.valid ? "true" : "false", r.count, r.span_ms, r.fps, r.mean_us,
|
||||
r.p50_us, r.p95_us, r.p99_us, r.max_us, r.hitches);
|
||||
}
|
||||
|
||||
extern "C" void psx_video_telemetry_snapshot(PsxVideoTelemetry *out) {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
/* Renderer fixtures have no guest CPU; exercise the neutral unavailable projection.
|
||||
* Atomic publication is covered independently by postfx_publication_test. */
|
||||
#include "gte_postfx_projection.h"
|
||||
void gte_postfx_projection_enable(int enabled) {(void)enabled;}
|
||||
int gte_postfx_projection_read(GtePostfxProjection *out) {(void)out;return 0;}
|
||||
@@ -34,7 +34,7 @@ WIN_LIBS = ["opengl32", "kernel32", "user32", "gdi32", "winmm", "imm32", "ole32"
|
||||
"oleaut32", "version", "uuid", "advapi32", "setupapi", "shell32", "dinput8"]
|
||||
SKIP_EXIT = 77
|
||||
WINDOWS = os.name == "nt" or platform.system().startswith(("MINGW", "MSYS", "CYGWIN"))
|
||||
KEYS = ("rb", "digest", "hires", "wide", "stencil")
|
||||
KEYS = ("rb", "digest", "hires", "wide")
|
||||
|
||||
|
||||
def parse(stdout):
|
||||
@@ -56,10 +56,6 @@ def main():
|
||||
ap.add_argument("--fixture", type=pathlib.Path)
|
||||
ap.add_argument("--scales", default="1,4")
|
||||
ap.add_argument("--frames", default="80")
|
||||
ap.add_argument("--interpolation", action="store_true",
|
||||
help="compare temporal hold presentation on the owning context")
|
||||
ap.add_argument("--wide-present", action="store_true")
|
||||
ap.add_argument("--quality", action="store_true", help="exercise bloom, PGXP depth and anisotropic LOD")
|
||||
ap.add_argument("--single", action="store_true",
|
||||
help="run the fixture once with no arguments; its exit code is the result")
|
||||
args = ap.parse_args()
|
||||
@@ -73,14 +69,12 @@ def main():
|
||||
print("Evidence directory:", dest)
|
||||
receipt = []
|
||||
|
||||
def run(command, overrides=None):
|
||||
def run(command):
|
||||
command = [str(c) for c in command]
|
||||
env = os.environ.copy()
|
||||
if overrides: env.update(overrides)
|
||||
r = subprocess.run(command, cwd=dest, capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", env=env)
|
||||
encoding="utf-8", errors="replace")
|
||||
receipt.append({"cmd": command, "exit": r.returncode,
|
||||
"env": overrides or {}, "stdout": r.stdout, "stderr": r.stderr[-4000:]})
|
||||
"stdout": r.stdout, "stderr": r.stderr[-4000:]})
|
||||
(dest / "receipt.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
||||
return r
|
||||
|
||||
@@ -139,10 +133,7 @@ def main():
|
||||
for s in [int(v) for v in args.scales.split(",") if v]:
|
||||
runs = {}
|
||||
for threaded in (0, 1):
|
||||
mode = ["interp-wide"] if args.wide_present else ["interp"] if args.interpolation else []
|
||||
if args.quality: mode = [*(mode or ["plain"]), "quality"]
|
||||
r = run([probe, s, threaded, args.frames, *mode],
|
||||
{"PSX_GL_WIDE_MARGIN_SCISSOR": "1", "PSX_GL_COVERAGE_FAST": "1"} if args.wide_present else None)
|
||||
r = run([probe, s, threaded, args.frames])
|
||||
if first and r.returncode == SKIP_EXIT:
|
||||
# No window/GL context on this host: skip. Only the first
|
||||
# run may skip; after it, 77 is a failure like any other.
|
||||
@@ -156,26 +147,6 @@ def main():
|
||||
if r.returncode or p["failures"] != 0 or any(p[k] is None for k in KEYS):
|
||||
ok = False
|
||||
runs[threaded] = p
|
||||
if args.wide_present:
|
||||
flags = (
|
||||
"PSX_GL_WIDE_MARGIN_SCISSOR", "PSX_GL_COVERAGE_FAST",
|
||||
"PSX_GL_STATE_QUERY_FAST", "PSX_GL_QUEUE_UPLOAD_FAST",
|
||||
"PSX_GL_BLOOM_FUSE_EXTEND", "PSX_GL_TEXEL_CACHE",
|
||||
"PSX_GL_HIW_COPY_FAST", "PSX_GL_IDENTITY_COPY_FAST",
|
||||
"PSX_GL_DRAWABLE_EVENTS", "PSX_RENDER_QUEUE_BATCH",
|
||||
"PSX_GL_HIW_ASYNC_PACE")
|
||||
controls = [{flag: "0" for flag in flags}]
|
||||
controls += [{flag: "0"} for flag in flags]
|
||||
controls += [{"PSX_GL_TEXEL_CACHE_LAYERS": "4"}]
|
||||
for control in controls:
|
||||
overrides = {flag: "1" for flag in flags}
|
||||
overrides.update(control)
|
||||
reference = run([probe, s, threaded, args.frames, *mode], overrides)
|
||||
baseline = parse(reference.stdout)
|
||||
if reference.returncode or baseline["failures"] != 0 or any(baseline[k] != p[k] for k in KEYS):
|
||||
print(f"FAIL scale {s}: controls {control} changed rendered output", baseline, p)
|
||||
ok = False
|
||||
|
||||
for k in KEYS:
|
||||
if runs[0][k] != runs[1][k]:
|
||||
print(f"FAIL scale {s}: {k} differs with the render thread on "
|
||||
@@ -183,7 +154,7 @@ def main():
|
||||
ok = False
|
||||
# Dynamic resolution: the same level steps between frames, recorded with
|
||||
# the thread on (scales above 1 only; 1x has no levels below it).
|
||||
for s in [int(v) for v in args.scales.split(",") if v and int(v) > 1 and not args.interpolation and not args.wide_present]:
|
||||
for s in [int(v) for v in args.scales.split(",") if v and int(v) > 1]:
|
||||
runs = {}
|
||||
for threaded in (0, 1):
|
||||
r = run([probe, s, threaded, args.frames, "dynres"])
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "../third_party/stb_image.h"
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "texture_pack_preload.hpp"
|
||||
#include <stdexcept>
|
||||
#include <atomic>
|
||||
#include "../src/png_write.h"
|
||||
|
||||
#include <array>
|
||||
@@ -109,6 +112,98 @@ bool decode_fails(DuckTexturePack* pack, uint64_t id) {
|
||||
return status == HD_TEXTURE_LOOKUP_ERROR;
|
||||
}
|
||||
|
||||
void preload(const fs::path& root) {
|
||||
namespace tp = psx_texpreload;
|
||||
const char* name = "texpage-P4-3631D2A04EC85311-9CBCDEA9305D7528-64x256-4-2-8x2-P0-15.png";
|
||||
png(root/"replacements"/name,16,4);
|
||||
std::vector<uint16_t> vram(1024*512);
|
||||
place(vram,65,258,2,words(4)); place(vram,512,400,16,words(16));
|
||||
{
|
||||
auto pack = create(root); auto q = query(vram,64,256,0,5,10,2,3);
|
||||
HdTexturePreloadResult r{};
|
||||
check(duck_texture_pack_preload(pack.get(),size_t(64)<<20,4,&r) == 1 && r.loaded == 1 &&
|
||||
r.requested == 1 && r.bytes == 16*4*4 && r.failed == 0,"preload decodes the pack");
|
||||
DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"preloaded fixture matches");
|
||||
DuckTexturePixels pixels{};
|
||||
check(duck_texture_pack_acquire_decoded(pack.get(),match.entry_id,&pixels) == HD_TEXTURE_LOOKUP_FOUND &&
|
||||
pixels.width == 16,"preloaded image is resident without a decode request");
|
||||
duck_texture_pixels_release(&pixels);
|
||||
duck_texture_pack_set_decode_budget(pack.get(),1);
|
||||
check(duck_texture_pack_preload(pack.get(),256,1,&r) == 1 && r.loaded == 1 &&
|
||||
r.failed == 0 && r.bytes == 256 && r.worker_errors == 0,
|
||||
"repeat counts existing residency without decoding under the reduced budget");
|
||||
check(duck_texture_pack_preload(pack.get(),255,1,&r) == -1 && r.bytes == 256,
|
||||
"a smaller lifetime cap rejects without hiding pinned residency");
|
||||
check(duck_texture_pack_acquire_decoded(pack.get(),match.entry_id,&pixels) == HD_TEXTURE_LOOKUP_FOUND,
|
||||
"a preloaded image survives budget eviction");
|
||||
duck_texture_pixels_release(&pixels);
|
||||
}
|
||||
{ /* Over the aggregate cap: partial, nothing pinned, async path still decodes. */
|
||||
auto pack = create(root); auto q = query(vram,64,256,0,5,10,2,3);
|
||||
HdTexturePreloadResult r{};
|
||||
check(duck_texture_pack_preload(pack.get(),16,4,&r) == 0 && r.loaded == 0 && r.over_budget == 1 &&
|
||||
r.bytes == 0,"oversized image is skipped by the preload cap");
|
||||
DuckTextureMatch match{};
|
||||
check(duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND,"capped fixture matches");
|
||||
DuckTexturePixels pixels = decoded(pack.get(),match.entry_id);
|
||||
check(pixels.width == 16,"an image the preload skipped decodes on demand");
|
||||
duck_texture_pixels_release(&pixels);
|
||||
}
|
||||
{ /* Injected worker failure: caught, reported, image left to the async path. */
|
||||
auto pack = create(root);
|
||||
tp::fault_mode() = 1;
|
||||
HdTexturePreloadResult r{};
|
||||
const int status = duck_texture_pack_preload(pack.get(),size_t(64)<<20,4,&r);
|
||||
tp::fault_mode() = 0;
|
||||
check(status == 0 && r.worker_errors == 1 && r.failed == 1 && r.loaded == 0,
|
||||
"a throwing decode is caught and reported as a partial preload");
|
||||
}
|
||||
{ /* Publication can fail after retaining a pinned prefix. */
|
||||
auto pack = create(root);
|
||||
tp::fault_mode() = 3;
|
||||
HdTexturePreloadResult r{};
|
||||
const int status = duck_texture_pack_preload(pack.get(),size_t(64)<<20,4,&r);
|
||||
tp::fault_mode() = 0;
|
||||
check(status == -1 && r.loaded == 1 && r.bytes == 16*4*4,
|
||||
"publication failure still reports every retained byte");
|
||||
check(duck_texture_pack_preload(pack.get(),256,1,&r) == 1 &&
|
||||
r.loaded == 1 && r.failed == 0 && r.bytes == 256,
|
||||
"publication retry accounts the retained prefix without decoding again");
|
||||
}
|
||||
{ /* Malformed image: failed, not pinned, async decode reports failure. */
|
||||
std::ofstream(root/"replacements"/name,std::ios::binary|std::ios::trunc) << "not a png";
|
||||
auto pack = create(root); auto q = query(vram,64,256,0,5,10,2,3);
|
||||
HdTexturePreloadResult r{};
|
||||
check(duck_texture_pack_preload(pack.get(),size_t(64)<<20,4,&r) == 0 && r.failed == 1 &&
|
||||
r.worker_errors == 0,"malformed image fails the preload item");
|
||||
DuckTextureMatch match{};
|
||||
if (duck_texture_pack_match(pack.get(),&q,&match) == HD_TEXTURE_LOOKUP_FOUND)
|
||||
check(decode_fails(pack.get(),match.entry_id),"malformed image still fails on demand");
|
||||
}
|
||||
}
|
||||
|
||||
void preload_pool() {
|
||||
namespace tp = psx_texpreload;
|
||||
std::atomic<int> ran{0};
|
||||
tp::fault_mode() = 2; /* the second thread cannot be created */
|
||||
size_t errors = tp::run(32,8,[&](size_t){ ++ran; });
|
||||
check(errors == 0 && ran == 32,"thread creation failure lowers concurrency, all items still run");
|
||||
tp::fault_mode() = 1; /* the first item throws bad_alloc */
|
||||
ran = 0;
|
||||
errors = tp::run(32,8,[&](size_t){ ++ran; });
|
||||
tp::fault_mode() = 0;
|
||||
check(errors == 1 && ran == 31,"a throwing item is caught and counted");
|
||||
ran = 0;
|
||||
errors = tp::run(32,8,[&](size_t k){ if (k % 4 == 0) throw std::runtime_error("x"); ++ran; });
|
||||
check(errors == 8 && ran == 24,"item exceptions never escape a pool thread");
|
||||
tp::Budget budget(100);
|
||||
check(budget.reserve(60) && !budget.reserve(41) && budget.reserve(40) && !budget.reserve(1),
|
||||
"aggregate budget reservations are bounded");
|
||||
budget.release(50);
|
||||
check(budget.reserve(50),"released bytes are reusable");
|
||||
}
|
||||
|
||||
void fixed_hash_vectors() {
|
||||
/* Independently produced by Python xxhash 3.6.0's native wheel. These
|
||||
* literal expectations cover XXH3's short/medium/long-input boundaries. */
|
||||
@@ -538,6 +633,26 @@ void split_coalesce_copy_parts(const fs::path& root) {
|
||||
check(duck_texture_pack_match_draw(pack.get(),&q,0,&match) == HD_TEXTURE_LOOKUP_NONE,"single-cover API preserves native fallback for composed draw");
|
||||
}
|
||||
|
||||
void release_while_another_source_splits(const fs::path& root) {
|
||||
config(root,"MaxVRAMWriteSplits: 4\nDumpC16Textures: true\nDumpTextureWidthThreshold: 1\nDumpTextureHeightThreshold: 1\n");
|
||||
std::vector<uint16_t> vram(1024*512);
|
||||
place(vram,0,0,4,words(16)); place(vram,8,0,8,words(64));
|
||||
auto pack=create(root); char error[512]{};
|
||||
duck_texture_pack_track_upload(pack.get(),0,0,4,4,vram.data(),vram.size());
|
||||
duck_texture_pack_track_upload(pack.get(),8,0,8,8,vram.data(),vram.size());
|
||||
auto first=query(vram,0,0,2,0,3,0,3);
|
||||
auto second=query(vram,0,0,2,8,15,0,7);
|
||||
duck_texture_pack_dump_draw(pack.get(),&first,0,error,sizeof error);
|
||||
duck_texture_pack_dump_draw(pack.get(),&second,0,error,sizeof error);
|
||||
DuckTexturePackInfo before{},after{}; duck_texture_pack_get_info(pack.get(),&before);
|
||||
check(before.pending_dump_sources==2,"both independent source observations are pending");
|
||||
// Retires the first upload entirely, while the second keeps right and lower clips.
|
||||
duck_texture_pack_invalidate(pack.get(),0,0,12,4);
|
||||
duck_texture_pack_get_info(pack.get(),&after);
|
||||
check(after.pending_dump_sources==1 && after.queued_dump_count==1,
|
||||
"released source finalizes even when surviving upload splits keep the total count steady");
|
||||
}
|
||||
|
||||
void resident_checkpoint(const fs::path& root) {
|
||||
std::vector<uint16_t> vram(1024*512);
|
||||
place(vram,0,0,8,std::vector<uint16_t>(8*16,0x3210));
|
||||
@@ -604,7 +719,8 @@ int main(int argc, char** argv) {
|
||||
palettes(root/"palettes"); st_alias_duplicates(root/"keys"); failed_payloads(root/"invalid"); dumping(root/"dump");
|
||||
official_binary_oracle(root/"official-oracle"); defaults_and_options(root/"defaults");
|
||||
immutable_palettes_and_pages(root/"snapshots"); split_coalesce_copy_parts(root/"lifecycle"); copied_pack_ownership(root/"ownership");
|
||||
resident_checkpoint(root/"resident");
|
||||
resident_checkpoint(root/"resident"); preload(root/"preload"); preload_pool();
|
||||
release_while_another_source_splits(root/"release-split");
|
||||
/* Every artifact is created beneath this fresh, explicitly named test
|
||||
* root. No user-provided paths participate in recursive cleanup. */
|
||||
if (argc > 1 && !std::strcmp(argv[1],"--keep-artifacts")) std::printf("ARTIFACT_ROOT: %s\n",root.u8string().c_str());
|
||||
|
||||
@@ -10,9 +10,8 @@ RENDERER = (ROOT / "runtime" / "src" / "gpu_gl_renderer.c").read_text(
|
||||
)
|
||||
|
||||
# The only second context is the present thread's ([video] present_thread,
|
||||
# docs/RENDER_THREAD.md), created in pt_begin under the existing render thread.
|
||||
# Temporal composition stays on the renderer's original context, which that
|
||||
# thread may own; interpolation itself creates no thread or context.
|
||||
# docs/RENDER_THREAD.md), created in pt_begin under the render thread, which
|
||||
# frame interpolation makes ineligible.
|
||||
assert RENDERER.count("SDL_GL_CreateContext(") == 2, (
|
||||
"temporal blending must not create a second GL context"
|
||||
)
|
||||
@@ -25,14 +24,8 @@ assert "SDL_CreateThread(" not in RENDERER, (
|
||||
assert "SDL_LockMutex(" not in RENDERER, (
|
||||
"the GL renderer must not hold an application mutex across driver work"
|
||||
)
|
||||
_swap = RENDERER[RENDERER.index("static void gl_window_swap(void) {"):]
|
||||
_swap = _swap[:_swap.index("\n}\n")]
|
||||
# SDL3 returns status and SDL2 returns void; both compile-time branches belong
|
||||
# to the same owner. Counting source occurrences alone mistakes these for two
|
||||
# independently owned swaps.
|
||||
assert 1 <= _swap.count("SDL_GL_SwapWindow(") <= 2
|
||||
assert RENDERER.count("SDL_GL_SwapWindow(") == _swap.count("SDL_GL_SwapWindow("), (
|
||||
"all OpenGL presentation must converge on gl_window_swap"
|
||||
assert RENDERER.count("SDL_GL_SwapWindow(") == 1, (
|
||||
"all OpenGL presentation must converge on one owned swap call"
|
||||
)
|
||||
assert "gl_renderer_interpolation_owns_cadence" in RENDERER
|
||||
assert "no motion vectors" in RENDERER
|
||||
|
||||
@@ -54,9 +54,7 @@ assert "if (source != s_interp_source) interp_reset_history_unlocked();" in sett
|
||||
assert "s_interp_source = source;" in setter
|
||||
|
||||
# ---- interp_capture's FLIP branch (gpu_gl_renderer.c) -----------------------
|
||||
wrapper = function_body(GL, "static int interp_capture(GLuint fbo,")
|
||||
assert "interp_capture_timed_impl(" in wrapper, "timed wrapper must invoke the capture implementation"
|
||||
capture = function_body(GL, "static int interp_capture_timed_impl(GLuint fbo,")
|
||||
capture = function_body(GL, "static int interp_capture(GLuint fbo,")
|
||||
flip_at = capture.index("if (s_interp_source == 1) {")
|
||||
flip = capture[flip_at:capture.index("} else {", flip_at)]
|
||||
flat = squash(flip)
|
||||
|
||||
@@ -284,25 +284,6 @@ int main(void) {
|
||||
&schedule, 0u, 1000000u, 60.0, 59.0),
|
||||
"output below source cadence should be rejected");
|
||||
|
||||
frame_interpolation_schedule_reset(&schedule);
|
||||
schedule.queued = 1;
|
||||
for (int i = 1; i <= 6; ++i) {
|
||||
uint64_t deadline;
|
||||
float alpha;
|
||||
CHECK(frame_interpolation_schedule_begin(&schedule, 1000000u,
|
||||
1000000u, 60.0, 60.0), "queued interval accepted");
|
||||
CHECK(frame_interpolation_schedule_next(&schedule, 1000000u,
|
||||
&deadline, &alpha), "queued interval has one output");
|
||||
CHECK(fabs((double)deadline - (1000000.0 + i * 1000000.0 / 60.0)) < 1.0,
|
||||
"early producer retains every presentation deadline");
|
||||
CHECK(!frame_interpolation_schedule_next(&schedule, 1000000u,
|
||||
&deadline, &alpha), "queued interval has no duplicate output");
|
||||
}
|
||||
CHECK(frame_interpolation_schedule_begin(&schedule, 2000000u,
|
||||
1000000u, 60.0, 60.0), "queued clock recovers after sustained stall");
|
||||
CHECK(frame_interpolation_schedule_end(&schedule) == 2016667u,
|
||||
"late queued producer reanchors after sustained stall");
|
||||
|
||||
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
/* test_func_override.c — pins the func_override tier's contract.
|
||||
*
|
||||
* The tier's failure modes are all SILENT: a registration that never gets
|
||||
* consulted, a package override that survives clear-mods, a guard that
|
||||
* corrupts instead of declining. None of those announce themselves at
|
||||
* runtime, so they need pinning here rather than field observation.
|
||||
*
|
||||
* The tests drive the real dispatcher entry point — g_psx_func_override_hook,
|
||||
* the same pointer the generated dispatch and the interpreter call through —
|
||||
* so nothing here exercises a test-only path.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "func_override.h"
|
||||
|
||||
extern int (*g_psx_func_override_hook)(CPUState *cpu, uint32_t phys);
|
||||
|
||||
static int g_fail = 0;
|
||||
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL %s:%d: ", __FILE__, __LINE__); \
|
||||
printf(__VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
g_fail = 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* ---- test doubles ------------------------------------------------------- */
|
||||
|
||||
/* Guest RAM the residency guard peeks at. func_override.c reads through
|
||||
* psx_peek_word_untraced specifically so this stays a plain host read with no
|
||||
* lockstep/oracle side effects — that is the property under test in
|
||||
* guard_declines_on_mismatch, and the reason this double is trivial. */
|
||||
#define FAKE_RAM_WORDS 64
|
||||
static uint32_t s_ram[FAKE_RAM_WORDS];
|
||||
|
||||
uint32_t psx_peek_word_untraced(uint32_t addr)
|
||||
{
|
||||
const uint32_t idx = (addr & 0x1FFFFFFFu) >> 2;
|
||||
return (idx < FAKE_RAM_WORDS) ? s_ram[idx] : 0xDEADBEEFu;
|
||||
}
|
||||
|
||||
/* Cycle-core doubles: func_override.c charges fixed credits through the
|
||||
* psx_advance_cycles inline (psx_cycles.h), which reads these globals. The
|
||||
* doubles keep it a plain counter bump so the credit tests can assert
|
||||
* against psx_cycle_count deltas directly. */
|
||||
uint64_t psx_cycle_count = 0;
|
||||
uint64_t psx_next_service_cycle = 0;
|
||||
int psx_in_device_service = 0;
|
||||
int g_event_step_conservative = 0;
|
||||
int g_ls_replay_active = 0;
|
||||
uint32_t g_psx_cyc_batch = 0;
|
||||
uint32_t g_psx_cyc_batch_limit = 0;
|
||||
void psx_devices_service_to_now(void) {}
|
||||
void psx_advance_cycles_slow(uint32_t cycles) { psx_cycle_count += cycles; }
|
||||
|
||||
/* Stands in for the recompiled dispatcher. Re-consults the hook exactly as
|
||||
* psx_dispatch_impl does, so func_override_call_original's one-shot bypass is
|
||||
* exercised through its real mechanism rather than assumed. */
|
||||
static int s_original_runs = 0;
|
||||
static int s_dispatch_depth = 0;
|
||||
|
||||
/* When set, the "original" body at this address calls ITSELF once — modelling
|
||||
* a self-recursive guest function, the case where the one-shot bypass must
|
||||
* already have been consumed so the recursive call re-consults the override
|
||||
* (what a guest-level wrap would observe). */
|
||||
static uint32_t s_recursive_original_at = 0;
|
||||
static int s_recursion_budget = 0;
|
||||
|
||||
void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t return_addr)
|
||||
{
|
||||
(void)return_addr;
|
||||
if (s_dispatch_depth > 8) return; /* runaway guard, not part of the contract */
|
||||
s_dispatch_depth++;
|
||||
if (!g_psx_func_override_hook ||
|
||||
!g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu)) {
|
||||
/* Nothing handled it: this is the original body running. */
|
||||
s_original_runs++;
|
||||
cpu->gpr[2] = 0xAAAAu;
|
||||
if (s_recursive_original_at &&
|
||||
(addr & 0x1FFFFFFFu) == (s_recursive_original_at & 0x1FFFFFFFu) &&
|
||||
s_recursion_budget > 0) {
|
||||
s_recursion_budget--;
|
||||
psx_dispatch_call(cpu, addr, return_addr);
|
||||
}
|
||||
}
|
||||
s_dispatch_depth--;
|
||||
}
|
||||
|
||||
/* ---- override implementations ------------------------------------------- */
|
||||
|
||||
static int s_impl_calls, s_decline_calls, s_wrap_calls;
|
||||
|
||||
static int impl_handles(CPUState *cpu)
|
||||
{
|
||||
CHECK(func_override_active(),"handled native override stack is active");
|
||||
s_impl_calls++;
|
||||
cpu->gpr[2] = 0x1234u;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int impl_declines(CPUState *cpu)
|
||||
{
|
||||
(void)cpu;
|
||||
CHECK(func_override_active(),"declining native override stack is active");
|
||||
s_decline_calls++;
|
||||
return 0; /* the decline-only probe idiom the TCP inventory relies on */
|
||||
}
|
||||
|
||||
static int impl_wraps(CPUState *cpu)
|
||||
{
|
||||
s_wrap_calls++;
|
||||
func_override_call_original(cpu);
|
||||
cpu->gpr[2] += 1u; /* prove we ran after the original */
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- helpers ------------------------------------------------------------ */
|
||||
|
||||
static void reset_all(void)
|
||||
{
|
||||
/* No teardown for direct registrations exists by design (they are
|
||||
* always-on), so each test group runs in a fresh process section by
|
||||
* using distinct addresses instead of clearing. Counters do reset. */
|
||||
s_impl_calls = s_decline_calls = s_wrap_calls = 0;
|
||||
s_original_runs = 0;
|
||||
s_recursive_original_at = 0;
|
||||
s_recursion_budget = 0;
|
||||
memset(s_ram, 0, sizeof(s_ram));
|
||||
}
|
||||
|
||||
static int consult(CPUState *cpu, uint32_t addr)
|
||||
{
|
||||
if (!g_psx_func_override_hook) return 0;
|
||||
const int handled = g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu);
|
||||
CHECK(!func_override_active(),"top-level native override stack has unwound");
|
||||
return handled;
|
||||
}
|
||||
|
||||
/* ---- tests -------------------------------------------------------------- */
|
||||
|
||||
static void test_install_is_null_until_registered(void)
|
||||
{
|
||||
/* The whole "costs nothing when unused" claim rests on this: with nothing
|
||||
* registered the hook pointer must stay NULL so dispatch matches a build
|
||||
* without the tier. */
|
||||
func_override_install();
|
||||
CHECK(g_psx_func_override_hook == NULL,
|
||||
"hook must stay NULL with nothing registered");
|
||||
}
|
||||
|
||||
static void test_add_rejects_bad_input(void)
|
||||
{
|
||||
CHECK(func_override_add("t.null_fn", 0x80001000u, NULL, 0) == FO_ERR_ARGS,
|
||||
"NULL fn must be refused");
|
||||
/* phys 0 collides with the "not inside an override" sentinel. */
|
||||
CHECK(func_override_add("t.zero", 0x80000000u, impl_handles, 0) == FO_ERR_ARGS,
|
||||
"address normalising to phys 0 must be refused");
|
||||
CHECK(func_override_add("t.zero2", 0x00000000u, impl_handles, 0) == FO_ERR_ARGS,
|
||||
"raw address 0 must be refused");
|
||||
static const uint32_t g[1] = {0};
|
||||
CHECK(func_override_add_guarded("t.g0", 0x80001000u, impl_handles, g, 0, 0)
|
||||
== FO_ERR_ARGS,
|
||||
"guarded with n_words 0 must be refused");
|
||||
CHECK(func_override_add_guarded("t.gmax", 0x80001000u, impl_handles, g,
|
||||
FO_MAX_GUARD_WORDS + 1, 0) == FO_ERR_ARGS,
|
||||
"guard word count over the cap must be refused");
|
||||
CHECK(func_override_add_guarded("t.gnull", 0x80001000u, impl_handles, NULL,
|
||||
2, 0) == FO_ERR_ARGS,
|
||||
"guarded with NULL words must be refused");
|
||||
}
|
||||
|
||||
static void test_duplicate_address_refused(void)
|
||||
{
|
||||
const uint32_t addr = 0x80002000u;
|
||||
CHECK(func_override_add("t.first", addr, impl_handles, 0) == FO_OK,
|
||||
"first registration must succeed");
|
||||
/* A silent last-wins here would be untraceable, which is why this is an
|
||||
* error and not an overwrite. KSEG vs physical must not defeat it. */
|
||||
CHECK(func_override_add("t.second", addr, impl_declines, 0) == FO_ERR_DUPLICATE,
|
||||
"same address twice must be refused");
|
||||
CHECK(func_override_add("t.second_phys", addr & 0x1FFFFFFFu, impl_declines, 0)
|
||||
== FO_ERR_DUPLICATE,
|
||||
"same address in physical form must also be refused");
|
||||
}
|
||||
|
||||
static void test_handled_and_declined_both_count_as_consults(void)
|
||||
{
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof(cpu));
|
||||
reset_all();
|
||||
|
||||
const uint32_t handled_at = 0x80003000u;
|
||||
const uint32_t declined_at = 0x80003100u;
|
||||
CHECK(func_override_add("t.handled", handled_at, impl_handles, 0) == FO_OK, "add handled");
|
||||
CHECK(func_override_add("t.declined", declined_at, impl_declines, 0) == FO_OK, "add declined");
|
||||
func_override_install();
|
||||
CHECK(g_psx_func_override_hook != NULL, "hook must install once populated");
|
||||
|
||||
CHECK(consult(&cpu, handled_at) == 1, "handled override must report handled");
|
||||
CHECK(cpu.gpr[2] == 0x1234u, "handled override must write $v0");
|
||||
CHECK(s_impl_calls == 1, "handled impl must run once");
|
||||
|
||||
CHECK(consult(&cpu, declined_at) == 0, "declining override must report not-handled");
|
||||
CHECK(s_decline_calls == 1, "declining impl must still run");
|
||||
|
||||
/* An unregistered address must not be claimed. */
|
||||
CHECK(consult(&cpu, 0x8000FF00u) == 0, "unregistered address must not be handled");
|
||||
|
||||
/* calls counts CONSULTS, so a decline-only probe proves reachability. */
|
||||
int found_handled = 0, found_declined = 0;
|
||||
for (int i = 0; i < func_override_count(); i++) {
|
||||
char id[FO_MAX_ID];
|
||||
uint32_t addr = 0;
|
||||
uint64_t calls = 0, misses = 0;
|
||||
int guarded = -1;
|
||||
if (!func_override_get_ex(i, id, sizeof(id), &addr, &calls, &misses,
|
||||
&guarded, NULL))
|
||||
continue;
|
||||
if (addr == (handled_at & 0x1FFFFFFFu)) {
|
||||
found_handled = 1;
|
||||
CHECK(calls == 1, "handled entry calls==1, got %llu",
|
||||
(unsigned long long)calls);
|
||||
CHECK(misses == 0, "handled entry must have no guard misses");
|
||||
CHECK(guarded == 0, "handled entry is unguarded");
|
||||
CHECK(strcmp(id, "t.handled") == 0, "id round-trips, got '%s'", id);
|
||||
}
|
||||
if (addr == (declined_at & 0x1FFFFFFFu)) {
|
||||
found_declined = 1;
|
||||
CHECK(calls == 1, "DECLINED entry must still count a consult, got %llu",
|
||||
(unsigned long long)calls);
|
||||
}
|
||||
}
|
||||
CHECK(found_handled && found_declined, "both entries must be enumerable");
|
||||
}
|
||||
|
||||
static void test_guard_declines_on_mismatch(void)
|
||||
{
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof(cpu));
|
||||
reset_all();
|
||||
|
||||
/* Address inside the fake RAM window so the guard can be satisfied. */
|
||||
const uint32_t addr = 0x80000040u; /* phys 0x40 -> word index 16 */
|
||||
static const uint32_t prologue[2] = {0x27BDFFE0u, 0xAFB20018u};
|
||||
CHECK(func_override_add_guarded("t.guarded", addr, impl_handles, prologue, 2, 0)
|
||||
== FO_OK, "guarded add must succeed");
|
||||
func_override_install();
|
||||
|
||||
/* Wrong code resident: must DECLINE and never run the impl. Corrupting
|
||||
* here instead of declining is the failure the guard exists to prevent. */
|
||||
s_ram[16] = 0xFFFFFFFFu;
|
||||
s_ram[17] = 0xFFFFFFFFu;
|
||||
CHECK(consult(&cpu, addr) == 0, "guard mismatch must decline");
|
||||
CHECK(s_impl_calls == 0, "guard mismatch must NOT run the override body");
|
||||
|
||||
/* Right code resident: must fire. */
|
||||
s_ram[16] = prologue[0];
|
||||
s_ram[17] = prologue[1];
|
||||
CHECK(consult(&cpu, addr) == 1, "guard match must handle");
|
||||
CHECK(s_impl_calls == 1, "guard match must run the override body once");
|
||||
|
||||
/* Partial match is still a miss. */
|
||||
s_ram[17] = 0u;
|
||||
CHECK(consult(&cpu, addr) == 0, "partial guard match must decline");
|
||||
CHECK(s_impl_calls == 1, "partial match must not run the body");
|
||||
|
||||
for (int i = 0; i < func_override_count(); i++) {
|
||||
char id[FO_MAX_ID];
|
||||
uint32_t a = 0;
|
||||
uint64_t calls = 0, misses = 0;
|
||||
int guarded = 0;
|
||||
if (!func_override_get_ex(i, id, sizeof(id), &a, &calls, &misses,
|
||||
&guarded, NULL))
|
||||
continue;
|
||||
if (a != (addr & 0x1FFFFFFFu)) continue;
|
||||
CHECK(guarded == 2, "guard word count must be reported, got %d", guarded);
|
||||
CHECK(misses == 2, "two guard misses expected, got %llu",
|
||||
(unsigned long long)misses);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_call_original_is_one_shot(void)
|
||||
{
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof(cpu));
|
||||
reset_all();
|
||||
|
||||
const uint32_t addr = 0x80004000u;
|
||||
CHECK(func_override_add("t.wrap", addr, impl_wraps, FO_CREDIT_SELF) == FO_OK, "add wrap");
|
||||
func_override_install();
|
||||
|
||||
CHECK(consult(&cpu, addr) == 1, "wrap must handle");
|
||||
CHECK(s_wrap_calls == 1, "wrap body runs once");
|
||||
/* The bypass must let the ORIGINAL through exactly once — not zero times
|
||||
* (wrap becomes replace) and not unboundedly (infinite re-entry). */
|
||||
CHECK(s_original_runs == 1, "original must run exactly once, ran %d",
|
||||
s_original_runs);
|
||||
CHECK(cpu.gpr[2] == 0xAAAAu + 1u,
|
||||
"wrap must observe then adjust the original's $v0, got 0x%X",
|
||||
cpu.gpr[2]);
|
||||
|
||||
/* Bypass must not persist: a second call re-consults the override. */
|
||||
CHECK(consult(&cpu, addr) == 1, "second call must still be handled");
|
||||
CHECK(s_wrap_calls == 2, "wrap body runs again on the second call");
|
||||
CHECK(s_original_runs == 2, "original runs once more, total %d",
|
||||
s_original_runs);
|
||||
}
|
||||
|
||||
static void test_bypass_is_consumed_so_recursion_reconsults(void)
|
||||
{
|
||||
/* The header promises: "if the original recursively calls itself, the
|
||||
* recursive calls consult the override again." That requires the one-shot
|
||||
* bypass to be CONSUMED (cleared) at the moment it is honoured, not merely
|
||||
* restored when call_original returns — restoring alone looks correct for
|
||||
* a non-recursive wrap and hides the bug. */
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof(cpu));
|
||||
reset_all();
|
||||
|
||||
const uint32_t addr = 0x80004100u;
|
||||
CHECK(func_override_add("t.wrap_rec", addr, impl_wraps, FO_CREDIT_SELF) == FO_OK,
|
||||
"add recursive wrap");
|
||||
func_override_install();
|
||||
|
||||
s_recursive_original_at = addr;
|
||||
s_recursion_budget = 1; /* the original self-calls exactly once */
|
||||
|
||||
CHECK(consult(&cpu, addr) == 1, "recursive wrap must handle");
|
||||
|
||||
/* Outer override runs, calls the original; the original self-calls, and
|
||||
* that inner call must land on the OVERRIDE again (wrap body twice), not
|
||||
* on the original a second time. */
|
||||
CHECK(s_wrap_calls == 2,
|
||||
"override must be re-consulted on the original's self-call, wrap ran %d",
|
||||
s_wrap_calls);
|
||||
CHECK(s_original_runs == 2,
|
||||
"each override invocation runs the original once, original ran %d",
|
||||
s_original_runs);
|
||||
}
|
||||
|
||||
static void test_call_original_outside_override_is_a_noop(void)
|
||||
{
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof(cpu));
|
||||
reset_all();
|
||||
func_override_call_original(&cpu); /* must not dispatch anything */
|
||||
CHECK(s_original_runs == 0,
|
||||
"call_original outside an override must do nothing");
|
||||
}
|
||||
|
||||
static void test_package_reset_drops_only_package_entries(void)
|
||||
{
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof(cpu));
|
||||
reset_all();
|
||||
|
||||
const uint32_t direct_at = 0x80005000u;
|
||||
const uint32_t pkg_at = 0x80005100u;
|
||||
CHECK(func_override_add("t.direct", direct_at, impl_handles, 0) == FO_OK,
|
||||
"direct add");
|
||||
CHECK(func_override_add_package("pkg.feature:a", pkg_at, impl_handles, NULL, 0, 0)
|
||||
== FO_OK, "package add");
|
||||
func_override_install();
|
||||
|
||||
CHECK(consult(&cpu, direct_at) == 1, "direct override armed");
|
||||
CHECK(consult(&cpu, pkg_at) == 1, "package override armed");
|
||||
|
||||
const int before = func_override_count();
|
||||
const int dropped = func_override_reset_package_armed();
|
||||
CHECK(dropped >= 1, "reset must report dropping the package entry");
|
||||
CHECK(func_override_count() == before - dropped,
|
||||
"count must shrink by exactly the dropped entries");
|
||||
|
||||
/* This is the netplay-divergence regression: after clear-mods the package
|
||||
* override must be gone, while the game's own always-on reimplementation
|
||||
* survives. */
|
||||
CHECK(consult(&cpu, pkg_at) == 0,
|
||||
"package override must NOT fire after reset (netplay divergence)");
|
||||
CHECK(consult(&cpu, direct_at) == 1,
|
||||
"direct override must survive reset");
|
||||
|
||||
/* Compaction must not corrupt surviving entries. */
|
||||
int seen_direct = 0;
|
||||
for (int i = 0; i < func_override_count(); i++) {
|
||||
char id[FO_MAX_ID];
|
||||
uint32_t a = 0;
|
||||
uint64_t calls = 0;
|
||||
if (!func_override_get_ex(i, id, sizeof(id), &a, &calls, NULL, NULL,
|
||||
NULL))
|
||||
continue;
|
||||
if (a == (direct_at & 0x1FFFFFFFu)) {
|
||||
seen_direct = 1;
|
||||
CHECK(strcmp(id, "t.direct") == 0,
|
||||
"surviving id intact after compaction, got '%s'", id);
|
||||
}
|
||||
CHECK(a != (pkg_at & 0x1FFFFFFFu),
|
||||
"dropped package entry must not remain enumerable");
|
||||
}
|
||||
CHECK(seen_direct, "direct entry must remain enumerable");
|
||||
|
||||
/* The address is free again, so a later plan can re-arm it. */
|
||||
CHECK(func_override_add_package("pkg.feature:a", pkg_at, impl_handles, NULL, 0, 0)
|
||||
== FO_OK, "re-arming after reset must succeed");
|
||||
CHECK(func_override_reset_package_armed() >= 1, "and drop again");
|
||||
}
|
||||
|
||||
static void test_get_ex_bounded_id_copy(void)
|
||||
{
|
||||
/* The id buffer size is the caller's to state; a short buffer must
|
||||
* truncate with termination, never overrun. */
|
||||
int checked = 0;
|
||||
for (int i = 0; i < func_override_count(); i++) {
|
||||
char small[4];
|
||||
uint32_t a = 0;
|
||||
memset(small, 0x7F, sizeof(small));
|
||||
if (!func_override_get_ex(i, small, sizeof(small), &a, NULL, NULL, NULL,
|
||||
NULL))
|
||||
continue;
|
||||
CHECK(small[sizeof(small) - 1] == '\0',
|
||||
"short id buffer must be NUL-terminated");
|
||||
CHECK(strlen(small) <= sizeof(small) - 1, "short id must be truncated");
|
||||
checked = 1;
|
||||
}
|
||||
CHECK(checked, "expected at least one entry to enumerate");
|
||||
|
||||
CHECK(func_override_get_ex(-1, NULL, 0, NULL, NULL, NULL, NULL, NULL) == 0,
|
||||
"negative index must fail");
|
||||
CHECK(func_override_get_ex(func_override_count(), NULL, 0, NULL, NULL, NULL,
|
||||
NULL, NULL) == 0,
|
||||
"out-of-range index must fail");
|
||||
}
|
||||
|
||||
static void test_credit_policy(void)
|
||||
{
|
||||
reset_all();
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof(cpu));
|
||||
|
||||
/* The credit is a required statement: FO_CREDIT_SELF or >= 0. Any other
|
||||
* negative value is a typo, not a policy. */
|
||||
CHECK(func_override_add("t.badcredit", 0x80006000u, impl_handles, -2)
|
||||
== FO_ERR_ARGS,
|
||||
"credit below FO_CREDIT_SELF must be refused");
|
||||
|
||||
CHECK(func_override_add("t.credit40", 0x80006100u, impl_handles, 40)
|
||||
== FO_OK, "fixed-credit add");
|
||||
CHECK(func_override_add("t.credit40d", 0x80006200u, impl_declines, 40)
|
||||
== FO_OK, "fixed-credit decline probe add");
|
||||
CHECK(func_override_add("t.creditself", 0x80006300u, impl_handles,
|
||||
FO_CREDIT_SELF)
|
||||
== FO_OK, "self-credit add");
|
||||
CHECK(func_override_add("t.credit0", 0x80006400u, impl_handles, 0)
|
||||
== FO_OK, "zero-credit add");
|
||||
func_override_install();
|
||||
|
||||
/* A handled call charges exactly the declared credit... */
|
||||
uint64_t before = psx_cycle_count;
|
||||
CHECK(consult(&cpu, 0x80006100u) == 1, "fixed-credit override handles");
|
||||
CHECK(psx_cycle_count - before == 40u,
|
||||
"handled call must charge the declared credit");
|
||||
|
||||
/* ...a DECLINE charges nothing (the original runs and self-charges
|
||||
through the normal backends, so a tier-side charge would double). */
|
||||
before = psx_cycle_count;
|
||||
CHECK(consult(&cpu, 0x80006200u) == 0, "decline probe declines");
|
||||
CHECK(psx_cycle_count == before, "a decline must charge nothing");
|
||||
|
||||
/* FO_CREDIT_SELF and 0 charge nothing from the tier. */
|
||||
before = psx_cycle_count;
|
||||
CHECK(consult(&cpu, 0x80006300u) == 1, "self-credit override handles");
|
||||
CHECK(psx_cycle_count == before, "SELF must not be charged by the tier");
|
||||
before = psx_cycle_count;
|
||||
CHECK(consult(&cpu, 0x80006400u) == 1, "zero-credit override handles");
|
||||
CHECK(psx_cycle_count == before, "credit 0 must charge nothing");
|
||||
|
||||
/* The declared policy is inspectable, not just documented. */
|
||||
int seen40 = 0, seenself = 0;
|
||||
for (int i = 0; i < func_override_count(); i++) {
|
||||
char id[FO_MAX_ID];
|
||||
uint32_t a = 0;
|
||||
int32_t credit = -99;
|
||||
if (!func_override_get_ex(i, id, sizeof(id), &a, NULL, NULL, NULL,
|
||||
&credit))
|
||||
continue;
|
||||
if (a == 0x00006100u) { seen40 = (credit == 40); }
|
||||
if (a == 0x00006300u) { seenself = (credit == FO_CREDIT_SELF); }
|
||||
}
|
||||
CHECK(seen40, "get_ex must report the fixed credit");
|
||||
CHECK(seenself, "get_ex must report FO_CREDIT_SELF");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
test_install_is_null_until_registered();
|
||||
test_add_rejects_bad_input();
|
||||
test_duplicate_address_refused();
|
||||
test_handled_and_declined_both_count_as_consults();
|
||||
test_guard_declines_on_mismatch();
|
||||
test_call_original_is_one_shot();
|
||||
test_bypass_is_consumed_so_recursion_reconsults();
|
||||
test_call_original_outside_override_is_a_noop();
|
||||
test_package_reset_drops_only_package_entries();
|
||||
test_get_ex_bounded_id_copy();
|
||||
test_credit_policy();
|
||||
|
||||
if (g_fail) {
|
||||
printf("func_override: FAILURES\n");
|
||||
return 1;
|
||||
}
|
||||
printf("func_override: all checks passed (%d entries registered)\n",
|
||||
func_override_count());
|
||||
return 0;
|
||||
}
|
||||
@@ -11,10 +11,17 @@
|
||||
* bottom-up) so a run against another tree can be compared byte for byte
|
||||
* (BLOOM_FIXTURE_NO_BLOOM builds it without the bloom API). */
|
||||
#define GL_PRESENT_TEST_HOOK(gen) bloom_capture_hook(gen)
|
||||
#include <stdint.h>
|
||||
static uint64_t pfx_fixture_clock(void);
|
||||
#define PFX_NOW_TICKS() pfx_fixture_clock()
|
||||
static void bloom_capture_hook(int gen);
|
||||
#define main scale_fixture_main
|
||||
#include "test_gl_scale_invariance.c"
|
||||
#undef main
|
||||
static uint64_t s_pfx_test_clock;
|
||||
static uint64_t pfx_fixture_clock(void) {
|
||||
return s_pfx_test_clock ? s_pfx_test_clock : SDL_GetPerformanceCounter();
|
||||
}
|
||||
void gpu_hd_textures_begin_upload(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
|
||||
void gpu_hd_textures_track_upload(int x,int y,int w,int h,const uint16_t* p){(void)x;(void)y;(void)w;(void)h;(void)p;}
|
||||
void gpu_hd_textures_invalidate(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
|
||||
@@ -54,6 +61,7 @@ static void bloom_capture_hook(int gen) {
|
||||
}
|
||||
|
||||
enum { SC_2D, SC_ADD, SC_3D, SC_HUD_OVER_3D, SC_PARTIAL, SC_N };
|
||||
enum { SC_DEPTH_ONLY = 99 };
|
||||
static const char *k_scene[SC_N] = { "2d", "additive", "3d", "hud-over-3d", "partial" };
|
||||
/* SC_PARTIAL's grey HUD panels (native x, y, w, h): one beside the lamp, one
|
||||
* covering part of it. */
|
||||
@@ -77,8 +85,10 @@ static uint8_t *frame(int scene, float bloom, uint64_t *vram_hash) {
|
||||
glb_set_mask_bits(0, 0); glb_set_semi_transparency(0, 0);
|
||||
glb_fill_rect(0, 0, FRAME_W, FRAME_H, 0x0000);
|
||||
/* A dark 3D ground everywhere: lit (may take a halo), below the threshold. */
|
||||
tri3d(0, 0, FRAME_W, 0, 0, FRAME_H, 0x0842);
|
||||
tri3d(FRAME_W, 0, FRAME_W, FRAME_H, 0, FRAME_H, 0x0842);
|
||||
if (scene != SC_DEPTH_ONLY) {
|
||||
tri3d(0, 0, FRAME_W, 0, 0, FRAME_H, 0x0842);
|
||||
tri3d(FRAME_W, 0, FRAME_W, FRAME_H, 0, FRAME_H, 0x0842);
|
||||
}
|
||||
switch (scene) {
|
||||
case SC_2D: /* a white HUD panel */
|
||||
glb_draw_flat_rect(120, 90, 80, 60, 0x7fff);
|
||||
@@ -88,6 +98,7 @@ static uint8_t *frame(int scene, float bloom, uint64_t *vram_hash) {
|
||||
glb_draw_flat_triangle(120, 150, 200, 150, 160, 80, 0x7fff);
|
||||
glb_set_semi_transparency(0, 0);
|
||||
break;
|
||||
case SC_DEPTH_ONLY:
|
||||
case SC_3D: /* a lit 3D polygon */
|
||||
tri3d(120, 150, 200, 150, 160, 80, 0x7fff);
|
||||
break;
|
||||
@@ -234,6 +245,7 @@ int main(int argc, char **argv) {
|
||||
fputs("void main(){ o = this is not glsl; }\n", f);
|
||||
fclose(f);
|
||||
}
|
||||
gl_renderer_render_thread_stop();
|
||||
for (int pass = 0; pass < 2; pass++) { /* 0: this thread, 1: render thread */
|
||||
uint64_t h = 0;
|
||||
gl_renderer_set_post_shader("pfx_invert.pfx");
|
||||
@@ -261,6 +273,69 @@ int main(int argc, char **argv) {
|
||||
check(lit > s_cap_w * s_cap_h / 2, "mask input: the 3D ground and lamp are lit");
|
||||
free(m);
|
||||
|
||||
/* A history input also works on the final window pass. The blue
|
||||
* channel observes the shader clock, so reload must reset its origin. */
|
||||
FILE *history_file=fopen("pfx_history.glsl","w");
|
||||
check(history_file!=NULL,"open history shader");
|
||||
if(history_file) {
|
||||
fputs("void main(){ float r=texelFetch(u_prev,ivec2(gl_FragCoord.xy)-u_org,0).r;"
|
||||
"o=vec4(r*0.5+u_add,0.0,clamp(u_time/4.0,0.0,1.0),1.0); }\n",history_file);
|
||||
fclose(history_file);
|
||||
}
|
||||
history_file=fopen("pfx_history.pfx","w");
|
||||
if(history_file) {
|
||||
fputs("pfx 1\npass history\n shader pfx_history.glsl\n input u_prev history\n uniform u_add 0.25\n",history_file);
|
||||
fclose(history_file);
|
||||
}
|
||||
psx_host_set_post_uniform("u_add",0.25f,0,0,0);
|
||||
s_pfx_test_clock=100*SDL_GetPerformanceFrequency();
|
||||
gl_renderer_set_post_shader("pfx_history.pfx");
|
||||
uint8_t *hist=frame(SC_PARTIAL,0,&h);
|
||||
size_t center=((size_t)(s_cap_h/2)*s_cap_w+s_cap_w/2)*3;
|
||||
check(abs(hist[center]-64)<=1 && hist[center+2]<=1,"first history frame is cleared and starts at time zero");
|
||||
free(hist);
|
||||
s_pfx_test_clock=102*SDL_GetPerformanceFrequency();
|
||||
hist=frame(SC_PARTIAL,0,&h);
|
||||
check(abs(hist[center]-96)<=1 && abs(hist[center+2]-128)<=1,"final pass samples the previous real image and its own elapsed time");
|
||||
free(hist);
|
||||
if(!s_rth_on) {
|
||||
for(int repeat=0;repeat<2;++repeat) {
|
||||
postfx_present(0,0,0,0,s_cap_w,s_cap_h);
|
||||
bloom_capture_hook(1);
|
||||
check(abs(s_cap[center]-112)<=1,"generated presents do not advance history");
|
||||
}
|
||||
}
|
||||
psx_host_set_post_uniform("u_add",0.125f,0,0,0);
|
||||
hist=frame(SC_PARTIAL,0,&h);
|
||||
check(abs(hist[center]-80)<=1,"live uniform update reaches the actual shader");
|
||||
free(hist);
|
||||
s_pfx_test_clock=104*SDL_GetPerformanceFrequency();
|
||||
gl_renderer_set_post_shader("pfx_history.pfx");
|
||||
hist=frame(SC_PARTIAL,0,&h);
|
||||
check(abs(hist[center]-32)<=1 && hist[center+2]<=1,"reload clears history and resets shader time while retaining live values");
|
||||
free(hist);
|
||||
s_pfx_test_clock=0;
|
||||
|
||||
FILE *depth_file=fopen("pfx_depth.glsl","w");
|
||||
if(depth_file) {
|
||||
fputs("void main(){ float d=texelFetch(u_depth,ivec2(gl_FragCoord.xy)-u_org,0).r;"
|
||||
"o=vec4(clamp(d/4000.0,0.0,1.0),0.0,0.0,1.0); }\n",depth_file);
|
||||
fclose(depth_file);
|
||||
}
|
||||
depth_file=fopen("pfx_depth.pfx","w");
|
||||
if(depth_file) {
|
||||
fputs("pfx 1\npass depth\n shader pfx_depth.glsl\n input u_depth depth\n",depth_file);
|
||||
fclose(depth_file);
|
||||
}
|
||||
gl_renderer_set_pgxp_depth(1);
|
||||
gl_renderer_set_post_shader("pfx_depth.pfx");
|
||||
uint8_t *depth=frame(SC_DEPTH_ONLY,0,&h);
|
||||
size_t outside=((size_t)(s_cap_h-1-20)*s_cap_w+20)*3;
|
||||
check(abs(depth[center]-128)<=3,"depth input reconstructs the triangle's GTE screen depth");
|
||||
check(depth[outside]<=1,"depth input is zero where no 3D primitive drew");
|
||||
free(depth);
|
||||
gl_renderer_set_pgxp_depth(0);
|
||||
|
||||
gl_renderer_set_post_shader("pfx_screen.pfx");
|
||||
check(gl_renderer_post_shader_loaded(), "source_down + blend screen chain loads");
|
||||
uint8_t *sc = frame(SC_PARTIAL, 0.0f, &h);
|
||||
|
||||
@@ -34,6 +34,7 @@ static void fixture_present(int generated);
|
||||
static void fixture_window(int slot);
|
||||
#define GL_PRESENT_THREAD_TEST_HOOK(slot) fixture_window(slot)
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gl_postfx_projection_fixture.h"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gl_batch_policy_lle.c"
|
||||
|
||||
@@ -11,8 +11,6 @@ static void fg_fixture_present(int generated);
|
||||
#include "test_gl_scale_invariance.c"
|
||||
#undef main
|
||||
#include "gpu_render.c"
|
||||
/* This renderer fixture has no emulation-side render-pass transaction. */
|
||||
int g_psx_render_pass_active = 0;
|
||||
#include "duckstation_texture_pack.h"
|
||||
#include "hd_texture_pack.h"
|
||||
#include "png_write.h"
|
||||
@@ -361,8 +359,9 @@ static void composition_scene(const char* root) {
|
||||
check(sample(20,8)[0]>200 && sample(20,8)[2]<20,"composed all-zero ST cutout retains destination");
|
||||
check(sample(24,8)[0]>200 && sample(24,8)[1]<20,"native transparent hole remains cutout");
|
||||
check(sample(32,8)[0]<20 && sample(32,8)[2]>200,"uncovered native opaque texel retains native color");
|
||||
/* Textured draws preserve the source STP bit in canonical guest VRAM. */
|
||||
uint16_t output[8];gr_vram_transfer_out(2,2,8,1,output);
|
||||
check(output[0]==0x03e0 && output[2]==0x03ff && output[6]==0x7c00,
|
||||
check(output[0]==0x03e0 && output[2]==0x83ff && output[6]==0x7c00,
|
||||
"composition does not change guest native draw or STP behavior");
|
||||
GpuHdTextureImage first={0},again={0};
|
||||
check(gpu_hd_textures_acquire_draw(tp,0,0,bounds,0,1,&first),"composed cache acquire");
|
||||
@@ -426,7 +425,7 @@ static void composition_scene(const char* root) {
|
||||
}
|
||||
}
|
||||
|
||||
static void cache_eviction_scene(const char* root) {
|
||||
static void cache_pressure_scene(const char* root) {
|
||||
uint16_t pressure_words[16]; for(int i=0;i<16;++i) pressure_words[i]=0x1234;
|
||||
DuckTextureKey key={0}; key.kind=DUCK_TEXTURE_UPLOAD;key.depth=HD_TEXTURE_DEPTH_16BPP;
|
||||
key.source_hash=duck_texture_hash_words_le(pressure_words,16);
|
||||
@@ -447,7 +446,7 @@ static void cache_eviction_scene(const char* root) {
|
||||
composed_key=image.cache_key;
|
||||
gpu_hd_textures_release_image(&image);if(!composed_key) SDL_Delay(1);
|
||||
}
|
||||
check(composed_key!=0,"complete composition ready before decoder eviction");
|
||||
check(composed_key!=0,"complete composition ready before cache pressure");
|
||||
gr_fill_rect(0,0,16,16,0x03e0);gr_draw_textured_rect(2,2,4,4,0,0,0,0,texture_page);capture();
|
||||
GpuHdTextureDiag before,after;gpu_hd_textures_get_diag(&before);
|
||||
gr_vram_transfer_in(640,64,16,1,pressure_words);int ready=0;
|
||||
@@ -456,20 +455,61 @@ static void cache_eviction_scene(const char* root) {
|
||||
gpu_hd_textures_release_image(&image);if(!ready) SDL_Delay(1);
|
||||
}
|
||||
check(ready,"full CPU budget pressure image decoded");gpu_hd_textures_get_diag(&after);
|
||||
check(after.decoded_bytes==1024*1024 && after.decode_evictions>before.decode_evictions,
|
||||
"pressure image evicts original decoded replacements");
|
||||
/* All fixture images fit in the bounded preload and remain pinned.
|
||||
* The configured 1 MiB on-demand budget must not evict that preload.
|
||||
* decoded_bytes accounts only the on-demand cache; pinned bytes are
|
||||
* separately bounded and are not charged against this budget. */
|
||||
check(before.decoded_images>=7 && before.decoded_bytes==0 &&
|
||||
after.decoded_bytes==before.decoded_bytes &&
|
||||
after.decoded_images==before.decoded_images &&
|
||||
after.decode_evictions==before.decode_evictions,
|
||||
"cache pressure retains pinned preloaded replacements");
|
||||
check(gpu_hd_textures_acquire_gl_draw(texture_page,0,0,bounds,0,0,&image) && !image.rgba,
|
||||
"GPU-resident replacement remains ready after CPU eviction");gpu_hd_textures_release_image(&image);
|
||||
"GPU-resident replacement remains ready under cache pressure");gpu_hd_textures_release_image(&image);
|
||||
check(gpu_hd_textures_acquire_draw(tp,0,0,parts,0,1,&image) && image.cache_key==composed_key,
|
||||
"complete composition survives eviction of all source PNGs");gpu_hd_textures_release_image(&image);
|
||||
"complete composition remains warm under cache pressure");gpu_hd_textures_release_image(&image);
|
||||
gr_draw_textured_rect(2,2,4,4,0,0,0,0,texture_page);capture();
|
||||
check(sample(8,8)[0]>200,"GPU-resident replacement still draws original HD detail");
|
||||
SDL_Delay(20);gpu_hd_textures_get_diag(&before);
|
||||
check(before.decoded_images==after.decoded_images && before.gl_uploads==after.gl_uploads,
|
||||
"warm draws do not decode or upload again after CPU eviction");
|
||||
"warm draws do not decode or upload again under cache pressure");
|
||||
pack_config(root,0);check(gpu_hd_textures_reload(error,sizeof(error)),"eviction fixture cleanup reload");
|
||||
state();gr_vram_transfer_in(512,0,4,4,source_words);wait_ready(0);wait_ready(1);
|
||||
}
|
||||
|
||||
/* Indexed replacements choose a live CLUT entry rather than a baked RGB
|
||||
* palette. Exercise the real upload/match/decode/shader path. */
|
||||
static void indexed_palette_scene(const char* root) {
|
||||
char dir[2048],path[2304],error[256]={0};
|
||||
snprintf(dir,sizeof dir,"%s/beetle",root);
|
||||
uint16_t words[16],palette[16]={0};
|
||||
for(int n=0;n<16;++n) words[n]=0x1111;
|
||||
palette[1]=0x03e0; palette[2]=0x7c00;
|
||||
snprintf(path,sizeof path,"%s/demo-texture-replacements/%x-index.png",dir,
|
||||
hd_texture_crc32_words_le(words,16));
|
||||
uint8_t rgba[64*16*4];
|
||||
for(int n=0;n<64*16;++n) {
|
||||
rgba[n*4]=2; rgba[n*4+1]=rgba[n*4+2]=0; rgba[n*4+3]=255;
|
||||
}
|
||||
FILE* f=fopen(path,"wb");check(f!=NULL,"indexed GPU fixture opens");
|
||||
if(f){check(png_write_rgba(f,rgba,64,16),"indexed GPU fixture writes");fclose(f);}
|
||||
check(gpu_hd_textures_configure(dir,1,0,error,sizeof error),"indexed GPU session opens");
|
||||
state();gr_vram_transfer_in(0,300,16,1,palette);gr_vram_transfer_in(512,0,4,4,words);
|
||||
const int bounds[4]={0,0,7,3};GpuHdTextureImage image={0};int ready=0;
|
||||
for(int n=0;n<2000&&!ready;++n) {
|
||||
ready=gpu_hd_textures_acquire_draw(8u,0,300,bounds,0,0,&image) && image.alpha_mode==5;
|
||||
gpu_hd_textures_release_image(&image);if(!ready)SDL_Delay(1);
|
||||
}
|
||||
check(ready,"indexed GPU path selects decoded palette-independent replacement");
|
||||
gr_fill_rect(0,0,16,16,0x001f);gr_draw_textured_rect(2,2,8,4,0,0,0,300,8u);capture();
|
||||
check(sample(8,8)[2]>200&&sample(8,8)[1]<20,"indexed shader uses replacement index and live blue CLUT entry");
|
||||
gr_vram_write(2,300,0x001f);
|
||||
gr_draw_textured_rect(2,2,8,4,0,0,0,300,8u);capture();
|
||||
check(sample(8,8)[0]>200&&sample(8,8)[2]<20,"indexed shader follows live CLUT change without changing replacement");
|
||||
uint16_t native[8];gr_vram_transfer_out(2,2,8,1,native);
|
||||
check(native[0]==0x03e0,"indexed presentation preserves native source-index RGB");
|
||||
}
|
||||
|
||||
int main(int argc,char** argv) {
|
||||
if(argc==2 && !strcmp(argv[1],"--native-baseline")) {
|
||||
char* native_args[]={"hd-baseline","4","twin","1"};
|
||||
@@ -538,7 +578,7 @@ int main(int argc,char** argv) {
|
||||
"ST black alpha127 remains occupied semitransparent black");
|
||||
gr_set_semi_transparency(0,0);
|
||||
composition_scene(argv[1]);state();wait_ready(0);wait_ready(1);
|
||||
cache_eviction_scene(argv[1]);
|
||||
cache_pressure_scene(argv[1]);
|
||||
|
||||
/* A queued draw must consume its source before a later source overwrite.
|
||||
* The next draw falls back because upload identity has been invalidated. */
|
||||
@@ -604,7 +644,7 @@ int main(int argc,char** argv) {
|
||||
gr_fill_rect(0,0,16,16,0x03e0);
|
||||
gr_draw_textured_rect(4,4,2,4,5,0,0,0,texture_page); capture();
|
||||
check(sample(16,16)[2]>200 && sample(16,16)[0]<20,"Beetle partial draw keeps upload-relative UV origin");
|
||||
check(vram[4*1024+4]==0x03e0 && vram[517]==0x83e0,
|
||||
check(vram[4*1024+4]==0x83e0 && vram[517]==0x83e0,
|
||||
"Beetle image preserves native draw RGB and source STP");
|
||||
gr_draw_textured_rect(8,4,4,4,4,0,0,0,texture_page); capture();
|
||||
check(sample(44,16)[1]>200,"Beetle native transparent zero still cuts out replacement");
|
||||
@@ -864,6 +904,7 @@ int main(int argc,char** argv) {
|
||||
check(fg_gen_hud_hd>0 && fg_gen_hud_bad==0,"Smooth motion + HD: generated HUD is the HD replacement");
|
||||
}
|
||||
}
|
||||
indexed_palette_scene(argv[1]);
|
||||
memcpy(reference,vram,sizeof(vram));
|
||||
gl_renderer_set_cpu_auth_dual(1);
|
||||
gpu_hd_textures_shutdown();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Original source-owned GL readback-coherence regression. No retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gl_postfx_projection_fixture.h"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gl_batch_policy_lle.c"
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
* surface), and with the thread on its record/acquire counters.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gl_postfx_projection_fixture.h"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gl_batch_policy_lle.c"
|
||||
@@ -132,23 +133,12 @@ static void test_live_resolution(int original_scale) {
|
||||
check(gl_renderer_set_internal_scale_live(original_scale) == original_scale,
|
||||
"live resolution recovers after refusal");
|
||||
}
|
||||
static uint64_t fixture_clock_us(void) {
|
||||
const uint64_t now = SDL_GetPerformanceCounter(), hz = SDL_GetPerformanceFrequency();
|
||||
return now / hz * 1000000u + now % hz * 1000000u / hz;
|
||||
}
|
||||
static int drawable_worker(void *data) {
|
||||
int *size = data;
|
||||
gl_drawable_size(&size[0], &size[1]);
|
||||
return 0;
|
||||
}
|
||||
static uint64_t fnv(const void *pp, size_t n, uint64_t h) {
|
||||
const uint8_t *p = (const uint8_t *)pp;
|
||||
for (size_t i = 0; i < n; i++) { h ^= p[i]; h *= 0x100000001b3ull; }
|
||||
return h;
|
||||
}
|
||||
static uint64_t rb_digest = 0xcbf29ce484222325ull;
|
||||
static int wide_present;
|
||||
static int quality_scene;
|
||||
static uint32_t rng = 0x2468ace1u;
|
||||
static uint32_t rnd(void) { rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; return rng; }
|
||||
|
||||
@@ -251,16 +241,6 @@ static void frame(int f, int buf_x) {
|
||||
gr_draw_flat_rect(576, 64, 32, 32, (uint16_t)(0x03e0 + f));
|
||||
gr_draw_textured_rect(buf_x + 100, 200, 32, 32, 0, 64, 512, 256, 0x0009);
|
||||
gr_copy_rect(buf_x + 10, 10, buf_x + 14, 13, 80, 50);
|
||||
/* Identity copies preserve colour/mask unless set-mask adds bit15. */
|
||||
mask_bits(f % 4 == 2, 0);
|
||||
gr_copy_rect(buf_x + 14, 13, buf_x + 14, 13, 80, 50);
|
||||
mask_bits(0, 0);
|
||||
if (quality_scene && f % 3 == 1) {
|
||||
/* Consume the copied bit15 mask before any later framebuffer clear. */
|
||||
mask_bits(0, 1);
|
||||
gr_draw_flat_rect(buf_x + 14, 13, 80, 50, (uint16_t)(0x7c00 + f));
|
||||
mask_bits(0, 0);
|
||||
}
|
||||
/* A half-VRAM upload every frame pushes the ring past its end. */
|
||||
{
|
||||
static uint16_t big[1024 * 256];
|
||||
@@ -277,36 +257,7 @@ static void frame(int f, int buf_x) {
|
||||
gr_vram_transfer_out(buf_x + 50, 50, 64, 32, out);
|
||||
rb_digest = fnv(out, sizeof out, rb_digest);
|
||||
}
|
||||
if (wide_present) {
|
||||
/* Reach both reveal margins after the last upload/readback, leaving
|
||||
* real queued work for the final present flush. Alternate the full
|
||||
* background gate so the pruning fallback is exercised too. */
|
||||
g_bg_full = f % 3 == 0;
|
||||
gr_wide_set_view(0, 0, 0, 0);
|
||||
g_prim_id = 1;
|
||||
gr_set_semi_transparency(f % 2, f % 4);
|
||||
if (quality_scene) {
|
||||
gr_set_precise_triangle(1, (buf_x-40)*65536, 20*65536,
|
||||
(buf_x+360)*65536, 20*65536, (buf_x+160)*65536, 220*65536);
|
||||
gr_set_depth_triangle(1, 2000.0f, 2200.0f, 2400.0f);
|
||||
}
|
||||
gr_draw_textured_triangle(buf_x-40, 20, 0, 0, buf_x+360, 20, 63, 0,
|
||||
buf_x+160, 220, 32, 63, 512, 256, 0x0008);
|
||||
if (quality_scene) {
|
||||
/* Minification, transparent/STP texels, and a later occluded draw
|
||||
* make depth coverage observable in the final colour image. */
|
||||
gr_set_semi_transparency(0, 0);
|
||||
gr_set_precise_triangle(1, (buf_x+80)*65536, 60*65536,
|
||||
(buf_x+100)*65536, 60*65536, (buf_x+90)*65536, 80*65536);
|
||||
gr_set_depth_triangle(1, 3000.0f, 3000.0f, 3000.0f);
|
||||
gr_draw_textured_triangle(buf_x+80, 60, 0, 0, buf_x+100, 60, 255, 0,
|
||||
buf_x+90, 80, 127, 255, 512, 256, 0x0008);
|
||||
gr_set_precise_triangle(0, 0, 0, 0, 0, 0, 0);
|
||||
gr_set_depth_triangle(0, 0, 0, 0);
|
||||
}
|
||||
check(gl_renderer_present_wide_fbo(buf_x, 0, FRAME_H, 0), "wide present");
|
||||
} else
|
||||
gl_renderer_present_vram(buf_x, 0, FRAME_W, FRAME_H, 0, 0);
|
||||
gl_renderer_present_vram(buf_x, 0, FRAME_W, FRAME_H, 0, 0);
|
||||
gl_renderer_render_thread_frame_boundary();
|
||||
}
|
||||
|
||||
@@ -315,9 +266,6 @@ int main(int argc, char **argv) {
|
||||
int threaded = argc > 2 && argv[2][0] == '1';
|
||||
int frames = argc > 3 ? atoi(argv[3]) : 80;
|
||||
int dynres = argc > 4 && !strcmp(argv[4], "dynres");
|
||||
wide_present = argc > 4 && !strcmp(argv[4], "interp-wide");
|
||||
int interp = wide_present || (argc > 4 && !strcmp(argv[4], "interp"));
|
||||
quality_scene = argc > 5 && !strcmp(argv[5], "quality");
|
||||
/* No video device, window or GL context (headless, Windows over SSH):
|
||||
* exit 77, which the harness reports as a CTest skip. */
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
|
||||
@@ -350,25 +298,10 @@ int main(int argc, char **argv) {
|
||||
gr_set_scale(scale);
|
||||
if (dynres) gl_renderer_set_dynamic_resolution(1);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if (getenv("PSX_GL_TEST_PRESENT_THREAD")) gl_renderer_set_present_thread(1, 3);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "FAIL context\n"); return 2; }
|
||||
check(gr_backend() == GR_BACKEND_OPENGL, "OpenGL backend");
|
||||
textures();
|
||||
if (wide_present && s_hiw) {
|
||||
check(hiw_ensure(0, FRAME_W) != NULL, "first hires buffer");
|
||||
check(hiw_ensure(512, 512+FRAME_W) != NULL, "second hires buffer");
|
||||
}
|
||||
gl_renderer_swap_timing_start(fixture_clock_us);
|
||||
gr_wide_configure(426, 53);
|
||||
if (quality_scene) {
|
||||
gl_renderer_set_bloom(1.0f);
|
||||
gl_renderer_set_pgxp_depth(1);
|
||||
gl_renderer_set_texture_lod(1, 16);
|
||||
}
|
||||
if (interp) {
|
||||
gl_renderer_set_interpolation_source(1);
|
||||
gl_renderer_set_interpolation(1, 60.0, 60.0, 60.0, 2);
|
||||
}
|
||||
if (threaded) {
|
||||
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
|
||||
check(gl_renderer_render_thread_active(), "render thread active");
|
||||
@@ -382,7 +315,6 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
static const int levels[] = { 2, 1, 3, 1, 4, 2 };
|
||||
int nsteps = 0, level = scale;
|
||||
const uint64_t drawable_queries_before = s_drawable_queries;
|
||||
for (int f = 0; f < frames; f++) {
|
||||
frame(f, (f & 1) ? 512 : 0);
|
||||
if (dynres && f % 7 == 6) { /* includes frames held at a sync point */
|
||||
@@ -437,70 +369,6 @@ int main(int argc, char **argv) {
|
||||
if (got) wide = fnv(wb, (size_t)ww * wh * 4, 0xcbf29ce484222325ull);
|
||||
free(wb);
|
||||
}
|
||||
if (interp) {
|
||||
check(s_interp_captures > 0 && s_interp_swaps > 0, "temporal frames presented");
|
||||
}
|
||||
/* Colour hashes alone do not verify the bloom-source stencil. Read its
|
||||
* bit on every canonical/window/wide target while the context is held. */
|
||||
uint64_t stencil = 0xcbf29ce484222325ull;
|
||||
if (quality_scene) {
|
||||
gl_renderer_render_thread_sync("test_stencil");
|
||||
GLuint fbos[1 + HIW_MAX_TILES + 8];
|
||||
int widths[1 + HIW_MAX_TILES + 8], heights[1 + HIW_MAX_TILES + 8], nf = 0;
|
||||
fbos[nf] = s_hr_fbo; widths[nf] = VRAM_W * s_hr_scale;
|
||||
heights[nf++] = VRAM_H * s_hr_scale;
|
||||
for (int t = 0; t < s_hiw_n; ++t) {
|
||||
fbos[nf] = s_hiw_t[t].fbo;
|
||||
widths[nf] = (s_hiw_t[t].x1 - s_hiw_t[t].x0) * scale;
|
||||
heights[nf++] = VRAM_H * scale;
|
||||
}
|
||||
/* The displayed wide surface is sufficient for the centre splice
|
||||
* and clipped mirror; canonical targets cover both buffer origins. */
|
||||
if (g_wide_cur) {
|
||||
fbos[nf] = g_wide_cur; widths[nf] = g_wide_w * scale;
|
||||
heights[nf++] = VRAM_H * scale;
|
||||
}
|
||||
for (int t = 0; t < nf; ++t) {
|
||||
size_t bytes = (size_t)widths[t] * heights[t];
|
||||
unsigned char *bits = malloc(bytes);
|
||||
check(bits != NULL, "stencil readback allocation");
|
||||
if (!bits) continue;
|
||||
/* Bit0 can remain deferred under disabled mask checking. Compare
|
||||
* alpha-derived semantic masks, preserving bloom bit1, rather
|
||||
* than comparing the unused deferred storage representation. */
|
||||
rebuild_target_stencil(fbos[t], widths[t], heights[t]);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, fbos[t]);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glReadPixels(0, 0, widths[t], heights[t], GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, bits);
|
||||
for (size_t p = 0; p < bytes; ++p) bits[p] &= 3;
|
||||
stencil = fnv(bits, bytes, stencil);
|
||||
free(bits);
|
||||
}
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
}
|
||||
if (wide_present && wide_margin_scissor()) {
|
||||
check(s_hq_center_pruned > 0, "queued centre shading was elided");
|
||||
printf("pruned_cmds=%llu\n", (unsigned long long)s_hq_center_pruned);
|
||||
}
|
||||
if (wide_present && hiw_upload_fast()) {
|
||||
check(s_hq_saved_uploads > s_hq_uploads, "queue uploaded once for multiple replay draws");
|
||||
printf("queue_uploads=%llu replay_draws=%llu\n", (unsigned long long)s_hq_uploads,
|
||||
(unsigned long long)s_hq_saved_uploads);
|
||||
}
|
||||
if (quality_scene && coverage_fast_enabled()) {
|
||||
|
||||
check(s_bcls_fused_draws > 0, "bloom stencil fused with colour draws");
|
||||
printf("fused_draws=%llu\n", (unsigned long long)s_bcls_fused_draws);
|
||||
check(s_coverage_depth_draws > 0, "coverage-only depth passes ran");
|
||||
printf("coverage_draws=%llu\n", (unsigned long long)s_coverage_depth_draws);
|
||||
}
|
||||
if (!getenv("PSX_GL_TEXEL_CACHE") || getenv("PSX_GL_TEXEL_CACHE")[0] != '0') {
|
||||
check(s_tc_ready == 1 && s_tc_hits > 0 && s_tc_misses > 0, "decoded indexed texels reused");
|
||||
printf("texel_cache_hits=%llu misses=%llu evictions=%llu\n",
|
||||
(unsigned long long)s_tc_hits, (unsigned long long)s_tc_misses,
|
||||
(unsigned long long)s_tc_evictions);
|
||||
}
|
||||
if (threaded) {
|
||||
RtStats st;
|
||||
rt_get_stats(&st);
|
||||
@@ -514,38 +382,11 @@ int main(int argc, char **argv) {
|
||||
check(st.acquires >= 2 && st.acquires <= (uint64_t)frames + 4, "acquires bounded");
|
||||
check(st.bytes > ((uint64_t)32 << 20), "ring wrapped (more than its size recorded)");
|
||||
}
|
||||
{
|
||||
uint64_t times[256], tail[2];
|
||||
int count = gl_renderer_swap_timestamps(times, 256);
|
||||
check(count > 2, "swap completion timestamps captured");
|
||||
for (int i=1; i<count; ++i)
|
||||
check(times[i] >= times[i-1], "swap clock is monotonic");
|
||||
check(gl_renderer_swap_timestamps(tail, 2) == 2 &&
|
||||
tail[0] == times[count-2] && tail[1] == times[count-1],
|
||||
"bounded swap query returns newest entries");
|
||||
}
|
||||
if (drawable_events_enabled())
|
||||
check(s_drawable_queries == drawable_queries_before, "no per-frame drawable queries");
|
||||
/* Pixel-size events publish resizes without querying the window on a
|
||||
* worker or taking its property lock on every frame boundary. */
|
||||
for (int k = 0; k < 2; ++k) {
|
||||
SDL_SetWindowSize(win, 400 + k * 80, 300 + k * 60);
|
||||
SDL_PumpEvents();
|
||||
gl_renderer_render_thread_frame_boundary();
|
||||
int expected[2] = {0}, seen[2] = {0};
|
||||
SDL_GL_GetDrawableSize(win, &expected[0], &expected[1]);
|
||||
SDL_Thread *worker = SDL_CreateThread(drawable_worker, "drawable-test", seen);
|
||||
check(worker != NULL, "drawable reader thread created");
|
||||
if (worker) SDL_WaitThread(worker, NULL);
|
||||
check(seen[0] == expected[0] && seen[1] == expected[1], "worker sees resized drawable");
|
||||
}
|
||||
gl_renderer_render_thread_sync("test_done");
|
||||
check(glGetError() == GL_NO_ERROR, "GL error");
|
||||
printf("rb=%016llx\n", (unsigned long long)rb_digest);
|
||||
printf("digest=%016llx\n", (unsigned long long)digest);
|
||||
printf("hires=%016llx\n", (unsigned long long)hires);
|
||||
printf("wide=%016llx\n", (unsigned long long)wide);
|
||||
printf("stencil=%016llx\n", (unsigned long long)stencil);
|
||||
printf("checks=%d failures=%d\n", checks, failures);
|
||||
gl_renderer_shutdown();
|
||||
check(!gl_renderer_render_thread_active(), "stopped by shutdown");
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
* 3x as the nearest rescale of all their pixels, before any redraw.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "gl_postfx_projection_fixture.h"
|
||||
#include "gpu_hd_texture_stubs.inc"
|
||||
#include "mod_texture_banks.c"
|
||||
#include "gl_batch_policy_lle.c"
|
||||
@@ -102,6 +103,15 @@ int psx_netplay_active(void){return 0;}
|
||||
GrBackend gr_backend(void){return GR_BACKEND_OPENGL;}
|
||||
void gr_refresh_backend(void){}
|
||||
#endif
|
||||
/* These renderer-only fixtures have no frontend telemetry/menu consumer. */
|
||||
void psx_video_telemetry_compose(int r,int s,int sw,int sh,int dw,int dh,int vw,int vh) {
|
||||
(void)r;(void)s;(void)sw;(void)sh;(void)dw;(void)dh;(void)vw;(void)vh;
|
||||
}
|
||||
void psx_video_telemetry_present(int result) {(void)result;}
|
||||
int psx_host_menu_overlay_image(const uint32_t **p,int *w,int *h) {
|
||||
*p=NULL;*w=*h=0;return 0;
|
||||
}
|
||||
|
||||
int gpu_display_is_depth24(void){return 0;}
|
||||
void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->width=320;out->height=240;}
|
||||
int psx_ws_prim_in_backdrop(void){return 0;}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "../third_party/stb_image.h"
|
||||
#include "hd_texture_pack.h"
|
||||
#include "texture_pack_preload.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
@@ -211,6 +212,24 @@ HdTextureDrawQuery query4(const std::vector<uint16_t>& vram,
|
||||
return query;
|
||||
}
|
||||
|
||||
void test_indexed_palette_fallback() {
|
||||
Fixture f;
|
||||
std::ostringstream stem; stem<<std::hex<<f.upload_hash;
|
||||
touch(f.replacements/(stem.str()+"-index.png"));
|
||||
auto* pack=open_pack(f.root); if(!pack)return;
|
||||
hd_texture_pack_track_upload(pack,10,20,8,4,f.upload_words.data(),f.upload_words.size(),nullptr);
|
||||
auto q=query4(f.vram,0,31); HdTextureMatch match{};
|
||||
check(hd_texture_pack_match(pack,&q,&match)==HD_TEXTURE_LOOKUP_FOUND &&
|
||||
match.entry.palette_hash==f.palette4_hash,"exact palette wins over indexed fallback");
|
||||
f.vram[7*1024+1020]^=1;
|
||||
hd_texture_pack_invalidate(pack,1020,7,1,1);
|
||||
check(hd_texture_pack_match(pack,&q,&match)==HD_TEXTURE_LOOKUP_FOUND &&
|
||||
match.entry.palette_hash==HD_TEXTURE_PALETTE_INDEXED,"changed CLUT uses palette-independent index key");
|
||||
q.depth=HD_TEXTURE_DEPTH_16BPP; q.u_last=7;
|
||||
check(hd_texture_pack_match(pack,&q,&match)==HD_TEXTURE_LOOKUP_NONE,"16bpp never uses indexed palette fallback");
|
||||
hd_texture_pack_destroy(pack);
|
||||
}
|
||||
|
||||
void test_crc_exact_little_endian() {
|
||||
const uint16_t ascii_words[] = {0x3231, 0x3433, 0x3635, 0x3837};
|
||||
check(hd_texture_crc32_words_le(ascii_words, 4) == 0x9AE0DAAFu,
|
||||
@@ -512,10 +531,81 @@ void test_bounded_async_decode(Fixture& fixture) {
|
||||
hd_texture_pixels_release(&pixels);
|
||||
}
|
||||
|
||||
void test_bounded_preload(Fixture& fixture) {
|
||||
namespace tp = psx_texpreload;
|
||||
{
|
||||
HdTexturePack* pack = open_pack(fixture.root);
|
||||
if (!pack) return;
|
||||
HdTexturePreloadResult r{};
|
||||
/* One valid 64x8 PNG; the other keys are empty (malformed) files. */
|
||||
const int status = hd_texture_pack_preload(pack, size_t{64} << 20, 4, &r);
|
||||
check(status >= 0 && r.loaded == 1 && r.bytes == 64u * 8u * 4u,
|
||||
"preload pins the valid replacement");
|
||||
check(r.requested == r.loaded + r.failed + r.over_budget &&
|
||||
(r.requested == 1 ? status == 1 : (status == 0 && r.failed >= 1)),
|
||||
"malformed replacements are reported, not counted as preloaded");
|
||||
hd_texture_pack_set_decode_budget(pack, 1);
|
||||
HdTexturePreloadResult repeat{};
|
||||
check(hd_texture_pack_preload(pack,64u*8u*4u,1,&repeat) >= 0 &&
|
||||
repeat.loaded == 1 && repeat.bytes == 64u*8u*4u &&
|
||||
repeat.failed == repeat.requested - 1,
|
||||
"repeat counts pinned images without decoding them under a reduced budget");
|
||||
check(hd_texture_pack_preload(pack,64u*8u*4u-1,1,&repeat) == -1 &&
|
||||
repeat.bytes == 64u*8u*4u,
|
||||
"a smaller lifetime cap rejects and reports retained residency");
|
||||
check(hd_texture_pack_track_upload(pack, 10, 20, 8, 4,
|
||||
fixture.upload_words.data(),
|
||||
fixture.upload_words.size(), nullptr) == 1,
|
||||
"preload fixture upload tracks");
|
||||
HdTexturePixels pixels{};
|
||||
check(hd_texture_pack_acquire_decoded(pack, fixture.upload_hash, fixture.palette4_hash,
|
||||
&pixels) == HD_TEXTURE_LOOKUP_FOUND &&
|
||||
pixels.width == 64,
|
||||
"a preloaded image is resident and survives budget eviction");
|
||||
hd_texture_pixels_release(&pixels);
|
||||
hd_texture_pack_destroy(pack);
|
||||
}
|
||||
{
|
||||
HdTexturePack* pack = open_pack(fixture.root);
|
||||
if (!pack) return;
|
||||
HdTexturePreloadResult r{};
|
||||
check(hd_texture_pack_preload(pack, 64, 4, &r) == 0 && r.loaded == 0 &&
|
||||
r.over_budget == 1 && r.bytes == 0,
|
||||
"the aggregate cap skips an oversized image");
|
||||
hd_texture_pack_destroy(pack);
|
||||
}
|
||||
{
|
||||
HdTexturePack* pack = open_pack(fixture.root);
|
||||
if (!pack) return;
|
||||
tp::fault_mode() = 1;
|
||||
HdTexturePreloadResult r{};
|
||||
const int status = hd_texture_pack_preload(pack, size_t{64} << 20, 4, &r);
|
||||
tp::fault_mode() = 0;
|
||||
check(status == 0 && r.worker_errors == 1 && r.loaded + 1 + r.over_budget <= r.requested,
|
||||
"an injected worker failure is caught and reported as partial");
|
||||
hd_texture_pack_destroy(pack);
|
||||
}
|
||||
{
|
||||
HdTexturePack* pack = open_pack(fixture.root);
|
||||
if (!pack) return;
|
||||
tp::fault_mode() = 3;
|
||||
HdTexturePreloadResult r{};
|
||||
const int status = hd_texture_pack_preload(pack, size_t{64} << 20, 4, &r);
|
||||
tp::fault_mode() = 0;
|
||||
check(status == -1 && r.loaded == 1 && r.bytes == 64u * 8u * 4u,
|
||||
"publication failure still accounts the retained pinned image");
|
||||
check(hd_texture_pack_preload(pack,64u*8u*4u,1,&r) >= 0 &&
|
||||
r.loaded == 1 && r.bytes == 64u*8u*4u && r.failed == r.requested-1,
|
||||
"publication retry accounts existing pinned residency");
|
||||
hd_texture_pack_destroy(pack);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
test_crc_exact_little_endian();
|
||||
test_indexed_palette_fallback();
|
||||
Fixture fixture;
|
||||
test_scan_mapping(fixture);
|
||||
test_palette_match_and_invalidation(fixture);
|
||||
@@ -523,6 +613,7 @@ int main() {
|
||||
test_wrapping(fixture);
|
||||
test_ambiguity_fallback();
|
||||
test_bounded_async_decode(fixture);
|
||||
test_bounded_preload(fixture);
|
||||
|
||||
if (failures) {
|
||||
std::fprintf(stderr, "test_hd_texture_pack: %d failure(s)\n", failures);
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
#include "input_replay.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#define CHECK(x) do { if (!(x)) { puts(#x); return 1; } } while (0)
|
||||
int main(int argc, char **argv) {
|
||||
CHECK(argc == 2);
|
||||
FILE *f = fopen(argv[1], "w"); CHECK(f);
|
||||
fputs("# neutral then steer\n2 ffff\n3 bfdf\n1 bfff\n", f); fclose(f);
|
||||
CHECK(input_replay_load(argv[1]));
|
||||
CHECK(input_replay_buttons(0) == -1);
|
||||
input_replay_on_load(100);
|
||||
CHECK(input_replay_buttons(99) == -1);
|
||||
CHECK(input_replay_buttons(100) == 65535);
|
||||
CHECK(input_replay_buttons(101) == 65535);
|
||||
CHECK(input_replay_buttons(102) == 0xbfdf);
|
||||
CHECK(input_replay_buttons(102) == 0xbfdf);
|
||||
CHECK(input_replay_buttons(104) == 0xbfdf);
|
||||
CHECK(input_replay_buttons(105) == 0xbfff);
|
||||
CHECK(input_replay_buttons(106) == 65535);
|
||||
CHECK(input_replay_buttons(102) == 0xbfdf);
|
||||
input_replay_on_load(200);
|
||||
CHECK(input_replay_frame(200) == 0);
|
||||
CHECK(input_replay_buttons(202) == 0xbfdf);
|
||||
f = fopen(argv[1], "w"); CHECK(f);
|
||||
fputs("0 bfff\n", f); fclose(f);
|
||||
CHECK(!input_replay_load(argv[1]));
|
||||
const char *bad[] = {"-1 bfff\n", "4294967296 bfff\n", "1 10000\n", "1 bfff junk\n", "1 -1\n"};
|
||||
for (unsigned i=0; i<sizeof bad/sizeof *bad; i++) {
|
||||
f = fopen(argv[1], "w"); CHECK(f); fputs(bad[i], f); fclose(f);
|
||||
CHECK(!input_replay_load(argv[1]));
|
||||
}
|
||||
remove(argv[1]);
|
||||
puts("input replay: PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mod_runtime.h"
|
||||
#include "func_override.h"
|
||||
#include "mod_netplay.h"
|
||||
#include "host_launch_timing.h"
|
||||
#include "host_overlay.h"
|
||||
@@ -30,8 +31,21 @@
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::array<uint8_t, 2 * 1024 * 1024> ram;
|
||||
extern "C" int (*g_psx_func_override_hook)(CPUState*, uint32_t);
|
||||
static int failures;
|
||||
static int activation_calls;
|
||||
static int override_activation_calls;
|
||||
static void override_activate() { ++override_activation_calls; }
|
||||
static int override_scope_checks;
|
||||
static int override_body(CPUState*) {
|
||||
char option[16]{};
|
||||
if (psx_mod_current_option_value("scope",option,sizeof option) &&
|
||||
!std::strcmp(option,"owned") && psx_mod_function_entry_active()) ++override_scope_checks;
|
||||
mod_runtime_on_vblank();
|
||||
if (psx_mod_current_option_value("scope",option,sizeof option) &&
|
||||
!std::strcmp(option,"owned") && psx_mod_function_entry_active()) ++override_scope_checks;
|
||||
return 1;
|
||||
}
|
||||
static int plugin_calls;
|
||||
static int restore_calls;
|
||||
static int option_renderer_activations;
|
||||
@@ -153,6 +167,23 @@ extern "C" void gpu_get_display_info(GpuDisplayInfo* out) {
|
||||
*out = g_test_display;
|
||||
}
|
||||
|
||||
/* func_override.c doubles: linked in so mod_runtime's package arming/reset
|
||||
* paths run for real. The tier's own behavior is pinned by
|
||||
* test_func_override.c; here the doubles only need to satisfy the link. */
|
||||
extern "C" uint32_t psx_peek_word_untraced(uint32_t) { return 0; }
|
||||
extern "C" void psx_dispatch_call(struct CPUState*, uint32_t, uint32_t) {}
|
||||
extern "C" {
|
||||
uint64_t psx_cycle_count = 0;
|
||||
uint64_t psx_next_service_cycle = 0;
|
||||
int psx_in_device_service = 0;
|
||||
int g_event_step_conservative = 0;
|
||||
int g_ls_replay_active = 0;
|
||||
uint32_t g_psx_cyc_batch = 0;
|
||||
uint32_t g_psx_cyc_batch_limit = 0;
|
||||
void psx_devices_service_to_now(void) {}
|
||||
void psx_advance_cycles_slow(uint32_t cycles) { psx_cycle_count += cycles; }
|
||||
}
|
||||
|
||||
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
|
||||
static int test_game_started = 1;
|
||||
extern "C" int fntrace_is_game_started(void) { return test_game_started; }
|
||||
@@ -523,6 +554,7 @@ int main(int argc, char** argv) {
|
||||
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-test";
|
||||
#endif
|
||||
std::error_code ec;
|
||||
|
||||
fs::remove_all(root, ec);
|
||||
const std::vector<uint8_t> stock(8 * 2352, 0);
|
||||
std::vector<uint8_t> overlay(3000);
|
||||
@@ -2422,6 +2454,68 @@ int main(int argc, char** argv) {
|
||||
"resolver preserves missing selection for existing downstream validation");
|
||||
}
|
||||
|
||||
{
|
||||
const auto base = root/"function-overrides";
|
||||
write_text(base/"bundled/override.test/1.0.0/manifest.toml", R"toml(
|
||||
format_version = 5
|
||||
id = "override.test"
|
||||
version = "1.0.0"
|
||||
name = "Overrides"
|
||||
resolver = "declarative"
|
||||
save_compatibility = "shared"
|
||||
[[target]]
|
||||
game_id = "SLUS-OVERRIDE"
|
||||
[[feature]]
|
||||
id = "replace"
|
||||
name = "Replace"
|
||||
default_enabled = true
|
||||
[[option]]
|
||||
feature = "replace"
|
||||
id = "scope"
|
||||
label = "Scope"
|
||||
type = "choice"
|
||||
default = "owned"
|
||||
[[option.choice]]
|
||||
value = "owned"
|
||||
label = "Owned"
|
||||
[[option.choice]]
|
||||
value = "other"
|
||||
label = "Other"
|
||||
[[plugin]]
|
||||
feature = "replace"
|
||||
id = "override.test"
|
||||
)toml");
|
||||
check(PSXRecompV4::mod_register_activation_plugin("override.test",override_activate),"register override activation");
|
||||
check(psx_mod_register_function_override("override.test:first",0x80006004,override_body,nullptr,0,0),"queue first override");
|
||||
check(psx_mod_register_function_override("override.test:second",0x80006000,override_body,nullptr,0,0),"queue second override");
|
||||
check(!psx_mod_register_function_override("override.test:bad",0x80006001,override_body,nullptr,0,0),"reject misaligned override");
|
||||
check(PSXRecompV4::mod_runtime_initialize(base,"SLUS-OVERRIDE",0x80002000,{},&error),error.c_str());
|
||||
check(PSXRecompV4::mod_runtime_commit({},&error),error.c_str());
|
||||
mod_runtime_activate_plugins();
|
||||
check(!mod_runtime_activation_error() && func_override_count()==2 && override_activation_calls==1,"selected overrides arm together");
|
||||
CPUState override_cpu{};
|
||||
check(g_psx_func_override_hook && g_psx_func_override_hook(&override_cpu,0x6004),"invoke package override");
|
||||
check(override_scope_checks==2 && !psx_mod_function_entry_active(),
|
||||
"override owns its resource context across VBlank and blocks mid-stack snapshots");
|
||||
char no_scope[16]{};
|
||||
check(!psx_mod_current_option_value("scope",no_scope,sizeof no_scope),"override restores its caller's context");
|
||||
mod_runtime_activate_plugins();
|
||||
check(!mod_runtime_activation_error() && func_override_count()==2 && override_activation_calls==2,"session reactivation replaces entries without duplicates");
|
||||
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error),error.c_str());
|
||||
check(func_override_count()==0,"cleared plan disarms all package entries");
|
||||
override_activation_calls=0;
|
||||
check(func_override_add("direct",0x80006000,override_body,0)==FO_OK,"register colliding direct override");
|
||||
check(PSXRecompV4::mod_runtime_initialize(base,"SLUS-OVERRIDE",0x80002000,{},&error),error.c_str());
|
||||
check(PSXRecompV4::mod_runtime_commit({},&error),error.c_str());
|
||||
mod_runtime_activate_plugins();
|
||||
check(mod_runtime_activation_error()!=nullptr && override_activation_calls==0,
|
||||
"late collision rejects launch before callbacks");
|
||||
check(func_override_count()==1,"collision rolls back all package entries and preserves direct entry");
|
||||
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error),error.c_str());
|
||||
mod_runtime_activate_plugins();
|
||||
check(!mod_runtime_activation_error() && func_override_count()==1,"vanilla session clears failure and retains direct tier");
|
||||
}
|
||||
|
||||
fs::remove_all(root, ec);
|
||||
if (failures) return 1;
|
||||
std::cout << "mod runtime tests passed\n";
|
||||
|
||||
@@ -1066,19 +1066,6 @@ int main(void) {
|
||||
CHECK(st.value_mismatch > 0);
|
||||
}
|
||||
|
||||
#ifdef PSX_NO_DEBUG_TOOLS
|
||||
/* Diagnostic-only receipts must stay empty while the numeric tests above
|
||||
* exercise real stores, projections and word-shadow misses. */
|
||||
{
|
||||
const PGXPStoreRecord *stores;
|
||||
const PGXPTriRecord *triangles;
|
||||
const PGXPWordMiss *misses;
|
||||
uint32_t capacity;
|
||||
CHECK(pgxp_store_ring(&stores, &capacity) == 0);
|
||||
CHECK(pgxp_tri_ring(&triangles, &capacity) == 0);
|
||||
CHECK(pgxp_word_miss_ring(&misses, &capacity) == 0);
|
||||
}
|
||||
#endif
|
||||
if (g_failures) {
|
||||
std::fprintf(stderr, "test_pgxp: %d FAILURES\n", g_failures);
|
||||
return 1;
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#include "gte_projection_publication.hpp"
|
||||
#include "postfx_debug_json.h"
|
||||
#include <atomic>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <thread>
|
||||
static int failures;
|
||||
static void check(bool ok,const char* why) {
|
||||
if(!ok){std::fprintf(stderr,"%s\n",why);++failures;}
|
||||
}
|
||||
int main() {
|
||||
GteProjectionPublication state; GtePostfxProjection out{};
|
||||
state.publish(1,2,3); check(!state.read(out),"disabled projection is unavailable");
|
||||
state.enable(true); state.publish(1,2,3);
|
||||
check(state.read(out)&&out.h==1&&out.ofx==2&&out.ofy==3,"first enabled tuple");
|
||||
state.enable(false);state.enable(true);
|
||||
check(!state.read(out),"re-enabled session cannot reuse a stale tuple");
|
||||
state.publish(1,2,3);check(state.read(out),"unchanged values publish in the new epoch");
|
||||
std::atomic<bool> done{false},bad{false};
|
||||
std::thread writer([&]{for(int i=1;i<200000;++i)state.publish(i,i*2,i*3);done=true;});
|
||||
auto read=[&]{while(!done.load()){GtePostfxProjection p{};
|
||||
if(state.read(p)&&(p.ofx!=p.h*2||p.ofy!=p.h*3))bad=true;}};
|
||||
std::thread a(read),b(read);writer.join();a.join();b.join();
|
||||
check(!bad,"concurrent readers never see mixed projection fields");
|
||||
PfxDebugLive live{};std::strcpy(live.name,"u_\"x\n");live.v[0]=1.25f;
|
||||
char json[512];check(pfx_debug_json(json,sizeof json,"C:\\a\"b\n","bad\tpreset",&live,1)>0,"serialize escaped diagnostics");
|
||||
check(std::strstr(json,R"("path":"C:\\a\"b\u000a")")!=nullptr,"path round trips without mutation");
|
||||
check(std::strstr(json,R"("u_\"x\u000a":[1.25,0,0,0])")!=nullptr,"uniform names are escaped");
|
||||
std::printf("{%s}\n",json);
|
||||
for(int cap=0;cap<160;++cap) {
|
||||
char guarded[192];std::memset(guarded,'X',sizeof guarded);
|
||||
const int n=pfx_debug_json(guarded+1,cap,"C:\\a\"b\n","bad\tpreset",&live,1);
|
||||
check(guarded[0]=='X'&&guarded[cap+1]=='X',"small output buffer stays within bounds");
|
||||
if(!n&&cap)check(guarded[1]==0,"failed JSON output is empty");
|
||||
}
|
||||
live.v[0]=std::numeric_limits<float>::infinity();
|
||||
check(!pfx_debug_json(json,sizeof json,"","",&live,1)&&!json[0],"nonfinite values fail without invalid JSON");
|
||||
return failures?1:0;
|
||||
}
|
||||
@@ -13,9 +13,6 @@ int g_ls_replay_active = 0;
|
||||
int g_ls_mode = 0;
|
||||
int g_precise_mode = 0;
|
||||
int g_psx_call_bail = 0;
|
||||
/* These standalone fixtures have no host-output sandbox or restored CPU. */
|
||||
int g_boot_state_cpu_timing_restored = 0;
|
||||
int psx_mod_host_output_scope(void) { return 0; }
|
||||
uint32_t i_mask = 0;
|
||||
uint64_t g_guest_store_count = 0;
|
||||
uint64_t g_mmio_access_count = 0;
|
||||
|
||||
@@ -34,9 +34,6 @@ int g_ls_replay_active = 0;
|
||||
int g_ls_mode = 0;
|
||||
int g_precise_mode = 0;
|
||||
int g_psx_call_bail = 0;
|
||||
/* These standalone fixtures have no host-output sandbox or restored CPU. */
|
||||
int g_boot_state_cpu_timing_restored = 0;
|
||||
int psx_mod_host_output_scope(void) { return 0; }
|
||||
uint32_t i_mask = 0;
|
||||
uint64_t g_guest_store_count = 0;
|
||||
uint64_t g_mmio_access_count = 0;
|
||||
|
||||
@@ -620,16 +620,6 @@ int main(void) {
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.device_reads==i+1 && st.aborted==i+1 && st.watchdog==0,
|
||||
"device read refusal is distinct from a watchdog");
|
||||
CHECK(st.timed_passes==i+1 && st.max_pass_ms>0,
|
||||
"timing resets per session and includes aborted passes");
|
||||
{
|
||||
double parts = st.total_gl_begin_ms + st.total_checkpoint_ms +
|
||||
st.total_verify_before_ms + st.total_guest_ms + st.total_gl_end_ms +
|
||||
st.total_restore_ms + st.total_verify_after_ms;
|
||||
CHECK(parts >= st.total_pass_ms - 0.000001 &&
|
||||
parts <= st.total_pass_ms + 0.000001,
|
||||
"phase wall times account for the entire transaction");
|
||||
}
|
||||
CHECK(st.last_device_read==reads[i][0] && st.last_device_read_width==reads[i][1],
|
||||
"device read diagnostic identifies the access");
|
||||
CHECK(st.last_failure.reason && strcmp(st.last_failure.reason,"device_read")==0,
|
||||
|
||||
@@ -999,6 +999,34 @@ static int local_temporal_draw(CPUState *cpu, void *user, uint32_t alpha) {
|
||||
return local_draw(cpu, user, 0);
|
||||
}
|
||||
|
||||
/* Shared-state digest deliberately excludes the local presentation image. */
|
||||
static uint64_t shared_digest(const CPUState *cpu) {
|
||||
uint64_t h = 0xcbf29ce484222325ull;
|
||||
const uint8_t *parts[] = { (const uint8_t *)cpu->gpr, s_ram, s_spad };
|
||||
const size_t sizes[] = { sizeof cpu->gpr, s_ram_live, sizeof s_spad };
|
||||
for (size_t p = 0; p < sizeof parts / sizeof parts[0]; ++p)
|
||||
for (size_t i = 0; i < sizes[p]; ++i) {
|
||||
h ^= parts[p][i];
|
||||
h *= 0x100000001b3ull;
|
||||
}
|
||||
h ^= psx_cycle_count;
|
||||
h *= 0x100000001b3ull;
|
||||
h ^= ((uint64_t)i_mask << 32) | i_stat;
|
||||
return h * 0x100000001b3ull;
|
||||
}
|
||||
|
||||
static int local_temporal_discard(CPUState *cpu, void *user, uint32_t alpha) {
|
||||
(void)local_temporal_draw(cpu, user, alpha);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int local_temporal_abort(CPUState *cpu, void *user, uint32_t alpha) {
|
||||
(void)local_temporal_draw(cpu, user, alpha);
|
||||
(void)render_pass_mmio_read_allowed(0x1F801800u, 1);
|
||||
CHECK(0, "device-read abort must not return to guest code");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Netplay local view: forward netplay frames only; guest state restored
|
||||
* exactly; a committed image cancels the crop request. */
|
||||
static void test_local_view(void) {
|
||||
@@ -1006,6 +1034,7 @@ static void test_local_view(void) {
|
||||
PSXModRenderPass r;
|
||||
RenderPassStats st;
|
||||
uint64_t cycle;
|
||||
uint64_t digest;
|
||||
render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1;
|
||||
memset(&r, 0, sizeof r);
|
||||
r.struct_size = sizeof r;
|
||||
@@ -1049,8 +1078,17 @@ static void test_local_view(void) {
|
||||
"guest state restored after the local view");
|
||||
psx_mod_set_render_pass_netplay(1);
|
||||
r.alpha_q16 = 32768u;
|
||||
digest = shared_digest(&cpu);
|
||||
CHECK(psx_mod_render_pass(&cpu, &r, local_temporal_draw, NULL) == 1,
|
||||
"opted-in temporal pass uses own-view scope");
|
||||
CHECK(shared_digest(&cpu) == digest,
|
||||
"actual opted-in temporal pass preserves shared digest");
|
||||
CHECK(!psx_mod_render_pass(&cpu, &r, local_temporal_discard, NULL) &&
|
||||
shared_digest(&cpu) == digest,
|
||||
"discarded temporal pass preserves shared digest");
|
||||
CHECK(!psx_mod_render_pass(&cpu, &r, local_temporal_abort, NULL) &&
|
||||
shared_digest(&cpu) == digest && s_open_passes == 0,
|
||||
"aborted temporal pass restores shared state and closes transaction");
|
||||
CHECK(s_ws_scope == 0 && psx_mod_local_view_scope() == 0 &&
|
||||
cpu.gpr[8] == 7 && s_ram[0x456] == 0x77 && psx_cycle_count == cycle,
|
||||
"temporal local pass restores scope, RAM, registers and clock");
|
||||
@@ -1058,6 +1096,10 @@ static void test_local_view(void) {
|
||||
CHECK(!psx_mod_render_pass(&cpu, &r, never_fn, NULL),
|
||||
"temporal local rendering never runs during rollback replay");
|
||||
s_resim = 0;
|
||||
psx_mod_set_render_pass_netplay(0);
|
||||
CHECK(!psx_mod_render_pass(&cpu, &r, never_fn, NULL) &&
|
||||
shared_digest(&cpu) == digest,
|
||||
"nonparticipating peer refuses temporal pass without shared-state change");
|
||||
r.x = 900;
|
||||
CHECK(!psx_mod_render_local_view(&cpu, &r, never_fn, NULL),
|
||||
"rect outside VRAM refused");
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/* SYS03 must preserve register RAM even after CloseTh marks the outgoing TCB
|
||||
* free. Generic register patterns only: no game addresses. */
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <setjmp.h>
|
||||
#include "cpu_state.h"
|
||||
#include "psx_scheduler.h"
|
||||
|
||||
static uint32_t ram[0x10000 / 4];
|
||||
static CPUState cpu;
|
||||
uint64_t s_frame_count = 0;
|
||||
|
||||
static uint32_t rd(uint32_t a) { assert(a < sizeof ram && !(a & 3)); return ram[a / 4]; }
|
||||
static void wr(uint32_t a, uint32_t v) { assert(a < sizeof ram && !(a & 3)); ram[a / 4] = v; }
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
int overlay_loader_shadow_native_thread_switch_bail(void) { return 0; }
|
||||
int parity_trace_is_armed(void) { return 0; }
|
||||
void debug_server_log_thread_event(uint32_t k, CPUState* c, uint32_t a, uint32_t b, uint32_t p)
|
||||
{ (void)k; (void)c; (void)a; (void)b; (void)p; }
|
||||
int psx_syscall(CPUState*, uint32_t);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const uint32_t pcb = 0xe000, old = 0xe1f4, target = old + 0xc0;
|
||||
ram[0x108 / 4] = pcb;
|
||||
ram[0x110 / 4] = old;
|
||||
ram[0x114 / 4] = 4 * 0xc0;
|
||||
ram[pcb / 4] = old;
|
||||
ram[old / 4] = 0x1000; /* Closed, but still the current thread until SYS03. */
|
||||
ram[target / 4] = 0x4000;
|
||||
cpu.read_word = rd;
|
||||
cpu.write_word = wr;
|
||||
for (unsigned i = 1; i < 32; i++) cpu.gpr[i] = 0x16160000u + i;
|
||||
cpu.gpr[4] = 3;
|
||||
cpu.gpr[5] = target;
|
||||
cpu.gpr[31] = 0x2104;
|
||||
cpu.cop0[12] = 0x401;
|
||||
cpu.hi = 0xaaaa;
|
||||
cpu.lo = 0xbbbb;
|
||||
if (setjmp(g_scheduler_jmpbuf) == 0) {
|
||||
psx_syscall(&cpu, 0);
|
||||
assert(!"must yield to scheduler");
|
||||
}
|
||||
assert(ram[old / 4] == 0x1000); /* Saving never reopens the retired thread. */
|
||||
assert(ram[pcb / 4] == target);
|
||||
assert(ram[(old + 8 + 22 * 4) / 4] == 0x16160016u);
|
||||
assert(ram[(old + 8 + 128) / 4] == 0x2104);
|
||||
assert(ram[(old + 8 + 132) / 4] == 0xaaaa && ram[(old + 8 + 136) / 4] == 0xbbbb);
|
||||
assert(g_sched_escape.target_tcb == target);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
"""CloseTh(current); ChangeTh(other) must still save the outgoing registers.
|
||||
|
||||
Links the production traps.c into test_syscall_closed_thread_context.c at -O0
|
||||
and -O2; every other runtime seam is an abort() stub (fixture_link.py).
|
||||
"""
|
||||
import argparse
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from fixture_link import build_and_run
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--cc', default='gcc')
|
||||
args = parser.parse_args()
|
||||
here = Path(__file__).resolve().parent
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
for opt in ('-O0', '-O2'):
|
||||
build_and_run(args.cc, here, here.parent, opt, Path(root),
|
||||
['traps.c'], 'test_syscall_closed_thread_context.c')
|
||||
print('PASS: closed outgoing thread keeps its context without reopening (O0/O2)')
|
||||
@@ -52,6 +52,42 @@ int main() {
|
||||
CHECK(sample.valid && sample.internal_scale == 10000 && sample.presents == 10002);
|
||||
CHECK(sample.present_status_available);
|
||||
CHECK(sample.present_errors == 1);
|
||||
PsxPresentCadence cadence{};
|
||||
psx_video_telemetry_cadence(0,1,&cadence);
|
||||
psx_video_telemetry_present_at(1,1000);
|
||||
psx_video_telemetry_present_at(1,11000);
|
||||
psx_video_telemetry_present_at(-1,21000);
|
||||
psx_video_telemetry_present_at(1,51000);
|
||||
psx_video_telemetry_cadence(0,0,&cadence);
|
||||
CHECK(cadence.valid && cadence.count==3 && cadence.mean_us==16666);
|
||||
CHECK(cadence.p50_us==10000 && cadence.max_us==30000 && cadence.hitches==1);
|
||||
psx_video_telemetry_cadence(35,0,&cadence);
|
||||
CHECK(cadence.valid && cadence.count==1 && cadence.max_us==30000);
|
||||
psx_video_telemetry_cadence(0,1,&cadence);
|
||||
psx_video_telemetry_cadence(0,0,&cadence);
|
||||
CHECK(cadence.valid && !cadence.count);
|
||||
psx_video_telemetry_present_at(1,100000);
|
||||
psx_video_telemetry_cadence(0,0,&cadence);
|
||||
CHECK(cadence.valid && !cadence.count); // producer reset re-anchors timing
|
||||
std::atomic<bool> cadence_done{false};
|
||||
std::atomic<bool> cadence_consistent{true};
|
||||
std::thread presenter([&] {
|
||||
for(uint64_t i=1;i<=20000;++i) psx_video_telemetry_present_at(1,100000+i*10000);
|
||||
cadence_done=true;
|
||||
});
|
||||
auto reader=[&](bool resets) {
|
||||
unsigned reads=0;
|
||||
do {
|
||||
PsxPresentCadence c{};
|
||||
psx_video_telemetry_cadence(0,resets && (++reads%17==0),&c);
|
||||
if(c.valid && c.count && (c.mean_us!=10000 || c.max_us!=10000 || c.hitches!=0))
|
||||
cadence_consistent=false;
|
||||
}while(!cadence_done.load());
|
||||
};
|
||||
std::thread other_reader([&]{reader(true);});
|
||||
reader(false);
|
||||
presenter.join();other_reader.join();
|
||||
CHECK(cadence_consistent.load());
|
||||
psx_video_telemetry_snapshot(nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ uint32_t g_debug_last_store_pc;
|
||||
uint32_t g_dirty_ram_code_gen;
|
||||
uint64_t g_dispatch_static_hits;
|
||||
uint32_t g_psx_mod_instruction_hooks;
|
||||
/* The interpreter consults the optional override tier even in this isolated
|
||||
* culling fixture. No overrides are registered here. */
|
||||
int (*g_psx_func_override_hook)(CPUState *, uint32_t);
|
||||
uint32_t psx_mod_memory_address(const CPUState *cpu, uint32_t pc, uint32_t word, uint32_t address) {
|
||||
(void)cpu; (void)pc; (void)word; return address;
|
||||
}
|
||||
@@ -531,6 +534,96 @@ static void test_screen_x_precedence(void) {
|
||||
gpu_ws_set_signed_x_bound_sites(NULL, NULL, 0);
|
||||
}
|
||||
|
||||
/* [[widescreen.cull.sxy]] and the screen_x `slti rt, x, 0` form through
|
||||
* real interpreted instructions. The fold is Wild 9's triangle reject (with
|
||||
* its interleaved mfc2/sw/sub left in RAM); the interpreter must produce the
|
||||
* same word as the helper chain the recompiler emits, and treat a variant
|
||||
* with any other fold or consumer word as vanilla code. */
|
||||
static uint32_t sxy(int x, int y) {
|
||||
return ((uint32_t)(uint16_t)(int16_t)y << 16) | (uint16_t)(int16_t)x;
|
||||
}
|
||||
static void test_sxy_fold(void) {
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof cpu);
|
||||
memset(test_ram, 0, sizeof test_ram);
|
||||
enum { AT = 1, T0 = 8, T3 = 11, S1 = 17, S4 = 20, S6 = 22 };
|
||||
const uint32_t base = CODE + 0x300;
|
||||
const uint32_t words[] = { 0x01110824u, 0x480B7000u, 0x02810824u, 0xAC880008u,
|
||||
0x01610824u, 0x02084022u, 0x14200001u };
|
||||
for (uint32_t i = 0; i < 7; i++) put_word(base + 4u * i, words[i]);
|
||||
const uint32_t vertices[] = { 3 };
|
||||
const uint32_t fold_a[] = { base, base + 8u, base + 0x10u, 0 };
|
||||
const uint32_t fold_e[] = { words[0], words[2], words[4], 0 };
|
||||
const uint32_t regs[] = { T0, S4, T3, 0 };
|
||||
const uint32_t cons_a[] = { base + 0x18u }, cons_e[] = { words[6] };
|
||||
gpu_ws_set_sxy_cull_sites(vertices, fold_a, fold_e, regs, cons_a, cons_e, 1);
|
||||
const uint32_t screen_x[] = { base + 0x40u };
|
||||
gpu_ws_set_cull_screen_x_sites(screen_x, 1);
|
||||
const uint32_t slti_at_s6 = i_type(0x0A, S6, AT, 0);
|
||||
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
const int m = pass ? 53 : 0;
|
||||
gpu_ws_set_margin_override(m);
|
||||
const uint32_t cases[][3] = {
|
||||
{ sxy(-60, 10), sxy(-53, 20), sxy(-100, 30) }, /* one in the band */
|
||||
{ sxy(-60, 10), sxy(-54, 20), sxy(-100, 30) }, /* all left of -m */
|
||||
{ sxy(-10, -1), sxy(100, -5), sxy(5, -9) }, /* all above */
|
||||
{ sxy(10, 10), sxy(400, 20), sxy(-5, 30) }, /* on screen */
|
||||
};
|
||||
for (int c = 0; c < 4; c++) {
|
||||
cpu.gpr[S1] = 0x80008000u;
|
||||
cpu.gpr[T0] = cases[c][0];
|
||||
cpu.gpr[S4] = cases[c][1];
|
||||
cpu.gpr[T3] = cases[c][2];
|
||||
(void)step(&cpu, base, words[0]);
|
||||
(void)step(&cpu, base + 8u, words[2]);
|
||||
(void)step(&cpu, base + 0x10u, words[4]);
|
||||
const uint32_t native = psx_ws_cull_sxy_and(psx_ws_cull_sxy_and(
|
||||
psx_ws_cull_sxy_and(0x80008000u, cases[c][0]), cases[c][1]),
|
||||
cases[c][2]);
|
||||
const uint32_t vanilla = 0x80008000u & cases[c][0] & cases[c][1] & cases[c][2];
|
||||
CHECK(cpu.gpr[AT] == native, "interp sxy fold == emitted helper chain, case %d m=%d", c, m);
|
||||
if (m == 0)
|
||||
CHECK(cpu.gpr[AT] == vanilla, "sxy fold vanilla at 4:3, case %d", c);
|
||||
}
|
||||
CHECK(m == 0 || psx_ws_cull_sxy_and(psx_ws_cull_sxy_and(psx_ws_cull_sxy_and(
|
||||
0x80008000u, sxy(-60, 10)), sxy(-53, 20)), sxy(-100, 30)) == 0u,
|
||||
"the band vertex keeps the triangle while widened");
|
||||
|
||||
/* A variant whose consumer differs is vanilla code. */
|
||||
put_word(base + 0x18u, 0x10200001u);
|
||||
cpu.gpr[S1] = 0x80008000u;
|
||||
cpu.gpr[T0] = sxy(-60, 10);
|
||||
cpu.gpr[S4] = sxy(-53, 20);
|
||||
cpu.gpr[T3] = sxy(-100, 30);
|
||||
(void)step(&cpu, base, words[0]);
|
||||
(void)step(&cpu, base + 8u, words[2]);
|
||||
(void)step(&cpu, base + 0x10u, words[4]);
|
||||
CHECK(cpu.gpr[AT] == 0x8000u, "sxy variant with another consumer stays vanilla, m=%d", m);
|
||||
put_word(base + 0x18u, words[6]);
|
||||
|
||||
/* An unlisted AND is vanilla. */
|
||||
cpu.gpr[S1] = 0x80008000u;
|
||||
(void)step(&cpu, base + 0x100u, words[0]);
|
||||
CHECK(cpu.gpr[AT] == (0x80008000u & cpu.gpr[T0]), "unlisted AND vanilla, m=%d", m);
|
||||
|
||||
/* screen_x slti rt,x,0 with x = SX << 16. */
|
||||
cpu.gpr[S6] = (uint32_t)-5 << 16;
|
||||
(void)step(&cpu, base + 0x40u, slti_at_s6);
|
||||
CHECK(cpu.gpr[AT] == (m ? 0u : 1u), "screen_x slti keeps the band, m=%d", m);
|
||||
cpu.gpr[S6] = (uint32_t)-54 << 16;
|
||||
(void)step(&cpu, base + 0x40u, slti_at_s6);
|
||||
CHECK(cpu.gpr[AT] == 1u, "screen_x slti rejects x < -m, m=%d", m);
|
||||
cpu.gpr[S6] = (uint32_t)-5 << 16;
|
||||
(void)step(&cpu, base + 0x140u, slti_at_s6);
|
||||
CHECK(cpu.gpr[AT] == 1u, "unlisted slti vanilla, m=%d", m);
|
||||
}
|
||||
gpu_ws_set_margin_override(-1);
|
||||
gpu_ws_set_sxy_cull_sites(NULL, NULL, NULL, NULL, NULL, NULL, 0);
|
||||
gpu_ws_set_cull_screen_x_sites(NULL, 0);
|
||||
CHECK(g_psx_ws_sxy_cull_n == 0, "sxy sites cleared");
|
||||
}
|
||||
|
||||
static void test_masked_reject(void) {
|
||||
CPUState cpu = {0};
|
||||
memset(test_ram, 0, sizeof test_ram);
|
||||
@@ -601,6 +694,7 @@ int main(int argc, char **argv) {
|
||||
test_interpreter();
|
||||
test_screen_x_precedence();
|
||||
test_masked_reject();
|
||||
test_sxy_fold();
|
||||
test_wide_nclip_branches();
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
|
||||
Reference in New Issue
Block a user