mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge remote-tracking branch 'origin/master' into landtmp/504
This commit is contained in:
@@ -129,7 +129,11 @@ The backend tier is resolved in `overlay_backend.c` (see `main.cpp` around the
|
||||
|
||||
Compiled overlays are stored in a content-addressed cache namespaced by
|
||||
compiler and target ABI (`<game>/gcc/<arch-abi>/…` vs `<game>/tcc/…`); a `gcc`
|
||||
shard wins over a `tcc` shard for the same region. See
|
||||
shard wins over a `tcc` shard for the same region. The `<arch-abi>` is the
|
||||
runtime's own (`PSX_OVERLAY_ARCH_ABI` in `overlay_loader.h`, the running slice
|
||||
of a universal macOS binary): the runtime exports it to every compile it spawns
|
||||
and macOS compiles pass the matching `-arch`, so an x86_64 runtime under Rosetta
|
||||
gets x86_64 shards even though Xcode's clang defaults to arm64. See
|
||||
[`docs/FEATURES.md`](FEATURES.md), [`docs/OVERLAY_CACHE_V2.md`](OVERLAY_CACHE_V2.md),
|
||||
and [`docs/ASYNC_OVERLAY_COMPILE.md`](ASYNC_OVERLAY_COMPILE.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Optional projection-distance scaling
|
||||
|
||||
`[video] fov_scale` accepts finite values greater than zero and at most 8;
|
||||
1.0 is faithful. A valid `PSX_GTE_FOV_SCALE` overrides the config value;
|
||||
invalid environment values are ignored. The whole string must be numeric
|
||||
apart from surrounding whitespace. Scaling is quantized to 0.001, with a
|
||||
minimum quantized denominator of 1. Values above one reduce effective H.
|
||||
This scales projection distance, not the field-of-view angle. Guest H storage
|
||||
is unchanged; RTPS/RTPT and their optional precision shadows use effective H.
|
||||
|
||||
The setting is loaded by a runtime-only parser extension. It does not change
|
||||
the shared recompiler parser, emitted BIOS/game C or emitter fingerprints.
|
||||
Consuming it requires updating the framework pin and rebuilding the runtime;
|
||||
neither game nor BIOS regeneration is required. Bundled OpenBIOS is supported.
|
||||
@@ -288,6 +288,14 @@ internal resolutions; a size change frees the old set.
|
||||
- `render_pass_stats` (TCP): passes, shedding, faults, dropped device
|
||||
stores, timing split (backup / guest code / capture / restore), presents
|
||||
made from pass images, and `status`.
|
||||
`refused` counts empty plans with wanted phases; pass-call refusals are
|
||||
separate: `pass_attempts`, `argument_refused`, `status_refused`,
|
||||
`begin_refused`, `checkpoint_refused`. `last_failure` is null until a refusal,
|
||||
then retains the failure-site reason, attempt/plan, guest cycle, rect/alpha,
|
||||
and GL begin inputs (actual requested/capture dimensions, scales, generation,
|
||||
source path and resource stage). FBO status and GL errors are numeric enums;
|
||||
`gl_error_before` is distinct from errors produced during that allocation.
|
||||
Success does not erase the record; a new mod session clears it.
|
||||
- `render_pass_refuse on=1` (TCP) or `PSX_RENDER_PASS_REFUSE=1`: the backend
|
||||
declines passes (`BACKEND`), to test a plugin's fallback.
|
||||
- `render_pass_dump path=<dir> count=<n>`: PNGs of the next n frames' images
|
||||
@@ -330,3 +338,22 @@ Tests (runtime ctest unless noted):
|
||||
The GPU half of the VRAM transaction (hr colour, stencil, raw mirror blits)
|
||||
needs an OpenGL context and is checked at runtime by
|
||||
`PSX_RENDER_PASS_VERIFY=1`.
|
||||
|
||||
### Nested mod callbacks and watchdog rollback
|
||||
|
||||
The host checkpoint includes the mod function-entry depth and current plugin
|
||||
owner. A pass can interrupt an outer callback; restoring to zero would lose
|
||||
that caller's context. Watchdog longjmp restores the saved depth/owner instead,
|
||||
and normal-return verification checks their balance. This context stays local
|
||||
to the committed session and is never part of a guest savestate.
|
||||
`render_pass_stats.last_abort_detail` names skipped host exits and remains
|
||||
latched across successful passes until session reset.
|
||||
|
||||
### Nested mod callback rollback
|
||||
|
||||
The host checkpoint includes function-entry depth and current plugin owner.
|
||||
A pass can interrupt an outer callback; watchdog rollback restores its exact
|
||||
saved context rather than resetting depth to zero. Normal-return verification
|
||||
also checks balance. This host context is never serialized in guest savestates.
|
||||
TCP render_pass_stats.last_abort_detail names skipped exits and remains latched
|
||||
across successes until session reset.
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Simultaneous stereo rendering
|
||||
|
||||
The paired API is an opt-in enhancement alongside temporal render passes. It
|
||||
uses the same frozen-time checkpoint, store policy, watchdog, VRAM journal and
|
||||
restore. It does not require interpolation, a FLIP source, a temporal plan or
|
||||
Q16 phase labels. Each eye callback receives LEFT=0 or RIGHT=1 explicitly.
|
||||
|
||||
Call `psx_mod_render_stereo(cpu, frame, callback, user)` from a game's measured
|
||||
draw boundary outside interrupts or active GPU DMA. The callback must rebuild
|
||||
and submit geometry, without waiting for VBlank or advancing game logic. The
|
||||
rect declares the capture region. Restore completes after each eye before the
|
||||
next one starts. Successful eye textures remain private until both transactions
|
||||
and capture/restore verification succeed. Failure retains the preceding complete
|
||||
pair. Host callback state is the plugin's responsibility, including selectors
|
||||
that must be cleared after watchdog rollback.
|
||||
|
||||
`period_vblanks` describes the game's draw cadence (1..8). An EMA of complete-pair
|
||||
host cost is compared with 80% of that cadence, using the runtime's live NTSC/PAL
|
||||
period. Expensive work is shed as a complete pair; every thirtieth request retries
|
||||
to recover a stale estimate. This is a conservative cadence budget, not a measured
|
||||
headset idle budget. Allocating or slow initial pairs may exceed it; completed
|
||||
guest work cannot be preempted by a host-time budget. Guest-cycle watchdogs remain
|
||||
the runaway bound.
|
||||
|
||||
OpenGL GPU-authoritative 15-bit rendering is required. CPU-authoritative dual
|
||||
raster, windowed high-resolution tiles, depth24 and native-wide rendering are
|
||||
initially unsupported. Session/resimulation/rewind/fast-forward and nesting gates
|
||||
remain. Faithful defaults do not call this API or enable stereo presentation.
|
||||
|
||||
`psx_mod_set_stereo_presentation(1)` opts into side-by-side output: one atomic pair
|
||||
per swap, LEFT on the left, RIGHT on the right, with each eye using the configured
|
||||
display aspect. Zero disables it. The first implementation uses ordinary game
|
||||
VBlank presentation cadence; it does not submit to OpenXR or create headset poses.
|
||||
Stereo and temporal interpolation should be configured separately; SBS takes
|
||||
precedence when a compatible complete pair is available. A pair is shown only while
|
||||
the plugin keeps submitting: it expires 24 VBlanks of guest time after its capture
|
||||
cycle (3x the slowest 8-VBlank cadence) and the presenter falls back to the normal
|
||||
flat image (menus, same-size FMV, a plugin that stopped submitting). Savestate/rollback loads and
|
||||
session resets drop the pair immediately.
|
||||
|
||||
Inside an eye callback, `psx_mod_render_view_offset(x,y,z)` replaces a scoped
|
||||
camera-space translation. RTPS and RTPT add it alongside TR before projection;
|
||||
the guest translation registers remain unchanged. Repeated transformations do
|
||||
not accumulate the offset. Normal returns and watchdog rollback restore the
|
||||
previous host value. Calls outside a render transaction are refused. The GTE
|
||||
test compares this seam against explicit pre-divide translation and measures
|
||||
12 pixels at Z=800 versus 3 pixels at Z=3200 for X=24 and H=400.
|
||||
|
||||
`psx_mod_render_view` (full pose) is an explicit per-eye projection. With
|
||||
`projection=1` it REPLACES the guest X/Y projection entirely: guest `H`, the
|
||||
widescreen/dome squash and the `[video] fov_scale` scaled-H do not feed it, and the
|
||||
guest-formula PGXP shadows are skipped for those vertices. The only coupling is
|
||||
`projection_h_ref`: when non-zero the focal lengths are multiplied by (fov-scaled
|
||||
guest `H`) / `projection_h_ref`, so a plugin can follow per-draw H changes. SZ, the
|
||||
depth cue and divide flags still come from the guest division. The pose exists only
|
||||
inside a render transaction and is restored with it, so nothing outside the sandbox
|
||||
sees it. `test_render_pose` covers the override together with `fov_scale`.
|
||||
|
||||
TCP inspection:
|
||||
|
||||
- `stereo_stats`: pair outcomes, eye entry cycles/state hashes under VERIFY,
|
||||
sampled view offsets, latched failed attempt/eye/retained pair, and published
|
||||
pair metadata. `render_pass_stats` continues to
|
||||
expose shared transaction checks, aborts, leaks and dropped stores; its eye
|
||||
transactions also increment pass counters.
|
||||
- `stereo_dump path=<absolute-directory> count=N`: arm dumps of the next N
|
||||
complete published pairs, with left/right PNGs, a side-by-side composite
|
||||
sharing one pair ID. Pair/cycle/view metadata is inspected through TCP
|
||||
`stereo_stats.captures`, retained for the current capture request (maximum
|
||||
100 records); the runtime writes no diagnostic JSON files. A capture client
|
||||
can use `tools/stereo_capture.py::write_capture_manifests` to write manifests
|
||||
after confirming all three PNGs for each returned ID.
|
||||
Arming another capture or resetting the session clears the metadata. It captures
|
||||
no scene on its own. Failed staging eyes never appear as published pairs.
|
||||
|
||||
Validation, source revisions and live limitations are recorded in
|
||||
[the feature provenance record](internal/upstream/atomic-stereo-render-views.md).
|
||||
The game's [VR execution plan](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/blob/master/docs/reverse/VR_EXECUTION_PLAN.md)
|
||||
records historical alpha evidence. Projection scale and eye separation remain
|
||||
game-specific and must be calibrated independently. This API also works without
|
||||
a headset backend; it does not enable a headset mode by default.
|
||||
@@ -431,9 +431,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**333 commands registered** — 320 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**337 commands registered** — 324 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
63 of 333 have prose above; **270 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
63 of 337 have prose above; **274 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -506,6 +506,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `dirty_insn_log` | ✓ | | |
|
||||
| `dirty_ram_stats` | ✓ | | ✓ |
|
||||
| `dirty_ram_unsupported` | ✓ | | |
|
||||
| `disasm` | ✓ | | |
|
||||
| `disc_select` | ✓ | | |
|
||||
| `disp_ring` | ✓ | | |
|
||||
| `dispatch_check` | ✓ | | |
|
||||
@@ -660,6 +661,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `render_pass_dump` | ✓ | | ✓ |
|
||||
| `render_pass_refuse` | ✓ | | ✓ |
|
||||
| `render_pass_stats` | ✓ | | ✓ |
|
||||
| `resident_status` | ✓ | | |
|
||||
| `restore_trace` | ✓ | | |
|
||||
| `restore_trace_clear` | ✓ | | |
|
||||
| `restore_trace_window` | ✓ | | |
|
||||
@@ -710,6 +712,8 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `stack_profile` | ✓ | | |
|
||||
| `starv_ring` | ✓ | | |
|
||||
| `step` | ✓ | | ✓ |
|
||||
| `stereo_dump` | ✓ | | |
|
||||
| `stereo_stats` | ✓ | | |
|
||||
| `synth_recurse` | ✓ | | |
|
||||
| `thread_ctx_ring` | ✓ | | |
|
||||
| `thread_trace` | ✓ | | |
|
||||
@@ -774,3 +778,6 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `xprobe_watch` | ✓ | | |
|
||||
|
||||
<!-- END AUTOGENERATED COMMAND INDEX -->
|
||||
|
||||
`wtrace_dump pc_lo=<hex> pc_hi=<hex>` filters the recorded producer PC, masked to the physical address (KSEG0/KSEG1/KUSEG alias, like `addr_lo`/`addr_hi`), with inclusive lower and exclusive upper bounds, before limiting results. Omitted bounds match every entry; DMA entries use their recorded initiator, and a DMA whose initiator is unknown (0) never matches an explicit PC bound. Address/frame/order filters still apply.
|
||||
`disasm addr=<hex> count=<1..256>` inspects aligned guest instructions; default count is 16. Resolved targets use the guest PC. Unknown words are shown verbatim.
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Atomic stereo pairs and scoped views: source and validation
|
||||
|
||||
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
|
||||
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
|
||||
Exact source commits:
|
||||
|
||||
- [0a955971](https://github.com/FractalEngineer/psxrecomp/commit/0a95597129cc2ee6d70397a8da9511831575eae4)
|
||||
- [0a4cf21f](https://github.com/FractalEngineer/psxrecomp/commit/0a4cf21fac88ef425ba0edd25871d7dd20ee0903)
|
||||
- [22bdc9e9](https://github.com/FractalEngineer/psxrecomp/commit/22bdc9e90d14f7a04b79c83b77611c4eb980fef4)
|
||||
- [734977c9](https://github.com/FractalEngineer/psxrecomp/commit/734977c93e80789e9210a262daa35a88f41c40d4)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
|
||||
that exact human author identity. The focused port consolidates the selected
|
||||
source hunks and corrections. Its commit also cites these sources and carries
|
||||
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
|
||||
|
||||
Adapted: Frozen-checkpoint paired rendering, private staging/atomic publication, scoped GTE rigid/asymmetric views, full-pose rollback and generic inverse pose math. TCP diagnostics and bounded capture tooling accompany the feature.
|
||||
|
||||
Deliberately excluded: OpenXR SDK/session/device code, producer-PC filtering, game replay callbacks/addresses and the reverted vertex-capture experiment.
|
||||
|
||||
The feature was reconstructed on upstream master
|
||||
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
|
||||
renderer/input policy and build integration. Validation after reconstruction:
|
||||
Paired abort/sandbox, GL refusal/source guards, pose/inverse-pose and GTE identity/parallax/rigid/asymmetric regressions.
|
||||
|
||||
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
|
||||
Game-specific integration remains in that repository. Consuming this change
|
||||
requires a deliberate framework pin update in the game repo and a rebuild.
|
||||
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
|
||||
|
||||
Historical alpha acceptance used the older framework checkpoint above. It is
|
||||
separate from current-master branch verification. Fresh game, renderer and
|
||||
headset checks, where applicable, are recorded in the PR validation notes;
|
||||
uncertain hardware/runtime behavior must not be inferred from a clean link.
|
||||
|
||||
## Current-master integration validation
|
||||
|
||||
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
|
||||
|
||||
Release pair abort/sandbox, pose/inverse-pose and GTE identity/parallax/rigid/asymmetric tests pass. The actual OpenGL fixture passes all 197 checks at both 1x and 4x on an RTX 4070 Ti (NVIDIA OpenGL 4.6, driver 616.92), including eye texture pixels, private staging, atomic publication, VRAM restoration, refusal gates, retention of the preceding pair on failure, PNG capture and TCP metadata. Diagnostic manifests are written by host tooling from bounded TCP metadata. Fresh in-headset stereo acceptance remains pending.
|
||||
|
||||
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
|
||||
|
||||
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Runtime projection-distance scaling: source and validation
|
||||
|
||||
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
|
||||
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
|
||||
Exact source commits:
|
||||
|
||||
- [82695b75](https://github.com/FractalEngineer/psxrecomp/commit/82695b75d79610ca7a99783adf9dd5195edfcdd0)
|
||||
- [5633e868](https://github.com/FractalEngineer/psxrecomp/commit/5633e86878611a14d3a78e8f9708452a07bc7238)
|
||||
- [bbd01ccf](https://github.com/FractalEngineer/psxrecomp/commit/bbd01ccfc91c1d9ebe199dad071df64e5b13d06d)
|
||||
- [39d478db](https://github.com/FractalEngineer/psxrecomp/commit/39d478db83132815c2c8c593fc719c4bf67301b0)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
|
||||
that exact human author identity. The focused port consolidates the selected
|
||||
source hunks and corrections. Its commit also cites these sources and carries
|
||||
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
|
||||
|
||||
Adapted: Optional effective-H scaling and environment/config precedence. Unlike the source fork, [video] fov_scale is read by a runtime-only parser extension, leaving all emitter inputs unchanged. Environment/range/quantization validation and PGXP shadows were corrected.
|
||||
|
||||
Deliberately excluded: Shared recompiler-parser fields, BIOS stamp refreshes, stereo views, XR code and game camera/culling policy.
|
||||
|
||||
The feature was reconstructed on upstream master
|
||||
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
|
||||
renderer/input policy and build integration. Validation after reconstruction:
|
||||
GTE identity/numerical/environment/quantization regressions and both committed BIOS fingerprint checks. Runtime config-parser regression is registered with CTest.
|
||||
|
||||
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
|
||||
Game-specific integration remains in that repository. Consuming this change
|
||||
requires a deliberate framework pin update in the game repo and a rebuild.
|
||||
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
|
||||
|
||||
Historical alpha acceptance used the older framework checkpoint above. It is
|
||||
separate from current-master branch verification. Fresh game, renderer and
|
||||
headset checks, where applicable, are recorded in the PR validation notes;
|
||||
uncertain hardware/runtime behavior must not be inferred from a clean link.
|
||||
|
||||
## Current-master integration validation
|
||||
|
||||
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
|
||||
|
||||
Release GTE tests pass for identity, numerical projection, invalid ratios, finite/range/full-string environment parsing and quantization. A registered runtime-only TOML regression covers defaults, valid values and rejected values. PGXP shadows use effective H while guest H stays canonical. Both committed BIOS fingerprint checks pass: the shared recompiler parser and generated BIOS files are unchanged from master.
|
||||
|
||||
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
|
||||
|
||||
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Offline plugin controller sources: source and validation
|
||||
|
||||
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
|
||||
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
|
||||
Exact source commits:
|
||||
|
||||
- [5a1264b7](https://github.com/FractalEngineer/psxrecomp/commit/5a1264b79b69b5d75f7b8507e305b1e13a911a50)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
|
||||
that exact human author identity. The focused port consolidates the selected
|
||||
source hunks and corrections. Its commit also cites these sources and carries
|
||||
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
|
||||
|
||||
Adapted: Only offline controller-source registration (documented as mod-trusted), validation/neutralization and the existing offline host-input boundary.
|
||||
|
||||
Deliberately excluded: OpenXR Touch action implementation, game bindings/deadzones/aiming and guest SIO replacements.
|
||||
|
||||
The feature was reconstructed on upstream master
|
||||
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
|
||||
renderer/input policy and build integration. Validation after reconstruction:
|
||||
Absent/invalid/declined/analog/digital source, detach and session-reset executable checks.
|
||||
|
||||
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
|
||||
Game-specific integration remains in that repository. Consuming this change
|
||||
requires a deliberate framework pin update in the game repo and a rebuild.
|
||||
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
|
||||
|
||||
Historical alpha acceptance used the older framework checkpoint above. It is
|
||||
separate from current-master branch verification. Fresh game, renderer and
|
||||
headset checks, where applicable, are recorded in the PR validation notes;
|
||||
uncertain hardware/runtime behavior must not be inferred from a clean link.
|
||||
|
||||
## Current-master integration validation
|
||||
|
||||
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
|
||||
|
||||
Release controller-source checks pass for absent, invalid, declined, analog/digital, detach and session-reset samples; they remain active with NDEBUG enabled. Integrated flat input reaches gameplay. Hardware controller mappings and live headset focus/reconnect behavior remain outside this test coverage.
|
||||
|
||||
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
|
||||
|
||||
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Render-pass callback rollback: source and validation
|
||||
|
||||
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
|
||||
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
|
||||
Exact source commits:
|
||||
|
||||
- [6d44e21d](https://github.com/FractalEngineer/psxrecomp/commit/6d44e21df7ccedd1acbfa31844a53bf8bd8b316e)
|
||||
- [35b209d4](https://github.com/FractalEngineer/psxrecomp/commit/35b209d406c168e14f51bb8bbd549742473c7388)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
|
||||
that exact human author identity. The focused port consolidates the selected
|
||||
source hunks and corrections. Its commit also cites these sources and carries
|
||||
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
|
||||
|
||||
Adapted: Callback depth/owner snapshot and restore on the existing render transaction, normal-return imbalance detection and focused recovery documentation.
|
||||
|
||||
Deliberately excluded: Paired-eye extension tests (included with stereo), game save files and scene-specific replay setup.
|
||||
|
||||
The feature was reconstructed on upstream master
|
||||
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
|
||||
renderer/input policy and build integration. Validation after reconstruction:
|
||||
Nested callback/owner rollback, nonzero outer callback, normal-return imbalance and watchdog recovery regressions.
|
||||
|
||||
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
|
||||
Game-specific integration remains in that repository. Consuming this change
|
||||
requires a deliberate framework pin update in the game repo and a rebuild.
|
||||
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
|
||||
|
||||
Historical alpha acceptance used the older framework checkpoint above. It is
|
||||
separate from current-master branch verification. Fresh game, renderer and
|
||||
headset checks, where applicable, are recorded in the PR validation notes;
|
||||
uncertain hardware/runtime behavior must not be inferred from a clean link.
|
||||
|
||||
## Current-master integration validation
|
||||
|
||||
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
|
||||
|
||||
Release abort/sandbox tests pass for nested callback depth/owner, nonzero outer context, mod-only normal-return imbalance and watchdog recovery. Fresh integrated flat gameplay is reachable after save/load. Historical slot-load recovery after an in-game watchdog is recorded separately.
|
||||
|
||||
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
|
||||
|
||||
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Render-pass refusal diagnostics: source and validation
|
||||
|
||||
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
|
||||
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
|
||||
Exact source commits:
|
||||
|
||||
- [257a88b0](https://github.com/FractalEngineer/psxrecomp/commit/257a88b06d26bb99a9e6066b89f839af41090118)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
|
||||
that exact human author identity. The focused port consolidates the selected
|
||||
source hunks and corrections. Its commit also cites these sources and carries
|
||||
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
|
||||
|
||||
Adapted: Refusal counters, latched GL/resource errors, TCP reporting and synthetic abort/sandbox coverage. The readiness source guard was updated for the preserved refusal chain.
|
||||
|
||||
Deliberately excluded: Stereo gates, OpenXR submission, game replay hooks, GL allocation-success policy changes and bulk historical captures.
|
||||
|
||||
The feature was reconstructed on upstream master
|
||||
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
|
||||
renderer/input policy and build integration. Validation after reconstruction:
|
||||
Abort/sandbox executables, render-pass source guards, GL internal-scale guards and regenerated TCP index.
|
||||
|
||||
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
|
||||
Game-specific integration remains in that repository. Consuming this change
|
||||
requires a deliberate framework pin update in the game repo and a rebuild.
|
||||
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
|
||||
|
||||
Historical alpha acceptance used the older framework checkpoint above. It is
|
||||
separate from current-master branch verification. Fresh game, renderer and
|
||||
headset checks, where applicable, are recorded in the PR validation notes;
|
||||
uncertain hardware/runtime behavior must not be inferred from a clean link.
|
||||
|
||||
## Current-master integration validation
|
||||
|
||||
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
|
||||
|
||||
Abort/sandbox regressions and the registered GL readiness guards pass. The updated guard follows the same refusal chain in ordinary and stereo builds. Integrated real-GL readback tests pass at 1x and 4x; injected driver out-of-memory behavior was not exercised.
|
||||
|
||||
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
|
||||
|
||||
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
|
||||
@@ -0,0 +1,43 @@
|
||||
# TCP guest instruction disassembly: source and validation
|
||||
|
||||
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
|
||||
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
|
||||
Exact source commits:
|
||||
|
||||
- [85c47e10](https://github.com/FractalEngineer/psxrecomp/commit/85c47e10a2fa6e112205e0e953a89ba120c4b139)
|
||||
- [d58db909](https://github.com/FractalEngineer/psxrecomp/commit/d58db9092a57cc692d0c9d936f5003fb4b3e0977)
|
||||
- [c2ed6e55](https://github.com/FractalEngineer/psxrecomp/commit/c2ed6e558f37212c36aedfdfa2c90863883f0aff)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
|
||||
that exact human author identity. The focused port consolidates the selected
|
||||
source hunks and corrections. Its commit also cites these sources and carries
|
||||
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
|
||||
|
||||
Adapted: The C-compatible bridge to the existing MIPS decoder, bounded TCP handler, build linkage and command docs. Formatting was corrected and covered during reconstruction.
|
||||
|
||||
Deliberately excluded: Guest instruction execution/codegen changes, new decoders, game-specific addresses and runtime printf debugging.
|
||||
|
||||
The feature was reconstructed on upstream master
|
||||
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
|
||||
renderer/input policy and build integration. Validation after reconstruction:
|
||||
Disassembly operand/target/coprocessor/buffer regressions, debug-tool ON/OFF server compilation and regenerated TCP index.
|
||||
|
||||
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
|
||||
Game-specific integration remains in that repository. Consuming this change
|
||||
requires a deliberate framework pin update in the game repo and a rebuild.
|
||||
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
|
||||
|
||||
Historical alpha acceptance used the older framework checkpoint above. It is
|
||||
separate from current-master branch verification. Fresh game, renderer and
|
||||
headset checks, where applicable, are recorded in the PR validation notes;
|
||||
uncertain hardware/runtime behavior must not be inferred from a clean link.
|
||||
|
||||
## Current-master integration validation
|
||||
|
||||
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
|
||||
|
||||
Release disassembly tests passed for arithmetic, multiply/divide, shifts, branch/jump targets, immediates, COP0/COP2, unknown words and bounded buffers. Checks execute with NDEBUG enabled. Full runtime builds passed with debug tools enabled and disabled; the generated command index passes.
|
||||
|
||||
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
|
||||
|
||||
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Write-trace producer-PC filters: source and validation
|
||||
|
||||
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
|
||||
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
|
||||
Exact source commits:
|
||||
|
||||
- [734977c9](https://github.com/FractalEngineer/psxrecomp/commit/734977c93e80789e9210a262daa35a88f41c40d4)
|
||||
|
||||
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
|
||||
that exact human author identity. The focused port consolidates the selected
|
||||
source hunks and corrections. Its commit also cites these sources and carries
|
||||
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
|
||||
|
||||
Adapted: Only full-PC low/high query filters and response bounds; recording is unchanged.
|
||||
|
||||
Deliberately excluded: The same source commit's OpenXR hand snapshots and pose math; no game memory-map constants.
|
||||
|
||||
The feature was reconstructed on upstream master
|
||||
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
|
||||
renderer/input policy and build integration. Validation after reconstruction:
|
||||
Six production-handler wrapped-ring regressions, debug-tool ON/OFF compilation and regenerated TCP index.
|
||||
|
||||
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
|
||||
Game-specific integration remains in that repository. Consuming this change
|
||||
requires a deliberate framework pin update in the game repo and a rebuild.
|
||||
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
|
||||
|
||||
Historical alpha acceptance used the older framework checkpoint above. It is
|
||||
separate from current-master branch verification. Fresh game, renderer and
|
||||
headset checks, where applicable, are recorded in the PR validation notes;
|
||||
uncertain hardware/runtime behavior must not be inferred from a clean link.
|
||||
|
||||
## Current-master integration validation
|
||||
|
||||
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
|
||||
|
||||
Six production-handler regressions passed against a wrapped trace ring, covering full PC segments, inclusive/exclusive bounds, filtering before count, combined filters, DMA metadata and response bounds. Full debug-enabled/disabled runtime builds and the generated command index pass.
|
||||
|
||||
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
|
||||
|
||||
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
|
||||
@@ -2672,6 +2672,13 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
if (d > 1.0) d = 1.0;
|
||||
s.scanline_strength = d; s.has_scanline_strength = true;
|
||||
});
|
||||
if (v.contains("fov_scale")) try_get([&]{
|
||||
const auto& n = toml::find(v, "fov_scale");
|
||||
if (n.is_floating()) s.fov_scale = n.as_floating();
|
||||
else if (n.is_integer()) s.fov_scale = static_cast<double>(n.as_integer());
|
||||
else return;
|
||||
s.has_fov_scale = true;
|
||||
});
|
||||
if (v.contains("auto_skip_fmv")) try_get([&]{
|
||||
s.auto_skip_fmv = toml::find<bool>(v, "auto_skip_fmv"); s.has_auto_skip_fmv = true;
|
||||
});
|
||||
@@ -3034,6 +3041,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
f << "scanlines = " << (s.scanlines ? "true" : "false") << "\n";
|
||||
if (s.has_scanline_strength)
|
||||
f << "scanline_strength = " << s.scanline_strength << "\n";
|
||||
if (s.has_fov_scale)
|
||||
f << "fov_scale = " << s.fov_scale << "\n";
|
||||
if (s.has_auto_skip_fmv)
|
||||
f << "auto_skip_fmv = " << (s.auto_skip_fmv ? "true" : "false") << "\n";
|
||||
/* turbo_loads is deliberately NOT written back: it is deprecated and no
|
||||
|
||||
@@ -1371,6 +1371,9 @@ struct UserSettings {
|
||||
// 0..1; the launcher ABI carries it as an integer percent.
|
||||
bool has_scanlines = false; bool scanlines = false;
|
||||
bool has_scanline_strength = false; double scanline_strength = 0.5;
|
||||
// [video] fov_scale: GTE projection-distance multiplier (1.0 = faithful).
|
||||
// Stored raw (int or float TOML); the runtime range-checks and warns.
|
||||
bool has_fov_scale = false; double fov_scale = 1.0;
|
||||
bool has_auto_skip_fmv = false; bool auto_skip_fmv = false; // skip FMVs
|
||||
// [video] turbo_loads: DEPRECATED AND IGNORED — the legacy home of the
|
||||
// generic Turbo loads switch, back when the launcher drew a row for it.
|
||||
|
||||
@@ -187,6 +187,30 @@ static void test_user_settings_round_trip() {
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
/* settings.toml [video] fov_scale: int and float TOML values read, absent stays
|
||||
* absent, and a launcher save round-trips it (range checking is the runtime's). */
|
||||
static void test_user_settings_fov_scale() {
|
||||
fs::path p = write_temp("psxrecomp_fov_settings.toml", "[video]\nfov_scale = 2\n");
|
||||
auto us = PSXRecompV4::load_user_settings(p);
|
||||
check(us.has_fov_scale && us.fov_scale == 2.0, "settings.toml integer fov_scale read");
|
||||
fs::remove(p);
|
||||
p = write_temp("psxrecomp_fov_settings_f.toml", "[video]\nfov_scale = 1.5\n");
|
||||
us = PSXRecompV4::load_user_settings(p);
|
||||
check(us.has_fov_scale && us.fov_scale == 1.5, "settings.toml float fov_scale read");
|
||||
fs::remove(p);
|
||||
p = write_temp("psxrecomp_fov_settings_s.toml", "[video]\nfov_scale = \"wide\"\n");
|
||||
us = PSXRecompV4::load_user_settings(p);
|
||||
check(!us.has_fov_scale, "non-numeric fov_scale leaves has_fov_scale false");
|
||||
fs::remove(p);
|
||||
PSXRecompV4::UserSettings out;
|
||||
out.fov_scale = 1.25; out.has_fov_scale = true;
|
||||
p = fs::temp_directory_path() / "psxrecomp_fov_roundtrip.toml";
|
||||
check(PSXRecompV4::save_user_settings(p, out), "save_user_settings writes fov_scale");
|
||||
auto back = PSXRecompV4::load_user_settings(p);
|
||||
check(back.has_fov_scale && back.fov_scale == 1.25, "fov_scale survives a save/load round trip");
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
/* Internal resolution (Settings -> Display): game.toml shipped default,
|
||||
* settings.toml precedence over the legacy factor, stable-id round trip,
|
||||
* and the legacy supersampling range widened to the runtime's 1..32. */
|
||||
@@ -358,6 +382,7 @@ int main() {
|
||||
test_user_settings_read();
|
||||
test_user_settings_absent_key();
|
||||
test_user_settings_round_trip();
|
||||
test_user_settings_fov_scale();
|
||||
test_texture_window_batching();
|
||||
test_pgxp_title_keys();
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ psxrecomp_add_runtime_target(psx-runtime
|
||||
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(disasm_test tests/test_disasm.cpp src/disasm_shim.cpp
|
||||
../recompiler/src/mips_decoder.cpp)
|
||||
target_include_directories(disasm_test PRIVATE include ../recompiler/include)
|
||||
add_test(NAME disasm_test COMMAND disasm_test)
|
||||
# Netplay: which BIOS a match boots, and its wire helpers.
|
||||
add_executable(netplay_bios_settle_test
|
||||
tests/test_netplay_bios_settle.c
|
||||
@@ -121,6 +125,10 @@ if(BUILD_TESTING)
|
||||
set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture")
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME wtrace_dump_test COMMAND ${CMAKE_COMMAND} -E env
|
||||
"CC=${CMAKE_C_COMPILER}" "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_wtrace_dump.py")
|
||||
set_tests_properties(wtrace_dump_test PROPERTIES SKIP_RETURN_CODE 77)
|
||||
add_test(NAME counted_mips_relocations_test COMMAND "${Python3_EXECUTABLE}"
|
||||
"${PSXRECOMP_ROOT}/tools/tests/test_counted_mips_relocations.py")
|
||||
add_test(NAME interp_mod_entry_guards_test COMMAND "${Python3_EXECUTABLE}"
|
||||
@@ -393,6 +401,23 @@ if(BUILD_TESTING)
|
||||
add_test(NAME sio_dualshock_rumble_test COMMAND sio_dualshock_rumble_test)
|
||||
set_tests_properties(sio_dualshock_rumble_test PROPERTIES DISABLED TRUE)
|
||||
|
||||
add_executable(vr_pose_math_test tests/test_vr_pose_math.c src/vr_pose_math.c)
|
||||
target_include_directories(vr_pose_math_test PRIVATE include)
|
||||
if(NOT MSVC)
|
||||
target_link_libraries(vr_pose_math_test PRIVATE m)
|
||||
endif()
|
||||
add_test(NAME vr_pose_math_test COMMAND vr_pose_math_test)
|
||||
|
||||
add_executable(projection_scale_config_test
|
||||
tests/test_projection_scale_config.cpp src/projection_scale_config.cpp)
|
||||
target_include_directories(projection_scale_config_test PRIVATE
|
||||
include ../recompiler/lib/toml11)
|
||||
add_test(NAME projection_scale_config_test COMMAND projection_scale_config_test)
|
||||
|
||||
add_executable(mod_controller_source_test tests/test_mod_controller_source.c src/mod_controller_source.c)
|
||||
target_include_directories(mod_controller_source_test PRIVATE include)
|
||||
add_test(NAME mod_controller_source_test COMMAND mod_controller_source_test)
|
||||
|
||||
add_executable(gte_register_access_test
|
||||
tests/test_gte_register_access.cpp
|
||||
src/gte.cpp
|
||||
@@ -791,6 +816,31 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME mod_runtime_test COMMAND mod_runtime_test)
|
||||
|
||||
add_executable(mod_resident_test
|
||||
tests/test_mod_resident.cpp
|
||||
src/mod_resident.cpp
|
||||
src/mod_runtime.cpp
|
||||
src/mod_packages.cpp
|
||||
src/mod_media.cpp
|
||||
src/disc_path.cpp
|
||||
src/cue_sheet.cpp
|
||||
src/iso_reader.cpp
|
||||
src/crc32.c
|
||||
src/psx_sha256.c)
|
||||
target_include_directories(mod_resident_test PRIVATE
|
||||
include
|
||||
../recompiler/include
|
||||
../recompiler/lib/toml11)
|
||||
target_link_libraries(mod_resident_test PRIVATE chdr-static)
|
||||
if(WIN32)
|
||||
if(MSVC)
|
||||
target_link_options(mod_resident_test PRIVATE /STACK:16777216)
|
||||
else()
|
||||
target_link_options(mod_resident_test PRIVATE -Wl,--stack,16777216)
|
||||
endif()
|
||||
endif()
|
||||
add_test(NAME mod_resident_test COMMAND mod_resident_test)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(autocompile_publication_test
|
||||
tests/test_autocompile_publication.c
|
||||
@@ -1234,6 +1284,17 @@ if(BUILD_TESTING)
|
||||
add_test(NAME release_stage_cache_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_release_stage_cache.py)
|
||||
# Overlay shards and BIOS modules are compiled for the RUNTIME's
|
||||
# architecture: an x86_64 macOS runtime under Rosetta used to get arm64
|
||||
# shards (arm64-only Xcode clang, no -arch) in its macos-x64 cache and
|
||||
# ran every overlay interpreted. Pins the PSX_OVERLAY_ARCH_ABI handoff
|
||||
# (autocompile.c), -arch on macOS, the refusal when the Python cannot
|
||||
# load the runtime's architecture, and unchanged Windows/Linux commands;
|
||||
# with a real cc it builds and loads each slice this Mac can run.
|
||||
add_test(NAME overlay_compile_arch_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_overlay_compile_arch.py
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
endif()
|
||||
|
||||
# Full card protocol regression: read, write, address echo, per-slot state,
|
||||
|
||||
@@ -229,6 +229,11 @@ extern uint32_t gte_geometry_correction_hits(void);
|
||||
extern void gte_geometry_correction_stats(uint32_t *lookups, uint32_t *hits,
|
||||
uint32_t *miss_unrecorded,
|
||||
uint32_t *miss_ambiguous);
|
||||
/* Same position-cache lookup without changing geometry-correction counters;
|
||||
* reserved for non-rendering probes that inspect a vertex before drawing. */
|
||||
extern int gte_geometry_correction_lookup_probe(uint32_t packed,
|
||||
int32_t *x16,
|
||||
int32_t *y16);
|
||||
|
||||
/* Exact NCLIP audit coverage: precise = all three SXY shadows are coherent and
|
||||
* word-validated; fallback = native integer-only; disagreements counts cases
|
||||
|
||||
@@ -42,6 +42,14 @@ uint32_t dma_get_dicr(void);
|
||||
uint32_t dma_get_dpcr(void);
|
||||
int dma_cdrom_transfer_active(void);
|
||||
int dma_gpu_linked_list_active(void);
|
||||
/* RAM-side effects of a completed CD-ROM (ch3) transfer of host-supplied
|
||||
* sector data, for seamless-loading adapters serving a game's own CdGetSector
|
||||
* or read loop: overlay capture before the first word, per-word RAM stores
|
||||
* under DMA attribution, executable-page invalidation, the CD DMA log (when
|
||||
* lba >= 0) and overlay capture on completion. Channel registers, IRQs and
|
||||
* guest timing are untouched. dest/bytes word aligned; returns 0 if the span
|
||||
* leaves live RAM. */
|
||||
int dma_host_cdrom_write(uint32_t dest, const uint8_t* src, uint32_t bytes, int lba);
|
||||
|
||||
typedef struct DMAChannelDebugState {
|
||||
uint32_t madr;
|
||||
|
||||
@@ -67,6 +67,18 @@ uint32_t gl_renderer_pass_plan(uint32_t period_vblanks,
|
||||
* rect at the same scale. Returns 0 when refused (nothing changed). */
|
||||
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
uint32_t period_vblanks, int reuse_backup);
|
||||
/* Snapshot at the last begin attempt, never reconstructed by a TCP query.
|
||||
* Strings are static reason names; GL enums are raw numeric values. */
|
||||
typedef struct GLRenderPassBeginDiag {
|
||||
const char *reason;
|
||||
const char *resource;
|
||||
uint32_t status, fbo_status, gl_error_before, gl_error;
|
||||
int active, open_gen, generation, valid, promoted;
|
||||
int hr_scale, out_scale, source_path, wide;
|
||||
int requested_w, requested_h, capture_w, capture_h;
|
||||
int generation_x, generation_y, generation_w, generation_h;
|
||||
} GLRenderPassBeginDiag;
|
||||
void gl_renderer_pass_begin_diag(GLRenderPassBeginDiag *out);
|
||||
/* Generations opened from now on are for a frame already on screen (1) or
|
||||
* for the next flip (0, default); render_pass_gen_flip_matches. */
|
||||
void gl_renderer_pass_set_flip_shown(int shown);
|
||||
@@ -90,6 +102,28 @@ uint64_t gl_renderer_pass_backups_reused(void);
|
||||
uint32_t gl_renderer_pass_image_textures(uint64_t *bytes);
|
||||
/* Debug: dump the images of the next `generations` shown frames as PNGs. */
|
||||
void gl_renderer_pass_dump_arm(const char *dir, int generations);
|
||||
/* Stereo shares the VRAM transaction, not the temporal generation/schedule. */
|
||||
uint32_t gl_renderer_stereo_unavailable(void);
|
||||
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse_backup);
|
||||
int gl_renderer_stereo_end(uint32_t eye, int keep);
|
||||
void gl_renderer_stereo_stage_reset(void);
|
||||
int gl_renderer_stereo_publish(uint64_t pair_id, uint64_t guest_cycle,
|
||||
const int32_t eye_view[2][3]);
|
||||
void gl_renderer_stereo_reset(void);
|
||||
int gl_renderer_stereo_set_presentation(uint32_t mode);
|
||||
void gl_renderer_stereo_dump_arm(const char *dir, int pairs);
|
||||
/* Bounded metadata for explicitly requested PNG evidence, queried over TCP. */
|
||||
typedef struct GLRenderStereoCapture {
|
||||
uint64_t pair_id, guest_cycle;
|
||||
uint32_t width, height;
|
||||
int32_t view_offset[2][3];
|
||||
} GLRenderStereoCapture;
|
||||
uint32_t gl_renderer_stereo_capture_records(GLRenderStereoCapture *out, uint32_t capacity);
|
||||
typedef struct GLRenderStereoDiag {
|
||||
uint64_t pair_id, guest_cycle, presents;
|
||||
uint32_t valid, staged_mask, mode, width, height;
|
||||
} GLRenderStereoDiag;
|
||||
void gl_renderer_stereo_diag(GLRenderStereoDiag *out);
|
||||
uint64_t gl_renderer_perf_ticks(void);
|
||||
uint64_t gl_renderer_perf_frequency(void);
|
||||
int gl_renderer_interpolation_owns_cadence(void);
|
||||
@@ -292,6 +326,7 @@ enum {
|
||||
GL_PRES_CPU = 2, /* CPU-readout quad present (24-bit FMV / forced) */
|
||||
GL_PRES_BLANK = 3, /* display-disabled black present */
|
||||
GL_PRES_INTERP = 4, /* host-refresh interpolation sub-present */
|
||||
GL_PRES_STEREO = 5, /* simultaneous left/right side-by-side pair */
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef PSX_GTE_VIEW_H
|
||||
#define PSX_GTE_VIEW_H
|
||||
#include <stdint.h>
|
||||
#include "mod_plugins.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Host enhancement ambient, checkpointed by render transactions. Identity
|
||||
* outside an explicitly scoped render callback; never writes guest TR regs. */
|
||||
void gte_render_view_get(int32_t xyz[3]);
|
||||
void gte_render_view_set(const int32_t xyz[3]);
|
||||
void gte_render_pose_get(PSXModRenderView *view);
|
||||
void gte_render_pose_set(const PSXModRenderView *view);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "mod_plugins.h"
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
/* Main (emulation) thread only; sources are not sampled in headless mode or
|
||||
* netplay. begin_frame() starts a new frame: the first sample() per player
|
||||
* invokes the callback, later ones in the same frame reuse its result.
|
||||
* reset() detaches all sources but leaves a neutral release pending for each
|
||||
* port that had one. sample() returns 1 with *released=1 for that release
|
||||
* frame (source gone; the caller restores the port's real SIO state), 0 when
|
||||
* the port has no source and no release pending. */
|
||||
void mod_controller_source_reset(void);
|
||||
void mod_controller_source_begin_frame(void);
|
||||
int mod_controller_source_sample(uint32_t player, PSXModControllerState *state, int *released);
|
||||
int mod_controller_source_present(uint32_t player);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -9,6 +9,8 @@ extern "C" {
|
||||
typedef void (*PSXModVBlankCallback)(void);
|
||||
typedef void (*PSXModActivationCallback)(void);
|
||||
struct CPUState;
|
||||
/* A callback that runs guest code inside psx_mod_render_pass() can be
|
||||
* abandoned by a watchdog longjmp; see the note at psx_mod_render_pass(). */
|
||||
typedef void (*PSXModFunctionEntryCallback)(struct CPUState* cpu,
|
||||
uint32_t address);
|
||||
/* Return nonzero to finish this opt-in function with the callback's return
|
||||
@@ -89,6 +91,9 @@ int psx_mod_game_started(void);
|
||||
* otherwise capacity must hold the entire file. Active sector mods apply. */
|
||||
int psx_mod_read_disc_file(const char* path, void* buffer, uint32_t capacity,
|
||||
uint32_t* size);
|
||||
/* Effective-disc LBA and byte size of a file (directory records patched by
|
||||
* active mods apply, so a relocated file reports its new extent). */
|
||||
int psx_mod_disc_file_extent(const char* path, uint32_t* lba, uint32_t* size);
|
||||
/* Experimental retained-texture service (currently OpenGL only). IDs are
|
||||
* nonzero, stable game-owned identities, NOT GL names. Banks are immutable
|
||||
* 16-bit PS1 texels/indices with a caller-selected row pitch (width).
|
||||
@@ -126,6 +131,42 @@ void psx_mod_write_word(uint32_t address, uint32_t value);
|
||||
*/
|
||||
void psx_mod_write_code_word(uint32_t address, uint32_t value);
|
||||
|
||||
/*
|
||||
* Services shared by seamless-loading adapters (resident disc data: see
|
||||
* mod_resident.h). Emulation-thread callbacks only.
|
||||
*
|
||||
* Run a guest function to completion from a hook: a0..a3 and $ra are set,
|
||||
* control returns when the guest reaches return_address, and the caller's
|
||||
* GPRs, PC, HI and LO are restored. COP0, GTE and timing deadlines keep the
|
||||
* callee's effects, as they would after an ordinary call. Snapshots wait
|
||||
* until the call returns (the host stack holds the continuation). Returns v0.
|
||||
*/
|
||||
uint32_t psx_mod_call_guest(struct CPUState* cpu, uint32_t function,
|
||||
uint32_t return_address, uint32_t a0, uint32_t a1,
|
||||
uint32_t a2, uint32_t a3);
|
||||
/* Deliver disc sectors into RAM exactly as a completed CD-ROM DMA would
|
||||
* (overlay capture, executable-page invalidation, CD DMA log when lba >= 0).
|
||||
* Word-aligned address and length. Returns 0 when the span leaves RAM. */
|
||||
int psx_mod_dma_write_ram(uint32_t address, const void* data, uint32_t bytes,
|
||||
int lba);
|
||||
/* Store bytes through the CPU store path (any alignment): for data the
|
||||
* original code produces with CPU stores, e.g. a decompressor's output. */
|
||||
int psx_mod_host_write_ram(uint32_t address, const void* data, uint32_t bytes);
|
||||
/* PsyQ SpuWrite by DMA, completed synchronously: transfer address, DMA-write
|
||||
* transfer mode, the words through the SPU's DMA write path (address
|
||||
* advance and sample-IRQ checks), then transfer mode stop when stop_after.
|
||||
* spu_address 8-aligned, guest_source/bytes word aligned, at most 512 KiB;
|
||||
* the transfer address wraps at the end of SPU RAM as on hardware.
|
||||
* Library bookkeeping (transfer callbacks, busy flags) stays the caller's. */
|
||||
int psx_mod_spu_upload(uint32_t spu_address, uint32_t guest_source,
|
||||
uint32_t bytes, int stop_after);
|
||||
/* PsyQ LoadImage completed synchronously: texture-cache flush, GP0 A0h
|
||||
* rectangle copy of w*h 16-bit pixels from guest RAM (provenance attributed
|
||||
* per word), then GP1(04h) DMA direction CPU->GP0 for uploads the library
|
||||
* would DMA (16 words or more). Caller drains earlier GPU work first. */
|
||||
int psx_mod_psyq_load_image(uint32_t x, uint32_t y, uint32_t w, uint32_t h,
|
||||
uint32_t guest_source);
|
||||
|
||||
/*
|
||||
* Allocate opt-in enhancement memory from Expansion 1. Until the first
|
||||
* allocation, the region remains hardware-faithful open bus. The returned
|
||||
@@ -415,7 +456,14 @@ uint32_t psx_mod_render_pass_plan(uint32_t period_vblanks,
|
||||
uint32_t shown_after_vblanks,
|
||||
uint32_t* alpha_q16, uint32_t max);
|
||||
/* Returns 1 when the pass ran and its image was queued, 0 when it was refused
|
||||
* or rolled back (state is restored either way). */
|
||||
* or rolled back (state is restored either way).
|
||||
* A watchdog abort rolls the pass back by longjmp, past every frame between
|
||||
* the watchdog and psx_mod_render_pass(): plugin callbacks, guest functions
|
||||
* and function-entry hooks they called. Runtime nesting state (including the
|
||||
* function-entry context) is restored, but plugin-owned state, held locks and
|
||||
* C++ destructors in those frames are NOT unwound. A callback run inside a
|
||||
* pass must keep nothing that needs cleanup (no locks, no RAII objects, no
|
||||
* partially updated plugin state) across guest code, or tolerate the abort. */
|
||||
int psx_mod_render_pass(struct CPUState* cpu, const PSXModRenderPass* pass,
|
||||
PSXModRenderPassFn fn, void* user);
|
||||
/*
|
||||
@@ -443,6 +491,48 @@ enum {
|
||||
PSX_MOD_RENDER_PASS_BUSY = 6
|
||||
};
|
||||
uint32_t psx_mod_render_pass_status(void);
|
||||
/* Simultaneous stereo capture, independent of temporal interpolation. Each
|
||||
* eye starts from the same guest state; CPU/RAM/devices/VRAM are restored
|
||||
* before the other eye and on failure. Publish only after both succeed.
|
||||
* The caller provides a draw-only callback at a main-thread frame boundary.
|
||||
* period_vblanks (1..8) describes the game's draw cadence for whole-pair cost
|
||||
* shedding. No alpha/time phase is used. Returns 1 for a published pair. */
|
||||
enum { PSX_MOD_EYE_LEFT = 0, PSX_MOD_EYE_RIGHT = 1 };
|
||||
typedef struct PSXModStereoFrame {
|
||||
uint32_t struct_size;
|
||||
uint32_t period_vblanks;
|
||||
uint16_t x, y, w, h;
|
||||
} PSXModStereoFrame;
|
||||
typedef int (*PSXModStereoFn)(struct CPUState*, void*, uint32_t eye);
|
||||
int psx_mod_render_stereo(struct CPUState *cpu, const PSXModStereoFrame *frame,
|
||||
PSXModStereoFn fn, void *user);
|
||||
uint32_t psx_mod_render_stereo_status(void);
|
||||
/* 0 disables stereo output; 1 shows a complete pair side by side. Capture
|
||||
* itself does not enable presentation. Default 0; applies to this session. */
|
||||
int psx_mod_set_stereo_presentation(uint32_t mode);
|
||||
/* Camera-space addition to RT*V+TR before RTPS/RTPT perspective division.
|
||||
* Valid only inside a render callback. Replaces (never accumulates) the host
|
||||
* offset and is restored automatically on completion or watchdog abort.
|
||||
* Units are the game's GTE camera units; IPD/world-scale calibration is game
|
||||
* specific. Zero is faithful. Guest TR registers are not modified. */
|
||||
int psx_mod_render_view_offset(int32_t x, int32_t y, int32_t z);
|
||||
/* Rigid camera transform after guest RT*V+TR, before division. Rotation is
|
||||
* row-major Q12; translation uses camera units. Optional projection supplies
|
||||
* focal lengths and centre deltas from guest OFX/OFY in Q16 pixel units.
|
||||
* Identity rotation with projection=0 preserves the architectural path.
|
||||
* projection=1 REPLACES the guest X/Y projection (guest H, widescreen squash and
|
||||
* [video] fov_scale do not apply); projection_h_ref != 0 scales the focal lengths
|
||||
* by (fov-scaled guest H)/ref. Valid only inside a render callback; restored with it. */
|
||||
typedef struct PSXModRenderView {
|
||||
uint32_t struct_size;
|
||||
int32_t rotation_q12[9], translation[3];
|
||||
uint32_t projection;
|
||||
uint32_t projection_h_ref; /* 0 absolute FOV; otherwise scale focal lengths by guest H/ref */
|
||||
int32_t fx_q16, fy_q16, cx_delta_q16, cy_delta_q16;
|
||||
} PSXModRenderView;
|
||||
int psx_mod_render_view(const PSXModRenderView *view);
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* When the game flips relative to the pass point. PENDING (default, reset at
|
||||
@@ -606,6 +696,25 @@ int psx_mod_set_controller_presentation_policy(
|
||||
uint32_t initial_mode,
|
||||
int config_capable);
|
||||
|
||||
/* Offline controller source owns a player's pad at normal input sampling.
|
||||
* Mod-trusted like any mod code (arbitrary native code; the setter does no
|
||||
* caller check). A declined/invalid sample delivers neutral, not the previous
|
||||
* held input.
|
||||
* Existing TCP overrides take priority; netplay/resim and eye redraws never
|
||||
* invoke the source. The runtime keeps coherent SIO type requests/recording.
|
||||
* Pass NULL to detach. Local keyboard/pad buttons remain merged for menus;
|
||||
* the source owns sticks and type. No source leaves faithful defaults intact.
|
||||
* Source and setter are main (emulation) thread only. Sources are sampled once
|
||||
* per frame, only by the normal offline sampler: not in headless mode, not in
|
||||
* netplay. The source's `analog` is the pad's capability; the final type still
|
||||
* goes through the multitap rule, mod mode override and presentation policy.
|
||||
* A bad struct_size or out-of-range value delivers neutral and logs to stderr. */
|
||||
typedef struct PSXModControllerState {
|
||||
uint32_t struct_size, buttons, lx, ly, rx, ry, analog;
|
||||
} PSXModControllerState;
|
||||
typedef int (*PSXModControllerSource)(PSXModControllerState *state);
|
||||
int psx_mod_set_controller_source(uint32_t player, PSXModControllerSource source);
|
||||
|
||||
/*
|
||||
* Register a C plugin before main() on the compilers supported by the runtime.
|
||||
* The registry itself uses function-local initialization, so constructor order
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
#pragma once
|
||||
/*
|
||||
* Resident disc resources for title-owned seamless-loading adapters.
|
||||
*
|
||||
* A seamless loader serves a game's blocking loads from memory instead of the
|
||||
* emulated drive, with the game's own code still consuming every byte. The
|
||||
* title-independent half lives here: read the listed files from the effective
|
||||
* (mod-patched) disc once, optionally derive data from them with the title's
|
||||
* own decoders, verify everything with SHA-256, and keep a cache that is
|
||||
* reused only by the exact same mod plan. The adapter keeps its loader hooks,
|
||||
* its catalog and its decoders.
|
||||
*
|
||||
* Cache key: SHA-256 over the active mod plan fingerprint, the adapter's
|
||||
* format id and the complete spec (paths, stock sizes/hashes, policy). Every
|
||||
* blob is re-verified when a cache is loaded; a mismatch rebuilds it. Packs
|
||||
* live in LOCALAPPDATA (or XDG_CACHE_HOME / ~/.cache)/<title>/seamless, or in
|
||||
* PSX_RESIDENT_CACHE when set. Only files named <format>-*.pack are pruned.
|
||||
*
|
||||
* Call psx_resident_prepare() from an activation callback (it reads the disc
|
||||
* through psx_mod_read_disc_file). Accessors are emulation-thread only.
|
||||
* TCP: {"cmd":"resident_status"} lists every prepared pack.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
enum {
|
||||
/* Serve mod-effective content. Files that differ from their stock hash
|
||||
* are counted (psx_resident_file_stock() == 0), never replaced. */
|
||||
PSX_RESIDENT_ALLOW_MODIFIED = 0,
|
||||
/* Fail preparation when any file with a stock hash differs: for adapters
|
||||
* whose hooks depend on the original layout. The title then loads
|
||||
* normally. */
|
||||
PSX_RESIDENT_REQUIRE_STOCK = 1
|
||||
};
|
||||
|
||||
typedef struct PSXResidentFile {
|
||||
const char* path; /* ISO 9660 path, e.g. "CD/TOMBA2.IMG" */
|
||||
uint32_t stock_size; /* original byte size; 0 = not catalogued */
|
||||
const char* stock_sha256; /* lowercase hex over the zero-padded sectors of
|
||||
* the original file; NULL = not catalogued */
|
||||
} PSXResidentFile;
|
||||
|
||||
typedef struct PSXResidentSink PSXResidentSink;
|
||||
|
||||
/* Store one derived blob (decoded asset, table, ...) for source file `file`.
|
||||
* `tag` and `meta` are the adapter's own; blobs are kept in emission order and
|
||||
* identical bytes are stored once. Returns 0 on failure (preparation fails). */
|
||||
int psx_resident_emit(PSXResidentSink* sink, uint32_t file, uint32_t tag,
|
||||
const uint32_t meta[4], const void* bytes, uint32_t size);
|
||||
|
||||
/* Called once per file during preparation, in spec order, with the file's
|
||||
* sector-padded effective content. `stock` is 1 when it matched its stock
|
||||
* hash. Return 0 to fail preparation (e.g. a stock decode mismatch). */
|
||||
typedef int (*PSXResidentDeriveFn)(PSXResidentSink* sink, uint32_t file,
|
||||
const uint8_t* data, uint32_t size,
|
||||
int stock, void* user);
|
||||
|
||||
typedef struct PSXResidentSpec {
|
||||
uint32_t struct_size; /* sizeof(PSXResidentSpec) */
|
||||
const char* title; /* cache folder, e.g. "MegaManX6Recomp" */
|
||||
const char* format; /* pack id; change it whenever the derived
|
||||
* data's meaning changes */
|
||||
const PSXResidentFile* files;
|
||||
uint32_t file_count;
|
||||
uint32_t policy; /* PSX_RESIDENT_ALLOW_MODIFIED / _REQUIRE_STOCK */
|
||||
uint32_t max_file_bytes; /* 0 = a full 80-minute CD */
|
||||
PSXResidentDeriveFn derive; /* optional */
|
||||
void* derive_user;
|
||||
uint32_t keep_packs; /* packs of this format kept besides the
|
||||
* active one; 0 = 3 */
|
||||
} PSXResidentSpec;
|
||||
|
||||
typedef struct PSXResidentPack PSXResidentPack;
|
||||
|
||||
/* Load (verified) or build the pack. NULL on failure; the reason is kept for
|
||||
* resident_status and the resident.prepare_failed mod counter is bumped. A
|
||||
* later prepare for the same title and format releases the earlier pack. */
|
||||
const PSXResidentPack* psx_resident_prepare(const PSXResidentSpec* spec);
|
||||
|
||||
uint32_t psx_resident_file_count(const PSXResidentPack* pack);
|
||||
/* Sector-padded effective bytes. `size` receives the file size, `padded` the
|
||||
* padded length (both optional). NULL for an invalid index. */
|
||||
const uint8_t* psx_resident_file(const PSXResidentPack* pack, uint32_t file,
|
||||
uint32_t* size, uint32_t* padded);
|
||||
/* Effective-disc LBA of the file's first sector (relocations by mods apply). */
|
||||
uint32_t psx_resident_file_lba(const PSXResidentPack* pack, uint32_t file);
|
||||
int psx_resident_file_stock(const PSXResidentPack* pack, uint32_t file);
|
||||
uint32_t psx_resident_modified_files(const PSXResidentPack* pack);
|
||||
/* `bytes` starting at effective LBA `lba`, entirely within one file's padded
|
||||
* content; NULL otherwise. `file` (optional) receives the file index. */
|
||||
const uint8_t* psx_resident_find_lba(const PSXResidentPack* pack, uint32_t lba,
|
||||
uint32_t bytes, uint32_t* file);
|
||||
|
||||
uint32_t psx_resident_derived_count(const PSXResidentPack* pack);
|
||||
/* Derived blob `index` in emission order. Optional out-params. */
|
||||
const uint8_t* psx_resident_derived(const PSXResidentPack* pack, uint32_t index,
|
||||
uint32_t* file, uint32_t* tag,
|
||||
uint32_t meta[4], uint32_t* size);
|
||||
|
||||
/* Lowercase hex SHA-256 of the guest bytes in [lo, hi) of each range, hashed
|
||||
* in order, compared with `sha256`. Guards an adapter's code contract. */
|
||||
typedef struct PSXResidentRange { uint32_t lo, hi; } PSXResidentRange;
|
||||
int psx_resident_guest_ranges_match(const PSXResidentRange* ranges,
|
||||
uint32_t count, const char* sha256);
|
||||
|
||||
/* Debug server: JSON array describing every prepared pack. */
|
||||
int psx_resident_status_json(char* out, uint32_t capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -5,6 +5,7 @@
|
||||
#ifdef __cplusplus
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
#include "recomp_launcher.h"
|
||||
#endif
|
||||
@@ -24,6 +25,11 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path = {},
|
||||
bool mod_runtime_clear_for_netplay(std::string* error = nullptr);
|
||||
const std::string& mod_runtime_fingerprint();
|
||||
const std::filesystem::path& mod_runtime_effective_disc_path();
|
||||
/* Read an effective-disc file as whole sectors (true end-of-file tail bytes
|
||||
* included). max_bytes = 0 allows a full CD. Emulation-thread only. */
|
||||
bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_bytes,
|
||||
std::vector<uint8_t>& padded, uint32_t& lba,
|
||||
uint32_t& size, std::string* error = nullptr);
|
||||
|
||||
#if defined(RECOMP_LAUNCHER)
|
||||
const ::RecompLauncherCModProvider* mod_runtime_launcher_provider();
|
||||
@@ -50,6 +56,14 @@ void mod_runtime_on_savestate_loaded(void);
|
||||
* launcher commit and before renderer/window initialization. */
|
||||
void mod_runtime_activate_plugins(void);
|
||||
void mod_runtime_on_vblank(void);
|
||||
/* Host-only context for a nested render transaction. Restore the interrupted
|
||||
* callback's depth/owner after longjmp; never serialize this into guest saves. */
|
||||
typedef struct ModFunctionEntryContext {
|
||||
uint32_t depth;
|
||||
const void *plugin;
|
||||
} ModFunctionEntryContext;
|
||||
void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out);
|
||||
void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in);
|
||||
void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector,
|
||||
uint8_t* bytes, uint32_t size);
|
||||
void mod_runtime_enable_disc_patches(void);
|
||||
|
||||
@@ -45,6 +45,34 @@ static inline int psx_overlay_code_segment_pc(uint32_t pc) {
|
||||
struct OverlayCallbacks;
|
||||
const struct OverlayCallbacks *overlay_loader_callbacks(void);
|
||||
|
||||
/* Canonical cache arch-abi tag (caches are namespaced per backend AND per
|
||||
* target so a Windows-x64 gcc DLL and, later, a same-OS arm64 build for the
|
||||
* same fragment never comingle). compile_overlays.py
|
||||
* computes the IDENTICAL string; keep the two mappings in lockstep
|
||||
* ("<os>-<arch>": win|linux|macos + x64|arm64|x86). It is this build's own
|
||||
* architecture -- the running slice of a universal macOS binary -- and the
|
||||
* runtime exports it as PSX_OVERLAY_ARCH_ABI to every compile it spawns
|
||||
* (autocompile.c) and passes it to bios_module_build.py, so shards and BIOS
|
||||
* modules are compiled for the architecture that loads them, not for the
|
||||
* architecture of the Python or the compiler binary. */
|
||||
#if defined(_WIN32)
|
||||
# define PSX_OVERLAY_ARCH_OS_ "win"
|
||||
#elif defined(__APPLE__)
|
||||
# define PSX_OVERLAY_ARCH_OS_ "macos"
|
||||
#else
|
||||
# define PSX_OVERLAY_ARCH_OS_ "linux"
|
||||
#endif
|
||||
#if defined(__aarch64__) || defined(_M_ARM64)
|
||||
# define PSX_OVERLAY_ARCH_CPU_ "arm64"
|
||||
#elif defined(__x86_64__) || defined(_M_X64)
|
||||
# define PSX_OVERLAY_ARCH_CPU_ "x64"
|
||||
#elif defined(__i386__) || defined(_M_IX86)
|
||||
# define PSX_OVERLAY_ARCH_CPU_ "x86"
|
||||
#else
|
||||
# define PSX_OVERLAY_ARCH_CPU_ "unknown"
|
||||
#endif
|
||||
#define PSX_OVERLAY_ARCH_ABI PSX_OVERLAY_ARCH_OS_ "-" PSX_OVERLAY_ARCH_CPU_
|
||||
|
||||
/* "<os>-<arch>" of this build (PSX_OVERLAY_ARCH_ABI), the cache layout's
|
||||
* arch-abi segment. Also keyed into the BIOS module cache. */
|
||||
const char *overlay_loader_arch_abi(void);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cerrno>
|
||||
#include <cctype>
|
||||
inline bool psx_projection_scale_valid(double v) {
|
||||
return std::isfinite(v) && v > 0.0 && v <= 8.0;
|
||||
}
|
||||
inline bool psx_projection_scale_parse(const char* text, double* out) {
|
||||
if (!text || !out) return false;
|
||||
char* end = nullptr;
|
||||
errno = 0;
|
||||
const double value = std::strtod(text, &end);
|
||||
if (end == text || errno == ERANGE) return false;
|
||||
while (*end && std::isspace(static_cast<unsigned char>(*end))) ++end;
|
||||
if (*end || !psx_projection_scale_valid(value)) return false;
|
||||
*out = value;
|
||||
return true;
|
||||
}
|
||||
/* Preserve the existing milliscale precision without rounding tiny positive
|
||||
* values to a zero denominator (which would silently restore identity). */
|
||||
inline int psx_projection_scale_denominator(double v) {
|
||||
const int quantized = static_cast<int>(v * 1000.0 + 0.5);
|
||||
return quantized < 1 ? 1 : quantized;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
#include <filesystem>
|
||||
|
||||
/* Host-only [video] extension. Malformed or out-of-range values warn on stderr and yield 1.0. */
|
||||
double psx_projection_scale_load_config(const std::filesystem::path& path);
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef PSX_DISASM_H
|
||||
#define PSX_DISASM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Format one MIPS R3000 instruction as text into out (NUL-terminated).
|
||||
* addr is used to resolve branch/jump targets. Returns the number of
|
||||
* characters written (excluding the NUL), or 0 if out/cap are unusable. */
|
||||
int psx_disasm_one(uint32_t word, uint32_t addr, char* out, int cap);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PSX_DISASM_H */
|
||||
@@ -94,6 +94,10 @@ void psx_host_write_word(uint32_t addr, uint32_t val);
|
||||
void psx_host_write_half(uint32_t addr, uint16_t val);
|
||||
void psx_host_write_byte(uint32_t addr, uint8_t val);
|
||||
|
||||
/* Side-effect-free aligned word peek (RAM / scratchpad / BIOS ROM only), for
|
||||
* the debug path. Returns 0 (and leaves *out alone) for anything else. */
|
||||
int psx_peek_word(uint32_t addr, uint32_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdint.h>
|
||||
|
||||
#include "psx_cycle_freeze.h" /* g_psx_render_pass_active, the time freeze */
|
||||
#include "gpu_gl_renderer.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
@@ -25,17 +26,29 @@ enum {
|
||||
};
|
||||
extern uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES];
|
||||
|
||||
typedef struct RenderPassFailure {
|
||||
const char *reason; /* static name, NULL until the first refusal */
|
||||
uint64_t attempt, plan, guest_cycle;
|
||||
uint32_t status, alpha_q16, struct_size;
|
||||
uint16_t x, y, w, h;
|
||||
GLRenderPassBeginDiag gl;
|
||||
} RenderPassFailure;
|
||||
|
||||
typedef struct RenderPassStats {
|
||||
uint64_t plans; /* psx_mod_render_pass_plan calls that planned >= 1 */
|
||||
uint64_t planned; /* phases planned (after shedding) */
|
||||
uint64_t wanted; /* phases wanted before shedding */
|
||||
uint64_t refused; /* plan calls that returned 0 while enabled */
|
||||
uint64_t pass_attempts; /* all psx_mod_render_pass calls */
|
||||
uint64_t argument_refused, status_refused, begin_refused, checkpoint_refused;
|
||||
RenderPassFailure last_failure; /* latched across successes; session reset clears */
|
||||
uint64_t passes; /* passes run to completion and presented */
|
||||
uint64_t aborted; /* passes rolled back by a fault */
|
||||
uint64_t discarded; /* passes whose plugin declined the image */
|
||||
uint64_t watchdog; /* of which: guest-cycle watchdog overruns */
|
||||
uint64_t vram_leaks; /* GPU writes outside the declared rect (dropped) */
|
||||
uint64_t nesting_repairs; /* aborts whose skipped exits the restore undid */
|
||||
char last_abort_detail[192]; /* skipped host exits; latched until session reset */
|
||||
uint64_t verify_checks; /* PSX_RENDER_PASS_VERIFY comparisons */
|
||||
uint64_t verify_mismatch; /* ... that found a difference */
|
||||
uint64_t spans; /* psx_mod_run_guest_span calls that reached stop */
|
||||
@@ -55,6 +68,19 @@ typedef struct RenderPassStats {
|
||||
void render_pass_get_stats(RenderPassStats *out);
|
||||
/* Clears counters and the sticky fault latch (new mod session). */
|
||||
void render_pass_reset_session(void);
|
||||
typedef struct RenderStereoStats {
|
||||
uint64_t attempts, pairs, refused, failed, shed;
|
||||
uint64_t last_pair_id, last_guest_cycle, eye_cycle[2];
|
||||
uint64_t eye_hash[2]; /* measured entry state when VERIFY is enabled */
|
||||
int32_t eye_view[2][3]; /* sampled from the actual projection ambient */
|
||||
double last_pair_ms, avg_pair_ms;
|
||||
int last_eye;
|
||||
const char *last_failure;
|
||||
uint64_t last_failed_attempt, retained_pair_id;
|
||||
int last_failed_eye;
|
||||
const char *last_failed_reason; /* latched across later successes */
|
||||
} RenderStereoStats;
|
||||
void render_stereo_get_stats(RenderStereoStats *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -192,6 +192,15 @@ void render_pass_journal_rollback(RenderPassJournal *j, uint16_t *vram,
|
||||
int vram_w);
|
||||
void render_pass_journal_free(RenderPassJournal *j);
|
||||
|
||||
/* A published stereo pair may stay on screen only while the plugin keeps
|
||||
* submitting. The pair is fresh while the guest clock is within
|
||||
* RENDER_PASS_STEREO_MAX_AGE_VBLANKS vblanks after the pair's capture cycle
|
||||
* (3x the 8-vblank maximum draw cadence). A clock behind the pair (savestate
|
||||
* or rollback load) is stale. */
|
||||
#define RENDER_PASS_STEREO_MAX_AGE_VBLANKS 24u
|
||||
int render_pass_stereo_pair_fresh(uint64_t pair_cycle, uint64_t now_cycle,
|
||||
uint32_t vblank_cycles);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "mod_plugins.h"
|
||||
/* XR right-handed X right/Y up/Z back; PSX X right/Y down/Z forward. */
|
||||
int vr_pose_to_view(const double q[4], const double p[3],
|
||||
const double origin_q[4], const double origin_p[3],
|
||||
const double fov[4], double units_per_meter,
|
||||
int width, int height, PSXModRenderView *out);
|
||||
/* Pose-to-recentered-PSX-camera transform, inverse to the view transform.
|
||||
* Row-major rotation maps PSX local axes; translation uses game camera units.
|
||||
* This does not apply any game's body/world matrix or aiming policy. */
|
||||
int vr_pose_to_transform(const double q[4],const double p[3],
|
||||
const double origin_q[4],const double origin_p[3],
|
||||
double units_per_meter,double rotation[9],double translation[3]);
|
||||
+32
-3
@@ -1,4 +1,4 @@
|
||||
# Shared psxrecomp runtime CMake helpers.
|
||||
# Shared psxrecomp runtime CMake helpers.
|
||||
#
|
||||
# Include this from either the framework runtime build or a sibling game
|
||||
# project. SDL3 is the default; set -DPSX_SDL_BACKEND=SDL2 for the legacy
|
||||
@@ -351,6 +351,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_vram_dirty.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_render.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_controller_source.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/vr_pose_math.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gpu_vk_renderer.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dma_gpu_ll.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dma.c
|
||||
@@ -400,6 +402,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/freeze_dump_policy.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/freeze_heartbeat.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gte.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/projection_scale_config.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/pgxp.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/pgxp_session.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/nd_intro_ot.c
|
||||
@@ -442,6 +445,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_media.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_resident.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_guest_services.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_texture_banks.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_keybinds.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_bios_backend.c
|
||||
@@ -455,6 +460,13 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
# (sljit Tier-2 in-process JIT backend removed 2026-07-15 — was disabled by
|
||||
# default since 2026-06-25; gaps fall to the interpreter, gcc/tcc unaffected.)
|
||||
)
|
||||
# TCP `disasm` command only (debug_server.c); stripped with the rest of the TCP
|
||||
# server when PSX_DEBUG_TOOLS is OFF.
|
||||
if(PSX_DEBUG_TOOLS)
|
||||
list(APPEND PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/recompiler/src/mips_decoder.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/disasm_shim.cpp)
|
||||
endif()
|
||||
|
||||
# Optional delay-sync netplay (recomp-net). Auto-discovers a sibling checkout
|
||||
# (…/recomp-net next to the game repo or next to psxrecomp). Override with
|
||||
@@ -1331,6 +1343,10 @@ function(_psxrt_stage_mod_catalog target preloaded_dir)
|
||||
AND IS_DIRECTORY "${preloaded_dir}")
|
||||
list(APPEND _audit_roots "${preloaded_dir}")
|
||||
endif()
|
||||
get_target_property(_extra_roots ${target} PSXRT_AUDIT_MOD_DIRS)
|
||||
if(_extra_roots)
|
||||
list(APPEND _audit_roots ${_extra_roots})
|
||||
endif()
|
||||
add_test(NAME psx_mod_plugin_audit_${target}
|
||||
COMMAND $<TARGET_FILE:${target}> --audit-mod-plugins ${_audit_roots})
|
||||
endif()
|
||||
@@ -1395,7 +1411,13 @@ function(psxrecomp_add_runtime_target target)
|
||||
# mods/builtin/packages) this target does not ship. They are left out of
|
||||
# <exe-dir>/mods/bundled entirely -- absent, not hidden -- so no packager
|
||||
# can ship them either. An id that is not a builtin fails configure.
|
||||
set(multiValueArgs EXTRAS_SOURCES GAME_GENERATED_FULL_C EXCLUDE_BUILTIN_MODS)
|
||||
#
|
||||
# AUDIT_MOD_DIRS names further authored package trees (manifest.toml files
|
||||
# are found recursively) that declare plugins this executable registers but
|
||||
# no bundled catalog ships, e.g. development-only packages. They are
|
||||
# audited, never staged.
|
||||
set(multiValueArgs EXTRAS_SOURCES GAME_GENERATED_FULL_C EXCLUDE_BUILTIN_MODS
|
||||
AUDIT_MOD_DIRS)
|
||||
cmake_parse_arguments(PSXRT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
|
||||
|
||||
# DEBUG_PORT and WINDOW_TITLE were previously required cmake-time defaults.
|
||||
@@ -2001,7 +2023,14 @@ function(psxrecomp_add_runtime_target target)
|
||||
# letting it stage would have it wipe and re-stage the runtime's catalog
|
||||
# with only the framework half.
|
||||
if(NOT PSXRT_COSIM)
|
||||
_psxrt_stage_mod_catalog("${target}" "${PSXRT_PRELOADED_MODS_DIR}"
|
||||
foreach(_audit_dir IN LISTS PSXRT_AUDIT_MOD_DIRS)
|
||||
if(NOT IS_DIRECTORY "${_audit_dir}")
|
||||
message(FATAL_ERROR
|
||||
"AUDIT_MOD_DIRS for target '${target}' is not a directory: ${_audit_dir}")
|
||||
endif()
|
||||
endforeach()
|
||||
set_property(TARGET ${target} PROPERTY PSXRT_AUDIT_MOD_DIRS "${PSXRT_AUDIT_MOD_DIRS}")
|
||||
_psxrt_stage_mod_catalog("${target}" "${PSXRT_PRELOADED_MODS_DIR}"
|
||||
${PSXRT_EXCLUDE_BUILTIN_MODS})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -982,9 +982,9 @@ void autocompile_configure(const char *cmd, const char *cwd) {
|
||||
}
|
||||
#else
|
||||
s_out_lock_init = 1;
|
||||
/* Pin WRITE cache + READ captures + flavor to the loader's canonical
|
||||
* values in the environment every child inherits: same contract as the
|
||||
* SetEnvironmentVariableA block in the Windows request path (see it for
|
||||
/* Pin WRITE cache + READ captures + flavor + arch-abi to the loader's
|
||||
* canonical values in the environment every child inherits: same contract
|
||||
* as the SetEnvironmentVariableA block in the Windows request path (see it for
|
||||
* why each matters). Done once here, on the emulation thread before any
|
||||
* worker exists, because glibc's setenv is not safe against a concurrent
|
||||
* getenv on another thread. autocompile_set_cache_paths() runs first. */
|
||||
@@ -996,6 +996,10 @@ void autocompile_configure(const char *cmd, const char *cwd) {
|
||||
snprintf(flavor_buf, sizeof flavor_buf, "%d", (int)PSX_OVERLAY_FLAVOR);
|
||||
setenv("PSX_OVERLAY_FLAVOR", flavor_buf, 1);
|
||||
}
|
||||
/* The architecture too (see the Windows block): Xcode's clang is an
|
||||
* arm64-only binary and defaults to arm64, so an x86_64 runtime under
|
||||
* Rosetta used to get arm64 shards it could never load. */
|
||||
setenv("PSX_OVERLAY_ARCH_ABI", PSX_OVERLAY_ARCH_ABI, 1);
|
||||
if (s_cmd[0]) autocompile_report_interpreter();
|
||||
#endif
|
||||
}
|
||||
@@ -1100,6 +1104,12 @@ int autocompile_request(void) {
|
||||
snprintf(flavor_buf, sizeof flavor_buf, "%d", (int)PSX_OVERLAY_FLAVOR);
|
||||
SetEnvironmentVariableA("PSX_OVERLAY_FLAVOR", flavor_buf);
|
||||
}
|
||||
/* And the ARCHITECTURE: shards must be this runtime's "<os>-<arch>"
|
||||
* (overlay_loader.h), whatever the Python or the compiler binary runs as.
|
||||
* On Windows the bundled Python, MinGW gcc and tcc are all x64, so this
|
||||
* names what the tool derives anyway; on macOS it is what makes an x64
|
||||
* runtime under Rosetta get x86_64 shards from an arm64-only clang. */
|
||||
SetEnvironmentVariableA("PSX_OVERLAY_ARCH_ABI", PSX_OVERLAY_ARCH_ABI);
|
||||
|
||||
/* cmd.exe /C resolves the command via PATH and supports the relative
|
||||
* paths in the configured line (cwd = project root). The WHOLE command is
|
||||
|
||||
+224
-9
@@ -27,6 +27,9 @@
|
||||
#include "overlay_backend.h"
|
||||
#include "cpu_state.h"
|
||||
#include "pgxp.h"
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
#include "psx_disasm.h"
|
||||
#endif
|
||||
#include "dma.h"
|
||||
#include "gpu.h"
|
||||
#include "gpu_render.h" /* gr_scale + gr_render_display_hires (screenshot_hires) */
|
||||
@@ -5185,6 +5188,60 @@ static void handle_get_registers(int id, const char *json)
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* disasm: disassemble guest instructions. {"cmd":"disasm","addr":"0x8001121C",
|
||||
* "count":32}. Reads live guest memory, so it works on EXE code and on
|
||||
* RAM-installed/overlay code alike. Uses the recompiler's MIPS decoder.
|
||||
* Reads go through psx_peek_word (main RAM / scratchpad / BIOS ROM only, no
|
||||
* device reads, no read hooks); a word outside those comes back as
|
||||
* {"word":null,"text":"<unreadable>"}. Compiled only with the debug tools,
|
||||
* like the sources it needs (runtime.cmake). */
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
static void handle_disasm(int id, const char *json)
|
||||
{
|
||||
char addr_str[32];
|
||||
if (!json_get_str(json, "addr", addr_str, sizeof(addr_str))) {
|
||||
send_err(id, "missing addr"); return;
|
||||
}
|
||||
const char *hex = addr_str;
|
||||
if (hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X')) hex += 2;
|
||||
char *hex_end = NULL;
|
||||
unsigned long long addr_val = strtoull(hex, &hex_end, 16);
|
||||
if (hex_end == hex || *hex_end != '\0' || addr_val > 0xFFFFFFFFull) {
|
||||
send_err(id, "bad addr"); return;
|
||||
}
|
||||
uint32_t addr = (uint32_t)addr_val & ~3u;
|
||||
int count = json_get_int(json, "count", 16);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 256) count = 256;
|
||||
|
||||
size_t bufsz = 512u + (size_t)count * 160u;
|
||||
char *out = (char *)malloc(bufsz);
|
||||
if (!out) { send_err(id, "oom"); return; }
|
||||
int pos = snprintf(out, bufsz,
|
||||
"{\"id\":%d,\"ok\":true,\"addr\":\"0x%08X\",\"count\":%d,\"lines\":[",
|
||||
id, addr, count);
|
||||
char text[128];
|
||||
for (int i = 0; i < count; i++) {
|
||||
uint32_t a = addr + (uint32_t)i * 4u;
|
||||
uint32_t w = 0;
|
||||
if (!psx_peek_word(a, &w)) {
|
||||
pos += snprintf(out + pos, bufsz - (size_t)pos,
|
||||
"%s{\"addr\":\"0x%08X\",\"word\":null,\"text\":\"<unreadable>\"}",
|
||||
i ? "," : "", a);
|
||||
continue;
|
||||
}
|
||||
text[0] = '\0';
|
||||
(void)psx_disasm_one(w, a, text, (int)sizeof(text));
|
||||
pos += snprintf(out + pos, bufsz - (size_t)pos,
|
||||
"%s{\"addr\":\"0x%08X\",\"word\":\"0x%08X\",\"text\":\"%s\"}",
|
||||
i ? "," : "", a, w, text);
|
||||
}
|
||||
pos += snprintf(out + pos, bufsz - (size_t)pos, "]}");
|
||||
debug_server_send_line(out);
|
||||
free(out);
|
||||
}
|
||||
#endif /* !PSX_NO_DEBUG_TOOLS */
|
||||
|
||||
static void handle_read_ram(int id, const char *json)
|
||||
{
|
||||
char addr_str[32];
|
||||
@@ -6479,10 +6536,10 @@ static void handle_gte_ring_dump(int id, const char *json)
|
||||
{
|
||||
extern unsigned long long gte_rtp_ring_total(void);
|
||||
extern int gte_rtp_ring_dump_json(char *out, int outsz, int max_count,
|
||||
int newest_first, long frame_filter);
|
||||
int newest_first, long frame_filter, int offset, int render_filter);
|
||||
int count = json_get_int(json, "count", 64);
|
||||
if (count < 1) count = 1;
|
||||
if (count > 512) count = 512;
|
||||
if (count > 4096) count = 4096;
|
||||
int newest = json_get_int(json, "newest", 1) != 0;
|
||||
long frame = (long)json_get_int(json, "frame", -1);
|
||||
|
||||
@@ -6490,7 +6547,11 @@ static void handle_gte_ring_dump(int id, const char *json)
|
||||
char *entries = (char *)malloc(BUF_SZ);
|
||||
char *reply = (char *)malloc(BUF_SZ + 256u);
|
||||
if (!entries || !reply) { free(entries); free(reply); send_err(id, "oom"); return; }
|
||||
int n = gte_rtp_ring_dump_json(entries, (int)BUF_SZ, count, newest, frame);
|
||||
int offset = json_get_int(json,"offset",0);
|
||||
if (offset < 0) offset=0;
|
||||
int render_filter = json_get_int(json,"render",-1);
|
||||
if (render_filter != 0 && render_filter != 1) render_filter=-1;
|
||||
int n = gte_rtp_ring_dump_json(entries, (int)BUF_SZ, count, newest, frame, offset, render_filter);
|
||||
snprintf(reply, BUF_SZ + 256u,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"emitted\":%d,\"entries\":[%s]}",
|
||||
id, gte_rtp_ring_total(), n, entries);
|
||||
@@ -7951,6 +8012,33 @@ static void handle_mod_counters(int id, const char *json)
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* Resident disc packs prepared by seamless-loading adapters (mod_resident.h):
|
||||
* {"cmd":"resident_status"}
|
||||
* -> packs: [{title, format, state, error, path, files, modified, derived,
|
||||
* blobs, bytes, ms}] */
|
||||
static void handle_resident_status(int id, const char *json)
|
||||
{
|
||||
extern int psx_resident_status_json(char *out, uint32_t capacity);
|
||||
(void)json;
|
||||
for (uint32_t cap = 4096u; cap <= (1u << 22); cap *= 4u) {
|
||||
char *packs = (char *)malloc(cap);
|
||||
if (!packs) break;
|
||||
if (psx_resident_status_json(packs, cap)) {
|
||||
char *buf = (char *)malloc(cap + 64u);
|
||||
if (buf) {
|
||||
snprintf(buf, cap + 64u, "{\"id\":%d,\"ok\":true,\"packs\":%s}", id, packs);
|
||||
debug_server_send_line(buf);
|
||||
free(buf);
|
||||
}
|
||||
free(packs);
|
||||
if (!buf) send_err(id, "alloc failed");
|
||||
return;
|
||||
}
|
||||
free(packs);
|
||||
}
|
||||
send_err(id, "resident status too large");
|
||||
}
|
||||
|
||||
static void handle_ws_hud_mode(int id, const char *json)
|
||||
{
|
||||
int v = json_get_int(json, "tag_rects", -1);
|
||||
@@ -8136,9 +8224,39 @@ static void handle_render_pass_stats(int id, const char *json)
|
||||
RenderPassStats st;
|
||||
uint64_t gd[10], image_bytes = 0;
|
||||
uint32_t image_textures;
|
||||
char failure_json[2048];
|
||||
char abort_detail_json[sizeof st.last_abort_detail * 6 + 1];
|
||||
DirtyRamSpanFailure sf;
|
||||
render_pass_get_stats(&st);
|
||||
dirty_ram_span_last_failure(&sf);
|
||||
json_escape_string(abort_detail_json, sizeof abort_detail_json,
|
||||
st.last_abort_detail);
|
||||
const RenderPassFailure *f = &st.last_failure;
|
||||
const GLRenderPassBeginDiag *b = &f->gl;
|
||||
if (!f->reason) {
|
||||
strcpy(failure_json, "null");
|
||||
} else {
|
||||
snprintf(failure_json, sizeof failure_json,
|
||||
"{\"reason\":\"%s\",\"attempt\":%llu,\"plan\":%llu,"
|
||||
"\"guest_cycle\":%llu,\"status\":%u,\"alpha_q16\":%u,"
|
||||
"\"struct_size\":%u,\"rect\":{\"x\":%u,\"y\":%u,\"w\":%u,\"h\":%u},"
|
||||
"\"gl\":{\"reason\":\"%s\",\"resource\":\"%s\",\"status\":%u,"
|
||||
"\"active\":%d,\"open_gen\":%d,\"generation\":%d,"
|
||||
"\"valid\":%d,\"promoted\":%d,\"hr_scale\":%d,\"out_scale\":%d,"
|
||||
"\"source_path\":%d,\"wide\":%d,"
|
||||
"\"requested_w\":%d,\"requested_h\":%d,\"capture_w\":%d,\"capture_h\":%d,"
|
||||
"\"generation_rect\":{\"x\":%d,\"y\":%d,\"w\":%d,\"h\":%d},"
|
||||
"\"fbo_status\":%u,\"gl_error_before\":%u,\"gl_error\":%u}}",
|
||||
f->reason, (unsigned long long)f->attempt, (unsigned long long)f->plan,
|
||||
(unsigned long long)f->guest_cycle, f->status, f->alpha_q16,
|
||||
f->struct_size, (unsigned)f->x, (unsigned)f->y, (unsigned)f->w, (unsigned)f->h,
|
||||
b->reason ? b->reason : "", b->resource ? b->resource : "", b->status,
|
||||
b->active, b->open_gen, b->generation, b->valid, b->promoted,
|
||||
b->hr_scale, b->out_scale, b->source_path, b->wide,
|
||||
b->requested_w, b->requested_h, b->capture_w, b->capture_h,
|
||||
b->generation_x, b->generation_y, b->generation_w, b->generation_h,
|
||||
b->fbo_status, b->gl_error_before, b->gl_error);
|
||||
}
|
||||
gl_renderer_pass_diag(gd);
|
||||
image_textures = gl_renderer_pass_image_textures(&image_bytes);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"plans\":%llu,\"planned\":%llu,"
|
||||
@@ -8158,7 +8276,11 @@ static void handle_render_pass_stats(int id, const char *json)
|
||||
"\"cost_us\":%llu,\"cost_rewarms\":%llu,\"frame_images\":%llu,"
|
||||
"\"journaled\":%llu,"
|
||||
"\"image_textures\":%u,\"image_bytes\":%llu,\"status\":%u,"
|
||||
"\"backups_reused\":%llu,\"spans\":%llu,\"span_failures\":%llu,"
|
||||
"\"backups_reused\":%llu,\"pass_attempts\":%llu,"
|
||||
"\"argument_refused\":%llu,\"status_refused\":%llu,"
|
||||
"\"begin_refused\":%llu,\"checkpoint_refused\":%llu,"
|
||||
"\"spans\":%llu,\"span_failures\":%llu,"
|
||||
"\"last_failure\":%s,\"last_abort_detail\":\"%s\","
|
||||
"\"span_fail\":{\"reason\":%u,\"pc\":\"0x%08X\",\"start\":\"0x%08X\","
|
||||
"\"stop\":\"0x%08X\",\"ra\":\"0x%08X\",\"after\":\"0x%08X\","
|
||||
"\"insns\":%llu}}",
|
||||
@@ -8188,7 +8310,11 @@ static void handle_render_pass_stats(int id, const char *json)
|
||||
(unsigned)image_textures, (unsigned long long)image_bytes,
|
||||
(unsigned)psx_mod_render_pass_status(),
|
||||
(unsigned long long)gl_renderer_pass_backups_reused(),
|
||||
(unsigned long long)st.pass_attempts,
|
||||
(unsigned long long)st.argument_refused, (unsigned long long)st.status_refused,
|
||||
(unsigned long long)st.begin_refused, (unsigned long long)st.checkpoint_refused,
|
||||
(unsigned long long)st.spans, (unsigned long long)st.span_failures,
|
||||
failure_json, abort_detail_json,
|
||||
(unsigned)sf.reason, (unsigned)sf.pc, (unsigned)sf.start_pc,
|
||||
(unsigned)sf.stop_pc, (unsigned)sf.ra, (unsigned)sf.after,
|
||||
(unsigned long long)sf.insns);
|
||||
@@ -8205,6 +8331,68 @@ static void handle_render_pass_refuse(int id, const char *json)
|
||||
(unsigned)psx_mod_render_pass_status());
|
||||
}
|
||||
|
||||
static void handle_stereo_stats(int id, const char *json) {
|
||||
RenderStereoStats s;
|
||||
GLRenderStereoDiag g;
|
||||
(void)json;
|
||||
GLRenderStereoCapture records[100];
|
||||
char captures[32768];
|
||||
size_t pos = 0;
|
||||
uint32_t count = gl_renderer_stereo_capture_records(records, 100);
|
||||
captures[pos++] = '[';
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const GLRenderStereoCapture *r = &records[i];
|
||||
int written = snprintf(captures + pos, sizeof captures - pos,
|
||||
"%s{\"pair_id\":%llu,\"guest_cycle\":%llu,\"width\":%u,\"height\":%u,"
|
||||
"\"left_eye\":0,\"right_eye\":1,\"view_offset\":[[%d,%d,%d],[%d,%d,%d]]}",
|
||||
i ? "," : "", (unsigned long long)r->pair_id,
|
||||
(unsigned long long)r->guest_cycle, r->width, r->height,
|
||||
r->view_offset[0][0], r->view_offset[0][1], r->view_offset[0][2],
|
||||
r->view_offset[1][0], r->view_offset[1][1], r->view_offset[1][2]);
|
||||
if (written < 0 || (size_t)written >= sizeof captures - pos - 2) {
|
||||
send_err(id, "capture metadata response overflow"); return;
|
||||
}
|
||||
pos += (size_t)written;
|
||||
}
|
||||
captures[pos++] = ']'; captures[pos] = '\0';
|
||||
render_stereo_get_stats(&s); gl_renderer_stereo_diag(&g);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"attempts\":%llu,\"pairs\":%llu,"
|
||||
"\"refused\":%llu,\"failed\":%llu,\"shed\":%llu,\"status\":%u,"
|
||||
"\"last_pair_id\":%llu,\"last_guest_cycle\":%llu,"
|
||||
"\"eye_cycles\":[%llu,%llu],\"eye_hashes\":[\"%016llx\",\"%016llx\"],"
|
||||
"\"eye_view\":[[%d,%d,%d],[%d,%d,%d]],"
|
||||
"\"last_eye\":%d,\"last_failure\":\"%s\","
|
||||
"\"last_failed_attempt\":%llu,\"last_failed_eye\":%d,"
|
||||
"\"last_failed_reason\":\"%s\",\"retained_pair_id\":%llu,"
|
||||
"\"last_pair_ms\":%.3f,\"avg_pair_ms\":%.3f,"
|
||||
"\"published\":{\"valid\":%u,\"pair_id\":%llu,\"guest_cycle\":%llu,"
|
||||
"\"width\":%u,\"height\":%u,\"staged_mask\":%u,\"mode\":%u,\"presents\":%llu},\"captures\":%s}",
|
||||
id, (unsigned long long)s.attempts, (unsigned long long)s.pairs,
|
||||
(unsigned long long)s.refused, (unsigned long long)s.failed,
|
||||
(unsigned long long)s.shed, psx_mod_render_stereo_status(),
|
||||
(unsigned long long)s.last_pair_id, (unsigned long long)s.last_guest_cycle,
|
||||
(unsigned long long)s.eye_cycle[0], (unsigned long long)s.eye_cycle[1],
|
||||
(unsigned long long)s.eye_hash[0], (unsigned long long)s.eye_hash[1],
|
||||
s.eye_view[0][0], s.eye_view[0][1], s.eye_view[0][2],
|
||||
s.eye_view[1][0], s.eye_view[1][1], s.eye_view[1][2],
|
||||
s.last_eye, s.last_failure ? s.last_failure : "",
|
||||
(unsigned long long)s.last_failed_attempt, s.last_failed_eye,
|
||||
s.last_failed_reason ? s.last_failed_reason : "",
|
||||
(unsigned long long)s.retained_pair_id,
|
||||
s.last_pair_ms, s.avg_pair_ms, g.valid, (unsigned long long)g.pair_id,
|
||||
(unsigned long long)g.guest_cycle, g.width, g.height, g.staged_mask,
|
||||
g.mode, (unsigned long long)g.presents, captures);
|
||||
}
|
||||
/* Bounded PNG evidence capture. Pair metadata stays on the TCP surface. */
|
||||
static void handle_stereo_dump(int id, const char *json) {
|
||||
char dir[400];
|
||||
int count = json_get_int(json, "count", 1);
|
||||
if (!json_get_str(json, "path", dir, sizeof dir)) { send_err(id, "missing path"); return; }
|
||||
if (count < 1 || count > 100) { send_err(id, "count must be 1..100"); return; }
|
||||
gl_renderer_stereo_dump_arm(dir, count);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"count\":%d}", id, count);
|
||||
}
|
||||
|
||||
/* render_pass_dump path=<dir> count=<n>: write the images (the game's own
|
||||
* frame, then each render pass in phase order) of the next n frames that get
|
||||
* render passes, as <dir>/g<frame>_<index>_a<phase q16>.png. */
|
||||
@@ -9832,7 +10020,7 @@ extern uint64_t gl_renderer_pres_total(void);
|
||||
|
||||
static void handle_gl_present_ring(int id, const char *json)
|
||||
{
|
||||
static const char *path_name[5] = { "vram", "wide", "cpu", "blank", "interp" };
|
||||
static const char *path_name[6] = { "vram", "wide", "cpu", "blank", "interp", "stereo" };
|
||||
int n = json_get_int(json, "n", 300);
|
||||
if (n < 1) n = 1;
|
||||
if (n > 4096) n = 4096;
|
||||
@@ -9851,7 +10039,7 @@ static void handle_gl_present_ring(int id, const char *json)
|
||||
pos += snprintf(buf + pos, bufsz - pos,
|
||||
"%s[%llu,%u,\"%s\",%u,[%d,%d,%d,%d],[%d,%d,%d,%d],[%u,%u,%u],%u,[%u,%u,%u,%u]]",
|
||||
first ? "" : ",", (unsigned long long)s, e.frame,
|
||||
e.path < 5 ? path_name[e.path] : "?", e.t_ms,
|
||||
e.path < 6 ? path_name[e.path] : "?", e.t_ms,
|
||||
e.dx, e.dy, e.w, e.h, e.lx, e.ly, e.lw, e.lh,
|
||||
e.px_r, e.px_g, e.px_b, e.glerr,
|
||||
e.src_r, e.src_g, e.src_b, e.src_valid);
|
||||
@@ -10008,7 +10196,7 @@ static void wtrace_fill_entry(WriteTraceEntry *e, uint64_t seq,
|
||||
* instead of the stale last-CPU-store PC (which is meaningless mid-DMA). */
|
||||
if (g_dma_exec_depth > 0) {
|
||||
e->dma_ch = (int8_t)g_dma_cur_ch;
|
||||
if (g_dma_initiator_pc) e->pc = g_dma_initiator_pc;
|
||||
e->pc = g_dma_initiator_pc; /* 0 = unknown; never a stale CPU store PC */
|
||||
} else {
|
||||
e->dma_ch = -1;
|
||||
}
|
||||
@@ -11615,6 +11803,22 @@ static void handle_wtrace_dump(int id, const char *json)
|
||||
if (json_get_str(json, "addr_hi", hi_str, sizeof(hi_str)))
|
||||
filter_hi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
|
||||
|
||||
/* Filter the recorded producer PC, segment-masked like the address filter
|
||||
* (KSEG0/KSEG1/KUSEG alias). DMA entries use the initiator PC stored by
|
||||
* wtrace_fill_entry; an unknown (0) initiator matches only when no PC
|
||||
* bound is given. Post-hoc only: recording, fingerprints and guest
|
||||
* execution are unchanged. pc_lo/pc_hi echo the raw request. */
|
||||
uint32_t pc_lo = 0, pc_hi = 0xFFFFFFFFu;
|
||||
int pc_filtered = 0;
|
||||
/* Default upper bound sits above every masked PC so no entry is excluded. */
|
||||
uint32_t pc_lo_m = 0, pc_hi_m = 0x20000000u;
|
||||
if (json_get_str(json, "pc_lo", lo_str, sizeof(lo_str))) {
|
||||
pc_lo = hex_to_u32(lo_str); pc_lo_m = pc_lo & 0x1FFFFFFFu; pc_filtered = 1;
|
||||
}
|
||||
if (json_get_str(json, "pc_hi", hi_str, sizeof(hi_str))) {
|
||||
pc_hi = hex_to_u32(hi_str); pc_hi_m = pc_hi & 0x1FFFFFFFu; pc_filtered = 1;
|
||||
}
|
||||
|
||||
/* Optional frame-window filter — the "query the ring for the window of
|
||||
* interest" primitive. Lets a caller reach entries in the MIDDLE of a deep,
|
||||
* high-traffic ring (which oldest-N / newest-N paging cannot). -1 = unbounded. */
|
||||
@@ -11638,8 +11842,9 @@ static void handle_wtrace_dump(int id, const char *json)
|
||||
size_t pos = 0;
|
||||
uint32_t emitted = 0;
|
||||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,\"entries\":[",
|
||||
id, (unsigned long long)total, avail);
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,"
|
||||
"\"pc_lo\":\"0x%08X\",\"pc_hi\":\"0x%08X\",\"entries\":[",
|
||||
id, (unsigned long long)total, avail, pc_lo, pc_hi);
|
||||
for (uint32_t i = 0; i < avail && emitted < MAX_OUT && pos < BUF_SZ - 256; i++) {
|
||||
uint32_t idx;
|
||||
if (newest_first) {
|
||||
@@ -11650,6 +11855,10 @@ static void handle_wtrace_dump(int id, const char *json)
|
||||
}
|
||||
WriteTraceEntry *e = &s_wtrace[idx];
|
||||
if (e->addr < filter_lo || e->addr >= filter_hi) continue;
|
||||
if (pc_filtered) {
|
||||
uint32_t epc = e->pc & 0x1FFFFFFFu;
|
||||
if (e->pc == 0 || epc < pc_lo_m || epc >= pc_hi_m) continue;
|
||||
}
|
||||
if (frame_lo >= 0 && (int)e->frame < frame_lo) continue;
|
||||
if (frame_hi >= 0 && (int)e->frame > frame_hi) continue;
|
||||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||||
@@ -14500,6 +14709,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "ws_hud_mode", handle_ws_hud_mode },
|
||||
{ "ws_tag_stats", handle_ws_tag_stats },
|
||||
{ "mod_counters", handle_mod_counters },
|
||||
{ "resident_status", handle_resident_status },
|
||||
{ "capture_mark", handle_capture_mark },
|
||||
{ "present_image_ring_stats", handle_present_image_ring_stats },
|
||||
{ "present_image_ring_get", handle_present_image_ring_get },
|
||||
@@ -14530,6 +14740,8 @@ static const CmdEntry s_commands[] = {
|
||||
{ "gl_ws_ablate", handle_gl_ws_ablate },
|
||||
{ "gl_interp", handle_gl_interp },
|
||||
{ "render_pass_stats", handle_render_pass_stats },
|
||||
{ "stereo_stats", handle_stereo_stats },
|
||||
{ "stereo_dump", handle_stereo_dump },
|
||||
{ "render_pass_dump", handle_render_pass_dump },
|
||||
{ "render_pass_refuse", handle_render_pass_refuse },
|
||||
{ "gl_wide_fast", handle_gl_wide_fast },
|
||||
@@ -14723,6 +14935,9 @@ static const CmdEntry s_commands[] = {
|
||||
{ "c0_history", handle_c0_history },
|
||||
{ "capture_quads", handle_capture_quads },
|
||||
{ "get_quads", handle_get_quads },
|
||||
#ifndef PSX_NO_DEBUG_TOOLS
|
||||
{ "disasm", handle_disasm },
|
||||
#endif
|
||||
{ "gte_state", handle_gte_state },
|
||||
{ "gte_ring_dump", handle_gte_ring_dump },
|
||||
{ "gte_intpl_dump", handle_gte_intpl_dump },
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
#include "psx_disasm.h"
|
||||
#include "mips_decoder.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
/* C-linkage bridge so the C debug server can use the C++ MIPS decoder that the
|
||||
* recompiler already ships (recompiler/src/mips_decoder.cpp). */
|
||||
|
||||
/* GTE data registers (COP2 rd index) — psx-spx. */
|
||||
static const char* const kCop2Data[32] = {
|
||||
"VXY0", "VZ0", "VXY1", "VZ1", "VXY2", "VZ2", "RGB", "OTZ",
|
||||
"IR0", "IR1", "IR2", "IR3", "SXY0", "SXY1", "SXY2", "SXYP",
|
||||
"SZ0", "SZ1", "SZ2", "SZ3", "RGB0", "RGB1", "RGB2", "RES1",
|
||||
"MAC0", "MAC1", "MAC2", "MAC3", "IRGB", "ORGB", "LZCS", "LZCR"
|
||||
};
|
||||
|
||||
/* GTE control registers (COP2 rd index) — psx-spx. */
|
||||
static const char* const kCop2Ctrl[32] = {
|
||||
"RT11RT12", "RT13RT21", "RT22RT23", "RT31RT32", "RT33", "TRX", "TRY", "TRZ",
|
||||
"L11L12", "L13L21", "L22L23", "L31L32", "L33", "RBK", "GBK", "BBK",
|
||||
"LR1LR2", "LR3LG1", "LG2LG3", "LB1LB2", "LB3", "RFC", "GFC", "BFC",
|
||||
"OFX", "OFY", "H", "DQA", "DQB", "ZSF3", "ZSF4", "FLAG"
|
||||
};
|
||||
|
||||
static const char* gte_cmd_name(uint32_t raw) {
|
||||
switch (raw & 0x3Fu) {
|
||||
case 0x01: return "RTPS";
|
||||
case 0x06: return "NCLIP";
|
||||
case 0x0C: return "OP";
|
||||
case 0x10: return "DPCS";
|
||||
case 0x11: return "INTPL";
|
||||
case 0x12: return "MVMVA";
|
||||
case 0x13: return "NCDS";
|
||||
case 0x14: return "CDP";
|
||||
case 0x16: return "NCDT";
|
||||
case 0x1B: return "NCCS";
|
||||
case 0x1C: return "CC";
|
||||
case 0x1E: return "NCS";
|
||||
case 0x20: return "NCT";
|
||||
case 0x28: return "SQR";
|
||||
case 0x29: return "DCPL";
|
||||
case 0x2A: return "DPCT";
|
||||
case 0x2D: return "AVSZ3";
|
||||
case 0x2E: return "AVSZ4";
|
||||
case 0x30: return "RTPT";
|
||||
case 0x3D: return "GPF";
|
||||
case 0x3E: return "GPL";
|
||||
case 0x3F: return "NCCT";
|
||||
default: return "GTE";
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" int psx_disasm_one(uint32_t word, uint32_t addr, char* out, int cap) {
|
||||
if (!out || cap <= 0) return 0;
|
||||
using PSXRecomp::DecodedInstruction;
|
||||
using PSXRecomp::InstrFormat;
|
||||
using PSXRecomp::MipsDecoder;
|
||||
|
||||
DecodedInstruction d = MipsDecoder::decode(word, addr);
|
||||
const char* m = d.mnemonic ? d.mnemonic : "???";
|
||||
auto rn = [](uint8_t r) { return MipsDecoder::register_name(r); };
|
||||
uint8_t cop = d.rd & 0x1F;
|
||||
|
||||
int n;
|
||||
if (d.format == InstrFormat::UNKNOWN) {
|
||||
n = std::snprintf(out, (size_t)cap, ".word 0x%08X", word);
|
||||
} else if (word == 0u) {
|
||||
n = std::snprintf(out, (size_t)cap, "NOP");
|
||||
} else if (std::strcmp(m, "JR") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s", m, rn(d.rs));
|
||||
} else if (std::strcmp(m, "JALR") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s", m, rn(d.rd), rn(d.rs));
|
||||
} else if (std::strcmp(m, "SLL") == 0 || std::strcmp(m, "SRL") == 0 ||
|
||||
std::strcmp(m, "SRA") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, %d", m, rn(d.rd), rn(d.rt),
|
||||
d.shamt);
|
||||
} else if (std::strcmp(m, "MFHI") == 0 || std::strcmp(m, "MFLO") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s", m, rn(d.rd));
|
||||
} else if (std::strcmp(m, "MTHI") == 0 || std::strcmp(m, "MTLO") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s", m, rn(d.rs));
|
||||
} else if (std::strcmp(m, "SYSCALL") == 0 || std::strcmp(m, "BREAK") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%s", m);
|
||||
} else if (std::strcmp(m, "MULT") == 0 || std::strcmp(m, "MULTU") == 0 ||
|
||||
std::strcmp(m, "DIV") == 0 || std::strcmp(m, "DIVU") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s", m, rn(d.rs), rn(d.rt));
|
||||
} else if (std::strcmp(m, "SLLV") == 0 || std::strcmp(m, "SRLV") == 0 ||
|
||||
std::strcmp(m, "SRAV") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, %s", m, rn(d.rd), rn(d.rt), rn(d.rs));
|
||||
} else if (d.format == InstrFormat::COP0) {
|
||||
if (std::strcmp(m, "MFC0") == 0 || std::strcmp(m, "MTC0") == 0)
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, $%u", m, rn(d.rt), (unsigned)cop);
|
||||
else if (std::strcmp(m, "RFE") == 0)
|
||||
n = std::snprintf(out, (size_t)cap, "%s", m);
|
||||
else
|
||||
n = std::snprintf(out, (size_t)cap, ".word 0x%08X", word);
|
||||
} else if (d.format == InstrFormat::COP2) {
|
||||
if (std::strcmp(m, "MFC2") == 0 || std::strcmp(m, "MTC2") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, $%s", m, rn(d.rt),
|
||||
kCop2Data[cop]);
|
||||
} else if (std::strcmp(m, "CFC2") == 0 || std::strcmp(m, "CTC2") == 0) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, $%s", m, rn(d.rt),
|
||||
kCop2Ctrl[cop]);
|
||||
} else if (d.rs & 0x10) { /* GTE command word */
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s 0x%07X", gte_cmd_name(d.raw),
|
||||
d.raw & 0x1FFFFFFu);
|
||||
} else {
|
||||
n = std::snprintf(out, (size_t)cap, ".word 0x%08X", word);
|
||||
}
|
||||
} else if (d.format == InstrFormat::J) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s 0x%08X", m, d.jump_target);
|
||||
} else if (d.is_branch && d.opcode != 0x04 && d.opcode != 0x05) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, 0x%08X", m, rn(d.rs), d.branch_target);
|
||||
} else if (d.is_branch) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, 0x%08X", m, rn(d.rs),
|
||||
rn(d.rt), d.branch_target);
|
||||
} else if (d.opcode == 0x0F) { /* LUI */
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, 0x%04X", m, rn(d.rt),
|
||||
(unsigned)d.uimm16);
|
||||
} else if (d.is_load || d.is_store) {
|
||||
const char* reg = (d.opcode == 0x32 || d.opcode == 0x3A) ? kCop2Data[d.rt & 31] : rn(d.rt);
|
||||
const char* prefix = (d.opcode == 0x32 || d.opcode == 0x3A) ? "$" : "";
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s%s, %d(%s)", m, prefix, reg,
|
||||
(int)d.imm16, rn(d.rs));
|
||||
} else if (d.format == InstrFormat::R || d.format == InstrFormat::SPECIAL) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, %s", m, rn(d.rd), rn(d.rs),
|
||||
rn(d.rt));
|
||||
} else if (d.opcode >= 0x0C && d.opcode <= 0x0E) {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, 0x%04X", m, rn(d.rt), rn(d.rs), (unsigned)d.uimm16);
|
||||
} else {
|
||||
n = std::snprintf(out, (size_t)cap, "%-8s %s, %s, %d", m, rn(d.rt), rn(d.rs),
|
||||
(int)d.imm16);
|
||||
}
|
||||
if (n < 0) { out[0] = '\0'; return 0; }
|
||||
return (n < cap) ? n : (cap - 1);
|
||||
}
|
||||
@@ -516,6 +516,39 @@ static void start_async_cdrom_transfer(void) {
|
||||
}
|
||||
}
|
||||
|
||||
int dma_host_cdrom_write(uint32_t dest, const uint8_t *src, uint32_t bytes, int lba) {
|
||||
if ((dest & 3u) || (bytes & 3u) || (!src && bytes)) return 0;
|
||||
const uint32_t addr = DMA_RAM_ADDR(dest);
|
||||
const uint32_t live = psx_ram_live_bytes();
|
||||
if (addr >= live || bytes > live - addr) return 0;
|
||||
if (!bytes) return 1;
|
||||
/* Same capture window as the real ch3 path: game data, not FMV buffers. */
|
||||
const int capture = addr < 0x1C0000u || addr >= PSX_MAIN_RAM_RETAIL_BYTES;
|
||||
const int saved_ch = g_dma_cur_ch;
|
||||
const uint32_t saved_madr = g_dma_cur_madr, saved_bcr = g_dma_cur_bcr;
|
||||
g_dma_exec_depth++;
|
||||
g_dma_cur_ch = 3;
|
||||
g_dma_cur_bcr = bytes / 4u;
|
||||
if (capture) overlay_capture_before_dma(addr, bytes);
|
||||
for (uint32_t i = 0; i < bytes; i += 4u) {
|
||||
uint32_t word;
|
||||
memcpy(&word, src + i, 4);
|
||||
g_dma_cur_madr = addr + i;
|
||||
psx_write_word(addr + i, word);
|
||||
}
|
||||
dirty_ram_mark_executable_range(addr, bytes);
|
||||
if (capture) {
|
||||
if (lba >= 0) cd_dma_log_push(lba, addr, bytes);
|
||||
extern uint8_t *memory_get_ram_ptr(void);
|
||||
overlay_capture_on_dma(addr, bytes, memory_get_ram_ptr() + addr);
|
||||
}
|
||||
g_dma_cur_ch = saved_ch;
|
||||
g_dma_cur_madr = saved_madr;
|
||||
g_dma_cur_bcr = saved_bcr;
|
||||
g_dma_exec_depth--;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void finish_async_cdrom_transfer(uint32_t final_addr) {
|
||||
finish_cdrom_dma_capture(final_addr, 1);
|
||||
DMAAsyncChannel *a = &cdrom_async;
|
||||
|
||||
+303
-39
@@ -81,6 +81,8 @@
|
||||
#include "mod_texture_banks.h"
|
||||
#include "frame_interpolation.h"
|
||||
#include "render_pass_plan.h"
|
||||
#include "psx_cycles.h"
|
||||
#include "psx_video_timing.h"
|
||||
#include "mod_plugins.h" /* PSX_MOD_RENDER_PASS_* reasons */
|
||||
#include "host_osd.h"
|
||||
#include "psx_savestate_menu.h"
|
||||
@@ -412,6 +414,7 @@ static const HiwTile *hiw_ensure(int x0, int x1);
|
||||
static void hiw_flush_queue(void);
|
||||
static GLuint make_tex(GLenum internal, int w, int h, GLenum fmt, GLenum type);
|
||||
static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb);
|
||||
static GLenum s_last_fbo_status;
|
||||
static void hiw_clear_rect(int x, int y, int w, int h, float r, float g, float b,
|
||||
float a, int stencil);
|
||||
static void hiw_mirror_copy(int sx, int sy, int dx, int dy, int w, int h);
|
||||
@@ -478,6 +481,9 @@ static void pass_note_new_frame(int origin_x, int origin_y, int source_path,
|
||||
int pw, int ph);
|
||||
static void pass_apply_promotion(void);
|
||||
static int pass_gen_present(uint64_t deadline);
|
||||
static int stereo_present(int w, int h);
|
||||
static void stereo_resources_release(void);
|
||||
static void stereo_invalidate(void);
|
||||
static uint64_t s_idle_ticks_accum_fwd(uint64_t add);
|
||||
/* Render-pass VRAM transaction (see "Render passes" below). While a pass is
|
||||
* open, GPU writes are confined to its rect: the scissor is intersected with
|
||||
@@ -4191,6 +4197,7 @@ static int make_fbo(GLuint *out_fbo, GLuint color_tex, GLuint stencil_rb) {
|
||||
p_glFramebufferRenderbuffer(PSXGL_FRAMEBUFFER, PSXGL_DEPTH_STENCIL_ATTACHMENT,
|
||||
PSXGL_RENDERBUFFER, stencil_rb);
|
||||
GLenum st = p_glCheckFramebufferStatus(PSXGL_FRAMEBUFFER);
|
||||
s_last_fbo_status = st;
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
if (st != PSXGL_FRAMEBUFFER_COMPLETE) {
|
||||
fprintf(stdout, "psxrecomp: GL FBO incomplete (0x%X)\n", st);
|
||||
@@ -4905,6 +4912,7 @@ void gl_renderer_sync_cpu(void) {
|
||||
}
|
||||
|
||||
void gl_renderer_invalidate_present(void) {
|
||||
stereo_invalidate();
|
||||
for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull;
|
||||
s_last_present_path = -1;
|
||||
s_force_present_remaining = 8;
|
||||
@@ -5936,6 +5944,24 @@ typedef struct PassGen {
|
||||
} PassGen;
|
||||
static int s_pass_flip_shown = 0;
|
||||
static PassGen s_pgen[2];
|
||||
/* Double-buffer complete pairs: the unpublished set may be overwritten while
|
||||
* the last complete pair remains visible. No temporal phases or generation. */
|
||||
typedef struct StereoPair {
|
||||
GLuint tex[2], fbo[2];
|
||||
int tw[2], th[2];
|
||||
int x, y, w, h;
|
||||
uint32_t mask;
|
||||
uint64_t id, cycle;
|
||||
int32_t view[2][3];
|
||||
} StereoPair;
|
||||
static StereoPair s_stereo_pair[2];
|
||||
static int s_stereo_current, s_stereo_valid, s_stereo_mode;
|
||||
static uint64_t s_stereo_presents;
|
||||
static char s_stereo_dump_dir[400];
|
||||
static int s_stereo_dump_left;
|
||||
enum { STEREO_CAPTURE_MAX = 100 };
|
||||
static GLRenderStereoCapture s_stereo_captures[STEREO_CAPTURE_MAX];
|
||||
static uint32_t s_stereo_capture_count;
|
||||
/* Slot textures are made as slots fill: [0, s_pgen_alloc_n) exist, all at
|
||||
* s_pgen_alloc_w x h. A generation never fills past pass_slot_cap, so two
|
||||
* generations stay inside its budget whatever the internal scale. */
|
||||
@@ -5997,12 +6023,32 @@ static void pass_gens_invalidate(void) {
|
||||
}
|
||||
|
||||
static int s_pass_force_refuse = -1; /* -1: read PSX_RENDER_PASS_REFUSE */
|
||||
static GLRenderPassBeginDiag s_pass_begin_diag;
|
||||
|
||||
void gl_renderer_pass_begin_diag(GLRenderPassBeginDiag *out) {
|
||||
if (out) *out = s_pass_begin_diag;
|
||||
}
|
||||
|
||||
static int pass_begin_refuse(const char *reason) {
|
||||
s_pass_begin_diag.reason = reason;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Keep pre-existing errors separate from this allocation's errors. */
|
||||
static uint32_t pass_gl_errors(void) {
|
||||
GLenum e;
|
||||
uint32_t first = 0;
|
||||
while ((e = glGetError()) != GL_NO_ERROR) {
|
||||
if (!first) first = (uint32_t)e;
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
void gl_renderer_pass_force_refuse(int on) {
|
||||
s_pass_force_refuse = on ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t gl_renderer_pass_unavailable(void) {
|
||||
static void pass_refusal_init(void) {
|
||||
if (s_pass_force_refuse < 0) {
|
||||
const char *e = getenv("PSX_RENDER_PASS_REFUSE");
|
||||
s_pass_force_refuse = (e && e[0] && e[0] != '0') ? 1 : 0;
|
||||
@@ -6010,6 +6056,10 @@ uint32_t gl_renderer_pass_unavailable(void) {
|
||||
fprintf(stderr, "psxrecomp: render passes refused by the backend "
|
||||
"(PSX_RENDER_PASS_REFUSE, debug)\n");
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t gl_renderer_pass_unavailable(void) {
|
||||
pass_refusal_init();
|
||||
if (!s_ctx || !s_raster_ok || !s_interp_enabled || s_interp_suspended ||
|
||||
s_interp_source != 1 || !(s_interp_source_hz > 0.0))
|
||||
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
|
||||
@@ -6133,6 +6183,9 @@ static int pass_make_color_fbo(GLuint *tex, GLuint *rb, GLuint *fbo,
|
||||
int *cur_w, int *cur_h, int w, int h,
|
||||
GLenum internal, GLenum fmt, GLenum type) {
|
||||
if (*fbo && *cur_w == w && *cur_h == h) return 1;
|
||||
s_pass_begin_diag.gl_error_before = pass_gl_errors();
|
||||
s_pass_begin_diag.gl_error = 0;
|
||||
s_pass_begin_diag.fbo_status = 0;
|
||||
if (*fbo) p_glDeleteFramebuffers(1, fbo);
|
||||
if (*tex) glDeleteTextures(1, tex);
|
||||
if (rb && *rb) p_glDeleteRenderbuffers(1, rb);
|
||||
@@ -6145,7 +6198,10 @@ static int pass_make_color_fbo(GLuint *tex, GLuint *rb, GLuint *fbo,
|
||||
p_glRenderbufferStorage(PSXGL_RENDERBUFFER, PSXGL_DEPTH24_STENCIL8, w, h);
|
||||
p_glBindRenderbuffer(PSXGL_RENDERBUFFER, 0);
|
||||
}
|
||||
if (!make_fbo(fbo, *tex, rb ? *rb : 0)) {
|
||||
int complete = make_fbo(fbo, *tex, rb ? *rb : 0);
|
||||
s_pass_begin_diag.fbo_status = (uint32_t)s_last_fbo_status;
|
||||
s_pass_begin_diag.gl_error = pass_gl_errors();
|
||||
if (!complete) {
|
||||
*cur_w = *cur_h = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -6164,7 +6220,10 @@ static void pass_gen_release(int gi) {
|
||||
/* Make sure slots [0, need) of generation gi exist at w x h. A size change
|
||||
* frees the old set first (the slot cap depends on the size only). */
|
||||
static int pass_gen_reserve(int gi, uint32_t need, int w, int h) {
|
||||
if (need > PASS_SLOTS || need > pass_slot_cap(w, h)) return 0;
|
||||
if (need > PASS_SLOTS || need > pass_slot_cap(w, h)) {
|
||||
s_pass_begin_diag.resource = "generation_slot_cap";
|
||||
return 0;
|
||||
}
|
||||
if (s_pgen_alloc_w[gi] != w || s_pgen_alloc_h[gi] != h) {
|
||||
pass_gen_release(gi);
|
||||
s_pgen_alloc_w[gi] = w;
|
||||
@@ -6172,11 +6231,17 @@ static int pass_gen_reserve(int gi, uint32_t need, int w, int h) {
|
||||
}
|
||||
while (s_pgen_alloc_n[gi] < need) {
|
||||
GLuint *t = &s_pgen_tex[gi][s_pgen_alloc_n[gi]];
|
||||
s_pass_begin_diag.gl_error_before = pass_gl_errors();
|
||||
s_pass_begin_diag.resource = "generation_texture";
|
||||
glGenTextures(1, t);
|
||||
if (!*t) return 0;
|
||||
if (!*t) {
|
||||
s_pass_begin_diag.gl_error = pass_gl_errors();
|
||||
return 0;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, *t);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA,
|
||||
GL_UNSIGNED_BYTE, NULL);
|
||||
s_pass_begin_diag.gl_error = pass_gl_errors();
|
||||
s_pgen_alloc_n[gi]++;
|
||||
s_pass_allocs++;
|
||||
}
|
||||
@@ -6266,6 +6331,7 @@ static void pass_free_color_fbo(GLuint *tex, GLuint *rb, GLuint *fbo,
|
||||
/* Context teardown (gl_renderer_shutdown): free the pass images, backups
|
||||
* and journal, and forget their names so a new context makes fresh ones. */
|
||||
static void pass_resources_release(void) {
|
||||
stereo_resources_release();
|
||||
pass_gen_release(0);
|
||||
pass_gen_release(1);
|
||||
pass_gens_invalidate();
|
||||
@@ -6321,13 +6387,34 @@ static void pass_journal_rollback(void) {
|
||||
render_pass_journal_rollback(&s_pj_cpu, s_vram, VRAM_W);
|
||||
}
|
||||
|
||||
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
uint32_t period_vblanks, int reuse_backup) {
|
||||
static int transaction_begin(int x, int y, int w, int h, int open_gen,
|
||||
uint32_t period_vblanks, int reuse_backup, int stereo) {
|
||||
int S = s_hr_scale, gi, wide, tw, th;
|
||||
PassGen *g;
|
||||
if (!gl_renderer_pass_ready() || s_pass_active) return 0;
|
||||
memset(&s_pass_begin_diag, 0, sizeof s_pass_begin_diag);
|
||||
s_pass_begin_diag.status = stereo ? gl_renderer_stereo_unavailable()
|
||||
: gl_renderer_pass_unavailable();
|
||||
s_pass_begin_diag.active = s_pass_active;
|
||||
s_pass_begin_diag.open_gen = open_gen;
|
||||
s_pass_begin_diag.hr_scale = S;
|
||||
s_pass_begin_diag.out_scale = s_out_scale;
|
||||
s_pass_begin_diag.source_path = s_interp_source_path;
|
||||
s_pass_begin_diag.capture_w = s_interp_w;
|
||||
s_pass_begin_diag.capture_h = s_interp_h;
|
||||
gi = 1 - s_pgen_cur;
|
||||
g = &s_pgen[gi];
|
||||
s_pass_begin_diag.generation = gi;
|
||||
s_pass_begin_diag.valid = g->valid;
|
||||
s_pass_begin_diag.promoted = g->promoted;
|
||||
s_pass_begin_diag.generation_x = g->x;
|
||||
s_pass_begin_diag.generation_y = g->y;
|
||||
s_pass_begin_diag.generation_w = g->w;
|
||||
s_pass_begin_diag.generation_h = g->h;
|
||||
if (s_pass_begin_diag.status != PSX_MOD_RENDER_PASS_READY)
|
||||
return pass_begin_refuse("gl_status");
|
||||
if (s_pass_active) return pass_begin_refuse("gl_active");
|
||||
if (x < 0 || y < 0 || w <= 0 || h <= 0 || x + w > VRAM_W || y + h > VRAM_H)
|
||||
return 0;
|
||||
return pass_begin_refuse("rect_range");
|
||||
s_pass_allocs_begin = s_pass_allocs;
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
@@ -6337,11 +6424,19 @@ int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
pass_wide_fbo_for(x) != 0;
|
||||
tw = (wide ? g_wide_w : w) * S;
|
||||
th = h * S;
|
||||
s_pass_begin_diag.wide = wide;
|
||||
s_pass_begin_diag.requested_w = tw;
|
||||
s_pass_begin_diag.requested_h = th;
|
||||
gi = 1 - s_pgen_cur;
|
||||
g = &s_pgen[gi];
|
||||
if (open_gen) {
|
||||
if (tw != s_interp_w || th != s_interp_h) return 0; /* not what is presented */
|
||||
if (!pass_gen_reserve(gi, 1u, tw, th)) return 0;
|
||||
if (stereo) {
|
||||
StereoPair *pair = &s_stereo_pair[1 - s_stereo_current];
|
||||
pair->x = x; pair->y = y; pair->w = w; pair->h = h;
|
||||
} else if (open_gen) {
|
||||
if (tw != s_interp_w || th != s_interp_h)
|
||||
return pass_begin_refuse("capture_size"); /* not what is presented */
|
||||
if (!pass_gen_reserve(gi, 1u, tw, th))
|
||||
return pass_begin_refuse("generation_reserve");
|
||||
memset(g, 0, sizeof *g);
|
||||
g->x = x; g->y = y; g->w = w; g->h = h;
|
||||
g->tex_w = tw; g->tex_h = th;
|
||||
@@ -6354,7 +6449,7 @@ int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
g->valid = 1;
|
||||
} else if (!g->valid || g->promoted || g->x != x || g->y != y ||
|
||||
g->w != w || g->h != h) {
|
||||
return 0;
|
||||
return pass_begin_refuse("generation_state");
|
||||
}
|
||||
|
||||
/* The previous pass of this frame restored exactly this backup and no
|
||||
@@ -6367,22 +6462,26 @@ int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
}
|
||||
s_pb_valid = 0;
|
||||
/* Back up the rect: hr color + stencil, raw mirror, wide band, CPU rows. */
|
||||
s_pass_begin_diag.resource = "backup_hr";
|
||||
if (!pass_make_color_fbo(&s_pb_hr_tex, &s_pb_hr_rb, &s_pb_hr_fbo,
|
||||
&s_pb_hr_w, &s_pb_hr_h, w * S, h * S,
|
||||
GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE) ||
|
||||
!pass_make_color_fbo(&s_pb_raw_tex, NULL, &s_pb_raw_fbo,
|
||||
GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE))
|
||||
return pass_begin_refuse("backup_hr");
|
||||
s_pass_begin_diag.resource = "backup_raw";
|
||||
if (!pass_make_color_fbo(&s_pb_raw_tex, NULL, &s_pb_raw_fbo,
|
||||
&s_pb_raw_w, &s_pb_raw_h, w, h, PSXGL_R16UI,
|
||||
PSXGL_RED_INTEGER, GL_UNSIGNED_SHORT))
|
||||
return 0;
|
||||
return pass_begin_refuse("backup_raw");
|
||||
pass_blit(s_hr_fbo, s_pb_hr_fbo, x * S, y * S, 0, 0, w * S, h * S,
|
||||
GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
pass_blit(s_raw_fbo, s_pb_raw_fbo, x, y, 0, 0, w, h, GL_COLOR_BUFFER_BIT);
|
||||
s_pb_wide_src = g_wide_w > 0 ? pass_wide_fbo_for(x) : 0;
|
||||
if (s_pb_wide_src) {
|
||||
s_pass_begin_diag.resource = "backup_wide";
|
||||
if (!pass_make_color_fbo(&s_pb_wide_tex, &s_pb_wide_rb, &s_pb_wide_fbo,
|
||||
&s_pb_wide_w, &s_pb_wide_h, g_wide_w * S,
|
||||
h * S, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE))
|
||||
return 0;
|
||||
return pass_begin_refuse("backup_wide");
|
||||
pass_blit(s_pb_wide_src, s_pb_wide_fbo, 0, y * S, 0, 0, g_wide_w * S,
|
||||
h * S, GL_COLOR_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
|
||||
}
|
||||
@@ -6390,7 +6489,7 @@ int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
free(s_pb_cpu);
|
||||
s_pb_cpu = (uint16_t *)malloc((size_t)w * (size_t)h * sizeof(uint16_t));
|
||||
s_pb_cpu_cap = s_pb_cpu ? (size_t)w * (size_t)h : 0;
|
||||
if (!s_pb_cpu) return 0;
|
||||
if (!s_pb_cpu) return pass_begin_refuse("backup_cpu");
|
||||
}
|
||||
for (int row = 0; row < h; row++)
|
||||
memcpy(s_pb_cpu + (size_t)row * w, s_vram + (size_t)(y + row) * VRAM_W + x,
|
||||
@@ -6420,30 +6519,10 @@ backed_up:
|
||||
return 1;
|
||||
}
|
||||
|
||||
void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
|
||||
static void transaction_restore(void) {
|
||||
int S = s_hr_scale, gi = 1 - s_pgen_cur;
|
||||
PassGen *g = &s_pgen[gi];
|
||||
(void)gi;
|
||||
if (!s_pass_active) return;
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
if (keep && alpha_q16 && g->valid && !g->promoted && g->n < PASS_SLOTS &&
|
||||
g->n < pass_slot_cap(g->tex_w, g->tex_h) &&
|
||||
pass_gen_reserve(gi, g->n + 1u, g->tex_w, g->tex_h)) {
|
||||
/* Passes arrive in ascending phase; keep the list sorted anyway. */
|
||||
uint32_t slot = g->n, at = g->n;
|
||||
GLuint t;
|
||||
pass_capture_into(s_pgen_tex[gi][slot], g);
|
||||
while (at > 1 && g->phase[at - 1] > alpha_q16) at--;
|
||||
t = s_pgen_tex[gi][slot];
|
||||
for (uint32_t i = slot; i > at; i--) {
|
||||
s_pgen_tex[gi][i] = s_pgen_tex[gi][i - 1];
|
||||
g->phase[i] = g->phase[i - 1];
|
||||
}
|
||||
s_pgen_tex[gi][at] = t;
|
||||
g->phase[at] = alpha_q16;
|
||||
g->n++;
|
||||
}
|
||||
/* Roll the journal, then the rect back. */
|
||||
pass_journal_rollback();
|
||||
pass_blit(s_pb_hr_fbo, s_hr_fbo, 0, 0, s_pass_x * S, s_pass_y * S,
|
||||
@@ -6485,6 +6564,190 @@ void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
|
||||
}
|
||||
}
|
||||
|
||||
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
uint32_t period, int reuse) {
|
||||
return transaction_begin(x, y, w, h, open_gen, period, reuse, 0);
|
||||
}
|
||||
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) {
|
||||
return transaction_begin(x, y, w, h, 0, 0, reuse, 1);
|
||||
}
|
||||
void gl_renderer_pass_end(uint32_t alpha_q16, int keep) {
|
||||
int gi = 1 - s_pgen_cur;
|
||||
PassGen *g = &s_pgen[gi];
|
||||
if (!s_pass_active) return;
|
||||
flush_flat_batch(); flush_tex_batch(); flush_cpu_upload();
|
||||
if (keep && alpha_q16 && g->valid && !g->promoted && g->n < PASS_SLOTS &&
|
||||
g->n < pass_slot_cap(g->tex_w, g->tex_h) &&
|
||||
pass_gen_reserve(gi, g->n + 1u, g->tex_w, g->tex_h)) {
|
||||
uint32_t slot = g->n, at = g->n;
|
||||
GLuint t;
|
||||
pass_capture_into(s_pgen_tex[gi][slot], g);
|
||||
while (at > 1 && g->phase[at - 1] > alpha_q16) at--;
|
||||
t = s_pgen_tex[gi][slot];
|
||||
for (uint32_t i = slot; i > at; i--) {
|
||||
s_pgen_tex[gi][i] = s_pgen_tex[gi][i - 1];
|
||||
g->phase[i] = g->phase[i - 1];
|
||||
}
|
||||
s_pgen_tex[gi][at] = t; g->phase[at] = alpha_q16; g->n++;
|
||||
}
|
||||
transaction_restore();
|
||||
}
|
||||
|
||||
uint32_t gl_renderer_stereo_unavailable(void) {
|
||||
pass_refusal_init();
|
||||
if (!s_ctx || !s_raster_ok || !s_hr_fbo || gpu_display_is_depth24())
|
||||
return PSX_MOD_RENDER_PASS_NO_PRESENTER;
|
||||
if (s_cpu_auth_dual || s_hiw || g_wide_w > 0 || s_pass_force_refuse > 0)
|
||||
return PSX_MOD_RENDER_PASS_BACKEND;
|
||||
return PSX_MOD_RENDER_PASS_READY;
|
||||
}
|
||||
void gl_renderer_stereo_stage_reset(void) {
|
||||
s_stereo_pair[1 - s_stereo_current].mask = 0;
|
||||
}
|
||||
static void stereo_invalidate(void) {
|
||||
s_stereo_valid = 0;
|
||||
s_stereo_pair[0].mask = s_stereo_pair[1].mask = 0;
|
||||
}
|
||||
void gl_renderer_stereo_reset(void) {
|
||||
stereo_invalidate(); s_stereo_mode = 0; s_stereo_dump_left = 0;
|
||||
s_stereo_capture_count = 0;
|
||||
s_stereo_presents = 0;
|
||||
}
|
||||
int gl_renderer_stereo_set_presentation(uint32_t mode) {
|
||||
if (mode > 1u) return 0;
|
||||
s_stereo_mode = (int)mode;
|
||||
return 1;
|
||||
}
|
||||
static void stereo_resources_release(void) {
|
||||
for (int gi = 0; gi < 2; gi++) for (int eye = 0; eye < 2; eye++) {
|
||||
StereoPair *p = &s_stereo_pair[gi];
|
||||
pass_free_color_fbo(&p->tex[eye], NULL, &p->fbo[eye],
|
||||
&p->tw[eye], &p->th[eye]);
|
||||
}
|
||||
stereo_invalidate();
|
||||
}
|
||||
int gl_renderer_stereo_end(uint32_t eye, int keep) {
|
||||
StereoPair *p = &s_stereo_pair[1 - s_stereo_current];
|
||||
PassGen g;
|
||||
int ok = 0, tw = s_pass_w * s_hr_scale, th = s_pass_h * s_hr_scale;
|
||||
if (!s_pass_active) return 0;
|
||||
flush_flat_batch(); flush_tex_batch(); flush_cpu_upload();
|
||||
if (keep && eye < 2u && (double)tw * th * 16.0 <= 256.0 * 1024.0 * 1024.0 &&
|
||||
pass_make_color_fbo(&p->tex[eye], NULL, &p->fbo[eye],
|
||||
&p->tw[eye], &p->th[eye], tw, th,
|
||||
GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE)) {
|
||||
memset(&g, 0, sizeof g);
|
||||
g.x = s_pass_x; g.y = s_pass_y; g.w = s_pass_w; g.h = s_pass_h;
|
||||
g.source_path = GL_PRES_VRAM;
|
||||
(void)pass_gl_errors(); /* distinguish this copy from preceding errors */
|
||||
pass_capture_into(p->tex[eye], &g);
|
||||
ok = pass_gl_errors() == 0;
|
||||
if (ok) p->mask |= 1u << eye;
|
||||
}
|
||||
transaction_restore();
|
||||
return keep ? ok : 1;
|
||||
}
|
||||
void gl_renderer_stereo_diag(GLRenderStereoDiag *out) {
|
||||
StereoPair *p = &s_stereo_pair[s_stereo_current];
|
||||
if (!out) return;
|
||||
memset(out, 0, sizeof *out);
|
||||
out->valid = (uint32_t)s_stereo_valid; out->mode = (uint32_t)s_stereo_mode;
|
||||
out->staged_mask = s_stereo_pair[1 - s_stereo_current].mask;
|
||||
out->pair_id = p->id; out->guest_cycle = p->cycle;
|
||||
out->width = (uint32_t)p->tw[0]; out->height = (uint32_t)p->th[0];
|
||||
out->presents = s_stereo_presents;
|
||||
}
|
||||
void gl_renderer_stereo_dump_arm(const char *dir, int pairs) {
|
||||
if (!dir || !*dir || pairs <= 0) { s_stereo_dump_left = 0; return; }
|
||||
snprintf(s_stereo_dump_dir, sizeof s_stereo_dump_dir, "%s", dir);
|
||||
s_stereo_dump_left = pairs > STEREO_CAPTURE_MAX ? STEREO_CAPTURE_MAX : pairs;
|
||||
s_stereo_capture_count = 0;
|
||||
}
|
||||
uint32_t gl_renderer_stereo_capture_records(GLRenderStereoCapture *out, uint32_t capacity) {
|
||||
uint32_t count = s_stereo_capture_count;
|
||||
if (!out) return 0;
|
||||
if (count > capacity) count = capacity;
|
||||
memcpy(out, s_stereo_captures, count * sizeof *out);
|
||||
return count;
|
||||
}
|
||||
static void stereo_dump(const StereoPair *p) {
|
||||
int w = p->tw[0], h = p->th[0];
|
||||
size_t n = (size_t)w * h * 3u;
|
||||
uint8_t *rgb = (uint8_t *)malloc(n * 2u);
|
||||
uint8_t *sbs = (uint8_t *)malloc(n * 2u);
|
||||
char path[512]; FILE *f;
|
||||
if (!rgb || !sbs) { free(rgb); free(sbs); return; }
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
glBindTexture(GL_TEXTURE_2D, p->tex[eye]);
|
||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, rgb + eye * n);
|
||||
snprintf(path, sizeof path, "%s/p%06llu_%s.png", s_stereo_dump_dir,
|
||||
(unsigned long long)p->id, eye ? "right" : "left");
|
||||
f = fopen(path, "wb");
|
||||
if (f) { (void)png_write_rgb(f, rgb + eye * n, w, h); fclose(f); }
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
for (int row = 0; row < h; row++) for (int eye = 0; eye < 2; eye++)
|
||||
memcpy(sbs + ((size_t)row * w * 2u + eye * w) * 3u,
|
||||
rgb + eye * n + (size_t)row * w * 3u, (size_t)w * 3u);
|
||||
snprintf(path, sizeof path, "%s/p%06llu_sbs.png", s_stereo_dump_dir,
|
||||
(unsigned long long)p->id);
|
||||
f = fopen(path, "wb");
|
||||
if (f) { (void)png_write_rgb(f, sbs, w * 2u, h); fclose(f); }
|
||||
if (s_stereo_capture_count < STEREO_CAPTURE_MAX) {
|
||||
GLRenderStereoCapture *record = &s_stereo_captures[s_stereo_capture_count++];
|
||||
record->pair_id = p->id; record->guest_cycle = p->cycle;
|
||||
record->width = (uint32_t)w; record->height = (uint32_t)h;
|
||||
memcpy(record->view_offset, p->view, sizeof record->view_offset);
|
||||
}
|
||||
free(rgb); free(sbs); s_stereo_dump_left--;
|
||||
}
|
||||
int gl_renderer_stereo_publish(uint64_t id, uint64_t cycle, const int32_t view[2][3]) {
|
||||
StereoPair *p = &s_stereo_pair[1 - s_stereo_current];
|
||||
if (p->mask != 3u || p->tw[0] != p->tw[1] || p->th[0] != p->th[1]) return 0;
|
||||
p->id = id; p->cycle = cycle;
|
||||
memcpy(p->view, view, sizeof p->view);
|
||||
s_stereo_current = 1 - s_stereo_current; s_stereo_valid = 1;
|
||||
if (s_stereo_dump_left > 0) stereo_dump(p);
|
||||
return 1;
|
||||
}
|
||||
static int stereo_present(int w, int h) {
|
||||
StereoPair *p = &s_stereo_pair[s_stereo_current];
|
||||
int ww, wh, lx, ly, lw, lh;
|
||||
/* A pair is shown only while the plugin keeps submitting; menus, FMV at the
|
||||
* same size and state loads stop publishing, and the last pair must not
|
||||
* stick on screen. Stale pairs fall back to the flat present below. */
|
||||
if (s_stereo_valid &&
|
||||
!render_pass_stereo_pair_fresh(p->cycle, psx_cycle_count, g_psx_vblank_cycles))
|
||||
return 0;
|
||||
if (!s_stereo_mode || !s_stereo_valid || s_pass_active ||
|
||||
p->w != w || p->h != h || p->tw[0] != w * s_hr_scale ||
|
||||
gl_renderer_stereo_unavailable() != PSX_MOD_RENDER_PASS_READY) return 0;
|
||||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||||
gl_perf_present_enter();
|
||||
letterbox_rect_aspect(ww, wh, s_aspect_num * 2, s_aspect_den, &lx, &ly, &lw, &lh);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||||
glDisable(GL_SCISSOR_TEST); glViewport(0, 0, ww, wh);
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT);
|
||||
for (int eye = 0; eye < 2; eye++) {
|
||||
int start = eye ? lw / 2 : 0, width = eye ? lw - lw / 2 : lw / 2;
|
||||
present_target_quad(p->tex[eye], p->tw[eye], p->th[eye],
|
||||
0, 0, p->tw[eye], p->th[eye], 0,
|
||||
lx + start, ly, width, lh, 1, 1, 1);
|
||||
}
|
||||
pres_record(GL_PRES_STEREO, p->x, p->y, w, h, lx, ly, lw, lh);
|
||||
hold_capture_drawable();
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
gl_swap_with_osd();
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
s_stereo_presents++; s_probe_swap++;
|
||||
gl_perf_present_exit(0);
|
||||
present_dirty_rect(p->x, p->y, p->x + w - 1, p->y + h - 1, 0);
|
||||
present_force_consumed();
|
||||
s_last_present_path = GL_PRES_STEREO;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Draw two presented images crossfaded (t = weight of b) into the window. */
|
||||
static int interp_present_pair(GLuint a, GLuint b, float t, int blend_mode);
|
||||
|
||||
@@ -7071,6 +7334,7 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
flush_flat_batch();
|
||||
flush_tex_batch();
|
||||
flush_cpu_upload();
|
||||
if (stereo_present(w, h)) return;
|
||||
if (s_force_present_remaining <= 0 &&
|
||||
s_last_present_path == GL_PRES_VRAM &&
|
||||
s_last_dx == disp_x && s_last_dy == disp_y &&
|
||||
|
||||
+147
-22
@@ -1,4 +1,5 @@
|
||||
#include "gte.h"
|
||||
#include "gte_view.h"
|
||||
#include "cpu_state.h"
|
||||
#include "nd_intro_ot.h"
|
||||
#include "pgxp.h"
|
||||
@@ -8,6 +9,7 @@
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
extern "C" int psx_netplay_active(void);
|
||||
|
||||
extern "C" uint32_t psx_read_word(uint32_t addr);
|
||||
extern "C" int gpu_ws_precise_nclip_enabled(void);
|
||||
@@ -360,25 +362,45 @@ extern "C" uint32_t gte_geometry_correction_hits(void) {
|
||||
return s_geom_hits;
|
||||
}
|
||||
|
||||
extern "C" int gte_geometry_correction_lookup(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16) {
|
||||
static int gte_geometry_correction_lookup_impl(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16,
|
||||
int count_stats) {
|
||||
if (s_speculative_depth != 0 || !s_geom_enabled || !s_geom_cache) return 0;
|
||||
int64_t slot = geom_slot(packed);
|
||||
if (slot < 0) return 0;
|
||||
s_geom_lookups++;
|
||||
if (count_stats) s_geom_lookups++;
|
||||
const GeomVertex &entry = s_geom_cache[slot];
|
||||
if (entry.generation != s_geom_generation) { s_geom_miss_unrec++; return 0; }
|
||||
if (entry.generation != s_geom_generation) {
|
||||
if (count_stats) s_geom_miss_unrec++;
|
||||
return 0;
|
||||
}
|
||||
/* Ambiguity gate, as in both references (beetle gFlags == 1): if two
|
||||
* DIFFERENT sub-pixel positions rounded to this same pixel, we cannot tell
|
||||
* which one this packet means, and guessing is what makes a vertex inherit
|
||||
* a neighbour's fraction. Fall back to the faithful integer position. */
|
||||
if (entry.ambiguous) { s_geom_miss_ambig++; return 0; }
|
||||
if (entry.ambiguous) {
|
||||
if (count_stats) s_geom_miss_ambig++;
|
||||
return 0;
|
||||
}
|
||||
if (x16) *x16 = entry.x16;
|
||||
if (y16) *y16 = entry.y16;
|
||||
s_geom_hits++;
|
||||
if (count_stats) s_geom_hits++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" int gte_geometry_correction_lookup(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16) {
|
||||
return gte_geometry_correction_lookup_impl(packed, x16, y16, 1);
|
||||
}
|
||||
|
||||
/* Read-only variant used by instrumentation probes. A probe must not change
|
||||
* the geometry-correction census that describes rendered vertices. */
|
||||
extern "C" int gte_geometry_correction_lookup_probe(uint32_t packed,
|
||||
int32_t *x16,
|
||||
int32_t *y16) {
|
||||
return gte_geometry_correction_lookup_impl(packed, x16, y16, 0);
|
||||
}
|
||||
|
||||
/* Render-pass checkpoint for the gte.cpp side of precision tracking: the
|
||||
* position cache entries a pass writes are journaled and put back, and the
|
||||
* speculative bracket depth is restored (a watchdog abort can leave one open).
|
||||
@@ -594,11 +616,12 @@ extern "C" int gte_dome_probe_dump(uint32_t* funcs, uint32_t* counts, int32_t* m
|
||||
// so types are referred to unqualified.
|
||||
extern "C" { extern uint64_t s_frame_count; }
|
||||
struct GteRtpRec {
|
||||
uint32_t seq, frame, caller_ra, cmd;
|
||||
uint32_t seq, frame, caller_ra, cmd, render_view;
|
||||
int16_t V0[3], V1[3], V2[3];
|
||||
int16_t RT[9];
|
||||
int32_t TR[3];
|
||||
uint16_t H; int32_t OFX, OFY;
|
||||
uint16_t Hs; /* H after fov_scale (== H when identity); SXY0..2 used Hs */
|
||||
int32_t SXY0, SXY1, SXY2;
|
||||
uint16_t SZ1, SZ2, SZ3;
|
||||
uint32_t FLAG;
|
||||
@@ -627,6 +650,28 @@ static uint64_t s_gte_latch_seq = 0;
|
||||
static inline int gte_sxx(int32_t p){ int v=p&0xFFFF; return v>=0x8000? v-0x10000:v; }
|
||||
static inline int gte_syy(int32_t p){ int v=(p>>16)&0xFFFF; return v>=0x8000? v-0x10000:v; }
|
||||
|
||||
static int32_t gte_h_scaled(const GTEState* gte);
|
||||
static int32_t s_render_view[3];
|
||||
static PSXModRenderView s_render_pose;
|
||||
static bool s_render_rotate;
|
||||
extern "C" void gte_render_pose_get(PSXModRenderView *view) { *view = s_render_pose; }
|
||||
extern "C" void gte_render_pose_set(const PSXModRenderView *view) {
|
||||
s_render_pose = *view;
|
||||
s_render_rotate = false;
|
||||
for (int i = 0; i < 9; ++i)
|
||||
if (view->rotation_q12[i] != (i % 4 == 0 ? 4096 : 0)) s_render_rotate = true;
|
||||
/* An all-zero reset record means identity. */
|
||||
if (!view->struct_size) s_render_rotate = false;
|
||||
for (int i = 0; i < 3; ++i) s_render_view[i] = view->translation[i];
|
||||
}
|
||||
|
||||
extern "C" void gte_render_view_get(int32_t xyz[3]) {
|
||||
for (int i = 0; i < 3; i++) xyz[i] = s_render_view[i];
|
||||
}
|
||||
extern "C" void gte_render_view_set(const int32_t xyz[3]) {
|
||||
for (int i = 0; i < 3; i++) s_render_view[i] = s_render_pose.translation[i] = xyz[i];
|
||||
}
|
||||
|
||||
static void gte_rtp_record(const GTEState* g, uint32_t cmd) {
|
||||
if (s_gte_replay_sandbox) return;
|
||||
if (!s_gte_rtp_ring) {
|
||||
@@ -638,10 +683,11 @@ static void gte_rtp_record(const GTEState* g, uint32_t cmd) {
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->caller_ra = s_gte_caller_ra;
|
||||
e->cmd = cmd;
|
||||
e->render_view = s_render_pose.struct_size || s_render_view[0] || s_render_view[1] || s_render_view[2];
|
||||
for (int i = 0; i < 3; i++) { e->V0[i]=g->V0[i]; e->V1[i]=g->V1[i]; e->V2[i]=g->V2[i]; }
|
||||
for (int r = 0; r < 3; r++) for (int c = 0; c < 3; c++) e->RT[r*3+c]=g->RT[r][c];
|
||||
for (int i = 0; i < 3; i++) e->TR[i]=g->TR[i];
|
||||
e->H=g->H; e->OFX=g->OFX; e->OFY=g->OFY;
|
||||
e->H=g->H; e->Hs=(uint16_t)gte_h_scaled(g); e->OFX=g->OFX; e->OFY=g->OFY;
|
||||
e->SXY0=g->SXY[0]; e->SXY1=g->SXY[1]; e->SXY2=g->SXY[2];
|
||||
e->SZ1=g->SZ[1]; e->SZ2=g->SZ[2]; e->SZ3=g->SZ[3];
|
||||
e->FLAG=g->FLAG;
|
||||
@@ -815,12 +861,12 @@ extern "C" int gte_latch_dump_json(char* out, int outsz, int max_count) {
|
||||
if (pos>outsz-700) break;
|
||||
pos+=snprintf(out+pos,outsz-pos,
|
||||
"%s{\"frame\":%u,\"ra\":\"0x%08X\",\"cmd\":\"0x%08X\","
|
||||
"\"RT\":[%d,%d,%d,%d,%d,%d,%d,%d,%d],\"TR\":[%d,%d,%d],\"H\":%u,"
|
||||
"\"RT\":[%d,%d,%d,%d,%d,%d,%d,%d,%d],\"TR\":[%d,%d,%d],\"H\":%u,\"Hs\":%u,"
|
||||
"\"V0\":[%d,%d,%d],\"V1\":[%d,%d,%d],\"V2\":[%d,%d,%d],"
|
||||
"\"S0\":[%d,%d],\"S1\":[%d,%d],\"S2\":[%d,%d],\"SZ\":[%u,%u,%u],\"FLAG\":\"0x%08X\"}",
|
||||
emitted?",":"", e->frame, e->caller_ra, e->cmd,
|
||||
e->RT[0],e->RT[1],e->RT[2],e->RT[3],e->RT[4],e->RT[5],e->RT[6],e->RT[7],e->RT[8],
|
||||
e->TR[0],e->TR[1],e->TR[2],(unsigned)e->H,
|
||||
e->TR[0],e->TR[1],e->TR[2],(unsigned)e->H,(unsigned)e->Hs,
|
||||
e->V0[0],e->V0[1],e->V0[2], e->V1[0],e->V1[1],e->V1[2], e->V2[0],e->V2[1],e->V2[2],
|
||||
gte_sxx(e->SXY0),gte_syy(e->SXY0),gte_sxx(e->SXY1),gte_syy(e->SXY1),
|
||||
gte_sxx(e->SXY2),gte_syy(e->SXY2),(unsigned)e->SZ1,(unsigned)e->SZ2,(unsigned)e->SZ3,e->FLAG);
|
||||
@@ -832,7 +878,7 @@ extern "C" int gte_latch_dump_json(char* out, int outsz, int max_count) {
|
||||
/* Format up to max_count recent entries as a JSON array body (no outer braces).
|
||||
* If frame_filter >= 0, only entries with that frame are emitted. Returns count. */
|
||||
extern "C" int gte_rtp_ring_dump_json(char* out, int outsz, int max_count,
|
||||
int newest_first, long frame_filter) {
|
||||
int newest_first, long frame_filter, int offset, int render_filter) {
|
||||
if (!s_gte_rtp_ring || outsz < 64) { if (out && outsz) out[0]=0; return 0; }
|
||||
uint64_t total = s_gte_rtp_seq;
|
||||
if (total == 0) { out[0]=0; return 0; }
|
||||
@@ -843,6 +889,8 @@ extern "C" int gte_rtp_ring_dump_json(char* out, int outsz, int max_count,
|
||||
if (seq < oldest || seq >= total) break;
|
||||
const GteRtpRec* e = &s_gte_rtp_ring[seq % GTE_RTP_RING_CAP];
|
||||
if (frame_filter >= 0 && e->frame != (uint32_t)frame_filter) continue;
|
||||
if (render_filter >= 0 && (int)(e->render_view != 0) != render_filter) continue;
|
||||
if (offset > 0) { --offset; continue; }
|
||||
auto sxx = [](int32_t p){ int v=p&0xFFFF; return v>=0x8000? v-0x10000:v; };
|
||||
auto syy = [](int32_t p){ int v=(p>>16)&0xFFFF; return v>=0x8000? v-0x10000:v; };
|
||||
if (pos > outsz - 700) break;
|
||||
@@ -850,13 +898,13 @@ extern "C" int gte_rtp_ring_dump_json(char* out, int outsz, int max_count,
|
||||
"%s{\"seq\":%u,\"frame\":%u,\"ra\":\"0x%08X\",\"cmd\":\"0x%08X\","
|
||||
"\"V0\":[%d,%d,%d],\"V1\":[%d,%d,%d],\"V2\":[%d,%d,%d],"
|
||||
"\"RT\":[%d,%d,%d,%d,%d,%d,%d,%d,%d],\"TR\":[%d,%d,%d],"
|
||||
"\"H\":%u,\"OFX\":%d,\"OFY\":%d,"
|
||||
"\"H\":%u,\"Hs\":%u,\"OFX\":%d,\"OFY\":%d,\"render_view\":%u,"
|
||||
"\"S0\":[%d,%d],\"S1\":[%d,%d],\"S2\":[%d,%d],"
|
||||
"\"SZ\":[%u,%u,%u],\"FLAG\":\"0x%08X\"}",
|
||||
emitted?",":"", e->seq, e->frame, e->caller_ra, e->cmd,
|
||||
e->V0[0],e->V0[1],e->V0[2], e->V1[0],e->V1[1],e->V1[2], e->V2[0],e->V2[1],e->V2[2],
|
||||
e->RT[0],e->RT[1],e->RT[2],e->RT[3],e->RT[4],e->RT[5],e->RT[6],e->RT[7],e->RT[8],
|
||||
e->TR[0],e->TR[1],e->TR[2], (unsigned)e->H, e->OFX, e->OFY,
|
||||
e->TR[0],e->TR[1],e->TR[2], (unsigned)e->H, (unsigned)e->Hs, e->OFX, e->OFY, e->render_view,
|
||||
sxx(e->SXY0),syy(e->SXY0), sxx(e->SXY1),syy(e->SXY1), sxx(e->SXY2),syy(e->SXY2),
|
||||
(unsigned)e->SZ1,(unsigned)e->SZ2,(unsigned)e->SZ3, e->FLAG);
|
||||
emitted++;
|
||||
@@ -875,6 +923,45 @@ extern "C" void gte_set_display_aspect(int num, int den) {
|
||||
s_ws_xden = d / a;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// VR: perspective (FOV) scale.
|
||||
//
|
||||
// The PS1 projection distance lives in GTE control register H; psx-spx:
|
||||
// fov = 2*atan(screen_width / (2*H))
|
||||
// gte_h_scaled applies H * num / den. A *FOV multiplier* v maps to
|
||||
// (num, den) = (1000, v*1000), i.e. H becomes H/v, so v > 1 widens the view.
|
||||
// Scaling H here avoids guest-code patches, which cannot change a statically
|
||||
// recompiled constant. Identity (num==den) by default, so the faithful path is
|
||||
// unchanged. Set from PSX_GTE_FOV_SCALE (float) or gte_set_fov_scale(num, den).
|
||||
// ---------------------------------------------------------------------------
|
||||
static int32_t s_h_scale_num = 1;
|
||||
static int32_t s_h_scale_den = 1;
|
||||
|
||||
extern "C" void gte_set_fov_scale(int num, int den) {
|
||||
if (num <= 0 || den <= 0) { s_h_scale_num = s_h_scale_den = 1; return; }
|
||||
s_h_scale_num = num;
|
||||
s_h_scale_den = den;
|
||||
}
|
||||
|
||||
static int32_t gte_h_scaled(const GTEState* gte) {
|
||||
if (s_h_scale_num == s_h_scale_den) return gte->H;
|
||||
if (gte->H == 0) return 0; // a guest H of 0 is a real (degenerate) projection
|
||||
// Both peers must compute identical SXY/MAC/FLAG: the scale is a local
|
||||
// display option, so it is forced to identity for the whole netplay session.
|
||||
if (psx_netplay_active()) {
|
||||
static bool warned = false;
|
||||
if (!warned) {
|
||||
warned = true;
|
||||
fprintf(stderr, "psxrecomp: fov_scale ignored while netplay is active\n");
|
||||
}
|
||||
return gte->H;
|
||||
}
|
||||
int64_t h = (int64_t)gte->H * s_h_scale_num / s_h_scale_den;
|
||||
if (h < 1) h = 1;
|
||||
if (h > 0xFFFF) h = 0xFFFF;
|
||||
return (int32_t)h;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RTPS — Perspective Transformation (internal, operates on given vertex V)
|
||||
//
|
||||
@@ -890,7 +977,7 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
|
||||
// Step 1: Matrix multiplication + translation
|
||||
auto dot = [&](int row, int mac_num) {
|
||||
int64_t acc = (int64_t)gte->TR[row] * 4096 +
|
||||
int64_t acc = ((int64_t)gte->TR[row] + (s_render_rotate ? 0 : s_render_view[row])) * 4096 +
|
||||
(int64_t)gte->RT[row][0] * V[0];
|
||||
acc = gte_mac44_stage(gte, acc, mac_num);
|
||||
acc += (int64_t)gte->RT[row][1] * V[1];
|
||||
@@ -900,6 +987,17 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
int64_t mac1 = dot(0, 1);
|
||||
int64_t mac2 = dot(1, 2);
|
||||
int64_t mac3 = dot(2, 3);
|
||||
if (s_render_rotate) {
|
||||
const int64_t v[3] = {mac1, mac2, mac3};
|
||||
int64_t result[3];
|
||||
for (int row = 0; row < 3; ++row) {
|
||||
int64_t sum = 0;
|
||||
for (int col = 0; col < 3; ++col)
|
||||
sum += v[col] * s_render_pose.rotation_q12[row * 3 + col];
|
||||
result[row] = (sum >> 12) + (int64_t)s_render_view[row] * 4096;
|
||||
}
|
||||
mac1 = result[0]; mac2 = result[1]; mac3 = result[2];
|
||||
}
|
||||
|
||||
// MAC1..3 overflow flags observe the unshifted 44-bit accumulator.
|
||||
gte->check_mac_overflow(mac1, 1);
|
||||
@@ -926,7 +1024,7 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
gte->push_sz(static_cast<int32_t>(mac3 >> 12));
|
||||
|
||||
// Step 3: Perspective division
|
||||
int32_t h_div_sz = gte_divide(gte->H, gte->SZ[3], gte->FLAG);
|
||||
int32_t h_div_sz = gte_divide(gte_h_scaled(gte), gte->SZ[3], gte->FLAG);
|
||||
|
||||
// Step 4: Project to screen coordinates. Squash X only when configured AND
|
||||
// this frame is being stretched — never on a 4:3-presented frame (FMV /
|
||||
@@ -979,6 +1077,32 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
dome_probe_note(gte->SZ[3]); /* locate the dome draw fn (far-vertex tally) */
|
||||
int64_t sx16 = gte->OFX + xterm;
|
||||
int64_t sy16 = gte->OFY + (int64_t)gte->IR2 * h_div_sz;
|
||||
/* Render-view projection override (psx_mod_render_view, projection=1).
|
||||
* Only reachable inside a render-pass sandbox (set/restored by render_pass.c,
|
||||
* rolled back with the GTE state), so no guest-visible state outside the
|
||||
* pass sees it. It is an explicit per-eye projection and REPLACES the guest
|
||||
* X/Y projection above entirely: the [video] fov_scale scaled-H, the
|
||||
* widescreen/dome squash and guest H do not feed it, except through
|
||||
* projection_h_ref, which deliberately multiplies the focal lengths by
|
||||
* (fov-scaled guest H) / projection_h_ref so a plugin can follow the game's
|
||||
* own per-draw H changes (and fov_scale) when it asks to. The guest-formula
|
||||
* PGXP shadows below (precise projection, projection tracking) describe the
|
||||
* guest projection and are skipped under the override. SZ, MAC0/IR0 depth
|
||||
* cue and the divide flags still come from the guest division above. */
|
||||
const bool proj_override = s_render_pose.projection && gte->SZ[3];
|
||||
if (proj_override) {
|
||||
sx16 = (int64_t)gte->OFX + s_render_pose.cx_delta_q16 +
|
||||
(int64_t)gte->IR1 * s_render_pose.fx_q16 / gte->SZ[3];
|
||||
sy16 = (int64_t)gte->OFY + s_render_pose.cy_delta_q16 +
|
||||
(int64_t)gte->IR2 * s_render_pose.fy_q16 / gte->SZ[3];
|
||||
if (s_render_pose.projection_h_ref) {
|
||||
const int64_t h = gte_h_scaled(gte), ref = s_render_pose.projection_h_ref;
|
||||
sx16 = (int64_t)gte->OFX + s_render_pose.cx_delta_q16 +
|
||||
((int64_t)gte->IR1 * s_render_pose.fx_q16 / gte->SZ[3]) * h / ref;
|
||||
sy16 = (int64_t)gte->OFY + s_render_pose.cy_delta_q16 +
|
||||
((int64_t)gte->IR2 * s_render_pose.fy_q16 / gte->SZ[3]) * h / ref;
|
||||
}
|
||||
}
|
||||
/* RTPS/RTPT reuse MAC0's overflow flags for each projected X/Y
|
||||
* accumulator before MAC0 is replaced by the depth-cue result. */
|
||||
gte->check_mac0_overflow(sx16);
|
||||
@@ -1000,10 +1124,10 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
* to native. Shadow only; the guest SXY, MAC and FLAG above are
|
||||
* already final. Skipped while the hooks record nothing (speculative
|
||||
* passes), where the shadow would be dropped anyway. */
|
||||
if (pgxp_preserve_projection() && pgxp_active()) {
|
||||
if (!proj_override && pgxp_preserve_projection() && pgxp_active()) {
|
||||
int32_t ex16, ey16;
|
||||
if (pgxp_project_precise(mac1, mac2, mac3, shift, gte->IR1, gte->IR2,
|
||||
gte->SZ[3], gte->H, gte->OFX, gte->OFY,
|
||||
gte->SZ[3], gte_h_scaled(gte), gte->OFX, gte->OFY,
|
||||
x_num, x_den, &ex16, &ey16) &&
|
||||
pgxp_ppp_accept(ex16, ey16, (uint32_t)gte->SXY[2])) {
|
||||
px16 = ex16;
|
||||
@@ -1011,17 +1135,18 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
}
|
||||
}
|
||||
pgxp_gte_push_sxy(px16, py16, gte->SZ[3], (uint32_t)gte->SXY[2]);
|
||||
if (pgxp_projection_tracking() && shift == 12 && !lm) {
|
||||
if (!proj_override && pgxp_projection_tracking() && shift == 12 && !lm) {
|
||||
const double z = (double)(mac3 >> 12);
|
||||
double hx = (double)gte->MAC1 * gte->H;
|
||||
const double h_proj = (double)gte_h_scaled(gte);
|
||||
double hx = (double)gte->MAC1 * h_proj;
|
||||
if (do_squash) hx = hx * s_ws_xnum / s_ws_xden;
|
||||
else if (s_ws_dome_on && s_ws_dome_num != s_ws_dome_den &&
|
||||
!gpu_ws_present_native_43() && gte->SZ[3] >= s_ws_far_threshold)
|
||||
hx = hx * s_ws_dome_num / s_ws_dome_den;
|
||||
PGXPProjection projection = {
|
||||
(float)(gte->OFX / 65536.0 * z + hx),
|
||||
(float)(gte->OFY / 65536.0 * z + (double)gte->MAC2 * gte->H),
|
||||
(float)z, (float)gte->H / 2.0f};
|
||||
(float)(gte->OFY / 65536.0 * z + (double)gte->MAC2 * h_proj),
|
||||
(float)z, (float)h_proj / 2.0f};
|
||||
pgxp_gte_set_projection(&projection);
|
||||
}
|
||||
}
|
||||
@@ -1234,7 +1359,7 @@ void gte_nclip(GTEState* gte, uint32_t instr) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int x = exact_x[i] >> 16;
|
||||
checked &= (exact_y[i] >> 16) == raw_y[i] && x > -4096 && x < 4096;
|
||||
checked &= gte->SZ[i + 1] >= gte->H / 2 && gte->SZ[i + 1] != 0;
|
||||
checked &= gte->SZ[i + 1] >= gte_h_scaled(gte) / 2 && gte->SZ[i + 1] != 0;
|
||||
if (raw_x[i] == 1023 && x > 1023) saturated = true;
|
||||
else if (raw_x[i] == -1024 && x < -1024) saturated = true;
|
||||
else checked &= x == raw_x[i];
|
||||
|
||||
+114
-15
@@ -6,6 +6,9 @@
|
||||
*/
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "projection_scale.hpp"
|
||||
#include "projection_scale_config.hpp"
|
||||
#include "mod_controller_source.h"
|
||||
#include "window_size.h" /* default game-window size */
|
||||
#include "internal_resolution.h" /* Settings -> Display -> Internal resolution */
|
||||
#include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */
|
||||
@@ -1447,6 +1450,9 @@ extern "C" void debug_get_fmv_config(int *auto_skip, uint32_t *total_table,
|
||||
static int g_video_depth24_trailing_margin = 8;
|
||||
static int g_video_aspect_num = 4;
|
||||
static int g_video_aspect_den = 3;
|
||||
/* [video] fov_scale — VR perspective multiplier on the GTE projection distance
|
||||
* H (1.0 = faithful). env PSX_GTE_FOV_SCALE overrides it. */
|
||||
static double g_fov_scale = 1.0;
|
||||
/* Resize-driven widescreen. The user's fixed aspect is still used to shape the
|
||||
* initial window; after the game window exists these values follow its live
|
||||
* aspect, clamped to 4:3..the widest mode offered by the title. */
|
||||
@@ -1917,6 +1923,8 @@ static void netplay_local_viewport_projection_aspect(
|
||||
static int g_ws_projection_num = 4;
|
||||
static int g_ws_projection_den = 3;
|
||||
static int g_ws_projection_mode = -1;
|
||||
extern "C" void gte_set_fov_scale(int num, int den);
|
||||
|
||||
static void refresh_widescreen_projection() {
|
||||
if (g_ws_engaged && !fntrace_is_game_started()) {
|
||||
g_ws_engaged = false;
|
||||
@@ -1924,6 +1932,12 @@ static void refresh_widescreen_projection() {
|
||||
gte_set_display_aspect(4, 3);
|
||||
gpu_ws_configure(4, 3, g_ws_anchor_addr, 0, 0);
|
||||
}
|
||||
/* VR FOV: scale the GTE projection distance H (fov = 2*atan(w/(2H))).
|
||||
* Independent of the widescreen squash; identity by default. */
|
||||
if (g_fov_scale > 0.0 && g_fov_scale != 1.0)
|
||||
gte_set_fov_scale(1000, psx_projection_scale_denominator(g_fov_scale));
|
||||
else
|
||||
gte_set_fov_scale(1, 1);
|
||||
if (!g_ws_engaged) return;
|
||||
|
||||
const bool wide = g_video_aspect_num * 3 != g_video_aspect_den * 4;
|
||||
@@ -5017,6 +5031,28 @@ static int effective_player_mode_for_sio(const PlayerInput& p, int sio_slot) {
|
||||
return effective_player_mode(p);
|
||||
}
|
||||
|
||||
/* Assert one slot's real SIO profile (connection, config capability) from its
|
||||
* device/mode/policy and return the pad mode to present at boot. Used by
|
||||
* refresh_player_devices and to restore a port once a mod controller source
|
||||
* detaches. dev_here: dev-any-input keeps P1 connected without a device. */
|
||||
static int assert_sio_pad_profile(int s, bool dev_here) {
|
||||
const PlayerInput& p = g_players[s];
|
||||
const int mode = effective_player_mode_for_sio(p, s);
|
||||
const ModControllerPresentationPolicy& policy = g_mod_controller_policy[s];
|
||||
const int boot_mode = policy.callback ? policy.initial_mode : mode;
|
||||
sio_set_pad_connected(s, (p.kind != 0 || dev_here) ? 1 : 0);
|
||||
/* DIGITAL mode == a plain digital controller that ignores the DualShock
|
||||
* config-mode commands (real SCPH-1080 behaviour); ANALOG or an
|
||||
* explicitly config-capable mod policy == a config-capable DualShock.
|
||||
* A digital pad that wrongly answered 0x43 sent Tomba 2's pad driver
|
||||
* down the config path -> phantom 0x00 reads.
|
||||
* Multitap taps are always digital (see sio_pad_on_multitap). */
|
||||
sio_set_pad_config_capable(
|
||||
s, policy.callback ? policy.config_capable
|
||||
: mode != PSXRecompV4::PAD_MODE_DIGITAL);
|
||||
return boot_mode;
|
||||
}
|
||||
|
||||
/* Open/close SDL handles so they match g_players, and (re)assert each slot's
|
||||
* PSX connection + pad type. Safe to call repeatedly (hotplug, boot).
|
||||
* While delay-sync netplay is active, SIO connection/type are owned by
|
||||
@@ -5028,22 +5064,9 @@ static void refresh_player_devices(void) {
|
||||
if (p.kind != 2) close_player(p); /* keyboard/none: no handle */
|
||||
else open_player(p, s);
|
||||
if (netplay) continue;
|
||||
const int mode = effective_player_mode_for_sio(p, s);
|
||||
const ModControllerPresentationPolicy& policy =
|
||||
g_mod_controller_policy[s];
|
||||
const int boot_mode = policy.callback ? policy.initial_mode : mode;
|
||||
sio_set_pad_connected(s, p.kind != 0 ? 1 : 0);
|
||||
const int boot_mode = assert_sio_pad_profile(s, false);
|
||||
sio_set_pad_analog(s, pad_mode_boot_analog(boot_mode),
|
||||
0x80, 0x80, 0x80, 0x80);
|
||||
/* DIGITAL mode == a plain digital controller that ignores the DualShock
|
||||
* config-mode commands (real SCPH-1080 behaviour); ANALOG or an
|
||||
* explicitly config-capable mod policy == a config-capable DualShock.
|
||||
* A digital pad that wrongly answered 0x43 sent Tomba 2's pad driver
|
||||
* down the config path -> phantom 0x00 reads.
|
||||
* Multitap taps are always digital (see sio_pad_on_multitap). */
|
||||
sio_set_pad_config_capable(
|
||||
s, policy.callback ? policy.config_capable
|
||||
: mode != PSXRecompV4::PAD_MODE_DIGITAL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6268,7 +6291,65 @@ static void sample_pad_into_sio(int override) {
|
||||
psx_start_consumer_enabled() ? psx_start_consumer_offline_frame() : 0u;
|
||||
for (int s = 0; s < n; s++) {
|
||||
PsxNetPad pad;
|
||||
if (!capture_pad_slot(s, &pad)) continue; /* no device in this port */
|
||||
PSXModControllerState source;
|
||||
int released = 0;
|
||||
if (mod_controller_source_sample((uint32_t)s, &source, &released)) {
|
||||
const bool dev_here = (dev_any_input_enabled() && s == 0);
|
||||
if (released) {
|
||||
/* Source detached/reset: deliver one neutral frame (even on a
|
||||
* port with no device) and give the port back its real SIO
|
||||
* connection/config state; a device resumes next frame. */
|
||||
const int boot_mode = assert_sio_pad_profile(s, dev_here);
|
||||
pad.buttons = 0xffff;
|
||||
pad.lx = pad.ly = pad.rx = pad.ry = 128;
|
||||
pad.analog = pad_mode_boot_analog(boot_mode) ? 1 : 0;
|
||||
pad.connected = 1;
|
||||
} else {
|
||||
/* Sample physical buttons separately; its axes/type cannot
|
||||
* steer the source's virtual pad. Guards release every source
|
||||
* channel. */
|
||||
PsxNetPad local;
|
||||
const int have_local = capture_pad_slot(s, &local);
|
||||
pad.buttons = (uint16_t)source.buttons;
|
||||
if (have_local) pad.buttons &= local.buttons;
|
||||
pad.lx = (uint8_t)source.lx; pad.ly = (uint8_t)source.ly;
|
||||
pad.rx = (uint8_t)source.rx; pad.ry = (uint8_t)source.ry;
|
||||
pad.connected = 1;
|
||||
/* Presentation goes through the same resolution as a physical
|
||||
* pad: the source's `analog` is the device capability, then
|
||||
* mod mode override, multitap rule and presentation policy. */
|
||||
const PlayerInput& p = g_players[s];
|
||||
int mode = effective_player_mode_for_sio(p, s);
|
||||
if (g_mod_controller_mode_override[s] >= 0 &&
|
||||
!(sio_pad_on_multitap(s) && !sio_get_multitap_analog()))
|
||||
mode = g_mod_controller_mode_override[s];
|
||||
if (!source.analog) mode = PSXRecompV4::PAD_MODE_DIGITAL;
|
||||
uint8_t st[4] = { pad.lx, pad.ly, pad.rx, pad.ry };
|
||||
int eff_mode = controller_policy_resolve_mode(
|
||||
s, s + 1, mode, pad_sources_for(p, dev_here), p,
|
||||
pad.buttons, st);
|
||||
if (!source.analog ||
|
||||
(sio_pad_on_multitap(s) && !sio_get_multitap_analog()))
|
||||
eff_mode = PSXRecompV4::PAD_MODE_DIGITAL;
|
||||
pad.analog = eff_mode == PSXRecompV4::PAD_MODE_ANALOG ? 1 : 0;
|
||||
if (!pad.analog)
|
||||
pad.lx = pad.ly = pad.rx = pad.ry = 128;
|
||||
if (savestate_input_guard_active()) {
|
||||
pad.buttons = 0xffff;
|
||||
pad.lx = pad.ly = pad.rx = pad.ry = 128;
|
||||
}
|
||||
/* The source occupies the port; config capability follows the
|
||||
* same rule as a physical pad (apply_pad_slot_to_sio and the
|
||||
* SIO layer keep multitap taps digital). */
|
||||
const ModControllerPresentationPolicy& policy =
|
||||
g_mod_controller_policy[s];
|
||||
sio_set_pad_connected(s, 1);
|
||||
sio_set_pad_config_capable(
|
||||
s, policy.callback
|
||||
? policy.config_capable
|
||||
: mode != PSXRecompV4::PAD_MODE_DIGITAL);
|
||||
}
|
||||
} else if (!capture_pad_slot(s, &pad)) continue; /* no device in this port */
|
||||
/* Push sticks every frame; request the pad type (digital/analog) through
|
||||
* the coherent channel so a policy switch is applied only at an idle,
|
||||
* non-config bus boundary (never mid-poll / mid-handshake). This is the
|
||||
@@ -7473,6 +7554,9 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
}
|
||||
/* Solo resim self-check replay republishes the recorded rows itself —
|
||||
* live sampling must not touch SIO during the replay window. */
|
||||
/* New frame: the low-latency re-sample later this frame reuses this
|
||||
* frame's mod controller source sample (callback runs once). */
|
||||
mod_controller_source_begin_frame();
|
||||
if (!psx_selfcheck_replay_input()) {
|
||||
if (g_headless)
|
||||
sample_headless_pad_into_sio(override);
|
||||
@@ -13971,6 +14055,7 @@ int main(int argc, char** argv) {
|
||||
(float)gc.runtime.video_scanline_strength;
|
||||
g_video_aspect_num = gc.runtime.video_aspect_num;
|
||||
g_video_aspect_den = gc.runtime.video_aspect_den;
|
||||
g_fov_scale = psx_projection_scale_load_config(game_config_path);
|
||||
g_low_latency_input = gc.runtime.video_low_latency_input ? 1 : 0;
|
||||
gl_renderer_set_texture_window_batching(
|
||||
gc.runtime.video_texture_window_batching ? 1 : 0);
|
||||
@@ -14418,6 +14503,19 @@ int main(int argc, char** argv) {
|
||||
if (us.has_scanlines) g_video_scanlines = us.scanlines;
|
||||
if (us.has_scanline_strength)
|
||||
g_video_scanline_strength = (float)us.scanline_strength;
|
||||
/* fov_scale precedence: game.toml < settings.toml < PSX_GTE_FOV_SCALE.
|
||||
* Bad values warn and leave the lower layer's value in place. */
|
||||
if (us.has_fov_scale) {
|
||||
if (psx_projection_scale_valid(us.fov_scale)) g_fov_scale = us.fov_scale;
|
||||
else std::fprintf(stderr, "psxrecomp: settings.toml [video] fov_scale %g ignored "
|
||||
"(must be > 0 and <= 8)\n", us.fov_scale);
|
||||
}
|
||||
if (const char* fov_env = std::getenv("PSX_GTE_FOV_SCALE")) {
|
||||
double value;
|
||||
if (psx_projection_scale_parse(fov_env, &value)) g_fov_scale = value;
|
||||
else std::fprintf(stderr, "psxrecomp: PSX_GTE_FOV_SCALE=\"%s\" ignored "
|
||||
"(must be a number > 0 and <= 8)\n", fov_env);
|
||||
}
|
||||
if (us.has_auto_skip_fmv) g_auto_skip_fmv = us.auto_skip_fmv ? 1 : 0;
|
||||
/* turbo_loads is deliberately NOT restored from settings.toml. It is a
|
||||
* write-only latch: the launcher stopped drawing a Turbo loads row when
|
||||
@@ -15916,6 +16014,7 @@ int main(int argc, char** argv) {
|
||||
* choices first so disabling a package cannot leave its prior state
|
||||
* latched across a soft return. */
|
||||
g_mod_controller_mode_override.fill(-1);
|
||||
mod_controller_source_reset();
|
||||
for (auto& policy : g_mod_controller_policy)
|
||||
policy = ModControllerPresentationPolicy{};
|
||||
g_mod_load_wall_multiplier = -1;
|
||||
|
||||
@@ -164,6 +164,28 @@ static inline uint32_t psx_phys_addr_store(uint32_t addr) {
|
||||
/* Expose RAM pointer for oracle comparison (find_first_divergence). */
|
||||
uint8_t *memory_get_ram_ptr(void) { return ram; }
|
||||
uint8_t *memory_get_scratchpad_ptr(void) { return scratchpad; }
|
||||
|
||||
/* Side-effect-FREE aligned word peek for the debug/observability path ONLY:
|
||||
* main RAM (every KUSEG/KSEG0/KSEG1 mirror), scratchpad and BIOS ROM. Never
|
||||
* touches MMIO, the lockstep/DS read hooks, read watches, or unmapped_fatal.
|
||||
* Returns 1 and fills *out when addr is backed by one of those; 0 otherwise. */
|
||||
int psx_peek_word(uint32_t addr, uint32_t *out) {
|
||||
uint32_t phys, off;
|
||||
const uint8_t *p;
|
||||
if (addr >= 0xC0000000u || (addr & 3u)) return 0;
|
||||
phys = addr & 0x1FFFFFFFu;
|
||||
if (psx_ram_resolve(addr, 4u, &off)) {
|
||||
p = &ram[off];
|
||||
} else if (phys >= 0x1F800000u && phys <= 0x1F8003FCu) {
|
||||
p = &scratchpad[phys - 0x1F800000u];
|
||||
} else if (phys >= 0x1FC00000u && phys <= 0x1FC7FFFCu) {
|
||||
p = &bios_rom[phys - 0x1FC00000u];
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (out) memcpy(out, p, sizeof(*out));
|
||||
return 1;
|
||||
}
|
||||
void memory_clear_low_boot_scratch(void) {
|
||||
memset(ram, 0, 0x10u);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "mod_controller_source.h"
|
||||
#include "sio.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
static PSXModControllerSource s_source[PSX_MAX_PLAYERS];
|
||||
static uint8_t s_release[PSX_MAX_PLAYERS];
|
||||
/* One sample per player per frame: the early and the low-latency re-sample in
|
||||
* main.cpp share the result so a stateful callback runs once. */
|
||||
static PSXModControllerState s_cache[PSX_MAX_PLAYERS];
|
||||
static uint8_t s_cache_valid[PSX_MAX_PLAYERS];
|
||||
static uint8_t s_cache_released[PSX_MAX_PLAYERS];
|
||||
static uint32_t s_bad_count[PSX_MAX_PLAYERS];
|
||||
#define BAD_LOG_INTERVAL 600u
|
||||
int psx_mod_set_controller_source(uint32_t player, PSXModControllerSource source) {
|
||||
if (player >= PSX_MAX_PLAYERS) return 0;
|
||||
if (s_source[player] && !source) s_release[player] = 1;
|
||||
else if (source) s_release[player] = 0;
|
||||
s_source[player] = source;
|
||||
s_cache_valid[player] = 0;
|
||||
s_bad_count[player] = 0;
|
||||
return 1;
|
||||
}
|
||||
/* Detach every source but keep a neutral release pending for each port that
|
||||
* had one, so held buttons/sticks are cleared even on a port with no device. */
|
||||
void mod_controller_source_reset(void) {
|
||||
for (uint32_t i = 0; i < PSX_MAX_PLAYERS; i++) {
|
||||
if (s_source[i]) s_release[i] = 1;
|
||||
s_source[i] = NULL;
|
||||
}
|
||||
memset(s_cache_valid, 0, sizeof s_cache_valid);
|
||||
memset(s_bad_count, 0, sizeof s_bad_count);
|
||||
}
|
||||
void mod_controller_source_begin_frame(void) {
|
||||
memset(s_cache_valid, 0, sizeof s_cache_valid);
|
||||
}
|
||||
int mod_controller_source_present(uint32_t player) {
|
||||
return player < PSX_MAX_PLAYERS && s_source[player] != NULL;
|
||||
}
|
||||
static void note_bad(uint32_t player, const PSXModControllerState *s) {
|
||||
if (s_bad_count[player]++ % BAD_LOG_INTERVAL) return;
|
||||
fprintf(stderr,
|
||||
"psxrecomp: mod controller source (player %u) returned an invalid "
|
||||
"state (struct_size %u, expected %u; buttons 0x%x lx %u ly %u rx %u "
|
||||
"ry %u analog %u); delivering neutral (%u so far)\n",
|
||||
(unsigned)player, (unsigned)s->struct_size,
|
||||
(unsigned)sizeof *s, (unsigned)s->buttons, (unsigned)s->lx,
|
||||
(unsigned)s->ly, (unsigned)s->rx, (unsigned)s->ry,
|
||||
(unsigned)s->analog, (unsigned)s_bad_count[player]);
|
||||
}
|
||||
int mod_controller_source_sample(uint32_t player, PSXModControllerState *state,
|
||||
int *released) {
|
||||
if (released) *released = 0;
|
||||
if (!state || player>=PSX_MAX_PLAYERS || (!s_source[player] && !s_release[player] && !s_cache_valid[player])) return 0;
|
||||
if (s_cache_valid[player]) {
|
||||
*state = s_cache[player];
|
||||
if (released) *released = s_cache_released[player];
|
||||
return 1;
|
||||
}
|
||||
PSXModControllerState neutral = {sizeof neutral, 0xffff, 128,128,128,128,1};
|
||||
int rel = 0;
|
||||
*state = neutral;
|
||||
if (s_release[player]) { s_release[player] = 0; rel = 1; }
|
||||
else {
|
||||
if (!s_source[player](state)) *state = neutral; /* declined: neutral */
|
||||
else if (state->struct_size != sizeof *state ||
|
||||
state->buttons > 0xffff || state->lx > 255 || state->ly > 255 ||
|
||||
state->rx > 255 || state->ry > 255 || state->analog > 1) {
|
||||
note_bad(player, state);
|
||||
*state = neutral;
|
||||
}
|
||||
if (!state->analog) state->lx = state->ly = state->rx = state->ry = 128;
|
||||
}
|
||||
s_cache[player] = *state;
|
||||
s_cache_released[player] = (uint8_t)rel;
|
||||
s_cache_valid[player] = 1;
|
||||
if (released) *released = rel;
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/* Guest services for seamless-loading adapters (mod_plugins.h). Each is the
|
||||
* one shared implementation of an effect several titles' adapters need; see
|
||||
* the header for the contract. */
|
||||
#include "mod_plugins.h"
|
||||
#include "cpu_state.h"
|
||||
#include "dma.h"
|
||||
#include "gpu.h"
|
||||
#include "interrupts.h"
|
||||
#include "psx_memory.h"
|
||||
#include "spu.h"
|
||||
#include <string.h>
|
||||
|
||||
uint32_t psx_mod_call_guest(struct CPUState* cpu, uint32_t function,
|
||||
uint32_t return_address, uint32_t a0, uint32_t a1,
|
||||
uint32_t a2, uint32_t a3) {
|
||||
uint32_t regs[32];
|
||||
const uint32_t pc = cpu->pc, hi = cpu->hi, lo = cpu->lo;
|
||||
memcpy(regs, cpu->gpr, sizeof regs);
|
||||
cpu->gpr[4] = a0;
|
||||
cpu->gpr[5] = a1;
|
||||
cpu->gpr[6] = a2;
|
||||
cpu->gpr[7] = a3;
|
||||
cpu->gpr[31] = return_address;
|
||||
psx_snapshot_host_call_begin();
|
||||
psx_dispatch_call(cpu, function, return_address);
|
||||
const uint32_t result = cpu->gpr[2];
|
||||
memcpy(cpu->gpr, regs, sizeof regs);
|
||||
cpu->pc = pc;
|
||||
cpu->hi = hi;
|
||||
cpu->lo = lo;
|
||||
psx_snapshot_host_call_end();
|
||||
return result;
|
||||
}
|
||||
|
||||
int psx_mod_dma_write_ram(uint32_t address, const void* data, uint32_t bytes,
|
||||
int lba) {
|
||||
return dma_host_cdrom_write(address, (const uint8_t*)data, bytes, lba);
|
||||
}
|
||||
|
||||
static int ram_span(uint32_t address, uint32_t bytes) {
|
||||
const uint32_t phys = address & 0x1FFFFFFFu;
|
||||
const uint32_t live = psx_ram_live_bytes();
|
||||
return address < 0xC0000000u && phys < live && bytes <= live - phys;
|
||||
}
|
||||
|
||||
int psx_mod_host_write_ram(uint32_t address, const void* data, uint32_t bytes) {
|
||||
const uint8_t* p = (const uint8_t*)data;
|
||||
if ((!p && bytes) || !ram_span(address, bytes)) return 0;
|
||||
while (bytes && (address & 3u)) { psx_host_write_byte(address++, *p++); --bytes; }
|
||||
for (; bytes >= 4; address += 4, p += 4, bytes -= 4) {
|
||||
uint32_t word;
|
||||
memcpy(&word, p, 4);
|
||||
psx_host_write_word(address, word);
|
||||
}
|
||||
while (bytes--) psx_host_write_byte(address++, *p++);
|
||||
return 1;
|
||||
}
|
||||
|
||||
#define SPU_RAM_BYTES 0x80000u
|
||||
#define SPU_TRANSFER_ADDR 0x1F801DA6u
|
||||
#define SPU_CONTROL 0x1F801DAAu
|
||||
|
||||
int psx_mod_spu_upload(uint32_t spu_address, uint32_t guest_source,
|
||||
uint32_t bytes, int stop_after) {
|
||||
if ((spu_address & 7u) || (guest_source & 3u) || (bytes & 3u) ||
|
||||
spu_address >= SPU_RAM_BYTES || bytes > SPU_RAM_BYTES ||
|
||||
!ram_span(guest_source, bytes))
|
||||
return 0;
|
||||
spu_write(SPU_TRANSFER_ADDR, spu_address >> 3);
|
||||
const uint16_t control = spu_ctrl_read();
|
||||
spu_write(SPU_CONTROL, (control & ~0x30u) | 0x20u);
|
||||
for (uint32_t i = 0; i < bytes; i += 4u) spu_dma_write(psx_mod_read_word(guest_source + i));
|
||||
if (stop_after) spu_write(SPU_CONTROL, control & ~0x30u);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int psx_mod_psyq_load_image(uint32_t x, uint32_t y, uint32_t w, uint32_t h,
|
||||
uint32_t guest_source) {
|
||||
if (!w || w > 1024u || !h || h > 512u || x >= 1024u || y >= 512u ||
|
||||
(guest_source & 3u))
|
||||
return 0;
|
||||
const uint32_t words = (w * h + 1u) / 2u;
|
||||
if (!ram_span(guest_source, words * 4u)) return 0;
|
||||
gpu_set_gp0_source(guest_source);
|
||||
gpu_write_gp1(0x04000000u);
|
||||
gpu_write_gp0(0x01000000u);
|
||||
gpu_write_gp0(0xA0000000u);
|
||||
gpu_write_gp0(x | (y << 16));
|
||||
gpu_write_gp0(w | (h << 16));
|
||||
for (uint32_t i = 0; i < words; i++) {
|
||||
gpu_set_gp0_source(guest_source + 4u * i);
|
||||
gpu_write_gp0(psx_mod_read_word(guest_source + 4u * i));
|
||||
}
|
||||
if (words >= 16u) gpu_write_gp1(0x04000002u);
|
||||
return 1;
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
/* Resident disc resources for seamless-loading adapters (mod_resident.h).
|
||||
*
|
||||
* Pack file (little endian):
|
||||
* "PSXRES01", u32 version, u32 files, u32 derived, u32 blobs,
|
||||
* u8 key[32], u64 payload_bytes,
|
||||
* files[] {u32 lba, size, padded, stock, blob}
|
||||
* derived[] {u32 file, tag, meta[4], blob}
|
||||
* blobs[] {u64 offset, u32 size, u32 zero, u8 sha256[32]}
|
||||
* u8 table_sha256[32] (everything above), payload.
|
||||
* Every blob is hashed again on load. Identical bytes share one blob. */
|
||||
#include "mod_resident.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "psx_sha256.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
using Digest = std::array<uint8_t, 32>;
|
||||
|
||||
constexpr char kMagic[8] = {'P', 'S', 'X', 'R', 'E', 'S', '0', '1'};
|
||||
constexpr uint32_t kVersion = 1;
|
||||
constexpr uint32_t kBlobAlign = 16;
|
||||
constexpr uint32_t kFullCd = 360000u * 2048u;
|
||||
constexpr uint64_t kMaxPackBytes = 1ull << 31;
|
||||
|
||||
struct FileRec { uint32_t lba, size, padded, stock, blob; };
|
||||
struct DerivedRec { uint32_t file, tag, meta[4], blob; };
|
||||
struct Blob { uint64_t offset; uint32_t size; Digest sha; };
|
||||
|
||||
} // namespace
|
||||
|
||||
struct PSXResidentPack {
|
||||
Bytes payload;
|
||||
std::vector<FileRec> files;
|
||||
std::vector<DerivedRec> derived;
|
||||
std::vector<Blob> blobs;
|
||||
std::vector<uint32_t> by_lba; /* file indices sorted by lba */
|
||||
uint32_t modified = 0;
|
||||
};
|
||||
|
||||
struct PSXResidentSink {
|
||||
PSXResidentPack* pack;
|
||||
std::map<Digest, uint32_t> index;
|
||||
uint32_t file_count;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
struct Record {
|
||||
std::string title, format, state, error, path;
|
||||
std::unique_ptr<PSXResidentPack> pack;
|
||||
double ms = 0;
|
||||
};
|
||||
std::vector<Record>& records() {
|
||||
static std::vector<Record> r;
|
||||
return r;
|
||||
}
|
||||
|
||||
Digest sha(const uint8_t* p, size_t n) {
|
||||
Digest d;
|
||||
psx_sha256_compute(p, n, d.data());
|
||||
return d;
|
||||
}
|
||||
std::string hex(const uint8_t* d, size_t n = 32) {
|
||||
std::string out(n * 2, '0');
|
||||
static const char* digits = "0123456789abcdef";
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
out[2 * i] = digits[d[i] >> 4];
|
||||
out[2 * i + 1] = digits[d[i] & 15];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void put32(Bytes& b, uint32_t v) { for (int i = 0; i < 4; i++) b.push_back(uint8_t(v >> (8 * i))); }
|
||||
void put64(Bytes& b, uint64_t v) { for (int i = 0; i < 8; i++) b.push_back(uint8_t(v >> (8 * i))); }
|
||||
uint32_t get32(const uint8_t* p) {
|
||||
return uint32_t(p[0]) | uint32_t(p[1]) << 8 | uint32_t(p[2]) << 16 | uint32_t(p[3]) << 24;
|
||||
}
|
||||
uint64_t get64(const uint8_t* p) { return uint64_t(get32(p)) | uint64_t(get32(p + 4)) << 32; }
|
||||
|
||||
bool valid_spec(const PSXResidentSpec* s) {
|
||||
if (!s || s->struct_size < sizeof(PSXResidentSpec) || !s->title || !*s->title ||
|
||||
!s->format || !*s->format || !s->files || !s->file_count ||
|
||||
s->policy > PSX_RESIDENT_REQUIRE_STOCK)
|
||||
return false;
|
||||
for (const char* p = s->format; *p; ++p)
|
||||
if (!(std::isalnum((unsigned char)*p) || *p == '-' || *p == '_' || *p == '.')) return false;
|
||||
for (const char* p = s->title; *p; ++p)
|
||||
if (*p == '/' || *p == '\\' || *p == ':' || *p == '.') return false;
|
||||
for (uint32_t i = 0; i < s->file_count; i++) {
|
||||
const auto& f = s->files[i];
|
||||
if (!f.path || !*f.path) return false;
|
||||
if (f.stock_sha256 && std::strlen(f.stock_sha256) != 64) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Digest cache_key(const PSXResidentSpec* s, const std::string& fingerprint) {
|
||||
psx_sha256_ctx ctx;
|
||||
psx_sha256_init(&ctx);
|
||||
auto text = [&](const std::string& t) {
|
||||
psx_sha256_update(&ctx, reinterpret_cast<const uint8_t*>(t.c_str()), t.size() + 1);
|
||||
};
|
||||
text("PSXRES01");
|
||||
text(fingerprint);
|
||||
text(s->title);
|
||||
text(s->format);
|
||||
text(std::to_string(s->policy) + "/" + std::to_string(s->max_file_bytes));
|
||||
for (uint32_t i = 0; i < s->file_count; i++) {
|
||||
const auto& f = s->files[i];
|
||||
text(f.path);
|
||||
text(std::to_string(f.stock_size));
|
||||
text(f.stock_sha256 ? f.stock_sha256 : "");
|
||||
}
|
||||
Digest d;
|
||||
psx_sha256_final(&ctx, d.data());
|
||||
return d;
|
||||
}
|
||||
|
||||
std::filesystem::path cache_dir(const char* title) {
|
||||
if (const char* p = std::getenv("PSX_RESIDENT_CACHE"))
|
||||
if (*p) return std::filesystem::path(p) / title;
|
||||
std::filesystem::path root;
|
||||
#ifdef _WIN32
|
||||
if (const char* p = std::getenv("LOCALAPPDATA")) root = p;
|
||||
#else
|
||||
if (const char* p = std::getenv("XDG_CACHE_HOME")) root = p;
|
||||
else if (const char* p = std::getenv("HOME")) root = std::filesystem::path(p) / ".cache";
|
||||
#endif
|
||||
if (root.empty()) throw std::runtime_error("no cache directory");
|
||||
return root / title / "seamless";
|
||||
}
|
||||
|
||||
uint32_t add_blob(PSXResidentSink& sink, const uint8_t* p, uint32_t n) {
|
||||
const Digest d = sha(p, n);
|
||||
auto it = sink.index.find(d);
|
||||
if (it != sink.index.end()) return it->second;
|
||||
auto& pack = *sink.pack;
|
||||
uint64_t offset = (pack.payload.size() + kBlobAlign - 1) & ~uint64_t(kBlobAlign - 1);
|
||||
if (offset + n > kMaxPackBytes) throw std::runtime_error("pack size limit");
|
||||
pack.payload.resize(size_t(offset));
|
||||
pack.payload.insert(pack.payload.end(), p, p + n);
|
||||
pack.blobs.push_back({offset, n, d});
|
||||
const uint32_t id = uint32_t(pack.blobs.size() - 1);
|
||||
sink.index.emplace(d, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
void index_lba(PSXResidentPack& pack) {
|
||||
pack.by_lba.resize(pack.files.size());
|
||||
for (uint32_t i = 0; i < pack.by_lba.size(); i++) pack.by_lba[i] = i;
|
||||
std::sort(pack.by_lba.begin(), pack.by_lba.end(),
|
||||
[&](uint32_t a, uint32_t b) { return pack.files[a].lba < pack.files[b].lba; });
|
||||
pack.modified = 0;
|
||||
for (const auto& f : pack.files) pack.modified += f.stock ? 0 : 1;
|
||||
}
|
||||
|
||||
std::unique_ptr<PSXResidentPack> build(const PSXResidentSpec* s, std::string& error) {
|
||||
auto pack = std::make_unique<PSXResidentPack>();
|
||||
PSXResidentSink sink{pack.get(), {}, s->file_count, {}};
|
||||
const uint32_t max = s->max_file_bytes ? s->max_file_bytes : kFullCd;
|
||||
Bytes data;
|
||||
for (uint32_t i = 0; i < s->file_count; i++) {
|
||||
const auto& f = s->files[i];
|
||||
uint32_t lba = 0, size = 0;
|
||||
if (!PSXRecompV4::mod_runtime_read_disc_file_sectors(f.path, max, data, lba, size, &error))
|
||||
return nullptr;
|
||||
int stock = 0;
|
||||
if (f.stock_sha256)
|
||||
stock = (!f.stock_size || f.stock_size == size) &&
|
||||
hex(sha(data.data(), data.size()).data()) == f.stock_sha256;
|
||||
if (!stock && s->policy == PSX_RESIDENT_REQUIRE_STOCK && f.stock_sha256) {
|
||||
error = std::string(f.path) + ": differs from the original disc";
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t blob = add_blob(sink, data.data(), uint32_t(data.size()));
|
||||
pack->files.push_back({lba, size, uint32_t(data.size()), uint32_t(stock), blob});
|
||||
if (s->derive) {
|
||||
const size_t before = pack->derived.size();
|
||||
if (!s->derive(&sink, i, data.data(), uint32_t(data.size()), stock, s->derive_user)) {
|
||||
error = sink.error.empty() ? std::string(f.path) + ": derive failed" : sink.error;
|
||||
return nullptr;
|
||||
}
|
||||
for (size_t d = before; d < pack->derived.size(); d++)
|
||||
if (pack->derived[d].file != i) {
|
||||
error = std::string(f.path) + ": derived blob names another file";
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
index_lba(*pack);
|
||||
return pack;
|
||||
}
|
||||
|
||||
Bytes table(const PSXResidentPack& p, const Digest& key) {
|
||||
Bytes t(kMagic, kMagic + 8);
|
||||
put32(t, kVersion);
|
||||
put32(t, uint32_t(p.files.size()));
|
||||
put32(t, uint32_t(p.derived.size()));
|
||||
put32(t, uint32_t(p.blobs.size()));
|
||||
t.insert(t.end(), key.begin(), key.end());
|
||||
put64(t, p.payload.size());
|
||||
for (const auto& f : p.files) {
|
||||
put32(t, f.lba); put32(t, f.size); put32(t, f.padded); put32(t, f.stock); put32(t, f.blob);
|
||||
}
|
||||
for (const auto& d : p.derived) {
|
||||
put32(t, d.file); put32(t, d.tag);
|
||||
for (uint32_t m : d.meta) put32(t, m);
|
||||
put32(t, d.blob);
|
||||
}
|
||||
for (const auto& b : p.blobs) {
|
||||
put64(t, b.offset); put32(t, b.size); put32(t, 0);
|
||||
t.insert(t.end(), b.sha.begin(), b.sha.end());
|
||||
}
|
||||
const Digest d = sha(t.data(), t.size());
|
||||
t.insert(t.end(), d.begin(), d.end());
|
||||
return t;
|
||||
}
|
||||
|
||||
void publish(const std::filesystem::path& path, const PSXResidentPack& p, const Digest& key) {
|
||||
std::filesystem::create_directories(path.parent_path());
|
||||
auto tmp = path;
|
||||
#ifdef _WIN32
|
||||
tmp += "." + std::to_string(GetCurrentProcessId()) + ".tmp";
|
||||
#else
|
||||
tmp += "." + std::to_string(getpid()) + ".tmp";
|
||||
#endif
|
||||
const Bytes t = table(p, key);
|
||||
{
|
||||
std::ofstream out(tmp, std::ios::binary | std::ios::trunc);
|
||||
out.write(reinterpret_cast<const char*>(t.data()), std::streamsize(t.size()));
|
||||
out.write(reinterpret_cast<const char*>(p.payload.data()), std::streamsize(p.payload.size()));
|
||||
out.close();
|
||||
if (!out) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(tmp, ec);
|
||||
throw std::runtime_error("cannot write " + tmp.string());
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (!MoveFileExW(tmp.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(tmp, ec);
|
||||
throw std::runtime_error("cannot publish " + path.string());
|
||||
}
|
||||
#else
|
||||
std::filesystem::rename(tmp, path);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Structure, key and every blob hash. Returns null on any mismatch. */
|
||||
std::unique_ptr<PSXResidentPack> load(const std::filesystem::path& path, const Digest& key,
|
||||
uint32_t file_count) {
|
||||
std::error_code ec;
|
||||
const auto bytes = std::filesystem::file_size(path, ec);
|
||||
if (ec || bytes < 64 || bytes > kMaxPackBytes) return nullptr;
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
uint8_t head[64];
|
||||
if (!in.read(reinterpret_cast<char*>(head), sizeof head)) return nullptr;
|
||||
if (std::memcmp(head, kMagic, 8) || get32(head + 8) != kVersion || get32(head + 12) != file_count ||
|
||||
std::memcmp(head + 24, key.data(), 32))
|
||||
return nullptr;
|
||||
const uint64_t nd = get32(head + 16), nb = get32(head + 20), payload = get64(head + 56);
|
||||
const uint64_t tsize = 64 + uint64_t(file_count) * 20 + nd * 28 + nb * 48;
|
||||
if (tsize + 32 > bytes || tsize + 32 + payload != bytes) return nullptr;
|
||||
Bytes raw(static_cast<size_t>(tsize + 32));
|
||||
std::memcpy(raw.data(), head, sizeof head);
|
||||
if (!in.read(reinterpret_cast<char*>(raw.data() + 64), std::streamsize(raw.size() - 64))) return nullptr;
|
||||
const uint8_t* p = raw.data();
|
||||
if (sha(p, size_t(tsize)) != *reinterpret_cast<const Digest*>(p + tsize)) return nullptr;
|
||||
auto pack = std::make_unique<PSXResidentPack>();
|
||||
const uint8_t* q = p + 64;
|
||||
for (uint32_t i = 0; i < file_count; i++, q += 20)
|
||||
pack->files.push_back({get32(q), get32(q + 4), get32(q + 8), get32(q + 12), get32(q + 16)});
|
||||
for (uint64_t i = 0; i < nd; i++, q += 28)
|
||||
pack->derived.push_back({get32(q), get32(q + 4), {get32(q + 8), get32(q + 12), get32(q + 16),
|
||||
get32(q + 20)}, get32(q + 24)});
|
||||
for (uint64_t i = 0; i < nb; i++, q += 48) {
|
||||
Blob b{get64(q), get32(q + 8), {}};
|
||||
std::memcpy(b.sha.data(), q + 16, 32);
|
||||
if (b.offset > payload || b.size > payload - b.offset) return nullptr;
|
||||
pack->blobs.push_back(b);
|
||||
}
|
||||
for (const auto& f : pack->files)
|
||||
if (f.blob >= nb || pack->blobs[f.blob].size != f.padded || f.padded % 2048 ||
|
||||
f.size > f.padded || f.padded - f.size >= 2048)
|
||||
return nullptr;
|
||||
for (const auto& d : pack->derived)
|
||||
if (d.blob >= nb || d.file >= file_count) return nullptr;
|
||||
pack->payload.resize(static_cast<size_t>(payload));
|
||||
if (!in.read(reinterpret_cast<char*>(pack->payload.data()), std::streamsize(payload))) return nullptr;
|
||||
for (const auto& b : pack->blobs)
|
||||
if (sha(pack->payload.data() + b.offset, b.size) != b.sha) return nullptr;
|
||||
index_lba(*pack);
|
||||
return pack;
|
||||
}
|
||||
|
||||
/* A cache from another disc with the same plan fingerprint cannot match the
|
||||
* extents of every listed file on this one. */
|
||||
bool extents_match(const PSXResidentPack& pack, const PSXResidentSpec* s) {
|
||||
for (uint32_t i = 0; i < s->file_count; i++) {
|
||||
uint32_t lba = 0, size = 0;
|
||||
if (!psx_mod_disc_file_extent(s->files[i].path, &lba, &size) ||
|
||||
lba != pack.files[i].lba || size != pack.files[i].size)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void prune(const std::filesystem::path& keep, const std::string& format, uint32_t count) {
|
||||
std::error_code ec;
|
||||
std::vector<std::filesystem::directory_entry> packs;
|
||||
const std::string prefix = format + "-";
|
||||
for (const auto& e : std::filesystem::directory_iterator(keep.parent_path(), ec)) {
|
||||
const auto name = e.path().filename().string();
|
||||
if (e.path() != keep && name.rfind(prefix, 0) == 0 && e.path().extension() == ".pack")
|
||||
packs.push_back(e);
|
||||
}
|
||||
if (packs.size() <= count) return;
|
||||
std::sort(packs.begin(), packs.end(), [](const auto& a, const auto& b) {
|
||||
std::error_code e1, e2;
|
||||
return a.last_write_time(e1) > b.last_write_time(e2);
|
||||
});
|
||||
for (size_t i = count; i < packs.size(); i++) std::filesystem::remove(packs[i].path(), ec);
|
||||
}
|
||||
|
||||
Record& record_for(const PSXResidentSpec* s) {
|
||||
auto& r = records();
|
||||
for (auto& rec : r)
|
||||
if (rec.title == s->title && rec.format == s->format) return rec;
|
||||
r.push_back({});
|
||||
r.back().title = s->title;
|
||||
r.back().format = s->format;
|
||||
return r.back();
|
||||
}
|
||||
|
||||
void json_string(std::string& out, const std::string& s) {
|
||||
out += '"';
|
||||
for (char c : s) {
|
||||
if (c == '"' || c == '\\') { out += '\\'; out += c; }
|
||||
else if ((unsigned char)c < 0x20) { char b[8]; std::snprintf(b, sizeof b, "\\u%04x", c); out += b; }
|
||||
else out += c;
|
||||
}
|
||||
out += '"';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int psx_resident_emit(PSXResidentSink* sink, uint32_t file, uint32_t tag,
|
||||
const uint32_t meta[4], const void* bytes, uint32_t size) {
|
||||
if (!sink || file >= sink->file_count || (!bytes && size)) return 0;
|
||||
try {
|
||||
const uint32_t blob = add_blob(*sink, static_cast<const uint8_t*>(bytes), size);
|
||||
DerivedRec d{file, tag, {0, 0, 0, 0}, blob};
|
||||
if (meta) std::memcpy(d.meta, meta, sizeof d.meta);
|
||||
sink->pack->derived.push_back(d);
|
||||
return 1;
|
||||
} catch (const std::exception& e) {
|
||||
sink->error = e.what();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" const PSXResidentPack* psx_resident_prepare(const PSXResidentSpec* spec) {
|
||||
if (!valid_spec(spec)) {
|
||||
psx_mod_counter_add("resident.invalid_spec", 1);
|
||||
return nullptr;
|
||||
}
|
||||
Record& rec = record_for(spec);
|
||||
rec.pack.reset();
|
||||
rec.error.clear();
|
||||
rec.path.clear();
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
try {
|
||||
const std::string& fingerprint = PSXRecompV4::mod_runtime_fingerprint();
|
||||
const Digest key = cache_key(spec, fingerprint);
|
||||
/* Without a committed plan there is no disc identity to key on: build
|
||||
* in memory and never reuse or publish. */
|
||||
const bool cacheable = !fingerprint.empty();
|
||||
std::filesystem::path path;
|
||||
std::unique_ptr<PSXResidentPack> pack;
|
||||
if (cacheable) {
|
||||
path = cache_dir(spec->title) / (std::string(spec->format) + "-" + hex(key.data(), 16) + ".pack");
|
||||
rec.path = path.string();
|
||||
pack = load(path, key, spec->file_count);
|
||||
if (pack && !extents_match(*pack, spec)) pack.reset();
|
||||
if (!pack && std::filesystem::exists(path)) psx_mod_counter_add("resident.cache_rejected", 1);
|
||||
}
|
||||
if (pack) {
|
||||
rec.state = "verified";
|
||||
std::error_code ec;
|
||||
std::filesystem::last_write_time(path, std::filesystem::file_time_type::clock::now(), ec);
|
||||
psx_mod_counter_add("resident.verified", 1);
|
||||
} else {
|
||||
pack = build(spec, rec.error);
|
||||
if (!pack) throw std::runtime_error(rec.error);
|
||||
if (cacheable) publish(path, *pack, key);
|
||||
rec.state = cacheable ? "prepared" : "prepared (uncached: no mod plan)";
|
||||
psx_mod_counter_add("resident.prepared", 1);
|
||||
}
|
||||
if (cacheable) prune(path, spec->format, spec->keep_packs ? spec->keep_packs : 3);
|
||||
rec.pack = std::move(pack);
|
||||
} catch (const std::exception& e) {
|
||||
rec.pack.reset();
|
||||
rec.state = "failed";
|
||||
if (rec.error.empty()) rec.error = e.what();
|
||||
psx_mod_counter_add("resident.prepare_failed", 1);
|
||||
}
|
||||
rec.ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
||||
return rec.pack.get();
|
||||
}
|
||||
|
||||
extern "C" uint32_t psx_resident_file_count(const PSXResidentPack* p) {
|
||||
return p ? uint32_t(p->files.size()) : 0;
|
||||
}
|
||||
|
||||
extern "C" const uint8_t* psx_resident_file(const PSXResidentPack* p, uint32_t file,
|
||||
uint32_t* size, uint32_t* padded) {
|
||||
if (!p || file >= p->files.size()) return nullptr;
|
||||
const auto& f = p->files[file];
|
||||
if (size) *size = f.size;
|
||||
if (padded) *padded = f.padded;
|
||||
return p->payload.data() + p->blobs[f.blob].offset;
|
||||
}
|
||||
|
||||
extern "C" uint32_t psx_resident_file_lba(const PSXResidentPack* p, uint32_t file) {
|
||||
return p && file < p->files.size() ? p->files[file].lba : 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_resident_file_stock(const PSXResidentPack* p, uint32_t file) {
|
||||
return p && file < p->files.size() ? int(p->files[file].stock) : 0;
|
||||
}
|
||||
|
||||
extern "C" uint32_t psx_resident_modified_files(const PSXResidentPack* p) {
|
||||
return p ? p->modified : 0;
|
||||
}
|
||||
|
||||
extern "C" const uint8_t* psx_resident_find_lba(const PSXResidentPack* p, uint32_t lba,
|
||||
uint32_t bytes, uint32_t* file) {
|
||||
if (!p || p->by_lba.empty()) return nullptr;
|
||||
/* Last file starting at or before lba. Files never overlap on a disc. */
|
||||
auto it = std::upper_bound(p->by_lba.begin(), p->by_lba.end(), lba,
|
||||
[&](uint32_t v, uint32_t i) { return v < p->files[i].lba; });
|
||||
if (it == p->by_lba.begin()) return nullptr;
|
||||
const uint32_t i = *--it;
|
||||
const auto& f = p->files[i];
|
||||
const uint64_t offset = uint64_t(lba - f.lba) * 2048u;
|
||||
if (offset > f.padded || bytes > f.padded - offset) return nullptr;
|
||||
if (file) *file = i;
|
||||
return p->payload.data() + p->blobs[f.blob].offset + offset;
|
||||
}
|
||||
|
||||
extern "C" uint32_t psx_resident_derived_count(const PSXResidentPack* p) {
|
||||
return p ? uint32_t(p->derived.size()) : 0;
|
||||
}
|
||||
|
||||
extern "C" const uint8_t* psx_resident_derived(const PSXResidentPack* p, uint32_t index,
|
||||
uint32_t* file, uint32_t* tag,
|
||||
uint32_t meta[4], uint32_t* size) {
|
||||
if (!p || index >= p->derived.size()) return nullptr;
|
||||
const auto& d = p->derived[index];
|
||||
if (file) *file = d.file;
|
||||
if (tag) *tag = d.tag;
|
||||
if (meta) std::memcpy(meta, d.meta, sizeof d.meta);
|
||||
if (size) *size = p->blobs[d.blob].size;
|
||||
return p->payload.data() + p->blobs[d.blob].offset;
|
||||
}
|
||||
|
||||
extern "C" int psx_resident_guest_ranges_match(const PSXResidentRange* ranges, uint32_t count,
|
||||
const char* sha256) {
|
||||
if (!ranges || !count || !sha256) return 0;
|
||||
psx_sha256_ctx ctx;
|
||||
psx_sha256_init(&ctx);
|
||||
uint8_t chunk[256];
|
||||
for (uint32_t r = 0; r < count; r++) {
|
||||
if (ranges[r].hi < ranges[r].lo) return 0;
|
||||
for (uint32_t a = ranges[r].lo; a < ranges[r].hi;) {
|
||||
const uint32_t n = std::min<uint32_t>(sizeof chunk, ranges[r].hi - a);
|
||||
for (uint32_t i = 0; i < n; i++) chunk[i] = psx_mod_read_byte(a + i);
|
||||
psx_sha256_update(&ctx, chunk, n);
|
||||
a += n;
|
||||
}
|
||||
}
|
||||
uint8_t d[32];
|
||||
psx_sha256_final(&ctx, d);
|
||||
return hex(d) == sha256;
|
||||
}
|
||||
|
||||
extern "C" int psx_resident_status_json(char* out, uint32_t capacity) {
|
||||
std::string s = "[";
|
||||
for (const auto& r : records()) {
|
||||
if (s.size() > 1) s += ',';
|
||||
s += "{\"title\":"; json_string(s, r.title);
|
||||
s += ",\"format\":"; json_string(s, r.format);
|
||||
s += ",\"state\":"; json_string(s, r.state);
|
||||
s += ",\"error\":"; json_string(s, r.error);
|
||||
s += ",\"path\":"; json_string(s, r.path);
|
||||
char b[256];
|
||||
const PSXResidentPack* p = r.pack.get();
|
||||
std::snprintf(b, sizeof b,
|
||||
",\"files\":%u,\"modified\":%u,\"derived\":%u,\"blobs\":%u,\"bytes\":%llu,\"ms\":%.1f}",
|
||||
p ? unsigned(p->files.size()) : 0u, p ? p->modified : 0u,
|
||||
p ? unsigned(p->derived.size()) : 0u, p ? unsigned(p->blobs.size()) : 0u,
|
||||
p ? (unsigned long long)p->payload.size() : 0ull, r.ms);
|
||||
s += b;
|
||||
}
|
||||
s += ']';
|
||||
if (!out || capacity <= s.size()) return 0;
|
||||
std::memcpy(out, s.c_str(), s.size() + 1);
|
||||
return 1;
|
||||
}
|
||||
@@ -1627,6 +1627,65 @@ extern "C" int psx_mod_read_disc_file(const char* path, void* buffer,
|
||||
} catch (...) { return 0; }
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_disc_file_extent(const char* path, uint32_t* lba,
|
||||
uint32_t* size) {
|
||||
using namespace PSXRecompV4;
|
||||
if (lba) *lba = 0;
|
||||
if (size) *size = 0;
|
||||
if (!path || !*path || !lba || !size) return 0;
|
||||
try {
|
||||
const auto& s = state();
|
||||
const auto& mount = s.effective_disc_path.empty() ? s.disc_path : s.effective_disc_path;
|
||||
if (mount.empty()) return 0;
|
||||
PS1::ISOReader reader;
|
||||
uint32_t first = 0, bytes = 0;
|
||||
bool directory = false;
|
||||
if (!reader.Open(mount.string()) ||
|
||||
!find_effective_file(reader, path, first, bytes, directory) ||
|
||||
directory || !bytes || bytes > kMaxDiscFileBytes)
|
||||
return 0;
|
||||
*lba = first;
|
||||
*size = bytes;
|
||||
return 1;
|
||||
} catch (...) { return 0; }
|
||||
}
|
||||
|
||||
namespace PSXRecompV4 {
|
||||
/* Whole effective sectors, including the bytes after end-of-file in the last
|
||||
* sector: a sector-granular read by the game receives exactly these. */
|
||||
bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_bytes,
|
||||
std::vector<uint8_t>& padded, uint32_t& lba,
|
||||
uint32_t& size, std::string* error) {
|
||||
padded.clear();
|
||||
lba = size = 0;
|
||||
auto fail = [&](const std::string& why) {
|
||||
if (error) *error = path + ": " + why;
|
||||
padded.clear();
|
||||
return false;
|
||||
};
|
||||
try {
|
||||
const auto& s = state();
|
||||
const auto& mount = s.effective_disc_path.empty() ? s.disc_path : s.effective_disc_path;
|
||||
if (mount.empty()) return fail("no disc mounted");
|
||||
PS1::ISOReader reader;
|
||||
bool directory = false;
|
||||
if (!reader.Open(mount.string())) return fail("cannot open disc");
|
||||
if (!find_effective_file(reader, path, lba, size, directory) || directory)
|
||||
return fail("not found on the disc");
|
||||
if (!size || size > kMaxDiscFileBytes || (max_bytes && size > max_bytes))
|
||||
return fail("unsupported size " + std::to_string(size));
|
||||
const uint32_t sectors = (size + 2047u) / 2048u;
|
||||
padded.resize(size_t(sectors) * 2048u);
|
||||
for (uint32_t i = 0; i < sectors; ++i)
|
||||
if (!read_effective_user_sector(reader, lba + i, padded.data() + size_t(i) * 2048u))
|
||||
return fail("unreadable sector " + std::to_string(lba + i));
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
return fail(e.what());
|
||||
}
|
||||
}
|
||||
} // namespace PSXRecompV4
|
||||
|
||||
extern "C" void mod_runtime_activate_plugins(void) {
|
||||
using namespace PSXRecompV4;
|
||||
RuntimeMods& s = state();
|
||||
@@ -1954,6 +2013,17 @@ extern "C" int psx_mod_function_entry_active(void) {
|
||||
return PSXRecompV4::function_entry_depth != 0;
|
||||
}
|
||||
|
||||
extern "C" void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) {
|
||||
out->depth = PSXRecompV4::function_entry_depth;
|
||||
out->plugin = PSXRecompV4::state().current_plugin;
|
||||
}
|
||||
|
||||
extern "C" void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) {
|
||||
PSXRecompV4::function_entry_depth = in->depth;
|
||||
PSXRecompV4::state().current_plugin =
|
||||
static_cast<const PSXRecompV4::ModResolution::Plugin *>(in->plugin);
|
||||
}
|
||||
|
||||
/* Apply the committed plan's disc writes and overlays to one sector. The
|
||||
* emulated drive reaches this only after mod_runtime_enable_disc_patches()
|
||||
* (reference reads before then must see the original image); host-side
|
||||
|
||||
@@ -1853,29 +1853,8 @@ static void refresh_bios_resident_flags(void) {
|
||||
cache_path_is_bios_resident(s_cache_idx[i].path);
|
||||
}
|
||||
|
||||
/* Canonical cache arch-abi tag (caches are namespaced per backend AND per
|
||||
* target so a Windows-x64 gcc DLL and, later, a same-OS arm64 build for the
|
||||
* same fragment never comingle). compile_overlays.py
|
||||
* computes the IDENTICAL string from platform.system()/machine(); keep the two
|
||||
* mappings in lockstep ("<os>-<arch>": win|linux|macos + x64|arm64|x86). */
|
||||
#if defined(_WIN32)
|
||||
# define PSX_OL_OS "win"
|
||||
#elif defined(__APPLE__)
|
||||
# define PSX_OL_OS "macos"
|
||||
#else
|
||||
# define PSX_OL_OS "linux"
|
||||
#endif
|
||||
#if defined(__aarch64__) || defined(_M_ARM64)
|
||||
# define PSX_OL_ARCH "arm64"
|
||||
#elif defined(__x86_64__) || defined(_M_X64)
|
||||
# define PSX_OL_ARCH "x64"
|
||||
#elif defined(__i386__) || defined(_M_IX86)
|
||||
# define PSX_OL_ARCH "x86"
|
||||
#else
|
||||
# define PSX_OL_ARCH "unknown"
|
||||
#endif
|
||||
#define PSX_OVERLAY_ARCH_ABI PSX_OL_OS "-" PSX_OL_ARCH
|
||||
|
||||
/* PSX_OVERLAY_ARCH_ABI (the cache layout's "<os>-<arch>") is defined in
|
||||
* overlay_loader.h, so autocompile.c can hand it to the compile it spawns. */
|
||||
const char *overlay_loader_arch_abi(void) { return PSX_OVERLAY_ARCH_ABI; }
|
||||
|
||||
#ifndef _WIN32
|
||||
|
||||
+20
-8
@@ -43,6 +43,8 @@
|
||||
/* gte.cpp — position-cache fallback tier (ambiguity-gated, G1.4 exact table) */
|
||||
extern "C" int gte_geometry_correction_lookup(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16);
|
||||
extern "C" int gte_geometry_correction_lookup_probe(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16);
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Shadow storage */
|
||||
@@ -1080,10 +1082,10 @@ static inline int32_t field_rebase(int32_t v16, uint32_t half, int32_t parsed) {
|
||||
return (int32_t)((int64_t)v16 + ((int64_t)as11 - as16) * 65536);
|
||||
}
|
||||
|
||||
extern "C" 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) {
|
||||
static int pgxp_get_precise_vertex_impl(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y,
|
||||
int32_t *x16, int32_t *y16,
|
||||
uint16_t *sz, int probe) {
|
||||
s_stats.lookups++;
|
||||
|
||||
int32_t px = 0, py = 0;
|
||||
@@ -1105,8 +1107,10 @@ extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
}
|
||||
}
|
||||
|
||||
if (!have && s_position_fallback &&
|
||||
gte_geometry_correction_lookup(packet_word, &px, &py)) {
|
||||
const int fallback_hit = !have && s_position_fallback &&
|
||||
(probe ? gte_geometry_correction_lookup_probe(packet_word, &px, &py)
|
||||
: gte_geometry_correction_lookup(packet_word, &px, &py));
|
||||
if (fallback_hit) {
|
||||
pz = 0; /* fallback never carries depth */
|
||||
have = PGXP_SRC_FALLBACK;
|
||||
}
|
||||
@@ -1149,13 +1153,21 @@ extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
return have;
|
||||
}
|
||||
|
||||
extern "C" 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) {
|
||||
return pgxp_get_precise_vertex_impl(addr, packet_word, int_x, int_y,
|
||||
x16, y16, sz, 0);
|
||||
}
|
||||
|
||||
extern "C" int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y) {
|
||||
const PGXPStats saved = s_stats;
|
||||
int32_t x16, y16;
|
||||
uint16_t sz;
|
||||
const int src = pgxp_get_precise_vertex(addr, packet_word, int_x, int_y,
|
||||
&x16, &y16, &sz);
|
||||
const int src = pgxp_get_precise_vertex_impl(addr, packet_word, int_x, int_y,
|
||||
&x16, &y16, &sz, 1);
|
||||
s_stats = saved;
|
||||
return src;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "projection_scale_config.hpp"
|
||||
#include "projection_scale.hpp"
|
||||
#include "toml.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
|
||||
/* A bad [video] fov_scale must never stop the game starting (this runs inside
|
||||
* main()'s --game try block): warn and fall back to faithful, like the
|
||||
* PSX_GTE_FOV_SCALE path. Integers (fov_scale = 2) are accepted. */
|
||||
double psx_projection_scale_load_config(const std::filesystem::path& path) {
|
||||
try {
|
||||
const auto config = toml::parse(path.string());
|
||||
if (!config.contains("video")) return 1.0;
|
||||
const auto& video = toml::find(config, "video");
|
||||
if (!video.contains("fov_scale")) return 1.0;
|
||||
const auto& node = toml::find(video, "fov_scale");
|
||||
double value;
|
||||
if (node.is_floating()) value = node.as_floating();
|
||||
else if (node.is_integer()) value = static_cast<double>(node.as_integer());
|
||||
else throw std::runtime_error("must be a number");
|
||||
if (!psx_projection_scale_valid(value))
|
||||
throw std::runtime_error("must be > 0 and <= 8");
|
||||
return value;
|
||||
} catch (const std::exception& e) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: [video] fov_scale ignored (%s); using 1.0 (faithful)\n", e.what());
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
@@ -462,6 +462,10 @@ static int build_module(const BmToolchain *tk, const char *dump_path,
|
||||
!append_s(cmd, sizeof(cmd), " --stem ") || !append_s(cmd, sizeof(cmd), stem) ||
|
||||
!append_s(cmd, sizeof(cmd), " --out ") || !append_q(cmd, sizeof(cmd), out_path) ||
|
||||
!append_s(cmd, sizeof(cmd), " --flavor ") || !append_s(cmd, sizeof(cmd), flavor) ||
|
||||
/* This build's architecture, not the Python's or the compiler's (an
|
||||
* x86_64 runtime under Rosetta, arm64-only Xcode clang). */
|
||||
!append_s(cmd, sizeof(cmd), " --arch-abi ") ||
|
||||
!append_s(cmd, sizeof(cmd), overlay_loader_arch_abi()) ||
|
||||
!append_s(cmd, sizeof(cmd), " --compiler ") || !append_s(cmd, sizeof(cmd), compiler)) {
|
||||
set_err(err, err_cap, "build command too long");
|
||||
return 0;
|
||||
|
||||
+237
-21
@@ -28,9 +28,12 @@
|
||||
#include "gpu.h"
|
||||
#include "gpu_gl_renderer.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "overlay_loader.h"
|
||||
#include "psx_icache.h"
|
||||
#include "timers.h"
|
||||
#include "psx_video_timing.h"
|
||||
#include "gte_view.h"
|
||||
|
||||
extern uint8_t *memory_get_ram_ptr(void);
|
||||
extern uint8_t *memory_get_scratchpad_ptr(void);
|
||||
@@ -115,6 +118,8 @@ typedef struct RenderPassNesting {
|
||||
int ov_active_depth;
|
||||
uint32_t ov_inprogress;
|
||||
void (*ov_flush)(void);
|
||||
ModFunctionEntryContext mod_entry;
|
||||
PSXModRenderView render_pose;
|
||||
uint32_t span_lo, span_hi; /* open guest span (dirty_ram_run_span) */
|
||||
} RenderPassNesting;
|
||||
|
||||
@@ -133,6 +138,8 @@ static void nesting_save(RenderPassNesting *n) {
|
||||
dirty_ram_ld_delay_save(&n->ld_delay);
|
||||
overlay_loader_native_nesting(&n->ov_active_depth, &n->ov_inprogress);
|
||||
n->ov_flush = g_overlay_flush_pending_cycles;
|
||||
mod_runtime_function_entry_context_save(&n->mod_entry);
|
||||
gte_render_pose_get(&n->render_pose);
|
||||
dirty_ram_span_get(&n->span_lo, &n->span_hi);
|
||||
}
|
||||
|
||||
@@ -151,6 +158,8 @@ static void nesting_restore(const RenderPassNesting *n) {
|
||||
dirty_ram_ld_delay_restore(&n->ld_delay);
|
||||
overlay_loader_set_native_nesting(n->ov_active_depth, n->ov_inprogress);
|
||||
g_overlay_flush_pending_cycles = n->ov_flush;
|
||||
mod_runtime_function_entry_context_restore(&n->mod_entry);
|
||||
gte_render_pose_set(&n->render_pose);
|
||||
dirty_ram_span_set(n->span_lo, n->span_hi);
|
||||
}
|
||||
|
||||
@@ -169,7 +178,9 @@ static int nesting_balanced(const RenderPassNesting *n) {
|
||||
now.precise_mode == n->precise_mode &&
|
||||
now.ov_active_depth == n->ov_active_depth &&
|
||||
now.ov_flush == n->ov_flush &&
|
||||
now.span_lo == n->span_lo && now.span_hi == n->span_hi;
|
||||
now.span_lo == n->span_lo && now.span_hi == n->span_hi &&
|
||||
now.mod_entry.depth == n->mod_entry.depth &&
|
||||
now.mod_entry.plugin == n->mod_entry.plugin;
|
||||
}
|
||||
|
||||
/* After a watchdog abort, before the restore: which exits the longjmp
|
||||
@@ -204,6 +215,9 @@ static int nesting_describe(const RenderPassNesting *n, int bb_defer_ck,
|
||||
RP_NOTE(now.exec_phase != n->exec_phase, "exec phase %d->%d",
|
||||
now.exec_phase, n->exec_phase);
|
||||
RP_NOTE(now.ld_delay.armed != n->ld_delay.armed, "pending load");
|
||||
RP_NOTE(now.mod_entry.depth != n->mod_entry.depth, "mod entries %+d",
|
||||
(int)now.mod_entry.depth - (int)n->mod_entry.depth);
|
||||
RP_NOTE(now.mod_entry.plugin != n->mod_entry.plugin, "mod owner");
|
||||
RP_NOTE(now.span_hi != n->span_hi, "guest span");
|
||||
#undef RP_NOTE
|
||||
return count;
|
||||
@@ -245,6 +259,20 @@ static uint32_t s_plan_period = 2;
|
||||
static uint64_t s_plan_serial, s_restored_plan;
|
||||
static uint64_t s_restored_cycle, s_restored_stores;
|
||||
static int s_restored_valid;
|
||||
static RenderPassFailure s_attempt;
|
||||
static const char *s_checkpoint_failure;
|
||||
static RenderStereoStats s_stereo;
|
||||
static int s_pair_busy;
|
||||
static uint32_t s_pair_retry;
|
||||
|
||||
static int pass_refuse(const char *reason, uint32_t status) {
|
||||
/* A refusal always carries a reason: last_failure.reason == NULL means
|
||||
* "no failure" to the TCP reader. */
|
||||
s_attempt.reason = reason ? reason : "unspecified";
|
||||
s_attempt.status = status;
|
||||
s_stats.last_failure = s_attempt;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* PSX_RENDER_PASS_WATCHDOG=<guest cycles> lowers the cut-off (debug), so a
|
||||
* title's rollback path can be exercised on real passes. */
|
||||
@@ -283,6 +311,12 @@ void render_pass_reset_session(void) {
|
||||
memset(g_render_pass_dropped_writes, 0, sizeof g_render_pass_dropped_writes);
|
||||
s_open_generation = 0;
|
||||
s_restored_valid = 0;
|
||||
memset(&s_attempt, 0, sizeof s_attempt);
|
||||
memset(&s_stereo, 0, sizeof s_stereo);
|
||||
s_stereo.last_eye = -1;
|
||||
s_pair_busy = 0;
|
||||
s_pair_retry = 0;
|
||||
gl_renderer_stereo_reset();
|
||||
gl_renderer_pass_set_flip_shown(0);
|
||||
}
|
||||
|
||||
@@ -296,7 +330,7 @@ int psx_mod_set_render_pass_flip(uint32_t mode) {
|
||||
|
||||
/* Everything that must hold before guest code may run frozen, as a
|
||||
* PSX_MOD_RENDER_PASS_* reason (READY = all of it holds). */
|
||||
static uint32_t pass_status(void) {
|
||||
static uint32_t transaction_status(int stereo) {
|
||||
uint32_t gl;
|
||||
if (s_stats.disabled) return PSX_MOD_RENDER_PASS_DISABLED;
|
||||
if (psx_netplay_active() || psx_netplay_is_resimulating() ||
|
||||
@@ -304,7 +338,7 @@ static uint32_t pass_status(void) {
|
||||
g_ls_mode || g_ls_replay_active)
|
||||
return PSX_MOD_RENDER_PASS_SESSION;
|
||||
if (psx_presentation_fast_forward()) return PSX_MOD_RENDER_PASS_FAST_FORWARD;
|
||||
gl = gl_renderer_pass_unavailable();
|
||||
gl = stereo ? gl_renderer_stereo_unavailable() : gl_renderer_pass_unavailable();
|
||||
if (gl != PSX_MOD_RENDER_PASS_READY) return gl;
|
||||
if (s_nesting || g_psx_render_pass_active || psx_get_in_exception() ||
|
||||
dma_gpu_linked_list_active())
|
||||
@@ -312,6 +346,35 @@ static uint32_t pass_status(void) {
|
||||
return PSX_MOD_RENDER_PASS_READY;
|
||||
}
|
||||
|
||||
static uint32_t pass_status(void) { return transaction_status(0); }
|
||||
uint32_t psx_mod_render_stereo_status(void) {
|
||||
return s_pair_busy ? PSX_MOD_RENDER_PASS_BUSY : transaction_status(1);
|
||||
}
|
||||
void render_stereo_get_stats(RenderStereoStats *out) { if (out) *out = s_stereo; }
|
||||
int psx_mod_set_stereo_presentation(uint32_t mode) {
|
||||
return mode <= 1u ? gl_renderer_stereo_set_presentation(mode) : 0;
|
||||
}
|
||||
int psx_mod_render_view_offset(int32_t x, int32_t y, int32_t z) {
|
||||
int32_t xyz[3] = {x, y, z};
|
||||
if (!g_psx_render_pass_active) return 0;
|
||||
gte_render_view_set(xyz);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
int psx_mod_render_view(const PSXModRenderView *view) {
|
||||
if (!g_psx_render_pass_active || !view || view->struct_size < sizeof *view ||
|
||||
view->projection > 1u || view->projection_h_ref > 65535u) return 0;
|
||||
/* Bound products well inside int64. Rigid matrices are supplied by caller. */
|
||||
for (int i = 0; i < 9; ++i)
|
||||
if (view->rotation_q12[i] < -4096 || view->rotation_q12[i] > 4096) return 0;
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (view->translation[i] < -65536 || view->translation[i] > 65536) return 0;
|
||||
if (view->projection && (view->fx_q16 <= 0 || view->fy_q16 <= 0)) return 0;
|
||||
gte_render_pose_set(view);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int passes_allowed(void) {
|
||||
return pass_status() == PSX_MOD_RENDER_PASS_READY;
|
||||
}
|
||||
@@ -431,17 +494,21 @@ static int checkpoint_save(const CPUState *cpu) {
|
||||
/* The whole live RAM: a pass's stores fold through the same geometry
|
||||
* (render_pass_store_to), so every byte one can reach is in here. */
|
||||
uint32_t ram_bytes = memory_get_ram_bytes();
|
||||
s_checkpoint_failure = NULL;
|
||||
if (s_ram_copy_cap < ram_bytes) {
|
||||
uint8_t *p = (uint8_t *)realloc(s_ram_copy, ram_bytes);
|
||||
if (!p) return 0;
|
||||
if (!p) { s_checkpoint_failure = "checkpoint_ram"; return 0; }
|
||||
s_ram_copy = p;
|
||||
s_ram_copy_cap = ram_bytes;
|
||||
}
|
||||
if (!s_dma_copy) {
|
||||
s_dma_copy = (uint8_t *)malloc(dma_len);
|
||||
if (!s_dma_copy) return 0;
|
||||
if (!s_dma_copy) { s_checkpoint_failure = "checkpoint_dma"; return 0; }
|
||||
}
|
||||
if (!gpu_pass_checkpoint_save()) {
|
||||
s_checkpoint_failure = "checkpoint_gpu";
|
||||
return 0;
|
||||
}
|
||||
if (!gpu_pass_checkpoint_save()) return 0;
|
||||
s_ck.cpu = *cpu;
|
||||
s_ck.ram_bytes = ram_bytes;
|
||||
memcpy(s_ram_copy, memory_get_ram_ptr(), ram_bytes);
|
||||
@@ -527,27 +594,54 @@ static void note_fault(const char *what) {
|
||||
}
|
||||
}
|
||||
|
||||
int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
PSXModRenderPassFn fn, void *user) {
|
||||
static int transaction_end(int eye, uint32_t alpha, int keep) {
|
||||
if (eye >= 0) return gl_renderer_stereo_end((uint32_t)eye, keep);
|
||||
gl_renderer_pass_end(alpha, keep);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int render_transaction(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
PSXModRenderPassFn fn, void *user, int eye) {
|
||||
uint64_t t0, t1, tb, tg, te, tr, hash_before = 0, hash_after = 0;
|
||||
uint64_t cycles_before;
|
||||
uint32_t leaks;
|
||||
int ok = 0, open, reuse;
|
||||
static uint32_t s_leaks_before;
|
||||
uint32_t status;
|
||||
|
||||
memset(&s_attempt, 0, sizeof s_attempt);
|
||||
s_attempt.attempt = ++s_stats.pass_attempts;
|
||||
s_attempt.plan = s_plan_serial;
|
||||
s_attempt.guest_cycle = psx_cycle_count;
|
||||
if (pass) {
|
||||
s_attempt.struct_size = pass->struct_size;
|
||||
if (pass->struct_size >= sizeof *pass) {
|
||||
s_attempt.alpha_q16 = pass->alpha_q16;
|
||||
s_attempt.x = pass->x; s_attempt.y = pass->y;
|
||||
s_attempt.w = pass->w; s_attempt.h = pass->h;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cpu || !pass || !fn || pass->struct_size < sizeof *pass ||
|
||||
pass->w == 0 || pass->h == 0 || pass->alpha_q16 == 0 ||
|
||||
pass->alpha_q16 >= 65536u)
|
||||
return 0;
|
||||
if (!passes_allowed()) return 0;
|
||||
pass->w == 0 || pass->h == 0 || (eye < 0 &&
|
||||
(pass->alpha_q16 == 0 || pass->alpha_q16 >= 65536u))) {
|
||||
s_stats.argument_refused++;
|
||||
return pass_refuse("arguments", transaction_status(eye >= 0));
|
||||
}
|
||||
status = transaction_status(eye >= 0);
|
||||
if (status != PSX_MOD_RENDER_PASS_READY) {
|
||||
s_stats.status_refused++;
|
||||
return pass_refuse("status", status);
|
||||
}
|
||||
|
||||
/* An overlay DLL may still hold cycles it has not published. They belong
|
||||
* to the live timeline: publish them now, or the pass's first store would
|
||||
* publish them into the frozen clock and the restore would drop them. */
|
||||
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
|
||||
|
||||
s_attempt.guest_cycle = psx_cycle_count;
|
||||
t0 = gl_renderer_perf_ticks();
|
||||
open = s_open_generation;
|
||||
open = eye < 0 ? s_open_generation : 0;
|
||||
{
|
||||
static double inv = 0.0;
|
||||
if (inv == 0.0) inv = 1000.0 / (double)gl_renderer_perf_frequency();
|
||||
@@ -557,21 +651,35 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
s_restored_cycle == psx_cycle_count &&
|
||||
s_restored_stores == g_guest_store_count;
|
||||
/* Frame N's own image is captured by the first pass after a plan. */
|
||||
if (!gl_renderer_pass_begin(pass->x, pass->y, pass->w, pass->h, open,
|
||||
s_plan_period, reuse))
|
||||
return 0;
|
||||
if (!(eye >= 0 ? gl_renderer_stereo_begin(pass->x, pass->y, pass->w,
|
||||
pass->h, reuse)
|
||||
: gl_renderer_pass_begin(pass->x, pass->y, pass->w,
|
||||
pass->h, open, s_plan_period, reuse))) {
|
||||
gl_renderer_pass_begin_diag(&s_attempt.gl);
|
||||
s_stats.begin_refused++;
|
||||
return pass_refuse(s_attempt.gl.reason ? s_attempt.gl.reason : "gl_begin",
|
||||
s_attempt.gl.status);
|
||||
}
|
||||
s_open_generation = 0;
|
||||
s_leaks_before = gl_renderer_pass_leaks();
|
||||
|
||||
if (!checkpoint_save(cpu)) {
|
||||
gl_renderer_pass_end(0, 0);
|
||||
return 0;
|
||||
gl_renderer_pass_begin_diag(&s_attempt.gl);
|
||||
s_stats.checkpoint_refused++;
|
||||
/* Capture the rejection status before rolling the GL transaction back. */
|
||||
status = transaction_status(eye >= 0);
|
||||
transaction_end(eye, 0, 0);
|
||||
return pass_refuse(s_checkpoint_failure ? s_checkpoint_failure : "checkpoint", status);
|
||||
}
|
||||
if (verify_on()) hash_before = state_hash(cpu);
|
||||
|
||||
tb = gl_renderer_perf_ticks();
|
||||
s_nesting = 1;
|
||||
cycles_before = psx_cycle_count;
|
||||
if (eye >= 0) {
|
||||
s_stereo.eye_cycle[eye] = cycles_before;
|
||||
s_stereo.eye_hash[eye] = verify_on() ? hash_before : 0;
|
||||
}
|
||||
(void)psx_cycle_freeze_begin(&s_freeze, watchdog_cycles(),
|
||||
watchdog_overrun);
|
||||
if (setjmp(s_abort_jmp) == 0) {
|
||||
@@ -582,6 +690,7 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
if (!ok) s_stats.discarded++;
|
||||
if (verify_on() && !nesting_balanced(&s_ck.nest)) {
|
||||
s_stats.verify_mismatch++;
|
||||
if (eye >= 0) ok = 0;
|
||||
fprintf(stderr, "psxrecomp: RENDER PASS VERIFY mismatch "
|
||||
"(host nesting not balanced after the pass)\n");
|
||||
}
|
||||
@@ -595,9 +704,12 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
if (nesting_describe(&s_ck.nest, s_freeze.bb_defer, s_abort_detail,
|
||||
sizeof s_abort_detail))
|
||||
s_stats.nesting_repairs++;
|
||||
snprintf(s_stats.last_abort_detail, sizeof s_stats.last_abort_detail,
|
||||
"%s", s_abort_detail);
|
||||
ok = 0;
|
||||
}
|
||||
s_stats.guest_cycles_last = psx_cycle_count - cycles_before;
|
||||
if (eye >= 0) gte_render_view_get(s_stereo.eye_view[eye]);
|
||||
tg = gl_renderer_perf_ticks();
|
||||
s_pass_cpu = NULL;
|
||||
|
||||
@@ -607,7 +719,10 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
ok = 0;
|
||||
}
|
||||
/* Capture (when kept), then roll the VRAM rect and renderer back. */
|
||||
gl_renderer_pass_end(ok ? pass->alpha_q16 : 0, ok);
|
||||
if (!transaction_end(eye, ok ? pass->alpha_q16 : 0, ok)) {
|
||||
ok = 0;
|
||||
if (eye >= 0) s_stereo.last_failure = "eye_capture";
|
||||
}
|
||||
te = gl_renderer_perf_ticks();
|
||||
checkpoint_restore(cpu);
|
||||
psx_cycle_freeze_end(&s_freeze);
|
||||
@@ -627,6 +742,7 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
s_stats.verify_checks++;
|
||||
if (hash_after != hash_before || !gl_renderer_pass_verify_vram()) {
|
||||
s_stats.verify_mismatch++;
|
||||
if (eye >= 0) { ok = 0; s_stereo.last_failure = "restore_verify"; }
|
||||
if (s_stats.verify_mismatch <= 8)
|
||||
fprintf(stderr, "psxrecomp: RENDER PASS VERIFY mismatch "
|
||||
"(state %016llx -> %016llx)\n",
|
||||
@@ -643,12 +759,112 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
s_stats.avg_pass_ms = s_stats.avg_pass_ms > 0.0
|
||||
? s_stats.avg_pass_ms * 0.9 + ms * 0.1 : ms;
|
||||
}
|
||||
gl_renderer_pass_note_cost(t1 - t0);
|
||||
if (eye < 0) gl_renderer_pass_note_cost(t1 - t0);
|
||||
if (ok) s_stats.passes++;
|
||||
else if (leaks) note_fault("VRAM write outside the pass rect");
|
||||
else if (s_stats.watchdog_flag) note_fault("guest-cycle watchdog");
|
||||
s_stats.watchdog_flag = 0;
|
||||
/* Present anything that fell due while the pass ran. */
|
||||
gl_renderer_pass_service_presents();
|
||||
if (eye < 0) gl_renderer_pass_service_presents();
|
||||
return ok;
|
||||
}
|
||||
|
||||
int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
|
||||
PSXModRenderPassFn fn, void *user) {
|
||||
return render_transaction(cpu, pass, fn, user, -1);
|
||||
}
|
||||
|
||||
typedef struct StereoCallback {
|
||||
PSXModStereoFn fn;
|
||||
void *user;
|
||||
uint32_t eye;
|
||||
} StereoCallback;
|
||||
static int stereo_eye_draw(CPUState *cpu, void *user, uint32_t unused) {
|
||||
StereoCallback *c = (StereoCallback *)user;
|
||||
(void)unused;
|
||||
return c->fn(cpu, c->user, c->eye);
|
||||
}
|
||||
|
||||
int psx_mod_render_stereo(CPUState *cpu, const PSXModStereoFrame *frame,
|
||||
PSXModStereoFn fn, void *user) {
|
||||
PSXModRenderPass rect;
|
||||
StereoCallback call;
|
||||
uint64_t t0, cycle, stores, mismatches, aborted;
|
||||
double budget_ms, elapsed;
|
||||
uint32_t status;
|
||||
int ok = 1;
|
||||
/* Refuse re-entry without changing the outer pair's counters/selector. */
|
||||
if (s_pair_busy) return 0;
|
||||
s_stereo.attempts++;
|
||||
s_stereo.last_eye = -1;
|
||||
if (!cpu || !frame || !fn || frame->struct_size < sizeof *frame ||
|
||||
!frame->w || !frame->h || !frame->period_vblanks ||
|
||||
frame->period_vblanks > 8u || (uint32_t)frame->x + frame->w > 1024u ||
|
||||
(uint32_t)frame->y + frame->h > 512u) {
|
||||
s_stereo.refused++; s_stereo.last_failure = "arguments"; return 0;
|
||||
}
|
||||
status = transaction_status(1);
|
||||
if (status != PSX_MOD_RENDER_PASS_READY) {
|
||||
s_stereo.refused++; s_stereo.last_failure = "status"; return 0;
|
||||
}
|
||||
budget_ms = frame->period_vblanks * (double)g_psx_vblank_cycles /
|
||||
33868.8 * 0.8;
|
||||
if (s_stereo.avg_pair_ms > budget_ms && ++s_pair_retry < 30u) {
|
||||
s_stereo.shed++; return 0;
|
||||
}
|
||||
s_pair_retry = 0;
|
||||
if (g_overlay_flush_pending_cycles) g_overlay_flush_pending_cycles();
|
||||
cycle = psx_cycle_count; stores = g_guest_store_count;
|
||||
mismatches = s_stats.verify_mismatch; aborted = s_stats.aborted;
|
||||
s_stereo.last_failure = NULL;
|
||||
s_stereo.eye_cycle[0] = s_stereo.eye_cycle[1] = 0;
|
||||
s_stereo.eye_hash[0] = s_stereo.eye_hash[1] = 0;
|
||||
gl_renderer_stereo_stage_reset();
|
||||
memset(&rect, 0, sizeof rect);
|
||||
rect.struct_size = sizeof rect;
|
||||
rect.x = frame->x; rect.y = frame->y; rect.w = frame->w; rect.h = frame->h;
|
||||
call.fn = fn; call.user = user;
|
||||
s_pair_busy = 1;
|
||||
s_plan_serial++; /* separate the backup lease from any temporal plan */
|
||||
t0 = gl_renderer_perf_ticks();
|
||||
for (call.eye = 0; call.eye < 2u; call.eye++) {
|
||||
s_stereo.last_eye = (int)call.eye;
|
||||
if (psx_cycle_count != cycle || g_guest_store_count != stores) {
|
||||
ok = 0; s_stereo.last_failure = "checkpoint_changed"; break;
|
||||
}
|
||||
if (!render_transaction(cpu, &rect, stereo_eye_draw, &call, (int)call.eye)) {
|
||||
ok = 0;
|
||||
if (!s_stereo.last_failure) s_stereo.last_failure =
|
||||
s_stats.aborted != aborted ? "eye_abort" : "eye_refused_or_discarded";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (s_stats.verify_mismatch != mismatches ||
|
||||
s_stereo.eye_cycle[0] != s_stereo.eye_cycle[1] ||
|
||||
(verify_on() && s_stereo.eye_hash[0] != s_stereo.eye_hash[1])) {
|
||||
ok = 0;
|
||||
if (!s_stereo.last_failure) s_stereo.last_failure = "eye_checkpoint_mismatch";
|
||||
}
|
||||
elapsed = (double)(gl_renderer_perf_ticks() - t0) * 1000.0 /
|
||||
(double)gl_renderer_perf_frequency();
|
||||
s_stereo.last_pair_ms = elapsed;
|
||||
s_stereo.avg_pair_ms = s_stereo.avg_pair_ms > 0.0
|
||||
? s_stereo.avg_pair_ms * 0.9 + elapsed * 0.1 : elapsed;
|
||||
if (ok && !gl_renderer_stereo_publish(s_stereo.attempts, cycle, s_stereo.eye_view)) {
|
||||
ok = 0; s_stereo.last_failure = "pair_publish";
|
||||
}
|
||||
if (ok) {
|
||||
s_stereo.pairs++;
|
||||
s_stereo.last_pair_id = s_stereo.attempts;
|
||||
s_stereo.last_guest_cycle = cycle;
|
||||
} else {
|
||||
s_stereo.failed++;
|
||||
s_stereo.last_failed_attempt = s_stereo.attempts;
|
||||
s_stereo.last_failed_eye = s_stereo.last_eye;
|
||||
s_stereo.last_failed_reason = s_stereo.last_failure;
|
||||
s_stereo.retained_pair_id = s_stereo.last_pair_id;
|
||||
gl_renderer_stereo_stage_reset();
|
||||
}
|
||||
s_pair_busy = 0;
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -315,3 +315,10 @@ void render_pass_journal_free(RenderPassJournal *j) {
|
||||
}
|
||||
j->n = 0;
|
||||
}
|
||||
|
||||
int render_pass_stereo_pair_fresh(uint64_t pair_cycle, uint64_t now_cycle,
|
||||
uint32_t vblank_cycles) {
|
||||
if (now_cycle < pair_cycle) return 0;
|
||||
return now_cycle - pair_cycle <=
|
||||
(uint64_t)RENDER_PASS_STEREO_MAX_AGE_VBLANKS * vblank_cycles;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "vr_pose_math.h"
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
static int matrix(const double q[4], double m[9]) {
|
||||
double n = 0; for(int i=0;i<4;i++) { if(!isfinite(q[i])) return 0; n+=q[i]*q[i]; }
|
||||
if(n < .5 || n > 1.5) return 0;
|
||||
double x=q[0]/sqrt(n),y=q[1]/sqrt(n),z=q[2]/sqrt(n),w=q[3]/sqrt(n);
|
||||
m[0]=1-2*(y*y+z*z);m[1]=2*(x*y-z*w);m[2]=2*(x*z+y*w);
|
||||
m[3]=2*(x*y+z*w);m[4]=1-2*(x*x+z*z);m[5]=2*(y*z-x*w);
|
||||
m[6]=2*(x*z-y*w);m[7]=2*(y*z+x*w);m[8]=1-2*(x*x+y*y);
|
||||
return 1;
|
||||
}
|
||||
int vr_pose_to_view(const double q[4], const double p[3],
|
||||
const double oq[4], const double op[3],
|
||||
const double f[4], double units, int w, int h,
|
||||
PSXModRenderView *out) {
|
||||
double e[9],o[9];const int sign[3]={1,-1,-1};
|
||||
if(!out || !matrix(q,e) || !matrix(oq,o) || !isfinite(units) ||
|
||||
units < 1 || units > 65536 || w<=0 || h<=0 || w>1024 || h>512) return 0;
|
||||
for(int i=0;i<3;i++) if(!isfinite(p[i]) || !isfinite(op[i])) return 0;
|
||||
for(int i=0;i<4;i++) if(!isfinite(f[i]) || fabs(f[i])>=1.56) return 0;
|
||||
double l=tan(f[0]),r=tan(f[1]),u=tan(f[2]),d=tan(f[3]);
|
||||
if(r-l<=.05 || u-d<=.05) return 0;
|
||||
PSXModRenderView v={0};v.struct_size=sizeof v;v.projection=1;
|
||||
for(int row=0;row<3;row++) {
|
||||
for(int col=0;col<3;col++) {
|
||||
double a=0;for(int k=0;k<3;k++)a+=e[k*3+row]*o[k*3+col];
|
||||
v.rotation_q12[row*3+col]=(int32_t)lround(a*sign[row]*sign[col]*4096);
|
||||
}
|
||||
double a=0;for(int k=0;k<3;k++)a+=e[k*3+row]*(op[k]-p[k]);
|
||||
a*=sign[row]*units;if(fabs(a)>65536) return 0;
|
||||
v.translation[row]=(int32_t)lround(a);
|
||||
}
|
||||
double fx=w/(r-l),fy=h/(u-d),cx=-l*fx-w*.5,cy=u*fy-h*.5;
|
||||
double pix[4]={fx,fy,cx,cy};
|
||||
for(int i=0;i<4;i++) if(!isfinite(pix[i]) || fabs(pix[i])>32767) return 0;
|
||||
v.fx_q16=(int32_t)lround(fx*65536);v.fy_q16=(int32_t)lround(fy*65536);
|
||||
v.cx_delta_q16=(int32_t)lround(cx*65536);v.cy_delta_q16=(int32_t)lround(cy*65536);
|
||||
*out=v;return 1;
|
||||
}
|
||||
int vr_pose_to_transform(const double q[4],const double p[3],
|
||||
const double oq[4],const double op[3],double units,
|
||||
double rotation[9],double translation[3]) {
|
||||
double e[9],o[9],r[9],t[3];const int sign[3]={1,-1,-1};
|
||||
if(!rotation || !translation || !matrix(q,e) || !matrix(oq,o) ||
|
||||
!isfinite(units) || units<1 || units>65536)return 0;
|
||||
for(int i=0;i<3;i++)if(!isfinite(p[i]) || !isfinite(op[i]))return 0;
|
||||
for(int row=0;row<3;row++) {
|
||||
for(int col=0;col<3;col++) {
|
||||
double a=0;for(int k=0;k<3;k++)a+=o[k*3+row]*e[k*3+col];
|
||||
r[row*3+col]=a*sign[row]*sign[col];
|
||||
}
|
||||
double a=0;for(int k=0;k<3;k++)a+=o[k*3+row]*(p[k]-op[k]);
|
||||
t[row]=a*sign[row]*units;if(!isfinite(t[row]) || fabs(t[row])>65536)return 0;
|
||||
}
|
||||
memcpy(rotation,r,sizeof r);memcpy(translation,t,sizeof t);return 1;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "psx_disasm.h"
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
static void require(bool condition, const char* expression, int line) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "FAIL line %d: %s\n", line, expression);
|
||||
std::exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
#define CHECK(expr) require((expr), #expr, __LINE__)
|
||||
static void check(unsigned word, unsigned pc, const char* expected) {
|
||||
char out[128];
|
||||
int n=psx_disasm_one(word, pc, out, sizeof(out));
|
||||
if (std::strcmp(out,expected)) {
|
||||
std::fprintf(stderr,"%08X: expected [%s], got [%s]\n",word,expected,out);
|
||||
CHECK(false);
|
||||
}
|
||||
CHECK(n == (int)std::strlen(expected));
|
||||
}
|
||||
int main() {
|
||||
check(0,0,"NOP");
|
||||
check(0x012A4020,0,"ADD $t0, $t1, $t2");
|
||||
check(0x012A0018,0,"MULT $t1, $t2");
|
||||
check(0x012A001B,0,"DIVU $t1, $t2");
|
||||
check(0x012A4004,0,"SLLV $t0, $t2, $t1");
|
||||
check(0x0521FFFF,0x80001000,"BGEZ $t1, 0x80001000");
|
||||
check(0x1920FFFE,0x80001000,"BLEZ $t1, 0x80000FFC");
|
||||
check(0x112AFFFE,0x80001000,"BEQ $t1, $t2, 0x80000FFC");
|
||||
check(0x08000000,0x8FFFFFFC,"J 0x90000000");
|
||||
check(0x3128FFFF,0,"ANDI $t0, $t1, 0xFFFF");
|
||||
check(0x2528FFFF,0,"ADDIU $t0, $t1, -1");
|
||||
check(0x8D28FFF8,0,"LW $t0, -8($t1)");
|
||||
check(0xC923FFF8,0,"LWC2 $VZ1, -8($t1)");
|
||||
check(0x40086000,0,"MFC0 $t0, $12");
|
||||
check(0x42000010,0,"RFE");
|
||||
check(0x48086800,0,"MFC2 $t0, $SXY1");
|
||||
check(0x4848D000,0,"CFC2 $t0, $H");
|
||||
check(0x4A080001,0,"RTPS 0x0080001");
|
||||
check(0x48200001,0,".word 0x48200001");
|
||||
check(0xFC000000,0,".word 0xFC000000");
|
||||
char tiny[4]={'x','x','x','x'};
|
||||
CHECK(psx_disasm_one(0,0,tiny,4)==3 && tiny[3]==0);
|
||||
CHECK(psx_disasm_one(0,0,tiny,1)==0 && tiny[0]==0);
|
||||
CHECK(psx_disasm_one(0,0,nullptr,4)==0);
|
||||
tiny[0]='x'; CHECK(psx_disasm_one(0,0,tiny,0)==0 && tiny[0]=='x');
|
||||
}
|
||||
@@ -289,10 +289,16 @@ class RenderPassGuards(unittest.TestCase):
|
||||
refuse = body(GL, "uint32_t gl_renderer_pass_unavailable(void)")
|
||||
self.assertRegex(strip_comments(refuse),
|
||||
r"if \([^;{}]*\bs_hiw\b[^;{}]*\)\s*return PSX_MOD_RENDER_PASS_BACKEND;")
|
||||
# Every way in goes through that refusal.
|
||||
for sig in ("int gl_renderer_pass_begin(", "uint32_t gl_renderer_pass_plan("):
|
||||
if sig in GL:
|
||||
self.assertIn("gl_renderer_pass_ready()", body(GL, sig), sig)
|
||||
self.assertIn("gl_renderer_pass_ready()", body(GL, "uint32_t gl_renderer_pass_plan("))
|
||||
begin = body(GL, "int gl_renderer_pass_begin(")
|
||||
if "transaction_begin(" in begin:
|
||||
self.assertIn("period, reuse, 0)", begin)
|
||||
begin = body(GL, "static int transaction_begin(")
|
||||
status = "s_pass_begin_diag.status"
|
||||
self.assertIn("gl_renderer_pass_unavailable()", begin)
|
||||
self.assertRegex(begin, r"if \(s_pass_begin_diag\.status != PSX_MOD_RENDER_PASS_READY\)\s*"
|
||||
r'return pass_begin_refuse\("gl_status"\);')
|
||||
self.assertLess(begin.index(status + " !="), begin.index("flush_flat_batch()"))
|
||||
self.assertIn("gl_renderer_pass_unavailable() == PSX_MOD_RENDER_PASS_READY",
|
||||
body(GL, "int gl_renderer_pass_ready(void)"))
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;ret
|
||||
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
|
||||
static uint16_t image[1024*512], oracle[1024*512];
|
||||
int g_psx_vram_dirty_tracking=0;
|
||||
uint64_t psx_cycle_count=0;uint32_t g_psx_vblank_cycles=564480u;
|
||||
uint64_t s_frame_count=0;
|
||||
void gpu_vram_dirty_mark_row_impl(uint32_t y){}
|
||||
void gpu_vram_dirty_mark_rect(int x,int y,int w,int h){}
|
||||
@@ -65,6 +66,82 @@ static void verify_bank_batching(void) {
|
||||
psx_mod_set_texture_bank_batching(0);s_tex_filter=0;
|
||||
glb_set_mask_bits(0,0);glb_set_semi_transparency(0,0);
|
||||
}
|
||||
static void verify_stereo_transactions(void) {
|
||||
uint16_t before[16*16], after[16*16];
|
||||
const int32_t views[2][3]={{-24,0,0},{24,0,0}};
|
||||
GLRenderStereoDiag diag;
|
||||
GLRenderStereoCapture record;
|
||||
glb_set_mask_bits(0,0); glb_set_semi_transparency(0,0);
|
||||
check(gl_renderer_fbo_peek(40,40,16,16,before),"stereo canonical snapshot");
|
||||
check(gl_renderer_stereo_unavailable()==PSX_MOD_RENDER_PASS_READY,"stereo GL ready");
|
||||
check(!gl_renderer_stereo_begin(-1,40,16,16,0),"stereo rejects invalid rectangle");
|
||||
s_hiw=1;
|
||||
check(gl_renderer_stereo_unavailable()==PSX_MOD_RENDER_PASS_BACKEND,"stereo refuses windowed tiles");
|
||||
check(!gl_renderer_stereo_begin(40,40,16,16,0),"stereo window refusal before GPU work");
|
||||
s_hiw=0;
|
||||
test_depth24=1;
|
||||
check(!gl_renderer_stereo_begin(40,40,16,16,0),"stereo refuses depth24");
|
||||
test_depth24=0;
|
||||
gl_renderer_stereo_stage_reset();
|
||||
check(gl_renderer_stereo_begin(40,40,16,16,0),"left eye transaction begins");
|
||||
glb_draw_flat_rect(40,40,16,16,0x001f);
|
||||
check(gl_renderer_stereo_end(0,1),"left eye captured and restored");
|
||||
check(!gl_renderer_stereo_publish(41,91000,views),"partial pair cannot publish");
|
||||
gl_renderer_stereo_diag(&diag);
|
||||
check(!diag.valid && diag.staged_mask==1,"partial left stays private");
|
||||
check(gl_renderer_stereo_begin(40,40,16,16,1),"right eye shares canonical backup");
|
||||
glb_draw_flat_rect(40,40,16,16,0x7c00);
|
||||
check(gl_renderer_stereo_end(1,1),"right eye captured and restored");
|
||||
check(gl_renderer_stereo_publish(41,91000,views),"complete pair publishes atomically");
|
||||
check(gl_renderer_fbo_peek(40,40,16,16,after),"stereo canonical readback");
|
||||
check(memcmp(before,after,sizeof before)==0,"eye draws restore canonical VRAM pixels");
|
||||
size_t bytes=(size_t)16*16*s_hr_scale*s_hr_scale*3;
|
||||
uint8_t *left=malloc(bytes),*right=malloc(bytes);
|
||||
if(!left || !right){check(0,"stereo readback allocation");free(left);free(right);return;}
|
||||
StereoPair *pair=&s_stereo_pair[s_stereo_current];
|
||||
glPixelStorei(GL_PACK_ALIGNMENT,1);
|
||||
glBindTexture(GL_TEXTURE_2D,pair->tex[0]);glGetTexImage(GL_TEXTURE_2D,0,GL_RGB,GL_UNSIGNED_BYTE,left);
|
||||
glBindTexture(GL_TEXTURE_2D,pair->tex[1]);glGetTexImage(GL_TEXTURE_2D,0,GL_RGB,GL_UNSIGNED_BYTE,right);
|
||||
glBindTexture(GL_TEXTURE_2D,0);
|
||||
check(left[0]>200 && left[2]==0,"left texture contains left draw");
|
||||
check(right[2]>200 && right[0]==0,"right texture contains right draw");
|
||||
check(memcmp(left,right,bytes)!=0,"both eye textures are independently captured");
|
||||
gl_renderer_stereo_stage_reset();
|
||||
check(gl_renderer_stereo_begin(40,40,16,16,0),"next left starts");
|
||||
glb_draw_flat_rect(40,40,16,16,0x03e0);
|
||||
check(gl_renderer_stereo_end(0,1),"next left stages");
|
||||
check(gl_renderer_stereo_begin(40,40,16,16,1),"next right starts");
|
||||
check(gl_renderer_stereo_end(1,0),"failed right restores without capture");
|
||||
check(!gl_renderer_stereo_publish(42,100000,views),"failed pair does not publish");
|
||||
gl_renderer_stereo_diag(&diag);
|
||||
check(diag.valid && diag.pair_id==41,"failed pair retains complete published pair");
|
||||
pair=&s_stereo_pair[s_stereo_current];
|
||||
glBindTexture(GL_TEXTURE_2D,pair->tex[0]);glGetTexImage(GL_TEXTURE_2D,0,GL_RGB,GL_UNSIGNED_BYTE,right);
|
||||
check(memcmp(left,right,bytes)==0,"failed staging preserves published texture pixels");
|
||||
glBindTexture(GL_TEXTURE_2D,0);
|
||||
free(left);free(right);
|
||||
gl_renderer_stereo_dump_arm(".",1);
|
||||
check(gl_renderer_stereo_capture_records(&record,1)==0,"capture request starts empty");
|
||||
gl_renderer_stereo_stage_reset();
|
||||
for(uint32_t eye=0;eye<2;eye++){
|
||||
check(gl_renderer_stereo_begin(40,40,16,16,eye!=0),"capture eye begins");
|
||||
glb_draw_flat_rect(40,40,16,16,eye?0x7c00:0x001f);
|
||||
check(gl_renderer_stereo_end(eye,1),"capture eye completes");
|
||||
}
|
||||
check(gl_renderer_stereo_publish(43,110000,views),"capture publishes complete pair");
|
||||
check(gl_renderer_stereo_capture_records(&record,1)==1,"capture metadata retained for TCP");
|
||||
check(record.pair_id==43 && record.guest_cycle==110000 && record.view_offset[1][0]==24,
|
||||
"capture metadata belongs to exact exported pair");
|
||||
check(record.width==16u*s_hr_scale && record.height==16u*s_hr_scale,"capture dimensions match textures");
|
||||
FILE *file=fopen("p000043_left.png","rb");check(file!=NULL,"left PNG evidence exported");if(file)fclose(file);
|
||||
file=fopen("p000043_right.png","rb");check(file!=NULL,"right PNG evidence exported");if(file)fclose(file);
|
||||
file=fopen("p000043_sbs.png","rb");check(file!=NULL,"SBS PNG evidence exported");if(file)fclose(file);
|
||||
file=fopen("p000043.json","rb");check(file==NULL,"runtime does not write diagnostic JSON");if(file)fclose(file);
|
||||
gl_renderer_stereo_reset();
|
||||
check(gl_renderer_stereo_capture_records(&record,1)==0,"session reset clears capture metadata");
|
||||
check(glGetError()==GL_NO_ERROR,"stereo transaction GL error");
|
||||
}
|
||||
|
||||
static void verify_oversize_wide_geometry(int scale) {
|
||||
/* Captured hallway triangles exceed the PS1 height limit. Proven geometry
|
||||
* must use identical canonical/wide passes, including the center-copy edge. */
|
||||
@@ -296,6 +373,7 @@ int main(int argc,char **argv){
|
||||
check(glb_vram_read(482,252)==0x001f,"palette update visible without replacing indices");
|
||||
verify("retained indices with animated guest CLUT");
|
||||
verify_bank_batching();
|
||||
verify_stereo_transactions();
|
||||
verify_oversize_wide_geometry(scale);
|
||||
verify_camera_plane_clip(scale);
|
||||
/* World and UI use different origins in an anchored wide frame. Keep the
|
||||
|
||||
@@ -80,6 +80,10 @@ int g_ws_tex_edge_pct=0;
|
||||
int psx_ws_prim_is_tagged(void){return 0;}
|
||||
void psx_ws_dbg_gate_frame_snapshot(void){}
|
||||
void gpu_depth24_upload_span_reset(void){}
|
||||
/* Guest clock the renderer's stereo-pair freshness reads; test stand-ins, as
|
||||
* in test_gl_readback_region.c. */
|
||||
uint64_t psx_cycle_count=0;
|
||||
uint32_t g_psx_vblank_cycles=564480u;
|
||||
|
||||
static uint16_t vram[1024*512], peek[1024*512];
|
||||
static int si_max_dim(void) { return s_gl_max_dim > 0 ? s_gl_max_dim : 1 << 30; }
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
#include "cpu_state.h"
|
||||
#include "gte.h"
|
||||
#include "pgxp.h"
|
||||
#include "projection_scale.hpp"
|
||||
#include "gte_view.h"
|
||||
#include "gte_nclip_stats.h"
|
||||
#include "render_pass_projection.h"
|
||||
#include <limits>
|
||||
extern "C" void gte_set_fov_scale(int, int);
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
@@ -10,6 +14,13 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#define CHECK(expr) do { \
|
||||
if (!(expr)) { \
|
||||
std::fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
using PSXRecomp::GTE::GTEState;
|
||||
using PSXRecomp::GTE::gte_cfc2;
|
||||
using PSXRecomp::GTE::gte_ctc2;
|
||||
@@ -56,6 +67,8 @@ extern "C" void gpu_pgxp_rederive_enable(void) {}
|
||||
static int g_test_shadow_diff = 0;
|
||||
extern "C" int psx_overlay_shadow_diff_active(void) { return g_test_shadow_diff; }
|
||||
extern "C" void psx_ws_note_gte_project(int) {}
|
||||
static int g_test_netplay_active = 0;
|
||||
extern "C" int psx_netplay_active(void) { return g_test_netplay_active; }
|
||||
extern "C" {
|
||||
uint64_t s_frame_count = 0;
|
||||
uint32_t g_debug_last_store_pc = 0;
|
||||
@@ -998,6 +1011,152 @@ int test_precision_speculative_transaction() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_render_pose() {
|
||||
PSXModRenderView pose = {};
|
||||
pose.struct_size = sizeof pose;
|
||||
pose.rotation_q12[2] = 4096; pose.rotation_q12[4] = 4096;
|
||||
pose.rotation_q12[6] = -4096;
|
||||
GTEState g;
|
||||
g.RT[0][0] = g.RT[1][1] = g.RT[2][2] = 4096;
|
||||
g.V0[0] = -800; g.V0[2] = 200; g.H = 400;
|
||||
gte_render_pose_set(&pose);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&g, g.V0, true);
|
||||
if (g.MAC1 != 200 || g.MAC3 != 800 || g.TR[0] != 0)
|
||||
return fail_value("rigid rotation before division",0,0,0,800,g.MAC3);
|
||||
pose = {}; pose.struct_size = sizeof pose;
|
||||
pose.rotation_q12[0] = pose.rotation_q12[4] = pose.rotation_q12[8] = 4096;
|
||||
pose.projection = 1; pose.fx_q16 = 200 << 16; pose.fy_q16 = 100 << 16;
|
||||
pose.cx_delta_q16 = 10 << 16; pose.cy_delta_q16 = -5 * 65536;
|
||||
g = GTEState(); g.RT[0][0] = g.RT[1][1] = g.RT[2][2] = 4096;
|
||||
g.V0[0] = 80; g.V0[1] = 160; g.V0[2] = 800;
|
||||
gte_render_pose_set(&pose);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&g, g.V0, true);
|
||||
if ((int16_t)g.SXY[2] != 30 || (int16_t)(g.SXY[2] >> 16) != 15)
|
||||
return fail_value("asymmetric projection",0,0,0,30,g.SXY[2] & 65535);
|
||||
pose.projection_h_ref = 400; g.H = 133;
|
||||
gte_render_pose_set(&pose);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&g, g.V0, true);
|
||||
if ((int16_t)g.SXY[2] != 16 || (int16_t)(g.SXY[2] >> 16) != 1)
|
||||
return fail_value("authored focal ratio",0,0,0,16,g.SXY[2] & 65535);
|
||||
pose = {}; gte_render_pose_set(&pose);
|
||||
/* Composition with [video] fov_scale (scaled guest H): an absolute override
|
||||
* (projection_h_ref == 0) replaces the guest projection entirely, so the
|
||||
* result does not depend on fov_scale; with projection_h_ref the focal
|
||||
* lengths follow the fov-scaled guest H. Outside the override (pose reset)
|
||||
* the guest path, including fov_scale, is untouched. */
|
||||
{
|
||||
GTEState base;
|
||||
base.RT[0][0] = base.RT[1][1] = base.RT[2][2] = 4096;
|
||||
base.V0[0] = 80; base.V0[1] = 160; base.V0[2] = 800;
|
||||
base.H = 400; base.OFX = 256 << 16; base.OFY = 120 << 16;
|
||||
PSXModRenderView ov = {}; ov.struct_size = sizeof ov;
|
||||
ov.rotation_q12[0] = ov.rotation_q12[4] = ov.rotation_q12[8] = 4096;
|
||||
ov.projection = 1; ov.fx_q16 = 200 << 16; ov.fy_q16 = 100 << 16;
|
||||
GTEState a = base, b = base, c = base, guest1 = base, guest2 = base;
|
||||
gte_set_fov_scale(1, 1);
|
||||
gte_render_pose_set(&ov);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&a, a.V0, true);
|
||||
gte_set_fov_scale(1, 2);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&b, b.V0, true);
|
||||
if (a.SXY[2] != b.SXY[2])
|
||||
return fail_value("absolute override independent of fov_scale",0,0,0,a.SXY[2],b.SXY[2]);
|
||||
ov.projection_h_ref = 400;
|
||||
gte_render_pose_set(&ov); /* scaled H = 200 -> half focal */
|
||||
PSXRecomp::GTE::gte_rtps_internal(&c, c.V0, true);
|
||||
if ((int16_t)c.SXY[2] != 256 + 10 || (int16_t)(c.SXY[2] >> 16) != 120 + 10)
|
||||
return fail_value("h_ref override follows fov-scaled H",0,0,0,266,c.SXY[2] & 65535);
|
||||
PSXModRenderView none = {}; gte_render_pose_set(&none);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&guest2, guest2.V0, true);
|
||||
gte_set_fov_scale(1, 1);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&guest1, guest1.V0, true);
|
||||
if (guest1.SXY[2] == guest2.SXY[2])
|
||||
return fail_value("guest path keeps fov_scale after override reset",0,0,0,guest1.SXY[2],guest2.SXY[2]);
|
||||
if (guest1.SXY[2] == a.SXY[2])
|
||||
return fail_value("override differs from guest projection (sanity)",0,0,0,guest1.SXY[2],a.SXY[2]);
|
||||
}
|
||||
gte_set_fov_scale(1, 1);
|
||||
std::puts("PASS: rigid rotation and asymmetric projection before division");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_render_view_parallax() {
|
||||
const int32_t zero[3] = {0, 0, 0}, offset[3] = {24, 0, 0};
|
||||
int shifts[2] = {};
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
GTEState base;
|
||||
base.RT[0][0] = base.RT[1][1] = base.RT[2][2] = 4096;
|
||||
base.H = 400; base.OFX = 256 << 16; base.OFY = 120 << 16;
|
||||
base.V0[2] = i ? 3200 : 800;
|
||||
GTEState baseline = base, eye = base, expected = base, repeat = base;
|
||||
gte_render_view_set(zero);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&baseline, baseline.V0, true);
|
||||
expected.TR[0] += 24;
|
||||
PSXRecomp::GTE::gte_rtps_internal(&expected, expected.V0, true);
|
||||
gte_render_view_set(offset);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&eye, eye.V0, true);
|
||||
PSXRecomp::GTE::gte_rtps_internal(&repeat, repeat.V0, true);
|
||||
if (eye.SXY[2] != expected.SXY[2] || eye.SZ[3] != expected.SZ[3] ||
|
||||
eye.MAC1 != expected.MAC1 || eye.TR[0] != base.TR[0] ||
|
||||
eye.SXY[2] != repeat.SXY[2]) {
|
||||
gte_render_view_set(zero);
|
||||
return fail_value("render view equals pre-divide translation", 0, 0, 0,
|
||||
expected.SXY[2], eye.SXY[2]);
|
||||
}
|
||||
shifts[i] = (int16_t)eye.SXY[2] - (int16_t)baseline.SXY[2];
|
||||
}
|
||||
gte_render_view_set(zero);
|
||||
if (shifts[0] != 12 || shifts[1] != 3)
|
||||
return fail_value("near/far render-view displacement", 0, 0, 0, 12u,
|
||||
static_cast<uint32_t>(shifts[0]));
|
||||
std::puts("PASS: view offset gives 12px at Z=800, 3px at Z=3200; TR unchanged; no accumulation");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_pgxp_probe_does_not_count_geometry_lookup() {
|
||||
constexpr uint32_t packed = (80u << 16) | 160u;
|
||||
constexpr int32_t x16 = (160 << 16) + 0x4000;
|
||||
constexpr int32_t y16 = (80 << 16) + 0x2000;
|
||||
const int fallback_was_enabled = pgxp_position_fallback();
|
||||
gte_geometry_correction_set(1);
|
||||
gte_test_seed_geometry(packed, x16, y16);
|
||||
pgxp_set_position_fallback(1);
|
||||
|
||||
uint32_t lookups0 = 0, hits0 = 0, miss_unrecorded0 = 0, miss_ambiguous0 = 0;
|
||||
gte_geometry_correction_stats(&lookups0, &hits0, &miss_unrecorded0,
|
||||
&miss_ambiguous0);
|
||||
if (pgxp_probe_precise_vertex(0xFFFFFFFFu, packed, 160, 80) !=
|
||||
PGXP_SRC_FALLBACK)
|
||||
return fail_value("PGXP fallback probe source", 0, 0, packed,
|
||||
PGXP_SRC_FALLBACK, PGXP_SRC_NATIVE);
|
||||
uint32_t lookups1 = 0, hits1 = 0, miss_unrecorded1 = 0, miss_ambiguous1 = 0;
|
||||
gte_geometry_correction_stats(&lookups1, &hits1, &miss_unrecorded1,
|
||||
&miss_ambiguous1);
|
||||
if (lookups1 != lookups0 || hits1 != hits0 ||
|
||||
miss_unrecorded1 != miss_unrecorded0 ||
|
||||
miss_ambiguous1 != miss_ambiguous0)
|
||||
return fail_value("PGXP probe leaves geometry counters unchanged", 0, 0,
|
||||
packed, lookups0, lookups1);
|
||||
|
||||
int32_t got_x = 0, got_y = 0;
|
||||
uint16_t got_z = 1;
|
||||
if (pgxp_get_precise_vertex(0xFFFFFFFFu, packed, 160, 80,
|
||||
&got_x, &got_y, &got_z) != PGXP_SRC_FALLBACK ||
|
||||
got_x != x16 || got_y != y16 || got_z != 0)
|
||||
return fail_value("PGXP draw uses geometry fallback", 0, 0, packed,
|
||||
static_cast<uint32_t>(x16), static_cast<uint32_t>(got_x));
|
||||
uint32_t lookups2 = 0, hits2 = 0, miss_unrecorded2 = 0, miss_ambiguous2 = 0;
|
||||
gte_geometry_correction_stats(&lookups2, &hits2, &miss_unrecorded2,
|
||||
&miss_ambiguous2);
|
||||
gte_geometry_correction_set(0);
|
||||
pgxp_set_position_fallback(fallback_was_enabled);
|
||||
if (lookups2 != lookups1 + 1u || hits2 != hits1 + 1u ||
|
||||
miss_unrecorded2 != miss_unrecorded1 ||
|
||||
miss_ambiguous2 != miss_ambiguous1)
|
||||
return fail_value("draw increments geometry counters", 0, 0, packed,
|
||||
lookups1 + 1u, lookups2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/* Preserve projection precision (docs/ENHANCEMENTS.md G1.11) changes the
|
||||
@@ -1111,7 +1270,56 @@ int test_preserve_projection_is_shadow_only() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_projection_scale() {
|
||||
double value = 2;
|
||||
for (const char* bad : {"", "nan", "inf", "9", "0", "-1", "2junk", "1e999", "1e-999"}) {
|
||||
CHECK(!psx_projection_scale_parse(bad, &value) && value == 2);
|
||||
}
|
||||
CHECK(psx_projection_scale_parse(" 8.0 ", &value) && value == 8);
|
||||
CHECK(!psx_projection_scale_valid(std::numeric_limits<double>::quiet_NaN()));
|
||||
CHECK(psx_projection_scale_denominator(0.00001) == 1);
|
||||
CPUState seed{};
|
||||
seed.gte_ctrl[0] = 4096; seed.gte_ctrl[2] = 4096; seed.gte_ctrl[4] = 4096;
|
||||
seed.gte_ctrl[26] = 320; seed.gte_data[0] = 100; seed.gte_data[1] = 1000;
|
||||
const uint32_t cmd = 0x80001;
|
||||
CPUState stock=seed; gte_set_fov_scale(1,1); gte_execute(&stock,cmd);
|
||||
CPUState identity=seed; gte_set_fov_scale(1000,1000); gte_execute(&identity,cmd);
|
||||
CHECK(same_gte(stock,identity));
|
||||
CPUState widened=seed; gte_set_fov_scale(1,2); gte_execute(&widened,cmd);
|
||||
CPUState reference=seed; reference.gte_ctrl[26]=160;
|
||||
gte_set_fov_scale(1,1); gte_execute(&reference,cmd);
|
||||
CHECK(widened.gte_ctrl[26] == 320 && widened.gte_data[14] == reference.gte_data[14]);
|
||||
CHECK(widened.gte_data[14] != stock.gte_data[14]);
|
||||
for (auto ratio : {std::pair<int,int>{0,1}, {-1,1}, {1,0}, {1,-1}}) {
|
||||
CPUState reset=seed; gte_set_fov_scale(ratio.first,ratio.second); gte_execute(&reset,cmd);
|
||||
CHECK(same_gte(stock,reset));
|
||||
}
|
||||
/* Guest H == 0 is a real (degenerate) projection: it must stay 0, not be
|
||||
* clamped to 1, under any scale. */
|
||||
{
|
||||
CPUState h0=seed; h0.gte_ctrl[26]=0;
|
||||
CPUState h0_stock=h0; gte_set_fov_scale(1,1); gte_execute(&h0_stock,cmd);
|
||||
CPUState h0_scaled=h0; gte_set_fov_scale(1,2); gte_execute(&h0_scaled,cmd);
|
||||
CHECK(h0_scaled.gte_ctrl[26] == 0);
|
||||
CHECK(same_gte(h0_stock,h0_scaled));
|
||||
gte_set_fov_scale(1000,500); h0_scaled=h0; gte_execute(&h0_scaled,cmd);
|
||||
CHECK(same_gte(h0_stock,h0_scaled));
|
||||
}
|
||||
/* Netplay forces identity so peers cannot diverge on SXY/MAC/FLAG. */
|
||||
{
|
||||
g_test_netplay_active = 1;
|
||||
CPUState np=seed; gte_set_fov_scale(1,2); gte_execute(&np,cmd);
|
||||
g_test_netplay_active = 0;
|
||||
CHECK(same_gte(stock,np));
|
||||
CPUState off=seed; gte_set_fov_scale(1,2); gte_execute(&off,cmd);
|
||||
CHECK(!same_gte(stock,off));
|
||||
}
|
||||
gte_set_fov_scale(1,1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main() {
|
||||
if (int rc = test_projection_scale()) return rc;
|
||||
if (int rc = test_hardware_register_semantics()) return rc;
|
||||
if (int rc = test_canonicalizer()) return rc;
|
||||
if (int rc = test_reads()) return rc;
|
||||
@@ -1124,8 +1332,11 @@ int main() {
|
||||
if (int rc = test_precise_nclip_is_title_scoped()) return rc;
|
||||
if (int rc = test_saturated_nclip_keeps_architectural_result()) return rc;
|
||||
if (int rc = test_precision_speculative_transaction()) return rc;
|
||||
if (int rc = test_pgxp_probe_does_not_count_geometry_lookup()) return rc;
|
||||
if (int rc = test_preserve_projection_is_shadow_only()) return rc;
|
||||
if (int rc = test_pgxp_culling()) return rc;
|
||||
if (int rc = test_render_view_parallax()) return rc;
|
||||
if (int rc = test_render_pose()) return rc;
|
||||
/* Signed host projection spans the camera plane without changing any
|
||||
* architectural register, including unsigned SZ and divider/screen flags. */
|
||||
pgxp_set_enabled(1);
|
||||
@@ -1147,6 +1358,25 @@ int main() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
/* The clipper's projection (hx, OFY/MAC2*H term, near_z = H/2) must use the
|
||||
* same scaled H as the guest's SXY, or culling disagrees with the draw. */
|
||||
gte_set_fov_scale(1,2);
|
||||
for (int depth : {1,149,900}) {
|
||||
GTEState g{};
|
||||
g.RT[0][0]=g.RT[1][1]=g.RT[2][2]=4096;
|
||||
g.V0[0]=100;g.V0[1]=30;g.TR[2]=depth;
|
||||
g.H=300;g.OFX=256*65536;g.OFY=120*65536;
|
||||
pgxp_set_projection_tracking(1);
|
||||
PSXRecomp::GTE::gte_rtps(&g,0x0180001u);
|
||||
PGXPProjection p;
|
||||
if (!pgxp_get_gte_projection(2,g.SXY[2],&p) ||
|
||||
p.z!=depth || p.x!=256*depth+15000 || p.y!=120*depth+4500 || p.near_z!=75) {
|
||||
std::fprintf(stderr,"FAIL scaled-H clipper projection at depth %d\n",depth);
|
||||
gte_set_fov_scale(1,1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
gte_set_fov_scale(1,1);
|
||||
pgxp_set_projection_tracking(0);
|
||||
std::puts("PASS: canonical GTE register helpers match GTEState transfer oracle");
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "mod_controller_source.h"
|
||||
#include "sio.h"
|
||||
#include <stdio.h>
|
||||
static int mode;
|
||||
static int calls;
|
||||
static int source(PSXModControllerState *p) {
|
||||
calls++;
|
||||
p->buttons=0xfeff;p->lx=200;p->ly=5;
|
||||
if(mode==1)return 0;
|
||||
if(mode==2)p->rx=256;
|
||||
if(mode==3)p->analog=0;
|
||||
if(mode==4)p->struct_size=1;
|
||||
return 1;
|
||||
}
|
||||
/* Evaluate side effects and failures even when NDEBUG is defined. */
|
||||
#define CHECK(expr) do { \
|
||||
if (!(expr)) { \
|
||||
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
int main(void) {
|
||||
PSXModControllerState p;
|
||||
int rel = -1;
|
||||
CHECK(!psx_mod_set_controller_source(PSX_MAX_PLAYERS,source));
|
||||
CHECK(!mod_controller_source_sample(0,&p,&rel) && rel==0);
|
||||
CHECK(psx_mod_set_controller_source(0,source));
|
||||
CHECK(mod_controller_source_sample(0,&p,&rel) && p.lx==200 && p.ly==5 && !rel);
|
||||
mod_controller_source_begin_frame();
|
||||
mode=1;CHECK(mod_controller_source_sample(0,&p,NULL) && p.buttons==0xffff && p.lx==128);
|
||||
mod_controller_source_begin_frame();
|
||||
mode=2;CHECK(mod_controller_source_sample(0,&p,NULL) && p.buttons==0xffff && p.rx==128);
|
||||
mod_controller_source_begin_frame();
|
||||
mode=3;CHECK(mod_controller_source_sample(0,&p,NULL) && !p.analog && p.lx==128 && p.ly==128);
|
||||
/* Wrong struct_size (ABI mismatch) is neutral (and logged to stderr). */
|
||||
mod_controller_source_begin_frame();
|
||||
mode=4;CHECK(mod_controller_source_sample(0,&p,NULL) && p.buttons==0xffff &&
|
||||
p.struct_size==sizeof p && p.lx==128);
|
||||
/* Once per frame: repeated samples reuse the first result. */
|
||||
mode=0;mod_controller_source_begin_frame();calls=0;
|
||||
CHECK(mod_controller_source_sample(0,&p,NULL) && p.lx==200);
|
||||
mode=1; /* a second callback invocation would now decline */
|
||||
CHECK(mod_controller_source_sample(0,&p,NULL) && p.lx==200 && calls==1);
|
||||
mode=0;mod_controller_source_begin_frame();
|
||||
CHECK(mod_controller_source_sample(0,&p,NULL) && calls==2);
|
||||
/* Detach: one neutral release frame, shared by both passes of that frame. */
|
||||
CHECK(psx_mod_set_controller_source(0,0));
|
||||
CHECK(mod_controller_source_sample(0,&p,&rel) && rel==1 && p.buttons==0xffff && p.lx==128);
|
||||
CHECK(mod_controller_source_sample(0,&p,&rel) && rel==1);
|
||||
mod_controller_source_begin_frame();
|
||||
CHECK(!mod_controller_source_sample(0,&p,&rel) && rel==0);
|
||||
/* Reset on a held source still delivers the neutral release, for every
|
||||
* port that had a source. */
|
||||
CHECK(psx_mod_set_controller_source(0,source));
|
||||
CHECK(psx_mod_set_controller_source(1,source));
|
||||
mod_controller_source_begin_frame();
|
||||
mode=0;CHECK(mod_controller_source_sample(0,&p,NULL) && p.buttons==0xfeff);
|
||||
mod_controller_source_reset();
|
||||
CHECK(!mod_controller_source_present(0) && !mod_controller_source_present(1));
|
||||
CHECK(mod_controller_source_sample(0,&p,&rel) && rel==1 && p.buttons==0xffff && p.lx==128 && p.ly==128);
|
||||
CHECK(mod_controller_source_sample(1,&p,&rel) && rel==1 && p.buttons==0xffff);
|
||||
mod_controller_source_begin_frame();
|
||||
CHECK(!mod_controller_source_sample(0,&p,&rel) && !mod_controller_source_sample(1,&p,&rel));
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/* Resident disc resources (mod_resident.h): build from the effective disc,
|
||||
* warm verification, corruption and plan-change invalidation, stock policy,
|
||||
* derived blobs, relocation, pruning and the guest-range guard. */
|
||||
#include "mod_resident.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "psx_sha256.h"
|
||||
#include "gpu.h"
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::array<uint8_t, 2 * 1024 * 1024> ram;
|
||||
static int failures;
|
||||
|
||||
extern "C" uint8_t psx_read_byte(uint32_t a) { return ram[a & 0x1fffffu]; }
|
||||
extern "C" void psx_write_byte(uint32_t a, uint8_t v) { ram[a & 0x1fffffu] = v; }
|
||||
extern "C" uint16_t psx_read_half(uint32_t a) {
|
||||
return (uint16_t)(ram[a & 0x1fffffu] | ram[(a + 1) & 0x1fffffu] << 8);
|
||||
}
|
||||
extern "C" void psx_write_half(uint32_t a, uint16_t v) {
|
||||
psx_write_byte(a, (uint8_t)v); psx_write_byte(a + 1, (uint8_t)(v >> 8));
|
||||
}
|
||||
extern "C" uint32_t psx_read_word(uint32_t a) {
|
||||
return (uint32_t)psx_read_half(a) | (uint32_t)psx_read_half(a + 2) << 16;
|
||||
}
|
||||
extern "C" void psx_write_word(uint32_t a, uint32_t v) {
|
||||
psx_write_half(a, (uint16_t)v); psx_write_half(a + 2, (uint16_t)(v >> 16));
|
||||
}
|
||||
extern "C" void psx_host_write_byte(uint32_t a, uint8_t v) { psx_write_byte(a, v); }
|
||||
extern "C" void psx_host_write_half(uint32_t a, uint16_t v) { psx_write_half(a, v); }
|
||||
extern "C" void psx_host_write_word(uint32_t a, uint32_t v) { psx_write_word(a, v); }
|
||||
extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; }
|
||||
extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) { return 0; }
|
||||
extern "C" void psx_ram_reset_size_request(void) {}
|
||||
extern "C" void psx_projection_reset_session(void) {}
|
||||
extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {}
|
||||
extern "C" int psx_ws_x_margin(void) { return 0; }
|
||||
extern "C" void gpu_get_display_info(GpuDisplayInfo* out) { *out = GpuDisplayInfo{}; }
|
||||
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
|
||||
extern "C" int fntrace_is_game_started(void) { return 1; }
|
||||
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
|
||||
extern "C" int gpu_ws_configured_x_reveal(void) { return 0; }
|
||||
extern "C" void gpu_ws_tag_hud_prim(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_tag_screen_mask_quad(uint32_t) {}
|
||||
extern "C" void gpu_ws_tag_radial_screen_mask_quad(uint32_t, float) {}
|
||||
|
||||
static void check(bool ok, const char* what) {
|
||||
if (!ok) { std::cerr << "FAIL: " << what << "\n"; failures++; }
|
||||
}
|
||||
|
||||
static std::string sha_hex(const uint8_t* p, size_t n) {
|
||||
uint8_t d[32];
|
||||
psx_sha256_compute(p, n, d);
|
||||
static const char* h = "0123456789abcdef";
|
||||
std::string s(64, '0');
|
||||
for (int i = 0; i < 32; i++) { s[2 * i] = h[d[i] >> 4]; s[2 * i + 1] = h[d[i] & 15]; }
|
||||
return s;
|
||||
}
|
||||
|
||||
static void write_bytes(const fs::path& p, const std::vector<uint8_t>& b) {
|
||||
fs::create_directories(p.parent_path());
|
||||
std::ofstream(p, std::ios::binary).write((const char*)b.data(), (std::streamsize)b.size());
|
||||
}
|
||||
static void write_text(const fs::path& p, const std::string& t) {
|
||||
fs::create_directories(p.parent_path());
|
||||
std::ofstream(p) << t;
|
||||
}
|
||||
|
||||
/* derive: one reversed copy of each file, plus a copy of file 0's bytes from
|
||||
* file 1 (identical bytes must share one blob). */
|
||||
static int derive_calls;
|
||||
static int derive(PSXResidentSink* sink, uint32_t file, const uint8_t* data, uint32_t size,
|
||||
int stock, void* user) {
|
||||
derive_calls++;
|
||||
std::vector<uint8_t> rev(data, data + size);
|
||||
std::reverse(rev.begin(), rev.end());
|
||||
const uint32_t meta[4] = {size, (uint32_t)stock, 0xabcdu, file};
|
||||
if (!psx_resident_emit(sink, file, 7, meta, rev.data(), size)) return 0;
|
||||
if (file == 1) {
|
||||
const auto* first = static_cast<const std::vector<uint8_t>*>(user);
|
||||
if (!psx_resident_emit(sink, file, 9, nullptr, first->data(), (uint32_t)first->size())) return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static std::string status() {
|
||||
std::vector<char> b(8192);
|
||||
return psx_resident_status_json(b.data(), (uint32_t)b.size()) ? std::string(b.data()) : "";
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::error_code ec;
|
||||
const fs::path root = fs::temp_directory_path() / "psx_mod_resident_test";
|
||||
fs::remove_all(root, ec);
|
||||
#ifdef _WIN32
|
||||
_putenv_s("PSX_RESIDENT_CACHE", (root / "cache").string().c_str());
|
||||
#else
|
||||
setenv("PSX_RESIDENT_CACHE", (root / "cache").string().c_str(), 1);
|
||||
#endif
|
||||
|
||||
/* ISO: root dir @20 -> DATA dir @21 -> A.BIN (3000 bytes @22..23),
|
||||
* B.BIN (5000 bytes @24..26). Non-zero bytes after each end of file. */
|
||||
std::vector<uint8_t> iso(30 * 2048);
|
||||
auto le32 = [&](size_t at, uint32_t n) { for (unsigned i = 0; i < 4; ++i) iso[at + i] = (uint8_t)(n >> (i * 8)); };
|
||||
auto record = [&](size_t at, uint32_t lba, uint32_t bytes, bool dir, const std::string& name) {
|
||||
iso[at] = (uint8_t)((33 + name.size() + 1) & ~size_t(1));
|
||||
le32(at + 2, lba); le32(at + 10, bytes); iso[at + 25] = dir ? 2 : 0;
|
||||
iso[at + 28] = 1; iso[at + 31] = 1; iso[at + 32] = (uint8_t)name.size();
|
||||
std::copy(name.begin(), name.end(), iso.begin() + at + 33);
|
||||
return (size_t)iso[at];
|
||||
};
|
||||
iso[16 * 2048] = 1; std::copy_n("CD001", 5, iso.begin() + 16 * 2048 + 1); iso[16 * 2048 + 6] = 1;
|
||||
record(16 * 2048 + 156, 20, 2048, true, std::string(1, '\0'));
|
||||
record(20 * 2048, 21, 2048, true, "DATA");
|
||||
const size_t a_rec = 21 * 2048;
|
||||
const size_t b_rec = a_rec + record(a_rec, 22, 3000, false, "A.BIN;1");
|
||||
record(b_rec, 24, 5000, false, "B.BIN;1");
|
||||
for (unsigned i = 0; i < 2 * 2048; ++i) iso[22 * 2048 + i] = i < 3000 ? (uint8_t)(i * 7) : 0xEE;
|
||||
for (unsigned i = 0; i < 3 * 2048; ++i) iso[24 * 2048 + i] = i < 5000 ? (uint8_t)(i * 13 + 1) : 0xDD;
|
||||
const std::vector<uint8_t> a_padded(iso.begin() + 22 * 2048, iso.begin() + 24 * 2048);
|
||||
const std::vector<uint8_t> b_padded(iso.begin() + 24 * 2048, iso.begin() + 27 * 2048);
|
||||
const std::string a_sha = sha_hex(a_padded.data(), a_padded.size());
|
||||
const std::string b_sha = sha_hex(b_padded.data(), b_padded.size());
|
||||
|
||||
const fs::path disc_root = root / "disc";
|
||||
const fs::path iso_path = disc_root / "game.iso";
|
||||
write_bytes(iso_path, iso);
|
||||
std::string error;
|
||||
check(PSXRecompV4::mod_runtime_initialize(disc_root, "RESIDENT", 0, {}, &error), "initialize");
|
||||
check(PSXRecompV4::mod_runtime_commit(iso_path, &error), "commit");
|
||||
check(!PSXRecompV4::mod_runtime_fingerprint().empty(), "a committed plan has a fingerprint");
|
||||
|
||||
uint32_t lba = 0, size = 0;
|
||||
check(psx_mod_disc_file_extent("DATA/B.BIN", &lba, &size) && lba == 24 && size == 5000,
|
||||
"extent query reports the effective record");
|
||||
check(!psx_mod_disc_file_extent("DATA", &lba, &size) && !lba && !size, "extent rejects directories");
|
||||
|
||||
PSXResidentFile files[2] = {{"DATA/A.BIN", 3000, a_sha.c_str()},
|
||||
{"data/b.bin", 5000, b_sha.c_str()}};
|
||||
PSXResidentSpec spec{};
|
||||
spec.struct_size = sizeof spec;
|
||||
spec.title = "ResidentTest";
|
||||
spec.format = "rt-v1";
|
||||
spec.files = files;
|
||||
spec.file_count = 2;
|
||||
spec.policy = PSX_RESIDENT_REQUIRE_STOCK;
|
||||
spec.derive = derive;
|
||||
spec.derive_user = (void*)&a_padded;
|
||||
spec.keep_packs = 2;
|
||||
|
||||
const PSXResidentPack* pack = psx_resident_prepare(&spec);
|
||||
check(pack != nullptr, "cold prepare");
|
||||
check(derive_calls == 2, "derive runs once per file on a cold build");
|
||||
check(status().find("\"state\":\"prepared\"") != std::string::npos, "status reports a prepared pack");
|
||||
uint32_t padded = 0;
|
||||
const uint8_t* a = psx_resident_file(pack, 0, &size, &padded);
|
||||
check(a && size == 3000 && padded == 4096 && std::equal(a_padded.begin(), a_padded.end(), a),
|
||||
"file bytes include the true sector tail");
|
||||
check(psx_resident_file_lba(pack, 1) == 24 && psx_resident_file_stock(pack, 0) &&
|
||||
psx_resident_file_stock(pack, 1) && !psx_resident_modified_files(pack), "lba and stock flags");
|
||||
uint32_t which = 99;
|
||||
const uint8_t* at = psx_resident_find_lba(pack, 25, 4096, &which);
|
||||
check(at && which == 1 && std::equal(b_padded.begin() + 2048, b_padded.end(), at), "find by lba inside a file");
|
||||
check(!psx_resident_find_lba(pack, 25, 4097, nullptr), "find rejects spans past the padded end");
|
||||
check(!psx_resident_find_lba(pack, 21, 16, nullptr), "find rejects lbas before every file");
|
||||
check(psx_resident_derived_count(pack) == 3, "derived blobs kept in order");
|
||||
uint32_t file = 0, tag = 0, meta[4] = {};
|
||||
const uint8_t* d = psx_resident_derived(pack, 1, &file, &tag, meta, &size);
|
||||
check(d && file == 1 && tag == 7 && size == 6144 && meta[0] == 6144 && meta[1] == 1 && meta[2] == 0xabcd &&
|
||||
d[0] == b_padded.back(), "derived blob metadata and bytes");
|
||||
const uint8_t* dup = psx_resident_derived(pack, 2, &file, &tag, nullptr, &size);
|
||||
check(dup == a && tag == 9, "identical bytes share one blob");
|
||||
|
||||
/* Warm: verified from disk, derive not called. */
|
||||
pack = psx_resident_prepare(&spec);
|
||||
check(pack && derive_calls == 2, "warm prepare reuses the cache");
|
||||
check(status().find("\"state\":\"verified\"") != std::string::npos, "status reports verification");
|
||||
check(std::equal(a_padded.begin(), a_padded.end(), psx_resident_file(pack, 0, nullptr, nullptr)),
|
||||
"warm bytes identical");
|
||||
|
||||
/* A flipped payload byte is caught by its blob hash and rebuilt. */
|
||||
fs::path pack_path;
|
||||
for (const auto& e : fs::directory_iterator(root / "cache" / "ResidentTest"))
|
||||
if (e.path().extension() == ".pack") pack_path = e.path();
|
||||
{
|
||||
std::fstream f(pack_path, std::ios::binary | std::ios::in | std::ios::out);
|
||||
f.seekp(-1, std::ios::end);
|
||||
char c = 0x55;
|
||||
f.write(&c, 1);
|
||||
}
|
||||
pack = psx_resident_prepare(&spec);
|
||||
check(pack && derive_calls == 4, "corrupt cache rebuilt");
|
||||
|
||||
/* Stock policy. */
|
||||
std::string wrong(64, '0');
|
||||
files[0].stock_sha256 = wrong.c_str();
|
||||
check(!psx_resident_prepare(&spec), "require-stock rejects a differing file");
|
||||
check(status().find("differs from the original disc") != std::string::npos, "failure reason kept");
|
||||
spec.policy = PSX_RESIDENT_ALLOW_MODIFIED;
|
||||
pack = psx_resident_prepare(&spec);
|
||||
check(pack && !psx_resident_file_stock(pack, 0) && psx_resident_modified_files(pack) == 1,
|
||||
"allow-modified serves and counts the differing file");
|
||||
files[0].stock_sha256 = a_sha.c_str();
|
||||
spec.policy = PSX_RESIDENT_REQUIRE_STOCK;
|
||||
|
||||
/* Old packs of this format are pruned to keep_packs; others untouched. */
|
||||
for (int i = 0; i < 4; i++) write_bytes(root / "cache" / "ResidentTest" / ("rt-v1-old" + std::to_string(i) + ".pack"), {1});
|
||||
write_bytes(root / "cache" / "ResidentTest" / "other-v1-x.pack", {1});
|
||||
pack = psx_resident_prepare(&spec);
|
||||
unsigned ours = 0, other = 0;
|
||||
for (const auto& e : fs::directory_iterator(root / "cache" / "ResidentTest")) {
|
||||
const auto name = e.path().filename().string();
|
||||
ours += name.rfind("rt-v1-", 0) == 0;
|
||||
other += name.rfind("other-", 0) == 0;
|
||||
}
|
||||
check(pack && ours == 3 && other == 1, "prune keeps the active pack plus keep_packs of its format only");
|
||||
|
||||
/* A mod plan that patches B and grows A through its directory record:
|
||||
* a new key (new pack), effective bytes and the relocated extent. */
|
||||
write_text(disc_root / "packages/rt.patch/1.0.0/manifest.toml",
|
||||
"format_version = 5\nid = \"rt.patch\"\nversion = \"1.0.0\"\nname = \"RT Patch\"\n"
|
||||
"[[target]]\ngame_id = \"RESIDENT\"\ndisc_sha256 = \"" + sha_hex(iso.data(), iso.size()) + "\"\n"
|
||||
"[[feature]]\nid = \"asset\"\nname = \"Asset\"\n"
|
||||
"[[patch]]\nfeature = \"asset\"\ntarget = \"disc_user\"\noffset = " + std::to_string(24 * 2048 + 2) + "\n"
|
||||
"expected = \"1b\"\nreplace = \"5a\"\n"
|
||||
"[[patch]]\nfeature = \"asset\"\ntarget = \"disc_user\"\noffset = " + std::to_string(a_rec + 10) + "\n"
|
||||
"expected = \"b80b0000\"\nreplace = \"00100000\"\n");
|
||||
write_text(disc_root / "state.toml",
|
||||
"format_version = 2\n[[feature]]\npackage_id = \"rt.patch\"\nid = \"asset\"\nenabled = true\n");
|
||||
const std::string stock_fingerprint = PSXRecompV4::mod_runtime_fingerprint();
|
||||
check(PSXRecompV4::mod_runtime_initialize(disc_root, "RESIDENT", 0, {}, &error) &&
|
||||
PSXRecompV4::mod_runtime_commit(iso_path, &error), "patched plan commit");
|
||||
check(PSXRecompV4::mod_runtime_fingerprint() != stock_fingerprint, "plan fingerprint changed");
|
||||
check(!psx_resident_prepare(&spec), "require-stock refuses the modded plan");
|
||||
spec.policy = PSX_RESIDENT_ALLOW_MODIFIED;
|
||||
const int before = derive_calls;
|
||||
pack = psx_resident_prepare(&spec);
|
||||
check(pack && derive_calls == before + 2, "modded plan builds its own pack");
|
||||
const uint8_t* b = psx_resident_file(pack, 1, &size, nullptr);
|
||||
check(b && b[2] == 0x5a && !psx_resident_file_stock(pack, 1), "effective patched bytes served");
|
||||
a = psx_resident_file(pack, 0, &size, &padded);
|
||||
check(a && size == 4096 && padded == 4096 && a[3500] == 0xEE && !psx_resident_file_stock(pack, 0),
|
||||
"grown file read through its patched directory record");
|
||||
check(status().find("\"modified\":2") != std::string::npos, "status counts modified files");
|
||||
|
||||
/* Guest-range guard. */
|
||||
for (uint32_t i = 0; i < 64; i++) ram[0x1000 + i] = (uint8_t)(i ^ 0x5a);
|
||||
std::vector<uint8_t> guarded(ram.begin() + 0x1000, ram.begin() + 0x1040);
|
||||
const std::string guard = sha_hex(guarded.data(), guarded.size());
|
||||
const PSXResidentRange ranges[2] = {{0x80001000u, 0x80001020u}, {0x80001020u, 0x80001040u}};
|
||||
check(psx_resident_guest_ranges_match(ranges, 2, guard.c_str()), "guard matches");
|
||||
ram[0x1030] ^= 1;
|
||||
check(!psx_resident_guest_ranges_match(ranges, 2, guard.c_str()), "guard detects a changed byte");
|
||||
|
||||
/* Invalid specs are refused before any disc access. */
|
||||
PSXResidentSpec bad = spec;
|
||||
bad.format = "bad/format";
|
||||
check(!psx_resident_prepare(&bad), "format ids are file-name safe");
|
||||
bad = spec;
|
||||
bad.struct_size = 4;
|
||||
check(!psx_resident_prepare(&bad), "struct size checked");
|
||||
|
||||
fs::remove_all(root, ec);
|
||||
if (failures) return 1;
|
||||
std::cout << "mod resident tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Overlay shards and BIOS modules are compiled for the RUNTIME's architecture.
|
||||
|
||||
THE BUG
|
||||
=======
|
||||
An x86_64 macOS runtime running under Rosetta compiled every overlay shard for
|
||||
arm64. The chain was: the runtime spawns `sh -c <compile command>`; the shell
|
||||
and the bundled Python inherit x86_64 from it, so compile_overlays.py named the
|
||||
cache `macos-x64` (platform.machine() is the process's own architecture). But
|
||||
it ran the compiler without `-arch`, and Xcode's clang is an arm64-only binary
|
||||
whose default target is its own architecture. So arm64 shards landed in the
|
||||
macos-x64 cache, the x64 runtime could not load one of them, and every overlay
|
||||
ran on the interpreter. Nothing failed loudly. bios_module_build.py had the
|
||||
same compile line.
|
||||
|
||||
THE CONTRACT PINNED HERE
|
||||
========================
|
||||
* The runtime exports its own "<os>-<arch>" (overlay_loader.h's
|
||||
PSX_OVERLAY_ARCH_ABI: the running slice, for a universal binary) to every
|
||||
compile it spawns, as PSX_OVERLAY_ARCH_ABI, and passes it to
|
||||
bios_module_build.py as --arch-abi.
|
||||
* compile_overlays.py builds for that architecture: the cache directory names
|
||||
it, and on macOS the compiler is told it with `-arch`.
|
||||
* Shards are validated by loading them into the Python that built them, so a
|
||||
Python that cannot load the runtime's architecture refuses up front instead
|
||||
of writing shards nobody loads. The Python's architecture is its process's:
|
||||
platform.machine() on macOS and Linux, but the interpreter's build on
|
||||
Windows, where platform.machine() reports the machine (an x64 Python
|
||||
emulated on Windows on ARM says ARM64; it must still build x64 shards).
|
||||
* Windows and Linux compile exactly as before: no new flags, the same
|
||||
directory.
|
||||
|
||||
The hermetic cases run everywhere. The real-toolchain cases need a POSIX C
|
||||
compiler (--cc); the per-slice cases need macOS, and the x86_64 slice needs
|
||||
Rosetta on Apple silicon.
|
||||
|
||||
python3 runtime/tests/test_overlay_compile_arch.py [--cc cc]
|
||||
ctest -R overlay_compile_arch_test
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import io
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, '..', '..'))
|
||||
TOOLS = os.path.join(ROOT, 'tools')
|
||||
INCLUDE = os.path.join(ROOT, 'runtime', 'include')
|
||||
sys.path.insert(0, TOOLS)
|
||||
|
||||
import compile_overlays as co # noqa: E402
|
||||
import bios_module_build as bmb # noqa: E402
|
||||
|
||||
CC = 'cc' # replaced from --cc in __main__
|
||||
|
||||
|
||||
# What sysconfig.get_platform() says for a native Windows Python built for
|
||||
# the machine it runs on (python.org spelling).
|
||||
_WIN_BUILD = {'AMD64': 'win-amd64', 'ARM64': 'win-arm64', 'x86': 'win32'}
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def host(system, machine, env_arch=None, build=None):
|
||||
"""Pretend this interpreter runs on `system`/`machine` (what
|
||||
platform.system()/machine() report), optionally spawned by a runtime that
|
||||
exported PSX_OVERLAY_ARCH_ABI=env_arch. On Windows platform.machine() is
|
||||
the machine's, and `build` is what sysconfig.get_platform() reports for
|
||||
the interpreter (default: built for that machine)."""
|
||||
env = {k: v for k, v in os.environ.items() if k != 'PSX_OVERLAY_ARCH_ABI'}
|
||||
if env_arch is not None:
|
||||
env['PSX_OVERLAY_ARCH_ABI'] = env_arch
|
||||
saved_os, saved_arch = co._TARGET_OS, co._TARGET_ARCH
|
||||
co.set_target_os(None)
|
||||
co.set_target_arch(None)
|
||||
if build is None:
|
||||
build = _WIN_BUILD.get(machine, 'win32') if system == 'Windows' else 'posix-build'
|
||||
try:
|
||||
with mock.patch.object(co.sysconfig, 'get_platform', return_value=build), \
|
||||
mock.patch.object(co.platform, 'system', return_value=system), \
|
||||
mock.patch.object(co.platform, 'machine', return_value=machine), \
|
||||
mock.patch.object(co.os, 'name', 'nt' if system == 'Windows' else 'posix'), \
|
||||
mock.patch.dict(os.environ, env, clear=True):
|
||||
yield
|
||||
finally:
|
||||
co.set_target_os(saved_os)
|
||||
co.set_target_arch(saved_arch)
|
||||
|
||||
|
||||
class _Ran:
|
||||
"""Records the compiler command instead of running it."""
|
||||
def __init__(self):
|
||||
self.cmds = []
|
||||
|
||||
def __call__(self, cmd, *a, **kw):
|
||||
self.cmds.append(list(cmd))
|
||||
return subprocess.CompletedProcess(cmd, 0, '', '')
|
||||
|
||||
|
||||
_FIXTURE = None
|
||||
|
||||
|
||||
def fixture():
|
||||
"""One shard source and include dir for every recorded command (the tcc
|
||||
path reads both), so commands from different cases compare equal."""
|
||||
global _FIXTURE
|
||||
if _FIXTURE is None:
|
||||
root = tempfile.mkdtemp(prefix='psx_ovl_arch_fx_')
|
||||
os.makedirs(os.path.join(root, 'inc'))
|
||||
with open(os.path.join(root, 'inc', 'h.h'), 'w') as f:
|
||||
f.write('\n')
|
||||
_FIXTURE = root
|
||||
return _FIXTURE
|
||||
|
||||
|
||||
def shard_command(compiler='gcc', gcc='gcc'):
|
||||
root = fixture()
|
||||
src = os.path.join(root, 'shard.c')
|
||||
with open(src, 'w') as f:
|
||||
f.write('int overlay_abi(void) { return 0; }\n')
|
||||
ran = _Ran()
|
||||
with mock.patch.object(co.subprocess, 'run', ran), \
|
||||
contextlib.redirect_stdout(io.StringIO()):
|
||||
co._compile_dll_direct(src, os.path.join(root, 'out.so'),
|
||||
[os.path.join(root, 'inc')],
|
||||
gcc=gcc, flavor=0, compiler=compiler, tcc='tcc')
|
||||
return ran.cmds[-1]
|
||||
|
||||
|
||||
def bios_module_command(gcc='gcc'):
|
||||
ran = _Ran()
|
||||
with mock.patch.object(bmb.subprocess, 'run', ran), \
|
||||
contextlib.redirect_stdout(io.StringIO()):
|
||||
bmb.compile_module(['/x/a.c'], '/x/m.so', ['/x/inc'],
|
||||
'SCPH1001', 0, 'gcc', gcc, 'tcc')
|
||||
return ran.cmds[-1]
|
||||
|
||||
|
||||
def arch_args(cmd):
|
||||
return [cmd[i + 1] for i, a in enumerate(cmd[:-1]) if a == '-arch']
|
||||
|
||||
|
||||
def run_main_until_guard(argv):
|
||||
"""compile_overlays.main() as the runtime spawns it, up to the arch guard.
|
||||
Returns (exit code, stdout). The game.toml does not exist on purpose: a
|
||||
run that gets past the guard fails there instead (FileNotFoundError)."""
|
||||
out = io.StringIO()
|
||||
err = io.StringIO()
|
||||
with mock.patch.object(sys, 'argv', ['compile_overlays.py'] + argv), \
|
||||
contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
|
||||
try:
|
||||
co.main()
|
||||
except SystemExit as exc:
|
||||
return exc.code, out.getvalue() + err.getvalue()
|
||||
except FileNotFoundError:
|
||||
return 'past-guard', out.getvalue() + err.getvalue()
|
||||
return 0, out.getvalue()
|
||||
|
||||
|
||||
MAIN_ARGV = ['--game-toml', '/nonexistent/game.toml', '--recompiler', '/nonexistent/r',
|
||||
'--runtime-include', INCLUDE, '--captures', '/nonexistent/c.json']
|
||||
|
||||
|
||||
class ToolTargetsRuntimeArch(unittest.TestCase):
|
||||
"""compile_overlays.py / bios_module_build.py, with the host mocked."""
|
||||
|
||||
def test_rosetta_x64_runtime_gets_x86_64_shards(self):
|
||||
# The reported case: x86_64 runtime, x86_64 Python (Rosetta).
|
||||
with host('Darwin', 'x86_64', 'macos-x64'):
|
||||
self.assertIsNone(co.apply_runtime_arch_abi(
|
||||
os.environ['PSX_OVERLAY_ARCH_ABI'], 'PSX_OVERLAY_ARCH_ABI'))
|
||||
self.assertEqual(co.cache_arch_abi(), 'macos-x64')
|
||||
self.assertIsNone(co.interpreter_arch_mismatch())
|
||||
self.assertEqual(arch_args(shard_command()), ['x86_64'])
|
||||
self.assertEqual(arch_args(bios_module_command()), ['x86_64'])
|
||||
|
||||
def test_arm64_runtime_gets_arm64_shards(self):
|
||||
with host('Darwin', 'arm64', 'macos-arm64'):
|
||||
self.assertIsNone(co.apply_runtime_arch_abi('macos-arm64', 'env'))
|
||||
self.assertEqual(co.cache_arch_abi(), 'macos-arm64')
|
||||
self.assertEqual(arch_args(shard_command()), ['arm64'])
|
||||
self.assertEqual(arch_args(bios_module_command()), ['arm64'])
|
||||
|
||||
def test_manual_run_on_macos_names_the_interpreters_arch(self):
|
||||
# No runtime pin (offline/AOT use): build for this interpreter, and
|
||||
# still say so, so an x86_64 Python never gets arm64 output.
|
||||
with host('Darwin', 'x86_64'):
|
||||
self.assertEqual(co.cache_arch_abi(), 'macos-x64')
|
||||
self.assertEqual(arch_args(shard_command()), ['x86_64'])
|
||||
with host('Darwin', 'arm64'):
|
||||
self.assertEqual(co.cache_arch_abi(), 'macos-arm64')
|
||||
self.assertEqual(arch_args(shard_command()), ['arm64'])
|
||||
|
||||
def test_main_adopts_the_runtime_pin(self):
|
||||
with host('Darwin', 'x86_64', 'macos-x64'):
|
||||
code, out = run_main_until_guard(MAIN_ARGV)
|
||||
self.assertEqual(code, 'past-guard', out)
|
||||
self.assertEqual(co.cache_arch_abi(), 'macos-x64')
|
||||
# The env wins over --arch-abi, like the cache dir and flavor pins.
|
||||
with host('Darwin', 'x86_64', 'macos-x64'):
|
||||
code, out = run_main_until_guard(MAIN_ARGV + ['--arch-abi', 'macos-arm64'])
|
||||
self.assertEqual(code, 'past-guard', out)
|
||||
self.assertIn('PSX_OVERLAY_ARCH_ABI overrides --arch-abi', out)
|
||||
self.assertEqual(co.cache_arch_abi(), 'macos-x64')
|
||||
|
||||
def test_bios_module_env_pin_beats_flag(self):
|
||||
with host('Darwin', 'x86_64', 'macos-x64'), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
with self.assertRaises(SystemExit): # past arch guard: no dump
|
||||
bmb.main(['--dump', '/nonexistent', '--stem', 'SCPH1001',
|
||||
'--out', '/nonexistent/m.so', '--arch-abi', 'macos-arm64'])
|
||||
self.assertIn('PSX_OVERLAY_ARCH_ABI overrides --arch-abi: macos-x64',
|
||||
out.getvalue())
|
||||
self.assertIn('not found', out.getvalue())
|
||||
self.assertNotIn('cannot build a BIOS module', out.getvalue())
|
||||
self.assertEqual(co.cache_arch_abi(), 'macos-x64')
|
||||
|
||||
def test_python_that_cannot_load_the_runtimes_arch_refuses(self):
|
||||
# A universal runtime's arm64 slice spawning an x86_64-only Python:
|
||||
# the shards could never be validated here, so build none, loudly.
|
||||
with host('Darwin', 'x86_64', 'macos-arm64'):
|
||||
code, out = run_main_until_guard(MAIN_ARGV)
|
||||
self.assertEqual(code, 2, out)
|
||||
self.assertIn('cannot build overlay shards', out)
|
||||
self.assertIn('macos-arm64', out)
|
||||
# --static writes C only and loads nothing: no guard.
|
||||
with host('Darwin', 'x86_64', 'macos-arm64'):
|
||||
code, out = run_main_until_guard(MAIN_ARGV + ['--static'])
|
||||
self.assertEqual(code, 'past-guard', out)
|
||||
with host('Darwin', 'x86_64', 'macos-arm64'), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
with self.assertRaises(SystemExit):
|
||||
bmb.main(['--dump', '/nonexistent', '--stem', 'SCPH1001',
|
||||
'--out', '/nonexistent/m.so'])
|
||||
self.assertIn('cannot build a BIOS module', out.getvalue())
|
||||
|
||||
def test_bad_or_foreign_pins_are_rejected(self):
|
||||
with host('Darwin', 'arm64'):
|
||||
for bad in ('macos', 'macos-ppc', 'test-arch', 'darwin-arm64'):
|
||||
self.assertIsNotNone(co.apply_runtime_arch_abi(bad, 'env'), bad)
|
||||
self.assertIn('built for macos', co.apply_runtime_arch_abi('win-x64', 'env'))
|
||||
self.assertEqual(co.cache_arch_abi(), 'macos-arm64') # unchanged
|
||||
with host('Darwin', 'arm64', 'linux-x64'):
|
||||
code, out = run_main_until_guard(MAIN_ARGV)
|
||||
self.assertEqual(code, 2, out)
|
||||
self.assertIn('PSX_OVERLAY_ARCH_ABI', out)
|
||||
|
||||
def test_windows_is_unaffected(self):
|
||||
for system, machine, gcc in (('Windows', 'AMD64', r'C:\msys64\mingw64\bin\gcc.exe'),
|
||||
('MSYS_NT-10.0-26100', 'x86_64', 'gcc')):
|
||||
with host(system, machine):
|
||||
before = (co.cache_arch_abi(), shard_command(gcc=gcc),
|
||||
shard_command('tcc'), bios_module_command(gcc))
|
||||
with host(system, machine, 'win-x64'):
|
||||
self.assertIsNone(co.apply_runtime_arch_abi('win-x64', 'env'))
|
||||
after = (co.cache_arch_abi(), shard_command(gcc=gcc),
|
||||
shard_command('tcc'), bios_module_command(gcc))
|
||||
self.assertIsNone(co.interpreter_arch_mismatch())
|
||||
self.assertEqual(before, after)
|
||||
self.assertEqual(after[0], 'win-x64')
|
||||
for cmd in after[1:]:
|
||||
self.assertEqual(arch_args(cmd), [], cmd)
|
||||
|
||||
def test_windows_process_arch_comes_from_the_build(self):
|
||||
# platform.machine() on Windows is the MACHINE's (WMI since 3.12):
|
||||
# an x64 Python emulated on Windows on ARM says ARM64, a 32-bit one
|
||||
# on x64 Windows says AMD64. The interpreter's build is the process.
|
||||
for machine, build, want in (('ARM64', 'win-amd64', 'x64'),
|
||||
('AMD64', 'win32', 'x86'),
|
||||
('ARM64', 'win-arm64', 'arm64'),
|
||||
('AMD64', 'mingw_x86_64_msvcrt_gnu', 'x64'),
|
||||
('AMD64', 'mingw_x86_64_ucrt_llvm', 'x64'),
|
||||
('ARM64', 'mingw_aarch64_ucrt_llvm', 'arm64'),
|
||||
('AMD64', 'mingw_i686_msvcrt_gnu', 'x86'),
|
||||
('AMD64', 'mingw', 'x64'),
|
||||
('ARM64', 'mingw', 'arm64'),
|
||||
('ARM64', 'win-arm32', 'arm64'),
|
||||
('AMD64', 'win-unknown', 'x64'),
|
||||
('ARM64', 'custom-abi-tag', 'arm64')):
|
||||
with host('Windows', machine, build=build):
|
||||
self.assertEqual(co.interpreter_arch(), want, (machine, build))
|
||||
|
||||
def test_windows_on_arm_runs_the_x64_zip_unchanged(self):
|
||||
# The x64 zip on Windows on ARM: runtime, Python and tcc all x64,
|
||||
# emulated. Same commands and directory as on x64 Windows; neither
|
||||
# tool refuses.
|
||||
with host('Windows', 'AMD64', 'win-x64'):
|
||||
self.assertIsNone(co.apply_runtime_arch_abi('win-x64', 'env'))
|
||||
want = (co.cache_arch_abi(), shard_command(gcc='gcc'),
|
||||
shard_command('tcc'), bios_module_command('gcc'))
|
||||
with host('Windows', 'ARM64', 'win-x64', build='win-amd64'):
|
||||
self.assertIsNone(co.apply_runtime_arch_abi('win-x64', 'env'))
|
||||
self.assertIsNone(co.interpreter_arch_mismatch())
|
||||
got = (co.cache_arch_abi(), shard_command(gcc='gcc'),
|
||||
shard_command('tcc'), bios_module_command('gcc'))
|
||||
self.assertEqual(got, want)
|
||||
self.assertEqual(got[0], 'win-x64')
|
||||
with host('Windows', 'ARM64', 'win-x64', build='win-amd64'):
|
||||
code, out = run_main_until_guard(MAIN_ARGV)
|
||||
self.assertEqual(code, 'past-guard', out)
|
||||
with host('Windows', 'ARM64', 'win-x64', build='win-amd64'), \
|
||||
contextlib.redirect_stdout(io.StringIO()) as out:
|
||||
with self.assertRaises(SystemExit): # past the guard: no dump
|
||||
bmb.main(['--dump', '/nonexistent', '--stem', 'SCPH1001',
|
||||
'--out', '/nonexistent/m.dll', '--arch-abi', 'win-x64'])
|
||||
self.assertNotIn('cannot build a BIOS module', out.getvalue())
|
||||
self.assertIn('not found', out.getvalue())
|
||||
# A real mismatch on Windows still refuses: an arm64 runtime cannot
|
||||
# use what an x64 Python validates.
|
||||
with host('Windows', 'ARM64', 'win-arm64', build='win-amd64'):
|
||||
code, out = run_main_until_guard(MAIN_ARGV)
|
||||
self.assertEqual(code, 2, out)
|
||||
self.assertIn('runs as x64', out)
|
||||
|
||||
def test_linux_is_unaffected(self):
|
||||
for machine, tag in (('x86_64', 'linux-x64'), ('aarch64', 'linux-arm64')):
|
||||
with host('Linux', machine):
|
||||
before = (co.cache_arch_abi(), shard_command(), bios_module_command())
|
||||
with host('Linux', machine, tag):
|
||||
self.assertIsNone(co.apply_runtime_arch_abi(tag, 'env'))
|
||||
after = (co.cache_arch_abi(), shard_command(), bios_module_command())
|
||||
self.assertEqual(before, after)
|
||||
self.assertEqual(after[0], tag)
|
||||
self.assertEqual(arch_args(after[1]), [])
|
||||
|
||||
def test_tcc_never_gets_arch_flags(self):
|
||||
with host('Darwin', 'x86_64', 'macos-x64'):
|
||||
co.apply_runtime_arch_abi('macos-x64', 'env')
|
||||
self.assertEqual(arch_args(shard_command('tcc')), [])
|
||||
|
||||
|
||||
def _runnable_macos_arches():
|
||||
"""Mach-O arches this Mac can execute: its own, plus x86_64 via Rosetta."""
|
||||
if sys.platform != 'darwin':
|
||||
return []
|
||||
arches = []
|
||||
for name in ('arm64', 'x86_64'):
|
||||
if subprocess.run(['arch', '-' + name, '/usr/bin/true'],
|
||||
capture_output=True).returncode == 0:
|
||||
arches.append(name)
|
||||
return arches
|
||||
|
||||
|
||||
def _lipo_archs(path):
|
||||
return subprocess.run(['lipo', '-archs', path], capture_output=True,
|
||||
text=True, check=True).stdout.split()
|
||||
|
||||
|
||||
@unittest.skipIf(os.name == 'nt', 'POSIX toolchain cases')
|
||||
class RealToolchain(unittest.TestCase):
|
||||
"""The real C compiler and the real runtime sources."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp(prefix='psx_ovl_arch_')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def _cc(self, *args):
|
||||
r = subprocess.run([CC, *args], capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stderr or r.stdout)
|
||||
|
||||
def _write(self, name, text):
|
||||
path = os.path.join(self.tmp, name)
|
||||
with open(path, 'w') as f:
|
||||
f.write(text)
|
||||
return path
|
||||
|
||||
def test_runtime_exports_its_own_arch_to_spawned_compiles(self):
|
||||
# autocompile.c + the four loader entry points it calls, nothing else;
|
||||
# on macOS built universal and run as each slice the Mac can execute.
|
||||
probe = self._write('probe.c', '''
|
||||
#include "autocompile.h"
|
||||
#include "overlay_loader.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
struct OverlayPreparedImage { unsigned id; };
|
||||
OverlayPreparedImage *overlay_loader_prepare_published(const char *p) { (void)p; return NULL; }
|
||||
int overlay_loader_commit_published(OverlayPreparedImage *i) { (void)i; return 0; }
|
||||
void overlay_loader_discard_prepared(OverlayPreparedImage *i) { (void)i; }
|
||||
void overlay_loader_rescan(void) { }
|
||||
int main(void) {
|
||||
const char *v;
|
||||
autocompile_set_cache_paths("cache", "captures.json");
|
||||
autocompile_configure("true", ".");
|
||||
v = getenv("PSX_OVERLAY_ARCH_ABI");
|
||||
printf("%s\\n", v ? v : "(unset)");
|
||||
return !(v && strcmp(v, PSX_OVERLAY_ARCH_ABI) == 0);
|
||||
}
|
||||
''')
|
||||
exe = os.path.join(self.tmp, 'probe')
|
||||
arches = _runnable_macos_arches()
|
||||
flags = [a for name in arches for a in ('-arch', name)]
|
||||
self._cc(*flags, '-I' + INCLUDE, probe,
|
||||
os.path.join(ROOT, 'runtime', 'src', 'autocompile.c'),
|
||||
'-o', exe, '-lpthread')
|
||||
runs = [(['arch', '-' + name, exe], name) for name in arches] or [([exe], None)]
|
||||
for argv, name in runs:
|
||||
r = subprocess.run(argv, capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||
got = r.stdout.split()[-1] if r.stdout.split() else ''
|
||||
if name is None:
|
||||
with host(platform.system(), platform.machine()):
|
||||
want = co.cache_arch_abi() # same OS, same arch as this host
|
||||
self.assertEqual(got, want)
|
||||
else:
|
||||
self.assertEqual(got, {'arm64': 'macos-arm64',
|
||||
'x86_64': 'macos-x64'}[name])
|
||||
|
||||
@unittest.skipUnless(sys.platform == 'darwin', 'macOS -arch')
|
||||
def test_shards_are_the_target_arch_and_load_in_it(self):
|
||||
# compile_overlays' real compile line, for each arch this Mac runs,
|
||||
# whatever this Python is; then load the shard in a process of that
|
||||
# architecture the way the runtime does (dlopen + overlay_abi).
|
||||
shard = self._write('shard.c', 'int overlay_abi(void) { return 0x5A5A; }\n')
|
||||
loader = self._write('loader.c', '''
|
||||
#include <dlfcn.h>
|
||||
#include <stdio.h>
|
||||
int main(int argc, char **argv) {
|
||||
void *h = dlopen(argv[1], RTLD_NOW);
|
||||
int (*abi)(void);
|
||||
if (!h) { printf("dlopen: %s\\n", dlerror()); return 1; }
|
||||
abi = (int (*)(void))dlsym(h, "overlay_abi");
|
||||
return !(abi && abi() == 0x5A5A);
|
||||
}
|
||||
''')
|
||||
loader_exe = os.path.join(self.tmp, 'loader')
|
||||
arches = _runnable_macos_arches()
|
||||
self._cc(*[a for n in arches for a in ('-arch', n)], loader, '-o', loader_exe)
|
||||
for name in arches:
|
||||
tag = {'arm64': 'macos-arm64', 'x86_64': 'macos-x64'}[name]
|
||||
out = os.path.join(self.tmp, f'{name}.so')
|
||||
with host('Darwin', platform.machine(), tag):
|
||||
co.apply_runtime_arch_abi(tag, 'env')
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
ok = co._compile_dll_direct(shard, out, [INCLUDE], gcc=CC)
|
||||
self.assertTrue(ok, name)
|
||||
self.assertEqual(_lipo_archs(out), [name])
|
||||
r = subprocess.run(['arch', '-' + name, loader_exe, out],
|
||||
capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, f'{name}: {r.stdout}{r.stderr}')
|
||||
|
||||
|
||||
def tearDownModule():
|
||||
if _FIXTURE:
|
||||
shutil.rmtree(_FIXTURE, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--cc', default=os.environ.get('CC', 'cc'))
|
||||
opts, rest = ap.parse_known_args()
|
||||
CC = opts.cc
|
||||
unittest.main(argv=[sys.argv[0]] + rest, verbosity=2)
|
||||
@@ -26,9 +26,22 @@ static int g_failures = 0;
|
||||
static uint32_t g_fb_packed = 0;
|
||||
static int32_t g_fb_x16 = 0, g_fb_y16 = 0;
|
||||
static int g_fb_valid = 0;
|
||||
static uint32_t g_fb_probe_lookups = 0;
|
||||
static uint32_t g_fb_render_lookups = 0;
|
||||
|
||||
extern "C" int gte_geometry_correction_lookup(uint32_t packed,
|
||||
int32_t *x16, int32_t *y16) {
|
||||
++g_fb_render_lookups;
|
||||
if (!g_fb_valid || packed != g_fb_packed) return 0;
|
||||
if (x16) *x16 = g_fb_x16;
|
||||
if (y16) *y16 = g_fb_y16;
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" int gte_geometry_correction_lookup_probe(uint32_t packed,
|
||||
int32_t *x16,
|
||||
int32_t *y16) {
|
||||
++g_fb_probe_lookups;
|
||||
if (!g_fb_valid || packed != g_fb_packed) return 0;
|
||||
if (x16) *x16 = g_fb_x16;
|
||||
if (y16) *y16 = g_fb_y16;
|
||||
@@ -555,6 +568,30 @@ int main(void) {
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(b.rect_bypass == a.rect_bypass + 1);
|
||||
CHECK(b.rect_partial == a.rect_partial + 2);
|
||||
|
||||
const int fallback_was_enabled = pgxp_position_fallback();
|
||||
pgxp_set_position_fallback(1);
|
||||
g_fb_valid = 1;
|
||||
g_fb_packed = PACKED;
|
||||
g_fb_x16 = 160 << 16;
|
||||
g_fb_y16 = 80 << 16;
|
||||
const uint32_t render_lookups = g_fb_render_lookups;
|
||||
const uint32_t probe_lookups = g_fb_probe_lookups;
|
||||
pgxp_get_stats(&a);
|
||||
CHECK(pgxp_probe_precise_vertex(0xFFFFFFFFu, PACKED, 160, 80) ==
|
||||
PGXP_SRC_FALLBACK);
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(std::memcmp(&a, &b, sizeof a) == 0);
|
||||
CHECK(g_fb_render_lookups == render_lookups);
|
||||
CHECK(g_fb_probe_lookups == probe_lookups + 1u);
|
||||
int32_t x16, y16;
|
||||
uint16_t sz;
|
||||
CHECK(pgxp_get_precise_vertex(0xFFFFFFFFu, PACKED, 160, 80,
|
||||
&x16, &y16, &sz) == PGXP_SRC_FALLBACK);
|
||||
CHECK(g_fb_render_lookups == render_lookups + 1u);
|
||||
CHECK(g_fb_probe_lookups == probe_lookups + 1u);
|
||||
g_fb_valid = 0;
|
||||
pgxp_set_position_fallback(fallback_was_enabled);
|
||||
}
|
||||
|
||||
/* --- preserve projection: the RTPS-side window check (G1.11) ---------- */
|
||||
|
||||
@@ -82,6 +82,11 @@ extern "C" int gte_geometry_correction_lookup(uint32_t, int32_t*, int32_t*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The session harness has no GTE backend; pre-draw probes must link as a no-op. */
|
||||
extern "C" int gte_geometry_correction_lookup_probe(uint32_t, int32_t*, int32_t*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- the runtime surface mod_runtime.cpp links against ------------------- */
|
||||
|
||||
static std::array<uint8_t, 2 * 1024 * 1024> ram;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "projection_scale_config.hpp"
|
||||
#include "projection_scale.hpp"
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
int main() {
|
||||
const auto file = std::filesystem::temp_directory_path() /
|
||||
("psx-projection-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".toml");
|
||||
struct Cleanup {
|
||||
std::filesystem::path path;
|
||||
~Cleanup() { std::error_code ec; std::filesystem::remove(path, ec); }
|
||||
} cleanup{file};
|
||||
auto load = [&](const char* text) {
|
||||
{ std::ofstream out(file); out << text; }
|
||||
return psx_projection_scale_load_config(file);
|
||||
};
|
||||
if (load("[game]\nname='fixture'\n") != 1.0 ||
|
||||
load("[video]\naspect='4:3'\n") != 1.0 ||
|
||||
load("[video]\nfov_scale=1.0\n") != 1.0 ||
|
||||
load("[video]\nfov_scale=2.0\n") != 2.0 ||
|
||||
load("[video]\nfov_scale=8.0\n") != 8.0) return 1;
|
||||
/* Integer TOML values are accepted. */
|
||||
if (load("[video]\nfov_scale=2\n") != 2.0 ||
|
||||
load("[video]\nfov_scale=1\n") != 1.0 ||
|
||||
load("[video]\nfov_scale=8\n") != 8.0) {
|
||||
std::fprintf(stderr, "integer fov_scale not accepted\n");
|
||||
return 1;
|
||||
}
|
||||
/* Bad values must not throw (a throw would stop the game starting): they
|
||||
* warn on stderr and fall back to 1.0. */
|
||||
for (const char* value : {"0.0", "0", "-1.0", "9.0", "9", "nan", "inf", "'wide'", "true", "[1.0]"}) {
|
||||
const std::string text = std::string("[video]\nfov_scale=") + value + "\n";
|
||||
double got = -1.0;
|
||||
try { got = load(text.c_str()); }
|
||||
catch (...) { std::fprintf(stderr, "threw on invalid scale %s\n", value); return 1; }
|
||||
if (got != 1.0) { std::fprintf(stderr, "invalid scale %s gave %g\n", value, got); return 1; }
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -35,6 +35,7 @@ extern void psx_write_word(uint32_t addr, uint32_t val);
|
||||
extern void psx_write_half(uint32_t addr, uint16_t val);
|
||||
extern void psx_write_byte(uint32_t addr, uint8_t val);
|
||||
extern uint8_t *memory_get_ram_ptr(void);
|
||||
extern uint8_t *memory_get_scratchpad_ptr(void);
|
||||
|
||||
/* Stub-side mutation counters (psx_ram_runtime_map_stubs.c). */
|
||||
extern int g_stub_device_restores;
|
||||
@@ -130,6 +131,39 @@ static void check_guest_access(int expanded) {
|
||||
check(psx_read_byte(0x80204005u) == 0x5Au, "byte reads back at its own address");
|
||||
}
|
||||
|
||||
/* ---- psx_peek_word: debug read that must never reach the bus ----------- */
|
||||
static void check_peek(int expanded) {
|
||||
uint32_t v = 0xDEADBEEFu;
|
||||
memset(memory_get_ram_ptr(), 0, PSX_MAIN_RAM_BACKING_BYTES);
|
||||
psx_write_word(0x80001000u, 0x11223344u);
|
||||
check(psx_peek_word(0x80001000u, &v) && v == 0x11223344u, "peek: KSEG0 RAM");
|
||||
check(psx_peek_word(0x00001000u, &v) && v == 0x11223344u, "peek: KUSEG RAM");
|
||||
check(psx_peek_word(0xA0001000u, &v) && v == 0x11223344u, "peek: KSEG1 RAM");
|
||||
v = 0;
|
||||
check(psx_peek_word(0x80201000u, &v) && ((v == 0x11223344u) == !expanded),
|
||||
"peek: 2nd mirror follows the live geometry");
|
||||
check(psx_peek_word(0x807FFFFCu, &v), "peek: top of the RAM window");
|
||||
memory_get_scratchpad_ptr()[0x10] = 0x78; memory_get_scratchpad_ptr()[0x13] = 0x12;
|
||||
check(psx_peek_word(0x1F800010u, &v) && (v & 0xFF) == 0x78 && (v >> 24) == 0x12,
|
||||
"peek: scratchpad");
|
||||
check(psx_peek_word(0x1F8003FCu, &v) && !psx_peek_word(0x1F800400u, &v),
|
||||
"peek: scratchpad end bound");
|
||||
check(psx_peek_word(0xBFC00000u, &v) && psx_peek_word(0x1FC7FFFCu, &v) &&
|
||||
!psx_peek_word(0x1FC80000u, &v), "peek: BIOS ROM bounds");
|
||||
/* Device / unmapped space is refused WITHOUT touching the bus (the stubbed
|
||||
* psx_fatal_halt traps if a real access reached unmapped_fatal). */
|
||||
v = 0xDEADBEEFu;
|
||||
check(!psx_peek_word(0x1F801044u, &v), "peek: JOY_RX refused");
|
||||
check(!psx_peek_word(0x1F801810u, &v), "peek: GPUREAD refused");
|
||||
check(!psx_peek_word(0x1F802000u, &v), "peek: expansion-2 MMIO refused");
|
||||
check(!psx_peek_word(0x1F000000u, &v), "peek: expansion-1 refused");
|
||||
check(!psx_peek_word(0x80800000u, &v), "peek: past the RAM window refused");
|
||||
check(!psx_peek_word(0xFFFE0130u, &v) && !psx_peek_word(0xC0000000u, &v),
|
||||
"peek: KSEG2 refused");
|
||||
check(!psx_peek_word(0x80001002u, &v), "peek: unaligned refused");
|
||||
check(v == 0xDEADBEEFu, "peek: refused reads leave *out untouched");
|
||||
}
|
||||
|
||||
/* ---- 3. a real DMA writing RAM from a guest-chosen MADR --------------- */
|
||||
#define DMA_BASE 0x1F801080u
|
||||
static void run_otc(uint32_t madr, uint32_t entries) {
|
||||
@@ -288,6 +322,7 @@ int main(void) {
|
||||
for (int expanded = 0; expanded < 2; ++expanded) {
|
||||
set_geometry(expanded);
|
||||
check_guest_access(expanded);
|
||||
check_peek(expanded);
|
||||
check_dma(expanded);
|
||||
}
|
||||
check_savestates();
|
||||
|
||||
@@ -21,8 +21,16 @@
|
||||
#include "cpu_state.h"
|
||||
#include "dirty_ram_interp.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "psx_cyc.h"
|
||||
|
||||
static ModFunctionEntryContext s_mod_entry;
|
||||
void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) {
|
||||
*out = s_mod_entry;
|
||||
}
|
||||
void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) {
|
||||
s_mod_entry = *in;
|
||||
}
|
||||
static int failures;
|
||||
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
|
||||
|
||||
@@ -161,6 +169,11 @@ uint32_t gl_renderer_pass_plan(uint32_t p, uint32_t s, uint32_t *a,
|
||||
return 0;
|
||||
}
|
||||
static int s_open_passes, s_kept;
|
||||
static uint32_t s_stereo_mask;
|
||||
static uint64_t s_stereo_published;
|
||||
void gl_renderer_pass_begin_diag(GLRenderPassBeginDiag *out) {
|
||||
memset(out, 0, sizeof *out);
|
||||
}
|
||||
void gl_renderer_pass_set_flip_shown(int shown) { (void)shown; }
|
||||
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
uint32_t period, int reuse_backup) {
|
||||
@@ -178,6 +191,31 @@ uint32_t gl_renderer_pass_leaks(void) { return 0; }
|
||||
int gl_renderer_pass_verify_vram(void) { return 1; }
|
||||
void gl_renderer_pass_note_cost(uint64_t t) { (void)t; }
|
||||
void gl_renderer_pass_service_presents(void) {}
|
||||
uint32_t g_psx_vblank_cycles = 564480u;
|
||||
uint32_t gl_renderer_stereo_unavailable(void) { return 0; }
|
||||
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) {
|
||||
return gl_renderer_pass_begin(x, y, w, h, 0, 0, reuse);
|
||||
}
|
||||
int gl_renderer_stereo_end(uint32_t eye, int keep) {
|
||||
gl_renderer_pass_end(0, keep);
|
||||
if (keep) s_stereo_mask |= 1u << eye;
|
||||
return 1;
|
||||
}
|
||||
void gl_renderer_stereo_stage_reset(void) { s_stereo_mask = 0; }
|
||||
void gl_renderer_stereo_reset(void) { s_stereo_mask = 0; s_stereo_published = 0; }
|
||||
int gl_renderer_stereo_set_presentation(uint32_t mode) { return mode <= 1; }
|
||||
static int32_t s_view[3];
|
||||
void gte_render_view_get(int32_t xyz[3]) { memcpy(xyz, s_view, sizeof s_view); }
|
||||
void gte_render_view_set(const int32_t xyz[3]) { memcpy(s_view, xyz, sizeof s_view); }
|
||||
static PSXModRenderView s_pose;
|
||||
void gte_render_pose_get(PSXModRenderView *v) { *v = s_pose; memcpy(v->translation, s_view, sizeof s_view); }
|
||||
void gte_render_pose_set(const PSXModRenderView *v) { s_pose = *v; memcpy(s_view, v->translation, sizeof s_view); }
|
||||
int gl_renderer_stereo_publish(uint64_t id, uint64_t cycle, const int32_t view[2][3]) {
|
||||
(void)cycle; (void)view;
|
||||
if (s_stereo_mask != 3u) return 0;
|
||||
s_stereo_published = id;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- an overlay shard's cycle shim (overlay_dispatch_preamble.c.inc) --- */
|
||||
static uint32_t s_shard_pending;
|
||||
@@ -204,6 +242,9 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) {
|
||||
int prev_ov_depth = s_ov_active_depth;
|
||||
uint32_t prev_ov_ip = s_ov_inprogress;
|
||||
void (*prev_flush)(void) = g_overlay_flush_pending_cycles;
|
||||
ModFunctionEntryContext prev_mod = s_mod_entry;
|
||||
s_mod_entry.depth++;
|
||||
s_mod_entry.plugin = f;
|
||||
(void)guard;
|
||||
psx_cyc_bb_defer_begin();
|
||||
g_call_unit_depth = prev_unit + 1; /* overlay_loader_call_native */
|
||||
@@ -232,6 +273,7 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) {
|
||||
}
|
||||
}
|
||||
|
||||
s_mod_entry = prev_mod;
|
||||
s_ld.armed = 0; /* retired on interpreter exit */
|
||||
g_precise_mode = prev_precise;
|
||||
g_dirty_interp_active = prev_active;
|
||||
@@ -281,7 +323,28 @@ static int leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int mod_leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
|
||||
(void)cpu; (void)user; (void)alpha_q16;
|
||||
s_mod_entry.depth++;
|
||||
s_mod_entry.plugin = user;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int stereo_draw(struct CPUState *cpu, void *user, uint32_t eye) {
|
||||
Frames frames = {5, user && eye == PSX_MOD_EYE_RIGHT};
|
||||
CHECK(psx_mod_render_view_offset(eye ? -24 : 24, 0, 0), "view setter inside eye");
|
||||
PSXModRenderView v = {0}; v.struct_size = sizeof v;
|
||||
v.rotation_q12[2] = 4096; v.rotation_q12[4] = 4096; v.rotation_q12[6] = -4096;
|
||||
v.projection = 1; v.fx_q16 = 200 << 16; v.fy_q16 = 100 << 16;
|
||||
v.translation[0] = eye ? -24 : 24;
|
||||
CHECK(psx_mod_render_view(&v), "full scoped pose accepted");
|
||||
|
||||
guest_frame(cpu, &frames, 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
typedef struct Live {
|
||||
ModFunctionEntryContext mod_entry;
|
||||
int bb_defer, unit, dma, host, dma_ch, active, phase, precise, dispatch;
|
||||
int ov_depth;
|
||||
uint32_t ov_ip, batch, resume, span_lo, span_hi;
|
||||
@@ -292,6 +355,7 @@ typedef struct Live {
|
||||
|
||||
static void snap(Live *l) {
|
||||
memset(l, 0, sizeof *l);
|
||||
l->mod_entry = s_mod_entry;
|
||||
l->bb_defer = g_psx_cyc_bb_defer;
|
||||
l->unit = g_call_unit_depth;
|
||||
l->dma = g_dma_exec_depth;
|
||||
@@ -321,6 +385,8 @@ static void check_live(const Live *a, const char *when) {
|
||||
#define SAME(field, what) do { \
|
||||
snprintf(m, sizeof m, "%s: %s restored", when, what); \
|
||||
CHECK(a->field == b.field, m); } while (0)
|
||||
SAME(mod_entry.depth, "mod callback depth");
|
||||
SAME(mod_entry.plugin, "mod callback owner");
|
||||
SAME(bb_defer, "g_psx_cyc_bb_defer");
|
||||
SAME(unit, "g_call_unit_depth");
|
||||
SAME(dma, "g_dma_exec_depth");
|
||||
@@ -366,6 +432,8 @@ int main(void) {
|
||||
/* The live context the pass interrupts: inside two generated functions
|
||||
* (the game's VSync entry hook), a native shard's flush hook installed,
|
||||
* the interpreter's resume latch and a pending load of its own. */
|
||||
s_mod_entry.depth = 1;
|
||||
s_mod_entry.plugin = &cpu;
|
||||
g_psx_cyc_bb_defer = 2;
|
||||
g_psx_cyc_batch = 9;
|
||||
g_psx_dispatch_depth = 3;
|
||||
@@ -390,6 +458,8 @@ int main(void) {
|
||||
CHECK(st.watchdog == 1 && st.aborted == 1 && st.passes == 0,
|
||||
"the watchdog cut it off once");
|
||||
CHECK(st.nesting_repairs == 1, "the landing found and undid skipped exits");
|
||||
CHECK(strstr(st.last_abort_detail, "mod entries +5") &&
|
||||
strstr(st.last_abort_detail, "mod owner"), "TCP abort detail includes skipped mod exits");
|
||||
CHECK(!st.disabled, "one fault does not disable passes");
|
||||
CHECK(s_open_passes == 0 && s_kept == 0, "the presenter closed the pass, no image");
|
||||
CHECK(!g_psx_render_pass_active, "time is live again");
|
||||
@@ -412,6 +482,34 @@ int main(void) {
|
||||
check_live(&live, "after a normal pass");
|
||||
CHECK(st.verify_mismatch == 0, "a balanced pass verifies clean");
|
||||
|
||||
/* One complete pair, then a right eye that aborts five guest frames deep.
|
||||
* The left staging eye never replaces the previous published pair. */
|
||||
{
|
||||
PSXModStereoFrame frame = {sizeof frame, 2, 0, 0, 320, 240};
|
||||
RenderStereoStats stereo;
|
||||
uint64_t kept;
|
||||
snap(&live);
|
||||
CHECK(psx_mod_render_stereo(&cpu, &frame, stereo_draw, NULL) == 1,
|
||||
"complete stereo pair publishes");
|
||||
kept = s_stereo_published;
|
||||
CHECK(psx_mod_render_stereo(&cpu, &frame, stereo_draw, &runaway) == 0,
|
||||
"right-eye watchdog discards the entire pair");
|
||||
render_stereo_get_stats(&stereo);
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(s_stereo_published == kept && s_stereo_mask == 0,
|
||||
"right-eye abort retains previous pair and clears staging");
|
||||
CHECK(stereo.last_failed_eye == 1 && stereo.retained_pair_id == kept,
|
||||
"failure producer records retained pair and right eye");
|
||||
CHECK(s_view[0] == 0 && s_view[1] == 0 && s_view[2] == 0,
|
||||
"watchdog restores render-view ambient");
|
||||
CHECK(!s_pose.struct_size && !s_pose.projection, "watchdog restores rotation/projection");
|
||||
CHECK(st.verify_mismatch == 0 && !g_psx_render_pass_active,
|
||||
"aborted eye restores machine state and time");
|
||||
check_live(&live, "after right-eye watchdog");
|
||||
CHECK(psx_mod_render_stereo(&cpu, &frame, stereo_draw, NULL) == 1,
|
||||
"a later pair recovers after the watchdog");
|
||||
}
|
||||
|
||||
/* 3. Verify mode flags a pass that leaves the nesting unbalanced (the
|
||||
* restore would otherwise hide it), and still restores it. */
|
||||
snap(&live);
|
||||
@@ -420,6 +518,12 @@ int main(void) {
|
||||
CHECK(st.verify_mismatch == 1, "an unbalanced pass is reported in verify mode");
|
||||
check_live(&live, "after an unbalanced pass");
|
||||
|
||||
snap(&live);
|
||||
(void)psx_mod_render_pass(&cpu, &pass, mod_leaky_pass_fn, NULL);
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.verify_mismatch == 2, "mod-only imbalance is reported");
|
||||
check_live(&live, "after a mod-only unbalanced pass");
|
||||
|
||||
/* 4. A guest span runs only inside a pass. */
|
||||
CHECK(psx_mod_run_guest_span(&cpu, 0x800143FCu, 0x8001473Cu) == 0,
|
||||
"a guest span outside a pass is refused");
|
||||
@@ -440,7 +544,8 @@ int main(void) {
|
||||
CHECK(psx_mod_render_pass(&cpu, &pass, span_pass_fn, &runaway) == 0,
|
||||
"a runaway span is rolled back");
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.watchdog == 2, "the watchdog cut the span off");
|
||||
/* Cumulative: the plain abort, the stereo right-eye abort, and this span. */
|
||||
CHECK(st.watchdog == 3, "the watchdog cut the span off");
|
||||
CHECK(s_span_hi == 0 && s_span_lo == 0, "the abort closed the open span");
|
||||
check_live(&live, "after a span watchdog abort");
|
||||
CHECK(s_prec_begins > 0 && s_prec_open == 0,
|
||||
|
||||
@@ -113,7 +113,7 @@ assert "render_pass_cost_add(" in body(
|
||||
gl, "void gl_renderer_pass_note_cost(uint64_t ticks) {"), (
|
||||
"the pass-cost average must leave allocating passes out")
|
||||
assert "s_pass_allocs_begin = s_pass_allocs;" in body(
|
||||
gl, "int gl_renderer_pass_begin("), (
|
||||
gl, "static int transaction_begin("), (
|
||||
"each pass must mark where its allocations start")
|
||||
# The average belongs to one presented image size. A plan at another size
|
||||
# passes an unknown cost (0), for which render_pass_plan_phases plans a single
|
||||
|
||||
@@ -315,7 +315,19 @@ static void test_store_policy(void) {
|
||||
CHECK(render_pass_mmio_class(0x1F801820u, 0, 4) == RENDER_PASS_DROP_OTHER, "MDEC dropped");
|
||||
}
|
||||
|
||||
static void test_stereo_pair_fresh(void) {
|
||||
const uint32_t vb = 564480u;
|
||||
const uint64_t c = 100000000ull;
|
||||
CHECK(render_pass_stereo_pair_fresh(c, c, vb), "pair fresh on its own cycle");
|
||||
CHECK(render_pass_stereo_pair_fresh(c, c + 8ull * vb, vb), "pair fresh across the slowest cadence");
|
||||
CHECK(render_pass_stereo_pair_fresh(c, c + 24ull * vb, vb), "pair fresh at the age limit");
|
||||
CHECK(!render_pass_stereo_pair_fresh(c, c + 24ull * vb + 1, vb),
|
||||
"pair stale once the plugin stops submitting");
|
||||
CHECK(!render_pass_stereo_pair_fresh(c, c - 1, vb), "clock behind pair (state load) is stale");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_stereo_pair_fresh();
|
||||
test_store_policy();
|
||||
test_counts_per_rate();
|
||||
test_shedding();
|
||||
|
||||
@@ -34,9 +34,17 @@
|
||||
#include "cpu_state.h"
|
||||
#include "dirty_ram_interp.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "mod_runtime.h"
|
||||
#include "psx_cycles.h"
|
||||
#include "timers.h"
|
||||
|
||||
static ModFunctionEntryContext s_mod_entry;
|
||||
void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) {
|
||||
*out = s_mod_entry;
|
||||
}
|
||||
void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) {
|
||||
s_mod_entry = *in;
|
||||
}
|
||||
static int failures;
|
||||
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
|
||||
|
||||
@@ -194,7 +202,8 @@ void gte_precision_checkpoint_rollback(void) { s_prec_open--; }
|
||||
|
||||
/* GPU and presenter. */
|
||||
uint64_t gpu_pass_state_hash(void) { return 42; }
|
||||
int gpu_pass_checkpoint_save(void) { return 1; }
|
||||
static int s_checkpoint_ok = 1;
|
||||
int gpu_pass_checkpoint_save(void) { return s_checkpoint_ok; }
|
||||
void gpu_pass_checkpoint_restore(void) {}
|
||||
static uint64_t s_ticks;
|
||||
uint64_t gl_renderer_perf_ticks(void) { return s_ticks += 1000; }
|
||||
@@ -207,12 +216,20 @@ uint32_t gl_renderer_pass_plan(uint32_t p, uint32_t s, uint32_t *a,
|
||||
if (wanted) *wanted = 0;
|
||||
return 0;
|
||||
}
|
||||
static int s_open_passes, s_kept;
|
||||
static int s_open_passes, s_kept, s_begin_ok = 1, s_diag_null;
|
||||
void gl_renderer_pass_begin_diag(GLRenderPassBeginDiag *out) {
|
||||
memset(out, 0, sizeof *out);
|
||||
if (s_diag_null) return;
|
||||
out->reason = "capture_size";
|
||||
out->requested_w = 320; out->requested_h = 240;
|
||||
out->capture_w = 512; out->capture_h = 240;
|
||||
}
|
||||
void gl_renderer_pass_set_flip_shown(int shown) { (void)shown; }
|
||||
int gl_renderer_pass_begin(int x, int y, int w, int h, int open_gen,
|
||||
uint32_t period, int reuse_backup) {
|
||||
(void)x; (void)y; (void)w; (void)h; (void)open_gen; (void)period;
|
||||
(void)reuse_backup;
|
||||
if (!s_begin_ok) return 0;
|
||||
s_open_passes++;
|
||||
return 1;
|
||||
}
|
||||
@@ -225,6 +242,35 @@ uint32_t gl_renderer_pass_leaks(void) { return 0; }
|
||||
int gl_renderer_pass_verify_vram(void) { return 1; }
|
||||
void gl_renderer_pass_note_cost(uint64_t t) { (void)t; }
|
||||
void gl_renderer_pass_service_presents(void) {}
|
||||
static uint32_t s_stereo_mask;
|
||||
static uint64_t s_stereo_published;
|
||||
static int s_capture_fail_eye = -1;
|
||||
uint32_t gl_renderer_stereo_unavailable(void) { return s_gl_status; }
|
||||
int gl_renderer_stereo_begin(int x, int y, int w, int h, int reuse) {
|
||||
return gl_renderer_pass_begin(x, y, w, h, 0, 0, reuse);
|
||||
}
|
||||
int gl_renderer_stereo_end(uint32_t eye, int keep) {
|
||||
gl_renderer_pass_end(0, keep);
|
||||
if (keep && (int)eye == s_capture_fail_eye) return 0;
|
||||
if (keep) s_stereo_mask |= 1u << eye;
|
||||
return 1;
|
||||
}
|
||||
void gl_renderer_stereo_stage_reset(void) { s_stereo_mask = 0; }
|
||||
void gl_renderer_stereo_reset(void) { s_stereo_mask = 0; s_stereo_published = 0; }
|
||||
int gl_renderer_stereo_set_presentation(uint32_t mode) { return mode <= 1; }
|
||||
static int32_t s_view[3];
|
||||
void gte_render_view_get(int32_t xyz[3]) { memcpy(xyz, s_view, sizeof s_view); }
|
||||
void gte_render_view_set(const int32_t xyz[3]) { memcpy(s_view, xyz, sizeof s_view); }
|
||||
static PSXModRenderView s_pose;
|
||||
void gte_render_pose_get(PSXModRenderView *v) { *v = s_pose; memcpy(v->translation, s_view, sizeof s_view); }
|
||||
void gte_render_pose_set(const PSXModRenderView *v) { s_pose = *v; memcpy(s_view, v->translation, sizeof s_view); }
|
||||
int gl_renderer_stereo_publish(uint64_t id, uint64_t cycle, const int32_t view[2][3]) {
|
||||
(void)cycle;
|
||||
(void)view;
|
||||
if (s_stereo_mask != 3u) return 0;
|
||||
s_stereo_published = id;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- 1. store policy --------------------------------------------------- */
|
||||
static void test_store_policy(void) {
|
||||
@@ -411,6 +457,48 @@ static void test_pass(void) {
|
||||
"no pass while the backend declines");
|
||||
s_gl_status = PSX_MOD_RENDER_PASS_READY;
|
||||
CHECK(psx_mod_render_pass_status() == PSX_MOD_RENDER_PASS_READY, "ready again");
|
||||
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.status_refused == 2 && st.refused == 0 &&
|
||||
st.last_failure.status == PSX_MOD_RENDER_PASS_BACKEND,
|
||||
"pass refusal counters are separate from plan refusals");
|
||||
s_begin_ok = 0;
|
||||
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0, "begin refusal");
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.begin_refused == 1 && st.checkpoint_refused == 0 &&
|
||||
strcmp(st.last_failure.reason, "capture_size") == 0 &&
|
||||
st.last_failure.gl.requested_w == 320 && st.last_failure.gl.capture_w == 512,
|
||||
"begin failure retains producer dimensions");
|
||||
uint64_t failed_attempt = st.last_failure.attempt;
|
||||
s_begin_ok = 1;
|
||||
s_checkpoint_ok = 0;
|
||||
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0, "checkpoint refusal");
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.checkpoint_refused == 1 && s_open_passes == 0 &&
|
||||
strcmp(st.last_failure.reason, "checkpoint_gpu") == 0 &&
|
||||
st.last_failure.attempt > failed_attempt,
|
||||
"checkpoint failure closes transaction without calling guest code");
|
||||
failed_attempt = st.last_failure.attempt;
|
||||
s_checkpoint_ok = 1;
|
||||
/* A refusal whose producer named no reason still reads as a failure. */
|
||||
s_begin_ok = 0;
|
||||
s_diag_null = 1;
|
||||
CHECK(psx_mod_render_pass(&cpu, &pass, never_fn, NULL) == 0, "anonymous begin refusal");
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.last_failure.reason && st.last_failure.reason[0] &&
|
||||
st.last_failure.attempt > failed_attempt,
|
||||
"refusal without a producer reason records a fallback reason");
|
||||
failed_attempt = st.last_failure.attempt;
|
||||
s_diag_null = 0;
|
||||
s_begin_ok = 1;
|
||||
CHECK(psx_mod_render_pass(&cpu, &pass, pass_fn, NULL) == 1, "success after refusals");
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(st.last_failure.attempt == failed_attempt && st.pass_attempts == 7,
|
||||
"success does not erase last failure");
|
||||
render_pass_reset_session();
|
||||
render_pass_get_stats(&st);
|
||||
CHECK(!st.last_failure.reason && st.pass_attempts == 0,
|
||||
"session reset clears failure diagnostics");
|
||||
}
|
||||
|
||||
/* ---- 2b. 8 MiB main RAM live (psx.enhancement.8mb-ram) ------------------ */
|
||||
@@ -572,6 +660,63 @@ static void test_journal(void) {
|
||||
#undef POLICY
|
||||
}
|
||||
|
||||
static int s_eye_calls, s_eye_decline = -1;
|
||||
static int pair_draw(CPUState *cpu, void *user, uint32_t eye) {
|
||||
(void)user;
|
||||
CHECK(cpu->gpr[8] == 123 && s_ram[0x456] == 0x77,
|
||||
"both eyes start from original CPU and RAM");
|
||||
CHECK(psx_mod_render_stereo_status() == PSX_MOD_RENDER_PASS_BUSY,
|
||||
"nested pair capture is busy");
|
||||
s_eye_calls++;
|
||||
CHECK(s_view[0] == 0, "view offset restored before each eye");
|
||||
CHECK(psx_mod_render_view_offset(eye ? 24 : -24, 0, 0), "scoped view accepted");
|
||||
PSXModRenderView v = {0}; v.struct_size = sizeof v;
|
||||
v.rotation_q12[2] = 4096; v.rotation_q12[4] = 4096; v.rotation_q12[6] = -4096;
|
||||
v.projection = 1; v.fx_q16 = 200 << 16; v.fy_q16 = 100 << 16;
|
||||
v.translation[0] = eye ? 24 : -24;
|
||||
CHECK(psx_mod_render_view(&v), "full scoped pose accepted");
|
||||
|
||||
cpu->gpr[8] = eye + 1; s_ram[0x456] = (uint8_t)eye;
|
||||
psx_advance_cycles(5000u + eye);
|
||||
return (int)eye != s_eye_decline;
|
||||
}
|
||||
static void test_stereo(void) {
|
||||
CPUState cpu = {0};
|
||||
PSXModStereoFrame f = {sizeof f, 2, 0, 240, 320, 240};
|
||||
RenderStereoStats stats;
|
||||
uint64_t published, cycle;
|
||||
render_pass_reset_session(); s_gl_status = 0; s_begin_ok = 1;
|
||||
cpu.gpr[8] = 123; s_ram[0x456] = 0x77;
|
||||
cycle = psx_cycle_count;
|
||||
CHECK(psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) == 1,
|
||||
"complete pair publishes without a temporal plan");
|
||||
render_stereo_get_stats(&stats);
|
||||
CHECK(stats.pairs == 1 && s_eye_calls == 2 && s_open_passes == 0,
|
||||
"two restored eye transactions form one pair");
|
||||
CHECK(stats.eye_cycle[0] == cycle && stats.eye_cycle[1] == cycle &&
|
||||
stats.eye_hash[0] == stats.eye_hash[1], "same checkpoint identifiers");
|
||||
CHECK(cpu.gpr[8] == 123 && s_ram[0x456] == 0x77 && psx_cycle_count == cycle,
|
||||
"live state restored after the pair");
|
||||
CHECK(s_view[0] == 0 && stats.eye_view[0][0] == -24 && stats.eye_view[1][0] == 24,
|
||||
"producer offsets are recorded and do not escape");
|
||||
CHECK(s_pose.struct_size == 0 && !s_pose.projection, "pose restored after pair");
|
||||
CHECK(!psx_mod_render_view_offset(1, 0, 0), "view rejected outside render callback");
|
||||
published = s_stereo_published;
|
||||
s_eye_decline = 1;
|
||||
CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) &&
|
||||
s_stereo_published == published && s_stereo_mask == 0,
|
||||
"right decline preserves previous complete pair");
|
||||
s_eye_decline = -1; s_capture_fail_eye = 1;
|
||||
CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL) &&
|
||||
s_stereo_published == published && s_stereo_mask == 0,
|
||||
"right texture capture failure cannot publish half a pair");
|
||||
s_capture_fail_eye = -1;
|
||||
CHECK(cpu.gpr[8] == 123 && s_ram[0x456] == 0x77 && psx_cycle_count == cycle,
|
||||
"capture failure still restores live state");
|
||||
f.period_vblanks = 0;
|
||||
CHECK(!psx_mod_render_stereo(&cpu, &f, pair_draw, NULL), "invalid cadence refused");
|
||||
}
|
||||
|
||||
static void test_texture_stream_journal(void) {
|
||||
RenderPassJournal j = {0};
|
||||
memcpy(s_vram, s_vram0, sizeof s_vram);
|
||||
@@ -598,6 +743,7 @@ int main(void) {
|
||||
test_pass();
|
||||
test_ram_8mb();
|
||||
test_journal();
|
||||
test_stereo();
|
||||
test_texture_stream_journal();
|
||||
CHECK(s_prec_open == 0, "precision checkpoints balanced");
|
||||
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "vr_pose_math.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#define CHECK(x) do {if(!(x)){fprintf(stderr,"FAIL line %d\n",__LINE__);return 1;}}while(0)
|
||||
int main(void) {
|
||||
const double q[4]={0,0,0,1},p[3]={-.0335,0,0},origin[3]={0,0,0};
|
||||
const double f[4]={-atan(1),atan(1),atan(.5),-atan(.5)};
|
||||
PSXModRenderView v;
|
||||
CHECK(vr_pose_to_view(q,p,q,origin,f,1000,512,240,&v));
|
||||
CHECK(v.translation[0]==34 && v.translation[1]==0 && v.translation[2]==0);
|
||||
CHECK(v.rotation_q12[0]==4096 && v.rotation_q12[4]==4096 && v.rotation_q12[8]==4096);
|
||||
CHECK(v.fx_q16==256*65536 && v.fy_q16==240*65536);
|
||||
CHECK(v.cx_delta_q16==0 && v.cy_delta_q16==0);
|
||||
double move[3]={0,.1,-.2};
|
||||
CHECK(vr_pose_to_view(q,move,q,origin,f,1000,512,240,&v));
|
||||
CHECK(v.translation[1]==100 && v.translation[2]==-200);
|
||||
const double yaw[4]={0,sin(.2),0,cos(.2)};
|
||||
CHECK(vr_pose_to_view(yaw,origin,q,origin,f,1000,512,240,&v));
|
||||
CHECK(v.rotation_q12[2]>0 && v.rotation_q12[6]<0);
|
||||
CHECK(vr_pose_to_view(yaw,move,yaw,move,f,1000,512,240,&v));
|
||||
CHECK(v.rotation_q12[0]==4096 && v.translation[0]==0 && v.translation[2]==0);
|
||||
double asymmetric[4]={-atan(.8),atan(1.2),atan(.7),-atan(.3)};
|
||||
CHECK(vr_pose_to_view(q,origin,q,origin,asymmetric,1000,512,240,&v));
|
||||
CHECK(v.cx_delta_q16==-3355443 && v.cy_delta_q16==3145728);
|
||||
double bad[4]={0,0,0,0};CHECK(!vr_pose_to_view(bad,p,q,origin,f,1000,512,240,&v));
|
||||
CHECK(!vr_pose_to_view(q,p,q,origin,f,NAN,512,240,&v));
|
||||
double r[9],t[3];
|
||||
CHECK(vr_pose_to_transform(q,move,q,origin,1000,r,t));
|
||||
CHECK(r[0]==1 && r[4]==1 && r[8]==1 && t[1]==-100 && t[2]==200);
|
||||
CHECK(vr_pose_to_transform(yaw,move,q,origin,1000,r,t));
|
||||
CHECK(r[2]<0 && r[6]>0); /* XR +Y rotation aims to PSX left. */
|
||||
CHECK(vr_pose_to_view(yaw,move,q,origin,f,1000,512,240,&v));
|
||||
for(int row=0;row<3;row++)for(int col=0;col<3;col++) {
|
||||
double n=0;for(int k=0;k<3;k++)n+=v.rotation_q12[row*3+k]/4096.0*r[k*3+col];
|
||||
CHECK(fabs(n-(row==col))<.0005);
|
||||
}
|
||||
for(int row=0;row<3;row++) {
|
||||
double n=v.translation[row];
|
||||
for(int k=0;k<3;k++)n+=v.rotation_q12[row*3+k]/4096.0*t[k];
|
||||
CHECK(fabs(n)<.6); /* View's integer translation quantizes the inverse. */
|
||||
}
|
||||
CHECK(vr_pose_to_transform(yaw,move,yaw,move,1000,r,t));
|
||||
CHECK(fabs(r[0]-1)<1e-10 && fabs(r[8]-1)<1e-10 && t[0]==0 && t[2]==0);
|
||||
CHECK(!vr_pose_to_transform(bad,p,q,origin,1000,r,t));
|
||||
CHECK(!vr_pose_to_transform(q,p,q,origin,NAN,r,t));
|
||||
puts("PASS: metric poses, axis signs, inverse controller transform, recenter and FOV");return 0;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Execute the production trace-query handler against a wrapped fixture ring.
|
||||
|
||||
Compile only the handler and its real JSON helpers, replacing the transport
|
||||
with stdout. This needs a C compiler, but no game image, SDL or live server.
|
||||
Exits 77 (ctest SKIP_RETURN_CODE) when no C compiler is available.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
SKIP_EXIT = 77
|
||||
|
||||
|
||||
def matching_brace(source, open_index):
|
||||
"""Index of the brace closing the one at open_index (skips strings/comments)."""
|
||||
depth, i, n = 0, open_index, len(source)
|
||||
while i < n:
|
||||
c = source[i]
|
||||
if source.startswith("/*", i):
|
||||
i = source.index("*/", i + 2) + 1
|
||||
elif source.startswith("//", i):
|
||||
i = source.index("\n", i)
|
||||
elif c in "\"'":
|
||||
i += 1
|
||||
while source[i] != c:
|
||||
i += 2 if source[i] == "\\" else 1
|
||||
elif c == "{":
|
||||
depth += 1
|
||||
elif c == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return i
|
||||
i += 1
|
||||
raise AssertionError("unbalanced braces")
|
||||
|
||||
|
||||
def find_compiler():
|
||||
"""argv prefix for the C compiler named by CC (default cc), or None."""
|
||||
setting = os.environ.get("CC", "cc")
|
||||
if shutil.which(setting):
|
||||
return [setting]
|
||||
parts = shlex.split(setting, posix=os.name != "nt")
|
||||
return parts if parts and shutil.which(parts[0]) else None
|
||||
|
||||
|
||||
class WriteTraceQuery(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
compiler = find_compiler()
|
||||
if compiler is None:
|
||||
raise unittest.SkipTest("no C compiler (set CC)")
|
||||
cls.temp = tempfile.TemporaryDirectory()
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
source = (Path(__file__).parents[1] / "src/debug_server.c").read_text(encoding="utf-8")
|
||||
|
||||
def function(name):
|
||||
match = re.search(r"static [^;{}]+\b" + name + r"\([^;{}]*\)\s*\{", source)
|
||||
if not match:
|
||||
raise AssertionError(f"production function missing: {name}")
|
||||
return source[match.start():matching_brace(source, match.end() - 1) + 1]
|
||||
|
||||
end = source.index("} WriteTraceEntry;") + len("} WriteTraceEntry;")
|
||||
start = source.rfind("typedef struct {", 0, end)
|
||||
harness = """#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#define WRITE_TRACE_CAP 8
|
||||
""" + source[start:end] + """
|
||||
static WriteTraceEntry ring[WRITE_TRACE_CAP], *s_wtrace=ring;
|
||||
static uint64_t s_wtrace_seq=10;
|
||||
static uint32_t s_wtrace_head=2;
|
||||
typedef struct { uint32_t pc; uint32_t gpr[32]; } CPUState;
|
||||
static CPUState *debug_cpu_ptr = NULL;
|
||||
static uint32_t s_frame_count = 0;
|
||||
uint32_t g_debug_current_func_addr = 0;
|
||||
uint32_t g_debug_last_store_pc = 0;
|
||||
int g_dma_exec_depth = 0, g_dma_cur_ch = -1;
|
||||
uint32_t g_dma_initiator_pc = 0;
|
||||
static void debug_server_send_line(const char *line) { puts(line); }
|
||||
static void send_err(int id, const char *error) { (void)id; (void)error; abort(); }
|
||||
"""
|
||||
for name in ("json_get_str", "json_get_int", "hex_to_u32", "wtrace_fill_entry",
|
||||
"handle_wtrace_dump"):
|
||||
harness += function(name) + "\n"
|
||||
# seq 7 is a DMA entry with an unknown (0) initiator PC.
|
||||
harness += """int main(int argc, char **argv) {
|
||||
if (argc > 4 && !strcmp(argv[1], "fill")) { /* fill <dma_depth> <initiator> <cpu_store_pc> */
|
||||
WriteTraceEntry e; memset(&e, 0, sizeof e);
|
||||
g_dma_exec_depth = atoi(argv[2]); g_dma_cur_ch = 2;
|
||||
g_dma_initiator_pc = (uint32_t)strtoul(argv[3], 0, 16);
|
||||
g_debug_last_store_pc = (uint32_t)strtoul(argv[4], 0, 16);
|
||||
wtrace_fill_entry(&e, 1, 0x1000, 0, 1, 4);
|
||||
printf("{\\"pc\\":%u,\\"dma_ch\\":%d}\\n", e.pc, e.dma_ch);
|
||||
return 0;
|
||||
}
|
||||
const uint32_t pcs[8]={0x80001000,0x80001004,0xA0001000,0x80001008,
|
||||
0x8000100C,0x00000000,0x80001004,0x00001004};
|
||||
for (int seq=2; seq<10; ++seq) {
|
||||
WriteTraceEntry *e=&ring[seq%WRITE_TRACE_CAP];
|
||||
e->seq=seq; e->pc=pcs[seq-2]; e->addr=0x1000+(seq-2)*4;
|
||||
e->frame=10+(seq-2)/2; e->width=4; e->dma_ch=seq==5?2:(seq==7?3:-1);
|
||||
}
|
||||
handle_wtrace_dump(7,argc>1?argv[1]:"{}");
|
||||
return 0;
|
||||
}
|
||||
"""
|
||||
path = Path(cls.temp.name)
|
||||
(path / "query.c").write_text(harness, encoding="utf-8", newline="\n")
|
||||
cls.exe = path / ("query.exe" if os.name == "nt" else "query")
|
||||
if Path(compiler[0]).stem.lower() in ("cl", "clang-cl"):
|
||||
out = ["/nologo", "/Fe:" + str(cls.exe), "/Fo:" + str(path) + os.sep]
|
||||
else:
|
||||
out = ["-o", str(cls.exe)]
|
||||
subprocess.run([*compiler, str(path / "query.c"), *out], check=True, cwd=path)
|
||||
|
||||
def query(self, **params):
|
||||
result = subprocess.check_output([str(self.exe), json.dumps(params)], text=True)
|
||||
reply = json.loads(result)
|
||||
self.assertEqual(reply["emitted"], len(reply["entries"]))
|
||||
return reply
|
||||
|
||||
def sequences(self, **params):
|
||||
return [e["seq"] for e in self.query(**params)["entries"]]
|
||||
|
||||
def fill(self, depth, initiator, store_pc):
|
||||
out = subprocess.check_output(
|
||||
[str(self.exe), "fill", str(depth), initiator, store_pc], text=True)
|
||||
return json.loads(out)
|
||||
|
||||
def test_fill_entry_producer_pc(self):
|
||||
self.assertEqual(self.fill(0, "0", "80001234"), {"pc": 0x80001234, "dma_ch": -1})
|
||||
self.assertEqual(self.fill(1, "80005000", "80001234"), {"pc": 0x80005000, "dma_ch": 2})
|
||||
# Unknown initiator: the DMA entry must not inherit the stale CPU store PC.
|
||||
self.assertEqual(self.fill(1, "0", "80001234"), {"pc": 0, "dma_ch": 2})
|
||||
|
||||
def test_default_wrapped_order(self):
|
||||
self.assertEqual(self.sequences(), list(range(2, 10)))
|
||||
|
||||
def test_pc_range_exclusive_upper(self):
|
||||
# seq 9 has KUSEG pc 0x1004, which aliases 0x80001004.
|
||||
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C"), [3, 5, 8, 9])
|
||||
|
||||
def test_segments_alias(self):
|
||||
self.assertEqual(self.sequences(pc_lo="A0001000", pc_hi="A0001004"), [2, 4])
|
||||
self.assertEqual(self.sequences(pc_lo="00001000", pc_hi="00001004"), [2, 4])
|
||||
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="80001008"), [3, 8, 9])
|
||||
|
||||
def test_unknown_dma_initiator_never_matches_pc_filter(self):
|
||||
self.assertIn(7, self.sequences())
|
||||
self.assertNotIn(7, self.sequences(pc_lo="0", pc_hi="FFFFFFFF"))
|
||||
self.assertEqual(self.sequences(pc_hi="80001000"), [])
|
||||
|
||||
def test_newest_and_count_after_filter(self):
|
||||
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C", newest=1, count=1), [9])
|
||||
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C", count=1), [3])
|
||||
|
||||
def test_combined_address_frame_filters(self):
|
||||
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="8000100C",
|
||||
addr_lo="80001008", addr_hi="80001018",
|
||||
frame_lo=11, frame_hi=12), [5])
|
||||
|
||||
def test_empty_ranges(self):
|
||||
self.assertEqual(self.sequences(pc_lo="80001004", pc_hi="80001004"), [])
|
||||
self.assertEqual(self.sequences(pc_hi="0"), [])
|
||||
|
||||
def test_dma_recorded_initiator_and_response_bounds(self):
|
||||
reply = self.query(pc_lo="80001008", pc_hi="8000100C")
|
||||
self.assertEqual(reply["pc_lo"], "0x80001008")
|
||||
self.assertEqual(reply["pc_hi"], "0x8000100C")
|
||||
self.assertEqual(reply["entries"][0]["dma_ch"], 2)
|
||||
self.assertEqual(reply["entries"][0]["seq"], 5)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if find_compiler() is None:
|
||||
print("SKIP: no C compiler (set CC)")
|
||||
sys.exit(SKIP_EXIT)
|
||||
unittest.main()
|
||||
@@ -33,7 +33,7 @@ the exact dump its code describes.
|
||||
USAGE (as the runtime invokes it)
|
||||
python3 bios_module_build.py --dump <SCPH1001.BIN> --toolchain <dir> \
|
||||
--stem SCPH1001 --out <cache>/SCPH1001_<crc8>.so --flavor 0 \
|
||||
--compiler gcc|tcc [--gcc <cc>] [--tcc <tcc>]
|
||||
--compiler gcc|tcc [--gcc <cc>] [--tcc <tcc>] [--arch-abi <os>-<arch>]
|
||||
|
||||
The toolchain dir is overlay_toolchain/ as tools/release_stage.py stages it
|
||||
(python, psxrecomp-bios, include/, bios/*.toml, recompiler/seeds/). Each
|
||||
@@ -143,7 +143,7 @@ def compile_module(sources: list[str], out: str, include_dirs: list[str],
|
||||
else:
|
||||
env, tc_dir = co._toolchain_env(gcc)
|
||||
pic = [] if co.is_windows() else ["-fPIC"]
|
||||
cmd = [gcc, "-shared", *pic, "-O2", *defines,
|
||||
cmd = [gcc, "-shared", *pic, *co.target_arch_flags("gcc"), "-O2", *defines,
|
||||
*[co.native_path(s) for s in sources],
|
||||
"-o", co.native_path(out),
|
||||
*[f"-I{co.native_path(d)}" for d in include_dirs], "-lm"]
|
||||
@@ -187,6 +187,9 @@ def main(argv=None) -> int:
|
||||
ap.add_argument("--stem", required=True, help="profile stem, e.g. SCPH1001 (bios/<stem>.toml)")
|
||||
ap.add_argument("--out", required=True, help="module path to publish (.so/.dll)")
|
||||
ap.add_argument("--flavor", type=int, default=0, help="PSX_OVERLAY_FLAVOR of the host build")
|
||||
ap.add_argument("--arch-abi", default=None,
|
||||
help="\"<os>-<arch>\" of the host build (overlay_loader.h's PSX_OVERLAY_ARCH_ABI); "
|
||||
"default: PSX_OVERLAY_ARCH_ABI from the environment, else this interpreter's")
|
||||
ap.add_argument("--compiler", choices=("gcc", "tcc"), default="gcc")
|
||||
ap.add_argument("--gcc", default="gcc")
|
||||
ap.add_argument("--tcc", default="tcc")
|
||||
@@ -197,6 +200,22 @@ def main(argv=None) -> int:
|
||||
ap.add_argument("--keep-work", action="store_true", help="keep the temporary emit tree (debugging)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
# Build for the HOST's architecture, which on macOS need not be the
|
||||
# compiler binary's default (an x86_64 runtime under Rosetta, arm64-only
|
||||
# Xcode clang). The self-check loads the module into this interpreter, so
|
||||
# the interpreter has to run as that architecture as well.
|
||||
env_arch = os.environ.get("PSX_OVERLAY_ARCH_ABI")
|
||||
if env_arch and args.arch_abi and env_arch != args.arch_abi:
|
||||
print(f"PSX_OVERLAY_ARCH_ABI overrides --arch-abi: {env_arch}")
|
||||
arch_err = co.apply_runtime_arch_abi(
|
||||
env_arch or args.arch_abi,
|
||||
"PSX_OVERLAY_ARCH_ABI" if env_arch else "--arch-abi")
|
||||
if arch_err:
|
||||
die(arch_err)
|
||||
arch_mismatch = co.interpreter_arch_mismatch()
|
||||
if arch_mismatch:
|
||||
die(f"cannot build a BIOS module: {arch_mismatch}")
|
||||
|
||||
tk = os.path.abspath(args.toolchain)
|
||||
# Everything absolute: the emitter runs with cwd inside the staged work
|
||||
# tree, and the compiler is handed paths from more than one directory.
|
||||
|
||||
+156
-19
@@ -56,6 +56,7 @@ except ImportError:
|
||||
import json
|
||||
import platform
|
||||
import re
|
||||
import sysconfig
|
||||
|
||||
|
||||
def codegen_ver(runtime_include: str) -> int:
|
||||
@@ -236,28 +237,141 @@ def native_path(p: str) -> str:
|
||||
return os.path.abspath(p)
|
||||
|
||||
|
||||
# The arch the shards are built for: the RUNTIME's, not this interpreter's.
|
||||
# None means "this interpreter's" (manual/offline use). The spawning runtime
|
||||
# pins it through PSX_OVERLAY_ARCH_ABI (autocompile.c), the same way it pins
|
||||
# the cache dir and the flavor, because the interpreter, the compiler and the
|
||||
# runtime need not share an architecture on macOS: an x86_64 runtime under
|
||||
# Rosetta runs an x86_64 Python, but Xcode's clang is an arm64-only binary and
|
||||
# defaults to arm64. Without -arch every shard it built was arm64, landed in
|
||||
# the macos-x64 cache and never loaded, so the x64 runtime ran every overlay
|
||||
# on the interpreter.
|
||||
_TARGET_ARCH = None
|
||||
_ARCH_ABI_OSES = ('win', 'linux', 'macos')
|
||||
_ARCH_ABI_ARCHES = ('x64', 'arm64', 'x86', 'unknown')
|
||||
# Apple's driver names for the arch-abi arches (clang -arch / gcc -arch).
|
||||
_DARWIN_ARCH_NAMES = {'x64': 'x86_64', 'arm64': 'arm64', 'x86': 'i386'}
|
||||
|
||||
|
||||
def _arch_abi_arch(machine: str) -> str:
|
||||
"""A machine/arch name (uname, PE, MSYS2 tag) in arch-abi spelling."""
|
||||
m = machine.lower()
|
||||
if m in ('amd64', 'x86_64', 'x64'):
|
||||
return 'x64'
|
||||
if m in ('arm64', 'aarch64'):
|
||||
return 'arm64'
|
||||
if m in ('i386', 'i686', 'x86'):
|
||||
return 'x86'
|
||||
return 'unknown'
|
||||
|
||||
|
||||
def interpreter_arch() -> str:
|
||||
"""Arch of THIS Python process, in arch-abi spelling: the only
|
||||
architecture ctypes can load here.
|
||||
|
||||
macOS and Linux: platform.machine() is uname's, which is the process's
|
||||
own (x86_64 for a Python running under Rosetta).
|
||||
|
||||
Native Windows: platform.machine() is the MACHINE's, not the process's
|
||||
(Win32_Processor through WMI since Python 3.12, PROCESSOR_ARCHITEW6432
|
||||
for a WOW64 process before), so an x64 Python emulated on Windows on ARM
|
||||
says ARM64 and a 32-bit one on x64 Windows says AMD64. The interpreter's
|
||||
own build names the process there: sysconfig.get_platform() is
|
||||
win-amd64 / win-arm64 / win32 for python.org builds (from the compiler
|
||||
tag in sys.version) and mingw_<arch>_<crt>_<cc> for MSYS2's."""
|
||||
if os.name == 'nt':
|
||||
plat = sysconfig.get_platform().lower()
|
||||
if plat == 'win32':
|
||||
return 'x86'
|
||||
m = re.match(r'(?:win-|mingw_)(amd64|arm64|x86_64|aarch64|i686)(?:_|$)', plat)
|
||||
if m:
|
||||
return _arch_abi_arch(m.group(1))
|
||||
# Older or unrecognized Python builds have no process-architecture
|
||||
# tag. Preserve the previous machine-name fallback for these callers.
|
||||
return _arch_abi_arch(platform.machine())
|
||||
return _arch_abi_arch(platform.machine())
|
||||
|
||||
|
||||
def target_os_tag() -> str:
|
||||
"""The "<os>" half of the arch-abi tag the shards are built for."""
|
||||
if _TARGET_OS in _ARCH_ABI_OSES:
|
||||
return _TARGET_OS
|
||||
if is_windows():
|
||||
return 'win'
|
||||
return {'Darwin': 'macos'}.get(platform.system(), 'linux')
|
||||
|
||||
|
||||
def set_target_arch(arch: str | None) -> None:
|
||||
global _TARGET_ARCH
|
||||
_TARGET_ARCH = arch
|
||||
|
||||
|
||||
def target_arch() -> str:
|
||||
"""The "<arch>" half: the runtime's when it said so, else this interpreter's."""
|
||||
return _TARGET_ARCH or interpreter_arch()
|
||||
|
||||
|
||||
def cache_arch_abi() -> str:
|
||||
"""Canonical cache arch-abi tag, IDENTICAL to overlay_loader.c's
|
||||
"""Canonical cache arch-abi tag, IDENTICAL to overlay_loader.h's
|
||||
PSX_OVERLAY_ARCH_ABI ("<os>-<arch>": win|linux|macos + x64|arm64|x86).
|
||||
gcc DLLs are namespaced under <game_id>/gcc/<arch-abi>/ so same-OS
|
||||
different-arch caches never comingle. Keep this
|
||||
mapping in lockstep with overlay_loader.c."""
|
||||
if _TARGET_OS in ('win', 'linux', 'macos'):
|
||||
os_tag = _TARGET_OS
|
||||
elif is_windows():
|
||||
os_tag = 'win'
|
||||
else:
|
||||
os_tag = {'Darwin': 'macos'}.get(platform.system(), 'linux')
|
||||
m = platform.machine().lower()
|
||||
if m in ('amd64', 'x86_64', 'x64'):
|
||||
arch = 'x64'
|
||||
elif m in ('arm64', 'aarch64'):
|
||||
arch = 'arm64'
|
||||
elif m in ('i386', 'i686', 'x86'):
|
||||
arch = 'x86'
|
||||
else:
|
||||
arch = 'unknown'
|
||||
return f'{os_tag}-{arch}'
|
||||
mapping in lockstep with overlay_loader.h."""
|
||||
return f'{target_os_tag()}-{target_arch()}'
|
||||
|
||||
|
||||
def apply_runtime_arch_abi(value: str | None, source: str) -> str | None:
|
||||
"""Build for the runtime's "<os>-<arch>" (PSX_OVERLAY_ARCH_ABI or
|
||||
--arch-abi). Returns None when adopted (or nothing was given), else why
|
||||
not. The OS half must agree with the OS the shards are built for: a
|
||||
disagreement means the cache would be written where no loader reads."""
|
||||
if not value:
|
||||
return None
|
||||
value = value.strip()
|
||||
os_tag, sep, arch = value.partition('-')
|
||||
if not sep or os_tag not in _ARCH_ABI_OSES or arch not in _ARCH_ABI_ARCHES:
|
||||
return (f'{source}={value!r} is not "<os>-<arch>" '
|
||||
f'(os: {"|".join(_ARCH_ABI_OSES)}; '
|
||||
f'arch: {"|".join(_ARCH_ABI_ARCHES)})')
|
||||
if os_tag != target_os_tag():
|
||||
return (f'{source}={value!r} names {os_tag}, but these shards are '
|
||||
f'built for {target_os_tag()}')
|
||||
set_target_arch(arch)
|
||||
return None
|
||||
|
||||
|
||||
def target_arch_flags(compiler: str = 'gcc') -> list[str]:
|
||||
"""Compiler flags that make a shard's architecture the target's.
|
||||
|
||||
macOS only: Apple's cc defaults to the architecture of the compiler
|
||||
binary, not of whoever runs it, so the target is always named. Other
|
||||
platforms are left exactly as they were (their toolchains target the one
|
||||
architecture the runtime and the bundled Python share)."""
|
||||
if compiler != 'gcc' or target_os_tag() != 'macos':
|
||||
return []
|
||||
name = _DARWIN_ARCH_NAMES.get(target_arch())
|
||||
# Non-Apple GCC passed via --gcc is unsupported on macOS; it may ignore -arch.
|
||||
return ['-arch', name] if name else []
|
||||
|
||||
|
||||
def interpreter_arch_mismatch() -> str | None:
|
||||
"""Why this interpreter cannot build shards for the target, or None.
|
||||
|
||||
Every shard is validated by loading it into this interpreter (ctypes:
|
||||
overlay_abi, pair id, exports) before it counts, and a process can only
|
||||
load its own architecture. So a mismatch cannot produce a usable cache;
|
||||
say so instead of writing shards nothing will load."""
|
||||
have = interpreter_arch()
|
||||
want = target_arch()
|
||||
if have == want or have == 'unknown':
|
||||
# 'unknown': nothing proves a mismatch; each shard's load check
|
||||
# still decides, as before this guard existed.
|
||||
return None
|
||||
return (f'the runtime is {cache_arch_abi()} but this Python runs as '
|
||||
f'{have} ({sys.executable}); compiled shards are validated by '
|
||||
f'loading them here, so the Python must run as {want} too. Use a '
|
||||
f'Python built for {want} (a universal one runs as its caller\'s '
|
||||
f'architecture).')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -5016,7 +5130,7 @@ def _compile_dll_direct(c_path: str, out_dll: str, include_dirs: list[str],
|
||||
# that conflicts with the host process. Use -shared without -fPIC.
|
||||
pic_flag = [] if is_windows() else ['-fPIC']
|
||||
cmd = [
|
||||
gcc, '-shared', *pic_flag, '-O2',
|
||||
gcc, '-shared', *pic_flag, *target_arch_flags('gcc'), '-O2',
|
||||
'-DPSX_OVERLAY_DLL_BUILD',
|
||||
# Overlays mirror the runtime's no-debug-tools build: the emitter guards
|
||||
# debug_server_cyc_observe (and friends) behind PSX_NO_DEBUG_TOOLS, and the
|
||||
@@ -6514,6 +6628,11 @@ def main():
|
||||
'to the sequential path).')
|
||||
ap.add_argument('--target-os', choices=['auto', 'win', 'linux', 'macos'], default='auto',
|
||||
help='target operating system for compiled overlay shards (default: auto)')
|
||||
ap.add_argument('--arch-abi', default=None,
|
||||
help='"<os>-<arch>" of the runtime that will load the shards '
|
||||
'(e.g. macos-x64). The spawning runtime injects '
|
||||
'PSX_OVERLAY_ARCH_ABI, which wins. Default: this '
|
||||
"interpreter's architecture.")
|
||||
args = ap.parse_args()
|
||||
target_os = args.target_os
|
||||
if target_os == 'auto':
|
||||
@@ -6567,6 +6686,24 @@ def main():
|
||||
if _fl is not None and _fl != args.flavor:
|
||||
print(f'[cache] PSX_OVERLAY_FLAVOR overrides --flavor: {_fl}')
|
||||
args.flavor = _fl
|
||||
# Architecture is pinned by the spawning runtime too (overlay_loader.h's
|
||||
# PSX_OVERLAY_ARCH_ABI): the shards must be the RUNTIME's architecture and
|
||||
# land in its arch-abi directory, whatever this interpreter or the
|
||||
# compiler binary happens to be.
|
||||
_env_arch = os.environ.get('PSX_OVERLAY_ARCH_ABI')
|
||||
if _env_arch:
|
||||
if args.arch_abi and _env_arch != args.arch_abi:
|
||||
print(f'[cache] PSX_OVERLAY_ARCH_ABI overrides --arch-abi: {_env_arch}')
|
||||
_arch_err = apply_runtime_arch_abi(_env_arch, 'PSX_OVERLAY_ARCH_ABI')
|
||||
else:
|
||||
_arch_err = apply_runtime_arch_abi(args.arch_abi, '--arch-abi')
|
||||
if _arch_err:
|
||||
ap.error(_arch_err)
|
||||
if not args.static:
|
||||
_arch_mismatch = interpreter_arch_mismatch()
|
||||
if _arch_mismatch:
|
||||
print(f'ERROR: cannot build overlay shards: {_arch_mismatch}')
|
||||
sys.exit(2)
|
||||
if not args.captures:
|
||||
ap.error('no captures file: set PSX_OVERLAY_CAPTURES (runtime injects it) '
|
||||
'or pass --captures for manual/offline use')
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Write host-owned stereo manifests from TCP metadata and complete PNG sets."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def write_capture_manifests(directory, records):
|
||||
directory = Path(directory)
|
||||
written = 0
|
||||
for record in records:
|
||||
pair_id = int(record["pair_id"])
|
||||
if pair_id < 0:
|
||||
raise ValueError("negative capture pair ID")
|
||||
prefix = f"p{pair_id:06d}"
|
||||
images = [directory / f"{prefix}_{eye}.png" for eye in ("left", "right", "sbs")]
|
||||
if not all(path.is_file() and path.stat().st_size for path in images):
|
||||
continue
|
||||
manifest = directory / f"{prefix}.json"
|
||||
if not manifest.exists():
|
||||
manifest.write_text(json.dumps(record, indent=2) + "\n", encoding="utf-8")
|
||||
written += 1
|
||||
return written
|
||||
Reference in New Issue
Block a user