Commit Graph

457 Commits

Author SHA1 Message Date
tetrisgm dc00e5a1e8 tex_pack: position-exact dedup; savestate-section groundwork (dormant)
DEDUP: a re-upload of identical content at a DIFFERENT rect was deduped by
hash+size, keeping the stale rect -- origins computed from it were off by the
position drift. Dedup now requires the same position; the origin/src handed to
the renderer come from the live containing upload. NOTE: this did NOT cure the
strip vertical offset (top row cut, bottom duplicated) -- that symptom persists
on screen and its root cause is still unidentified. The change stands on its
own correctness, not as that fix.

SAVESTATE SECTION (BS_SEC_TEXPACK): written and applied correctly in isolation
(telemetry showed 1360 entries rebuilt from the state's own VRAM), but with the
section present the overall state load FAILS in a LATER section, cause not yet
isolated -- and old builds hard-reject unknown tags (`default: return 0`), so
the section also breaks backward compatibility. The boot_state wiring is
REVERTED; tex_pack_state_bytes/write/apply remain, dormant, for whoever picks
this up with the loader diagnosis. Until then: after loading a savestate,
textures the game does not re-upload stay low-res -- known limitation again.

The first-save defer fix verified working in passing: "deferring slot 4 -- no
safe resume PC (hint=0x00000000)" followed by a save at a real PC.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 23:58:03 -07:00
tetrisgm 27c18f8f8f cpu: scale only CPU charges, not device-service ones
Charges made while servicing a device are the device's own time, not CPU
instruction retirement. Scaling those too compressed DMA and peripheral work by
the same factor, which is not an overclock but a speed hack on the whole
machine. DuckStation overclocks the CPU alone and device delays keep their
real-world duration; same intent here.

Correct, but NOT the boot failure. Measured after the change, with 8 MB on:
100% and 300% boot; 400/600/800/900/1000% all reach frames but never enable the
display. So the threshold sits between 300% and 400%, far below the >900% that
hueponik's pal100full8 requires.

What the failure is not:
  - not performance. Frames advance steadily at ~39/s throughout (455 -> 2574
    over 54s) while the display stays off, on a host that runs DuckStation at
    900% fine;
  - not device-service scaling, per this commit;
  - not a hang. The guest keeps running, it just never turns the display on.

Prime remaining suspect is the deferred-charge batching: g_psx_cyc_batch
accumulates RAW cycles against g_psx_cyc_batch_limit, but the charge is scaled
only at publish, so the batch limit and the scaled deadline comparison are in
different units. Devices would then be serviced progressively later as the
factor grows, which fits a threshold rather than a clean break. Untested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:33:39 -07:00
tetrisgm 30652ac334 cpu: overclock by scaling the per-instruction cycle charge
The guest runs as native code, so "faster CPU" cannot mean "execute quicker" --
it means CHARGE LESS for the same work. Every device schedules off
psx_cycle_count and the CRTC fires VBlank on a fixed cycle period, so scaling
the per-instruction charge down by N lets the CPU complete N times the work
inside one frame while timers, SPU, CDROM and the refresh rate keep their
real-world rates. Scaling the VBlank period instead would slow everything
EXCEPT the CPU, which is backwards.

Q16 fixed point with a carried remainder, not a divide: this is the hottest path
in the runtime and integer division of a 1-2 cycle instruction cost would floor
to zero and lose all time. The accumulator keeps the total charge exact instead
of drifting by the truncation on every call. Stock (65536) short-circuits, so a
non-overclocked build pays one predictable compare.

[video] cpu_overclock, percent, 100 = stock. Needed because hueponik's
pal100full8 requires >900%.

STATUS, measured, not assumed: 100% and 300% boot and run. 1000% does NOT reach
a display -- and it is neither hung nor host-bound, frames advance steadily at
~39/s (455 -> 2574 over 54s) while the guest never enables the display. So the
guest's own boot path stalls at that ratio; the mechanism moves time correctly
but something in the boot sequence does not survive the CPU running 10x ahead of
its devices. Not yet isolated, and it is the blocker for pal100full8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:28:23 -07:00
tetrisgm 4210c764b1 present: bezel artwork behind the frame ([video] bezel)
A still image drawn behind the game, filling whatever the letterbox or
pillarbox leaves over -- the trick vertical shmups use on horizontal displays,
where the dead space carries artwork so it reads as designed rather than as a
limitation. MAME calls the same idea bezel artwork.

Drawn across the whole drawable before the game quad, which is simpler and
safer than filling two margin strips: no margin arithmetic, it covers pillarbox
and letterbox alike, and the frame paints over the centre.

The reason this is the right shape and present_edge_fill was not: edge fill
stretches one edge COLUMN of the live frame across the margin. That suits a flat
menu backdrop and turns track geometry into horizontal smears, so it needs to
know whether the current frame is a menu -- a question it answers with
force_4_3, which is only ever set by the widescreen path. A still image has no
such question. It looks the same in a race and in a menu, which is the point.

Loaded after the GL context exists (the first attempt sat next to tex_pack_init,
which runs before the window, so g_gl_active was false and it silently never
loaded). Decoded with stb_image, already linked for the HD texture pack. A
relative path resolves against the disc directory, matching the pack convention,
so artwork can sit beside the disc.

Verified on a 7680-wide maximised window with 4:3 content: margins that were
pure black now carry the image on both sides, with the frame untouched in the
middle.

Known limits: the image is stretched to the drawable, so an aspect that does not
match the window will distort (cover-crop is a small follow-up), and this has
only been exercised in 4:3 pillarbox, not with the widescreen mod active.

For WipEout 3 the team wallpapers by MotorcycleEmptiness
(awesome-wipeout.github.io) suit this exactly; the shipped default is a
placeholder gradient used to verify the path, not final art.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 20:45:54 -07:00
tetrisgm d0ead48928 Merge remote-tracking branch 'origin/feat/rbengine' into feat/ws-cull-widen
# Conflicts:
#	runtime/src/debug_server.c
#	runtime/src/gpu.c
#	runtime/src/gpu_gl_renderer.c
#	runtime/src/tex_pack.cpp
2026-08-17 17:42:40 -07:00
tetrisgm 774b326fca pgxp: declare the depth API and wire [video] pgxp_depth through config
The header contract and config plumbing for the depth work; the implementation
landed in the preceding commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:38:25 -07:00
Alex Vanderveen 37ef57e667 Merge origin/feat/rbengine into pr/ws-ui-groups.
Keep ws_ui_groups diagnostic root and feat/rbengine's ram-mapped src_addr.
Keep feat/rbengine's recomp-net pointer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 20:27:34 -04:00
tetrisgm ab04da86e3 pgxp: depth from the absolute GTE screen Z, not the normalised perspective weight
PGXP depth shattered the scene, and the reason was a category error rather than
a tuning problem. It derived depth from a_q, which is q/qmax -- a perspective
weight normalised WITHIN one triangle. Every triangle therefore got its own
private 0..1 range, so the value said nothing about where one triangle sat
relative to another, which is the only thing a depth buffer is for.

The real depth was already in hand and being thrown away. prepare_texture_triangle
reads a 16-bit GTE screen Z per vertex via gte_precision_load_word, then
normalises it away on the very next line because UV correction only needs
relative weights. That Z is a single global scale, so it is exactly what a depth
buffer wants.

Adds gr_set_depth_triangle alongside gr_set_perspective_triangle rather than
widening it: the two carry different quantities for different purposes, and a
null-safe hook leaves the software backend untouched. The GL backend takes it as
its own vertex attribute (a_z, location 10) and maps it straight to NDC.

Verified in 3D attract-mode gameplay: with pgxp_depth on the scene renders
intact -- track, perspective-correct texturing and effects all correct -- where
before it shattered. On the 2D title screen the on/off frames are bit-identical,
because no primitive there carries GTE provenance, so a_z is 0 and depth stays
off. Still default-off in config; this makes the feature viable, it does not
turn it on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:49:15 -07:00
tetrisgm e1061c6458 tex_pack: read the fragment's real palette index instead of guessing the ink
Hardens the palette-agnostic fallback against defects an adversarial review of
the first cut turned up. The feature is dormant on this title's title screen
(every draw hits its exact key), so none of this changed a pixel here -- it is
the difference between a mechanism that happens not to fire and one that is
correct when it does.

The recolour shader guessed: it scanned the CLUT for "the first non-zero entry
after index 0" and called that the ink. That assumed the hole is always index
0, applied one colour to the whole primitive regardless of which entry a given
fragment actually addresses, and at 8bpp could run 255 dependent texelFetches
per fragment. It now reads the index the hardware would have read -- one fetch,
the same arithmetic as TEX_FS's fetch_texel, but with the index and the CLUT
lookup split, because HD ink overhanging a hole texel must resolve a different
entry than the one under it. Falls back to the image's own ink index for those
overhang pixels (which is every glyph edge, since the pack shape is finer than
the texel grid), and discards only when the ink itself has been blanked -- which
is what vanilla does there too.

Colour is now rescaled by the pack pixel's intensity against the image's full-ink
maximum. Under the mono gate that is exactly 1.0 in the interior, so it is a
no-op there; at an edge it carries LINEAR's ramp onto the live colour, so the
fallback antialiases like the exact-key path instead of hardening into a dilated
silhouette.

Four more, each a real defect:
- The donor was whichever filename directory_iterator yielded first, and that
  order is unspecified -- which variant donated a texture's shape could differ
  between runs and machines. Now the lowest key in the sorted set.
- No depth gate: a 15bpp draw has pal_n == 0 so its exact key never matches, and
  it would have been served a 4bpp-dumped image whose src_w is 4x wrong.
- The non-mask path did `return 0`, abandoning the whole upload scan, while the
  neighbouring key and exclude tests both `continue`. A newer non-mask upload
  could therefore block an older one that matched on its exact key.
- The recolour flag was not cleared on set_replacement's early returns, so a
  cleared slot left it armed for whichever primitive got a replacement next.

Also gates on the image having been analysed at this draw's texel depth and
grid, and unbinds the VRAM mirror from unit 1 after the draw -- pack_flush
renders into that same texture, and leaving it bound for reading is undefined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:33:41 -07:00
tetrisgm e1a55da286 tex_pack: palette-agnostic substitution with runtime CLUT recolour
A pack entry is keyed (texture, palette), so a draw whose CLUT ships no file
falls back to the original low-res texture even when the pack plainly has the
right IMAGE -- the texture hash identifies it, only the colour is unknown.

On an exact-key miss we now borrow any palette variant of the same texture for
its shape and take the colour from the live CLUT in the shader. REPL_FS gains
the VRAM usampler (on unit 1; unit 0 already holds the replacement image, and
sharing it would have the sampler read the pack PNG as if it were VRAM) and
reads the first non-zero palette entry as the ink. STP comes from that live
entry too, since a different CLUT may set the bit differently and reading it
off the stale image would blend the glyph wrongly. Alpha still decides the
cutout: that is the SHAPE, and the pack is the authority on it.

The gate is a property of the DATA, not a list of texture hashes we happen to
know are fonts: recolour only where the pack image has a single ink colour, so
there is no colour structure to flatten. image_is_mask computes it once at
decode. Multi-colour images decline and fall through unchanged, counted in
repl_multicol.

Also adds repl_calls / repl_nocontain / repl_notex, because "hit + miss" could
not distinguish "the pack has no image for this texture" from "no upload backs
this primitive", and those want opposite fixes. That distinction is what showed
the fallback does not fire on the WipEout 3 title screen at all -- every draw
there already hits its exact key -- so the substitution path is byte-identical
where it does not apply, and the blocky title-screen text has some other cause.
In gameplay the misses are overwhelmingly repl_notex: textures with no pack
entry of any palette, which recolour cannot help.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 16:20:59 -07:00
Alex Vanderveen b423486563 toolchain, texture pack update 2026-08-17 18:55:34 -04:00
tetrisgm 0bdf338571 tex_pack: substitute HD replacement textures at draw time
Completes the feature tex_pack.h has always described. Until now it identified
replacement textures and never swapped them: it opened files only for writing,
and its known-set was built by parsing filenames, so a pack could report
hundreds of "matched" entries while changing nothing on screen.

Decode a matched entry lazily via stb_image (declarations only; the
implementation is compiled in psx_window_icon.cpp, whose STBI_NO_STDIO must be
matched here, hence reading the file ourselves), cache it per key, and hand the
draw path the image plus the mapping from texture-page UVs into it:

    s = (u - origin_u) / src_w     t = (v - origin_v) / src_h

so the replacement's own resolution never appears at the call site — a 16x
atlas and a 1:1 image are addressed identically.

The GL side draws a replaced prim through its own program (REPL_FS, sharing
TEX_VS so positions, the perspective divide and the native-wide transforms stay
bit-identical) and its own draw, leaving the batch path untouched. Two things
that cost real debugging time and must not be re-derived:

  - frag.a is the PS1 MASK BIT, not opacity, and blend_factor's alpha is the
    dual-source destination factor. Writing either as coverage sets the mask
    bit on every replaced pixel and makes later masked draws vanish.
  - The two-pass mask/stencil protocol is NOT duplicated. tex_batch_draw_passes
    is parameterised on the program's uniform locations and the mask flag, and
    both paths call it. A hand-written second copy dropped the s_mask_check
    fixup and the semi == 4 dual-source case.

RECTS AND SPRITES ARE TRACKED BUT NOT SUBSTITUTED. The backend consumes a
replacement in its textured-triangle path only; rect prims take their own route
and never sample it, so arming one produced primitives that drew nothing. On
WipEout 3 that is the whole 2D layer — HUD meters, boxes, pickup indicators,
and every glyph, since the font atlases are addressed by sprites — which is why
enabling a pack erased the interface while the 3D scene rendered correctly.
Identity tracking still runs, so the dumper, the match set and the UV census
are unaffected; only application is withheld until that path is wired.

Also adds the instrumentation that found this after four wrong guesses:
"tex_pack repl=0/1" toggles substitution live (an A/B needing a rebuild does
not get run), and "tex_pack sub=armed" reports WHICH textures were substituted
and where they landed on screen, which is what distinguished "the HUD is being
replaced by the wrong image" from "replacement corrupts state the HUD needs".

Measured in-race at 32:9 with princepeter64's Wip3out pack: 292 entries
indexed, 7 decoded, zero decode failures, 8006 replaced draws, full HUD intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:18:24 -07:00
Alex Vanderveen a643fab71d Make the FMV present filter a real setting
The reconstruction added in the previous commit was env-only (PSX_FMV_FILTER),
which is fine for trying it out and no good for shipping it. Promote it to
[video] fmv_filter in both game.toml and settings.toml, spelled
"nearest" | "bilinear" | "sharp" | "bicubic" (default bicubic), and push the
resolved value to the renderer instead of having the renderer read getenv.

PSX_FMV_FILTER still overrides for a single run without touching the user's
settings, which is what an env knob is actually for.

Also bridges the value to recomp-ui's launcher settings in both directions so
the Display settings row round-trips: recomp-ui stores it 1-based (0 = unset,
so a host predating the field gets the default rather than Nearest), the
config enum is 0-based, and the conversion is confined to two helpers at the
boundary.

Verified end to end: with fmv_filter = "bilinear" in settings.toml the
presented pixels match the bilinear reference exactly (mean |dx| 0.930,
hard-step fraction 0.14%), where the default build produces the bicubic
numbers (1.038 / 0.34%).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:34:23 -04:00
tetrisgm d4e7c25a9b Fill the pillarbox margins of 4:3-pinned presents with the frame's edges
WipEout 3 gates widescreen to GTE-active frames (game.toml gte_game_mode), so
the title, menus and loading screens present at native 4:3 by design. On a 32:9
display that leaves them as a small island with black over most of the screen.

Extend the frame's own leftmost and rightmost columns across those margins.
PRESENT_VS interpolates v_uv.x as mix(u_uv_rect.x, u_uv_rect.z, p.x), so
passing the same u for both ends makes a quad sample exactly one texture
column -- clamp-to-edge, which the present texture is already set to. The 4:3
content is untouched: same position, same scale, no distortion. Only the
margins change, which is why this beats stretching (2.67x-wide text at 32:9)
and cropping (cuts off menu rows you have to read).

Both GL present paths need it. gl_renderer_present is the CPU-readback path;
the FBO path (GL_PRES_VRAM, via present_target_quad) is the one that actually
runs at internal scale > 1, which is every configuration this matters for --
patching only the first looked like the feature silently doing nothing.

Not applied to MDEC video (depth24), whose edge pixels are real picture
content and would smear, nor to the depth24 trailing-margin crop, which leaves
black on the right deliberately. Live A/B via the ws_menu_edge_fill command
rather than a rebuild, since this is a look and not a correctness fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 12:24:19 -07:00
Alex Vanderveen 6a9002032f memory optimizations for 8mb wipeout, performance in general, turbo++ 2026-08-17 14:32:01 -04:00
tetrisgm 572b9210d8 auto_ui_squash: join on submission order; instrument the silent gates
Three changes, one fix and two pieces of visibility.

1. ws_ui_group_assign gains a third join: consecutive in the ordering-table
walk AND touching. WipEout 3's speed/shield readout ends at x=288 and the
meter bar it belongs to starts at x=306 with a different CLUT, so neither
shared columns nor a matching key could join them. The readout stood alone,
its bbox midpoint 256.5 landed in the middle third against a screen centre of
254, and it stayed pinned near centre while its own meters went to the right
edge -- a 2 px gap in 4:3 opening to 159 framebuffer pixels at 32:9.

No anchor threshold can fix that: the genuinely centred "pit ... check"
message sits FARTHER RIGHT (midpoint 300.5) than the mis-anchored readout, so
any threshold reaching the readout drags the message with it. Submission order
is the only signal that separates them, and the array already carries it.

Simulated against seven captures spanning three aspects and two code states,
this forms three unions and changes exactly one anchor: [225,288] centre
becomes [225,486] right. The other two unions ([208,240]+[240,361],
[364,447]+[450,473]) are within an anchor. Confirmed in-race at 32:9: 7 runs,
lap readout left, speed readout and meters together on the right.

2. ws_ui_groups reports why primitives were NOT admitted to the partition.
Each gate in ws_ui_prepass_add is a silent return, so a primitive left at its
raw 4:3 X while its neighbours are squashed -- which is how a HUD mark ends up
stranded mid-screen -- was indistinguishable from one never drawn. In-race at
32:9: opcode 367, not_axis 410, everything else 0.

3. geom_correction reports perspective-UV arming with a real denominator.
perspective_triangles could previously only be compared against gp0_draw,
which counts untextured primitives that are correctly never armed, so it read
as a coverage figure without being one. texcorr.attempts counts exactly the
textured triangles reaching the predicate. In-race at 32:9: 199599 submitted,
96.15% armed, 3.85% rejected for a missing per-vertex Z, 0% for correction
disabled or a missing packet address.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 03:03:33 -07:00
tetrisgm 72196d3dc9 auto_ui_squash: group HUD primitives that are one visual element
The thirds anchor is chosen per union-find run, and runs only joined when the
group key matched within JOIN_GAP. That key folds CLUT, texpage, a 24px Y band
and the poly-vs-rect family together, so a digit and the box it is drawn on
could never land in the same run. They got independent anchors and were
squashed about different points, which pulls one piece of a readout toward a
screen edge and leaves the rest behind -- worse the wider the frame.

On WipEout 3's race HUD at 32:9 the lap readout draws its digits at
y=[219,227] x=[137,202] and the box they label at y=[228,244] x=[138,202].
The digits anchored left, the box anchored centre, and the box landed about
500 screen pixels from the digits it belongs to.

Join on being one visual element as well as on the key: share screen columns
and touch vertically within STACK_GAP. Both halves are needed. Column overlap
alone merged the top-left lap counter with the bottom-left lap timer 200
scanlines below, and since union is transitive that chained all 71 HUD
primitives into one run spanning the full width, which anchors centre and
drags the corners inward. A strict row intersection missed the stacked case
above, whose seam is one pixel.

WsUiGroupItem carries y/height for this; the anchor stays horizontal. After:
9 runs, bottom-left readout whole and anchored left, timer and energy bar
right, speed centre -- matching the 4:3 layout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 02:31:31 -07:00
tetrisgm fd625f0996 Interpreter parity for cull.widen; raise the GL supersampling ceiling
Two follow-ups to [[widescreen.cull.widen]], both found by testing it.

INTERPRETER PARITY. The widen site type was recompiler-only: the AOT image
evaluated the widened compare while the dirty-RAM interpreter, and any overlay
shard built without the config, still evaluated the vanilla one. The same site
would then cull differently depending on which backend happened to run it.
Adds a runtime site registry mirroring psx_ws_cull_keep_site, wires it from the
config at startup, and consults it from the interpreter's SLT and SLTI paths
before the keep lookup so a config migrated from keep to widen cannot be served
the pinned answer by a stale entry.

GL SUPERSAMPLING CEILING 4x -> 16x. SW_MAX_INTERNAL_SCALE was applied to every
backend, which is the wrong constraint for GL. It exists because the software
path allocates a VRAM-sized hi-res MIRROR costing 1 MB * scale^2 (256 MB at
16x); under GL that mirror stays at 1x (glb_set_scale) and the cost is an FBO
instead. Capping GL at 4x limited it to about 2048x960 internal on hardware
that handles far more.

GL_MAX_INTERNAL_SCALE moves to gpu_render.h (main.cpp needs it to pick the
ceiling before a context exists), main.cpp selects the ceiling per backend, and
the config range becomes 1..16. init_gpu_raster now also clamps to the driver's
real GL_MAX_TEXTURE_SIZE / GL_MAX_RENDERBUFFER_SIZE and says so: previously an
over-large scale reached glTexImage2D, failed silently, and surfaced only as
"GL FBO incomplete" with no hint that the scale caused it.

Measured on WipEout 3 SE (PAL, NVIDIA, Release build + debug tools), sustained
present rate in-race, same track:

    scale  internal     fps
      1    508x256      50.0
      6    3048x1536    49.8
      9    4572x2304    50.2
     16    8128x4096    50.0

Flat across the range - the title is guest-bound, so 8K internal is free on this
hardware. 50 fps is full speed for a PAL disc.
2026-08-17 01:41:58 -07:00
tetrisgm a05ad08651 Add [[widescreen.cull.widen]]: move the cull bound instead of pinning it
[[widescreen.cull.keep]] forces a comparison's verdict while wide. That is
correct for a separately proven binary decision, and config_schema.md already
says as much -- "keep has no queue policy and is appropriate only for a
separately proven binary verdict", and prefer an aspect cone "when the original
predicate is a camera-frustum test".

At a clip-code packer it is actively wrong. Pinning the classifier tells the
clipper that nothing crosses the screen edge, so polygons that do are never
subdivided; they are submitted whole with coordinates outside the GPU's legal
primitive range and the hardware discards the entire primitive. The visible
result is geometry disappearing as the camera moves -- widening the view draws
LESS, not more.

Measured on a WipEout 3 game repo, same track and camera path: ~323 primitives
emitted with the pinned sites active against ~761 with the margin at 0. Live
A/B via the ws_margin debug command also showed inverted ship steering and
broken AI with the pins active, both of which cleared at margin 0 while the
16:9 image was retained.

This adds the widening alternative:

    [[widescreen.cull.widen]]
    address  = "0x8017032C"
    expected = "0x28850000"
    mode     = "imm_lower"

Modes name which operand carries the bound and which way it travels:
  imm_upper  SLTI/SLTIU  coord <  imm + m
  imm_lower  SLTI/SLTIU  coord <  imm - m
  bound_rt   SLT/SLTU    rs    <  rt  + m
  bound_rs   SLT/SLTU    rs + m <  rt

Every mode reduces to the vanilla comparison at margin 0, so 4:3 output stays
bit-for-bit identical -- the same guarantee keep sites give. The immediate modes
reuse the existing psx_ws_cull_slti / psx_ws_cull_slti_lower helpers; the
register-bound modes need psx_ws_cull_slt_widen, added here.

The parser rejects a mode that does not match the instruction shape (an
immediate mode on a register compare would widen an operand that does not
exist), rejects duplicate addresses, and full-word guards the site as keep does
so an overlay variant at the same VA is left alone. Widen sites contribute to
overlay cache identity, otherwise migrating a site from keep to widen would
silently reuse the pinned overlay.

No game config uses this yet; migrating a title's sites is a separate change
that needs each site's operand roles established first.
2026-08-17 00:42:30 -07:00
tetrisgm 6de1ad4a05 Add ws_ui_groups: expose the auto_ui_squash partition
auto_ui_squash squashes each spatial run of UI primitives about that run's own
anchor, so a HUD element split across two runs gets two anchors and comes apart
as the frame widens -- elements drifting toward opposite edges, glyphs sliding
off the background box they sit on.

Diagnosing that required knowing which run each primitive landed in, and nothing
exposed it. The two things an observer could reach were both insufficient:
group.key is a hash of CLUT/texpage/Y-band/family, so unequal keys say two prims
differ without saying WHICH component differed; and the anchor takes only three
values (left/centre/right thirds), so equal anchors do not prove two prims
actually co-grouped rather than coincidentally landing on the same third.

ws_ui_groups dumps, for the last prepass, each primitive's op / key / raw key
inputs (y, h, and the derived 24px band and poly-vs-rect family) / union-find
root / final anchor, plus the frame-level active, squash, dense, rank, disp_x,
disp_w and join_gap. Root plus raw inputs together answer "did these two merge,
and if not, what split them".

Carries two small support changes:
- WsUiGroupItem gains a diagnostic `root`, written by ws_ui_group_assign in all
  three paths (dense, alloc-failure fallback, and the union-find path). Never
  read by the transform path.
- WsUiPrepassItem retains y/h/op, which the key otherwise folds away.

Read-only and query-driven, per the ring-buffer rule: capture is continuous and
the observer asks for a window later.
2026-08-17 00:27:36 -07:00
tetrisgm 6bddaab930 Add ws_ui_groups: expose the auto_ui_squash partition
auto_ui_squash squashes each spatial run of UI primitives about that run's own
anchor, so a HUD element split across two runs gets two anchors and comes apart
as the frame widens -- elements drifting toward opposite edges, glyphs sliding
off the background box they sit on.

Diagnosing that required knowing which run each primitive landed in, and nothing
exposed it. The two things an observer could reach were both insufficient:
group.key is a hash of CLUT/texpage/Y-band/family, so unequal keys say two prims
differ without saying WHICH component differed; and the anchor takes only three
values (left/centre/right thirds), so equal anchors do not prove two prims
actually co-grouped rather than coincidentally landing on the same third.

ws_ui_groups dumps, for the last prepass, each primitive's op / key / raw key
inputs (y, h, and the derived 24px band and poly-vs-rect family) / union-find
root / final anchor, plus the frame-level active, squash, dense, rank, disp_x,
disp_w and join_gap. Root plus raw inputs together answer "did these two merge,
and if not, what split them".

Carries two small support changes:
- WsUiGroupItem gains a diagnostic `root`, written by ws_ui_group_assign in all
  three paths (dense, alloc-failure fallback, and the union-find path). Never
  read by the transform path.
- WsUiPrepassItem retains y/h/op, which the key otherwise folds away.

Read-only and query-driven, per the ring-buffer rule: capture is continuous and
the observer asks for a window later.
2026-08-16 23:02:17 -07:00
Alex Vanderveen aa208a9100 loader optimizations pushing for 8mb performance 2026-08-17 00:42:48 -04:00
Alex Vanderveen 84ea64d613 texture pack support 2026-08-16 21:55:51 -04:00
Alex Vanderveen cf742f3190 Treat RAM below a high-loading boot EXE as overlay region
The overlay-region model assumed the boot EXE sits at the bottom of RAM:
[KERNEL_END, text_end) is main-EXE text, [text_end, RAM) is runtime-loaded
overlay code. That holds for a load_address of 0x80010000, where the text
base equals the kernel-window end and the region below the text is empty.

It is false for a boot EXE that loads HIGH and streams its gameplay code
into the RAM beneath itself. Klonoa loads a 46 KB stub at 0x80180000 and
runs its overlays from 0x10000-0x130000, so the floor landed at 0x18B000
and the entire overlay region fell below it:

  - dirty_ram_clear_image_baseline() wiped the dirty bits across
    [0x10000, 0x18B000) believing it was the clean compiled text image.
  - the dirty_ram_dispatch_inner admit heuristic requires phys >= floor,
    so a JALR into an overlay page the CD had DMA'd there (never touched
    by the CPU write hooks, so never marked dirty) was refused by the
    interpreter, fell through to psx_unknown_dispatch and fail-fast
    exit(1) — Klonoa, frame 687, target 0x80123D00.

Pin the text BASE (g_text_image_lo) alongside the floor and route the
gates through one predicate: the overlay region is both sides of the text
image, not just above it. Street Fighter Alpha 3 (0x80113B00) and
Bomberman Fantasy Race (0x8003004C) load high too.

No-op for bottom-loading games: g_text_image_lo stays at its 0x10000
default, the below-text clause is empty, and the baseline clear starts at
the same page as before.

Klonoa now runs 6874+ frames at a locked 60 fps with fail-fast on and
zero unknown dispatches; it previously died at frame 687.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 21:21:28 -04:00
Alex Vanderveen 76945d3a65 8mb ram patches + toolchain windows hotfix 2026-08-16 21:19:05 -04:00
Alex Vanderveen 16bfa4144f 8mb support 2026-08-16 17:59:20 -04:00
Alex Vanderveen 1be8f88839 Merge origin/master into feat/rbengine.
Keep the 8 MB RAM builtin alongside PGXP Precision after the master merge.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-16 14:11:58 -04:00
Alex Vanderveen 8fb75061b4 8mb mod for wipeout and recomp-net bump, rb engine bump, mods in netplay enabled. 2026-08-16 13:35:48 -04:00
Matthew Stanley ad55fecdcf Gate WipEout local viewport native-wide to split frames 2026-08-15 22:53:24 -07:00
Matthew Stanley 41c232fb47 Fix precise NCLIP on PGXP-backed GTE shadows 2026-08-15 21:46:47 -07:00
Matthew Stanley bdd7ade6e8 Merge remote-tracking branch 'origin/master' into codex/netplay-local-viewport 2026-08-15 21:40:56 -07:00
Matthew Stanley b2eeb0f866 Add opt-in netplay local viewport extraction 2026-08-15 20:51:23 -07:00
Matthew Stanley 200b5bf003 Add opt-in widescreen culling keep hooks 2026-08-15 19:42:43 -07:00
Matthew Stanley 38bdeeaade pgxp: emit hook macros + dual build targets + flavor-namespaced caches (phase 2)
Emitter side of the value-propagation engine (ENHANCEMENTS.md G1.10):

- recompiler/src/pgxp_hook_emitter.{h,cpp} (new, in the codegen-hash
  surface): ONE hook grammar shared by BOTH emitters, wrapping loads/
  stores/COP2 transfers and precision-carrying ALU ops (moves, add/sub,
  or-merge, shifts, LUI, HI/LO, muldiv) with PGXP_*() macros. Clobberable
  sources are captured into locals before the statement. Precision-
  destroying ops (AND/XOR/SLT/...) are deliberately unhooked - validate-
  on-read drops their stale shadows for free.
- code_generator.cpp: central append in translate_instruction; LWC2/SWC2
  emissions capture the RAW loaded/stored word (a masked GTE register
  write must validate against the word as loaded).
- strict_translator.cpp: translate() wraps translate_impl()'s c_code with
  the shared grammar; deferred load-delay variants hook the psx_ldd temp.
- BIOS regenerated, fingerprints verified CHANGED (SCPH1001 + OpenBIOS;
  42.6k/12.2k macro sites). Base runtime + 33/33 tests green against it.
- Gating: macros expand only under -DPSX_PGXP=1. runtime.cmake grows a
  PGXP option on psxrecomp_add_runtime_target (defines PSX_PGXP=1 +
  PSX_OVERLAY_FLAVOR=2, _pgxp exe suffix) and PSX_PGXP_VARIANT to build
  the sibling target from the same generated tree (framework runtime +
  game wrapper). No ABI version bump needed: the appended PGXPHooks
  table is NULL-tolerant and the flavor half of the ABI tag rejects any
  host/DLL flavor mix.
- Shard caches: flavor folded into the cache tag (cg<V>_<hash>_gc<hash>
  _f<flavor>) in overlay_loader.c + compile_overlays.py so base and pgxp
  caches never share a directory; compile_overlays.py passes
  -DPSX_PGXP=1 when flavor bit 2 is set; overlay preamble forwards
  psx_pgxp_* through the table. PSX_OVERLAY_CODEGEN_VER 9 -> 10.
- Env overrides PSX_GEOMETRY_CORRECTION / PSX_PERSPECTIVE_TEXTURING /
  PSX_PGXP_CPU_MODE (validation path: free-running headless boots arm
  from the first vertex); census grew produced/swc2_stores and the pgxp
  verb reports suppress/active gate state.

Validated: PSXRecomp_pgxp.exe boots the recompiled BIOS with hooks armed
(engine active, per-vertex lookups flowing; the BIOS shell has no RTPS
content so produced=0 there is correct - coverage measurement moves to a
3D title in phase 3).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 19:03:36 -07:00
Matthew Stanley 2ba74f3b4b pgxp: land clean-room value-propagation engine (phases 0+1, issue #92)
The G1.2/G1.3 mechanism: per-word RAM/scratchpad + per-GPR + per-GTE-reg
shadows carrying sub-pixel projections BY PROVENANCE, validated against
the actual guest word on every read (never guessed from rounded position
- the measured 93.1%-ambiguous G1.4 dead end). Clean-room from psx-spx +
public design docs; vendored duckstation (CC BY-NC-ND) and beetle-psx
(GPL) trees remain black-box oracles only - no code ported.

- runtime/src/pgxp.cpp + pgxp.h: engine, per-vertex GetPreciseVertex
  (dataflow -> ambiguity-gated position cache -> native) with truncation
  agreement + tolerance safeguards; stats; test accessors.
- pgxp_hooks.h: 5-entry hook funnel; PGXP_*() macros no-op unless
  -DPSX_PGXP=1 (codegen-time gate over ONE generated tree); appended
  PGXPHooks* table member in OverlayCallbacks (ABI bump ships with the
  Phase-2 emitter change).
- Both interpreters wired: loads/stores/COP2 + precision-carrying
  arithmetic (moves, add/sub, or-merge, 16-bit shifts, LUI, HI/LO).
- gte.cpp: RTPS feeds the GTE reg shadows; retired s_precise_sxy FIFO,
  hashed s_precision_store, and plain-store invalidation (redundant
  under validate-on-read; store hot path gets 3 calls back).
- gpu.c: prepare_precise_triangle rewritten per-vertex; perspective UVs
  share the same provenance and compose per triangle.
- [video] pgxp_cpu_mode / pgxp_tolerance keys; TCP pgxp verb live-
  toggles geometry/texture/cpu_mode/tolerance; geom_correction census
  grew the pgxp dataflow counters (G1.9 gate metric).
- tests: new pgxp_test (registered); gte_register_access_test updated
  to precise invalidation contract. 33/33 enabled tests green.

Also fixed while landing (both broke a fresh master tree):
- runtime.cmake: once-only psx_game_version.txt stamp (multi-target
  file(GENERATE) collision broke every fresh single-config configure).
- test_overlay_capture_retry: SDL3 rot (SDL_SetMainReady gone, inverted
  SDL_Init check) -> psx_sdl_init.

Tracked as beads-eio.3.48; postmortem + this landing: ENHANCEMENTS.md
G1-G1.10.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 18:34:47 -07:00
Matt Stanley d4cc25dd4d Merge pull request #144 from Alexbeav/workbench/libcrypt-sbi-v1-20260815
runtime: add SBI subchannel replacements for LibCrypt
2026-08-15 16:00:04 -07:00
Matthew Stanley 3de7f9cb52 Bump boot-state version for SBI subchannel state 2026-08-15 15:35:21 -07:00
Alexandros Mandravillis 992f5b0bf0 build: keep SDL3 main implementation in one translation unit 2026-08-15 12:19:16 +03:00
Alexandros Mandravillis cebe738fd1 runtime: add SBI subchannel replacements for LibCrypt 2026-08-15 12:15:23 +03:00
Matthew Stanley 32c6b34986 fix(runtime): clarify PSX overlay controls 2026-08-14 19:12:37 -07:00
Matthew Stanley cab7d00f9a Fix PSX launcher hotkeys and audio persistence 2026-08-14 15:22:29 -07:00
Alex Vanderveen a9a61b4d47 Merge origin/master into feat/rbengine.
Keep recomp-net at occupied_mask (d65918f); master's pin was an ancestor.
Brings in rewind/save-state controls from master.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 02:57:25 -04:00
Alex Vanderveen 31e18d1e9c netplay fmv patch again 2026-08-13 17:38:22 -04:00
Alex Vanderveen fa9b5e027d rb patch cross platform + post fmv 2026-08-13 16:37:22 -04:00
Alex Vanderveen 853e5808c4 post fmv heal + cross os pacing and rb engine bump 2026-08-13 15:20:40 -04:00
Alex Vanderveen 6640c331b6 post FMV hotfix 2026-08-13 14:46:29 -04:00
Alex Vanderveen 9253f229a1 fmv heal patch + appicon 2026-08-13 10:37:18 -04:00
Alex Vanderveen 5d61770322 post FMV heal patch 2026-08-13 10:10:17 -04:00
Alex Vanderveen 4274a76a19 fmv hardening 2026-08-13 01:28:56 -04:00
Alex Vanderveen 9482615b29 Pass lobby occupied_mask into recomp-net for sparse seats.
Bumps recomp-net so empty lobby seats no longer block READY/admit; launch
builds the mask from seated members (online + LAN).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-12 22:37:08 -04:00