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:
Shokunin
2026-10-24 14:08:25 -07:00
95 changed files with 4187 additions and 1756 deletions
+23
View File
@@ -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.
+17
View File
@@ -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.
-49
View File
@@ -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.
-12
View File
@@ -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
+2 -9
View File
@@ -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,
+4 -2
View File
@@ -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
View File
@@ -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`.
---
+23
View File
@@ -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.
+10 -79
View File
@@ -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
View File
@@ -1 +1 @@
86f7329c2692e76133c40fa11cc8cfac214302d8238e05369dc8b263f4dc76fd
d887bd825dee12170e8bf22577543114787a3c7f5f2c9bf5e62b14c990561564
+19
View File
@@ -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
+5
View File
@@ -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.
+2
View File
@@ -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;
+61 -1
View File
@@ -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";
+138
View File
@@ -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,
+17
View File
@@ -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;
+18
View File
@@ -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";
+28
View File
@@ -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;
+2 -2
View File
@@ -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
View File
@@ -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);
-3
View File
@@ -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
+241
View File
@@ -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 */
+19
View File
@@ -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. */
+2 -16
View File
@@ -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
+12
View File
@@ -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;
}
};
+30
View File
@@ -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);
+7
View File
@@ -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.
-4
View File
@@ -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;
-17
View File
@@ -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
+1
View File
@@ -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 {
+33
View File
@@ -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.
+2
View File
@@ -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)
+8 -2
View File
@@ -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;
+43
View File
@@ -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;
}
-7
View File
@@ -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 */
+100
View File
@@ -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
+12
View File
@@ -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 {
+31
View File
@@ -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
+1 -1
View File
@@ -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
+91 -7
View File
@@ -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 },
+78 -2
View File
@@ -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). */
+128 -3
View File
@@ -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;
+1 -1
View File
@@ -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;
+296
View File
@@ -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
View File
@@ -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;
-213
View File
@@ -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;
}
+57
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+37 -1
View File
@@ -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;
+10
View File
@@ -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
View File
@@ -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) {
-2
View File
@@ -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] : "?";
}
-66
View File
@@ -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
View File
@@ -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
+10
View File
@@ -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,
+13 -1
View File
@@ -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
View File
@@ -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) {
-17
View File
@@ -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,
+3 -1
View File
@@ -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;
+2 -6
View File
@@ -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)
+1 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+79
View File
@@ -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;}
+6 -35
View File
@@ -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"])
+117 -1
View File
@@ -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;
}
+519
View File
@@ -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;
}
+77 -2
View File
@@ -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);
+1
View File
@@ -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"
+53 -12
View File
@@ -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
View File
@@ -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"
+2 -161
View File
@@ -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");
+10
View File
@@ -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;}
+91
View File
@@ -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);
-35
View File
@@ -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;
}
+94
View File
@@ -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";
-13
View File
@@ -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;
+40
View File
@@ -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;
-10
View File
@@ -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,
+42
View File
@@ -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)')
+36
View File
@@ -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;
}
+94
View File
@@ -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);