mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 19:07:47 -04:00
Integrate MMX6 adaptive renderer with current framework master
Preserve current segmented dispatch, active-plan mod hooks and software scale limits. Extend function filters through the active plan, retain the last-store pointer with overlay ABI 25, and keep default config fingerprints stable. Refresh BIOS emitter stamps after byte-identical regeneration and reconcile regression fixtures.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# OpenGL performance campaign
|
||||
|
||||
Tracking: central Beads `beads-jod2`, beneath `beads-eio.3` (psxrecomp framework).
|
||||
Branch: `perf/opengl-burndown-20260904`.
|
||||
Starting revision: `bf61b1054313e1c80d5c2bf1b14a1e50f2e54b8a`.
|
||||
This is the audited integration revision, not a claim about a shipped release.
|
||||
|
||||
## Objective and constraints
|
||||
|
||||
Improve sustained guest speed, frame consistency, and resource use at identical
|
||||
settings and correctness. OpenGL is the primary renderer. Vulkan completeness
|
||||
is a separate qualification exercise, never a substitute for an OpenGL fix.
|
||||
Use GPT-5.5 agents for bounded implementation and independent adversarial review
|
||||
of their code, measurements, and acceptance claims.
|
||||
|
||||
The primary route is Ape Escape, Crabby Beach, approaching and facing the water.
|
||||
Inspect existing `ApeEscapeRecomp/saves/scph1001` states before choosing one;
|
||||
preserve originals and record BIOS, state hash, scene, and input sequence.
|
||||
Add quiet gameplay, facing away, and a transition, plus Tomba 2 and Mega Man X6
|
||||
regression routes. Compare guest speed, game-produced frames, and host presents
|
||||
separately to avoid treating original-game slowdown as host overload.
|
||||
|
||||
Test unrestricted, 50%, and 25% host CPU budgets, with affinity recorded as a
|
||||
separate variable. Never change guest clocks to simulate hardware. CPU throttling
|
||||
does not emulate older caches, memory bandwidth, or integrated graphics. Real
|
||||
acceptance targets are an older 2-core/4-thread integrated-graphics PC and a
|
||||
Deck-class handheld, with desktop and supported-platform regression coverage.
|
||||
|
||||
## Work packages
|
||||
|
||||
This table defines the approved work, not its live issue status. Beads owns
|
||||
status and dependencies; experiment results belong in the evidence ledger.
|
||||
|
||||
| ID | Work and candidate experiments | Required evidence |
|
||||
| --- | --- | --- |
|
||||
| P0-1 | Repeatable route runner, artifact/config identity, machine-readable A/B reports | Same guest work, reproducible baseline, measured noise |
|
||||
| P0-2 | Release flags, effective settings, overlay inventory, cache rejection and compiler failures | Clean-install Windows/Linux behavior without development caches |
|
||||
| P1-1 | Unused audio/GPU histories, memory observers, tracing/logging, diagnostic allocations | Measured saved work; retain functional observers and cheap health counters |
|
||||
| P1-2 | GL batch breaks, texture/CLUT dependencies, packing, readbacks, uploads, buffer streaming | Identical order, masks, blending, texture feedback, VRAM and enhanced output |
|
||||
| P1-3 | Swap blocking, pacer ownership, spinning, refresh/focus, interpolation and audio delivery | No speed drift, latency regression or new underruns |
|
||||
| P1-4 | Overlay discovery, validation, loading, fallback, compilation contention and cache growth | Correct identity across replacement, self-modification, restore and cold installs |
|
||||
| P2-1 | Generated code, dispatch, continuations, state traffic, interpreter, lookup/linking | Exceptions, delay slots, resumes and invalidation across all execution tiers |
|
||||
| P2-2 | RAM/MMIO, aliases, observers, cycle publication, deadlines and idle detection | Exact cycle charges, side effects and event ordering |
|
||||
| P2-3 | GTE/PGXP state copies, inactive hooks and shadow validation | Exact GTE state and unchanged enabled enhancements |
|
||||
| P2-4 | SPU/resampling, CD/XA, MDEC, DMA, disc/decompression/card I/O; SIMD/batching/prefetch | Audio, IRQs, backpressure, decode output and completion timing |
|
||||
| P2-5 | Allocation, bandwidth, locality, texture caches, mods/UI, rewind, netplay | Bounded memory, enabled-feature parity and inexpensive inactive paths |
|
||||
| P2-6 | PGO/LTO, compiler options, code layout, portable SIMD and build costs | Multi-title gains, arithmetic parity and portable release artifacts |
|
||||
| P3-1 | Threads, helper processes and GPU compute for attributed hotspots | End-to-end constrained-host gains including all transfer/sync costs |
|
||||
| P3-2 | Software renderer hotspots and separate Vulkan qualification | Explicit coverage gaps; OpenGL acceptance remains mandatory |
|
||||
| P4-1 | Release integration and recurring performance gates | Clean-install route, correctness and resource checks on shipped artifacts |
|
||||
|
||||
Complete broad attribution before concentrating implementation on the largest
|
||||
cost. Every area receives a measured disposition, including already optimized,
|
||||
rejected, or deferred with a concrete missing prerequisite. Do not infer impact
|
||||
from source size, suggestive names, or another console's bottlenecks.
|
||||
|
||||
## Measurement and acceptance
|
||||
|
||||
Follow [HOST_OPTIMIZATION_CONTRACT.md](HOST_OPTIMIZATION_CONTRACT.md).
|
||||
Use at least three interleaved A/B pairs with all raw results, minima, medians,
|
||||
discard reasons, a preregistered primary metric, and the 5% retention gate for
|
||||
added complexity. Use forced faithful/candidate selectors where practical.
|
||||
|
||||
Separate warm-cache throughput (fixed guest work; no compiler activity) from
|
||||
first-use and transition campaigns where compilation/loading is the subject.
|
||||
The latter must be explicitly labeled and must not be represented as satisfying
|
||||
the quiet-host warm-cache gate. Use 120-second paced windows for stutter.
|
||||
Record source and artifact hashes, generated code, BIOS, toolchain, effective
|
||||
settings, overlay contents, power state, pacing owner and instrumentation.
|
||||
|
||||
Attribute CPU samples and thread waits, GL scene/present time, readback/upload
|
||||
bytes, execution tiers, device work, audio underruns, memory, allocations, page
|
||||
faults and I/O. Expensive profiling is for attribution; final timing must also
|
||||
run without it. Missing or reset counters must never become zero-cost evidence.
|
||||
|
||||
Compare deterministic guest state at intermediate checkpoints, including cycles,
|
||||
deadlines, pending exceptions, device state, RAM/VRAM/SPU RAM and overlay identity.
|
||||
Add rendered-output/audio comparisons and candidate hit witnesses. Exercise
|
||||
invalidation, aliases, exceptions, DMA, save/load, rewind and netplay as affected.
|
||||
Do not claim parity from a fallback that silently avoids the candidate.
|
||||
|
||||
For offload, specify ownership, immutable inputs, publication deadlines, bounded
|
||||
queues, backpressure, cancellation and restore before coding. Compare inline
|
||||
against one worker first. No races on guest memory, altered device ordering, or
|
||||
implicit cross-thread GL context use. Count IPC, copying, driver contention,
|
||||
memory and synchronization in the gain, especially on low-core-count/iGPU hosts.
|
||||
|
||||
## Initial evidence and completion
|
||||
|
||||
Source inspection found always-on audio PCM history and GP0 history, potential
|
||||
GL full-VRAM readback, and multiple VRAM copies. These are hypotheses, not
|
||||
measured causes of Crabby Beach slowdown. Widescreen overhang detection is
|
||||
functional; its census already has a gate. Vulkan already has a staging cache.
|
||||
Cycle batching, event deadlines, sparse probes, MDEC SIMD and GPU-direct GL
|
||||
presentation already exist and must not be proposed as new work without audit.
|
||||
|
||||
Append accepted, rejected and incomplete experiments to an evidence ledger with
|
||||
raw-data links. Keep issue status in central Beads and record landed commits.
|
||||
Do not close the campaign until every work package has a disposition and accepted
|
||||
improvements reach validated title releases. Target sustained real-time execution
|
||||
at preserved settings on the selected hardware, without new correctness, audio,
|
||||
or pacing failures. Record hardware limitations rather than hiding them.
|
||||
@@ -1,5 +1,31 @@
|
||||
# Widescreen support (`feat/widescreen`)
|
||||
|
||||
## Opt-in native-wide view anchoring (2026-09-12)
|
||||
|
||||
`gpu_ws_set_view_anchor(camera, min, max, active)` reads a game's signed
|
||||
16-bit camera and clamp bounds at BG renderer setup. It shifts only the host
|
||||
wide mirror, preserving the guest camera and canonical VRAM. The native
|
||||
view's left edge appears at `extra + shift`; `extra` remains the symmetric
|
||||
width budget. The geometry helper distributes reveal toward the room interior
|
||||
and centers rooms narrower than the frame with explicit padding.
|
||||
|
||||
SW, OpenGL and Vulkan implement `gr_wide_set_view`. Explicit HUD packets keep
|
||||
their existing physical origin. OpenGL's canonical-center copy is disabled
|
||||
while anchoring is active because world and UI no longer share an origin.
|
||||
|
||||
For the existing Capcom BG2D layout, bracket each layer's packet producer with
|
||||
`gpu_ws_bg2d_begin_view_layer` / `gpu_ws_bg2d_end_view_layer`. The independent
|
||||
layer mask permits parallax backdrops to anchor to their own map bounds;
|
||||
tile generation and ring refill use that layer's actual left/right coverage.
|
||||
Actor activation uses a constant envelope covering either edge origin.
|
||||
Zero configuration is inactive; 4:3 retains the original path.
|
||||
|
||||
Tests: `ws_view_anchor_test`, `ws_view_anchor_gpu_test`, and the hidden real
|
||||
OpenGL `test_gl_readback_region.c` fixture. MMX6 is the first consumer;
|
||||
owner gameplay validation is still pending (`beads-eio.3.146`).
|
||||
|
||||
## Earlier squash-mode implementation
|
||||
|
||||
Status as of 2026-06-13. Branch `feat/widescreen` in **both** `psxrecomp`
|
||||
(framework) and `TombaRecomp` (game opt-in + config), pushed to remote.
|
||||
Experimental. Latest: the far-backdrop edge void is largely fixed (2D backdrop
|
||||
|
||||
+10
-2
@@ -345,7 +345,7 @@ clip_edge_width = 320 # 1..1024; default screen_w_imms[0]
|
||||
- `bgez_sites` and `clip_edge_x_load_sites` (with the width) contribute to the
|
||||
overlay-cache identity only when non-empty.
|
||||
|
||||
Explicit `bias_sites` / `range_sites` may opt into an additional resident
|
||||
Explicit `bias_sites` / `bias_lower_sites` / `range_sites` may opt into an additional resident
|
||||
object lead without widening terrain or render queues:
|
||||
|
||||
```toml
|
||||
@@ -357,13 +357,21 @@ range_sites = ["0x80069BB0"]
|
||||
```
|
||||
|
||||
`activation_guard_pixels` is added only to the live margin emitted at those
|
||||
two explicit site families, and only while widescreen reveals extra world.
|
||||
three explicit site families, and only while widescreen reveals extra world.
|
||||
At true 4:3 it is exactly zero. `guard_pixels` remains the shared
|
||||
render/terrain participation guard; keep it small when terrain producers or
|
||||
model queues have fixed capacity. Both values are restricted to `[0, 256]`
|
||||
and contribute to native-overlay cache identity. Changing the activation
|
||||
guard requires regenerating the game and overlay code.
|
||||
|
||||
`bias_lower_sites` is the lower-endpoint counterpart to `bias_sites`:
|
||||
an `ADDI`/`ADDIU` camera-relative bound subtracts the activation margin from
|
||||
its original immediate. Both native code and the dirty-RAM path apply it.
|
||||
For strip-based enemy spawning, expand the outer strip edge, initial and
|
||||
vertical scan X bounds, and any associated respawn-reset interval together.
|
||||
Keep authored placement flags and vertical bounds intact. Empty is inert;
|
||||
configured sites require regeneration.
|
||||
|
||||
## Runtime block
|
||||
|
||||
Consumed by the cmake macro `psxrecomp_v4_add_runtime_target` (eventually)
|
||||
|
||||
@@ -213,6 +213,15 @@ on a fixed region -> next.
|
||||
|
||||
## 5. Status / Log (update every session)
|
||||
|
||||
- **2026-10-01 — MMX6 adaptive framework integration.** The opt-in function
|
||||
filters from the MMX6 branch are being reconciled with the active-plan
|
||||
function-entry registry and master overlay store-PC forwarding. A handled
|
||||
filter publishes return registers and `pc=$ra`; snapshot requests defer until
|
||||
its host callback returns. The combined callback ABI is v25 so stale v24
|
||||
overlays cannot mix the two incompatible callback layouts. The adaptive
|
||||
view, host tile packets, retained texture banks and screen masks remain
|
||||
presentation-only enhancements; faithful guest timing is unchanged.
|
||||
|
||||
- **2026-09-30 — Original-disc overlay discovery (beads-eio.3.215).**
|
||||
Ape Escape Primordial Ooze exposed return-adjacent functions omitted by the
|
||||
archive scanner: a scheduled LUI/load before stack setup and an RA save beyond
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
# Performance experiment ledger
|
||||
|
||||
Campaign: [OpenGL performance campaign](../PERFORMANCE_CAMPAIGN.md).
|
||||
Live work status: central Beads `beads-jod2`.
|
||||
|
||||
Record unsuccessful, incomplete and rejected experiments as well as accepted
|
||||
ones. Local game-derived artifacts are not release fixtures. Hashes below make
|
||||
the retained local evidence identifiable without distributing a savestate/ROM.
|
||||
|
||||
## CRAB-DISCOVERY-001: verify a usable starting scene
|
||||
|
||||
Date: 2026-09-04 (session date). Verdict: exploratory evidence only; no candidate
|
||||
optimization, A/B comparison, production claim or correctness qualification.
|
||||
|
||||
- Host: Windows, Ryzen 7 9800X3D, 8 cores / 16 logical processors. Graphics
|
||||
adapters reported by the OS: RTX 3080 Ti, AMD integrated graphics, Meta virtual
|
||||
monitor. Actual GL device and power state were not recorded in this capture.
|
||||
- Historical binary source: `_wt-ape-crabperf-20260901/build-dbg`, CMake Release
|
||||
with `PSX_DEBUG_TOOLS=ON`. Existing report identifies build
|
||||
`v0.3.2-alpha-139-g1bf70960-dirty`. Current source HEAD is not proof of binary
|
||||
provenance. This executable was not built from the campaign branch.
|
||||
- Executable SHA256:
|
||||
`3b27bda4417522eb1ab331e4d2b194828f1d38fb9bbb165d1a78eb2156142ba1`.
|
||||
- State: `saves-dbg/scph1001/state_800A3660_slot01.pst`, version 5, BIOS wordsum
|
||||
`F67ECB99`, entry `800A3660`, codegen hash `A4319B6F`, ABI 21, codegen version 10.
|
||||
SHA256 `2dfc142e65eb32e179b67d352f510d15f6d97efd22b3b43a9eef24301f3ad5a9`.
|
||||
- Executable, saves, assets, BIOS assets and mods were copied into the campaign
|
||||
worktree's `.local/crabby-baseline`. Writable cards/settings use that copy.
|
||||
CLI selected OpenGL, no launcher, debug port 4680. Settings selected 2x SSAA
|
||||
and antialiasing. `PSX_RUNTIME_PERF_DIAG=1`; no host CPU cap was applied.
|
||||
- Restore receipt: `savestate_status` generation advanced from 0 to 1 with
|
||||
`pending=0,last_ok=1,last_op=load,last_slot=1`. Presented screenshot visibly
|
||||
confirms the sandcastle area on the beach facing the water.
|
||||
- Original `ApeEscapeRecomp/saves/scph1001` slots 00/01 were v3 and slots
|
||||
06/07/08/09/11 were v2; they cannot be used as v5 checkpoints.
|
||||
|
||||
The passive 10-second sample is retained locally at
|
||||
`.local/crabby-baseline/stall-10s.json`, SHA256
|
||||
`448d6a77b08479e0cca9bf11ffa21af1b3c2408ba70b9a3f1a0a7696f4a10b28`.
|
||||
Snapshot start timestamps differ by 10.038749 seconds; commands are sequential,
|
||||
so these are not atomic snapshots of a fixed amount of guest work.
|
||||
|
||||
| Observation | Value | Interpretation limit |
|
||||
| --- | --- | --- |
|
||||
| Overlay native dispatch delta | 0 | Does not include statically compiled game execution |
|
||||
| Overlay interpreter fallback delta | 33,702 | Dispatch events, not CPU-time share |
|
||||
| Interpreted instruction delta | 425,410 | Some kernel fallback is expected |
|
||||
| Interpreter block delta | 33,714 | No claim that reducing calls changes the primary bottleneck |
|
||||
| Compilation runs/configured | 0 / 0 | Cache/compilation provenance differs from a prepared release campaign |
|
||||
| Rolling phase samples: interpreter / static / GPU | 1.59% / 80.11% / 18.30% | Instrumented phase stamps, not an independent host CPU profiler |
|
||||
|
||||
This observation rejects the inference that zero overlay-native dispatches
|
||||
alone prove interpretation dominates the slowdown. It does not reject overlay
|
||||
optimization as a candidate. No controlled compiler/process/power-state audit
|
||||
was performed during this exploratory sample.
|
||||
|
||||
Separately queried `frame_perf` reported approximately 16.68 ms total and
|
||||
16.65 ms `emu_cpu_ms_avg`. The latter is calculated from total wall time minus
|
||||
present wall time, not OS thread CPU time. GPU queries span command-stream
|
||||
intervals; do not interpret these fields as independent exclusive CPU/GPU busy
|
||||
time or add their values together. Run without timer-query profiling and obtain
|
||||
host-thread CPU samples before choosing an optimization from these values.
|
||||
|
||||
## HOST-BUDGET-001: verify the throttle denominator
|
||||
|
||||
Date: 2026-09-04 (session date). Verdict: launcher calibration, not an accepted
|
||||
runtime optimization or a substitute for low-end hardware.
|
||||
|
||||
On the same 16-logical-CPU host, a single Python process busy-looped until
|
||||
`time.process_time()` advanced by 2.5 seconds. Every run used affinity mask
|
||||
`0x1`. The game was stopped. The sequence below was a calibration sequence,
|
||||
not an interleaved optimization campaign. Reports live in
|
||||
`.local/host-calibration/{unrestricted,core50,core25}.json`.
|
||||
|
||||
| Requested budget | Applied Job CpuRate (out of 10,000) | Wall seconds | Process CPU seconds |
|
||||
| --- | --- | --- | --- |
|
||||
| Unrestricted | none | 2.580994 | 2.500000 |
|
||||
| 50% of one logical CPU | 313 | 4.679147 | 2.531250 |
|
||||
| 25% of one logical CPU | 157 | 9.515031 | 2.500000 |
|
||||
|
||||
`--cpu-percent` is a percentage of the whole host's CPU scheduling budget.
|
||||
`--core-percent` normalizes to one logical CPU equivalent independently of
|
||||
affinity; it limits the whole launched job, including helper threads/processes.
|
||||
This does not selectively slow the emulation thread, lower CPU frequency, or
|
||||
simulate an older GPU/cache/memory subsystem. Reports retain requested and
|
||||
applied rates, including integer rounding.
|
||||
|
||||
An earlier 0.5-CPU-second witness did not demonstrate effective throttling even
|
||||
at a 3% host budget. Longer witnesses did, so sub-second smoke timing is not a
|
||||
valid cap calibration. Repeat calibration on each host and use sustained game
|
||||
windows rather than assuming the scheduler's short-term behavior is linear.
|
||||
|
||||
## ROUTE-SMOKE-001: live runner protocol validation
|
||||
|
||||
Date: 2026-09-04 (session date). Verdict: successful diagnostic smoke only.
|
||||
No optimization candidate, target-provenance manifest, or acceptance comparison.
|
||||
|
||||
Used the isolated executable/state from CRAB-DISCOVERY-001, OpenGL, no host cap,
|
||||
`PSX_RUNTIME_PERF_DIAG=1`, and explicitly `PSX_GL_PERF=0`. A local shortened
|
||||
route held neutral input for 120 ticks. The runner confirmed completed slot-1
|
||||
restore (generation 1, `last_ok=1`) and route completion (`active=false`, one
|
||||
step consumed, `remaining=0`). Raw evidence:
|
||||
`.local/crabby-baseline/runner-smoke.json`.
|
||||
|
||||
- Route wall time: approximately 2.141 seconds, including setup/status overhead.
|
||||
- Counter observation window: approximately 2.672 seconds and 161 frame ticks,
|
||||
including sequential snapshots/control traffic, not exactly 120 guest ticks.
|
||||
- GL frame timing correctly reported unavailable with timer queries disabled.
|
||||
- No native overlay compilation was configured; cache provenance remains
|
||||
unsuitable for a prepared-release acceptance baseline.
|
||||
|
||||
The process exited after the smoke. Original saves/cards were not modified.
|
||||
The committed 7,200-tick route is a longer stationary workload manifest; this
|
||||
smoke does not validate its full-duration timing or the analog approach route.
|
||||
See [tool usage and remaining gates](TOOLS.md).
|
||||
|
||||
## MEMORY-TRACE-001: remove empty production write-trace calls
|
||||
|
||||
Dates: 2026-09-04 to 2026-09-05. Verdict: retain as a narrowly scoped dead-work
|
||||
simplification. Local write benchmark improves; gameplay gain is unproven.
|
||||
This does not complete the campaign or establish the cause of the beach slowdown.
|
||||
|
||||
### Change and correctness evidence
|
||||
|
||||
Guard the six RAM/scratchpad byte, halfword and word calls to
|
||||
`debug_server_trace_write_check` with the existing `PSX_NO_DEBUG_TOOLS` switch.
|
||||
That function already returns immediately in production. This also removes its
|
||||
old-value argument loads. Debug builds keep the original calls. No functional
|
||||
memory observer, store, cycle charge, MMIO path, runtime setting, ABI or state
|
||||
layout changes. Separate `-O2 -fno-lto` object inspection found the baseline's
|
||||
undefined trace symbol and confirmed its absence from the candidate object.
|
||||
|
||||
`runtime/tests/test_memory_debug_trace_guard.py` compiles actual `memory.c`
|
||||
against real runtime headers, with external device/observer stubs. It checks
|
||||
all six writes through four RAM mirrors and scratchpad aliases, expected bytes
|
||||
and adjacent sentinels, observer/store counts, and production/debug trace-call
|
||||
witnesses. Optional `--baseline-memory` also checks the old production source.
|
||||
The compact state hash covers touched bytes and selected counters, not complete
|
||||
guest/device state. Structural checks preserve functional RAM observer order.
|
||||
An earlier synthetic store model and then a fake-header fixture were rejected
|
||||
during review; neither is used by the committed test.
|
||||
|
||||
### Build provenance
|
||||
|
||||
Framework baseline is `8ec6498d323a7abd04a3058afcbb5e36e62a8ec3`; candidate differs
|
||||
only by the six guards in `runtime/src/memory.c`. Source SHA256:
|
||||
|
||||
- Baseline: `693be507fa53fded3e8e276f4ac0f7293d3879f3d53a8044fa8c383b4e097b99`.
|
||||
- Candidate: `4fdb391d4f74da1ef2d31ce7cd61a0e0b958abab8304da37d83a2c1bb0e85253`.
|
||||
|
||||
Both title executables were rebuilt, using the same copied Ape Escape generated
|
||||
code and assets from `_wt-ape-crabperf-20260901`. Provenance trees/build logs are
|
||||
retained under `.local/ape-build-provenance`. CMake Release uses MinGW GCC/G++
|
||||
15.2.0 from `C:/msys64/mingw64/bin`, `-O3 -DNDEBUG`, debug tools OFF, static runtime ON,
|
||||
SDL3, recomp UI and rewind ON, netplay OFF, and block cycles ON. Vulkan was
|
||||
enabled in the build but was not the selected/tested renderer. The first build
|
||||
failed with the wrong windres shim; successful builds explicitly selected
|
||||
`C:/msys64/mingw64/bin/windres.exe`.
|
||||
|
||||
- A executable SHA256: `f0c893a5743d8dfddcde75fe275d525c99c66294c5333fdf48b797de0b21096e`.
|
||||
- B executable SHA256: `6fd00dc4ed8a7e9cf66f1bdd64ebaafd376d867dd5687c126bcd56f8e03dbf12`.
|
||||
- A successful build-log SHA256: `c9a042d2ccab65d248a9b91b0b74d1106fc80fe74f611ea5eccfb27f350ad252`.
|
||||
- B successful build-log SHA256: `3d5326d5fa5de696cb02fd3c021a2336b92362c04fcfc3f00a560a79ec34ad8e`.
|
||||
|
||||
The generated BIOS warned that it was stale against the current emitter
|
||||
fingerprint. It was deliberately identical in A/B to retain compatibility with
|
||||
the available v5 checkpoint. No native overlay compilation/loading was
|
||||
configured. These are controlled local integration artifacts, **not** clean
|
||||
release builds or qualification of regenerated code and shipped overlay caches.
|
||||
|
||||
### Local write benchmark
|
||||
|
||||
Actual memory implementation, external device stubs, and a genuinely empty
|
||||
trace function in a separate translation unit; GCC C11 `-O2 -fno-lto`, production
|
||||
macro enabled. Each run executes 50 million loops of six writes. Both artifacts
|
||||
warm up with one million loops before measured A1/B1/A2/B2/A3/B3 order. Host is
|
||||
the Ryzen 7 9800X3D, High Performance power plan, affinity `0x1`, no CPU cap.
|
||||
Primary metric is launch-to-completion wall seconds, lower is better.
|
||||
|
||||
| Pair | A seconds | B seconds |
|
||||
| --- | --- | --- |
|
||||
| 1 | 1.4162425 | 1.1504406 |
|
||||
| 2 | 1.4181166 | 1.1467513 |
|
||||
| 3 | 1.4099258 | 1.1160858 |
|
||||
| Minimum | 1.4099258 | 1.1160858 |
|
||||
| Median | 1.4162425 | 1.1467513 |
|
||||
|
||||
Median improvement: 19.03%; minimum improvement: 20.84%. All six runs produced
|
||||
identical selected state/counters: `state=dcc19de5`, 300 million stores,
|
||||
150 million parity calls, 150 million card checks, zero trace calls. This
|
||||
workload emphasizes write-call overhead and does not predict gameplay gain.
|
||||
The compiler-process monitor reported no observed compilers; thermal/frequency
|
||||
telemetry was not collected.
|
||||
|
||||
### Crabby Beach OpenGL comparison
|
||||
|
||||
Same v5 state as CRAB-DISCOVERY-001, staged copies of saves/cards/settings.
|
||||
OpenGL context reported NVIDIA 3.3.0 driver 610.74, 2x SSAA and antialiasing,
|
||||
165 Hz display, wall-clock 59.94 Hz pacing, driver vsync OFF. Both uncapped
|
||||
smokes sustained approximately 60 guest Hz with no reported audio underruns.
|
||||
A screenshot outside the acceptance runs confirmed the water-facing beach
|
||||
scene. Smoke timings are excluded: they are not three pairs and B's screenshot
|
||||
overlapped its smoke window.
|
||||
|
||||
Measured A/B uses `PSX_LOAD_SLOT=1`, `PSX_RUNTIME_PERF_DIAG=1`, `PSX_GL_PERF=0`,
|
||||
`PSX_BENCH_WINDOW=120:420`. Primary metric is the runtime's exact 300-frame
|
||||
`BENCH wall_ms`, not launcher lifespan. Every run uses a 25%-of-one-logical-core
|
||||
Job budget (raw CpuRate 157/10000) and affinity `0xFFFF`, limiting the entire
|
||||
job. Every process is intentionally stopped after 35 seconds. Timed windows
|
||||
complete before timeout. This scheduling cap is not low-end hardware emulation.
|
||||
|
||||
| Pair | A wall ms | B wall ms |
|
||||
| --- | --- | --- |
|
||||
| 1 | 8458.662 | 8588.874 |
|
||||
| 2 | 8312.503 | 8274.390 |
|
||||
| 3 | 8341.652 | 7946.127 |
|
||||
| Minimum | 8312.503 | 7946.127 |
|
||||
| Median | 8341.652 | 8274.390 |
|
||||
|
||||
Median improvement: **0.81%, within observed noise**; minimum improvement:
|
||||
4.41%. No convincing scene-level gain. Each window reports 180,114 interpreted
|
||||
instructions, 15,564 fallback dispatches, and zero overlay loads, invalidations,
|
||||
or captures. Those matching counters are not full-state parity. Under the
|
||||
severe budget both builds slow down and underflow host audio. No compilers or
|
||||
monitor errors were observed; configuration/mod hashes remained unchanged.
|
||||
No screenshots were taken during these six runs. No runs were discarded.
|
||||
|
||||
### Retained evidence and remaining gates
|
||||
|
||||
All following paths are relative to the worktree and are intentionally ignored:
|
||||
|
||||
| Artifact | SHA256 |
|
||||
| --- | --- |
|
||||
| `.local/memory-trace-bench/campaign.json` | `fd1944a1fc3a8ea750e37ceda9fb12367d9bb85adc8f12d01fdc28df3a3b06ad` |
|
||||
| `.local/game-memory-ab/campaign.json` | `9d0c70ae9b9d221bc601e24cf7563dcdc61f84fecbae97806e7a7d068d8b5e1d` |
|
||||
| `.local/run_memory_bench.py` | `90f6fec928f33f3d5e6395d31ce25d7f86ba16359d79851dd8903ef9ef48057d` |
|
||||
| `.local/run_game_ab.py` | `e3e9fd310600c3dbde14d4b2b4fff741cee2a063c14bbfc9ffe7f8416f71bff9` |
|
||||
|
||||
JSON retains per-run evidence and artifact/configuration hashes; game/state
|
||||
assets are not distributed. The focused compiled regression passes, as do the
|
||||
FMV quiet and dirty-text admission/continuation guard tests. The combined
|
||||
campaign/host-launcher/stall-report suite passes all 63 tests, and CMake's
|
||||
registration guard finds all 120 runtime/recompiler test files registered.
|
||||
These are not a full runtime CTest pass or cross-platform validation. Two unrelated
|
||||
structural tests also fail on the immutable baseline; details are in
|
||||
[the audit](RUNTIME_AUDIT.md#validation-gaps).
|
||||
|
||||
Retain the change only as removal of already-empty production work, under the
|
||||
contract's simplification allowance, not as a 5% gameplay optimization. Full
|
||||
checkpoint parity, multi-title/low-end hardware, moving/transition routes,
|
||||
long-window frame consistency, clean-install and cross-platform release gates
|
||||
remain open. Broader GPU/audio history and GL costs remain separate candidates;
|
||||
the flat gameplay result does not justify focusing the campaign on memory.
|
||||
|
||||
## PACER-COST-001: attribute idle pacing CPU cost
|
||||
|
||||
Date: 2026-09-05. Verdict: baseline attribution only, no pacing change or
|
||||
gameplay speed claim. Compile actual `frame_pacing.c` and `host_time.c` against
|
||||
SDL3 with GCC 15.2.0 `-O2 -DPSX_SDL3`. The driver calls `frame_pacer_wait` 600
|
||||
times at 59.94 Hz with no guest work, using `GetThreadTimes` for calling-thread
|
||||
CPU time and SDL's performance counter for wall time. The first call anchors
|
||||
without waiting, so 599 periods take approximately 9.9933 seconds.
|
||||
|
||||
| Run | Wall seconds | Thread CPU seconds |
|
||||
| --- | --- | --- |
|
||||
| 1 | 9.993298 | 0.812500 |
|
||||
| 2 | 9.993297 | 0.859375 |
|
||||
| 3 | 9.993297 | 0.765625 |
|
||||
|
||||
This is approximately 7.7-8.6% of one logical CPU spent on idle pacing on the
|
||||
Ryzen 7 9800X3D, High Performance plan, unrestricted scheduling and inherited
|
||||
affinity. The existing sleep decision stops sleeping below two milliseconds,
|
||||
then spins until the deadline. This local cost makes the spin tail worth
|
||||
measuring in gameplay; it does not establish an actual pacer bug or explain
|
||||
exclusive wall-time phase attribution under a Job cap. No candidate was tested.
|
||||
Agents held compilation during these runs; no continuous compiler-process or
|
||||
thermal monitor was attached to this exploratory probe.
|
||||
|
||||
Local retained driver `.local/pacer-cost.c`, SHA256
|
||||
`d98305437fb4e8c86769f31893e43bf63ade8568d761f522e0842817a92ac105`;
|
||||
executable `.local/pacer-cost.exe`, SHA256
|
||||
`7aa80698ca5d3a831d0d651601b3c5b2e8bc3665e2eec82f5e426c308eae7ddb`;
|
||||
raw output `.local/pacer-cost-results.txt`, SHA256
|
||||
`749fc5185fa512e3a00390f0022fa8939993763ca3b9f6e123ba76ff6654fea1`.
|
||||
|
||||
## HISTORY-002: isolated GPU and audio history candidates
|
||||
|
||||
Date: 2026-09-05. Correctness milestone; controlled resource/time comparison
|
||||
pending. No gameplay gain accepted from the correctness runs below.
|
||||
|
||||
### Candidate boundaries
|
||||
|
||||
- `GP0-HISTORY-001`: compile the GP0 command-history allocation, recording and
|
||||
diagnostic guest-stack scan only when debug tools are enabled. Production
|
||||
history accessors remain linkable but report zero capacity/count and empty
|
||||
dumps. Keep source tracking, opcode counters, WS census, functional overhang
|
||||
detection, and all command execution unchanged. Removes a 104 MiB allocation
|
||||
request; debug builds retain always-on history.
|
||||
- `AUDIO-PCM-001`: compile out four PCM history arrays and their writes in
|
||||
`PSX_NO_DEBUG_TOOLS` builds. Preserve every tap's head, nonzero/audible/peak
|
||||
statistics, rates, event records and pump/underrun/mute counters. Production
|
||||
WAV dump returns the existing error result without opening a file; debug
|
||||
WAV history is unchanged. Actual GCC object BSS is 83,886,272 bytes with debug
|
||||
history and 16,777,408 without it: exactly 64 MiB less backing capacity.
|
||||
|
||||
Both use the existing build switch, not a new runtime selector or lazy allocator.
|
||||
Capacity is not equivalent to resident memory or a measured speed improvement.
|
||||
Packed production audio tap counters may share cache lines between callback and
|
||||
emulation threads; require the real bridge-audio path in performance checks.
|
||||
|
||||
### Focused checks
|
||||
|
||||
The GPU test compiles actual `gpu.c` and real headers with external renderer
|
||||
stubs. It compares exact serialized GPU snapshots and the entire VRAM at eleven
|
||||
checkpoints across environment, fill, upload, copy, polyline and NOP commands.
|
||||
It also checks debug history count, copy source/stack pointer and polyline
|
||||
header, and production unavailability. This tests parser/state behavior, not
|
||||
the rendering correctness of a stubbed backend. Known pre-existing polyline
|
||||
source attribution was not changed or silently redefined by the test.
|
||||
|
||||
The audio test compiles actual `audio_trace.c` in both modes. All four taps'
|
||||
observed statistics/events compare equally, including a new event after ring
|
||||
wrap, signed extremes, audible thresholds, rates and resets. Debug WAV headers
|
||||
and sample bytes are checked for all taps and for wrap/clamped eviction;
|
||||
production must not create the requested WAV. Review rejected the first test's
|
||||
hard-coded output comparison and replaced it with real observations.
|
||||
|
||||
Both focused tests passed root execution. The 63 campaign/host/stall-report
|
||||
tests passed, as did the memory-trace regression. CMake's registration guard
|
||||
finds 122 test files registered. This is not a full cross-platform CTest pass.
|
||||
|
||||
### Isolated build provenance
|
||||
|
||||
B is the previous memory-guard baseline, C adds only the GP0 gate, and D adds
|
||||
only the audio gate. All use identical local-only endpoint memory/restore/
|
||||
renderer/audio witnesses and an optional checkpoint probe. Original B from
|
||||
MEMORY-TRACE-001 is preserved. Source trees, exact options/commands/logs and
|
||||
hashes are in `.local/ape-build-provenance/builds-bcd-manifest.json`, SHA256
|
||||
`176ed3d54939fab3498ced5d1e3dd38566a9ef76e7e4cb23f5514a8d9a740530`.
|
||||
|
||||
| Variant | Executable SHA256 |
|
||||
| --- | --- |
|
||||
| B metrics | `f1310b19eb3633d81e01717f36ab60f4d6cd4d53aa3838adf0e614d10bbf4914` |
|
||||
| C GP0 | `74063d9dadef152d023c0dead18eabde854773c4399f7f52f49713fb67315045` |
|
||||
| D PCM | `e9f525296dfdb0be2a50596a1198bb9735be0371d21afa627cc6e08dfe05bd39` |
|
||||
|
||||
Release GCC 15.2.0, SDL3, static runtime, debug tools OFF, rewind ON, netplay OFF,
|
||||
same donor generated game/BIOS and saved scene as MEMORY-TRACE-001. The stale
|
||||
generated-BIOS warning persists. A default all-target build failed in an
|
||||
unrelated donor title test on a long Windows dependency-file path; explicit
|
||||
`psx-runtime` builds succeeded. No release qualification is claimed.
|
||||
|
||||
### Real OpenGL checkpoint comparison
|
||||
|
||||
Separate uncapped 20-second processes restore the same slot-1 v5 beach state.
|
||||
All variants report active OpenGL (`gl_active=1`, `gr_backend=1`), completed
|
||||
restore (`generation=1,pending=0,last_ok=1,last_op=load,last_slot=1`), and active
|
||||
non-legacy bridge output with advancing host-tap frames. The local-only probe
|
||||
writes raw boot-state blobs at frames 120, 121 and 420, plus CPU timing sidecars.
|
||||
For **both C and D versus B, every blob and sidecar is byte-identical**.
|
||||
|
||||
| Frame | Raw guest snapshot SHA256 (all B/C/D) |
|
||||
| --- | --- |
|
||||
| 120 | `505f7d94c5c7e518e099669ed1623296d9210abc30eab4bc907956e91ff652c6` |
|
||||
| 121 | `1e0b19c3a913733aba9bb403b1e01a1c9d309cc919e3accd9c52f3d341d3429b` |
|
||||
| 420 | `55ebbe6a36ac2674952a4672575dfb619547ffec0a059f9a277bb955b9d074e2` |
|
||||
|
||||
Each blob is 3,685,569 bytes. The existing save format covers CPU registers,
|
||||
RAM, scratchpad, VRAM, SPU RAM and serialized device/IRQ/clock/cache/dirty state.
|
||||
The 411-byte sidecars add multiply/divide and GTE completion timestamps, all 33
|
||||
read-absorb bytes and load-timing scalars. CPU function pointers, host audio
|
||||
queues, SDL/pacer/presentation histories and diagnostics are excluded. This is
|
||||
one scene's checkpoint evidence, not exhaustive state/restore or multi-title
|
||||
qualification. Full campaign correctness gates remain open.
|
||||
|
||||
Evidence: `.local/history-correctness-002/report.json`, SHA256
|
||||
`7feb56b0f0119688e460ff46b027d3aaf9549021e996a935830e26a6154b21f2`;
|
||||
driver `.local/run_history_checks.py`, SHA256
|
||||
`f6f29b505225fc0ff8688d9a86921be60590a81288980c5fd7d4c8b56961d0f4`.
|
||||
Raw game-derived snapshots remain local and uncommitted.
|
||||
|
||||
The first attempt, `.local/history-correctness-001`, failed closed because the
|
||||
staged checkpoint hook was absent despite an earlier build report. Explicit
|
||||
source and binary-string checks plus forced rebuilds corrected the artifact.
|
||||
That failed attempt is retained, not counted as successful state comparison.
|
||||
|
||||
Checkpoint saves can publish deferred cycles and force GL readback. These runs
|
||||
also overlapped unrelated builds, so **none of their timing or memory values is
|
||||
acceptance evidence**. The timed runner explicitly unsets checkpoint capture.
|
||||
The preregistered primary metric is exact-frame-420 process PrivateUsage; the
|
||||
secondary metric is frames-120:420 wall time, with three interleaved pairs for
|
||||
each independent candidate and a validated 25%-one-core Job cap. It requires
|
||||
stable input/artifact hashes, active OpenGL/audio and completed restore, and
|
||||
aborts on compiler contamination or missing/nonfinite metrics.
|
||||
|
||||
### Timing gate outcome and disposition
|
||||
|
||||
The benchmark agent monitored the host for ten minutes without finding 30
|
||||
consecutive seconds free of compiler/benchmark activity. Concurrent SNES builds
|
||||
included Mega Man X and Super Mario Kart. No timed candidate process was
|
||||
launched and `.local/game-gp0-memory-ab` was not created. Unrelated work was
|
||||
not stopped. The power plan remained High performance before and after;
|
||||
thermal state was not measured.
|
||||
|
||||
The prepared twelve-run driver is `.local/run_game_gpu_memory_ab.py`, SHA256
|
||||
`ce01b28c627dd5a8d0abe3217a49e2fd1d576fc7128153de502487f69a96f64b`.
|
||||
It includes `cc.exe` in compiler-contamination detection. Resume the paired
|
||||
campaign in a quiet host window before making retention or speedup claims.
|
||||
|
||||
Disposition: committed candidate implementation in the isolated performance
|
||||
worktree, **pending performance acceptance**, not release qualification. The
|
||||
104 MiB GP0 allocation request and 64 MiB PCM static-storage capacity removals
|
||||
are established; resident-memory savings and gameplay speedups are not.
|
||||
Focused parity tests and real OpenGL checkpoints passed, but the broader
|
||||
correctness matrix and the quiet-host comparisons remain open.
|
||||
|
||||
## UNCAP-HISTORY-003: uncapped OpenGL throughput
|
||||
|
||||
Date: 2026-09-05. User-requested follow-up to HISTORY-002. Uncapped frame
|
||||
throughput removes a limiter that can hide spare capacity; it does not isolate
|
||||
the process from concurrent host work or simulate lower-end hardware.
|
||||
|
||||
### Mechanism and provenance
|
||||
|
||||
`PSX_VSYNC=0` alone leaves the wall-clock pacer active. Debug/manual/load turbo
|
||||
can skip presentation, and Ape's historical `ape.framerate.uncapped` plugin
|
||||
actually selects frame interpolation. None is appropriate here. The native
|
||||
VBlank-rate API already permits zero frontend pacing without changing guest
|
||||
cycle accounting (`mod_plugins.h`, `psx_mod_set_native_vblank_rate`).
|
||||
|
||||
Identical local-only staged `main.cpp` hooks set `g_frame_period_ms=0` and
|
||||
`g_video_vsync=0` after host-refresh selection and before GL context creation
|
||||
when `PSX_PERF_UNCAPPED=1`. These are the same host-pacing effects as the native
|
||||
API; the hook does not call the API or change guest clocks. It refuses active
|
||||
interpolation. A separate endpoint `[BENCH_PACING]` reports actual period,
|
||||
effective swap interval, wall-pacing decision, interpolation and load-turbo
|
||||
state. Normal presentation and the audio bridge remain enabled.
|
||||
|
||||
The three incremental Release `psx-runtime` builds succeeded. Original paced
|
||||
executables and the previous manifest remain unchanged; staged original
|
||||
`main.cpp` files are backed up. Tracked runtime sources were not changed for
|
||||
this experiment. New manifest:
|
||||
`.local/ape-build-provenance/uncapped-builds-manifest.json`, SHA256
|
||||
`184dfa8a1d0305fc9f1d5aafdd6dda9cec4d0d24b8a73bf684775eefa7103ca4`.
|
||||
|
||||
| Variant | Uncapped executable SHA256 |
|
||||
| --- | --- |
|
||||
| B baseline | `8050206bda873fd42a8cc790c0b9b556680cdcb966893a1414d3774bbbf11dd6` |
|
||||
| C GP0 gate | `a43233c44f9c6f6f1a8b7115bc77260de0292571c556e44a5afd97bb4b00d042` |
|
||||
| D PCM gate | `566297db442e5e062b2b90864302a20d4cbdbe769496d6cdf7d2560089aff5f3` |
|
||||
|
||||
### Uncapped correctness smoke
|
||||
|
||||
Root ran separate 12-second uncapped processes with checkpoint capture enabled.
|
||||
All B/C/D raw guest snapshots and CPU sidecars at frames 120, 121 and 420 are
|
||||
byte-identical to one another **and to the paced B checkpoints in HISTORY-002**.
|
||||
All restore, OpenGL and bridge-audio witnesses passed. Endpoint pacing reports
|
||||
zero period, swap interval, wall pacing, interpolation and load turbo. These
|
||||
checkpoint runs are not timing or memory acceptance measurements.
|
||||
|
||||
Evidence: `.local/uncapped-correctness-001/report.json`, SHA256
|
||||
`551732b3411c74a113d14edf46f185d364684863ec199d223f3b444ee9474709`;
|
||||
driver `.local/check_uncapped_history.py`, SHA256
|
||||
`8acb9e3894286237cfbba6b3f3e468c5ff3698c9762c2de488164fa151bd3e91`.
|
||||
The existing save-format coverage/exclusions from HISTORY-002 still apply.
|
||||
|
||||
### Timed protocol
|
||||
|
||||
Three interleaved B/C and B/D pairs, reversing within-pair order for pair two:
|
||||
`B,C,B,D; C,B,D,B; B,C,B,D`. Each process restores the same beach save and runs
|
||||
with unrestricted requested host CPU budget, affinity `0xFFFF`, the same
|
||||
OpenGL 2x SSAA/AA settings and diagnostic counters, and no checkpoint capture.
|
||||
Primary metrics are wall time for guest frames 120:420 and guest VBlanks/s
|
||||
(`300000 / wall_ms`), not unique rendered game frames or physical display FPS.
|
||||
PrivateUsage at frame 420 is secondary. Twelve-second process timeouts provide
|
||||
bounded cleanup; whole-process CPU totals are not the fixed-window CPU cost.
|
||||
|
||||
The driver requires unchanged artifacts/settings/game/state/mod inventories,
|
||||
completed restore, active OpenGL/audio, effective uncapped pacing and matching
|
||||
guest-work counters. Compiler observations are retained instead of waiting for
|
||||
a quiet host; monitor failures invalidate results. No result is automatically
|
||||
accepted based only on the absence of observed compilers. Eight synthetic
|
||||
driver checks passed root execution.
|
||||
|
||||
Local driver `.local/run_uncapped_history_ab.py`, SHA256
|
||||
`ab3fd3efd56b3088a11221d907a5202101dbfff21fb19186b647cef97bf7c8d3`;
|
||||
tests `.local/test_run_uncapped_history_ab.py`, SHA256
|
||||
`3a4b19138e1272490534b3bea84099e32a856d9d922c368fa9f85329abf23044`.
|
||||
|
||||
Uncapped audio production outruns the physical playback timeline. An active
|
||||
audio device and advancing host tap do not establish identical callback counts,
|
||||
absence of overflow or normal-speed listening quality. Neither title-wide
|
||||
correctness nor low-end realtime pacing/audio acceptance follows from this
|
||||
throughput experiment.
|
||||
|
||||
### Short-window results
|
||||
|
||||
All twelve runs passed the fixed-window, effective-pacing, restore, backend,
|
||||
audio and inventory checks. No monitored compiler processes or monitor errors
|
||||
were observed. High performance power plan before/after; no thermal telemetry
|
||||
or continuous monitoring of all other host workloads. Classification remains
|
||||
`exploratory_no_compiler_observed`, not automatic acceptance.
|
||||
|
||||
| Comparison | Baseline wall ms (three runs) | Candidate wall ms (three runs) | Median VBlanks/s B -> candidate | Median throughput change |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| GP0 gate | 1205.440, 1247.664, 1181.284 | 1178.411, 1181.626, 1177.182 | 248.872 -> 254.580 | +2.29% |
|
||||
| PCM gate | 1212.670, 1185.505, 1215.057 | 1165.711, 1208.837, 1179.628 | 247.388 -> 254.317 | +2.80% |
|
||||
|
||||
Within-pair throughput changes were +2.29%, +5.59%, +0.35% for GP0 and +4.03%,
|
||||
-1.93%, +3.00% for PCM. This spread, especially the PCM sign reversal, prevents
|
||||
treating the modest median gains as robust. Fastest-run throughput changes were
|
||||
only +0.35% and +1.70%, respectively. No additive combined gain is established:
|
||||
each candidate was tested independently.
|
||||
|
||||
Endpoint PrivateUsage fell in every pair: GP0 by 102.87, 103.63 and 105.49 MiB;
|
||||
PCM by 62.00, 63.31 and 64.07 MiB. This measures process private commit in this
|
||||
scene, not resident physical memory. The amount includes allocator/driver
|
||||
variation and is not an exact measurement of the removed arrays alone.
|
||||
|
||||
Raw report `.local/game-uncapped-history-ab/campaign.json`, SHA256
|
||||
`5e6477c3969eb8799aee300ffa153903d39c48010a814be76f2cfc19413afb4d`;
|
||||
power witness `.local/game-uncapped-history-ab/power_plan.json`, SHA256
|
||||
`034c141dd4dd57cdbd10819bf884aa01c628e1fb496dc1bee83979aa52a55da4`.
|
||||
All stdout/stderr and host reports are retained beside the report. Root
|
||||
recomputed paired throughput and memory deltas from the raw rows.
|
||||
|
||||
Because the measured windows last only about 1.2 seconds, a separate replication
|
||||
uses frames 120:1920 (1,800 guest VBlanks), identical executables/settings and
|
||||
the same twelve-run order. This extends the workload past the GP0 history ring's
|
||||
wrap point without changing game speed settings or rendering quality. It uses
|
||||
a separately named driver/output directory and 15-second process timeouts;
|
||||
the short-window artifacts are preserved.
|
||||
|
||||
### Longer-window replication
|
||||
|
||||
All twelve runs passed the same evidence checks at frames 120:1920. B/D GP0
|
||||
history totals reached 1,180,535 commands, beyond the 1,048,576-entry capacity;
|
||||
C remained zero/zero. Work counters matched within each candidate comparison.
|
||||
This is a workload/wrap witness, not a new byte-level checkpoint at frame 1920.
|
||||
|
||||
| Comparison | Baseline wall ms (three runs) | Candidate wall ms (three runs) | Median VBlanks/s B -> candidate | Median throughput change |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| GP0 gate | 7102.496, 7222.937, 7202.571 | 7108.128, 7020.108, 6990.751 | 249.911 -> 256.406 | +2.60% |
|
||||
| PCM gate | 7295.579, 7127.430, 7366.212 | 6982.187, 7070.005, 6977.142 | 246.725 -> 257.799 | +4.49% |
|
||||
|
||||
Within-pair throughput changes: GP0 -0.08%, +2.89%, +3.03%; PCM +4.49%,
|
||||
+0.81%, +5.58%. Fastest-run throughput changes: +1.60% and +2.15%.
|
||||
PrivateUsage fell in every pair: GP0 by 105.18, 106.13 and 106.69 MiB;
|
||||
PCM by 63.12, 61.83 and 63.25 MiB.
|
||||
|
||||
Compiler activity was observed in two runs: GP0 pair-one C (32 observations)
|
||||
and PCM pair-one B (30). Observed names included `cc.exe`, `cc1.exe`,
|
||||
`cmake.exe`, `collect2.exe`, `g++.exe`, `gcc.exe`, `ld.exe` and `ninja.exe`.
|
||||
The monitor covers the full process lifetime, not precisely the measured
|
||||
frame window. No monitor errors occurred. All runs remain in the report;
|
||||
no post-hoc removal of the slower/contaminated runs was used. Classification:
|
||||
`exploratory_contaminated`. High performance power plan before/after;
|
||||
thermal state was not measured.
|
||||
|
||||
Evidence:
|
||||
|
||||
- `.local/run_uncapped_history_long_ab.py`, SHA256
|
||||
`459c852c71fd0f4c927a331a91867566704e1fb811b4f2be38fb1a55db6a19d9`.
|
||||
- `.local/game-uncapped-history-long-ab/campaign.json`, SHA256
|
||||
`f71891ad99b20ab5bb1f2e148931494c90951f713d25c8ddd6d4107b436007e7`.
|
||||
- `.local/game-uncapped-history-long-ab/power_plan.json`, SHA256
|
||||
`7f115cc8e9fa6998f4e01f557098113881fbe2b1264f0434060fd0621a7179a0`.
|
||||
|
||||
Root reviewed the longer driver's delta: only window/frame-count/FPS numerator,
|
||||
output path, metric labels and timeout differ from the short driver. Synthetic
|
||||
checks accepted a correctly converted 1,800-frame observation and rejected the
|
||||
old 300-frame observation. Root recomputed paired changes from raw results.
|
||||
|
||||
### Disposition
|
||||
|
||||
Uncapped benchmarking is useful here: it exposes approximately 250 guest
|
||||
VBlanks/s of throughput instead of hiding spare capacity behind the 60 Hz
|
||||
limiter. Both passes suggest modest gains, but run variation and the longer
|
||||
pass's contamination prevent claiming a reliable general speedup. These are
|
||||
not additive gains or evidence that Crabby Beach slowdowns are fixed.
|
||||
|
||||
Reduced process private commit is consistent across all twelve candidate pairs.
|
||||
Together with focused parity and the three exact guest checkpoints, this
|
||||
supports retaining the candidates in the isolated burndown branch for further
|
||||
qualification. Normal-speed audio/pacing, constrained-host comparisons,
|
||||
multi-title correctness and actual low-end hardware remain open. Do not ship
|
||||
the local uncap hook as a gameplay default.
|
||||
|
||||
## MANUAL-INTEGRATION-004: user sign-off and master integration
|
||||
|
||||
Date: 2026-09-05. The user reviewed the Tomba 2 and Mega Man X6 manual builds,
|
||||
reported that they "look good", and explicitly authorized committing and
|
||||
merging the work to main/master. This is user acceptance of the supplied test
|
||||
builds, not an instrumented audio comparison, full-title playthrough or a new
|
||||
performance measurement.
|
||||
|
||||
Both manual builds used the combined guards from `04371d60`, normal pacing,
|
||||
OpenGL, recomp-ui, production debug-tools OFF, rewind ON, existing generated
|
||||
game/BIOS code and isolated copied memory cards. Recomp-ui displayed verified
|
||||
discs with BIOS paths supplied. Final executable SHA256 values:
|
||||
|
||||
- Tomba 2: `6d978d8e2a736aa8b94cb076091117edbe76f6fa7c7f061df227233be8a648ad`.
|
||||
- Mega Man X6: `4c99e7a6092122b6675daf83f52eddb76a71b0b5290b474c7f4d494df84135b2`.
|
||||
|
||||
The initial Tomba build disabled rewind to bypass a dependency lookup; review
|
||||
rejected that workaround and the final build restored rewind. Mega Man X6's
|
||||
older recomp-ui headers were incompatible with the runtime, so its manual
|
||||
build used the compatible Tomba 2 recomp-ui checkout. Neither original game
|
||||
installation was edited. Local build logs/settings/screenshots remain under
|
||||
the original burndown worktree's `.local/manual-validation` directory.
|
||||
|
||||
Integration uses `origin/master` at `17f49ad3` as its base. Only the six campaign
|
||||
commits `6488414a`, `d38f97e1`, `8ec6498d`, `28e9c844`, `0cf93d57`, and
|
||||
`04371d60` were transplanted; the older review-branch ancestry was excluded.
|
||||
All six applied without conflicts. The runtime delta remains only the three
|
||||
production diagnostic guards; benchmark uncapping and local game artifacts
|
||||
are not part of the merge.
|
||||
|
||||
The first integration GPU test failed because its old polyline fixture supplied
|
||||
only one vertex before the terminator. Upstream now correctly requires two
|
||||
vertices. The fixture now supplies both; the upstream runtime fix is unchanged.
|
||||
On the integrated sources, all three actual-source guard regressions and the
|
||||
63 campaign/host/stall-report tests pass. The previously tested game executables
|
||||
are not rebuilds of this newer upstream base; no such equivalence is claimed.
|
||||
The CMake registration guard also passes with 133 test files registered.
|
||||
|
||||
Disposition: user-approved integration of the scoped optimizations, tests and
|
||||
measurement tools. The original WIP label records the historical state at
|
||||
implementation time. The broader burndown, general speedup qualification and
|
||||
actual low-end hardware coverage remain open after this merge.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Runtime Audit
|
||||
|
||||
Source audit for the runtime burndown. The matrix records the initial source
|
||||
disposition; follow-up implementation and qualification are noted below.
|
||||
The memory-trace candidate has been implemented and measured. See
|
||||
[MEMORY-TRACE-001](EXPERIMENTS.md#memory-trace-001-remove-empty-production-write-trace-calls)
|
||||
for results and limitations. Paths are repository-relative.
|
||||
|
||||
Follow-up: [HISTORY-002](EXPERIMENTS.md#history-002-isolated-gpu-and-audio-history-candidates)
|
||||
implements production-only GP0 history and audio PCM-storage gates. Focused
|
||||
tests and real OpenGL guest/CPU-timing checkpoints pass.
|
||||
[UNCAP-HISTORY-003](EXPERIMENTS.md#uncap-history-003-uncapped-opengl-throughput)
|
||||
adds uncapped throughput and process-private-commit measurements; normal-speed
|
||||
and low-end acceptance remain open. No shipped census, GL hold-copy, pacing,
|
||||
threading or Vulkan behavior has changed.
|
||||
[PACER-COST-001](EXPERIMENTS.md#pacer-cost-001-attribute-idle-pacing-cpu-cost)
|
||||
provides idle-pacer CPU attribution, not a speedup claim.
|
||||
|
||||
[MANUAL-INTEGRATION-004](EXPERIMENTS.md#manual-integration-004-user-sign-off-and-master-integration)
|
||||
records the user's Tomba 2/Mega Man X6 sign-off and approval to integrate the
|
||||
scoped guards. This does not close the wider performance/correctness campaign.
|
||||
|
||||
## Disposition Matrix
|
||||
|
||||
| Area | Current disposition | Evidence | Candidate / next check | Correctness guardrails |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Host CPU budget | Tooling calibration only, not an optimization. | `docs/performance/TOOLS.md:22-24` defines `--core-percent` as one logical CPU-equivalent, separate from affinity. Sustained calibration on this 16-logical-CPU host: unrestricted 2.580994 wall / 2.500000 CPU sec, core50 raw 313 -> 4.679147 wall / 2.531250 CPU sec, core25 raw 157 -> 9.515031 wall / 2.500000 CPU sec in `docs/performance/EXPERIMENTS.md:64-91`. | Use to expose CPU-limited behavior; do not treat as low-end hardware equivalence. | Does not change guest clocks, CPU frequency, or isolate the emulation thread. |
|
||||
| P1-1 audio observer | Active production observer; comment is stale. | `runtime/include/audio_trace.h:40-42` defines 4 taps. `runtime/src/audio_trace.c:17-36` stores 4 taps of `2^22` stereo frames: 64 MiB PCM, plus `EV_RING_CAP = 2^19` events at `runtime/src/audio_trace.c:20-54` (16 MiB by current struct layout). Header/source comments still describe 3 taps / 48 MiB. Hot writes are `audio_trace_pcm` / `audio_trace_event` at `runtime/src/audio_trace.c:92-126`; host-audio calls/events appear at `runtime/src/main.cpp:2071`, `runtime/src/main.cpp:3086-3166`, and `runtime/src/main.cpp:3195`. | `AUDIO-TRACE-001`: measure footprint and CPU cost, then gate or reduce trace only with an explicit diagnostic selector. | Preserve underrun/drop/mute observability and dump workflows; keep cheap health counters if full trace is gated. |
|
||||
| P1-1 GPU histories | Mixed active/lazy/toggleable observers. | GP0 ring lazy-allocates at `runtime/src/gpu.c:4859-4866` and records commands at `runtime/src/gpu.c:5058`, with public entry shape at `runtime/include/gpu.h:106-129`. A0/C0 histories are bounded at `runtime/src/gpu.c:4346-4414` and `runtime/src/gpu.c:4459-4494`. GL coherence/present rings are metadata observers at `runtime/include/gpu_gl_renderer.h:137-176`. | Measure before gating. Split active default costs from already-gated diagnostics. | These support route, selfcheck, widescreen, and renderer-debug workflows. |
|
||||
| P1-1 `CENSUS-001` WS census | Active by default in ordinary draw path; only debug server consumes/toggles it. | `ws_census_on = 1` at `runtime/src/gpu.c:4950`. Every drawing primitive can call `ws_census_record` from `runtime/src/gpu.c:5062-5066`; the early return is at `runtime/src/gpu.c:5006-5007`, but default-on records allocate a 48 MiB ring at `runtime/src/gpu.c:4937-4950` and read camera with two `psx_read_half` calls at `runtime/src/gpu.c:5017-5018`. The only `gpu_ws_census_*` consumer found is debug server on/off/dump/seq at `runtime/src/debug_server.c:8014-8030` and command registration at `runtime/src/debug_server.c:13540`. `psx_read_half` itself observes data-shard/load-store modes at `runtime/src/memory.c:1744-1753`, so changing census reads is not automatically behavior-neutral. | Candidate: default-off or debug-tool gate after proving no route/selfcheck/widescreen automation depends on default-on census data. | Preserve widescreen diagnosis and avoid changing lockstep/data-shard observer behavior in diagnostic modes. |
|
||||
| P1-1 / P2-5 `MEMORY-TRACE-001` | Implemented debug-tool guard, not a planned attribution probe. | `debug_server_trace_write_check` is declared at `runtime/src/memory.c:955-958` and implemented at `runtime/src/debug_server.c:9449`. Six hot write sites are already guarded by `#ifndef PSX_NO_DEBUG_TOOLS`: RAM/scratchpad word at `runtime/src/memory.c:1683-1685` and `runtime/src/memory.c:1718-1725`, half at `runtime/src/memory.c:1815-1817` and `runtime/src/memory.c:1849-1853`, byte at `runtime/src/memory.c:2154-2156` and `runtime/src/memory.c:2184-2187`. | Current candidate is validating this guard/removal surface, not inventing new instrumentation. | Do not change immediate write semantics, cycle accounting, parity last-writer, dirty-RAM, text/overlay watches, card-data checks, or MMIO ordering. |
|
||||
| P2-1/P2-2 CPU write-call path | Real native-game exercise exists; hotspot still needs samples. | Generated Ape Escape code calls through `cpu->write_word`, `cpu->write_half`, and `cpu->write_byte`, e.g. `.local/ape-build-provenance/ape-src/generated/SCUS_944.23_full_00.c:120`, `.local/ape-build-provenance/ape-src/generated/SCUS_944.23_full_00.c:421`, `.local/ape-build-provenance/ape-src/generated/SCUS_944.23_full_00.c:11843`. Runtime assigns these pointers to `psx_write_*` at `runtime/src/main.cpp:13843-13847`; implementations start at `runtime/src/memory.c:1585`, `runtime/src/memory.c:1791`, and `runtime/src/memory.c:2134`. | Candidate: immediate-call removal / native write-call fast path, measured separately from renderer work under host CPU budgets. | Scope is call overhead only; no observer, MMIO, load/store recording, or cycle behavior change. |
|
||||
| P1-2 GL CPU uploads | Active when CPU writes VRAM. | Pending upload rects are capped at 16 at `runtime/src/gpu_gl_renderer.c:437-456`; overflow flushes at `runtime/src/gpu_gl_renderer.c:541-589`. Flush converts 1555 to RGBA, updates upload/raw textures, streams VBO data, and draws rects at `runtime/src/gpu_gl_renderer.c:1297-1378`. Upload timing diagnostics are already env-gated at `runtime/src/gpu_gl_renderer.c:459-470`. | `GL-UPLOAD-001`: count overflow, bytes, conversion time, and flush causes on replay. | Preserve VRAM feedback, masks, CLUT/page dependencies, and dual/netplay CPU-auth mode. |
|
||||
| P1-2 GL pack / feedback | Active when drawn dirty VRAM is sampled. | Draws accumulate a dirty union at `runtime/src/gpu_gl_renderer.c:1504-1530`. Sampling can force pack at `runtime/src/gpu_gl_renderer.c:1452-1472`; pack mirrors dirty HR FBO data back to raw texture at `runtime/src/gpu_gl_renderer.c:1424-1450`. | `GL-PACK-001`: measure pack frequency and dirty area before considering narrower tracking. | Preserve texture feedback, CLUT sampling, PGXP/subpixel edge widening, mask/stencil, and native-wide output. |
|
||||
| P1-2 GL batch splits | Deferred until batch diagnostics implicate it. | Semi-transparent textured primitives are isolated at `runtime/src/gpu_gl_renderer.c:1958-2055`; comments tie this to STP ordering at `runtime/src/gpu_gl_renderer.c:2009-2018`. Non-triangle flat geometry is immediate at `runtime/src/gpu_gl_renderer.c:1872-1939`. Batch diagnostics exist at `runtime/include/gpu_gl_renderer.h:214-219` and `runtime/src/debug_server.c:7733-7754`. | Use existing `frame_perf`/batch diagnostics to prove split cost before changes. | Draw order, semi-transparency, mask checks, and feedback are high-risk. |
|
||||
| P1-2 GL readbacks/probes | Mostly already gated or non-steady-state. | Full VRAM readback is `ensure_cpu` at `runtime/src/gpu_gl_renderer.c:1479-1497`; normal 15-bit present uses GPU-direct selection at `runtime/src/main.cpp:7047-7078`. `gl_fbo_peek` / `gl_vram_diff` are debug commands at `runtime/src/debug_server.c:9036-9091`; present pixel probes require `PSX_GL_PRESENT_PROBE=1` at `runtime/src/gpu_gl_renderer.c:789-835`. | Verify route logs do not hit unexpected `ensure_cpu` or debug readbacks during measurement. | Keep diagnostic readbacks available; disabling unused tools is not a measured optimization. |
|
||||
| P1-2 hold-last copy | Existing capture calls are unconditional in ordinary present paths; safe gating is not implemented. | `hold_capture_drawable` copies before swap at `runtime/src/gpu_gl_renderer.c:717-734` and is called by CPU/blank/VRAM/wide presents at `runtime/src/gpu_gl_renderer.c:3016`, `runtime/src/gpu_gl_renderer.c:3032`, `runtime/src/gpu_gl_renderer.c:4234`, and `runtime/src/gpu_gl_renderer.c:4338`. Consumers include netplay hold at `runtime/src/main.cpp:5578-5611` and rewind pause at `runtime/src/main.cpp:6174-6189`. `gl_swap_with_osd` draws OSD after capture and before swap at `runtime/src/gpu_gl_renderer.c:3961-4034`, so capture is pre-OSD. | `GL-HOLD-001`: gate capture only when all activation/transition dependencies are enumerated. | Preserve rewind pause, netplay catch-up, interpolation hold, blank/tip-hold transitions, and pre-OSD capture ordering. |
|
||||
| P1-3 pacing/audio threading | No change candidate without profile attribution. | Vsync ownership and swap interval are derived at `runtime/src/main.cpp:2846-2875`; wall pacing is a deadline cap at `runtime/src/main.cpp:2878-2913`; vblank present calls body then pacer wait at `runtime/src/main.cpp:7423-7449`. | Measure wait/swap/pacer/audio underrun counters under calibrated host budgets. | No speed drift, latency regression, focus/refresh regression, or new underruns. |
|
||||
| P1-4 overlays/compilation | Not attributed as runtime contention; several safeguards already avoid hot-path capture. | Overlay capture is enabled/configured from runtime config at `runtime/src/main.cpp:11929-11957`. Universal guest writes clear compact page evidence instead of serializing snapshots at `runtime/src/memory.c:786-800`; dirty interpreter admits decodable CD-DMA overlay words at `runtime/src/dirty_ram_interp.c:2848-2851`. | Separate cold-start/transition attribution from warm steady-state; do not “optimize” overlay capture without samples. | Preserve overlay identity, replacement, self-modification, restore, and cold installs. |
|
||||
| P2-3 GTE/PGXP | Several CPU-side optimizations/gates already exist; residual cost needs samples. | GTE debug/projection rings are under `#ifndef PSX_NO_DEBUG_TOOLS` around `runtime/src/gte.cpp:1788-1818`. GTE register accessors read/write CPU state directly (`runtime/src/gte.cpp:2003-2032`, `runtime/src/gte.cpp:2036`, `runtime/src/gte.cpp:2110`), while timing stalls live in `runtime/src/psx_cycles.c:700-770`. PGXP hooks have a single hot gate at `runtime/src/pgxp.cpp:81-85`; hook bodies early return when inactive, e.g. `runtime/src/pgxp.cpp:419-423`. Renderer dirty tracking still depends on PGXP/subpixel at `runtime/src/gpu_gl_renderer.c:1504-1530`. | Attribute PGXP/GTE CPU cost separately from GL pack/upload before proposing changes. | Exact GTE state, command deadlines, load-delay give-back, and unchanged enhanced output. |
|
||||
| P2-4 SPU/CD/MDEC/DMA/I/O | Some subsystem optimizations/gates already exist; audio trace remains concrete. | Audio evidence above. SPU already skips the 24-voice walk when enabled with no active voices and no shadow tap at `runtime/src/spu.c:999-1033`. MDEC trace is debug-gated at `runtime/src/mdec.c:122-130`; MDEC has an SSE2 row path at `runtime/src/mdec.c:610-630`. DMA writes also go through `psx_write_word` in `runtime/src/dma.c:586`, `runtime/src/dma.c:652`, `runtime/src/dma.c:763`, `runtime/src/dma.c:855`, `runtime/src/dma.c:884`, `runtime/src/dma.c:913`, and `runtime/src/dma.c:1152`. | Start with audio trace and write-call attribution; expand only if samples point to SPU/CD/MDEC/DMA. | Preserve audio, IRQs, DMA timing, decode output, backpressure, and I/O behavior. |
|
||||
| P2-6 build options | Debug tools already compile out; no build-option gain claimed. | `PSX_NO_DEBUG_TOOLS` is defined when debug tools are off at `runtime/runtime.cmake:1436-1439`. GL perf queries are compiled out or can be disabled with `PSX_GL_PERF=0` at `runtime/src/gpu_gl_renderer.c:3466-3559`. | Build-flag A/B only after route attribution. | Multi-title correctness, portable artifacts, arithmetic parity. |
|
||||
| P3-1 offload | Deferred. | No attributed hotspot suitable for offload yet; GL context/swap/feedback paths above constrain candidates. | Consider only after upload, pack, audio, or write-call costs dominate including sync/transfer overhead. | Preserve frame order, context ownership, pacing, and synchronization. |
|
||||
| P3-2 Vulkan/software | Separate status. | OpenGL path has native-wide fast-path controls at `runtime/src/gpu_gl_renderer.c:1604-1628` and mirror skipping at `runtime/src/gpu_gl_renderer.c:1801-1816`, `runtime/src/gpu_gl_renderer.c:1856-1868`. | Track Vulkan/software separately; do not use as OpenGL acceptance substitute. | Explicit backend coverage and parity. |
|
||||
|
||||
## Validation Gaps
|
||||
|
||||
Independent Windows CPU sampling was attempted with WPR's CPU profile. Starting
|
||||
the trace failed with policy error `0xc5585011`; WPR subsequently reported no
|
||||
active recording. No CPU sample attribution was obtained. Wall-time phase
|
||||
counters under a Job CPU cap are not exclusive thread CPU time, including the
|
||||
large observed pacer intervals. No pacing defect is established by those values.
|
||||
|
||||
The GP0 ring's current public entry contains 104 bytes on the tested MinGW ABI,
|
||||
not the old source comment's implied size. At `1 << 20` entries its allocation
|
||||
request is 104 MiB. This is capacity, not a measured resident-memory reduction.
|
||||
Its only public readers found in the runtime are the debug server's GP0 history
|
||||
commands. The copy-builder global `g_gp0_last_copy_sp` is referenced only by the
|
||||
ring's own capture/record code. A production-only gate remains a candidate;
|
||||
do not remove functional GP0 source tracking, command counters, or draw ordering
|
||||
along with this history.
|
||||
|
||||
Known baseline structural-test failures are not caused by these candidates:
|
||||
|
||||
- `runtime/tests/test_runtime_perf_diag_guards.py:50` expects stale pacer-call
|
||||
placement.
|
||||
- `runtime/tests/test_interpreter_perf_guards.py:306` asserts inline cycle-limit
|
||||
invalidation for listed GTE/device functions.
|
||||
|
||||
Root verified the same failures against immutable provenance under
|
||||
`.local/ape-build-provenance/framework-8ec6498d/...`; treat them as validation
|
||||
gaps before using those tests as acceptance for write-call work.
|
||||
|
||||
## Vulkan Qualification Audit
|
||||
|
||||
Read-only follow-up, 2026-09-05. No Vulkan build/run or performance claim.
|
||||
The backend is substantially implemented: its vtable wires primitive rendering,
|
||||
textures, masks, PGXP, transfers and native-wide callbacks (`gpu_vk_renderer.c`,
|
||||
`vkb_backend` near line 3273). Its introductory phase-status comment is stale.
|
||||
|
||||
- `PSX_ENABLE_VULKAN=ON` is not proof of an active renderer. CMake only defines
|
||||
`PSX_HAVE_VULKAN` after header/tool checks; otherwise `gpu_vk_renderer.c:35`
|
||||
compiles inert stubs. Context initialization can also fail and select software
|
||||
(`main.cpp:13623`). A real qualification run must witness the active backend.
|
||||
- Readback is implemented but can force full VRAM pack/copy/wait through
|
||||
`ensure_cpu` (`gpu_vk_renderer.c:2308`). It is not the normal 15-bit GPU-direct
|
||||
present path. Missing evidence: mixed-command readback parity and counts
|
||||
proving ordinary presentation does not unexpectedly enter this path.
|
||||
- Native-wide presentation exists, but Vulkan omits the vtable's
|
||||
`wide_dump_full` callback. `gr_wide_supported()` only checks
|
||||
`render_wide_display`, so presentation support does not imply complete
|
||||
diagnostic capture support (`gpu_render.c:165`, `gpu_render.h:197`).
|
||||
- Netplay explicitly forces a Vulkan request to software because the present
|
||||
path is not CPU-authoritative (`main.cpp:13075`). Preserve this correctness
|
||||
guard; it is not an OpenGL optimization.
|
||||
- Existing Vulkan tests cover source patterns and an upload-alignment helper,
|
||||
not a live context drawing and comparing pixels. Required follow-up is a
|
||||
backend matrix for primitive order, textures, blending, masks, feedback,
|
||||
transfers, native-wide, depth24, capture, and initialization/fallback cases.
|
||||
|
||||
These findings justify a separate qualification track, not a recommendation to
|
||||
switch users away from OpenGL or a conclusion that Vulkan is unimplemented.
|
||||
|
||||
## Local CPU Attribution Tools
|
||||
|
||||
Read-only inventory, 2026-09-05: WPR, xperf and WPA are installed, but the
|
||||
attempted WPR CPU profile failed with policy error `0xc5585011`. PATH and
|
||||
known-install-path checks found no AMD uProf, Intel VTune, Very Sleepy or
|
||||
samply. This is a bounded inventory, not proof that no profiler exists anywhere
|
||||
on the machine. No installation, elevation or policy change was attempted.
|
||||
|
||||
MinGW `gprof` is available, but requires an instrumented `-pg` rebuild and
|
||||
cannot faithfully sample the existing Release artifact. `perf_host.py` supplies
|
||||
whole-process CPU time, not stacks. Campaign phase counters are wall time, not
|
||||
exclusive CPU time. `host_thread_probe.py` can suspend a same-user thread and
|
||||
inspect its context/stack; repeated captures could establish coarse thread
|
||||
stack occupancy with symbols, but are perturbing and must not be reported as
|
||||
per-function CPU percentages. Cross-check such leads with controlled narrow
|
||||
A/B experiments until a statistical CPU profiler can be used.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Performance campaign tools
|
||||
|
||||
These tools begin P0-1 in the [campaign](../PERFORMANCE_CAMPAIGN.md). They collect
|
||||
diagnostic evidence, not an automatic optimization acceptance verdict. See the
|
||||
[experiment ledger](EXPERIMENTS.md) for verified local artifacts and limitations.
|
||||
|
||||
## Scoped host budget
|
||||
|
||||
Use a native Windows Python, not an MSYS path shim. For example, from the
|
||||
campaign worktree in PowerShell:
|
||||
|
||||
```powershell
|
||||
$python = 'C:\Users\Matthew\AppData\Local\Programs\Python\Python312\python.exe'
|
||||
& $python tools/perf_host.py --core-percent 50 --affinity 0x1 --timeout 300 --report .local/host.json -- C:\path\ApeEscapeRecomp.exe --renderer opengl --no-launcher --debug-port 4680
|
||||
```
|
||||
|
||||
Prepare isolated writable settings, saves, and memory cards before launch. The
|
||||
launcher inherits its working directory; run from the isolated game directory
|
||||
with an absolute script path when that is where the game expects its assets.
|
||||
Preserve the title's required DLL search path and BIOS/disc configuration.
|
||||
|
||||
`--core-percent 50` budgets the entire job at half of one logical CPU equivalent;
|
||||
`--cpu-percent 50` budgets half of the whole machine. Neither changes guest
|
||||
clocks. Affinity is a separate control; holding it at one CPU also removes
|
||||
parallelism. Compare identical affinity across runs, and separately test normal
|
||||
affinity. Omit both percentage options for unrestricted execution. On Windows,
|
||||
the child starts suspended, receives the job cap/affinity, then resumes hidden.
|
||||
Timeout/error cleanup kills only the launched job. Report JSON includes applied
|
||||
and read-back limits; stdout/stderr are separate adjacent log files.
|
||||
|
||||
Calibrate with a sustained CPU-time workload on each host. Short bursts can
|
||||
escape effective throttling. A cap does not reproduce older GPU, cache, memory,
|
||||
or power behavior. Keep real low-end machines in the acceptance matrix.
|
||||
|
||||
## Diagnostic route capture
|
||||
|
||||
Start a compatible Release build with `PSX_DEBUG_TOOLS=ON` and a local debug
|
||||
port. Use the same effective settings, prepared overlays, BIOS, and isolated
|
||||
checkpoint across samples. Do not expose the debug server to untrusted hosts.
|
||||
|
||||
```powershell
|
||||
& $python tools/perf_campaign.py sample --port 4680 --route tools/perf_routes/ape_crabby_water_idle.json --restore-slot 1 --state-file .local/crabby-baseline/saves/scph1001/state_800A3660_slot01.pst --timeout 240 --out .local/A1.json --label A1 --provenance .local/target-A.json
|
||||
```
|
||||
|
||||
The committed route references a local v5 state by SHA256; no game data is
|
||||
distributed. It holds neutral digital input for 7,200 native route ticks. This
|
||||
is the stationary water-facing workload, not the analog approach to the water.
|
||||
The runner rejects analog axes because this runtime's native route protocol
|
||||
does not support them. `--timeout` bounds route polling, not the entire tool
|
||||
invocation; individual TCP calls, snapshots, and restore have additional time.
|
||||
|
||||
`--state-file` hashes an operator-supplied file; it does not upload it or change
|
||||
the runtime's save directory. Verify that slot 1 actually resolves to that file.
|
||||
The runner waits for a successful completed restore generation, not just the
|
||||
staging acknowledgement. Without restore options, starting state is an operator
|
||||
precondition. Guest execution continues during sequential setup/snapshots, so
|
||||
this tool does not yet guarantee identical checkpoint-to-route guest work.
|
||||
|
||||
Samples retain raw before/after counters, route completion, phase attribution,
|
||||
and partial failure evidence. Disabled or missing GL timer data is unavailable,
|
||||
never zero. `summary.route_wall_s` includes route setup/status-poll overhead;
|
||||
the counter observation window is longer. Neither is exclusive CPU busy time.
|
||||
Paced route time alone will not reveal spare CPU headroom at full speed.
|
||||
|
||||
## Target provenance and comparison
|
||||
|
||||
`--provenance` takes a JSON object with these required fields:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `artifact_sha256` | SHA256 of the actual running executable |
|
||||
| `settings_sha256` | SHA256 of a retained manifest covering all effective config, overrides, and mods |
|
||||
| `bios_sha256` | SHA256 of the BIOS input |
|
||||
| `overlay_inventory_sha256` | SHA256 of a retained, sorted inventory including generated/cache artifact hashes |
|
||||
| `host_id` | Stable host identity; retain OS, driver, actual GL device, power and display details alongside it |
|
||||
| `cpu_budget` | `{"unrestricted": true}` or applied `{"cap": 313, "denominator": 10000, "affinity": "0x1"}` |
|
||||
| `instrumentation` | Nonempty object recording build flags, profiler and diagnostic environment settings |
|
||||
|
||||
Record affinity even for unrestricted runs. Optional `variant_selector` names
|
||||
the faithful/candidate selector. These declarations are operator evidence, not
|
||||
attestation of the running process. Keep build/source/toolchain manifests and
|
||||
host launcher reports with them. Samples without target provenance can be
|
||||
collected but cannot be compared by the A/B command.
|
||||
|
||||
Collect at least three nonoverlapping pairs in A1, B1, A2, B2, A3, B3 order:
|
||||
|
||||
```powershell
|
||||
& $python tools/perf_campaign.py ab --a .local/A1.json --b .local/B1.json --a .local/A2.json --b .local/B2.json --a .local/A3.json --b .local/B3.json --out .local/AB.json
|
||||
```
|
||||
|
||||
Declare the primary metric and direction at capture time. Comparison requires
|
||||
matching routes, metrics, and environments, stable artifacts/selectors within
|
||||
each side, successful samples, and timestamp evidence of interleaving. It
|
||||
reports raw values, best values, and medians. `--discard 2:compiler-activity`
|
||||
removes the entire original second pair and retains its artifact references and
|
||||
reason; at least three pairs must remain. Never discard merely slow samples.
|
||||
|
||||
An accepted *pair* only means comparison eligibility. Correctness checkpoints,
|
||||
candidate hit witnesses, noise characterization, profiling-off production
|
||||
timing, stutter/audio measurements, and low-end hardware qualification remain
|
||||
separate required campaign gates.
|
||||
|
||||
## Tests
|
||||
|
||||
```powershell
|
||||
& $python -m unittest tools.tests.test_perf_campaign tools.tests.test_perf_host tools.tests.test_stall_report
|
||||
```
|
||||
@@ -1 +1 @@
|
||||
5f69759d2f0a66ea1014b64a159f2f44fe3f79447972521209fb266b7e20e1de
|
||||
012434af40660484f2a6990dd9f6b2acb35d00bc514c3e85ba0a49ca211f3d33
|
||||
|
||||
@@ -1 +1 @@
|
||||
439e85a797f227c8adbc8445832bb45438f357af54ff1ba834302c32003ad12e
|
||||
aa351622ddd9c496b61bd0982e58230cb4fc3aac6e2a5a1e4899ccd51fc6dbe9
|
||||
|
||||
@@ -101,6 +101,8 @@ struct CodeGenConfig {
|
||||
// nop replaced with `a1 += margin`, or move rD,a1 replaced with
|
||||
// `rD = a1 + margin`. See config_loader.h. Empty = default.
|
||||
std::set<uint32_t> ws_cull_bias_sites;
|
||||
// Lower endpoint counterpart: addi/addiu immediate minus activation margin.
|
||||
std::set<uint32_t> ws_cull_bias_lower_sites;
|
||||
std::set<uint32_t> ws_cull_range_sites;
|
||||
std::set<uint32_t> ws_cull_a1_sites;
|
||||
std::set<uint32_t> ws_cull_screen_x_sites;
|
||||
|
||||
@@ -1087,7 +1087,8 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
// same address holds different code across scene variants, so a mismatch is
|
||||
// expected — apply the transform only where the bytes match, else fall
|
||||
// through to the vanilla translation (see CodeGenConfig::overlay_mode).
|
||||
if (config_.ws_cull_bias_sites.count(addr)) {
|
||||
if (config_.ws_cull_bias_sites.count(addr) ||
|
||||
config_.ws_cull_bias_lower_sites.count(addr)) {
|
||||
if (opcode == 0x08 || opcode == 0x09) { // addi / addiu
|
||||
uint32_t rs = get_rs(instr), rt = get_rt(instr);
|
||||
int16_t imm = get_imm16(instr);
|
||||
@@ -1097,9 +1098,13 @@ std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr)
|
||||
"(psx_ws_x_margin() > 0 ? psx_ws_x_margin() + {} : 0)",
|
||||
config_.ws_cull_activation_guard_pixels)
|
||||
: "psx_ws_x_margin()";
|
||||
return fmt::format("{} = {} + ((int32_t){} + {});{}",
|
||||
reg_name(rt), reg_name(rs), imm, margin,
|
||||
comment);
|
||||
std::string code = fmt::format("{} = {} + ((int32_t){} {} {});",
|
||||
reg_name(rt), reg_name(rs), imm,
|
||||
config_.ws_cull_bias_lower_sites.count(addr) ? "-" : "+", margin);
|
||||
// Preserve the ordinary ALU shadow update, including source capture
|
||||
// before an in-place add. Margin zero must also be a PGXP identity.
|
||||
PSXRecomp::append_pgxp_hooks(instr, code);
|
||||
return config_.indent + code + comment;
|
||||
} else if (!config_.overlay_mode) {
|
||||
fmt::print(stderr, "ERROR: [widescreen.cull] bias site 0x{:08X} is not "
|
||||
"addi/addiu (opcode 0x{:02X})\n", addr, opcode);
|
||||
@@ -2920,7 +2925,7 @@ GeneratedFunction CodeGenerator::generate_function(
|
||||
if (config_.mod_function_entry_funcs.count(func.start_addr)) {
|
||||
body_ss << config_.indent
|
||||
<< fmt::format(
|
||||
"psx_mod_function_entry(cpu, 0x{:08X}u);"
|
||||
"if (psx_mod_function_entry(cpu, 0x{:08X}u)) return;"
|
||||
" /* trusted opt-in game-mod hook */\n",
|
||||
func.start_addr);
|
||||
}
|
||||
@@ -3433,7 +3438,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
|
||||
ss << "#endif\n";
|
||||
ss << "extern int psx_game_text_native_ok(uint32_t addr); /* stale-static guard (dispatch shard) */\n";
|
||||
ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n";
|
||||
ss << "extern void psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
|
||||
ss << "extern int psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
|
||||
ss << "extern void psx_datashard_ret(CPUState* cpu); /* data-shard capture finalize */\n";
|
||||
ss << "extern int psx_vsync_query_hle_enter(CPUState* cpu, uint32_t func, uint32_t counter_addr, uint32_t gpustat_ptr_addr, uint32_t timer1_ptr_addr, uint32_t timer1_cache_addr); /* load_accel.c */\n";
|
||||
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
|
||||
|
||||
@@ -64,6 +64,8 @@ uint32_t overlay_codegen_config_hash(const GameConfig& c) {
|
||||
h.words("sprite_tag_funcs", c.ws_sprite_tag_funcs);
|
||||
h.words("mod_function_entry_funcs", c.mod_function_entry_funcs);
|
||||
h.words("cull_bias", c.ws_cull_bias_sites);
|
||||
if (!c.ws_cull_bias_lower_sites.empty())
|
||||
h.words("cull_bias_lower", c.ws_cull_bias_lower_sites);
|
||||
h.words("cull_range", c.ws_cull_range_sites);
|
||||
h.words("cull_a1", c.ws_cull_a1_sites);
|
||||
h.words("cull_screen_x", c.ws_cull_screen_x_sites);
|
||||
@@ -1755,6 +1757,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
|
||||
// Optional [widescreen.cull] block — world-space draw-cull widening.
|
||||
std::vector<uint32_t> ws_cull_bias_sites, ws_cull_range_sites, ws_cull_a1_sites;
|
||||
std::vector<uint32_t> ws_cull_bias_lower_sites;
|
||||
std::vector<uint32_t> ws_cull_screen_x_sites;
|
||||
std::vector<uint32_t> ws_cull_slti_sites;
|
||||
std::vector<uint32_t> ws_cull_slti_lower_sites;
|
||||
@@ -1792,6 +1795,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
out.push_back(parse_hex(a, fmt::format("widescreen.cull.{}", key)));
|
||||
};
|
||||
load_sites("bias_sites", ws_cull_bias_sites);
|
||||
load_sites("bias_lower_sites", ws_cull_bias_lower_sites);
|
||||
load_sites("range_sites", ws_cull_range_sites);
|
||||
load_sites("a1_sites", ws_cull_a1_sites);
|
||||
load_sites("screen_x_sites", ws_cull_screen_x_sites);
|
||||
@@ -2233,6 +2237,7 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
/*vsync_event_horizon_extra_sites*/ vsync_event_horizon_extra_sites,
|
||||
/*vsync_event_horizon_any*/ vsync_event_horizon_any,
|
||||
/*ws_cull_bias_sites*/ ws_cull_bias_sites,
|
||||
/*ws_cull_bias_lower_sites*/ ws_cull_bias_lower_sites,
|
||||
/*ws_cull_range_sites*/ ws_cull_range_sites,
|
||||
/*ws_cull_a1_sites*/ ws_cull_a1_sites,
|
||||
/*ws_cull_screen_x_sites*/ ws_cull_screen_x_sites,
|
||||
|
||||
@@ -918,6 +918,8 @@ struct GameConfig {
|
||||
// rD = a1 + margin (caller-margin classifier variants)
|
||||
// All Ghidra-evidenced; empty by default. Changing these requires a regen.
|
||||
std::vector<uint32_t> ws_cull_bias_sites;
|
||||
// Lower endpoint of a camera-relative activation/reset interval.
|
||||
std::vector<uint32_t> ws_cull_bias_lower_sites;
|
||||
std::vector<uint32_t> ws_cull_range_sites;
|
||||
std::vector<uint32_t> ws_cull_a1_sites;
|
||||
// Explicit `sltiu rt,sx,W` render rejects for cases where codegen function
|
||||
|
||||
@@ -224,6 +224,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
std::set<uint32_t> load_charge_batch_funcs; // [recompiler] load_charge_batch*
|
||||
std::map<uint32_t, std::array<uint32_t, 4>> vsync_query_hle_funcs;
|
||||
std::set<uint32_t> ws_cull_bias, ws_cull_range, ws_cull_a1; // [widescreen.cull]
|
||||
std::set<uint32_t> ws_cull_bias_lower;
|
||||
std::set<uint32_t> ws_cull_screen_x; // [widescreen.cull] screen_x_sites
|
||||
std::set<uint32_t> ws_cull_slti; // [widescreen.cull] slti_sites
|
||||
std::set<uint32_t> ws_cull_slti_lower; // [widescreen.cull] slti_lower_sites
|
||||
@@ -292,6 +293,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
cfg.vsync_counter_addr, cfg.vsync_gpustat_ptr_addr,
|
||||
cfg.vsync_timer1_ptr_addr, cfg.vsync_timer1_cache_addr };
|
||||
ws_cull_bias.insert(cfg.ws_cull_bias_sites.begin(), cfg.ws_cull_bias_sites.end());
|
||||
ws_cull_bias_lower.insert(cfg.ws_cull_bias_lower_sites.begin(), cfg.ws_cull_bias_lower_sites.end());
|
||||
ws_cull_range.insert(cfg.ws_cull_range_sites.begin(), cfg.ws_cull_range_sites.end());
|
||||
ws_cull_a1.insert(cfg.ws_cull_a1_sites.begin(), cfg.ws_cull_a1_sites.end());
|
||||
ws_cull_screen_x.insert(cfg.ws_cull_screen_x_sites.begin(), cfg.ws_cull_screen_x_sites.end());
|
||||
@@ -395,6 +397,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
mod_entry_funcs.insert(wscfg.mod_function_entry_funcs.begin(),
|
||||
wscfg.mod_function_entry_funcs.end());
|
||||
ws_cull_bias.insert(wscfg.ws_cull_bias_sites.begin(), wscfg.ws_cull_bias_sites.end());
|
||||
ws_cull_bias_lower.insert(wscfg.ws_cull_bias_lower_sites.begin(), wscfg.ws_cull_bias_lower_sites.end());
|
||||
ws_cull_range.insert(wscfg.ws_cull_range_sites.begin(), wscfg.ws_cull_range_sites.end());
|
||||
ws_cull_a1.insert(wscfg.ws_cull_a1_sites.begin(), wscfg.ws_cull_a1_sites.end());
|
||||
ws_cull_screen_x.insert(wscfg.ws_cull_screen_x_sites.begin(), wscfg.ws_cull_screen_x_sites.end());
|
||||
@@ -1458,6 +1461,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
codegen_config.vsync_query_hle_funcs = vsync_query_hle_funcs;
|
||||
codegen_config.ws_bg2d_init_func = ws_bg2d_init_func;
|
||||
codegen_config.ws_cull_bias_sites = ws_cull_bias;
|
||||
codegen_config.ws_cull_bias_lower_sites = ws_cull_bias_lower;
|
||||
codegen_config.ws_cull_range_sites = ws_cull_range;
|
||||
codegen_config.ws_cull_a1_sites = ws_cull_a1;
|
||||
codegen_config.ws_cull_screen_x_sites = ws_cull_screen_x;
|
||||
|
||||
@@ -346,13 +346,21 @@ result = 1
|
||||
const auto range = write_config(root, "range-cull", R"toml(
|
||||
[widescreen.cull]
|
||||
range_sites = ["0x80012340"]
|
||||
bias_lower_sites = ["0x80012348"]
|
||||
activation_guard_pixels = 256
|
||||
)toml");
|
||||
const auto range_config = PSXRecompV4::load_game_config(range);
|
||||
check(range_config.ws_cull_range_sites ==
|
||||
std::vector<uint32_t>{0x80012340u} &&
|
||||
range_config.ws_cull_bias_lower_sites ==
|
||||
std::vector<uint32_t>{0x80012348u} &&
|
||||
range_config.ws_cull_activation_guard_pixels == 256,
|
||||
"parser preserves explicit range sites and activation guard");
|
||||
auto changed_lower_config = range_config;
|
||||
changed_lower_config.ws_cull_bias_lower_sites.clear();
|
||||
check(PSXRecompV4::overlay_codegen_config_hash(range_config) !=
|
||||
PSXRecompV4::overlay_codegen_config_hash(changed_lower_config),
|
||||
"lower activation endpoint changes overlay cache identity");
|
||||
|
||||
const auto bad_activation_guard =
|
||||
write_config(root, "range-cull-bad-activation-guard", R"toml(
|
||||
@@ -774,6 +782,27 @@ void codegen_tests() {
|
||||
std::string::npos,
|
||||
"activation bias gains the same isolated resident-object lead");
|
||||
|
||||
PSXRecomp::CodeGenConfig lower_bias_config;
|
||||
lower_bias_config.ws_cull_bias_lower_sites.insert(0x80010000u);
|
||||
for (const uint32_t opcode : {0x2082FFD0u, 0x2482FFD0u}) {
|
||||
const std::string lower = generate_first_instruction(opcode, {}, false, lower_bias_config);
|
||||
check(lower.find("((int32_t)-48 - psx_ws_x_margin())") != std::string::npos,
|
||||
"ADDI/ADDIU lower endpoint expands left without changing camera source");
|
||||
check(lower.find("PGXP_ALU(") != std::string::npos &&
|
||||
lower.find("_pgx1 = cpu->gpr[4]") != std::string::npos,
|
||||
"lower bound preserves native ALU precision tracking");
|
||||
}
|
||||
lower_bias_config.ws_cull_activation_guard_pixels = 32;
|
||||
const std::string lower_guard = generate_first_instruction(
|
||||
0x2482FFD0u, {}, false, lower_bias_config);
|
||||
check(lower_guard.find("-48 - (psx_ws_x_margin() > 0 ? psx_ws_x_margin() + 32 : 0)") !=
|
||||
std::string::npos,
|
||||
"lower endpoint includes activation guard only while wide");
|
||||
const std::string lower_mismatch = generate_first_instruction(
|
||||
0x3482FFD0u, {}, true, lower_bias_config); // ORI variant is not an add.
|
||||
check(lower_mismatch.find("psx_ws_x_margin()") == std::string::npos,
|
||||
"lower bias leaves a nonmatching overlay opcode unchanged");
|
||||
|
||||
PSXRecomp::CodeGenConfig branch_keep_config;
|
||||
branch_keep_config.ws_cull_branch_keep_sites.insert(0x80010000u);
|
||||
const std::string branch_keep = generate_first_instruction(
|
||||
|
||||
@@ -98,6 +98,10 @@ if(BUILD_TESTING)
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_mirror_pc_preserved_guards.py")
|
||||
add_test(NAME segment_miss_wiring_test COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_segment_miss_wiring.py")
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_test(NAME ws_activation_bias_exec_test COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_ws_activation_bias_exec.py" "${CMAKE_C_COMPILER}")
|
||||
endif()
|
||||
add_test(NAME gpu_textured_dot_nw_shift_test COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gpu_textured_dot_nw_shift.py")
|
||||
add_test(NAME gl_readback_runner_test COMMAND "${Python3_EXECUTABLE}"
|
||||
@@ -413,9 +417,30 @@ if(BUILD_TESTING)
|
||||
add_test(NAME gpu_textured_dot_nw_shift_exec_test
|
||||
COMMAND gpu_textured_dot_nw_shift_exec_test)
|
||||
|
||||
add_executable(ws_view_anchor_test tests/test_ws_view_anchor.c)
|
||||
add_executable(ws_view_anchor_gpu_test tests/test_ws_view_anchor_gpu.c
|
||||
src/psx_video_timing.c)
|
||||
foreach(_anchor_test ws_view_anchor ws_view_anchor_gpu)
|
||||
target_include_directories(${_anchor_test}_test PRIVATE include src)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(${_anchor_test}_test PRIVATE -ffunction-sections -fdata-sections)
|
||||
if(NOT APPLE)
|
||||
target_link_options(${_anchor_test}_test PRIVATE -Wl,--gc-sections)
|
||||
endif()
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
target_link_libraries(${_anchor_test}_test PRIVATE m)
|
||||
endif()
|
||||
endforeach()
|
||||
add_test(NAME ws_view_anchor_test COMMAND ws_view_anchor_test)
|
||||
add_test(NAME ws_view_anchor_gpu_test COMMAND ws_view_anchor_gpu_test)
|
||||
|
||||
add_executable(ws_repeat_rect_test tests/test_ws_repeat_rect.c)
|
||||
target_include_directories(ws_repeat_rect_test PRIVATE include)
|
||||
add_test(NAME ws_repeat_rect_test COMMAND ws_repeat_rect_test)
|
||||
add_executable(ws_screen_mask_test tests/test_ws_screen_mask.c)
|
||||
target_include_directories(ws_screen_mask_test PRIVATE include)
|
||||
add_test(NAME ws_screen_mask_test COMMAND ws_screen_mask_test)
|
||||
|
||||
add_executable(mod_gpu_dma_aperture_test
|
||||
tests/test_mod_gpu_dma_aperture.c
|
||||
@@ -931,6 +956,21 @@ if(BUILD_TESTING)
|
||||
|
||||
find_package(Python3 COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang" AND NOT MSVC
|
||||
AND NOT CMAKE_CROSSCOMPILING)
|
||||
add_test(NAME audio_trace_pcm_guard_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_audio_trace_pcm_guard.py
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
add_test(NAME gpu_gp0_history_guard_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gpu_gp0_history_guard.py
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
add_test(NAME memory_debug_trace_guard_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_memory_debug_trace_guard.py
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
endif()
|
||||
add_test(NAME fntrace_c_linkage_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fntrace_c_linkage.py)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_test(NAME overlay_retry_c11_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_overlay_retry_c11.py --compiler ${CMAKE_C_COMPILER})
|
||||
@@ -943,6 +983,8 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_disc_picker_ascii.py)
|
||||
add_test(NAME disc_companion_test COMMAND ${Python3_EXECUTABLE} ${PSXRECOMP_ROOT}/tools/tests/test_disc_companion.py)
|
||||
add_test(NAME sbi_registry_test COMMAND ${Python3_EXECUTABLE} ${PSXRECOMP_ROOT}/tools/tests/test_sbi_registry.py)
|
||||
add_test(NAME perf_campaign_test COMMAND ${Python3_EXECUTABLE} ${PSXRECOMP_ROOT}/tools/tests/test_perf_campaign.py)
|
||||
add_test(NAME perf_host_test COMMAND ${Python3_EXECUTABLE} ${PSXRECOMP_ROOT}/tools/tests/test_perf_host.py)
|
||||
add_test(NAME frame_interpolation_context_ownership_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_frame_interpolation_context_ownership.py)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
* audio_trace.h - always-on audio observability rings (PCM taps + event ring).
|
||||
* audio_trace.h - audio health counters, event ring and debug PCM history.
|
||||
*
|
||||
* Port of the snesrecomp audio-campaign measurement harness (audio_trace.{c,h},
|
||||
* commit 67ead27 lineage) to the PSX pipeline, following the cross-system tap
|
||||
* model in F:/Projects/_audio_round2/ROUND2_PLAN.md: always-on ring buffers
|
||||
* recording every sample/event from process start, dumped AFTER the fact via
|
||||
* model in F:/Projects/_audio_round2/ROUND2_PLAN.md: debug-tool builds keep
|
||||
* always-on ring buffers recording from process start, dumped AFTER the fact via
|
||||
* TCP debug commands — never arm-then-reproduce.
|
||||
*
|
||||
* Taps (PSX pipeline):
|
||||
@@ -21,9 +21,11 @@
|
||||
* output — giving symmetric `audio_*` commands on ports 4370 and 4380.
|
||||
*
|
||||
* Threading: each tap has a single writer. Readers (debug-server thread)
|
||||
* take a stable [oldest, head) snapshot; the PCM rings hold ~95 s at 44100,
|
||||
* so a dump cannot be lapped mid-read in practice. Indices are C11 atomics
|
||||
* (release on publish, acquire on read).
|
||||
* take a stable [oldest, head) snapshot; in debug-tool builds the PCM rings
|
||||
* hold ~95 s at 44100, so a dump cannot be lapped mid-read in practice.
|
||||
* Production PSX_NO_DEBUG_TOOLS builds keep counters/events but compile out
|
||||
* PCM history and WAV dumps. Indices are C11 atomics (release on publish,
|
||||
* acquire on read).
|
||||
*/
|
||||
#ifndef PSX_AUDIO_TRACE_H
|
||||
#define PSX_AUDIO_TRACE_H
|
||||
@@ -118,7 +120,9 @@ uint32_t audio_trace_events_get(AudioTraceEvent *out, uint32_t max);
|
||||
/* Write a tap slice as a stereo s16 WAV at the tap's rate. start=-1,count=0
|
||||
* dumps the whole currently-buffered ring. `start`/`count` are absolute frame
|
||||
* indices on the tap's production timeline. Returns frames written, or -1 on
|
||||
* error (unwritable path / empty ring / slice already evicted). */
|
||||
* error (unwritable path / empty ring / slice already evicted). In
|
||||
* PSX_NO_DEBUG_TOOLS production builds, PCM history is not present and this
|
||||
* returns -1 without creating/opening `path`. */
|
||||
int64_t audio_trace_dump_wav(int tap, const char *path,
|
||||
int64_t start, uint64_t count);
|
||||
|
||||
|
||||
+44
-4
@@ -109,11 +109,12 @@ typedef struct {
|
||||
void gpu_arm_shaded_quad_capture(void);
|
||||
int gpu_get_shaded_quad_capture(const GpuSqCapEntry** out);
|
||||
|
||||
/* Per-frame GP0 command ring (always-on; query via debug server).
|
||||
* Each entry records the GP0 command header + up to 6 payload words
|
||||
* (longer commands like 0x3C shaded textured quad are truncated to 6).
|
||||
/* Per-frame GP0 command ring (always-on in debug-tool builds).
|
||||
* Each entry records up to GPU_GP0_RING_MAX_WORDS command words, including
|
||||
* the header; longer commands are truncated.
|
||||
* Stamped with the s_frame_count value at issue time so a debug
|
||||
* client can pull all commands for any frame in the ring window. */
|
||||
* client can pull all commands for any frame in the ring window.
|
||||
* PSX_NO_DEBUG_TOOLS builds report zero total/capacity and empty dumps/spans. */
|
||||
#define GPU_GP0_RING_MAX_WORDS 12
|
||||
typedef struct {
|
||||
uint32_t frame;
|
||||
@@ -175,6 +176,33 @@ void gpu_ws_configure(int aspect_num, int aspect_den,
|
||||
/* [widescreen] full_2d: opt a pure-2D sprite game into the widescreen present
|
||||
* path (treat every in-game frame as gameplay, since it never tags 3D prims). */
|
||||
void gpu_ws_set_full_2d(int on);
|
||||
/* Signed 16-bit camera/min/max; active is a nonzero byte. Requires bg2d hooks.
|
||||
* Zero addresses disable the feature. No guest memory is written. */
|
||||
void gpu_ws_set_view_anchor(uint32_t camera, uint32_t min, uint32_t max, uint32_t active);
|
||||
/* Optional authored scene bounds when native camera locks do not delimit
|
||||
* visible geometry. Set before layer setup; disable when leaving the scene.
|
||||
* Changes presentation only, and has no effect without view anchoring. */
|
||||
void gpu_ws_set_view_bounds_override(int enabled, int minimum, int maximum);
|
||||
/* Bracket each bg2d packet producer, with its guest packet pointer. The
|
||||
* independent-layer mask identifies parallax backdrops that may anchor to
|
||||
* their own map edges; linked/foreground layers retain the world origin. */
|
||||
void gpu_ws_bg2d_begin_view_layer(unsigned layer, uint32_t packet, unsigned independent_mask);
|
||||
void gpu_ws_bg2d_end_view_layer(unsigned layer, uint32_t packet);
|
||||
/* Opt-in host-produced SPRT_16 packets, 32-byte slots in enhancement DMA RAM.
|
||||
* The 16-byte standard packet is followed by signed view shift, left/right
|
||||
* padding and GPU_WS_BG2D_PACKET_MAGIC, optionally ORed with MIRROR_X.
|
||||
* Coordinates use signed 16-bit X; ordinary
|
||||
* packets retain PS1 signed 11-bit coordinates. The guest tile loop/ring stays
|
||||
* native while this arena is registered. Allocate once during mod activation. */
|
||||
void gpu_ws_bg2d_set_host_arena(uint32_t base, uint32_t size);
|
||||
#define GPU_WS_BG2D_PACKET_MAGIC 0x58364247u
|
||||
#define GPU_WS_BG2D_BANK_PACKET_MAGIC 0x58364248u
|
||||
/* Bank packets pack signed shift in +16 low half and immutable bank ID in
|
||||
* its high half. Their indices use the bank, but palettes remain live VRAM.
|
||||
* Keep the original magic readable for pending packets in older snapshots. */
|
||||
#define GPU_WS_BG2D_MIRROR_X 0x80000000u
|
||||
struct WsViewAnchor;
|
||||
int gpu_ws_bg2d_get_view(unsigned layer, struct WsViewAnchor *view);
|
||||
void gpu_ws_set_auto_ui_squash(int on);
|
||||
/* [widescreen.bg2d] Capcom 2D background tile-loop widen — hooked at the renderer's
|
||||
* column-count / start-tile-col / start-screen-x instructions. Identity at 4:3
|
||||
@@ -287,6 +315,8 @@ void gpu_ws_set_aspect_cone(const uint32_t *addresses,
|
||||
const uint32_t queue_capacities[3],
|
||||
const uint32_t queue_type_masks[3]);
|
||||
int psx_ws_is_cull_bias_site(uint32_t pc);
|
||||
void gpu_ws_set_bias_lower_cull_sites(const uint32_t *sites, int nsites);
|
||||
int psx_ws_is_cull_bias_lower_site(uint32_t pc);
|
||||
int psx_ws_is_cull_slti_site(uint32_t pc);
|
||||
int psx_ws_is_cull_slti_lower_site(uint32_t pc);
|
||||
int psx_ws_is_cull_negsub_site(uint32_t pc);
|
||||
@@ -392,6 +422,14 @@ void gpu_ws_tag_black_reveal_rect(uint32_t prim);
|
||||
* reveal margins only. The caller verifies the composite's source period.
|
||||
* Original UVs, texel density, and canonical VRAM writes are unchanged. */
|
||||
void gpu_ws_tag_repeat_rect(uint32_t prim, int32_t period);
|
||||
/* Title-identified flat screen mask: extend its off-screen vertical boundary
|
||||
* to the adaptive edge, preserving the interior opening and canonical image.
|
||||
* Packet guarded, cleared on reset/load, inert outside native-wide gameplay. */
|
||||
void gpu_ws_tag_screen_mask_quad(uint32_t prim);
|
||||
/* Pure emulation-thread predicate for a known native-only effect/scene.
|
||||
* Nonzero forces native 4:3; zero defers. No GPU calls or guest writes inside.
|
||||
* Host configuration survives reset/load; classification reads live state. */
|
||||
void gpu_ws_set_native_scene_predicate(int (*predicate)(void));
|
||||
/* Targeted alternative for sprite-heavy 2D games: corner-anchor only primitives
|
||||
* whose ordering-table packet lives in the configured half-open RAM range. */
|
||||
void gpu_ws_set_nw_left_hud_packet_range(uint32_t lo, uint32_t hi);
|
||||
@@ -486,7 +524,9 @@ typedef struct {
|
||||
int xnum, xden; /* squash factor */
|
||||
int mode; /* 0 = off, 1 = squash, 2 = native-wide */
|
||||
int nw_extra; /* native-wide frame growth (display px), 0 if off */
|
||||
int view_anchor, view_left, view_right, view_shift, view_pad_left, view_pad_right;
|
||||
uint64_t cur_frame;
|
||||
uint32_t bg2d_generations; /* Game background submissions, independent of vblank. */
|
||||
uint32_t last_tag_frame; /* frame of newest tagged prim */
|
||||
uint32_t last_3d_frame; /* frame of newest shaded prim (diagnostic) */
|
||||
uint32_t gte_verts; /* RTPS/RTPT verts in the last completed frame */
|
||||
|
||||
@@ -21,6 +21,8 @@ int gl_renderer_init_context(struct SDL_Window *win);
|
||||
/* Select a retained immutable bank for the next textured submission; zero
|
||||
* selects live VRAM. Emulation/GL owning thread only. Returns 0 if unavailable. */
|
||||
int gl_renderer_select_texture_bank(uint16_t id);
|
||||
/* Retain texture indices while sampling current guest CLUTs (fades/animation). */
|
||||
int gl_renderer_select_texture_bank_live_clut(uint16_t id);
|
||||
int gl_renderer_texture_banks_supported(void);
|
||||
|
||||
/* Set the GL swap interval / vsync mode (1=vsync, 0=immediate, -1=adaptive).
|
||||
|
||||
@@ -117,6 +117,10 @@ void gr_set_draw_offset(int x, int y);
|
||||
* the canonical present path. */
|
||||
int gr_wide_supported(void);
|
||||
void gr_wide_configure(int wide_w, int offset);
|
||||
/* Presentation-only origin and padding, independent of the symmetric budget.
|
||||
* enabled also forbids reusing the canonical center: HUD and world may use
|
||||
* different origins. Backends must flush queued draws before changing this. */
|
||||
void gr_wide_set_view(int enabled, int shift, int pad_left, int pad_right);
|
||||
void gr_wide_set_target(int base_x);
|
||||
void gr_wide_disable_target(void);
|
||||
void gr_wide_clear(int base_x, int y, int h, uint16_t color);
|
||||
@@ -194,6 +198,7 @@ typedef struct GpuRenderBackend {
|
||||
* facade then reports gr_wide_supported() == 0 and the caller keeps the
|
||||
* canonical present). */
|
||||
void (*wide_configure)(int wide_w, int offset);
|
||||
void (*wide_set_view)(int enabled, int shift, int pad_left, int pad_right);
|
||||
void (*wide_set_target)(int base_x);
|
||||
void (*wide_disable_target)(void);
|
||||
void (*wide_clear)(int base_x, int y, int h, uint16_t color);
|
||||
|
||||
@@ -109,6 +109,7 @@ void sw_set_draw_offset(int x, int y);
|
||||
* base_x. wide_w<=0 disables. set_target selects/clears the mirror per back
|
||||
* buffer; render_wide_display is the present source for the displayed buffer. */
|
||||
void sw_wide_configure(int wide_w, int offset);
|
||||
void sw_wide_set_view(int enabled, int shift, int pad_left, int pad_right);
|
||||
void sw_wide_set_target(int base_x);
|
||||
void sw_wide_disable_target(void);
|
||||
void sw_wide_clear(int base_x, int y, int h, uint16_t color);
|
||||
|
||||
@@ -523,12 +523,15 @@ bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void));
|
||||
* select it by id, and it runs only while a resolved plan activates that id. */
|
||||
bool mod_register_function_entry_plugin(const std::string& id, uint32_t address,
|
||||
PSXModFunctionEntryCallback callback);
|
||||
bool mod_register_function_filter_plugin(const std::string& id, uint32_t address,
|
||||
PSXModFunctionFilterCallback callback);
|
||||
bool mod_plugin_registered(const std::string& id);
|
||||
void mod_invoke_activation_plugin(const std::string& id);
|
||||
void mod_invoke_vblank_plugin(const std::string& id);
|
||||
struct ModFunctionEntryHook {
|
||||
uint32_t address = 0;
|
||||
PSXModFunctionEntryCallback callback = nullptr;
|
||||
PSXModFunctionFilterCallback filter = nullptr;
|
||||
};
|
||||
/* Hooks one implementation registered, in registration order. mod_runtime
|
||||
* flattens these into an address table when the plan's plugins activate. */
|
||||
|
||||
@@ -11,6 +11,10 @@ typedef void (*PSXModActivationCallback)(void);
|
||||
struct CPUState;
|
||||
typedef void (*PSXModFunctionEntryCallback)(struct CPUState* cpu,
|
||||
uint32_t address);
|
||||
/* Return nonzero to finish this opt-in function with the callback's return
|
||||
* registers. The runtime publishes pc=$ra; zero executes the original body. */
|
||||
typedef int (*PSXModFunctionFilterCallback)(struct CPUState* cpu,
|
||||
uint32_t address);
|
||||
|
||||
/*
|
||||
* Register a trusted, statically linked plugin implementation. Package
|
||||
@@ -27,10 +31,15 @@ int psx_mod_register_function_entry_plugin(
|
||||
* interpreted entry, so the hook contract does not depend on the backend.
|
||||
* Hooks match by code address (segment bits ignored) and run only for plugins
|
||||
* the active plan resolved; the table is rebuilt at plugin activation. */
|
||||
void psx_mod_function_entry(struct CPUState* cpu, uint32_t address);
|
||||
int psx_mod_register_function_filter_plugin(
|
||||
const char* id, uint32_t address, PSXModFunctionFilterCallback callback);
|
||||
int psx_mod_function_entry(struct CPUState* cpu, uint32_t address);
|
||||
/* Active function-entry hook count (0 = none). Hot callers test it before the
|
||||
* call, so a run without an active hook pays one load per interpreted entry. */
|
||||
extern uint32_t g_psx_mod_function_entry_hooks;
|
||||
/* Entry callbacks can make nested guest calls while retaining host registers.
|
||||
* Save/load and rewind must wait until that host context has returned. */
|
||||
int psx_mod_function_entry_active(void);
|
||||
|
||||
/* Narrow guest services available to trusted plugin callbacks. */
|
||||
int psx_mod_game_started(void);
|
||||
|
||||
@@ -112,7 +112,9 @@
|
||||
* DLLs used to keep a private copy, so an overlay store left the host's
|
||||
* value naming an older store. The DLL now writes through this pointer
|
||||
* (cpu_state.h maps the name in PSX_OVERLAY_DLL_BUILD). */
|
||||
#define PSX_OVERLAY_ABI_VERSION 24
|
||||
/* v25: function-entry callbacks may return an opt-in filter result while
|
||||
* retaining the v24 last_store_pc pointer. */
|
||||
#define PSX_OVERLAY_ABI_VERSION 25
|
||||
|
||||
/* Process-lifetime overlay candidate capacity. Every accepted manifest F
|
||||
* record consumes one slot, even when another DLL carries an identical
|
||||
@@ -350,7 +352,7 @@ typedef struct OverlayCallbacks {
|
||||
* emitter change (Phase 2 of docs/ENHANCEMENTS.md G1 value propagation). */
|
||||
const PGXPHooks *pgxp;
|
||||
int32_t (*ws_screen_x_bound)(int32_t vanilla);
|
||||
void (*mod_function_entry)(CPUState *cpu, uint32_t address);
|
||||
int (*mod_function_entry)(CPUState *cpu, uint32_t address);
|
||||
/* The host's g_debug_last_store_pc (ABI v24; see the version-history note
|
||||
* above). Overlay stores write it exactly as static and interpreted stores
|
||||
* do. NULL leaves the DLL writing a private copy nothing reads. */
|
||||
|
||||
@@ -247,9 +247,9 @@ int32_t psx_ws_player_x_bound(int32_t vanilla) {
|
||||
int32_t psx_ws_screen_x_bound(int32_t vanilla) {
|
||||
return g_cbs.ws_screen_x_bound ? g_cbs.ws_screen_x_bound(vanilla) : vanilla;
|
||||
}
|
||||
void psx_mod_function_entry(CPUState *cpu, uint32_t address) {
|
||||
int psx_mod_function_entry(CPUState *cpu, uint32_t address) {
|
||||
overlay_flush_cycles();
|
||||
if (g_cbs.mod_function_entry) g_cbs.mod_function_entry(cpu, address);
|
||||
return g_cbs.mod_function_entry ? g_cbs.mod_function_entry(cpu, address) : 0;
|
||||
}
|
||||
int32_t psx_ws_depth_bound(int32_t imm) {
|
||||
return g_cbs.ws_depth_bound ? g_cbs.ws_depth_bound(imm) : imm;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef PSXRECOMP_WS_SCREEN_MASK_H
|
||||
#define PSXRECOMP_WS_SCREEN_MASK_H
|
||||
#include <stdint.h>
|
||||
|
||||
/* Extend only an off-screen vertical boundary of a title-identified mask.
|
||||
* The interior/slanted light boundary stays at its original coordinates.
|
||||
* Result is wholly outside canonical VRAM; no triangle size limit applies. */
|
||||
typedef struct { int x, y, w, h; } WsScreenMaskBand;
|
||||
static inline int ws_screen_mask_band(const int32_t x[4], const int32_t y[4],
|
||||
int width, int height, int margin,
|
||||
WsScreenMaskBand *out) {
|
||||
if (!out || width <= 0 || height <= 0 || margin <= 0 ||
|
||||
y[0] != y[1] || y[2] != y[3] || y[0] != 0 || y[2] != height)
|
||||
return 0;
|
||||
if (x[0] == x[2] && x[0] <= 0 && x[1] > x[0] && x[3] > x[0] &&
|
||||
x[0] > -margin) {
|
||||
*out = (WsScreenMaskBand){-margin, 0, margin + x[0], height};
|
||||
return 1;
|
||||
}
|
||||
if (x[1] == x[3] && x[1] >= width && x[0] < x[1] && x[2] < x[1] &&
|
||||
x[1] < width + margin) {
|
||||
*out = (WsScreenMaskBand){x[1], 0, width + margin - x[1], height};
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifndef PSX_WS_VIEW_ANCHOR_H
|
||||
#define PSX_WS_VIEW_ANCHOR_H
|
||||
|
||||
/* Host presentation geometry. Camera bounds describe the native view's left
|
||||
* edge; the room's right edge is camera_max + native_width. */
|
||||
typedef struct WsViewAnchor {
|
||||
int left, right;
|
||||
int shift;
|
||||
int pad_left, pad_right;
|
||||
} WsViewAnchor;
|
||||
|
||||
static inline WsViewAnchor ws_view_anchor(int extra, int camera,
|
||||
int camera_min, int camera_max) {
|
||||
WsViewAnchor v = {extra, extra, 0, 0, 0};
|
||||
if (extra <= 0 || camera_min > camera_max ||
|
||||
camera < camera_min || camera > camera_max) return v;
|
||||
int avail_left = camera - camera_min;
|
||||
int avail_right = camera_max - camera;
|
||||
int total = 2 * extra;
|
||||
v.left = avail_left < extra ? avail_left : extra;
|
||||
v.right = avail_right < total - v.left ? avail_right : total - v.left;
|
||||
v.left = avail_left < total - v.right ? avail_left : total - v.right;
|
||||
int padding = total - v.left - v.right;
|
||||
v.pad_left = padding / 2;
|
||||
v.pad_right = padding - v.pad_left;
|
||||
v.shift = v.left + v.pad_left - extra;
|
||||
return v;
|
||||
}
|
||||
#endif
|
||||
@@ -1,11 +1,10 @@
|
||||
/*
|
||||
* audio_trace.c - always-on audio observability rings. See audio_trace.h.
|
||||
*
|
||||
* Memory: 3 PCM taps x 2^22 stereo frames x 4 bytes = 48 MiB static, plus a
|
||||
* 2^19-entry event ring (16 MiB). Static BSS, zero cost until touched; the
|
||||
* recording hot path is a memcpy per audio block and is compiled into
|
||||
* Release/production builds too (always-on ring-buffer discipline — probes
|
||||
* query history, they never arm recording).
|
||||
* Debug-tool builds keep 4 PCM taps x 2^22 stereo frames x 4 bytes = 64 MiB
|
||||
* static history, plus a 2^19-entry event ring (16 MiB). Production
|
||||
* PSX_NO_DEBUG_TOOLS builds retain counters/events but compile out PCM history
|
||||
* storage and WAV dumps.
|
||||
*/
|
||||
#include "audio_trace.h"
|
||||
|
||||
@@ -14,8 +13,10 @@
|
||||
#include <string.h>
|
||||
|
||||
/* 2^22 frames @ 44100 ~= 95 s per tap. Power of two so wrap is a mask. */
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
#define PCM_RING_FRAMES (1u << 22)
|
||||
#define PCM_RING_MASK (PCM_RING_FRAMES - 1u)
|
||||
#endif
|
||||
|
||||
#define EV_RING_CAP (1u << 19)
|
||||
#define EV_RING_MASK (EV_RING_CAP - 1u)
|
||||
@@ -26,7 +27,9 @@
|
||||
#define AUDIBLE_ABS 256
|
||||
|
||||
typedef struct {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
int16_t pcm[PCM_RING_FRAMES * 2];
|
||||
#endif
|
||||
_Atomic uint64_t head; /* frames ever written; ring pos = head & MASK */
|
||||
uint64_t nonzero;
|
||||
uint64_t audible;
|
||||
@@ -98,9 +101,11 @@ void audio_trace_pcm(int tap, const int16_t *stereo, int frames)
|
||||
for (int f = 0; f < frames; f++) {
|
||||
int16_t l = stereo[f * 2 + 0];
|
||||
int16_t r = stereo[f * 2 + 1];
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
uint32_t pos = (uint32_t)((head + (uint64_t)f) & PCM_RING_MASK);
|
||||
t->pcm[pos * 2 + 0] = l;
|
||||
t->pcm[pos * 2 + 1] = r;
|
||||
#endif
|
||||
int32_t al = l < 0 ? -(int32_t)l : l;
|
||||
int32_t ar = r < 0 ? -(int32_t)r : r;
|
||||
int32_t a = al > ar ? al : ar;
|
||||
@@ -187,6 +192,7 @@ uint32_t audio_trace_events_get(AudioTraceEvent *out, uint32_t max)
|
||||
|
||||
/* ---- WAV dump --------------------------------------------------------- */
|
||||
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
static void wav_write_header(FILE *fp, uint32_t data_bytes, uint32_t rate)
|
||||
{
|
||||
uint8_t h[44];
|
||||
@@ -261,3 +267,14 @@ int64_t audio_trace_dump_wav(int tap, const char *path,
|
||||
fclose(fp);
|
||||
return (int64_t)count;
|
||||
}
|
||||
#else
|
||||
int64_t audio_trace_dump_wav(int tap, const char *path,
|
||||
int64_t start, uint64_t count)
|
||||
{
|
||||
(void)tap;
|
||||
(void)path;
|
||||
(void)start;
|
||||
(void)count;
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -5379,6 +5379,9 @@ static void handle_gpu_state(int id, const char *json)
|
||||
"\"present_native_43\":%d,\"x_margin\":%d,"
|
||||
"\"activation_margin\":%d,\"squash\":[%d,%d],"
|
||||
"\"mode\":%d,\"nw_extra\":%d,"
|
||||
"\"view_anchor\":%d,\"view_left\":%d,\"view_right\":%d,"
|
||||
"\"view_shift\":%d,\"view_padding\":[%d,%d],"
|
||||
"\"bg2d_generations\":%u,"
|
||||
"\"cur_frame\":%llu,\"last_tag_frame\":%u,\"last_3d_frame\":%u,"
|
||||
"\"gte_verts\":%u,\"last_world3d_frame\":%u,"
|
||||
"\"ovh_prims\":%u,\"last_ovh_frame\":%u,"
|
||||
@@ -5409,6 +5412,9 @@ static void handle_gpu_state(int id, const char *json)
|
||||
ws.present_native_43, ws.x_margin, ws.activation_margin,
|
||||
ws.xnum, ws.xden,
|
||||
ws.mode, ws.nw_extra,
|
||||
ws.view_anchor, ws.view_left, ws.view_right, ws.view_shift,
|
||||
ws.view_pad_left, ws.view_pad_right,
|
||||
ws.bg2d_generations,
|
||||
(unsigned long long)ws.cur_frame, ws.last_tag_frame,
|
||||
ws.last_3d_frame, ws.gte_verts, ws.last_world3d_frame,
|
||||
ws.ovh_prims, ws.last_ovh_frame,
|
||||
|
||||
@@ -2038,7 +2038,9 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->gpr[rt] = widened;
|
||||
else
|
||||
cpu->gpr[rt] = a + (uint32_t)simm
|
||||
+ (psx_ws_is_cull_bias_site(pc)
|
||||
+ (psx_ws_is_cull_bias_lower_site(pc)
|
||||
? 0u - (uint32_t)psx_ws_activation_margin()
|
||||
: psx_ws_is_cull_bias_site(pc)
|
||||
? (uint32_t)psx_ws_activation_margin() : 0u);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, (uint32_t)simm);
|
||||
cpu->gpr[0] = 0;
|
||||
@@ -2054,7 +2056,9 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->gpr[rt] = (uint32_t)psx_ws_screen_x_bound(simm);
|
||||
else
|
||||
cpu->gpr[rt] = a + (uint32_t)simm
|
||||
+ (psx_ws_is_cull_bias_site(pc)
|
||||
+ (psx_ws_is_cull_bias_lower_site(pc)
|
||||
? 0u - (uint32_t)psx_ws_activation_margin()
|
||||
: psx_ws_is_cull_bias_site(pc)
|
||||
? (uint32_t)psx_ws_activation_margin() : 0u);
|
||||
psx_pgxp_alu(cpu, insn, cpu->gpr[rt], a, (uint32_t)simm);
|
||||
cpu->gpr[0] = 0;
|
||||
@@ -2972,8 +2976,8 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
* to one load per interpreted entry. */
|
||||
{
|
||||
extern uint32_t g_psx_mod_function_entry_hooks;
|
||||
extern void psx_mod_function_entry(CPUState *cpu, uint32_t address);
|
||||
if (g_psx_mod_function_entry_hooks) psx_mod_function_entry(cpu, addr);
|
||||
extern int psx_mod_function_entry(CPUState *cpu, uint32_t address);
|
||||
if (g_psx_mod_function_entry_hooks && psx_mod_function_entry(cpu, addr)) return 1;
|
||||
}
|
||||
|
||||
/* Per-PC entry counter (visible via dirty_ram_stats). */
|
||||
@@ -3369,9 +3373,14 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
* function-entry hooks as a surfaced interpreter entry. */
|
||||
{
|
||||
extern uint32_t g_psx_mod_function_entry_hooks;
|
||||
extern void psx_mod_function_entry(CPUState *, uint32_t);
|
||||
if (g_psx_mod_function_entry_hooks)
|
||||
psx_mod_function_entry(cpu, target);
|
||||
extern int psx_mod_function_entry(CPUState *, uint32_t);
|
||||
if (g_psx_mod_function_entry_hooks &&
|
||||
psx_mod_function_entry(cpu, target)) {
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
g_dirty_interp_chain_target = cpu->pc;
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
}
|
||||
pc = target;
|
||||
current_page = target_phys >> 12;
|
||||
|
||||
+316
-26
@@ -21,6 +21,7 @@
|
||||
#include "gpu_sw_renderer.h"
|
||||
#include "gpu_vram_dirty.h"
|
||||
#include "gpu_render.h"
|
||||
#include "ws_view_anchor.h"
|
||||
#include "text_xlate.h"
|
||||
#include "crash_trace.h"
|
||||
#include "debug_server.h"
|
||||
@@ -42,6 +43,7 @@
|
||||
#include "ws_prepass_guard.h"
|
||||
#include "ws_hud_anchor.h"
|
||||
#include "ws_repeat_rect.h"
|
||||
#include "ws_screen_mask.h"
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -182,6 +184,7 @@ typedef struct { uint32_t key; uint32_t stamp; int32_t anchor_x; } WsTag;
|
||||
static WsTag ws_tags[WS_TAG_BUCKETS];
|
||||
static WsHudAnchorTag ws_hud_anchor_tags[WS_HUD_ANCHOR_TABLE_SIZE];
|
||||
static WsHudAnchorTag ws_reveal_clear_tags[WS_HUD_ANCHOR_TABLE_SIZE];
|
||||
static WsHudAnchorTag ws_screen_mask_tags[WS_HUD_ANCHOR_TABLE_SIZE];
|
||||
static WsRepeatRectTag ws_repeat_rect_tags[WS_REPEAT_RECT_TAG_TABLE_SIZE];
|
||||
static uint32_t ws_last_tag_stamp = (uint32_t)-1000; /* frame of newest tag */
|
||||
static uint32_t ws_last_3d_stamp = (uint32_t)-1000; /* frame of newest shaded prim (diagnostic) */
|
||||
@@ -413,6 +416,7 @@ static void ws_reset_scene_history(void) {
|
||||
static uint32_t s_ws_fmv_frame_cache = 0xFFFFFFFFu;
|
||||
static int s_ws_fmv_cached = 0;
|
||||
static PSXModRetainedScenePredicate s_ws_retained_scene_predicate;
|
||||
static int (*s_ws_native_scene_predicate)(void);
|
||||
static WsSceneHold s_ws_scene_hold;
|
||||
static uint32_t ws_display_origin(void);
|
||||
|
||||
@@ -421,8 +425,14 @@ void psx_mod_set_retained_scene_predicate(PSXModRetainedScenePredicate predicate
|
||||
ws_scene_hold_reset(&s_ws_scene_hold);
|
||||
}
|
||||
|
||||
void gpu_ws_set_native_scene_predicate(int (*predicate)(void)) {
|
||||
s_ws_native_scene_predicate = predicate;
|
||||
}
|
||||
|
||||
int gpu_ws_present_native_43(void) {
|
||||
if (!ws_engaged()) return 0;
|
||||
if (ws_mode == 2 && s_ws_native_scene_predicate && s_ws_native_scene_predicate())
|
||||
return 1;
|
||||
int game_mode = ws_game_mode();
|
||||
if (!game_mode) ws_scene_latch.confirmed = 0;
|
||||
int native_43 = !game_mode || ws_2d_only_scene();
|
||||
@@ -519,6 +529,50 @@ static int ws_nw_offset(void) {
|
||||
}
|
||||
int ws_nw_extra(void) { return 2 * ws_nw_offset(); }
|
||||
|
||||
static uint32_t ws_view_camera_addr, ws_view_min_addr, ws_view_max_addr, ws_view_active_addr;
|
||||
static int ws_view_bounds_override, ws_view_bounds_min, ws_view_bounds_max;
|
||||
static WsViewAnchor ws_view;
|
||||
static uint32_t ws_view_frame = UINT32_MAX;
|
||||
void gpu_ws_set_view_anchor(uint32_t camera, uint32_t min, uint32_t max, uint32_t active) {
|
||||
ws_view_camera_addr = camera;
|
||||
ws_view_min_addr = min;
|
||||
ws_view_max_addr = max;
|
||||
ws_view_active_addr = active;
|
||||
ws_view_bounds_override = 0;
|
||||
ws_view_frame = UINT32_MAX;
|
||||
memset(&ws_view, 0, sizeof(ws_view));
|
||||
gr_wide_set_view(0, 0, 0, 0);
|
||||
}
|
||||
void gpu_ws_set_view_bounds_override(int enabled, int minimum, int maximum) {
|
||||
ws_view_bounds_override = enabled && minimum <= maximum;
|
||||
ws_view_bounds_min = minimum;
|
||||
ws_view_bounds_max = maximum;
|
||||
}
|
||||
static int ws_view_enabled(void) {
|
||||
return ws_view_camera_addr && ws_view_min_addr && ws_view_max_addr &&
|
||||
ws_view_active_addr && ws_native_wide_active() && ws_disp_w() <= 384;
|
||||
}
|
||||
static WsViewAnchor ws_view_current(void) {
|
||||
WsViewAnchor centered = {ws_nw_offset(), ws_nw_offset(), 0, 0, 0};
|
||||
if (!ws_view_enabled() || (uint32_t)s_frame_count - ws_view_frame > 1u)
|
||||
return centered;
|
||||
return ws_view;
|
||||
}
|
||||
/* Sample at the BG renderer setup, after guest camera clamp and before either
|
||||
* ring refill or packet generation. Intermediate camera writes are ignored. */
|
||||
static void ws_view_sample(void) {
|
||||
if (!ws_view_enabled()) return;
|
||||
ws_view = ws_view_anchor(ws_nw_offset(),
|
||||
(int16_t)psx_read_half(ws_view_camera_addr),
|
||||
ws_view_bounds_override ? ws_view_bounds_min : (int16_t)psx_read_half(ws_view_min_addr),
|
||||
ws_view_bounds_override ? ws_view_bounds_max : (int16_t)psx_read_half(ws_view_max_addr));
|
||||
if (!psx_read_byte(ws_view_active_addr)) {
|
||||
ws_view.left = ws_view.right = ws_nw_offset();
|
||||
ws_view.shift = ws_view.pad_left = ws_view.pad_right = 0;
|
||||
}
|
||||
ws_view_frame = (uint32_t)s_frame_count;
|
||||
}
|
||||
|
||||
int ws_nw_present_width(void) {
|
||||
int wide_w = 0;
|
||||
if (ws_local_viewport_layout(NULL, NULL, &wide_w, NULL))
|
||||
@@ -629,6 +683,18 @@ void gpu_ws_set_explicit_cull_sites(const uint32_t *bias, int nbias,
|
||||
int psx_ws_is_cull_bias_site(uint32_t pc) {
|
||||
return ws_explicit_site(ws_explicit_bias_sites, ws_explicit_bias_n, pc);
|
||||
}
|
||||
static uint32_t ws_explicit_bias_lower_sites[WS_EXPLICIT_CULL_SITES_MAX];
|
||||
static int ws_explicit_bias_lower_n = 0;
|
||||
void gpu_ws_set_bias_lower_cull_sites(const uint32_t *sites, int nsites) {
|
||||
if (nsites < 0) nsites = 0;
|
||||
if (nsites > WS_EXPLICIT_CULL_SITES_MAX) nsites = WS_EXPLICIT_CULL_SITES_MAX;
|
||||
ws_explicit_bias_lower_n = nsites;
|
||||
for (int i = 0; i < nsites; i++)
|
||||
ws_explicit_bias_lower_sites[i] = sites[i] & 0x1FFFFFFFu;
|
||||
}
|
||||
int psx_ws_is_cull_bias_lower_site(uint32_t pc) {
|
||||
return ws_explicit_site(ws_explicit_bias_lower_sites, ws_explicit_bias_lower_n, pc);
|
||||
}
|
||||
int psx_ws_is_cull_slti_site(uint32_t pc) {
|
||||
return ws_explicit_site(ws_explicit_slti_sites, ws_explicit_slti_n, pc);
|
||||
}
|
||||
@@ -1145,7 +1211,10 @@ int psx_ws_x_margin(void) {
|
||||
* returning zero until the first 3D frame permanently bakes a 4:3 frustum
|
||||
* into those lists. */
|
||||
if (ws_native_wide_configured())
|
||||
return ws_nw_configured_offset() + ws_cull_guard_pixels;
|
||||
/* A constant envelope covers both possible edge origins. Do not shrink
|
||||
* actor activation as the view slides, or prebuilt spawn lists go stale. */
|
||||
return ws_nw_configured_offset() * (ws_view_camera_addr ? 2 : 1)
|
||||
+ ws_cull_guard_pixels;
|
||||
if (!ws_active()) return 0;
|
||||
return (160 * (ws_xden - ws_xnum) + ws_xnum / 2) / ws_xnum
|
||||
+ ws_cull_guard_pixels;
|
||||
@@ -1214,6 +1283,108 @@ static uint32_t g_bg2d_layer_struct_stride = 0x54;
|
||||
static uint32_t g_bg2d_packet_cap = 1000;
|
||||
static int g_bg2d_native_cols = 21;
|
||||
static int g_bg2d_parent_links = 1;
|
||||
static uint32_t g_bg2d_host_base, g_bg2d_host_size;
|
||||
static uint32_t g_bg2d_host_generation;
|
||||
|
||||
void gpu_ws_bg2d_set_host_arena(uint32_t base, uint32_t size) {
|
||||
g_bg2d_host_base = base & 0x1fffffffu;
|
||||
g_bg2d_host_size = size >= 32u && !(base & 31u) ? size : 0u;
|
||||
}
|
||||
|
||||
static uint32_t ws_bg2d_host_packet(void) {
|
||||
uint32_t command = gp0_cmd_source_addr & 0x1fffffffu;
|
||||
if (!g_bg2d_host_size || command < g_bg2d_host_base + 4u) return 0;
|
||||
uint32_t offset = command - g_bg2d_host_base - 4u;
|
||||
if ((offset & 31u) || offset > g_bg2d_host_size - 32u) return 0;
|
||||
uint32_t packet = command - 4u;
|
||||
uint32_t magic = psx_read_word(packet + 28u) & ~GPU_WS_BG2D_MIRROR_X;
|
||||
return magic == GPU_WS_BG2D_PACKET_MAGIC || magic == GPU_WS_BG2D_BANK_PACKET_MAGIC ? packet : 0;
|
||||
}
|
||||
#define WS_VIEW_LAYERS 8
|
||||
static int ws_view_layer = -1;
|
||||
static unsigned ws_view_independent_mask;
|
||||
static struct {
|
||||
uint32_t begin, end, frame;
|
||||
WsViewAnchor view;
|
||||
} ws_view_layers[WS_VIEW_LAYERS];
|
||||
|
||||
static WsViewAnchor ws_bg2d_layer_view(int layer) {
|
||||
WsViewAnchor v = ws_view_current();
|
||||
if (!ws_view_enabled() || layer < 0 || layer >= WS_VIEW_LAYERS ||
|
||||
!(ws_view_independent_mask & (1u << layer))) return v;
|
||||
uint32_t b = g_bg2d_layer_base + (unsigned)layer * g_bg2d_layer_struct_stride;
|
||||
if ((int8_t)psx_read_byte(b + 0x52) >= 0) return v;
|
||||
int sx = (int16_t)psx_read_half(b + 0xa);
|
||||
int lo = psx_read_byte(b + 0x4d) * 256;
|
||||
int hi = (psx_read_byte(b + 0x4e) + 1) * 256;
|
||||
int width = ws_disp_w(), extra = ws_nw_offset();
|
||||
int frame_width = width + 2 * extra;
|
||||
/* Keep the foreground's desired origin until it would expose void in
|
||||
* this slower layer. Shift that layer as a whole; do not stretch tiles. */
|
||||
if (hi - lo >= frame_width && sx >= lo && sx + width <= hi) {
|
||||
int origin = extra + v.shift;
|
||||
int max_origin = sx - lo;
|
||||
int min_origin = frame_width - (hi - sx);
|
||||
if (origin > max_origin) origin = max_origin;
|
||||
if (origin < min_origin) origin = min_origin;
|
||||
v.shift = origin - extra;
|
||||
v.left = origin;
|
||||
v.right = 2 * extra - origin;
|
||||
} else if (g_bg2d_host_size && hi > lo && hi - lo < frame_width) {
|
||||
int pad = frame_width - (hi - lo);
|
||||
v.pad_left = pad / 2;
|
||||
v.pad_right = pad - v.pad_left;
|
||||
v.shift = sx - lo + v.pad_left - extra;
|
||||
v.left = sx > lo ? sx - lo : 0;
|
||||
v.right = hi > sx + width ? hi - sx - width : 0;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
void gpu_ws_bg2d_begin_view_layer(unsigned layer, uint32_t packet, unsigned independent_mask) {
|
||||
if (g_bg2d_host_size) {
|
||||
ws_view_sample();
|
||||
if (layer == 0) ++g_bg2d_host_generation;
|
||||
}
|
||||
if (!ws_view_enabled() || layer >= WS_VIEW_LAYERS) return;
|
||||
ws_view_independent_mask = independent_mask;
|
||||
ws_view_layer = (int)layer;
|
||||
ws_view_layers[layer].begin = packet & 0x1fffffffu;
|
||||
ws_view_layers[layer].end = ws_view_layers[layer].begin;
|
||||
ws_view_layers[layer].frame = (uint32_t)s_frame_count;
|
||||
}
|
||||
int gpu_ws_bg2d_get_view(unsigned layer, WsViewAnchor *view) {
|
||||
if (!view || !ws_native_wide_active() || ws_disp_w() > 384) return 0;
|
||||
*view = ws_bg2d_layer_view((int)layer);
|
||||
return ws_disp_w();
|
||||
}
|
||||
void gpu_ws_bg2d_end_view_layer(unsigned layer, uint32_t packet) {
|
||||
if (!ws_view_enabled() || layer >= WS_VIEW_LAYERS) return;
|
||||
ws_view_layers[layer].end = packet & 0x1fffffffu;
|
||||
ws_view_layers[layer].view = ws_bg2d_layer_view((int)layer);
|
||||
ws_view_layer = -1;
|
||||
}
|
||||
static WsViewAnchor ws_view_packet(void) {
|
||||
uint32_t packet = ws_bg2d_host_packet();
|
||||
if (packet) {
|
||||
WsViewAnchor v = ws_view_current();
|
||||
uint32_t metadata = psx_read_word(packet + 16u);
|
||||
v.shift = (psx_read_word(packet + 28u) & ~GPU_WS_BG2D_MIRROR_X) ==
|
||||
GPU_WS_BG2D_BANK_PACKET_MAGIC ? (int16_t)metadata : (int32_t)metadata;
|
||||
v.pad_left = (int32_t)psx_read_word(packet + 20u);
|
||||
v.pad_right = (int32_t)psx_read_word(packet + 24u);
|
||||
v.left = ws_nw_offset() + v.shift - v.pad_left;
|
||||
v.right = 2 * ws_nw_offset() - v.left - v.pad_left - v.pad_right;
|
||||
return v;
|
||||
}
|
||||
if (!ws_view_enabled()) return ws_view_current();
|
||||
uint32_t addr = gp0_cmd_source_addr & 0x1fffffffu;
|
||||
for (unsigned i = 0; i < WS_VIEW_LAYERS; i++)
|
||||
if ((uint32_t)s_frame_count - ws_view_layers[i].frame <= 1u &&
|
||||
addr >= ws_view_layers[i].begin && addr < ws_view_layers[i].end)
|
||||
return ws_view_layers[i].view;
|
||||
return ws_view_current();
|
||||
}
|
||||
|
||||
void gpu_ws_bg2d_configure(uint32_t layer_base, uint32_t ring_base,
|
||||
uint32_t map_size_addr, uint32_t layer_stride_addr,
|
||||
@@ -1234,20 +1405,26 @@ void gpu_ws_bg2d_set_parent_links(int on) {
|
||||
}
|
||||
|
||||
static int ws_bg2d_left_cols(void) {
|
||||
if (g_bg2d_host_size) return 0;
|
||||
if (!ws_native_wide_active()) return 0;
|
||||
/* Only the ~320 gameplay mode; the engine's 512 hi-res mode (title) draws
|
||||
* its own 33 columns and centres itself — never double-shift it. */
|
||||
if (ws_disp_w() > 384) return 0;
|
||||
int off = ws_nw_offset(); /* per-side reveal in screen px */
|
||||
int off = ws_bg2d_layer_view(ws_view_layer).left;
|
||||
if (off <= 0) return 0;
|
||||
return (off + 15) / 16; /* ceil to whole tile columns */
|
||||
}
|
||||
static int ws_bg2d_right_cols(void) {
|
||||
if (g_bg2d_host_size) return 0;
|
||||
if (!ws_native_wide_active() || ws_disp_w() > 384) return 0;
|
||||
return (ws_bg2d_layer_view(ws_view_layer).right + 15) / 16;
|
||||
}
|
||||
/* Column count: base + both-side reveal. */
|
||||
static void mmx6_bg_refill_tick(void); /* defined below (ring-freshness fix) */
|
||||
int psx_ws_bg2d_cols(int base) {
|
||||
g_bg2d_native_cols = base;
|
||||
mmx6_bg_refill_tick();
|
||||
return base + 2 * ws_bg2d_left_cols();
|
||||
return base + ws_bg2d_left_cols() + ws_bg2d_right_cols();
|
||||
}
|
||||
/* Start tile column: refill before the first column is consumed, then begin LEFT
|
||||
* earlier in the ring. The count hook retains the same tick for layouts that load
|
||||
@@ -1270,9 +1447,9 @@ int psx_ws_bg2d_startx(int x) { return x - ws_bg2d_left_cols() * 16; }
|
||||
* Identity (no extra streaming) at 4:3 / 512 hi-res, so the ring is byte-identical
|
||||
* there. The 64-col ring has ample slack (visible ~21 cols) for ±LEFT more. */
|
||||
int psx_ws_bg2d_stream_left(int x) { return x - ws_bg2d_left_cols() * 16; }
|
||||
int psx_ws_bg2d_stream_right(int x) { return x + ws_bg2d_left_cols() * 16; }
|
||||
int psx_ws_bg2d_stream_right(int x) { return x + ws_bg2d_right_cols() * 16; }
|
||||
int psx_ws_bg2d_undercap(int counter, int native_cap) {
|
||||
int cap = ws_bg2d_left_cols() > 0 ? (int)g_bg2d_packet_cap : native_cap;
|
||||
int cap = ws_bg2d_left_cols() + ws_bg2d_right_cols() > 0 ? (int)g_bg2d_packet_cap : native_cap;
|
||||
return counter < cap;
|
||||
}
|
||||
|
||||
@@ -1298,6 +1475,11 @@ void psx_ws_mmx6_bg_stage_init(void) {
|
||||
if (!ws_clear_reveal || ws_mode != 2 || frame == last_frame) return;
|
||||
last_frame = frame;
|
||||
g_mmx6_void_generation++;
|
||||
/* A host tile arena rebuilds its backbuffer every submission. A scrolling
|
||||
* effect can initialize a ring every frame (MMX6 rain and museum layers);
|
||||
* clearing BOTH display bands here erases the already-rendered frontbuffer.
|
||||
* Its next draw already clears reveal strips, using host_generation. */
|
||||
if (g_bg2d_host_size) return;
|
||||
ws_clear_all_reveal_margins();
|
||||
}
|
||||
|
||||
@@ -1453,10 +1635,10 @@ long gpu_ws_mmx6_refill_cols(void) { return g_mmx6_refill_cols; }
|
||||
* No-op at 4:3 / 512 hi-res (left==0). Idempotent over the native columns the engine
|
||||
* already streamed correctly, so re-streaming them is harmless. */
|
||||
void psx_ws_mmx6_bg_refill_all(void) {
|
||||
if (g_bg2d_host_size) return;
|
||||
if (!g_mmx6_freshfix) return;
|
||||
if (!ws_native_wide_active() || ws_disp_w() > 384) return;
|
||||
int left = ws_bg2d_left_cols();
|
||||
if (left <= 0) return;
|
||||
if (ws_nw_offset() <= 0) return;
|
||||
g_mmx6_refill_cols = 0;
|
||||
for (uint32_t layer = 0; layer < g_bg2d_layer_count; layer++) {
|
||||
uint32_t lbase = g_bg2d_layer_base + layer * g_bg2d_layer_struct_stride;
|
||||
@@ -1470,10 +1652,13 @@ void psx_ws_mmx6_bg_refill_all(void) {
|
||||
sx += (int16_t)psx_read_half(pbase + 0xa);
|
||||
sy += (int16_t)psx_read_half(pbase + 0xe);
|
||||
}
|
||||
WsViewAnchor layer_view = ws_bg2d_layer_view((int)layer);
|
||||
int left = (layer_view.left + 15) / 16;
|
||||
int right = (layer_view.right + 15) / 16;
|
||||
int sxr = sx;
|
||||
if (sxr < 0) sxr += 0xf;
|
||||
int start_col = sxr >> 4;
|
||||
for (int ci = -left; ci < g_bg2d_native_cols + left; ci++) {
|
||||
for (int ci = -left; ci < g_bg2d_native_cols + right; ci++) {
|
||||
int world_x = sx + ci * 16;
|
||||
if (world_x < 0) {
|
||||
bg2d_clear_column((int)layer, start_col + ci, sy - 0x10);
|
||||
@@ -1493,6 +1678,7 @@ static void mmx6_bg_refill_tick(void) {
|
||||
uint32_t f = (uint32_t)s_frame_count;
|
||||
if (f == g_mmx6_refill_frame) return;
|
||||
g_mmx6_refill_frame = f;
|
||||
ws_view_sample();
|
||||
psx_ws_mmx6_bg_refill_all();
|
||||
}
|
||||
|
||||
@@ -1861,7 +2047,13 @@ void gpu_ws_get_debug(GpuWsDebug* out) {
|
||||
out->xden = ws_xden;
|
||||
out->mode = ws_mode;
|
||||
out->nw_extra = ws_nw_extra();
|
||||
WsViewAnchor view = ws_view_current();
|
||||
out->view_anchor = ws_view_enabled();
|
||||
out->view_left = view.left; out->view_right = view.right;
|
||||
out->view_shift = view.shift;
|
||||
out->view_pad_left = view.pad_left; out->view_pad_right = view.pad_right;
|
||||
out->cur_frame = s_frame_count;
|
||||
out->bg2d_generations = g_bg2d_host_generation;
|
||||
out->last_tag_frame = ws_last_tag_stamp;
|
||||
out->last_3d_frame = ws_last_3d_stamp;
|
||||
out->gte_verts = ws_gte_prev_verts;
|
||||
@@ -2024,6 +2216,18 @@ void gpu_ws_tag_repeat_rect(uint32_t prim, int32_t period) {
|
||||
period, &guard, (uint32_t)s_frame_count);
|
||||
}
|
||||
|
||||
void gpu_ws_tag_screen_mask_quad(uint32_t prim) {
|
||||
if (!ws_native_wide_configured() || (prim & 3u) || prim > UINT32_MAX - 4u)
|
||||
return;
|
||||
uint32_t words[12], count = 0;
|
||||
if (!ws_hud_command_words(prim + 4u, words, &count) || count != 5u ||
|
||||
((words[0] >> 24) & 0xfdu) != 0x28u) return;
|
||||
WsPrepassPacketGuard guard = ws_prepass_packet_guard(words, count);
|
||||
ws_hud_anchor_insert(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
(prim + 4u) & 0x1ffffcu, 0, &guard,
|
||||
(uint32_t)s_frame_count);
|
||||
}
|
||||
|
||||
static int ws_nw_explicit_hud_delta(int32_t *out_delta) {
|
||||
if (out_delta) *out_delta = 0;
|
||||
if (!ws_native_wide_active() || gp0_cmd_source_addr == 0xFFFFFFFFu)
|
||||
@@ -2458,6 +2662,7 @@ static int ws_nw_left_hud_packet(void) {
|
||||
}
|
||||
static int32_t ws_nw_hud_shift(int32_t x, int32_t w) {
|
||||
if (!ws_native_wide_active()) return 0;
|
||||
if (g_bg2d_host_size && ws_tagged_world_primitive()) return 0;
|
||||
int32_t off = ws_nw_offset();
|
||||
if (off <= 0) return 0;
|
||||
int hud_edge = ws_tagged_hud_edge();
|
||||
@@ -2807,7 +3012,10 @@ void gpu_vertical_split_debug(int *active, int *left_age, int *right_age) {
|
||||
* when native-wide is active and that buffer is a known display buffer; else
|
||||
* disable mirroring for this draw. Called when the draw env changes. */
|
||||
static void ws_nw_sync_target(void) {
|
||||
if (!ws_native_wide_active()) { gr_wide_disable_target(); return; }
|
||||
if (!ws_native_wide_active()) {
|
||||
gr_wide_set_view(0, 0, 0, 0);
|
||||
gr_wide_disable_target(); return;
|
||||
}
|
||||
gr_wide_configure(ws_nw_present_width(), ws_nw_offset());
|
||||
int local_base = 0;
|
||||
if (ws_local_viewport_draw_target(&local_base)) {
|
||||
@@ -2943,6 +3151,7 @@ static void gpu_reset_state(int clear_vram) {
|
||||
gp0_cmd_source_addr = 0xFFFFFFFFu;
|
||||
ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE);
|
||||
ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE);
|
||||
ws_hud_anchor_clear(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE);
|
||||
ws_repeat_rect_tag_clear(ws_repeat_rect_tags);
|
||||
polyline_color = 0;
|
||||
polyline_prev_x = polyline_prev_y = 0;
|
||||
@@ -3617,7 +3826,9 @@ static int32_t sign_extend(uint32_t val, int bits) {
|
||||
|
||||
/* Parse a vertex position word: signed 11-bit X and Y */
|
||||
static void parse_vertex(uint32_t word, int32_t* x, int32_t* y) {
|
||||
*x = sign_extend(word & 0x7FFu, 11);
|
||||
int extended = ws_bg2d_host_packet() || (g_bg2d_host_size &&
|
||||
ws_native_wide_active() && ws_tagged_world_primitive());
|
||||
*x = extended ? (int16_t)word : sign_extend(word & 0x7FFu, 11);
|
||||
*y = sign_extend((word >> 16) & 0x7FFu, 11);
|
||||
}
|
||||
|
||||
@@ -3794,8 +4005,10 @@ static inline void draw_area_host_x_bounds(int32_t *left, int32_t *right) {
|
||||
/* Use the union of the guest draw area and the widescreen mirror.
|
||||
* Wider staging areas may share the framebuffer X origin; clamping them
|
||||
* to the mirror width would drop valid canonical VRAM writes. */
|
||||
int32_t wide_left = (int32_t)draw_area_left - margin;
|
||||
int32_t wide_right = (int32_t)draw_area_left + (int32_t)ws_disp_w() + margin - 1;
|
||||
WsViewAnchor v = ws_view_packet();
|
||||
int32_t wide_left = (int32_t)draw_area_left - margin - (v.shift > 0 ? v.shift : 0);
|
||||
int32_t wide_right = (int32_t)draw_area_left + (int32_t)ws_disp_w() + margin - 1
|
||||
- (v.shift < 0 ? v.shift : 0);
|
||||
if (wide_left < *left) *left = wide_left;
|
||||
if (wide_right > *right) *right = wide_right;
|
||||
}
|
||||
@@ -3946,6 +4159,21 @@ static void gp0_exec_mono_quad(void) {
|
||||
int rej_b = psx_gpu_triangle_oversize(vx, vy, 2, 1, 3);
|
||||
if (rej_a && rej_b) return;
|
||||
|
||||
/* Each added band is outside the original screen and executes in the
|
||||
* mask's own OT position/blend mode. Canonical VRAM clips it away. */
|
||||
if (ws_native_wide_active() && gp0_cmd_source_addr != UINT32_MAX &&
|
||||
ws_hud_anchor_lookup(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE,
|
||||
gp0_cmd_source_addr & 0x1ffffcu, gp0_cmd_buf, 5u,
|
||||
(uint32_t)s_frame_count, NULL)) {
|
||||
WsScreenMaskBand band;
|
||||
if (ws_screen_mask_band(vx, vy, ws_disp_w(), ws_disp_h(),
|
||||
draw_area_wide_x_margin(), &band)) {
|
||||
gr_set_semi_transparency(semi_trans, (int)semi_transparency);
|
||||
gr_draw_flat_rect(band.x + draw_offset_x, band.y + draw_offset_y,
|
||||
band.w, band.h, color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Full-screen filters are commonly encoded as an axis-aligned quad. Drawing
|
||||
* a semi-transparent quad as two independent triangles blends their shared
|
||||
* diagonal twice; render the equivalent rectangle once to avoid that seam.
|
||||
@@ -4614,8 +4842,13 @@ static void gp0_exec_textured_16x16(void) {
|
||||
/* Never suppress MMX6 BG packets at a guessed finite-map boundary. The
|
||||
* classifier cannot distinguish an authored layer entering the reveal from
|
||||
* a stale ring slot; suppressing here caused the stage-start black flicker. */
|
||||
int ws_w = ws_sprt_fixed_transform(&x0, y0, 16);
|
||||
x0 += ws_nw_hud_shift(x0, 16); /* native-wide HUD corner re-anchor (no-op else) */
|
||||
uint32_t host_packet = ws_bg2d_host_packet();
|
||||
int host_tile = host_packet != 0;
|
||||
/* A pending full-width OT can outlive a resize back to 4:3. Keep its
|
||||
* central tiles; the normal draw area clips the extra columns. Dropping
|
||||
* every host packet here would blank that transition frame. */
|
||||
int ws_w = host_tile ? 0 : ws_sprt_fixed_transform(&x0, y0, 16);
|
||||
if (!host_tile) x0 += ws_nw_hud_shift(x0, 16);
|
||||
x0 += draw_offset_x; y0 += draw_offset_y;
|
||||
int u0 = gp0_cmd_buf[2] & 0xFF;
|
||||
int v0 = (gp0_cmd_buf[2] >> 8) & 0xFF;
|
||||
@@ -4627,12 +4860,21 @@ static void gp0_exec_textured_16x16(void) {
|
||||
int dw = (ws_w && ws_w != 16) ? ws_w : 16;
|
||||
if (draw_area_out_rect(x0, y0, dw, 16)) return;
|
||||
}
|
||||
uint16_t bank = host_tile &&
|
||||
(psx_read_word(host_packet + 28u) & ~GPU_WS_BG2D_MIRROR_X) == GPU_WS_BG2D_BANK_PACKET_MAGIC ?
|
||||
(uint16_t)(psx_read_word(host_packet + 16u) >> 16) : 0;
|
||||
if (bank && (gr_backend() != GR_BACKEND_OPENGL ||
|
||||
!gl_renderer_select_texture_bank_live_clut(bank))) return;
|
||||
setup_textured_draw(color24, semi_trans, raw_texture);
|
||||
if (ws_w && ws_w != 16)
|
||||
if (host_tile && (psx_read_word(host_packet + 28u) & GPU_WS_BG2D_MIRROR_X))
|
||||
gr_draw_textured_rect_scaled(x0, y0, 16, 16, u0 + 15, v0, u0 - 1, v0 + 16,
|
||||
clut_x, clut_y, current_texpage());
|
||||
else if (ws_w && ws_w != 16)
|
||||
gr_draw_textured_rect_scaled(x0, y0, ws_w, 16, u0, v0, u0 + 16, v0 + 16,
|
||||
clut_x, clut_y, current_texpage());
|
||||
else
|
||||
gr_draw_textured_rect(x0, y0, 16, 16, u0, v0, clut_x, clut_y, current_texpage());
|
||||
if (bank) (void)gl_renderer_select_texture_bank(0);
|
||||
}
|
||||
|
||||
/* ---- GP0 command execution ---- */
|
||||
@@ -5281,15 +5523,14 @@ static uint32_t gp0_opcode_count[256];
|
||||
|
||||
uint32_t gpu_get_opcode_count(uint8_t op) { return gp0_opcode_count[op]; }
|
||||
|
||||
/* ---- Per-frame GP0 command ring (always-on, queried via debug server) ---- */
|
||||
/* We record every GP0 command (header + up to 6 payload words) with the
|
||||
* frame number it was issued in. Per CLAUDE.md ring-buffer rule: capture
|
||||
* is continuous and observers query a window of interest later, not arm-
|
||||
* then-record. ~34 MB at 1M entries. Polyline / long commands get the
|
||||
* first 6 payload words; that's enough for the header + first vertex pair
|
||||
* + first uv/color word for diagnosing per-primitive state. */
|
||||
/* ---- Per-frame GP0 command ring (debug tools only) -----------------------
|
||||
* We record every GP0 command with the frame number it was issued in. Polyline
|
||||
* / long commands get the current public word cap, enough for diagnosing
|
||||
* per-primitive state. Production builds leave the public accessors linkable
|
||||
* but report an unavailable/empty capture. */
|
||||
|
||||
extern uint64_t s_frame_count; /* defined in debug_server.c */
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
extern uint32_t g_debug_last_store_pc; /* defined in debug_server.c */
|
||||
extern uint32_t g_debug_current_func_addr; /* defined in debug_server.c */
|
||||
uint32_t debug_guest_ra(void); /* accessor in debug_server.c (guest $ra) */
|
||||
@@ -5366,10 +5607,6 @@ uint64_t gpu_gp0_ring_total(void) { return gp0_ring_seq; }
|
||||
uint32_t gpu_gp0_ring_capacity(void) { return GP0_RING_CAP; }
|
||||
uint32_t gpu_gp0_ring_max_words(void){ return GPU_GP0_RING_MAX_WORDS; }
|
||||
|
||||
void gpu_set_gp0_source(uint32_t addr) {
|
||||
gp0_next_source_addr = addr;
|
||||
}
|
||||
|
||||
/* Fill `out[0..max_out-1]` with entries from the requested frame; returns
|
||||
* count. Walks from oldest in-buffer to newest so iteration order matches
|
||||
* draw order within a frame. */
|
||||
@@ -5400,6 +5637,30 @@ void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest) {
|
||||
if (out_oldest) *out_oldest = gp0_ring[start].frame;
|
||||
if (out_newest) *out_newest = gp0_ring[newest_idx].frame;
|
||||
}
|
||||
#else
|
||||
static void gp0_ring_record(const uint32_t *words, int n) {
|
||||
(void)words;
|
||||
(void)n;
|
||||
}
|
||||
|
||||
uint64_t gpu_gp0_ring_total(void) { return 0; }
|
||||
uint32_t gpu_gp0_ring_capacity(void) { return 0; }
|
||||
uint32_t gpu_gp0_ring_max_words(void) { return GPU_GP0_RING_MAX_WORDS; }
|
||||
int gpu_gp0_ring_dump_frame(uint32_t frame, GpuGp0RingEntry *out, int max_out) {
|
||||
(void)frame;
|
||||
(void)out;
|
||||
(void)max_out;
|
||||
return 0;
|
||||
}
|
||||
void gpu_gp0_ring_frame_span(uint32_t *out_oldest, uint32_t *out_newest) {
|
||||
if (out_oldest) *out_oldest = 0;
|
||||
if (out_newest) *out_newest = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
void gpu_set_gp0_source(uint32_t addr) {
|
||||
gp0_next_source_addr = addr;
|
||||
}
|
||||
|
||||
/* ---- Draw census ring (ALWAYS-ON) -----------------------------------------
|
||||
* Every drawn primitive (GP0 opcode 0x20-0x7F) records: frame, the prim's DMA
|
||||
@@ -5535,6 +5796,34 @@ static void gp0_execute_command(void) {
|
||||
gp0_ring_record(gp0_cmd_buf, gp0_words_needed);
|
||||
extern void ws_bg_phase_note(uint32_t op);
|
||||
ws_bg_phase_note(opcode); /* native-wide 2D-backdrop stretch: background-phase latch */
|
||||
if (ws_view_camera_addr || g_bg2d_host_size) {
|
||||
WsViewAnchor v = ws_view_current();
|
||||
/* Draw env packets select the back-buffer band after linked-list
|
||||
* begin. Clear narrow-room padding at its first actual draw. */
|
||||
static uint32_t clear_frame[512];
|
||||
static uint32_t clear_generation[512];
|
||||
if (opcode >= 0x20 && opcode <= 0x7F &&
|
||||
(ws_view_enabled() || (g_bg2d_host_size && ws_native_wide_active() && ws_disp_w() <= 384)) &&
|
||||
(g_bg2d_host_size || v.pad_left || v.pad_right) && draw_area_top < 512 &&
|
||||
(g_bg2d_host_size
|
||||
? clear_generation[draw_area_top] != g_bg2d_host_generation
|
||||
: clear_frame[draw_area_top] != (uint32_t)s_frame_count + 1u) &&
|
||||
ws_is_fb_base(draw_area_left)) {
|
||||
clear_frame[draw_area_top] = (uint32_t)s_frame_count + 1u;
|
||||
clear_generation[draw_area_top] = g_bg2d_host_generation;
|
||||
gr_wide_clear_margins((int)draw_area_left, (int)draw_area_top,
|
||||
(int)draw_area_bottom - (int)draw_area_top + 1, 0, 3);
|
||||
}
|
||||
v = ws_view_packet();
|
||||
int32_t hud_delta;
|
||||
/* Existing HUD placement stays fixed in the physical frame. Only the
|
||||
* wide world mirror receives the camera-edge origin and room clip. */
|
||||
if (ws_nw_left_hud_packet() ||
|
||||
(!(g_bg2d_host_size && ws_tagged_world_primitive()) &&
|
||||
ws_nw_explicit_hud_delta(&hud_delta)))
|
||||
v.shift = v.pad_left = v.pad_right = 0;
|
||||
gr_wide_set_view(ws_view_enabled(), v.shift, v.pad_left, v.pad_right);
|
||||
}
|
||||
|
||||
/* Draw-census: capture every drawing primitive's first vertex + camera. */
|
||||
if (opcode >= 0x20 && opcode <= 0x7F) {
|
||||
@@ -6292,6 +6581,7 @@ int gpu_snapshot_read(const uint8_t *p, uint32_t len) {
|
||||
ws_scene_hold_reset(&s_ws_scene_hold);
|
||||
ws_hud_anchor_clear(ws_hud_anchor_tags, WS_HUD_ANCHOR_TABLE_SIZE);
|
||||
ws_hud_anchor_clear(ws_reveal_clear_tags, WS_HUD_ANCHOR_TABLE_SIZE);
|
||||
ws_hud_anchor_clear(ws_screen_mask_tags, WS_HUD_ANCHOR_TABLE_SIZE);
|
||||
ws_repeat_rect_tag_clear(ws_repeat_rect_tags);
|
||||
/* Sync renderer clip/scissor to restored GP0(E3/E4); vars alone leave GL
|
||||
* on a stale draw area after savestate load. */
|
||||
|
||||
@@ -512,6 +512,7 @@ static void present_bezel(int ww, int wh, int lx, int ly, int lw, int lh);
|
||||
static int s_raster_ok = 0; /* full GPU pipeline available */
|
||||
static GLuint s_bank_tex[65536];
|
||||
static GLuint s_selected_bank_tex;
|
||||
static int s_selected_bank_live_clut, s_tb_bank_live_clut;
|
||||
|
||||
/* Authoritative VRAM: hr color texture + stencil (mask bit) FBO. */
|
||||
static GLuint s_hr_tex = 0, s_hr_fbo = 0, s_hr_rb = 0;
|
||||
@@ -555,6 +556,7 @@ static float s_pq[3];
|
||||
|
||||
/* TEX program uniforms. */
|
||||
static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1;
|
||||
static GLint s_uPalette = -1;
|
||||
static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1;
|
||||
static GLint s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1;
|
||||
static GLint s_uLimits = -1;
|
||||
@@ -693,7 +695,8 @@ static int s_wide_suppress = 0;
|
||||
|
||||
/* X-translation (native px) from canonical VRAM space into the active wide
|
||||
* surface: local_x = vram_x - base_x + OFFSET. Same as SW wide_dx(). */
|
||||
static inline int wide_dx(void) { return g_wide_off - g_wide_cur_base; }
|
||||
static int view_enabled, view_shift, view_pad_left, view_pad_right;
|
||||
static inline int wide_dx(void) { return g_wide_off + view_shift - g_wide_cur_base; }
|
||||
|
||||
/* ---- dirty-rect helpers ------------------------------------------------- */
|
||||
static void rect_clear(DirtyRect *r) { r->set = 0; }
|
||||
@@ -1315,6 +1318,7 @@ static const char *TEX_FS =
|
||||
"flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n"
|
||||
"flat in int v_semi; /* GP0 command has semi-transparency enabled */\n"
|
||||
"uniform usampler2D u_vram;\n"
|
||||
"uniform usampler2D u_palette;\n"
|
||||
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
|
||||
"uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n"
|
||||
"uniform ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n"
|
||||
@@ -1326,6 +1330,11 @@ static const char *TEX_FS =
|
||||
" if (any(greaterThanEqual(p, textureSize(u_vram, 0)))) return 0;\n"
|
||||
" return int(texelFetch(u_vram, p, 0).r);\n"
|
||||
"}\n"
|
||||
"int palette_at(int x, int y){\n"
|
||||
" ivec2 p = ivec2(x & 1023, y & 511);\n"
|
||||
" if (any(greaterThanEqual(p, textureSize(u_palette, 0)))) return 0;\n"
|
||||
" return int(texelFetch(u_palette, p, 0).r);\n"
|
||||
"}\n"
|
||||
"int fetch_texel(int u, int v){\n"
|
||||
" u &= 255; v &= 255;\n"
|
||||
" if ((u_twin.x | u_twin.y) != 0) {\n"
|
||||
@@ -1337,10 +1346,10 @@ static const char *TEX_FS =
|
||||
" }\n"
|
||||
" if (v_depth == 0) {\n"
|
||||
" int px = vram_at(v_tpage.x + (u >> 2), v_tpage.y + v);\n"
|
||||
" return vram_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n"
|
||||
" return palette_at(v_clut.x + ((px >> ((u & 3) * 4)) & 0xF), v_clut.y);\n"
|
||||
" } else if (v_depth == 1) {\n"
|
||||
" int px = vram_at(v_tpage.x + (u >> 1), v_tpage.y + v);\n"
|
||||
" return vram_at(v_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), v_clut.y);\n"
|
||||
" return palette_at(v_clut.x + ((px >> ((u & 1) * 8)) & 0xFF), v_clut.y);\n"
|
||||
" }\n"
|
||||
" return vram_at(v_tpage.x + u, v_tpage.y + v);\n"
|
||||
"}\n"
|
||||
@@ -2197,7 +2206,7 @@ static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth,
|
||||
int clut_x, int clut_y) {
|
||||
if (!s_pack_dirty.set) return;
|
||||
int page_w = depth == 0 ? 64 : depth == 1 ? 128 : 256; /* VRAM columns */
|
||||
if (rect_intersects(&s_pack_dirty, tpage_x, tpage_y,
|
||||
if (tpage_x >= 0 && rect_intersects(&s_pack_dirty, tpage_x, tpage_y,
|
||||
tpage_x + page_w - 1, tpage_y + 255)) {
|
||||
flush_flat_batch();
|
||||
flush_tex_batch(); /* queued draws are part of s_pack_dirty — realise them before packing */
|
||||
@@ -2327,7 +2336,9 @@ static void wide_target_begin(int dx, GLint uXoff, GLint uXhalf) {
|
||||
if (sy < 0) { sh += sy; sy = 0; }
|
||||
if (sy + sh > VRAM_H) sh = VRAM_H - sy;
|
||||
if (sh < 0) sh = 0;
|
||||
glScissor(0, sy * s_out_scale, g_wide_w * s_out_scale, sh * s_out_scale);
|
||||
glScissor(view_pad_left * s_out_scale, sy * s_out_scale,
|
||||
(g_wide_w - view_pad_left - view_pad_right) * s_out_scale,
|
||||
sh * s_out_scale);
|
||||
}
|
||||
p_glUniform1f(uXoff, (float)dx);
|
||||
p_glUniform1f(uXhalf, (float)g_wide_w / 2.0f);
|
||||
@@ -2389,13 +2400,13 @@ static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h
|
||||
/* True if [lo,hi] (canonical draw-x) lies strictly inside the 4:3 frame, so the
|
||||
* prim adds nothing to either reveal margin and its mirror can be skipped. */
|
||||
static int mirror_x_center_only(int lo, int hi) {
|
||||
if (!s_wide_fast) return 0;
|
||||
if (!s_wide_fast || view_enabled) return 0;
|
||||
int base = g_wide_cur_base, native_w = g_wide_w - 2 * g_wide_off;
|
||||
if (native_w <= 0) return 0;
|
||||
return (lo >= base) && (hi < base + native_w);
|
||||
}
|
||||
static int mirror_geo_center_only(const int *xs, int n) {
|
||||
if (!s_wide_fast) return 0;
|
||||
if (!s_wide_fast || view_enabled) return 0;
|
||||
int lo = xs[0], hi = xs[0];
|
||||
for (int i = 1; i < n; i++) { if (xs[i] < lo) lo = xs[i]; if (xs[i] > hi) hi = xs[i]; }
|
||||
return mirror_x_center_only(lo, hi);
|
||||
@@ -2844,6 +2855,10 @@ static void flush_tex_batch(void) {
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex);
|
||||
p_glUniform1i(s_uVram, 0);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0 + 1);
|
||||
glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex);
|
||||
p_glUniform1i(s_uPalette, 1);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]);
|
||||
p_glUniform1i(s_uMaskset, s_tb_mask);
|
||||
p_glUniform1i(s_uFilter, s_tb_filter);
|
||||
@@ -3239,6 +3254,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
flush_cpu_upload(); /* if a CPU->VRAM upload is pending it flushes the batch first */
|
||||
if (!s_selected_bank_tex)
|
||||
flush_pack_if_sampling(base_x, base_y, depth, clut_x, clut_y);
|
||||
else if (s_selected_bank_live_clut)
|
||||
flush_pack_if_sampling(-1, 0, depth, clut_x, clut_y);
|
||||
mark_prim_dirty(xs, ys, 3, 1 /* textured */);
|
||||
|
||||
/* Append to the textured batch. Flush first if this prim's blend/mask/twin/
|
||||
@@ -3278,7 +3295,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
!mod_texture_bank_batchable(s_selected_bank_tex != 0, s_mask_check, semi);
|
||||
int reason = -1;
|
||||
if (s_tb_n > 0) {
|
||||
if (s_tb_bank_tex != s_selected_bank_tex) reason = 0;
|
||||
if (s_tb_bank_tex != s_selected_bank_tex || s_tb_bank_live_clut != s_selected_bank_live_clut) reason = 0;
|
||||
else if (isolate) reason = 0;
|
||||
else if (batch_semi != s_tb_semi) reason = 1;
|
||||
else if (s_mask_set != s_tb_mask) reason = 2;
|
||||
@@ -3295,6 +3312,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
if (s_tb_n == 0) { /* opening a batch: capture its keyed state */
|
||||
s_tb_semi = batch_semi; s_tb_mask = s_mask_set; s_tb_filter = s_tex_filter; s_tb_gate = gate;
|
||||
s_tb_bank_tex = s_selected_bank_tex;
|
||||
s_tb_bank_live_clut = s_selected_bank_live_clut;
|
||||
s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy;
|
||||
}
|
||||
float *vp = &s_tb[s_tb_n * TEXV];
|
||||
@@ -4199,6 +4217,7 @@ static int init_gpu_raster(void) {
|
||||
}
|
||||
|
||||
s_uVram = p_glGetUniformLocation(s_tex_prog, "u_vram");
|
||||
s_uPalette = p_glGetUniformLocation(s_tex_prog, "u_palette");
|
||||
s_uTpage = p_glGetUniformLocation(s_tex_prog, "u_tpage");
|
||||
s_uClut = p_glGetUniformLocation(s_tex_prog, "u_clut");
|
||||
s_uDepth = p_glGetUniformLocation(s_tex_prog, "u_depth");
|
||||
@@ -4441,6 +4460,7 @@ int gl_renderer_select_texture_bank(uint16_t id) {
|
||||
uint32_t width, height;
|
||||
const uint16_t* pixels;
|
||||
GLint alignment, row_length;
|
||||
s_selected_bank_live_clut = 0;
|
||||
if (!id) { s_selected_bank_tex = 0; return 1; }
|
||||
if (!gl_renderer_texture_banks_supported()) return 0;
|
||||
if (!s_bank_tex[id]) {
|
||||
@@ -4470,6 +4490,12 @@ int gl_renderer_select_texture_bank(uint16_t id) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int gl_renderer_select_texture_bank_live_clut(uint16_t id) {
|
||||
if (!gl_renderer_select_texture_bank(id)) return 0;
|
||||
s_selected_bank_live_clut = id != 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int gl_renderer_init_context(SDL_Window *win) {
|
||||
s_win = win;
|
||||
s_present_w = 0;
|
||||
@@ -4583,6 +4609,7 @@ void gl_renderer_shutdown(void) {
|
||||
}
|
||||
memset(s_bank_tex, 0, sizeof s_bank_tex);
|
||||
s_selected_bank_tex = s_tb_bank_tex = 0;
|
||||
s_selected_bank_live_clut = s_tb_bank_live_clut = 0;
|
||||
if (s_ctx) {
|
||||
ensure_cpu();
|
||||
SDL_GL_DeleteContext(s_ctx); s_ctx = NULL;
|
||||
@@ -4968,6 +4995,17 @@ static GLuint wide_fbo_for(int base_x) {
|
||||
/* Enable native-wide with a wide width + centering offset (native px), or
|
||||
* disable (wide_w <= 0). Re-allocates if the width changed. Mirrors
|
||||
* sw_wide_configure. */
|
||||
static void glb_wide_set_view(int enabled, int shift, int pad_left, int pad_right) {
|
||||
if (!enabled) shift = pad_left = pad_right = 0;
|
||||
if (view_enabled == enabled && view_shift == shift &&
|
||||
view_pad_left == pad_left && view_pad_right == pad_right) return;
|
||||
flush_flat_batch(); flush_tex_batch();
|
||||
view_enabled = enabled;
|
||||
view_shift = shift;
|
||||
view_pad_left = pad_left;
|
||||
view_pad_right = pad_right;
|
||||
}
|
||||
|
||||
static void glb_wide_configure(int wide_w, int offset) {
|
||||
if (!s_raster_ok) return;
|
||||
double t0 = cw_ms(); s_cw_wide_cfgs++;
|
||||
@@ -5099,6 +5137,7 @@ static int glb_render_wide_display(uint32_t *out, int pitch, int base_x,
|
||||
/* Fold any pending CPU->VRAM uploads into the canonical FBO first (uploads
|
||||
* are never mirrored to wide, but draws after them are; keep op order) and
|
||||
* make sure all wide-FBO draws have completed before the readback. */
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
hiw_flush_queue(); /* windowed: queued wide mirrors */
|
||||
@@ -6864,7 +6903,7 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
* x-translated by the reveal offset. No-op when s_wide_fast is off (then the
|
||||
* mirror drew the full surface, as before). Shared by both present paths. */
|
||||
static void wide_blit_center(GLuint wide_fbo, int base_x, int disp_y, int disp_h) {
|
||||
if (!s_wide_fast || g_wide_w <= 0) return;
|
||||
if (!s_wide_fast || view_enabled || g_wide_w <= 0) return;
|
||||
int native_w = g_wide_w - 2 * g_wide_off;
|
||||
if (native_w <= 0) return;
|
||||
int S = s_out_scale;
|
||||
@@ -7016,6 +7055,7 @@ static const GpuRenderBackend GL_BACKEND = {
|
||||
.set_draw_area = glb_set_draw_area, .get_draw_area = glb_get_draw_area,
|
||||
.set_draw_offset = glb_set_draw_offset,
|
||||
.wide_configure = glb_wide_configure,
|
||||
.wide_set_view = glb_wide_set_view,
|
||||
.wide_set_target = glb_wide_set_target,
|
||||
.wide_disable_target = glb_wide_disable_target,
|
||||
.wide_clear = glb_wide_clear,
|
||||
|
||||
@@ -48,6 +48,7 @@ static const GpuRenderBackend SW_BACKEND = {
|
||||
.get_draw_area = sw_get_draw_area,
|
||||
.set_draw_offset = sw_set_draw_offset,
|
||||
.wide_configure = sw_wide_configure,
|
||||
.wide_set_view = sw_wide_set_view,
|
||||
.wide_set_target = sw_wide_set_target,
|
||||
.wide_disable_target = sw_wide_disable_target,
|
||||
.wide_clear = sw_wide_clear,
|
||||
@@ -167,6 +168,9 @@ int gr_wide_supported(void) { return g_b->render_wide_display != 0; }
|
||||
void gr_wide_configure(int wide_w, int offset) {
|
||||
if (g_b->wide_configure) g_b->wide_configure(wide_w, offset);
|
||||
}
|
||||
void gr_wide_set_view(int enabled, int shift, int pad_left, int pad_right) {
|
||||
if (g_b->wide_set_view) g_b->wide_set_view(enabled, shift, pad_left, pad_right);
|
||||
}
|
||||
void gr_wide_set_target(int base_x) {
|
||||
if (g_b->wide_set_target) g_b->wide_set_target(base_x);
|
||||
}
|
||||
|
||||
@@ -163,19 +163,21 @@ static inline RTarget rt_hires(void) {
|
||||
* into the OTHER buffer's band in the shared wide surface and corrupt it
|
||||
* (top/bottom edge flicker as the buffers flip). Canonical VRAM never showed
|
||||
* this because its draw-area clip already confines it. Scale matches SSAA. */
|
||||
static int view_enabled, view_shift, view_pad_left, view_pad_right;
|
||||
static inline RTarget rt_wide(void) {
|
||||
int s = g_scale;
|
||||
RTarget t;
|
||||
t.buf = g_wide_cur;
|
||||
t.w = g_wide_w * s; t.h = VRAM_HEIGHT * s; t.s = s;
|
||||
t.cx1 = 0; t.cy1 = g_clip_y1 * s;
|
||||
t.cx2 = g_wide_w * s - 1; t.cy2 = g_clip_y2 * s + (s - 1);
|
||||
t.cx1 = view_pad_left * s; t.cy1 = g_clip_y1 * s;
|
||||
t.cx2 = (g_wide_w - view_pad_right) * s - 1;
|
||||
t.cy2 = g_clip_y2 * s + (s - 1);
|
||||
return t;
|
||||
}
|
||||
|
||||
/* X-translation (native px) from canonical VRAM space into the active wide
|
||||
* surface: local_x = vram_x - base_x + OFFSET. */
|
||||
static inline int wide_dx(void) { return g_wide_off - g_wide_cur_base; }
|
||||
static inline int wide_dx(void) { return g_wide_off + view_shift - g_wide_cur_base; }
|
||||
|
||||
/* Native-wide 2D-backdrop X-stretch (SW renderer), mirroring the GL
|
||||
* wide_set_bd_scale exactly. The far 2D backdrop (sprite-tagged tile grid: sky
|
||||
@@ -1768,6 +1770,16 @@ static uint16_t *wide_surf_for(int base_x) {
|
||||
|
||||
/* Enable native-wide with a wide width + centering offset (native px), or
|
||||
* disable (wide_w <= 0). Re-allocates if the width changed. */
|
||||
void sw_wide_set_view(int enabled, int shift, int pad_left, int pad_right) {
|
||||
if (!enabled) shift = pad_left = pad_right = 0;
|
||||
if (view_enabled == enabled && view_shift == shift &&
|
||||
view_pad_left == pad_left && view_pad_right == pad_right) return;
|
||||
view_enabled = enabled;
|
||||
view_shift = shift;
|
||||
view_pad_left = pad_left;
|
||||
view_pad_right = pad_right;
|
||||
}
|
||||
|
||||
void sw_wide_configure(int wide_w, int offset) {
|
||||
if (wide_w <= 0) { wide_free_all(); g_wide_w = 0; g_wide_off = 0; return; }
|
||||
if (wide_w != g_wide_w) wide_free_all();
|
||||
|
||||
@@ -2781,7 +2781,8 @@ static void set_vp_scissor(VkCommandBuffer cb) {
|
||||
/* ---- native-wide compositor helpers ------------------------------------- */
|
||||
/* X-translation (native px) from canonical VRAM space into the active wide
|
||||
* surface: local_x = vram_x - base_x + OFFSET. Same as GL/SW wide_dx(). */
|
||||
static int wide_dx(void) { return s_wide_offset - s_wide_cur_base; }
|
||||
static int view_enabled, view_shift, view_pad_left, view_pad_right;
|
||||
static int wide_dx(void) { return s_wide_offset + view_shift - s_wide_cur_base; }
|
||||
|
||||
static void wide_free_all(void) {
|
||||
int any = 0;
|
||||
@@ -2896,7 +2897,8 @@ static void wide_pass_begin(VkCommandBuffer cb) {
|
||||
int y1 = s_da_y1 < 0 ? 0 : s_da_y1;
|
||||
int y2 = s_da_y2 >= VRAM_H ? VRAM_H - 1 : s_da_y2;
|
||||
int hh = y2 - y1 + 1; if (hh < 0) hh = 0;
|
||||
VkRect2D sc = { { 0, y1 * S }, { (uint32_t)(s_wide_w * S), (uint32_t)(hh * S) } };
|
||||
VkRect2D sc = { { view_pad_left * S, y1 * S },
|
||||
{ (uint32_t)((s_wide_w - view_pad_left - view_pad_right) * S), (uint32_t)(hh * S) } };
|
||||
p_vkCmdSetScissor(cb, 0, 1, &sc);
|
||||
}
|
||||
|
||||
@@ -3375,6 +3377,17 @@ static void vkb_copy_rect(int sx,int sy,int dx,int dy,int w,int h){
|
||||
* disable (wide_w <= 0). No-ops when nothing changed (gpu.c calls this on
|
||||
* every draw-area set via ws_nw_sync_target — Part-A lesson: the wide entry
|
||||
* points sit inside guest emulation, keep the common path free). */
|
||||
static void vkb_wide_set_view(int enabled, int shift, int pad_left, int pad_right) {
|
||||
if (!enabled) shift = pad_left = pad_right = 0;
|
||||
if (view_enabled == enabled && view_shift == shift &&
|
||||
view_pad_left == pad_left && view_pad_right == pad_right) return;
|
||||
flush_tex_batch(); flush_geometry();
|
||||
view_enabled = enabled;
|
||||
view_shift = shift;
|
||||
view_pad_left = pad_left;
|
||||
view_pad_right = pad_right;
|
||||
}
|
||||
|
||||
static void vkb_wide_configure(int wide_w, int offset) {
|
||||
if (!s_ready) return;
|
||||
if (wide_w == s_wide_w && offset == s_wide_offset) return;
|
||||
@@ -3562,6 +3575,7 @@ static const GpuRenderBackend VK_BACKEND = {
|
||||
.get_draw_area = vkb_get_draw_area,
|
||||
.set_draw_offset = vkb_set_draw_offset,
|
||||
.wide_configure = vkb_wide_configure,
|
||||
.wide_set_view = vkb_wide_set_view,
|
||||
.wide_set_target = vkb_wide_set_target,
|
||||
.wide_disable_target = vkb_wide_disable_target,
|
||||
.wide_clear = vkb_wide_clear,
|
||||
|
||||
@@ -642,6 +642,8 @@ uint32_t psx_irq_resume_context_snapshot_pc(void)
|
||||
|
||||
int psx_irq_resume_context_snapshot_safe_at(uint32_t resume_pc)
|
||||
{
|
||||
extern int psx_mod_function_entry_active(void);
|
||||
if (psx_mod_function_entry_active()) return 0;
|
||||
if (g_cosim_dirty_pump_site == 0)
|
||||
return 1;
|
||||
|
||||
|
||||
+77
-39
@@ -418,9 +418,11 @@ static void apply_offline_pad_count(int game_players, bool multitap_enabled)
|
||||
g_offline_pad_count = n;
|
||||
}
|
||||
/* ARGB8888 staging buffer. The 576-row maximum preserves the full interlaced
|
||||
* PAL active canvas. Allocated once the supersampling scale is known. */
|
||||
* PAL active canvas; wide frames can require more than the canonical width. */
|
||||
static uint32_t* sdl_pixel_buf = nullptr;
|
||||
static size_t s_pixel_buf_px = 0; /* sdl_pixel_buf capacity in pixels */
|
||||
static size_t sdl_pixel_buf_capacity = 0;
|
||||
static int sdl_texture_width = 0;
|
||||
static int sdl_texture_height = 0;
|
||||
|
||||
typedef void (*ModFrameHook)(void);
|
||||
|
||||
@@ -1421,6 +1423,17 @@ static int g_video_aspect_den = 3;
|
||||
static bool g_ws_adaptive_view = false;
|
||||
static int g_ws_adaptive_max_num = 16;
|
||||
static int g_ws_adaptive_max_den = 9;
|
||||
static bool ensure_sdl_pixel_buf_capacity(size_t pixels) {
|
||||
if (pixels <= sdl_pixel_buf_capacity) return true;
|
||||
void* next = std::realloc(sdl_pixel_buf, pixels * sizeof(uint32_t));
|
||||
if (!next) {
|
||||
std::fprintf(stderr, "psxrecomp: failed to allocate %zu present pixels\n", pixels);
|
||||
return false;
|
||||
}
|
||||
sdl_pixel_buf = (uint32_t*)next;
|
||||
sdl_pixel_buf_capacity = pixels;
|
||||
return true;
|
||||
}
|
||||
/* game.toml [netplay] local_viewport = "vertical_split": during real netplay,
|
||||
* present only this peer's native split-screen half and stretch it to the
|
||||
* window. Presentation-only; the guest still renders the original framebuffer. */
|
||||
@@ -2108,6 +2121,27 @@ static int netplay_gl_dual_quality(void) {
|
||||
}
|
||||
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
static int ensure_sdl_present_texture(int min_width, int min_height) {
|
||||
if (sdl_texture && min_width <= sdl_texture_width &&
|
||||
min_height <= sdl_texture_height) return 0;
|
||||
const int width = std::max(min_width, sdl_texture_width);
|
||||
const int height = std::max(min_height, sdl_texture_height);
|
||||
SDL_Texture* next = SDL_CreateTexture(sdl_renderer,
|
||||
SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height);
|
||||
if (!next) {
|
||||
std::fprintf(stderr, "psxrecomp: SDL_CreateTexture %dx%d failed: %s\n",
|
||||
width, height, SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
SDL_SetTextureScaleMode(next,
|
||||
g_video_aa ? SDL_ScaleModeLinear : SDL_ScaleModeNearest);
|
||||
if (sdl_texture) SDL_DestroyTexture(sdl_texture);
|
||||
sdl_texture = next;
|
||||
sdl_texture_width = width;
|
||||
sdl_texture_height = height;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Create SDL_Renderer + streaming texture for software present. Used when
|
||||
* netplay runs without a live GL context (software renderer selected, or GL
|
||||
* init failed). CPU-auth + OpenGL present does not need this. */
|
||||
@@ -2139,28 +2173,21 @@ static int ensure_sw_sdl_present(void) {
|
||||
}
|
||||
if (!sdl_texture) {
|
||||
const int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
sdl_texture = SDL_CreateTexture(
|
||||
sdl_renderer,
|
||||
SDL_PIXELFORMAT_ARGB8888,
|
||||
SDL_TEXTUREACCESS_STREAMING,
|
||||
640 * tex_scale,
|
||||
(int)PSX_DISPLAY_PRESENT_MAX_HEIGHT * tex_scale);
|
||||
if (!sdl_texture) {
|
||||
if (ensure_sdl_present_texture(
|
||||
640 * tex_scale,
|
||||
(int)PSX_DISPLAY_PRESENT_MAX_HEIGHT * tex_scale) != 0) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: netplay SW present: SDL_CreateTexture failed: %s\n",
|
||||
SDL_GetError());
|
||||
return -1;
|
||||
}
|
||||
SDL_SetTextureScaleMode(sdl_texture,
|
||||
g_video_aa ? SDL_ScaleModeLinear : SDL_ScaleModeNearest);
|
||||
}
|
||||
if (!sdl_pixel_buf) {
|
||||
int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
if (tex_scale > SW_MAX_INTERNAL_SCALE) tex_scale = SW_MAX_INTERNAL_SCALE;
|
||||
s_pixel_buf_px = (size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT *
|
||||
tex_scale;
|
||||
sdl_pixel_buf = (uint32_t*)std::malloc(s_pixel_buf_px * sizeof(uint32_t));
|
||||
if (!sdl_pixel_buf) {
|
||||
if (!ensure_sdl_pixel_buf_capacity(
|
||||
(size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT *
|
||||
tex_scale)) {
|
||||
std::fprintf(stderr, "psxrecomp: netplay SW present: staging alloc failed\n");
|
||||
return -1;
|
||||
}
|
||||
@@ -3454,9 +3481,11 @@ static void teardown_game_session_keep_lobby(void) {
|
||||
g_gl_active = false;
|
||||
}
|
||||
if (sdl_texture) { SDL_DestroyTexture(sdl_texture); sdl_texture = nullptr; }
|
||||
sdl_texture_width = sdl_texture_height = 0;
|
||||
if (sdl_renderer) { SDL_DestroyRenderer(sdl_renderer); sdl_renderer = nullptr; }
|
||||
if (sdl_window) { SDL_DestroyWindow(sdl_window); sdl_window = nullptr; }
|
||||
if (sdl_pixel_buf) { std::free(sdl_pixel_buf); sdl_pixel_buf = nullptr; s_pixel_buf_px = 0; }
|
||||
if (sdl_pixel_buf) { std::free(sdl_pixel_buf); sdl_pixel_buf = nullptr; }
|
||||
sdl_pixel_buf_capacity = 0;
|
||||
/* Rematch must re-run force_sw (and prefer CPU-auth GL present again). */
|
||||
s_netplay_sw_gpu_locked = 0;
|
||||
s_netplay_gl_present = 0;
|
||||
@@ -7292,6 +7321,13 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
(turbo_loads_active || fmv_skip_active) ? 1 : 0;
|
||||
|
||||
if (g_headless) {
|
||||
/* Headless skips the presenter below, but still needs the same
|
||||
* game-entry widescreen activation for its rendered frame dumps. */
|
||||
if (!g_ws_engaged && fntrace_is_game_started()) {
|
||||
g_ws_engaged = true;
|
||||
g_ws_projection_mode = -1;
|
||||
}
|
||||
refresh_widescreen_projection();
|
||||
ep.skip_pace = 1;
|
||||
return ep;
|
||||
}
|
||||
@@ -7619,6 +7655,14 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
: (w + (uint32_t)ws_nw_extra());
|
||||
}
|
||||
|
||||
/* The software wide readback can exceed the canonical 640px staging
|
||||
* width, especially on high-resolution and resized adaptive frames. */
|
||||
const int staging_scale = std::max(1, std::min(
|
||||
std::max(g_video_scale, gr_scale()), SW_MAX_INTERNAL_SCALE));
|
||||
const size_t staging_pixels = (size_t)present_w * std::max(h, present_h) *
|
||||
(size_t)staging_scale * staging_scale;
|
||||
if (!ensure_sdl_pixel_buf_capacity(staging_pixels)) return ep;
|
||||
|
||||
/* Native-wide invariant: canonical (320-wide) content is NEVER
|
||||
* stretched across the wide window — a game frame that cannot present
|
||||
* wide (compositor unsupported, or the surface fallback below)
|
||||
@@ -7726,7 +7770,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
: (int)di.display_x;
|
||||
/* The staging buffer is sized for at most SW_MAX_INTERNAL_SCALE;
|
||||
* a GL wide surface at a higher scale falls back to canonical. */
|
||||
int n = ((size_t)sw * (size_t)h * (size_t)s <= s_pixel_buf_px)
|
||||
int n = ((size_t)sw * (size_t)h * (size_t)s <= sdl_pixel_buf_capacity)
|
||||
? gr_render_wide_display(sdl_pixel_buf, (int)(sw * sizeof(uint32_t)),
|
||||
base_x, (int)di.display_y, (int)h)
|
||||
: 0;
|
||||
@@ -7858,6 +7902,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
return ep;
|
||||
if (!sdl_renderer || !sdl_texture)
|
||||
return ep;
|
||||
if (ensure_sdl_present_texture(std::max(1, src_w), std::max(1, src_h)) != 0)
|
||||
return ep;
|
||||
SDL_Rect src = { 0, 0, src_w, src_h };
|
||||
SDL_UpdateTexture(sdl_texture, &src, sdl_pixel_buf,
|
||||
(int)(src_w * sizeof(uint32_t)));
|
||||
@@ -7868,14 +7914,11 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
const int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
const int tex_h = (int)PSX_DISPLAY_PRESENT_MAX_HEIGHT * tex_scale;
|
||||
if (src_w > 0 && src_h > 0 && src_h < tex_h) {
|
||||
static uint32_t s_black_pad[640 * 4]; /* covers g_video_scale <= 4 */
|
||||
const int pad_cap = (int)(sizeof(s_black_pad) / sizeof(s_black_pad[0]));
|
||||
const int pad_w = (src_w <= pad_cap) ? src_w : pad_cap;
|
||||
for (int i = 0; i < pad_w; i++)
|
||||
s_black_pad[i] = 0xFF000000u;
|
||||
SDL_Rect pad = { 0, src_h, pad_w, 1 };
|
||||
SDL_UpdateTexture(sdl_texture, &pad, s_black_pad,
|
||||
(int)(pad_w * sizeof(uint32_t)));
|
||||
static std::vector<uint32_t> black_pad;
|
||||
black_pad.assign((size_t)src_w, 0xFF000000u);
|
||||
SDL_Rect pad = { 0, src_h, src_w, 1 };
|
||||
SDL_UpdateTexture(sdl_texture, &pad, black_pad.data(),
|
||||
(int)(src_w * sizeof(uint32_t)));
|
||||
}
|
||||
|
||||
/* FMV (24-bit) frames are authored 4:3 with no GTE squash to compensate
|
||||
@@ -13453,6 +13496,9 @@ int main(int argc, char** argv) {
|
||||
gpu_ws_set_slti_lower_cull_sites(
|
||||
gc.ws_cull_slti_lower_sites.data(),
|
||||
(int)gc.ws_cull_slti_lower_sites.size());
|
||||
gpu_ws_set_bias_lower_cull_sites(
|
||||
gc.ws_cull_bias_lower_sites.data(),
|
||||
(int)gc.ws_cull_bias_lower_sites.size());
|
||||
gpu_ws_set_negsub_cull_sites(
|
||||
gc.ws_cull_negsub_sites.data(), (int)gc.ws_cull_negsub_sites.size());
|
||||
gpu_ws_set_vxrange_cull_sites(
|
||||
@@ -16113,10 +16159,9 @@ session_reboot:
|
||||
* active_scale), so an 8K GL scale never sizes this buffer by S^2. */
|
||||
int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
if (tex_scale > SW_MAX_INTERNAL_SCALE) tex_scale = SW_MAX_INTERNAL_SCALE;
|
||||
s_pixel_buf_px = (size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT *
|
||||
tex_scale;
|
||||
sdl_pixel_buf = (uint32_t*)std::malloc(s_pixel_buf_px * sizeof(uint32_t));
|
||||
if (!sdl_pixel_buf) {
|
||||
if (!ensure_sdl_pixel_buf_capacity(
|
||||
(size_t)640 * tex_scale * PSX_DISPLAY_PRESENT_MAX_HEIGHT *
|
||||
tex_scale)) {
|
||||
std::fprintf(stderr, "failed to allocate %dx staging buffer\n", tex_scale);
|
||||
return 1;
|
||||
}
|
||||
@@ -16124,19 +16169,12 @@ session_reboot:
|
||||
|
||||
if (!g_gl_active && !g_vk_active) {
|
||||
const int tex_scale = netplay_cpu_auth_gpu() ? 1 : g_video_scale;
|
||||
sdl_texture = SDL_CreateTexture(
|
||||
sdl_renderer,
|
||||
SDL_PIXELFORMAT_ARGB8888,
|
||||
SDL_TEXTUREACCESS_STREAMING,
|
||||
640 * tex_scale,
|
||||
(int)PSX_DISPLAY_PRESENT_MAX_HEIGHT * tex_scale
|
||||
);
|
||||
if (!sdl_texture) {
|
||||
if (ensure_sdl_present_texture(
|
||||
640 * tex_scale,
|
||||
(int)PSX_DISPLAY_PRESENT_MAX_HEIGHT * tex_scale) != 0) {
|
||||
std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
SDL_SetTextureScaleMode(sdl_texture,
|
||||
g_video_aa ? SDL_ScaleModeLinear : SDL_ScaleModeNearest);
|
||||
}
|
||||
log_present_cadence();
|
||||
}
|
||||
|
||||
@@ -1786,10 +1786,12 @@ static void isc_store(uint32_t addr, uint32_t val, uint32_t width) {
|
||||
if ((cache_ctrl & (PSX_BIU_DCACHE_ENABLE | PSX_BIU_LOCK)) ==
|
||||
PSX_BIU_DCACHE_ENABLE) {
|
||||
uint32_t off = addr & 0x3FFu;
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
uint32_t old = 0;
|
||||
for (uint32_t i = 0; i < width; i++)
|
||||
old |= (uint32_t)scratchpad[(off + i) & 0x3FFu] << (8u * i);
|
||||
debug_server_trace_write_check(0x1F800000u + off, old, val, (uint8_t)width);
|
||||
#endif
|
||||
for (uint32_t i = 0; i < width; i++)
|
||||
scratchpad[(off + i) & 0x3FFu] = (uint8_t)(val >> (8u * i));
|
||||
}
|
||||
@@ -1922,7 +1924,9 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
}
|
||||
}
|
||||
if (phys == D44_PHYS) d44_note(phys, read_ram_word(phys), val);
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_trace_write_check(phys, read_ram_word(phys), val, 4);
|
||||
#endif
|
||||
parity_trace_note_write(phys, 4, effective_store_pc());
|
||||
card_data_writes_check(phys, val, 4);
|
||||
dirty_ram_mark_kernel_write(phys);
|
||||
@@ -1955,12 +1959,14 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) {
|
||||
if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return;
|
||||
if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) {
|
||||
uint32_t off = phys - 0x1F800000u;
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_trace_write_check(phys,
|
||||
(uint32_t)scratchpad[off]
|
||||
| ((uint32_t)scratchpad[off + 1] << 8)
|
||||
| ((uint32_t)scratchpad[off + 2] << 16)
|
||||
| ((uint32_t)scratchpad[off + 3] << 24),
|
||||
val, 4);
|
||||
#endif
|
||||
scratchpad[off] = (uint8_t)(val);
|
||||
scratchpad[off + 1] = (uint8_t)(val >> 8);
|
||||
scratchpad[off + 2] = (uint8_t)(val >> 16);
|
||||
@@ -2051,7 +2057,9 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
|
||||
uint32_t phys = psx_phys_addr_store(addr);
|
||||
|
||||
if (phys < RAM_SIZE) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_trace_write_check(phys, (uint32_t)read_ram_half(phys), (uint32_t)val, 2);
|
||||
#endif
|
||||
parity_trace_note_write(phys, 2, effective_store_pc());
|
||||
card_data_writes_check(phys, (uint32_t)val, 2);
|
||||
dirty_ram_mark_kernel_write(phys);
|
||||
@@ -2083,9 +2091,11 @@ static void psx_write_half_raw(uint32_t addr, uint16_t val) {
|
||||
if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return;
|
||||
if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) {
|
||||
uint32_t off = phys - 0x1F800000u;
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_trace_write_check(phys,
|
||||
(uint32_t)scratchpad[off] | ((uint32_t)scratchpad[off + 1] << 8),
|
||||
(uint32_t)val, 2);
|
||||
#endif
|
||||
scratchpad[off] = (uint8_t)(val);
|
||||
scratchpad[off + 1] = (uint8_t)(val >> 8);
|
||||
return;
|
||||
@@ -2410,7 +2420,9 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
|
||||
uint32_t phys = psx_phys_addr_store(addr);
|
||||
|
||||
if (phys < RAM_SIZE) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_trace_write_check(phys, (uint32_t)ram[phys], (uint32_t)val, 1);
|
||||
#endif
|
||||
parity_trace_note_write(phys, 1, effective_store_pc());
|
||||
card_data_writes_check(phys, (uint32_t)val, 1);
|
||||
dirty_ram_mark_kernel_write(phys);
|
||||
@@ -2438,8 +2450,10 @@ static void psx_write_byte_raw(uint32_t addr, uint8_t val) {
|
||||
}
|
||||
if (phys >= 0x1F000000u && phys <= 0x1F7FFFFFu) return;
|
||||
if (phys >= 0x1F800000u && phys <= 0x1F8003FFu) {
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
debug_server_trace_write_check(phys, (uint32_t)scratchpad[phys - 0x1F800000u],
|
||||
(uint32_t)val, 1);
|
||||
#endif
|
||||
scratchpad[phys - 0x1F800000u] = val;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ struct RegisteredPlugin {
|
||||
PSXModVBlankCallback vblank = nullptr;
|
||||
/* Generated-function entry hooks, keyed by guest address. One id may
|
||||
* observe several functions. */
|
||||
std::vector<std::pair<uint32_t, PSXModFunctionEntryCallback>> function_entries;
|
||||
std::vector<ModFunctionEntryHook> function_entries;
|
||||
};
|
||||
|
||||
std::map<std::string, RegisteredPlugin>& registered_plugins() {
|
||||
@@ -1459,8 +1459,18 @@ bool mod_register_function_entry_plugin(const std::string& id, uint32_t address,
|
||||
RegisteredPlugin& plugin = registered_plugins()[id];
|
||||
/* Code addresses alias across KUSEG/KSEG0/KSEG1; one function is one hook. */
|
||||
for (const auto& hook : plugin.function_entries)
|
||||
if (((hook.first ^ address) & 0x1FFFFFFFu) == 0u) return false;
|
||||
plugin.function_entries.emplace_back(address, callback);
|
||||
if (((hook.address ^ address) & 0x1FFFFFFFu) == 0u) return false;
|
||||
plugin.function_entries.push_back({address, callback, nullptr});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool mod_register_function_filter_plugin(const std::string& id, uint32_t address,
|
||||
PSXModFunctionFilterCallback callback) {
|
||||
if (!valid_id(id) || !address || !callback) return false;
|
||||
RegisteredPlugin& plugin = registered_plugins()[id];
|
||||
for (const auto& hook : plugin.function_entries)
|
||||
if (((hook.address ^ address) & 0x1FFFFFFFu) == 0u) return false;
|
||||
plugin.function_entries.push_back({address, nullptr, callback});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1487,9 +1497,7 @@ std::vector<ModFunctionEntryHook> mod_function_entry_hooks(const std::string& id
|
||||
std::vector<ModFunctionEntryHook> hooks;
|
||||
const auto found = registered_plugins().find(id);
|
||||
if (found == registered_plugins().end()) return hooks;
|
||||
for (const auto& hook : found->second.function_entries)
|
||||
hooks.push_back(ModFunctionEntryHook{hook.first, hook.second});
|
||||
return hooks;
|
||||
return found->second.function_entries;
|
||||
}
|
||||
|
||||
void mod_clear_plugins_for_tests() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mod_runtime.h"
|
||||
#include "cpu_state.h"
|
||||
|
||||
#include "disc_path.h"
|
||||
#include "iso_reader.h"
|
||||
@@ -94,6 +95,7 @@ RuntimeMods& state() {
|
||||
struct ActiveFunctionEntryHook {
|
||||
uint32_t key = 0;
|
||||
PSXModFunctionEntryCallback callback = nullptr;
|
||||
PSXModFunctionFilterCallback filter = nullptr;
|
||||
const ModResolution::Plugin* plugin = nullptr;
|
||||
};
|
||||
|
||||
@@ -101,6 +103,7 @@ std::vector<ActiveFunctionEntryHook>& active_function_entry_hooks() {
|
||||
static std::vector<ActiveFunctionEntryHook> value;
|
||||
return value;
|
||||
}
|
||||
unsigned function_entry_depth;
|
||||
|
||||
inline uint32_t function_entry_key(uint32_t address) {
|
||||
return address & 0x1FFFFFFFu;
|
||||
@@ -117,7 +120,8 @@ void build_function_entry_hooks(const RuntimeMods& s) {
|
||||
auto& table = active_function_entry_hooks();
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins)
|
||||
for (const ModFunctionEntryHook& hook : mod_function_entry_hooks(plugin.id))
|
||||
table.push_back({function_entry_key(hook.address), hook.callback, &plugin});
|
||||
table.push_back({function_entry_key(hook.address), hook.callback,
|
||||
hook.filter, &plugin});
|
||||
/* Stable: hooks sharing an address keep plan (plugin order) order. */
|
||||
std::stable_sort(table.begin(), table.end(),
|
||||
[](const ActiveFunctionEntryHook& a,
|
||||
@@ -1563,9 +1567,16 @@ extern "C" int psx_mod_register_function_entry_plugin(
|
||||
return mod_register_function_entry_plugin(id, address, callback) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" void psx_mod_function_entry(CPUState* cpu, uint32_t address) {
|
||||
extern "C" int psx_mod_register_function_filter_plugin(
|
||||
const char* id, uint32_t address, PSXModFunctionFilterCallback callback) {
|
||||
using namespace PSXRecompV4;
|
||||
if (!g_psx_mod_function_entry_hooks || !cpu) return;
|
||||
if (!id || !address || !callback) return 0;
|
||||
return mod_register_function_filter_plugin(id, address, callback) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_function_entry(CPUState* cpu, uint32_t address) {
|
||||
using namespace PSXRecompV4;
|
||||
if (!g_psx_mod_function_entry_hooks || !cpu) return 0;
|
||||
RuntimeMods& s = state();
|
||||
const auto& table = active_function_entry_hooks();
|
||||
const uint32_t key = function_entry_key(address);
|
||||
@@ -1575,9 +1586,21 @@ extern "C" void psx_mod_function_entry(CPUState* cpu, uint32_t address) {
|
||||
for (; it != table.end() && it->key == key; ++it) {
|
||||
const ModResolution::Plugin* previous = s.current_plugin;
|
||||
s.current_plugin = it->plugin;
|
||||
it->callback(cpu, address);
|
||||
++function_entry_depth;
|
||||
if (it->callback) it->callback(cpu, address);
|
||||
const int handled = it->filter && it->filter(cpu, address);
|
||||
--function_entry_depth;
|
||||
s.current_plugin = previous;
|
||||
if (handled) {
|
||||
cpu->pc = cpu->gpr[31];
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_function_entry_active(void) {
|
||||
return PSXRecompV4::function_entry_depth != 0;
|
||||
}
|
||||
|
||||
extern "C" void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector,
|
||||
|
||||
@@ -1316,6 +1316,8 @@ typedef struct {
|
||||
uint8_t load_failed;
|
||||
int resident;
|
||||
int capacity_suppressed;
|
||||
int loaded_check_count;
|
||||
int loaded_check_result;
|
||||
char path[768];
|
||||
} CacheEntry;
|
||||
static CacheEntry s_cache_idx[CACHE_IDX_CAP];
|
||||
@@ -1879,6 +1881,8 @@ static int add_posix_cache_file(const PsxOverlayCacheFile *file, void *opaque) {
|
||||
e->tier = (uint8_t)tier;
|
||||
e->manifest_ok = 0;
|
||||
e->load_failed = 0;
|
||||
e->loaded_check_count = -1;
|
||||
e->loaded_check_result = 0;
|
||||
e->resident = cache_path_is_bios_resident(file->path);
|
||||
e->capacity_suppressed = 0;
|
||||
snprintf(e->path, sizeof(e->path), "%s", file->path);
|
||||
@@ -1924,6 +1928,8 @@ static void scan_one_cache_dir(const char *dir, int tier) {
|
||||
e->tier = (uint8_t)tier;
|
||||
e->manifest_ok = 0;
|
||||
e->load_failed = 0;
|
||||
e->loaded_check_count = -1;
|
||||
e->loaded_check_result = 0;
|
||||
e->capacity_suppressed = 0;
|
||||
snprintf(e->path, sizeof(e->path), "%s", full);
|
||||
e->resident = cache_path_is_bios_resident(full);
|
||||
@@ -2591,7 +2597,7 @@ static void init_callbacks(void) {
|
||||
}
|
||||
{
|
||||
extern int32_t psx_ws_screen_x_bound(int32_t vanilla);
|
||||
extern void psx_mod_function_entry(CPUState *cpu, uint32_t address);
|
||||
extern int psx_mod_function_entry(CPUState *cpu, uint32_t address);
|
||||
s_callbacks.ws_screen_x_bound = psx_ws_screen_x_bound;
|
||||
s_callbacks.mod_function_entry = psx_mod_function_entry;
|
||||
}
|
||||
@@ -3196,6 +3202,20 @@ static int dll_already_loaded(const char *path) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* A lazy manifest chain may have hundreds of functions from the same DLL.
|
||||
* Re-comparing every long Windows path (including locale case folding) for
|
||||
* each function dominates uncovered CPS continuation dispatches. Loaded paths
|
||||
* are append-only: cache membership per artifact until that set grows. This
|
||||
* caches only path membership; live code/manifest validation is unchanged. */
|
||||
static int cache_entry_already_loaded(int ci) {
|
||||
CacheEntry *e = &s_cache_idx[ci];
|
||||
if (e->loaded_check_count != s_nloaded_paths) {
|
||||
e->loaded_check_result = dll_already_loaded(e->path);
|
||||
e->loaded_check_count = s_nloaded_paths;
|
||||
}
|
||||
return e->loaded_check_result;
|
||||
}
|
||||
|
||||
static void overlay_library_close(OverlayLibraryHandle handle) {
|
||||
if (!handle) return;
|
||||
#ifdef _WIN32
|
||||
@@ -3533,7 +3553,7 @@ static int lazy_is_loadable(int li, uint32_t region_start, uint32_t seg,
|
||||
s_cache_idx[ci].region_start == region_start) &&
|
||||
!s_cache_idx[ci].load_failed &&
|
||||
!s_cache_idx[ci].capacity_suppressed &&
|
||||
!dll_already_loaded(s_cache_idx[ci].path) &&
|
||||
!cache_entry_already_loaded(ci) &&
|
||||
lazy_man_contains(&lm->fn, seg, phys) && lazy_man_matches(lm);
|
||||
}
|
||||
|
||||
@@ -3553,7 +3573,7 @@ static int lazy_load_selected(int li) {
|
||||
if (ci < 0 || ci >= s_cache_idx_count || ci >= CACHE_IDX_CAP ||
|
||||
s_cache_idx[ci].load_failed ||
|
||||
s_cache_idx[ci].capacity_suppressed ||
|
||||
dll_already_loaded(s_cache_idx[ci].path) ||
|
||||
cache_entry_already_loaded(ci) ||
|
||||
s_cache_idx[ci].func_count <= 0) return 0;
|
||||
/* If proactive warming has not reached this fragment yet, prefer the
|
||||
* historical synchronous path over running a potentially hot function in
|
||||
|
||||
@@ -766,6 +766,8 @@ uint32_t savestate_last_save_pc(void) {
|
||||
|
||||
void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
|
||||
if (s_save_pending < 0 && s_load_pending < 0) return; /* hot path: nothing staged */
|
||||
extern int psx_mod_function_entry_active(void);
|
||||
if (psx_mod_function_entry_active()) return;
|
||||
|
||||
if (s_save_pending >= 0) {
|
||||
int slot = s_save_pending;
|
||||
|
||||
@@ -187,8 +187,9 @@ uint32_t psx_ws_backdrop_value(uint32_t orig, int end, int cols) {
|
||||
int32_t psx_ws_depth_bound(int32_t imm) { return imm; }
|
||||
int32_t psx_ws_player_x_bound(int32_t vanilla) { return vanilla; }
|
||||
int32_t psx_ws_screen_x_bound(int32_t vanilla) { return vanilla; }
|
||||
void psx_mod_function_entry(CPUState *cpu, uint32_t address) {
|
||||
int psx_mod_function_entry(CPUState *cpu, uint32_t address) {
|
||||
(void)cpu; (void)address;
|
||||
return 0;
|
||||
}
|
||||
int psx_netplay_is_resimulating(void) { return 0; }
|
||||
int psx_game_text_native_ok(uint32_t address) { (void)address; return 1; }
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build actual audio_trace.c in debug and PSX_NO_DEBUG_TOOLS modes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
DEFAULT_GCC = pathlib.Path(r"C:\msys64\mingw64\bin\gcc.exe")
|
||||
|
||||
DRIVER_C = r"""
|
||||
#include "audio_trace.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define PCM_RING_FRAMES (1u << 22)
|
||||
|
||||
static void fail(const char *msg) {
|
||||
fprintf(stderr, "FAIL: %s\n", msg);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static uint32_t rd32(const unsigned char *p) {
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
static int16_t rd16s(const unsigned char *p) {
|
||||
uint16_t u = (uint16_t)p[0] | (uint16_t)((uint16_t)p[1] << 8);
|
||||
return (int16_t)u;
|
||||
}
|
||||
|
||||
static void push_small_taps(void) {
|
||||
int16_t t0[] = {
|
||||
0, 0,
|
||||
1, -1,
|
||||
256, -256,
|
||||
257, -257,
|
||||
32767, -32768,
|
||||
};
|
||||
int16_t t1[] = { -300, 0, 0, 300, 0, 0 };
|
||||
int16_t t2[] = { 42, -42, 512, -512 };
|
||||
int16_t t3[] = { 0, 0, 999, -999, -32768, 32767 };
|
||||
audio_trace_pcm(AUDIO_TAP_SPU_OUT, t0, 5);
|
||||
audio_trace_pcm(AUDIO_TAP_CD_IN, t1, 3);
|
||||
audio_trace_pcm(AUDIO_TAP_HOST, t2, 2);
|
||||
audio_trace_pcm(AUDIO_TAP_VOICES, t3, 3);
|
||||
}
|
||||
|
||||
static void check_small_stats(void) {
|
||||
AudioTraceStats st;
|
||||
audio_trace_get_stats(&st);
|
||||
if (st.tap_frames[0] != 5 || st.tap_nonzero[0] != 4 ||
|
||||
st.tap_audible[0] != 2 || st.tap_peak[0] != 32768)
|
||||
fail("tap0 stats");
|
||||
if (st.tap_frames[1] != 3 || st.tap_nonzero[1] != 2 ||
|
||||
st.tap_audible[1] != 2 || st.tap_peak[1] != 300)
|
||||
fail("tap1 stats");
|
||||
if (st.tap_frames[2] != 2 || st.tap_nonzero[2] != 2 ||
|
||||
st.tap_audible[2] != 1 || st.tap_peak[2] != 512)
|
||||
fail("tap2 stats");
|
||||
if (st.tap_frames[3] != 3 || st.tap_nonzero[3] != 2 ||
|
||||
st.tap_audible[3] != 2 || st.tap_peak[3] != 32768)
|
||||
fail("tap3 stats");
|
||||
}
|
||||
|
||||
static void print_stats(const char *label) {
|
||||
AudioTraceStats st;
|
||||
audio_trace_get_stats(&st);
|
||||
printf("%s", label);
|
||||
for (int t = 0; t < AUDIO_TAP_COUNT; t++) {
|
||||
printf(" tap%d=%llu/%llu/%llu/%d/%u",
|
||||
t,
|
||||
(unsigned long long)st.tap_frames[t],
|
||||
(unsigned long long)st.tap_nonzero[t],
|
||||
(unsigned long long)st.tap_audible[t],
|
||||
st.tap_peak[t],
|
||||
audio_trace_tap_rate(t));
|
||||
}
|
||||
printf(" pump=%llu/%llu/%llu/%u/%u/%llu/%llu events=%llu total=%llu\n",
|
||||
(unsigned long long)st.pump_calls,
|
||||
(unsigned long long)st.pump_skips,
|
||||
(unsigned long long)st.underruns,
|
||||
st.queue_hiwater,
|
||||
st.queue_lowater,
|
||||
(unsigned long long)st.mute_events,
|
||||
(unsigned long long)st.unmute_events,
|
||||
(unsigned long long)st.events_total,
|
||||
(unsigned long long)audio_trace_events_total());
|
||||
}
|
||||
|
||||
static void push_events(void) {
|
||||
audio_trace_note_frame(77);
|
||||
audio_trace_event(AUDIO_EV_RENDER, 2048, 1234);
|
||||
audio_trace_event(AUDIO_EV_PUMP_SKIP, 222, 0);
|
||||
audio_trace_event(AUDIO_EV_UNDERRUN, 3, 1);
|
||||
audio_trace_event(AUDIO_EV_MUTE, 64, 0);
|
||||
audio_trace_event(AUDIO_EV_UNMUTE, 128, 0);
|
||||
audio_trace_event(AUDIO_EV_CD_PUSH, 0xABCDEF01u, 17);
|
||||
}
|
||||
|
||||
static void check_events(void) {
|
||||
AudioTraceStats st;
|
||||
AudioTraceEvent ev[8];
|
||||
audio_trace_get_stats(&st);
|
||||
if (st.pump_calls != 1 || st.pump_skips != 1 || st.underruns != 1 ||
|
||||
st.mute_events != 1 || st.unmute_events != 1 ||
|
||||
st.queue_hiwater != 1234 || st.queue_lowater != 1234 ||
|
||||
st.events_total != 6)
|
||||
fail("event stats");
|
||||
if (audio_trace_events_total() != 6) fail("event total");
|
||||
if (audio_trace_events_get(ev, 8) != 6) fail("event get count");
|
||||
if (ev[0].seq != 0 || ev[0].sample_idx != 5 || ev[0].frame != 77 ||
|
||||
ev[0].kind != AUDIO_EV_RENDER || ev[0].a != 2048 || ev[0].b != 1234)
|
||||
fail("render event payload");
|
||||
if (ev[5].kind != AUDIO_EV_CD_PUSH || ev[5].a != 0xABCDEF01u ||
|
||||
ev[5].b != 17)
|
||||
fail("cd event payload");
|
||||
}
|
||||
|
||||
static void check_post_wrap_event(uint64_t expected_sample_idx) {
|
||||
AudioTraceStats st;
|
||||
AudioTraceEvent ev[8];
|
||||
audio_trace_note_frame(88);
|
||||
audio_trace_event(AUDIO_EV_DMA_READ, 12, 0x12345678u);
|
||||
audio_trace_get_stats(&st);
|
||||
if (st.events_total != 7 || audio_trace_events_total() != 7)
|
||||
fail("post-wrap event count");
|
||||
if (audio_trace_events_get(ev, 8) != 7) fail("post-wrap event get count");
|
||||
if (ev[6].seq != 6 || ev[6].sample_idx != expected_sample_idx ||
|
||||
ev[6].frame != 88 || ev[6].kind != AUDIO_EV_DMA_READ ||
|
||||
ev[6].a != 12 || ev[6].b != 0x12345678u)
|
||||
fail("post-wrap event payload");
|
||||
}
|
||||
|
||||
static void print_events(const char *label) {
|
||||
AudioTraceEvent ev[8];
|
||||
uint32_t got = audio_trace_events_get(ev, 8);
|
||||
printf("%s count=%u", label, got);
|
||||
for (uint32_t i = 0; i < got; i++) {
|
||||
printf(" e%u=%llu/%llu/%u/%u/%u/%u",
|
||||
i,
|
||||
(unsigned long long)ev[i].seq,
|
||||
(unsigned long long)ev[i].sample_idx,
|
||||
ev[i].frame,
|
||||
ev[i].kind,
|
||||
ev[i].a,
|
||||
ev[i].b);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
static int file_exists(const char *path) {
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp) return 0;
|
||||
fclose(fp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void push_wrap_tail(void) {
|
||||
int16_t frame[2];
|
||||
for (uint32_t i = 0; i < PCM_RING_FRAMES - 5u; i++) {
|
||||
frame[0] = (int16_t)(i & 0x7FFFu);
|
||||
frame[1] = (int16_t)(-(int32_t)(i & 0x7FFFu));
|
||||
audio_trace_pcm(AUDIO_TAP_SPU_OUT, frame, 1);
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
frame[0] = (int16_t)(1000 + i);
|
||||
frame[1] = (int16_t)(-2000 - i);
|
||||
audio_trace_pcm(AUDIO_TAP_SPU_OUT, frame, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static const int16_t *tap_expected(int tap, uint32_t *frames) {
|
||||
static const int16_t t0[] = {
|
||||
0, 0, 1, -1, 256, -256, 257, -257, 32767, -32768,
|
||||
};
|
||||
static const int16_t t1[] = { -300, 0, 0, 300, 0, 0 };
|
||||
static const int16_t t2[] = { 42, -42, 512, -512 };
|
||||
static const int16_t t3[] = { 0, 0, 999, -999, -32768, 32767 };
|
||||
switch (tap) {
|
||||
case AUDIO_TAP_SPU_OUT: *frames = 5; return t0;
|
||||
case AUDIO_TAP_CD_IN: *frames = 3; return t1;
|
||||
case AUDIO_TAP_HOST: *frames = 2; return t2;
|
||||
default: *frames = 3; return t3;
|
||||
}
|
||||
}
|
||||
|
||||
static void check_wav_samples(const char *path, uint32_t rate,
|
||||
const int16_t *expected, uint32_t frames) {
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp) fail("wav open");
|
||||
unsigned char data[44 + 64];
|
||||
size_t want = 44u + (size_t)frames * 4u;
|
||||
if (want > sizeof(data)) fail("wav test buffer too small");
|
||||
size_t n = fread(data, 1, want, fp);
|
||||
fclose(fp);
|
||||
if (n != want) fail("wav size");
|
||||
if (memcmp(data, "RIFF", 4) != 0 || memcmp(data + 8, "WAVEfmt ", 8) != 0 ||
|
||||
memcmp(data + 36, "data", 4) != 0)
|
||||
fail("wav tags");
|
||||
if (rd32(data + 24) != rate) fail("wav rate");
|
||||
if (rd32(data + 40) != frames * 4u) fail("wav data bytes");
|
||||
for (uint32_t i = 0; i < frames * 2u; i++) {
|
||||
if (rd16s(data + 44u + i * 2u) != expected[i])
|
||||
fail("wav data");
|
||||
}
|
||||
}
|
||||
|
||||
static void check_short_wavs(const char *base_path, int production) {
|
||||
char path[512];
|
||||
for (int tap = 0; tap < AUDIO_TAP_COUNT; tap++) {
|
||||
uint32_t frames = 0;
|
||||
const int16_t *expected = tap_expected(tap, &frames);
|
||||
snprintf(path, sizeof(path), "%s_tap%d.wav", base_path, tap);
|
||||
remove(path);
|
||||
int64_t wrote = audio_trace_dump_wav(tap, path, 0, frames);
|
||||
if (production) {
|
||||
if (wrote != -1) fail("production short wav should be unavailable");
|
||||
if (file_exists(path)) fail("production short wav created file");
|
||||
} else {
|
||||
if (wrote != (int64_t)frames) fail("debug short wav frame count");
|
||||
check_wav_samples(path, audio_trace_tap_rate(tap), expected, frames);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) fail("usage");
|
||||
const char *path = argv[1];
|
||||
int production = strcmp(argv[2], "production") == 0;
|
||||
|
||||
audio_trace_init();
|
||||
audio_trace_set_tap_rate(AUDIO_TAP_SPU_OUT, 48000);
|
||||
audio_trace_set_tap_rate(AUDIO_TAP_CD_IN, 44101);
|
||||
audio_trace_set_tap_rate(AUDIO_TAP_HOST, 32000);
|
||||
audio_trace_set_tap_rate(AUDIO_TAP_VOICES, 22050);
|
||||
push_small_taps();
|
||||
check_small_stats();
|
||||
print_stats("small");
|
||||
push_events();
|
||||
check_events();
|
||||
print_events("events");
|
||||
check_short_wavs(path, production);
|
||||
push_wrap_tail();
|
||||
|
||||
AudioTraceStats st;
|
||||
audio_trace_get_stats(&st);
|
||||
if (st.tap_frames[AUDIO_TAP_SPU_OUT] != PCM_RING_FRAMES + 4ull)
|
||||
fail("wrapped tap frame total");
|
||||
check_post_wrap_event(st.tap_frames[AUDIO_TAP_SPU_OUT]);
|
||||
print_stats("wrap");
|
||||
print_events("events_after_wrap");
|
||||
|
||||
int64_t wrote = audio_trace_dump_wav(AUDIO_TAP_SPU_OUT, path,
|
||||
(int64_t)PCM_RING_FRAMES, 4);
|
||||
if (production) {
|
||||
if (wrote != -1) fail("production wav should be unavailable");
|
||||
if (file_exists(path)) fail("production wav created file");
|
||||
} else {
|
||||
int16_t wrapped[] = {
|
||||
1000, -2000, 1001, -2001, 1002, -2002, 1003, -2003,
|
||||
};
|
||||
if (wrote != 4) fail("debug wav frame count");
|
||||
check_wav_samples(path, 48000u, wrapped, 4);
|
||||
{
|
||||
int16_t oldest[] = { 32767, -32768 };
|
||||
if (audio_trace_dump_wav(AUDIO_TAP_SPU_OUT, path, 0, 1) != 1)
|
||||
fail("debug evicted prefix should clamp");
|
||||
check_wav_samples(path, 48000u, oldest, 1);
|
||||
}
|
||||
}
|
||||
|
||||
audio_trace_init();
|
||||
audio_trace_get_stats(&st);
|
||||
for (int tap = 0; tap < AUDIO_TAP_COUNT; tap++) {
|
||||
if (st.tap_frames[tap] || st.tap_nonzero[tap] || st.tap_audible[tap] ||
|
||||
st.tap_peak[tap] || audio_trace_tap_total(tap))
|
||||
fail("reset tap");
|
||||
}
|
||||
if (st.events_total || st.pump_calls || st.pump_skips || st.underruns ||
|
||||
st.queue_hiwater || st.queue_lowater || st.mute_events ||
|
||||
st.unmute_events || audio_trace_events_total())
|
||||
fail("reset events");
|
||||
print_stats("reset");
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def default_cc() -> str:
|
||||
if DEFAULT_GCC.exists():
|
||||
return str(DEFAULT_GCC).replace("\\", "/")
|
||||
for name in ("cc", "gcc", "clang"):
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
return str(DEFAULT_GCC).replace("\\", "/")
|
||||
|
||||
|
||||
def normalize_cc(cc: str) -> str:
|
||||
if "\\" in cc and len(cc) >= 3 and cc[1] == ":":
|
||||
return cc.replace("\\", "/")
|
||||
return cc
|
||||
|
||||
|
||||
def build(cc: str, work: pathlib.Path, production: bool) -> pathlib.Path:
|
||||
source = work / "audio_trace_guard_driver.c"
|
||||
source.write_text(DRIVER_C, encoding="utf-8")
|
||||
exe = work / ("audio_trace_prod.exe" if production else "audio_trace_debug.exe")
|
||||
cmd = [
|
||||
cc,
|
||||
"-std=c11",
|
||||
"-O2",
|
||||
"-fno-lto",
|
||||
"-I",
|
||||
str(ROOT / "runtime" / "include"),
|
||||
]
|
||||
if production:
|
||||
cmd.append("-DPSX_NO_DEBUG_TOOLS=1")
|
||||
cmd.extend([
|
||||
str(source),
|
||||
str(ROOT / "runtime" / "src" / "audio_trace.c"),
|
||||
"-o",
|
||||
str(exe),
|
||||
])
|
||||
subprocess.run(cmd, cwd=work, check=True)
|
||||
return exe
|
||||
|
||||
|
||||
def run(exe: pathlib.Path, path: pathlib.Path, mode: str) -> str:
|
||||
return subprocess.run(
|
||||
[str(exe), str(path), mode],
|
||||
cwd=exe.parent,
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.strip()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--cc", default=default_cc())
|
||||
args = parser.parse_args()
|
||||
cc = normalize_cc(args.cc)
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="psx-audio-trace-") as td:
|
||||
work = pathlib.Path(td)
|
||||
debug = build(cc, work, False)
|
||||
prod = build(cc, work, True)
|
||||
debug_out = run(debug, work / "debug.wav", "debug")
|
||||
prod_out = run(prod, work / "prod.wav", "production")
|
||||
if debug_out != prod_out:
|
||||
raise AssertionError(f"stats/event output changed:\n{debug_out}\n{prod_out}")
|
||||
print(debug_out)
|
||||
print("PASS: audio trace PCM history is debug-tool only; stats/events match")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except AssertionError as exc:
|
||||
print(f"FAIL: {exc}")
|
||||
sys.exit(1)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"FAIL: command exited {exc.returncode}: {' '.join(map(str, exc.cmd))}")
|
||||
sys.exit(1)
|
||||
@@ -72,6 +72,15 @@ int main(int argc,char **argv){
|
||||
printf("driver=%s renderer=%s scale=%d\n",glGetString(GL_VERSION),glGetString(GL_RENDERER),scale);
|
||||
glb_set_draw_area(0,0,1023,511);glb_set_mask_bits(0,0);glb_set_semi_transparency(0,0);glb_set_color_modulation(128,128,128,1);
|
||||
verify("initial upload");
|
||||
/* Adaptive backdrop reflection must retain every edge texel at both native
|
||||
* and supersampled scales; the ordinary rect stays forward-facing. */
|
||||
for(int x=0;x<16;++x) glb_vram_write(512+x,0,(uint16_t)(0x400+x+1));
|
||||
glb_draw_textured_rect(700,200,16,1,0,0,0,0,0x108);
|
||||
glb_draw_textured_rect_scaled(720,200,16,1,15,0,-1,1,0,0,0x108);
|
||||
for(int x=0;x<16;++x) {
|
||||
check(glb_vram_read(700+x,200)==0x401+x,"forward panorama texel");
|
||||
check(glb_vram_read(720+x,200)==0x410-x,"reflected panorama texel");
|
||||
}
|
||||
glb_draw_flat_rect(1020,511,1,1,0x7fff);
|
||||
check(glb_vram_read(1020,511)==0x7fff,"test pixel value");
|
||||
GlCohEvent event;int found=0;
|
||||
@@ -109,8 +118,8 @@ int main(int argc,char **argv){
|
||||
check(glb_vram_read(40,40)==0,"depth24 cleared band immediate CPU read");
|
||||
check(glb_vram_read(33,33)==0x3210,"newer overlapping texture survives clear");
|
||||
verify("depth24 leave coherence without subsequent primitive");
|
||||
/* Retained banks use their own texels AND CLUT, and bank/VRAM transitions
|
||||
* must split batches without changing painter order. No retail assets. */
|
||||
/* Run retained-bank tests with the normal draw area before the wide-view
|
||||
* regression narrows it to the gameplay rectangle. */
|
||||
static uint16_t bank[256*128];
|
||||
bank[0]=0x001f; bank[1]=0x03e0; bank[2]=0x7c00;
|
||||
bank[16]=0x1111; /* 4-bit indices, CLUT at (0,0) */
|
||||
@@ -127,7 +136,47 @@ int main(int argc,char **argv){
|
||||
check(gl_renderer_select_texture_bank(0),"reset bank after direct texture");
|
||||
check(glb_vram_read(302,252)==0x001f,"retained 16-bit texel");
|
||||
verify("retained banks and original VRAM ordered together");
|
||||
/* A streamed scene can retain indices while CLUT uploads/fades continue.
|
||||
* Alternate both modes of the SAME bank with pending draws: palette source
|
||||
* must participate in the batch key, and stock packets must reset it. */
|
||||
glb_vram_write(1,0,0x7c00);
|
||||
check(gl_renderer_select_texture_bank_live_clut(7),"select bank with live CLUT");
|
||||
glb_draw_shaded_textured_triangle(400,250,64,0,0x808080,432,250,64,0,0x808080,400,282,64,0,0x808080,0,0,0,1);
|
||||
check(gl_renderer_select_texture_bank(7),"same bank with retained CLUT");
|
||||
glb_draw_shaded_textured_triangle(440,250,64,0,0x808080,472,250,64,0,0x808080,440,282,64,0,0x808080,0,0,0,1);
|
||||
check(glb_vram_read(402,252)==0x7c00,"live guest palette used");
|
||||
check(glb_vram_read(442,252)==0x03e0,"same-bank palette source is a batch key");
|
||||
glb_vram_write(1,0,0x001f);
|
||||
check(gl_renderer_select_texture_bank_live_clut(7),"live CLUT after update");
|
||||
glb_draw_shaded_textured_triangle(480,250,64,0,0x808080,512,250,64,0,0x808080,480,282,64,0,0x808080,0,0,0,1);
|
||||
check(gl_renderer_select_texture_bank(0),"reset live-CLUT bank");
|
||||
check(glb_vram_read(482,252)==0x001f,"palette update visible without replacing indices");
|
||||
verify("retained indices with animated guest CLUT");
|
||||
verify_bank_batching();
|
||||
/* World and UI use different origins in an anchored wide frame. Keep the
|
||||
* canonical-center optimization enabled to catch an erroneous blit over the
|
||||
* completed mirror, and change origins with a pending flat batch. */
|
||||
glb_set_draw_area(0,0,319,239);glb_set_precise_triangle(0,0,0,0,0,0,0);
|
||||
glb_wide_configure(426,53);glb_wide_set_target(0);
|
||||
s_wide_fast=1;
|
||||
uint32_t *wide_pixels=calloc((size_t)426*240*scale*scale,sizeof(uint32_t));
|
||||
if(!wide_pixels)return 2;
|
||||
for(int shift=-53;shift<=53;shift+=53){
|
||||
glb_wide_clear(0,0,240,0);
|
||||
glb_wide_set_view(1,shift,0,0);
|
||||
glb_draw_flat_rect(-106,0,532,240,0x7c00);
|
||||
glb_draw_flat_rect(160,40,3,3,0x03e0);
|
||||
glb_wide_set_view(1,0,0,0);
|
||||
glb_draw_flat_rect(160,20,3,3,0x001f);
|
||||
check(glb_render_wide_display(wide_pixels,426*scale*4,0,0,240)>0,"anchored wide readback");
|
||||
check(wide_pixels[(40*scale)*(426*scale)+(213+shift)*scale]==0xff00f800u,"anchored world marker");
|
||||
check(wide_pixels[(20*scale)*(426*scale)+213*scale]==0xfff80000u,"centered dialogue marker");
|
||||
check(wide_pixels[(60*scale)*(426*scale)]==0xff0000f8u,"anchored left edge");
|
||||
check(wide_pixels[(60*scale)*(426*scale)+425*scale]==0xff0000f8u,"anchored right edge");
|
||||
}
|
||||
glb_wide_set_view(0,0,0,0);
|
||||
check(wide_dx()==53,"disabled view preserves original origin");
|
||||
free(wide_pixels);
|
||||
printf("checks=%d failures=%d\n",checks,failures);
|
||||
gl_renderer_shutdown();SDL_DestroyWindow(win);SDL_Quit();return failures?1:0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate production GP0 history gating against actual gpu.c.
|
||||
|
||||
This is a parser/register/VRAM regression with renderer stubs, not a full
|
||||
backend rendering-correctness test.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
GPU_C = ROOT / "runtime" / "src" / "gpu.c"
|
||||
DEFAULT_GCC = pathlib.Path(r"C:\msys64\mingw64\bin\gcc.exe")
|
||||
|
||||
DRIVER_C = r"""
|
||||
#include "gpu.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
extern uint64_t s_frame_count;
|
||||
uint32_t gpu_get_opcode_count(uint8_t op);
|
||||
uint64_t gpu_get_gp0_count(void);
|
||||
void gpu_get_gp0_stats(uint64_t* nop, uint64_t* fill, uint64_t* draw,
|
||||
uint64_t* env, uint64_t* copy);
|
||||
uint32_t gpu_snapshot_bytes(void);
|
||||
void gpu_snapshot_write(uint8_t *p);
|
||||
|
||||
static uint32_t timeline_hash = 2166136261u;
|
||||
static int checkpoint_failed = 0;
|
||||
static FILE *trace_file;
|
||||
|
||||
static uint32_t mix(uint32_t h, uint32_t v) {
|
||||
return (h ^ v) * 16777619u;
|
||||
}
|
||||
|
||||
static void checkpoint(void) {
|
||||
uint32_t n = gpu_snapshot_bytes();
|
||||
uint8_t *snap = (uint8_t *)malloc(n ? n : 1u);
|
||||
if (!snap) {
|
||||
checkpoint_failed = 1;
|
||||
return;
|
||||
}
|
||||
gpu_snapshot_write(snap);
|
||||
if (fwrite(&n, sizeof(n), 1, trace_file) != 1 ||
|
||||
fwrite(snap, 1, n, trace_file) != n) checkpoint_failed = 1;
|
||||
timeline_hash = mix(timeline_hash, n);
|
||||
for (uint32_t i = 0; i < n; i++) timeline_hash = mix(timeline_hash, snap[i]);
|
||||
free(snap);
|
||||
for (int y = 0; y < 512; y++) {
|
||||
uint16_t row[1024];
|
||||
for (int x = 0; x < 1024; x++) {
|
||||
row[x] = gpu_vram_peek(x, y);
|
||||
timeline_hash = mix(timeline_hash, row[x]);
|
||||
}
|
||||
if (fwrite(row, sizeof(row), 1, trace_file) != 1) checkpoint_failed = 1;
|
||||
}
|
||||
}
|
||||
|
||||
static void emit_env(uint32_t word) {
|
||||
gpu_write_gp0(word);
|
||||
s_frame_count++;
|
||||
checkpoint();
|
||||
}
|
||||
|
||||
static void emit_fill(uint32_t color, uint32_t xy, uint32_t wh) {
|
||||
gpu_write_gp0(0x02000000u | color);
|
||||
gpu_write_gp0(xy);
|
||||
gpu_write_gp0(wh);
|
||||
s_frame_count++;
|
||||
checkpoint();
|
||||
}
|
||||
|
||||
static void emit_upload_2x1(uint32_t xy, uint32_t pixels) {
|
||||
gpu_write_gp0(0xA0000000u);
|
||||
gpu_write_gp0(xy);
|
||||
gpu_write_gp0((1u << 16) | 2u);
|
||||
gpu_write_gp0(pixels);
|
||||
s_frame_count++;
|
||||
checkpoint();
|
||||
}
|
||||
|
||||
static void emit_copy(uint32_t src_xy, uint32_t dst_xy, uint32_t wh) {
|
||||
gpu_write_gp0(0x80000000u);
|
||||
gpu_write_gp0(src_xy);
|
||||
gpu_write_gp0(dst_xy);
|
||||
gpu_write_gp0(wh);
|
||||
s_frame_count++;
|
||||
checkpoint();
|
||||
}
|
||||
|
||||
static void emit_polyline(void) {
|
||||
gpu_write_gp0(0x4800007Fu);
|
||||
gpu_write_gp0((20u << 16) | 10u);
|
||||
/* The first two vertices are mandatory, before any terminator. */
|
||||
gpu_write_gp0((25u << 16) | 15u);
|
||||
gpu_write_gp0(0x55555555u);
|
||||
s_frame_count++;
|
||||
checkpoint();
|
||||
}
|
||||
|
||||
static uint32_t hash_state(void) {
|
||||
GpuDrawArea a;
|
||||
uint64_t nop = 0, fill = 0, draw = 0, env = 0, copy = 0;
|
||||
gpu_get_draw_area(&a);
|
||||
gpu_get_gp0_stats(&nop, &fill, &draw, &env, ©);
|
||||
uint32_t h = 2166136261u;
|
||||
#define MIX(v) do { h = (h ^ (uint32_t)(v)) * 16777619u; } while (0)
|
||||
MIX(gpu_get_gp0_count());
|
||||
MIX(nop); MIX(fill); MIX(draw); MIX(env); MIX(copy);
|
||||
MIX(gpu_get_opcode_count(0x00)); MIX(gpu_get_opcode_count(0x02));
|
||||
MIX(gpu_get_opcode_count(0xA0)); MIX(gpu_get_opcode_count(0xE1));
|
||||
MIX(gpu_get_opcode_count(0xE3)); MIX(gpu_get_opcode_count(0xE4));
|
||||
MIX(gpu_get_opcode_count(0xE5)); MIX(gpu_get_opcode_count(0xE6));
|
||||
MIX(a.left); MIX(a.top); MIX(a.right); MIX(a.bottom);
|
||||
MIX((uint32_t)a.offset_x); MIX((uint32_t)a.offset_y);
|
||||
MIX(gpu_vram_peek(16, 5)); MIX(gpu_vram_peek(31, 5));
|
||||
MIX(gpu_vram_peek(40, 9)); MIX(gpu_vram_peek(41, 9));
|
||||
MIX(gpu_read_gpustat());
|
||||
MIX(gpu_ws_census_seq());
|
||||
MIX(timeline_hash);
|
||||
return h;
|
||||
#undef MIX
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
GpuGp0RingEntry entries[16], copy_entries[16], poly_entries[16];
|
||||
uint32_t oldest = 99, newest = 99;
|
||||
if (argc != 2 || !(trace_file = fopen(argv[1], "wb"))) return 2;
|
||||
|
||||
gpu_init();
|
||||
gpu_ws_census_set(0);
|
||||
checkpoint();
|
||||
gpu_set_gp0_source(0x00123400u);
|
||||
emit_env(0xE10003FFu);
|
||||
gpu_set_gp0_source(0x00123404u);
|
||||
emit_env(0xE3000000u | (3u << 10) | 2u);
|
||||
gpu_set_gp0_source(0x00123408u);
|
||||
emit_env(0xE4000000u | (80u << 10) | 90u);
|
||||
gpu_set_gp0_source(0x0012340Cu);
|
||||
emit_env(0xE5000000u | (8u << 11) | 7u);
|
||||
gpu_set_gp0_source(0x00123410u);
|
||||
emit_env(0xE6000001u);
|
||||
gpu_set_gp0_source(0x00123414u);
|
||||
emit_fill(0x0000FFu, (5u << 16) | 16u, (1u << 16) | 16u);
|
||||
gpu_set_gp0_source(0x00123418u);
|
||||
emit_upload_2x1((9u << 16) | 40u, 0x9ABC1234u);
|
||||
gpu_set_gp0_source(0x0012341Cu);
|
||||
emit_copy((9u << 16) | 40u, (10u << 16) | 42u, (1u << 16) | 2u);
|
||||
gpu_set_gp0_source(0x00123420u);
|
||||
emit_polyline();
|
||||
gpu_set_gp0_source(0x00123424u);
|
||||
emit_env(0x00000000u);
|
||||
if (fclose(trace_file) != 0) checkpoint_failed = 1;
|
||||
if (checkpoint_failed) {
|
||||
fprintf(stderr, "checkpoint allocation/write failed\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
gpu_gp0_ring_frame_span(&oldest, &newest);
|
||||
int dumped = gpu_gp0_ring_dump_frame(0, entries, 16);
|
||||
int copy_dumped = gpu_gp0_ring_dump_frame(7, copy_entries, 16);
|
||||
int poly_dumped = gpu_gp0_ring_dump_frame(8, poly_entries, 16);
|
||||
printf("hash=%08x total=%llu cap=%u max=%u span=%u:%u dump0=%d "
|
||||
"src=%08x op=%02x cmd0=%08x "
|
||||
"copydump=%d copysrc=%08x copyop=%02x copycsp=%08x "
|
||||
"polydump=%d polyop=%02x polycmd0=%08x\n",
|
||||
hash_state(), (unsigned long long)gpu_gp0_ring_total(),
|
||||
gpu_gp0_ring_capacity(), gpu_gp0_ring_max_words(), oldest, newest,
|
||||
dumped, dumped > 0 ? entries[0].src_addr : 0u,
|
||||
dumped > 0 ? entries[0].opcode : 0u,
|
||||
dumped > 0 ? entries[0].cmd[0] : 0u,
|
||||
copy_dumped, copy_dumped > 0 ? copy_entries[0].src_addr : 0u,
|
||||
copy_dumped > 0 ? copy_entries[0].opcode : 0u,
|
||||
copy_dumped > 0 ? copy_entries[0].csp : 0u,
|
||||
poly_dumped, poly_dumped > 0 ? poly_entries[0].opcode : 0u,
|
||||
poly_dumped > 0 ? poly_entries[0].cmd[0] : 0u);
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
STUBS_C = r"""
|
||||
#include "gpu.h"
|
||||
#include "gpu_render.h"
|
||||
#include "cpu_state.h"
|
||||
#include "color_lut.h"
|
||||
#include "ws_aspect_cone_math.h"
|
||||
#include "ws_ui_group.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
uint64_t s_frame_count = 0;
|
||||
uint32_t g_debug_last_store_pc = 0x80012340u;
|
||||
uint32_t g_psx_ram_size = 0x00200000u;
|
||||
uint32_t g_psx_ram_mask = 0x001FFFFFu;
|
||||
void psx_video_timing_set_pal(int pal) { (void)pal; }
|
||||
GrBackend gr_backend(void) { return GR_BACKEND_SOFTWARE; }
|
||||
int gl_renderer_texture_banks_supported(void) { return 0; }
|
||||
int gl_renderer_select_texture_bank(uint16_t id) { return id == 0; }
|
||||
int gl_renderer_select_texture_bank_live_clut(uint16_t id) { return id == 0; }
|
||||
uint16_t mod_texture_packet_bank(uint32_t p, const uint32_t *words, uint32_t n)
|
||||
{ (void)p; (void)words; (void)n; return 0; }
|
||||
int mod_texture_packet_precision(uint32_t p, float q[3], float xy[6])
|
||||
{ (void)p; (void)q; (void)xy; return 0; }
|
||||
void psx_host_write_half(uint32_t addr, uint16_t val) { (void)addr; (void)val; }
|
||||
uint32_t g_debug_current_func_addr = 0x80045670u;
|
||||
CPUState *debug_cpu_ptr = 0;
|
||||
uint32_t i_stat = 0;
|
||||
uint32_t i_mask = 0;
|
||||
static uint16_t *g_vram;
|
||||
|
||||
uint32_t debug_guest_ra(void) { return 0x8000AAAAu; }
|
||||
uint32_t debug_guest_sp(void) { return 0x8010FFF0u; }
|
||||
uint8_t *memory_get_ram_ptr(void) { static uint8_t ram[2 * 1024 * 1024]; return ram; }
|
||||
uint16_t psx_read_half(uint32_t addr) { (void)addr; return 0; }
|
||||
uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; }
|
||||
uint32_t psx_read_word(uint32_t addr) { (void)addr; return 0; }
|
||||
void psx_fatal_halt(const char *reason) { fprintf(stderr, "%s\n", reason); abort(); }
|
||||
void crash_trace_note_gpu_fatal(uint32_t opcode, uint32_t word) { (void)opcode; (void)word; }
|
||||
void event_ring_note_gpu_irq(uint32_t stat) { (void)stat; }
|
||||
int g_psx_vram_dirty_tracking = 0;
|
||||
int g_exec_phase = 0;
|
||||
void gpu_vram_dirty_mark_all(void) {}
|
||||
void gpu_vram_dirty_mark_row_impl(uint32_t row) { (void)row; }
|
||||
void gpu_vram_dirty_mark_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h)
|
||||
{ (void)x; (void)y; (void)w; (void)h; }
|
||||
void text_xlate_vram_upload(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
|
||||
{ (void)x; (void)y; (void)w; (void)h; }
|
||||
|
||||
int ws_cull_should_keep(uint32_t addr) { (void)addr; return 1; }
|
||||
int ws_ui_group_should_keep(uint32_t addr) { (void)addr; return 1; }
|
||||
uint32_t psx_mod_gpu_dma_resolve_address(uint32_t address) { return address; }
|
||||
void ws_ui_group_assign(WsUiGroupItem *items, size_t count,
|
||||
int32_t display_width, int dense_menu)
|
||||
{ (void)items; (void)count; (void)display_width; (void)dense_menu; }
|
||||
int32_t ws_ui_anchor_for_bounds(int32_t x, int32_t width, int32_t display_width)
|
||||
{ (void)x; (void)width; return display_width / 2; }
|
||||
int gte_geometry_correction_enabled(void) { return 0; }
|
||||
void pgxp_set_enabled(int enabled) { (void)enabled; }
|
||||
int pgxp_get_gte_sxy_checked(uint32_t slot, uint32_t packed, int require_valid,
|
||||
int32_t *x16, int32_t *y16)
|
||||
{ (void)slot; (void)packed; (void)require_valid; (void)x16; (void)y16; return 0; }
|
||||
int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y,
|
||||
int32_t *x16, int32_t *y16, uint16_t *sz)
|
||||
{ (void)addr; (void)packet_word; (void)int_x; (void)int_y; (void)x16; (void)y16; (void)sz; return 0; }
|
||||
int gte_precision_load_word(uint32_t addr, uint32_t packed,
|
||||
int32_t *x16, int32_t *y16, uint16_t *z)
|
||||
{ (void)addr; (void)packed; (void)x16; (void)y16; (void)z; return 0; }
|
||||
void pgxp_invalidate_all(void) {}
|
||||
void pgxp_invalidate_word(uint32_t addr) { (void)addr; }
|
||||
void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; }
|
||||
void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux)
|
||||
{ (void)kind; (void)detail; (void)aux; }
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
int psx_netplay_active(void) { return 0; }
|
||||
int sio_hold_present_for_card(void) { return 0; }
|
||||
uint32_t psx_compiled_irq_resume_pc = 0;
|
||||
uint32_t psx_last_irq_check_pc = 0;
|
||||
uint32_t psx_netplay_rb_sticky_bb_pc = 0;
|
||||
void mod_runtime_on_vblank(void) {}
|
||||
void sio_ape_card_unstick_pump(void) {}
|
||||
void psx_write_half(uint32_t addr, uint16_t val) { (void)addr; (void)val; }
|
||||
int mdec_recently_active(uint32_t within_frames) { (void)within_frames; return 0; }
|
||||
uint32_t psx_ws_widen_angle_q12(uint32_t vanilla, int extent_pixels)
|
||||
{ (void)extent_pixels; return vanilla; }
|
||||
int psx_ws_aspect_cone_contains(int32_t x, int32_t z, int32_t y,
|
||||
int32_t fx, int32_t fz, int32_t fy,
|
||||
uint32_t threshold, int extent_pixels)
|
||||
{ (void)x; (void)z; (void)y; (void)fx; (void)fz; (void)fy; (void)threshold; (void)extent_pixels; return 0; }
|
||||
uint32_t sw_perspective_triangle_count(void) { return 0; }
|
||||
bool screen_kind_from_name(const char* name, ScreenKind* out)
|
||||
{ (void)name; if (out) *out = SCREEN_RAW; return 0; }
|
||||
ColorLut* color_lut_create(const ColorSettings* settings)
|
||||
{ (void)settings; return 0; }
|
||||
void color_lut_destroy(ColorLut* lut) { (void)lut; }
|
||||
bool color_lut_is_passthrough(const ColorLut* lut) { (void)lut; return 1; }
|
||||
void color_lut_map555(const ColorLut* lut, uint16_t bgr555,
|
||||
uint8_t* r, uint8_t* g, uint8_t* b)
|
||||
{ (void)lut; if (r) *r = (uint8_t)(bgr555 & 0x1F); if (g) *g = (uint8_t)((bgr555 >> 5) & 0x1F); if (b) *b = (uint8_t)((bgr555 >> 10) & 0x1F); }
|
||||
int ws_sprt_fixed_transform(int32_t *x, int32_t y, int w)
|
||||
{ (void)x; (void)y; return w; }
|
||||
int ws_nw_hud_shift(int32_t x, int w) { (void)x; (void)w; return 0; }
|
||||
int ws_is_fb_base(int x) { (void)x; return 0; }
|
||||
int ws_local_viewport_draw_target(int *base) { if (base) *base = 0; return 0; }
|
||||
void ws_nw_sync_target(void) {}
|
||||
int ws_active(void) { return 0; }
|
||||
int ws_engaged(void) { return 0; }
|
||||
|
||||
void gr_init(uint16_t *vram) { g_vram = vram; }
|
||||
uint16_t gr_vram_read(int x, int y) { return g_vram[(y & 511) * 1024 + (x & 1023)]; }
|
||||
void gr_vram_transfer_in(int x, int y, int w, int h, const uint16_t *pixels)
|
||||
{
|
||||
for (int yy = 0; yy < h; yy++)
|
||||
for (int xx = 0; xx < w; xx++)
|
||||
g_vram[((y + yy) & 511) * 1024 + ((x + xx) & 1023)] = pixels[yy * w + xx];
|
||||
}
|
||||
void gr_fill_rect(int x, int y, int w, int h, uint16_t color)
|
||||
{
|
||||
for (int yy = 0; yy < h; yy++)
|
||||
for (int xx = 0; xx < w; xx++)
|
||||
g_vram[((y + yy) & 511) * 1024 + ((x + xx) & 1023)] = color;
|
||||
}
|
||||
void gr_wide_clear(int x, int y, int h, uint16_t color)
|
||||
{ (void)x; (void)y; (void)h; (void)color; }
|
||||
void gr_wide_disable_target(void) {}
|
||||
void gr_wide_configure(int width, int offset) { (void)width; (void)offset; }
|
||||
void gr_wide_set_target(int base) { (void)base; }
|
||||
void gr_wide_set_view(int active, int shift, int pad_left, int pad_right)
|
||||
{ (void)active; (void)shift; (void)pad_left; (void)pad_right; }
|
||||
void gr_wide_clear_margins(int base, int y, int h, uint16_t color, int mode)
|
||||
{ (void)base; (void)y; (void)h; (void)color; (void)mode; }
|
||||
void gr_set_draw_area(int l, int t, int r, int b) { (void)l; (void)t; (void)r; (void)b; }
|
||||
void gr_set_draw_offset(int x, int y) { (void)x; (void)y; }
|
||||
void gr_set_mask_bits(int set, int check) { (void)set; (void)check; }
|
||||
void gr_set_texture_window(uint32_t value) { (void)value; }
|
||||
void gr_set_semi_transparency(int enabled, int mode) { (void)enabled; (void)mode; }
|
||||
void gr_set_color_modulation(int r, int g, int b, int raw)
|
||||
{ (void)r; (void)g; (void)b; (void)raw; }
|
||||
void gr_set_perspective_triangle(int enabled, float a, float b, float c)
|
||||
{ (void)enabled; (void)a; (void)b; (void)c; }
|
||||
void gr_set_precise_triangle(int enabled, int32_t ax, int32_t ay, int32_t bx,
|
||||
int32_t by, int32_t cx, int32_t cy)
|
||||
{ (void)enabled; (void)ax; (void)ay; (void)bx; (void)by; (void)cx; (void)cy; }
|
||||
void gr_copy_rect(int sx, int sy, int dx, int dy, int w, int h)
|
||||
{ (void)sx; (void)sy; (void)dx; (void)dy; (void)w; (void)h; }
|
||||
void gr_draw_flat_rect(int x, int y, int w, int h, uint16_t color)
|
||||
{ (void)x; (void)y; (void)w; (void)h; (void)color; }
|
||||
void gr_draw_flat_triangle(int x0,int y0,int x1,int y1,int x2,int y2,uint16_t c)
|
||||
{ (void)x0;(void)y0;(void)x1;(void)y1;(void)x2;(void)y2;(void)c; }
|
||||
void gr_draw_gouraud_triangle(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1,int x2,int y2,uint16_t c2)
|
||||
{ (void)x0;(void)y0;(void)c0;(void)x1;(void)y1;(void)c1;(void)x2;(void)y2;(void)c2; }
|
||||
void gr_draw_line(int x0,int y0,int x1,int y1,uint16_t c)
|
||||
{ (void)x0;(void)y0;(void)x1;(void)y1;(void)c; }
|
||||
void gr_draw_shaded_line(int x0,int y0,uint16_t c0,int x1,int y1,uint16_t c1)
|
||||
{ (void)x0;(void)y0;(void)c0;(void)x1;(void)y1;(void)c1; }
|
||||
void gr_draw_textured_rect(int x,int y,int w,int h,int u,int v,uint16_t clut_x,uint16_t clut_y,uint16_t texpage)
|
||||
{ (void)x;(void)y;(void)w;(void)h;(void)u;(void)v;(void)clut_x;(void)clut_y;(void)texpage; }
|
||||
void gr_draw_textured_rect_scaled(int x,int y,int w,int h,int u0,int v0,int u1,int v1,uint16_t clut_x,uint16_t clut_y,uint16_t texpage)
|
||||
{ (void)x;(void)y;(void)w;(void)h;(void)u0;(void)v0;(void)u1;(void)v1;(void)clut_x;(void)clut_y;(void)texpage; }
|
||||
void gr_draw_textured_triangle(int x0,int y0,int u0,int v0,int x1,int y1,int u1,int v1,int x2,int y2,int u2,int v2,uint16_t clut_x,uint16_t clut_y,uint16_t texpage)
|
||||
{ (void)x0;(void)y0;(void)u0;(void)v0;(void)x1;(void)y1;(void)u1;(void)v1;(void)x2;(void)y2;(void)u2;(void)v2;(void)clut_x;(void)clut_y;(void)texpage; }
|
||||
void gr_draw_shaded_textured_triangle(int x0,int y0,int u0,int v0,uint32_t c0,int x1,int y1,int u1,int v1,uint32_t c1,int x2,int y2,int u2,int v2,uint32_t c2,uint16_t clut_x,uint16_t clut_y,uint16_t texpage,int raw)
|
||||
{ (void)x0;(void)y0;(void)u0;(void)v0;(void)c0;(void)x1;(void)y1;(void)u1;(void)v1;(void)c1;(void)x2;(void)y2;(void)u2;(void)v2;(void)c2;(void)clut_x;(void)clut_y;(void)texpage;(void)raw; }
|
||||
"""
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def default_cc() -> str:
|
||||
if DEFAULT_GCC.exists():
|
||||
return str(DEFAULT_GCC).replace("\\", "/")
|
||||
for name in ("cc", "gcc", "clang"):
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
return str(DEFAULT_GCC)
|
||||
|
||||
|
||||
def function_body(source: str, signature: str) -> str:
|
||||
start = source.index(signature)
|
||||
brace = source.index("{", start)
|
||||
depth = 0
|
||||
for pos in range(brace, len(source)):
|
||||
if source[pos] == "{":
|
||||
depth += 1
|
||||
elif source[pos] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return source[brace : pos + 1]
|
||||
raise AssertionError(f"unterminated function: {signature}")
|
||||
|
||||
|
||||
def assert_source_shape(gpu: str) -> None:
|
||||
guard = gpu.index("#ifndef PSX_NO_DEBUG_TOOLS", gpu.index("debug tools only"))
|
||||
alt = gpu.index("#else", guard)
|
||||
end = gpu.index("#endif", alt)
|
||||
debug_block = gpu[guard:alt]
|
||||
prod_block = gpu[alt:end]
|
||||
for token in ("GP0_RING_CAP", "static GpuGp0RingEntry *gp0_ring",
|
||||
"gp0_capture_builder_chain", "g_gp0_last_copy_sp",
|
||||
"debug_guest_ra()", "debug_guest_sp()"):
|
||||
if token not in debug_block:
|
||||
fail(f"debug GP0 history block lost {token}")
|
||||
if token in prod_block:
|
||||
fail(f"production GP0 history block still references {token}")
|
||||
for token in ("gpu_gp0_ring_total(void) { return 0; }",
|
||||
"gpu_gp0_ring_capacity(void) { return 0; }",
|
||||
"return GPU_GP0_RING_MAX_WORDS;",
|
||||
"return 0;"):
|
||||
if token not in prod_block:
|
||||
fail(f"production unavailable accessor missing {token}")
|
||||
set_source = function_body(gpu, "void gpu_set_gp0_source(")
|
||||
if "gp0_next_source_addr = addr;" not in set_source:
|
||||
fail("gpu_set_gp0_source no longer preserves source tracking")
|
||||
execute = function_body(gpu, "static void gp0_execute_command(")
|
||||
order = [execute.index(token) for token in (
|
||||
"gp0_opcode_count[opcode]++",
|
||||
"gp0_ring_record(gp0_cmd_buf, gp0_words_needed)",
|
||||
"ws_bg_phase_note(opcode)",
|
||||
"ws_census_record(opcode, cvx, cvy)",
|
||||
"ws_note_overhang(opcode)",
|
||||
)]
|
||||
if order != sorted(order):
|
||||
fail("GP0 command accounting/ring/WS ordering changed")
|
||||
if "gp0_ring_record(hdr_only, 1);" not in gpu:
|
||||
fail("polyline header capture hook was removed")
|
||||
|
||||
|
||||
def write_sources(work: pathlib.Path) -> None:
|
||||
(work / "driver.c").write_text(DRIVER_C, encoding="utf-8")
|
||||
(work / "stubs.c").write_text(STUBS_C, encoding="utf-8")
|
||||
|
||||
|
||||
def build(cc: str, work: pathlib.Path, name: str, prod: bool) -> pathlib.Path:
|
||||
exe = work / f"{name}.exe"
|
||||
cmd = [
|
||||
cc, "-std=c11", "-O2", "-ffunction-sections", "-fdata-sections",
|
||||
"-I", str(ROOT / "runtime" / "include"),
|
||||
]
|
||||
if prod:
|
||||
cmd.append("-DPSX_NO_DEBUG_TOOLS")
|
||||
cmd.extend([
|
||||
str(work / "driver.c"),
|
||||
str(GPU_C),
|
||||
str(work / "stubs.c"),
|
||||
])
|
||||
if platform.system() == "Darwin":
|
||||
cmd.append("-Wl,-dead_strip")
|
||||
else:
|
||||
cmd.append("-Wl,--gc-sections")
|
||||
cmd.extend(["-o", str(exe)])
|
||||
subprocess.run(cmd, cwd=work, check=True)
|
||||
return exe
|
||||
|
||||
|
||||
def run(exe: pathlib.Path) -> dict[str, str]:
|
||||
proc = subprocess.run([str(exe), str(exe.with_suffix(".state"))],
|
||||
cwd=exe.parent, check=True, text=True,
|
||||
stdout=subprocess.PIPE)
|
||||
return dict(part.split("=", 1) for part in proc.stdout.strip().split())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--cc", default=default_cc())
|
||||
args = parser.parse_args()
|
||||
cc = args.cc.replace("\\", "/")
|
||||
|
||||
assert_source_shape(GPU_C.read_text(encoding="utf-8"))
|
||||
with tempfile.TemporaryDirectory(prefix="psx-gp0-history-") as td:
|
||||
work = pathlib.Path(td)
|
||||
write_sources(work)
|
||||
debug = build(cc, work, "debug", prod=False)
|
||||
prod = build(cc, work, "prod", prod=True)
|
||||
debug_out = run(debug)
|
||||
prod_out = run(prod)
|
||||
debug_state = debug.with_suffix(".state").read_bytes()
|
||||
prod_state = prod.with_suffix(".state").read_bytes()
|
||||
if not debug_state or debug_state != prod_state:
|
||||
fail("production gate changed checkpoint GPU snapshot/VRAM bytes")
|
||||
|
||||
if debug_out["hash"] != prod_out["hash"]:
|
||||
fail("production GP0 history gate changed guest-visible GPU state")
|
||||
if prod_out["total"] != "0" or prod_out["cap"] != "0":
|
||||
fail("production GP0 history remains available")
|
||||
if prod_out["span"] != "0:0" or prod_out["dump0"] != "0":
|
||||
fail("production GP0 history accessors should report empty capture")
|
||||
if prod_out["copydump"] != "0" or prod_out["polydump"] != "0":
|
||||
fail("production GP0 history frame dumps should be empty")
|
||||
if debug_out["total"] != "10" or int(debug_out["cap"]) == 0:
|
||||
fail(f"unexpected debug GP0 history: {debug_out}")
|
||||
if debug_out["src"] != "00123400" or debug_out["op"] != "e1":
|
||||
fail("debug GP0 history lost source/opcode tracking")
|
||||
if debug_out["cmd0"] != "e10003ff":
|
||||
fail("debug GP0 history lost command-word capture")
|
||||
if debug_out["copydump"] != "1" or debug_out["copysrc"] != "0012341c":
|
||||
fail("debug GP0 history lost copy source tracking")
|
||||
if debug_out["copyop"] != "80" or debug_out["copycsp"] != "8010fff0":
|
||||
fail("debug GP0 history lost copy builder diagnostics")
|
||||
if debug_out["polydump"] != "1" or debug_out["polyop"] != "48":
|
||||
fail("debug GP0 history lost polyline header capture")
|
||||
if debug_out["polycmd0"] != "4800007f":
|
||||
fail("debug GP0 history lost polyline command word")
|
||||
print("PASS: actual gpu.c production GP0 history gate preserves GPU state")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except AssertionError as exc:
|
||||
print(f"FAIL: {exc}")
|
||||
sys.exit(1)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"FAIL: command exited {exc.returncode}: {' '.join(map(str, exc.cmd))}")
|
||||
sys.exit(1)
|
||||
@@ -23,7 +23,8 @@ uint32_t g_psx_ram_mask = 0x001FFFFFu;
|
||||
* additional exported command handlers despite --gc-sections. */
|
||||
GrBackend gr_backend(void) { return GR_BACKEND_SOFTWARE; }
|
||||
int gl_renderer_texture_banks_supported(void) { return 0; }
|
||||
int gl_renderer_select_texture_bank(uint16_t id) { (void)id; return 0; }
|
||||
int gl_renderer_select_texture_bank(uint16_t id) { return id == 0; }
|
||||
int gl_renderer_select_texture_bank_live_clut(uint16_t id) { return id == 0; }
|
||||
uint16_t mod_texture_packet_bank(uint32_t source, const uint32_t *words,
|
||||
uint32_t count) {
|
||||
(void)source; (void)words; (void)count;
|
||||
@@ -42,6 +43,7 @@ static struct {
|
||||
int u, v;
|
||||
uint16_t clut_x, clut_y, texpage;
|
||||
} last_textured_rect;
|
||||
static struct { int calls, x, y, w, h, u0, v0, u1, v1; } last_scaled_rect;
|
||||
|
||||
static uint32_t pack_vertex(int16_t x, int16_t y) {
|
||||
return (uint16_t)x | ((uint32_t)(uint16_t)y << 16);
|
||||
@@ -335,7 +337,11 @@ void gr_draw_textured_rect_scaled(int x, int y, int w, int h,
|
||||
int u0, int v0, int u1, int v1,
|
||||
uint16_t clut_x, uint16_t clut_y,
|
||||
uint16_t texpage) {
|
||||
(void)x; (void)y; (void)w; (void)h; (void)u0; (void)v0; (void)u1;
|
||||
last_scaled_rect.calls++;
|
||||
last_scaled_rect.x=x; last_scaled_rect.y=y;
|
||||
last_scaled_rect.w=w; last_scaled_rect.h=h;
|
||||
last_scaled_rect.u0=u0; last_scaled_rect.v0=v0;
|
||||
last_scaled_rect.u1=u1; last_scaled_rect.v1=v1;
|
||||
(void)v1; (void)clut_x; (void)clut_y; (void)texpage;
|
||||
}
|
||||
void gr_draw_line(int x0, int y0, int x1, int y1, uint16_t color) {
|
||||
@@ -373,6 +379,9 @@ void gr_get_draw_area(int *x1, int *y1, int *x2, int *y2) {
|
||||
void gr_set_draw_offset(int x, int y) { (void)x; (void)y; }
|
||||
int gr_wide_supported(void) { return 1; }
|
||||
void gr_wide_configure(int wide_w, int offset) { (void)wide_w; (void)offset; }
|
||||
void gr_wide_set_view(int enabled, int shift, int left, int right) {
|
||||
(void)enabled; (void)shift; (void)left; (void)right;
|
||||
}
|
||||
void gr_wide_set_target(int base_x) { (void)base_x; }
|
||||
void gr_wide_disable_target(void) {}
|
||||
void gr_wide_clear(int base_x, int y, int h, uint16_t color) {
|
||||
|
||||
@@ -13,12 +13,11 @@ def main():
|
||||
assert static_dispatch.index('g_exec_phase = 3;') < static_dispatch.index(
|
||||
'psx_overlay_dispatch(cpu, addr);') < static_dispatch.index(
|
||||
'g_exec_phase = previous_phase;') < static_dispatch.index('if (handled) return 1;')
|
||||
assert source.count('psx_mod_function_entry(cpu, addr);') == 1
|
||||
assert source.count('psx_mod_function_entry(cpu, addr)') == 1
|
||||
# Every interpreted entry reaches these calls: without an active hook they
|
||||
# must cost one load, never a call into the plugin table.
|
||||
assert 'if (g_psx_mod_function_entry_hooks) psx_mod_function_entry(cpu, addr);' in source
|
||||
assert ('if (g_psx_mod_function_entry_hooks)\n'
|
||||
' psx_mod_function_entry(cpu, target);') in source
|
||||
assert 'if (g_psx_mod_function_entry_hooks && psx_mod_function_entry(cpu, addr)) return 1;' in source
|
||||
assert 'if (g_psx_mod_function_entry_hooks &&\n' in source
|
||||
local_start = source.index('if (allow_local_dirty_flow && target != 0 &&')
|
||||
local_end = source.index('current_page = target_phys >> 12;', local_start)
|
||||
local = source[local_start:local_end]
|
||||
@@ -28,9 +27,9 @@ def main():
|
||||
assert 'g_dirty_interp_chain_target = target;' in probe
|
||||
assert 'OV_FPLOG_RET1();' in probe
|
||||
assert 'psx_overlay_dispatch(cpu' not in probe
|
||||
assert local.count('psx_mod_function_entry(cpu, target);') == 1
|
||||
assert local.count('psx_mod_function_entry(cpu, target)') == 1
|
||||
assert local.index('overlay_loader_dispatch(cpu, target)') < local.index(
|
||||
'psx_mod_function_entry(cpu, target);') < local.index('pc = target;')
|
||||
'psx_mod_function_entry(cpu, target)') < local.index('pc = target;')
|
||||
print('interpreter mod entry guards: PASS')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,577 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compile actual memory.c to validate production debug-trace guards."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
MEMORY_C = ROOT / "runtime" / "src" / "memory.c"
|
||||
TRACE_CALL = "debug_server_trace_write_check"
|
||||
DEFAULT_GCC = pathlib.Path(r"C:\msys64\mingw64\bin\gcc.exe")
|
||||
|
||||
HARNESS_COMMON_H = r"""
|
||||
#ifndef PSX_MEMORY_TRACE_HARNESS_COMMON_H
|
||||
#define PSX_MEMORY_TRACE_HARNESS_COMMON_H
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
#include "cpu_state.h"
|
||||
#include "dirty_ram_interp.h"
|
||||
#include "psx_bios_image.h"
|
||||
#include "render_pass_plan.h"
|
||||
|
||||
extern int g_ls_mode;
|
||||
extern int g_ls_suppress_record;
|
||||
extern volatile int g_ds_recording;
|
||||
extern int g_dma_exec_depth;
|
||||
extern int g_dma_cur_ch;
|
||||
extern uint32_t g_dma_cur_madr;
|
||||
extern uint32_t g_dma_cur_bcr;
|
||||
extern uint32_t g_dma_initiator_pc;
|
||||
extern uint32_t g_debug_last_store_pc;
|
||||
extern uint32_t g_debug_current_func_addr;
|
||||
extern uint64_t s_frame_count;
|
||||
extern uint32_t g_overlay_region_floor;
|
||||
extern uint32_t g_text_image_lo;
|
||||
extern int g_ram_read_watch_active;
|
||||
extern uint64_t psx_cycle_count;
|
||||
extern uint64_t g_psx_cycle_fast_limit;
|
||||
extern int g_event_step_conservative;
|
||||
extern int g_ls_replay_active;
|
||||
extern CPUState *debug_cpu_ptr;
|
||||
extern void (*g_overlay_flush_pending_cycles)(void);
|
||||
extern uint32_t g_dirty_ram_exec_page_bitmap[DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS];
|
||||
extern uint32_t g_dirty_ram_exec_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS];
|
||||
extern uint32_t g_dirty_ram_dispatch_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS];
|
||||
|
||||
extern unsigned g_trace_write_calls;
|
||||
extern unsigned g_parity_write_calls;
|
||||
extern unsigned g_card_write_calls;
|
||||
extern uint32_t g_trace_old_accum;
|
||||
extern uint32_t g_trace_new_accum;
|
||||
|
||||
int fntrace_is_game_started(void);
|
||||
int psx_get_in_exception(void);
|
||||
uint32_t ls_read_hook(uint32_t addr, uint8_t width, uint32_t value);
|
||||
void ls_write_hook(uint32_t addr, uint8_t width, uint32_t value);
|
||||
void ds_note_read(uint32_t addr, uint8_t width);
|
||||
void ds_note_write(uint32_t addr, uint8_t width);
|
||||
void ds_note_dma_write(void);
|
||||
void psx_devices_mmio_sync(void);
|
||||
void psx_fatal_halt(const char *reason);
|
||||
void psx_irq_refresh_cause_ip2(void);
|
||||
int sio_card_should_hold_imask_bit7(void);
|
||||
void sio_card_handoff_on_imask(uint32_t old_mask, uint32_t new_mask);
|
||||
uint32_t sio_read(uint32_t addr);
|
||||
void sio_write(uint32_t addr, uint32_t val);
|
||||
void sio_tick(uint32_t cycles);
|
||||
uint32_t dma_read(uint32_t addr);
|
||||
void dma_write(uint32_t addr, uint32_t val);
|
||||
void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask);
|
||||
uint32_t timers_read(uint32_t addr);
|
||||
void timers_write(uint32_t addr, uint32_t val);
|
||||
uint32_t cdrom_read(uint32_t addr);
|
||||
void cdrom_write(uint32_t addr, uint32_t val);
|
||||
uint32_t gpu_read_gpuread(void);
|
||||
uint32_t gpu_read_gpustat(void);
|
||||
void gpu_set_gp0_source(uint32_t source);
|
||||
void gpu_write_gp0(uint32_t val);
|
||||
void gpu_write_gp1(uint32_t val);
|
||||
uint32_t mdec_read(uint32_t addr);
|
||||
void mdec_write(uint32_t addr, uint32_t val);
|
||||
uint16_t spu_read(uint32_t addr);
|
||||
void spu_write(uint32_t addr, uint16_t val);
|
||||
void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width);
|
||||
void debug_server_trace_mmio_read(uint32_t addr, uint32_t val, uint8_t width);
|
||||
void debug_server_trace_ram_read_watch(uint32_t phys, uint32_t val);
|
||||
void debug_server_trace_entryint_write(uint32_t phys, uint32_t old_val,
|
||||
uint32_t new_val, uint8_t width);
|
||||
void parity_trace_note_write(uint32_t addr, uint32_t width, uint32_t writer_pc);
|
||||
int card_data_writes_check(uint32_t phys, uint32_t value, uint8_t width);
|
||||
void overlay_loader_note_code_write(void);
|
||||
void overlay_loader_active_write_check(uint32_t phys, uint32_t size);
|
||||
void overlay_loader_resync_validation_after_restore(void);
|
||||
|
||||
#endif
|
||||
"""
|
||||
|
||||
STUBS_C = r"""
|
||||
#include "harness_common.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static const PsxKernelBody s_psx_bios_kernel_bodies[1] = {{0, 0, 0}};
|
||||
const PsxKernelBody *psx_bios_kernel_bodies = s_psx_bios_kernel_bodies;
|
||||
uint32_t psx_bios_kernel_body_count = 0;
|
||||
const PsxKernelPatchRange *psx_bios_kernel_patch_ranges = 0;
|
||||
uint32_t psx_bios_kernel_patch_range_count = 0;
|
||||
PsxBiosImageInfo psx_bios_image = {0};
|
||||
|
||||
int g_ls_mode = 0;
|
||||
int g_ls_suppress_record = 0;
|
||||
volatile int g_ds_recording = 0;
|
||||
int g_dma_exec_depth = 0;
|
||||
int g_dma_cur_ch = -1;
|
||||
uint32_t g_dma_cur_madr = 0;
|
||||
uint32_t g_dma_cur_bcr = 0;
|
||||
uint32_t g_dma_initiator_pc = 0;
|
||||
uint32_t g_debug_last_store_pc = 0;
|
||||
uint32_t g_debug_current_func_addr = 0;
|
||||
uint64_t s_frame_count = 0;
|
||||
uint32_t g_overlay_region_floor = 0x200000u;
|
||||
uint32_t g_text_image_lo = 0x10000u;
|
||||
int g_ram_read_watch_active = 0;
|
||||
uint64_t psx_cycle_count = 0;
|
||||
uint64_t g_psx_cycle_fast_limit = 0;
|
||||
int g_event_step_conservative = 0;
|
||||
int g_ls_replay_active = 0;
|
||||
CPUState *debug_cpu_ptr = 0;
|
||||
void (*g_overlay_flush_pending_cycles)(void) = 0;
|
||||
uint32_t g_dirty_ram_exec_page_bitmap[DIRTY_RAM_EXEC_PAGE_BITMAP_WORDS] = {0};
|
||||
uint32_t g_dirty_ram_exec_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS] = {0};
|
||||
uint32_t g_dirty_ram_dispatch_pc_bitmap[DIRTY_RAM_EXEC_BITMAP_WORDS] = {0};
|
||||
uint32_t g_dirty_ram_dispatch_seg_bitmap[PSX_CODE_SEGMENT_COUNT][DIRTY_RAM_EXEC_BITMAP_WORDS] = {{0}};
|
||||
int g_psx_render_pass_active = 0;
|
||||
|
||||
/* These services are inactive in the ordinary RAM/scratchpad trace probes. */
|
||||
void psx_icache_isc_store(uint32_t biu, uint32_t addr, uint32_t value) {
|
||||
(void)biu; (void)addr; (void)value; abort();
|
||||
}
|
||||
void ls_shadow_record_unreplayable(void) { abort(); }
|
||||
int render_pass_store_to(const RenderPassStoreTarget *t, uint32_t addr,
|
||||
uint32_t value, uint32_t width) {
|
||||
(void)t; (void)addr; (void)value; (void)width; abort();
|
||||
}
|
||||
|
||||
unsigned g_trace_write_calls = 0;
|
||||
unsigned g_parity_write_calls = 0;
|
||||
unsigned g_card_write_calls = 0;
|
||||
uint32_t g_trace_old_accum = 0;
|
||||
uint32_t g_trace_new_accum = 0;
|
||||
|
||||
int fntrace_is_game_started(void) { return 0; }
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
uint32_t ls_read_hook(uint32_t addr, uint8_t width, uint32_t value) {
|
||||
(void)addr; (void)width; return value;
|
||||
}
|
||||
void ls_write_hook(uint32_t addr, uint8_t width, uint32_t value) {
|
||||
(void)addr; (void)width; (void)value;
|
||||
}
|
||||
void ds_note_read(uint32_t addr, uint8_t width) { (void)addr; (void)width; }
|
||||
void ds_note_write(uint32_t addr, uint8_t width) { (void)addr; (void)width; }
|
||||
void ds_note_dma_write(void) {}
|
||||
void psx_devices_mmio_sync(void) {}
|
||||
void psx_fatal_halt(const char *reason) { fprintf(stderr, "%s\n", reason); abort(); }
|
||||
void psx_irq_refresh_cause_ip2(void) {}
|
||||
int sio_card_should_hold_imask_bit7(void) { return 0; }
|
||||
void sio_card_handoff_on_imask(uint32_t old_mask, uint32_t new_mask) {
|
||||
(void)old_mask; (void)new_mask;
|
||||
}
|
||||
uint32_t sio_read(uint32_t addr) { (void)addr; return 0; }
|
||||
void sio_write(uint32_t addr, uint32_t val) { (void)addr; (void)val; }
|
||||
void sio_tick(uint32_t cycles) { (void)cycles; }
|
||||
uint32_t dma_read(uint32_t addr) { (void)addr; return 0; }
|
||||
void dma_write(uint32_t addr, uint32_t val) { (void)addr; (void)val; }
|
||||
void dma_write_masked(uint32_t addr, uint32_t val, uint32_t mask) {
|
||||
(void)addr; (void)val; (void)mask;
|
||||
}
|
||||
uint32_t timers_read(uint32_t addr) { (void)addr; return 0; }
|
||||
void timers_write(uint32_t addr, uint32_t val) { (void)addr; (void)val; }
|
||||
uint32_t cdrom_read(uint32_t addr) { (void)addr; return 0; }
|
||||
void cdrom_write(uint32_t addr, uint32_t val) { (void)addr; (void)val; }
|
||||
uint32_t gpu_read_gpuread(void) { return 0; }
|
||||
uint32_t gpu_read_gpustat(void) { return 0; }
|
||||
void gpu_set_gp0_source(uint32_t source) { (void)source; }
|
||||
void gpu_write_gp0(uint32_t val) { (void)val; }
|
||||
void gpu_write_gp1(uint32_t val) { (void)val; }
|
||||
uint32_t mdec_read(uint32_t addr) { (void)addr; return 0; }
|
||||
void mdec_write(uint32_t addr, uint32_t val) { (void)addr; (void)val; }
|
||||
uint16_t spu_read(uint32_t addr) { (void)addr; return 0; }
|
||||
void spu_write(uint32_t addr, uint16_t val) { (void)addr; (void)val; }
|
||||
void debug_server_trace_mmio_write(uint32_t addr, uint32_t val, uint8_t width) {
|
||||
(void)addr; (void)val; (void)width;
|
||||
}
|
||||
void debug_server_trace_mmio_read(uint32_t addr, uint32_t val, uint8_t width) {
|
||||
(void)addr; (void)val; (void)width;
|
||||
}
|
||||
void debug_server_trace_ram_read_watch(uint32_t phys, uint32_t val) {
|
||||
(void)phys; (void)val;
|
||||
}
|
||||
void debug_server_trace_entryint_write(uint32_t phys, uint32_t old_val,
|
||||
uint32_t new_val, uint8_t width) {
|
||||
(void)phys; (void)old_val; (void)new_val; (void)width;
|
||||
}
|
||||
void parity_trace_note_write(uint32_t addr, uint32_t width, uint32_t writer_pc) {
|
||||
(void)addr; (void)width; (void)writer_pc; g_parity_write_calls++;
|
||||
}
|
||||
int card_data_writes_check(uint32_t phys, uint32_t value, uint8_t width) {
|
||||
(void)phys; (void)value; (void)width; g_card_write_calls++; return 0;
|
||||
}
|
||||
void overlay_loader_note_code_write(void) {}
|
||||
void overlay_loader_active_write_check(uint32_t phys, uint32_t size) {
|
||||
(void)phys; (void)size;
|
||||
}
|
||||
void overlay_loader_resync_validation_after_restore(void) {}
|
||||
"""
|
||||
|
||||
TRACE_COUNTING_C = r"""
|
||||
#include "harness_common.h"
|
||||
void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
|
||||
uint32_t new_val, uint8_t width) {
|
||||
(void)phys;
|
||||
g_trace_write_calls++;
|
||||
g_trace_old_accum ^= old_val + (uint32_t)width;
|
||||
g_trace_new_accum ^= new_val + ((uint32_t)width << 24);
|
||||
}
|
||||
"""
|
||||
|
||||
TRACE_EMPTY_C = r"""
|
||||
#include "harness_common.h"
|
||||
void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
|
||||
uint32_t new_val, uint8_t width) {
|
||||
(void)phys; (void)old_val; (void)new_val; (void)width;
|
||||
}
|
||||
"""
|
||||
|
||||
DRIVER_C = r"""
|
||||
#include "harness_common.h"
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
uint8_t *memory_get_ram_ptr(void);
|
||||
uint8_t *memory_get_scratchpad_ptr(void);
|
||||
void psx_write_word(uint32_t addr, uint32_t val);
|
||||
void psx_write_half(uint32_t addr, uint16_t val);
|
||||
void psx_write_byte(uint32_t addr, uint8_t val);
|
||||
extern uint64_t g_guest_store_count;
|
||||
|
||||
static uint32_t hash_bytes(const uint8_t *bytes, const uint32_t *points,
|
||||
unsigned count, uint32_t h) {
|
||||
for (unsigned i = 0; i < count; i++) h = (h ^ bytes[points[i]]) * 16777619u;
|
||||
return h;
|
||||
}
|
||||
|
||||
static void seed_memory(void) {
|
||||
uint8_t *ram = memory_get_ram_ptr();
|
||||
uint8_t *sp = memory_get_scratchpad_ptr();
|
||||
ram[0x1000u] = 0x10u; ram[0x1001u] = 0x32u;
|
||||
ram[0x1002u] = 0x54u; ram[0x1003u] = 0x76u;
|
||||
ram[0x1004u] = 0x98u; ram[0x1005u] = 0xBAu; ram[0x1006u] = 0xDCu;
|
||||
sp[0] = 0x11u; sp[1] = 0x22u; sp[2] = 0x33u; sp[3] = 0x44u;
|
||||
sp[4] = 0x55u; sp[5] = 0x66u; sp[6] = 0x77u;
|
||||
}
|
||||
|
||||
static void do_six_writes(unsigned index) {
|
||||
uint32_t mirror = (index & 3u) << 21;
|
||||
uint32_t sp_segment = (index & 1u) ? 0x80000000u : 0u;
|
||||
psx_write_word(0x80001000u + mirror, 0xA1B2C3D4u);
|
||||
psx_write_word(0x1F800000u + sp_segment, 0x10203040u);
|
||||
psx_write_half(0xA0001004u + mirror, 0xCAFEu);
|
||||
psx_write_half(0x1F800004u + sp_segment, 0xBEEFu);
|
||||
psx_write_byte(0x00001006u + mirror, 0x5Au);
|
||||
psx_write_byte(0x1F800006u + sp_segment, 0x6Bu);
|
||||
}
|
||||
|
||||
static int check_written_bytes(void) {
|
||||
static const uint8_t expected_ram[] = {0xD4, 0xC3, 0xB2, 0xA1, 0xFE, 0xCA, 0x5A, 0};
|
||||
static const uint8_t expected_sp[] = {0x40, 0x30, 0x20, 0x10, 0xEF, 0xBE, 0x6B, 0};
|
||||
for (unsigned i = 0; i < sizeof(expected_ram); ++i) {
|
||||
if (memory_get_ram_ptr()[0x1000u + i] != expected_ram[i] ||
|
||||
memory_get_scratchpad_ptr()[i] != expected_sp[i]) return 0;
|
||||
}
|
||||
return memory_get_ram_ptr()[0xFFFu] == 0;
|
||||
}
|
||||
|
||||
static uint32_t state_hash(void) {
|
||||
static const uint32_t ram_points[] = {
|
||||
0x1000u, 0x1001u, 0x1002u, 0x1003u, 0x1004u, 0x1005u, 0x1006u,
|
||||
};
|
||||
static const uint32_t sp_points[] = {
|
||||
0x000u, 0x001u, 0x002u, 0x003u, 0x004u, 0x005u, 0x006u,
|
||||
};
|
||||
uint32_t h = 2166136261u;
|
||||
h = hash_bytes(memory_get_ram_ptr(), ram_points, 7u, h);
|
||||
h = hash_bytes(memory_get_scratchpad_ptr(), sp_points, 7u, h);
|
||||
h ^= (uint32_t)g_guest_store_count * 3u;
|
||||
h ^= g_parity_write_calls * 5u;
|
||||
h ^= g_card_write_calls * 7u;
|
||||
return h;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
unsigned loops = 1;
|
||||
if (argc == 3 && argv[1][0] == '-' && argv[1][1] == '-' &&
|
||||
argv[1][2] == 'l') {
|
||||
loops = (unsigned)strtoul(argv[2], 0, 0);
|
||||
if (loops == 0) loops = 1;
|
||||
}
|
||||
seed_memory();
|
||||
for (unsigned i = 0; i < loops; i++) do_six_writes(i);
|
||||
if (!check_written_bytes()) {
|
||||
fprintf(stderr, "incorrect RAM/scratchpad bytes or alias folding\n");
|
||||
return 1;
|
||||
}
|
||||
printf("state=%08x trace_calls=%u parity=%u card=%u stores=%llu old=%08x new=%08x\n",
|
||||
state_hash(), g_trace_write_calls, g_parity_write_calls,
|
||||
g_card_write_calls, (unsigned long long)g_guest_store_count,
|
||||
g_trace_old_accum, g_trace_new_accum);
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def default_cc() -> str:
|
||||
if DEFAULT_GCC.exists():
|
||||
return str(DEFAULT_GCC).replace("\\", "/")
|
||||
for name in ("cc", "gcc", "clang"):
|
||||
path = shutil.which(name)
|
||||
if path:
|
||||
return path
|
||||
return str(DEFAULT_GCC)
|
||||
|
||||
|
||||
def normalize_cc(cc: str) -> str:
|
||||
if "\\" in cc and len(cc) >= 3 and cc[1] == ":":
|
||||
return cc.replace("\\", "/")
|
||||
return cc
|
||||
|
||||
|
||||
def source_text(path: pathlib.Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def assert_guarded(memory: str) -> None:
|
||||
if memory.count(TRACE_CALL) != 8:
|
||||
fail("memory.c should contain one extern plus seven trace write call sites")
|
||||
extern = memory.find("extern void " + TRACE_CALL)
|
||||
if extern < 0:
|
||||
fail("missing trace write extern")
|
||||
cursor = extern + len("extern void " + TRACE_CALL)
|
||||
for index in range(7):
|
||||
pos = memory.find(TRACE_CALL + "(", cursor)
|
||||
if pos < 0:
|
||||
fail(f"missing trace write call site {index + 1}")
|
||||
line_start = memory.rfind("\n", 0, pos)
|
||||
guard = memory.rfind("#ifndef PSX_NO_DEBUG_TOOLS", 0, pos)
|
||||
end_before = memory.rfind("#endif", 0, pos)
|
||||
if guard < 0 or guard < end_before:
|
||||
fail(f"trace write call site {index + 1} is not production-guarded")
|
||||
if line_start - guard > 300:
|
||||
fail(f"trace write call site {index + 1} guard is too far away")
|
||||
cursor = pos + len(TRACE_CALL)
|
||||
|
||||
|
||||
def assert_observers_preserved(memory: str) -> None:
|
||||
required = (
|
||||
("psx_write_word_raw", "static uint16_t psx_read_half_raw", (
|
||||
"parity_trace_note_write(phys, 4, effective_store_pc())",
|
||||
"card_data_writes_check(phys, val, 4)",
|
||||
"dirty_ram_mark_kernel_write(phys)",
|
||||
"text_guard_note_write(phys, val, 4)",
|
||||
"overlay_watch_note_write(phys, 4)",
|
||||
"ram[phys] = (uint8_t)(val)",
|
||||
)),
|
||||
("psx_write_half_raw", "static uint8_t psx_read_byte_raw", (
|
||||
"parity_trace_note_write(phys, 2, effective_store_pc())",
|
||||
"card_data_writes_check(phys, (uint32_t)val, 2)",
|
||||
"dirty_ram_mark_kernel_write(phys)",
|
||||
"text_guard_note_write(phys, (uint32_t)val, 2)",
|
||||
"overlay_watch_note_write(phys, 2)",
|
||||
"ram[phys] = (uint8_t)(val)",
|
||||
)),
|
||||
("psx_write_byte_raw", None, (
|
||||
"parity_trace_note_write(phys, 1, effective_store_pc())",
|
||||
"card_data_writes_check(phys, (uint32_t)val, 1)",
|
||||
"dirty_ram_mark_kernel_write(phys)",
|
||||
"text_guard_note_write(phys, (uint32_t)val, 1)",
|
||||
"overlay_watch_note_write(phys, 1)",
|
||||
"ram[phys] = val",
|
||||
)),
|
||||
)
|
||||
for function, successor, tokens in required:
|
||||
start = memory.find(f"static void {function}")
|
||||
if start < 0:
|
||||
fail(f"missing {function}")
|
||||
end = len(memory) if successor is None else memory.find(successor, start + 1)
|
||||
if successor is not None and end < 0:
|
||||
fail(f"could not bound {function}")
|
||||
body = memory[start:end]
|
||||
last = -1
|
||||
for token in tokens:
|
||||
pos = body.find(token)
|
||||
if pos < 0:
|
||||
fail(f"{function} lost observer/store token: {token}")
|
||||
if pos <= last:
|
||||
fail(f"{function} observer/store order changed at: {token}")
|
||||
last = pos
|
||||
|
||||
|
||||
def write_fixture(work: pathlib.Path, memory_source: pathlib.Path) -> pathlib.Path:
|
||||
for name, content in (("harness_common.h", HARNESS_COMMON_H),
|
||||
("stubs.c", STUBS_C),
|
||||
("trace_counting.c", TRACE_COUNTING_C),
|
||||
("trace_empty.c", TRACE_EMPTY_C),
|
||||
("driver.c", DRIVER_C)):
|
||||
(work / name).write_text(content, encoding="utf-8")
|
||||
return memory_source.resolve()
|
||||
|
||||
|
||||
def build(cc: str, work: pathlib.Path, memory_source: pathlib.Path, name: str,
|
||||
defines: tuple[str, ...], trace_source: str) -> pathlib.Path:
|
||||
work = work.resolve()
|
||||
memory_copy = write_fixture(work, memory_source)
|
||||
exe = work / (name + ".exe")
|
||||
cmd = [
|
||||
cc,
|
||||
"-std=c11",
|
||||
"-O2",
|
||||
"-fno-lto",
|
||||
"-ffunction-sections",
|
||||
"-fdata-sections",
|
||||
"-I",
|
||||
str(work),
|
||||
"-I",
|
||||
str(ROOT / "runtime" / "include"),
|
||||
]
|
||||
for define in defines:
|
||||
cmd.append(f"-D{define}")
|
||||
cmd.extend([
|
||||
str(work / "driver.c"),
|
||||
str(memory_copy),
|
||||
str(ROOT / "runtime" / "src" / "kernel_patch_ranges.c"),
|
||||
str(ROOT / "runtime" / "src" / "psx_ram_geometry.c"),
|
||||
str(work / "stubs.c"),
|
||||
str(work / trace_source),
|
||||
"-Wl,-dead_strip" if sys.platform == "darwin" else "-Wl,--gc-sections",
|
||||
"-o",
|
||||
str(exe),
|
||||
])
|
||||
subprocess.run(cmd, cwd=work, check=True)
|
||||
return exe
|
||||
|
||||
|
||||
def parse(line: str) -> dict[str, str]:
|
||||
return dict(part.split("=", 1) for part in line.split())
|
||||
|
||||
|
||||
def run(exe: pathlib.Path, loops: int = 1) -> dict[str, str]:
|
||||
cmd = [str(exe)]
|
||||
if loops != 1:
|
||||
cmd.extend(["--loops", str(loops)])
|
||||
proc = subprocess.run(cmd, cwd=exe.parent, check=True, text=True,
|
||||
stdout=subprocess.PIPE)
|
||||
return parse(proc.stdout.strip())
|
||||
|
||||
|
||||
def run_regression(cc: str) -> None:
|
||||
with tempfile.TemporaryDirectory(prefix="psx-memory-trace-") as td:
|
||||
root = pathlib.Path(td)
|
||||
prod_dir = root / "prod"
|
||||
dbg_dir = root / "debug"
|
||||
prod_dir.mkdir()
|
||||
dbg_dir.mkdir()
|
||||
prod = build(cc, prod_dir, MEMORY_C, "current_prod",
|
||||
("PSX_NO_DEBUG_TOOLS",), "trace_counting.c")
|
||||
debug = build(cc, dbg_dir, MEMORY_C, "current_debug",
|
||||
tuple(), "trace_counting.c")
|
||||
prod_out = run(prod, 4)
|
||||
debug_out = run(debug, 4)
|
||||
if prod_out["trace_calls"] != "0":
|
||||
fail("production memory.c still calls debug_server_trace_write_check")
|
||||
if debug_out["trace_calls"] != "24":
|
||||
fail("debug memory.c no longer calls all six trace write probes")
|
||||
if prod_out["state"] != debug_out["state"]:
|
||||
fail("production guard changed guest-visible write state")
|
||||
for field in ("parity", "card", "stores"):
|
||||
if prod_out[field] != debug_out[field]:
|
||||
fail(f"production guard changed {field} count")
|
||||
|
||||
|
||||
def run_with_baseline(cc: str, baseline: pathlib.Path) -> None:
|
||||
if not baseline.exists():
|
||||
fail(f"baseline memory source does not exist: {baseline}")
|
||||
with tempfile.TemporaryDirectory(prefix="psx-memory-trace-baseline-") as td:
|
||||
root = pathlib.Path(td)
|
||||
old_dir = root / "old"
|
||||
new_dir = root / "new"
|
||||
old_dir.mkdir()
|
||||
new_dir.mkdir()
|
||||
old = build(cc, old_dir, baseline, "baseline_prod",
|
||||
("PSX_NO_DEBUG_TOOLS",), "trace_counting.c")
|
||||
new = build(cc, new_dir, MEMORY_C, "candidate_prod",
|
||||
("PSX_NO_DEBUG_TOOLS",), "trace_counting.c")
|
||||
old_out = run(old, 4)
|
||||
new_out = run(new, 4)
|
||||
if old_out["trace_calls"] != "24":
|
||||
fail("baseline production source did not execute the six trace calls")
|
||||
if new_out["trace_calls"] != "0":
|
||||
fail("candidate production source still executes trace calls")
|
||||
if old_out["state"] != new_out["state"]:
|
||||
fail("candidate production changed guest-visible state vs baseline")
|
||||
for field in ("parity", "card", "stores"):
|
||||
if old_out[field] != new_out[field]:
|
||||
fail(f"candidate production changed {field} count vs baseline")
|
||||
|
||||
|
||||
def build_bench(cc: str, out_dir: pathlib.Path, baseline: pathlib.Path) -> None:
|
||||
if not baseline.exists():
|
||||
fail(f"baseline memory source does not exist: {baseline}")
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
old_dir = out_dir / "baseline_prod_empty"
|
||||
new_dir = out_dir / "candidate_prod_empty"
|
||||
old_dir.mkdir(parents=True, exist_ok=True)
|
||||
new_dir.mkdir(parents=True, exist_ok=True)
|
||||
old = build(cc, old_dir, baseline, "baseline_prod_empty",
|
||||
("PSX_NO_DEBUG_TOOLS",), "trace_empty.c")
|
||||
new = build(cc, new_dir, MEMORY_C, "candidate_prod_empty",
|
||||
("PSX_NO_DEBUG_TOOLS",), "trace_empty.c")
|
||||
print(f"baseline={old}")
|
||||
print(f"candidate={new}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--cc", default=default_cc())
|
||||
parser.add_argument("--baseline-memory", type=pathlib.Path)
|
||||
parser.add_argument("--build-bench", type=pathlib.Path,
|
||||
help="build old/candidate empty-trace binaries here")
|
||||
args = parser.parse_args()
|
||||
args.cc = normalize_cc(args.cc)
|
||||
|
||||
memory = source_text(MEMORY_C)
|
||||
assert_guarded(memory)
|
||||
assert_observers_preserved(memory)
|
||||
|
||||
if args.build_bench:
|
||||
if not args.baseline_memory:
|
||||
fail("--build-bench requires --baseline-memory")
|
||||
build_bench(args.cc, args.build_bench, args.baseline_memory)
|
||||
return 0
|
||||
|
||||
run_regression(args.cc)
|
||||
if args.baseline_memory:
|
||||
run_with_baseline(args.cc, args.baseline_memory)
|
||||
print("PASS: actual memory.c debug trace guards preserve production state")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
sys.exit(main())
|
||||
except AssertionError as exc:
|
||||
print(f"FAIL: {exc}")
|
||||
sys.exit(1)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"FAIL: command exited {exc.returncode}: {' '.join(map(str, exc.cmd))}")
|
||||
sys.exit(1)
|
||||
@@ -111,6 +111,24 @@ static void test_active_entry(CPUState*, uint32_t address) {
|
||||
static void test_disabled_entry(CPUState*, uint32_t) { disabled_entry_hits++; }
|
||||
static void test_unselected_entry(CPUState*, uint32_t) { unselected_entry_hits++; }
|
||||
|
||||
static bool filter_handles;
|
||||
static bool filter_context_active;
|
||||
static unsigned active_filter_hits;
|
||||
static unsigned inactive_filter_hits;
|
||||
static uint32_t active_filter_last;
|
||||
static int test_active_filter(CPUState* cpu, uint32_t address) {
|
||||
++active_filter_hits;
|
||||
active_filter_last = address;
|
||||
filter_context_active = psx_mod_function_entry_active() != 0;
|
||||
if (!filter_handles) return 0;
|
||||
cpu->gpr[2] = 42;
|
||||
return 1;
|
||||
}
|
||||
static int test_inactive_filter(CPUState*, uint32_t) {
|
||||
++inactive_filter_hits;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void test_activation_plugin(void) {
|
||||
activation_calls++;
|
||||
}
|
||||
@@ -428,6 +446,42 @@ int main() {
|
||||
disabled_entry_hits == 0 && unselected_entry_hits == 0,
|
||||
"an offline rematch after netplay must arm exactly its plan's hooks");
|
||||
|
||||
check(psx_mod_register_function_filter_plugin(
|
||||
"runtime.test-vblank", 0x80003008u, test_active_filter) == 1,
|
||||
"active plan's return filter must register");
|
||||
check(psx_mod_register_function_filter_plugin(
|
||||
"runtime.test-vblank", 0xA0003008u, test_active_filter) == 0,
|
||||
"filter aliases must not register duplicate hooks");
|
||||
check(psx_mod_register_function_filter_plugin(
|
||||
"runtime.test-disabled-entry", 0x80003008u, test_inactive_filter) == 1 &&
|
||||
psx_mod_register_function_filter_plugin(
|
||||
"runtime.unselected-entry", 0x80003008u, test_inactive_filter) == 1,
|
||||
"inactive filter implementations must register without running");
|
||||
entry_cpu.pc = 0x80003008u;
|
||||
entry_cpu.gpr[31] = 0x80004000u;
|
||||
entry_cpu.gpr[2] = 99;
|
||||
check(!psx_mod_function_entry(&entry_cpu, entry_cpu.pc) && active_filter_hits == 0,
|
||||
"new filters must wait for active-plan table rebuild");
|
||||
mod_runtime_activate_plugins();
|
||||
check(g_psx_mod_function_entry_hooks == 2 &&
|
||||
!psx_mod_function_entry(&entry_cpu, entry_cpu.pc) &&
|
||||
active_filter_hits == 1 && filter_context_active &&
|
||||
entry_cpu.pc == 0x80003008u && entry_cpu.gpr[2] == 99,
|
||||
"unhandled active filter must preserve native fallthrough");
|
||||
filter_handles = true;
|
||||
entry_cpu.pc = 0xA0003008u;
|
||||
check(psx_mod_function_entry(&entry_cpu, entry_cpu.pc) == 1 &&
|
||||
active_filter_hits == 2 && active_filter_last == 0xA0003008u &&
|
||||
entry_cpu.pc == entry_cpu.gpr[31] && entry_cpu.gpr[2] == 42 &&
|
||||
inactive_filter_hits == 0 && !psx_mod_function_entry_active(),
|
||||
"handled alias filter must publish return registers and pc=$ra");
|
||||
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str());
|
||||
check(!psx_mod_function_entry(&entry_cpu, 0x80003008u) &&
|
||||
active_filter_hits == 2 && g_psx_mod_function_entry_hooks == 0,
|
||||
"clearing the active plan must also disarm return filters");
|
||||
check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str());
|
||||
mod_runtime_activate_plugins();
|
||||
|
||||
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
|
||||
ram[0x1100] = 0; ram[0x1101] = 0;
|
||||
ram[0x1102] = 1; ram[0x1103] = 0;
|
||||
|
||||
@@ -373,10 +373,16 @@ class ToolchainStagingTest(unittest.TestCase):
|
||||
for d in (self.tools, self.include, self.bios, self.recomp, self.mingw):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
touch(os.path.join(self.tools, 'compile_overlays.py'))
|
||||
touch(os.path.join(self.tools, 'bios_module_build.py'))
|
||||
touch(os.path.join(self.include, 'overlay_api.h'))
|
||||
touch(os.path.join(self.include, 'overlay_dispatch_preamble.c.inc'))
|
||||
touch(os.path.join(self.include, 'psx_bios_module_glue.c.inc'))
|
||||
touch(os.path.join(self.include, 'psx_bios_module.h'))
|
||||
touch(os.path.join(self.bios, 'SCPH1001.toml'))
|
||||
touch(os.path.join(self.recomp, 'psxrecomp-game.exe'))
|
||||
touch(os.path.join(self.recomp, 'psxrecomp-bios.exe'))
|
||||
touch(os.path.join(self.framework, 'recompiler', 'seeds',
|
||||
'phase2_ghidra_seeds.json'))
|
||||
for d in ('libgcc_s_seh-1.dll', 'libstdc++-6.dll',
|
||||
'libwinpthread-1.dll'):
|
||||
touch(os.path.join(self.mingw, d))
|
||||
|
||||
@@ -34,6 +34,8 @@ assert "psx_irq_resume_context_snapshot_safe(void)" in INTERRUPTS
|
||||
assert "psx_irq_resume_context_snapshot_safe_at(uint32_t resume_pc)" in INTERRUPTS
|
||||
assert "psx_irq_resume_context_snapshot_pc(void)" in INTERRUPTS
|
||||
assert "g_cosim_dirty_pump_site == 0" in INTERRUPTS
|
||||
assert INTERRUPTS.index('if (psx_mod_function_entry_active()) return 0;') < INTERRUPTS.index('if (g_cosim_dirty_pump_site == 0)')
|
||||
assert 'if (psx_mod_function_entry_active()) return;' in STATE
|
||||
assert "case 1: /* transfer surface: target PC is materialized in CPUState */" in INTERRUPTS
|
||||
assert "cands[n++] = psx_irq_resume_context_snapshot_pc();" in STATE
|
||||
assert "cands[n++] = psx_irq_resume_context_snapshot_pc();" in REWIND
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""Execute the actual ADDI/ADDIU bodies with isolated widescreen services."""
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
source = (Path(__file__).resolve().parents[1] / 'src/dirty_ram_interp.c').read_text(encoding='utf-8')
|
||||
start = source.index(' case 0x08: /* ADDI rt, rs, simm')
|
||||
end = source.index(' case 0x0A: /* SLTI */', start)
|
||||
cases = source[start:end]
|
||||
fixture = r'''
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
typedef struct { uint32_t gpr[32]; } CPUState;
|
||||
static int margin;
|
||||
static int psx_ws_activation_margin(void) { return margin; }
|
||||
static int psx_ws_is_cull_bias_site(uint32_t pc) { return pc == 4; }
|
||||
static int psx_ws_is_cull_bias_lower_site(uint32_t pc) { return pc == 8; }
|
||||
static int psx_ws_angle_site(uint32_t pc, uint32_t insn, uint32_t *out) { return 0; }
|
||||
static int psx_ws_is_signed_x_bound_site(uint32_t pc, uint32_t insn) { return 0; }
|
||||
static int psx_ws_screen_x_bound(int imm) { assert(0); return imm; }
|
||||
#define psx_pgxp_alu(...) ((void)0)
|
||||
static int execute(CPUState *cpu, uint32_t pc, uint32_t insn) {
|
||||
unsigned rs = (insn >> 21) & 31, rt = (insn >> 16) & 31;
|
||||
int32_t simm = (int16_t)insn;
|
||||
switch (insn >> 26) {
|
||||
@CASES@
|
||||
default: assert(0); return 1;
|
||||
}
|
||||
}
|
||||
int main(void) {
|
||||
const int margins[] = {0, 85, 138, 234};
|
||||
const uint32_t cameras[] = {0, 51, 1024, 0xffff, 0xffffffff};
|
||||
for (unsigned op = 8; op <= 9; op++)
|
||||
for (unsigned m = 0; m < 4; m++)
|
||||
for (unsigned c = 0; c < 5; c++)
|
||||
for (unsigned kind = 0; kind < 3; kind++) {
|
||||
CPUState cpu, expected;
|
||||
for (unsigned r = 0; r < 32; r++) cpu.gpr[r] = r * 7919;
|
||||
cpu.gpr[4] = cameras[c];
|
||||
expected = cpu;
|
||||
margin = margins[m];
|
||||
int imm = kind == 1 ? 368 : -48;
|
||||
expected.gpr[2] = cameras[c] + (uint32_t)imm;
|
||||
if (kind == 1) expected.gpr[2] += margin;
|
||||
if (kind == 2) expected.gpr[2] -= margin;
|
||||
assert(execute(&cpu, kind * 4, (op << 26) | (4 << 21) | (2 << 16) | (uint16_t)imm) == 0);
|
||||
assert(memcmp(&cpu, &expected, sizeof cpu) == 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
'''.replace('@CASES@', cases)
|
||||
with tempfile.TemporaryDirectory(prefix='ws-activation-') as directory:
|
||||
path = Path(directory)
|
||||
(path / 'test.c').write_text(fixture, encoding='utf-8')
|
||||
subprocess.run([sys.argv[1], '-std=c99', str(path / 'test.c'), '-o', str(path / 'test.exe')], check=True)
|
||||
subprocess.run([str(path / 'test.exe')], check=True)
|
||||
print('activation bias ADDI/ADDIU: 4:3 identity, both sides, unrelated sites and register preservation PASS')
|
||||
@@ -0,0 +1,29 @@
|
||||
#ifdef NDEBUG
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include "ws_screen_mask.h"
|
||||
#include "ws_hud_anchor.h"
|
||||
int main(void) {
|
||||
int32_t x[4]={-96,160,-96,68},y[4]={0,0,240,240};
|
||||
WsScreenMaskBand b;
|
||||
for(int margin=0;margin<16000;margin+=53) {
|
||||
int result=ws_screen_mask_band(x,y,320,240,margin,&b);
|
||||
assert(result==(margin>96));
|
||||
if(result) assert(b.x==-margin && b.x+b.w==-96 && b.y==0 && b.h==240);
|
||||
}
|
||||
x[0]=160;x[1]=416;x[2]=308;x[3]=416;
|
||||
assert(ws_screen_mask_band(x,y,320,240,694,&b));
|
||||
assert(b.x==416 && b.w==598);
|
||||
x[3]=415; assert(!ws_screen_mask_band(x,y,320,240,694,&b));
|
||||
x[3]=416;y[3]=239;assert(!ws_screen_mask_band(x,y,320,240,694,&b));
|
||||
/* Same packet address with different geometry/colour must lose its tag. */
|
||||
WsHudAnchorTag tags[WS_HUD_ANCHOR_TABLE_SIZE]={0};
|
||||
uint32_t words[5]={0x2a7b7b7b,0x0000ffa0,0xa0,0xf0ffa0,0xf00044};
|
||||
WsPrepassPacketGuard guard=ws_prepass_packet_guard(words,5);
|
||||
ws_hud_anchor_insert(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,0,&guard,20);
|
||||
assert(ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0));
|
||||
words[2]++;assert(!ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0));
|
||||
words[2]--;ws_hud_anchor_clear(tags,WS_HUD_ANCHOR_TABLE_SIZE);
|
||||
assert(!ws_hud_anchor_lookup(tags,WS_HUD_ANCHOR_TABLE_SIZE,0xa68b4,words,5,20,0));
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#ifdef NDEBUG
|
||||
#undef NDEBUG
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "ws_view_anchor.h"
|
||||
#include "../src/gpu_sw_renderer.c"
|
||||
|
||||
int g_ws_bd_stretch_on, g_ws_bd_stretch_pct;
|
||||
int psx_ws_prim_in_backdrop(void) { return 0; }
|
||||
int g_psx_vram_dirty_tracking;
|
||||
void gpu_vram_dirty_mark_row_impl(uint32_t y) { (void)y; }
|
||||
void gpu_vram_dirty_mark_rect(int x, int y, int w, int h) {
|
||||
(void)x; (void)y; (void)w; (void)h;
|
||||
}
|
||||
void gpu_vram_dirty_mark_all(void) {}
|
||||
|
||||
static uint16_t native[1024 * 512], reference[1024 * 512];
|
||||
|
||||
static void check(int cx, int lo, int hi, int left, int right, int shift) {
|
||||
WsViewAnchor v = ws_view_anchor(53, cx, lo, hi);
|
||||
assert(v.left == left && v.right == right && v.shift == shift);
|
||||
assert(v.left + v.right + v.pad_left + v.pad_right == 106);
|
||||
assert(53 + v.shift - v.left == v.pad_left);
|
||||
assert(53 + v.shift + 320 + v.right == 426 - v.pad_right);
|
||||
}
|
||||
|
||||
static void draw(int shift, int pad_left, int pad_right) {
|
||||
sw_wide_clear(0, 0, 240, 0);
|
||||
memset(native, 0, sizeof(native));
|
||||
sw_wide_set_view(1, shift, pad_left, pad_right);
|
||||
/* Every column has a distinct world color. Include both potential reveal
|
||||
* regions so the renderer, rather than the fixture, decides visibility. */
|
||||
for (int x = -106; x < 426; x++)
|
||||
sw_draw_flat_rect(x, 40, 1, 1, (uint16_t)(0x4000 | ((x + 107) & 1023)));
|
||||
/* Existing left/center/right HUD placements, independent of world origin. */
|
||||
sw_wide_set_view(1, 0, 0, 0);
|
||||
sw_draw_flat_rect(19 - 53, 10, 3, 1, 31);
|
||||
sw_draw_flat_rect(160, 10, 3, 1, 31);
|
||||
sw_draw_flat_rect(300 + 53, 10, 3, 1, 31);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
/* Same framing at 32:9 / 64:9 and beyond; finite rooms keep symmetric
|
||||
* padding while a full stage transfers the reveal to the available side. */
|
||||
const int extras[] = {267, 693, 1600};
|
||||
for (unsigned i = 0; i < sizeof extras / sizeof extras[0]; ++i) {
|
||||
int e = extras[i];
|
||||
WsViewAnchor left = ws_view_anchor(e,0,0,5120);
|
||||
WsViewAnchor right = ws_view_anchor(e,5120,0,5120);
|
||||
WsViewAnchor room = ws_view_anchor(e,200,200,200);
|
||||
assert(left.left==0 && left.right==2*e && left.shift==-e);
|
||||
assert(right.left==2*e && right.right==0 && right.shift==e);
|
||||
assert(room.left==0 && room.right==0 && room.shift==0);
|
||||
assert(room.pad_left==e && room.pad_right==e);
|
||||
}
|
||||
check(0, 0, 1000, 0, 106, -53);
|
||||
check(20, 0, 1000, 20, 86, -33);
|
||||
check(500, 0, 1000, 53, 53, 0);
|
||||
check(980, 0, 1000, 86, 20, 33);
|
||||
check(1000, 0, 1000, 106, 0, 53);
|
||||
check(200, 200, 200, 0, 0, 0);
|
||||
check(200, 200, 249, 0, 49, -25);
|
||||
check(249, 200, 249, 49, 0, 24);
|
||||
/* Continuous, monotonic origin for every pixel of a full-room traversal. */
|
||||
int previous = -53;
|
||||
for (int x = 0; x <= 1000; x++) {
|
||||
WsViewAnchor v = ws_view_anchor(53, x, 0, 1000);
|
||||
assert(v.shift >= previous && v.shift <= previous + 1);
|
||||
previous = v.shift;
|
||||
}
|
||||
assert(ws_view_anchor(53, -1, 0, 1000).shift == 0);
|
||||
assert(ws_view_anchor(53, 500, 1000, 0).shift == 0);
|
||||
assert(ws_view_anchor(0, 0, 0, 1000).left == 0);
|
||||
|
||||
sw_renderer_init(native);
|
||||
sw_set_draw_area(0, 0, 319, 239);
|
||||
sw_wide_configure(426, 53);
|
||||
sw_wide_set_target(0);
|
||||
draw(0, 0, 0);
|
||||
memcpy(reference, native, sizeof(native));
|
||||
for (int shift = -53; shift <= 53; shift++) {
|
||||
draw(shift, 0, 0);
|
||||
assert(memcmp(reference, native, sizeof(native)) == 0);
|
||||
assert(g_wide_cur[40 * 426] == (uint16_t)(0x4000 | (54 - shift)));
|
||||
assert(g_wide_cur[40 * 426 + 425] == (uint16_t)(0x4000 | (479 - shift)));
|
||||
assert(g_wide_cur[10 * 426 + 19] == 31);
|
||||
assert(g_wide_cur[10 * 426 + 213] == 31);
|
||||
assert(g_wide_cur[10 * 426 + 406] == 31);
|
||||
}
|
||||
draw(-25, 28, 29);
|
||||
for (int x = 0; x < 426; x++)
|
||||
assert((g_wide_cur[40 * 426 + x] != 0) == (x >= 28 && x < 397));
|
||||
assert(g_wide_cur[10 * 426 + 19] == 31); /* HUD can occupy padding. */
|
||||
sw_wide_set_view(0, 77, 28, 29);
|
||||
assert(wide_dx() == 53 && rt_wide().cx1 == 0 && rt_wide().cx2 == 425);
|
||||
puts("ws_view_anchor: geometry, edge pixels, fixed HUD, narrow-room clip, canonical identity PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* Reuse the executable GPU fixture, including its existing HUD regression. */
|
||||
#define main textured_dot_regression_main
|
||||
#include "test_gpu_textured_dot_nw_shift_exec.c"
|
||||
#undef main
|
||||
|
||||
static void half_at(uint32_t addr, uint16_t value) {
|
||||
memcpy((uint8_t *)test_ram + (addr & 0x1fffffu), &value, 2);
|
||||
}
|
||||
int main(void) {
|
||||
assert(textured_dot_regression_main() == 0);
|
||||
reset_gpu_state_for_test();
|
||||
configure_native_wide_16_9();
|
||||
const uint32_t lower_sites[] = {0x80029fd0u};
|
||||
assert(!psx_ws_is_cull_bias_lower_site(lower_sites[0]));
|
||||
gpu_ws_set_bias_lower_cull_sites(lower_sites, 1);
|
||||
assert(psx_ws_is_cull_bias_lower_site(0xa0029fd0u));
|
||||
assert(!psx_ws_is_cull_bias_lower_site(0x80029fd4u));
|
||||
gpu_ws_set_bias_lower_cull_sites(NULL, 0);
|
||||
assert(!psx_ws_is_cull_bias_lower_site(lower_sites[0]));
|
||||
gpu_ws_set_view_anchor(0x80097202u, 0x80097216u, 0x80097214u, 0x800971f8u);
|
||||
half_at(0x80097214u, 5120);
|
||||
half_at(0x80097216u, 0);
|
||||
for (unsigned i = 0; i < 3; i++) {
|
||||
uint32_t b = 0x971f8u + i * 0x54;
|
||||
((uint8_t *)test_ram)[b] = 1;
|
||||
((uint8_t *)test_ram)[b + 0x52] = 255;
|
||||
((uint8_t *)test_ram)[b + 0x4e] = 25;
|
||||
}
|
||||
half_at(0x80097202u, 50);
|
||||
half_at(0x80097256u, 25);
|
||||
half_at(0x800972aau, 12);
|
||||
g_mmx6_freshfix = 0; /* This fixture isolates geometry from tile decoding. */
|
||||
gpu_ws_bg2d_begin_view_layer(0, 0x80010000u, 6);
|
||||
assert(psx_ws_bg2d_startcol(3, 63) == 63);
|
||||
assert(psx_ws_bg2d_cols(21) == 29);
|
||||
gpu_ws_bg2d_end_view_layer(0, 0x80011000u);
|
||||
gpu_ws_bg2d_begin_view_layer(1, 0x80011000u, 6);
|
||||
assert(psx_ws_bg2d_startcol(1, 63) == 63);
|
||||
assert(psx_ws_bg2d_cols(21) == 29);
|
||||
gpu_ws_bg2d_end_view_layer(1, 0x80012000u);
|
||||
gpu_ws_bg2d_begin_view_layer(2, 0x80012000u, 6);
|
||||
assert(psx_ws_bg2d_startcol(0, 63) == 63);
|
||||
assert(psx_ws_bg2d_cols(21) == 28);
|
||||
gpu_ws_bg2d_end_view_layer(2, 0x80013000u);
|
||||
gp0_cmd_source_addr = 0x12004;
|
||||
WsViewAnchor bg = ws_view_packet();
|
||||
assert(bg.shift == -41 && bg.left == 12 && bg.right == 94);
|
||||
gp0_cmd_source_addr = 0x10004;
|
||||
assert(ws_view_packet().shift == -3);
|
||||
/* Transient camera writes do not change geometry after renderer setup. */
|
||||
half_at(0x80097202u, 0);
|
||||
assert(ws_view_current().left == 50);
|
||||
s_frame_count++;
|
||||
gpu_ws_bg2d_begin_view_layer(0, 0x80014000u, 6);
|
||||
assert(psx_ws_bg2d_startcol(0, 63) == 0);
|
||||
assert(psx_ws_bg2d_cols(21) == 28);
|
||||
assert(ws_view_current().left == 0 && ws_view_current().right == 106);
|
||||
/* A camera lock inside continuous geometry must not become a new scene
|
||||
* edge. An explicit scene bound still anchors at the real outer edges. */
|
||||
half_at(0x80097202u, 1968); half_at(0x80097216u, 1968);
|
||||
gpu_ws_set_view_bounds_override(1, 0, 5120);
|
||||
ws_view_sample();
|
||||
assert(ws_view_current().left == 53 && ws_view_current().shift == 0);
|
||||
half_at(0x80097202u, 0); ws_view_sample();
|
||||
assert(ws_view_current().left == 0 && ws_view_current().right == 106);
|
||||
gpu_ws_set_view_bounds_override(0, 0, 5120);
|
||||
half_at(0x80097202u, 1968); ws_view_sample();
|
||||
assert(ws_view_current().left == 0 && ws_view_current().right == 106);
|
||||
half_at(0x80097216u, 0); half_at(0x80097202u, 0);
|
||||
/* With the option disabled every original BG helper remains centered. */
|
||||
gpu_ws_set_view_anchor(0, 0, 0, 0);
|
||||
assert(psx_ws_bg2d_cols(21) == 29);
|
||||
assert(psx_ws_bg2d_startx(0) == -64);
|
||||
assert(psx_ws_bg2d_stream_right(336) == 400);
|
||||
ws_mode = 0;
|
||||
assert(psx_ws_bg2d_cols(21) == 21);
|
||||
assert(psx_ws_bg2d_startx(0) == 0);
|
||||
/* The opt-in host arena keeps guest loops and the ring native, while
|
||||
* its packet coordinates remain signed 16-bit beyond PS1's +/-1024. */
|
||||
configure_native_wide_16_9();
|
||||
gpu_ws_bg2d_set_host_arena(0x80100000u, 64u); /* mapped fixture memory */
|
||||
half_at(0x10001c, 0x4247u); half_at(0x10001e, 0x5836u);
|
||||
gp0_cmd_source_addr = 0x00100004u;
|
||||
int32_t px, py;
|
||||
parse_vertex(pack_vertex(1400, 20), &px, &py);
|
||||
assert(px == 1400 && py == 20);
|
||||
/* Reflection reverses the exact 16 texels, without reading the adjacent
|
||||
* tile's first column. Its metadata is confined to the registered arena. */
|
||||
half_at(0x10001e, 0xd836u);
|
||||
gp0_cmd_buf[0]=0x7d808080u;
|
||||
gp0_cmd_buf[1]=pack_vertex(400,20);
|
||||
gp0_cmd_buf[2]=0x79804000u;
|
||||
gp0_exec_textured_16x16();
|
||||
assert(last_scaled_rect.calls==1 && last_scaled_rect.x==400);
|
||||
assert(last_scaled_rect.w==16 && last_scaled_rect.h==16);
|
||||
assert(last_scaled_rect.u0==15 && last_scaled_rect.u1==-1);
|
||||
ws_mode = 0;
|
||||
gp0_cmd_buf[1]=pack_vertex(20,20);
|
||||
int before_resize_calls=last_scaled_rect.calls;
|
||||
gp0_exec_textured_16x16();
|
||||
assert(last_scaled_rect.calls==before_resize_calls+1 && last_scaled_rect.x==20);
|
||||
configure_native_wide_16_9();
|
||||
half_at(0x10001e, 0x5836u);
|
||||
/* New metadata packs a bank ID beside a signed view shift; old saved
|
||||
* packet metadata remains supported, with no accidental sign extension. */
|
||||
half_at(0x10001c, 0x4248u);
|
||||
half_at(0x100010, (uint16_t)-53); half_at(0x100012, 0x6001u);
|
||||
assert(ws_view_packet().shift == -53);
|
||||
half_at(0x10001c, 0x4247u); half_at(0x100012, 0xffffu);
|
||||
assert(ws_view_packet().shift == -53);
|
||||
gp0_cmd_source_addr = 0x00010004u;
|
||||
parse_vertex(pack_vertex(1400, 20), &px, &py);
|
||||
assert(px == -648 && py == 20); /* Ordinary hardware packet unchanged. */
|
||||
gpu_ws_tag_world_primitive(0x80010000u, 1);
|
||||
parse_vertex(pack_vertex(1400, 20), &px, &py);
|
||||
assert(px == 1400 && py == 20);
|
||||
parse_vertex(pack_vertex(-1400, 20), &px, &py);
|
||||
assert(px == -1400 && py == 20);
|
||||
gpu_ws_tag_world_primitive(0x80010000u, 0);
|
||||
parse_vertex(pack_vertex(1400, 20), &px, &py);
|
||||
assert(px == -648 && py == 20);
|
||||
assert(psx_ws_bg2d_cols(21) == 21);
|
||||
assert(psx_ws_bg2d_startcol(3, 63) == 3);
|
||||
assert(psx_ws_bg2d_startx(-5) == -5);
|
||||
assert(psx_ws_bg2d_stream_left(100) == 100);
|
||||
assert(psx_ws_bg2d_stream_right(436) == 436);
|
||||
gpu_ws_bg2d_set_host_arena(0, 0);
|
||||
puts("ws_view_anchor_gpu: sampled camera, per-layer coverage, packet origin, disabled identity PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -5,11 +5,12 @@
|
||||
|
||||
uint64_t s_frame_count;
|
||||
static uint32_t state_word;
|
||||
static int world, calls, fmv, hold;
|
||||
static int world, calls, fmv, hold, native_effect;
|
||||
uint32_t psx_read_word(uint32_t address) { (void)address; return state_word; }
|
||||
int mdec_recently_active(uint32_t frames) { (void)frames; return fmv; }
|
||||
static int world_scene(void) { ++calls; return world; }
|
||||
static int retained_scene(void) { return hold; }
|
||||
static int native_scene(void) { return native_effect; }
|
||||
static int native_43(void) { ++s_frame_count; return gpu_ws_present_native_43(); }
|
||||
|
||||
int main(void) {
|
||||
@@ -38,6 +39,12 @@ int main(void) {
|
||||
assert(native_43() == 1); /* 24-bit FMV veto */
|
||||
display_depth = 0;
|
||||
assert(native_43() == 0);
|
||||
gpu_ws_set_native_scene_predicate(native_scene);
|
||||
native_effect=1; assert(native_43()==1); /* Native attack overrides world. */
|
||||
native_effect=0; assert(native_43()==0); /* Recovery restores adaptive. */
|
||||
native_effect=1; ws_mode=0; assert(native_43()==0); /* Off stays inert. */
|
||||
ws_mode=2; gpu_ws_set_native_scene_predicate(NULL);
|
||||
assert(native_43()==0); /* No policy left behind after unregister. */
|
||||
psx_mod_set_retained_scene_predicate(retained_scene);
|
||||
hold = 0;
|
||||
assert(native_43() == 0);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,867 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Launch a benchmark child under a process-scoped host CPU budget.
|
||||
|
||||
Windows uses a Job Object hard CPU-rate cap. The cap is applied before the child
|
||||
thread is resumed, so the target does not do meaningful work outside the budget.
|
||||
Other platforms may run unrestricted, but CPU throttling fails explicitly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ctypes
|
||||
import ctypes.wintypes
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Sequence
|
||||
|
||||
|
||||
class PerfHostError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedThrottle(PerfHostError):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class LaunchResult:
|
||||
pid: int
|
||||
exit_code: int | None
|
||||
timed_out: bool
|
||||
wall_seconds: float
|
||||
cpu_user_seconds: float | None
|
||||
cpu_kernel_seconds: float | None
|
||||
cpu_total_seconds: float | None
|
||||
|
||||
|
||||
def parse_affinity(text: str | None) -> int | None:
|
||||
if text is None or text == "":
|
||||
return None
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return None
|
||||
if "," in text:
|
||||
mask = 0
|
||||
for part in text.split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
raise ValueError("empty CPU index in affinity list")
|
||||
idx = int(part, 10)
|
||||
if idx < 0:
|
||||
raise ValueError("negative CPU index in affinity list")
|
||||
mask |= 1 << idx
|
||||
if mask == 0:
|
||||
raise ValueError("affinity mask may not be zero")
|
||||
return mask
|
||||
mask = int(text, 0)
|
||||
if mask <= 0:
|
||||
raise ValueError("affinity mask may not be zero")
|
||||
return mask
|
||||
|
||||
|
||||
def validate_cpu_percent(value: int | None) -> int | None:
|
||||
if value is None:
|
||||
return None
|
||||
if value < 1 or value > 100:
|
||||
raise ValueError("--cpu-percent must be in 1..100")
|
||||
return value
|
||||
|
||||
|
||||
def validate_core_percent(value: float | None) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not math.isfinite(value) or value <= 0.0 or value > 100.0:
|
||||
raise ValueError("--core-percent must be finite and in (0, 100]")
|
||||
return value
|
||||
|
||||
|
||||
def topology_report() -> dict[str, Any]:
|
||||
affinity = None
|
||||
if hasattr(os, "sched_getaffinity"):
|
||||
try:
|
||||
affinity = sorted(os.sched_getaffinity(0)) # type: ignore[attr-defined]
|
||||
except OSError:
|
||||
affinity = None
|
||||
return {
|
||||
"system": platform.system(),
|
||||
"release": platform.release(),
|
||||
"version": platform.version(),
|
||||
"machine": platform.machine(),
|
||||
"logical_cpu_count": os.cpu_count(),
|
||||
"process_affinity": affinity,
|
||||
}
|
||||
|
||||
|
||||
def _filetime_seconds(ft: Any) -> float:
|
||||
ticks = (int(ft.dwHighDateTime) << 32) | int(ft.dwLowDateTime)
|
||||
return ticks / 10_000_000.0
|
||||
|
||||
|
||||
if os.name == "nt":
|
||||
wintypes = ctypes.wintypes
|
||||
|
||||
CREATE_SUSPENDED = 0x00000004
|
||||
CREATE_NO_WINDOW = 0x08000000
|
||||
STARTF_USESHOWWINDOW = 0x00000001
|
||||
STARTF_USESTDHANDLES = 0x00000100
|
||||
SW_HIDE = 0
|
||||
INFINITE = 0xFFFFFFFF
|
||||
TERMINATE_WAIT_MS = 5000
|
||||
WAIT_OBJECT_0 = 0
|
||||
WAIT_TIMEOUT = 0x00000102
|
||||
JobObjectExtendedLimitInformation = 9
|
||||
JobObjectCpuRateControlInformation = 15
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
||||
JOB_OBJECT_CPU_RATE_CONTROL_ENABLE = 0x1
|
||||
JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP = 0x4
|
||||
|
||||
class STARTUPINFOW(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("cb", wintypes.DWORD),
|
||||
("lpReserved", wintypes.LPWSTR),
|
||||
("lpDesktop", wintypes.LPWSTR),
|
||||
("lpTitle", wintypes.LPWSTR),
|
||||
("dwX", wintypes.DWORD),
|
||||
("dwY", wintypes.DWORD),
|
||||
("dwXSize", wintypes.DWORD),
|
||||
("dwYSize", wintypes.DWORD),
|
||||
("dwXCountChars", wintypes.DWORD),
|
||||
("dwYCountChars", wintypes.DWORD),
|
||||
("dwFillAttribute", wintypes.DWORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("wShowWindow", wintypes.WORD),
|
||||
("cbReserved2", wintypes.WORD),
|
||||
("lpReserved2", ctypes.POINTER(ctypes.c_byte)),
|
||||
("hStdInput", wintypes.HANDLE),
|
||||
("hStdOutput", wintypes.HANDLE),
|
||||
("hStdError", wintypes.HANDLE),
|
||||
]
|
||||
|
||||
class PROCESS_INFORMATION(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("hProcess", wintypes.HANDLE),
|
||||
("hThread", wintypes.HANDLE),
|
||||
("dwProcessId", wintypes.DWORD),
|
||||
("dwThreadId", wintypes.DWORD),
|
||||
]
|
||||
|
||||
class JOBOBJECT_BASIC_LIMIT_INFORMATION(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("PerProcessUserTimeLimit", ctypes.c_longlong),
|
||||
("PerJobUserTimeLimit", ctypes.c_longlong),
|
||||
("LimitFlags", wintypes.DWORD),
|
||||
("MinimumWorkingSetSize", ctypes.c_size_t),
|
||||
("MaximumWorkingSetSize", ctypes.c_size_t),
|
||||
("ActiveProcessLimit", wintypes.DWORD),
|
||||
("Affinity", ctypes.c_size_t),
|
||||
("PriorityClass", wintypes.DWORD),
|
||||
("SchedulingClass", wintypes.DWORD),
|
||||
]
|
||||
|
||||
class IO_COUNTERS(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("ReadOperationCount", ctypes.c_ulonglong),
|
||||
("WriteOperationCount", ctypes.c_ulonglong),
|
||||
("OtherOperationCount", ctypes.c_ulonglong),
|
||||
("ReadTransferCount", ctypes.c_ulonglong),
|
||||
("WriteTransferCount", ctypes.c_ulonglong),
|
||||
("OtherTransferCount", ctypes.c_ulonglong),
|
||||
]
|
||||
|
||||
class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("BasicLimitInformation", JOBOBJECT_BASIC_LIMIT_INFORMATION),
|
||||
("IoInfo", IO_COUNTERS),
|
||||
("ProcessMemoryLimit", ctypes.c_size_t),
|
||||
("JobMemoryLimit", ctypes.c_size_t),
|
||||
("PeakProcessMemoryUsed", ctypes.c_size_t),
|
||||
("PeakJobMemoryUsed", ctypes.c_size_t),
|
||||
]
|
||||
|
||||
class JOBOBJECT_CPU_RATE_CONTROL_INFORMATION(ctypes.Structure):
|
||||
_fields_ = [("ControlFlags", wintypes.DWORD), ("CpuRate", wintypes.DWORD)]
|
||||
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
kernel32.CreateProcessW.argtypes = [
|
||||
wintypes.LPCWSTR,
|
||||
wintypes.LPWSTR,
|
||||
wintypes.LPVOID,
|
||||
wintypes.LPVOID,
|
||||
wintypes.BOOL,
|
||||
wintypes.DWORD,
|
||||
wintypes.LPVOID,
|
||||
wintypes.LPCWSTR,
|
||||
ctypes.POINTER(STARTUPINFOW),
|
||||
ctypes.POINTER(PROCESS_INFORMATION),
|
||||
]
|
||||
kernel32.CreateProcessW.restype = wintypes.BOOL
|
||||
kernel32.CreateJobObjectW.argtypes = [wintypes.LPVOID, wintypes.LPCWSTR]
|
||||
kernel32.CreateJobObjectW.restype = wintypes.HANDLE
|
||||
kernel32.SetInformationJobObject.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
ctypes.c_int,
|
||||
wintypes.LPVOID,
|
||||
wintypes.DWORD,
|
||||
]
|
||||
kernel32.SetInformationJobObject.restype = wintypes.BOOL
|
||||
kernel32.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]
|
||||
kernel32.AssignProcessToJobObject.restype = wintypes.BOOL
|
||||
kernel32.SetProcessAffinityMask.argtypes = [wintypes.HANDLE, ctypes.c_size_t]
|
||||
kernel32.SetProcessAffinityMask.restype = wintypes.BOOL
|
||||
kernel32.GetProcessAffinityMask.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
ctypes.POINTER(ctypes.c_size_t),
|
||||
ctypes.POINTER(ctypes.c_size_t),
|
||||
]
|
||||
kernel32.GetProcessAffinityMask.restype = wintypes.BOOL
|
||||
kernel32.GetCurrentProcess.argtypes = []
|
||||
kernel32.GetCurrentProcess.restype = wintypes.HANDLE
|
||||
kernel32.IsProcessInJob.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
wintypes.HANDLE,
|
||||
ctypes.POINTER(wintypes.BOOL),
|
||||
]
|
||||
kernel32.IsProcessInJob.restype = wintypes.BOOL
|
||||
kernel32.QueryInformationJobObject.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
ctypes.c_int,
|
||||
wintypes.LPVOID,
|
||||
wintypes.DWORD,
|
||||
ctypes.POINTER(wintypes.DWORD),
|
||||
]
|
||||
kernel32.QueryInformationJobObject.restype = wintypes.BOOL
|
||||
kernel32.ResumeThread.argtypes = [wintypes.HANDLE]
|
||||
kernel32.ResumeThread.restype = wintypes.DWORD
|
||||
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
||||
kernel32.WaitForSingleObject.restype = wintypes.DWORD
|
||||
kernel32.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
|
||||
kernel32.GetExitCodeProcess.restype = wintypes.BOOL
|
||||
kernel32.GetProcessTimes.argtypes = [
|
||||
wintypes.HANDLE,
|
||||
ctypes.POINTER(wintypes.FILETIME),
|
||||
ctypes.POINTER(wintypes.FILETIME),
|
||||
ctypes.POINTER(wintypes.FILETIME),
|
||||
ctypes.POINTER(wintypes.FILETIME),
|
||||
]
|
||||
kernel32.GetProcessTimes.restype = wintypes.BOOL
|
||||
kernel32.TerminateJobObject.argtypes = [wintypes.HANDLE, wintypes.UINT]
|
||||
kernel32.TerminateJobObject.restype = wintypes.BOOL
|
||||
kernel32.TerminateProcess.argtypes = [wintypes.HANDLE, wintypes.UINT]
|
||||
kernel32.TerminateProcess.restype = wintypes.BOOL
|
||||
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
||||
kernel32.CloseHandle.restype = wintypes.BOOL
|
||||
|
||||
|
||||
def _winerr(prefix: str) -> PerfHostError:
|
||||
return PerfHostError(f"{prefix}: WinError {ctypes.get_last_error()}")
|
||||
|
||||
|
||||
def validate_timeout(value: float | None) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
if not math.isfinite(value) or value < 0.0:
|
||||
raise ValueError("--timeout must be a finite non-negative number")
|
||||
if value * 1000.0 > 0xFFFFFFFE:
|
||||
raise ValueError("--timeout is too large for the Windows wait API")
|
||||
return value
|
||||
|
||||
|
||||
def affinity_scaled_cpu_rate_units(
|
||||
cpu_percent: float | None,
|
||||
affinity_mask: int | None,
|
||||
logical_cpu_count: int | None,
|
||||
) -> int | None:
|
||||
if cpu_percent is None or not logical_cpu_count:
|
||||
return None
|
||||
affinity_cpus = affinity_mask.bit_count() if affinity_mask is not None else logical_cpu_count
|
||||
if affinity_cpus <= 0:
|
||||
return None
|
||||
units = math.ceil(cpu_percent * affinity_cpus * 100 / logical_cpu_count)
|
||||
return min(10000, max(1, units))
|
||||
|
||||
|
||||
def one_core_cpu_rate_units(core_percent: float | None, logical_cpu_count: int | None) -> int | None:
|
||||
if core_percent is None or not logical_cpu_count:
|
||||
return None
|
||||
units = math.ceil(core_percent * 100 / logical_cpu_count)
|
||||
return min(10000, max(1, units))
|
||||
|
||||
|
||||
def host_cpu_rate_units(cpu_percent: int | None) -> int | None:
|
||||
if cpu_percent is None:
|
||||
return None
|
||||
return min(10000, max(1, cpu_percent * 100))
|
||||
|
||||
|
||||
def _path_text(path: Path | None) -> str | None:
|
||||
return str(path) if path is not None else None
|
||||
|
||||
|
||||
def _set_job_kill_on_close(job: int) -> None:
|
||||
info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
|
||||
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
||||
ok = kernel32.SetInformationJobObject(
|
||||
job,
|
||||
JobObjectExtendedLimitInformation,
|
||||
ctypes.byref(info),
|
||||
ctypes.sizeof(info),
|
||||
)
|
||||
if not ok:
|
||||
raise _winerr("SetInformationJobObject(KILL_ON_JOB_CLOSE) failed")
|
||||
|
||||
|
||||
def _set_job_cpu_rate(job: int, cpu_rate: int) -> int:
|
||||
info = JOBOBJECT_CPU_RATE_CONTROL_INFORMATION()
|
||||
info.ControlFlags = JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP
|
||||
info.CpuRate = cpu_rate
|
||||
ok = kernel32.SetInformationJobObject(
|
||||
job,
|
||||
JobObjectCpuRateControlInformation,
|
||||
ctypes.byref(info),
|
||||
ctypes.sizeof(info),
|
||||
)
|
||||
if not ok:
|
||||
raise _winerr("SetInformationJobObject(CPU_RATE_CONTROL) failed")
|
||||
return cpu_rate
|
||||
|
||||
|
||||
def _resolve_application(argv: Sequence[str]) -> str | None:
|
||||
if not argv:
|
||||
raise ValueError("missing executable after --")
|
||||
exe = argv[0]
|
||||
if any(ch in exe for ch in "\\/:"):
|
||||
return exe
|
||||
found = shutil.which(exe)
|
||||
return found
|
||||
|
||||
|
||||
def _query_windows_affinity(process: int) -> dict[str, int] | None:
|
||||
if not hasattr(kernel32, "GetProcessAffinityMask"):
|
||||
return None
|
||||
proc_mask = ctypes.c_size_t()
|
||||
sys_mask = ctypes.c_size_t()
|
||||
ok = kernel32.GetProcessAffinityMask(
|
||||
process, ctypes.byref(proc_mask), ctypes.byref(sys_mask)
|
||||
)
|
||||
if not ok:
|
||||
return None
|
||||
return {"process_mask": int(proc_mask.value), "system_mask": int(sys_mask.value)}
|
||||
|
||||
|
||||
def _validate_windows_affinity_mask(affinity_mask: int | None) -> None:
|
||||
if affinity_mask is None:
|
||||
return
|
||||
size_t_bits = ctypes.sizeof(ctypes.c_size_t) * 8
|
||||
if affinity_mask.bit_length() > size_t_bits:
|
||||
raise ValueError(
|
||||
f"affinity mask 0x{affinity_mask:x} exceeds {size_t_bits}-bit size_t"
|
||||
)
|
||||
current_process = kernel32.GetCurrentProcess() if hasattr(kernel32, "GetCurrentProcess") else None
|
||||
current_affinity = (
|
||||
_query_windows_affinity(current_process) if current_process is not None else None
|
||||
)
|
||||
system_mask = current_affinity["system_mask"] if current_affinity else None
|
||||
if system_mask is not None and affinity_mask & ~system_mask:
|
||||
raise ValueError(
|
||||
f"affinity mask 0x{affinity_mask:x} is outside system mask 0x{system_mask:x}"
|
||||
)
|
||||
|
||||
|
||||
def _is_process_in_job(process: int, job: int | None = None) -> bool | None:
|
||||
if not hasattr(kernel32, "IsProcessInJob"):
|
||||
return None
|
||||
result = wintypes.BOOL()
|
||||
ok = kernel32.IsProcessInJob(process, job, ctypes.byref(result))
|
||||
if not ok:
|
||||
return None
|
||||
return bool(result.value)
|
||||
|
||||
|
||||
def _query_job_cpu_rate(job: int) -> dict[str, int] | None:
|
||||
if not hasattr(kernel32, "QueryInformationJobObject"):
|
||||
return None
|
||||
info = JOBOBJECT_CPU_RATE_CONTROL_INFORMATION()
|
||||
returned = wintypes.DWORD()
|
||||
ok = kernel32.QueryInformationJobObject(
|
||||
job,
|
||||
JobObjectCpuRateControlInformation,
|
||||
ctypes.byref(info),
|
||||
ctypes.sizeof(info),
|
||||
ctypes.byref(returned),
|
||||
)
|
||||
if not ok:
|
||||
return None
|
||||
return {"control_flags": int(info.ControlFlags), "cpu_rate_units": int(info.CpuRate)}
|
||||
|
||||
|
||||
def _open_windows_stdio(
|
||||
stdout_log: Path | None,
|
||||
stderr_log: Path | None,
|
||||
) -> tuple[list[Any], dict[str, Any]]:
|
||||
files: list[Any] = []
|
||||
info = {
|
||||
"stdout_log": _path_text(stdout_log),
|
||||
"stderr_log": _path_text(stderr_log),
|
||||
"stdio_mode": "log-files" if stdout_log or stderr_log else "detached",
|
||||
}
|
||||
if not stdout_log and not stderr_log:
|
||||
return files, info
|
||||
|
||||
import msvcrt
|
||||
|
||||
stdin = open(os.devnull, "rb")
|
||||
files.append(stdin)
|
||||
os.set_handle_inheritable(msvcrt.get_osfhandle(stdin.fileno()), True)
|
||||
|
||||
if stdout_log:
|
||||
stdout_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
stdout = open(stdout_log, "wb")
|
||||
files.append(stdout)
|
||||
else:
|
||||
stdout = open(os.devnull, "wb")
|
||||
files.append(stdout)
|
||||
os.set_handle_inheritable(msvcrt.get_osfhandle(stdout.fileno()), True)
|
||||
|
||||
if stderr_log:
|
||||
stderr_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
mode = "ab" if stdout_log and stderr_log == stdout_log else "wb"
|
||||
stderr = open(stderr_log, mode)
|
||||
files.append(stderr)
|
||||
else:
|
||||
stderr = open(os.devnull, "wb")
|
||||
files.append(stderr)
|
||||
os.set_handle_inheritable(msvcrt.get_osfhandle(stderr.fileno()), True)
|
||||
|
||||
info["stdin_handle"] = "NUL"
|
||||
return files, info
|
||||
|
||||
|
||||
def launch_windows(
|
||||
argv: Sequence[str],
|
||||
cpu_percent: int | None,
|
||||
affinity_mask: int | None,
|
||||
timeout: float | None,
|
||||
cwd: str | None = None,
|
||||
stdout_log: Path | None = None,
|
||||
stderr_log: Path | None = None,
|
||||
cpu_rate_units: int | None = None,
|
||||
cpu_rate_mode: str = "host_percent",
|
||||
core_percent: float | None = None,
|
||||
) -> tuple[LaunchResult, dict[str, Any]]:
|
||||
if not argv:
|
||||
raise ValueError("missing executable after --")
|
||||
_validate_windows_affinity_mask(affinity_mask)
|
||||
|
||||
si = STARTUPINFOW()
|
||||
si.cb = ctypes.sizeof(si)
|
||||
si.dwFlags = STARTF_USESHOWWINDOW
|
||||
si.wShowWindow = SW_HIDE
|
||||
stdio_files, stdio_info = _open_windows_stdio(stdout_log, stderr_log)
|
||||
if stdio_files:
|
||||
import msvcrt
|
||||
|
||||
si.dwFlags |= STARTF_USESTDHANDLES
|
||||
si.hStdInput = msvcrt.get_osfhandle(stdio_files[0].fileno())
|
||||
si.hStdOutput = msvcrt.get_osfhandle(stdio_files[1].fileno())
|
||||
si.hStdError = msvcrt.get_osfhandle(stdio_files[2].fileno())
|
||||
pi = PROCESS_INFORMATION()
|
||||
cmdline = ctypes.create_unicode_buffer(subprocess.list2cmdline(list(argv)))
|
||||
app = _resolve_application(argv)
|
||||
creation_flags = CREATE_SUSPENDED | CREATE_NO_WINDOW
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
ok = kernel32.CreateProcessW(
|
||||
app,
|
||||
cmdline,
|
||||
None,
|
||||
None,
|
||||
bool(stdio_files),
|
||||
creation_flags,
|
||||
None,
|
||||
cwd,
|
||||
ctypes.byref(si),
|
||||
ctypes.byref(pi),
|
||||
)
|
||||
if not ok:
|
||||
raise _winerr("CreateProcessW failed")
|
||||
except BaseException:
|
||||
for f in stdio_files:
|
||||
f.close()
|
||||
raise
|
||||
|
||||
job = None
|
||||
logical_cpu_count = os.cpu_count()
|
||||
if cpu_rate_units is None:
|
||||
cpu_rate_units = host_cpu_rate_units(cpu_percent)
|
||||
current_process = kernel32.GetCurrentProcess() if hasattr(kernel32, "GetCurrentProcess") else None
|
||||
parent_in_job = (
|
||||
_is_process_in_job(current_process, None) if current_process is not None else None
|
||||
)
|
||||
child_in_job_before_assign = _is_process_in_job(pi.hProcess, None)
|
||||
effective: dict[str, Any] = {
|
||||
"platform": "windows",
|
||||
"throttle_supported": True,
|
||||
"cpu_rate_mode": cpu_rate_mode,
|
||||
"core_percent": core_percent,
|
||||
"job_cpu_rate_units": None,
|
||||
"job_cpu_rate_denominator": 10000,
|
||||
"job_cpu_rate_readback": None,
|
||||
"job_denominator_note": (
|
||||
"Windows Job Object CpuRate units; 10000 is 100 percent of the "
|
||||
"OS job CPU-rate denominator, not a single-thread slowdown model. "
|
||||
"Record affinity separately and calibrate with a one-worker "
|
||||
"CPU-bound witness on the target host."
|
||||
),
|
||||
"parent_process_in_job": parent_in_job,
|
||||
"child_process_in_job_before_assign": child_in_job_before_assign,
|
||||
"nested_job_note": (
|
||||
"If a parent job already has CPU rate control, Windows applies "
|
||||
"nested job rates relative to that parent. This report records "
|
||||
"whether the host and child were already in a job before assignment."
|
||||
),
|
||||
"affinity_cpu_count": affinity_mask.bit_count() if affinity_mask is not None else None,
|
||||
"affinity_scaled_job_cpu_rate_units": affinity_scaled_cpu_rate_units(
|
||||
cpu_percent, affinity_mask, logical_cpu_count
|
||||
),
|
||||
"core_percent_job_cpu_rate_units": one_core_cpu_rate_units(
|
||||
core_percent, logical_cpu_count
|
||||
),
|
||||
"affinity_scaled_note": (
|
||||
"Informational only: raw Job Object CpuRate units that would make "
|
||||
"--cpu-percent apply to the requested affinity set as a fraction "
|
||||
"of this host's logical CPU count. --core-percent instead applies "
|
||||
"a one-logical-CPU-equivalent conversion, independent of affinity."
|
||||
),
|
||||
"affinity_mask": affinity_mask,
|
||||
"affinity_applied": False,
|
||||
"affinity_actual": None,
|
||||
**stdio_info,
|
||||
}
|
||||
timed_out = False
|
||||
exit_code: int | None = None
|
||||
user_s = kernel_s = total_s = None
|
||||
try:
|
||||
job = kernel32.CreateJobObjectW(None, None)
|
||||
if not job:
|
||||
raise _winerr("CreateJobObjectW failed")
|
||||
_set_job_kill_on_close(job)
|
||||
if cpu_rate_units is not None:
|
||||
effective["job_cpu_rate_units"] = _set_job_cpu_rate(job, cpu_rate_units)
|
||||
effective["job_cpu_rate_readback"] = _query_job_cpu_rate(job)
|
||||
if affinity_mask is not None:
|
||||
if not kernel32.SetProcessAffinityMask(pi.hProcess, affinity_mask):
|
||||
raise _winerr("SetProcessAffinityMask failed")
|
||||
effective["affinity_applied"] = True
|
||||
effective["affinity_actual"] = _query_windows_affinity(pi.hProcess)
|
||||
if not kernel32.AssignProcessToJobObject(job, pi.hProcess):
|
||||
raise _winerr("AssignProcessToJobObject failed")
|
||||
if kernel32.ResumeThread(pi.hThread) == 0xFFFFFFFF:
|
||||
raise _winerr("ResumeThread failed")
|
||||
|
||||
wait_ms = INFINITE if timeout is None else max(0, int(timeout * 1000))
|
||||
wait = kernel32.WaitForSingleObject(pi.hProcess, wait_ms)
|
||||
if wait == WAIT_TIMEOUT:
|
||||
timed_out = True
|
||||
kernel32.TerminateJobObject(job, 1)
|
||||
done = kernel32.WaitForSingleObject(pi.hProcess, TERMINATE_WAIT_MS)
|
||||
if done == WAIT_TIMEOUT:
|
||||
raise PerfHostError("child did not exit after timeout termination")
|
||||
elif wait != WAIT_OBJECT_0:
|
||||
raise _winerr("WaitForSingleObject failed")
|
||||
|
||||
code = wintypes.DWORD()
|
||||
if not kernel32.GetExitCodeProcess(pi.hProcess, ctypes.byref(code)):
|
||||
raise _winerr("GetExitCodeProcess failed")
|
||||
exit_code = int(code.value)
|
||||
|
||||
creation = wintypes.FILETIME()
|
||||
exit_ft = wintypes.FILETIME()
|
||||
kernel_ft = wintypes.FILETIME()
|
||||
user_ft = wintypes.FILETIME()
|
||||
if kernel32.GetProcessTimes(
|
||||
pi.hProcess,
|
||||
ctypes.byref(creation),
|
||||
ctypes.byref(exit_ft),
|
||||
ctypes.byref(kernel_ft),
|
||||
ctypes.byref(user_ft),
|
||||
):
|
||||
user_s = _filetime_seconds(user_ft)
|
||||
kernel_s = _filetime_seconds(kernel_ft)
|
||||
total_s = user_s + kernel_s
|
||||
except BaseException:
|
||||
if job:
|
||||
kernel32.TerminateJobObject(job, 1)
|
||||
if pi.hProcess:
|
||||
kernel32.TerminateProcess(pi.hProcess, 1)
|
||||
kernel32.WaitForSingleObject(pi.hProcess, TERMINATE_WAIT_MS)
|
||||
raise
|
||||
finally:
|
||||
if pi.hThread:
|
||||
kernel32.CloseHandle(pi.hThread)
|
||||
if pi.hProcess:
|
||||
kernel32.CloseHandle(pi.hProcess)
|
||||
if job:
|
||||
kernel32.CloseHandle(job)
|
||||
for f in stdio_files:
|
||||
f.close()
|
||||
|
||||
result = LaunchResult(
|
||||
pid=int(pi.dwProcessId),
|
||||
exit_code=exit_code,
|
||||
timed_out=timed_out,
|
||||
wall_seconds=time.perf_counter() - start,
|
||||
cpu_user_seconds=user_s,
|
||||
cpu_kernel_seconds=kernel_s,
|
||||
cpu_total_seconds=total_s,
|
||||
)
|
||||
return result, effective
|
||||
|
||||
|
||||
def launch_portable(
|
||||
argv: Sequence[str],
|
||||
cpu_percent: int | None,
|
||||
affinity_mask: int | None,
|
||||
timeout: float | None,
|
||||
cwd: str | None = None,
|
||||
stdout_log: Path | None = None,
|
||||
stderr_log: Path | None = None,
|
||||
cpu_rate_units: int | None = None,
|
||||
cpu_rate_mode: str = "host_percent",
|
||||
core_percent: float | None = None,
|
||||
) -> tuple[LaunchResult, dict[str, Any]]:
|
||||
if cpu_percent is not None or cpu_rate_units is not None or core_percent is not None:
|
||||
raise UnsupportedThrottle("CPU throttling is only supported on Windows via Job Objects")
|
||||
if affinity_mask is not None and not hasattr(os, "sched_setaffinity"):
|
||||
raise UnsupportedThrottle("affinity is unsupported on this platform")
|
||||
|
||||
def set_affinity() -> None:
|
||||
if affinity_mask is not None and hasattr(os, "sched_setaffinity"):
|
||||
cpus = {i for i in range(affinity_mask.bit_length()) if affinity_mask & (1 << i)}
|
||||
os.sched_setaffinity(0, cpus) # type: ignore[attr-defined]
|
||||
|
||||
start = time.perf_counter()
|
||||
stdout_f = None
|
||||
stderr_f = None
|
||||
stdout_target: Any = None
|
||||
stderr_target: Any = None
|
||||
if stdout_log:
|
||||
stdout_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
stdout_f = open(stdout_log, "wb")
|
||||
stdout_target = stdout_f
|
||||
if stderr_log:
|
||||
stderr_log.parent.mkdir(parents=True, exist_ok=True)
|
||||
stderr_f = open(stderr_log, "ab" if stdout_log and stderr_log == stdout_log else "wb")
|
||||
stderr_target = stderr_f
|
||||
proc = subprocess.Popen(
|
||||
list(argv),
|
||||
cwd=cwd,
|
||||
preexec_fn=set_affinity if affinity_mask is not None and os.name == "posix" else None,
|
||||
start_new_session=os.name == "posix",
|
||||
stdout=stdout_target,
|
||||
stderr=stderr_target,
|
||||
)
|
||||
timed_out = False
|
||||
try:
|
||||
proc.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
if os.name == "posix":
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
else:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
except BaseException:
|
||||
if proc.poll() is None:
|
||||
if os.name == "posix":
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
else:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
raise
|
||||
finally:
|
||||
if stdout_f:
|
||||
stdout_f.close()
|
||||
if stderr_f:
|
||||
stderr_f.close()
|
||||
result = LaunchResult(
|
||||
pid=proc.pid,
|
||||
exit_code=proc.returncode,
|
||||
timed_out=timed_out,
|
||||
wall_seconds=time.perf_counter() - start,
|
||||
cpu_user_seconds=None,
|
||||
cpu_kernel_seconds=None,
|
||||
cpu_total_seconds=None,
|
||||
)
|
||||
return result, {
|
||||
"platform": "portable",
|
||||
"throttle_supported": False,
|
||||
"cpu_rate_mode": cpu_rate_mode,
|
||||
"core_percent": core_percent,
|
||||
"job_cpu_rate_units": None,
|
||||
"job_cpu_rate_denominator": None,
|
||||
"affinity_mask": affinity_mask,
|
||||
"affinity_applied": affinity_mask is not None and hasattr(os, "sched_setaffinity"),
|
||||
"stdout_log": _path_text(stdout_log),
|
||||
"stderr_log": _path_text(stderr_log),
|
||||
"stdio_mode": "log-files" if stdout_log or stderr_log else "inherit",
|
||||
}
|
||||
|
||||
|
||||
def run_host(
|
||||
argv: Sequence[str],
|
||||
cpu_percent: int | None,
|
||||
affinity_mask: int | None,
|
||||
timeout: float | None,
|
||||
cwd: str | None = None,
|
||||
stdout_log: Path | None = None,
|
||||
stderr_log: Path | None = None,
|
||||
core_percent: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
cpu_percent = validate_cpu_percent(cpu_percent)
|
||||
core_percent = validate_core_percent(core_percent)
|
||||
if cpu_percent is not None and core_percent is not None:
|
||||
raise ValueError("--cpu-percent and --core-percent are mutually exclusive")
|
||||
timeout = validate_timeout(timeout)
|
||||
cpu_rate_mode = "host_percent"
|
||||
cpu_rate_units = host_cpu_rate_units(cpu_percent)
|
||||
if core_percent is not None:
|
||||
cpu_rate_mode = "one_core_percent"
|
||||
cpu_rate_units = one_core_cpu_rate_units(core_percent, os.cpu_count())
|
||||
launcher = launch_windows if os.name == "nt" else launch_portable
|
||||
result, effective = launcher(
|
||||
argv,
|
||||
cpu_percent,
|
||||
affinity_mask,
|
||||
timeout,
|
||||
cwd,
|
||||
stdout_log,
|
||||
stderr_log,
|
||||
cpu_rate_units,
|
||||
cpu_rate_mode,
|
||||
core_percent,
|
||||
)
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"host": topology_report(),
|
||||
"request": {
|
||||
"cpu_percent": cpu_percent,
|
||||
"core_percent": core_percent,
|
||||
"affinity": affinity_mask,
|
||||
"timeout_seconds": timeout,
|
||||
"argv": list(argv),
|
||||
"cwd": cwd,
|
||||
"stdout_log": _path_text(stdout_log),
|
||||
"stderr_log": _path_text(stderr_log),
|
||||
},
|
||||
"effective": effective,
|
||||
"process": {
|
||||
"pid": result.pid,
|
||||
"exit_code": result.exit_code,
|
||||
"timed_out": result.timed_out,
|
||||
"wall_seconds": result.wall_seconds,
|
||||
"cpu_user_seconds": result.cpu_user_seconds,
|
||||
"cpu_kernel_seconds": result.cpu_kernel_seconds,
|
||||
"cpu_total_seconds": result.cpu_total_seconds,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Run a command under a process-scoped host CPU budget."
|
||||
)
|
||||
parser.add_argument("--cpu-percent", type=int, default=None,
|
||||
help="Windows Job Object hard CPU cap, 1..100 percent.")
|
||||
parser.add_argument("--core-percent", type=float, default=None,
|
||||
help=(
|
||||
"Windows cap as percent of one logical CPU; "
|
||||
"converts to raw Job Object units using host "
|
||||
"logical CPU count, independent of affinity."
|
||||
))
|
||||
parser.add_argument("--affinity", default=None,
|
||||
help="Affinity mask, e.g. 0x3, or CPU list, e.g. 0,1.")
|
||||
parser.add_argument("--timeout", type=float, default=None,
|
||||
help="Seconds before terminating only the launched job/process.")
|
||||
parser.add_argument("--report", type=Path, default=None,
|
||||
help="Write JSON report to this path. Defaults to stdout.")
|
||||
parser.add_argument("--stdout-log", type=Path, default=None,
|
||||
help="Write child stdout to this path.")
|
||||
parser.add_argument("--stderr-log", type=Path, default=None,
|
||||
help="Write child stderr to this path.")
|
||||
parser.add_argument("command", nargs=argparse.REMAINDER,
|
||||
help="Command to run after --.")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: Sequence[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
ns = parser.parse_args(argv)
|
||||
command = list(ns.command)
|
||||
if command and command[0] == "--":
|
||||
command = command[1:]
|
||||
if not command:
|
||||
parser.error("missing command after --")
|
||||
stdout_log = ns.stdout_log
|
||||
stderr_log = ns.stderr_log
|
||||
if ns.report:
|
||||
if stdout_log is None:
|
||||
stdout_log = ns.report.with_name(ns.report.name + ".stdout.log")
|
||||
if stderr_log is None:
|
||||
stderr_log = ns.report.with_name(ns.report.name + ".stderr.log")
|
||||
|
||||
try:
|
||||
report = run_host(
|
||||
command,
|
||||
ns.cpu_percent,
|
||||
parse_affinity(ns.affinity),
|
||||
ns.timeout,
|
||||
os.getcwd(),
|
||||
stdout_log,
|
||||
stderr_log,
|
||||
ns.core_percent,
|
||||
)
|
||||
except (PerfHostError, OSError, ValueError) as exc:
|
||||
error = {
|
||||
"schema_version": 1,
|
||||
"host": topology_report(),
|
||||
"request": {
|
||||
"cpu_percent": ns.cpu_percent,
|
||||
"core_percent": ns.core_percent,
|
||||
"affinity": ns.affinity,
|
||||
"timeout_seconds": ns.timeout,
|
||||
"argv": command,
|
||||
"cwd": os.getcwd(),
|
||||
"stdout_log": _path_text(stdout_log),
|
||||
"stderr_log": _path_text(stderr_log),
|
||||
},
|
||||
"error": str(exc),
|
||||
}
|
||||
body = json.dumps(error, indent=2, sort_keys=True)
|
||||
if ns.report:
|
||||
ns.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
ns.report.write_text(body + "\n", encoding="utf-8")
|
||||
else:
|
||||
print(body)
|
||||
return 2
|
||||
|
||||
body = json.dumps(report, indent=2, sort_keys=True)
|
||||
if ns.report:
|
||||
ns.report.parent.mkdir(parents=True, exist_ok=True)
|
||||
ns.report.write_text(body + "\n", encoding="utf-8")
|
||||
else:
|
||||
print(body)
|
||||
return int(report["process"]["exit_code"] or 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "ape-crabby-water-idle",
|
||||
"title": "Ape Escape (USA)",
|
||||
"scene": "Crabby Beach, sandcastle area facing the water",
|
||||
"state_sha256": "2dfc142e65eb32e179b67d352f510d15f6d97efd22b3b43a9eef24301f3ad5a9",
|
||||
"notes": "Stationary initial workload, 7200 native input-route ticks. Requires the separately supplied compatible v5 slot01 state. This does not reproduce an analog approach to the water and is not a portable savestate fixture. Original state remains local; see docs/performance/EXPERIMENTS.md for identity and limitations.",
|
||||
"steps": [
|
||||
{"frames": 7200, "buttons": 65535}
|
||||
]
|
||||
}
|
||||
@@ -22,8 +22,9 @@ static uint32_t seen_address, cycles;
|
||||
static int32_t bound(int32_t x) { return x * 2; }
|
||||
static int margin120(void) { return 120; }
|
||||
static void advance(uint32_t n) { cycles += n; }
|
||||
static void entry(CPUState *cpu, uint32_t address) {
|
||||
static int entry(CPUState *cpu, uint32_t address) {
|
||||
assert(cycles == 17); seen_cpu = cpu; seen_address = address;
|
||||
return 1;
|
||||
}
|
||||
int main(void) {
|
||||
CPUState cpu = {0};
|
||||
@@ -36,12 +37,12 @@ int main(void) {
|
||||
assert(psx_ws_screen_x_bound(-256) == -512);
|
||||
assert(psx_ws_screen_x_bound(256) == 512);
|
||||
psx_advance_cycles(17);
|
||||
psx_mod_function_entry(&cpu, 0x80045770);
|
||||
assert(psx_mod_function_entry(&cpu, 0x80045770) == 1);
|
||||
assert(seen_cpu == &cpu && seen_address == 0x80045770);
|
||||
callbacks.ws_screen_x_bound = 0; callbacks.mod_function_entry = 0;
|
||||
overlay_init(&callbacks);
|
||||
assert(psx_ws_screen_x_bound(-256) == -256);
|
||||
psx_mod_function_entry(&cpu, 0);
|
||||
assert(psx_mod_function_entry(&cpu, 0) == 0);
|
||||
assert(seen_address == 0x80045770);
|
||||
/* bgez_sites / clip_edge_x_load_sites in overlay code: identity with no
|
||||
* margin callback (4:3 host), widened by the live margin with one. */
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Focused tests for perf_campaign.py evidence semantics."""
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location("perf_campaign", ROOT / "perf_campaign.py")
|
||||
PERF = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader is not None
|
||||
sys.modules[SPEC.name] = PERF
|
||||
SPEC.loader.exec_module(PERF)
|
||||
|
||||
|
||||
def base_reply(name, params):
|
||||
if name == "ping":
|
||||
return {"id": 1, "ok": True, "frame": 100, "dispatch_miss_total": 0,
|
||||
"dispatch_miss_unique": 0}
|
||||
if name == "freeze_check":
|
||||
return {"id": 1, "ok": True, "psx_cycle_count": 1_000_000,
|
||||
"frame_count": 100, "dirty_ram_insns": 10,
|
||||
"dirty_ram_blocks": 2, "dirty_ram_aborts": 0}
|
||||
if name == "overlay_loader_status":
|
||||
return {"id": 1, "ok": True, "loads": 1, "invalidations": 0,
|
||||
"revalidations": 0, "dispatch_native": 100,
|
||||
"dispatch_interp_fallback": 5, "stale_blocked": 0}
|
||||
if name == "autocompile_status":
|
||||
return {"id": 1, "ok": True, "compile": {"runs": 1, "fails": 0,
|
||||
"shard_fail_total": 0, "configured": 1, "degraded": 0}}
|
||||
if name == "dirty_ram_stats":
|
||||
return {"id": 1, "ok": True, "blocks_run": 2, "insns_run": 10,
|
||||
"native_handoffs": 1, "per_pc": []}
|
||||
if name == "kernel_bless":
|
||||
return {"id": 1, "ok": True, "entries": 0, "clean": 0,
|
||||
"mismatch": 0, "native_hits": 0}
|
||||
if name == "frame_perf":
|
||||
return {"id": 1, "ok": True, "samples": 30,
|
||||
"all": {"total_ms_avg": 16.0, "total_ms_max": 20.0}}
|
||||
if name == "phase_profile":
|
||||
return {"id": 1, "ok": True, "window_s": params["window"], "samples": 100,
|
||||
"interp_share": 0.1, "native_share": 0.2, "static_share": 0.3,
|
||||
"gpu_share": 0.1, "other_share": 0.3, "exc_share": 0.0}
|
||||
if name == "phase_hot":
|
||||
return {"id": 1, "ok": True, "set": params.get("set", "native"),
|
||||
"phase_samples_total": 10, "hash_drops": 0,
|
||||
"top": [{"addr": "0x80010000", "samples": 10}]}
|
||||
if name in ("bios_info", "game_options", "pace_state", "turbo_state", "gpu_state"):
|
||||
return {"id": 1, "ok": True, "name": name}
|
||||
if name in ("clear_input", "input_route_clear", "input_route_stop"):
|
||||
return {"id": 1, "ok": True}
|
||||
if name == "input_route_append":
|
||||
return {"id": 1, "ok": True, "steps": params["_step"]}
|
||||
if name == "input_route_start":
|
||||
return {"id": 1, "ok": True, "steps": 2, "start_frame": 100}
|
||||
if name == "input_route_status":
|
||||
return {"id": 1, "ok": True, "active": False, "steps": 2,
|
||||
"index": 2, "remaining": 0}
|
||||
if name == "starv_ring":
|
||||
return {"id": 1, "ok": True, "total": 2, "returned": 2, "entries": [
|
||||
{"seq": 0, "kind": 15, "us": 1000, "cyc": 100, "func": "0x1",
|
||||
"store_pc": "0x0", "in_exc": 0},
|
||||
{"seq": 1, "kind": 15, "us": 2000, "cyc": 200, "func": "0x2",
|
||||
"store_pc": "0x0", "in_exc": 0},
|
||||
]}
|
||||
raise AssertionError(name)
|
||||
|
||||
|
||||
class FakeClient:
|
||||
def __init__(self, overrides=None):
|
||||
self.overrides = overrides or {}
|
||||
self.calls = []
|
||||
self.append_count = 0
|
||||
self.sample_index = 0
|
||||
|
||||
def cmd(self, name, **params):
|
||||
self.calls.append((name, params))
|
||||
if name == "input_route_append":
|
||||
self.append_count += 1
|
||||
params = dict(params)
|
||||
params["_step"] = self.append_count
|
||||
if name in self.overrides:
|
||||
value = self.overrides[name]
|
||||
if callable(value):
|
||||
return value(name, params)
|
||||
return value
|
||||
reply = base_reply(name, params)
|
||||
if name == "freeze_check":
|
||||
self.sample_index += 1
|
||||
reply = dict(reply)
|
||||
reply["psx_cycle_count"] += self.sample_index * 1_000_000
|
||||
reply["frame_count"] += self.sample_index * 60
|
||||
reply["dirty_ram_insns"] += self.sample_index * 10
|
||||
reply["dirty_ram_blocks"] += self.sample_index
|
||||
elif name in ("ping", "overlay_loader_status", "dirty_ram_stats"):
|
||||
reply = json.loads(json.dumps(reply))
|
||||
bump = self.sample_index
|
||||
if name == "ping":
|
||||
reply["frame"] += bump * 60
|
||||
if name == "overlay_loader_status":
|
||||
reply["dispatch_native"] += bump * 100
|
||||
if name == "dirty_ram_stats":
|
||||
reply["insns_run"] += bump * 10
|
||||
reply["blocks_run"] += bump
|
||||
elif name == "phase_hot":
|
||||
reply = json.loads(json.dumps(reply))
|
||||
reply["phase_samples_total"] += self.sample_index * 10
|
||||
reply["top"][0]["samples"] += self.sample_index * 10
|
||||
return reply
|
||||
|
||||
|
||||
class FakeSocket:
|
||||
def __init__(self, reply):
|
||||
self.reply = reply
|
||||
self.closed = False
|
||||
|
||||
def sendall(self, _body):
|
||||
pass
|
||||
|
||||
def recv(self, _size):
|
||||
if self.reply is None:
|
||||
return b""
|
||||
data = self.reply
|
||||
self.reply = None
|
||||
return data
|
||||
|
||||
def close(self):
|
||||
self.closed = True
|
||||
|
||||
|
||||
class PerfCampaignTests(unittest.TestCase):
|
||||
def test_subparser_does_not_reset_root_common_options(self):
|
||||
args = PERF.build_parser().parse_args([
|
||||
"--port", "4680", "sample", "--route", "r.json", "--out", "o.json"])
|
||||
self.assertEqual(args.port, 4680)
|
||||
|
||||
def test_subparser_can_override_root_common_options(self):
|
||||
args = PERF.build_parser().parse_args([
|
||||
"--port", "4370", "sample", "--port", "4680",
|
||||
"--route", "r.json", "--out", "o.json"])
|
||||
self.assertEqual(args.port, 4680)
|
||||
|
||||
def test_client_rejects_response_id_mismatch(self):
|
||||
with mock.patch.object(PERF.DEBUG_CLIENT, "connect",
|
||||
return_value=FakeSocket(b'{"id":99,"ok":true}')):
|
||||
reply = PERF.Client("127.0.0.1", 1).cmd("ping")
|
||||
self.assertFalse(reply["ok"])
|
||||
self.assertIn("response id mismatch", reply["error"])
|
||||
|
||||
def test_load_route_validates_and_hashes_json(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
path = Path(td) / "route.json"
|
||||
path.write_text(json.dumps({"name": "crabby", "steps": [
|
||||
{"frames": 2, "buttons": "0xFFFF"},
|
||||
{"frames": 3, "buttons": 0xBFFF},
|
||||
]}), encoding="utf-8")
|
||||
route = PERF.load_route(path)
|
||||
self.assertEqual(route["name"], "crabby")
|
||||
self.assertEqual(route["total_frames"], 5)
|
||||
self.assertEqual(route["steps"][1]["buttons"], 0xBFFF)
|
||||
self.assertEqual(len(route["sha256"]), 64)
|
||||
|
||||
def test_analog_route_fails_closed_because_native_route_is_digital_only(self):
|
||||
step = PERF.RouteStep(frames=1, buttons=0xFFFF, lx=0)
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "digital-only"):
|
||||
step.append_params()
|
||||
|
||||
def test_snapshot_marks_frame_perf_unavailable_when_optional_command_fails(self):
|
||||
client = FakeClient({"frame_perf": {"id": 1, "ok": False, "error": "no samples"}})
|
||||
snap = PERF.snapshot(client, phase_window=5, hot_top=10)
|
||||
self.assertNotIn("frame_perf", snap["commands"])
|
||||
self.assertIn("optional frame_perf unavailable", snap["warnings"][0])
|
||||
|
||||
def test_snapshot_keeps_optional_provenance_warnings(self):
|
||||
client = FakeClient({"bios_info": {"id": 1, "ok": False, "error": "missing"}})
|
||||
snap = PERF.snapshot(client, phase_window=5, hot_top=10)
|
||||
self.assertIn("optional bios_info unavailable", snap["warnings"][0])
|
||||
|
||||
def test_counter_reset_fails_closed(self):
|
||||
before = PERF.snapshot(FakeClient(), phase_window=5, hot_top=10)
|
||||
after = PERF.snapshot(FakeClient(), phase_window=5, hot_top=10)
|
||||
after["commands"]["dirty_ram_stats"]["insns_run"] = 0
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "counter reset"):
|
||||
PERF.summarize_window(before, after)
|
||||
|
||||
def test_missing_counter_fails_closed(self):
|
||||
before = PERF.snapshot(FakeClient(), phase_window=5, hot_top=10)
|
||||
after = PERF.snapshot(FakeClient(), phase_window=5, hot_top=10)
|
||||
del after["commands"]["freeze_check"]["psx_cycle_count"]
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "missing field"):
|
||||
PERF.summarize_window(before, after)
|
||||
|
||||
def test_run_sample_records_raw_evidence_and_summary(self):
|
||||
args = argparse_like(timeout=10.0, poll=0.0, hot_top=10, starv_count=4,
|
||||
label="A1", primary_metric="summary.route_wall_s",
|
||||
lower_is_better=True, host="127.0.0.1", port=4370)
|
||||
route = {"name": "unit", "steps": [
|
||||
{"frames": 1, "buttons": 0xFFFF, "lx": None, "ly": None, "rx": None, "ry": None},
|
||||
{"frames": 1, "buttons": 0xBFFF, "lx": None, "ly": None, "rx": None, "ry": None},
|
||||
], "total_frames": 2}
|
||||
with mock.patch.object(PERF.time, "monotonic", side_effect=monotonic_clock()):
|
||||
sample = PERF.run_sample(FakeClient(), route, args)
|
||||
self.assertTrue(sample["success"], sample.get("error"))
|
||||
self.assertIn("before", sample)
|
||||
self.assertIn("after", sample)
|
||||
self.assertGreater(sample["summary"]["cycle_delta"], 0)
|
||||
self.assertGreaterEqual(sample["summary"]["route_wall_s"], 0.0)
|
||||
self.assertIn("observed_counter_window_wall_s", sample["summary"])
|
||||
self.assertIn("debug-tools TCP", sample["limitations"][0])
|
||||
|
||||
def test_run_sample_keeps_partial_report_on_failure(self):
|
||||
args = argparse_like(timeout=10.0, poll=0.0, hot_top=10, starv_count=4,
|
||||
label="A1", primary_metric="summary.route_wall_s",
|
||||
lower_is_better=True, host="127.0.0.1", port=4370)
|
||||
route = {"name": "unit", "steps": [
|
||||
{"frames": 1, "buttons": 0xFFFF, "lx": 0, "ly": None, "rx": None, "ry": None},
|
||||
], "total_frames": 1}
|
||||
sample = PERF.run_sample(FakeClient(), route, args)
|
||||
self.assertFalse(sample["success"])
|
||||
self.assertIn("digital-only", sample["error"])
|
||||
self.assertIn("partial_after", sample)
|
||||
|
||||
def test_run_sample_allows_missing_frame_perf(self):
|
||||
args = argparse_like(timeout=10.0, poll=0.0, hot_top=10, starv_count=4,
|
||||
label="A1", primary_metric="summary.route_wall_s",
|
||||
lower_is_better=True, host="127.0.0.1", port=4370)
|
||||
route = {"name": "unit", "steps": [
|
||||
{"frames": 1, "buttons": 0xFFFF, "lx": None, "ly": None, "rx": None, "ry": None},
|
||||
{"frames": 1, "buttons": 0xBFFF, "lx": None, "ly": None, "rx": None, "ry": None},
|
||||
], "total_frames": 2}
|
||||
with mock.patch.object(PERF.time, "monotonic", side_effect=monotonic_clock()):
|
||||
sample = PERF.run_sample(
|
||||
FakeClient({"frame_perf": {"id": 1, "ok": False, "error": "disabled"}}),
|
||||
route, args)
|
||||
self.assertTrue(sample["success"], sample.get("error"))
|
||||
self.assertFalse(sample["summary"]["frame_perf_available"])
|
||||
self.assertIn("optional frame_perf unavailable", "\n".join(sample["warnings"]))
|
||||
|
||||
def test_route_done_requires_all_steps_consumed(self):
|
||||
client = FakeClient({"input_route_status": {"id": 1, "ok": True,
|
||||
"active": False, "steps": 2, "index": 1,
|
||||
"remaining": 0}})
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "before consuming"):
|
||||
PERF.wait_route_done(client, timeout_s=1.0, poll_s=0.01, expected_steps=2)
|
||||
|
||||
def test_restore_wait_requires_completion_generation_and_success(self):
|
||||
class RestoreClient:
|
||||
def __init__(self):
|
||||
self.statuses = [
|
||||
{"id": 1, "ok": True, "generation": 1, "pending": 0,
|
||||
"last_ok": 1, "last_op": "save", "last_slot": 0},
|
||||
{"id": 3, "ok": True, "generation": 2, "pending": 0,
|
||||
"last_ok": 1, "last_op": "load", "last_slot": 2},
|
||||
]
|
||||
|
||||
def cmd(self, name, **params):
|
||||
if name == "savestate_status":
|
||||
return self.statuses.pop(0)
|
||||
if name == "savestate":
|
||||
self.staged = params
|
||||
return {"id": 2, "ok": True, "op": "load", "slot": 2}
|
||||
raise AssertionError(name)
|
||||
|
||||
client = RestoreClient()
|
||||
witness = PERF.wait_restore_done(client, slot=2, timeout_s=1.0, poll_s=0.01)
|
||||
self.assertEqual(client.staged, {"op": "load", "slot": 2})
|
||||
self.assertEqual(witness["after"]["generation"], 2)
|
||||
|
||||
def test_state_file_hash_must_match_route_manifest(self):
|
||||
args = argparse_like(timeout=10.0, poll=0.0, hot_top=10, starv_count=4,
|
||||
label="A1", primary_metric="summary.route_wall_s",
|
||||
lower_is_better=True, host="127.0.0.1", port=4370,
|
||||
restore_slot=1, restore_timeout=1.0)
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
state = Path(td) / "state.pst"
|
||||
state.write_bytes(b"state")
|
||||
args.state_file = state
|
||||
route = {"name": "unit", "state_sha256": "wrong", "steps": [
|
||||
{"frames": 1, "buttons": 0xFFFF, "lx": None, "ly": None, "rx": None, "ry": None},
|
||||
], "total_frames": 1}
|
||||
client = FakeClient()
|
||||
sample = PERF.run_sample(client, route, args)
|
||||
self.assertFalse(sample["success"])
|
||||
self.assertIn("state_sha256", sample["error"])
|
||||
self.assertEqual(client.calls, [])
|
||||
|
||||
def test_ab_requires_three_pairs(self):
|
||||
sample = successful_sample(1.0)
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "at least three"):
|
||||
PERF.summarize_ab([sample, sample], [sample, sample], "summary.route_wall_s", True)
|
||||
|
||||
def test_ab_reports_min_median_and_interleaving(self):
|
||||
a = [successful_sample(v) for v in (10.0, 12.0, 11.0)]
|
||||
b = [successful_sample(v) for v in (9.0, 9.5, 10.0)]
|
||||
stamp_interleaved(a, b)
|
||||
report = PERF.summarize_ab(a, b, "summary.route_wall_s", True)
|
||||
self.assertEqual(report["accepted_pairs"], 3)
|
||||
self.assertEqual(report["declared_pair_order"], ["A1", "B1", "A2", "B2", "A3", "B3"])
|
||||
self.assertEqual(report["interleaving_proof"]["status"], "proven")
|
||||
self.assertAlmostEqual(report["best_of_n"]["change_percent"], 10.0)
|
||||
self.assertAlmostEqual(report["medians"]["change_percent"], (11.0 - 9.5) / 11.0 * 100.0)
|
||||
|
||||
def test_metric_path_must_exist(self):
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "missing metric"):
|
||||
PERF.metric_from_sample(successful_sample(1.0), "summary.nope")
|
||||
|
||||
def test_ab_rejects_mismatched_route_identity(self):
|
||||
a = [successful_sample(v) for v in (1.0, 2.0, 3.0)]
|
||||
b = [successful_sample(v) for v in (1.0, 2.0, 3.0)]
|
||||
b[1]["provenance"]["route"]["sha256"] = "different"
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "route identity differs"):
|
||||
PERF.summarize_ab(a, b, "summary.route_wall_s", True)
|
||||
|
||||
def test_ab_rejects_non_finite_metric(self):
|
||||
sample = successful_sample(float("nan"))
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "not finite"):
|
||||
PERF.metric_from_sample(sample, "summary.route_wall_s")
|
||||
|
||||
def test_ab_rejects_unproven_interleaving(self):
|
||||
a = [successful_sample(v) for v in (1.0, 2.0, 3.0)]
|
||||
b = [successful_sample(v) for v in (1.0, 2.0, 3.0)]
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "interleaving not proven"):
|
||||
PERF.summarize_ab(a, b, "summary.route_wall_s", True)
|
||||
|
||||
def test_ab_rejects_missing_target_provenance(self):
|
||||
a = [successful_sample(v) for v in (1.0, 2.0, 3.0)]
|
||||
b = [successful_sample(v) for v in (1.0, 2.0, 3.0)]
|
||||
stamp_interleaved(a, b)
|
||||
del b[0]["provenance"]["target"]
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "target"):
|
||||
PERF.summarize_ab(a, b, "summary.route_wall_s", True)
|
||||
|
||||
def test_ab_allows_different_target_between_sides_but_not_within_side(self):
|
||||
a = [successful_sample(v) for v in (1.0, 2.0, 3.0)]
|
||||
b = [successful_sample(v) for v in (1.0, 2.0, 3.0)]
|
||||
for sample in b:
|
||||
sample["provenance"]["target"]["artifact_sha256"] = sha("b")
|
||||
sample["provenance"]["target"]["variant_selector"] = "candidate"
|
||||
stamp_interleaved(a, b)
|
||||
report = PERF.summarize_ab(a, b, "summary.route_wall_s", True)
|
||||
self.assertEqual(
|
||||
report["identity"]["sides"]["B"]["target_binary"]["artifact_sha256"],
|
||||
sha("b"))
|
||||
b[2]["provenance"]["target"]["artifact_sha256"] = sha("c")
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "within side B"):
|
||||
PERF.summarize_ab(a, b, "summary.route_wall_s", True)
|
||||
|
||||
def test_discards_exclude_pairs_before_minimum_rule(self):
|
||||
a = [successful_sample(v) for v in (10.0, 100.0, 12.0, 11.0)]
|
||||
b = [successful_sample(v) for v in (9.0, 100.0, 9.5, 10.0)]
|
||||
stamp_interleaved(a, b)
|
||||
kept_a, kept_b, discarded = PERF.apply_discards(
|
||||
a, b, [{"pair": 2, "reason": "compiler"}])
|
||||
report = PERF.summarize_ab(kept_a, kept_b, "summary.route_wall_s", True, discarded)
|
||||
self.assertEqual(report["accepted_pairs"], 3)
|
||||
self.assertEqual(report["raw_a"], [10.0, 12.0, 11.0])
|
||||
self.assertEqual(report["discarded_pairs"][0]["a"], None)
|
||||
|
||||
def test_load_target_provenance_requires_explicit_keys(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
path = Path(td) / "target.json"
|
||||
path.write_text(json.dumps(target_provenance()), encoding="utf-8")
|
||||
loaded = PERF.load_target_provenance(path)
|
||||
self.assertEqual(loaded["artifact_sha256"], sha("a"))
|
||||
self.assertEqual(len(loaded["source_sha256"]), 64)
|
||||
bad = Path(td) / "bad.json"
|
||||
bad.write_text("{}", encoding="utf-8")
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "missing required keys"):
|
||||
PERF.load_target_provenance(bad)
|
||||
|
||||
def test_target_provenance_rejects_empty_or_malformed_values(self):
|
||||
target = target_provenance()
|
||||
target["artifact_sha256"] = ""
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "artifact_sha256"):
|
||||
PERF.validate_target_shape(target, "target")
|
||||
target = target_provenance()
|
||||
target["host_id"] = None
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "host_id"):
|
||||
PERF.validate_target_shape(target, "target")
|
||||
target = target_provenance()
|
||||
target["instrumentation"] = {}
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "instrumentation"):
|
||||
PERF.validate_target_shape(target, "target")
|
||||
target = target_provenance()
|
||||
target["cpu_budget"] = {"cap": 1, "denominator": 1}
|
||||
with self.assertRaisesRegex(PERF.CampaignError, "affinity"):
|
||||
PERF.validate_target_shape(target, "target")
|
||||
|
||||
|
||||
def argparse_like(**kwargs):
|
||||
class Obj:
|
||||
pass
|
||||
obj = Obj()
|
||||
for key, value in kwargs.items():
|
||||
setattr(obj, key, value)
|
||||
return obj
|
||||
|
||||
|
||||
def successful_sample(value):
|
||||
return {
|
||||
"success": True,
|
||||
"label": "x",
|
||||
"primary_metric": {"name": "summary.route_wall_s", "lower_is_better": True},
|
||||
"provenance": {
|
||||
"tcp": {"host": "127.0.0.1", "port": 4370},
|
||||
"repo": {"commit": "tools", "branch": "b", "dirty": False},
|
||||
"target": target_provenance(),
|
||||
"route": {"name": "unit", "sha256": "abc", "total_frames": 2,
|
||||
"state_sha256": "state", "restore_witness": {"generation": 1}},
|
||||
},
|
||||
"summary": {
|
||||
"route_wall_s": value,
|
||||
"route_start_wall": 0.0,
|
||||
"route_end_wall": 0.5,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def target_provenance():
|
||||
return {
|
||||
"artifact_sha256": sha("a"),
|
||||
"settings_sha256": sha("1"),
|
||||
"bios_sha256": sha("b"),
|
||||
"overlay_inventory_sha256": sha("0"),
|
||||
"host_id": "host",
|
||||
"cpu_budget": {"cap": 1, "denominator": 1, "affinity": "all"},
|
||||
"instrumentation": {"debug_tcp": True, "gl_perf": True},
|
||||
"variant_selector": "baseline",
|
||||
}
|
||||
|
||||
|
||||
def monotonic_clock(start=1000.0, step=0.01):
|
||||
current = start
|
||||
while True:
|
||||
current += step
|
||||
yield current
|
||||
|
||||
|
||||
def sha(seed):
|
||||
return (seed * 64)[:64]
|
||||
|
||||
|
||||
def stamp_interleaved(a, b):
|
||||
t = 0.0
|
||||
for pair_a, pair_b in zip(a, b):
|
||||
pair_a["summary"]["route_start_wall"] = t
|
||||
pair_a["summary"]["route_end_wall"] = t + 0.4
|
||||
t += 1.0
|
||||
pair_b["summary"]["route_start_wall"] = t
|
||||
pair_b["summary"]["route_end_wall"] = t + 0.4
|
||||
t += 1.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for perf_host.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SPEC = importlib.util.spec_from_file_location("perf_host", ROOT / "tools" / "perf_host.py")
|
||||
PERF = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader is not None
|
||||
sys.modules["perf_host"] = PERF
|
||||
SPEC.loader.exec_module(PERF)
|
||||
|
||||
|
||||
class PerfHostTests(unittest.TestCase):
|
||||
def test_parse_affinity_accepts_mask_and_list(self):
|
||||
self.assertEqual(PERF.parse_affinity("0x3"), 0x3)
|
||||
self.assertEqual(PERF.parse_affinity("1,3"), 0b1010)
|
||||
self.assertIsNone(PERF.parse_affinity(None))
|
||||
|
||||
def test_parse_affinity_rejects_zero_and_negative_index(self):
|
||||
with self.assertRaises(ValueError):
|
||||
PERF.parse_affinity("0")
|
||||
with self.assertRaises(ValueError):
|
||||
PERF.parse_affinity("0,-1")
|
||||
|
||||
def test_validate_cpu_percent_range(self):
|
||||
self.assertEqual(PERF.validate_cpu_percent(25), 25)
|
||||
self.assertIsNone(PERF.validate_cpu_percent(None))
|
||||
with self.assertRaises(ValueError):
|
||||
PERF.validate_cpu_percent(0)
|
||||
with self.assertRaises(ValueError):
|
||||
PERF.validate_cpu_percent(101)
|
||||
|
||||
def test_validate_core_percent_range(self):
|
||||
self.assertEqual(PERF.validate_core_percent(12.5), 12.5)
|
||||
self.assertIsNone(PERF.validate_core_percent(None))
|
||||
with self.assertRaises(ValueError):
|
||||
PERF.validate_core_percent(0.0)
|
||||
with self.assertRaises(ValueError):
|
||||
PERF.validate_core_percent(float("inf"))
|
||||
|
||||
def test_affinity_scaled_cpu_rate_units(self):
|
||||
self.assertEqual(PERF.affinity_scaled_cpu_rate_units(50, 0x1, 16), 313)
|
||||
self.assertEqual(PERF.affinity_scaled_cpu_rate_units(25, 0x3, 8), 625)
|
||||
self.assertEqual(PERF.affinity_scaled_cpu_rate_units(50, None, 16), 5000)
|
||||
self.assertIsNone(PERF.affinity_scaled_cpu_rate_units(None, 0x1, 16))
|
||||
self.assertEqual(PERF.one_core_cpu_rate_units(50, 16), 313)
|
||||
self.assertEqual(PERF.one_core_cpu_rate_units(25, 16), 157)
|
||||
|
||||
def test_core_percent_computes_raw_rate_units(self):
|
||||
fake = PERF.LaunchResult(
|
||||
pid=123,
|
||||
exit_code=0,
|
||||
timed_out=False,
|
||||
wall_seconds=0.125,
|
||||
cpu_user_seconds=None,
|
||||
cpu_kernel_seconds=None,
|
||||
cpu_total_seconds=None,
|
||||
)
|
||||
with mock.patch.object(PERF, "launch_windows",
|
||||
return_value=(fake, {"platform": "windows"})) as launcher, \
|
||||
mock.patch.object(PERF.os, "name", "nt"), \
|
||||
mock.patch.object(PERF.os, "cpu_count", return_value=16):
|
||||
PERF.run_host(["tool"], None, 0x1, 1.0, core_percent=50)
|
||||
self.assertEqual(launcher.call_args.args[7], 313)
|
||||
self.assertEqual(launcher.call_args.args[8], "one_core_percent")
|
||||
|
||||
def test_cpu_percent_and_core_percent_are_mutually_exclusive(self):
|
||||
with self.assertRaises(ValueError):
|
||||
PERF.run_host(["tool"], 50, 0x1, 1.0, core_percent=50)
|
||||
|
||||
def test_portable_throttle_fails_honestly(self):
|
||||
with self.assertRaises(PERF.UnsupportedThrottle):
|
||||
PERF.launch_portable([sys.executable, "-c", "pass"], 25, None, 1.0)
|
||||
|
||||
def test_run_host_records_report_shape_with_mock_launcher(self):
|
||||
fake = PERF.LaunchResult(
|
||||
pid=123,
|
||||
exit_code=7,
|
||||
timed_out=False,
|
||||
wall_seconds=0.125,
|
||||
cpu_user_seconds=0.01,
|
||||
cpu_kernel_seconds=0.02,
|
||||
cpu_total_seconds=0.03,
|
||||
)
|
||||
with mock.patch.object(PERF, "launch_portable",
|
||||
return_value=(fake, {"platform": "portable"})), \
|
||||
mock.patch.object(PERF.os, "name", "posix"):
|
||||
report = PERF.run_host(["tool", "arg"], None, 0x3, 2.0, "cwd")
|
||||
self.assertEqual(report["request"]["argv"], ["tool", "arg"])
|
||||
self.assertEqual(report["request"]["affinity"], 0x3)
|
||||
self.assertEqual(report["effective"]["platform"], "portable")
|
||||
self.assertEqual(report["process"]["exit_code"], 7)
|
||||
|
||||
def test_main_writes_error_report_for_unsupported_throttle(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
report_path = Path(td) / "report.json"
|
||||
with mock.patch.object(
|
||||
PERF,
|
||||
"run_host",
|
||||
side_effect=PERF.UnsupportedThrottle(
|
||||
"CPU throttling is only supported on Windows via Job Objects"
|
||||
),
|
||||
):
|
||||
rc = PERF.main([
|
||||
"--cpu-percent", "50",
|
||||
"--report", str(report_path),
|
||||
"--",
|
||||
sys.executable, "-c", "pass",
|
||||
])
|
||||
body = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
self.assertEqual(rc, 2)
|
||||
self.assertIn("CPU throttling is only supported", body["error"])
|
||||
|
||||
def test_windows_failure_terminates_scoped_job(self):
|
||||
if os.name != "nt":
|
||||
self.skipTest("Windows API layer is only defined on Windows")
|
||||
|
||||
calls: list[str] = []
|
||||
|
||||
class FakeKernel:
|
||||
def CreateProcessW(self, app, cmd, psa, tsa, inherit, flags, env, cwd, si, pi):
|
||||
pi._obj.hProcess = 100
|
||||
pi._obj.hThread = 200
|
||||
pi._obj.dwProcessId = 300
|
||||
return True
|
||||
|
||||
def CreateJobObjectW(self, attrs, name):
|
||||
return 400
|
||||
|
||||
def SetInformationJobObject(self, *args):
|
||||
return True
|
||||
|
||||
def SetProcessAffinityMask(self, *args):
|
||||
return True
|
||||
|
||||
def GetProcessAffinityMask(self, process, process_mask, system_mask):
|
||||
process_mask._obj.value = 0x3
|
||||
system_mask._obj.value = 0xFFFF
|
||||
return True
|
||||
|
||||
def AssignProcessToJobObject(self, *args):
|
||||
calls.append("assign")
|
||||
return False
|
||||
|
||||
def ResumeThread(self, *args):
|
||||
calls.append("resume")
|
||||
return 0
|
||||
|
||||
def TerminateJobObject(self, *args):
|
||||
calls.append("terminate_job")
|
||||
return True
|
||||
|
||||
def TerminateProcess(self, *args):
|
||||
calls.append("terminate_process")
|
||||
return True
|
||||
|
||||
def WaitForSingleObject(self, *args):
|
||||
calls.append("wait")
|
||||
return PERF.WAIT_OBJECT_0
|
||||
|
||||
def CloseHandle(self, handle):
|
||||
calls.append(f"close:{handle}")
|
||||
return True
|
||||
|
||||
with mock.patch.object(PERF, "kernel32", FakeKernel()), \
|
||||
mock.patch.object(PERF.ctypes, "get_last_error", return_value=5):
|
||||
with self.assertRaisesRegex(PERF.PerfHostError, "AssignProcessToJobObject"):
|
||||
PERF.launch_windows([sys.executable, "-c", "pass"], 50, None, 1.0)
|
||||
self.assertIn("terminate_job", calls)
|
||||
self.assertIn("terminate_process", calls)
|
||||
self.assertNotIn("resume", calls)
|
||||
|
||||
calls.clear()
|
||||
kernel = FakeKernel()
|
||||
with mock.patch.object(PERF, "kernel32", kernel), \
|
||||
mock.patch.object(kernel, "AssignProcessToJobObject",
|
||||
side_effect=KeyboardInterrupt):
|
||||
with self.assertRaises(KeyboardInterrupt):
|
||||
PERF.launch_windows([sys.executable, "-c", "pass"], 50, None, 1.0)
|
||||
self.assertIn("terminate_process", calls)
|
||||
self.assertNotIn("resume", calls)
|
||||
|
||||
def test_windows_get_exit_code_failure_terminates_scoped_job(self):
|
||||
if os.name != "nt":
|
||||
self.skipTest("Windows API layer is only defined on Windows")
|
||||
|
||||
calls: list[str] = []
|
||||
|
||||
class FakeKernel:
|
||||
def CreateProcessW(self, app, cmd, psa, tsa, inherit, flags, env, cwd, si, pi):
|
||||
pi._obj.hProcess = 100
|
||||
pi._obj.hThread = 200
|
||||
pi._obj.dwProcessId = 300
|
||||
return True
|
||||
|
||||
def CreateJobObjectW(self, attrs, name):
|
||||
return 400
|
||||
|
||||
def SetInformationJobObject(self, *args):
|
||||
return True
|
||||
|
||||
def AssignProcessToJobObject(self, *args):
|
||||
return True
|
||||
|
||||
def ResumeThread(self, *args):
|
||||
calls.append("resume")
|
||||
return 0
|
||||
|
||||
def WaitForSingleObject(self, *args):
|
||||
return PERF.WAIT_OBJECT_0
|
||||
|
||||
def GetExitCodeProcess(self, *args):
|
||||
return False
|
||||
|
||||
def TerminateJobObject(self, *args):
|
||||
calls.append("terminate_job")
|
||||
return True
|
||||
|
||||
def TerminateProcess(self, *args):
|
||||
calls.append("terminate_process")
|
||||
return True
|
||||
|
||||
def CloseHandle(self, handle):
|
||||
calls.append(f"close:{handle}")
|
||||
return True
|
||||
|
||||
with mock.patch.object(PERF, "kernel32", FakeKernel()), \
|
||||
mock.patch.object(PERF.ctypes, "get_last_error", return_value=6):
|
||||
with self.assertRaisesRegex(PERF.PerfHostError, "GetExitCodeProcess"):
|
||||
PERF.launch_windows([sys.executable, "-c", "pass"], None, None, 1.0)
|
||||
self.assertIn("resume", calls)
|
||||
self.assertIn("terminate_job", calls)
|
||||
self.assertIn("terminate_process", calls)
|
||||
|
||||
def test_windows_create_process_failure_closes_stdio_files(self):
|
||||
if os.name != "nt":
|
||||
self.skipTest("Windows API layer is only defined on Windows")
|
||||
|
||||
class FakeKernel:
|
||||
def CreateProcessW(self, *args):
|
||||
return False
|
||||
|
||||
with tempfile.TemporaryDirectory() as td, \
|
||||
mock.patch.object(PERF, "kernel32", FakeKernel()), \
|
||||
mock.patch.object(PERF.ctypes, "get_last_error", return_value=2):
|
||||
out = Path(td) / "child.out"
|
||||
err = Path(td) / "child.err"
|
||||
with self.assertRaisesRegex(PERF.PerfHostError, "CreateProcessW"):
|
||||
PERF.launch_windows([sys.executable, "-c", "pass"], None, None, 1.0,
|
||||
stdout_log=out, stderr_log=err)
|
||||
out.unlink()
|
||||
err.unlink()
|
||||
|
||||
@unittest.skipIf(os.name != "nt", "Windows Job Object test")
|
||||
def test_windows_real_child_unrestricted(self):
|
||||
report = PERF.run_host([sys.executable, "-c", "import sys; sys.exit(3)"],
|
||||
None, None, 5.0)
|
||||
self.assertEqual(report["process"]["exit_code"], 3)
|
||||
self.assertFalse(report["process"]["timed_out"])
|
||||
self.assertEqual(report["effective"]["platform"], "windows")
|
||||
|
||||
@unittest.skipIf(os.name != "nt", "Windows Job Object test")
|
||||
def test_windows_real_child_preserves_logs(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
report_path = Path(td) / "report.json"
|
||||
rc = PERF.main([
|
||||
"--timeout", "5",
|
||||
"--report", str(report_path),
|
||||
"--",
|
||||
sys.executable, "-c",
|
||||
"import sys; print('out'); print('err', file=sys.stderr); sys.exit(3)",
|
||||
])
|
||||
body = json.loads(report_path.read_text(encoding="utf-8"))
|
||||
stdout_log = Path(body["effective"]["stdout_log"])
|
||||
stderr_log = Path(body["effective"]["stderr_log"])
|
||||
self.assertEqual(rc, 3)
|
||||
self.assertEqual(stdout_log.read_text(encoding="utf-8").strip(), "out")
|
||||
self.assertEqual(stderr_log.read_text(encoding="utf-8").strip(), "err")
|
||||
|
||||
@unittest.skipIf(os.name != "nt", "Windows Job Object test")
|
||||
def test_windows_real_child_exit_code_259_is_preserved(self):
|
||||
report = PERF.run_host([sys.executable, "-c", "import sys; sys.exit(259)"],
|
||||
None, None, 5.0)
|
||||
self.assertEqual(report["process"]["exit_code"], 259)
|
||||
self.assertFalse(report["process"]["timed_out"])
|
||||
|
||||
@unittest.skipIf(os.name != "nt", "Windows Job Object test")
|
||||
def test_windows_real_child_timeout_with_cap(self):
|
||||
report = PERF.run_host([sys.executable, "-c", "while True: pass"],
|
||||
25, None, 0.2)
|
||||
self.assertTrue(report["process"]["timed_out"])
|
||||
self.assertEqual(report["effective"]["job_cpu_rate_units"], 2500)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user