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>
Wire lib/retcomm-rbengine beside recomp-net and replace MotK in-tree sched,
hash_confirm, input hist, snap ring, and rb_post with thin PSX shims so the
ecosystem can share invent/pacing policy outside each title facade.
Co-authored-by: Cursor <cursoragent@cursor.com>
PR #134 fixed test_netplay_snap_ring.c's harness. This addresses why it broke:
its only build instructions were a gcc command line in a header comment, and a
comment cannot fail. 16 of the 41 C/C++ tests in runtime/tests/ were in that
state, plus 3 python guards. Four had already rotted against the runtime API.
Worse, two tests that WERE registered did not link on origin/master --
sio_dualshock_rumble_test (psx_read_word / psx_get_cycle_count / i_mask /
g_debug_last_store_pc) and spu_fidelity_test (crc32_update) -- so `ctest` in a
runtime tree did not build at all.
Registering them fixes the instance. check_test_registration.cmake fixes the
class: it reads both CMakeLists.txt files as text and fails configure on any
test file that is neither registered nor listed with a reason. Reading them as
text (rather than adding a top-level CMakeLists.txt) keeps the BIOS-free
recompiler tree configurable, which is the only tree a newcomer can configure;
both projects call the guard, so each catches orphans in the other.
CONFIGURE_DEPENDS matters: without it, ADDING a test would not re-run configure.
Also incorporates TechnicallyComputers' review of #134. -Werror turns a
pre-existing Linux warning into a hard failure: clock_gettime/CLOCK_MONOTONIC
need _POSIX_C_SOURCE under a strict -std=cNN. Both halves are needed -- the
define in netplay_snap_ring.c fixes a direct compile of that TU, but not the
harness, which includes <stdio.h> first and so has already latched glibc's
<features.h>. Verified on Ubuntu 24.04 / GCC 13.3.
Test-file changes:
* repaired 4 rotted tests (stubs / a changed parameter list), no logic edits
* build recipes replaced with `ctest -R <name>`, so exactly one description
of how a test builds exists and the build executes it
* known-failing tests registered DISABLED, not de-registered -- de-registering
is the mechanism that hid these in the first place; DISABLED keeps them in
`ctest -N` and countable without turning the run red
* sio_card_protocol_test runs in a build-tree scratch dir; it writes
test_dir/card1.mcd, and that is a TRACKED fixture
* removed three committed test binaries and widened the .gitignore rule that
missed them (it only covered *.exe, and these were Linux builds)
Disabled, each with a bead: sio_card_protocol_test (147/309),
sio_dualshock_rumble_test (27), spu_fidelity_test (4 ADSR), aot_overlay_discovery
(cwd), and the 3 pre-existing perf guards.
Verified:
* adding an unregistered test file fails the build with an actionable message;
removing it passes
* runtime ctest 32/32 pass, 3 disabled
* recompiler ctest: same 5 failures as a pristine origin/master baseline
* Tomba, Tomba 2 and MMX6 build against this framework and were each driven
240s of attract + randomised pad fuzz (~195 injections, 47+ combos):
59.6 fps sustained, zero stalls, all three reached real gameplay
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Load acceleration moved to the Mods catalog in 125d84d
(psx.enhancement.fast-loading "Fast Loading (host pacing)" and
psx.enhancement.cd-speed, both `game_id = "*"` so they ship with every
title, both default-off), and recomp-ui deliberately stopped drawing a
generic Turbo loads row. But the legacy switch was left live here with
`offer_turbo_loads` defaulting TRUE, so any title that had not explicitly
migrated still got turbo forced on — with no UI to turn it back off.
That shipped to players. MegaManX6Recomp v1.0.4 and v1.0.5 both carry
`[runtime] turbo_loads = true` in the game.toml inside the release zip
(verified by extracting the artifacts). Two paths forced it on:
* game.toml [runtime] turbo_loads set g_turbo_loads_enabled directly;
* a stale [video] turbo_loads in settings.toml was restored, and the
seed write persisted it — so one run latched the value permanently
and no later game.toml change could undo it.
Reported as MegaManX6Recomp#14: the WARNING screen self-advances and load
spinners run fast, with Turbo confirmed off in Mods. Tomba 2 hit the same
defect (fixed per-title), and MMX5 is also unmigrated. Third occurrence of
one config default, so fix the class instead of the titles.
Both keys are still PARSED, so existing configs load without error, but
neither is honoured:
* offer_turbo_loads defaults false and turbo_loads_offered becomes a
constexpr false, matching how ws_offered / skip_fmv_offered /
frame_interpolation_offered already model mod-owned features. No title
can offer the generic switch, so forgetting to migrate is no longer
punished.
* game.toml turbo_loads logs a deprecation line naming the Fast Loading
mod and enables nothing.
* settings.toml [video] turbo_loads is neither restored nor written back
out, so the stale row is dropped on the first save — same write-only
latch reasoning already documented for fast_boot, which bit harder here
because there was no UI control at all.
Also fixes a related hole found while auditing: the in-game Settings apply
path wrote both g_turbo_loads_enabled and g_auto_skip_fmv from a launcher
snapshot taken BEFORE mod_runtime_activate_plugins() ran, so the first
in-game Apply silently turned a player's enabled Fast Loading or Tweaks FMV
feature back off. Both are now gated on their offered flags.
The supported surfaces are unchanged: the Fast Loading / CD Speed mod
plugins still enable acceleration (verified: "mod selected 8x load
acceleration"), and the `turbo_loads` TCP debug command still toggles it
for development.
Verified against MMX6 built on this framework: with game.toml
`turbo_loads = true` AND `offer_turbo_loads = true` AND a stale
settings.toml `turbo_loads = true`, both deprecation lines fire and the
debug server reports enabled=0, turbo_frames=0. With the keys removed and
no mods enabled, it boots silently with enabled=0.
test_mod_load_acceleration.py is updated to pin the new contract; it also
had a stale assertion on the old 2..16 multiplier range, which is now
pinned to the symbolic PSX_MOD_LOAD_ACCEL_MAX bound. The guard-test suite
goes from 9 pre-existing failures to 8 (no new failures; this one fixed).
Refs: beads-eio.3.26, MegaManX6Recomp#14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The mouse-button commit added #include <SDL.h> to psx_keybinds.c, which does
not compile: that translation unit is not given SDL's include directory, and
the codebase reaches SDL through runtime/include/psx_sdl.h (the SDL2/SDL3
shim) rather than the vendor header directly. psx_keybinds.h already includes
the shim, so the raw include is both wrong and unnecessary.
Corrected in-port rather than bounced back, since the rest of the commit is
sound.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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>
083959df added runtime/src/nd_intro_ot.c + nd_intro_ot.h, and 02b08dae /
later commits call psx_nd_note_wood_batch_ot(), psx_nd_wood_batch_ot(),
psx_nd_note_sibling_ot_hint() and psx_nd_sibling_ot_hint() from gte.cpp and
debug_server.c. runtime.cmake keeps an explicit source list (no globbing), so
the TU was never compiled and every link failed with undefined references.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>