The shared recomp-ui launcher already exposes fullscreen as a universal
tri-state (RecompLauncherCSettings.fullscreen: 0 off / 1 borderless /
2 exclusive), but the PSX runtime collapsed it to a bool everywhere and
only ever used SDL_WINDOW_FULLSCREEN_DESKTOP. Widen the runtime to carry
the full tri-state end to end, matching the SNES/Genesis engines:
- config_loader: UserSettings::fullscreen is now int (0/1/2) instead of
bool. settings.toml persists it as a bare int; loading still accepts
the old bool form (true -> 1) for back-compat with settings.toml files
written before this change, then clamps to [0,2].
- main.cpp: removed every `!= 0` / `? 1 : 0` collapse between
UserSettings, the seed struct, and RecompLauncherCSettings, so 0/1/2
round-trips unchanged through the launcher and back into settings.toml.
- main.cpp: win_flags now maps 2 -> SDL_WINDOW_FULLSCREEN (exclusive
mode change) and 1 -> SDL_WINDOW_FULLSCREEN_DESKTOP (borderless), via
a small psx_fullscreen_flag_for_mode() helper shared with the hotkey.
- main.cpp: the Alt+Enter / Cmd+Ctrl+F hotkey now detects "currently
fullscreen" by testing the SDL_WINDOW_FULLSCREEN bit (set in both
SDL_WINDOW_FULLSCREEN and SDL_WINDOW_FULLSCREEN_DESKTOP) instead of
only the DESKTOP flag, and toggles between windowed and the
CONFIGURED tri-state mode rather than hardcoding DESKTOP.
- launcher.cpp/launcher.rml (the legacy RmlUi launcher, still built
whenever PSX_LAUNCHER is on): fullscreen is now a 3-way cycle button
(Off/Borderless/Exclusive) following the same cycle_*/*_label pattern
as the existing CRT/texture-filter controls, instead of an On/Off
toggle.
Validated with g++ -fsyntax-only (mingw64) against config_loader.cpp
standalone, and main.cpp with no launcher flags, PSX_LAUNCHER only,
RECOMP_LAUNCHER only (against the real recomp-ui RecompLauncherCSettings
header), and both together - all clean. launcher.cpp (RmlUi) was
reviewed by exact pattern-match against the file's existing cycle_crt/
crt_label code (lib/RmlUi isn't checked out in this worktree, so it
could not be compiled).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Register configured range_sites with the dirty-RAM interpreter so overlay-resident classifiers use the same two-margin widening already emitted by native codegen.
Co-authored-by: Opok <novelasevilliousesp@gmail.com>
Add opt-in slti/sltiu far-bound scaling with matched native and interpreter semantics, correct immediate sign extension, and an overlay ABI forwarder for live aspect state.
Co-authored-by: Opok <novelasevilliousesp@gmail.com>
Add an opt-in horizontal low-edge transform for subu rd,zero,rt classifiers, with matching native and dirty-RAM interpreter behavior and guarded overlay variants.
Co-authored-by: Opok <novelasevilliousesp@gmail.com>
The debug server is compiled into every build (recorders no-op under
PSX_NO_DEBUG_TOOLS; emu-thread pump sites early-out on a null check), so
starting the listener observes the REAL production hot path instead of the
instrumented build whose shifted IRQ-delivery timing diverges from it.
Loud init banners (opt-in, listening, socket/bind failure). Also adds
PSX_EVENT_STEP_CONSERVATIVE=1 env opt-in so the conservative device-step
diagnostic is reachable without TCP. Releases set neither: no port opens.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit fixes for the async shard-publication pipeline (45a2dda):
- autocompile_shutdown(): stops the pipeline, kills the compiler tree via a
kill-on-close job object (assigned while the child is SUSPENDED so cmd.exe
cannot spawn grandchildren outside it), and JOINS the watcher + preparer —
never abandoning a thread inside the Windows loader lock at ExitProcess.
Wired first into shutdown_runtime(). Crash exits reap the tree via the job.
- Failed off-thread prepares retry (3 attempts, 100 ms) on the worker before
the artifact silently reverts to a synchronous emu-thread first-map; retry
and giveup counters exposed in autocompile_status_json.
- Capacity caps fail closed and LOUD: cand_register shouts once at CAND_CAP;
load_one_dll_prepared refuses at MAX_LOADED_DLLS before registering, so
dll_already_loaded can never lose track and double-register on rescan.
- Publication confinement is filesystem-resolved: published_path_valid
canonicalizes the cache root and the pipe path (GetFinalPathNameByHandle /
realpath), prefix-checks canonical forms, and the worker maps the canonical
string while holding a no-delete-share guard handle (TOCTOU). `..`/junction
spellings that passed the lexical check are now rejected — proven by the
new overlay_path_canon_test boundary test.
- test_autocompile_publication gains a shutdown-mid-run scenario asserting
prepared == committed + discarded with nothing queued; perf guards updated
to require validate -> guard-handle -> canonical first-map ordering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the Ape Escape literals in the recomp-ui launcher block with
config-driven values so every PSX title can consume the shared seam:
- gi.name/gi.region <- game.toml [game] name + new optional region key,
falling back to serial-prefix derivation (SCUS/SLUS/LSP -> USA,
SCES/SLES -> Europe, SCPS/SLPS/SLPM -> Japan; unknown -> no badge)
- gi.num_players <- new optional [game] players key (default 1)
- gi.widescreen_supported <- ws_offered (same source as the RmlUi path)
- Localization parity: language_labels/num_languages from
[runtime].languages, language_index matched from the resolved language,
and the chosen language written back to settings on Launch (mirrors
launcher.cpp's bounds-checked write-back)
config_loader: GameConfig gains region (string) + players (int, default 1),
parsed from [game]; existing configs without the keys are unchanged
(recompiler_patch_test 11/11, defaults probe verified against Ape's toml).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace manual gi identity/caps setup with launcher_profile_apply("psx",
&gi), which bundles theme/platform/rom_noun/pad-mode caps + all has_*
deep-settings flags as one unit. Ape Escape now overrides only what the
profile can't know (name, region, pad-mode lock state, aspect mask,
disc-verify off).
Map PSXRecompV4::UserSettings <-> RecompLauncherCSettings both ways for
the deep fields the profile now exposes: window_width, renderer,
supersampling, antialiasing, texture_filter, screen_kind,
frame_interpolation(+fps), spu_hq, auto_skip_fmv, turbo_loads, and
bios_path (string, snprintf-guarded). Fix the pre-existing
texture_filter write-back, which read the legacy ls.linear_filter bool
even though has_texture_filter=1 makes the launcher edit ls.texture_filter
directly — it now reads the right field.
Verified via build-recompui + LNG_SCRIPT screenshots: Settings now shows
the full PSX surface (Window size/Renderer/Supersampling/Aspect/Texture
filtering/Antialiasing/Screen model/Frame interpolation/Skip FMVs/Turbo
loads/Fullscreen, SPU high-quality/Analog deadzone, and a SYSTEM/BIOS
row with Browse), not the old pad-mode+aspect-only surface.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the RECOMP_LAUNCHER block in runtime/src/main.cpp to pass the
new RecompLauncherCSettings.pad_mode[2]/aspect_index and
RecompLauncherCGameInfo.pad_mode_supported/pad_mode_selectable/
allow_hybrid/locked_pad_mode/lock_device/aspect_mask fields, sourced
from the same GameConfig-derived locals (ctrl_lock_mode,
ctrl_allow_hybrid, ctrl_lock_device, p1_mode, ws_offered,
ws_ultrawide_offered) the existing RmlUi launcher path already uses.
- ls.pad_mode[0/1] seeded from seed.p1_mode/p2_mode; ls.aspect_index
derived from seed.aspect_num/aspect_den (4:3/16:9/21:9).
- gi.pad_mode_supported = 1 (PSX always has pad modes);
gi.pad_mode_selectable = !ctrl_lock_mode; gi.allow_hybrid =
ctrl_allow_hybrid; gi.locked_pad_mode = p1_mode; gi.lock_device =
ctrl_lock_device; gi.aspect_mask = 0x1 | (ws_offered<<1) |
(ws_ultrawide_offered<<2).
- On LAUNCH, ls.pad_mode[]/aspect_index are copied back into
seed.p1_mode/p2_mode/aspect_num/aspect_den so the launcher's choice
persists to settings.toml exactly like the RmlUi path.
Verified against the real Ape Escape game.toml: aspect_mask=0x7
(4:3+16:9+21:9, offer_ultrawide=true) and pad_mode=[1,1] (analog,
matching default_mode="analog") both flow through correctly. The pad-
mode selector itself renders hidden/locked to Analog in the built
launcher, because Ape Escape's [controller] block sets lock_mode =
true (intentional: it's a dual-analog-only title) -- gi.pad_mode_selectable
correctly evaluates to 0 for this game, same as the RmlUi path's
ginfo.lock_mode already did.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When built with the recomp-ui submodule (RECOMP_LAUNCHER defined), run the shared
recomp_launcher_run_window() instead of the RmlUi psx_launcher::run(); RmlUi stays
the default when the define is absent. Maps UserSettings <-> RecompLauncherCSettings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tier mis-compiled and has been hard-gated off since 2026-06-25; overlay
gaps fall to gcc/tcc shards and the dirty-RAM interpreter, unchanged.
Re-do on master of the removal validated on the stale branch (6da8c02),
reconciled with everything master grew since the fork:
- Delete overlay_sljit.{c,h}, overlay_compile_worker.{c,h}, lib/sljit,
runtime/tests/sljit_*.c, and the runtime.cmake sources/include-dirs.
- Strip the sljit provider/backend from code_provider.{c,h},
overlay_backend.{c,h} ("sljit" in overlay_backend/PSX_OVERLAY_BACKEND now
degrades to auto so stale configs still boot), main.cpp, debug_server.c
(sljit_status/sljit_async/sljit_try TCP commands), overlay_loader.{c,h}
(live policy, JIT-on-miss, persisted-shard cache, async worker glue,
psx_sljit_call/cop2/memx host helpers — reachable only via the removed
cpu->sljit_helpers table), and cpu_state.h (SLJIT_HLP_* enum + the
sljit_helpers[] member; CPUState layout change reshards via the cg tag).
- Master-only entanglement resolved before deletion: 28165d6's Einhander WS
player-X bound emit existed in three paths (gcc code_generator, sljit,
dirty_ram_interp). The gcc shard link contract now forwards
psx_ws_player_x_bound (ABI v13, previous commit), so deleting the sljit
emit path loses no widescreen behavior; ffc6688's game-dispatch block
inside the dead psx_sljit_call helper goes with it.
- Reword comments that described sljit as a live backend (gpu.{c,h},
ws_*_detect.h, cosim_state.c, dirty_ram_interp.c, autocompile.h,
boot_state.h, config_loader.h, compile_overlays.py, and the CPS marker
comments the recompiler emits).
recompiler psxrecomp-game builds clean; game runtimes rebuilt + boot-verified
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- psx_cyc.h / psx_cycles.c: _BitScanForward/_BitScanReverse shims for
__builtin_ctz/__builtin_clz under _MSC_VER
- full_function_emitter.cpp / main_psx.cpp: emit a portable .CRT$XCU
init-pointer instead of GCC-only __attribute__((constructor)) in
generated dispatch/game code
- main.cpp: declare cross-language globals extern "C" so MSVC links
them against their C definitions (no-op for GCC/Clang)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The default controller map folds the left stick onto the D-pad bits
(up=dpup,lefty- … right=dpright,leftx+) so the stick acts as a D-pad
for digital-only games. In ANALOG pad mode that fold is wrong: the
stick already drives the analog axes, and on a real DualShock the
stick and D-pad are independent. A dual-analog title that uses the
D-pad as its own control therefore saw phantom D-pad presses from
every stick movement — and constant presses from stick centre drift.
Ape Escape is the concrete victim: it is locked to analog, its D-pad
left/right rotates the camera, so walking with the left stick (or a
drifting Series-X stick at rest, past the 12%/4000 deadzone) spun the
camera with no D-pad input. Reported by VGEsoterica.
Fix in the framework (class fix, all analog titles inherit): resolve
the effective pad type before building the button word, and when the
pad presents as analog this frame, suppress the LEFTX/LEFTY/RIGHTX/
RIGHTY axis sources as button contributors. Trigger axes (L2/R2) and
button sources are unaffected; digital mode keeps the stick->D-pad
fold (its only outlet there). The physical D-pad still drives the
D-pad bits, so Ape's camera rotate works from the D-pad as intended.
Stick/axis output is byte-for-byte unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Triage of Tomba2Recomp GH issue #1 exposed three ways a release exe
behaves differently on the build machine than on every user machine:
- DEFAULT_BIOS_PATH baked an ABSOLUTE build-tree path into the binary,
so promoted/release exes silently load the builder's BIOS wherever
that path exists and the clean-install picker flow is never
exercised where releases are validated. runtime.cmake now refuses
absolute values (loud configure warning, converts to the relative
default); dev checkouts keep prompt-free boots via a new
resolve_bios_path rung that walks exe-dir ancestors for
psxrecomp-v4/<relative>, a layout only dev trees have.
- A settings.toml with a TOML syntax error was silently discarded
wholesale (user's renderer choice, BIOS/disc paths - everything),
and the next launcher save then overwrote the file with defaults.
load_user_settings now reports parse_error; the runtime preserves
the broken file as settings.toml.bad and warns via message box +
stderr.
- package_release.ps1 gains guards: fail packaging if the staged exe
contains a drive-letter absolute SCPH1001.BIN path, if the stage
carries BIOS/disc/save/sidecar files (SCPH*.BIN, *.cue, *.iso,
*.mcd, bios.cfg, disc.cfg, settings.toml, keybinds.ini,
overlay_captures.json), or if saves/ is non-empty.
Verified: rebuilt Tomba2 against this tree - configure warning fires
on the absolute arg, strings scan of the exe finds no drive-letter
BIOS path, and a boot with no [bios] setting and no bios.cfg resolves
the BIOS through the psxrecomp-v4 dev rung.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tomba 16:9: rooms, sky falls, and task-card sequences presented stretched
because native-wide's 2D-backdrop stretch fires wholesale in scenes whose
background phase never ends. Such scenes now present native 4:3 from the
faithful canonical buffer, keyed on DEEP UNTAGGED POLYGON OVERHANG (>=24px
past the canonical window, >=4 polys, sustained 2 frames) — the only world
signal that survived adversarial scenes (shaded prims, GTE volume, any-poly
and shallow overhang all falsified by census evidence; lineage in gpu.c).
Scoped to sprite-tag titles; full_2d (MMX6) and gte_game_mode (Ape) paths
unchanged. Plus: native-wide never stretches a canonical-width game frame
(fallback pillarboxes); always-on present-classification ring (TCP
present_ring) across all backends; debug input injection drives the full
pad model (hybrid d-pad drop + stick axes); savestate TCP honesty; census
tagged column + ws_hud_mode A/B (HUD-corners deferred: ENHANCEMENTS.md W2).
USER-VALIDATED on Tomba (SCUS-94236).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2D-only-scene classifier (fixes room/fall/title-card stretch, Tomba):
- Native-wide's 2D-backdrop stretch fires wholesale in scenes whose
background phase never ends (room interiors, sky-only falls), stretching
the whole frame. Such scenes now present native 4:3 from the faithful
canonical buffer (gpu_ws_present_native_43).
- The world signal is DEEP UNTAGGED POLYGON OVERHANG: polys whose raw SX
extent crosses >=24px past the canonical window, >=4 per frame, sustained
2+ consecutive frames. Lineage of rejected signals (each defeated by a
real scene, census-evidenced): shaded-prim presence (title-card letter
tiles are gouraud), GTE projection volume (task-jingle projects
world-scale counts; title screen idles at 1680 verts), any-poly overhang
(card letters slide in from off-screen), shallow overhang (jingle
sparkles poke <=20px). Outdoor terrain measures 55 deep-overhang
polys/frame vs 0-1 for every observed 2D scene/effect.
- Scoped to sprite-tag titles: full_2d (MMX6) and gte_game_mode (Ape)
classification is unchanged.
Native-wide present invariant:
- A game frame that cannot present wide (compositor fallback) pillarboxes
4:3 instead of stretching the canonical read across the wide window
(all backends; squash mode still stretches by design).
HUD corner re-anchoring for sprite-tag titles ([widescreen] nw_hud_corners):
- Poly/line prims never shift (they are the world / tagged billboards);
rect-family prims shift only when UNTAGGED — the proven hud_sprt_squash
discriminator — so enabling nw_hud_corners is safe without a packet
arena range.
Observability (always-on, institutional):
- present_ring: per-present classification ring (path, present width,
game_mode, native_43, wide fallback, tag age, gte_verts, ovh_prims)
for every backend; TCP "present_ring". Answers "why did this frame
stretch" after the fact.
- gpu_state ws block gains last_3d_frame, gte_verts, last_world3d_frame,
ovh_prims, last_ovh_frame; GTE vert counting is now always-on.
Input injection drives the full pad model:
- set_input/press now resolve pad type like real sampling: injected d-pad
drops a hybrid pad to digital (movement works), optional lx/ly/rx/ry
stick override (0..255), type via the coherent request channel.
Previously buttons-only: menus reacted, walking never did.
Savestate honesty:
- savestate_request_save/load return staged/refused; the TCP handler
reports ok:false instead of silently acking loads it cannot perform
(LLE host-fiber runs). Torn-restore reliability audit remains open.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dirty-RAM text-image guard (685f38d) only armed when the loose local
boot-EXE file next to game.toml existed - found via find_project_root's
repo-marker walk. End-user release installs have neither a repo checkout
nor the EXE file (it is copyrighted game data), so the guard silently
never armed. Without it, fntrace's alternate game-start latch
(psx_game_address_in_text && dirty_ram_text_native_ok) cannot fire for
titles whose BIOS handoff enters text inside the shell-window shadow
(RAM 0x30000-0x5AFFF -> shell ROM until game-start): the shadow never
lifts, execution runs dead shell ROM bytes over real game text, and the
boot dies in a jump to PC=0 (Ape Escape v0.0.2/v0.0.3 release zips,
frame 14, both HLE and LLE boots; in-repo runs armed invisibly and
masked the bug during validation).
Fix, class-level for every title:
- main.cpp: defer guard arming until the disc path is resolved and add a
second source - extract the boot EXE from the mounted disc image via
ISOReader (basename of game.toml's exe field, SYSTEM.CNF BOOT token
fallback). The disc is the same bytes the BIOS loads, i.e. the true
reference; every install has one. Local EXE file still wins for dev
checkouts. A loud WARNING prints when neither source arms (previously
silent, which is how the broken releases shipped green).
- fntrace.c: the alternate game-start latch now requires a registered
image (dirty_ram_text_image_registered). Its safety argument - "RAM
still holds shell bytes that differ from the game image, so it cannot
fire prematurely" - only holds when an image exists; with no image,
native_ok degrades to !dirty and untouched text pages pass it,
latching game-start before the EXE loads and clearing the dirty
baseline mid-load. Without an image, only the exact entry_pc dispatch
may latch (the pre-685f38d conservative behavior).
Verified: v0.0.3 zip contents + rebuilt exe in an isolated directory
(no repo above it) boots to the Ape Escape intro on both HLE and LLE
paths, guard armed from disc SCUS_944.23; in-repo runs still arm from
the local EXE.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The controller map had no l3/r3 rows at all, so pad bits 1/2 could never
be produced by a gamepad — DualShock-only games (Ape Escape) were
unplayable. Keyboard slots existed but shipped unbound.
- main.cpp: PAD_L3/PAD_R3 bits; controller_map 14->16 entries; defaults
l3=leftstick, r3=rightstick; input.ini template + doc comment updated.
- psx_keybinds.c: keyboard defaults L3=T, R3=Y (continue the Q/W/E/R
shoulder row). Launcher Controls page picks them up automatically.
- beetle_main.cpp: same T/Y keys on the oracle so psx-beetle stays
drivable for L3/R3 games.
- tools/package_release.ps1: shipped input.ini template gains l3/r3.
- README: keyboard + controller map tables.
Wire is unchanged: sio.c transmits the button word verbatim, matching
Beetle's dualshock.cpp. Validated in-game on Ape Escape (user-confirmed
R3/L3 inputs on a real pad).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GTE honors per-instruction lm (bit 10) and sf (bit 19) in the depth-cue and
lighting family (INTPL/DPCS/DPCT/DPCL/NCDS/NCDT/CDP/...) with the faithful
hardware formula and always-on GTE observability rings (gte_ring_dump,
gte_intpl_dump, gte_frame_stats, gte_latch_dump). Fixes Crash Bash
menu-character strobe/flicker and in-game flattening (signed INTPL vertex
tweens were clamped to zero by the hardcoded lm=1).
Also: dispatch no longer shadows runtime-relocated game code in the BIOS
shell window (native-ok byte compare routes matching text to the game path);
stale GPUSTAT-poll VBlank over-delivery fix + present pacing tooling.
Cross-title validation (GTE_LM_FIX.md): Crash Bash (fix target), Ape Escape,
Tomba 2, Mega Man X6 - all rebuilt branch-flavor (regen included) and verified.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three producer/delivery fixes, each measured on MMX5 attract (port 4520
audio_stats, always-on rings):
1. Guest-cycle sample budget (main.cpp): the SPU pump produced 44100/60
samples per host present — assuming a 60.00 Hz vblank against the real
NTSC 59.94 Hz and quantizing to presents. Production measured 43950/s
vs the device's 44100/s. Now the budget derives from psx_cycle_count:
exactly 768 guest cycles per output frame (33.8688 MHz / 44100), so
audio tracks guest time like every other device.
2. Bounded pacer catch-up (frame_pacing.c): frame_pacer_wait re-anchored
(forgave all debt) whenever a frame ran >1 period late, permanently
slipping the timeline on every heavy frame — measured 59.80 Hz average
against the 59.94 target. Debt up to 8 periods is now repaid by running
unpaced, so transient stalls no longer starve the audio ring; only
sustained sub-realtime forgives.
3. PI bridge servo (recomp_audio_drc.h): the P-only fill servo needed a
standing fill deficit of corr/kp to hold any steady correction (0.3%
skew = 7.5 ms of permanently eaten margin -> jitter spikes hit the dry
floor). Added an anti-windup integral term that holds steady clock skew
at zero fill error, and raised target/ring 50/150 -> 80/250 ms for
emulated-machine jitter (tens of ms, far beyond host-driver jitter).
Also: bridge dry-pulls during an intentional turbo mute are resynced
past, not reported as gaps (the mute IS the silence).
Measured, MMX5 attract, 150 s steady windows:
legacy push baseline: ~360 underrun events / 150 s (queue to 0 = gap)
bridge as merged: ~90 underrun events-equivalent / 150 s
this commit: 1 underrun event / 150 s, fill ~55-73 ms,
production 43981/s, corr -0.5% (absorbs the
guest's residual ~0.27% sub-realtime pace).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>