mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 10:59:16 -04:00
Merge origin/master into feat/high-frame-rate
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,9 +77,15 @@ rather than LGPL linkage.
|
||||
|
||||
The runtime expects an end-user bundle at
|
||||
`<exe_dir>/overlay_toolchain/{python/, tcc/tcc.exe, compile_overlays.py, …}`
|
||||
(`runtime/src/main.cpp`). **No script in this repository populates that
|
||||
directory**, so if release packaging supplies it, that step lives outside this
|
||||
repo and the TinyCC license notice must be shipped alongside it there.
|
||||
(`runtime/src/main.cpp`). `tools/release_stage.py` (`stage_toolchain`) populates
|
||||
it on Windows from the pinned, unmodified upstream TinyCC 0.9.27 win64 binary
|
||||
zip. That zip carries no license file, so the notice — LGPL-2.1 text, copyright
|
||||
and the exact source URL — lives at `runtime/licenses/TinyCC-LICENSE.txt`
|
||||
(shipped as `licenses/TinyCC-LICENSE.txt`) and is also staged beside the binary
|
||||
as `overlay_toolchain/tcc/COPYING`. Staging fails if the notice does not name the
|
||||
pinned version and archive hash, and the release zip verifier
|
||||
(`docs/ci/templates/game-release.yml`) rejects a zip that bundles `tcc.exe`
|
||||
without both copies.
|
||||
|
||||
Developers with `gcc` on `PATH` use the gcc tier instead; the bundled tcc matters
|
||||
only for end-user release packages (`docs/BUILDING.md`).
|
||||
|
||||
@@ -869,6 +869,293 @@ PNG at 768x480 scale-2 windowed (row-pitch bug in the hires readback) —
|
||||
the census + the player's own captures carried the session; fix it before
|
||||
the formal Crash/Tomba2 A/B.
|
||||
|
||||
### G1.11 — PGXP as a title's default: hook build, mod at session start, dataflow only, exact projection (2026-10-01)
|
||||
|
||||
Ridge Racer Type 4 turns PGXP on by default. Getting there took framework
|
||||
changes that are all opt-in. With PGXP off, nothing changes for any title.
|
||||
With PGXP on, two changes apply without a new option: the
|
||||
`psx.enhancement.pgxp` mod now actually arms PGXP (item 2), and fully precise
|
||||
axis-aligned quads take the triangle path (item 4).
|
||||
|
||||
**What R4 got from G1.10 as shipped: nothing.** Base flavor, PGXP mod on:
|
||||
0% dataflow hits, 3.6% position-cache hits, 0 of 282k textured triangles with
|
||||
perspective UVs. R4's course renderer copies SXY out of the GTE with
|
||||
`mfc2`/`sw`/`lw`/`sw`, and only `swc2` is shadowed in the base flavor. The
|
||||
hook flavor already tracks all of it, with no R4-specific hooks: R4's near
|
||||
polygon subdivision interpolates in 3D on the GTE and projects every
|
||||
sub-vertex with RTPT.
|
||||
|
||||
1. **`psxrecomp_add_game_runtime(... PGXP)`** builds the title's one runtime
|
||||
as the hook flavor (`-DPSX_PGXP=1`, overlay flavor 2, no `_pgxp` suffix, no
|
||||
clone). Generated C already carries the `PGXP_*()` sites, so a committed
|
||||
`generated/` does not change. The option is parsed, so it can no longer be
|
||||
swallowed by an earlier multi-value argument. Test:
|
||||
`game_runtime_pgxp_test` (the real function body under `cmake -P`).
|
||||
2. **The PGXP mod arms at session start.** `psx.enhancement.pgxp`'s
|
||||
activation used to arm the corrections, and main.cpp's renderer setup,
|
||||
which runs after activation, then re-applied the `[video]` baseline and
|
||||
switched them off. The activation now records a request
|
||||
(`pgxp_mod_request`), and the renderer setup's session arming
|
||||
(`psx_pgxp_session_arm`, `runtime/src/pgxp_session.cpp`) takes it, reads
|
||||
and clears it, and arms `[video]` OR the request. The
|
||||
`PSX_GEOMETRY_CORRECTION` / `PSX_PERSPECTIVE_TEXTURING` /
|
||||
`PSX_PGXP_CPU_MODE` / `PSX_PGXP_CULLING` env overrides still win. Because
|
||||
the arming takes the request, a later session whose plan is empty (netplay
|
||||
clears every mod, default-on ones included) cannot inherit it.
|
||||
`reset_mod_owned_presentation()` also clears it. A title that wants PGXP
|
||||
on by default overrides the builtin manifest at the same id and version
|
||||
with `default_enabled = true` (MOD_PACKAGES.md). It must not use
|
||||
`[video] geometry_correction` for that, because netplay does not clear it
|
||||
(see `pgxp_mod_only`, G1.12). Tests: `pgxp_session_test` runs the real
|
||||
arming against the real mod runtime, the builtin plugin and the builtin's
|
||||
own manifest (default-on override, netplay clear with and without the
|
||||
reset, the off switch, options, env overrides). R4's `r4_pgxp_boot`
|
||||
boots the built runtime and fails if main.cpp stops calling the arming.
|
||||
3. **Two `[video]` keys** (game.toml; live over the TCP `pgxp` verb):
|
||||
|
||||
```toml
|
||||
[video]
|
||||
pgxp_tolerance = -1.0 # existing key: no clamp
|
||||
pgxp_position_fallback = false # default true
|
||||
pgxp_preserve_projection = true # default false
|
||||
```
|
||||
|
||||
- `pgxp_position_fallback = false` ("dataflow only", the reference
|
||||
implementations' default) skips the G1.4 position cache and stops
|
||||
`geom_note` filling it. At hook-build coverage the cache only hands
|
||||
unrelated 3D fractions to CPU-built 2D polygons: on R4 (tolerance off,
|
||||
cache on), 30% of the HUD tachometer needle's vertices (rms 0.86 px).
|
||||
- `pgxp_preserve_projection = true` shadows the exact projection of each
|
||||
vertex, from the unshifted MACs and a true divide
|
||||
(`pgxp_project_precise`), instead of the GTE's integer IR1/IR2/SZ3 path.
|
||||
Only sf=1 vertices with no IR, SZ3 or divide saturation qualify; the rest
|
||||
keep the IR path. Guest SXY, MAC0 and FLAG are untouched (the GTE oracle
|
||||
test runs RTPS/RTPT with it on). The exact position can sit below the
|
||||
guest integer or more than a pixel above it, so truncation agreement
|
||||
becomes the window -4 < precise - native < 5 px. A packet half at or
|
||||
beyond the GTE saturation limit (-1024 / 1023) must still agree exactly.
|
||||
RTPS applies the same window: an exact projection outside it is not
|
||||
shadowed, and the vertex keeps the IR path's shadow, which always
|
||||
agrees, so it still draws precise instead of being rejected to native
|
||||
(`ppp_window_fallback` counts these; `pgxp_ppp_accept`). The exact math
|
||||
is skipped while the hooks record nothing (speculative passes). The
|
||||
tolerance clamp now measures the offset both ways. One non-visual
|
||||
consumer: `[widescreen] precise_nclip` takes its branch sign from the
|
||||
shadows, so in a title that sets both, that sign follows the exact
|
||||
projection too.
|
||||
- Keep `pgxp_tolerance` at its 0.5 default only for low-coverage titles
|
||||
(the G1.10 Ape result). At hook-build coverage the clamp is harmful: on
|
||||
R4 it keeps 26% of vertices, leaves 52% of drawn triangles mixed (a
|
||||
4K capture shows a hairline across the road) and shakes more than stock
|
||||
(tables below). The default stays 0.5 because changing it would change
|
||||
every title that already arms PGXP through `[video]`.
|
||||
4. **Fully precise axis-aligned textured quads skip the 2D rectangle
|
||||
shortcut.** `gp0_exec_textured_quad` draws a quad whose integer vertices
|
||||
and UVs form an axis-aligned rectangle with `gr_draw_textured_rect`, at the
|
||||
native position. A world quad seen straight on (R4's building facades)
|
||||
lands there too, and against its precise neighbours that opened an
|
||||
L-shaped background seam at 4K. When PGXP is correcting and all four
|
||||
vertices are dataflow-precise (`pgxp_probe_precise_vertex`), the quad is
|
||||
drawn as its two triangles (`rect_bypass`). A rectangle with only some
|
||||
corners precise, such as a CPU-built sprite with one corner copied from
|
||||
SXY, keeps the shortcut and counts as `rect_partial`. Drawing it as
|
||||
triangles would mix precise and native vertices. R4 has none: 0 in every
|
||||
run below.
|
||||
|
||||
Observability: `geom_correction`'s `pgxp` object reports the switches,
|
||||
`ppp_produced` / `ppp_window_fallback`, geometry-corrected triangles as
|
||||
`tri_precise` / `tri_mixed` / `tri_native` (the G1.1 crack exposure),
|
||||
`rect_bypass` / `rect_partial`, and the NCLIP counters of G1.12.
|
||||
|
||||
**What `tri_mixed` can and cannot see.** It counts drawn triangles with one
|
||||
or two precise vertices: a shared edge whose vertex is precise in one
|
||||
triangle and native in its neighbour. Under preserve-projection that
|
||||
disagreement is up to the window, several pixels, not a fraction. A vertex
|
||||
is decided per packet word, so two triangles that share a projected vertex
|
||||
make the same decision. `tri_mixed` is blind to the other crack classes: an
|
||||
all-precise triangle beside a game-built 2D one, and a gap where the game
|
||||
culled a face (G1.12). Zero mixed triangles is a statement about those runs,
|
||||
not a proof of no seams.
|
||||
|
||||
**R4 measurements.** Helter Skelter Grand Prix from a grid savestate, Cross
|
||||
held, 60-frame lead, 360-frame window, native internal resolution, macOS
|
||||
arm64, every run 0 dispatch and 0 segment misses. "Dataflow" is the share of
|
||||
GPU vertex lookups that took a validated shadow.
|
||||
|
||||
| build / settings | dataflow | position cache | drawn triangles mixed | perspective UVs |
|
||||
|---|---|---|---|---|
|
||||
| base flavor, mod on, framework defaults (R4 before) | 0% | 3.6% | 8.9% | 0 / 282k |
|
||||
| hook flavor, framework defaults (tol 0.5, cache, IR path) | 25.7% | 0.002% | 51.6% (152k / 295k) | 100% |
|
||||
| hook flavor, tol off, dataflow only, exact projection | 99.88% | 0 | 0 / 312k | 100% |
|
||||
| **R4 shipped** (the above plus precise culling, G1.12) | **99.91%** | 0 | **0 / 389k** | **100%** |
|
||||
|
||||
The vertices left native are the HUD needle (CPU-built from a sin/cos table)
|
||||
and vertices at the GTE saturation limit far off-screen, which draw where the
|
||||
hardware puts them. Other content with R4's shipped settings:
|
||||
|
||||
| run | dataflow | drawn triangles mixed | truncation rejects |
|
||||
|---|---|---|---|
|
||||
| boot through the menus, Grand Prix setup and car select | 96.6% (rest 2D UI) | 0 / 1.83M | 0 |
|
||||
| attract demo, 3000 frames | 95.2% (rest 2D UI) | 8 / 1.49M | 12 |
|
||||
| 2P VS, pad 1 driving, 360 frames | 100% | 0 / 467k | 0 |
|
||||
| 2P VS, widescreen Fit, both pads driving, 600 frames | 99.992% | 149 / 730k (0.02%) | 174 |
|
||||
|
||||
Every truncation-rejected vertex in the per-vertex dump of the last run
|
||||
(208 census vertices) has a packet half at the saturation limit. Of those, 94
|
||||
come from the effect emitter (store PC `0x800173F0`). The rest come from the
|
||||
course and near-subdivision stores (`0x80064284..90`, `0x80065B00..08`). No
|
||||
in-range vertex was rejected, so the 149 mixed triangles each have a corner
|
||||
clamped far off-screen. The exact-minus-native offsets of shadowed 1P
|
||||
vertices measured -1.87 .. +2.60 px (R4: H = 290).
|
||||
|
||||
Frame-to-frame vertex wobble (`dE`: rms change of drawn-minus-exact between
|
||||
consecutive frames along tracked vertices, native px; x9 = 4K output px; the
|
||||
reference is the exact projection; slow start from the grid, 152 frames,
|
||||
43k tracked steps; near = SZ3 < 1000):
|
||||
|
||||
| mode | err | dE | dE near | dE at 4K |
|
||||
|---|---|---|---|---|
|
||||
| native (PGXP off) | 0.834 | 0.321 | 0.460 | 2.89 |
|
||||
| hook flavor, tol 0.5, IR path | 0.807 | 0.374 | 0.545 | 3.36 |
|
||||
| tol off, IR path | 0.075 | 0.025 | 0.052 | 0.23 |
|
||||
|
||||
At speed (360-frame lead): native 0.417, tol 0.5 0.504, IR path 0.032. With
|
||||
preserve-projection the drawn position is the reference itself, so its row
|
||||
would read 0 by construction, not by measurement. It only shows that no drawn
|
||||
vertex fell back to the IR path or native in that window. What is
|
||||
established independently is that the exact projection is right: the
|
||||
`pgxp_test` and `gte_register_access_test` oracles check it against the GTE
|
||||
inputs.
|
||||
|
||||
**Visible changes.** Polygons stop wobbling and road, wall and sign textures
|
||||
stop bending (visible at every scale for textures, above native for
|
||||
geometry). Far thin features draw at their true size: distant lane dashes and
|
||||
the start line appear in the mirror, and the START!! board at the far end of
|
||||
the straight looks smaller. The first frame or two after a savestate load
|
||||
draw without PGXP (shadows are dropped on load, R4 builds packets a frame
|
||||
ahead). Before G1.12, the far road beyond the start gantry and over crests
|
||||
drew as strips with sky and backdrop between them, at 4K and in 2P. That was
|
||||
a culling gap, not texture minification.
|
||||
|
||||
**Not covered.** Semi-transparent mono quads still take their own rectangle
|
||||
path (UI boxes; none seen in R4's 3D); a PGXP depth buffer; savestates of the
|
||||
hook and base flavors are not interchangeable; above native resolution the GL
|
||||
native pack comes from the corrected high-resolution surface, so a title that
|
||||
reads VRAM back to the CPU would see it (R4 issued no GP0 C0 read from boot
|
||||
through a 2P race).
|
||||
|
||||
### G1.12 — Precise culling, the mod as a title's one switch (2026-10-01)
|
||||
|
||||
**The crack G1.11 did not see.** With every drawn vertex precise, R4's far
|
||||
road still broke into a ladder of strips with sky between them, beyond the
|
||||
start gantry and over crests, at 4K and in 2P. Same frame, PGXP off: solid
|
||||
road. R4's road emitter (`0x80066268`) projects a row with RTPT, runs NCLIP
|
||||
on both triangles (`0x8006630C`, `0x8006636C`) and skips the row when
|
||||
neither is front-facing (`slt`/`xor`/`beqz` at `0x800663AC`). Far rows whose
|
||||
native height rounds to 0 have MAC0 = 0 and are skipped. Natively their
|
||||
neighbours meet at the same integer y, so nothing is missing. With PGXP the
|
||||
neighbours sit at their exact y, 0.7 px apart at Helter Skelter's crest, and
|
||||
the skipped row is now a visible gap. `tri_mixed` could not see it: every
|
||||
drawn vertex was precise, and the missing face draws no triangle at all.
|
||||
|
||||
**The fix: NCLIP takes the sign PGXP draws.** A third PGXP option,
|
||||
`culling`, belongs to the `psx.enhancement.pgxp` package and is off in the
|
||||
builtin. A title's override turns it on, and the player can switch it off on
|
||||
the Mods page. While geometry correction is armed, `gte_nclip` computes the
|
||||
exact determinant of the three SXY register shadows
|
||||
(`pgxp_gte_nclip_precise`). It uses them only when all three are live,
|
||||
describe the current register words, and pass the same truncation agreement
|
||||
and tolerance the GPU applies to them. When that sign is nonzero and differs
|
||||
from the integer MAC0's, NCLIP returns the exact sign, with magnitude equal
|
||||
to the rounded exact doubled area, at least 1. FLAG keeps the integer
|
||||
result's overflow bits; matching signs leave MAC0 untouched. This is
|
||||
DuckStation's "PGXP culling" with a minimal footprint.
|
||||
|
||||
This changes guest-visible MAC0, and so the game's control flow, which is
|
||||
why it is opt-in at three levels:
|
||||
- only the mod arms it, so netplay, which clears every mod, never runs it,
|
||||
and there is deliberately no `[video]` key for it;
|
||||
- it needs geometry correction (`psx_pgxp_session_resolve`);
|
||||
- it is held off in every pass that is compared against another execution:
|
||||
the speculative and replay passes, which record no shadows, and the whole
|
||||
overlay shadow diff (`psx_overlay_shadow_diff_active`), including its
|
||||
interpreter pass, so both passes see the integer MAC0.
|
||||
|
||||
`[widescreen] precise_nclip` (`gte_nclip_precise_bltz`) compares against the
|
||||
MAC0 the guest read, so it stays consistent. Live toggle: TCP
|
||||
`pgxp {"culling":0|1}`; validation override: `PSX_PGXP_CULLING`.
|
||||
|
||||
Counters, kept whenever geometry correction is armed, culling on or off:
|
||||
- `nclip_precise`: NCLIPs with an exact determinant;
|
||||
- `nclip_disagree`: of those, the exact sign is nonzero and differs from the
|
||||
integer one. Each is a face the game culls or keeps on a winding the drawn
|
||||
face does not have, the crack class `tri_mixed` misses;
|
||||
- `nclip_corrected`: of those, MAC0 replaced.
|
||||
|
||||
On R4 (1P window above), 178,896 of 623,784 NCLIPs disagree (28.7%). With
|
||||
culling the game emits 24.5% more triangles (389k against 312k); in 2P the
|
||||
figure is 21.6%.
|
||||
|
||||
**Measured on R4** (same-frame 4K A/B from the research frame gate, Helter
|
||||
Skelter start straight frame 200, crest frame 420, 2P VS start frame 150): the
|
||||
ladder and the backdrop blocks are gone, and the far road is continuous with
|
||||
its lane dashes. In places it reads better than PGXP off, whose integer rows
|
||||
smear. Thin lateral features that the integer test dropped now draw at their
|
||||
true size: start-grid lines across the road, the namco plate under the
|
||||
START!! board.
|
||||
|
||||
- **Packet headroom.** R4 builds packets bottom-up in two buffers of 141,176
|
||||
bytes (`0x800ADD10` / `0x800D0488`, set at `0x80021064`, no overflow
|
||||
check). Sampling every second frame over 40 s drives, the high-water mark
|
||||
was 62.6 → 74.3 KB in 1P and 63.8 → 83.4 KB at the 2P VS start (PGXP
|
||||
without → with culling), against 137.9 KB usable below the buffer's top-down
|
||||
region (60% at peak). A title with longer draw distance must re-measure.
|
||||
- **Guest identity.** `fp_identity`, 12000 frames with no input (boot,
|
||||
menus, attract race), cold overlay cache, fresh memory cards. The stock
|
||||
build (framework before this work, base flavor), the hook build with the
|
||||
mod disabled, and the
|
||||
hook build with PGXP on but culling off are IDENTICAL pairwise, with no
|
||||
tolerance. With culling on, the run diverges at frame 10319, the first
|
||||
frame of the attract race, in RAM and scratchpad writes only (`sp`, `sc`,
|
||||
`wc`, `ws`): the packets differ. `cyc`, `mmio` and `mc` agree through frame
|
||||
12000, so the game's register traffic, including its flips and DMA kicks,
|
||||
is unchanged. 2P VS from the start, both pads driving: one flip every 2.0
|
||||
VBlanks with culling and without.
|
||||
- **Cost.** CPU ms per emulated frame (headless, turbo, 1P, native, 600-frame
|
||||
samples, three interleaved rounds, load average about 7):
|
||||
|
||||
| build | median | vs stock |
|
||||
|---|---|---|
|
||||
| stock (base flavor) | 4.37 | |
|
||||
| hook flavor, PGXP off | 4.66 | +7% |
|
||||
| hook flavor, PGXP on, culling off | 5.02 | +15% |
|
||||
| hook flavor, R4 shipped | 5.07 | +16% |
|
||||
|
||||
In real time under R4's default stack (internal resolution Match display:
|
||||
10x, 4800 lines; widescreen Fit; frame rate at the 120 Hz display,
|
||||
interpolated; three interleaved 8 s samples, load average 6 to 12), guest
|
||||
VBlank/s and frame time:
|
||||
|
||||
| | PGXP off | PGXP, no culling | R4 shipped |
|
||||
|---|---|---|---|
|
||||
| 2P VS, both pads driving | 59.3 (16.84 ms) | 58.8 (17.12 ms) | 56.6 (18.42 ms) |
|
||||
| 1P race | 59.3 (17.00 ms) | 56.1 (18.64 ms) | 57.0 (18.14 ms) |
|
||||
|
||||
Most of the 2P cost is the extra triangles, about 1 ms of GL scene time at
|
||||
that scale: the GL renderer's cost is per primitive. In 1P the two PGXP
|
||||
arms are within noise. On this branch 2P draws no interpolated frames, so
|
||||
it was not measured with them.
|
||||
|
||||
**`[video] pgxp_mod_only` (game.toml, default false).** For a title that
|
||||
ships PGXP through the mod (R4): the mod becomes the one switch. The
|
||||
`[video] geometry_correction` / `perspective_texturing` / `pgxp_cpu_mode`
|
||||
values, game.toml's and the player's `settings.toml`, are not applied, and
|
||||
the launcher hides its Perspective textures row
|
||||
(`has_geometry_precision = 0`). Without it, that row showed "off" while the
|
||||
default-on mod corrected textures, unticking it did nothing, and ticking it
|
||||
kept texture correction on after the player switched the mod off. Netplay
|
||||
then runs with PGXP fully off.
|
||||
|
||||
## IR1 — Internal resolution presets (Native … 8K) and the GL scale ceiling (2026-09-26)
|
||||
|
||||
**What the player gets.** Settings → Display → **Internal resolution**: Native,
|
||||
@@ -979,3 +1266,17 @@ surface, including copies between the buffers and 1000-column copies staged
|
||||
in chunks; at 18x both buffers must read back at S with two tiles. In R4 on an M4, the 8K preset reports
|
||||
`effective_scale 18, internal_lines 4320, hr_scale 1, hires_fbo 5760x9216`, and
|
||||
`screenshot_hires` in a race is 5760×4320 with the rear-view mirror present.
|
||||
|
||||
## DD1 — Opt-in draw-distance clamps (2026-10-01)
|
||||
|
||||
`[[draw_distance.clamp]]` (docs/config_schema.md) lists a title's
|
||||
ordering-table range guards. While a trusted mod has called
|
||||
`psx_mod_set_draw_distance_clamp(1)`, the guard's depth index is clamped to
|
||||
the last safe slot before the guard runs, so far geometry the game would drop
|
||||
stays in the farthest bucket (drawn first, under everything nearer) instead
|
||||
of popping in later. Raising the limit instead can push a biased primitive
|
||||
past the end of the table. Off by default and at every session start;
|
||||
identity when off; main executable only (overlay code keeps its own code);
|
||||
native code and the dirty-RAM interpreter agree
|
||||
(`draw_distance_codegen_test`, `draw_distance_interp_test`). First user: R4's
|
||||
course renderers (RidgeRacerType4Recomp, Max Detail).
|
||||
|
||||
@@ -268,6 +268,14 @@ is zero-init.
|
||||
- `NETPLAY_LOBBY_URL "ws://host:port"` — compile-time default lobby URL
|
||||
(`PSX_NET_LOBBY_DEFAULT_URL`; env `PSX_NET_LOBBY_URL` still wins at runtime)
|
||||
- `ENABLE_SETUP_WIZARD` — force `PSX_SETUP_WIZARD=ON` for that target
|
||||
- `PGXP` — build the title's one runtime as the PGXP hook flavor
|
||||
(`-DPSX_PGXP=1`, overlay cache flavor `_f2`, no `_pgxp` suffix). The generated
|
||||
C already carries the hook sites, so `generated/` does not change. PGXP stays
|
||||
off at runtime until `psx.enhancement.pgxp` or the `[video]` keys arm it; with
|
||||
it off the guest runs exactly as in the base flavor. Savestates of the two
|
||||
flavors are not interchangeable. A title that ships PGXP on by default does so
|
||||
through a default-on override of `psx.enhancement.pgxp` plus
|
||||
`[video] pgxp_mod_only = true`. See `ENHANCEMENTS.md` G1.11/G1.12.
|
||||
|
||||
Minimal opt-in example:
|
||||
|
||||
|
||||
@@ -549,9 +549,11 @@ defensive, for the state listed here.
|
||||
| World-scene predicate | `psx_mod_set_world_scene_predicate` | NULL | every session |
|
||||
| Retained-scene predicate | `psx_mod_set_retained_scene_predicate` | NULL | every session |
|
||||
| Adaptive backdrop preload | `psx_mod_set_adaptive_backdrop_preload` | 0 | every session |
|
||||
| Draw-distance clamps | `psx_mod_set_draw_distance_clamp` | off | every session |
|
||||
| Bezel artwork | `psx_mod_set_bezel_artwork` | none (see below) | every session |
|
||||
| Frame-interpolation blend mode | `psx_mod_set_frame_interpolation_blend` | default | every session |
|
||||
| Native VBlank pacing and its rate | `psx_mod_set_native_vblank_rate` | off, 0 | every session |
|
||||
| PGXP request (`psx.enhancement.pgxp`) | the builtin `psx.pgxp` activation (`pgxp_mod_request`) | none | every session |
|
||||
| Frame period, if native VBlank pacing was on | `psx_mod_set_native_vblank_rate` | first-session value | later sessions |
|
||||
| Frame interpolation and its rate | `psx_mod_set_frame_interpolation` | first-session value | later sessions |
|
||||
| Vsync forced off | `psx_mod_set_frame_interpolation`, `psx_mod_set_native_vblank_rate` | first-session value | later sessions |
|
||||
@@ -567,6 +569,20 @@ soft-returns, behaves exactly as without the reset. The RAM request matters on
|
||||
a rematch because `memory_init()` latches the requested geometry again at every
|
||||
boot, including the rematch's.
|
||||
|
||||
The PGXP request is how the builtin `psx.enhancement.pgxp` package arms
|
||||
geometry and texture correction, and its CPU-mode and precise-culling options.
|
||||
Its activation records the request. The renderer setup runs after activation,
|
||||
and its session arming (`psx_pgxp_session_arm`,
|
||||
`runtime/include/pgxp_session.h`) takes the request: it reads it, clears it,
|
||||
and arms PGXP from it and the `[video]` keys together. Arming the corrections
|
||||
from activation directly does not work: the renderer setup applies the
|
||||
`[video]` baseline afterwards and would switch them back off. A netplay session
|
||||
clears the plan, default-on packages included, so it gets the `[video]` keys
|
||||
alone, and nothing at all with `[video] pgxp_mod_only`. A title that ships
|
||||
PGXP on by default overrides the builtin at the same id and version, with
|
||||
`default_enabled = true` and, if it wants it, the `culling` option's default set
|
||||
to `"true"` (ENHANCEMENTS.md G1.11/G1.12).
|
||||
|
||||
Bezel artwork has two parts. The reset clears the artwork path, so the next
|
||||
session start loads nothing unless its own activation selects artwork again.
|
||||
The OpenGL renderer drops the loaded texture itself when it shuts down, which
|
||||
@@ -622,6 +638,12 @@ selects uncapped host speed. A zero-frame release stops acceleration as soon as
|
||||
the sustained-load predicate clears, which is appropriate for timing-sensitive
|
||||
or speedrun-oriented packages.
|
||||
|
||||
`psx_mod_set_draw_distance_clamp(enabled)` switches the title's
|
||||
`[[draw_distance.clamp]]` sites (docs/config_schema.md) on for the session: a
|
||||
far primitive the game would drop past the end of its ordering table is kept
|
||||
in the farthest slot. It returns 0 when the title lists no sites. More
|
||||
primitives mean more guest work, so it belongs to an opt-in feature.
|
||||
|
||||
`psx_mod_set_disc_speed(divisor, instant_max_per_frame)` is the guest-visible
|
||||
alternative. Divisors 2 and 4 shorten emulated CD deadlines; zero selects the
|
||||
bounded instant scheduler. Because this changes interrupt timing, packages
|
||||
|
||||
@@ -181,6 +181,35 @@ Without a lobby start (LAN or Direct IP):
|
||||
Sim authority: pad **slot 0** is the session host (`START`, state transfer).
|
||||
Guests rearrange among seats **1..N−1**.
|
||||
|
||||
**When the start fails.** A LAN host listens on the room's UDP
|
||||
port (7777 by default; the browser scans 7777 to 7808), a guest on a port the
|
||||
system picks, and a command-line start on the `--net-bind` address. When the
|
||||
start fails, the runtime does the library's steps again (it reads the listen
|
||||
address, looks up a name, and binds) and says what the system answered:
|
||||
|
||||
- The port is in use (Windows error 10048, Linux 98, macOS 48): "Netplay could
|
||||
not open UDP port 7777 on 0.0.0.0 (system error 10048). Another program or
|
||||
the operating system holds that port. Choose another port and start again."
|
||||
- The system does not hand the port out (Windows error 10013 for a range it
|
||||
keeps for Hyper-V or WSL, error 13 for a port below 1024 elsewhere): "The
|
||||
operating system does not allow this port. Choose another port and start
|
||||
again."
|
||||
- The listen address is not an address of this computer (Windows error 10049,
|
||||
Linux 99, macOS 49): "Netplay could not listen on 192.168.1.50 (system error
|
||||
10049). That address is not one of this computer's addresses. Check the
|
||||
listen address and start again."
|
||||
- Any other answer: the port, the address, the system's error number and, when
|
||||
it is short plain text, the system's own words. No cause is named.
|
||||
|
||||
A listen address that is not `address:port` (the address may be left out:
|
||||
`:7777` listens on every address) and a peer address that cannot be read get
|
||||
their own sentence. The peer address is named only when the listen address
|
||||
opened. An online start that fails says only that the online connection could
|
||||
not start. The build is named only when it has no netplay. The log line adds
|
||||
the system's own text in every case. A command-line start exits with code 1; a
|
||||
match started from the launcher returns to the room with the sentence on the
|
||||
status line.
|
||||
|
||||
---
|
||||
|
||||
## Multitap + seat ceiling
|
||||
|
||||
@@ -60,7 +60,8 @@ Columns: **N** = native, **D** = DuckStation oracle.
|
||||
| `window_size` | ✓ | | `w`, `h` | Resize the game window as a player would (Fit / adaptive widescreen follow it): window-shaped checks in `--hidden-window` runs. Pair with `present_shot` |
|
||||
| `render_pass_dump` | ✓ | | `path`, `count` | Write the images (the game's own frame, then each pass in phase order) of the next `count` frames that get passes as `<path>/g<frame>_<index>_a<phase q16>.png` |
|
||||
| `render_pass_refuse` | ✓ | | `on` | Make the OpenGL backend decline render passes (`status` 2, BACKEND), as a renderer mode without them would; tests a plugin's fallback. `PSX_RENDER_PASS_REFUSE=1` does the same from start |
|
||||
| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate |
|
||||
| `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate. Also `texture_correction`. Its `pgxp` object adds the engine's per-lookup counters, the `position_fallback` / `preserve_projection` / `culling` switches, `ppp_produced` and `ppp_window_fallback` (exact projections outside the agreement window, shadowed with the IR path instead), geometry-corrected triangles split into `tri_precise` / `tri_mixed` / `tri_native` (the mixed share is the mesh-cracking exposure), `rect_bypass` / `rect_partial` (fully / partly precise textured quads that matched the 2D rectangle shortcut; only the former leave it) and `nclip_precise` / `nclip_disagree` / `nclip_corrected` (NCLIPs with an exact determinant, whose exact sign differs from the integer one, and that precise culling corrected); `ENHANCEMENTS.md` G1.11/G1.12 |
|
||||
| `pgxp` | ✓ | | `geometry`, `texture`, `cpu_mode`, `tolerance`, `position_fallback`, `preserve_projection`, `culling` (all optional) | Live-tune PGXP for same-scene A/B: arm or disarm geometry / texture correction and set the engine knobs, without a restart. `culling` changes guest NCLIP results, so a toggle mid-run makes the run diverge from one without it. Replies with the resulting state (`enabled`, `cpu_mode`, `tolerance`, `position_fallback`, `preserve_projection`, `culling`, `suppress`, `active`). `ENHANCEMENTS.md` G1.10-G1.12 |
|
||||
| `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history |
|
||||
| `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native |
|
||||
| `dma_state` | ✓ | ✓ | — | DPCR, DICR, all 7 channel states (madr/bcr/chcr) |
|
||||
@@ -430,9 +431,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i
|
||||
|
||||
## Complete command index (generated)
|
||||
|
||||
**332 commands registered** — 319 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
**334 commands registered** — 321 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
|
||||
|
||||
62 of 332 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.
|
||||
64 of 334 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.
|
||||
|
||||
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
|
||||
|
||||
@@ -561,6 +562,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `gl_fbo_peek` | ✓ | | |
|
||||
| `gl_interp` | ✓ | | ✓ |
|
||||
| `gl_present_ring` | ✓ | | |
|
||||
| `gl_texwin_batch` | ✓ | | |
|
||||
| `gl_vram_diff` | ✓ | | |
|
||||
| `gl_wide_fast` | ✓ | | |
|
||||
| `gl_ws_ablate` | ✓ | | |
|
||||
@@ -578,6 +580,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `guest_tty_dump` | ✓ | | |
|
||||
| `history` | ✓ | ✓ | ✓ |
|
||||
| `hle_dump` | ✓ | | ✓ |
|
||||
| `host_profile` | ✓ | | |
|
||||
| `idle_skip` | ✓ | | |
|
||||
| `imask_trace` | ✓ | | |
|
||||
| `input_route_append` | ✓ | | |
|
||||
@@ -634,7 +637,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `pc_probe_arm` | ✓ | | |
|
||||
| `pc_probe_clear` | ✓ | | |
|
||||
| `pc_probe_dump` | ✓ | | |
|
||||
| `pgxp` | ✓ | | |
|
||||
| `pgxp` | ✓ | | ✓ |
|
||||
| `pgxp_miss_ring` | ✓ | | |
|
||||
| `pgxp_shadow` | ✓ | | |
|
||||
| `phase_hot` | ✓ | | |
|
||||
@@ -645,7 +648,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
|
||||
| `present_ring` | ✓ | | |
|
||||
| `present_shot` | ✓ | | ✓ |
|
||||
| `present_shot_seq` | ✓ | | ✓ |
|
||||
| `press` | ✓ | ✓ | |
|
||||
| `press` | ✓ | ✓ | ✓ |
|
||||
| `probe_clear` | ✓ | | |
|
||||
| `probe_trace` | ✓ | | |
|
||||
| `quit` | ✓ | | ✓ |
|
||||
|
||||
@@ -274,6 +274,39 @@ per key; a longer list is logged rather than silently truncated. `bgez_sites`
|
||||
and `clip_edge_x_load_sites` enter the overlay-cache identity only when they
|
||||
are non-empty, so titles that do not use them keep their caches.
|
||||
|
||||
`screen_x_sites` is the explicit form of the `auto_screen_x` compare, for a
|
||||
title whose width is not in `screen_w_imms` or whose function has no paired
|
||||
height compare. It takes three instructions:
|
||||
|
||||
| Instruction at the site | While widened |
|
||||
|---|---|
|
||||
| `sltiu rt, SX, W` | `rt = -m <= SX < W + m` |
|
||||
| `lui rt, W` (a screen edge kept as `W << 16`) | `rt = (W + m) << 16`; a negative `W` moves left instead |
|
||||
| `bltz x, outside` where `x` is `SX << 16` | taken while `SX < -m` |
|
||||
|
||||
The last two cover a renderer that keeps a screen X in the high half of a
|
||||
register (`sll x, sx, 16`), tests the sign of differences against a `lui`-held
|
||||
edge and masks whole points at once; polygon splitters do this. `bltz_sites`
|
||||
does not fit that `bltz`: it compares the whole register with `-m`, and every
|
||||
negative `SX << 16` is far below that. Widen such tests rather than keeping
|
||||
the branch: a splitter that takes its pieces from a fixed list runs out of
|
||||
entries when every off-screen half is kept, and then drops polygons inside
|
||||
the 4:3 area.
|
||||
|
||||
```toml
|
||||
[widescreen.cull]
|
||||
screen_x_sites = [
|
||||
"0x80010100", # sltiu s0,v1,0x200 midpoint inside the screen?
|
||||
"0x80010200", # lui s6,0x200 right edge as 512 << 16
|
||||
"0x80010210", # bltz t0,outside t0 = SX << 16
|
||||
]
|
||||
```
|
||||
|
||||
`screen_x_sites` is applied when code is generated: native code and overlay
|
||||
shards. The dirty-RAM interpreter does not consult this list. In the main EXE
|
||||
a listed address holding another non-branch instruction is a hard build error;
|
||||
a branch other than `bltz` at a listed address is left as it is.
|
||||
|
||||
---
|
||||
|
||||
## Strategic direction: native-wide rendering (explored 2026-06-13, shelved)
|
||||
|
||||
@@ -45,9 +45,12 @@ Template: [`templates/game-release.yml`](templates/game-release.yml).
|
||||
executable, `assets/`, `bios/openbios.bin` + notice, `game.toml`,
|
||||
`game_options.toml`, the mod catalog (verified against the manifest the
|
||||
build published, developer-channel packages pruned), `overlay_toolchain/`,
|
||||
third-party notices; refuse if anything kit-shaped is in the stage; sign on
|
||||
Windows; zip.
|
||||
6. Verify the zip: executable + OpenBIOS + catalog present; no `psxrecomp/`,
|
||||
third-party notices (psxrecomp's in `licenses/`, plus recomp-ui's license
|
||||
as `licenses/recomp-ui-LICENSE` and its font/image notices as
|
||||
`assets/{fonts,img}/NOTICE.md`); refuse if anything kit-shaped is in the
|
||||
stage; sign on Windows; zip.
|
||||
6. Verify the zip: executable + OpenBIOS + catalog + recomp-ui license and
|
||||
font/image notices present; no `psxrecomp/`,
|
||||
`recomp-ui/`, CLI, emitters at the root, sources, generated C, dumps,
|
||||
per-machine mod state.
|
||||
|
||||
|
||||
@@ -598,7 +598,7 @@ jobs:
|
||||
LIST="$(unzip -Z1 "${ZIP}")"
|
||||
# 1. It is a game: the executable, the bundled OpenBIOS, and the
|
||||
# runtime data sit at the root.
|
||||
for must in 'bios/openbios.bin' 'bios/OpenBIOS.LICENSE' 'game.toml' 'psx_game_version.txt' 'assets/fonts/' 'mods/bundled/'; do
|
||||
for must in 'bios/openbios.bin' 'bios/OpenBIOS.LICENSE' 'game.toml' 'psx_game_version.txt' 'assets/fonts/' 'licenses/recomp-ui-LICENSE' 'assets/fonts/NOTICE.md' 'assets/img/NOTICE.md' 'mods/bundled/'; do
|
||||
if ! grep -q "^${must}" <<<"${LIST}"; then
|
||||
echo "::error::release zip lacks ${must}" >&2
|
||||
printf '%s\n' "${LIST}" | head -40 >&2
|
||||
@@ -626,6 +626,16 @@ jobs:
|
||||
grep -E '\.c$' <<<"${LIST}" | grep -v '^overlay_toolchain/' | head -20 >&2
|
||||
exit 1
|
||||
fi
|
||||
# 2b. A bundled TinyCC ships its LGPL-2.1 notice beside the binary and
|
||||
# in licenses/ (its win64 zip carries none of its own).
|
||||
if grep -q '^overlay_toolchain/tcc/tcc\.exe$' <<<"${LIST}"; then
|
||||
for must in 'overlay_toolchain/tcc/COPYING' 'licenses/TinyCC-LICENSE.txt'; do
|
||||
if ! grep -qx "${must}" <<<"${LIST}"; then
|
||||
echo "::error::release zip bundles TinyCC without ${must}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
# 3. No BIOS dump, disc, or memory card; the only image is OpenBIOS.
|
||||
if grep -iE '(^|/)SCPH|\.(cue|iso|img|chd|mcd|mcr)$' <<<"${LIST}" | grep -qv '\.toml$'; then
|
||||
echo "::error::release zip contains a BIOS dump, disc, or memory card" >&2
|
||||
|
||||
@@ -56,6 +56,7 @@ How the two configs relate:
|
||||
[prepare_disc] # optional; data-track digests for prepare/verify
|
||||
[netplay] # optional; TOC / cue policy for online
|
||||
[recompiler]
|
||||
[draw_distance] # optional; opt-in far-geometry clamps (below)
|
||||
[runtime]
|
||||
[audit]
|
||||
```
|
||||
@@ -127,6 +128,12 @@ respective files.
|
||||
|
||||
### Multi-disc selection
|
||||
|
||||
Relative paths in executable-side `settings.toml` (`[disc] path`, `[bios] path`,
|
||||
`[memcard] dir`, `card1`, `card2`) and in `disc.cfg` / `bios.cfg` resolve from the
|
||||
executable directory, including when launched from another working directory.
|
||||
Absolute paths and UNC paths keep their original location. A relative `--disc`
|
||||
command-line argument still resolves from the caller's working directory.
|
||||
|
||||
A build whose `discs` array has more than one entry grows a **Disc Selection**
|
||||
dropdown in the launcher, above the Serial/Region/ISO-header checklist. The
|
||||
choice is persisted in `settings.toml`:
|
||||
@@ -372,6 +379,53 @@ vertical scan X bounds, and any associated respawn-reset interval together.
|
||||
Keep authored placement flags and vertical bounds intact. Empty is inert;
|
||||
configured sites require regeneration.
|
||||
|
||||
## Draw-distance clamps (`[[draw_distance.clamp]]`)
|
||||
|
||||
Many PS1 renderers drop a primitive whose depth index falls past the end of
|
||||
their ordering table (`sltiu t, z, N; beqz t, reject`). The limit protects the
|
||||
OT, so raising it can write past the table. A title can list such guards and
|
||||
let a trusted mod keep the far primitive in the farthest slot instead:
|
||||
|
||||
```toml
|
||||
[[draw_distance.clamp]]
|
||||
address = "0x80061230"
|
||||
expected = "0x2C4A01C0" # sltiu t2, v0, 0x1C0
|
||||
reg = 2 # v0: the depth index the guard tests
|
||||
max = 0x1BF # last safe index
|
||||
|
||||
[[draw_distance.clamp]]
|
||||
address = "0x80066FF8"
|
||||
expected = "0x2441FFFF" # addiu at, v0, -1 (then sltiu at, at, 0x1BF)
|
||||
reg = 2
|
||||
max = 0x1BF
|
||||
```
|
||||
|
||||
- While a mod has called `psx_mod_set_draw_distance_clamp(1)`, `reg` is
|
||||
clamped to `max` (signed: `if ((int32_t)reg > max) reg = max`) immediately
|
||||
before the instruction at `address` runs. The original guard then keeps
|
||||
the primitive and the code that follows indexes the table with the clamped
|
||||
value. A negative (wrapped) value is left alone, so a guard that also
|
||||
rejects too-near primitives keeps doing so.
|
||||
- Off, the default and the state at every session start, the sites run the
|
||||
original code. Netplay never activates a mod, so it stays vanilla.
|
||||
- `expected` is the complete instruction word and must be an ALU
|
||||
instruction (`ADDI`/`ADDIU`/`SLTI`/`SLTIU`/`ANDI`/`ORI`/`XORI`, or a
|
||||
SPECIAL shift/arithmetic/logic/`SLT`/`SLTU`) that reads `reg`. `reg` is
|
||||
1..31; `max` is a signed 32-bit integer. Addresses are unique by physical
|
||||
address.
|
||||
- Main executable only. Generation fails when the listed word is not
|
||||
`expected`, or when the previous instruction loads `reg` (its value would
|
||||
still be in the load-delay slot). Captured overlay code at a listed address
|
||||
keeps its own code, so the sites do not enter the overlay-cache identity.
|
||||
The dirty-RAM interpreter applies a clamp where the game's text image holds
|
||||
the listed word.
|
||||
- Pick `max` so the clamped index reaches no further than the farthest index
|
||||
the unclamped code can already produce. Choose the register the following
|
||||
code really indexes with: in the `addiu` form above, the guard tests `at`
|
||||
but the slot is computed from `v0`.
|
||||
- Regenerate after changing the list. With no sites the generated code is
|
||||
unchanged.
|
||||
|
||||
## Runtime block
|
||||
|
||||
Consumed by the cmake macro `psxrecomp_v4_add_runtime_target` (eventually)
|
||||
@@ -564,6 +618,30 @@ Settings surface. A game migrating Skip FMVs into its built-in mod catalog sets
|
||||
it to false. The runtime then hides the Settings row, ignores stale persisted
|
||||
values, and leaves activation to the selected trusted plugin.
|
||||
|
||||
### Texture-window batching (`texture_window_batching`, OpenGL)
|
||||
|
||||
```toml
|
||||
[video]
|
||||
texture_window_batching = true # game.toml only; default false
|
||||
```
|
||||
|
||||
The OpenGL renderer draws consecutive textured primitives in one batch while
|
||||
their blend, mask and filter state match. By default a GP0(E2h) texture-window
|
||||
change also ends the batch, although each vertex carries its primitive's
|
||||
texture window. With this key on, primitives with different windows share a
|
||||
batch. The image is the same either way; only the number of draws changes.
|
||||
While mask checking (GP0(E6h) bit 1) is on, a window change still ends the
|
||||
batch, because an opaque batch updates the mask bits of its own texels only
|
||||
after its colour pass.
|
||||
|
||||
It is for games that tile textures through per-primitive windows. Ridge Racer
|
||||
Type 4's split screen changes the window about 515 times a frame, which drew
|
||||
about 180 batches a frame instead of 17, and every batch that reaches the
|
||||
native-wide margins is drawn again into the wide surface. The software and
|
||||
Vulkan renderers ignore the key.
|
||||
`PSX_GL_TEXWIN_BATCH=0|1` overrides it for one run, and the TCP command
|
||||
`gl_texwin_batch on=<0|1>` switches it live.
|
||||
|
||||
### Local rewind (`settings.toml`)
|
||||
|
||||
Rewind is a player setting, not a game one: it lives in the user's
|
||||
|
||||
@@ -1 +1 @@
|
||||
13726a0f0c2be1cae753c2afdb2e7af421e08d79e2309250e04dd52f7d4d8c7c
|
||||
de0c986b340a37640488d10f25f3bb4da1c501d1c41fa9e70b1872873aa88952
|
||||
|
||||
@@ -1 +1 @@
|
||||
fe1bcc25dbc83f7c8f47fa466994d2df37f6dd86d259e386f5f12ce4ca3c0eca
|
||||
bdd5426f55a08961c40855a8261c302a6e3fd1f00c1074333d4ee997e0317c9e
|
||||
|
||||
@@ -3,7 +3,7 @@ id = "psx.enhancement.pgxp"
|
||||
version = "1.0.0"
|
||||
name = "PGXP Precision (geometry + textures)"
|
||||
author = "psxrecomp"
|
||||
description = "Sub-pixel vertex precision and perspective-correct texturing (the PGXP technique): the runtime shadows the GTE's full-precision projections and follows them to the GPU, so 3D polygons stop wobbling between whole pixels and large floor/wall textures stop warping. Visual-only: the game's own math, culling and readback see exactly what real hardware produces."
|
||||
description = "Sub-pixel vertex precision and perspective-correct texturing (the PGXP technique): the runtime shadows the GTE's full-precision projections and follows them to the GPU, so 3D polygons stop wobbling between whole pixels and large floor/wall textures stop warping. Visual-only unless its precise culling option is on: the game's own math, culling and readback see exactly what real hardware produces."
|
||||
resolver = "declarative"
|
||||
save_compatibility = "shared"
|
||||
|
||||
@@ -28,6 +28,15 @@ group = "Visual"
|
||||
type = "boolean"
|
||||
default = "false"
|
||||
|
||||
[[option]]
|
||||
feature = "pgxp"
|
||||
id = "culling"
|
||||
label = "Precise culling"
|
||||
description = "Also decide which faces the game culls from the same full-precision positions that PGXP draws, so distant rows the hardware rounds to zero height are no longer dropped and leave no gaps between their neighbours. Unlike the rest of PGXP this changes what the game itself computes (its NCLIP results), so it is off unless a game turns it on; netplay always runs without it."
|
||||
group = "Visual"
|
||||
type = "boolean"
|
||||
default = "false"
|
||||
|
||||
[[plugin]]
|
||||
feature = "pgxp"
|
||||
id = "psx.pgxp"
|
||||
|
||||
@@ -531,6 +531,63 @@ if(BUILD_TESTING)
|
||||
add_test(NAME ws_cull_edge_codegen_test
|
||||
COMMAND ws_cull_edge_codegen_test)
|
||||
|
||||
# [widescreen.cull] screen_x_sites on `lui` and `bltz` (a screen X kept in
|
||||
# the high half of a register): emitted code for hand-written instruction
|
||||
# words, the main-EXE guard, and the shared math in ws_cull_edge.h.
|
||||
add_executable(ws_cull_screen_x_hi_codegen_test
|
||||
tests/ws_cull_screen_x_hi_codegen_test.cpp
|
||||
src/ps1_exe_parser.cpp
|
||||
src/mips_decoder.cpp
|
||||
src/control_flow.cpp
|
||||
src/function_analysis.cpp
|
||||
src/code_generator.cpp
|
||||
src/pgxp_hook_emitter.cpp
|
||||
src/annotations.cpp
|
||||
src/bios_address_model.cpp
|
||||
src/config_loader.cpp
|
||||
src/recompiler_patch.cpp
|
||||
)
|
||||
target_include_directories(ws_cull_screen_x_hi_codegen_test PRIVATE
|
||||
include
|
||||
src
|
||||
lib/rabbitizer/include
|
||||
lib/rabbitizer/cplusplus/include
|
||||
lib/fmt/include
|
||||
lib/toml11
|
||||
lib/ELFIO
|
||||
)
|
||||
target_link_libraries(ws_cull_screen_x_hi_codegen_test PRIVATE fmt rabbitizer)
|
||||
add_test(NAME ws_cull_screen_x_hi_codegen_test
|
||||
COMMAND ws_cull_screen_x_hi_codegen_test)
|
||||
|
||||
# [[draw_distance.clamp]]: config rules, the emitted main-EXE clamp and
|
||||
# its guards, the shared declaration and the unchanged overlay identity.
|
||||
add_executable(draw_distance_codegen_test
|
||||
tests/draw_distance_codegen_test.cpp
|
||||
src/ps1_exe_parser.cpp
|
||||
src/mips_decoder.cpp
|
||||
src/control_flow.cpp
|
||||
src/function_analysis.cpp
|
||||
src/code_generator.cpp
|
||||
src/pgxp_hook_emitter.cpp
|
||||
src/annotations.cpp
|
||||
src/bios_address_model.cpp
|
||||
src/config_loader.cpp
|
||||
src/recompiler_patch.cpp
|
||||
)
|
||||
target_include_directories(draw_distance_codegen_test PRIVATE
|
||||
include
|
||||
src
|
||||
lib/rabbitizer/include
|
||||
lib/rabbitizer/cplusplus/include
|
||||
lib/fmt/include
|
||||
lib/toml11
|
||||
lib/ELFIO
|
||||
)
|
||||
target_link_libraries(draw_distance_codegen_test PRIVATE fmt rabbitizer)
|
||||
add_test(NAME draw_distance_codegen_test
|
||||
COMMAND draw_distance_codegen_test)
|
||||
|
||||
# The same explicit cull kinds in the dirty-RAM interpreter: gpu.c's
|
||||
# sorted, capped site store and real interpreted bgez/bltz/branch-keep/
|
||||
# clip-edge instructions at margin 0 and at a forced margin. The test
|
||||
@@ -560,6 +617,33 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_BINARY_DIR}/ws_cull_edge_interp_stderr.txt)
|
||||
endif()
|
||||
|
||||
# [[draw_distance.clamp]] in the dirty-RAM interpreter: draw_distance.c's
|
||||
# store and switch and real interpreted guard instructions. The test
|
||||
# #includes draw_distance.c and dirty_ram_interp.c (as above).
|
||||
if(NOT MSVC)
|
||||
add_executable(draw_distance_interp_test
|
||||
../runtime/tests/test_draw_distance_interp.c
|
||||
../runtime/src/ws_aspect_cone_math.c)
|
||||
target_include_directories(draw_distance_interp_test PRIVATE
|
||||
../runtime/include
|
||||
../runtime/src)
|
||||
target_compile_options(draw_distance_interp_test PRIVATE
|
||||
-O2 -flto -ffunction-sections -fdata-sections)
|
||||
if(APPLE)
|
||||
target_link_options(draw_distance_interp_test PRIVATE
|
||||
-flto -Wl,-dead_strip)
|
||||
else()
|
||||
target_link_options(draw_distance_interp_test PRIVATE
|
||||
-flto -Wl,--gc-sections)
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
target_link_libraries(draw_distance_interp_test PRIVATE m)
|
||||
endif()
|
||||
add_test(NAME draw_distance_interp_test
|
||||
COMMAND draw_distance_interp_test
|
||||
${CMAKE_CURRENT_BINARY_DIR}/draw_distance_interp_stderr.txt)
|
||||
endif()
|
||||
|
||||
# Uncached (KSEG1) code charges a fetch before every instruction; cached
|
||||
# code keeps the line-leader rule (test_uncached_fetch_charge.py checks
|
||||
# the BIOS emitter on OpenBIOS and the cycle model).
|
||||
@@ -698,6 +782,21 @@ if(BUILD_TESTING)
|
||||
target_link_libraries(rewind_settings_test PRIVATE fmt)
|
||||
add_test(NAME rewind_settings_test COMMAND rewind_settings_test)
|
||||
|
||||
# Launcher-saved paths stay relative inside a portable game folder.
|
||||
add_executable(portable_settings_paths_test
|
||||
tests/portable_settings_paths_test.cpp
|
||||
src/config_loader.cpp
|
||||
src/ps1_exe_parser.cpp
|
||||
)
|
||||
target_include_directories(portable_settings_paths_test PRIVATE
|
||||
include
|
||||
src
|
||||
lib/fmt/include
|
||||
lib/toml11
|
||||
)
|
||||
target_link_libraries(portable_settings_paths_test PRIVATE fmt)
|
||||
add_test(NAME portable_settings_paths_test COMMAND portable_settings_paths_test)
|
||||
|
||||
# Needs bios/SCPH1001.toml — run from the framework root.
|
||||
add_test(NAME bios_address_model_test
|
||||
COMMAND bios_address_model_test
|
||||
@@ -716,11 +815,25 @@ if(BUILD_TESTING)
|
||||
--cmake ${CMAKE_COMMAND} --cc ${CMAKE_C_COMPILER}
|
||||
--cxx ${CMAKE_CXX_COMPILER} --ninja ${CMAKE_MAKE_PROGRAM})
|
||||
endif()
|
||||
# MFC0/CFC0 read COP0 from before their own cycle charge: the production
|
||||
# interpreter decoder with cycle charging on, a device event that raises
|
||||
# CAUSE.IP2 on the read's own cycle, other seams stubbed to abort()
|
||||
# (runtime/tests/fixture_link.py needs nm and GNU ld diagnostics).
|
||||
if(Python3_Interpreter_FOUND AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
add_test(NAME interp_cop0_read_sample
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_interp_cop0_read_sample.py"
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
endif()
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME optional_netplay_auth
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_optional_netplay_auth.py
|
||||
--cxx ${CMAKE_CXX_COMPILER})
|
||||
add_test(NAME bios_emitter_stamps
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_bios_emitter_stamps.py
|
||||
--cmake ${CMAKE_COMMAND})
|
||||
add_test(NAME pgo_rebuild_flow
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_pgo_rebuild_flow.py)
|
||||
@@ -1081,6 +1194,7 @@ if(BUILD_TESTING)
|
||||
frame_interpolation_flip_source
|
||||
hybrid_dev_any_input
|
||||
launcher_pad_mode_wiring
|
||||
netplay_start_failure_wiring
|
||||
jalr_link_register_guards
|
||||
gl_internal_scale_guards
|
||||
launcher_vulkan_option
|
||||
@@ -1160,6 +1274,17 @@ target_include_directories(reachable_discovery_test PRIVATE
|
||||
)
|
||||
target_link_libraries(reachable_discovery_test PRIVATE fmt)
|
||||
|
||||
# Straight-line interpretation keeps going through game text that dispatch
|
||||
# refuses (live bytes differ from the boot EXE) instead of handing each
|
||||
# instruction back: the production interpreter linked into a fixture, other
|
||||
# seams stubbed to abort() (runtime/tests/fixture_link.py needs nm and GNU ld).
|
||||
if(BUILD_TESTING AND Python3_Interpreter_FOUND AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
add_test(NAME interp_refused_game_text
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_interp_refused_game_text.py"
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
endif()
|
||||
|
||||
# GTE deadline ownership includes pending CPU charges from both host sinks.
|
||||
if(BUILD_TESTING AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
foreach(_mode IN ITEMS O0 O2)
|
||||
@@ -1175,6 +1300,16 @@ if(BUILD_TESTING AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# The store that kicks a GPU linked-list DMA holds the CPU until the walk
|
||||
# ends: the production dma.c and dma_gpu_ll.c linked into a fixture, other
|
||||
# seams stubbed to abort() (runtime/tests/fixture_link.py needs nm and GNU ld).
|
||||
if(BUILD_TESTING AND Python3_Interpreter_FOUND AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
add_test(NAME dma_gpu_list_cpu_hold
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_dma_gpu_list_cpu_hold.py"
|
||||
--cc ${CMAKE_C_COMPILER})
|
||||
endif()
|
||||
|
||||
# Mult/div deadline ownership: the same rule for MULT/DIV and MFLO/MFHI.
|
||||
if(BUILD_TESTING AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
|
||||
foreach(_mode IN ITEMS O0 O2)
|
||||
|
||||
@@ -179,6 +179,11 @@ struct CodeGenConfig {
|
||||
// scales tan(angle) by the current horizontal reveal factor.
|
||||
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle_sites;
|
||||
|
||||
// [[draw_distance.clamp]]: main-EXE ordering-table range guards. While
|
||||
// g_psx_draw_distance_clamp is set the site's register is clamped to its
|
||||
// max before the instruction runs; overlay code keeps its own code.
|
||||
std::vector<PSXRecompV4::DrawDistanceClampSite> draw_distance_clamp_sites;
|
||||
|
||||
// Exact model-participation cosine compares that gain a camera-horizontal
|
||||
// aspect envelope while preserving the vanilla vertical cone.
|
||||
PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone;
|
||||
@@ -483,8 +488,10 @@ private:
|
||||
|
||||
// Instruction translation
|
||||
std::string translate_instruction(uint32_t addr, uint32_t instr);
|
||||
std::string translate_instruction_body(uint32_t addr, uint32_t instr);
|
||||
std::string mod_instruction_call(uint32_t addr, uint32_t instr) const;
|
||||
std::string translate_instruction_core(uint32_t addr, uint32_t instr);
|
||||
// The [[draw_distance.clamp]] statement for (addr, instr), or "".
|
||||
std::string draw_distance_clamp_prefix(uint32_t addr, uint32_t instr) const;
|
||||
|
||||
// Register name mapping
|
||||
static std::string reg_name(int reg_num);
|
||||
|
||||
@@ -838,6 +838,14 @@ std::string CodeGenerator::generate_branch_condition(uint32_t instr, uint32_t ad
|
||||
(ws_cull_bltz_pcs_.count(addr) || config_.ws_cull_bltz_sites.count(addr)))
|
||||
return fmt::format("psx_ws_cull_bltz({}) /* ws cull (left edge) */",
|
||||
reg_name(rs));
|
||||
// [widescreen.cull] screen_x_sites on a bltz: the tested register
|
||||
// holds a screen X in its high half (`SX << 16`). bltz_sites
|
||||
// compares the whole register with -margin and so never widens
|
||||
// this form; take the branch only while SX < -margin. Identity at
|
||||
// 4:3.
|
||||
if (regimm_op == 0x00 && config_.ws_cull_screen_x_sites.count(addr))
|
||||
return fmt::format("psx_ws_cull_bltz_hi({}) /* ws cull (left edge, X in the high half) */",
|
||||
reg_name(rs));
|
||||
if (regimm_op == 0x00) {
|
||||
const std::string n = nclip_sign_branch("<");
|
||||
if (!n.empty()) return n;
|
||||
@@ -897,10 +905,58 @@ std::string CodeGenerator::mod_instruction_call(uint32_t addr, uint32_t instr) c
|
||||
});
|
||||
return selected ? fmt::format("psx_mod_instruction(cpu, 0x{:08X}u, 0x{:08X}u);\n", addr, instr) : "";
|
||||
}
|
||||
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
|
||||
return mod_instruction_call(addr, instr) + translate_instruction_body(addr, instr);
|
||||
|
||||
// [[draw_distance.clamp]]: a main-EXE ordering-table range guard. While a
|
||||
// mod has the clamps on, the site's register is clamped to `max` before the
|
||||
// instruction runs, so the guard keeps a far primitive in the last OT slot
|
||||
// instead of dropping it. Identity while off. Captured overlays may hold
|
||||
// unrelated code at a listed address and keep their own code.
|
||||
std::string CodeGenerator::draw_distance_clamp_prefix(uint32_t addr,
|
||||
uint32_t instr) const {
|
||||
if (config_.overlay_mode || config_.draw_distance_clamp_sites.empty())
|
||||
return {};
|
||||
for (const auto& site : config_.draw_distance_clamp_sites) {
|
||||
if ((site.address & 0x1FFFFFFFu) != (addr & 0x1FFFFFFFu)) continue;
|
||||
if (instr != site.expected) {
|
||||
fmt::print(stderr,
|
||||
"ERROR: [[draw_distance.clamp]] expected 0x{:08X} at "
|
||||
"0x{:08X}, found 0x{:08X}\n",
|
||||
site.expected, addr, instr);
|
||||
std::exit(1);
|
||||
}
|
||||
// A load into the register in the previous slot is still in flight
|
||||
// (MIPS-I load delay): the clamp would act on the stale value.
|
||||
if (const auto prev = exe_.read_word(addr - 4u)) {
|
||||
const uint32_t pop = *prev >> 26;
|
||||
if (pop >= 0x20u && pop <= 0x26u && get_rt(*prev) == site.reg) {
|
||||
fmt::print(stderr,
|
||||
"ERROR: [[draw_distance.clamp]] site 0x{:08X} is "
|
||||
"the load-delay slot of a load into reg {}\n",
|
||||
addr, site.reg);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
return fmt::format(
|
||||
"if (g_psx_draw_distance_clamp && (int32_t){0} > {1}) {0} = "
|
||||
"(uint32_t)({1}); /* draw-distance clamp: keep far geometry */",
|
||||
reg_name((int)site.reg), site.max);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
std::string CodeGenerator::translate_instruction_body(uint32_t addr, uint32_t instr) {
|
||||
|
||||
// A selected mod instruction site's hook, then any draw-distance clamp, run
|
||||
// before the instruction. The core translation carries its own indent; the
|
||||
// prefixes get the same.
|
||||
std::string CodeGenerator::translate_instruction(uint32_t addr, uint32_t instr) {
|
||||
std::string out;
|
||||
if (std::string mod = mod_instruction_call(addr, instr); !mod.empty())
|
||||
out = config_.indent + mod;
|
||||
if (std::string clamp = draw_distance_clamp_prefix(addr, instr); !clamp.empty())
|
||||
out += config_.indent + clamp + "\n";
|
||||
return out + translate_instruction_core(addr, instr);
|
||||
}
|
||||
|
||||
std::string CodeGenerator::translate_instruction_core(uint32_t addr, uint32_t instr) {
|
||||
uint32_t opcode = (instr >> 26) & 0x3F;
|
||||
uint32_t funct = instr & 0x3F;
|
||||
|
||||
@@ -1354,12 +1410,22 @@ std::string CodeGenerator::translate_instruction_body(uint32_t addr, uint32_t in
|
||||
return fmt::format("{} = psx_ws_cull_sltiu({}, {});"
|
||||
" /* ws explicit screen-x cull */{}",
|
||||
reg_name(rt), reg_name(rs), imm, comment);
|
||||
} else if (opcode == 0x0F && get_rt(instr) != 0) {
|
||||
// lui rt,W: a screen edge kept in the high half of a register
|
||||
// (compared with `SX << 16`). Move it out by the live margin.
|
||||
uint32_t rt = get_rt(instr);
|
||||
uint16_t imm = get_imm16_u(instr);
|
||||
return fmt::format("{} = psx_ws_cull_lui_hi(0x{:04X}u);"
|
||||
" /* ws explicit screen-x edge (high half) */{}",
|
||||
reg_name(rt), imm, comment);
|
||||
} else if (!config_.overlay_mode) {
|
||||
fmt::print(stderr, "ERROR: [widescreen.cull] screen_x site 0x{:08X} "
|
||||
"is not sltiu (opcode 0x{:02X})\n", addr, opcode);
|
||||
"is not sltiu, lui or bltz (0x{:08X})\n", addr, instr);
|
||||
std::exit(1);
|
||||
}
|
||||
// Overlay variant: the configured main-EXE PC holds different code.
|
||||
// (A `bltz` at a listed address is a branch and never comes here; it
|
||||
// is widened in generate_branch_condition.)
|
||||
}
|
||||
// Widescreen automatic horizontal-FOV cull widening ([widescreen.cull]
|
||||
// auto_screen_x). ws_auto_cull_func_ is set when this function carries the
|
||||
@@ -3516,6 +3582,12 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
|
||||
ss << "extern int psx_ws_cull_bltz(uint32_t v); /* ws cull signed left edge (gpu.c) */\n";
|
||||
ss << "extern int psx_ws_cull_bgez(uint32_t v); /* ws cull signed left keep (gpu.c) */\n";
|
||||
ss << "extern uint32_t psx_ws_clip_edge_x(uint32_t v, uint32_t w); /* ws cull clip-edge bound load (gpu.c) */\n";
|
||||
// Only for a config that lists screen_x_sites, so the declarations of
|
||||
// every other title stay byte-identical.
|
||||
if (!config_.ws_cull_screen_x_sites.empty()) {
|
||||
ss << "extern uint32_t psx_ws_cull_lui_hi(uint32_t imm); /* ws screen-x edge in the high half (gpu.c) */\n";
|
||||
ss << "extern int psx_ws_cull_bltz_hi(uint32_t v); /* ws cull left edge, X in the high half (gpu.c) */\n";
|
||||
}
|
||||
ss << "extern int psx_ws_cull_vxrange(uint32_t x, uint32_t imm); /* ws masked-u16 X window */\n";
|
||||
ss << "extern int32_t psx_ws_depth_bound(int32_t imm); /* ws aspect-scaled far bound */\n";
|
||||
ss << "extern int32_t psx_ws_plane_nx(int32_t nx); /* ws side-plane normal-X scale (gpu.c) */\n";
|
||||
@@ -3536,6 +3608,8 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
|
||||
ss << "extern int psx_game_option_store(uint32_t addr, int val); /* persisted OPTION restore-at-init (game_options.c) */\n";
|
||||
ss << "extern uint32_t psx_ws_backdrop_value(uint32_t orig, int is_end, int window_cols); /* ws backdrop preload (gpu.c) */\n";
|
||||
ss << "extern void gte_ws_set_suppress(int on); /* widescreen far-backdrop un-squash (gte.cpp) */\n";
|
||||
if (!config_.overlay_mode && !config_.draw_distance_clamp_sites.empty())
|
||||
ss << "extern uint32_t g_psx_draw_distance_clamp; /* [[draw_distance.clamp]] switch (draw_distance.c) */\n";
|
||||
ss << "extern uint32_t g_debug_last_store_pc; /* exact PC of the executing SW/SH/SB — wtrace/readtrace producer attribution (debug_server.c) */\n\n";
|
||||
}
|
||||
|
||||
|
||||
@@ -696,6 +696,17 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
rt.video_pgxp_tolerance =
|
||||
toml::find<double>(video, "pgxp_tolerance");
|
||||
}
|
||||
if (video.contains("pgxp_position_fallback")) {
|
||||
rt.video_pgxp_position_fallback =
|
||||
toml::find<bool>(video, "pgxp_position_fallback");
|
||||
}
|
||||
if (video.contains("pgxp_preserve_projection")) {
|
||||
rt.video_pgxp_preserve_projection =
|
||||
toml::find<bool>(video, "pgxp_preserve_projection");
|
||||
}
|
||||
if (video.contains("pgxp_mod_only")) {
|
||||
rt.video_pgxp_mod_only = toml::find<bool>(video, "pgxp_mod_only");
|
||||
}
|
||||
if (video.contains("crt_filter")) {
|
||||
const auto mode = toml::find<std::string>(video, "crt_filter");
|
||||
if (mode == "raw") rt.video_screen_kind = 0;
|
||||
@@ -746,6 +757,10 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
|
||||
if (video.contains("low_latency_input")) {
|
||||
rt.video_low_latency_input = toml::find<bool>(video, "low_latency_input");
|
||||
}
|
||||
if (video.contains("texture_window_batching")) {
|
||||
rt.video_texture_window_batching =
|
||||
toml::find<bool>(video, "texture_window_batching");
|
||||
}
|
||||
if (video.contains("vsync")) {
|
||||
const auto mode = toml::find<std::string>(video, "vsync");
|
||||
if (mode == "on" || mode == "vsync") rt.video_vsync = 1;
|
||||
@@ -905,6 +920,70 @@ fs::path find_project_root(const fs::path& config_path) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool draw_distance_clamp_reads(uint32_t instr, uint32_t reg) {
|
||||
if (reg == 0u || reg > 31u) return false;
|
||||
const uint32_t op = instr >> 26;
|
||||
const uint32_t rs = (instr >> 21) & 31u;
|
||||
const uint32_t rt = (instr >> 16) & 31u;
|
||||
// ADDI, ADDIU, SLTI, SLTIU, ANDI, ORI, XORI read rs (LUI reads nothing).
|
||||
if (op >= 0x08u && op <= 0x0Eu) return rs == reg;
|
||||
if (op != 0x00u) return false;
|
||||
const uint32_t funct = instr & 0x3Fu;
|
||||
// SLL, SRL, SRA: rt. SLLV, SRLV, SRAV: rt and rs.
|
||||
if (funct == 0x00u || funct == 0x02u || funct == 0x03u) return rt == reg;
|
||||
if (funct == 0x04u || funct == 0x06u || funct == 0x07u)
|
||||
return rs == reg || rt == reg;
|
||||
// ADD, ADDU, SUB, SUBU, AND, OR, XOR, NOR, SLT, SLTU.
|
||||
if (funct >= 0x20u && funct <= 0x2Bu && funct != 0x28u && funct != 0x29u)
|
||||
return rs == reg || rt == reg;
|
||||
return false;
|
||||
}
|
||||
|
||||
// [[draw_distance.clamp]] (docs/config_schema.md "Draw-distance clamps").
|
||||
static std::vector<DrawDistanceClampSite> parse_draw_distance_clamps(
|
||||
const toml::value& cfg, const fs::path& config_path) {
|
||||
std::vector<DrawDistanceClampSite> sites;
|
||||
if (!cfg.contains("draw_distance")) return sites;
|
||||
const toml::value& dd = toml::find(cfg, "draw_distance");
|
||||
if (!dd.contains("clamp")) return sites;
|
||||
const auto& items = toml::find<toml::array>(dd, "clamp");
|
||||
std::set<uint32_t> seen;
|
||||
for (const auto& item : items) {
|
||||
DrawDistanceClampSite site;
|
||||
site.address = parse_hex(toml::find<std::string>(item, "address"),
|
||||
"draw_distance.clamp.address");
|
||||
site.expected = parse_hex(toml::find<std::string>(item, "expected"),
|
||||
"draw_distance.clamp.expected");
|
||||
const int64_t reg = toml::find<int64_t>(item, "reg");
|
||||
const int64_t max = toml::find<int64_t>(item, "max");
|
||||
if ((site.address & 3u) != 0u)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: draw-distance clamp address 0x{:08X} is not "
|
||||
"instruction-aligned", config_path.string(), site.address));
|
||||
if (!seen.insert(site.address & 0x1FFFFFFFu).second)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: duplicate draw-distance clamp address 0x{:08X}",
|
||||
config_path.string(), site.address));
|
||||
if (reg < 1 || reg > 31)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: draw-distance clamp at 0x{:08X}: reg must be 1..31",
|
||||
config_path.string(), site.address));
|
||||
if (max < INT32_MIN || max > INT32_MAX)
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: draw-distance clamp at 0x{:08X}: max is not a signed "
|
||||
"32-bit value", config_path.string(), site.address));
|
||||
site.reg = (uint32_t)reg;
|
||||
site.max = (int32_t)max;
|
||||
if (!draw_distance_clamp_reads(site.expected, site.reg))
|
||||
throw std::runtime_error(fmt::format(
|
||||
"{}: draw-distance clamp at 0x{:08X}: expected 0x{:08X} must "
|
||||
"be an ALU instruction that reads reg {}",
|
||||
config_path.string(), site.address, site.expected, site.reg));
|
||||
sites.push_back(site);
|
||||
}
|
||||
return sites;
|
||||
}
|
||||
|
||||
// Derive the output filename stem from a rom basename. Mirrors the Python
|
||||
// audit_config.py logic: strip a trailing .BIN/.EXE (case-insensitive) but
|
||||
// preserve dotted names like "SCUS_942.36" unchanged.
|
||||
@@ -2435,6 +2514,8 @@ GameConfig load_game_config(const fs::path& config_path_in) {
|
||||
loaded.ws_cull_clip_edge_x_load_sites =
|
||||
std::move(ws_cull_clip_edge_x_load_sites);
|
||||
loaded.ws_cull_clip_edge_width = ws_cull_clip_edge_width;
|
||||
loaded.draw_distance_clamp_sites =
|
||||
parse_draw_distance_clamps(cfg, config_path);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
@@ -2855,6 +2936,19 @@ UserSettings load_user_settings(const fs::path& path) {
|
||||
return s;
|
||||
}
|
||||
|
||||
fs::path relative_to_folder(const fs::path& p, const fs::path& folder) {
|
||||
if (p.empty() || folder.empty() || !p.is_absolute()) return p;
|
||||
std::error_code ec;
|
||||
const fs::path base = fs::weakly_canonical(folder, ec);
|
||||
if (ec) return p;
|
||||
const fs::path full = fs::weakly_canonical(p, ec);
|
||||
if (ec) return p;
|
||||
const fs::path r = full.lexically_relative(base);
|
||||
// Outside the folder (other drive, or a "../" climb) stays absolute.
|
||||
if (r.empty() || r.is_absolute() || *r.begin() == "..") return p;
|
||||
return r;
|
||||
}
|
||||
|
||||
bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
std::error_code ec;
|
||||
if (!path.parent_path().empty())
|
||||
@@ -2868,6 +2962,11 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
std::string str = p.generic_string();
|
||||
return str;
|
||||
};
|
||||
// Paths inside the game folder are stored relative to it, so a portable
|
||||
// folder still finds its disc, BIOS and memory cards after it is moved or
|
||||
// copied to another PC. Readers anchor relative paths on the exe
|
||||
// directory, which is where settings.toml lives.
|
||||
auto rel = [&](const fs::path& p) { return fwd(relative_to_folder(p, path.parent_path())); };
|
||||
|
||||
f << "# psxrecomp user settings - written by the launcher. Safe to hand-edit.\n";
|
||||
f << "# Overrides the bundled game.toml; the command line overrides this file.\n\n";
|
||||
@@ -2970,11 +3069,11 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
f << "lobby_url = \"" << s.netplay_lobby_url << "\"\n";
|
||||
}
|
||||
if (s.has_bios_path)
|
||||
f << "\n[bios]\npath = \"" << fwd(s.bios_path) << "\"\n";
|
||||
f << "\n[bios]\npath = \"" << rel(s.bios_path) << "\"\n";
|
||||
if (s.has_disc_path || s.has_disc_index) {
|
||||
f << "\n[disc]\n";
|
||||
if (s.has_disc_path)
|
||||
f << "path = \"" << fwd(s.disc_path) << "\"\n";
|
||||
f << "path = \"" << rel(s.disc_path) << "\"\n";
|
||||
/* Only meaningful for a multi-disc title; harmless (and informative)
|
||||
* for a single-disc one, where it is always 1. */
|
||||
if (s.has_disc_index)
|
||||
@@ -2984,11 +3083,11 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
|
||||
s.has_memcard1_enabled || s.has_memcard2_enabled) {
|
||||
f << "\n[memcard]\n";
|
||||
if (s.has_memcard_dir)
|
||||
f << "dir = \"" << fwd(s.memcard_dir) << "\"\n";
|
||||
f << "dir = \"" << rel(s.memcard_dir) << "\"\n";
|
||||
if (s.has_memcard1_path)
|
||||
f << "card1 = \"" << fwd(s.memcard1_path) << "\"\n";
|
||||
f << "card1 = \"" << rel(s.memcard1_path) << "\"\n";
|
||||
if (s.has_memcard2_path)
|
||||
f << "card2 = \"" << fwd(s.memcard2_path) << "\"\n";
|
||||
f << "card2 = \"" << rel(s.memcard2_path) << "\"\n";
|
||||
if (s.has_memcard1_enabled)
|
||||
f << "enable1 = " << (s.memcard1_enabled ? "true" : "false") << "\n";
|
||||
if (s.has_memcard2_enabled)
|
||||
|
||||
@@ -120,6 +120,23 @@ struct WidescreenAngleSite {
|
||||
uint32_t expected = 0; // guarded ADDI/ADDIU with rs == zero
|
||||
};
|
||||
|
||||
// [[draw_distance.clamp]]: an ordering-table range guard that a trusted mod
|
||||
// may turn from "drop the far primitive" into "keep it in the last slot".
|
||||
// While the runtime switch is on (psx_mod_set_draw_distance_clamp), `reg` is
|
||||
// clamped to `max` (signed) immediately before the instruction at `address`
|
||||
// runs. Main EXE only; the complete instruction word is the guard.
|
||||
struct DrawDistanceClampSite {
|
||||
uint32_t address = 0;
|
||||
uint32_t expected = 0; // the instruction; `reg` must be one of its sources
|
||||
uint32_t reg = 0; // 1..31
|
||||
int32_t max = 0;
|
||||
};
|
||||
|
||||
// True when `instr` is an ALU instruction (I-type or SPECIAL R-type, not a
|
||||
// load, store, branch or jump) that reads GPR `reg`. These are the only
|
||||
// instructions a draw-distance clamp may precede.
|
||||
bool draw_distance_clamp_reads(uint32_t instr, uint32_t reg);
|
||||
|
||||
// Aspect-aware horizontal participation cone. The exact compare sites are
|
||||
// full-word guarded because overlay variants can reuse a virtual address for
|
||||
// unrelated code. Registers are MIPS GPR indices captured at each comparison.
|
||||
@@ -501,11 +518,47 @@ struct RuntimeConfig {
|
||||
// the clamp. Live-tunable over TCP (pgxp verb).
|
||||
double video_pgxp_tolerance = 0.5;
|
||||
|
||||
// pgxp_position_fallback: let a vertex with no validated dataflow shadow
|
||||
// take the fraction of the projection last cached at its integer screen
|
||||
// position (the G1.4 exact table). Default true (unchanged behaviour).
|
||||
// A title built with the PGXP hooks (psxrecomp_add_game_runtime PGXP)
|
||||
// reaches near-total dataflow coverage, so for it the cache only hands
|
||||
// unrelated fractions to CPU-built 2D polygons; such a title sets false
|
||||
// ("dataflow only", the reference implementations' default).
|
||||
// Live-tunable over TCP (pgxp verb). docs/ENHANCEMENTS.md G1.11.
|
||||
bool video_pgxp_position_fallback = true;
|
||||
|
||||
// pgxp_preserve_projection: shadow the exact projection of each vertex
|
||||
// (from the GTE's unshifted MACs and a true divide) instead of the GTE's
|
||||
// own integer-IR one, which removes the residual wobble of near geometry.
|
||||
// Guest-visible GTE results are unchanged. Truncation agreement becomes a
|
||||
// bounded window (pgxp.h PGXP_PPP_AGREE_*). Default false. Live-tunable
|
||||
// over TCP (pgxp verb). docs/ENHANCEMENTS.md G1.11.
|
||||
bool video_pgxp_preserve_projection = false;
|
||||
|
||||
// pgxp_mod_only: the title ships PGXP through the psx.enhancement.pgxp
|
||||
// mod (typically a default-on override of it), which is then the one
|
||||
// switch. The [video] geometry_correction / perspective_texturing /
|
||||
// pgxp_cpu_mode values -- game.toml's and the player's settings.toml --
|
||||
// are not applied, and the launcher hides its Perspective textures row,
|
||||
// so the player is never shown a second control that does nothing or
|
||||
// keeps half of PGXP on after they switch the mod off. Netplay, which
|
||||
// clears the mod, then runs with PGXP fully off. Default false
|
||||
// (unchanged behaviour). game.toml only. docs/ENHANCEMENTS.md G1.12.
|
||||
bool video_pgxp_mod_only = false;
|
||||
|
||||
// offer_vulkan: expose the experimental Vulkan renderer in the launcher.
|
||||
// Defaults false even for Vulkan-enabled builds; developers must opt in per
|
||||
// game once visuals are validated.
|
||||
bool video_offer_vulkan = false;
|
||||
|
||||
// texture_window_batching: OpenGL only. Let textured primitives with
|
||||
// different GP0(E2h) texture windows share one draw (the window rides in
|
||||
// each vertex) instead of ending the batch at every window change. The
|
||||
// image is identical; games that tile textures with per-primitive windows
|
||||
// draw in far fewer batches. Off by default; a game opts in.
|
||||
bool video_texture_window_batching = false;
|
||||
|
||||
// low_latency_input: re-sample the pad after the wall-clock pacer (just
|
||||
// before present) so the next CPU frame reads near-fresh input instead of
|
||||
// input ~one frame stale. Default on. vsync: present/swap mode —
|
||||
@@ -961,7 +1014,9 @@ struct GameConfig {
|
||||
std::vector<uint32_t> ws_cull_range_sites;
|
||||
std::vector<uint32_t> ws_cull_a1_sites;
|
||||
// Explicit `sltiu rt,sx,W` render rejects for cases where codegen function
|
||||
// splitting separates the paired vertical test from auto_screen_x.
|
||||
// splitting separates the paired vertical test from auto_screen_x. Also
|
||||
// the two forms of a screen X kept in the high half of a register:
|
||||
// `lui rt,W` (the edge as W << 16) and `bltz` on `sx << 16`.
|
||||
std::vector<uint32_t> ws_cull_screen_x_sites;
|
||||
|
||||
// [widescreen.cull] slti_sites — explicit signed right-edge widen sites
|
||||
@@ -1258,6 +1313,9 @@ struct GameConfig {
|
||||
// [widescreen.cull] clip_edge_width -- the screen width a right-edge clip
|
||||
// bound equals. 0 = the first screen_w_imms entry (0x140 by default).
|
||||
uint32_t ws_cull_clip_edge_width = 0;
|
||||
// [[draw_distance.clamp]] -- opt-in "keep far geometry" clamps. Empty by
|
||||
// default; inert until a mod switches them on; regen required.
|
||||
std::vector<DrawDistanceClampSite> draw_distance_clamp_sites;
|
||||
};
|
||||
|
||||
// Effective clip_edge_width: explicit value, else screen_w_imms[0], else 320.
|
||||
@@ -1472,6 +1530,11 @@ UserSettings load_user_settings(const std::filesystem::path& path);
|
||||
// Write settings.toml deterministically. Returns false on I/O failure.
|
||||
bool save_user_settings(const std::filesystem::path& path, const UserSettings& s);
|
||||
|
||||
// `p` relative to `folder` when it lies inside it; otherwise `p` unchanged.
|
||||
// Keeps portable game folders portable once the launcher saves a path.
|
||||
std::filesystem::path relative_to_folder(const std::filesystem::path& p,
|
||||
const std::filesystem::path& folder);
|
||||
|
||||
// Surgical upsert of `key = true|false` under [controller] in game.toml.
|
||||
// Preserves comments and unrelated keys. Creates [controller] if missing.
|
||||
// Used to persist launcher multitap_analog into the title game config.
|
||||
|
||||
@@ -244,6 +244,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
std::vector<PSXRecompV4::WidescreenCullKeepSite> ws_cull_keep;
|
||||
std::vector<PSXRecompV4::WidescreenCullScaleSite> ws_cull_scale;
|
||||
std::vector<PSXRecompV4::WidescreenAngleSite> ws_cull_angle;
|
||||
std::vector<PSXRecompV4::DrawDistanceClampSite> draw_distance_clamps; // [[draw_distance.clamp]]
|
||||
PSXRecompV4::WidescreenAspectConeConfig ws_aspect_cone;
|
||||
int ws_cull_activation_guard_pixels = 0;
|
||||
std::vector<uint32_t> ws_cull_w_imms = { 0x140, 0x141 }; // [widescreen.cull] screen_w_imms
|
||||
@@ -320,6 +321,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
ws_cull_keep = cfg.ws_cull_keep_sites;
|
||||
ws_cull_scale = cfg.ws_cull_scale_sites;
|
||||
ws_cull_angle = cfg.ws_cull_angle_sites;
|
||||
draw_distance_clamps = cfg.draw_distance_clamp_sites;
|
||||
ws_aspect_cone = cfg.ws_aspect_cone;
|
||||
ws_cull_activation_guard_pixels =
|
||||
cfg.ws_cull_activation_guard_pixels;
|
||||
@@ -641,6 +643,8 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
check_set("widescreen.backdrop.x_sites", ws_backdrop_x);
|
||||
check_set("widescreen.backdrop.unsquash_funcs", ws_backdrop_unsquash);
|
||||
check_set("persist_options.init_store_pc", persist_init_sites);
|
||||
for (const auto& site : draw_distance_clamps)
|
||||
check("draw_distance.clamp.address", site.address);
|
||||
check("widescreen.bg2d.count_site", ws_bg2d_count_site);
|
||||
check("widescreen.bg2d.startcol_site", ws_bg2d_startcol_site);
|
||||
check("widescreen.bg2d.startx_site", ws_bg2d_startx_site);
|
||||
@@ -1494,6 +1498,7 @@ static int psxrecomp_game_main(int argc, char** argv) {
|
||||
codegen_config.ws_cull_keep_sites = ws_cull_keep;
|
||||
codegen_config.ws_cull_scale_sites = ws_cull_scale;
|
||||
codegen_config.ws_cull_angle_sites = ws_cull_angle;
|
||||
codegen_config.draw_distance_clamp_sites = draw_distance_clamps;
|
||||
codegen_config.ws_aspect_cone = ws_aspect_cone;
|
||||
codegen_config.ws_cull_w_imms = ws_cull_w_imms;
|
||||
codegen_config.ws_cull_h_imms = ws_cull_h_imms;
|
||||
|
||||
@@ -190,7 +190,8 @@ CodeGenConfig move_exact_config_sites(const CodeGenConfig& cfg, At at) {
|
||||
v.ws_bg2d_bufbase_site = at(v.ws_bg2d_bufbase_site);
|
||||
v.ws_bg2d_cap_site = at(v.ws_bg2d_cap_site);
|
||||
// Physically matched, kept as written: ws_signed_x_bound_sites,
|
||||
// ws_cull_keep_sites, ws_cull_angle_sites, ws_aspect_cone.sites.
|
||||
// ws_cull_keep_sites, ws_cull_angle_sites, ws_aspect_cone.sites,
|
||||
// draw_distance_clamp_sites.
|
||||
return v;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,386 @@
|
||||
// [[draw_distance.clamp]]: config parsing and validation, the emitted
|
||||
// main-EXE clamp (and its absence everywhere else), the main-EXE guards, the
|
||||
// shared declaration, and the overlay-cache identity (unchanged: overlays
|
||||
// keep their own code at a listed address).
|
||||
#include "code_generator.h"
|
||||
#include "config_loader.h"
|
||||
#include "control_flow.h"
|
||||
#include "../../runtime/include/draw_distance.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <cstdlib>
|
||||
#include <process.h>
|
||||
#else
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kBase = 0x80010000u;
|
||||
int failures = 0;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", message);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
void append_word(std::vector<uint8_t>& bytes, uint32_t word) {
|
||||
bytes.push_back(static_cast<uint8_t>(word));
|
||||
bytes.push_back(static_cast<uint8_t>(word >> 8));
|
||||
bytes.push_back(static_cast<uint8_t>(word >> 16));
|
||||
bytes.push_back(static_cast<uint8_t>(word >> 24));
|
||||
}
|
||||
|
||||
fs::path write_temp_config(const char* stem, const std::string& body) {
|
||||
const auto nonce = std::chrono::high_resolution_clock::now()
|
||||
.time_since_epoch().count();
|
||||
fs::path path = fs::temp_directory_path() /
|
||||
(std::string(stem) + "-" + std::to_string(nonce) + ".toml");
|
||||
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||
out << body;
|
||||
return path;
|
||||
}
|
||||
|
||||
std::string base_config() {
|
||||
return R"toml([game]
|
||||
name = "Draw Distance Test"
|
||||
id = "TEST-00000"
|
||||
exe = "TEST.EXE"
|
||||
load_address = "0x80010000"
|
||||
entry_pc = "0x80010000"
|
||||
text_size = "0x1000"
|
||||
stack_base = "0x801FFFF0"
|
||||
|
||||
[recompiler]
|
||||
seeds = "seeds.txt"
|
||||
out_dir = "generated"
|
||||
)toml";
|
||||
}
|
||||
|
||||
std::string clamp(const char* address, const char* expected, int reg,
|
||||
const char* max) {
|
||||
return std::string("\n[[draw_distance.clamp]]\naddress = \"") + address +
|
||||
"\"\nexpected = \"" + expected + "\"\nreg = " + std::to_string(reg) +
|
||||
"\nmax = " + max + "\n";
|
||||
}
|
||||
|
||||
PSXRecompV4::GameConfig load(const std::string& body) {
|
||||
fs::path path = write_temp_config("draw-distance", body);
|
||||
auto config = PSXRecompV4::load_game_config(path);
|
||||
fs::remove(path);
|
||||
return config;
|
||||
}
|
||||
|
||||
bool load_throws_with(const std::string& body, const char* needle) {
|
||||
fs::path path = write_temp_config("draw-distance-bad", body);
|
||||
bool matched = false;
|
||||
try {
|
||||
(void)PSXRecompV4::load_game_config(path);
|
||||
} catch (const std::exception& e) {
|
||||
matched = std::string(e.what()).find(needle) != std::string::npos;
|
||||
if (!matched) std::fprintf(stderr, " (threw: %s)\n", e.what());
|
||||
}
|
||||
fs::remove(path);
|
||||
return matched;
|
||||
}
|
||||
|
||||
// A function made of `words` followed by `jr ra ; nop`.
|
||||
PSXRecomp::GeneratedFunction generate(std::initializer_list<uint32_t> words,
|
||||
const PSXRecomp::CodeGenConfig& config) {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
exe.header.initial_pc = kBase;
|
||||
for (uint32_t w : words) append_word(exe.code_data, w);
|
||||
append_word(exe.code_data, 0x03E00008u);
|
||||
append_word(exe.code_data, 0x00000000u);
|
||||
const uint32_t size = static_cast<uint32_t>(exe.code_data.size());
|
||||
exe.header.file_size = size;
|
||||
|
||||
PSXRecomp::Function function{};
|
||||
function.start_addr = kBase;
|
||||
function.end_addr = kBase + size;
|
||||
function.size = size;
|
||||
function.name = "clamp_site_test";
|
||||
|
||||
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
||||
const auto cfg = analyzer.analyze_function(function);
|
||||
PSXRecomp::CodeGenerator generator(exe, config);
|
||||
return generator.generate_function(function, cfg);
|
||||
}
|
||||
|
||||
// Runs `body` in a child and reports whether it exited with a failure status
|
||||
// (the main-EXE guards call std::exit(1)). Windows has no fork: the Nth call
|
||||
// re-runs this binary as `--exit-probe N`, which runs only the Nth body.
|
||||
const char* g_self = nullptr;
|
||||
int g_exit_probe = -1;
|
||||
int g_probe_seq = 0;
|
||||
|
||||
template <class F>
|
||||
bool exits_with_failure(F&& body) {
|
||||
#if defined(_WIN32)
|
||||
const int seq = g_probe_seq++;
|
||||
if (g_exit_probe >= 0) {
|
||||
if (seq != g_exit_probe) return false;
|
||||
body();
|
||||
std::fflush(nullptr);
|
||||
std::_Exit(0);
|
||||
}
|
||||
std::fflush(nullptr);
|
||||
const std::string quoted = std::string("\"") + g_self + "\"";
|
||||
const std::string n = std::to_string(seq);
|
||||
const intptr_t rc = _spawnl(_P_WAIT, g_self, quoted.c_str(), "--exit-probe",
|
||||
n.c_str(), nullptr);
|
||||
return rc > 0;
|
||||
#else
|
||||
(void)g_probe_seq;
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
if (!std::freopen("/dev/null", "w", stderr)) _exit(3);
|
||||
body();
|
||||
_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (pid < 0 || waitpid(pid, &status, 0) != pid) return false;
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr uint32_t kSltiuT2V0 = 0x2C4A01C0u; // sltiu t2, v0, 0x1C0
|
||||
constexpr uint32_t kAddiuAtV0 = 0x2441FFFFu; // addiu at, v0, -1
|
||||
constexpr uint32_t kSravV0 = 0x00561007u; // srav v0, s6, v0
|
||||
constexpr uint32_t kLwV0 = 0x8C820060u; // lw v0, 0x60(a0)
|
||||
constexpr uint32_t kLwV1 = 0x8C830060u; // lw v1, 0x60(a0)
|
||||
constexpr uint32_t kBeqZero = 0x10000002u; // b +2 (beq zero, zero)
|
||||
constexpr uint32_t kNop = 0x00000000u;
|
||||
|
||||
const char* kClampV0 =
|
||||
"if (g_psx_draw_distance_clamp && (int32_t)cpu->gpr[2] > 447) "
|
||||
"cpu->gpr[2] = (uint32_t)(447);";
|
||||
|
||||
PSXRecomp::CodeGenConfig with_site(uint32_t address, uint32_t expected,
|
||||
uint32_t reg, int32_t max) {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
PSXRecompV4::DrawDistanceClampSite site;
|
||||
site.address = address;
|
||||
site.expected = expected;
|
||||
site.reg = reg;
|
||||
site.max = max;
|
||||
config.draw_distance_clamp_sites.push_back(site);
|
||||
return config;
|
||||
}
|
||||
|
||||
void reads_classifier() {
|
||||
using PSXRecompV4::draw_distance_clamp_reads;
|
||||
check(draw_distance_clamp_reads(kSltiuT2V0, 2), "sltiu reads rs");
|
||||
check(!draw_distance_clamp_reads(kSltiuT2V0, 10), "sltiu does not read rt");
|
||||
check(draw_distance_clamp_reads(kAddiuAtV0, 2), "addiu reads rs");
|
||||
check(draw_distance_clamp_reads(kSravV0, 2) &&
|
||||
draw_distance_clamp_reads(kSravV0, 22),
|
||||
"srav reads rs and rt");
|
||||
check(draw_distance_clamp_reads(0x00021080u, 2), "sll reads rt"); // sll v0,v0,2
|
||||
check(draw_distance_clamp_reads(0x0062102Bu, 3), "sltu reads rs"); // sltu v0,v1,v0
|
||||
check(!draw_distance_clamp_reads(0x3C02800Bu, 2), "lui reads nothing");
|
||||
check(!draw_distance_clamp_reads(kLwV0, 4), "a load is not a clamp site");
|
||||
check(!draw_distance_clamp_reads(0x10400002u, 2), "a branch is not a clamp site");
|
||||
check(!draw_distance_clamp_reads(0x00430018u, 2), "mult is not a clamp site");
|
||||
check(!draw_distance_clamp_reads(0x03E00008u, 31), "jr is not a clamp site");
|
||||
check(!draw_distance_clamp_reads(kSltiuT2V0, 0), "reg 0 is refused");
|
||||
}
|
||||
|
||||
void loader() {
|
||||
auto none = load(base_config());
|
||||
check(none.draw_distance_clamp_sites.empty(), "no clamps by default");
|
||||
|
||||
auto two = load(base_config() +
|
||||
clamp("0x80061230", "0x2C4A01C0", 2, "0x1BF") +
|
||||
clamp("0x80066FF8", "0x2441FFFF", 2, "447"));
|
||||
check(two.draw_distance_clamp_sites.size() == 2, "two clamps parsed");
|
||||
if (two.draw_distance_clamp_sites.size() == 2) {
|
||||
const auto& a = two.draw_distance_clamp_sites[0];
|
||||
const auto& b = two.draw_distance_clamp_sites[1];
|
||||
check(a.address == 0x80061230u && a.expected == kSltiuT2V0 &&
|
||||
a.reg == 2u && a.max == 0x1BF,
|
||||
"first clamp fields");
|
||||
check(b.address == 0x80066FF8u && b.expected == kAddiuAtV0 &&
|
||||
b.max == 447,
|
||||
"second clamp fields (decimal max)");
|
||||
}
|
||||
auto neg = load(base_config() + clamp("0x80061230", "0x2C4A01C0", 2, "-5"));
|
||||
check(neg.draw_distance_clamp_sites.size() == 1 &&
|
||||
neg.draw_distance_clamp_sites[0].max == -5,
|
||||
"a negative max is accepted");
|
||||
|
||||
check(load_throws_with(base_config() + clamp("0x80061232", "0x2C4A01C0", 2, "1"),
|
||||
"not instruction-aligned"),
|
||||
"unaligned address refused");
|
||||
check(load_throws_with(base_config() +
|
||||
clamp("0x80061230", "0x2C4A01C0", 2, "1") +
|
||||
clamp("0xA0061230", "0x2C4A01C0", 2, "1"),
|
||||
"duplicate draw-distance clamp"),
|
||||
"duplicate physical address refused");
|
||||
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 0, "1"),
|
||||
"reg must be 1..31"),
|
||||
"reg 0 refused");
|
||||
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 32, "1"),
|
||||
"reg must be 1..31"),
|
||||
"reg 32 refused");
|
||||
check(load_throws_with(base_config() + clamp("0x80061230", "0x2C4A01C0", 3, "1"),
|
||||
"must be an ALU instruction that reads reg 3"),
|
||||
"a reg the instruction does not read is refused");
|
||||
check(load_throws_with(base_config() + clamp("0x80061230", "0x8C820060", 4, "1"),
|
||||
"must be an ALU instruction"),
|
||||
"a load site is refused");
|
||||
check(load_throws_with(base_config() +
|
||||
clamp("0x80061230", "0x2C4A01C0", 2, "0x100000000"),
|
||||
"not a signed 32-bit value"),
|
||||
"max outside int32 refused");
|
||||
}
|
||||
|
||||
void emission() {
|
||||
// The clamp runs immediately before the guard it protects.
|
||||
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
|
||||
const auto out = generate({kSravV0, kSltiuT2V0, kNop}, cfg).full_code;
|
||||
const size_t at = out.find(kClampV0);
|
||||
const size_t guard = out.find("0x80010004:");
|
||||
check(at != std::string::npos, "main-EXE site emits the clamp");
|
||||
check(at != std::string::npos && guard != std::string::npos && at < guard,
|
||||
"the clamp precedes the guard instruction");
|
||||
check(at != std::string::npos &&
|
||||
out.find(kClampV0, at + 1) == std::string::npos,
|
||||
"exactly one clamp emitted");
|
||||
// The guard's statement may open a PGXP operand-capture block first
|
||||
// (`{ uint32_t _pgx1 = ...; cpu->gpr[10] = ...`); it still starts on the
|
||||
// line after the clamp, at the same indent.
|
||||
const size_t next = at == std::string::npos ? std::string::npos
|
||||
: out.find('\n', at);
|
||||
const size_t next_end = next == std::string::npos ? std::string::npos
|
||||
: out.find('\n', next + 1);
|
||||
check(at != std::string::npos && at >= 5 &&
|
||||
out.compare(at - 5, 5, "\n ") == 0 &&
|
||||
next != std::string::npos && out.compare(next, 5, "\n ") == 0 &&
|
||||
out.compare(next + 5, 1, " ") != 0 &&
|
||||
out.substr(next, next_end - next).find("cpu->gpr[10] =") !=
|
||||
std::string::npos,
|
||||
"the clamp and the guard share the statement indent");
|
||||
|
||||
// The addiu form clamps the source the slot index is computed from.
|
||||
const auto addiu = generate({kSravV0, kAddiuAtV0, kNop},
|
||||
with_site(kBase + 4u, kAddiuAtV0, 2, 447)).full_code;
|
||||
check(addiu.find(kClampV0) != std::string::npos,
|
||||
"addiu site emits the clamp on its source");
|
||||
|
||||
// A site in a branch delay slot is clamped inside the slot.
|
||||
const auto ds = generate({kBeqZero, kSltiuT2V0, kNop, kNop},
|
||||
with_site(kBase + 4u, kSltiuT2V0, 2, 447)).full_code;
|
||||
check(ds.find(kClampV0) != std::string::npos, "delay-slot site is clamped");
|
||||
|
||||
// No sites: nothing about the clamp anywhere.
|
||||
PSXRecomp::CodeGenConfig plain{};
|
||||
const auto vanilla = generate({kSravV0, kSltiuT2V0, kNop}, plain).full_code;
|
||||
check(vanilla.find("draw_distance") == std::string::npos &&
|
||||
vanilla.find("draw-distance") == std::string::npos,
|
||||
"no sites: vanilla code");
|
||||
|
||||
// A site elsewhere leaves this instruction alone.
|
||||
const auto other = generate({kSravV0, kSltiuT2V0, kNop},
|
||||
with_site(kBase + 0x100u, kSltiuT2V0, 2, 447)).full_code;
|
||||
check(other.find("draw-distance") == std::string::npos,
|
||||
"unlisted address: vanilla code");
|
||||
|
||||
// Captured overlays keep their own code at a listed address, matching
|
||||
// word or not.
|
||||
auto overlay = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
|
||||
overlay.overlay_mode = true;
|
||||
check(generate({kSravV0, kSltiuT2V0, kNop}, overlay).full_code.find(
|
||||
"draw-distance") == std::string::npos,
|
||||
"overlay variant with the listed word stays vanilla");
|
||||
check(!exits_with_failure([&] { generate({kSravV0, kAddiuAtV0, kNop}, overlay); }),
|
||||
"overlay variant with another word generates");
|
||||
}
|
||||
|
||||
void guards() {
|
||||
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
|
||||
check(!exits_with_failure([&] { generate({kSravV0, kSltiuT2V0, kNop}, cfg); }),
|
||||
"matching word generates");
|
||||
check(exits_with_failure([&] { generate({kSravV0, kAddiuAtV0, kNop}, cfg); }),
|
||||
"a main-EXE word mismatch fails generation");
|
||||
check(exits_with_failure([&] { generate({kLwV0, kSltiuT2V0, kNop}, cfg); }),
|
||||
"a site in the load-delay slot of its register fails generation");
|
||||
check(!exits_with_failure([&] { generate({kLwV1, kSltiuT2V0, kNop}, cfg); }),
|
||||
"a load into another register before the site is fine");
|
||||
}
|
||||
|
||||
void shared_decls() {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
std::vector<PSXRecomp::GeneratedFunction> functions;
|
||||
{
|
||||
PSXRecomp::CodeGenerator plain(exe);
|
||||
check(plain.build_shared_decls_header(functions).find(
|
||||
"g_psx_draw_distance_clamp") == std::string::npos,
|
||||
"no sites: the declarations do not change");
|
||||
}
|
||||
{
|
||||
const auto cfg = with_site(kBase + 4u, kSltiuT2V0, 2, 447);
|
||||
PSXRecomp::CodeGenerator gen(exe, cfg);
|
||||
check(gen.build_shared_decls_header(functions).find(
|
||||
"extern uint32_t g_psx_draw_distance_clamp;") != std::string::npos,
|
||||
"sites: the switch is declared");
|
||||
}
|
||||
}
|
||||
|
||||
void overlay_identity_unchanged() {
|
||||
auto none = load(base_config());
|
||||
auto some = load(base_config() + clamp("0x80061230", "0x2C4A01C0", 2, "0x1BF"));
|
||||
check(PSXRecompV4::overlay_codegen_config_hash(none) ==
|
||||
PSXRecompV4::overlay_codegen_config_hash(some),
|
||||
"clamps do not enter the overlay-cache identity");
|
||||
}
|
||||
|
||||
void runtime_value() {
|
||||
check(psx_draw_distance_clamp_value(0x1C0u, 0x1BF) == 0x1BFu, "clamps past max");
|
||||
check(psx_draw_distance_clamp_value(0x7FFu, 0x1BF) == 0x1BFu, "clamps far past max");
|
||||
check(psx_draw_distance_clamp_value(0x1BFu, 0x1BF) == 0x1BFu, "max is kept");
|
||||
check(psx_draw_distance_clamp_value(5u, 0x1BF) == 5u, "near values unchanged");
|
||||
check(psx_draw_distance_clamp_value(0u, 0x1BF) == 0u, "zero unchanged");
|
||||
check(psx_draw_distance_clamp_value(0xFFFFFFFFu, 0x1BE) == 0xFFFFFFFFu,
|
||||
"a negative value is left for the original predicate");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_self = argv[0];
|
||||
if (argc >= 3 && std::string(argv[1]) == "--exit-probe")
|
||||
g_exit_probe = std::atoi(argv[2]);
|
||||
reads_classifier();
|
||||
loader();
|
||||
emission();
|
||||
guards();
|
||||
shared_decls();
|
||||
overlay_identity_unchanged();
|
||||
runtime_value();
|
||||
if (failures) {
|
||||
std::fprintf(stderr, "draw_distance_codegen_test: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
std::printf("draw_distance_codegen_test: OK\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/* portable_settings_paths_test
|
||||
*
|
||||
* A portable game folder must keep working after it is moved or copied to
|
||||
* another PC. The launcher saves the disc, BIOS and memory-card paths the
|
||||
* player used; saved as absolute paths they pinned the folder to its first
|
||||
* location. What this pins:
|
||||
*
|
||||
* 1. a path inside the settings folder is written relative to it;
|
||||
* 2. a path outside it (a NAS disc, another drive) stays absolute;
|
||||
* 3. the relative form reads back unchanged, so the reader's exe-directory
|
||||
* anchoring resolves it on the new PC.
|
||||
*/
|
||||
#include "config_loader.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
using PSXRecompV4::relative_to_folder;
|
||||
|
||||
static int failures = 0;
|
||||
|
||||
static void check(bool cond, const char* what) {
|
||||
if (!cond) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", what);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
const fs::path game = fs::temp_directory_path() / "psx portable paths test" / "game";
|
||||
std::error_code ec;
|
||||
fs::remove_all(game.parent_path(), ec);
|
||||
fs::create_directories(game / "inputs" / "disc-1");
|
||||
fs::create_directories(game.parent_path() / "elsewhere");
|
||||
|
||||
const fs::path disc = game / "inputs" / "disc-1" / "Game (Europe).chd";
|
||||
const fs::path outside = game.parent_path() / "elsewhere" / "bios.bin";
|
||||
|
||||
check(relative_to_folder(disc, game).generic_string() == "inputs/disc-1/Game (Europe).chd",
|
||||
"a path inside the folder becomes relative");
|
||||
check(relative_to_folder(outside, game) == outside, "a path outside the folder stays absolute");
|
||||
check(relative_to_folder(fs::path("inputs/x.bin"), game) == fs::path("inputs/x.bin"),
|
||||
"an already relative path is unchanged");
|
||||
check(relative_to_folder(fs::path(), game).empty(), "an empty path stays empty");
|
||||
|
||||
PSXRecompV4::UserSettings s{};
|
||||
s.bios_path = game / "inputs" / "[EU] SCPH5552.BIN"; s.has_bios_path = true;
|
||||
s.disc_path = disc; s.has_disc_path = true;
|
||||
s.memcard_dir = game / "saves"; s.has_memcard_dir = true;
|
||||
s.memcard1_path = game / "saves" / "card1.mcd"; s.has_memcard1_path = true;
|
||||
s.memcard2_path = outside; s.has_memcard2_path = true;
|
||||
const fs::path settings = game / "settings.toml";
|
||||
check(PSXRecompV4::save_user_settings(settings, s), "settings save");
|
||||
|
||||
std::stringstream text;
|
||||
text << std::ifstream(settings).rdbuf();
|
||||
const std::string body = text.str();
|
||||
check(body.find("path = \"inputs/[EU] SCPH5552.BIN\"") != std::string::npos, "BIOS saved relative");
|
||||
check(body.find("path = \"inputs/disc-1/Game (Europe).chd\"") != std::string::npos, "disc saved relative");
|
||||
check(body.find("dir = \"saves\"") != std::string::npos, "memory card folder saved relative");
|
||||
check(body.find("card1 = \"saves/card1.mcd\"") != std::string::npos, "card 1 saved relative");
|
||||
check(body.find(outside.generic_string()) != std::string::npos, "outside card stays absolute");
|
||||
|
||||
const PSXRecompV4::UserSettings back = PSXRecompV4::load_user_settings(settings);
|
||||
check(back.has_disc_path && back.disc_path == fs::path("inputs/disc-1/Game (Europe).chd"),
|
||||
"relative disc path reads back unchanged");
|
||||
check(back.has_memcard1_path && back.memcard1_path == fs::path("saves/card1.mcd"),
|
||||
"relative card path reads back unchanged");
|
||||
|
||||
fs::remove_all(game.parent_path(), ec);
|
||||
if (failures) return 1;
|
||||
std::printf("portable_settings_paths_test: all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Every committed generated/<stem>.emitter.sha matches the current emitter.
|
||||
|
||||
runtime.cmake compares these stamps only at title configure time, and only
|
||||
fatally in release CI, so a change to a fingerprint input (emitter sources,
|
||||
cycle headers, a profile or its seeds) that forgets to restamp sails through
|
||||
the test suite. This test makes that drift fail here instead.
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--cmake", required=True)
|
||||
ap.add_argument("--root", type=Path,
|
||||
default=Path(__file__).resolve().parents[2])
|
||||
args = ap.parse_args()
|
||||
root = args.root
|
||||
stamps = sorted((root / "generated").glob("*.emitter.sha"))
|
||||
assert stamps, "no generated/*.emitter.sha stamps found"
|
||||
stale = []
|
||||
for stamp in stamps:
|
||||
stem = stamp.name[:-len(".emitter.sha")]
|
||||
profile = root / "bios" / (stem + ".toml")
|
||||
assert profile.exists(), f"{stamp.name} has no bios/{stem}.toml profile"
|
||||
p = subprocess.run(
|
||||
[args.cmake, f"-DROOT={root}", f"-DPROFILE=bios/{stem}.toml",
|
||||
"-P", str(root / "tools" / "bios_emitter_fingerprint.cmake")],
|
||||
cwd=root, capture_output=True, text=True, timeout=120)
|
||||
assert p.returncode == 0, p.stderr
|
||||
now = p.stdout.strip().splitlines()[-1].strip()
|
||||
saved = stamp.read_text(encoding="utf-8").strip()
|
||||
if now != saved:
|
||||
stale.append(f"{stem}: stamp {saved} now {now}")
|
||||
assert not stale, ("stale BIOS emitter stamps (run tools/regen_bios.sh "
|
||||
"--config bios/<stem>.toml and commit generated/):\n "
|
||||
+ "\n ".join(stale))
|
||||
print(f"PASS: {len(stamps)} BIOS emitter stamps match the emitter")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -571,7 +571,7 @@ def main():
|
||||
#include <stddef.h>
|
||||
#include <assert.h>
|
||||
#include "psx_memory.h"
|
||||
int psx_mod_set_main_ram_8mb(int enabled); /* runtime psx_ram_geometry.c */
|
||||
void psx_ram_request_8mb(int enabled); /* runtime psx_ram_geometry.c */
|
||||
typedef struct { uint32_t pc; } CPUState;
|
||||
static int allowed = 1, calls = 0, irqs = 0;
|
||||
static uint32_t resumed;
|
||||
@@ -591,7 +591,7 @@ int psx_vsync_query_hle_try(CPUState* cpu, uint32_t a) { (void)cpu; (void)a; ret
|
||||
harness += r"""
|
||||
static void set_geometry(int expanded) {
|
||||
psx_ram_reset_size_request();
|
||||
if (expanded) psx_mod_set_main_ram_8mb(1);
|
||||
if (expanded) psx_ram_request_8mb(1);
|
||||
psx_ram_apply_size_request();
|
||||
assert(memory_get_ram_bytes() == (expanded ? 0x00800000u : 0x00200000u));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ def main() -> int:
|
||||
/* As CodeGenerator::emit_runtime_externs declares them in every shard. */
|
||||
extern int psx_ws_cull_bgez(uint32_t v);
|
||||
extern uint32_t psx_ws_clip_edge_x(uint32_t v, uint32_t w);
|
||||
extern uint32_t psx_ws_cull_lui_hi(uint32_t imm);
|
||||
extern int psx_ws_cull_bltz_hi(uint32_t v);
|
||||
|
||||
void func_80010000(CPUState *cpu) {
|
||||
psx_cyc_bb_defer_begin();
|
||||
@@ -45,6 +47,10 @@ void func_80010000(CPUState *cpu) {
|
||||
* writes both in overlay code, so the shim must define them. */
|
||||
cpu->gpr[8] = (uint32_t)psx_ws_cull_bgez(cpu->gpr[9]);
|
||||
cpu->gpr[10] = psx_ws_clip_edge_x(cpu->gpr[11], 0x140u);
|
||||
/* [widescreen.cull] screen_x_sites on lui / bltz (a screen X in the high
|
||||
* half of a register): emitted in overlay code too. */
|
||||
cpu->gpr[22] = psx_ws_cull_lui_hi(0x0200u);
|
||||
if (psx_ws_cull_bltz_hi(cpu->gpr[8])) cpu->gpr[12] = 1u;
|
||||
psx_cyc_bb_defer_flush();
|
||||
psx_cyc_bb_defer_end();
|
||||
if (psx_slice_block(cpu, 0x80010000u, 1u, 0)) return;
|
||||
|
||||
@@ -278,6 +278,76 @@ static void test_internal_resolution_settings() {
|
||||
}
|
||||
}
|
||||
|
||||
/* [video] texture_window_batching: a game.toml-only OpenGL batching opt-in
|
||||
* (the image is unchanged; see gpu_gl_renderer.c s_twin_batching). Off unless
|
||||
* the game asks for it. */
|
||||
static void test_texture_window_batching() {
|
||||
fs::path p = write_game_toml("psxrecomp_twin_default.toml", "");
|
||||
auto gc = PSXRecompV4::load_game_config(p);
|
||||
check(!gc.runtime.video_texture_window_batching,
|
||||
"texture_window_batching defaults OFF");
|
||||
fs::remove(p);
|
||||
fs::path q = write_game_toml("psxrecomp_twin_on.toml",
|
||||
"[video]\n"
|
||||
"texture_window_batching = true\n");
|
||||
auto gq = PSXRecompV4::load_game_config(q);
|
||||
check(gq.runtime.video_texture_window_batching,
|
||||
"[video] texture_window_batching = true is honoured");
|
||||
fs::remove(q);
|
||||
}
|
||||
|
||||
/* docs/ENHANCEMENTS.md G1.11: the PGXP title keys. Defaults keep the
|
||||
* historical behaviour (tolerance 0.5, position cache consulted, IR-path
|
||||
* shadows); a title built with the hooks sets all three. */
|
||||
static void test_pgxp_title_keys() {
|
||||
fs::path p = write_game_toml("psxrecomp_pgxp_keys_default.toml", "");
|
||||
auto gc = PSXRecompV4::load_game_config(p);
|
||||
check(gc.runtime.video_pgxp_tolerance == 0.5,
|
||||
"pgxp_tolerance defaults to 0.5");
|
||||
check(gc.runtime.video_pgxp_position_fallback,
|
||||
"pgxp_position_fallback defaults ON (unchanged behaviour)");
|
||||
check(!gc.runtime.video_pgxp_preserve_projection,
|
||||
"pgxp_preserve_projection defaults OFF (unchanged behaviour)");
|
||||
check(!gc.runtime.video_pgxp_mod_only,
|
||||
"pgxp_mod_only defaults OFF (unchanged behaviour)");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_game_toml("psxrecomp_pgxp_keys_dataflow.toml",
|
||||
"[video]\n"
|
||||
"pgxp_tolerance = -1.0\n"
|
||||
"pgxp_position_fallback = false\n"
|
||||
"pgxp_preserve_projection = true\n"
|
||||
"pgxp_mod_only = true\n");
|
||||
gc = PSXRecompV4::load_game_config(p);
|
||||
check(gc.runtime.video_pgxp_mod_only, "pgxp_mod_only = true is honoured");
|
||||
check(gc.runtime.video_pgxp_tolerance < 0.0,
|
||||
"pgxp_tolerance = -1.0 disables the clamp");
|
||||
check(!gc.runtime.video_pgxp_position_fallback,
|
||||
"pgxp_position_fallback = false is honoured");
|
||||
check(gc.runtime.video_pgxp_preserve_projection,
|
||||
"pgxp_preserve_projection = true is honoured");
|
||||
check(!gc.runtime.video_geometry_correction &&
|
||||
!gc.runtime.video_perspective_texturing,
|
||||
"the tuning keys do not turn PGXP on by themselves");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_game_toml("psxrecomp_pgxp_keys_bad.toml",
|
||||
"[video]\n"
|
||||
"pgxp_preserve_projection = 1\n");
|
||||
bool rejected = false;
|
||||
try { (void)PSXRecompV4::load_game_config(p); } catch (const std::exception&) { rejected = true; }
|
||||
check(rejected, "pgxp_preserve_projection must be a boolean");
|
||||
fs::remove(p);
|
||||
|
||||
p = write_game_toml("psxrecomp_pgxp_keys_bad_mod_only.toml",
|
||||
"[video]\n"
|
||||
"pgxp_mod_only = \"yes\"\n");
|
||||
rejected = false;
|
||||
try { (void)PSXRecompV4::load_game_config(p); } catch (const std::exception&) { rejected = true; }
|
||||
check(rejected, "pgxp_mod_only must be a boolean");
|
||||
fs::remove(p);
|
||||
}
|
||||
|
||||
int main() {
|
||||
test_internal_resolution_game_toml();
|
||||
test_internal_resolution_settings();
|
||||
@@ -288,6 +358,8 @@ int main() {
|
||||
test_user_settings_read();
|
||||
test_user_settings_absent_key();
|
||||
test_user_settings_round_trip();
|
||||
test_texture_window_batching();
|
||||
test_pgxp_title_keys();
|
||||
|
||||
if (failures) {
|
||||
std::fprintf(stderr, "video_enhancement_settings_test: %d failure(s)\n",
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
// [widescreen.cull] screen_x_sites on `lui` and `bltz`: a screen X kept in the
|
||||
// high half of a register. Emitted native code for hand-written instruction
|
||||
// words, the main-EXE guard, overlay variants, the declarations (only when the
|
||||
// key is used), and the shared runtime math (ws_cull_edge.h) the emitted
|
||||
// helpers call.
|
||||
#include "code_generator.h"
|
||||
#include "control_flow.h"
|
||||
#include "../../runtime/include/ws_cull_edge.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <cstdlib>
|
||||
#include <process.h>
|
||||
#else
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kBase = 0x80010000u;
|
||||
int failures = 0;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", message);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
bool has(const std::string& text, const char* needle) {
|
||||
return text.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
void append_word(std::vector<uint8_t>& bytes, uint32_t word) {
|
||||
bytes.push_back(static_cast<uint8_t>(word));
|
||||
bytes.push_back(static_cast<uint8_t>(word >> 8));
|
||||
bytes.push_back(static_cast<uint8_t>(word >> 16));
|
||||
bytes.push_back(static_cast<uint8_t>(word >> 24));
|
||||
}
|
||||
|
||||
// A tiny function: <word> ; nop ; addiu v1,zero,1 ; jr ra ; nop. A branch in
|
||||
// the first slot targets the jr (kBase+12).
|
||||
std::string generate_first_instruction(uint32_t first_word,
|
||||
const PSXRecomp::CodeGenConfig& config) {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
exe.header.initial_pc = kBase;
|
||||
exe.header.file_size = 20;
|
||||
append_word(exe.code_data, first_word);
|
||||
append_word(exe.code_data, 0x00000000u);
|
||||
append_word(exe.code_data, 0x24030001u);
|
||||
append_word(exe.code_data, 0x03E00008u);
|
||||
append_word(exe.code_data, 0x00000000u);
|
||||
|
||||
PSXRecomp::Function function{};
|
||||
function.start_addr = kBase;
|
||||
function.end_addr = kBase + 20u;
|
||||
function.size = 20u;
|
||||
function.name = "ws_cull_screen_x_hi_test";
|
||||
|
||||
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
||||
const auto cfg = analyzer.analyze_function(function);
|
||||
PSXRecomp::CodeGenerator generator(exe, config);
|
||||
return generator.generate_function(function, cfg).full_code;
|
||||
}
|
||||
|
||||
// Runs `body` in a child and reports whether it exited with a failure status.
|
||||
// The main-EXE guard calls std::exit(1), which a plain call cannot observe.
|
||||
// Windows has no fork: the Nth call re-runs this binary as `--exit-probe N`,
|
||||
// which repeats the tests but runs only the Nth body (every other call returns
|
||||
// false unrun) and exits 0 when that body returns.
|
||||
const char* g_self = nullptr;
|
||||
int g_exit_probe = -1;
|
||||
int g_probe_seq = 0;
|
||||
|
||||
template <class F>
|
||||
bool exits_with_failure(F&& body) {
|
||||
#if defined(_WIN32)
|
||||
const int seq = g_probe_seq++;
|
||||
if (g_exit_probe >= 0) {
|
||||
if (seq != g_exit_probe) return false;
|
||||
body();
|
||||
std::fflush(nullptr);
|
||||
std::_Exit(0);
|
||||
}
|
||||
std::fflush(nullptr);
|
||||
const std::string quoted = std::string("\"") + g_self + "\"";
|
||||
const std::string n = std::to_string(seq);
|
||||
const intptr_t rc = _spawnl(_P_WAIT, g_self, quoted.c_str(), "--exit-probe",
|
||||
n.c_str(), nullptr);
|
||||
return rc > 0;
|
||||
#else
|
||||
(void)g_probe_seq;
|
||||
std::fflush(nullptr);
|
||||
const pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
if (!std::freopen("/dev/null", "w", stderr)) _exit(3);
|
||||
body();
|
||||
_exit(0);
|
||||
}
|
||||
int status = 0;
|
||||
if (pid < 0 || waitpid(pid, &status, 0) != pid) return false;
|
||||
return WIFEXITED(status) && WEXITSTATUS(status) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
constexpr uint32_t kSltiuV0 = 0x2C620200u; // sltiu v0, v1, 0x200
|
||||
constexpr uint32_t kLuiS6 = 0x3C160200u; // lui s6, 0x200
|
||||
constexpr uint32_t kLuiZero = 0x3C000200u; // lui zero, 0x200
|
||||
constexpr uint32_t kBltzT0 = 0x05000002u; // bltz t0, +2
|
||||
constexpr uint32_t kBgezT0 = 0x05010002u; // bgez t0, +2
|
||||
constexpr uint32_t kBltzalT0 = 0x05100002u; // bltzal t0, +2
|
||||
constexpr uint32_t kAddiuV0 = 0x24820070u; // addiu v0, a0, 0x70
|
||||
|
||||
const char* const kLuiHelper =
|
||||
"cpu->gpr[22] = psx_ws_cull_lui_hi(0x0200u); "
|
||||
"/* ws explicit screen-x edge (high half) */";
|
||||
const char* const kBltzHelper =
|
||||
"psx_ws_cull_bltz_hi(cpu->gpr[8]) /* ws cull (left edge, X in the high half) */";
|
||||
|
||||
void unlisted_instructions_stay_vanilla() {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
config.emit_comments = true;
|
||||
const auto lui = generate_first_instruction(kLuiS6, config);
|
||||
check(has(lui, "cpu->gpr[22] = 0x0200 << 16;"), "unlisted lui stays vanilla");
|
||||
check(!has(lui, "psx_ws_cull_lui_hi"), "unlisted lui does not call the helper");
|
||||
const auto bltz = generate_first_instruction(kBltzT0, config);
|
||||
check(has(bltz, "(int32_t)cpu->gpr[8] < 0"), "unlisted bltz stays vanilla");
|
||||
check(!has(bltz, "psx_ws_cull_bltz_hi"), "unlisted bltz does not call the helper");
|
||||
|
||||
// A site at another address does not touch these instructions.
|
||||
config.ws_cull_screen_x_sites.insert(kBase + 0x100u);
|
||||
check(generate_first_instruction(kLuiS6, config) == lui,
|
||||
"a screen_x site elsewhere leaves the lui's function unchanged");
|
||||
check(generate_first_instruction(kBltzT0, config) == bltz,
|
||||
"a screen_x site elsewhere leaves the bltz's function unchanged");
|
||||
}
|
||||
|
||||
void listed_sites_emit_the_helpers() {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
config.emit_comments = true;
|
||||
config.ws_cull_screen_x_sites.insert(kBase);
|
||||
|
||||
const auto lui = generate_first_instruction(kLuiS6, config);
|
||||
check(has(lui, kLuiHelper), "a screen_x site on lui routes through psx_ws_cull_lui_hi");
|
||||
check(!has(lui, "cpu->gpr[22] = 0x0200 << 16;"), "the vanilla lui is replaced");
|
||||
check(!exits_with_failure([&] { generate_first_instruction(kLuiS6, config); }),
|
||||
"a screen_x site on lui generates");
|
||||
|
||||
const auto bltz = generate_first_instruction(kBltzT0, config);
|
||||
check(has(bltz, kBltzHelper), "a screen_x site on bltz routes through psx_ws_cull_bltz_hi");
|
||||
check(!has(bltz, "(int32_t)cpu->gpr[8] < 0"), "the vanilla bltz predicate is replaced");
|
||||
check(!exits_with_failure([&] { generate_first_instruction(kBltzT0, config); }),
|
||||
"a screen_x site on bltz generates");
|
||||
|
||||
// The existing form is unchanged.
|
||||
check(has(generate_first_instruction(kSltiuV0, config),
|
||||
"cpu->gpr[2] = psx_ws_cull_sltiu(cpu->gpr[3], 512); "
|
||||
"/* ws explicit screen-x cull */"),
|
||||
"a screen_x site on sltiu still routes through psx_ws_cull_sltiu");
|
||||
}
|
||||
|
||||
// bltz_sites is the low-half form and keeps its meaning when an address is in
|
||||
// both lists.
|
||||
void bltz_sites_wins_over_screen_x_sites() {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
config.ws_cull_screen_x_sites.insert(kBase);
|
||||
config.ws_cull_bltz_sites.insert(kBase);
|
||||
const auto both = generate_first_instruction(kBltzT0, config);
|
||||
check(has(both, "psx_ws_cull_bltz(cpu->gpr[8])") && !has(both, "psx_ws_cull_bltz_hi"),
|
||||
"an address in bltz_sites keeps the low-half helper");
|
||||
}
|
||||
|
||||
void main_exe_guard_and_overlay_variants() {
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
config.ws_cull_screen_x_sites.insert(kBase);
|
||||
check(exits_with_failure([&] { generate_first_instruction(kAddiuV0, config); }),
|
||||
"a screen_x site holding addiu fails main-EXE generation");
|
||||
check(exits_with_failure([&] { generate_first_instruction(kLuiZero, config); }),
|
||||
"a screen_x site holding lui into $zero fails main-EXE generation");
|
||||
|
||||
// Other branches at a listed address keep their vanilla predicate.
|
||||
const auto bgez = generate_first_instruction(kBgezT0, config);
|
||||
check(has(bgez, "(int32_t)cpu->gpr[8] >= 0") && !has(bgez, "psx_ws_cull_bltz_hi"),
|
||||
"a bgez at a screen_x site is not widened");
|
||||
const auto bltzal = generate_first_instruction(kBltzalT0, config);
|
||||
check(!has(bltzal, "psx_ws_cull_bltz_hi"), "a bltzal at a screen_x site is not widened");
|
||||
|
||||
// Captured overlays may hold unrelated code at a listed address: keep it.
|
||||
// A matching instruction is widened like the main EXE, so the shard must
|
||||
// link both helpers (overlay_dispatch_preamble.c.inc defines them).
|
||||
config.overlay_mode = true;
|
||||
check(!exits_with_failure([&] { generate_first_instruction(kAddiuV0, config); }),
|
||||
"an overlay variant holding addiu at a screen_x site generates");
|
||||
check(!has(generate_first_instruction(kAddiuV0, config), "psx_ws_cull_"),
|
||||
"an overlay variant holding addiu stays vanilla");
|
||||
check(has(generate_first_instruction(kLuiS6, config), "psx_ws_cull_lui_hi(0x0200u)"),
|
||||
"an overlay lui at a screen_x site emits psx_ws_cull_lui_hi");
|
||||
check(has(generate_first_instruction(kBltzT0, config), "psx_ws_cull_bltz_hi(cpu->gpr[8])"),
|
||||
"an overlay bltz at a screen_x site emits psx_ws_cull_bltz_hi");
|
||||
}
|
||||
|
||||
// The two prototypes are written only for a config that lists screen_x_sites,
|
||||
// so every other title's generated declarations are byte-identical.
|
||||
void declarations_only_when_the_key_is_used() {
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = kBase;
|
||||
std::vector<PSXRecomp::GeneratedFunction> functions;
|
||||
|
||||
PSXRecomp::CodeGenerator plain(exe);
|
||||
const auto without = plain.build_shared_decls_header(functions);
|
||||
check(!has(without, "psx_ws_cull_lui_hi") && !has(without, "psx_ws_cull_bltz_hi"),
|
||||
"a config without screen_x_sites declares neither helper");
|
||||
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
config.ws_cull_screen_x_sites.insert(kBase);
|
||||
PSXRecomp::CodeGenerator used(exe, config);
|
||||
const auto with = used.build_shared_decls_header(functions);
|
||||
check(has(with, "extern uint32_t psx_ws_cull_lui_hi(uint32_t imm);"),
|
||||
"a config with screen_x_sites declares psx_ws_cull_lui_hi");
|
||||
check(has(with, "extern int psx_ws_cull_bltz_hi(uint32_t v);"),
|
||||
"a config with screen_x_sites declares psx_ws_cull_bltz_hi");
|
||||
}
|
||||
|
||||
void runtime_math() {
|
||||
// Identity at 4:3 (margin 0): the lui's own value, the plain sign test.
|
||||
for (uint32_t imm : {0x0000u, 0x0001u, 0x0140u, 0x0200u, 0x0280u, 0x7FFFu,
|
||||
0x8000u, 0xFF60u, 0xFFFFu}) {
|
||||
check(psx_ws_cull_lui_hi_value(imm, 0) == (imm << 16),
|
||||
"lui edge is the vanilla value at margin 0");
|
||||
}
|
||||
for (int32_t x : {-1000, -193, -192, -1, 0, 1, 511, 512, 1000}) {
|
||||
const uint32_t v = (uint32_t)x << 16;
|
||||
check(psx_ws_cull_bltz_hi_value(v, 0) == (x < 0 ? 1 : 0),
|
||||
"bltz is vanilla at margin 0");
|
||||
check(psx_ws_cull_bltz_hi_value(v | 0xFFFFu, 0) == (x < 0 ? 1 : 0),
|
||||
"bltz is vanilla at margin 0 with a fraction in the low half");
|
||||
}
|
||||
|
||||
// 21:9 on a 512-wide display: margin 192.
|
||||
check(psx_ws_cull_lui_hi_value(0x0200u, 192) == (704u << 16),
|
||||
"right edge 512 moves to 704");
|
||||
check(psx_ws_cull_lui_hi_value(0xFF60u, 192) == ((uint32_t)-352 << 16),
|
||||
"left edge -160 moves to -352");
|
||||
check(psx_ws_cull_lui_hi_value(0x0000u, 192) == 0u, "a zero edge is unchanged");
|
||||
check(psx_ws_cull_bltz_hi_value((uint32_t)-192 << 16, 192) == 0, "bltz keeps x = -m");
|
||||
check(psx_ws_cull_bltz_hi_value((uint32_t)-193 << 16, 192) == 1, "bltz rejects x < -m");
|
||||
check(psx_ws_cull_bltz_hi_value(((uint32_t)-193 << 16) | 0xFFFFu, 192) == 1,
|
||||
"bltz rejects x < -m with a fraction in the low half");
|
||||
check(psx_ws_cull_bltz_hi_value((uint32_t)-5 << 16, 192) == 0,
|
||||
"bltz keeps the revealed band");
|
||||
check(psx_ws_cull_bltz_hi_value(5u << 16, 192) == 0, "bltz keeps an on-screen x");
|
||||
|
||||
// 16:9 on a 320-wide display: margin 53.
|
||||
check(psx_ws_cull_lui_hi_value(0x0140u, 53) == (373u << 16),
|
||||
"right edge 320 moves to 373");
|
||||
check(psx_ws_cull_bltz_hi_value((uint32_t)-53 << 16, 53) == 0 &&
|
||||
psx_ws_cull_bltz_hi_value((uint32_t)-54 << 16, 53) == 1,
|
||||
"bltz boundary at margin 53");
|
||||
|
||||
// The form bltz_sites cannot widen: a whole-register compare with -m.
|
||||
check(((int32_t)((uint32_t)-5 << 16) < -192) &&
|
||||
psx_ws_cull_bltz_hi_value((uint32_t)-5 << 16, 192) == 0,
|
||||
"a whole-register compare rejects x = -5; the high-half one keeps it");
|
||||
|
||||
check(psx_ws_cull_lui_hi_value(0x0200u, -7) == (0x0200u << 16) &&
|
||||
psx_ws_cull_bltz_hi_value((uint32_t)-1 << 16, -7) == 1,
|
||||
"a negative margin is treated as 0");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_self = argv[0];
|
||||
if (argc == 3 && std::string(argv[1]) == "--exit-probe") {
|
||||
g_exit_probe = std::atoi(argv[2]);
|
||||
if (!std::freopen("NUL", "w", stderr)) return 3;
|
||||
}
|
||||
unlisted_instructions_stay_vanilla();
|
||||
listed_sites_emit_the_helpers();
|
||||
bltz_sites_wins_over_screen_x_sites();
|
||||
main_exe_guard_and_overlay_variants();
|
||||
declarations_only_when_the_key_is_used();
|
||||
runtime_math();
|
||||
|
||||
if (g_exit_probe >= 0) return 3; // no call had that probe index
|
||||
if (failures != 0) {
|
||||
std::fprintf(stderr, "ws_cull_screen_x_hi_codegen_test: %d failure(s)\n",
|
||||
failures);
|
||||
return 1;
|
||||
}
|
||||
std::puts("PASS: ws cull screen_x high-half forms");
|
||||
return 0;
|
||||
}
|
||||
+50
-1
@@ -38,6 +38,9 @@ if(BUILD_TESTING)
|
||||
add_executable(ws_scene_hold_test tests/test_ws_scene_hold.c)
|
||||
target_include_directories(ws_scene_hold_test PRIVATE include)
|
||||
add_test(NAME ws_scene_hold_test COMMAND ws_scene_hold_test)
|
||||
add_executable(mdec_snapshot_activity_test tests/test_mdec_snapshot_activity.c)
|
||||
target_include_directories(mdec_snapshot_activity_test PRIVATE include src)
|
||||
add_test(NAME mdec_snapshot_activity_test COMMAND mdec_snapshot_activity_test)
|
||||
add_executable(mod_texture_banks_test tests/test_mod_texture_banks.c src/mod_texture_banks.c)
|
||||
target_include_directories(mod_texture_banks_test PRIVATE include)
|
||||
add_test(NAME mod_texture_banks_test COMMAND mod_texture_banks_test)
|
||||
@@ -375,6 +378,42 @@ if(BUILD_TESTING)
|
||||
target_include_directories(pgxp_test PRIVATE include)
|
||||
add_test(NAME pgxp_test COMMAND pgxp_test)
|
||||
|
||||
# psx.enhancement.pgxp at session start (docs/ENHANCEMENTS.md G1.11): the
|
||||
# real builtin activation (mod API stubbed) and the pgxp_session.h
|
||||
# resolver main.cpp's renderer setup uses, in main.cpp's order. Pins that
|
||||
# the [video] baseline no longer undoes the mod, that a cleared (netplay)
|
||||
# plan keeps the baseline, and that the env overrides win.
|
||||
# The session arming main.cpp runs (src/pgxp_session.cpp) against the
|
||||
# real mod runtime, the real builtin plugin and the builtin's own
|
||||
# manifest: default-on, netplay clear, the off switch, options, env
|
||||
# overrides and [video] pgxp_mod_only.
|
||||
add_executable(pgxp_session_test tests/test_pgxp_session.cpp
|
||||
src/pgxp_session.cpp src/mod_builtin_pgxp.c src/pgxp.cpp
|
||||
src/psx_ram_geometry.c
|
||||
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(pgxp_session_test PRIVATE
|
||||
include
|
||||
../recompiler/include
|
||||
../recompiler/lib/toml11)
|
||||
target_compile_definitions(pgxp_session_test PRIVATE
|
||||
PSX_BUILTIN_PGXP_MANIFEST="${CMAKE_CURRENT_SOURCE_DIR}/../mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml")
|
||||
target_link_libraries(pgxp_session_test PRIVATE chdr-static)
|
||||
if(WIN32)
|
||||
if(MSVC)
|
||||
target_link_options(pgxp_session_test PRIVATE /STACK:16777216)
|
||||
else()
|
||||
target_link_options(pgxp_session_test PRIVATE -Wl,--stack,16777216)
|
||||
endif()
|
||||
endif()
|
||||
add_test(NAME pgxp_session_test COMMAND pgxp_session_test)
|
||||
|
||||
add_executable(psx_instr_cost_test tests/test_psx_instr_cost.cpp)
|
||||
target_include_directories(psx_instr_cost_test PRIVATE include)
|
||||
add_test(NAME psx_instr_cost_test COMMAND psx_instr_cost_test)
|
||||
@@ -414,8 +453,10 @@ if(BUILD_TESTING)
|
||||
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||
target_compile_options(gpu_textured_dot_nw_shift_exec_test PRIVATE
|
||||
-ffunction-sections -fdata-sections)
|
||||
# Apple ld has no --gc-sections; -dead_strip is its equivalent (as
|
||||
# for isc_store_test above), so the test links and runs on macOS.
|
||||
target_link_options(gpu_textured_dot_nw_shift_exec_test PRIVATE
|
||||
-Wl,--gc-sections)
|
||||
$<IF:$<PLATFORM_ID:Darwin>,-Wl$<COMMA>-dead_strip,-Wl$<COMMA>--gc-sections>)
|
||||
endif()
|
||||
add_test(NAME gpu_textured_dot_nw_shift_exec_test
|
||||
COMMAND gpu_textured_dot_nw_shift_exec_test)
|
||||
@@ -1050,6 +1091,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME setup_bios_assets_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_setup_bios_assets.py)
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
add_test(NAME launch_disc_path_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_launch_disc_path.py --compiler ${CMAKE_CXX_COMPILER})
|
||||
add_test(NAME persisted_settings_paths_test COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_persisted_settings_paths.py --compiler ${CMAKE_CXX_COMPILER})
|
||||
endif()
|
||||
add_test(NAME disc_picker_ascii_test COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_disc_picker_ascii.py)
|
||||
@@ -1101,6 +1143,13 @@ if(BUILD_TESTING)
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_mod_catalog_layout.py
|
||||
--cmake ${CMAKE_COMMAND})
|
||||
# psxrecomp_add_game_runtime(... PGXP): the real function body under
|
||||
# cmake -P, forwarding the hook-flavor option for the title's one
|
||||
# runtime (docs/ENHANCEMENTS.md G1.11).
|
||||
add_test(NAME game_runtime_pgxp_test
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_game_runtime_pgxp.py
|
||||
--cmake ${CMAKE_COMMAND})
|
||||
# tools/mod_channel_filter.py strips developer features from a staged
|
||||
# catalog; every entry that served one ([[requirement]] included) must
|
||||
# leave with it.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/* draw_distance.h - opt-in draw-distance clamps ([[draw_distance.clamp]]).
|
||||
*
|
||||
* Many PS1 renderers drop a primitive whose depth falls past the end of their
|
||||
* ordering table: `sltiu t, z, N; beqz t, reject`. The limit protects the OT,
|
||||
* so simply raising it can write past the table. A title lists such
|
||||
* predicates in game.toml together with the register that carries the depth
|
||||
* index and the last safe value:
|
||||
*
|
||||
* [[draw_distance.clamp]]
|
||||
* address = "0x80061230"
|
||||
* expected = "0x2C4A01C0" # sltiu t2, v0, 0x1C0
|
||||
* reg = 2 # v0
|
||||
* max = 0x1BF
|
||||
*
|
||||
* While a trusted mod has switched the clamps on, the listed register is
|
||||
* clamped to `max` (signed) immediately before the listed instruction runs,
|
||||
* so a far primitive is kept in the farthest OT slot instead of being
|
||||
* dropped. Off (the default, reset at every session start) the sites run the
|
||||
* original code. Main executable only: generated main-EXE code applies the
|
||||
* clamp, captured overlay code at the same address keeps its own code, and
|
||||
* the dirty-RAM interpreter applies it where the game's text image still
|
||||
* holds the listed instruction.
|
||||
*
|
||||
* docs/config_schema.md "Draw-distance clamps" has the config rules.
|
||||
*/
|
||||
#ifndef PSX_DRAW_DISTANCE_H
|
||||
#define PSX_DRAW_DISTANCE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct PSXDrawDistanceClampSite {
|
||||
uint32_t address; /* any segment; stored physical */
|
||||
uint32_t expected; /* the complete instruction word at address */
|
||||
uint32_t reg; /* 1..31: a source register of `expected` */
|
||||
int32_t max; /* signed upper bound */
|
||||
} PSXDrawDistanceClampSite;
|
||||
|
||||
#define PSX_DRAW_DISTANCE_CLAMP_SITES_MAX 256
|
||||
|
||||
/* Nonzero while the clamps are on. Generated main-EXE code reads it at every
|
||||
* configured site. */
|
||||
extern uint32_t g_psx_draw_distance_clamp;
|
||||
/* On and at least one site configured: the interpreter's per-instruction
|
||||
* gate. */
|
||||
extern int g_psx_draw_distance_clamp_live;
|
||||
|
||||
/* The title's sites (game config). Copied, sorted, capped at
|
||||
* PSX_DRAW_DISTANCE_CLAMP_SITES_MAX (a longer list is logged). Returns the
|
||||
* number kept. */
|
||||
int psx_draw_distance_set_clamp_sites(const PSXDrawDistanceClampSite *sites,
|
||||
int count);
|
||||
int psx_draw_distance_clamp_site_count(void);
|
||||
/* The site at `pc` whose instruction is `insn`, or NULL. */
|
||||
const PSXDrawDistanceClampSite *psx_draw_distance_clamp_find(uint32_t pc,
|
||||
uint32_t insn);
|
||||
|
||||
/* The value the clamp leaves in the register. */
|
||||
static inline uint32_t psx_draw_distance_clamp_value(uint32_t value,
|
||||
int32_t max) {
|
||||
return (int32_t)value > max ? (uint32_t)max : value;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PSX_DRAW_DISTANCE_H */
|
||||
@@ -388,6 +388,12 @@ int psx_ws_cull_bgez(uint32_t v);
|
||||
* becomes -margin and one of `w` becomes w+margin; interior bounds and 4:3
|
||||
* are unchanged (see ws_cull_edge.h). */
|
||||
uint32_t psx_ws_clip_edge_x(uint32_t v, uint32_t w);
|
||||
/* [widescreen.cull] screen_x_sites on `lui` and `bltz`: a screen X kept in the
|
||||
* high half of a register. The edge `lui rt, W` becomes (W + margin) << 16 and
|
||||
* the `bltz` is taken while SX < -margin (see ws_cull_edge.h). Identity at
|
||||
* 4:3. */
|
||||
uint32_t psx_ws_cull_lui_hi(uint32_t imm);
|
||||
int psx_ws_cull_bltz_hi(uint32_t v);
|
||||
int psx_ws_cull_vxrange(uint32_t x, uint32_t imm);
|
||||
/* True if a run of instruction words carries the screen-extent reject signature
|
||||
* (a width compare AND a height compare from the configured immediate sets).
|
||||
|
||||
@@ -335,6 +335,14 @@ uint64_t gl_renderer_perf_prim_split(double *out_tex_frac);
|
||||
* isolation, blend-mode, mask, filter, backdrop-gate, texture-window, capacity. */
|
||||
void gl_renderer_batch_diag(uint64_t out[8]);
|
||||
|
||||
/* Texture-window batching ([video] texture_window_batching; default off).
|
||||
* On: textured prims with different GP0(E2h) texture windows share a batch
|
||||
* (each vertex carries its window), except while mask checking is on. The
|
||||
* image is the same either way; only the number of draws changes. Safe to
|
||||
* call before GL context creation. */
|
||||
void gl_renderer_set_texture_window_batching(int on);
|
||||
int gl_renderer_get_texture_window_batching(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -516,6 +516,17 @@ int psx_mod_set_load_acceleration(uint32_t wall_clock_multiplier,
|
||||
int psx_mod_set_disc_speed(uint32_t divisor,
|
||||
uint32_t instant_max_per_frame);
|
||||
|
||||
/*
|
||||
* Turn the title's [[draw_distance.clamp]] sites on or off for this session
|
||||
* (draw_distance.h): a far primitive the game would drop past the end of its
|
||||
* ordering table is kept in the farthest slot instead. Returns 1 when the
|
||||
* title configured sites, 0 when it has none (the call then changes nothing
|
||||
* the guest sees). Off by default and reset to off at every session start;
|
||||
* it adds guest work, so call it from activation, never in netplay.
|
||||
*/
|
||||
int psx_mod_set_draw_distance_clamp(int enabled);
|
||||
int psx_mod_draw_distance_clamp_enabled(void);
|
||||
|
||||
/* Controller presentation values exposed to trusted game-owned plugins. */
|
||||
enum {
|
||||
PSX_MOD_CONTROLLER_ANALOG = 1,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#ifndef NETPLAY_EXIT_REASON_H
|
||||
#define NETPLAY_EXIT_REASON_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
@@ -30,6 +31,40 @@ int netplay_boot_mismatch_final(uint32_t mismatch_since_ms, uint32_t now_ms,
|
||||
|
||||
#define NETPLAY_BOOT_MISMATCH_GRACE_MS 3000u
|
||||
|
||||
/* Why a netplay start failed, in one sentence. The start reported
|
||||
* every failure as "built without recomp-net, or bind/peer invalid": a player
|
||||
* whose system held the port read first that the build had no netplay.
|
||||
* `start_rc` is psx_netplay_start's result, `has_netplay` whether the build
|
||||
* has the netplay library. For a LAN start (-3) the caller does the library's
|
||||
* steps again and passes what it found: `bind_probe` and, for a refused bind
|
||||
* or socket, the system's error number and text. The sentence says a cause
|
||||
* only when the system's answer names one:
|
||||
* address in use (Windows 10048, Linux 98, macOS 48): another program or
|
||||
* the system holds the port, choose another port;
|
||||
* access denied (Windows 10013, POSIX 13): the system does not allow the
|
||||
* port (a range Windows keeps for Hyper-V or WSL, a port below 1024
|
||||
* elsewhere), choose another port;
|
||||
* address not available (Windows 10049, Linux 99, macOS 49): the listen
|
||||
* address is not one of this computer's, check the listen address;
|
||||
* any other number: the number and the system's text, no cause.
|
||||
* The peer address is named only when the listen address opened and the peer
|
||||
* address cannot be read. The build is named only when it has no netplay.
|
||||
* Every sentence fits the lobby client's last_error. */
|
||||
enum {
|
||||
NETPLAY_BIND_NOT_TRIED = -1, /* not a LAN start, or a build without the library */
|
||||
NETPLAY_BIND_OK = 0, /* listen address opened, peer address reads well */
|
||||
NETPLAY_BIND_FAILED = 1, /* the system refused the bind */
|
||||
NETPLAY_BIND_BAD_ADDRESS = 2, /* the listen address is not "address:port", or its name is unknown */
|
||||
NETPLAY_BIND_NO_SOCKET = 3, /* the system made no socket */
|
||||
NETPLAY_BIND_PEER_BAD = 4 /* listen address opened; the peer address cannot be read */
|
||||
};
|
||||
#define NETPLAY_START_FAILURE_CAP 192
|
||||
void netplay_start_failure_text(int start_rc, int has_netplay,
|
||||
const char *bind_hostport,
|
||||
const char *peer_hostport, int bind_probe,
|
||||
int sys_error, const char *sys_text,
|
||||
char *out, size_t cap);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -220,6 +220,15 @@ int psx_ws_cull_bgez(uint32_t v) {
|
||||
uint32_t psx_ws_clip_edge_x(uint32_t v, uint32_t w) {
|
||||
return psx_ws_clip_edge_x_value(v, w, psx_ws_x_margin());
|
||||
}
|
||||
/* [widescreen.cull] screen_x_sites on `lui` and `bltz` (a screen X in the high
|
||||
* half of a register). Emitted in overlay code at a listed address whose
|
||||
* instruction matches, so the shard must define them. Same math as gpu.c. */
|
||||
uint32_t psx_ws_cull_lui_hi(uint32_t imm) {
|
||||
return psx_ws_cull_lui_hi_value(imm, psx_ws_x_margin());
|
||||
}
|
||||
int psx_ws_cull_bltz_hi(uint32_t v) {
|
||||
return psx_ws_cull_bltz_hi_value(v, psx_ws_x_margin());
|
||||
}
|
||||
int psx_ws_cull_vxrange(uint32_t x, uint32_t imm) {
|
||||
int32_t margin = psx_ws_x_margin();
|
||||
uint32_t bound = (uint32_t)(int32_t)(int16_t)(uint16_t)imm;
|
||||
|
||||
+150
-1
@@ -27,6 +27,9 @@ extern "C" {
|
||||
* if the allocation fails. Idempotent. */
|
||||
void pgxp_set_enabled(int enabled);
|
||||
int pgxp_enabled(void);
|
||||
/* Armed and not inside a suppression bracket (speculative/replay pass):
|
||||
* whether the hooks record anything right now. */
|
||||
int pgxp_active(void);
|
||||
|
||||
/* Tier-2 propagation through CPU arithmetic (default off, like the reference
|
||||
* implementations' default). Off is SAFE — value validation already stops
|
||||
@@ -39,6 +42,110 @@ int pgxp_cpu_mode(void);
|
||||
void pgxp_set_tolerance(float pixels);
|
||||
float pgxp_tolerance(void);
|
||||
|
||||
/* Position-cache fallback tier (docs/ENHANCEMENTS.md G1.4/G1.11). On (the
|
||||
* default, unchanged behaviour): a vertex with no validated dataflow shadow
|
||||
* may take the sub-pixel fraction of the projection last cached at its integer
|
||||
* screen position. Off ("dataflow only"): such a vertex draws native, and
|
||||
* gte.cpp stops filling the cache. A title whose build carries the PGXP hooks
|
||||
* reaches near-total dataflow coverage, so for it the cache only adds wrong
|
||||
* fractions to CPU-built 2D polygons that happen to share a position with
|
||||
* some unrelated 3D vertex. Live-tunable. */
|
||||
void pgxp_set_position_fallback(int enabled);
|
||||
int pgxp_position_fallback(void);
|
||||
|
||||
/* Preserve projection precision (docs/ENHANCEMENTS.md G1.11). Off (the
|
||||
* default): RTPS/RTPT shadow the pre-truncation position the GTE itself
|
||||
* computes from the integer IR1/IR2 and its UNR divide, so the shadow always
|
||||
* truncates to the guest's SXY. On: they shadow the exact projection of the
|
||||
* vertex, from the unshifted MACs (12 more fractional bits than IR/SZ3) and a
|
||||
* true divide (pgxp_project_precise), which removes the residual wobble of
|
||||
* near geometry. Guest-visible SXY, MAC0 and FLAG are unchanged either way.
|
||||
* Because that position can then differ from the guest integer by more than
|
||||
* the fraction, truncation agreement becomes a window (PGXP_PPP_AGREE_*) and
|
||||
* a vertex at the GTE's saturation limit must still agree exactly. One
|
||||
* consumer is not visual: [widescreen] precise_nclip takes its branch sign
|
||||
* from these shadows, so in a title that sets both, that (already
|
||||
* non-faithful) widescreen sign follows the exact projection too. */
|
||||
void pgxp_set_preserve_projection(int enabled);
|
||||
int pgxp_preserve_projection(void);
|
||||
|
||||
/* Truncation-agreement window under preserve-projection, in whole pixels:
|
||||
* accept -BELOW < precise - native < ABOVE. For a value-validated shadow
|
||||
* whose packet half is neither at the GTE saturation limit nor wrapped by the
|
||||
* GPU's 11-bit parse (both of which need exact agreement), the guest
|
||||
* integer is the IR path's floor of the same projection, so the difference is
|
||||
* bounded by the IR path's own error: the dropped fraction (< 1 px), IR1/IR2
|
||||
* truncation times H/SZ3 (< 2 px without divide overflow), and SZ3's
|
||||
* truncation scaling the offset from the screen centre by up to
|
||||
* |offset|/SZ3. The window is a sanity bound over that, not a filter of
|
||||
* correct vertices: for R4 (H = 290) every on-screen vertex lies within it
|
||||
* (measured range -1.9 .. 2.6 px), and what it rejects is an extreme near
|
||||
* vertex far off-screen, which then draws where the hardware puts it. */
|
||||
#define PGXP_PPP_AGREE_BELOW 4
|
||||
#define PGXP_PPP_AGREE_ABOVE 5
|
||||
|
||||
/* The exact projection for preserve-projection mode, in the 16.16 screen
|
||||
* space of the IR path. Inputs are RTPS's unshifted 44-bit MAC1..3, the shift
|
||||
* (12 for sf=1), the IR1/IR2 the GTE stored, the SZ3 it pushed, H, OFX/OFY
|
||||
* and the horizontal widescreen factor x_num/x_den the IR path applied (1/1
|
||||
* when none). Returns 1 with the position in x16/y16 when the vertex
|
||||
* qualifies (sf=1, IR1/IR2 not saturated, SZ3 not saturated and nonzero, and
|
||||
* no divide overflow: H < 2*SZ3), else 0 and the caller keeps the IR path. */
|
||||
int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3, int shift,
|
||||
int32_t ir1, int32_t ir2, uint32_t sz3, uint32_t h,
|
||||
int32_t ofx, int32_t ofy, int64_t x_num, int64_t x_den,
|
||||
int32_t *x16, int32_t *y16);
|
||||
|
||||
/* Whether an exact projection may be the shadow of the SXY word `packed`:
|
||||
* the consumer's truncation agreement (the PGXP_PPP_AGREE_* window, exact at
|
||||
* the saturation limits) on both halves. When it fails, RTPS keeps the IR
|
||||
* path's shadow, which always agrees, so the vertex still draws precise
|
||||
* instead of being rejected at the GPU. Counts ppp_produced on accept and
|
||||
* ppp_window_fallback on reject. */
|
||||
int pgxp_ppp_accept(int32_t x16, int32_t y16, uint32_t packed);
|
||||
|
||||
/* Precise culling (docs/ENHANCEMENTS.md G1.12). Off (the default): NCLIP is
|
||||
* exactly the hardware's. On, while geometry correction is armed: when the
|
||||
* three SXY FIFO shadows are live, describe the current register words and
|
||||
* pass the same acceptance the GPU applies when it draws them, and the sign
|
||||
* of their exact determinant differs from the integer one, NCLIP's MAC0
|
||||
* takes the exact sign (magnitude: the exact doubled area rounded, at least
|
||||
* 1). A game that culls on that sign then keeps the sub-pixel faces PGXP
|
||||
* draws with positive area -- the far road rows a native zero-area test
|
||||
* drops, which open gaps between their precisely placed neighbours -- and
|
||||
* drops the slivers whose drawn winding is reversed. This CHANGES
|
||||
* guest-visible MAC0 and so guest control flow: only the mod arms it
|
||||
* (netplay clears the mod), and gte.cpp holds it off in every pass that is
|
||||
* compared against another execution (overlay shadow diff, speculative and
|
||||
* replay passes). Live-tunable over TCP. */
|
||||
void pgxp_set_culling(int enabled);
|
||||
int pgxp_culling(void);
|
||||
|
||||
/* NCLIP's exact determinant from the SXY0..2 register shadows, in 2^-32
|
||||
* px^2 units (16.16 times 16.16). Returns 1 when all three shadows are live,
|
||||
* carry the register words in sxy[] and pass the GPU consumer's truncation
|
||||
* agreement and tolerance clamp; else 0. Counts nclip_precise. */
|
||||
int pgxp_gte_nclip_precise(const uint32_t sxy[3], int64_t *cross);
|
||||
|
||||
/* Mod-owned request (the framework's psx.enhancement.pgxp package).
|
||||
*
|
||||
* The mod's activation runs at session start, before main.cpp's renderer
|
||||
* setup, and that setup applies the [video] baseline (geometry_correction,
|
||||
* perspective_texturing, pgxp_cpu_mode). An activation that armed the
|
||||
* corrections directly was switched straight back off there. So the
|
||||
* activation only records a request, and the session arming
|
||||
* (psx_pgxp_session_arm, pgxp_session.h) takes it -- reads and clears it --
|
||||
* and combines it with the baseline. A request therefore lives only from one
|
||||
* session's activation to that session's arming: a later session whose plan
|
||||
* is empty (netplay, or the mod disabled) cannot inherit it.
|
||||
* reset_mod_owned_presentation() also clears it at every session start. */
|
||||
void pgxp_mod_request(int enabled, int cpu_mode, int culling);
|
||||
/* Returns nonzero when this session's plan asked for PGXP; *cpu_mode and
|
||||
* *culling (either may be NULL) receive the mod's options. */
|
||||
int pgxp_mod_requested(int *cpu_mode, int *culling);
|
||||
/* pgxp_mod_requested, then clears the request. */
|
||||
int pgxp_mod_request_take(int *cpu_mode, int *culling);
|
||||
|
||||
/* Drop all shadows (savestate load, raw RAM restore, timeline breaks).
|
||||
* O(1) via generation bump. Deferred while suppressed. */
|
||||
void pgxp_invalidate_all(void);
|
||||
@@ -85,11 +192,18 @@ enum {
|
||||
* On DATAFLOW/FALLBACK, *x16/*y16 hold the sub-pixel position (16.16, same
|
||||
* coordinate space as the packet halves); *sz is the projected depth or 0.
|
||||
* Safeguards applied here: the integer part must match the native parse
|
||||
* (truncation agreement) and the tolerance clamp. */
|
||||
* (truncation agreement; a bounded window under preserve-projection) and the
|
||||
* tolerance clamp. The fallback tier is skipped when position fallback is
|
||||
* off. */
|
||||
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);
|
||||
|
||||
/* Same decision as pgxp_get_precise_vertex, without counting it in the stats
|
||||
* (the caller looks the vertex up again when it draws). */
|
||||
int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y);
|
||||
|
||||
/* --- observability -------------------------------------------------------- */
|
||||
|
||||
typedef struct PGXPStats {
|
||||
@@ -113,9 +227,44 @@ typedef struct PGXPStats {
|
||||
uint64_t word_mismatch;
|
||||
uint64_t word_partial;
|
||||
uint64_t word_no_z;
|
||||
uint64_t ppp_produced; /* of those, shadowed with the exact projection */
|
||||
uint64_t ppp_window_fallback;/* exact projection outside the agreement
|
||||
* window: shadowed with the IR path instead */
|
||||
/* Geometry-corrected triangles by how many of their 3 vertices came out
|
||||
* precise (gpu.c prepare_precise_triangle). `mixed` is the mesh-cracking
|
||||
* exposure of docs/ENHANCEMENTS.md G1.1: an edge between a precise and a
|
||||
* native vertex can disagree with its neighbour by the dropped fraction. */
|
||||
uint64_t tri_precise; /* all three precise */
|
||||
uint64_t tri_mixed; /* one or two precise */
|
||||
uint64_t tri_native; /* none precise */
|
||||
/* Textured quads that are axis-aligned rectangles in integer screen space
|
||||
* (and in UV) whose four vertices are all dataflow-precise, so they were
|
||||
* drawn as two precise triangles instead of the native 2D rectangle
|
||||
* shortcut; rect_partial: such rectangles with 1..3 precise vertices,
|
||||
* which keep the shortcut (drawing them as triangles would mix). */
|
||||
uint64_t rect_bypass;
|
||||
uint64_t rect_partial;
|
||||
/* NCLIPs while geometry correction is armed (docs/ENHANCEMENTS.md G1.12):
|
||||
* nclip_precise had an exact determinant; nclip_disagree: its sign (non-
|
||||
* zero) differs from the integer MAC0's, so the game's cull decision on
|
||||
* that face disagrees with the face PGXP draws -- the crack exposure that
|
||||
* tri_* cannot see, because a culled face draws no triangle at all;
|
||||
* nclip_corrected: of those, MAC0 replaced by precise culling. */
|
||||
uint64_t nclip_precise;
|
||||
uint64_t nclip_disagree;
|
||||
uint64_t nclip_corrected;
|
||||
} PGXPStats;
|
||||
|
||||
void pgxp_get_stats(PGXPStats *out);
|
||||
/* Count one geometry-corrected triangle with `precise` (0..3) precise
|
||||
* vertices. */
|
||||
void pgxp_note_triangle(int precise);
|
||||
/* One rectangle-shortcut quad sent down the triangle path (all_precise) or
|
||||
* kept on the shortcut with only some precise vertices. */
|
||||
void pgxp_note_rect_bypass(int all_precise);
|
||||
/* One NCLIP whose exact sign disagreed with MAC0, and whether MAC0 was
|
||||
* corrected. */
|
||||
void pgxp_note_nclip(int disagree, int corrected);
|
||||
|
||||
/* --- gte.cpp forwarding surface (v14 ABI compat) -------------------------- */
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#ifndef PSXRECOMP_PGXP_SESSION_H
|
||||
#define PSXRECOMP_PGXP_SESSION_H
|
||||
|
||||
/*
|
||||
* Which PGXP corrections a session arms (docs/ENHANCEMENTS.md G1.11/G1.12).
|
||||
*
|
||||
* main.cpp drives the whole lifecycle through the two functions at the end of
|
||||
* this header, compiled from src/pgxp_session.cpp, which
|
||||
* runtime/tests/test_pgxp_session.cpp links and drives the same way:
|
||||
*
|
||||
* reset_mod_owned_presentation() -> psx_pgxp_session_reset()
|
||||
* mod_runtime_activate_plugins() -> psx.enhancement.pgxp's activation
|
||||
* records pgxp_mod_request(...)
|
||||
* renderer setup (session_reboot) -> psx_pgxp_session_arm(&config, ...)
|
||||
*
|
||||
* The arm TAKES the mod request (reads and clears it), so a request lives only
|
||||
* from one session's activation to that session's arming. A later session
|
||||
* with an empty plan -- netplay clears it, or the player disabled the mod --
|
||||
* cannot inherit it, even if the reset were skipped.
|
||||
*
|
||||
* Inputs, in order of precedence:
|
||||
* 1. env_*: the validation overrides PSX_GEOMETRY_CORRECTION,
|
||||
* PSX_PERSPECTIVE_TEXTURING, PSX_PGXP_CPU_MODE and PSX_PGXP_CULLING (-1
|
||||
* when unset, else 0 or 1). They win over everything, so an A/B run can
|
||||
* switch PGXP off under an enabled mod.
|
||||
* 2. video_* OR mod_*: the [video] baseline (game.toml, then settings.toml
|
||||
* and the launcher) and the psx.enhancement.pgxp request this session's
|
||||
* activation recorded. Either one arms a correction. With mod_only
|
||||
* ([video] pgxp_mod_only, a title that ships PGXP through the mod) the
|
||||
* baseline is ignored and the mod is the one switch.
|
||||
*
|
||||
* The mod arms both geometry and texture correction; its CPU-mode option adds
|
||||
* tier-2 propagation and its culling option precise culling. Precise culling
|
||||
* changes guest-visible NCLIP results, so it has no [video] key (netplay does
|
||||
* not clear [video]); it needs geometry correction.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct PSXPgxpSessionInputs {
|
||||
int video_geometry;
|
||||
int video_texture;
|
||||
int video_cpu_mode;
|
||||
int env_geometry;
|
||||
int env_texture;
|
||||
int env_cpu_mode;
|
||||
int env_culling;
|
||||
int mod_enabled;
|
||||
int mod_cpu_mode;
|
||||
int mod_culling;
|
||||
int mod_only;
|
||||
} PSXPgxpSessionInputs;
|
||||
|
||||
typedef struct PSXPgxpSessionArm {
|
||||
int geometry;
|
||||
int texture;
|
||||
int cpu_mode;
|
||||
int culling;
|
||||
} PSXPgxpSessionArm;
|
||||
|
||||
static inline int psx_pgxp_session_pick(int env, int baseline, int mod) {
|
||||
if (env >= 0) return env ? 1 : 0;
|
||||
return (baseline || mod) ? 1 : 0;
|
||||
}
|
||||
|
||||
static inline PSXPgxpSessionArm psx_pgxp_session_resolve(
|
||||
const PSXPgxpSessionInputs* in) {
|
||||
PSXPgxpSessionArm arm;
|
||||
const int use_video = in->mod_only ? 0 : 1;
|
||||
arm.geometry = psx_pgxp_session_pick(in->env_geometry,
|
||||
use_video && in->video_geometry,
|
||||
in->mod_enabled);
|
||||
arm.texture = psx_pgxp_session_pick(in->env_texture,
|
||||
use_video && in->video_texture,
|
||||
in->mod_enabled);
|
||||
arm.cpu_mode = psx_pgxp_session_pick(in->env_cpu_mode,
|
||||
use_video && in->video_cpu_mode,
|
||||
in->mod_enabled && in->mod_cpu_mode);
|
||||
arm.culling = arm.geometry &&
|
||||
psx_pgxp_session_pick(in->env_culling, 0,
|
||||
in->mod_enabled && in->mod_culling);
|
||||
return arm;
|
||||
}
|
||||
|
||||
/* Parse one of the env overrides: unset -> -1, "0" or empty -> 0, else 1
|
||||
* (the rule the runtime has always used for them). */
|
||||
static inline int psx_pgxp_session_env_flag(const char* value) {
|
||||
if (!value) return -1;
|
||||
return (*value && *value != '0') ? 1 : 0;
|
||||
}
|
||||
|
||||
/* The [video] PGXP settings main.cpp hands the arming: the player's baseline
|
||||
* (game.toml, then settings.toml and the launcher) and the title's tuning
|
||||
* keys. */
|
||||
typedef struct PSXPgxpSessionConfig {
|
||||
int video_geometry;
|
||||
int video_texture;
|
||||
int video_cpu_mode;
|
||||
float tolerance;
|
||||
int position_fallback;
|
||||
int preserve_projection;
|
||||
int mod_only;
|
||||
} PSXPgxpSessionConfig;
|
||||
|
||||
/* Session start, before plugin activation: forget any mod request. */
|
||||
void psx_pgxp_session_reset(void);
|
||||
|
||||
/* The renderer setup's arming, once per session after activation: reads the
|
||||
* env overrides, takes the mod request, resolves, and applies the result to
|
||||
* the engine (gte_geometry_correction_set, gpu_texture_correction_set,
|
||||
* pgxp_set_cpu_mode / _culling) along with the tuning keys. `used` (may be
|
||||
* NULL) receives the inputs it resolved from. */
|
||||
PSXPgxpSessionArm psx_pgxp_session_arm(const PSXPgxpSessionConfig* config,
|
||||
PSXPgxpSessionInputs* used);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -1,14 +1,14 @@
|
||||
#ifndef PSX_WS_CULL_EDGE_H
|
||||
#define PSX_WS_CULL_EDGE_H
|
||||
/* ============================================================================
|
||||
* Exact screen-X edge widening for two explicit [widescreen.cull] site kinds
|
||||
* Exact screen-X edge widening for explicit [widescreen.cull] site kinds
|
||||
*
|
||||
* Self-contained (depends only on <stdint.h>) so one implementation is shared
|
||||
* by the runtime helpers in gpu.c (native code and the dirty-RAM interpreter
|
||||
* both call those) and by the recompiler's unit test.
|
||||
*
|
||||
* `m` is the live per-side reveal, psx_ws_x_margin(): zero at 4:3, so both
|
||||
* functions are the identity there and faithful builds are unchanged.
|
||||
* `m` is the live per-side reveal, psx_ws_x_margin(): zero at 4:3, so every
|
||||
* function is the identity there and faithful builds are unchanged.
|
||||
*
|
||||
* bgez_sites -- `bgez SX, keep` in a signed per-vertex left-edge chain:
|
||||
*
|
||||
@@ -33,6 +33,20 @@
|
||||
* edge (a rear-view mirror, one half of a split screen) and stays unchanged,
|
||||
* so interior viewports keep their vanilla culls while full-width viewports
|
||||
* widen on each side that touches the display edge.
|
||||
*
|
||||
* screen_x_sites on `lui` and `bltz` -- a screen X kept in the high half of
|
||||
* a register (`SX << 16`), as a renderer does when it tests the sign of
|
||||
* differences and masks whole points at once:
|
||||
*
|
||||
* lui edge, W ; right screen edge as W << 16
|
||||
* sll x, sx, 16
|
||||
* sub t, x, edge ; t >= 0: the point is right of the screen
|
||||
* ...
|
||||
* bltz x, outside ; the point is left of the screen
|
||||
*
|
||||
* The `lui` moves to (W + m) << 16 and the `bltz` is taken only while
|
||||
* SX < -m. bltz_sites does not fit this `bltz`: it compares the whole
|
||||
* register with -m, and every negative `SX << 16` is far below that.
|
||||
* ========================================================================== */
|
||||
#include <stdint.h>
|
||||
|
||||
@@ -58,6 +72,26 @@ static inline uint32_t psx_ws_clip_edge_x_value(uint32_t v, uint32_t w,
|
||||
return v;
|
||||
}
|
||||
|
||||
/* Widened value of `lui rt, imm` when imm is a screen-X edge in pixels: a
|
||||
* right edge (imm > 0) moves right by m, a left edge (imm < 0) moves left by
|
||||
* m. `imm` is the 16-bit immediate as encoded. */
|
||||
static inline uint32_t psx_ws_cull_lui_hi_value(uint32_t imm, int32_t m)
|
||||
{
|
||||
int32_t edge = (int32_t)(int16_t)(uint16_t)imm;
|
||||
if (m > 0) {
|
||||
if (edge > 0) edge += m;
|
||||
else if (edge < 0) edge -= m;
|
||||
}
|
||||
return (uint32_t)edge << 16;
|
||||
}
|
||||
|
||||
/* bltz predicate (1 = branch taken = outside) for a screen X in the high
|
||||
* half of `v`: taken while SX < -m. */
|
||||
static inline int psx_ws_cull_bltz_hi_value(uint32_t v, int32_t m)
|
||||
{
|
||||
return (int32_t)v < -((m > 0 ? m : 0) * 65536);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
TinyCC (Tiny C Compiler) 0.9.27
|
||||
Copyright (C) 2001-2004 Fabrice Bellard and the TinyCC contributors.
|
||||
|
||||
Release packages for Windows bundle the unmodified upstream TinyCC 0.9.27
|
||||
win64 binary distribution under overlay_toolchain/tcc/ (tcc.exe, libtcc.dll,
|
||||
its headers and runtime library). It runs as a separate program that compiles
|
||||
overlay shards on the player's machine; the game does not link against it.
|
||||
|
||||
TinyCC is licensed under the GNU Lesser General Public License, version 2.1,
|
||||
reproduced below. Source code for this exact version:
|
||||
https://download.savannah.gnu.org/releases/tinycc/tcc-0.9.27.tar.bz2
|
||||
Binary distribution shipped (SHA-256 34a721949a2583fdff725312da092fa0f5f1f284b702e6f811c6954714faabb2):
|
||||
https://download.savannah.gnu.org/releases/tinycc/tcc-0.9.27-win64-bin.zip
|
||||
Project home: https://bellard.org/tcc/
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Moe Ghoul>, 1 April 1990
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
+14
-1
@@ -372,6 +372,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/host_sampler.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/debug_trace_ranges.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/dirty_ram_interp.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/draw_distance.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/game_dispatch_compat.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_segment_miss.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/fntrace.c
|
||||
@@ -400,6 +401,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/freeze_heartbeat.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/gte.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/pgxp.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/pgxp_session.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/nd_intro_ot.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/crc32.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_sha256.c
|
||||
@@ -2464,12 +2466,20 @@ endfunction()
|
||||
# ENABLE_SETUP_WIZARD
|
||||
# PRELOADED_MODS_DIR "${CMAKE_CURRENT_SOURCE_DIR}/mods/preloaded"
|
||||
# EXCLUDE_BUILTIN_MODS psx.presentation.bezel # optional; builtins not shipped
|
||||
# PGXP # optional; the runtime is the PGXP hook flavor
|
||||
# )
|
||||
#
|
||||
# Remaining args are forwarded to psxrecomp_add_runtime_target.
|
||||
# ---------------------------------------------------------------------------
|
||||
function(psxrecomp_add_game_runtime target)
|
||||
set(options ENABLE_NETPLAY_IF_PRESENT ENABLE_SETUP_WIZARD)
|
||||
# PGXP: build the title's ONE runtime as the PGXP hook flavor (see the PGXP
|
||||
# option of psxrecomp_add_runtime_target): -DPSX_PGXP=1, overlay flavor 2,
|
||||
# no _pgxp suffix and no clone beside it. The generated C already carries
|
||||
# the PGXP_*() hook sites, so committed generated/ does not change; only
|
||||
# the compile define does. Parsed here rather than left to the unparsed
|
||||
# tail so it cannot be swallowed by a multi-value argument written before
|
||||
# it (CODEGEN_SETUP_SOURCES would take it as a source file).
|
||||
set(options ENABLE_NETPLAY_IF_PRESENT ENABLE_SETUP_WIZARD PGXP)
|
||||
set(oneValueArgs
|
||||
GEN_MARKER
|
||||
GEN_FULL_FALLBACK
|
||||
@@ -2662,6 +2672,9 @@ function(psxrecomp_add_game_runtime target)
|
||||
endif()
|
||||
|
||||
set(_psxg_forwarded_args ${PSXG_UNPARSED_ARGUMENTS})
|
||||
if(PSXG_PGXP)
|
||||
list(APPEND _psxg_forwarded_args PGXP)
|
||||
endif()
|
||||
if(NOT "${PSXG_PRELOADED_MODS_DIR}" STREQUAL "")
|
||||
list(APPEND _psxg_forwarded_args
|
||||
PRELOADED_MODS_DIR "${PSXG_PRELOADED_MODS_DIR}")
|
||||
|
||||
@@ -5284,21 +5284,29 @@ static void handle_geom_correction(int id, const char *json)
|
||||
uint64_t tc_att = 0, tc_arm = 0, tc_off = 0, tc_nosrc = 0, tc_noz = 0;
|
||||
gpu_texture_correction_stats(&tc_att, &tc_arm, &tc_off, &tc_nosrc, &tc_noz);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,"
|
||||
"\"geometry_correction\":%d,"
|
||||
"\"geometry_correction\":%d,\"texture_correction\":%d,"
|
||||
"\"geometry_vertex_hits\":%u,"
|
||||
"\"perspective_triangles\":%u,"
|
||||
"\"texcorr\":{\"attempts\":%llu,\"armed\":%llu,"
|
||||
"\"no_correction\":%llu,\"no_source\":%llu,\"no_depth\":%llu},"
|
||||
"\"lookups\":%u,\"miss_unrecorded\":%u,\"miss_ambiguous\":%u,"
|
||||
"\"pgxp\":{\"enabled\":%d,\"cpu_mode\":%d,\"tolerance\":%.3f,"
|
||||
"\"position_fallback\":%d,\"preserve_projection\":%d,"
|
||||
"\"culling\":%d,"
|
||||
"\"lookups\":%llu,\"dataflow_hit\":%llu,\"fallback_hit\":%llu,"
|
||||
"\"native\":%llu,\"value_mismatch\":%llu,\"trunc_reject\":%llu,"
|
||||
"\"tolerance_reject\":%llu,\"w_valid\":%llu,"
|
||||
"\"produced\":%llu,\"swc2_stores\":%llu,"
|
||||
"\"produced\":%llu,\"swc2_stores\":%llu,\"ppp_produced\":%llu,"
|
||||
"\"ppp_window_fallback\":%llu,"
|
||||
"\"tri_precise\":%llu,\"tri_mixed\":%llu,\"tri_native\":%llu,"
|
||||
"\"rect_bypass\":%llu,\"rect_partial\":%llu,"
|
||||
"\"nclip_precise\":%llu,\"nclip_disagree\":%llu,"
|
||||
"\"nclip_corrected\":%llu,"
|
||||
"\"word\":{\"lookups\":%llu,\"hit\":%llu,\"untracked\":%llu,"
|
||||
"\"mismatch\":%llu,\"partial\":%llu,\"no_z\":%llu}}}",
|
||||
id,
|
||||
gte_geometry_correction_enabled(),
|
||||
gpu_texture_correction_enabled(),
|
||||
(unsigned)hits,
|
||||
(unsigned)gpu_texture_correction_hits(),
|
||||
(unsigned long long)tc_att, (unsigned long long)tc_arm,
|
||||
@@ -5306,6 +5314,8 @@ static void handle_geom_correction(int id, const char *json)
|
||||
(unsigned long long)tc_noz,
|
||||
(unsigned)lookups, (unsigned)unrec, (unsigned)ambig,
|
||||
pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(),
|
||||
pgxp_position_fallback(), pgxp_preserve_projection(),
|
||||
pgxp_culling(),
|
||||
(unsigned long long)ps.lookups,
|
||||
(unsigned long long)ps.dataflow_hit,
|
||||
(unsigned long long)ps.fallback_hit,
|
||||
@@ -5316,6 +5326,16 @@ static void handle_geom_correction(int id, const char *json)
|
||||
(unsigned long long)ps.w_valid,
|
||||
(unsigned long long)ps.produced,
|
||||
(unsigned long long)ps.swc2_stores,
|
||||
(unsigned long long)ps.ppp_produced,
|
||||
(unsigned long long)ps.ppp_window_fallback,
|
||||
(unsigned long long)ps.tri_precise,
|
||||
(unsigned long long)ps.tri_mixed,
|
||||
(unsigned long long)ps.tri_native,
|
||||
(unsigned long long)ps.rect_bypass,
|
||||
(unsigned long long)ps.rect_partial,
|
||||
(unsigned long long)ps.nclip_precise,
|
||||
(unsigned long long)ps.nclip_disagree,
|
||||
(unsigned long long)ps.nclip_corrected,
|
||||
(unsigned long long)ps.word_lookups,
|
||||
(unsigned long long)ps.word_hit,
|
||||
(unsigned long long)ps.word_untracked,
|
||||
@@ -5455,8 +5475,9 @@ static void handle_pgxp_miss_ring(int id, const char *json)
|
||||
}
|
||||
|
||||
/* pgxp — live-tune the value-propagation engine for one-toggle isolation runs
|
||||
* without a rebuild: {"cmd":"pgxp","cpu_mode":0|1,"tolerance":F}. Fields are
|
||||
* optional; the reply echoes the resulting state (same shape as
|
||||
* without a rebuild: {"cmd":"pgxp","cpu_mode":0|1,"tolerance":F,
|
||||
* "position_fallback":0|1,"preserve_projection":0|1,"culling":0|1}. Fields
|
||||
* are optional; the reply echoes the resulting state (same shape as
|
||||
* geom_correction's "pgxp" object, flattened). */
|
||||
static void handle_pgxp(int id, const char *json)
|
||||
{
|
||||
@@ -5475,6 +5496,15 @@ static void handle_pgxp(int id, const char *json)
|
||||
int cm = json_get_int(json, "cpu_mode", -1);
|
||||
if (cm >= 0)
|
||||
pgxp_set_cpu_mode(cm != 0);
|
||||
int pf = json_get_int(json, "position_fallback", -1);
|
||||
if (pf >= 0)
|
||||
pgxp_set_position_fallback(pf != 0);
|
||||
int pp = json_get_int(json, "preserve_projection", -1);
|
||||
if (pp >= 0)
|
||||
pgxp_set_preserve_projection(pp != 0);
|
||||
int cu = json_get_int(json, "culling", -1);
|
||||
if (cu >= 0)
|
||||
pgxp_set_culling(cu != 0);
|
||||
/* tolerance is fractional (sub-pixel), so scan it directly — json_get_int
|
||||
* would truncate 0.5 to 0. */
|
||||
const char *p = strstr(json, "\"tolerance\"");
|
||||
@@ -5485,8 +5515,12 @@ static void handle_pgxp(int id, const char *json)
|
||||
pgxp_set_tolerance((float)strtod(p, NULL));
|
||||
}
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"cpu_mode\":%d,"
|
||||
"\"tolerance\":%.3f,\"suppress\":%u,\"active\":%d}",
|
||||
"\"tolerance\":%.3f,\"position_fallback\":%d,"
|
||||
"\"preserve_projection\":%d,\"culling\":%d,"
|
||||
"\"suppress\":%u,\"active\":%d}",
|
||||
id, pgxp_enabled(), pgxp_cpu_mode(), (double)pgxp_tolerance(),
|
||||
pgxp_position_fallback(), pgxp_preserve_projection(),
|
||||
pgxp_culling(),
|
||||
(unsigned)pgxp_test_suppress_depth(), pgxp_test_active());
|
||||
}
|
||||
|
||||
@@ -8282,6 +8316,20 @@ static void handle_gl_wide_fast(int id, const char *json)
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d}", id, gl_renderer_get_wide_fast());
|
||||
}
|
||||
|
||||
/* gl_texwin_batch on=<0|1>: [video] texture_window_batching, live. 1 = textured
|
||||
* prims with different GP0(E2h) texture windows share a batch; 0 = a window
|
||||
* change ends the batch. The image is the same; for A/B of batch counts
|
||||
* (frame_perf batch_diag[6] = window flushes) and frame time. */
|
||||
extern void gl_renderer_set_texture_window_batching(int on);
|
||||
extern int gl_renderer_get_texture_window_batching(void);
|
||||
static void handle_gl_texwin_batch(int id, const char *json)
|
||||
{
|
||||
int on = json_get_int(json, "on", -1);
|
||||
if (on >= 0) gl_renderer_set_texture_window_batching(on);
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"on\":%d}", id,
|
||||
gl_renderer_get_texture_window_batching());
|
||||
}
|
||||
|
||||
/* Live GTE widescreen-squash toggle (diagnostic for 8C far-backdrop void):
|
||||
* ws_aspect num=<n> den=<d> calls gte_set_display_aspect at runtime so we can
|
||||
* compare squash ON (e.g. 16/9) vs OFF (1/1) in-place without a relaunch. */
|
||||
@@ -14562,6 +14610,7 @@ static const CmdEntry s_commands[] = {
|
||||
{ "render_pass_dump", handle_render_pass_dump },
|
||||
{ "render_pass_refuse", handle_render_pass_refuse },
|
||||
{ "gl_wide_fast", handle_gl_wide_fast },
|
||||
{ "gl_texwin_batch", handle_gl_texwin_batch },
|
||||
{ "synth_recurse", handle_synth_recurse },
|
||||
{ "gl_fbo_peek", handle_gl_fbo_peek },
|
||||
{ "gl_vram_diff", handle_gl_vram_diff },
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "starvation_ring.h"
|
||||
#include "fntrace.h" /* fntrace_is_game_started / fntrace_mark_game_started */
|
||||
#include "psx_segment_miss.h" /* segment misses in static game code (§5.5) */
|
||||
#include "draw_distance.h" /* [[draw_distance.clamp]] sites */
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
@@ -523,6 +524,19 @@ static inline int span_forced(uint32_t phys) {
|
||||
return phys >= s_span_lo && phys < s_span_hi;
|
||||
}
|
||||
|
||||
/* Game text that dispatch refuses because its live bytes differ from the boot
|
||||
* EXE. Dispatch routes such a PC straight back to the interpreter, so
|
||||
* straight-line interpretation keeps going instead of handing it back (see
|
||||
* the hand-back in dirty_ram_dispatch_inner). */
|
||||
static int interp_refused_game_text(uint32_t pc) {
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
return psx_game_address_in_text(pc) && !psx_game_text_native_ok(pc);
|
||||
#else
|
||||
(void)pc;
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Forward decls from memory.c — used to read instruction bytes. */
|
||||
extern uint8_t *memory_get_ram_ptr(void);
|
||||
extern void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len);
|
||||
@@ -1529,6 +1543,22 @@ static int exec_one_fetched(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
(ld_rt != 0u);
|
||||
const uint32_t ld_before = is_ld ? cpu->gpr[ld_rt] : 0u;
|
||||
|
||||
/* [[draw_distance.clamp]] (draw_distance.h): while a mod has the clamps
|
||||
* on, a listed main-EXE site clamps its register before it runs, exactly
|
||||
* as the generated code does. Captured overlay code keeps its own code,
|
||||
* so only the game's text image qualifies. One load when off. */
|
||||
#ifdef PSX_HAS_GAME_DISPATCH
|
||||
if (g_psx_draw_distance_clamp_live) {
|
||||
const PSXDrawDistanceClampSite *dd =
|
||||
psx_draw_distance_clamp_find(pc, insn);
|
||||
if (dd && psx_game_address_in_text(pc)) {
|
||||
cpu->gpr[dd->reg] =
|
||||
psx_draw_distance_clamp_value(cpu->gpr[dd->reg], dd->max);
|
||||
cpu->gpr[0] = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const int rv = exec_one_fetched_inner(cpu, pc, insn, next_pc_out);
|
||||
|
||||
if (is_ld) {
|
||||
@@ -1566,6 +1596,17 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
|
||||
*next_pc_out = pc + 4;
|
||||
|
||||
/* MFC0/CFC0 observe COP0 as it stood when the instruction began. The fetch
|
||||
* and cycle charges below can reach a device deadline and run its event,
|
||||
* which may raise CAUSE.IP2 in the middle of this instruction. An IRQ
|
||||
* becomes visible at the next instruction boundary. Compiled code defers
|
||||
* its base charge to the block boundary (GCC/Clang builds), so it does
|
||||
* not show this for the base charge. Example: the kernel's syscall
|
||||
* handler (`mfc0 a1,Cause`) saved 0x420 instead of 0x20 when a CD IRQ
|
||||
* fell due on its own cycle. */
|
||||
const uint32_t cop0_read = (opc == 0x10u && (rs == 0u || rs == 2u))
|
||||
? cpu->cop0[rd] : 0u;
|
||||
|
||||
#ifdef PSX_ENABLE_BLOCK_CYCLES
|
||||
/* Instruction FETCH cost (I-cache) — charged FIRST, before the §1 base, exactly
|
||||
* like Beetle ReadInstruction precedes the per-instruction base (cpu.cpp). HIT=+0,
|
||||
@@ -2190,7 +2231,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->ld_absorb = 0u;
|
||||
cpu->ld_which_t = (uint8_t)rt;
|
||||
#endif
|
||||
cpu->gpr[rt] = cpu->cop0[rd];
|
||||
cpu->gpr[rt] = cop0_read;
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -2199,7 +2240,7 @@ static int exec_one_fetched_inner(CPUState *cpu, uint32_t pc, uint32_t insn,
|
||||
cpu->ld_absorb = 0u;
|
||||
cpu->ld_which_t = (uint8_t)rt;
|
||||
#endif
|
||||
cpu->gpr[rt] = cpu->cop0[rd];
|
||||
cpu->gpr[rt] = cop0_read;
|
||||
cpu->gpr[0] = 0;
|
||||
return 0;
|
||||
}
|
||||
@@ -3564,6 +3605,17 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
uint32_t next_page = next_phys >> 12;
|
||||
if ((!current_page_dirty || next_page != current_page) &&
|
||||
!dirty_ram_is_dirty(next_phys)) {
|
||||
/* Clean game text whose live bytes differ from the boot EXE (for
|
||||
* example a BIOS shell that runs interpreted in RAM the game's
|
||||
* EXE later owns): dispatch refuses it and re-enters the
|
||||
* interpreter one instruction later. Handing back here cost a
|
||||
* full dispatch round trip per instruction. Keep interpreting and
|
||||
* re-test every instruction until dispatch can take the flow. */
|
||||
if (interp_refused_game_text(pc)) {
|
||||
current_page_dirty = 0;
|
||||
current_page = next_page;
|
||||
continue;
|
||||
}
|
||||
cpu->pc = pc;
|
||||
if (dirty_ram_pump_boundary(cpu, pc, 3)) {
|
||||
g_dirty_ram_blocks_run++;
|
||||
|
||||
@@ -767,6 +767,25 @@ static void start_async_gpu_linked_list(void) {
|
||||
gpu_ot_polls_this_walk = 0;
|
||||
}
|
||||
|
||||
/* PSX-SPX "CPU Operation during DMA": DMA outruns CPU memory access, and a CPU
|
||||
* read of RAM or I/O waits while DMA moves data. So the kicking store of a GPU
|
||||
* linked-list transfer returns only when the walk ends: guest code that
|
||||
* rewrites a packet right after DrawOTag (the retail SCPH1001 intro re-copies
|
||||
* its text packets from a template, clearing the link) cannot reach it before
|
||||
* the walk does. A cyclic list still ends at the walker's node limit. The hold
|
||||
* also ends if an advance moves no list word. */
|
||||
static void hold_cpu_for_gpu_linked_list(void) {
|
||||
if (psx_in_device_service || g_ls_replay_active || g_psx_render_pass_active)
|
||||
return;
|
||||
while (gpu_linked_list.active && ((channels[2].chcr >> 24) & 1u) &&
|
||||
channel_enabled(2)) {
|
||||
const uint32_t moved = gpu_linked_list.total_words;
|
||||
psx_advance_cycles(dma_gpu_ll_cycles_to_event(&gpu_linked_list));
|
||||
psx_devices_service_to_now();
|
||||
if (gpu_linked_list.active && gpu_linked_list.total_words == moved) break;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t execute_ch2_gpu(void) {
|
||||
uint32_t chcr = channels[2].chcr;
|
||||
uint32_t direction = chcr & 1; /* 0=to RAM, 1=from RAM (to device) */
|
||||
@@ -982,6 +1001,7 @@ static void try_execute(int ch) {
|
||||
while (g_psx_render_pass_active && gpu_linked_list.active)
|
||||
dma_gpu_ll_advance(&gpu_linked_list, UINT32_MAX,
|
||||
&gpu_ll_ops, NULL);
|
||||
hold_cpu_for_gpu_linked_list();
|
||||
} else {
|
||||
schedule_delayed_complete(2, execute_ch2_gpu(),
|
||||
DMA_GPU_CYCLES_PER_WORD);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/* draw_distance.c - the [[draw_distance.clamp]] site store and switch
|
||||
* (draw_distance.h). */
|
||||
#include "draw_distance.h"
|
||||
#include "mod_plugins.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
uint32_t g_psx_draw_distance_clamp = 0;
|
||||
int g_psx_draw_distance_clamp_live = 0;
|
||||
|
||||
static PSXDrawDistanceClampSite s_sites[PSX_DRAW_DISTANCE_CLAMP_SITES_MAX];
|
||||
static int s_site_count = 0;
|
||||
|
||||
static void update_live(void) {
|
||||
g_psx_draw_distance_clamp_live =
|
||||
(g_psx_draw_distance_clamp != 0u && s_site_count > 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
static int site_cmp(const void *a, const void *b) {
|
||||
const uint32_t x = ((const PSXDrawDistanceClampSite *)a)->address;
|
||||
const uint32_t y = ((const PSXDrawDistanceClampSite *)b)->address;
|
||||
return (x > y) - (x < y);
|
||||
}
|
||||
|
||||
int psx_draw_distance_set_clamp_sites(const PSXDrawDistanceClampSite *sites,
|
||||
int count) {
|
||||
if (!sites || count < 0) count = 0;
|
||||
if (count > PSX_DRAW_DISTANCE_CLAMP_SITES_MAX) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: [[draw_distance.clamp]] lists %d sites; keeping "
|
||||
"the first %d\n",
|
||||
count, PSX_DRAW_DISTANCE_CLAMP_SITES_MAX);
|
||||
count = PSX_DRAW_DISTANCE_CLAMP_SITES_MAX;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
s_sites[i] = sites[i];
|
||||
s_sites[i].address &= 0x1FFFFFFFu;
|
||||
}
|
||||
if (count > 1) qsort(s_sites, (size_t)count, sizeof s_sites[0], site_cmp);
|
||||
s_site_count = count;
|
||||
update_live();
|
||||
return count;
|
||||
}
|
||||
|
||||
int psx_draw_distance_clamp_site_count(void) { return s_site_count; }
|
||||
|
||||
const PSXDrawDistanceClampSite *psx_draw_distance_clamp_find(uint32_t pc,
|
||||
uint32_t insn) {
|
||||
const uint32_t phys = pc & 0x1FFFFFFFu;
|
||||
int lo = 0, hi = s_site_count;
|
||||
while (lo < hi) {
|
||||
const int mid = lo + (hi - lo) / 2;
|
||||
if (s_sites[mid].address < phys) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
if (lo < s_site_count && s_sites[lo].address == phys &&
|
||||
s_sites[lo].expected == insn)
|
||||
return &s_sites[lo];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* mod_plugins.h. Process-wide host state: the session start resets it to off
|
||||
* (main.cpp reset_mod_owned_presentation), so only a session whose plan
|
||||
* activates a plugin that asks for it, never netplay, runs with it on. */
|
||||
int psx_mod_set_draw_distance_clamp(int enabled) {
|
||||
g_psx_draw_distance_clamp = enabled ? 1u : 0u;
|
||||
update_live();
|
||||
return s_site_count > 0;
|
||||
}
|
||||
|
||||
int psx_mod_draw_distance_clamp_enabled(void) {
|
||||
return g_psx_draw_distance_clamp != 0u;
|
||||
}
|
||||
+65
-11
@@ -885,6 +885,15 @@ int psx_ws_cull_bgez(uint32_t v) {
|
||||
uint32_t psx_ws_clip_edge_x(uint32_t v, uint32_t w) {
|
||||
return psx_ws_clip_edge_x_value(v, w, psx_ws_x_margin());
|
||||
}
|
||||
/* [widescreen.cull] screen_x_sites on a screen X kept in the high half of a
|
||||
* register: `lui rt, W` becomes (W + margin) << 16, and `bltz` is taken only
|
||||
* while SX < -margin. Identity at margin 0 (4:3). */
|
||||
uint32_t psx_ws_cull_lui_hi(uint32_t imm) {
|
||||
return psx_ws_cull_lui_hi_value(imm, psx_ws_x_margin());
|
||||
}
|
||||
int psx_ws_cull_bltz_hi(uint32_t v) {
|
||||
return psx_ws_cull_bltz_hi_value(v, psx_ws_x_margin());
|
||||
}
|
||||
/* Per-primitive X-reject bound ([widescreen.cull] xclip_load_sites). While
|
||||
* the margins are revealed the reject is disabled (INT32_MAX passes every
|
||||
* ANDI-masked u16 screen X, including wrapped off-left coords at 655xx); the
|
||||
@@ -4182,11 +4191,14 @@ uint32_t gpu_texture_correction_hits(void) {
|
||||
* packet words is resolved independently: the address-keyed dataflow shadow
|
||||
* first (validated against the actual word — exact provenance, survives
|
||||
* ordering-table reordering), the ambiguity-gated position cache second, the
|
||||
* parsed integers last. Mixing precise and native vertices in one triangle
|
||||
* is correct — a native vertex is exactly where the uncorrected pipeline put
|
||||
* it, so shared edges between neighbouring triangles cannot disagree by more
|
||||
* than the sub-pixel fraction. The integer delta folds in draw offsets and
|
||||
* any widescreen adjustment already applied by the caller. */
|
||||
* parsed integers last. A vertex left native is exactly where the
|
||||
* uncorrected pipeline put it. Where it meets a neighbour's precise copy of
|
||||
* the same vertex, their shared edge disagrees by that vertex's precise-minus-
|
||||
* native offset: under a fraction of a pixel on the IR path, but up to the
|
||||
* PGXP_PPP_AGREE_* window (several pixels) under preserve-projection — that
|
||||
* is the G1.1 crack the tri_mixed counter measures. The integer delta folds
|
||||
* in draw offsets and any widescreen adjustment already applied by the
|
||||
* caller. */
|
||||
static void prepare_precise_triangle(int i0, int i1, int i2,
|
||||
const int32_t vx[3], const int32_t vy[3]) {
|
||||
gr_set_perspective_triangle(0, 0.0f, 0.0f, 0.0f);
|
||||
@@ -4196,7 +4208,7 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
|
||||
}
|
||||
const int idx[3] = { i0, i1, i2 };
|
||||
int32_t fx[3], fy[3];
|
||||
int any_precise = 0;
|
||||
int any_precise = 0, n_precise = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
uint32_t word = gp0_cmd_buf[idx[i]];
|
||||
int32_t raw_x, raw_y;
|
||||
@@ -4207,11 +4219,14 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
|
||||
int32_t px, py;
|
||||
uint16_t sz;
|
||||
if (pgxp_get_precise_vertex(addr, word, raw_x, raw_y,
|
||||
&px, &py, &sz) != PGXP_SRC_NATIVE)
|
||||
&px, &py, &sz) != PGXP_SRC_NATIVE) {
|
||||
any_precise = 1;
|
||||
n_precise++;
|
||||
}
|
||||
fx[i] = (int32_t)((int64_t)px + (int64_t)(vx[i] - raw_x) * 65536);
|
||||
fy[i] = (int32_t)((int64_t)py + (int64_t)(vy[i] - raw_y) * 65536);
|
||||
}
|
||||
pgxp_note_triangle(n_precise); /* G1.1 crack exposure: mixed share */
|
||||
if (!any_precise) {
|
||||
gr_set_precise_triangle(0, 0,0, 0,0, 0,0);
|
||||
return;
|
||||
@@ -4219,6 +4234,34 @@ static void prepare_precise_triangle(int i0, int i1, int i2,
|
||||
gr_set_precise_triangle(1, fx[0],fy[0], fx[1],fy[1], fx[2],fy[2]);
|
||||
}
|
||||
|
||||
/* A textured quad whose four packet vertices form an axis-aligned rectangle
|
||||
* (in integer screen space and in UV) is drawn by the 2D rectangle shortcut,
|
||||
* at the native integer position and without perspective. That is right for a
|
||||
* CPU-built sprite, but a GTE-projected world quad can land on such a
|
||||
* rectangle too (a facade seen straight on), and its neighbours are drawn
|
||||
* precise, so the shortcut would open a sub-pixel seam along every shared
|
||||
* edge (docs/ENHANCEMENTS.md G1.11). Returns how many of the four vertices
|
||||
* carry a dataflow-precise position (0 while PGXP is not correcting). Only a
|
||||
* quad with all four precise leaves the shortcut and is drawn as its two
|
||||
* triangles, like any other world quad: one with some corners precise (say a
|
||||
* CPU-built sprite with one corner copied from SXY) would otherwise become
|
||||
* mixed triangles, so it keeps the shortcut and counts as rect_partial. */
|
||||
static int textured_quad_precise_vertices(const int idx[4]) {
|
||||
if (!gte_geometry_correction_enabled() && !s_texture_correction_enabled)
|
||||
return 0;
|
||||
if (gp0_cmd_source_addr == 0xFFFFFFFFu) return 0;
|
||||
int n = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint32_t word = gp0_cmd_buf[idx[i]];
|
||||
int32_t raw_x, raw_y;
|
||||
parse_vertex(word, &raw_x, &raw_y);
|
||||
if (pgxp_probe_precise_vertex(gp0_cmd_source_addr + (uint32_t)idx[i] * 4u,
|
||||
word, raw_x, raw_y) == PGXP_SRC_DATAFLOW)
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Arming rate for perspective-correct UVs, per condition.
|
||||
*
|
||||
* perspective_triangles alone cannot answer "how much texture warp is left",
|
||||
@@ -4757,10 +4800,21 @@ static void gp0_exec_textured_quad(void) {
|
||||
|
||||
setup_textured_draw(color24, semi_trans, raw_texture);
|
||||
|
||||
if (vy[0] == vy[1] && vy[2] == vy[3] &&
|
||||
vx[0] == vx[2] && vx[1] == vx[3] &&
|
||||
u[0] == u[2] && u[1] == u[3] &&
|
||||
v[0] == v[1] && v[2] == v[3]) {
|
||||
int as_rect = vy[0] == vy[1] && vy[2] == vy[3] &&
|
||||
vx[0] == vx[2] && vx[1] == vx[3] &&
|
||||
u[0] == u[2] && u[1] == u[3] &&
|
||||
v[0] == v[1] && v[2] == v[3];
|
||||
static const int k_quad_words[4] = { 1, 3, 5, 7 };
|
||||
if (as_rect) {
|
||||
const int n_precise = textured_quad_precise_vertices(k_quad_words);
|
||||
if (n_precise == 4) {
|
||||
pgxp_note_rect_bypass(1);
|
||||
as_rect = 0;
|
||||
} else if (n_precise > 0) {
|
||||
pgxp_note_rect_bypass(0);
|
||||
}
|
||||
}
|
||||
if (as_rect) {
|
||||
int x = vx[0] < vx[1] ? vx[0] : vx[1];
|
||||
int y = vy[0] < vy[2] ? vy[0] : vy[2];
|
||||
int w = vx[0] < vx[1] ? vx[1] - vx[0] : vx[0] - vx[1];
|
||||
|
||||
@@ -537,13 +537,14 @@ static int s_scratch_w = 0, s_scratch_h = 0;
|
||||
static GLuint s_geo_prog = 0, s_geo_vao = 0, s_geo_vbo = 0;
|
||||
static GLuint s_tex_prog = 0, s_tex_vao = 0, s_tex_vbo = 0;
|
||||
/* Textured vertex: pos(2) uv(2) col(4) tpage(2) clut(2) depth(1) raw(1) limits(4)
|
||||
* semi(1) q(1)
|
||||
* semi(1) q(1) twin(1)
|
||||
* — per-prim texture state in flat attributes so prims batch (see flush_tex_batch).
|
||||
* q is the perspective weight ([video] perspective_texturing): 0 = affine, the
|
||||
* PS1-faithful default, which makes the vertex shader's w exactly 1.0 and the
|
||||
* fragment shader read the noperspective varying — i.e. bit-identical to the
|
||||
* pre-feature pipeline. */
|
||||
#define TEXV 20
|
||||
* pre-feature pipeline. twin is the prim's GP0(E2h) texture window, its low 20
|
||||
* bits as a whole float (mask x, mask y, offset x, offset y; 5 bits each). */
|
||||
#define TEXV 21
|
||||
static GLuint s_blit_prog = 0, s_blit_vao = 0, s_blit_vbo = 0;
|
||||
static GLuint s_blit_hi_prog = 0; /* windowed hi surface blit */
|
||||
static GLint s_uBhSrc = -1, s_uBhPass = -1, s_uBhMaskset = -1, s_uBhSrcDiv = -1;
|
||||
@@ -567,7 +568,7 @@ static float s_pq[3];
|
||||
static GLint s_uVram = -1, s_uTpage = -1, s_uClut = -1, s_uDepth = -1;
|
||||
static GLint s_uPalette = -1;
|
||||
static GLint s_uRaw = -1, s_uSemipass = -1, s_uSemimode = -1;
|
||||
static GLint s_uTwin = -1, s_uMaskset = -1, s_uFilter = -1;
|
||||
static GLint s_uMaskset = -1, s_uFilter = -1;
|
||||
static GLint s_uLimits = -1;
|
||||
/* Native-wide x-projection uniforms (per program). u_xoff = x translation in
|
||||
* native px (0 canonical), u_xhalf = x clip half-extent in native px (512
|
||||
@@ -1269,9 +1270,10 @@ static const char *GEO_FS =
|
||||
/* Textured program. Per-prim texture state (texpage, clut, depth, raw, uv
|
||||
* limits) is carried in FLAT vertex attributes — constant across a prim's
|
||||
* vertices — instead of uniforms, so consecutive textured prims with the same
|
||||
* blend/mask/texture-window state batch into one draw (see flush_tex_batch).
|
||||
* The remaining uniforms (u_twin/u_maskset/u_filter/u_semipass) are the batch
|
||||
* keys + per-pass state. */
|
||||
* blend/mask state batch into one draw (see flush_tex_batch). The texture
|
||||
* window rides in the vertex too (a_twin), so a GP0(E2h) change need not end
|
||||
* a batch. The remaining uniforms (u_maskset/u_filter/u_semipass) are the
|
||||
* batch keys + per-pass state. */
|
||||
static const char *TEX_VS =
|
||||
"#version 330\n"
|
||||
"layout(location=0) in vec2 a_pos;\n"
|
||||
@@ -1284,6 +1286,7 @@ static const char *TEX_VS =
|
||||
"layout(location=7) in vec4 a_limits;\n"
|
||||
"layout(location=8) in float a_semi;\n"
|
||||
"layout(location=9) in float a_q; /* persp weight; 0 = affine (default) */\n"
|
||||
"layout(location=10) in float a_twin; /* GP0(E2h) bits 0..19 */\n"
|
||||
"uniform float u_shift;\n"
|
||||
"uniform float u_xoff; /* native-wide x translation (px); 0 canonical */\n"
|
||||
"uniform float u_xhalf; /* x clip half-extent (px); 512 canonical */\n"
|
||||
@@ -1294,11 +1297,13 @@ static const char *TEX_VS =
|
||||
"flat out int v_persp;\n"
|
||||
"flat out ivec2 v_tpage; flat out ivec2 v_clut; flat out int v_depth;\n"
|
||||
"flat out int v_raw; flat out ivec4 v_limits; flat out int v_semi;\n"
|
||||
"flat out int v_twin;\n"
|
||||
"void main(){ v_uv = a_uv; v_uv_p = a_uv; v_col = a_col;\n"
|
||||
" v_persp = (a_q > 0.0) ? 1 : 0;\n"
|
||||
" v_tpage = ivec2(a_tpage + 0.5); v_clut = ivec2(a_clut + 0.5);\n"
|
||||
" v_depth = int(a_depth + 0.5); v_raw = int(a_raw + 0.5);\n"
|
||||
" v_semi = int(a_semi + 0.5);\n"
|
||||
" v_twin = int(a_twin + 0.5);\n"
|
||||
" v_limits = ivec4(floor(a_limits + 0.5));\n"
|
||||
" /* u_shift: align GL's center-sample grid with the PS1 integer grid (see\n"
|
||||
" * GEO_VS) so interpolated uv at a fragment equals the PS1 DDA value. */\n"
|
||||
@@ -1326,11 +1331,11 @@ static const char *TEX_FS =
|
||||
"flat in int v_raw; /* 1 = no color modulation */\n"
|
||||
"flat in ivec4 v_limits; /* prim uv sampling bounds (inclusive, post-wrap) */\n"
|
||||
"flat in int v_semi; /* GP0 command has semi-transparency enabled */\n"
|
||||
"flat in int v_twin; /* texture window, GP0(E2h) bits 0..19 */\n"
|
||||
"uniform usampler2D u_vram;\n"
|
||||
"uniform usampler2D u_palette;\n"
|
||||
"uniform int u_semipass; /* 0=all texels, 1=STP=0 only, 2=STP=1 only */\n"
|
||||
"uniform int u_semimode; /* PS1 blend mode; drives dual-source factors */\n"
|
||||
"uniform ivec4 u_twin; /* texture window: mask_x, mask_y, off_x, off_y */\n"
|
||||
"uniform int u_maskset; /* GP0(E6h) set-mask: OR bit15 into output */\n"
|
||||
"uniform int u_filter; /* 1 = bilinear */\n"
|
||||
"uniform float u_shift;\n"
|
||||
@@ -1346,9 +1351,12 @@ static const char *TEX_FS =
|
||||
"}\n"
|
||||
"int fetch_texel(int u, int v){\n"
|
||||
" u &= 255; v &= 255;\n"
|
||||
" if ((u_twin.x | u_twin.y) != 0) {\n"
|
||||
" u = (u & ~(u_twin.x * 8)) | ((u_twin.z & u_twin.x) * 8);\n"
|
||||
" v = (v & ~(u_twin.y * 8)) | ((u_twin.w & u_twin.y) * 8);\n"
|
||||
" /* texture window: mask_x, mask_y, off_x, off_y (8-px units) */\n"
|
||||
" ivec4 tw = ivec4(v_twin & 31, (v_twin >> 5) & 31,\n"
|
||||
" (v_twin >> 10) & 31, (v_twin >> 15) & 31);\n"
|
||||
" if ((tw.x | tw.y) != 0) {\n"
|
||||
" u = (u & ~(tw.x * 8)) | ((tw.z & tw.x) * 8);\n"
|
||||
" v = (v & ~(tw.y * 8)) | ((tw.w & tw.y) * 8);\n"
|
||||
" } else {\n"
|
||||
" u = clamp(u, v_limits.x, v_limits.z);\n"
|
||||
" v = clamp(v, v_limits.y, v_limits.w);\n"
|
||||
@@ -2467,9 +2475,10 @@ static void wide_clear_bd_scale(GLint uScale, GLint uCenter) {
|
||||
|
||||
/* ---- textured-prim batching -------------------------------------------- *
|
||||
* Consecutive textured prims sharing blend/mask/texwindow/filter coalesce into
|
||||
* one draw. Per-prim texture state (texpage/clut/depth/raw/uv-limits) rides in
|
||||
* the vertex (TEXV flat attributes), so only `semi` (blend) and the global
|
||||
* mask/twin/filter are batch keys. flush_tex_batch() draws the queued verts; it
|
||||
* one draw. Per-prim texture state (texpage/clut/depth/raw/uv-limits/texture
|
||||
* window) rides in the vertex (TEXV flat attributes), so only `semi` (blend)
|
||||
* and the global mask/filter are batch keys, plus the texture window unless
|
||||
* texture-window batching is on (below). flush_tex_batch() draws the queued verts; it
|
||||
* is called before any op that reads VRAM, writes it outside the batch, or
|
||||
* changes batch state (see callers: flush_cpu_upload, flush_pack_if_sampling,
|
||||
* every non-textured glb_ wrapper, and the present path). Drawing reads only the
|
||||
@@ -2484,6 +2493,25 @@ static GLuint s_tb_bank_tex;
|
||||
static int s_tb_twin[4] = {0, 0, 0, 0};
|
||||
static uint64_t s_batch_total = 0, s_batch_reason[7];
|
||||
|
||||
/* Texture-window batching ([video] texture_window_batching, OpenGL; off by
|
||||
* default). Off: a GP0(E2h) texture-window change ends the open textured batch
|
||||
* (batch reason 5), as before. On: prims with different windows share a batch,
|
||||
* since each vertex carries its own window. Games that tile far textures with
|
||||
* per-prim windows (R4's split screen: ~515 E2 changes and ~180 batches a game
|
||||
* frame, ~17 with this on) then draw in a tenth of the batches, which matters
|
||||
* most on the native-wide path, where a batch reaching the margins is drawn
|
||||
* again into the wide surface.
|
||||
* The result is pixel-identical: an opaque batch draws its colour in one
|
||||
* painter-ordered pass and semi-transparent prims are still drawn one per
|
||||
* batch. The one exception is mask checking (GP0(E6h) bit 1): an opaque batch
|
||||
* fixes the stencil for its STP=1 texels after its colour pass, so a later
|
||||
* prim in the same batch is not stopped by an earlier one's mask bit. The
|
||||
* window therefore stays a batch key while the check is on, and this mode
|
||||
* never makes a batch longer than the default mode would there. */
|
||||
static int s_twin_batching = 0;
|
||||
void gl_renderer_set_texture_window_batching(int on) { s_twin_batching = on ? 1 : 0; }
|
||||
int gl_renderer_get_texture_window_batching(void) { return s_twin_batching; }
|
||||
|
||||
void gl_renderer_batch_diag(uint64_t out[8]) {
|
||||
out[0] = s_batch_total;
|
||||
for (int i = 0; i < 7; i++) out[i + 1] = s_batch_reason[i];
|
||||
@@ -2616,7 +2644,6 @@ typedef struct {
|
||||
uint8_t check; /* mask-check state at draw time */
|
||||
uint8_t filter;
|
||||
GLuint tex; /* sampled texture (bank or raw mirror) */
|
||||
int twin[4];
|
||||
int ax0, ay0, ax1, ay1; /* draw area, inclusive */
|
||||
size_t vfirst; /* float offset into s_hq_v */
|
||||
int vcount; /* vertices */
|
||||
@@ -2689,7 +2716,6 @@ static int hiw_enqueue_tex(int nverts, int semi, int mirror, int gate) {
|
||||
c->mask = (uint8_t)s_tb_mask;
|
||||
c->filter = (uint8_t)s_tb_filter;
|
||||
c->tex = s_tb_bank_tex ? s_tb_bank_tex : s_raw_tex;
|
||||
memcpy(c->twin, s_tb_twin, sizeof c->twin);
|
||||
if (wq) hiw_wide_set(c, gate);
|
||||
return wq;
|
||||
}
|
||||
@@ -2747,7 +2773,6 @@ static void hiw_replay_wide(void) {
|
||||
p_glUniform1f(s_tex_uXscale, c->wscale);
|
||||
p_glUniform1f(s_tex_uXcenter, c->wcenter);
|
||||
glBindTexture(GL_TEXTURE_2D, c->tex);
|
||||
p_glUniform4i(s_uTwin, c->twin[0], c->twin[1], c->twin[2], c->twin[3]);
|
||||
p_glUniform1i(s_uMaskset, c->mask);
|
||||
p_glUniform1i(s_uFilter, c->filter);
|
||||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)),
|
||||
@@ -2824,7 +2849,6 @@ static void hiw_flush_queue(void) {
|
||||
cur = HQ_TEX;
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, c->tex);
|
||||
p_glUniform4i(s_uTwin, c->twin[0], c->twin[1], c->twin[2], c->twin[3]);
|
||||
p_glUniform1i(s_uMaskset, c->mask);
|
||||
p_glUniform1i(s_uFilter, c->filter);
|
||||
p_glBufferData(PSXGL_ARRAY_BUFFER, (ptrdiff_t)((size_t)c->vcount * TEXV * sizeof(float)),
|
||||
@@ -2876,7 +2900,6 @@ static void flush_tex_batch(void) {
|
||||
glBindTexture(GL_TEXTURE_2D, s_tb_bank_tex && !s_tb_bank_live_clut ? s_tb_bank_tex : s_raw_tex);
|
||||
p_glUniform1i(s_uPalette, 1);
|
||||
p_glActiveTexture(PSXGL_TEXTURE0);
|
||||
p_glUniform4i(s_uTwin, s_tb_twin[0], s_tb_twin[1], s_tb_twin[2], s_tb_twin[3]);
|
||||
p_glUniform1i(s_uMaskset, s_tb_mask);
|
||||
p_glUniform1i(s_uFilter, s_tb_filter);
|
||||
p_glBindVertexArray(s_tex_vao);
|
||||
@@ -3280,7 +3303,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
|
||||
/* Append to the textured batch. Flush first if this prim's blend/mask/twin/
|
||||
* filter differ from the open batch, or the buffer is full. Per-prim texture
|
||||
* state goes in the vertex; only these keys force a new draw. */
|
||||
* state goes in the vertex; only these keys force a new draw (the window
|
||||
* not at all under texture-window batching, see s_twin_batching). */
|
||||
{
|
||||
flush_flat_batch(); /* painter order: flat GEO before textured */
|
||||
int twx = s_tw_mask_x, twy = s_tw_mask_y, tox = s_tw_off_x, toy = s_tw_off_y;
|
||||
@@ -3321,8 +3345,9 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
else if (s_mask_set != s_tb_mask) reason = 2;
|
||||
else if (s_tex_filter != s_tb_filter) reason = 3;
|
||||
else if (gate != s_tb_gate) reason = 4;
|
||||
else if (twx != s_tb_twin[0] || twy != s_tb_twin[1] ||
|
||||
tox != s_tb_twin[2] || toy != s_tb_twin[3]) reason = 5;
|
||||
else if ((!s_twin_batching || s_mask_check) &&
|
||||
(twx != s_tb_twin[0] || twy != s_tb_twin[1] ||
|
||||
tox != s_tb_twin[2] || toy != s_tb_twin[3])) reason = 5;
|
||||
}
|
||||
if (reason >= 0) {
|
||||
s_batch_reason[reason]++;
|
||||
@@ -3335,6 +3360,8 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
s_tb_bank_live_clut = s_selected_bank_live_clut;
|
||||
s_tb_twin[0] = twx; s_tb_twin[1] = twy; s_tb_twin[2] = tox; s_tb_twin[3] = toy;
|
||||
}
|
||||
/* The prim's window, exact as a float (20 bits). */
|
||||
float twin = (float)(twx | (twy << 5) | (tox << 10) | (toy << 15));
|
||||
float *vp = &s_tb[s_tb_n * TEXV];
|
||||
for (int i = 0; i < 3; i++, vp += TEXV) {
|
||||
vp[0] = s_pc_valid ? s_pc_x[i] : (float)xs[i];
|
||||
@@ -3348,6 +3375,7 @@ static void gpu_textured_triangle(const int *xs, const int *ys,
|
||||
vp[16] = (float)lim[2]; vp[17] = (float)lim[3];
|
||||
vp[18] = semi >= 0 ? (float)(semi + 1) : 0.0f; /* a_semi code */
|
||||
vp[19] = s_pq_valid ? s_pq[i] : 0.0f; /* a_q; 0 = affine */
|
||||
vp[20] = twin; /* a_twin */
|
||||
}
|
||||
s_tb_n += 3;
|
||||
if (isolate) flush_tex_batch(); /* draw this semi prim alone, in submission order */
|
||||
@@ -4246,7 +4274,6 @@ static int init_gpu_raster(void) {
|
||||
s_uRaw = p_glGetUniformLocation(s_tex_prog, "u_raw");
|
||||
s_uSemipass = p_glGetUniformLocation(s_tex_prog, "u_semipass");
|
||||
s_uSemimode = p_glGetUniformLocation(s_tex_prog, "u_semimode");
|
||||
s_uTwin = p_glGetUniformLocation(s_tex_prog, "u_twin");
|
||||
s_uMaskset = p_glGetUniformLocation(s_tex_prog, "u_maskset");
|
||||
s_uFilter = p_glGetUniformLocation(s_tex_prog, "u_filter");
|
||||
s_uLimits = p_glGetUniformLocation(s_tex_prog, "u_limits");
|
||||
@@ -4339,6 +4366,7 @@ static int init_gpu_raster(void) {
|
||||
p_glVertexAttribPointer(7, 4, GL_FLOAT, GL_FALSE, st, (void*)(14*sizeof(float))); p_glEnableVertexAttribArray(7); /* limits */
|
||||
p_glVertexAttribPointer(8, 1, GL_FLOAT, GL_FALSE, st, (void*)(18*sizeof(float))); p_glEnableVertexAttribArray(8); /* semi */
|
||||
p_glVertexAttribPointer(9, 1, GL_FLOAT, GL_FALSE, st, (void*)(19*sizeof(float))); p_glEnableVertexAttribArray(9); /* q */
|
||||
p_glVertexAttribPointer(10, 1, GL_FLOAT, GL_FALSE, st, (void*)(20*sizeof(float))); p_glEnableVertexAttribArray(10); /* twin */
|
||||
}
|
||||
|
||||
p_glGenVertexArrays(1, &s_blit_vao);
|
||||
|
||||
+64
-3
@@ -12,6 +12,9 @@ extern "C" uint32_t psx_read_word(uint32_t addr);
|
||||
extern "C" int gpu_ws_precise_nclip_enabled(void);
|
||||
extern "C" uint64_t s_frame_count; /* debug_server.c */
|
||||
extern "C" uint32_t g_debug_last_store_pc; /* debug_server.c */
|
||||
/* overlay_loader.c: nonzero while an overlay shadow diff (interpreter record
|
||||
* plus native replay of one candidate) is running. */
|
||||
extern "C" int psx_overlay_shadow_diff_active(void);
|
||||
|
||||
namespace PSXRecomp {
|
||||
namespace GTE {
|
||||
@@ -445,6 +448,8 @@ extern "C" void gte_precision_checkpoint_rollback(void) {
|
||||
static inline void geom_note(uint32_t packed, int64_t x16, int64_t y16) {
|
||||
if (s_speculative_depth != 0 || s_gte_replay_sandbox || !s_geom_enabled ||
|
||||
!s_geom_cache) return;
|
||||
/* Dataflow-only (G1.11): nothing will read the cache, so do not fill it. */
|
||||
if (!pgxp_position_fallback()) return;
|
||||
/* Saturated off-screen projections are unsuitable for subpixel recovery. */
|
||||
int32_t x = (int16_t)(packed & 0xFFFFu);
|
||||
int32_t y = (int16_t)(packed >> 16);
|
||||
@@ -923,6 +928,9 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
// this frame is being stretched — never on a 4:3-presented frame (FMV /
|
||||
// full-2D screen), so content and present stay locked.
|
||||
int64_t xterm = (int64_t)gte->IR1 * h_div_sz;
|
||||
/* Horizontal factor applied to xterm below, for the exact-projection
|
||||
* shadow (pgxp_project_precise) to apply the same one. */
|
||||
int64_t x_num = 1, x_den = 1;
|
||||
bool do_squash = (s_ws_xnum != s_ws_xden) && !gpu_ws_present_native_43();
|
||||
const bool dome_call = ws_dome_call_matches();
|
||||
// Curved backdrops are authored to cover the original 4:3 projection.
|
||||
@@ -943,8 +951,11 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
if (!s_gte_replay_sandbox) s_ws_sz_far++;
|
||||
}
|
||||
}
|
||||
if (do_squash)
|
||||
if (do_squash) {
|
||||
xterm = xterm * s_ws_xnum / s_ws_xden;
|
||||
x_num = s_ws_xnum;
|
||||
x_den = s_ws_xden;
|
||||
}
|
||||
// Native-wide dome expansion remains a diagnostic-only depth probe.
|
||||
else if (s_ws_dome_on && s_ws_dome_num != s_ws_dome_den &&
|
||||
!gpu_ws_present_native_43()) {
|
||||
@@ -956,6 +967,8 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
}
|
||||
if (sz >= s_ws_far_threshold) {
|
||||
xterm = xterm * s_ws_dome_num / s_ws_dome_den;
|
||||
x_num = s_ws_dome_num;
|
||||
x_den = s_ws_dome_den;
|
||||
if (!s_gte_replay_sandbox) s_ws_sz_far++;
|
||||
}
|
||||
}
|
||||
@@ -975,8 +988,25 @@ void gte_rtps_internal(GTEState* gte, int16_t* V, bool setMac0, uint32_t instr)
|
||||
const int64_t kLim = (int64_t)4096 << 16;
|
||||
int64_t cx16 = sx16 < -kLim ? -kLim : (sx16 > kLim - 1 ? kLim - 1 : sx16);
|
||||
int64_t cy16 = sy16 < -kLim ? -kLim : (sy16 > kLim - 1 ? kLim - 1 : sy16);
|
||||
pgxp_gte_push_sxy((int32_t)cx16, (int32_t)cy16, gte->SZ[3],
|
||||
(uint32_t)gte->SXY[2]);
|
||||
int32_t px16 = (int32_t)cx16, py16 = (int32_t)cy16;
|
||||
/* Preserve projection precision (G1.11): shadow the exact projection
|
||||
* instead when the vertex qualifies and it lies inside the GPU's
|
||||
* truncation-agreement window; outside it the IR path's shadow is
|
||||
* kept, so the vertex still draws precise rather than being rejected
|
||||
* 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()) {
|
||||
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,
|
||||
x_num, x_den, &ex16, &ey16) &&
|
||||
pgxp_ppp_accept(ex16, ey16, (uint32_t)gte->SXY[2])) {
|
||||
px16 = ex16;
|
||||
py16 = ey16;
|
||||
}
|
||||
}
|
||||
pgxp_gte_push_sxy(px16, py16, gte->SZ[3], (uint32_t)gte->SXY[2]);
|
||||
}
|
||||
geom_note((uint32_t)gte->SXY[2], sx16, sy16);
|
||||
|
||||
@@ -1092,6 +1122,37 @@ void gte_nclip(GTEState* gte, uint32_t instr) {
|
||||
(int64_t)sx2 * (sy0 - sy1);
|
||||
gte->check_mac0_overflow(mac0);
|
||||
int32_t out = static_cast<int32_t>(mac0);
|
||||
/* PGXP's NCLIP (docs/ENHANCEMENTS.md G1.12). While geometry correction is
|
||||
* armed, compare the sign of the exact determinant of the three vertices
|
||||
* PGXP will draw with the integer one; a disagreement is a face the game
|
||||
* culls (or keeps) on a winding the drawn face does not have. Counted
|
||||
* always; with precise culling on, MAC0 takes the exact sign. Never in a
|
||||
* pass that is compared against another execution of the same code: the
|
||||
* speculative and replay passes record no shadows, so the overlay shadow
|
||||
* diff holds it off for its interpreter pass too and both passes see the
|
||||
* integer MAC0. FLAG keeps the integer result's overflow bits. */
|
||||
if (s_geom_enabled && !s_gte_replay_sandbox && s_speculative_depth == 0 &&
|
||||
!psx_overlay_shadow_diff_active()) {
|
||||
const uint32_t words[3] = { (uint32_t)gte->SXY[0], (uint32_t)gte->SXY[1],
|
||||
(uint32_t)gte->SXY[2] };
|
||||
int64_t cross = 0;
|
||||
if (pgxp_gte_nclip_precise(words, &cross)) {
|
||||
const int sp = (cross > 0) - (cross < 0);
|
||||
const int sn = (out > 0) - (out < 0);
|
||||
if (sp != 0 && sp != sn) {
|
||||
const int corrected = pgxp_culling();
|
||||
if (corrected) {
|
||||
const uint64_t mag = cross < 0 ? (uint64_t)(-cross)
|
||||
: (uint64_t)cross;
|
||||
uint64_t area = (mag + (1ull << 31)) >> 32;
|
||||
if (area < 1) area = 1;
|
||||
if (area > 0x7FFFFFFFull) area = 0x7FFFFFFFull;
|
||||
out = sp > 0 ? (int32_t)area : -(int32_t)area;
|
||||
}
|
||||
pgxp_note_nclip(1, corrected);
|
||||
}
|
||||
}
|
||||
}
|
||||
s_nclip_last_native = out;
|
||||
s_nclip_last_precise_valid = false;
|
||||
/* Compute an exact 16.16 determinant for configured branch consumers, but
|
||||
|
||||
+293
-45
@@ -37,8 +37,10 @@
|
||||
extern "C" void psx_event_step_conservative_env_init(void);
|
||||
#include "overlay_backend.h"
|
||||
#include "gpu.h"
|
||||
#include "draw_distance.h"
|
||||
#include "display_scanout.h"
|
||||
#include "pgxp.h"
|
||||
#include "pgxp_session.h"
|
||||
#include "interrupts.h"
|
||||
#include "psx_video_timing.h"
|
||||
#include "present_ring.h"
|
||||
@@ -147,6 +149,7 @@ extern "C" void psx_game_codegen_forward_if_built(int argc, char** argv);
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <cctype>
|
||||
@@ -1246,6 +1249,15 @@ static int g_video_geometry_correction = 0;
|
||||
static int g_video_perspective_texturing = 0;
|
||||
static int g_video_pgxp_cpu_mode = 0;
|
||||
static float g_video_pgxp_tolerance = 0.5f;
|
||||
/* docs/ENHANCEMENTS.md G1.11: dataflow-only and exact-projection shadows.
|
||||
* Defaults keep the historical behaviour. game.toml [video] only. */
|
||||
static int g_video_pgxp_position_fallback = 1;
|
||||
static int g_video_pgxp_preserve_projection = 0;
|
||||
/* [video] pgxp_mod_only (G1.12): the title ships PGXP through the
|
||||
* psx.enhancement.pgxp mod, which is then the one switch -- the [video]
|
||||
* geometry_correction / perspective_texturing / pgxp_cpu_mode values are not
|
||||
* applied and the launcher hides its Perspective textures row. */
|
||||
static int g_video_pgxp_mod_only = 0;
|
||||
static int g_video_renderer = PSXRecompV4::DEFAULT_VIDEO_RENDERER;
|
||||
|
||||
/* Settings -> Display -> Internal resolution (internal_resolution.h). The
|
||||
@@ -1576,9 +1588,14 @@ static void reset_mod_owned_presentation(void) {
|
||||
psx_mod_set_world_scene_predicate(nullptr);
|
||||
psx_mod_set_retained_scene_predicate(nullptr);
|
||||
psx_mod_set_adaptive_backdrop_preload(0);
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
g_bezel_path.clear();
|
||||
g_frame_interpolation_blend = g_frame_interpolation_blend_default;
|
||||
g_frame_interpolation_source = PSX_MOD_FRAME_SOURCE_VBLANK;
|
||||
/* The PGXP mod's request: only this session's activation may set it; the
|
||||
* renderer setup takes it and combines it with the [video] baseline
|
||||
* (pgxp_session.h). */
|
||||
psx_pgxp_session_reset();
|
||||
/* Render-pass counters, the disabled-after-faults latch and any open
|
||||
* plan generation belong to the session that made them. */
|
||||
render_pass_reset_session();
|
||||
@@ -2494,6 +2511,17 @@ static std::filesystem::path sidecar_cfg_path(const char* argv0, const char* fil
|
||||
return exe_dir_from_argv(argv0) / filename;
|
||||
}
|
||||
|
||||
/* Relative paths in settings.toml and the bios.cfg / disc.cfg sidecars are
|
||||
* relative to the game folder. Resolve them against the exe directory, never
|
||||
* the working directory: a shortcut or frontend that starts the game from
|
||||
* elsewhere must still find its own memory cards and BIOS. Absolute paths are
|
||||
* returned unchanged; empty stays empty. */
|
||||
static std::filesystem::path anchor_on_exe_dir(const char* argv0,
|
||||
const std::filesystem::path& p) {
|
||||
if (p.empty()) return p;
|
||||
return PSXRecompV4::host_resolve(exe_dir_from_argv(argv0), p);
|
||||
}
|
||||
|
||||
static std::filesystem::path read_cached_path(const char* argv0, const char* filename) {
|
||||
std::ifstream f(sidecar_cfg_path(argv0, filename));
|
||||
if (!f.is_open()) return {};
|
||||
@@ -2502,13 +2530,17 @@ static std::filesystem::path read_cached_path(const char* argv0, const char* fil
|
||||
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
|
||||
line.pop_back();
|
||||
}
|
||||
return line.empty() ? std::filesystem::path{} : std::filesystem::path(line);
|
||||
return line.empty() ? std::filesystem::path{}
|
||||
: anchor_on_exe_dir(argv0, std::filesystem::path(line));
|
||||
}
|
||||
|
||||
static void write_cached_path(const char* argv0, const char* filename,
|
||||
const std::filesystem::path& path) {
|
||||
std::ofstream f(sidecar_cfg_path(argv0, filename), std::ios::trunc);
|
||||
if (f.is_open()) f << path.string() << "\n";
|
||||
// Relative inside the game folder so a moved portable folder still works;
|
||||
// read_cached_path anchors relative paths on the exe directory.
|
||||
if (f.is_open())
|
||||
f << PSXRecompV4::relative_to_folder(path, exe_dir_from_argv(argv0)).generic_string() << "\n";
|
||||
}
|
||||
|
||||
/* Nobody is at the screen: never block on a modal dialog or a file picker. */
|
||||
@@ -2698,6 +2730,12 @@ static std::filesystem::path normalize_disc_path_for_launch(const std::filesyste
|
||||
return PSXRecompV4::resolve_disc_path(path).mount;
|
||||
}
|
||||
|
||||
static std::filesystem::path resolve_persisted_disc_path(
|
||||
const std::filesystem::path& path, const std::filesystem::path& exe_dir) {
|
||||
if (path.empty()) return {};
|
||||
return normalize_disc_path_for_launch(PSXRecompV4::host_resolve(exe_dir, path));
|
||||
}
|
||||
|
||||
/* Which image of a MULTI-DISC set to mount, given the roster this build was
|
||||
* made from (game.toml [game] discs), the player's persisted [disc] selected
|
||||
* index, and the [disc] path the launcher last wrote.
|
||||
@@ -2985,7 +3023,7 @@ static std::filesystem::path resolve_disc_for_runtime(const std::filesystem::pat
|
||||
|
||||
std::filesystem::path cached = read_cached_path(argv0, "disc.cfg");
|
||||
if (!cached.empty()) {
|
||||
cached = normalize_disc_path_for_launch(cached);
|
||||
cached = resolve_persisted_disc_path(cached, exe_dir_from_argv(argv0));
|
||||
}
|
||||
if (!cached.empty() && std::filesystem::exists(cached) &&
|
||||
validate_disc_for_launch(cached, game_id)) {
|
||||
@@ -3483,6 +3521,160 @@ static void netplay_soft_exit(const char *origin) {
|
||||
std::exit(0);
|
||||
}
|
||||
|
||||
/* "address:port" as the netplay library reads it: the port follows
|
||||
* the last colon and must be a number up to 65535, an empty address means
|
||||
* every address of this computer, and a name is looked up (IPv4). Returns 0
|
||||
* and the address, or -1 for a text the library refuses too. */
|
||||
static int netplay_hostport_addr(const char* hostport, sockaddr_in* out) {
|
||||
const char* colon = hostport ? std::strrchr(hostport, ':') : nullptr;
|
||||
char host[128];
|
||||
if (!colon || (size_t)(colon - hostport) >= sizeof(host) || colon[1] == '\0')
|
||||
return -1;
|
||||
std::memcpy(host, hostport, (size_t)(colon - hostport));
|
||||
host[colon - hostport] = '\0';
|
||||
char* end = nullptr;
|
||||
const unsigned long port = std::strtoul(colon + 1, &end, 10);
|
||||
if (*end != '\0' || port > 65535ul)
|
||||
return -1;
|
||||
*out = sockaddr_in{};
|
||||
out->sin_family = AF_INET;
|
||||
out->sin_port = htons((uint16_t)port);
|
||||
if (!host[0] || std::strcmp(host, "0.0.0.0") == 0) {
|
||||
out->sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
return 0;
|
||||
}
|
||||
if (inet_pton(AF_INET, host, &out->sin_addr) == 1)
|
||||
return 0;
|
||||
addrinfo hints{};
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
hints.ai_protocol = IPPROTO_UDP;
|
||||
addrinfo* found = nullptr;
|
||||
if (getaddrinfo(host, nullptr, &hints, &found) != 0)
|
||||
return -1;
|
||||
int rc = -1;
|
||||
for (const addrinfo* r = found; r; r = r->ai_next) {
|
||||
if (r->ai_family == AF_INET && r->ai_addr &&
|
||||
(size_t)r->ai_addrlen >= sizeof(*out)) {
|
||||
out->sin_addr = ((const sockaddr_in*)r->ai_addr)->sin_addr;
|
||||
rc = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) freeaddrinfo(found);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Why a LAN netplay start failed. The netplay library answers a
|
||||
* refused bind, a listen address it cannot read and a peer address it cannot
|
||||
* use with the same -1 and no error code, so do its steps again here and keep
|
||||
* the system's answer: the error number and its text. The listen address is
|
||||
* read and a name looked up as the library does, then bound. No SO_REUSEADDR,
|
||||
* so a port another program holds is reported; the library's own socket is
|
||||
* closed by now. Only when the listen address could be opened is the peer
|
||||
* address read, so the peer is named as the cause only when it is one.
|
||||
* `tried` receives the address that was bound, as numbers. */
|
||||
static int netplay_bind_probe(const char* bind_hostport, const char* peer_hostport,
|
||||
char* tried, size_t tried_cap, int* sys_error,
|
||||
char* sys_text, size_t sys_cap) {
|
||||
if (sys_error) *sys_error = 0;
|
||||
if (sys_text && sys_cap) sys_text[0] = '\0';
|
||||
if (tried && tried_cap) tried[0] = '\0';
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa;
|
||||
WSAStartup(MAKEWORD(2, 2), &wsa);
|
||||
#endif
|
||||
sockaddr_in addr{};
|
||||
if (netplay_hostport_addr(bind_hostport, &addr) != 0)
|
||||
return NETPLAY_BIND_BAD_ADDRESS;
|
||||
if (tried && tried_cap) {
|
||||
char ip[INET_ADDRSTRLEN] = "";
|
||||
inet_ntop(AF_INET, &addr.sin_addr, ip, sizeof(ip));
|
||||
std::snprintf(tried, tried_cap, "%s:%u", ip, (unsigned)ntohs(addr.sin_port));
|
||||
}
|
||||
int err = 0;
|
||||
int result = NETPLAY_BIND_OK;
|
||||
#ifdef _WIN32
|
||||
const SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s == INVALID_SOCKET) {
|
||||
err = WSAGetLastError();
|
||||
result = NETPLAY_BIND_NO_SOCKET;
|
||||
} else {
|
||||
if (bind(s, (const sockaddr*)&addr, sizeof(addr)) != 0) {
|
||||
err = WSAGetLastError();
|
||||
result = NETPLAY_BIND_FAILED;
|
||||
}
|
||||
closesocket(s);
|
||||
}
|
||||
if (err && sys_text && sys_cap) {
|
||||
FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, (DWORD)err, 0, sys_text, (DWORD)sys_cap, nullptr);
|
||||
for (size_t n = std::strlen(sys_text);
|
||||
n && (sys_text[n - 1] == '\n' || sys_text[n - 1] == '\r' ||
|
||||
sys_text[n - 1] == ' ' || sys_text[n - 1] == '.'); --n)
|
||||
sys_text[n - 1] = '\0';
|
||||
}
|
||||
#else
|
||||
const int s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s < 0) {
|
||||
err = errno;
|
||||
result = NETPLAY_BIND_NO_SOCKET;
|
||||
} else {
|
||||
if (bind(s, (const sockaddr*)&addr, sizeof(addr)) != 0) {
|
||||
err = errno;
|
||||
result = NETPLAY_BIND_FAILED;
|
||||
}
|
||||
close(s);
|
||||
}
|
||||
if (err && sys_text && sys_cap)
|
||||
std::snprintf(sys_text, sys_cap, "%s", std::strerror(err));
|
||||
#endif
|
||||
if (sys_error) *sys_error = err;
|
||||
if (result != NETPLAY_BIND_OK)
|
||||
return result;
|
||||
/* The library refuses a peer address it cannot read, one with port 0 and
|
||||
* a name it cannot look up. */
|
||||
if (peer_hostport && peer_hostport[0]) {
|
||||
sockaddr_in peer{};
|
||||
if (netplay_hostport_addr(peer_hostport, &peer) != 0 || peer.sin_port == 0)
|
||||
return NETPLAY_BIND_PEER_BAD;
|
||||
}
|
||||
return NETPLAY_BIND_OK;
|
||||
}
|
||||
|
||||
/* A netplay start failed: print what failed and return the sentence for the
|
||||
* player. The old line named the build first ("built without
|
||||
* recomp-net, or bind/peer invalid") for every failure; a player whose system
|
||||
* held the port read that the build had no netplay. For a LAN start the
|
||||
* library's steps are done again here and the system's answer is reported. */
|
||||
static const char* netplay_start_failure(int nrc, const PsxNetplayConfig& cfg) {
|
||||
#if defined(PSX_HAS_RECOMP_NET)
|
||||
const int netplay_built = 1;
|
||||
#else
|
||||
const int netplay_built = 0;
|
||||
#endif
|
||||
static char why[NETPLAY_START_FAILURE_CAP];
|
||||
char sys_text[160] = "";
|
||||
char tried[32] = "";
|
||||
int sys_error = 0;
|
||||
const int bind_probe = (netplay_built && nrc == -3)
|
||||
? netplay_bind_probe(cfg.bind_hostport, cfg.peer_hostport, tried, sizeof(tried),
|
||||
&sys_error, sys_text, sizeof(sys_text))
|
||||
: NETPLAY_BIND_NOT_TRIED;
|
||||
/* The sentence names the address that was bound: a name is shown as the
|
||||
* address it stands for. */
|
||||
netplay_start_failure_text(nrc, netplay_built,
|
||||
tried[0] ? tried : cfg.bind_hostport,
|
||||
cfg.peer_hostport, bind_probe, sys_error, sys_text,
|
||||
why, sizeof(why));
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: netplay start failed (%d) — %s%s%s%s (slot=%d bind=%s peer=%s)\n",
|
||||
nrc, why, sys_text[0] ? " [system: " : "", sys_text,
|
||||
sys_text[0] ? "]" : "", cfg.local_slot, cfg.bind_hostport,
|
||||
cfg.peer_hostport);
|
||||
return why;
|
||||
}
|
||||
|
||||
static void shutdown_runtime(void) {
|
||||
/* (sljit removed 2026-07-15: overlay_compile_worker_stop joined the
|
||||
* off-thread JIT worker here; the worker no longer exists.) */
|
||||
@@ -13225,7 +13417,12 @@ namespace {
|
||||
gi->assist_binding_count = PSX_ASSIST_BIND_COUNT;
|
||||
gi->has_skip_fmv = skip_fmv_offered_b ? 1 : 0;
|
||||
gi->has_turbo_loads = turbo_loads_offered_b ? 1 : 0;
|
||||
gi->has_geometry_precision = 1;
|
||||
/* The Perspective textures row. Hidden for a title that ships PGXP
|
||||
* through the psx.enhancement.pgxp mod ([video] pgxp_mod_only): the
|
||||
* Mods page is then its one switch, and the row's value is not
|
||||
* applied, so showing it would be a second control that does
|
||||
* nothing. */
|
||||
gi->has_geometry_precision = g_video_pgxp_mod_only ? 0 : 1;
|
||||
gi->has_rewind_depth = 1;
|
||||
#if defined(RECOMP_LAUNCHER_HAS_INTERNAL_RESOLUTION)
|
||||
/* The Supersampling row becomes an Internal resolution list. */
|
||||
@@ -13794,6 +13991,11 @@ int main(int argc, char** argv) {
|
||||
gc.runtime.video_perspective_texturing ? 1 : 0;
|
||||
g_video_pgxp_cpu_mode = gc.runtime.video_pgxp_cpu_mode ? 1 : 0;
|
||||
g_video_pgxp_tolerance = (float)gc.runtime.video_pgxp_tolerance;
|
||||
g_video_pgxp_position_fallback =
|
||||
gc.runtime.video_pgxp_position_fallback ? 1 : 0;
|
||||
g_video_pgxp_preserve_projection =
|
||||
gc.runtime.video_pgxp_preserve_projection ? 1 : 0;
|
||||
g_video_pgxp_mod_only = gc.runtime.video_pgxp_mod_only ? 1 : 0;
|
||||
g_video_renderer = gc.runtime.video_renderer;
|
||||
g_video_screen = gc.runtime.video_screen_kind;
|
||||
g_video_scanlines = gc.runtime.video_scanlines;
|
||||
@@ -13802,6 +14004,8 @@ int main(int argc, char** argv) {
|
||||
g_video_aspect_num = gc.runtime.video_aspect_num;
|
||||
g_video_aspect_den = gc.runtime.video_aspect_den;
|
||||
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);
|
||||
g_video_vsync = gc.runtime.video_vsync;
|
||||
g_frame_interpolation = gc.runtime.video_frame_interpolation ? 1 : 0;
|
||||
g_frame_interpolation_fps = gc.runtime.video_frame_interpolation_fps;
|
||||
@@ -13940,6 +14144,17 @@ int main(int argc, char** argv) {
|
||||
gpu_ws_set_angle_sites(
|
||||
addresses.data(), expected.data(), (int)addresses.size());
|
||||
}
|
||||
{
|
||||
/* [[draw_distance.clamp]]: stored for the interpreter; the
|
||||
* switch stays off until a mod's activation turns it on. */
|
||||
std::vector<PSXDrawDistanceClampSite> sites;
|
||||
sites.reserve(gc.draw_distance_clamp_sites.size());
|
||||
for (const auto& site : gc.draw_distance_clamp_sites)
|
||||
sites.push_back({site.address, site.expected, site.reg,
|
||||
site.max});
|
||||
(void)psx_draw_distance_set_clamp_sites(sites.data(),
|
||||
(int)sites.size());
|
||||
}
|
||||
{
|
||||
std::vector<uint32_t> addresses, expected, thresholds;
|
||||
std::vector<uint32_t> object_regs, x_regs, z_regs, y_regs;
|
||||
@@ -14270,12 +14485,12 @@ int main(int argc, char** argv) {
|
||||
g_hotkey_pad_fast_forward_toggle = normalize_hotkey_pad_binding(
|
||||
us.hotkey_pad_fast_forward_toggle, 0);
|
||||
if (us.has_bios_path && !bios_from_cli && !us.bios_path.empty()) {
|
||||
settings_bios_storage = us.bios_path.string();
|
||||
settings_bios_storage = anchor_on_exe_dir(argv[0], us.bios_path).string();
|
||||
bios_path = settings_bios_storage.c_str();
|
||||
bios_explicit = true;
|
||||
}
|
||||
if (us.has_disc_path && !disc_override_path)
|
||||
resolved_disc = normalize_disc_path_for_launch(us.disc_path);
|
||||
resolved_disc = resolve_persisted_disc_path(us.disc_path, exe_dir_from_argv(argv[0]));
|
||||
/* Multi-disc precedence. [disc] selected is authoritative ONLY WHEN
|
||||
* PRESENT; absent it, [disc] path decides and the index is derived
|
||||
* from it.
|
||||
@@ -14308,9 +14523,11 @@ int main(int argc, char** argv) {
|
||||
if (idx >= 0) selected_disc_index = idx + 1;
|
||||
}
|
||||
}
|
||||
if (us.has_memcard_dir) memcard_dir = us.memcard_dir;
|
||||
if (us.has_memcard1_path) memcard1_path = us.memcard1_path;
|
||||
if (us.has_memcard2_path) memcard2_path = us.memcard2_path;
|
||||
/* Relative [memcard] values anchor on the exe directory; the
|
||||
* per-game options file also lives in memcard_dir. */
|
||||
if (us.has_memcard_dir) memcard_dir = anchor_on_exe_dir(argv[0], us.memcard_dir);
|
||||
if (us.has_memcard1_path) memcard1_path = anchor_on_exe_dir(argv[0], us.memcard1_path);
|
||||
if (us.has_memcard2_path) memcard2_path = anchor_on_exe_dir(argv[0], us.memcard2_path);
|
||||
if (us.has_memcard1_enabled) memcard1_enabled = us.memcard1_enabled;
|
||||
if (us.has_memcard2_enabled) memcard2_enabled = us.memcard2_enabled;
|
||||
if (us.has_multitap_enabled) multitap_enabled = us.multitap_enabled;
|
||||
@@ -15226,10 +15443,14 @@ int main(int argc, char** argv) {
|
||||
|
||||
lr = rui_rc;
|
||||
|
||||
if (lr == 0) {
|
||||
/* The launcher hands back the player's edits on Quit as well as on
|
||||
* Play (recomp_launcher.h: "*io still holds the edits"), so both
|
||||
* fold them into seed. Disc choice stays Play-only: Quit launches
|
||||
* nothing. */
|
||||
if (lr == 0 || lr == 1) {
|
||||
seed.netplay_player_name = ls.netplay_player_name;
|
||||
seed.has_netplay_player_name = true;
|
||||
if (rui_out_disc[0]) {
|
||||
if (lr == 0 && rui_out_disc[0]) {
|
||||
seed.disc_path = rui_out_disc;
|
||||
seed.has_disc_path = true;
|
||||
}
|
||||
@@ -15238,7 +15459,7 @@ int main(int argc, char** argv) {
|
||||
* launcher sees it as an ordinary settings row. Written for
|
||||
* multi-disc titles only -- a single-disc game has nothing to
|
||||
* select and should not grow a meaningless key. */
|
||||
if (game_discs.size() > 1 && ls.disc_index > 0) {
|
||||
if (lr == 0 && game_discs.size() > 1 && ls.disc_index > 0) {
|
||||
selected_disc_index = ls.disc_index;
|
||||
seed.disc_index = ls.disc_index;
|
||||
seed.has_disc_index = true;
|
||||
@@ -15482,6 +15703,9 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
|
||||
if (lr == 1) {
|
||||
/* Keep settings changed and then dismissed with Quit. */
|
||||
PSXRecompV4::save_user_settings(
|
||||
exe_dir_from_argv(argv[0]) / "settings.toml", seed);
|
||||
std::fprintf(stdout, "psxrecomp: launcher closed; exiting.\n");
|
||||
if (overlay_init_thread.joinable())
|
||||
overlay_init_thread.join();
|
||||
@@ -15643,8 +15867,8 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
memcard1_enabled = seed.memcard1_enabled;
|
||||
memcard2_enabled = seed.memcard2_enabled;
|
||||
if (seed.has_memcard1_path) memcard1_path = seed.memcard1_path;
|
||||
if (seed.has_memcard2_path) memcard2_path = seed.memcard2_path;
|
||||
if (seed.has_memcard1_path) memcard1_path = anchor_on_exe_dir(argv[0], seed.memcard1_path);
|
||||
if (seed.has_memcard2_path) memcard2_path = anchor_on_exe_dir(argv[0], seed.memcard2_path);
|
||||
if (seed.has_language) resolved_language = seed.language;
|
||||
{
|
||||
const int n = std::min(PSX_MAX_PLAYERS,
|
||||
@@ -15980,17 +16204,31 @@ session_reboot:
|
||||
/* Env overrides (debug/validation path, like PSX_BIOS_HLE): arm the
|
||||
* corrections from process start so free-running (headless) boots can be
|
||||
* measured from the first projected vertex — a TCP toggle always arrives
|
||||
* after the interesting window. '0' = off, anything else = on. */
|
||||
if (const char* e = std::getenv("PSX_GEOMETRY_CORRECTION"))
|
||||
g_video_geometry_correction = (*e && *e != '0') ? 1 : 0;
|
||||
if (const char* e = std::getenv("PSX_PERSPECTIVE_TEXTURING"))
|
||||
g_video_perspective_texturing = (*e && *e != '0') ? 1 : 0;
|
||||
if (const char* e = std::getenv("PSX_PGXP_CPU_MODE"))
|
||||
g_video_pgxp_cpu_mode = (*e && *e != '0') ? 1 : 0;
|
||||
gte_geometry_correction_set(g_video_geometry_correction);
|
||||
gpu_texture_correction_set(g_video_perspective_texturing);
|
||||
pgxp_set_cpu_mode(g_video_pgxp_cpu_mode);
|
||||
pgxp_set_tolerance(g_video_pgxp_tolerance);
|
||||
* after the interesting window. '0' = off, anything else = on. They win
|
||||
* over the PGXP mod too, so an A/B run can switch it off. */
|
||||
/* The [video] baseline plus the psx.enhancement.pgxp request this
|
||||
* session's activation recorded (start_mod_session ran above, for the
|
||||
* first boot and for a rematch); psx_pgxp_session_arm takes that request,
|
||||
* so it cannot outlive this session. Applying the baseline alone here
|
||||
* used to switch an enabled mod straight back off. The g_video_* values
|
||||
* stay the player's settings: the mod never leaks into a launcher seed.
|
||||
* With [video] pgxp_mod_only the mod is the one switch (pgxp_session.h). */
|
||||
PSXPgxpSessionConfig pgxp_cfg{};
|
||||
pgxp_cfg.video_geometry = g_video_geometry_correction;
|
||||
pgxp_cfg.video_texture = g_video_perspective_texturing;
|
||||
pgxp_cfg.video_cpu_mode = g_video_pgxp_cpu_mode;
|
||||
pgxp_cfg.tolerance = g_video_pgxp_tolerance;
|
||||
pgxp_cfg.position_fallback = g_video_pgxp_position_fallback;
|
||||
pgxp_cfg.preserve_projection = g_video_pgxp_preserve_projection;
|
||||
pgxp_cfg.mod_only = g_video_pgxp_mod_only;
|
||||
PSXPgxpSessionInputs pgxp_in{};
|
||||
const PSXPgxpSessionArm pgxp_arm = psx_pgxp_session_arm(&pgxp_cfg, &pgxp_in);
|
||||
if (pgxp_in.env_geometry >= 0) g_video_geometry_correction = pgxp_in.env_geometry;
|
||||
if (pgxp_in.env_texture >= 0) g_video_perspective_texturing = pgxp_in.env_texture;
|
||||
if (pgxp_in.env_cpu_mode >= 0) g_video_pgxp_cpu_mode = pgxp_in.env_cpu_mode;
|
||||
/* [video] texture_window_batching A/B (same image, fewer GL draws). */
|
||||
if (const char* e = std::getenv("PSX_GL_TEXWIN_BATCH"))
|
||||
gl_renderer_set_texture_window_batching((*e && *e != '0') ? 1 : 0);
|
||||
/* Scanlines: env override wins over config, same as the corrections above,
|
||||
* so a headless/free-run boot can be captured with the effect armed from the
|
||||
* first present. PSX_SCANLINES=0/1; PSX_SCANLINE_STRENGTH=0..1. Pushed to the
|
||||
@@ -16003,12 +16241,14 @@ session_reboot:
|
||||
}
|
||||
gl_renderer_set_scanlines(g_video_scanlines ? 1 : 0,
|
||||
g_video_scanline_strength);
|
||||
if (g_video_geometry_correction || g_video_perspective_texturing) {
|
||||
if (pgxp_arm.geometry || pgxp_arm.texture) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: geometry correction %s, perspective texturing %s%s\n",
|
||||
g_video_geometry_correction ? "on" : "off",
|
||||
g_video_perspective_texturing ? "on" : "off",
|
||||
(g_video_geometry_correction && requested_scale < 2)
|
||||
"psxrecomp: geometry correction %s, perspective texturing %s%s%s%s\n",
|
||||
pgxp_arm.geometry ? "on" : "off",
|
||||
pgxp_arm.texture ? "on" : "off",
|
||||
pgxp_arm.culling ? ", precise culling" : "",
|
||||
pgxp_in.mod_enabled ? " (PGXP mod)" : "",
|
||||
(pgxp_arm.geometry && requested_scale < 2)
|
||||
? " (needs [video] supersampling >= 2 to be visible)" : "");
|
||||
}
|
||||
/* Display aspect. Identity at the default 4:3. The present letterbox uses
|
||||
@@ -16692,22 +16932,26 @@ session_reboot:
|
||||
s_netplay_present_sim_watermark = 0; /* §74: sim restarts per session */
|
||||
const int nrc = psx_netplay_start(&net_cfg);
|
||||
if (nrc != 0) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: netplay start failed (%d) — built without recomp-net, "
|
||||
"or bind/peer invalid (slot=%d bind=%s peer=%s)\n",
|
||||
nrc, net_cfg.local_slot, net_cfg.bind_hostport, net_cfg.peer_hostport);
|
||||
return 1;
|
||||
const char* const why = netplay_start_failure(nrc, net_cfg);
|
||||
if (!g_netplay_from_lobby)
|
||||
return 1;
|
||||
/* A match from the launcher returns to the room, where the
|
||||
* status line shows the sentence. No session is open and the
|
||||
* guest is not entered: see the scheduler. */
|
||||
netplay_soft_exit("netplay_start_failed");
|
||||
g_netplay_exit_reason_text = why;
|
||||
} else {
|
||||
apply_netplay_local_viewport_aspect(net_cfg.enabled);
|
||||
std::printf("psxrecomp: netplay transport=%s slot=%d input_player=%d delay=%d "
|
||||
"force_turn=%d bind=%s peer=%s session=%u\n",
|
||||
psx_netplay_transport_name(),
|
||||
net_cfg.local_slot, net_cfg.input_player, net_cfg.input_delay,
|
||||
net_cfg.force_turn ? 1 : 0,
|
||||
net_cfg.bind_hostport,
|
||||
(std::strcmp(psx_netplay_transport_name(), "ice") == 0)
|
||||
? "(ice)" : net_cfg.peer_hostport,
|
||||
(unsigned)net_cfg.session_id);
|
||||
}
|
||||
apply_netplay_local_viewport_aspect(net_cfg.enabled);
|
||||
std::printf("psxrecomp: netplay transport=%s slot=%d input_player=%d delay=%d "
|
||||
"force_turn=%d bind=%s peer=%s session=%u\n",
|
||||
psx_netplay_transport_name(),
|
||||
net_cfg.local_slot, net_cfg.input_player, net_cfg.input_delay,
|
||||
net_cfg.force_turn ? 1 : 0,
|
||||
net_cfg.bind_hostport,
|
||||
(std::strcmp(psx_netplay_transport_name(), "ice") == 0)
|
||||
? "(ice)" : net_cfg.peer_hostport,
|
||||
(unsigned)net_cfg.session_id);
|
||||
}
|
||||
|
||||
/* Initialize CPU state. */
|
||||
@@ -17058,6 +17302,10 @@ session_reboot:
|
||||
}
|
||||
}
|
||||
|
||||
/* A match whose start failed has no session and must not run the guest.
|
||||
* Go back to the room; do not boot the game offline. */
|
||||
if (psx_return_to_lobby_requested() && g_netplay_from_lobby)
|
||||
goto soft_return_lobby;
|
||||
psx_scheduler_run(&cpu);
|
||||
if (psx_return_to_lobby_requested() && g_netplay_from_lobby)
|
||||
goto soft_return_lobby;
|
||||
|
||||
+34
-37
@@ -20,13 +20,12 @@
|
||||
|
||||
extern uint64_t s_frame_count;
|
||||
|
||||
/* FMV-activity detector: host display-frame stamp of the newest colour
|
||||
* (15/24-bit) MDEC decode. Used only by mdec_recently_active() for local
|
||||
* frontend / rewind policy — never folded into netplay digests. */
|
||||
static uint64_t mdec_last_color_decode_frame = (uint64_t)0 - 1000u;
|
||||
/* Guest-cycle stamp of the same event — snap age is relative to this so
|
||||
* peers with identical FIFO/tables but different present rates hash equal. */
|
||||
static uint64_t mdec_last_color_decode_cycle = (uint64_t)0 - 1000u;
|
||||
/* FMV-activity detector: guest-cycle stamp of the newest colour (15/24-bit)
|
||||
* MDEC decode, read by mdec_recently_active() / mdec_color_age_cycles(). The
|
||||
* snapshot carries its age, so peers with identical FIFO/tables but different
|
||||
* present rates hash equal. MDEC_NEVER_DECODED = no colour decode yet. */
|
||||
#define MDEC_NEVER_DECODED ((uint64_t)0 - 1000u)
|
||||
static uint64_t mdec_last_color_decode_cycle = MDEC_NEVER_DECODED;
|
||||
|
||||
enum {
|
||||
MDEC_CMD_NOP = 0,
|
||||
@@ -691,7 +690,6 @@ static void execute_decode(void) {
|
||||
mdec.decode_input_pos = pos;
|
||||
/* FMV detector: stamp colour (15/24-bit) decodes only — streamed video.
|
||||
* The 4/8-bit luma path above is texture decompression, not video. */
|
||||
mdec_last_color_decode_frame = s_frame_count;
|
||||
mdec_last_color_decode_cycle = psx_cycle_count;
|
||||
trace_event(MDEC_EVT_DECODE_DONE, mdec.output_size);
|
||||
}
|
||||
@@ -784,7 +782,7 @@ int mdec_recently_active(uint32_t within_frames) {
|
||||
}
|
||||
|
||||
uint64_t mdec_color_age_cycles(void) {
|
||||
if (mdec_last_color_decode_cycle == (uint64_t)0 - 1000u)
|
||||
if (mdec_last_color_decode_cycle == MDEC_NEVER_DECODED)
|
||||
return (uint64_t)0 - 1u;
|
||||
if (psx_cycle_count < mdec_last_color_decode_cycle)
|
||||
return (uint64_t)0 - 1u;
|
||||
@@ -796,10 +794,9 @@ void mdec_init(void) {
|
||||
memset(mdec_trace, 0, sizeof(mdec_trace));
|
||||
mdec_trace_seq = 0;
|
||||
mdec_trace_head = 0;
|
||||
/* Rematch resets s_frame_count; a stale stamp makes mdec_recently_active
|
||||
* wrap and lie for the whole next session. */
|
||||
mdec_last_color_decode_frame = (uint64_t)0 - 1000u;
|
||||
mdec_last_color_decode_cycle = (uint64_t)0 - 1000u;
|
||||
/* A rematch resets the guest clock; a stale stamp would make
|
||||
* mdec_recently_active lie for the whole next session. */
|
||||
mdec_last_color_decode_cycle = MDEC_NEVER_DECODED;
|
||||
for (int i = 0; i < 64; i++) {
|
||||
mdec.y_quant[i] = 1;
|
||||
mdec.uv_quant[i] = 1;
|
||||
@@ -1057,7 +1054,14 @@ void mdec_debug_dma_out_end(uint32_t addr, uint32_t words) {
|
||||
}
|
||||
|
||||
/* ---- boot_state snapshot (variable-length input/output FIFOs) ------------ */
|
||||
#define MDEC_SNAP_VER 1u
|
||||
/* v2: the colour-decode age is MDEC_SNAP_AGE_NEVER when nothing was decoded.
|
||||
* v1 wrote age 1000 for that case, which a load turned into "decoded 1000
|
||||
* cycles ago" -- a false FMV for the next ~8 frames after every snapshot load
|
||||
* made before the first video (issue #475). v1 payloads still load. */
|
||||
#define MDEC_SNAP_VER 2u
|
||||
#define MDEC_SNAP_VER_V1 1u
|
||||
#define MDEC_SNAP_AGE_NEVER UINT64_MAX
|
||||
#define MDEC_SNAP_V1_AGE_NEVER 1000ull
|
||||
#define MDEC_SNAP_INPUT_MAX (4u * 1024u * 1024u) /* halfwords */
|
||||
#define MDEC_SNAP_OUTPUT_MAX (8u * 1024u * 1024u) /* bytes */
|
||||
|
||||
@@ -1116,10 +1120,11 @@ void mdec_snapshot_write(uint8_t *p) {
|
||||
(void)pst_w_u32(&w, mdec.input_count);
|
||||
(void)pst_w_u32(&w, mdec.output_size);
|
||||
/* Guest-cycle age (not host s_frame_count) — netplay aux digests this blob. */
|
||||
if (psx_cycle_count >= mdec_last_color_decode_cycle)
|
||||
age = psx_cycle_count - mdec_last_color_decode_cycle;
|
||||
if (mdec_last_color_decode_cycle == MDEC_NEVER_DECODED ||
|
||||
psx_cycle_count < mdec_last_color_decode_cycle)
|
||||
age = MDEC_SNAP_AGE_NEVER;
|
||||
else
|
||||
age = 1000ull;
|
||||
age = psx_cycle_count - mdec_last_color_decode_cycle;
|
||||
(void)pst_w_u64(&w, age);
|
||||
for (uint32_t i = 0; i < mdec.input_count; i++)
|
||||
(void)pst_w_u16(&w, mdec.input ? mdec.input[i] : 0u);
|
||||
@@ -1144,7 +1149,8 @@ static uint32_t mdec_wire_u32(const uint8_t *p, uint32_t off) {
|
||||
int mdec_snapshot_validate(const uint8_t *p, uint32_t len) {
|
||||
uint32_t input_count, output_size;
|
||||
if (!p || len < mdec_snap_fixed_bytes()) return 0;
|
||||
if (mdec_wire_u32(p, 0u) != MDEC_SNAP_VER) return 0;
|
||||
if (mdec_wire_u32(p, 0u) != MDEC_SNAP_VER &&
|
||||
mdec_wire_u32(p, 0u) != MDEC_SNAP_VER_V1) return 0;
|
||||
input_count = mdec_wire_u32(p, MDEC_SNAP_OFF_COUNTS);
|
||||
output_size = mdec_wire_u32(p, MDEC_SNAP_OFF_COUNTS + 4u);
|
||||
if (input_count > MDEC_SNAP_INPUT_MAX || output_size > MDEC_SNAP_OUTPUT_MAX)
|
||||
@@ -1157,11 +1163,12 @@ int mdec_snapshot_validate(const uint8_t *p, uint32_t len) {
|
||||
int mdec_snapshot_read(const uint8_t *p, uint32_t len) {
|
||||
PstR r;
|
||||
uint32_t ver = 0, input_count = 0, output_size = 0, reserved;
|
||||
uint64_t age = 1000ull;
|
||||
uint64_t age = MDEC_SNAP_AGE_NEVER;
|
||||
int16_t s16;
|
||||
if (!mdec_snapshot_validate(p, len)) return 0;
|
||||
pst_r_init(&r, p, len);
|
||||
if (!pst_r_u32(&r, &ver) || ver != MDEC_SNAP_VER) return 0;
|
||||
if (!pst_r_u32(&r, &ver) ||
|
||||
(ver != MDEC_SNAP_VER && ver != MDEC_SNAP_VER_V1)) return 0;
|
||||
if (!pst_r_u32(&r, &mdec.command) ||
|
||||
!pst_r_u32(&r, &mdec.expected_halfwords) ||
|
||||
!pst_r_u32(&r, &mdec.last_status) ||
|
||||
@@ -1213,24 +1220,14 @@ int mdec_snapshot_read(const uint8_t *p, uint32_t len) {
|
||||
}
|
||||
if (output_size && !pst_r_bytes(&r, mdec.output, output_size))
|
||||
return 0;
|
||||
/* Age is guest cycles since last colour decode (SNAP_VER=1 payload). */
|
||||
if (age > (1ull << 40))
|
||||
age = (1ull << 40);
|
||||
if (age >= psx_cycle_count)
|
||||
mdec_last_color_decode_cycle = 0;
|
||||
/* Age is guest cycles since the last colour decode. "Never" stays never:
|
||||
* restoring it as a recent decode reports FMV that is not playing. A decode
|
||||
* older than the restored clock's origin cannot be recent either. */
|
||||
if (ver == MDEC_SNAP_VER_V1 && age == MDEC_SNAP_V1_AGE_NEVER)
|
||||
age = MDEC_SNAP_AGE_NEVER;
|
||||
if (age == MDEC_SNAP_AGE_NEVER || age > psx_cycle_count)
|
||||
mdec_last_color_decode_cycle = MDEC_NEVER_DECODED;
|
||||
else
|
||||
mdec_last_color_decode_cycle = psx_cycle_count - age;
|
||||
/* Refresh host-frame hysteresis for local FMV policy only (~1 frame ≈
|
||||
* 338688 cycles @ NTSC). Cap so recently_active stays meaningful. */
|
||||
{
|
||||
const uint64_t cycles_per_frame = 338688ull;
|
||||
uint64_t frames_ago = age / cycles_per_frame;
|
||||
if (frames_ago > 100000ull)
|
||||
frames_ago = 100000ull;
|
||||
if (frames_ago >= s_frame_count)
|
||||
mdec_last_color_decode_frame = 0;
|
||||
else
|
||||
mdec_last_color_decode_frame = s_frame_count - frames_ago;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,16 @@
|
||||
* Sub-pixel vertex precision + perspective-correct texturing is a property of
|
||||
* the emulated GTE/GPU pair, not of any particular disc, so it ships here and
|
||||
* mods/builtin/packages/psx.enhancement.pgxp targets game_id "*". Default off
|
||||
* (the faithful floor, docs/ENHANCEMENTS.md G1.9); enabling arms the value-
|
||||
* propagation engine (pgxp.cpp) with the user-validated defaults — tolerance
|
||||
* clamp 0.5px, cpu-mode per the mod option.
|
||||
* (the faithful floor, docs/ENHANCEMENTS.md G1.9); enabling arms geometry and
|
||||
* texture correction on the value-propagation engine (pgxp.cpp), cpu-mode and
|
||||
* precise culling (G1.12) per the mod options. The title's other [video] PGXP
|
||||
* keys (the tolerance clamp, default 0.5px) still apply.
|
||||
*
|
||||
* Activation only RECORDS the request (pgxp_mod_request). It runs before the
|
||||
* renderer setup in main.cpp, whose session arming (pgxp_session.cpp) takes
|
||||
* the request and arms the engine from it and the [video] baseline. Arming
|
||||
* here directly used to be undone by that baseline, so the mod did nothing
|
||||
* (docs/ENHANCEMENTS.md G1.11).
|
||||
*
|
||||
* Coverage note: the engine reaches near-total dataflow coverage on a binary
|
||||
* compiled with the PGXP hook variant (-DPSX_PGXP=1, PSX_PGXP_VARIANT); on a
|
||||
@@ -21,10 +28,6 @@
|
||||
|
||||
#define PKG_PGXP "psx.enhancement.pgxp"
|
||||
|
||||
/* gte.cpp / gpu.c — the two correction toggles this mod arms. */
|
||||
extern void gte_geometry_correction_set(int enabled);
|
||||
extern void gpu_texture_correction_set(int enabled);
|
||||
|
||||
static int pgxp_option_flag(const char* feature, const char* id) {
|
||||
char text[16] = "";
|
||||
return psx_mod_option_value(PKG_PGXP, feature, id, text, sizeof text) &&
|
||||
@@ -32,9 +35,8 @@ static int pgxp_option_flag(const char* feature, const char* id) {
|
||||
}
|
||||
|
||||
static void builtin_pgxp_activate(void) {
|
||||
gte_geometry_correction_set(1);
|
||||
gpu_texture_correction_set(1); /* also arms the shadow engine */
|
||||
pgxp_set_cpu_mode(pgxp_option_flag("pgxp", "cpu_mode"));
|
||||
pgxp_mod_request(1, pgxp_option_flag("pgxp", "cpu_mode"),
|
||||
pgxp_option_flag("pgxp", "culling"));
|
||||
}
|
||||
|
||||
PSX_MOD_CONSTRUCTOR(psx_register_builtin_pgxp_plugin) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "netplay_exit_reason.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
@@ -36,6 +37,129 @@ const char *netplay_exit_reason_text(const char *origin)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* What a refused bind means, from the system's error number. The numbers of
|
||||
* Windows, Linux and macOS do not overlap for these three causes. */
|
||||
enum {
|
||||
BIND_ERROR_OTHER = 0,
|
||||
BIND_ERROR_IN_USE, /* WSAEADDRINUSE 10048, EADDRINUSE 98 (Linux), 48 (macOS) */
|
||||
BIND_ERROR_NOT_ALLOWED, /* WSAEACCES 10013, EACCES 13 */
|
||||
BIND_ERROR_NO_SUCH_ADDR /* WSAEADDRNOTAVAIL 10049, EADDRNOTAVAIL 99 (Linux), 49 (macOS) */
|
||||
};
|
||||
|
||||
static int bind_error_kind(int sys_error)
|
||||
{
|
||||
switch (sys_error) {
|
||||
case 10048: case 98: case 48: return BIND_ERROR_IN_USE;
|
||||
case 10013: case 13: return BIND_ERROR_NOT_ALLOWED;
|
||||
case 10049: case 99: case 49: return BIND_ERROR_NO_SUCH_ADDR;
|
||||
default: return BIND_ERROR_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
/* The system's own text goes into a sentence only when it is short and plain
|
||||
* ASCII: a text in the system's language and code page is not shown as
|
||||
* broken letters, and a long one is not cut in the middle. */
|
||||
static int sys_text_fits(const char *text)
|
||||
{
|
||||
size_t n = 0;
|
||||
if (!text || !text[0]) return 0;
|
||||
for (; text[n]; ++n) {
|
||||
if ((unsigned char)text[n] < 0x20 || (unsigned char)text[n] > 0x7e)
|
||||
return 0;
|
||||
}
|
||||
return n <= 80;
|
||||
}
|
||||
|
||||
void netplay_start_failure_text(int start_rc, int has_netplay,
|
||||
const char *bind_hostport,
|
||||
const char *peer_hostport, int bind_probe,
|
||||
int sys_error, const char *sys_text,
|
||||
char *out, size_t cap)
|
||||
{
|
||||
char address[64] = "";
|
||||
char system[112] = "";
|
||||
const char *port = "";
|
||||
const char *colon;
|
||||
if (!out || !cap) return;
|
||||
out[0] = '\0';
|
||||
if (!bind_hostport) bind_hostport = "";
|
||||
if (!peer_hostport) peer_hostport = "";
|
||||
/* "address:port": the port is what follows the last colon. No address
|
||||
* means every address of this computer. */
|
||||
colon = strrchr(bind_hostport, ':');
|
||||
if (colon && (size_t)(colon - bind_hostport) < sizeof(address)) {
|
||||
memcpy(address, bind_hostport, (size_t)(colon - bind_hostport));
|
||||
address[colon - bind_hostport] = '\0';
|
||||
port = colon + 1;
|
||||
if (!address[0]) snprintf(address, sizeof(address), "0.0.0.0");
|
||||
}
|
||||
/* "(system error N)" or "(system error N: the system's text)". */
|
||||
if (sys_text_fits(sys_text))
|
||||
snprintf(system, sizeof(system), "(system error %d: %s)", sys_error, sys_text);
|
||||
else
|
||||
snprintf(system, sizeof(system), "(system error %d)", sys_error);
|
||||
if (!has_netplay) {
|
||||
snprintf(out, cap, "This build has no netplay: it was built without "
|
||||
"the netplay library.");
|
||||
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED &&
|
||||
bind_error_kind(sys_error) == BIND_ERROR_NOT_ALLOWED) {
|
||||
/* Access denied. The system does not hand the port out: a port range
|
||||
* that Windows keeps for Hyper-V or WSL, a port another program
|
||||
* holds for itself alone on Windows, or a port below 1024 elsewhere.
|
||||
* "Another program holds it" would send the player looking for a
|
||||
* program that may not exist; another port helps in every case. */
|
||||
snprintf(out, cap,
|
||||
"Netplay could not open UDP port %.5s on %.15s (system error "
|
||||
"%d). The operating system does not allow this port. Choose "
|
||||
"another port and start again.",
|
||||
port, address, sys_error);
|
||||
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED &&
|
||||
bind_error_kind(sys_error) == BIND_ERROR_IN_USE) {
|
||||
snprintf(out, cap,
|
||||
"Netplay could not open UDP port %.5s on %.15s (system error "
|
||||
"%d). Another program or the operating system holds that "
|
||||
"port. Choose another port and start again.",
|
||||
port, address, sys_error);
|
||||
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED &&
|
||||
bind_error_kind(sys_error) == BIND_ERROR_NO_SUCH_ADDR) {
|
||||
/* No port is held and another port does not help: the listen address
|
||||
* is not an address of this computer (a copied command line, a
|
||||
* network that is down). */
|
||||
snprintf(out, cap,
|
||||
"Netplay could not listen on %.15s (system error %d). That "
|
||||
"address is not one of this computer's addresses. Check the "
|
||||
"listen address and start again.",
|
||||
address, sys_error);
|
||||
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED) {
|
||||
/* Any other answer: the number and the system's text, no cause. */
|
||||
snprintf(out, cap, "Netplay could not open UDP port %.5s on %.15s %s.",
|
||||
port, address, system);
|
||||
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_NO_SOCKET) {
|
||||
snprintf(out, cap, "Netplay could not open a network socket %s.", system);
|
||||
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_BAD_ADDRESS) {
|
||||
snprintf(out, cap,
|
||||
"Netplay could not start: \"%.60s\" is not an address and a "
|
||||
"port to listen on.", bind_hostport);
|
||||
} else if (start_rc == -3 && bind_probe == NETPLAY_BIND_PEER_BAD) {
|
||||
snprintf(out, cap,
|
||||
"Netplay could not start: the other player's address "
|
||||
"\"%.60s\" could not be used.", peer_hostport);
|
||||
} else if (start_rc == -3) {
|
||||
/* The listen address opened and the peer address reads well, or
|
||||
* nothing was tried: no cause is known. */
|
||||
snprintf(out, cap,
|
||||
"Netplay could not start its connection on %.60s.",
|
||||
bind_hostport);
|
||||
} else if (start_rc == -4) {
|
||||
/* Five causes share this code (a missing peer or relay setting, a
|
||||
* relay start that failed, a build without the relay library); the
|
||||
* code does not say which. */
|
||||
snprintf(out, cap, "Netplay could not start the online connection.");
|
||||
} else {
|
||||
snprintf(out, cap, "Netplay could not start (code %d).", start_rc);
|
||||
}
|
||||
}
|
||||
|
||||
int netplay_boot_mismatch_final(uint32_t mismatch_since_ms, uint32_t now_ms,
|
||||
uint32_t grace_ms)
|
||||
{
|
||||
|
||||
@@ -362,6 +362,11 @@ static int s_diff_mode = 0;
|
||||
* entry while every other validated candidate follows its normal live route. */
|
||||
static uint32_t s_diff_addr = 0;
|
||||
static int s_in_shadow = 0;
|
||||
/* gte.cpp holds PGXP's precise culling off while a shadow diff runs: it reads
|
||||
* host-only shadows, which the native (speculative) pass does not record, so
|
||||
* the two passes would see different NCLIP results (docs/ENHANCEMENTS.md
|
||||
* G1.12). */
|
||||
int psx_overlay_shadow_diff_active(void) { return s_in_shadow; }
|
||||
/* Candidate whose shadow NATIVE pass is currently executing (NULL outside it).
|
||||
* Set ONLY around run_shadow_diff's native pass — never during the interp pass,
|
||||
* which must stay pure interp. Lets the CPS continuation re-entry path below
|
||||
|
||||
+187
-10
@@ -36,6 +36,7 @@
|
||||
#include "cpu_state.h"
|
||||
#include "psx_memory.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
@@ -78,6 +79,9 @@ static uint32_t s_gen = 1;
|
||||
static int s_enabled = 0;
|
||||
static int s_cpu_mode = 0;
|
||||
static float s_tolerance = 0.5f; /* user-validated seam clamp (G1.10) */
|
||||
static int s_position_fallback = 1; /* G1.4 cache tier (G1.11 switch) */
|
||||
static int s_preserve_projection = 0; /* exact-projection shadows (G1.11) */
|
||||
static int s_culling = 0; /* precise NCLIP sign (G1.12) */
|
||||
static uint32_t s_suppress = 0;
|
||||
static int s_deferred_invalidate = 0;
|
||||
|
||||
@@ -126,6 +130,7 @@ extern "C" void pgxp_set_enabled(int enabled) {
|
||||
}
|
||||
|
||||
extern "C" int pgxp_enabled(void) { return s_enabled; }
|
||||
extern "C" int pgxp_active(void) { return g_pgxp_active; }
|
||||
|
||||
extern "C" void pgxp_set_cpu_mode(int enabled) { s_cpu_mode = enabled ? 1 : 0; }
|
||||
extern "C" int pgxp_cpu_mode(void) { return s_cpu_mode; }
|
||||
@@ -133,6 +138,41 @@ extern "C" int pgxp_cpu_mode(void) { return s_cpu_mode; }
|
||||
extern "C" void pgxp_set_tolerance(float pixels) { s_tolerance = pixels; }
|
||||
extern "C" float pgxp_tolerance(void) { return s_tolerance; }
|
||||
|
||||
extern "C" void pgxp_set_position_fallback(int enabled) {
|
||||
s_position_fallback = enabled ? 1 : 0;
|
||||
}
|
||||
extern "C" int pgxp_position_fallback(void) { return s_position_fallback; }
|
||||
|
||||
extern "C" void pgxp_set_preserve_projection(int enabled) {
|
||||
s_preserve_projection = enabled ? 1 : 0;
|
||||
}
|
||||
extern "C" int pgxp_preserve_projection(void) { return s_preserve_projection; }
|
||||
|
||||
extern "C" void pgxp_set_culling(int enabled) { s_culling = enabled ? 1 : 0; }
|
||||
extern "C" int pgxp_culling(void) { return s_culling; }
|
||||
|
||||
static int s_mod_request = 0;
|
||||
static int s_mod_request_cpu = 0;
|
||||
static int s_mod_request_cull = 0;
|
||||
|
||||
extern "C" void pgxp_mod_request(int enabled, int cpu_mode, int culling) {
|
||||
s_mod_request = enabled ? 1 : 0;
|
||||
s_mod_request_cpu = (enabled && cpu_mode) ? 1 : 0;
|
||||
s_mod_request_cull = (enabled && culling) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int pgxp_mod_requested(int *cpu_mode, int *culling) {
|
||||
if (cpu_mode) *cpu_mode = s_mod_request_cpu;
|
||||
if (culling) *culling = s_mod_request_cull;
|
||||
return s_mod_request;
|
||||
}
|
||||
|
||||
extern "C" int pgxp_mod_request_take(int *cpu_mode, int *culling) {
|
||||
const int requested = pgxp_mod_requested(cpu_mode, culling);
|
||||
pgxp_mod_request(0, 0, 0);
|
||||
return requested;
|
||||
}
|
||||
|
||||
extern "C" void pgxp_suppress_begin(void) {
|
||||
++s_suppress;
|
||||
recompute_active();
|
||||
@@ -152,6 +192,22 @@ extern "C" void pgxp_get_stats(PGXPStats *out) {
|
||||
if (out) *out = s_stats;
|
||||
}
|
||||
|
||||
extern "C" void pgxp_note_rect_bypass(int all_precise) {
|
||||
if (all_precise) s_stats.rect_bypass++;
|
||||
else s_stats.rect_partial++;
|
||||
}
|
||||
|
||||
extern "C" void pgxp_note_nclip(int disagree, int corrected) {
|
||||
if (disagree) s_stats.nclip_disagree++;
|
||||
if (corrected) s_stats.nclip_corrected++;
|
||||
}
|
||||
|
||||
extern "C" void pgxp_note_triangle(int precise) {
|
||||
if (precise >= 3) s_stats.tri_precise++;
|
||||
else if (precise > 0) s_stats.tri_mixed++;
|
||||
else s_stats.tri_native++;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Address mapping + validation */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
@@ -807,6 +863,54 @@ extern "C" void psx_pgxp_muldiv(struct CPUState *cpu, uint32_t instr,
|
||||
/* GTE producer */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
extern "C" int pgxp_project_precise(int64_t mac1, int64_t mac2, int64_t mac3,
|
||||
int shift, int32_t ir1, int32_t ir2,
|
||||
uint32_t sz3, uint32_t h,
|
||||
int32_t ofx, int32_t ofy,
|
||||
int64_t x_num, int64_t x_den,
|
||||
int32_t *x16, int32_t *y16) {
|
||||
/* Qualify exactly the vertices whose guest projection is the plain
|
||||
* IR * (H / SZ3) of the continuous one: sf=1, no IR1/IR2 clamp (the
|
||||
* stored IR equals MAC >> 12), SZ3 unclamped and nonzero, and no UNR
|
||||
* divide overflow (the GTE saturates the quotient when H >= 2*SZ3). */
|
||||
if (shift != 12 || x_den <= 0) return 0;
|
||||
if ((mac1 >> 12) != (int64_t)ir1 || (mac2 >> 12) != (int64_t)ir2) return 0;
|
||||
const int64_t z_int = mac3 >> 12;
|
||||
if (z_int <= 0 || z_int > 0xFFFF || (uint32_t)z_int != sz3) return 0;
|
||||
if ((int64_t)h >= 2 * z_int) return 0;
|
||||
/* (MAC/4096) * H / (MAC3/4096): the 4096s cancel. Double keeps ~52 bits,
|
||||
* far below the 1/65536 px the shadow carries. Host-only and visual-only,
|
||||
* so cross-platform bit equality of this value is not required. */
|
||||
const double k = (double)h * 65536.0 / (double)mac3;
|
||||
double fx = (double)ofx +
|
||||
(double)mac1 * k * (double)x_num / (double)x_den;
|
||||
double fy = (double)ofy + (double)mac2 * k;
|
||||
/* Same transport bound as the IR path (gte.cpp): an extreme value must
|
||||
* not wrap an int32 and masquerade as an on-screen shadow. */
|
||||
const double lim = 4096.0 * 65536.0;
|
||||
if (fx < -lim) fx = -lim;
|
||||
if (fx > lim - 1.0) fx = lim - 1.0;
|
||||
if (fy < -lim) fy = -lim;
|
||||
if (fy > lim - 1.0) fy = lim - 1.0;
|
||||
*x16 = (int32_t)std::floor(fx);
|
||||
*y16 = (int32_t)std::floor(fy);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Defined below with the GPU consumer. */
|
||||
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half);
|
||||
|
||||
extern "C" int pgxp_ppp_accept(int32_t x16, int32_t y16, uint32_t packed) {
|
||||
const int16_t hx = (int16_t)(packed & 0xFFFFu);
|
||||
const int16_t hy = (int16_t)(packed >> 16);
|
||||
if (pgxp_agrees(x16, hx, hx) && pgxp_agrees(y16, hy, hy)) {
|
||||
s_stats.ppp_produced++;
|
||||
return 1;
|
||||
}
|
||||
s_stats.ppp_window_fallback++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" void pgxp_gte_push_sxy(int32_t x16, int32_t y16, uint16_t sz3,
|
||||
uint32_t packed) {
|
||||
if (!g_pgxp_active) return;
|
||||
@@ -840,6 +944,35 @@ extern "C" int pgxp_get_gte_sxy_checked(uint32_t index, uint32_t expect,
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Defined below with the GPU consumer. */
|
||||
static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x,
|
||||
int32_t int_y, uint32_t word);
|
||||
|
||||
extern "C" int pgxp_gte_nclip_precise(const uint32_t sxy[3], int64_t *cross) {
|
||||
if (!g_pgxp_active) return 0;
|
||||
int64_t x[3], y[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const PGXPValue *pv = &s_gte[12 + i];
|
||||
if (!pv_live(pv) || (pv->flags & PGXP_F_VXY) != PGXP_F_VXY ||
|
||||
pv->value != sxy[i])
|
||||
return 0;
|
||||
/* The GPU parses the packet half as the same signed integer for every
|
||||
* value the GTE can produce (its saturation limits are -0x400 and
|
||||
* 0x3FF), so the register half stands in for the native parse. */
|
||||
if (pgxp_accept(pv->x16, pv->y16, (int16_t)(sxy[i] & 0xFFFFu),
|
||||
(int16_t)(sxy[i] >> 16), sxy[i]) != 0)
|
||||
return 0;
|
||||
x[i] = pv->x16;
|
||||
y[i] = pv->y16;
|
||||
}
|
||||
/* NCLIP's determinant, sx0*(sy1-sy2) + sx1*(sy2-sy0) + sx2*(sy0-sy1),
|
||||
* on 16.16 positions. Positions are bounded to +-2^28 (the transport
|
||||
* clamp), so every product fits in 64 bits. */
|
||||
*cross = (x[1] - x[0]) * (y[2] - y[0]) - (y[1] - y[0]) * (x[2] - x[0]);
|
||||
s_stats.nclip_precise++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) {
|
||||
/* Invalidation-class bookkeeping: runs even with the engine disarmed so
|
||||
* seeded/leftover shadows can never outlive a guest register write. Only
|
||||
@@ -874,6 +1007,40 @@ 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);
|
||||
}
|
||||
|
||||
/* Truncation agreement for one axis. `native` is the GPU's parse of the
|
||||
* packet half, `half` the raw 16-bit half in the word. Exact (integer part
|
||||
* equals the native parse) unless preserve-projection is on; then the precise
|
||||
* position may sit a bounded distance from it (PGXP_PPP_AGREE_*), except
|
||||
* when the half is at or beyond the GTE saturation limits: there the guest
|
||||
* integer is a clamp that says nothing about the vertex, or (beyond them) a
|
||||
* CPU-modified word the GPU's 11-bit parse wraps. */
|
||||
static inline int pgxp_agrees(int32_t p16, int32_t native, int16_t half) {
|
||||
if (!s_preserve_projection || half <= -0x400 || half >= 0x3FF)
|
||||
return (p16 >> 16) == native;
|
||||
const int64_t d = (int64_t)p16 - (int64_t)native * 65536;
|
||||
return d > -(int64_t)PGXP_PPP_AGREE_BELOW * 65536 &&
|
||||
d < (int64_t)PGXP_PPP_AGREE_ABOVE * 65536;
|
||||
}
|
||||
|
||||
/* The consumer-side safeguards on one candidate position: truncation
|
||||
* agreement on both axes, then the tolerance clamp. Returns 0 when accepted,
|
||||
* 1 for a truncation reject, 2 for a tolerance reject. Shared by the GPU
|
||||
* lookup and the precise NCLIP so culling and drawing believe exactly the
|
||||
* same vertices. */
|
||||
static inline int pgxp_accept(int32_t px, int32_t py, int32_t int_x,
|
||||
int32_t int_y, uint32_t word) {
|
||||
if (!pgxp_agrees(px, int_x, (int16_t)(word & 0xFFFFu)) ||
|
||||
!pgxp_agrees(py, int_y, (int16_t)(word >> 16)))
|
||||
return 1;
|
||||
if (s_tolerance >= 0.0f) {
|
||||
float dx = (float)((int64_t)px - (int64_t)int_x * 65536) * (1.0f / 65536.0f);
|
||||
float dy = (float)((int64_t)py - (int64_t)int_y * 65536) * (1.0f / 65536.0f);
|
||||
if (std::fabs(dx) > s_tolerance || std::fabs(dy) > s_tolerance)
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -899,7 +1066,8 @@ extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
}
|
||||
}
|
||||
|
||||
if (!have && gte_geometry_correction_lookup(packet_word, &px, &py)) {
|
||||
if (!have && s_position_fallback &&
|
||||
gte_geometry_correction_lookup(packet_word, &px, &py)) {
|
||||
pz = 0; /* fallback never carries depth */
|
||||
have = PGXP_SRC_FALLBACK;
|
||||
}
|
||||
@@ -913,17 +1081,15 @@ extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
py = field_rebase(py, packet_word >> 16, int_y);
|
||||
/* Truncation agreement: the GPU parsed 11-bit integers out of the
|
||||
* packet; a precise position whose integer part disagrees (a
|
||||
* wrapped/CPU-modified coordinate) must not be believed. */
|
||||
if ((px >> 16) != int_x || (py >> 16) != int_y) {
|
||||
* wrapped/CPU-modified coordinate) must not be believed. Then the
|
||||
* tolerance clamp. */
|
||||
const int why = pgxp_accept(px, py, int_x, int_y, packet_word);
|
||||
if (why == 1) {
|
||||
s_stats.trunc_reject++;
|
||||
have = 0;
|
||||
} else if (s_tolerance >= 0.0f) {
|
||||
float dx = (float)(px - (int_x << 16)) * (1.0f / 65536.0f);
|
||||
float dy = (float)(py - (int_y << 16)) * (1.0f / 65536.0f);
|
||||
if (dx > s_tolerance || dy > s_tolerance) {
|
||||
s_stats.tolerance_reject++;
|
||||
have = 0;
|
||||
}
|
||||
} else if (why == 2) {
|
||||
s_stats.tolerance_reject++;
|
||||
have = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -944,6 +1110,17 @@ extern "C" int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
return have;
|
||||
}
|
||||
|
||||
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);
|
||||
s_stats = saved;
|
||||
return src;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* Legacy v14 SWC2 tracker — kept as the base-flavour feed */
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* pgxp_session.cpp -- the per-session PGXP arming main.cpp runs
|
||||
* (docs/ENHANCEMENTS.md G1.11/G1.12). See pgxp_session.h for the sequence.
|
||||
* Kept out of main.cpp so runtime/tests/test_pgxp_session.cpp runs exactly
|
||||
* this code against the real mod runtime and builtin plugin. */
|
||||
#include "pgxp_session.h"
|
||||
#include "pgxp.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" void gte_geometry_correction_set(int enabled);
|
||||
extern "C" void gpu_texture_correction_set(int enabled);
|
||||
|
||||
extern "C" void psx_pgxp_session_reset(void) {
|
||||
pgxp_mod_request(0, 0, 0);
|
||||
}
|
||||
|
||||
extern "C" PSXPgxpSessionArm psx_pgxp_session_arm(
|
||||
const PSXPgxpSessionConfig* config, PSXPgxpSessionInputs* used) {
|
||||
PSXPgxpSessionInputs in{};
|
||||
in.env_geometry =
|
||||
psx_pgxp_session_env_flag(std::getenv("PSX_GEOMETRY_CORRECTION"));
|
||||
in.env_texture =
|
||||
psx_pgxp_session_env_flag(std::getenv("PSX_PERSPECTIVE_TEXTURING"));
|
||||
in.env_cpu_mode =
|
||||
psx_pgxp_session_env_flag(std::getenv("PSX_PGXP_CPU_MODE"));
|
||||
in.env_culling =
|
||||
psx_pgxp_session_env_flag(std::getenv("PSX_PGXP_CULLING"));
|
||||
in.video_geometry = config->video_geometry ? 1 : 0;
|
||||
in.video_texture = config->video_texture ? 1 : 0;
|
||||
in.video_cpu_mode = config->video_cpu_mode ? 1 : 0;
|
||||
in.mod_only = config->mod_only ? 1 : 0;
|
||||
/* Take, not peek: the request belongs to this session alone. */
|
||||
in.mod_enabled = pgxp_mod_request_take(&in.mod_cpu_mode, &in.mod_culling);
|
||||
|
||||
const PSXPgxpSessionArm arm = psx_pgxp_session_resolve(&in);
|
||||
pgxp_set_tolerance(config->tolerance);
|
||||
pgxp_set_position_fallback(config->position_fallback);
|
||||
pgxp_set_preserve_projection(config->preserve_projection);
|
||||
pgxp_set_culling(arm.culling);
|
||||
gte_geometry_correction_set(arm.geometry);
|
||||
gpu_texture_correction_set(arm.texture);
|
||||
pgxp_set_cpu_mode(arm.cpu_mode);
|
||||
if (used) *used = in;
|
||||
return arm;
|
||||
}
|
||||
@@ -45,6 +45,23 @@ static struct {
|
||||
} last_textured_rect;
|
||||
static struct { int calls, x, y, w, h, u0, v0, u1, v1; } last_scaled_rect;
|
||||
|
||||
/* PGXP stub state: the quad case in main() switches geometry correction on
|
||||
* and makes the packet words in a bitmask (bit n = word n at g_test_quad_base)
|
||||
* report a dataflow-precise vertex. */
|
||||
static int g_test_geometry_correction = 0;
|
||||
static uint32_t g_test_quad_base = 0xFFFFFFFFu;
|
||||
static uint32_t g_test_precise_words = 0;
|
||||
static int g_test_rect_bypass = 0;
|
||||
static int g_test_rect_partial = 0;
|
||||
|
||||
static int test_word_precise(uint32_t addr) {
|
||||
if (g_test_quad_base == 0xFFFFFFFFu || addr < g_test_quad_base) return 0;
|
||||
uint32_t word = (addr - g_test_quad_base) / 4u;
|
||||
return word < 32u && ((g_test_precise_words >> word) & 1u);
|
||||
}
|
||||
static int g_test_precise_triangles = 0;
|
||||
static int g_test_textured_triangles = 0;
|
||||
|
||||
/* Bounding box of every flat/gouraud triangle drawn since the last reset. */
|
||||
static struct { int calls, min_x, max_x, min_y, max_y; } gpu_exec_triangles;
|
||||
static inline void gpu_exec_reset_triangles(void) {
|
||||
@@ -103,14 +120,30 @@ void psx_fatal_halt(const char *reason) {
|
||||
}
|
||||
|
||||
void pgxp_set_enabled(int enabled) { (void)enabled; }
|
||||
int gte_geometry_correction_enabled(void) { return 0; }
|
||||
int gte_geometry_correction_enabled(void) { return g_test_geometry_correction; }
|
||||
int pgxp_get_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y,
|
||||
int32_t *x16, int32_t *y16, uint16_t *sz) {
|
||||
(void)addr; (void)packet_word; (void)int_x; (void)int_y;
|
||||
(void)x16; (void)y16; (void)sz;
|
||||
(void)packet_word;
|
||||
*x16 = int_x * 65536;
|
||||
*y16 = int_y * 65536;
|
||||
*sz = 0;
|
||||
if (test_word_precise(addr)) {
|
||||
*x16 += 0x4000;
|
||||
return PGXP_SRC_DATAFLOW;
|
||||
}
|
||||
return PGXP_SRC_NATIVE;
|
||||
}
|
||||
int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y) {
|
||||
(void)packet_word; (void)int_x; (void)int_y;
|
||||
return test_word_precise(addr) ? PGXP_SRC_DATAFLOW : PGXP_SRC_NATIVE;
|
||||
}
|
||||
void pgxp_note_triangle(int precise) { (void)precise; }
|
||||
void pgxp_note_rect_bypass(int all_precise) {
|
||||
if (all_precise) g_test_rect_bypass++;
|
||||
else g_test_rect_partial++;
|
||||
}
|
||||
int gte_precision_load_word(uint32_t addr, uint32_t packed,
|
||||
int32_t *x16, int32_t *y16, uint16_t *z) {
|
||||
(void)addr; (void)packed; (void)x16; (void)y16; (void)z;
|
||||
@@ -204,7 +237,8 @@ void gr_set_color_modulation(int r, int g, int b, int raw_texture) {
|
||||
}
|
||||
void gr_set_precise_triangle(int enabled, int32_t x0, int32_t y0,
|
||||
int32_t x1, int32_t y1, int32_t x2, int32_t y2) {
|
||||
(void)enabled; (void)x0; (void)y0; (void)x1; (void)y1; (void)x2; (void)y2;
|
||||
(void)x0; (void)y0; (void)x1; (void)y1; (void)x2; (void)y2;
|
||||
if (enabled) g_test_precise_triangles++;
|
||||
}
|
||||
void gr_set_perspective_triangle(int enabled, float q0, float q1, float q2) {
|
||||
(void)enabled; (void)q0; (void)q1; (void)q2;
|
||||
@@ -231,6 +265,7 @@ void gr_draw_textured_triangle(int x0, int y0, int u0, int v0,
|
||||
int x2, int y2, int u2, int v2,
|
||||
uint16_t clut_x, uint16_t clut_y,
|
||||
uint16_t texpage) {
|
||||
g_test_textured_triangles++;
|
||||
(void)x0; (void)y0; (void)u0; (void)v0; (void)x1; (void)y1; (void)u1;
|
||||
(void)v1; (void)x2; (void)y2; (void)u2; (void)v2; (void)clut_x;
|
||||
(void)clut_y; (void)texpage;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/* Production dirty-RAM interpreter decoder, built with per-instruction cycle
|
||||
* charging on, for test_interp_cop0_read_sample.c. */
|
||||
#define PSX_ENABLE_BLOCK_CYCLES 1
|
||||
#define PSX_NO_DEBUG_TOOLS 1
|
||||
#include "../src/dirty_ram_interp.c"
|
||||
|
||||
int interp_test_step(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t *next) {
|
||||
return exec_one_fetched(cpu, pc, insn, next);
|
||||
}
|
||||
@@ -99,6 +99,9 @@ void cdrom_debug_snapshot(void *out) { (void)out; }
|
||||
uint32_t cdrom_dma_sector_word_count(void) { return 0; }
|
||||
void dma_gpu_ll_start(void *s, uint32_t a, uint32_t m) { (void)s; (void)a; (void)m; }
|
||||
void dma_gpu_ll_cancel(void *s) { (void)s; }
|
||||
/* The kick's CPU hold asks for the next walk event; the stubbed start leaves
|
||||
* the walk inactive, so the hold never loops. */
|
||||
uint32_t dma_gpu_ll_cycles_to_event(const void *s) { (void)s; return 0xFFFFFFFFu; }
|
||||
void gpu_ws_begin_linked_list(void) {}
|
||||
void gpu_ws_end_linked_list(void) {}
|
||||
void gpu_ws_prepass_linked_list(uint32_t a) { (void)a; }
|
||||
|
||||
@@ -24,6 +24,12 @@ Mask runs (mode mask) change the GP0(E6) mask-check bit after a line, a flat
|
||||
triangle or an opaque textured rect and before its batch is drawn: the draw
|
||||
must keep the check bit it was submitted under, at 1x, above it and in the
|
||||
window mode.
|
||||
Texture-window runs (mode twin) draw prims that alternate GP0(E2h) windows
|
||||
with [video] texture_window_batching off and on: the native VRAM, the frame at
|
||||
S and the wide surface must be the same both ways, at 1x, above it and in the
|
||||
window mode, and on must draw in fewer batches. Each of those runs also checks
|
||||
raw rects drawn through every window against the PS1 window rule computed in C
|
||||
from the VRAM, so a decode error both ways share still fails.
|
||||
Pass runs (mode passes, where the renderer has the frame-rate stack's render
|
||||
passes) check that the window mode refuses them and the full-VRAM surface
|
||||
offers them.
|
||||
@@ -255,6 +261,29 @@ def main():
|
||||
r.stderr.strip()[-600:])
|
||||
if r.returncode or not parsed or parsed[1]:
|
||||
ok = False
|
||||
# Texture-window batching off vs on: (label, scale, env).
|
||||
for label, s, extra in (("full", 1, {}), ("full", 4, {}), ("full", 9, {}),
|
||||
("window", 9, {"PSX_GL_HIRES_WINDOW": "1"}),
|
||||
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})):
|
||||
e = dict(env)
|
||||
e.update(extra)
|
||||
got = {}
|
||||
for on in (0, 1):
|
||||
r = run([dest / "probe", s, "twin", on], env=e)
|
||||
parsed = parse_run(r.stdout)
|
||||
m = re.search(r"^twin_flushes=(\d+) batches=(\d+)$", r.stdout, re.M)
|
||||
print(f"twin {label} {s} batching={on}: exit={r.returncode}",
|
||||
r.stdout.strip().splitlines()[-5:], r.stderr.strip()[-600:])
|
||||
if r.returncode or not parsed or parsed[1] or not m:
|
||||
ok = False
|
||||
got[on] = (parsed[2] if parsed else None, parse_hires(r.stdout),
|
||||
parse_hires(r.stdout, "wide"), int(m[2]) if m else None)
|
||||
if None in got[0][:3] or got[0][:3] != got[1][:3]:
|
||||
print(f"FAIL twin {label} {s}x: batching changes the image:", got)
|
||||
ok = False
|
||||
if got[0][3] is None or got[1][3] is None or got[1][3] >= got[0][3]:
|
||||
print(f"FAIL twin {label} {s}x: batching on did not draw fewer batches:", got)
|
||||
ok = False
|
||||
# Render passes: offered on the full-VRAM surface, refused in the window mode.
|
||||
if not passes:
|
||||
print("passes: skipped (this renderer has no render passes)")
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/* The store that kicks a GPU linked-list DMA returns only when the walk ends.
|
||||
*
|
||||
* PSX-SPX "CPU Operation during DMA": the CPU waits while DMA moves data. A
|
||||
* guest that rewrites a packet right after DrawOTag therefore cannot reach it
|
||||
* before the walk does. The retail SCPH1001 intro does exactly that: it
|
||||
* re-copies its text packets from a template, clearing the link word, right
|
||||
* after the kick. With the CPU running alongside the walk, the copy reached a
|
||||
* packet first and the walk followed link 0 into low RAM.
|
||||
*
|
||||
* Here the list is A -> B -> end. Right after the kicking store returns, the
|
||||
* "CPU" clears B's header, then time runs until the walk is done:
|
||||
*
|
||||
* - held (the default): the GPU received A's and B's payload in order, and
|
||||
* guest time advanced by the walk inside the store;
|
||||
* - control: a kick made from inside the device service is not held (the
|
||||
* walk is still running when the store returns).
|
||||
*
|
||||
* Production dma.c and dma_gpu_ll.c (test_dma_gpu_list_cpu_hold.py links
|
||||
* them); memory, GPU and clock are seams, everything else aborts. */
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
void dma_init(void);
|
||||
void dma_write(uint32_t addr, uint32_t val);
|
||||
void dma_advance(uint32_t cycles);
|
||||
int dma_gpu_linked_list_active(void);
|
||||
uint32_t dma_cycles_to_internal_event(void);
|
||||
|
||||
/* ---- clock and device service ------------------------------------------ */
|
||||
uint64_t psx_cycle_count;
|
||||
uint64_t psx_next_service_cycle;
|
||||
int psx_in_device_service;
|
||||
int g_event_step_conservative;
|
||||
int g_ls_replay_active;
|
||||
int g_psx_render_pass_active;
|
||||
uint32_t g_psx_cyc_batch;
|
||||
uint32_t g_psx_cyc_batch_limit;
|
||||
|
||||
static uint64_t s_serviced_to;
|
||||
void psx_devices_service_to_now(void) {
|
||||
if (psx_in_device_service) return;
|
||||
psx_in_device_service = 1;
|
||||
const uint64_t now = psx_cycle_count;
|
||||
dma_advance((uint32_t)(now - s_serviced_to));
|
||||
s_serviced_to = now;
|
||||
const uint32_t next = dma_cycles_to_internal_event();
|
||||
psx_next_service_cycle = next == 0xFFFFFFFFu ? UINT64_MAX : now + (next ? next : 1u);
|
||||
psx_in_device_service = 0;
|
||||
}
|
||||
void psx_advance_cycles_slow(uint32_t cycles) {
|
||||
psx_cycle_count += cycles;
|
||||
psx_devices_service_to_now();
|
||||
}
|
||||
|
||||
/* ---- memory ------------------------------------------------------------- */
|
||||
uint32_t g_psx_ram_size = 0x00200000u;
|
||||
uint32_t g_psx_ram_mask = 0x001FFFFFu;
|
||||
static uint8_t ram[0x00200000u];
|
||||
uint8_t *memory_get_ram_ptr(void) { return ram; }
|
||||
uint32_t psx_read_word(uint32_t a) {
|
||||
uint32_t v; memcpy(&v, ram + (a & 0x1FFFFCu), 4); return v;
|
||||
}
|
||||
void psx_write_word(uint32_t a, uint32_t v) { memcpy(ram + (a & 0x1FFFFCu), &v, 4); }
|
||||
uint32_t psx_mod_gpu_dma_resolve_address(uint32_t a) { return a; }
|
||||
|
||||
/* ---- GPU ---------------------------------------------------------------- */
|
||||
static uint32_t s_gp0[64];
|
||||
static unsigned s_gp0_n;
|
||||
void gpu_write_gp0(uint32_t val) { if (s_gp0_n < 64u) s_gp0[s_gp0_n] = val; ++s_gp0_n; }
|
||||
void gpu_set_gp0_source(uint32_t addr) { (void)addr; }
|
||||
void gpu_set_gp0_linked_list_node(uint32_t addr, uint32_t n) { (void)addr; (void)n; }
|
||||
void gpu_ws_begin_linked_list(void) {}
|
||||
void gpu_ws_end_linked_list(void) {}
|
||||
void gpu_ws_prepass_linked_list(uint32_t start_addr) { (void)start_addr; }
|
||||
void gpu_ws_validate_linked_list_header(uint32_t addr, uint32_t h) { (void)addr; (void)h; }
|
||||
void gpu_ws_validate_linked_list_node(uint32_t addr, uint32_t n) { (void)addr; (void)n; }
|
||||
|
||||
/* ---- observers and IRQ (inert) ----------------------------------------- */
|
||||
uint32_t i_stat;
|
||||
uint32_t g_debug_current_func_addr;
|
||||
uint32_t g_debug_last_store_pc;
|
||||
uint64_t s_frame_count;
|
||||
uint64_t g_io_openbus_reads;
|
||||
uint64_t g_io_openbus_writes;
|
||||
void gpu_timeline_note(uint8_t kind, uint32_t a, uint32_t b) { (void)kind; (void)a; (void)b; }
|
||||
void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux) {
|
||||
(void)kind; (void)detail; (void)aux;
|
||||
}
|
||||
void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; }
|
||||
|
||||
/* ---- scenario ------------------------------------------------------------ */
|
||||
#define NODE_A 0x00001000u
|
||||
#define NODE_B 0x00002000u
|
||||
#define NODE_C 0x00003000u
|
||||
#define END 0x00FFFFFFu
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fputc('\n', stderr); \
|
||||
++failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static void build_list(void) {
|
||||
memset(ram, 0, sizeof ram);
|
||||
psx_write_word(NODE_A, (2u << 24) | NODE_B);
|
||||
psx_write_word(NODE_A + 4u, 0xE1000001u);
|
||||
psx_write_word(NODE_A + 8u, 0xE2000002u);
|
||||
psx_write_word(NODE_B, (1u << 24) | NODE_C);
|
||||
psx_write_word(NODE_B + 4u, 0x01000003u);
|
||||
psx_write_word(NODE_C, END);
|
||||
}
|
||||
|
||||
/* Kicks the list; `in_service` makes the kick look like one from inside the
|
||||
* device service. Returns 1 when the walk was still active after the store. */
|
||||
static int kick(int in_service) {
|
||||
dma_init();
|
||||
s_gp0_n = 0;
|
||||
psx_cycle_count = 1000u;
|
||||
s_serviced_to = psx_cycle_count;
|
||||
psx_next_service_cycle = UINT64_MAX;
|
||||
build_list();
|
||||
dma_write(0x1F8010F0u, 0x00000800u); /* DPCR: channel 2 enabled */
|
||||
dma_write(0x1F8010A0u, NODE_A); /* MADR */
|
||||
dma_write(0x1F8010A4u, 0u); /* BCR */
|
||||
psx_in_device_service = in_service;
|
||||
dma_write(0x1F8010A8u, 0x01000401u); /* CHCR: from RAM, list, start */
|
||||
psx_in_device_service = 0;
|
||||
return dma_gpu_linked_list_active();
|
||||
}
|
||||
|
||||
static void finish_walk(void) {
|
||||
for (int i = 0; i < 1000000 && dma_gpu_linked_list_active(); i++) {
|
||||
psx_cycle_count += 1u;
|
||||
psx_devices_service_to_now();
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
/* Held: the walk is over when the kicking store returns. */
|
||||
const uint64_t before = 1000u;
|
||||
const int active = kick(0);
|
||||
CHECK(!active, "walk still active after the kicking store");
|
||||
CHECK(psx_cycle_count > before, "guest time did not advance during the kick");
|
||||
psx_write_word(NODE_B, 0u); /* the guest's packet copy */
|
||||
finish_walk();
|
||||
CHECK(s_gp0_n == 3u && s_gp0[0] == 0xE1000001u && s_gp0[1] == 0xE2000002u &&
|
||||
s_gp0[2] == 0x01000003u,
|
||||
"GPU received %u words (%08X %08X %08X), expected A's and B's payload",
|
||||
s_gp0_n, s_gp0[0], s_gp0[1], s_gp0[2]);
|
||||
|
||||
/* Control: a kick from inside the device service is not held. */
|
||||
CHECK(kick(1), "a kick from inside the device service was held");
|
||||
finish_walk();
|
||||
CHECK(!dma_gpu_linked_list_active(), "control walk did not finish");
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("PASS: the GPU linked-list kick holds the CPU until the walk ends");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
"""The GPU linked-list DMA kick holds the CPU until the walk ends.
|
||||
|
||||
Links the production dma.c and dma_gpu_ll.c into test_dma_gpu_list_cpu_hold.c
|
||||
at -O0 and -O2; every other runtime seam is an abort() stub (fixture_link.py).
|
||||
"""
|
||||
import argparse
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from fixture_link import build_and_run
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--cc', default='gcc')
|
||||
args = parser.parse_args()
|
||||
here = Path(__file__).resolve().parent
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
for opt in ('-O0', '-O2'):
|
||||
build_and_run(args.cc, here, here.parent, opt, Path(root),
|
||||
['dma.c', 'dma_gpu_ll.c'], 'test_dma_gpu_list_cpu_hold.c')
|
||||
print('PASS: the GPU linked-list kick holds the CPU until the walk ends (O0/O2)')
|
||||
@@ -0,0 +1,324 @@
|
||||
/* [[draw_distance.clamp]] in the dirty-RAM interpreter.
|
||||
*
|
||||
* Generated main-EXE code clamps a listed site's register before the guard
|
||||
* runs (draw_distance_codegen_test covers the emit). Main-EXE code that runs
|
||||
* from the interpreter goes through dirty_ram_interp.c instead, which looks
|
||||
* the PC up in draw_distance.c's sorted store. This test runs the production
|
||||
* code of both files:
|
||||
*
|
||||
* - the store: physical addresses, sort, word-guarded lookup, the cap and
|
||||
* its log line, empty and NULL lists;
|
||||
* - the switch: psx_mod_set_draw_distance_clamp's return value and the
|
||||
* interpreter's live gate;
|
||||
* - real interpreted instructions (exec_one_fetched) with the switch off
|
||||
* and on: the sltiu and addiu guard forms, an unlisted twin, a
|
||||
* mismatched word, a site outside the game's text image, and a negative
|
||||
* index that the original guard still rejects.
|
||||
*
|
||||
* The sources are #included (gpu.c for the widescreen site stores the
|
||||
* interpreter also consults) so the test can call the interpreter's static
|
||||
* entry point; unreferenced code is dropped at link time (LTO plus section
|
||||
* GC / dead_strip), so only the stubs below are needed. */
|
||||
#define PSX_HAS_GAME_DISPATCH 1
|
||||
#include "../src/draw_distance.c"
|
||||
#include "../src/gpu.c"
|
||||
#include "../src/dirty_ram_interp.c"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#define dup _dup
|
||||
#define dup2 _dup2
|
||||
#define fileno _fileno
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
/* ---- link stubs for code reachable from exec_one_fetched ------------------ */
|
||||
uint64_t s_frame_count;
|
||||
uint32_t g_debug_current_func_addr;
|
||||
uint32_t g_debug_last_store_pc;
|
||||
uint32_t g_dirty_ram_code_gen;
|
||||
uint64_t g_dispatch_static_hits;
|
||||
int g_rfe_escape_pending;
|
||||
int g_exc_escape_reason;
|
||||
int g_psx_call_bail;
|
||||
uint64_t g_psx_bail_first;
|
||||
uint64_t g_psx_bail_resolved;
|
||||
uint32_t i_stat;
|
||||
uint32_t i_mask;
|
||||
uint64_t psx_cycle_count;
|
||||
uint32_t g_psx_mod_function_entry_hooks;
|
||||
uint32_t g_psx_mod_instruction_hooks;
|
||||
uint32_t g_psx_ram_size = PSX_MAIN_RAM_RETAIL_BYTES;
|
||||
uint32_t g_psx_ram_mask = PSX_MAIN_RAM_RETAIL_BYTES - 1u;
|
||||
|
||||
static uint8_t test_ram[0x00200000u];
|
||||
|
||||
uint8_t *memory_get_ram_ptr(void) { return test_ram; }
|
||||
uint8_t psx_read_byte(uint32_t a) { return test_ram[a & 0x1FFFFFu]; }
|
||||
uint16_t psx_read_half(uint32_t a) {
|
||||
uint16_t v; memcpy(&v, test_ram + (a & 0x1FFFFEu), sizeof v); return v;
|
||||
}
|
||||
uint32_t psx_read_word(uint32_t a) {
|
||||
uint32_t v; memcpy(&v, test_ram + (a & 0x1FFFFCu), sizeof v); return v;
|
||||
}
|
||||
uint8_t psx_cyc_load_byte(CPUState *cpu, uint32_t a, uint32_t rt, uint32_t m) {
|
||||
(void)cpu; (void)rt; (void)m; return psx_read_byte(a);
|
||||
}
|
||||
uint32_t psx_cyc_lwc2_read(CPUState *cpu, uint32_t a) { (void)cpu; return psx_read_word(a); }
|
||||
int dirty_ram_is_dirty(uint32_t phys) { (void)phys; return 1; }
|
||||
int mdec_recently_active(uint32_t f) { (void)f; return 0; }
|
||||
void gte_execute(CPUState *cpu, uint32_t cmd) { (void)cpu; (void)cmd; }
|
||||
void gte_precision_store_word(uint32_t a, uint8_t r) { (void)a; (void)r; }
|
||||
uint32_t gte_read_ctrl(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; }
|
||||
uint32_t gte_read_data(CPUState *cpu, uint8_t r) { (void)cpu; (void)r; return 0; }
|
||||
void gte_write_ctrl(CPUState *cpu, uint8_t r, uint32_t v) { (void)cpu; (void)r; (void)v; }
|
||||
void gte_write_data(CPUState *cpu, uint8_t r, uint32_t v) { (void)cpu; (void)r; (void)v; }
|
||||
int overlay_loader_call_native(CPUState *cpu, uint32_t a) { (void)cpu; (void)a; return 0; }
|
||||
void psx_bail_record(uint32_t ra, uint32_t sp, uint32_t pc, uint32_t gsp) {
|
||||
(void)ra; (void)sp; (void)pc; (void)gsp;
|
||||
}
|
||||
void psx_break(CPUState *cpu, uint32_t code, uint32_t pc) { (void)cpu; (void)code; (void)pc; }
|
||||
void psx_check_interrupts(struct CPUState *cpu) { (void)cpu; }
|
||||
void psx_dispatch_call(CPUState *cpu, uint32_t t, uint32_t r) { (void)cpu; (void)t; (void)r; }
|
||||
void psx_fatal_halt(const char *reason) { fprintf(stderr, "fatal: %s\n", reason); exit(2); }
|
||||
void psx_get_freeze_diag(uint64_t *a, uint32_t *b, int *c, int *d, uint64_t *e,
|
||||
uint64_t *f) {
|
||||
(void)a; (void)b; (void)c; (void)d; (void)e; (void)f;
|
||||
}
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
int psx_interrupts_checked_at_current_cycle(uint32_t pc) { (void)pc; return 1; }
|
||||
void psx_pgxp_alu(struct CPUState *c, uint32_t i, uint32_t r, uint32_t a, uint32_t b) {
|
||||
(void)c; (void)i; (void)r; (void)a; (void)b;
|
||||
}
|
||||
void psx_pgxp_cop2(struct CPUState *c, uint32_t i, uint32_t v, uint32_t a) {
|
||||
(void)c; (void)i; (void)v; (void)a;
|
||||
}
|
||||
void psx_pgxp_load(struct CPUState *c, uint32_t i, uint32_t a, uint32_t v) {
|
||||
(void)c; (void)i; (void)a; (void)v;
|
||||
}
|
||||
void psx_pgxp_muldiv(struct CPUState *c, uint32_t i, uint32_t h, uint32_t l,
|
||||
uint32_t a, uint32_t b) {
|
||||
(void)c; (void)i; (void)h; (void)l; (void)a; (void)b;
|
||||
}
|
||||
void psx_pgxp_store(struct CPUState *c, uint32_t i, uint32_t a, uint32_t v) {
|
||||
(void)c; (void)i; (void)a; (void)v;
|
||||
}
|
||||
void psx_rfe_mark_escape(void) {}
|
||||
int psx_syscall(CPUState *cpu, uint32_t code) { (void)cpu; (void)code; return 0; }
|
||||
int psx_dispatch_game_compiled(CPUState *cpu, uint32_t a) { (void)cpu; (void)a; return 0; }
|
||||
int psx_game_is_function_entry(uint32_t a) { (void)a; return 0; }
|
||||
int psx_game_text_native_ok(uint32_t a) { (void)a; return 0; }
|
||||
int psx_game_text_native_ok_full(uint32_t a) { (void)a; return 0; }
|
||||
int psx_mod_function_entry(struct CPUState *cpu, uint32_t a) { (void)cpu; (void)a; return 0; }
|
||||
void psx_mod_instruction(struct CPUState *cpu, uint32_t a, uint32_t i) { (void)cpu; (void)a; (void)i; }
|
||||
|
||||
/* The game's text image for this test: [TEXT_LO, TEXT_HI). */
|
||||
#define TEXT_LO 0x80020000u
|
||||
#define TEXT_HI 0x80030000u
|
||||
int psx_game_address_in_text(uint32_t addr) {
|
||||
const uint32_t phys = addr & 0x1FFFFFFFu;
|
||||
return phys >= (TEXT_LO & 0x1FFFFFFFu) && phys < (TEXT_HI & 0x1FFFFFFFu);
|
||||
}
|
||||
|
||||
/* ---- harness --------------------------------------------------------------- */
|
||||
static int failures;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fputc('\n', stderr); \
|
||||
++failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
enum { AT = 1, V0 = 2, T2 = 10 };
|
||||
#define SLTIU_T2_V0 0x2C4A01C0u /* sltiu t2, v0, 0x1C0 */
|
||||
#define ADDIU_AT_V0 0x2441FFFFu /* addiu at, v0, -1 */
|
||||
#define SLTIU_AT_AT 0x2C2101BFu /* sltiu at, at, 0x1BF */
|
||||
|
||||
static void step(CPUState *cpu, uint32_t pc, uint32_t insn) {
|
||||
uint32_t next = 0;
|
||||
cpu->pc = pc;
|
||||
(void)exec_one_fetched(cpu, pc, insn, &next);
|
||||
}
|
||||
|
||||
static void put_word(uint32_t addr, uint32_t v) { memcpy(test_ram + (addr & 0x1FFFFFu), &v, 4); }
|
||||
|
||||
static const char *s_capture_path = "draw_distance_interp_stderr.txt";
|
||||
|
||||
static void capture_stderr(void (*fn)(void), char *buf, size_t cap) {
|
||||
FILE *tmp = fopen(s_capture_path, "w+b");
|
||||
buf[0] = '\0';
|
||||
if (!tmp) {
|
||||
CHECK(0, "cannot open the stderr capture file %s", s_capture_path);
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
fflush(stderr);
|
||||
int saved = dup(fileno(stderr));
|
||||
dup2(fileno(tmp), fileno(stderr));
|
||||
fn();
|
||||
fflush(stderr);
|
||||
dup2(saved, fileno(stderr));
|
||||
close(saved);
|
||||
rewind(tmp);
|
||||
size_t n = fread(buf, 1, cap - 1, tmp);
|
||||
buf[n] = '\0';
|
||||
fclose(tmp);
|
||||
remove(s_capture_path);
|
||||
}
|
||||
|
||||
/* ---- store ----------------------------------------------------------------- */
|
||||
static PSXDrawDistanceClampSite big[300];
|
||||
static void set_300(void) { (void)psx_draw_distance_set_clamp_sites(big, 300); }
|
||||
|
||||
static void test_store(void) {
|
||||
const PSXDrawDistanceClampSite sites[] = {
|
||||
{ 0x80020010u, SLTIU_T2_V0, V0, 0x1BF },
|
||||
{ 0xA0020000u, ADDIU_AT_V0, V0, 0x1BF }, /* KSEG1 spelling */
|
||||
{ 0x00020008u, SLTIU_T2_V0, V0, 0x100 }, /* KUSEG spelling */
|
||||
};
|
||||
CHECK(psx_draw_distance_set_clamp_sites(sites, 3) == 3, "three stored");
|
||||
CHECK(psx_draw_distance_clamp_site_count() == 3, "count 3");
|
||||
CHECK(s_sites[0].address == 0x00020000u && s_sites[1].address == 0x00020008u &&
|
||||
s_sites[2].address == 0x00020010u,
|
||||
"stored sorted and physical");
|
||||
const PSXDrawDistanceClampSite *s = psx_draw_distance_clamp_find(0x80020000u, ADDIU_AT_V0);
|
||||
CHECK(s && s->reg == V0 && s->max == 0x1BF, "KSEG0 lookup of a KSEG1 entry");
|
||||
CHECK(psx_draw_distance_clamp_find(0x00020008u, SLTIU_T2_V0) != NULL,
|
||||
"KUSEG lookup");
|
||||
CHECK(psx_draw_distance_clamp_find(0x80020010u, ADDIU_AT_V0) == NULL,
|
||||
"another word at a listed address misses");
|
||||
CHECK(psx_draw_distance_clamp_find(0x80020004u, SLTIU_T2_V0) == NULL,
|
||||
"between entries misses");
|
||||
CHECK(psx_draw_distance_clamp_find(0x80020014u, SLTIU_T2_V0) == NULL,
|
||||
"above the last entry misses");
|
||||
CHECK(psx_draw_distance_clamp_find(0x8001FFFCu, SLTIU_T2_V0) == NULL,
|
||||
"below the first entry misses");
|
||||
|
||||
CHECK(psx_draw_distance_set_clamp_sites(NULL, 5) == 0, "NULL list stores none");
|
||||
CHECK(psx_draw_distance_set_clamp_sites(sites, -1) == 0, "negative count stores none");
|
||||
CHECK(psx_draw_distance_clamp_find(0x80020010u, SLTIU_T2_V0) == NULL, "empty store misses");
|
||||
|
||||
for (int i = 0; i < 300; i++) {
|
||||
big[i].address = 0x80040000u + 4u * (uint32_t)(299 - i);
|
||||
big[i].expected = SLTIU_T2_V0;
|
||||
big[i].reg = V0;
|
||||
big[i].max = 0x1BF;
|
||||
}
|
||||
char log[512];
|
||||
capture_stderr(set_300, log, sizeof log);
|
||||
CHECK(psx_draw_distance_clamp_site_count() == PSX_DRAW_DISTANCE_CLAMP_SITES_MAX,
|
||||
"over-long list stored %d", psx_draw_distance_clamp_site_count());
|
||||
CHECK(strstr(log, "lists 300 sites; keeping the first 256") != NULL,
|
||||
"overflow not logged (got \"%s\")", log);
|
||||
int hits = 0;
|
||||
for (int i = 0; i < 300; i++)
|
||||
hits += psx_draw_distance_clamp_find(big[i].address, SLTIU_T2_V0) != NULL;
|
||||
CHECK(hits == 256, "%d of the first 256 kept", hits);
|
||||
}
|
||||
|
||||
/* ---- switch ---------------------------------------------------------------- */
|
||||
static void test_switch(void) {
|
||||
(void)psx_draw_distance_set_clamp_sites(NULL, 0);
|
||||
CHECK(psx_mod_set_draw_distance_clamp(1) == 0, "no sites: the setter says so");
|
||||
CHECK(psx_mod_draw_distance_clamp_enabled(), "the switch is still recorded");
|
||||
CHECK(!g_psx_draw_distance_clamp_live, "no sites: interpreter gate stays off");
|
||||
const PSXDrawDistanceClampSite one = { 0x80020000u, SLTIU_T2_V0, V0, 0x1BF };
|
||||
(void)psx_draw_distance_set_clamp_sites(&one, 1);
|
||||
CHECK(g_psx_draw_distance_clamp_live, "sites after the switch: gate on");
|
||||
CHECK(psx_mod_set_draw_distance_clamp(0) == 1, "sites: the setter says so");
|
||||
CHECK(!g_psx_draw_distance_clamp_live && !g_psx_draw_distance_clamp,
|
||||
"off clears both");
|
||||
CHECK(psx_mod_set_draw_distance_clamp(7) == 1 && g_psx_draw_distance_clamp == 1u,
|
||||
"any nonzero is on");
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
}
|
||||
|
||||
/* ---- interpreted instructions ---------------------------------------------- */
|
||||
static void test_interpreter(void) {
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof cpu);
|
||||
const uint32_t site = TEXT_LO + 0x100u, twin = TEXT_LO + 0x200u;
|
||||
const uint32_t a_site = TEXT_LO + 0x300u, a_guard = a_site + 4u;
|
||||
const uint32_t outside = TEXT_HI + 0x100u;
|
||||
put_word(site, SLTIU_T2_V0);
|
||||
put_word(twin, SLTIU_T2_V0);
|
||||
put_word(a_site, ADDIU_AT_V0);
|
||||
put_word(a_guard, SLTIU_AT_AT);
|
||||
put_word(outside, SLTIU_T2_V0);
|
||||
const PSXDrawDistanceClampSite sites[] = {
|
||||
{ site, SLTIU_T2_V0, V0, 0x1BF },
|
||||
{ a_site, ADDIU_AT_V0, V0, 0x1BF },
|
||||
{ outside, SLTIU_T2_V0, V0, 0x1BF },
|
||||
};
|
||||
(void)psx_draw_distance_set_clamp_sites(sites, 3);
|
||||
|
||||
/* Off: the original guard rejects the far index and keeps the register. */
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, site, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[T2] == 0u && cpu.gpr[V0] == 0x300u, "off: vanilla reject");
|
||||
|
||||
(void)psx_mod_set_draw_distance_clamp(1);
|
||||
/* On: kept in the last slot. */
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, site, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x1BFu && cpu.gpr[T2] == 1u, "on: clamped and kept (v0=%X t2=%u)",
|
||||
cpu.gpr[V0], cpu.gpr[T2]);
|
||||
/* Near indices are untouched. */
|
||||
cpu.gpr[V0] = 0x40u;
|
||||
step(&cpu, site, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x40u && cpu.gpr[T2] == 1u, "on: near index unchanged");
|
||||
/* The same instruction at an unlisted address stays vanilla. */
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, twin, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x300u && cpu.gpr[T2] == 0u, "on: unlisted twin vanilla");
|
||||
/* Another word at a listed address stays vanilla. */
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, site, ADDIU_AT_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x300u && cpu.gpr[AT] == 0x2FFu,
|
||||
"on: mismatched word vanilla");
|
||||
/* Outside the game's text image (captured overlay code): vanilla. */
|
||||
cpu.gpr[V0] = 0x300u;
|
||||
step(&cpu, outside, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0x300u && cpu.gpr[T2] == 0u, "on: outside text vanilla");
|
||||
|
||||
/* The addiu form: `addiu at,v0,-1; sltiu at,at,0x1BF` rejects 0 (too
|
||||
* near) and >= 0x1C0 (too far). The clamp keeps far, never near. */
|
||||
cpu.gpr[V0] = 0x900u;
|
||||
step(&cpu, a_site, ADDIU_AT_V0);
|
||||
step(&cpu, a_guard, SLTIU_AT_AT);
|
||||
CHECK(cpu.gpr[V0] == 0x1BFu && cpu.gpr[AT] == 1u, "on: addiu form keeps far");
|
||||
cpu.gpr[V0] = 0u;
|
||||
step(&cpu, a_site, ADDIU_AT_V0);
|
||||
step(&cpu, a_guard, SLTIU_AT_AT);
|
||||
CHECK(cpu.gpr[V0] == 0u && cpu.gpr[AT] == 0u, "on: addiu form still rejects near");
|
||||
/* A negative (wrapped) index is left for the original guard. */
|
||||
cpu.gpr[V0] = 0xFFFFFFF0u;
|
||||
step(&cpu, site, SLTIU_T2_V0);
|
||||
CHECK(cpu.gpr[V0] == 0xFFFFFFF0u && cpu.gpr[T2] == 0u, "on: negative index vanilla");
|
||||
|
||||
(void)psx_mod_set_draw_distance_clamp(0);
|
||||
cpu.gpr[V0] = 0x900u;
|
||||
step(&cpu, a_site, ADDIU_AT_V0);
|
||||
step(&cpu, a_guard, SLTIU_AT_AT);
|
||||
CHECK(cpu.gpr[V0] == 0x900u && cpu.gpr[AT] == 0u, "off again: vanilla");
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc > 1) s_capture_path = argv[1];
|
||||
test_store();
|
||||
test_switch();
|
||||
test_interpreter();
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("PASS draw-distance clamp store, switch and interpreted sites");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""psxrecomp_add_game_runtime(... PGXP) builds the title's one runtime as the
|
||||
PGXP hook flavor (docs/ENHANCEMENTS.md G1.11).
|
||||
|
||||
Runs the REAL psxrecomp_add_game_runtime body from runtime.cmake under
|
||||
`cmake -P`, with psxrecomp_add_runtime_target and the target-scoped commands
|
||||
stubbed to record what they receive. Checks that:
|
||||
|
||||
1. without PGXP nothing PGXP-related is forwarded (default unchanged);
|
||||
2. PGXP is forwarded to psxrecomp_add_runtime_target, as the bare option
|
||||
that selects the hook flavor for the primary (no PGXP_CLONE, so no _pgxp
|
||||
suffix and no sibling);
|
||||
3. PGXP written after CODEGEN_SETUP_SOURCES is still the option, not a
|
||||
source file (it used to be swallowed by the multi-value argument);
|
||||
4. the setup-host path (no generated C) forwards it the same way.
|
||||
"""
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
RUNTIME_CMAKE = REPO / "runtime" / "runtime.cmake"
|
||||
|
||||
|
||||
def extract_function(text: str, name: str) -> str:
|
||||
m = re.search(r"^function\(%s\b.*?^endfunction\(\)" % re.escape(name),
|
||||
text, re.S | re.M)
|
||||
if not m:
|
||||
raise SystemExit("FAIL: %s not found in runtime.cmake" % name)
|
||||
return m.group(0)
|
||||
|
||||
|
||||
STUBS = r'''
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
set(CMAKE_CURRENT_SOURCE_DIR "@SRC@")
|
||||
set(PSXRECOMP_ROOT "@SRC@/psxrecomp")
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
set(PSX_RECOMP_UI OFF)
|
||||
function(psxrecomp_add_runtime_target target)
|
||||
set_property(GLOBAL PROPERTY RT_TARGET "${target}")
|
||||
set_property(GLOBAL PROPERTY RT_ARGS "${ARGN}")
|
||||
endfunction()
|
||||
function(psx_split_exclude_builtin_mods ids tail)
|
||||
set(${ids} "${ARGN}" PARENT_SCOPE)
|
||||
set(${tail} "" PARENT_SCOPE)
|
||||
endfunction()
|
||||
function(target_compile_definitions)
|
||||
endfunction()
|
||||
function(target_include_directories)
|
||||
endfunction()
|
||||
'''
|
||||
|
||||
REPORT = r'''
|
||||
get_property(_t GLOBAL PROPERTY RT_TARGET)
|
||||
get_property(_a GLOBAL PROPERTY RT_ARGS)
|
||||
message(STATUS "RT_TARGET=${_t}")
|
||||
message(STATUS "RT_ARGS=${_a}")
|
||||
'''
|
||||
|
||||
|
||||
def run_case(cmake: str, body: str, call: str, game_c: bool):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
tmp = Path(td)
|
||||
(tmp / "generated").mkdir()
|
||||
if game_c:
|
||||
(tmp / "generated" / "GAME_dispatch.c").write_text("/* stub */\n")
|
||||
(tmp / "generated" / "GAME_full.c").write_text("/* stub */\n")
|
||||
(tmp / "VERSION").write_text("1.2.3\n")
|
||||
driver = tmp / "driver.cmake"
|
||||
driver.write_text(STUBS.replace("@SRC@", tmp.as_posix()) + body +
|
||||
"\n" + call + "\n" + REPORT)
|
||||
r = subprocess.run([cmake, "-P", str(driver)], capture_output=True,
|
||||
text=True)
|
||||
out = r.stdout + r.stderr
|
||||
if r.returncode != 0:
|
||||
return None, None, out
|
||||
target = re.search(r"RT_TARGET=(.*)", out).group(1).strip()
|
||||
args = re.search(r"RT_ARGS=(.*)", out).group(1).strip().split(";")
|
||||
return target, args, out
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--cmake", default=shutil.which("cmake") or "cmake")
|
||||
args = ap.parse_args()
|
||||
if shutil.which(args.cmake) is None and not Path(args.cmake).is_file():
|
||||
print("SKIP: no cmake executable available (%s)" % args.cmake)
|
||||
return 0
|
||||
body = extract_function(RUNTIME_CMAKE.read_text(encoding="utf-8"),
|
||||
"psxrecomp_add_game_runtime")
|
||||
failures = 0
|
||||
|
||||
def check(ok, what, out=""):
|
||||
nonlocal failures
|
||||
print(("ok " if ok else "FAIL ") + what)
|
||||
if not ok:
|
||||
failures += 1
|
||||
if out:
|
||||
print(out)
|
||||
|
||||
base = ('psxrecomp_add_game_runtime(psx-runtime\n'
|
||||
' WINDOW_TITLE "Probe"\n'
|
||||
' GEN_MARKER "generated/GAME_dispatch.c"\n'
|
||||
' GEN_FULL_GLOB "generated/GAME_full.c"\n'
|
||||
' @EXTRA@)')
|
||||
|
||||
t, a, out = run_case(args.cmake, body, base.replace("@EXTRA@", ""), True)
|
||||
check(a is not None, "configure without PGXP runs", out)
|
||||
if a is not None:
|
||||
check(t == "psx-runtime", "the primary target is psx-runtime")
|
||||
check("PGXP" not in a and "PGXP_CLONE" not in a,
|
||||
"without PGXP nothing PGXP-related is forwarded")
|
||||
|
||||
t, a, out = run_case(args.cmake, body, base.replace("@EXTRA@", "PGXP"), True)
|
||||
check(a is not None, "configure with PGXP runs", out)
|
||||
if a is not None:
|
||||
check(t == "psx-runtime", "PGXP keeps the primary target name")
|
||||
check(a.count("PGXP") == 1, "PGXP is forwarded once")
|
||||
check("PGXP_CLONE" not in a,
|
||||
"the primary is not marked as the _pgxp clone (no suffix)")
|
||||
|
||||
late = base.replace(
|
||||
"@EXTRA@",
|
||||
'CODEGEN_SETUP_SOURCES "codegen_setup.c"\n PGXP')
|
||||
t, a, out = run_case(args.cmake, body, late, True)
|
||||
check(a is not None, "configure with PGXP after CODEGEN_SETUP_SOURCES runs",
|
||||
out)
|
||||
if a is not None:
|
||||
check("PGXP" in a,
|
||||
"PGXP after a multi-value argument is still the option")
|
||||
|
||||
t, a, out = run_case(args.cmake, body, base.replace("@EXTRA@", "PGXP"),
|
||||
False)
|
||||
check(a is not None, "setup-host configure with PGXP runs", out)
|
||||
if a is not None:
|
||||
check("PGXP" in a, "the setup host gets the same flavor")
|
||||
|
||||
if failures:
|
||||
print("test_game_runtime_pgxp: %d FAILURES" % failures)
|
||||
return 1
|
||||
print("test_game_runtime_pgxp: all checks passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -44,6 +44,17 @@
|
||||
* check on (then off) or off (then on) before their batch is drawn: with
|
||||
* the check on the rect's pixels stay, with it off they are overwritten,
|
||||
* in the native VRAM and in the frame at S.
|
||||
* - mode "twin" (with or without the window), argv[3] 0/1: [video]
|
||||
* texture_window_batching off/on. Textured prims alternating GP0(E2h)
|
||||
* texture windows (opaque, semi-transparent, set-mask, mask check,
|
||||
* bilinear; inside the frame and into native-wide margins). The runner
|
||||
* checks the native VRAM, the frame at S and the wide surface are the same
|
||||
* with it off and on; this run checks that on never flushes for a window
|
||||
* change except under mask check, and off flushes at every one. An oracle
|
||||
* independent of the renderer then draws raw rects through each window
|
||||
* into both page depths: every pixel must be the texel the PS1 window rule
|
||||
* picks from the VRAM as read back, in the native VRAM and at S, so a
|
||||
* window decode error both settings share still fails.
|
||||
* - mode "passes" (built only where the renderer has render passes, the
|
||||
* frame-rate stack; the runner defines PSX_TEST_RENDER_PASSES): with the
|
||||
* flip-aware frame blend ready, the full-VRAM surface offers render passes
|
||||
@@ -381,6 +392,197 @@ static int mask_main(int scale, int window) {
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---- mode "twin": texture-window batching --------------------------------
|
||||
* argv[3] "0"/"1" = [video] texture_window_batching off/on. Textured prims that
|
||||
* alternate GP0(E2h) windows: opaque polys and rects inside the frame and into
|
||||
* the native-wide margins, semi-transparent ones, a run under set-mask, a run
|
||||
* under mask check (which must still split at every window change) and a
|
||||
* bilinear run. The runner compares the native VRAM, the frame at S and the
|
||||
* wide surface of the two runs; this run checks the window flushes and the
|
||||
* window oracle (twin_oracle). */
|
||||
#define TWIN_TPAGE4 0x0008 /* 4-bit page at (512, 0), CLUT at (512, 256) */
|
||||
#define TWIN_TPAGE15 0x0109 /* 15-bit page at (576, 0) */
|
||||
static const uint32_t twin_windows[4] = {
|
||||
0, /* no window */
|
||||
0x1Cu, /* u repeats every 32 */
|
||||
0x18u | (0x1Eu << 5) | (0x08u << 10) | (0x02u << 15),
|
||||
0x1Fu | (0x1Fu << 5) | (0x05u << 10) | (0x0Au << 15), /* one 8x8 tile */
|
||||
};
|
||||
static void twin_quad(int i, int x, int y, int w, int h, uint16_t tp) {
|
||||
glb_set_texture_window(twin_windows[i & 3]);
|
||||
if (i % 3 == 2) {
|
||||
glb_draw_textured_rect(x, y, w, h, 3 + i, 5 + 2 * i, 512, 256, tp);
|
||||
return;
|
||||
}
|
||||
int u0 = 2 * i, v0 = i, u1 = u0 + 90 + i, v1 = v0 + 70;
|
||||
glb_draw_shaded_textured_triangle(x, y, u0, v0, 0x808080, x + w, y + 3, u1, v0, 0x6090b0,
|
||||
x, y + h, u0, v1, 0xb09060, 512, 256, tp, 0);
|
||||
glb_draw_shaded_textured_triangle(x + w, y + 3, u1, v0, 0x6090b0, x, y + h, u0, v1, 0xb09060,
|
||||
x + w - 7, y + h + 2, u1, v1, 0x80a080, 512, 256, tp, 0);
|
||||
}
|
||||
/* The window oracle: what the GP0(E2h) window must sample, from the PS1 rule
|
||||
* and the VRAM as read back, so the off/on comparison cannot hide a decode
|
||||
* error both modes share. Raw opaque rects (no dither, one texel per native
|
||||
* pixel), one per window and page, drawn back to back so they share batches
|
||||
* when the key is on. Every pixel is checked in the native VRAM and, at its
|
||||
* centre, in the frame at S. Counts the pixels each window drew. */
|
||||
#define TWIN_OR_W 70
|
||||
#define TWIN_OR_H 20
|
||||
static uint16_t twin_texel(const uint16_t *v, uint16_t tp, int u, int t) {
|
||||
int px = (tp & 15) * 64, py = ((tp >> 4) & 1) * 256;
|
||||
if (((tp >> 7) & 3) == 2) return v[((py + t) & 511) * 1024 + ((px + u) & 1023)];
|
||||
int idx = (v[((py + t) & 511) * 1024 + ((px + (u >> 2)) & 1023)] >> ((u & 3) * 4)) & 15;
|
||||
return v[256 * 1024 + 512 + idx]; /* CLUT at (512, 256) */
|
||||
}
|
||||
static void twin_oracle(int scale, int drawn[4]) {
|
||||
/* Texels for the windows that reach past the scene's 64x64 pages. */
|
||||
static uint16_t more[64 * 128];
|
||||
for (int i = 0; i < 64 * 128; i++)
|
||||
more[i] = (uint16_t)(((i * 0x1d0bu) ^ (i >> 4)) | ((i & 7) == 5 ? 0x8000u : 0));
|
||||
glb_vram_transfer_in(512, 64, 64, 64, more); /* 4-bit rows 64.. */
|
||||
glb_vram_transfer_in(576, 64, 64, 64, more + 64 * 64); /* 15-bit rows 64.. */
|
||||
glb_vram_transfer_in(640, 0, 64, 128, more); /* 15-bit u 64..127 */
|
||||
glb_set_draw_area(0, 0, FRAME_W - 1, FRAME_H - 1);
|
||||
glb_set_mask_bits(0, 0);
|
||||
glb_set_semi_transparency(0, 0);
|
||||
glb_set_texture_filter(0);
|
||||
glb_set_color_modulation(128, 128, 128, 1); /* raw: the texel as is */
|
||||
gl_renderer_sync_cpu();
|
||||
check(gl_renderer_fbo_peek(0, 0, 1024, 512, vram), "oracle: VRAM before");
|
||||
int fw = FRAME_W * scale, fh = FRAME_H * scale, ow = 0, oh = 0;
|
||||
uint32_t *before = (uint32_t *)malloc((size_t)fw * fh * 4);
|
||||
uint32_t *after = (uint32_t *)malloc((size_t)fw * fh * 4);
|
||||
int nb = before ? gl_renderer_read_display_hires(0, 0, FRAME_W, FRAME_H, before, fw * fh, &ow, &oh) : 0;
|
||||
check(nb == fw * fh, "oracle: frame at S before");
|
||||
for (int i = 0; i < 8; i++) {
|
||||
glb_set_texture_window(twin_windows[i & 3]);
|
||||
glb_draw_textured_rect(4 + (i & 3) * 78, 2 + (i >> 2) * 22, TWIN_OR_W, TWIN_OR_H,
|
||||
9 * i + 3, 5 * i + 1, 512, 256, i < 4 ? TWIN_TPAGE4 : TWIN_TPAGE15);
|
||||
}
|
||||
glb_set_texture_window(0);
|
||||
glb_set_color_modulation(128, 128, 128, 0);
|
||||
gl_renderer_sync_cpu();
|
||||
check(gl_renderer_fbo_peek(0, 0, 1024, 512, peek), "oracle: native peek");
|
||||
int na = after ? gl_renderer_read_display_hires(0, 0, FRAME_W, FRAME_H, after, fw * fh, &ow, &oh) : 0;
|
||||
check(na == fw * fh, "oracle: frame at S after");
|
||||
long bad = 0, bad_hi = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint32_t w = twin_windows[i & 3];
|
||||
int mx = w & 31, my = (w >> 5) & 31, ox = (w >> 10) & 31, oy = (w >> 15) & 31;
|
||||
int x0 = 4 + (i & 3) * 78, y0 = 2 + (i >> 2) * 22;
|
||||
for (int dy = 0; dy < TWIN_OR_H; dy++)
|
||||
for (int dx = 0; dx < TWIN_OR_W; dx++) {
|
||||
int u = (9 * i + 3 + dx) & 255, t = (5 * i + 1 + dy) & 255;
|
||||
u = (u & ~(mx * 8)) | ((ox & mx) * 8);
|
||||
t = (t & ~(my * 8)) | ((oy & my) * 8);
|
||||
uint16_t texel = twin_texel(vram, i < 4 ? TWIN_TPAGE4 : TWIN_TPAGE15, u, t);
|
||||
int x = x0 + dx, y = y0 + dy;
|
||||
uint16_t want = texel ? texel : vram[y * 1024 + x];
|
||||
if (texel) drawn[i & 3]++;
|
||||
if (peek[y * 1024 + x] != want) {
|
||||
if (!bad) fprintf(stderr, "oracle: window %d page %d px (%d,%d) native %04x want %04x\n",
|
||||
i & 3, i >> 2, x, y, peek[y * 1024 + x], want);
|
||||
bad++;
|
||||
}
|
||||
if (nb == fw * fh && na == fw * fh) {
|
||||
size_t c = (size_t)(y * scale + scale / 2) * fw + (size_t)(x * scale + scale / 2);
|
||||
uint32_t got = after[c] & 0xFFFFFFu; /* BGRA: R bits 16.., B bits 0.. */
|
||||
int ok = texel ? (((got >> 19) & 31) == (texel & 31) &&
|
||||
((got >> 11) & 31) == ((texel >> 5) & 31) &&
|
||||
((got >> 3) & 31) == ((texel >> 10) & 31))
|
||||
: got == (before[c] & 0xFFFFFFu);
|
||||
bad_hi += !ok;
|
||||
}
|
||||
}
|
||||
}
|
||||
free(before);
|
||||
free(after);
|
||||
printf("twin_oracle drawn=%d,%d,%d,%d native_bad=%ld hires_bad=%ld\n",
|
||||
drawn[0], drawn[1], drawn[2], drawn[3], bad, bad_hi);
|
||||
check(bad == 0, "oracle: native VRAM is what the texture window samples");
|
||||
check(bad_hi == 0, "oracle: frame at S is what the texture window samples");
|
||||
for (int k = 0; k < 4; k++) check(drawn[k] > TWIN_OR_W * TWIN_OR_H / 2, "oracle: every window draws");
|
||||
}
|
||||
static int twin_main(int scale, int window, int on) {
|
||||
static uint16_t page[64 * 64], page15[64 * 64], clut[16];
|
||||
for (int i = 0; i < 64 * 64; i++) page[i] = (uint16_t)((i * 0x2469u) ^ (i >> 2) ^ 0x1111u);
|
||||
for (int i = 0; i < 64 * 64; i++)
|
||||
page15[i] = (uint16_t)(((i * 0x0843u) & 0x7fffu) | (((i >> 5) & 3) == 1 ? 0x8000u : 0));
|
||||
for (int i = 0; i < 16; i++) clut[i] = (uint16_t)(i ? (0x0421u * (uint16_t)i) | ((i & 3) == 3 ? 0x8000u : 0) : 0);
|
||||
glb_vram_transfer_in(512, 0, 64, 64, page);
|
||||
glb_vram_transfer_in(576, 0, 64, 64, page15);
|
||||
glb_vram_transfer_in(512, 256, 16, 1, clut);
|
||||
gl_renderer_set_texture_window_batching(on);
|
||||
check(gl_renderer_get_texture_window_batching() == on, "texture-window batching set");
|
||||
glb_set_draw_area(0, 0, 1023, 511);
|
||||
glb_set_draw_offset(0, 0);
|
||||
glb_set_mask_bits(0, 0);
|
||||
glb_set_semi_transparency(0, 0);
|
||||
glb_set_color_modulation(128, 128, 128, 0);
|
||||
if (window) check(hiw_ensure(0, FRAME_W) != NULL, "window covers the frame");
|
||||
glb_fill_rect(0, 0, FRAME_W, FRAME_H, 0x0c63);
|
||||
glb_wide_configure(426, 53);
|
||||
glb_wide_set_target(0);
|
||||
glb_set_draw_area(0, 0, FRAME_W - 1, FRAME_H - 1);
|
||||
uint64_t r5 = s_batch_reason[5], total = s_batch_total;
|
||||
/* A: 24 opaque prims, a new window each (23 changes); 4-bit and 15-bit,
|
||||
* some reaching the margins. */
|
||||
for (int i = 0; i < 24; i++)
|
||||
twin_quad(i, -40 + (i % 6) * 66, 8 + (i / 6) * 40, 70 + (i & 1) * 20, 44,
|
||||
(i & 4) ? TWIN_TPAGE15 : TWIN_TPAGE4);
|
||||
/* B: semi-transparent, every mode (drawn one per batch either way). */
|
||||
for (int i = 0; i < 4; i++) {
|
||||
glb_set_semi_transparency(1, i);
|
||||
twin_quad(i + 1, 20 + i * 70, 150, 80, 40, TWIN_TPAGE15);
|
||||
}
|
||||
glb_set_semi_transparency(0, 0);
|
||||
/* C1: set-mask, 4 prims (3 changes). */
|
||||
glb_set_mask_bits(1, 0);
|
||||
for (int i = 0; i < 4; i++) twin_quad(i, 30 + i * 60, 180, 50, 30, TWIN_TPAGE4);
|
||||
/* C2: mask check, 6 overlapping prims (5 changes; split in both modes). */
|
||||
glb_set_mask_bits(0, 1);
|
||||
for (int i = 0; i < 6; i++) twin_quad(i + 2, 10 + i * 40, 170, 90, 50, TWIN_TPAGE15);
|
||||
glb_set_mask_bits(0, 0);
|
||||
/* D: bilinear, 4 prims (3 changes). */
|
||||
glb_set_texture_filter(1);
|
||||
for (int i = 0; i < 4; i++) twin_quad(i + 3, -20 + i * 95, 205, 90, 30, TWIN_TPAGE4);
|
||||
flush_tex_batch();
|
||||
glb_set_texture_filter(0);
|
||||
glb_set_texture_window(0);
|
||||
uint64_t flushes = s_batch_reason[5] - r5, batches = s_batch_total - total;
|
||||
printf("twin_flushes=%llu batches=%llu\n", (unsigned long long)flushes,
|
||||
(unsigned long long)batches);
|
||||
check(flushes == (on ? 5u : 34u), on ? "batching on: window flushes only under mask check"
|
||||
: "batching off: every window change flushes");
|
||||
int drawn[4] = {0, 0, 0, 0};
|
||||
twin_oracle(scale, drawn);
|
||||
glb_set_draw_area(0, 0, 1023, 511);
|
||||
gl_renderer_sync_cpu();
|
||||
check(gl_renderer_fbo_peek(0, 0, 1024, 512, peek), "native peek");
|
||||
uint64_t digest = fnv(peek, sizeof peek, 0xcbf29ce484222325ull);
|
||||
int fw = FRAME_W * scale, fh = FRAME_H * scale, ow = 0, oh = 0;
|
||||
uint32_t *img = (uint32_t *)malloc((size_t)fw * fh * 4);
|
||||
int n = img ? gl_renderer_read_display_hires(0, 0, FRAME_W, FRAME_H, img, fw * fh, &ow, &oh) : 0;
|
||||
check(n == fw * fh && ow == fw && oh == fh, "hires readback size");
|
||||
uint64_t hires = n ? fnv(img, (size_t)fw * fh * 4, 0xcbf29ce484222325ull) : 0;
|
||||
free(img);
|
||||
uint64_t wide = 0;
|
||||
{
|
||||
int ww = 426 * scale, wh = 512 * scale, gw = 0, gh = 0;
|
||||
uint32_t *wb = (uint32_t *)malloc((size_t)ww * wh * 4);
|
||||
int got = wb ? glb_wide_dump_full(wb, ww * wh, &gw, &gh, 0) : 0;
|
||||
check(got == ww * wh && gw == ww && gh == wh, "wide surface dump size");
|
||||
if (got) wide = fnv(wb, (size_t)ww * wh * 4, 0xcbf29ce484222325ull);
|
||||
free(wb);
|
||||
}
|
||||
check(glGetError() == GL_NO_ERROR, "GL error");
|
||||
printf("digest=%016llx\n", (unsigned long long)digest);
|
||||
printf("hires=%016llx\n", (unsigned long long)hires);
|
||||
printf("wide=%016llx\n", (unsigned long long)wide);
|
||||
printf("checks=%d failures=%d\n", checks, failures);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
|
||||
#if defined(PSX_TEST_RENDER_PASSES)
|
||||
/* Mode "passes": see the header. */
|
||||
static int passes_main(int scale, int window) {
|
||||
@@ -642,6 +844,12 @@ int main(int argc, char **argv) {
|
||||
return rc;
|
||||
}
|
||||
#endif
|
||||
if (!strcmp(mode, "twin")) {
|
||||
if (si.windowed) check(si.hr_scale == 1, "window mode engaged");
|
||||
int rc = twin_main(scale, si.windowed, argc > 3 && argv[3][0] == '1');
|
||||
gl_renderer_shutdown(); SDL_DestroyWindow(win); SDL_Quit();
|
||||
return rc;
|
||||
}
|
||||
if (!strcmp(mode, "mask")) {
|
||||
if (si.windowed) check(si.hr_scale == 1, "window mode engaged");
|
||||
int rc = mask_main(scale, si.windowed);
|
||||
|
||||
@@ -263,6 +263,11 @@ int gte_precision_load_word(uint32_t addr, uint32_t packed,
|
||||
{ (void)addr; (void)packed; (void)x16; (void)y16; (void)z; return 0; }
|
||||
void pgxp_invalidate_all(void) {}
|
||||
void pgxp_invalidate_word(uint32_t addr) { (void)addr; }
|
||||
void pgxp_note_triangle(int precise) { (void)precise; }
|
||||
void pgxp_note_rect_bypass(int all_precise) { (void)all_precise; }
|
||||
int pgxp_probe_precise_vertex(uint32_t addr, uint32_t packet_word,
|
||||
int32_t int_x, int32_t int_y)
|
||||
{ (void)addr; (void)packet_word; (void)int_x; (void)int_y; return 0; }
|
||||
void psx_irq_raise(uint32_t bit, uint32_t detail) { (void)bit; (void)detail; }
|
||||
void event_ring_record_aux(uint16_t kind, uint8_t detail, uint32_t aux)
|
||||
{ (void)kind; (void)detail; (void)aux; }
|
||||
|
||||
@@ -194,6 +194,59 @@ int main(void) {
|
||||
exec_dot_and_expect(54, 42);
|
||||
|
||||
test_background_packet_tags();
|
||||
puts("gpu_textured_dot_nw_shift_exec_test: PASS (HUD and background tags)");
|
||||
/* PGXP (docs/ENHANCEMENTS.md G1.11): a textured quad that is an
|
||||
* axis-aligned rectangle in integer screen space and UV takes the 2D
|
||||
* rectangle shortcut -- unless PGXP is correcting and all four vertices
|
||||
* carry a dataflow-precise position, in which case it is drawn as two
|
||||
* precise triangles so it meets its precise neighbours without a seam.
|
||||
* With only some corners precise it keeps the shortcut (two triangles
|
||||
* would mix precise and native vertices) and counts as rect_partial. */
|
||||
{
|
||||
static const uint32_t quad[9] = {
|
||||
0x2C808080u, /* FT4, opaque */
|
||||
(73u << 16) | 98u, 0x7F0ABF1Au, /* v0 + uv0/clut */
|
||||
(73u << 16) | 73u, 0x0039BF0Cu, /* v1 + uv1/tpage */
|
||||
(52u << 16) | 98u, 0x2C39B01Au, /* v2 + uv2 */
|
||||
(52u << 16) | 73u, 0x2C39B00Cu, /* v3 + uv3 */
|
||||
};
|
||||
const uint32_t base = 0x20000u;
|
||||
for (int pass = 0; pass < 4; pass++) {
|
||||
reset_gpu_state_for_test();
|
||||
memcpy(gp0_cmd_buf, quad, sizeof quad);
|
||||
gp0_cmd_source_addr = base;
|
||||
memcpy(&test_ram[base / 4u], quad, sizeof quad);
|
||||
last_textured_rect.calls = 0;
|
||||
last_scaled_rect.calls = 0;
|
||||
g_test_textured_triangles = 0;
|
||||
g_test_precise_triangles = 0;
|
||||
g_test_rect_bypass = 0;
|
||||
g_test_rect_partial = 0;
|
||||
/* pass 0: PGXP off, all precise; pass 1: on, no precise vertex
|
||||
* (a CPU-built sprite); pass 2: on, only vertex 2 (packet word 5)
|
||||
* precise; pass 3: on, all four (words 1, 3, 5, 7) precise. */
|
||||
static const uint32_t k_masks[4] = { 0xAAu, 0u, 1u << 5, 0xAAu };
|
||||
g_test_geometry_correction = pass > 0;
|
||||
g_test_quad_base = base;
|
||||
g_test_precise_words = k_masks[pass];
|
||||
gp0_exec_textured_quad();
|
||||
if (pass < 3) {
|
||||
assert(last_textured_rect.calls + last_scaled_rect.calls == 1);
|
||||
assert(g_test_textured_triangles == 0);
|
||||
assert(g_test_rect_bypass == 0);
|
||||
assert(g_test_rect_partial == (pass == 2 ? 1 : 0));
|
||||
} else {
|
||||
assert(last_textured_rect.calls + last_scaled_rect.calls == 0);
|
||||
assert(g_test_textured_triangles == 2);
|
||||
assert(g_test_precise_triangles == 2);
|
||||
assert(g_test_rect_bypass == 1);
|
||||
assert(g_test_rect_partial == 0);
|
||||
}
|
||||
}
|
||||
g_test_geometry_correction = 0;
|
||||
g_test_quad_base = 0xFFFFFFFFu;
|
||||
g_test_precise_words = 0;
|
||||
}
|
||||
|
||||
puts("gpu_textured_dot_nw_shift_exec_test: PASS (HUD and background tags, PGXP rectangles)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include "cpu_state.h"
|
||||
#include "gte.h"
|
||||
#include "gte_nclip_stats.h"
|
||||
#include "pgxp.h"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -50,6 +52,8 @@ extern "C" int gpu_ws_precise_nclip_enabled(void) {
|
||||
return g_test_precise_nclip_enabled;
|
||||
}
|
||||
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) {}
|
||||
extern "C" {
|
||||
uint64_t s_frame_count = 0;
|
||||
@@ -654,6 +658,144 @@ int test_precise_nclip_is_title_scoped() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* PGXP precise culling (docs/ENHANCEMENTS.md G1.12): NCLIP's MAC0 takes the
|
||||
* exact sign only while geometry correction and culling are armed, only when
|
||||
* the three SXY shadows are believed, and never in a compared pass. */
|
||||
int test_pgxp_culling() {
|
||||
gte_precision_tracking_set(1);
|
||||
gte_geometry_correction_set(1);
|
||||
pgxp_set_preserve_projection(1);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
|
||||
/* A far road row: the three vertices all round to y = 113 (MAC0 0, the
|
||||
* game culls it); exact y 112.64 / 113.12 / 113.83 (positive area). */
|
||||
const uint32_t packed[3] = {(113u << 16) | 100u, (113u << 16) | 220u,
|
||||
(113u << 16) | 160u};
|
||||
const int32_t x16[3] = {100 << 16, 220 << 16, 160 << 16};
|
||||
const int32_t y16[3] = {(112 << 16) + 41943, (113 << 16) + 7864,
|
||||
(113 << 16) + 54394};
|
||||
const int64_t cross =
|
||||
((int64_t)x16[1] - x16[0]) * ((int64_t)y16[2] - y16[0]) -
|
||||
((int64_t)y16[1] - y16[0]) * ((int64_t)x16[2] - x16[0]);
|
||||
const int32_t area = (int32_t)((uint64_t)(cross + (1ll << 31)) >> 32);
|
||||
auto nclip = [&](const uint32_t (&w)[3], const int32_t (&xs)[3],
|
||||
const int32_t (&ys)[3], uint32_t *flag) {
|
||||
CPUState cpu{};
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
cpu.gte_data[12 + i] = w[i];
|
||||
gte_test_seed_precise_projection(i, w[i], xs[i], ys[i], 100);
|
||||
}
|
||||
gte_execute(&cpu, 0x06u);
|
||||
if (flag) *flag = cpu.gte_ctrl[31];
|
||||
return static_cast<int32_t>(cpu.gte_data[24]);
|
||||
};
|
||||
PGXPStats a{}, b{};
|
||||
pgxp_get_stats(&a);
|
||||
uint32_t flag_off = 0, flag_on = 0;
|
||||
|
||||
/* Culling off: the guest sees the hardware's 0; the disagreement is
|
||||
* counted (the crack exposure), not corrected. */
|
||||
pgxp_set_culling(0);
|
||||
if (int32_t m = nclip(packed, x16, y16, &flag_off); m != 0)
|
||||
return fail_value("culling off keeps MAC0", 0, 0x06u, 0, 0u, (uint32_t)m);
|
||||
pgxp_get_stats(&b);
|
||||
if (b.nclip_disagree != a.nclip_disagree + 1 ||
|
||||
b.nclip_corrected != a.nclip_corrected)
|
||||
return fail_value("culling off counts the disagreement", 0, 0x06u, 0,
|
||||
1u, (uint32_t)(b.nclip_disagree - a.nclip_disagree));
|
||||
|
||||
/* Culling on: MAC0 is the exact doubled area, rounded (>= 1). */
|
||||
pgxp_set_culling(1);
|
||||
if (area < 1)
|
||||
return fail_value("test area", 0, 0x06u, 0, 1u, (uint32_t)area);
|
||||
if (int32_t m = nclip(packed, x16, y16, &flag_on); m != area)
|
||||
return fail_value("culling on takes the exact sign", 0, 0x06u, 0,
|
||||
(uint32_t)area, (uint32_t)m);
|
||||
if (flag_on != flag_off)
|
||||
return fail_value("culling keeps FLAG", 0, 0x06u, 0, flag_off, flag_on);
|
||||
pgxp_get_stats(&a);
|
||||
if (a.nclip_corrected != b.nclip_corrected + 1)
|
||||
return fail_value("culling on counts the correction", 0, 0x06u, 0, 1u,
|
||||
(uint32_t)(a.nclip_corrected - b.nclip_corrected));
|
||||
|
||||
/* Reversed winding: the exact sign is negative, magnitude at least 1. */
|
||||
{
|
||||
const uint32_t w[3] = {packed[0], packed[2], packed[1]};
|
||||
const int32_t xs[3] = {x16[0], x16[2], x16[1]};
|
||||
const int32_t ys[3] = {y16[0], y16[2], y16[1]};
|
||||
if (int32_t m = nclip(w, xs, ys, nullptr); m != -area)
|
||||
return fail_value("culling keeps the reversed sign", 0, 0x06u, 0,
|
||||
(uint32_t)-area, (uint32_t)m);
|
||||
}
|
||||
/* A native-positive sliver whose exact winding is reversed: the game
|
||||
* now sees it back-facing. Native (0,0),(10,0),(5,1): MAC0 +10; exact
|
||||
* y2 = -0.25 (inside the window). */
|
||||
{
|
||||
const uint32_t w[3] = {0u, 10u, (1u << 16) | 5u};
|
||||
const int32_t xs[3] = {0, 10 << 16, 5 << 16};
|
||||
const int32_t ys[3] = {0, 0, -(1 << 14)};
|
||||
pgxp_set_culling(0);
|
||||
if (int32_t m = nclip(w, xs, ys, nullptr); m != 10)
|
||||
return fail_value("native sliver MAC0", 0, 0x06u, 0, 10u, (uint32_t)m);
|
||||
pgxp_set_culling(1);
|
||||
if (int32_t m = nclip(w, xs, ys, nullptr); m != -3)
|
||||
return fail_value("reversed sliver MAC0", 0, 0x06u, 0,
|
||||
(uint32_t)-3, (uint32_t)m);
|
||||
}
|
||||
/* Signs that agree keep the integer MAC0 exactly. */
|
||||
{
|
||||
const uint32_t w[3] = {0u, 10u, (4u << 16) | 5u};
|
||||
const int32_t xs[3] = {0, 10 << 16, 5 << 16};
|
||||
const int32_t ys[3] = {1 << 15, 0, (4 << 16) + (1 << 15)};
|
||||
if (int32_t m = nclip(w, xs, ys, nullptr); m != 40)
|
||||
return fail_value("agreeing sign keeps MAC0", 0, 0x06u, 0, 40u,
|
||||
(uint32_t)m);
|
||||
}
|
||||
/* A stale shadow (the register word changed under it) fails closed. */
|
||||
{
|
||||
const uint32_t w[3] = {packed[0], packed[1], packed[2]};
|
||||
CPUState cpu{};
|
||||
for (uint32_t i = 0; i < 3; ++i) {
|
||||
cpu.gte_data[12 + i] = w[i];
|
||||
gte_test_seed_precise_projection(i, w[i] ^ (i == 2 ? 1u : 0u),
|
||||
x16[i], y16[i], 100);
|
||||
}
|
||||
gte_execute(&cpu, 0x06u);
|
||||
if (cpu.gte_data[24] != 0u)
|
||||
return fail_value("stale shadow keeps MAC0", 0, 0x06u, 0, 0u,
|
||||
cpu.gte_data[24]);
|
||||
}
|
||||
/* Compared passes see the hardware result: the overlay shadow diff (its
|
||||
* interpreter pass included) and a speculative pass. */
|
||||
g_test_shadow_diff = 1;
|
||||
if (int32_t m = nclip(packed, x16, y16, nullptr); m != 0)
|
||||
return fail_value("shadow diff holds culling off", 0, 0x06u, 0, 0u,
|
||||
(uint32_t)m);
|
||||
g_test_shadow_diff = 0;
|
||||
gte_precision_speculative_begin();
|
||||
{
|
||||
CPUState cpu{};
|
||||
for (uint32_t i = 0; i < 3; ++i) cpu.gte_data[12 + i] = packed[i];
|
||||
gte_execute(&cpu, 0x06u);
|
||||
gte_precision_speculative_end();
|
||||
if (cpu.gte_data[24] != 0u)
|
||||
return fail_value("speculative pass holds culling off", 0, 0x06u,
|
||||
0, 0u, cpu.gte_data[24]);
|
||||
}
|
||||
/* Geometry correction off: nothing is corrected or counted. */
|
||||
gte_geometry_correction_set(0);
|
||||
pgxp_get_stats(&a);
|
||||
if (int32_t m = nclip(packed, x16, y16, nullptr); m != 0)
|
||||
return fail_value("geometry off keeps MAC0", 0, 0x06u, 0, 0u, (uint32_t)m);
|
||||
pgxp_get_stats(&b);
|
||||
if (b.nclip_precise != a.nclip_precise || b.nclip_disagree != a.nclip_disagree)
|
||||
return fail_value("geometry off counts nothing", 0, 0x06u, 0, 0u,
|
||||
(uint32_t)(b.nclip_precise - a.nclip_precise));
|
||||
pgxp_set_culling(0);
|
||||
pgxp_set_preserve_projection(0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int test_precision_speculative_transaction() {
|
||||
constexpr uint32_t address = 0x00123450u;
|
||||
constexpr uint32_t packed = 0x00420021u;
|
||||
@@ -758,6 +900,117 @@ int test_precision_speculative_transaction() {
|
||||
|
||||
} // namespace
|
||||
|
||||
/* Preserve projection precision (docs/ENHANCEMENTS.md G1.11) changes the
|
||||
* host-only SXY shadow and nothing the guest can see: RTPS/RTPT leave exactly
|
||||
* the reference oracle's registers (SXY, MAC0, IR, FLAG, ...) with it on, and
|
||||
* a qualifying vertex's shadow is the exact projection. */
|
||||
int test_preserve_projection_is_shadow_only() {
|
||||
gte_precision_tracking_set(1);
|
||||
for (uint32_t function : {0x01u, 0x30u}) {
|
||||
for (unsigned iteration = 0; iteration < 256; ++iteration) {
|
||||
CPUState seed;
|
||||
randomize_gte(seed);
|
||||
CPUState expected = seed;
|
||||
CPUState actual = seed;
|
||||
const uint32_t cmd = (random_u32() & ~0x3Fu) | function;
|
||||
gte_test_execute_reference(&expected, cmd);
|
||||
pgxp_set_preserve_projection(1);
|
||||
gte_execute(&actual, cmd);
|
||||
pgxp_set_preserve_projection(0);
|
||||
if (!same_gte(expected, actual))
|
||||
return fail_state("preserve projection guest state", iteration,
|
||||
function, cmd, expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
/* One qualifying vertex with fractional MAC bits: RT not a pure scale,
|
||||
* a translation, H = 300, OFX/OFY = (160.25, 119.5). */
|
||||
CPUState cpu{};
|
||||
auto pack = [](int32_t lo, int32_t hi) {
|
||||
return (static_cast<uint32_t>(hi & 0xFFFF) << 16) |
|
||||
static_cast<uint32_t>(lo & 0xFFFF);
|
||||
};
|
||||
const int32_t rt[9] = {4100, 3, 0, 0, 4090, 5, 0, 0, 4096};
|
||||
cpu.gte_ctrl[0] = pack(rt[0], rt[1]);
|
||||
cpu.gte_ctrl[1] = pack(rt[2], rt[3]);
|
||||
cpu.gte_ctrl[2] = pack(rt[4], rt[5]);
|
||||
cpu.gte_ctrl[3] = pack(rt[6], rt[7]);
|
||||
cpu.gte_ctrl[4] = static_cast<uint32_t>(rt[8]);
|
||||
const int32_t tr[3] = {7, -3, 40};
|
||||
cpu.gte_ctrl[5] = static_cast<uint32_t>(tr[0]);
|
||||
cpu.gte_ctrl[6] = static_cast<uint32_t>(tr[1]);
|
||||
cpu.gte_ctrl[7] = static_cast<uint32_t>(tr[2]);
|
||||
const int32_t ofx = (160 << 16) + 0x4000, ofy = (119 << 16) + 0x8000;
|
||||
cpu.gte_ctrl[24] = static_cast<uint32_t>(ofx);
|
||||
cpu.gte_ctrl[25] = static_cast<uint32_t>(ofy);
|
||||
cpu.gte_ctrl[26] = 300u;
|
||||
const int32_t v[3] = {101, -53, 937};
|
||||
cpu.gte_data[0] = pack(v[0], v[1]);
|
||||
cpu.gte_data[1] = static_cast<uint32_t>(v[2]);
|
||||
const uint32_t rtps = 0x00080001u; /* RTPS, sf=1, lm=0 */
|
||||
const int64_t mac1 = (int64_t)tr[0] * 4096 + (int64_t)rt[0] * v[0] +
|
||||
(int64_t)rt[1] * v[1] + (int64_t)rt[2] * v[2];
|
||||
const int64_t mac2 = (int64_t)tr[1] * 4096 + (int64_t)rt[3] * v[0] +
|
||||
(int64_t)rt[4] * v[1] + (int64_t)rt[5] * v[2];
|
||||
const int64_t mac3 = (int64_t)tr[2] * 4096 + (int64_t)rt[6] * v[0] +
|
||||
(int64_t)rt[7] * v[1] + (int64_t)rt[8] * v[2];
|
||||
const double ex = ofx / 65536.0 + (double)mac1 * 300.0 / (double)mac3;
|
||||
const double ey = ofy / 65536.0 + (double)mac2 * 300.0 / (double)mac3;
|
||||
|
||||
CPUState ir_run = cpu;
|
||||
gte_execute(&ir_run, rtps);
|
||||
PreciseState ir{};
|
||||
gte_test_get_precise_projection(2, &ir.packed, &ir.x16, &ir.y16, &ir.z,
|
||||
&ir.valid);
|
||||
pgxp_set_preserve_projection(1);
|
||||
CPUState ppp_run = cpu;
|
||||
gte_execute(&ppp_run, rtps);
|
||||
pgxp_set_preserve_projection(0);
|
||||
PreciseState ppp{};
|
||||
gte_test_get_precise_projection(2, &ppp.packed, &ppp.x16, &ppp.y16, &ppp.z,
|
||||
&ppp.valid);
|
||||
if (!same_gte(ir_run, ppp_run))
|
||||
return fail_state("preserve projection keeps the guest SXY", 0, 1, rtps,
|
||||
ir_run, ppp_run);
|
||||
const uint32_t sxy2 = ppp_run.gte_data[14];
|
||||
const int32_t native_x = static_cast<int16_t>(sxy2 & 0xFFFFu);
|
||||
const int32_t native_y = static_cast<int16_t>(sxy2 >> 16);
|
||||
if (!ir.valid || ir.packed != sxy2 || (ir.x16 >> 16) != native_x ||
|
||||
(ir.y16 >> 16) != native_y)
|
||||
return fail_value("IR-path shadow truncates to SXY2", 0, 14, sxy2,
|
||||
static_cast<uint32_t>(native_x),
|
||||
static_cast<uint32_t>(ir.x16 >> 16));
|
||||
if (!ppp.valid || ppp.packed != sxy2 || ppp.z != ir.z)
|
||||
return fail_value("exact-projection shadow keys the same word", 0, 14,
|
||||
sxy2, sxy2, ppp.packed);
|
||||
if (ppp.x16 != (int32_t)std::floor(ex * 65536.0) ||
|
||||
ppp.y16 != (int32_t)std::floor(ey * 65536.0))
|
||||
return fail_value("exact-projection shadow", 0, 14, sxy2,
|
||||
static_cast<uint32_t>((int32_t)std::floor(ex * 65536.0)),
|
||||
static_cast<uint32_t>(ppp.x16));
|
||||
if (ppp.x16 == ir.x16 && ppp.y16 == ir.y16)
|
||||
return fail_value("exact projection differs from the IR path", 0, 14,
|
||||
sxy2, 0, 0);
|
||||
|
||||
/* Divide overflow (H >= 2*SZ3): the guest quotient saturates, so the
|
||||
* shadow stays on the IR path even with the mode on. */
|
||||
CPUState near_cpu = cpu;
|
||||
near_cpu.gte_data[1] = 100u; /* SZ3 = 140 < H/2 */
|
||||
pgxp_set_preserve_projection(1);
|
||||
gte_execute(&near_cpu, rtps);
|
||||
pgxp_set_preserve_projection(0);
|
||||
PreciseState sat{};
|
||||
gte_test_get_precise_projection(2, &sat.packed, &sat.x16, &sat.y16, &sat.z,
|
||||
&sat.valid);
|
||||
const uint32_t near_sxy = near_cpu.gte_data[14];
|
||||
if (!sat.valid ||
|
||||
(sat.x16 >> 16) != static_cast<int16_t>(near_sxy & 0xFFFFu))
|
||||
return fail_value("divide overflow keeps the IR shadow", 0, 14,
|
||||
near_sxy, near_sxy & 0xFFFFu,
|
||||
static_cast<uint32_t>(sat.x16 >> 16));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main() {
|
||||
if (int rc = test_hardware_register_semantics()) return rc;
|
||||
if (int rc = test_canonicalizer()) return rc;
|
||||
@@ -769,6 +1022,8 @@ int main() {
|
||||
if (int rc = test_precise_sxy_invalidation()) return rc;
|
||||
if (int rc = test_precise_nclip_is_title_scoped()) return rc;
|
||||
if (int rc = test_precision_speculative_transaction()) return rc;
|
||||
if (int rc = test_preserve_projection_is_shadow_only()) return rc;
|
||||
if (int rc = test_pgxp_culling()) return rc;
|
||||
std::puts("PASS: canonical GTE register helpers match GTEState transfer oracle");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* MFC0/CFC0 in the dirty-RAM interpreter read COP0 as it stood when the
|
||||
* instruction began.
|
||||
*
|
||||
* The interpreter charges an instruction's fetch and base cycle before its
|
||||
* body runs. When that charge reaches a device deadline, the device event runs
|
||||
* first and may raise CAUSE.IP2 (the interrupt line). The IRQ belongs to the
|
||||
* next instruction boundary, which is where compiled code, batching its
|
||||
* charges, makes it visible. This runs the production decoder
|
||||
* (interp_cop0_decoder.c) with cycle charging on and a device service that
|
||||
* raises IP2 on the read's own cycle:
|
||||
*
|
||||
* - the read with the event due on its own cycle sees CAUSE without IP2,
|
||||
* and CAUSE holds IP2 once the instruction has finished;
|
||||
* - controls: no event, and an IRQ already raised before the instruction.
|
||||
*
|
||||
* test_interp_cop0_read_sample.py links it; every seam not defined here is an
|
||||
* abort() stub. */
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "cpu_state.h"
|
||||
|
||||
int interp_test_step(CPUState *cpu, uint32_t pc, uint32_t insn, uint32_t *next);
|
||||
|
||||
/* Clock, cache and device seams read on the charged path. */
|
||||
uint64_t psx_cycle_count;
|
||||
uint64_t psx_next_service_cycle;
|
||||
uint64_t g_psx_cycle_fast_limit;
|
||||
int psx_in_device_service;
|
||||
int g_event_step_conservative;
|
||||
uint32_t g_psx_cyc_batch;
|
||||
uint32_t g_psx_cyc_batch_limit;
|
||||
int g_psx_cyc_bb_defer;
|
||||
uint32_t *g_psx_cyc_local_acc;
|
||||
int g_psx_icache_active; /* 0: no fetch cost */
|
||||
uint32_t g_psx_icache_tv[1024];
|
||||
uint32_t g_psx_mod_instruction_hooks; /* no mod instruction sites */
|
||||
|
||||
static CPUState *s_device_cpu;
|
||||
static unsigned s_services;
|
||||
void psx_devices_service_to_now(void) {
|
||||
++s_services;
|
||||
s_device_cpu->cop0[13] |= 0x400u; /* the device raises CAUSE.IP2 */
|
||||
psx_next_service_cycle = UINT64_MAX;
|
||||
}
|
||||
|
||||
static int failures;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, "FAIL %s:%d: ", __FILE__, __LINE__); \
|
||||
fprintf(stderr, __VA_ARGS__); \
|
||||
fputc('\n', stderr); \
|
||||
++failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
enum { A1 = 5, CAUSE = 13 };
|
||||
#define CAUSE_SYSCALL 0x20u /* ExcCode 8 */
|
||||
#define CAUSE_IP2 0x400u
|
||||
|
||||
/* Runs `insn` (MFC0/CFC0 a1,$13) once and returns what it read. The device
|
||||
* deadline falls on the instruction's own cycle, or never. */
|
||||
static uint32_t read_cause(uint32_t insn, int due_on_own_cycle, int raised_before) {
|
||||
static CPUState cpu;
|
||||
memset(&cpu, 0, sizeof cpu);
|
||||
cpu.pc = 0x80020000u; /* outside the BIOS load-delay window */
|
||||
cpu.read_fudge = 32;
|
||||
cpu.ld_which_t = 32;
|
||||
cpu.cop0[CAUSE] = CAUSE_SYSCALL | (raised_before ? CAUSE_IP2 : 0u);
|
||||
s_device_cpu = &cpu;
|
||||
s_services = 0;
|
||||
psx_cycle_count = 100000u;
|
||||
psx_next_service_cycle = due_on_own_cycle ? psx_cycle_count + 1u : UINT64_MAX;
|
||||
|
||||
uint32_t next = 0;
|
||||
CHECK(interp_test_step(&cpu, cpu.pc, insn, &next) == 0,
|
||||
"0x%08X transferred control", (unsigned)insn);
|
||||
CHECK(s_services == (due_on_own_cycle ? 1u : 0u),
|
||||
"0x%08X: %u device services, expected %d", (unsigned)insn, s_services,
|
||||
due_on_own_cycle);
|
||||
const uint32_t cause_after = CAUSE_SYSCALL |
|
||||
((due_on_own_cycle || raised_before) ? CAUSE_IP2 : 0u);
|
||||
CHECK(cpu.cop0[CAUSE] == cause_after,
|
||||
"0x%08X: CAUSE after the instruction is 0x%08X", (unsigned)insn,
|
||||
(unsigned)cpu.cop0[CAUSE]);
|
||||
return cpu.gpr[A1];
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const uint32_t mfc0 = 0x40056800u; /* mfc0 a1,$13 */
|
||||
const uint32_t cfc0 = 0x40456800u; /* cfc0 a1,$13 */
|
||||
for (int i = 0; i < 2; i++) {
|
||||
const uint32_t insn = i ? cfc0 : mfc0;
|
||||
uint32_t got = read_cause(insn, 1, 0);
|
||||
CHECK(got == CAUSE_SYSCALL,
|
||||
"0x%08X with the IRQ due on its own cycle read 0x%08X",
|
||||
(unsigned)insn, (unsigned)got);
|
||||
got = read_cause(insn, 0, 0);
|
||||
CHECK(got == CAUSE_SYSCALL, "0x%08X with no event read 0x%08X",
|
||||
(unsigned)insn, (unsigned)got);
|
||||
got = read_cause(insn, 0, 1);
|
||||
CHECK(got == (CAUSE_SYSCALL | CAUSE_IP2),
|
||||
"0x%08X with the IRQ raised before it read 0x%08X",
|
||||
(unsigned)insn, (unsigned)got);
|
||||
}
|
||||
if (failures) {
|
||||
fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
puts("PASS MFC0/CFC0 read CAUSE from before their own cycle charge");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"""MFC0/CFC0 read COP0 from before their own cycle charge.
|
||||
|
||||
Links the production decoder (interp_cop0_decoder.c, cycle charging on) into
|
||||
test_interp_cop0_read_sample.c at -O0 and -O2; every other runtime seam is an
|
||||
abort() stub (fixture_link.py).
|
||||
"""
|
||||
import argparse
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from fixture_link import build_and_run
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--cc', default='gcc')
|
||||
args = parser.parse_args()
|
||||
here = Path(__file__).resolve().parent
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
for opt in ('-O0', '-O2'):
|
||||
build_and_run(args.cc, here, here.parent, opt, Path(root),
|
||||
[str(here / 'interp_cop0_decoder.c')],
|
||||
'test_interp_cop0_read_sample.c')
|
||||
print('PASS: MFC0/CFC0 read CAUSE from before their own cycle charge (O0/O2)')
|
||||
@@ -0,0 +1,135 @@
|
||||
/* Straight-line interpretation into game text that dispatch refuses keeps
|
||||
* interpreting; it hands back at the first PC dispatch accepts.
|
||||
*
|
||||
* Layout (all NOPs): 0x40000-0x40FFF is a dirty page, where interpretation
|
||||
* starts; 0x60000-0x60FFF is a second dirty page, for the control.
|
||||
* 0x40000-0x4FFFF is game text. 0x40000-0x41FFF holds live bytes that differ
|
||||
* from the boot EXE, so dispatch refuses it (psx_game_text_native_ok is
|
||||
* false); from 0x42000 on the text matches again and runs compiled.
|
||||
*
|
||||
* Before the change the interpreter handed back at 0x41000, the first clean
|
||||
* page. Dispatch refused that PC and re-entered the interpreter, which handed
|
||||
* back again one instruction later: about 1,000 hand-backs to cross one page.
|
||||
* Now it interprets through the refused text, hands back once, and dispatch
|
||||
* runs the compiled code at 0x42000.
|
||||
*
|
||||
* Production dirty_ram_interp.c (test_interp_refused_game_text.py links it);
|
||||
* memory and dispatch answers are seams, everything else aborts. */
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "cpu_state.h"
|
||||
|
||||
int dirty_ram_dispatch(CPUState *cpu, uint32_t addr, uint32_t stop_addr);
|
||||
extern uint64_t g_dirty_ram_blocks_run; /* one per interpreter hand-back */
|
||||
|
||||
uint32_t g_psx_ram_size = 0x00200000u;
|
||||
uint32_t g_psx_ram_mask = 0x001FFFFFu;
|
||||
static uint8_t ram[0x00200000u]; /* zero = NOP */
|
||||
uint8_t *memory_get_ram_ptr(void) { return ram; }
|
||||
uint32_t psx_read_word(uint32_t a) { (void)a; return 0; }
|
||||
|
||||
int dirty_ram_is_dirty(uint32_t phys) {
|
||||
const uint32_t page = (phys & 0x1FFFFFFFu) >> 12;
|
||||
return page == 0x40u || page == 0x60u;
|
||||
}
|
||||
int psx_game_address_in_text(uint32_t addr) {
|
||||
const uint32_t p = addr & 0x1FFFFFFFu;
|
||||
return p >= 0x40000u && p < 0x50000u;
|
||||
}
|
||||
int psx_game_text_native_ok(uint32_t addr) {
|
||||
return psx_game_address_in_text(addr) && ((addr & 0x1FFFFFFFu) >> 12) >= 0x42u;
|
||||
}
|
||||
int psx_game_text_native_ok_full(uint32_t addr) { return psx_game_text_native_ok(addr); }
|
||||
int psx_is_dispatchable(uint32_t pc) { (void)pc; return 1; }
|
||||
uint32_t g_psx_mod_instruction_hooks; /* no mod instruction sites */
|
||||
|
||||
static unsigned s_compiled_calls;
|
||||
static uint32_t s_compiled_addr;
|
||||
int psx_dispatch_game_compiled(CPUState *cpu, uint32_t addr) {
|
||||
(void)cpu;
|
||||
++s_compiled_calls;
|
||||
s_compiled_addr = addr;
|
||||
return 1; /* handled */
|
||||
}
|
||||
|
||||
/* No interrupt is pending; no kernel slot, overlay, mod hook or thread switch
|
||||
* applies on this path. */
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
void psx_check_interrupts(CPUState *cpu) { (void)cpu; }
|
||||
void psx_check_interrupts_at(CPUState *cpu, uint32_t resume_pc) { (void)cpu; (void)resume_pc; }
|
||||
int psx_interrupts_checked_at_current_cycle(uint32_t pc) { (void)pc; return 1; }
|
||||
void psx_rfe_escape_check(CPUState *cpu) { (void)cpu; }
|
||||
int psx_kernel_bless_dispatchable(uint32_t phys) { (void)phys; return 0; }
|
||||
int psx_kernel_patch_range_ends_at(uint32_t phys) { (void)phys; return 0; }
|
||||
void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len) { (void)phys; (void)len; }
|
||||
void fntrace_maybe_mark_game_started(CPUState *cpu, uint32_t addr) { (void)cpu; (void)addr; }
|
||||
int overlay_fp_enabled(void) { return 0; }
|
||||
int overlay_loader_dispatch(CPUState *cpu, uint32_t addr) { (void)cpu; (void)addr; return 0; }
|
||||
int overlay_loader_is_candidate(uint32_t phys) { (void)phys; return 0; }
|
||||
int psx_overlay_dispatch(CPUState *cpu, uint32_t addr) { (void)cpu; (void)addr; return 0; }
|
||||
int psx_overlay_static_can_dispatch(uint32_t addr) { (void)addr; return 0; }
|
||||
uint32_t psx_overlay_resident_crc_at(uint32_t phys, int *valid) {
|
||||
(void)phys; *valid = 0; return 0;
|
||||
}
|
||||
int psx_mod_function_entry(CPUState *cpu, uint32_t addr) { (void)cpu; (void)addr; return 0; }
|
||||
/* [[draw_distance.clamp]] is off: fixture_link would stub this data symbol as a
|
||||
* function, whose code bytes read as non-zero and run the clamp lookup. */
|
||||
int g_psx_draw_distance_clamp_live = 0;
|
||||
/* The segment-miss recorder sees every clean game-text miss; nothing to record. */
|
||||
int psx_game_is_function_entry(uint32_t addr) { (void)addr; return 0; }
|
||||
uint32_t psx_segment_miss_note(uint32_t addr, int (*is_entry)(uint32_t),
|
||||
uint32_t ra, uint32_t sp, uint32_t frame) {
|
||||
(void)addr; (void)is_entry; (void)ra; (void)sp; (void)frame; return 0;
|
||||
}
|
||||
uint64_t s_frame_count;
|
||||
void psx_ra_tripwire(CPUState *cpu, uint32_t a, uint32_t b, uint32_t c) {
|
||||
(void)cpu; (void)a; (void)b; (void)c;
|
||||
}
|
||||
void psx_pgxp_alu(CPUState *cpu, uint32_t insn, uint32_t r, uint32_t a, uint32_t b) {
|
||||
(void)cpu; (void)insn; (void)r; (void)a; (void)b;
|
||||
}
|
||||
|
||||
static int failures;
|
||||
|
||||
/* Interprets from `start`; checks the hand-back count and what dispatch ran. */
|
||||
static void run_from(uint32_t start, unsigned expect_handbacks,
|
||||
uint32_t expect_compiled, uint32_t expect_pc) {
|
||||
CPUState cpu;
|
||||
memset(&cpu, 0, sizeof cpu);
|
||||
cpu.read_word = psx_read_word;
|
||||
cpu.pc = start;
|
||||
s_compiled_calls = 0;
|
||||
s_compiled_addr = 0;
|
||||
const uint64_t blocks = g_dirty_ram_blocks_run;
|
||||
dirty_ram_dispatch(&cpu, start, 0);
|
||||
const unsigned handbacks = (unsigned)(g_dirty_ram_blocks_run - blocks);
|
||||
printf("start=%08X hand-backs=%u compiled=%u@%08X pc=%08X\n", (unsigned)start,
|
||||
handbacks, s_compiled_calls, (unsigned)s_compiled_addr, (unsigned)cpu.pc);
|
||||
if (handbacks != expect_handbacks) {
|
||||
fprintf(stderr, "FAIL: from %08X: %u hand-backs, expected %u\n",
|
||||
(unsigned)start, handbacks, expect_handbacks);
|
||||
++failures;
|
||||
}
|
||||
if (s_compiled_calls != (expect_compiled ? 1u : 0u) || s_compiled_addr != expect_compiled) {
|
||||
fprintf(stderr, "FAIL: from %08X: compiled dispatch %u@%08X, expected %08X\n",
|
||||
(unsigned)start, s_compiled_calls, (unsigned)s_compiled_addr,
|
||||
(unsigned)expect_compiled);
|
||||
++failures;
|
||||
}
|
||||
if (expect_pc && cpu.pc != expect_pc) {
|
||||
fprintf(stderr, "FAIL: from %08X: pc %08X, expected %08X\n",
|
||||
(unsigned)start, (unsigned)cpu.pc, (unsigned)expect_pc);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
/* Through refused text: one hand-back, then compiled code at 0x42000. */
|
||||
run_from(0x80040FF0u, 1u, 0x80042000u, 0u);
|
||||
/* Control: a clean page outside game text is handed back at once. */
|
||||
run_from(0x80060FF0u, 1u, 0u, 0x80061000u);
|
||||
if (failures) return 1;
|
||||
puts("PASS: interpretation runs through refused game text to an accepted page");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Straight-line interpretation does not hand refused game text back.
|
||||
|
||||
Links the production dirty_ram_interp.c (release build, game dispatch on) into
|
||||
test_interp_refused_game_text.c at -O0 and -O2; every other runtime seam is an
|
||||
abort() stub (fixture_link.py).
|
||||
"""
|
||||
import argparse
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from fixture_link import build_and_run
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--cc', default='gcc')
|
||||
args = parser.parse_args()
|
||||
here = Path(__file__).resolve().parent
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
for opt in ('-O0', '-O2'):
|
||||
build_and_run(args.cc, here, here.parent, opt, Path(root),
|
||||
['dirty_ram_interp.c'], 'test_interp_refused_game_text.c',
|
||||
defines=('PSX_HAS_GAME_DISPATCH', 'PSX_NO_DEBUG_TOOLS'))
|
||||
print('PASS: interpretation runs through refused game text (O0/O2)')
|
||||
@@ -0,0 +1,98 @@
|
||||
#undef NDEBUG /* the Release test target must execute its assertions */
|
||||
/* The MDEC FMV-activity stamp survives a snapshot round trip as itself:
|
||||
* "never decoded" stays never (issue #475: it used to load as "decoded 1000
|
||||
* cycles ago", a false FMV for ~8 frames after every early snapshot load),
|
||||
* a real recent decode stays recent, and v1 payloads still load. */
|
||||
#include "../src/mdec.c"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
uint64_t psx_cycle_count;
|
||||
uint64_t s_frame_count;
|
||||
int debug_server_fmv_quiet(void) { return 1; }
|
||||
|
||||
#define FRAME 338688ull
|
||||
|
||||
static uint8_t *snap(uint32_t *len) {
|
||||
*len = mdec_snapshot_bytes();
|
||||
uint8_t *p = (uint8_t *)malloc(*len);
|
||||
assert(p);
|
||||
mdec_snapshot_write(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
static void put_u32(uint8_t *p, uint32_t off, uint32_t v) {
|
||||
for (int i = 0; i < 4; i++) p[off + i] = (uint8_t)(v >> (8 * i));
|
||||
}
|
||||
static void put_u64(uint8_t *p, uint32_t off, uint64_t v) {
|
||||
for (int i = 0; i < 8; i++) p[off + i] = (uint8_t)(v >> (8 * i));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
uint32_t len;
|
||||
uint8_t *p;
|
||||
const uint32_t age_off = MDEC_SNAP_OFF_COUNTS + 8u;
|
||||
|
||||
/* 1. Nothing decoded: save, load later in the session -> still idle. */
|
||||
mdec_init();
|
||||
psx_cycle_count = 100ull * FRAME;
|
||||
assert(!mdec_recently_active(8));
|
||||
p = snap(&len);
|
||||
psx_cycle_count = 200ull * FRAME;
|
||||
assert(mdec_snapshot_read(p, len));
|
||||
assert(!mdec_recently_active(8));
|
||||
assert(mdec_color_age_cycles() == (uint64_t)0 - 1u);
|
||||
free(p);
|
||||
|
||||
/* 2. A real decode 3 frames before the save stays 3 frames old. */
|
||||
mdec_init();
|
||||
psx_cycle_count = 500ull * FRAME;
|
||||
mdec_last_color_decode_cycle = psx_cycle_count - 3ull * FRAME;
|
||||
p = snap(&len);
|
||||
mdec_init();
|
||||
assert(!mdec_recently_active(8));
|
||||
psx_cycle_count = 500ull * FRAME;
|
||||
assert(mdec_snapshot_read(p, len));
|
||||
assert(mdec_recently_active(8));
|
||||
assert(mdec_color_age_cycles() == 3ull * FRAME);
|
||||
free(p);
|
||||
|
||||
/* 3. A v1 payload's "never" (age 1000) loads as never... */
|
||||
mdec_init();
|
||||
psx_cycle_count = 50ull * FRAME;
|
||||
p = snap(&len);
|
||||
put_u32(p, 0u, MDEC_SNAP_VER_V1);
|
||||
put_u64(p, age_off, MDEC_SNAP_V1_AGE_NEVER);
|
||||
assert(mdec_snapshot_validate(p, len));
|
||||
assert(mdec_snapshot_read(p, len));
|
||||
assert(!mdec_recently_active(8));
|
||||
/* ...and any other v1 age is a real decode. */
|
||||
put_u64(p, age_off, 2ull * FRAME);
|
||||
assert(mdec_snapshot_read(p, len));
|
||||
assert(mdec_recently_active(8));
|
||||
assert(mdec_color_age_cycles() == 2ull * FRAME);
|
||||
free(p);
|
||||
|
||||
/* 4. A decode older than the restored clock's origin is not recent. */
|
||||
mdec_init();
|
||||
psx_cycle_count = 900ull * FRAME;
|
||||
mdec_last_color_decode_cycle = 100ull * FRAME;
|
||||
p = snap(&len);
|
||||
psx_cycle_count = 10ull * FRAME;
|
||||
assert(mdec_snapshot_read(p, len));
|
||||
assert(!mdec_recently_active(8));
|
||||
free(p);
|
||||
|
||||
/* 5. Unknown versions are still refused. */
|
||||
mdec_init();
|
||||
p = snap(&len);
|
||||
put_u32(p, 0u, 3u);
|
||||
assert(!mdec_snapshot_validate(p, len));
|
||||
assert(!mdec_snapshot_read(p, len));
|
||||
free(p);
|
||||
|
||||
puts("mdec_snapshot_activity_test: PASS");
|
||||
return 0;
|
||||
}
|
||||
@@ -124,6 +124,7 @@ for reset in (
|
||||
"psx_mod_set_world_scene_predicate(nullptr);",
|
||||
"psx_mod_set_retained_scene_predicate(nullptr);",
|
||||
"psx_mod_set_adaptive_backdrop_preload(0);",
|
||||
"(void)psx_mod_set_draw_distance_clamp(0);",
|
||||
"g_bezel_path.clear();",
|
||||
"g_frame_interpolation_blend = g_frame_interpolation_blend_default;",
|
||||
"g_frame_interpolation_source = PSX_MOD_FRAME_SOURCE_VBLANK;",
|
||||
|
||||
@@ -34,6 +34,147 @@ int main(void)
|
||||
CHECK(netplay_exit_reason_text("") == NULL);
|
||||
CHECK(netplay_exit_reason_text(NULL) == NULL);
|
||||
|
||||
/* A start that failed says what failed. */
|
||||
{
|
||||
char why[NETPLAY_START_FAILURE_CAP];
|
||||
static const char long_text[] =
|
||||
"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234";
|
||||
/* The port is in use: the port, the address and the system's error
|
||||
* are named, the player is told what to do, and the build is not
|
||||
* blamed. */
|
||||
netplay_start_failure_text(-3, 1, "127.0.0.1:47810", "127.0.0.1:47811",
|
||||
NETPLAY_BIND_FAILED, 10048, "in use", why, sizeof(why));
|
||||
CHECK(strstr(why, "UDP port 47810 on 127.0.0.1") != NULL);
|
||||
CHECK(strstr(why, "system error 10048") != NULL);
|
||||
CHECK(strstr(why, "Another program or the operating system holds that port") != NULL);
|
||||
CHECK(strstr(why, "Choose another port") != NULL);
|
||||
CHECK(strstr(why, "built without") == NULL && strstr(why, "recomp-net") == NULL);
|
||||
CHECK(strlen(why) < 192);
|
||||
/* The same cause has another number on Linux (98) and on macOS (48).
|
||||
* The longest port and address still fit last_error. */
|
||||
{
|
||||
static const int in_use[] = { 10048, 98, 48 };
|
||||
size_t k;
|
||||
for (k = 0; k < sizeof(in_use) / sizeof(in_use[0]); ++k) {
|
||||
netplay_start_failure_text(-3, 1, "255.255.255.255:65535", "", NETPLAY_BIND_FAILED,
|
||||
in_use[k], "", why, sizeof(why));
|
||||
CHECK(strstr(why, "UDP port 65535 on 255.255.255.255 (system error ") != NULL);
|
||||
CHECK(strstr(why, "holds that port. Choose another port and start again.") != NULL);
|
||||
CHECK(strlen(why) < 192);
|
||||
}
|
||||
}
|
||||
/* A LAN host's own port, any address; no address means any address. */
|
||||
netplay_start_failure_text(-3, 1, "0.0.0.0:7777", "", NETPLAY_BIND_FAILED, 98, NULL, why, sizeof(why));
|
||||
CHECK(strstr(why, "UDP port 7777 on 0.0.0.0 (system error 98)") != NULL);
|
||||
CHECK(strstr(why, "holds that port") != NULL);
|
||||
netplay_start_failure_text(-3, 1, ":7777", "", NETPLAY_BIND_FAILED, 98, NULL, why, sizeof(why));
|
||||
CHECK(strstr(why, "UDP port 7777 on 0.0.0.0 (system error 98)") != NULL);
|
||||
/* A port the system does not hand out (a range Windows keeps for
|
||||
* Hyper-V or WSL: 10013; a port below 1024 elsewhere: 13). The player
|
||||
* is not sent looking for another program. */
|
||||
{
|
||||
static const int denied[] = { 10013, 13 };
|
||||
size_t k;
|
||||
for (k = 0; k < sizeof(denied) / sizeof(denied[0]); ++k) {
|
||||
netplay_start_failure_text(-3, 1, "127.0.0.1:47810", "127.0.0.1:47811",
|
||||
NETPLAY_BIND_FAILED, denied[k], "denied", why, sizeof(why));
|
||||
CHECK(strstr(why, "UDP port 47810 on 127.0.0.1 (system error ") != NULL);
|
||||
CHECK(strstr(why, "The operating system does not allow this port.") != NULL);
|
||||
CHECK(strstr(why, "Choose another port and start again.") != NULL);
|
||||
CHECK(strstr(why, "Another program") == NULL && strstr(why, "holds") == NULL);
|
||||
CHECK(strstr(why, "built without") == NULL);
|
||||
CHECK(strlen(why) < 192);
|
||||
}
|
||||
CHECK(strstr(why, "(system error 13)") != NULL);
|
||||
/* The number alone does not decide: a port that could be bound is not "not allowed". */
|
||||
netplay_start_failure_text(-3, 1, "127.0.0.1:47810", "x", NETPLAY_BIND_OK, 10013, "", why, sizeof(why));
|
||||
CHECK(strstr(why, "does not allow") == NULL);
|
||||
}
|
||||
/* The listen address is not one of this computer's (Windows 10049,
|
||||
* Linux 99, macOS 49). No port is held and another port does not
|
||||
* help, so neither is said. */
|
||||
{
|
||||
static const int no_address[] = { 10049, 99, 49 };
|
||||
size_t k;
|
||||
for (k = 0; k < sizeof(no_address) / sizeof(no_address[0]); ++k) {
|
||||
netplay_start_failure_text(-3, 1, "192.168.1.50:7777", "", NETPLAY_BIND_FAILED,
|
||||
no_address[k], "not available", why, sizeof(why));
|
||||
CHECK(strstr(why, "Netplay could not listen on 192.168.1.50 (system error ") != NULL);
|
||||
CHECK(strstr(why, "That address is not one of this computer's addresses.") != NULL);
|
||||
CHECK(strstr(why, "Check the listen address and start again.") != NULL);
|
||||
CHECK(strstr(why, "holds") == NULL && strstr(why, "another port") == NULL);
|
||||
CHECK(strstr(why, "does not allow") == NULL);
|
||||
}
|
||||
CHECK(strstr(why, "(system error 49)") != NULL);
|
||||
netplay_start_failure_text(-3, 1, "255.255.255.255:65535", "", NETPLAY_BIND_FAILED,
|
||||
10049, "", why, sizeof(why));
|
||||
CHECK(strlen(why) < 192 && strstr(why, "start again.") != NULL);
|
||||
}
|
||||
/* Any other answer of the system: the number and the system's text,
|
||||
* and no cause and no advice the code cannot know. */
|
||||
netplay_start_failure_text(-3, 1, "10.0.0.1:7777", "", NETPLAY_BIND_FAILED, 101,
|
||||
"Network is unreachable", why, sizeof(why));
|
||||
CHECK(strcmp(why, "Netplay could not open UDP port 7777 on 10.0.0.1 "
|
||||
"(system error 101: Network is unreachable).") == 0);
|
||||
CHECK(strstr(why, "holds") == NULL && strstr(why, "Choose") == NULL && strstr(why, "does not allow") == NULL);
|
||||
/* Without a text, a text in another alphabet or a long text: the number alone. */
|
||||
netplay_start_failure_text(-3, 1, "10.0.0.1:7777", "", NETPLAY_BIND_FAILED, 101, NULL, why, sizeof(why));
|
||||
CHECK(strcmp(why, "Netplay could not open UDP port 7777 on 10.0.0.1 (system error 101).") == 0);
|
||||
netplay_start_failure_text(-3, 1, "10.0.0.1:7777", "", NETPLAY_BIND_FAILED, 10055,
|
||||
"\xcc\xe7 \xe4\xe9\xe1\xe8\xdd\xf3\xe9\xec\xef", why, sizeof(why));
|
||||
CHECK(strcmp(why, "Netplay could not open UDP port 7777 on 10.0.0.1 (system error 10055).") == 0);
|
||||
netplay_start_failure_text(-3, 1, "10.0.0.1:7777", "", NETPLAY_BIND_FAILED, 10055, long_text, why, sizeof(why));
|
||||
CHECK(strcmp(why, "Netplay could not open UDP port 7777 on 10.0.0.1 (system error 10055).") == 0);
|
||||
/* The longest values with the longest text that is shown (80 letters) fit. */
|
||||
netplay_start_failure_text(-3, 1, "255.255.255.255:65535", "", NETPLAY_BIND_FAILED, -2147483647,
|
||||
long_text + 5, why, sizeof(why));
|
||||
CHECK(strlen(why) < 192 && strstr(why, "01234).") != NULL);
|
||||
/* The system made no socket: no port and no address is blamed. */
|
||||
netplay_start_failure_text(-3, 1, "0.0.0.0:7777", "", NETPLAY_BIND_NO_SOCKET, 24,
|
||||
"Too many open files", why, sizeof(why));
|
||||
CHECK(strcmp(why, "Netplay could not open a network socket (system error 24: Too many open files).") == 0);
|
||||
/* The build is named only when the build has no netplay. */
|
||||
netplay_start_failure_text(-1, 0, "127.0.0.1:47810", "", NETPLAY_BIND_NOT_TRIED, 0, "", why, sizeof(why));
|
||||
CHECK(strstr(why, "This build has no netplay") != NULL);
|
||||
CHECK(strstr(why, "port") == NULL);
|
||||
/* The listen address opened and the peer address cannot be read: the
|
||||
* peer address is the cause. */
|
||||
netplay_start_failure_text(-3, 1, "127.0.0.1:47810", "not an address", NETPLAY_BIND_PEER_BAD, 0, "",
|
||||
why, sizeof(why));
|
||||
CHECK(strstr(why, "the other player's address \"not an address\"") != NULL);
|
||||
CHECK(strstr(why, "holds that port") == NULL);
|
||||
/* The peer address is not blamed when nothing was tried, or when the
|
||||
* listen address opened and the peer address reads well: no cause is
|
||||
* known then. */
|
||||
netplay_start_failure_text(-3, 1, "localhost:40010", "127.0.0.1:40011", NETPLAY_BIND_NOT_TRIED, 0, "",
|
||||
why, sizeof(why));
|
||||
CHECK(strcmp(why, "Netplay could not start its connection on localhost:40010.") == 0);
|
||||
netplay_start_failure_text(-3, 1, "127.0.0.1:40010", "127.0.0.1:40011", NETPLAY_BIND_OK, 0, "",
|
||||
why, sizeof(why));
|
||||
CHECK(strcmp(why, "Netplay could not start its connection on 127.0.0.1:40010.") == 0);
|
||||
CHECK(strstr(why, "other player") == NULL);
|
||||
/* A listen address that is not address:port. */
|
||||
netplay_start_failure_text(-3, 1, "47810", "", NETPLAY_BIND_BAD_ADDRESS, 0, "", why, sizeof(why));
|
||||
CHECK(strstr(why, "\"47810\" is not an address and a port") != NULL);
|
||||
/* A host waiting for the first peer (no peer address), port free. */
|
||||
netplay_start_failure_text(-3, 1, "0.0.0.0:7777", "", NETPLAY_BIND_OK, 0, "", why, sizeof(why));
|
||||
CHECK(strstr(why, "its connection on 0.0.0.0:7777") != NULL);
|
||||
/* Online (relay): five causes share the code, so none is named and
|
||||
* the player is not sent to a log a released game does not show. */
|
||||
netplay_start_failure_text(-4, 1, "0.0.0.0:0", "", NETPLAY_BIND_NOT_TRIED, 0, "", why, sizeof(why));
|
||||
CHECK(strcmp(why, "Netplay could not start the online connection.") == 0);
|
||||
/* Any other code. */
|
||||
netplay_start_failure_text(-2, 1, "", "", NETPLAY_BIND_NOT_TRIED, 0, "", why, sizeof(why));
|
||||
CHECK(strstr(why, "(code -2)") != NULL);
|
||||
/* Missing strings and a short buffer are safe. */
|
||||
netplay_start_failure_text(-3, 1, NULL, NULL, NETPLAY_BIND_FAILED, 5, NULL, why, sizeof(why));
|
||||
CHECK(strlen(why) > 20 && strlen(why) < 192);
|
||||
memset(why, 'x', sizeof(why));
|
||||
netplay_start_failure_text(-3, 1, "127.0.0.1:47810", "", NETPLAY_BIND_FAILED, 10048, "", why, 8);
|
||||
CHECK(strlen(why) == 7 && why[8] == 'x');
|
||||
netplay_start_failure_text(-3, 1, "127.0.0.1:47810", "", NETPLAY_BIND_FAILED, 10048, "", NULL, 0);
|
||||
}
|
||||
|
||||
/* No mismatch seen: never final. */
|
||||
CHECK(!netplay_boot_mismatch_final(0u, 100000u, NETPLAY_BOOT_MISMATCH_GRACE_MS));
|
||||
/* Seen, grace not yet over, then over. */
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Guard that a failed netplay start says what failed.
|
||||
|
||||
The start printed "built without recomp-net, or bind/peer invalid" for every
|
||||
failure. A player whose system held the UDP port read first that the build
|
||||
had no netplay, and a match started from the launcher closed the program with
|
||||
no text. main.cpp now does the library's steps again, reports the system's
|
||||
answer, and takes a launcher match back to the room with the sentence. The
|
||||
sentences are tested in test_netplay_exit_reason.c; a refused bind needs a
|
||||
held port and the launcher path needs a room, so that main.cpp probes, prints,
|
||||
stops a command-line start and returns a launcher match is checked in the
|
||||
source.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MAIN = (ROOT / "runtime" / "src" / "main.cpp").read_text(encoding="utf-8")
|
||||
|
||||
# The old sentence, which named the build for every failure, is gone.
|
||||
assert "built without recomp-net, " not in MAIN, "the start failure still blames the build first"
|
||||
|
||||
starts = [m.start() for m in re.finditer(r"psx_netplay_start\(&net_cfg\)", MAIN)]
|
||||
assert len(starts) == 1, "main.cpp must start a netplay session in exactly one place"
|
||||
after = MAIN[starts[0] : starts[0] + 900]
|
||||
|
||||
# A failed start asks for the sentence, stops a command-line start with code 1,
|
||||
# and takes a launcher match back to the room with the sentence kept for the
|
||||
# status line.
|
||||
failed = after.index("if (nrc != 0) {")
|
||||
block = after[failed : after.index("} else {", failed)]
|
||||
assert "netplay_start_failure(nrc, net_cfg)" in block, "the failure is not explained"
|
||||
assert re.search(r"if \(!g_netplay_from_lobby\)\s*return 1;", block), "a command-line start must stop with exit code 1"
|
||||
ends = block.index('netplay_soft_exit("netplay_start_failed");')
|
||||
assert block.index("return 1;") < ends, "a command-line start must stop before the return to the room"
|
||||
assert "g_netplay_exit_reason_text = why;" in block[ends:], (
|
||||
"the sentence must be kept for the launcher's status line after the soft exit"
|
||||
)
|
||||
assert "psx_lobby_set_last_error(g_netplay_exit_reason_text);" in MAIN, "the soft return no longer shows the reason"
|
||||
|
||||
# The guest is not entered after a failed launcher start: the return to the
|
||||
# lobby is taken before the scheduler runs.
|
||||
runs = [m.start() for m in re.finditer(r"(?m)^\s*psx_scheduler_run\(&cpu\);", MAIN)]
|
||||
assert len(runs) == 1, "expected one scheduler entry"
|
||||
assert re.search(
|
||||
r"if \(psx_return_to_lobby_requested\(\) && g_netplay_from_lobby\)\s*goto soft_return_lobby;\s*$",
|
||||
MAIN[runs[0] - 260 : runs[0]],
|
||||
), "a failed launcher start would boot the game offline: no return to the lobby before the scheduler"
|
||||
|
||||
# The explanation: the bind is tried again for a LAN start only, the system's
|
||||
# answer goes into the sentence and the log line, and the build is named from
|
||||
# the build's own definition.
|
||||
helper = MAIN[MAIN.index("static const char* netplay_start_failure(int nrc") :]
|
||||
helper = helper[: helper.index("\n}\n") + 3]
|
||||
assert re.search(r"#if defined\(PSX_HAS_RECOMP_NET\)\s*const int netplay_built = 1;\s*#else\s*const int netplay_built = 0;",
|
||||
helper), "whether the build has netplay must come from the build"
|
||||
assert re.search(r"\(netplay_built && nrc == -3\)\s*\? netplay_bind_probe\(cfg\.bind_hostport, cfg\.peer_hostport, tried, sizeof\(tried\),\s*"
|
||||
r"&sys_error, sys_text, sizeof\(sys_text\)\)\s*: NETPLAY_BIND_NOT_TRIED;", helper), (
|
||||
"the bind must be tried again for a LAN start, and only then"
|
||||
)
|
||||
# The sentence names the address that was bound, and gets the system's number and text.
|
||||
assert re.search(r"tried\[0\] \? tried : cfg\.bind_hostport,\s*cfg\.peer_hostport, bind_probe, sys_error, sys_text,", helper), (
|
||||
"the sentence must get the bound address, the probe result and the system's answer"
|
||||
)
|
||||
assert "netplay_start_failure_text(nrc, netplay_built," in helper, "the sentence is not built"
|
||||
assert "std::fprintf(stderr" in helper and "netplay start failed (%d)" in helper and "[system: " in helper, (
|
||||
"the log line must carry the sentence and the system's text"
|
||||
)
|
||||
|
||||
# The probe binds without address reuse, on both platforms, and keeps the
|
||||
# system's error number.
|
||||
probe = MAIN[MAIN.index("static int netplay_bind_probe(") : MAIN.index("static const char* netplay_start_failure(int nrc")]
|
||||
assert "SO_REUSEADDR" not in probe, "the probe must not ask for address reuse: a held port would go unseen"
|
||||
assert "WSAGetLastError()" in probe and "errno" in probe, "the system's error number is not read on both platforms"
|
||||
assert probe.count("bind(s, (const sockaddr*)&addr, sizeof(addr))") == 2, "the probe must bind on both platforms"
|
||||
assert "closesocket(s);" in probe and "close(s);" in probe, "the probe must close its socket"
|
||||
assert probe.count("result = NETPLAY_BIND_NO_SOCKET;") == 2, "a socket that could not be made is not a refused port"
|
||||
|
||||
# A sentence says a cause only when the probe found it.
|
||||
# The listen address is read the way the library reads it: a name is looked up, so a held port behind a name
|
||||
# is found, and no address before the colon means every address. The probe never skips the bind.
|
||||
reader = MAIN[MAIN.index("static int netplay_hostport_addr(") : MAIN.index("/* Why a LAN netplay start failed")]
|
||||
assert "getaddrinfo(host, nullptr, &hints, &found)" in reader, "a host name must be looked up before the bind"
|
||||
assert re.search(r'if \(!host\[0\] \|\| std::strcmp\(host, "0\.0\.0\.0"\) == 0\) \{\s*out->sin_addr\.s_addr = htonl\(INADDR_ANY\);',
|
||||
reader), "no address before the colon must mean every address, as in the library"
|
||||
assert "NETPLAY_BIND_NOT_TRIED" not in probe, "the probe must not leave a listen address untried"
|
||||
assert re.search(r"if \(netplay_hostport_addr\(bind_hostport, &addr\) != 0\)\s*return NETPLAY_BIND_BAD_ADDRESS;", probe), (
|
||||
"a listen address the library refuses must be reported as such"
|
||||
)
|
||||
# The peer address is read only after the listen address opened, and is named only when it cannot be read.
|
||||
opened = probe.index("if (result != NETPLAY_BIND_OK)")
|
||||
peer = probe.index("netplay_hostport_addr(peer_hostport, &peer) != 0 || peer.sin_port == 0")
|
||||
assert opened < peer < probe.index("return NETPLAY_BIND_PEER_BAD;"), "the peer address is judged before the listen address"
|
||||
|
||||
REASON = (ROOT / "runtime" / "src" / "netplay_exit_reason.c").read_text(encoding="utf-8")
|
||||
text = REASON[REASON.index("void netplay_start_failure_text(") :]
|
||||
text = text[: text.index("\n}\n")]
|
||||
# Each cause the system names has its own sentence, chosen by the number; any other number gets the neutral one.
|
||||
order = [
|
||||
"bind_error_kind(sys_error) == BIND_ERROR_NOT_ALLOWED",
|
||||
"The operating system does not allow this port.",
|
||||
"bind_error_kind(sys_error) == BIND_ERROR_IN_USE",
|
||||
"holds that ",
|
||||
"bind_error_kind(sys_error) == BIND_ERROR_NO_SUCH_ADDR",
|
||||
"address is not one of this computer's addresses.",
|
||||
"start_rc == -3 && bind_probe == NETPLAY_BIND_FAILED) {",
|
||||
'Netplay could not open UDP port %.5s on %.15s %s."',
|
||||
"bind_probe == NETPLAY_BIND_PEER_BAD",
|
||||
"the other player's address ",
|
||||
]
|
||||
at = [text.index(piece) for piece in order]
|
||||
assert at == sorted(at), "the start-failure sentences are not chosen in the expected order"
|
||||
assert text.count("the other player's address ") == 1, "the peer address is blamed in more than one case"
|
||||
assert re.search(r"case 10048: case 98: case 48:\s*return BIND_ERROR_IN_USE;", REASON), "address in use: Windows, Linux, macOS"
|
||||
assert re.search(r"case 10013: case 13:\s*return BIND_ERROR_NOT_ALLOWED;", REASON), "access denied: Windows, POSIX"
|
||||
assert re.search(r"case 10049: case 99: case 49:\s*return BIND_ERROR_NO_SUCH_ADDR;", REASON), (
|
||||
"address not available: Windows, Linux, macOS"
|
||||
)
|
||||
# The online failure names no log and no setting: the code does not say which of its causes it was.
|
||||
online = text[text.index("start_rc == -4") :]
|
||||
online = online[: online.index("} else {")]
|
||||
assert "The log" not in online and "setting" not in online.split("*/")[-1], "the online sentence says more than the code knows"
|
||||
|
||||
print("netplay start failure wiring test: PASS")
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Relative paths persisted in settings.toml and the bios.cfg / disc.cfg
|
||||
sidecars resolve from the executable directory, not the working directory.
|
||||
|
||||
Compiles the real adapters from main.cpp (normalize_disc_path_for_launch,
|
||||
resolve_persisted_disc_path, anchor_on_exe_dir) into a probe and runs it from
|
||||
an unrelated working directory. Also checks that every persisted-path consumer
|
||||
in main.cpp goes through an adapter. No game data is used.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
# Every place main.cpp reads a persisted path must anchor it on the exe
|
||||
# directory. Each line below must appear in main.cpp exactly as written.
|
||||
CALL_SITES = [
|
||||
'resolved_disc = resolve_persisted_disc_path(us.disc_path, exe_dir_from_argv(argv[0]))',
|
||||
'cached = resolve_persisted_disc_path(cached, exe_dir_from_argv(argv0))',
|
||||
': anchor_on_exe_dir(argv0, std::filesystem::path(line));',
|
||||
'settings_bios_storage = anchor_on_exe_dir(argv[0], us.bios_path).string();',
|
||||
'memcard_dir = anchor_on_exe_dir(argv[0], us.memcard_dir);',
|
||||
'memcard1_path = anchor_on_exe_dir(argv[0], us.memcard1_path);',
|
||||
'memcard2_path = anchor_on_exe_dir(argv[0], us.memcard2_path);',
|
||||
'memcard1_path = anchor_on_exe_dir(argv[0], seed.memcard1_path);',
|
||||
'memcard2_path = anchor_on_exe_dir(argv[0], seed.memcard2_path);',
|
||||
]
|
||||
|
||||
|
||||
def run(args, **kwargs):
|
||||
return subprocess.run(args, capture_output=True, text=True,
|
||||
encoding='utf-8', errors='replace', **kwargs)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--compiler', default=os.environ.get('CXX', 'c++'))
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
text = (root / 'src/main.cpp').read_text(encoding='utf-8')
|
||||
for site in CALL_SITES:
|
||||
assert site in text, 'main.cpp no longer anchors: ' + site
|
||||
adapters = []
|
||||
for name in ('normalize_disc_path_for_launch', 'resolve_persisted_disc_path',
|
||||
'anchor_on_exe_dir'):
|
||||
start = text.index('static std::filesystem::path ' + name + '(')
|
||||
adapters.append(text[start:text.index('\n}\n', start) + 2])
|
||||
with tempfile.TemporaryDirectory(prefix='persisted paths spaces ') as tmp:
|
||||
base = Path(tmp)
|
||||
product, cwd = base / 'moved product', base / 'unrelated cwd'
|
||||
(product / 'inputs').mkdir(parents=True)
|
||||
(cwd / 'inputs').mkdir(parents=True)
|
||||
relative = 'inputs/Some Game [Disc 1] (v1.1).chd'
|
||||
(product / relative).write_bytes(b'authored placeholder')
|
||||
(cwd / relative).write_bytes(b'wrong cwd decoy')
|
||||
source = base / 'probe.cpp'
|
||||
# The probe's exe_dir_from_argv() stands in for the real one: the
|
||||
# "argv0" it receives is the product directory itself.
|
||||
source.write_text('''#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include "host_path.h"
|
||||
#include "disc_path.h"
|
||||
static std::filesystem::path exe_dir_from_argv(const char* dir) {
|
||||
return std::filesystem::path(dir);
|
||||
}
|
||||
''' + '\n'.join(adapters) + '''
|
||||
int main(int argc, char** argv) {
|
||||
(void)argc;
|
||||
const std::string mode = argv[1];
|
||||
const std::filesystem::path path = argv[2];
|
||||
const std::filesystem::path resolved =
|
||||
mode == "persisted" ? resolve_persisted_disc_path(path, argv[3])
|
||||
: mode == "anchor" ? anchor_on_exe_dir(argv[3], path)
|
||||
: normalize_disc_path_for_launch(path);
|
||||
// libstdc++'s generic_string() collapses a UNC "\\\\server" root to "/server";
|
||||
// print the native spelling with forward slashes so both C++ libraries agree.
|
||||
std::string out = resolved.string();
|
||||
for (char& c : out) if (c == '\\\\') c = '/';
|
||||
std::cout << out;
|
||||
}
|
||||
''', encoding='utf-8')
|
||||
compiler = shutil.which(args.compiler) or args.compiler
|
||||
env = os.environ.copy()
|
||||
env['PATH'] = str(Path(compiler).parent) + os.pathsep + env.get('PATH', '')
|
||||
exe = base / ('probe.exe' if os.name == 'nt' else 'probe')
|
||||
subprocess.run([compiler, '-std=c++17', '-Werror=return-type', '-I'+str(root/'include'),
|
||||
'-I'+str(root.parent/'recompiler/include'), str(source),
|
||||
str(root/'src/disc_path.cpp'), str(root/'src/cue_sheet.cpp'), '-o', str(exe)],
|
||||
check=True, env=env)
|
||||
|
||||
def probe(mode, path):
|
||||
result = run([str(exe), mode, str(path), str(product)], cwd=cwd, env=env)
|
||||
assert result.returncode == 0, (mode, path, result.stderr)
|
||||
return result.stdout
|
||||
|
||||
def check(mode, path, expected):
|
||||
out = probe(mode, path)
|
||||
assert out == expected, (mode, path, out, expected)
|
||||
|
||||
def check_same(mode, path, expected):
|
||||
out = probe(mode, path)
|
||||
assert Path(out).resolve() == Path(expected).resolve(), (mode, path, out, expected)
|
||||
|
||||
# [disc] path and disc.cfg
|
||||
check('persisted', relative, (product/relative).as_posix())
|
||||
check('persisted', product/relative, (product/relative).as_posix())
|
||||
check('persisted', '', '')
|
||||
check('persisted', 'inputs/missing.chd', (product/'inputs/missing.chd').as_posix())
|
||||
check('cli', relative, (cwd/relative).as_posix())
|
||||
if os.name == 'nt':
|
||||
check('persisted', r'\\server\share\Some Game [Disc 1].chd',
|
||||
'//server/share/Some Game [Disc 1].chd')
|
||||
# [bios] path, bios.cfg and [memcard] dir/card1/card2
|
||||
check_same('anchor', '.', product)
|
||||
check_same('anchor', 'cards/card1.mcd', product/'cards/card1.mcd')
|
||||
check_same('anchor', 'bios/SCPH1001.BIN', product/'bios/SCPH1001.BIN')
|
||||
check('anchor', product/'cards', (product/'cards').as_posix())
|
||||
check('anchor', '', '')
|
||||
print('persisted paths: disc (relative, absolute, empty, missing, CLI cwd, '
|
||||
'Windows UNC) and BIOS/memory card (".", relative, absolute, empty) passed')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "pgxp.h"
|
||||
#include "pgxp_hooks.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
@@ -388,6 +389,284 @@ int main(void) {
|
||||
}
|
||||
g_fb_valid = 0;
|
||||
|
||||
/* --- dataflow only (G1.11): position fallback off skips the cache --- */
|
||||
g_fb_valid = 1; g_fb_packed = PACKED; g_fb_x16 = X16; g_fb_y16 = Y16;
|
||||
CHECK(pgxp_position_fallback() == 1); /* default: unchanged */
|
||||
pgxp_set_position_fallback(0);
|
||||
{
|
||||
int32_t x, y; uint16_t z;
|
||||
CHECK(lookup(0xFFFFFFFFu, PACKED, 160, 80, &x, &y, &z) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
CHECK(x == (160 << 16) && y == (80 << 16) && z == 0);
|
||||
/* a validated dataflow shadow is unaffected */
|
||||
produce_at(ADDR_A);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, &x, &y, &z) == PGXP_SRC_DATAFLOW);
|
||||
/* a stale shadow no longer falls through to the cache */
|
||||
uint32_t other = (81u << 16) | 161u;
|
||||
g_fb_packed = other;
|
||||
g_fb_x16 = (161 << 16) | 0x8000;
|
||||
g_fb_y16 = (81 << 16) | 0x4000;
|
||||
CHECK(lookup(ADDR_A, other, 161, 81, &x, &y, &z) == PGXP_SRC_NATIVE);
|
||||
pgxp_set_position_fallback(1);
|
||||
CHECK(lookup(ADDR_A, other, 161, 81, &x, &y, &z) == PGXP_SRC_FALLBACK);
|
||||
}
|
||||
g_fb_valid = 0;
|
||||
|
||||
/* --- preserve projection (G1.11): the agreement window ---------------- */
|
||||
{
|
||||
CHECK(pgxp_preserve_projection() == 0); /* default: unchanged */
|
||||
auto seed = [](int32_t x16, int32_t y16, uint32_t packed) {
|
||||
pgxp_gte_push_sxy(x16, y16, SZ3, packed);
|
||||
psx_pgxp_cop2(nullptr, SWC2(14), packed, ADDR_A);
|
||||
};
|
||||
const int32_t half = 1 << 15;
|
||||
/* exact projection half a pixel BELOW the guest integer: the IR path
|
||||
* can never produce that, so off rejects it and on accepts it */
|
||||
seed((160 << 16) - half, Y16, PACKED);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
pgxp_set_preserve_projection(1);
|
||||
{
|
||||
int32_t x, y;
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, &x, &y, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
CHECK(x == (160 << 16) - half && y == Y16);
|
||||
}
|
||||
/* window edges: (-BELOW, +ABOVE) px exclusive */
|
||||
const int32_t hi = PGXP_PPP_AGREE_ABOVE, lo = PGXP_PPP_AGREE_BELOW;
|
||||
seed(((160 + hi) << 16) - 1, Y16, PACKED);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
seed((160 + hi) << 16, Y16, PACKED);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
seed(((160 - lo) << 16) + 1, Y16, PACKED);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
seed((160 - lo) << 16, Y16, PACKED);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
seed(X16, (80 + hi) << 16, PACKED); /* Y axis too */
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
/* a half beyond the GTE range, which the GPU's 11-bit parse wraps
|
||||
* (a CPU-modified word), needs exact agreement. A shadow describes the
|
||||
* half read as int16 (0x0402 = 1026); the lookup rebases it onto the
|
||||
* 11-bit field the GPU parsed (-1022), keeping the fraction: 1026.5
|
||||
* draws at -1021.5, which truncates to -1022. A shadow whose integer
|
||||
* is not the half's int16 does not describe the word. */
|
||||
const uint32_t wrap = (80u << 16) | 0x0402u;
|
||||
seed((1026 << 16) + half, Y16, wrap);
|
||||
CHECK(lookup(ADDR_A, wrap, -1022, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
seed(-(1022 << 16) + half, Y16, wrap);
|
||||
CHECK(lookup(ADDR_A, wrap, -1022, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
seed((1026 << 16) - half, Y16, wrap); /* 1025.5: not exact */
|
||||
CHECK(lookup(ADDR_A, wrap, -1022, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
/* an 11-bit wrapped coordinate is far outside any window */
|
||||
seed(X16 + (2048 << 16), Y16, PACKED);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
/* at the GTE saturation limit the guest integer is a clamp: the
|
||||
* window does not apply, agreement must be exact */
|
||||
const uint32_t sat = (80u << 16) | 0x3FFu; /* SX saturated at 1023 */
|
||||
seed((1023 << 16) + half, Y16, sat);
|
||||
CHECK(lookup(ADDR_A, sat, 1023, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW); /* exact agreement ok */
|
||||
seed((1024 << 16) + half, Y16, sat);
|
||||
CHECK(lookup(ADDR_A, sat, 1023, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
const uint32_t sat_lo = (0xFC00u << 16) | 160u; /* SY at -1024 */
|
||||
seed(X16, -(1024 << 16) - half, sat_lo);
|
||||
CHECK(lookup(ADDR_A, sat_lo, 160, -1024, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
/* the tolerance clamp measures the offset both ways */
|
||||
seed((160 << 16) - half, Y16, PACKED);
|
||||
pgxp_set_tolerance(0.25f);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
pgxp_set_tolerance(0.75f);
|
||||
CHECK(lookup(ADDR_A, PACKED, 160, 80, nullptr, nullptr, nullptr) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
pgxp_set_preserve_projection(0);
|
||||
}
|
||||
|
||||
/* --- pgxp_project_precise: the exact projection and its qualifiers --- */
|
||||
{
|
||||
/* V = (100, -50, 1000) through the identity: MAC = V * 4096 (sf=1),
|
||||
* IR = V, SZ3 = 1000, H = 300, OFX/OFY = (160, 120) in 16.16. The
|
||||
* exact screen position is (160 + 100 * 0.3, 120 - 50 * 0.3). */
|
||||
const int64_t m1 = 100 * 4096 + 1234; /* fractional MAC bits */
|
||||
const int64_t m2 = -50 * 4096 + 777;
|
||||
const int64_t m3 = 1000 * 4096 + 2048;
|
||||
const int32_t ofx = 160 << 16, ofy = 120 << 16;
|
||||
int32_t x, y;
|
||||
CHECK(pgxp_project_precise(m1, m2, m3, 12, (int32_t)(m1 >> 12),
|
||||
(int32_t)(m2 >> 12), (uint32_t)(m3 >> 12),
|
||||
300, ofx, ofy, 1, 1, &x, &y) == 1);
|
||||
const double ex = 160.0 + (double)m1 * 300.0 / (double)m3;
|
||||
const double ey = 120.0 + (double)m2 * 300.0 / (double)m3;
|
||||
CHECK(x == (int32_t)std::floor(ex * 65536.0));
|
||||
CHECK(y == (int32_t)std::floor(ey * 65536.0));
|
||||
/* the horizontal widescreen factor is applied to X only */
|
||||
int32_t xs, ys;
|
||||
CHECK(pgxp_project_precise(m1, m2, m3, 12, (int32_t)(m1 >> 12),
|
||||
(int32_t)(m2 >> 12), (uint32_t)(m3 >> 12),
|
||||
300, ofx, ofy, 3, 4, &xs, &ys) == 1);
|
||||
CHECK(ys == y);
|
||||
CHECK(xs == (int32_t)std::floor((160.0 + (double)m1 * 300.0 /
|
||||
(double)m3 * 0.75) * 65536.0));
|
||||
/* disqualified: sf=0, a clamped IR, SZ3 0 or clamped, divide
|
||||
* overflow (H >= 2*SZ3) -- the caller keeps the IR path */
|
||||
CHECK(pgxp_project_precise(m1, m2, m3, 0, (int32_t)(m1 >> 12),
|
||||
(int32_t)(m2 >> 12), 1000, 300, ofx, ofy,
|
||||
1, 1, &x, &y) == 0);
|
||||
CHECK(pgxp_project_precise(m1, m2, m3, 12, 0x7FFF, (int32_t)(m2 >> 12),
|
||||
1000, 300, ofx, ofy, 1, 1, &x, &y) == 0);
|
||||
CHECK(pgxp_project_precise(m1, m2, 2048, 12, (int32_t)(m1 >> 12),
|
||||
(int32_t)(m2 >> 12), 0, 300, ofx, ofy, 1, 1,
|
||||
&x, &y) == 0);
|
||||
CHECK(pgxp_project_precise(m1, m2, (int64_t)0x12345 << 12, 12,
|
||||
(int32_t)(m1 >> 12), (int32_t)(m2 >> 12),
|
||||
0xFFFF, 300, ofx, ofy, 1, 1, &x, &y) == 0);
|
||||
CHECK(pgxp_project_precise(m1, m2, 150 * 4096, 12, (int32_t)(m1 >> 12),
|
||||
(int32_t)(m2 >> 12), 150, 300, ofx, ofy, 1, 1,
|
||||
&x, &y) == 0);
|
||||
CHECK(pgxp_project_precise(m1, m2, 151 * 4096, 12, (int32_t)(m1 >> 12),
|
||||
(int32_t)(m2 >> 12), 151, 300, ofx, ofy, 1, 1,
|
||||
&x, &y) == 1);
|
||||
}
|
||||
|
||||
/* --- probe: the same decision, not counted --- */
|
||||
{
|
||||
produce_at(ADDR_A);
|
||||
PGXPStats a, b;
|
||||
pgxp_get_stats(&a);
|
||||
CHECK(pgxp_probe_precise_vertex(ADDR_A, PACKED, 160, 80) ==
|
||||
PGXP_SRC_DATAFLOW);
|
||||
CHECK(pgxp_probe_precise_vertex(ADDR_A, PACKED, 161, 80) ==
|
||||
PGXP_SRC_NATIVE);
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(std::memcmp(&a, &b, sizeof a) == 0);
|
||||
pgxp_note_rect_bypass(1);
|
||||
pgxp_note_rect_bypass(0);
|
||||
pgxp_note_rect_bypass(0);
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(b.rect_bypass == a.rect_bypass + 1);
|
||||
CHECK(b.rect_partial == a.rect_partial + 2);
|
||||
}
|
||||
|
||||
/* --- preserve projection: the RTPS-side window check (G1.11) ---------- */
|
||||
{
|
||||
pgxp_set_preserve_projection(1);
|
||||
PGXPStats a, b;
|
||||
pgxp_get_stats(&a);
|
||||
const int32_t half = 1 << 15;
|
||||
/* inside the window: the exact projection is the shadow */
|
||||
CHECK(pgxp_ppp_accept((160 << 16) - half, Y16, PACKED) == 1);
|
||||
CHECK(pgxp_ppp_accept(((160 + PGXP_PPP_AGREE_ABOVE) << 16) - 1, Y16,
|
||||
PACKED) == 1);
|
||||
/* outside it (either axis): RTPS keeps the IR path's shadow */
|
||||
CHECK(pgxp_ppp_accept((160 + PGXP_PPP_AGREE_ABOVE) << 16, Y16,
|
||||
PACKED) == 0);
|
||||
CHECK(pgxp_ppp_accept(X16, (80 - PGXP_PPP_AGREE_BELOW) << 16,
|
||||
PACKED) == 0);
|
||||
/* at the saturation limit agreement is exact */
|
||||
const uint32_t sat = (80u << 16) | 0x3FFu;
|
||||
CHECK(pgxp_ppp_accept((1023 << 16) + half, Y16, sat) == 1);
|
||||
CHECK(pgxp_ppp_accept((1024 << 16) + half, Y16, sat) == 0);
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(b.ppp_produced == a.ppp_produced + 3);
|
||||
CHECK(b.ppp_window_fallback == a.ppp_window_fallback + 3);
|
||||
pgxp_set_preserve_projection(0);
|
||||
}
|
||||
|
||||
/* --- NCLIP's exact determinant (G1.12) -------------------------------- */
|
||||
{
|
||||
/* A far road row: native y 113, 113, 113 (zero area), exact y
|
||||
* 112.64 / 113.12 / 113.83 -> positive area. */
|
||||
const uint32_t w0 = (113u << 16) | 100u;
|
||||
const uint32_t w1 = (113u << 16) | 220u;
|
||||
const uint32_t w2 = (113u << 16) | 160u;
|
||||
const int32_t y0 = (112 << 16) + (int32_t)(0.64 * 65536);
|
||||
const int32_t y1 = (113 << 16) + (int32_t)(0.12 * 65536);
|
||||
const int32_t y2 = (113 << 16) + (int32_t)(0.83 * 65536);
|
||||
auto seed3 = [&](void) {
|
||||
pgxp_test_seed_gte_sxy(0, w0, 100 << 16, y0, SZ3, 1);
|
||||
pgxp_test_seed_gte_sxy(1, w1, 220 << 16, y1, SZ3, 1);
|
||||
pgxp_test_seed_gte_sxy(2, w2, 160 << 16, y2, SZ3, 1);
|
||||
};
|
||||
const uint32_t words[3] = { w0, w1, w2 };
|
||||
auto expect_cross = [&](void) {
|
||||
return ((int64_t)(220 << 16) - (100 << 16)) * ((int64_t)y2 - y0) -
|
||||
((int64_t)y1 - y0) * ((int64_t)(160 << 16) - (100 << 16));
|
||||
};
|
||||
PGXPStats a, b;
|
||||
pgxp_get_stats(&a);
|
||||
int64_t cross = 0;
|
||||
/* IR path: y0 = 112.64 does not truncate to 113 -> not believed */
|
||||
seed3();
|
||||
CHECK(pgxp_gte_nclip_precise(words, &cross) == 0);
|
||||
/* preserve-projection window: believed, exact determinant */
|
||||
pgxp_set_preserve_projection(1);
|
||||
seed3();
|
||||
CHECK(pgxp_gte_nclip_precise(words, &cross) == 1);
|
||||
CHECK(cross == expect_cross());
|
||||
CHECK(cross > 0);
|
||||
/* a stale shadow (word changed under it) fails closed */
|
||||
const uint32_t stale[3] = { w0, w1, w2 ^ 1u };
|
||||
CHECK(pgxp_gte_nclip_precise(stale, &cross) == 0);
|
||||
/* the tolerance clamp applies as at the GPU */
|
||||
pgxp_set_tolerance(0.25f);
|
||||
CHECK(pgxp_gte_nclip_precise(words, &cross) == 0);
|
||||
pgxp_set_tolerance(-1.0f);
|
||||
/* nothing inside a suppression bracket */
|
||||
pgxp_suppress_begin();
|
||||
CHECK(pgxp_gte_nclip_precise(words, &cross) == 0);
|
||||
pgxp_suppress_end();
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(b.nclip_precise == a.nclip_precise + 1);
|
||||
pgxp_note_nclip(1, 0);
|
||||
pgxp_note_nclip(1, 1);
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(b.nclip_disagree == a.nclip_disagree + 2);
|
||||
CHECK(b.nclip_corrected == a.nclip_corrected + 1);
|
||||
pgxp_set_preserve_projection(0);
|
||||
}
|
||||
|
||||
/* --- the mod request lives until the session's arming takes it -------- */
|
||||
{
|
||||
CHECK(pgxp_culling() == 0); /* default off */
|
||||
pgxp_set_culling(1);
|
||||
CHECK(pgxp_culling() == 1);
|
||||
pgxp_set_culling(0);
|
||||
int cpu = -1, cull = -1;
|
||||
pgxp_mod_request(1, 1, 1);
|
||||
CHECK(pgxp_mod_requested(&cpu, &cull) == 1 && cpu == 1 && cull == 1);
|
||||
CHECK(pgxp_mod_requested(nullptr, nullptr) == 1); /* peek keeps it */
|
||||
CHECK(pgxp_mod_request_take(&cpu, &cull) == 1 && cpu == 1 && cull == 1);
|
||||
CHECK(pgxp_mod_request_take(&cpu, &cull) == 0 && cpu == 0 && cull == 0);
|
||||
pgxp_mod_request(0, 1, 1); /* options need the mod */
|
||||
CHECK(pgxp_mod_requested(&cpu, &cull) == 0 && cpu == 0 && cull == 0);
|
||||
}
|
||||
|
||||
/* --- triangle census (G1.1 crack exposure) --- */
|
||||
{
|
||||
PGXPStats a, b;
|
||||
pgxp_get_stats(&a);
|
||||
pgxp_note_triangle(3);
|
||||
pgxp_note_triangle(2);
|
||||
pgxp_note_triangle(1);
|
||||
pgxp_note_triangle(0);
|
||||
pgxp_get_stats(&b);
|
||||
CHECK(b.tri_precise == a.tri_precise + 1);
|
||||
CHECK(b.tri_mixed == a.tri_mixed + 2);
|
||||
CHECK(b.tri_native == a.tri_native + 1);
|
||||
}
|
||||
|
||||
/* --- suppression bracket: nothing records inside it --- */
|
||||
pgxp_invalidate_all();
|
||||
pgxp_suppress_begin();
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
/* test_pgxp_session.cpp -- does the psx.enhancement.pgxp mod arm PGXP, and
|
||||
* only for the session whose plan holds it? (docs/ENHANCEMENTS.md G1.11/G1.12)
|
||||
*
|
||||
* The mod's activation runs at session start; main.cpp's renderer setup runs
|
||||
* after it and applies the [video] baseline. Before G1.11 the activation
|
||||
* armed the corrections directly and the baseline switched them straight back
|
||||
* off, so the mod did nothing. Now the activation records a request and the
|
||||
* session arming (src/pgxp_session.cpp, the code main.cpp calls) takes it.
|
||||
*
|
||||
* This test links the real mod runtime and package manager, the real builtin
|
||||
* plugin (src/mod_builtin_pgxp.c, registered by its constructor) and the real
|
||||
* arming, stages the framework's own builtin manifest (and a title override
|
||||
* of it with default_enabled = true, as a title that ships PGXP on by default
|
||||
* does), and drives sessions in main.cpp's order:
|
||||
*
|
||||
* commit (offline) or mod_runtime_clear_for_netplay (netplay)
|
||||
* reset_mod_owned_presentation() -> psx_pgxp_session_reset()
|
||||
* mod_runtime_activate_plugins() -> builtin_pgxp_activate (if planned)
|
||||
* renderer setup -> psx_pgxp_session_arm(...)
|
||||
*
|
||||
* The engine setters the arming calls are stubbed to record what it armed.
|
||||
*/
|
||||
#include "mod_runtime.h"
|
||||
#include "mod_packages.h"
|
||||
#include "mod_plugins.h"
|
||||
#include "pgxp.h"
|
||||
#include "pgxp_session.h"
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "gpu.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/* POSIX setenv/unsetenv are missing on MinGW; an empty _putenv_s value unsets. */
|
||||
static void test_setenv(const char *name, const char *value) {
|
||||
#ifdef _WIN32
|
||||
_putenv_s(name, value);
|
||||
#else
|
||||
setenv(name, value, 1);
|
||||
#endif
|
||||
}
|
||||
static void test_unsetenv(const char *name) {
|
||||
#ifdef _WIN32
|
||||
_putenv_s(name, "");
|
||||
#else
|
||||
unsetenv(name);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifndef PSX_BUILTIN_PGXP_MANIFEST
|
||||
#error "PSX_BUILTIN_PGXP_MANIFEST must name mods/builtin/.../manifest.toml"
|
||||
#endif
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, \
|
||||
#cond); \
|
||||
g_failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* ---- what the arming armed (gte.cpp / gpu.c setters, stubbed) ------------ */
|
||||
|
||||
static int g_geometry = -1;
|
||||
static int g_texture = -1;
|
||||
extern "C" void gte_geometry_correction_set(int enabled) { g_geometry = enabled; }
|
||||
extern "C" void gpu_texture_correction_set(int enabled) { g_texture = enabled; }
|
||||
/* pgxp.cpp's position-cache tier lives in gte.cpp. */
|
||||
extern "C" int gte_geometry_correction_lookup(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;
|
||||
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) {
|
||||
const uint32_t o = a & 0x1fffffu;
|
||||
return (uint16_t)(ram[o] | ((uint16_t)ram[o + 1] << 8));
|
||||
}
|
||||
extern "C" void psx_write_half(uint32_t a, uint16_t v) {
|
||||
const uint32_t o = a & 0x1fffffu;
|
||||
ram[o] = (uint8_t)v;
|
||||
ram[o + 1] = (uint8_t)(v >> 8);
|
||||
}
|
||||
extern "C" uint32_t psx_read_word(uint32_t a) {
|
||||
const uint32_t o = a & 0x1fffffu;
|
||||
return (uint32_t)ram[o] | ((uint32_t)ram[o + 1] << 8) |
|
||||
((uint32_t)ram[o + 2] << 16) | ((uint32_t)ram[o + 3] << 24);
|
||||
}
|
||||
extern "C" void psx_write_word(uint32_t a, uint32_t v) {
|
||||
const uint32_t o = a & 0x1fffffu;
|
||||
for (int i = 0; i < 4; i++) ram[o + i] = (uint8_t)(v >> (8 * i));
|
||||
}
|
||||
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" 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) {}
|
||||
/* Render-pass state mod_runtime.cpp reads (no pass runs here). */
|
||||
extern "C" { int g_psx_render_pass_active; uint64_t g_guest_store_count; }
|
||||
|
||||
/* ---- staging ------------------------------------------------------------- */
|
||||
|
||||
static void write_text(const fs::path& path, const std::string& text) {
|
||||
fs::create_directories(path.parent_path());
|
||||
std::ofstream out(path, std::ios::binary);
|
||||
out << text;
|
||||
}
|
||||
|
||||
static std::string read_text(const fs::path& path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
static std::string replace_once(std::string s, const std::string& from,
|
||||
const std::string& to) {
|
||||
const size_t at = s.find(from);
|
||||
CHECK(at != std::string::npos);
|
||||
if (at != std::string::npos) s.replace(at, from.size(), to);
|
||||
return s;
|
||||
}
|
||||
|
||||
/* A mods root holding the builtin manifest as `manifest` (verbatim, or a
|
||||
* title's default-on override of it) and an optional state.toml. */
|
||||
static fs::path stage(const char* name, const std::string& manifest,
|
||||
const std::string& state) {
|
||||
const fs::path root = fs::temp_directory_path() /
|
||||
(std::string("psxrecomp-pgxp-session-") + name);
|
||||
std::error_code ec;
|
||||
fs::remove_all(root, ec);
|
||||
write_text(root / "packages/psx.enhancement.pgxp/1.0.0/manifest.toml",
|
||||
manifest);
|
||||
if (!state.empty()) write_text(root / "state.toml", state);
|
||||
std::vector<char> zeros(8 * 2352, 0);
|
||||
write_text(root / "disc.bin", std::string(zeros.begin(), zeros.end()));
|
||||
write_text(root / "disc.cue",
|
||||
"FILE \"disc.bin\" BINARY\n"
|
||||
" TRACK 01 MODE2/2352\n"
|
||||
" INDEX 01 00:00:00\n");
|
||||
return root;
|
||||
}
|
||||
|
||||
static bool commit(const fs::path& root) {
|
||||
std::string error;
|
||||
if (!PSXRecompV4::mod_runtime_initialize(root, "SLUS-PGXPT", 0x80010000u,
|
||||
{}, &error)) {
|
||||
std::fprintf(stderr, "initialize: %s\n", error.c_str());
|
||||
return false;
|
||||
}
|
||||
if (!PSXRecompV4::mod_runtime_commit(root / "disc.cue", &error)) {
|
||||
std::fprintf(stderr, "commit: %s\n", error.c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ---- one session start, as main.cpp sequences it ------------------------- */
|
||||
|
||||
static PSXPgxpSessionConfig g_cfg;
|
||||
|
||||
static PSXPgxpSessionArm session(bool do_reset = true) {
|
||||
if (do_reset) psx_pgxp_session_reset(); /* reset_mod_owned_presentation */
|
||||
mod_runtime_activate_plugins();
|
||||
g_geometry = g_texture = -1;
|
||||
return psx_pgxp_session_arm(&g_cfg, nullptr);
|
||||
}
|
||||
|
||||
static bool armed(const PSXPgxpSessionArm& a, int g, int t, int c, int cull) {
|
||||
return a.geometry == g && a.texture == t && a.cpu_mode == c &&
|
||||
a.culling == cull && g_geometry == g && g_texture == t &&
|
||||
pgxp_cpu_mode() == c && pgxp_culling() == cull;
|
||||
}
|
||||
|
||||
static void netplay_clear(void) {
|
||||
std::string error;
|
||||
CHECK(PSXRecompV4::mod_runtime_clear_for_netplay(&error));
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
for (const char* v : {"PSX_GEOMETRY_CORRECTION", "PSX_PERSPECTIVE_TEXTURING",
|
||||
"PSX_PGXP_CPU_MODE", "PSX_PGXP_CULLING"})
|
||||
test_unsetenv(v);
|
||||
g_cfg = PSXPgxpSessionConfig{};
|
||||
g_cfg.tolerance = -1.0f;
|
||||
g_cfg.position_fallback = 0;
|
||||
g_cfg.preserve_projection = 1;
|
||||
|
||||
const std::string builtin = read_text(PSX_BUILTIN_PGXP_MANIFEST);
|
||||
CHECK(builtin.find("id = \"psx.enhancement.pgxp\"") != std::string::npos);
|
||||
CHECK(builtin.find("default_enabled = false") != std::string::npos);
|
||||
CHECK(builtin.find("id = \"culling\"") != std::string::npos);
|
||||
/* A title that ships PGXP on by default overrides the builtin at the
|
||||
* same id and version: the feature on, and here precise culling too. */
|
||||
std::string title = replace_once(builtin, "default_enabled = false",
|
||||
"default_enabled = true");
|
||||
{
|
||||
const size_t opt = title.find("id = \"culling\"");
|
||||
const size_t def = title.find("default = \"false\"", opt);
|
||||
CHECK(opt != std::string::npos && def != std::string::npos);
|
||||
if (def != std::string::npos)
|
||||
title.replace(def, std::strlen("default = \"false\""),
|
||||
"default = \"true\"");
|
||||
}
|
||||
|
||||
/* The builtin's default: nothing planned, the faithful floor, and the
|
||||
* tuning keys are applied regardless (they are inert while off). */
|
||||
CHECK(commit(stage("builtin", builtin, "")));
|
||||
CHECK(armed(session(), 0, 0, 0, 0));
|
||||
CHECK(pgxp_tolerance() < 0.0f && pgxp_position_fallback() == 0 &&
|
||||
pgxp_preserve_projection() == 1);
|
||||
|
||||
/* A title's default-on override: the mod arms geometry, texture and its
|
||||
* culling option at the first boot (the G1.11 bug left it all off). */
|
||||
const fs::path title_root = stage("title", title, "");
|
||||
CHECK(commit(title_root));
|
||||
CHECK(armed(session(), 1, 1, 0, 1));
|
||||
|
||||
/* Netplay rematch: the plan is cleared, nothing activates, PGXP is off
|
||||
* -- default-on packages included. */
|
||||
netplay_clear();
|
||||
CHECK(armed(session(), 0, 0, 0, 0));
|
||||
/* ...even if the session-start reset were skipped: the offline
|
||||
* session's arming took its request, so nothing is left to inherit. */
|
||||
CHECK(commit(title_root));
|
||||
CHECK(armed(session(), 1, 1, 0, 1));
|
||||
netplay_clear();
|
||||
CHECK(armed(session(false), 0, 0, 0, 0));
|
||||
|
||||
/* Offline rematch after netplay: on again. */
|
||||
CHECK(commit(title_root));
|
||||
CHECK(armed(session(), 1, 1, 0, 1));
|
||||
|
||||
/* The player's off switch on the Mods page. */
|
||||
const std::string disabled =
|
||||
"format_version = 2\n"
|
||||
"[[package]]\n"
|
||||
"id = \"psx.enhancement.pgxp\"\n"
|
||||
"version = \"1.0.0\"\n"
|
||||
"[[feature]]\n"
|
||||
"package_id = \"psx.enhancement.pgxp\"\n"
|
||||
"id = \"pgxp\"\n"
|
||||
"enabled = false\n";
|
||||
CHECK(commit(stage("title-off", title, disabled)));
|
||||
CHECK(armed(session(), 0, 0, 0, 0));
|
||||
|
||||
/* Options: culling off, CPU mode on. */
|
||||
const std::string options =
|
||||
"format_version = 2\n"
|
||||
"[[package]]\n"
|
||||
"id = \"psx.enhancement.pgxp\"\n"
|
||||
"version = \"1.0.0\"\n"
|
||||
"[[feature]]\n"
|
||||
"package_id = \"psx.enhancement.pgxp\"\n"
|
||||
"id = \"pgxp\"\n"
|
||||
"enabled = true\n"
|
||||
"[feature.values]\n"
|
||||
"culling = false\n"
|
||||
"cpu_mode = true\n";
|
||||
CHECK(commit(stage("title-options", title, options)));
|
||||
CHECK(armed(session(), 1, 1, 1, 0));
|
||||
|
||||
/* [video] baseline: a player's keys work without the mod and add to it;
|
||||
* culling has no [video] key. */
|
||||
CHECK(commit(stage("title-off2", title, disabled)));
|
||||
g_cfg.video_texture = 1;
|
||||
CHECK(armed(session(), 0, 1, 0, 0));
|
||||
g_cfg.video_geometry = 1;
|
||||
CHECK(armed(session(), 1, 1, 0, 0));
|
||||
/* ...unless the title made the mod the one switch (pgxp_mod_only): then
|
||||
* a stale settings.toml value cannot keep half of PGXP on. */
|
||||
g_cfg.mod_only = 1;
|
||||
CHECK(armed(session(), 0, 0, 0, 0));
|
||||
CHECK(commit(title_root));
|
||||
CHECK(armed(session(), 1, 1, 0, 1));
|
||||
netplay_clear();
|
||||
CHECK(armed(session(), 0, 0, 0, 0));
|
||||
g_cfg.mod_only = 0;
|
||||
g_cfg.video_texture = 0;
|
||||
g_cfg.video_geometry = 0;
|
||||
|
||||
/* Validation env overrides win over both. Culling needs geometry. */
|
||||
CHECK(commit(title_root));
|
||||
test_setenv("PSX_GEOMETRY_CORRECTION", "0");
|
||||
test_setenv("PSX_PERSPECTIVE_TEXTURING", "0");
|
||||
CHECK(armed(session(), 0, 0, 0, 0));
|
||||
test_setenv("PSX_PERSPECTIVE_TEXTURING", "1");
|
||||
CHECK(armed(session(), 0, 1, 0, 0));
|
||||
test_unsetenv("PSX_GEOMETRY_CORRECTION");
|
||||
test_unsetenv("PSX_PERSPECTIVE_TEXTURING");
|
||||
test_setenv("PSX_PGXP_CULLING", "0");
|
||||
CHECK(armed(session(), 1, 1, 0, 0));
|
||||
test_unsetenv("PSX_PGXP_CULLING");
|
||||
CHECK(commit(stage("builtin2", builtin, "")));
|
||||
test_setenv("PSX_GEOMETRY_CORRECTION", "1");
|
||||
test_setenv("PSX_PGXP_CULLING", "1");
|
||||
CHECK(armed(session(), 1, 0, 0, 1));
|
||||
test_setenv("PSX_PERSPECTIVE_TEXTURING", ""); /* set but empty = off (Windows: unset, same result) */
|
||||
CHECK(armed(session(), 1, 0, 0, 1));
|
||||
test_unsetenv("PSX_GEOMETRY_CORRECTION");
|
||||
test_unsetenv("PSX_PGXP_CULLING");
|
||||
test_unsetenv("PSX_PERSPECTIVE_TEXTURING");
|
||||
|
||||
CHECK(psx_pgxp_session_env_flag(nullptr) == -1);
|
||||
CHECK(psx_pgxp_session_env_flag("0") == 0);
|
||||
CHECK(psx_pgxp_session_env_flag("") == 0);
|
||||
CHECK(psx_pgxp_session_env_flag("1") == 1);
|
||||
CHECK(psx_pgxp_session_env_flag("yes") == 1);
|
||||
|
||||
if (g_failures) {
|
||||
std::fprintf(stderr, "test_pgxp_session: %d FAILURES\n", g_failures);
|
||||
return 1;
|
||||
}
|
||||
std::printf("test_pgxp_session: all checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -400,7 +400,10 @@ class ToolchainStagingTest(unittest.TestCase):
|
||||
'python_url': self.py_zip,
|
||||
'python_sha256': 'unused',
|
||||
'tcc_url': self.tcc_zip,
|
||||
'tcc_sha256': 'unused',
|
||||
# The fetch is stubbed; the version and hash are the real pin's
|
||||
# because the staged TinyCC notice must name them.
|
||||
'tcc_version': self.orig_pins['tcc_version'],
|
||||
'tcc_sha256': self.orig_pins['tcc_sha256'],
|
||||
}
|
||||
rs.get_pinned_archive = lambda url, _sha, _dest, log=print: url
|
||||
|
||||
@@ -418,6 +421,44 @@ class ToolchainStagingTest(unittest.TestCase):
|
||||
'overlay_dispatch_preamble.c.inc')))
|
||||
self.assertTrue(os.path.isfile(os.path.join(
|
||||
self.stage, 'overlay_toolchain', 'bios', 'SCPH1001.toml')))
|
||||
self.assertTrue(os.path.isfile(os.path.join(
|
||||
self.stage, 'overlay_toolchain', 'tcc', 'COPYING')))
|
||||
|
||||
|
||||
class TinyccNoticeTest(unittest.TestCase):
|
||||
"""The bundled TinyCC (LGPL-2.1) ships with its notice (issue #474)."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.mkdtemp(prefix='tcc_notice_')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def test_every_pinned_tcc_has_a_notice_naming_it(self):
|
||||
pinned = [p for p in rs.TOOLCHAIN_PINS.values() if p['tcc_url']]
|
||||
self.assertTrue(pinned, 'no platform pins a tcc')
|
||||
with open(rs.TCC_NOTICE, encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
for pins in pinned:
|
||||
self.assertIn(pins['tcc_version'], text)
|
||||
self.assertIn(pins['tcc_sha256'], text)
|
||||
self.assertIn('GNU LESSER GENERAL PUBLIC LICENSE', text)
|
||||
self.assertIn('Version 2.1', text)
|
||||
|
||||
def test_notice_is_staged_beside_the_binary(self):
|
||||
pins = next(p for p in rs.TOOLCHAIN_PINS.values() if p['tcc_url'])
|
||||
rs.stage_tcc_notice(self.tmp, pins)
|
||||
with open(os.path.join(self.tmp, 'COPYING'), encoding='utf-8') as f:
|
||||
staged = f.read()
|
||||
with open(rs.TCC_NOTICE, encoding='utf-8') as f:
|
||||
self.assertEqual(staged, f.read())
|
||||
|
||||
def test_a_notice_for_another_version_fails_loudly(self):
|
||||
pins = dict(next(p for p in rs.TOOLCHAIN_PINS.values() if p['tcc_url']))
|
||||
pins['tcc_version'] = '9.9.99'
|
||||
with self.assertRaises(rs.StageError):
|
||||
rs.stage_tcc_notice(self.tmp, pins)
|
||||
self.assertFalse(os.path.exists(os.path.join(self.tmp, 'COPYING')))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -158,7 +158,9 @@ for fn in ("void debug_server_trace_write_check(",
|
||||
"rolled-back pass writes must stay out of the frame fingerprints: " + fn)
|
||||
|
||||
rp = (SRC / "render_pass.c").read_text(encoding="utf-8")
|
||||
for gate in ("psx_netplay_active()", "psx_rewind_is_open()",
|
||||
# Live netplay frames run passes (presentation only, both peers simulate the
|
||||
# same ticks); rollback resimulation replays ticks already seen and refuses.
|
||||
for gate in ("psx_netplay_is_resimulating()", "psx_rewind_is_open()",
|
||||
"psx_selfcheck_resim_active()", "psx_get_in_exception()",
|
||||
"dma_gpu_linked_list_active()",
|
||||
"psx_presentation_fast_forward()"):
|
||||
|
||||
@@ -377,6 +377,22 @@ if [[ -d "${FRAMEWORK}/runtime/licenses" ]]; then
|
||||
fi
|
||||
[[ -f "${FRAMEWORK}/THIRD_PARTY_ATTRIBUTION.md" ]] && cp -a "${FRAMEWORK}/THIRD_PARTY_ATTRIBUTION.md" "${STAGE}/licenses/"
|
||||
[[ -f "${FRAMEWORK}/LICENSE" ]] && cp -a "${FRAMEWORK}/LICENSE" "${STAGE}/licenses/psxrecomp-LICENSE"
|
||||
# recomp-ui: the executable links it (MIT) and assets/ carries its fonts and
|
||||
# images (OFL-1.1, CC BY-SA 4.0), so its license and asset notices ship too.
|
||||
# The build's RECOMP_UI_ROOT says which tree it linked; <root>/recomp-ui is the
|
||||
# New Project Layout default.
|
||||
if ! grep -qs '^PSX_RECOMP_UI:BOOL=OFF' "${BUILD_DIR}/CMakeCache.txt"; then
|
||||
UI_ROOT="$(sed -n 's/^RECOMP_UI_ROOT:[A-Z]*=//p' "${BUILD_DIR}/CMakeCache.txt" 2>/dev/null | head -n1 || true)"
|
||||
[[ -n "${UI_ROOT}" && -f "${UI_ROOT}/recomp_ui.cmake" ]] || UI_ROOT="${ROOT}/recomp-ui"
|
||||
[[ -f "${UI_ROOT}/recomp_ui.cmake" ]] || { echo "error: recomp-ui assets are staged but no recomp-ui tree found (RECOMP_UI_ROOT / ${ROOT}/recomp-ui)" >&2; exit 1; }
|
||||
[[ -f "${UI_ROOT}/LICENSE" ]] || { echo "error: ${UI_ROOT}/LICENSE missing" >&2; exit 1; }
|
||||
cp -a "${UI_ROOT}/LICENSE" "${STAGE}/licenses/recomp-ui-LICENSE"
|
||||
for sub in fonts img; do
|
||||
notice="${UI_ROOT}/assets/common/${sub}/NOTICE.md"
|
||||
[[ -f "${notice}" ]] || { echo "error: ${notice} missing" >&2; exit 1; }
|
||||
cp -a "${notice}" "${STAGE}/assets/${sub}/NOTICE.md"
|
||||
done
|
||||
fi
|
||||
if [[ -z "$(ls -A "${STAGE}/licenses" 2>/dev/null)" ]]; then
|
||||
echo "error: no third-party notices staged (${FRAMEWORK}/runtime/licenses empty?)" >&2
|
||||
exit 1
|
||||
|
||||
@@ -623,6 +623,29 @@ TOOLCHAIN_BIOS_EMITTER = {'win': 'psxrecomp-bios.exe', 'linux': 'psxrecomp-bios'
|
||||
'macos-x64': 'psxrecomp-bios', 'macos-arm64': 'psxrecomp-bios'}
|
||||
|
||||
|
||||
# TinyCC is LGPL-2.1 and its win64 binary zip carries no license file, so the
|
||||
# notice (license text, copyright, exact source URL) travels inside tcc/ too;
|
||||
# runtime/licenses/ ships the same file as licenses/TinyCC-LICENSE.txt.
|
||||
TCC_NOTICE = os.path.join(os.path.dirname(_TOOLS_DIR), 'runtime', 'licenses',
|
||||
'TinyCC-LICENSE.txt')
|
||||
|
||||
|
||||
def stage_tcc_notice(tcc_dir, pins):
|
||||
"""Write TinyCC's license notice as <tcc_dir>/COPYING; fail if the notice
|
||||
is missing or does not name the pinned version and archive."""
|
||||
if not os.path.isfile(TCC_NOTICE):
|
||||
_die('TinyCC notice missing at %s; the bundled tcc cannot ship '
|
||||
'without its LGPL-2.1 notice' % TCC_NOTICE)
|
||||
with open(TCC_NOTICE, encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
for needle in (pins['tcc_version'], pins['tcc_sha256'],
|
||||
'GNU LESSER GENERAL PUBLIC LICENSE'):
|
||||
if needle not in text:
|
||||
_die('%s does not name %s; update it with the tcc pin'
|
||||
% (TCC_NOTICE, needle))
|
||||
shutil.copy2(TCC_NOTICE, os.path.join(tcc_dir, 'COPYING'))
|
||||
|
||||
|
||||
def stage_toolchain(stage, recomp_dir, recomp_tools, recomp_include, dl_cache,
|
||||
platform_tag=None, mingw_bin=None, log=print):
|
||||
"""Stage the self-contained overlay toolchain (interpreter + recompiler +
|
||||
@@ -667,6 +690,7 @@ def stage_toolchain(stage, recomp_dir, recomp_tools, recomp_include, dl_cache,
|
||||
inner = os.path.join(tcc_tmp, 'tcc')
|
||||
shutil.copytree(inner if os.path.isdir(inner) else tcc_tmp,
|
||||
os.path.join(toolchain, 'tcc'), dirs_exist_ok=True)
|
||||
stage_tcc_notice(os.path.join(toolchain, 'tcc'), pins)
|
||||
|
||||
recompiler = TOOLCHAIN_RECOMPILER[platform_tag]
|
||||
src_recompiler = os.path.join(recomp_dir, recompiler)
|
||||
|
||||
@@ -309,6 +309,24 @@ class PackagerScriptContract(unittest.TestCase):
|
||||
self.assertNotIn("--project-file", text)
|
||||
self.assertNotIn("--project-dir", text)
|
||||
|
||||
def test_bundled_packager_ships_recomp_ui_notices(self):
|
||||
# The executable links recomp-ui and assets/ carries its fonts and
|
||||
# images; their license and notices must ship with them.
|
||||
text = (FW / "tools" / "package_game_release.sh").read_text(encoding="utf-8")
|
||||
self.assertIn('"${STAGE}/licenses/recomp-ui-LICENSE"', text)
|
||||
self.assertIn('assets/common/${sub}/NOTICE.md', text)
|
||||
self.assertIn('"${STAGE}/assets/${sub}/NOTICE.md"', text)
|
||||
self.assertIn("RECOMP_UI_ROOT", text, "must follow the tree the build linked")
|
||||
self.assertIn('[[ -f "${UI_ROOT}/recomp_ui.cmake" ]] ||', text,
|
||||
"missing recomp-ui tree must fail packaging")
|
||||
self.assertIn('[[ -f "${notice}" ]] ||', text,
|
||||
"missing asset notices must fail packaging")
|
||||
template = TEMPLATE.read_text(encoding="utf-8")
|
||||
for path in ("licenses/recomp-ui-LICENSE", "assets/fonts/NOTICE.md",
|
||||
"assets/img/NOTICE.md"):
|
||||
self.assertIn(f"'{path}'", template,
|
||||
f"the CI zip verifier must require {path}")
|
||||
|
||||
def test_scripts_are_executable_and_parse(self):
|
||||
for rel in ("tools/package_game_release.sh", "tools/ci/check_generated.sh"):
|
||||
p = FW / rel
|
||||
|
||||
@@ -51,6 +51,9 @@ int main(void) {
|
||||
assert(psx_ws_cull_bgez(0u) == 1 && psx_ws_cull_bgez((uint32_t)-1) == 0);
|
||||
assert(psx_ws_clip_edge_x(0u, 0x140u) == 0u);
|
||||
assert(psx_ws_clip_edge_x(0x140u, 0x140u) == 0x140u);
|
||||
/* screen_x_sites on lui / bltz: a screen X in the high half. */
|
||||
assert(psx_ws_cull_lui_hi(0x0200u) == (0x0200u << 16));
|
||||
assert(psx_ws_cull_bltz_hi((uint32_t)-1 << 16) == 1 && psx_ws_cull_bltz_hi(0u) == 0);
|
||||
callbacks.ws_x_margin = margin120;
|
||||
overlay_init(&callbacks);
|
||||
assert(psx_ws_cull_bgez((uint32_t)-120) == 1);
|
||||
@@ -59,6 +62,9 @@ int main(void) {
|
||||
assert(psx_ws_clip_edge_x(0x140u, 0x140u) == 0x140u + 120u);
|
||||
assert(psx_ws_clip_edge_x(98u, 0x140u) == 98u); /* interior viewport edge */
|
||||
assert(psx_ws_clip_edge_x(222u, 0x140u) == 222u);
|
||||
assert(psx_ws_cull_lui_hi(0x0200u) == ((0x0200u + 120u) << 16));
|
||||
assert(psx_ws_cull_bltz_hi((uint32_t)-120 << 16) == 0);
|
||||
assert(psx_ws_cull_bltz_hi((uint32_t)-121 << 16) == 1);
|
||||
return 0;
|
||||
}
|
||||
'''
|
||||
|
||||
Reference in New Issue
Block a user