Pass generated shard paths via a SOURCES_FILE so cmake -P is not killed by
CreateProcess's ~8191-char limit, and keep has_bios on for OpenBIOS-only
setup SDKs so the first-run wizard still offers SCPH1001.
Co-authored-by: Cursor <cursoragent@cursor.com>
Routing a player to a controller silently discarded every keybinds.ini and
mouse bind, which defeats the pad-in-hand plus mouse-for-aiming combination
those binds exist for.
This is the same behaviour change as the original commit in PR #107, but
applied through pad_sources_for() rather than to the button merge alone. That
distinction is the point: every consumer of the source set - the button merge,
the HYBRID auto-switch detectors and the analog stick fold - now learns about
the keyboard in the same instant.
Widening only the button merge (the original shape) let a keyboard D-pad press
assert the D-pad bits while hybrid_dpad_active never saw them, so a mod-driven
hybrid pad would report D-pad input while still presenting ANALOG - the exact
failure the hybrid logic exists to prevent, and silent. Commit 3ba40b02 had
previously removed a keyboard-always-P1 behaviour for related reasons.
The PSX pad word is active-low and the merge is an AND, so an unpressed source
is a no-op and pad-only players are unaffected.
NEEDS HANDS-ON VALIDATION: Tomba's hybrid controller mod is the only consumer
of HYBRID mode. It should be exercised with a controller routed AND keyboard
binds live, confirming the pad still auto-switches. If the two cannot coexist,
the agreed fallback is for the mod to disable keyboard/mouse dual binding while
active, documented in the mod.
Co-Authored-By: Alexandros Mandravillis <Alexbeav@live.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hybrid kept reappearing in titles that never meant to ship it, because it was
the ZERO value of PadMode and therefore the default everywhere: PlayerInput,
p1_mode/p2_mode, ctrl_locked_*, and default_p1/p2_mode all initialised to it.
A game then had to opt OUT with [controller] allow_hybrid = false, and every
shipping title did exactly that or pinned a mode - so no title actually ran it.
Hybrid now has exactly one route: a trusted game plugin calling
psx_mod_set_controller_mode_override(), which is how Tomba's hybrid controller
mod already requests it. Everything that made it reachable by accident is gone:
- defaults are ANALOG (PlayerInput, p1/p2, ctrl_locked_*, default_p1/p2)
- [controller] allow_hybrid is removed, along with the startup clamp that
existed only to undo the bad default
- game.toml default_mode/p1_mode/p2_mode = hybrid now THROWS, naming the
mod API as the only route, rather than being silently coerced
- a stale settings.toml hybrid migrates to analog via
pad_mode_from_settings_string(); a user should not fail to launch over a
value they never typed
- pad_mode_to_string() never writes hybrid back
Deliberately KEPT, because the mod runs on it: PAD_MODE_HYBRID,
PSX_MOD_CONTROLLER_HYBRID, psx_mod_set_controller_mode_override(),
hybrid_stick_active/hybrid_dpad_active, PlayerInput::hybrid_analog and
psx_keybinds_dpad_active.
Paired with the recomp-ui change that drops the Hybrid segment and the
allow_hybrid field from RecompLauncherCGameInfo - the two must land together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The runtime decided which sources may drive a pad slot in three independent
places - the button merge in capture_pad_slot, the HYBRID analog/digital
auto-switch (hybrid_stick_active / hybrid_dpad_active), and the analog stick
fold. Nothing kept them in step.
That is a live hazard rather than a tidiness issue. If the button merge is
widened without widening the hybrid detectors, a source can assert the D-pad
while the hybrid state machine never sees it, so the pad reports D-pad presses
while still presenting ANALOG and never falls back to digital - the exact
failure the hybrid logic exists to prevent, and silent when it happens.
Commit 3ba40b02 previously removed a keyboard-always-P1 behaviour for related
reasons; the hybrid gates were written assuming it stayed removed.
pad_sources_for(p, dev_here) now returns the source set once (device,
keybinds, all_pads) and all three consumers read it. Input-routing POLICY
becomes a change to that one function instead of three conditions that must
be edited together.
Pure refactor - behaviour is unchanged:
device == (p.kind != 0), as before
keybinds == (p.kind == 1 || dev_here), the same expression the hybrid
D-pad detector already used
all_pads == dev_here, as before
dev_here implies slot 0, so the previously hardcoded player 1 in
pad_from_keyboard(1) and psx_keybinds_sticks(keys, 1, ...) is the same value
as `player`. Where a consumer is now reached that was not before (kind==1 in
analog mode re-entering psx_keybinds_sticks), the call is idempotent: it only
sets fixed extremes on held binds, and pad_sticks_for already applied it with
the same player.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
cdrom_init() is deliberately non-fatal -- BIOS-only targets run with an empty
drive on purpose -- so when a disc IS requested and nothing mounts, the game
boots into an empty drive and renders nothing. That is the "black screen on a
.cue that works as a .bin" symptom.
The earlier validate_disc_for_launch() pass cannot catch it: identify_disc()
reads the data-track FILE directly, so a cue whose sheet the mounting reader
rejects still earns a green "Disc verified" badge and the player is left
staring at black with no explanation.
Master reports the actual failure and names the likely cause; PR #110 dropped
the check while keeping every part it depends on -- cdrom_has_disc() in
cdrom.h, resolve_disc_path()/DiscPathResolution in disc_path.h, and
launcher_warning() -- so only the call site and the disc_path.h include were
missing. Block restored verbatim from origin/master.
Verified no false positive: Tomba boots past cdrom_init into gameplay with a
good disc (PSX_NO_LAUNCHER=1, reaches the in-game window). The negative case (a
cue that verifies but will not mount) was not synthesised.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #110 replaced master's plan() call with an inline
"(bios_hle && !keep_intro) || fast_boot" boot-skip decision that passes
bios_hle straight through as the call-HLE axis. That conflates two axes with
DIFFERENT per-image requirements, which is exactly the bug bios_hle_plan.c was
written to fix: call-HLE needs a DeliverEvent anchor (deliver_event_ret), the
boot-skip needs only shell_entry_phys.
On an image exporting no DeliverEvent anchor -- OpenBIOS, which is bundled and
default for several titles, and bios_hle defaults ON -- the inline form turns
call-HLE on anyway, servicing B0 events with mismatched semantics. That is the
OpenBIOS bring-up wedge: the shell menu hangs in a CD-event wait under HLE and
boots fine under LLE.
Restores the plan() call and both denial notices, so an unavailable axis is
reported rather than silently taken (or silently cancelling the other).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BIOS
- resolve_bios_path() now consults PSXRecompV4::resolve_bios_rom() again, so
either filename convention resolves: a dump folder holding
"US-PSX-SCPH1001.BIN" satisfies a request for "SCPH1001.BIN". Restored at
both rungs (direct hit and the exe-dir upward walk).
- gi.has_bios = psx_bios_has_selectable() restores the launcher BIOS row.
It asks the registry rather than psx_bios_image, which is not populated
until a backend activates.
Input
- anti_deadzone was gone entirely: PR #110 inlined the stick maths into
axes_to_pad_pair() and implemented only the deadzone half, so [controller]
anti_deadzone in game.toml was parsed and silently ignored. axes_to_pad_pair
now calls the shared psx_stick_to_dualshock() (runtime/src/psx_stick.c),
keeping PR's per-player deadzone and restoring master's anti-deadzone.
- Launcher device<->source mapping used raw string compares, so a hand-edited
"Keyboard" or " none " was misclassified as a gamepad. Both read sites and
both writeback sites now use launcher_source_from_device(), which trims and
lowercases first. PR's N-player loop and GUID preservation are kept -- they
supersede master's 2-player launcher_device_from_source pair.
Shutdown
- autocompile_shutdown() restored ahead of capture teardown, so closing the
window no longer leaves cmd/python/gcc running against cache and capture
files while the main thread does synchronous shutdown work (the window
appears frozen until that process tree exits).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #110 keeps the widescreen engine and most of its configuration (full_2d,
bg2d, clear_reveal, cull_guard_pixels, explicit/slti/negsub/vxrange/depth/
plane_nx/xclip cull sites, dome sites) but drops five setters that master
calls from the same [widescreen] config block:
- gpu_ws_set_auto_ui_squash
- gpu_ws_set_activation_guard_pixels
- gpu_ws_set_cull_keep_sites
- gpu_ws_set_angle_sites
- gpu_ws_set_aspect_cone
All five still have live config fields in recompiler/src/config_loader.h and
live implementations in runtime/src/gpu.c -- only the main.cpp call sites were
missing, so every game.toml that configures them was silently ignored and the
funnel-cull / aspect-cone quality work never engaged.
Blocks are taken verbatim from origin/master and inserted at the same points
in the config block.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #110 keeps almost the whole mods integration - mod_plugins.h, every
psx_mod_set_* callback, mod_runtime_commit, mod_runtime_activate_plugins -
but four call sites present on master are absent, which leaves the entire
feature inert:
- mod_runtime_initialize scan <exe_dir>/mods + load persisted state
- mod_runtime_launcher_provider gi.mods, the launcher Mods tab
- mod_runtime_effective_disc_path mount a mod's private patched image
- mod_runtime_enable_disc_patches apply exe/overlay patches after the guard
Without initialize() nothing else can fire, so a build stages all of a
title's mod packages next to the exe and then never reads them: Tomba showed
an empty Mods tab and fell back to the legacy Display "View mode" row instead
of the widescreen mod.
gi.mods is set at both launcher GameInfo sites so the tab appears on either
path. Blocks are taken verbatim from origin/master.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Include mod_plugins/mod_runtime and restore master’s four-arg file picker
so the integrate←master merge builds.
Co-authored-by: Cursor <cursoragent@cursor.com>
Closes the four gaps named in issue #103 and lands the separable, verified
parts of PR #102. See docs/internal/SPU_FIDELITY_103.md for the per-piece
disposition and the oracle-verification queue.
SPU DSP (issue #103), clean-room from hardware documentation:
- Reverb: the documented 22050 Hz engine — vLIN/vRIN input, same-side and
crossed different-side IIR reflections, 4-tap comb early echo, both
all-pass filters, vLOUT/vROUT output, and the work-area address advance.
Gated on SPUCNT bit 7 for WRITES only; reads, IRQ checks, address advance
and output always run, as on hardware. Addressing is confined to
[mBASE, 0x80000). Per-voice sends from EON, CD send from SPUCNT bits 0+2.
- Noise: 16-bit LFSR with the documented parity feedback, clocked at the
SPUCNT bits 8-13 rate; NON voices substitute it for their ADPCM sample.
- Volume sweeps: all 24 voice L/R plus main L/R. Bit 15 selects sweep vs
direct; sweep is a live envelope on the same rate machinery as ADSR, and a
guest read of a sweeping register returns the live level.
- Capture buffers: CD L/R and voice 1/3 output written to SPU RAM, so a game
that parks the IRQ address there still gets its interrupt.
- SPU IRQ (I_STAT bit 9): checked at every SPU RAM access class — FIFO, DMA
both directions, ADPCM block fetch, capture writes, reverb work-area
accesses — gated on SPUCNT bit 6, mirrored in SPUSTAT bit 6, acked by
clearing SPUCNT bit 6.
The CD-only fast path in spu_render was removed rather than extended: the
SPU now has per-frame work that must run in every path, and an FMV (XA audio
with zero active voices) is exactly the CD-with-reverb case.
This is a clean-room implementation from the psx-spx/nocash register map and
documented algorithm. Beetle's source was deliberately not consulted: it is
GPL-2.0-or-later and this project is PolyForm Noncommercial. PR #16 was
parked for exactly that reason and PR #13's reverb was separately wrong (it
gated on bit 15 of dAPF1, an address offset, instead of SPUCNT bit 7). The
one place this knowingly deviates from hardware is the 22.05 -> 44.1 kHz
reconstruction filter, which the documentation does not specify; it is
isolated in a single function and flagged for oracle comparison.
CAUSE.IP2 (from PR #102):
IP2 is combinational on R3000A — it mirrors the interrupt line and falls the
instant the guest acks I_STAT or masks the source. It was being OR'd in at
delivery and never cleared, in TWO places (the compiled path and
psx_interpreter.c, the latter missed by PR #102), leaving a phantom pending
interrupt that can spin a kernel dispatcher looping on CAUSE.IP & SR.IM.
psx_irq_refresh_cause_ip2() is now the sole writer of bit 10, refreshed at
raise, I_STAT ack, I_MASK write, HLE context restore and power-on. Verified
against the Beetle oracle's irq.cpp/cpu.cpp rather than asserted, and
covered by test_cause_ip2_combinational.
Mid-dispatch audio pump (from PR #102), with a fix: routed through a gate
that mirrors sdl_audio_update's mute/sink decision. Pumping unconditionally
from the VBlank edge, as submitted, would have pushed real audio during every
turbo-load hard mute and defeated the freeze-in-place mute model.
SPU RAM DMA readback (from PR #102): DMA4 in the SPU->RAM direction wrote
literal zeros; SPU RAM is readable memory and titles carry state through it
across an Exec boundary.
Also fixes an unrelated build race found on the way: tools/embed_spirv.py
wrote its intermediate SPIR-V to <source>.spv, so the two runtime targets
that embed the same shaders raced on one path and one deleted the file the
other was reading.
Co-Authored-By: Alexandros Mandravillis <Alexbeav@live.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wires the two [video] settings into the recomp-ui Display page: seeds ls.* from
settings.toml, writes the player's choice back, and sets the new
has_geometry_precision capability. Geometry precision is a property of the PS1
pipeline rather than of any particular disc, so every PSX title exposes it.
Pairs with recomp-ui feat/psx-geometry-precision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Makes the parked projection/perspective work reachable and gets it into the
renderers people actually use. Addresses issue #92 (PGXP-style geometry and
texture correction).
The GTE side cache, SWC2 RAM provenance, GP0 triangle preparation and the
software-renderer consumption path already landed in 2ceaf5a ("park projection
and perspective correction", from kem0x PR #14), but were unreachable: the
generic setters gte_geometry_correction_set() / gpu_texture_correction_set()
had no production caller, and the corrected vertices went straight to
sw_set_precise_triangle(), bypassing the renderer vtable — while the default
renderer is OpenGL.
This adds:
- [video] geometry_correction and [video] perspective_texturing, in game.toml
and settings.toml, both defaulting false. Independent knobs; the launcher
seed marks them present so a save round-trips a hand-edited key instead of
dropping it. Startup names the active pair, and notes when geometry
correction is on below supersampling 2 (where it cannot be seen).
- gr_set_precise_triangle() / gr_set_perspective_triangle() on the
GpuRenderBackend vtable, so gpu.c hands the correction to whichever backend
is live rather than to the software rasterizer directly. NULL entries no-op.
- OpenGL and Vulkan consumption. Positions are already float end-to-end, so
sub-pixel vertices drop in; the readback dirty rect widens by one pixel to
cover them. Perspective UVs use a per-vertex a_q weight and clip coordinates
pre-multiplied by w = 1/q, letting the hardware's own perspective divide
interpolate a smooth UV varying beside the affine noperspective one. a_q == 0
— the default, and what every prim gets with the feature off — makes w
exactly 1.0 and selects the affine varying, so the faithful path is the
pre-feature pipeline unchanged.
The PS1-visible GTE SXY FIFO stays integer either way: guest-side screen-bounds
culling and SXY readback are untouched, and this remains visual-only.
Also fixes a pre-existing concurrent-build race found while validating: every
runtime target runs embed_spirv.py over the same shaders, and each wrote
"<shader>.spv" next to the source, so under -j N one process deleted the file
another had just written (FileNotFoundError on a shader that compiled fine).
The intermediates now go to a per-invocation temp dir.
Parked-work provenance retained; see ENHANCEMENTS.md G1.
Co-authored-by: Kareem Olim <kareemolim@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Players asked for CD speeds beyond the curated 2x/4x list and for the two
loading accelerators to stop being one control (GH TombaRecomp#5: a
speedrunner wants shorter loads but NOT the host speed-up, which runs past
the loading screen and breaks a skip).
Three changes, all general:
- psx_mod_set_disc_speed accepted only divisor 2 or 4. That was policy,
not safety: cdrom.c divides the sector delay and floors the result at
CDROM_MIN_DELAY, and apply_speed leaves XA streaming at authentic
timing, so no positive divisor can produce a zero-delay storm or
pitch-shift FMV audio. Accept 1..PSX_MOD_DISC_SPEED_MAX and let the
mod's description carry the risk warning.
- psx_mod_set_load_acceleration accepted 2..16. Host pacing cannot desync
the guest at all (every frame, CD deadline, interrupt and callback still
happens), so the ceiling was arbitrary. Accept 1..PSX_MOD_LOAD_ACCEL_MAX,
with 1 meaning authentic and 0 still meaning uncapped.
- New option key disabled_by: an option names a BOOLEAN sibling that
overrides it, so "tick Instant and the speed box stops mattering" needs
no second widget type. The parser rejects a dangling, self-referential
or non-boolean reference rather than leaving a control silently always
enabled, and the provider resolves the link against the live selection
so the launcher and the plugins can never disagree about whether a
control counts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Relative bios.cfg / settings picks like bios/SCPH1001.BIN broke after
rebuild when cwd was build/; launcher verify also ignored exe-anchored
resolve. Absolutize sidecars, hydrate from bios.cfg, and resolve in
ae_bios_verify.
Co-authored-by: Cursor <cursoragent@cursor.com>