diff --git a/docs/SAVESTATE_LOAD_PERFORMANCE.md b/docs/SAVESTATE_LOAD_PERFORMANCE.md deleted file mode 100644 index 50c58def..00000000 --- a/docs/SAVESTATE_LOAD_PERFORMANCE.md +++ /dev/null @@ -1,205 +0,0 @@ -# Savestate load performance — solo and netplay - -Status: **shipped** (MotK / Bomberman runtime + vendored `recomp-net`). -Scope: host-side restore hitch after F-key / protocol load, and lockstep -correctness when both peers restore together. - -This is not the CD “load window” turbo work in [`LOAD_TIME_ZERO.md`](LOAD_TIME_ZERO.md). -That doc is about *in-game* disc loads. This one is about **user savestate -restore** (`.pst` / `boot_state_*`) feeling instant and staying in sync under -delay-netplay. - ---- - -## Symptoms that drove the work - -| Mode | Symptom | Host FPS | Root class | -|------|---------|----------|------------| -| Solo | Picture frozen ~1–several seconds after load; worse on 2nd/3rd load of same slot | ~60 | Giant `psx_advance_cycles` / IRQ blackout / present latch | -| Netplay | Host `applied, waiting for guest…`; guest stuck at `applying after transfer…` then disconnect | N/A | INPUT / admit deadlock across apply ↔ ready | -| Netplay spam | Hang when mashing load while holding directions | N/A | Same tip-starvation class; tip runway emptied by `hard_resync` | - ---- - -## Solo restore path - -Call chain (HLE scheduler required): - -``` -F-key / debug → savestate_request_load - → savestate_poll (block leader) - → boot_state_load - → psx_cycles_resync_after_restore - → interrupts_resync_after_restore - → cdrom_accelerate_after_savestate - → psx_frontend_on_savestate_loaded - → psx_scheduler_resume_at(pc) /* longjmp; does not return */ -``` - -### 1. Anchor host-only cycle deadlines (`psx_cycles_resync_after_restore`) - -**Bug:** `gte_ts_done` / `muldiv_ts_done` and load-absorb fields live on -`CPUState` but are **not** in the savestate wire format. After a warm load, -`psx_cycle_count` rewinds to the snapshot while those deadlines still sit on -the pre-load live timeline. The next GTE / muldiv stall then advances -`(live_ts − restored_cycle)` in one shot — tens of millions of cycles, many -nested presents, `chk(e=0)`, sticky VBlank, no draw. - -**Fix:** After restore, set `gte_ts_done` / `muldiv_ts_done` to the restored -`psx_cycle_count`, clear absorb/fudge state, and re-anchor device sync / -idle-skip latches. - -- `runtime/src/psx_cycles.c` — `psx_cycles_resync_after_restore` -- Called from `savestate_poll` immediately after a successful `boot_state_load` - -### 2. Clear absolute IRQ cooldowns (`interrupts_resync_after_restore`) - -**Bug:** `post_exception_cooldown_until` is an absolute guest-cycle stamp. -Leaving it in the future after a clock rewind blocks every IRQ (including -VBlank) until the restored clock “catches up” — freeze for however long the -user played past the save, while host FPS stays ~60. - -**Fix:** Zero the cooldown and related exception / VBlank phase bookkeeping. - -### 3. Cap CD second-response debt (`cdrom_accelerate_after_savestate`) - -Restored / imminent CD command delays (ReadTOC, Init, seeks) can freeze the -picture for ~1s after the restored frame presents. A short post-load boost -window clamps outstanding delays so the display recovers immediately. - -### 4. Frontend present / audio re-anchor (`psx_frontend_on_savestate_loaded`) - -After restore: - -- Force present (`s_force_present_after_load`) so a disabled-display blank - latch or smooth-60 duplicate does not hide the restored frame. -- Invalidate GL present-dirty early-out (`gl_renderer_invalidate_present`) — - critical on **2nd+ load of the same slot**, where the framebuffer can match - the last swap and skip `SwapWindow`. -- Reset frame pacer + FPS baseline (admit / hitch can leave deadlines in the past). -- Resync guest-cycle→audio sample budgeting. - -### 5. Smaller / faster `.pst` I/O (boot_state v4 zlib) - -Large sections may be zlib-compressed on save (`BS_SEC` pad bit0). Shrinks -disk and helps slow storage; older readers still accept uncompressed v3. - -### 6. No post-load request cooldown - -A former 12-frame (earlier 60-frame) “load ignored” debounce in -`request_load_inner` was removed. It was only key-repeat padding and could -break netplay: protocol path entered `LOAD_APPLYING` while the cooldown -silently refused to stage `s_load_pending`. - -Overlapping loads are gated elsewhere: - -- Solo: single `s_load_pending` slot (last request wins). -- Netplay: `np_xfer_busy()` until the barrier clears. - -### Diagnostics - -`PSX_POST_LOAD_PROBE=1` arms a short post-load window that logs advance size, -IRQ check outcomes, dirty/idle/horizon, and host ms (`main.cpp` + cycle -attribution). Use this when a freeze returns; do **not** pause the runtime to -measure — extend the probe / rings instead. - ---- - -## Netplay load path - -Host-only initiate (`psx_netplay_request_load`). Guest follows via STATE_* on -the same UDP/relay path as inputs. - -### High-level sequence - -``` -host: hash PROBE(op=LOAD, size, crc) -guest: REPLY match? - yes → both stage savestate_request_load_protocol + LOAD_APPLYING - no → host STATE_BEGIN/CHUNK → guest writes sandbox - → both stage load in np_apply_ready_state (transfer complete) -both: admit while savestate_pending (guest cycles → savestate_poll) -both: local LOADED → LOAD_READY -host: ready PROBE(op=LOAD, size=0, NP_LOAD_READY_CRC) -guest: ACK when applied -both: hard_resync + prime_delay_inputs (once) -both: stay in LOAD_READY until try_admit succeeds → resume lockstep -``` - -### Correctness / performance rules (do not regress) - -1. **Stage apply only when both peers have the bytes** - - Do **not** call `savestate_request_load_protocol` on the host at SEND - begin. Host would restore during transfer, enter ready early, and starve - the guest of tips needed for `savestate_poll`. - - Hash-miss: both stage in `np_apply_ready_state` after transfer. - - Hash-hit: both stage when the probe reply is handled. - -2. **Keep INPUT flowing until mutual ready** - - `LOAD_APPLYING` / enter `LOAD_READY`: do **not** set - `input_send_suppress`. - - Suppress only inside `np_commit_load_sync` for the - `hard_resync` → `prime_delay_inputs` window (prime clears suppress). - - App barrier (`psx_netplay_poll_admit`) freezes sim after apply / - during ready; that is separate from INPUT emission. - -3. **Ready probe must not stall INPUT (`recomp-net`)** - - `rnet_session_state_probe` with `LOAD` + `total_size == 0` sets - `state_stall_sim = 0` (same as SAVE coord). - - Previously `state_stall_sim = 1` blocked `send_input_bundle` on the host - as soon as the first peer finished apply — same deadlock as suppress, - worse under spam because `hard_resync` leaves only ~D tip frames of - runway. - - Hash probes (`total_size != 0`) still stall until finish / transfer. - -4. **`hard_resync` + prime once at mutual ready, not at apply** - - Clearing rings / `sim_tick → 0` at apply time lets the later peer wipe - the earlier peer’s tip and stall resume. - - After mutual ready: clear local **and** remote rings, prime neutral - delay prefix, wait for `try_admit` (fresh tip + INPUT_CONFIRM) before - dropping `LOAD_READY`. - -5. **Peer disconnect during barrier** - - `psx_netplay_peer_disconnected(0)` while `psx_netplay_in_load_barrier()` - so rx silence for a multi-second restore does not soft-exit to lobby. - BYE / `peer_gone` still honored. - -6. **Spam loads** - - Host ignores new requests while `np_xfer_busy()`. - - No savestate-layer frame cooldown (see solo §6). - -### Where the code lives - -| Piece | Location | -|-------|----------| -| App xfer / barrier | `runtime/src/psx_netplay.c` | -| Session stall / tip / hard_resync | `lib/recomp-net/src/session/rnet_session.c` | -| Protocol notes | `lib/recomp-net/docs/protocol.md` | -| Staging API | `savestate_request_load_protocol` (bypasses netplay user block) | - ---- - -## Expected log lines (healthy netplay load) - -``` -netplay load slot=N — hash probe (…) -netplay load slot=N — hashes match, applying… # or transferring / applying after transfer -savestate: LOADED slot N … -netplay load slot=N — applied, waiting for guest… # host -netplay guest load slot=N — applied, waiting for host… # or ready acked -netplay load slot=N — mutual ready, waiting lockstep… -netplay load slot=N — peer ready, resuming lockstep -``` - -Stuck on `waiting for guest` / guest never leaving `applying…` → tip -starvation (rules 1–3). Sticky `INPUT desync … stalled` → confirm/hash -disagreement after resume (inspect tip epoch / history collisions). - ---- - -## Related docs - -- [`LOAD_TIME_ZERO.md`](LOAD_TIME_ZERO.md) — in-game CD load wall-time (different problem). -- [`CYCLE_TIMING_ARCH.md`](CYCLE_TIMING_ARCH.md) — cycle / GTE stall model. -- `lib/recomp-net/docs/protocol.md` — STATE_PROBE / post-load ready rendezvous. -- `runtime/include/boot_state.h` — `.pst` section version / zlib flags. diff --git a/docs/internal/README.md b/docs/internal/README.md index 91857f24..5ab8d0de 100644 --- a/docs/internal/README.md +++ b/docs/internal/README.md @@ -40,7 +40,3 @@ behind a specific subsystem. ## Overlay cache internals - [`SLJIT_PERSIST_CACHE.md`](SLJIT_PERSIST_CACHE.md) — persisted overlay-shard cache design. - -## Savestate / netplay -- [`../SAVESTATE_LOAD_PERFORMANCE.md`](../SAVESTATE_LOAD_PERFORMANCE.md) — - solo restore hitch fixes and netplay load barrier / INPUT rules. diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index b11613ba..06f889f5 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -617,21 +617,6 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path& rt.deadzone = static_cast(n); rt.has_deadzone = true; } - if (ct.contains("multitap_port")) { - const auto n = toml::find(ct, "multitap_port"); - if (n != 1 && n != 2) - throw std::runtime_error(fmt::format( - "[controller] multitap_port must be 1 or 2, got {}", n)); - rt.multitap_port = static_cast(n); - rt.has_multitap_port = true; - } - // LEGACY per-game pad-config opt-in (default modern). Tomba sets this so - // its launcher Hybrid mode's analog<->digital type flip doesn't make libpad - // manufacture a disconnect; no other title is affected. Full history and - // the removal plan live in psxrecomp runtime/src/sio.c (g_pad_legacy_cfg). - if (ct.contains("legacy_pad_config")) { - rt.legacy_pad_config = toml::find(ct, "legacy_pad_config"); - } if (ct.contains("anti_deadzone")) { const auto n = toml::find(ct, "anti_deadzone"); if (n < 0 || n > 32767) @@ -2092,56 +2077,39 @@ UserSettings load_user_settings(const fs::path& path) { } if (doc.contains("controller")) { const toml::value& ct = toml::find(doc, "controller"); - static const char* kDevKeys[] = { - "p1_device", "p2_device", "p3_device", "p4_device", "p5_device"}; - static const char* kModeKeys[] = { - "p1_mode", "p2_mode", "p3_mode", "p4_mode", "p5_mode"}; - static const char* kDzKeys[] = { - "p1_deadzone", "p2_deadzone", "p3_deadzone", "p4_deadzone", - "p5_deadzone"}; - static const char* kAnalogKeys[] = { - "p1_analog", "p2_analog", "p3_analog", "p4_analog", "p5_analog"}; - for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) { - if (ct.contains(kDevKeys[i])) try_get([&]{ - const auto v = toml::find(ct, kDevKeys[i]); - if (!v.empty()) { - s.p_device[i] = v; - s.has_p_device[i] = true; - } - }); - // Legacy boolean form first (true->analog, false->digital); the - // string `*_mode` keys override when present. - if (ct.contains(kAnalogKeys[i])) try_get([&]{ - s.p_mode[i] = toml::find(ct, kAnalogKeys[i]) - ? PAD_MODE_ANALOG : PAD_MODE_DIGITAL; - s.has_p_mode[i] = true; - }); - if (ct.contains(kModeKeys[i])) try_get([&]{ - s.p_mode[i] = pad_mode_from_string( - toml::find(ct, kModeKeys[i]), PAD_MODE_HYBRID); - s.has_p_mode[i] = true; - }); - if (ct.contains(kDzKeys[i])) try_get([&]{ - const auto n = toml::find(ct, kDzKeys[i]); - if (n >= 0 && n <= 32767) { - s.p_deadzone[i] = (int)n; - s.has_p_deadzone[i] = true; - } - }); - } + if (ct.contains("p1_device")) try_get([&]{ + const auto v = toml::find(ct, "p1_device"); + if (!v.empty()) { s.p1_device = v; s.has_p1_device = true; } + }); + if (ct.contains("p2_device")) try_get([&]{ + const auto v = toml::find(ct, "p2_device"); + if (!v.empty()) { s.p2_device = v; s.has_p2_device = true; } + }); + // Legacy boolean form first (true->analog, false->digital); the new + // string `*_mode` keys override when present. + if (ct.contains("p1_analog")) try_get([&]{ + s.p1_mode = toml::find(ct, "p1_analog") + ? PAD_MODE_ANALOG : PAD_MODE_DIGITAL; + s.has_p1_mode = true; + }); + if (ct.contains("p2_analog")) try_get([&]{ + s.p2_mode = toml::find(ct, "p2_analog") + ? PAD_MODE_ANALOG : PAD_MODE_DIGITAL; + s.has_p2_mode = true; + }); + if (ct.contains("p1_mode")) try_get([&]{ + s.p1_mode = pad_mode_from_string( + toml::find(ct, "p1_mode"), PAD_MODE_HYBRID); + s.has_p1_mode = true; + }); + if (ct.contains("p2_mode")) try_get([&]{ + s.p2_mode = pad_mode_from_string( + toml::find(ct, "p2_mode"), PAD_MODE_HYBRID); + s.has_p2_mode = true; + }); if (ct.contains("deadzone")) try_get([&]{ const auto n = toml::find(ct, "deadzone"); - if (n >= 0 && n <= 32767) { - s.deadzone = (int)n; - s.has_deadzone = true; - /* Legacy global: fill any slot that was not given pN_deadzone. */ - for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) { - if (!s.has_p_deadzone[i]) { - s.p_deadzone[i] = s.deadzone; - s.has_p_deadzone[i] = true; - } - } - } + if (n >= 0 && n <= 32767) { s.deadzone = (int)n; s.has_deadzone = true; } }); } return s; @@ -2237,36 +2205,19 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) { f << "enable2 = " << (s.memcard2_enabled ? "true" : "false") << "\n"; } - { - bool any_ctrl = s.has_deadzone; - for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) { - if (s.has_p_device[i] || s.has_p_mode[i] || s.has_p_deadzone[i]) - any_ctrl = true; - } - if (any_ctrl) { - static const char* kDevKeys[] = { - "p1_device", "p2_device", "p3_device", "p4_device", "p5_device"}; - static const char* kModeKeys[] = { - "p1_mode", "p2_mode", "p3_mode", "p4_mode", "p5_mode"}; - static const char* kDzKeys[] = { - "p1_deadzone", "p2_deadzone", "p3_deadzone", "p4_deadzone", - "p5_deadzone"}; - f << "\n[controller]\n"; - for (int i = 0; i < UserSettings::kMaxControllerPlayers; ++i) { - if (s.has_p_device[i]) - f << kDevKeys[i] << " = \"" << s.p_device[i] << "\"\n"; - if (s.has_p_mode[i]) - f << kModeKeys[i] << " = \"" - << pad_mode_to_string(s.p_mode[i]) << "\"\n"; - if (s.has_p_deadzone[i]) - f << kDzKeys[i] << " = " << s.p_deadzone[i] << "\n"; - } - /* Keep a global deadzone= for older readers (mirrors P1). */ - if (s.has_deadzone || s.has_p_deadzone[0]) - f << "deadzone = " - << (s.has_p_deadzone[0] ? s.p_deadzone[0] : s.deadzone) - << "\n"; - } + if (s.has_p1_device || s.has_p2_device || s.has_p1_mode || s.has_p2_mode || + s.has_deadzone) { + f << "\n[controller]\n"; + if (s.has_p1_device) + f << "p1_device = \"" << s.p1_device << "\"\n"; + if (s.has_p1_mode) + f << "p1_mode = \"" << pad_mode_to_string(s.p1_mode) << "\"\n"; + if (s.has_p2_device) + f << "p2_device = \"" << s.p2_device << "\"\n"; + if (s.has_p2_mode) + f << "p2_mode = \"" << pad_mode_to_string(s.p2_mode) << "\"\n"; + if (s.has_deadzone) + f << "deadzone = " << s.deadzone << "\n"; } if (s.has_language) { diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 0549d4e1..2ac9b96b 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -454,22 +454,6 @@ struct RuntimeConfig { bool has_deadzone = false; int deadzone = 0; - // multitap_port: console port that hosts the SCPH-1070 when offline/netplay - // arms multitap (players/slot_count >= 3). 1 = Port 1 (default, most games), - // 2 = Port 2 (Bomberman Party Edition, Jigsaw Madness, S.C.A.R.S., …). - bool has_multitap_port = false; - int multitap_port = 1; - - // legacy_pad_config: per-game pad-protocol compatibility opt-in. false (default) - // = the modern DualShock config state machine (proper 0x43 enter/exit, config id - // 0xF3 only while in config) — required by MMX6 and the correct default for every - // title. true = the pre-98aa688 behaviour (config commands always answer 0xF3, no - // enter/exit tracking). Only Tomba opts in: its libpad re-detect — triggered by the - // launcher Hybrid mode's analog<->digital type flip — manufactures a 1-frame "pad - // unplugged" under the modern SM (menu unpause / phantom input). The legacy answers - // make that re-detect benign. Scoped per-game; no other title's behaviour changes. - // Wired to sio_set_legacy_cfg(); see sio.c g_pad_legacy_cfg. - bool legacy_pad_config = false; // anti_deadzone: minimum radial analog output after leaving deadzone, in // raw SDL axis units (0..32767). This is a game-owned response setting used // to compensate a title's own internal stick deadzone. Absent => 0. @@ -1007,29 +991,22 @@ struct UserSettings { bool has_memcard1_enabled = false; bool memcard1_enabled = true; bool has_memcard2_enabled = false; bool memcard2_enabled = true; - // [controller] — per-player input device + pad type + deadzone. - // device is one of: + // [controller] — per-player input device + pad type. device is one of: // "none" — no pad in this port (port not connected) // "keyboard" — driven by the keyboard map (input.ini) // "" — an SDL game-controller GUID (SDL_JoystickGetGUIDString) - // Modes (see PadMode): hybrid / analog / digital. Defaults: P1 keyboard, - // P2–P5 none. Deadzone default is 10% (3277/32767). TOML keys: - // pN_device / pN_mode / pN_deadzone (N=1..5). Legacy bare `deadzone` - // still fills any slot that lacks pN_deadzone. - static constexpr int kMaxControllerPlayers = 5; - bool has_p_device[kMaxControllerPlayers] = {}; - std::string p_device[kMaxControllerPlayers] = { - "keyboard", "none", "none", "none", "none"}; - bool has_p_mode[kMaxControllerPlayers] = {}; - int p_mode[kMaxControllerPlayers] = { - PAD_MODE_HYBRID, PAD_MODE_HYBRID, PAD_MODE_HYBRID, - PAD_MODE_HYBRID, PAD_MODE_HYBRID}; - bool has_p_deadzone[kMaxControllerPlayers] = {}; - int p_deadzone[kMaxControllerPlayers] = { - 3277, 3277, 3277, 3277, 3277}; /* ~10% of 32767 */ - // Legacy global deadzone (settings.toml `deadzone=`). Applied to slots - // that do not have an explicit pN_deadzone. Default 10%. - bool has_deadzone = false; int deadzone = 3277; + // p1_mode/p2_mode select the emulated pad behaviour (see PadMode): + // hybrid (default) / analog / digital. Defaults: P1 keyboard, P2 none. + bool has_p1_device = false; std::string p1_device = "keyboard"; + bool has_p2_device = false; std::string p2_device = "none"; + // Pad input mode per player (see PadMode): hybrid (default) / analog / + // digital. Persisted as p1_mode/p2_mode strings. Legacy p1_analog/p2_analog + // booleans are still read for back-compat (true->analog, false->digital). + bool has_p1_mode = false; int p1_mode = PAD_MODE_HYBRID; + bool has_p2_mode = false; int p2_mode = PAD_MODE_HYBRID; + // Analog-stick deadzone, raw SDL axis units (0..32767). The launcher edits + // this as 0-100% (raw = pct*32767/100), mirroring snesrecomp's GamepadDeadzone. + bool has_deadzone = false; int deadzone = 12000; // Localization: the launcher's chosen language code (feeds RuntimeConfig // .language / g_lang). "off"/"jp"/"" = untranslated native game. Persisted to // settings.toml [localization].language. diff --git a/recompiler/tests/test_cli_project_packaging.py b/recompiler/tests/test_cli_project_packaging.py index bbc16857..518dd6e3 100644 --- a/recompiler/tests/test_cli_project_packaging.py +++ b/recompiler/tests/test_cli_project_packaging.py @@ -17,14 +17,6 @@ def main() -> int: cli = (ROOT / "recompiler/src/main_cli.cpp").read_text(encoding="utf-8") bios = (ROOT / "recompiler/src/main_bios.cpp").read_text(encoding="utf-8") package = (ROOT / "tools/build_cli.py").read_text(encoding="utf-8") - launcher = (ROOT / "recomp-ui/src/recomp_launcher.h").read_text( - encoding="utf-8") - launcher_cmake = (ROOT / "recomp-ui/recomp_ui.cmake").read_text( - encoding="utf-8") - boot_timing_header = ( - ROOT / "recomp-ui/src/common/launcher_boot_timing.h") - boot_timing_source = ( - ROOT / "recomp-ui/src/common/launcher_boot_timing.c") require(cli, 'bios_config = \\"psxrecomp/bios/{}\\"', "generated game.toml does not name its BIOS profile") @@ -38,6 +30,9 @@ def main() -> int: "BIOS sources are not emitted where runtime.cmake discovers them") require(cli, 'set(PSXRECOMP_BIOS_STEMS \\"{}\\"', "generated CMake does not select the emitted BIOS backend") + require(cli, 'set(PSX_RECOMP_UI OFF CACHE BOOL \\"\\" FORCE)', + "self-contained CLI projects must not require a game-owned " + "recomp-ui checkout") copy_pos = cli.find('fmt::print("[1/4] Copying build framework') game_pos = cli.find('fmt::print("[2/4] Recompiling game executable') @@ -61,20 +56,8 @@ def main() -> int: "psxrecomp-bios config mode lacks the ROM override") require(bios, 'a == "--out-dir"', "psxrecomp-bios config mode lacks the output override") - require(launcher, "RecompLauncherCModProvider", - "pinned recomp-ui predates the runtime Mods provider API") - require(launcher, "const RecompLauncherCModProvider* mods;", - "pinned recomp-ui GameInfo lacks the Mods provider field") - if not boot_timing_header.is_file(): - raise AssertionError( - "pinned recomp-ui lacks launcher_boot_timing.h required by main.cpp") - if not boot_timing_source.is_file(): - raise AssertionError( - "pinned recomp-ui lacks the launcher boot timing implementation") - require(launcher_cmake, "common/launcher_boot_timing.c", - "pinned recomp-ui does not compile launcher boot timing") - print("PASS: CLI profiles, stage order, BIOS overrides, and launcher API agree") + print("PASS: CLI profiles, stage order, BIOS overrides, and UI isolation agree") return 0 diff --git a/runtime/include/boot_state.h b/runtime/include/boot_state.h index 5b66bdbd..fae60d57 100644 --- a/runtime/include/boot_state.h +++ b/runtime/include/boot_state.h @@ -1,7 +1,6 @@ #ifndef PSX_BOOT_STATE_H #define PSX_BOOT_STATE_H -#include #include #include "cpu_state.h" @@ -37,13 +36,8 @@ extern "C" { #define BOOT_STATE_MAGIC 0x50535842u /* "PSXB" */ /* v1 = incomplete RAM-only; v2 = full machine but host-struct memcpy (padding); - * v3 = little-endian field wire (portable Win/Linux/macOS ARM); - * v4 = v3 + optional zlib on large sections (section pad bit0 = compressed). */ -#define BOOT_STATE_VERSION 4u -/* Older readers reject v4; load still accepts v3 uncompressed blobs. */ -#define BOOT_STATE_VERSION_MIN_READ 3u -/* Section pad bit0: payload is u32 LE uncompressed_len + zlib deflate bytes. */ -#define BOOT_STATE_SEC_ZLIB 1u + * v3 = little-endian field wire (portable Win/Linux/macOS ARM). */ +#define BOOT_STATE_VERSION 3u /* * On-disk header (v3): nine little-endian uint32 fields at offset 0 (36 bytes), @@ -67,12 +61,11 @@ typedef struct { #define BOOT_STATE_HEADER_WIRE_BYTES 36u /* - * Section stream (v3/v4): section_count records, each laid out as + * Section stream (v3): section_count records, each laid out as * uint32_t tag; LE (one of BS_SEC_*) - * uint32_t pad; LE flags (v3: 0; v4: BOOT_STATE_SEC_ZLIB optional) + * uint32_t pad; LE 0 * uint64_t len; LE payload byte count * uint8_t payload[len]; (module payloads are LE field wires too) - * When BOOT_STATE_SEC_ZLIB is set, payload = u32 LE raw_len + zlib(raw). * An unknown tag, a length mismatch, or a missing required section on load is a * hard reject (incomplete restore is never allowed) -> normal boot + recapture. */ @@ -91,7 +84,6 @@ enum { BS_SEC_DMA = 0x0C, /* DMA channels[7] + dpcr/dicr + async-transfer state */ BS_SEC_SIO = 0x0D, /* SIO regs + pad-config FSM + memcard FSM */ BS_SEC_DIRTY = 0x0E, /* dirty-RAM page bitmap (guest-written code pages) */ - BS_SEC_MDEC = 0x0F, /* MDEC command/FIFOs/quant/scale (FMV decode resume) */ }; /* Save a COMPLETE snapshot at game handoff. Returns 1 on success. */ @@ -103,17 +95,6 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, int boot_state_load(const char* path, uint32_t bios_checksum, uint32_t entry_pc, CPUState* cpu); -/* Same as boot_state_load, but from an already-buffered .pst image (netplay). */ -int boot_state_load_buffer(const uint8_t* file, size_t file_len, - uint32_t bios_checksum, uint32_t entry_pc, - CPUState* cpu); - -/* Header-only integrity check (no section inflate/apply). Returns 1 if this - * build can load the image; 0 and fills reason (when non-NULL) on reject. */ -int boot_state_check_buffer(const uint8_t* file, size_t file_len, - uint32_t bios_checksum, uint32_t entry_pc, - char* reason, size_t reason_cap); - /* Register a deferred capture: when boot_state_trigger_capture() fires (from * fntrace at game-start), serialize to path. One-shot. */ void boot_state_set_capture(const char* path, uint32_t bios_checksum, diff --git a/runtime/include/cdrom.h b/runtime/include/cdrom.h index 8a687489..7ef03f47 100644 --- a/runtime/include/cdrom.h +++ b/runtime/include/cdrom.h @@ -79,18 +79,6 @@ int cdrom_load_in_progress(void); * bridge used by cdrom_load_in_progress(). Diagnostics only. */ int cdrom_data_read_active(void); -/* After savestate restore: clamp long CD second-response / read-start delays - * and arm a short boost window so post-load ReadTOC/seek/Init waits do not - * freeze the picture for ~1s+. Completions still fire (IRQs preserved). */ -void cdrom_accelerate_after_savestate(void); -/* Call once per host vblank while the boost window is armed. */ -void cdrom_savestate_boost_vblank(void); -/* Non-zero while boost is armed AND a CD wait is outstanding (pending - * second response or non-XA read). Lets turbo_loads unpace those waits. */ -int cdrom_savestate_cd_wait_active(void); -/* Remaining boost vblanks (0 when idle). Diagnostics / post-load probe. */ -int cdrom_savestate_boost_vblanks_remaining(void); - /* MMIO read/write (0x1F801800-0x1F801803) */ uint32_t cdrom_read(uint32_t addr); void cdrom_write(uint32_t addr, uint32_t value); diff --git a/runtime/include/dirty_ram_interp.h b/runtime/include/dirty_ram_interp.h index 6f1cc6cc..fbdc9b30 100644 --- a/runtime/include/dirty_ram_interp.h +++ b/runtime/include/dirty_ram_interp.h @@ -117,20 +117,10 @@ static inline int overlay_cache_window_contains(uint32_t phys) { uint32_t dirty_ram_get_bitmap(void); uint32_t dirty_ram_get_bitmap_word(uint32_t word_index); uint32_t dirty_ram_get_bitmap_word_count(void); -void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count); -/* After bulk RAM restore (savestate): bump overlay page gens + lazy-miss epoch - * so native overlays re-hash against restored bytes. */ -void overlay_watch_invalidate_after_ram_restore(void); void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len); void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes, uint32_t len); int dirty_ram_text_native_ok(uint32_t phys); -/* Exact CFG ranges; exec_pc clips ranges that end before the resume PC. */ -int dirty_ram_text_native_ok_ranges_from(const uint32_t *lo_len_pairs, - uint32_t count, - uint32_t exec_pc); -int dirty_ram_text_native_ok_ranges(const uint32_t *lo_len_pairs, - uint32_t count); int dirty_ram_text_image_registered(void); /* Bless an intentional runtime data patch (e.g. text_xlate string/glyph tables) * into the text reference image so it is not mistaken for self-modifying code. */ diff --git a/runtime/include/gpu.h b/runtime/include/gpu.h index 811b220e..a1f720c0 100644 --- a/runtime/include/gpu.h +++ b/runtime/include/gpu.h @@ -47,18 +47,14 @@ void gpu_display_pixel_rgb(const GpuDisplayInfo* di, uint32_t x, uint32_t y, uint8_t* r, uint8_t* g, uint8_t* b); uint32_t gpu_display_pixel_argb(const GpuDisplayInfo* di, uint32_t x, uint32_t y); /* Depth24: RGB columns covered by CPU→VRAM uploads since the last reset. - * Returns 0 when no uploads yet, crtc_w when coverage is full/unknown-beyond, - * else the covered RGB width. Present black-fills [limit, crtc_w) without - * shrinking the CRTC-derived draw width (avoids a flickering black pillar). */ + * Returns crtc_w when unknown / full coverage. Present uses this to blank a + * trailing margin without shrinking the CRTC-derived width globally. */ uint32_t gpu_depth24_rgb_limit(uint32_t display_x, uint32_t crtc_w); void gpu_depth24_upload_span_reset(void); /* MotK intro cuts retarget GP1(07h). While hold > 0, present should skip * Swap (keep prior frame) so stale trailing VRAM never flashes. One tick * per vblank; returns non-zero while the hold is still active after tick. */ int gpu_depth24_present_hold_tick(void); -/* After savestate restore: clear ephemeral hold so the restored depth24 - * frame can present immediately (span/prev_h come from the GPU snap). */ -void gpu_depth24_on_savestate_loaded(void); /* GP1(06h)/GP1(07h)/GP1(08h) fields for debug (gpu_state). */ void gpu_get_crtc_debug(uint32_t *x1, uint32_t *x2, uint32_t *y1, uint32_t *y2, uint32_t *hres1_out, uint32_t *hres2_out); diff --git a/runtime/include/gpu_gl_renderer.h b/runtime/include/gpu_gl_renderer.h index a12b653f..895bd537 100644 --- a/runtime/include/gpu_gl_renderer.h +++ b/runtime/include/gpu_gl_renderer.h @@ -65,21 +65,6 @@ void gl_renderer_flush_cpu_uploads(void); * reloaded identical frame still reaches the window (double/triple buffer). */ void gl_renderer_invalidate_present(void); -/* After savestate restore: push the CPU VRAM mirror into the GL FBO. Needed - * when the load happened while GP1 depth24 was on — the normal path skips - * framebuffer-sized uploads, which also skipped the full-VRAM boot_state - * blit and left post-FMV menus without texture pages. */ -void gl_renderer_restage_vram_after_savestate(void); - -/* Post-savestate freeze probe: skip/swap/dirty-mark counters (GL present path). - * take() returns deltas since the previous take/reset. Safe no-ops when GL is - * inactive. rect_dirty tests the current present-tile dirty bits. */ -void gl_renderer_present_probe_reset(void); -void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, - uint64_t *dirty_mark_delta, - int *force_remaining); -int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h); - /* THE present path for 15-bit frames: blit the display region straight from * the authoritative VRAM FBO into a letterboxed rect (no readback). * Deterministic — used for every 15-bit frame. linear = filter on scale. diff --git a/runtime/include/gpu_vk_renderer.h b/runtime/include/gpu_vk_renderer.h index 47820fa8..fa12405e 100644 --- a/runtime/include/gpu_vk_renderer.h +++ b/runtime/include/gpu_vk_renderer.h @@ -45,9 +45,6 @@ void vk_renderer_present_blank(void); * server, 24-bit present). No-op when the Vulkan path is inactive. */ void vk_renderer_sync_cpu(void); -/* After savestate restore: push CPU VRAM into the Vulkan image (see GL twin). */ -void vk_renderer_restage_vram_after_savestate(void); - /* Set the present policy: 1=tear-free, 0=IMMEDIATE (lowest latency, may tear), * -1=MAILBOX. Tear-free prefers MAILBOX because the frontend already paces * frames; unsupported modes fall back to FIFO (always available). */ diff --git a/runtime/include/interrupts.h b/runtime/include/interrupts.h index 31ae9177..362664c2 100644 --- a/runtime/include/interrupts.h +++ b/runtime/include/interrupts.h @@ -24,12 +24,6 @@ struct CPUState; void interrupts_init(void); -/* Save-state restore: re-anchor host-only IRQ pacing after psx_cycle_count is - * overwritten from a snapshot. Clears absolute-cycle cooldowns / deferred - * switches that would otherwise strand delivery until the restored clock - * catches up to the pre-load host timeline (warm-reload picture freeze). */ -void interrupts_resync_after_restore(void); - /* Central IRQ-raise choke point: sets the I_STAT bit AND records the raise edge * into the always-on device-event ring (device_trace) with the guest cycle. * Every hardware source (VBLANK/GPU/CDROM/DMA/timers/SIO/SPU) funnels its raise @@ -49,17 +43,6 @@ int psx_interrupt_delivery_needed(const struct CPUState* cpu); void psx_interrupt_delivery_diag(uint64_t *need_defer, uint64_t *need_irq, uint64_t *skip_none, uint64_t *skip_sr, uint64_t *skip_cooldown, uint64_t *skip_nested); - -/* Hot-path counters for psx_check_interrupts (monotonic). Any out_* may be NULL. - * entry = every call - * fast_sr = early return: pending I_STAT but IEc/IM2 clear - * fast_none = early return: nothing pending - * mid = total_checks (past fast paths; toward / into deliver eval) - * eval = reached irq_deliver_eval label - * irq_deliv = g_irq_deliver_count (any HW IRQ exception taken) */ -void psx_interrupt_check_path_diag(uint64_t *entry, uint64_t *fast_sr, - uint64_t *fast_none, uint64_t *mid, - uint64_t *eval, uint64_t *irq_deliv); /* Interrupt check with the compiled guest PC to resume if a game-installed * handler later RFEs to the sentinel outside the synchronous host window. */ void psx_check_interrupts_at(struct CPUState* cpu, uint32_t resume_pc); @@ -87,11 +70,6 @@ uint32_t cycles_to_next_event(void); /* Query whether we are currently inside an exception handler dispatch. */ int psx_get_in_exception(void); -/* Most recent block-leader IRQ-check guest PC / compiled resume latch. - * Used by the post-savestate freeze probe (vblank-time "where was the game"). */ -uint32_t psx_last_irq_check_pc(void); -uint32_t psx_compiled_irq_resume_pc(void); - /* Snapshot internal counters for the freeze_check diagnostic. Any out_* * pointer may be NULL. All counters are monotonically non-decreasing * (dispatch_count resets each VBlank). */ diff --git a/runtime/include/load_accel.h b/runtime/include/load_accel.h index 3610899b..526c5225 100644 --- a/runtime/include/load_accel.h +++ b/runtime/include/load_accel.h @@ -31,8 +31,6 @@ void psx_vsync_query_hle_set_horizon_any(int on); void psx_vsync_query_hle_add_event_horizon_site(uint32_t return_pc); void psx_vsync_query_hle_add_extra_event_horizon_site(uint32_t return_pc); void psx_vsync_query_hle_stats_json(char* buf, int cap); -/* Cumulative VSync(-1) event-horizon skips (for post-load probe deltas). */ -void psx_vsync_query_hle_horizon_totals(uint64_t *hits, uint64_t *cycles); #ifdef __cplusplus } diff --git a/runtime/include/mdec.h b/runtime/include/mdec.h index edc83fbb..0db6e52f 100644 --- a/runtime/include/mdec.h +++ b/runtime/include/mdec.h @@ -79,11 +79,6 @@ void mdec_debug_dma_in_end(uint32_t addr, uint32_t words); void mdec_debug_dma_out_start(uint32_t addr, uint32_t words); void mdec_debug_dma_out_end(uint32_t addr, uint32_t words); -/* boot_state / savestate: full MDEC FIFO + tables (required for FMV resume). */ -uint32_t mdec_snapshot_bytes(void); -void mdec_snapshot_write(uint8_t *p); -int mdec_snapshot_read(const uint8_t *p, uint32_t len); - #ifdef __cplusplus } #endif diff --git a/runtime/include/mod_runtime.h b/runtime/include/mod_runtime.h index a4f5a66a..b62056ea 100644 --- a/runtime/include/mod_runtime.h +++ b/runtime/include/mod_runtime.h @@ -18,9 +18,6 @@ bool mod_runtime_initialize(const std::filesystem::path& root, std::string* error = nullptr); bool mod_runtime_commit(const std::filesystem::path& disc_path = {}, std::string* error = nullptr); -/* Drop the in-session mod plan for a netplay launch without rewriting the - * user's persisted offline selection on disk. */ -bool mod_runtime_clear_for_netplay(std::string* error = nullptr); const std::string& mod_runtime_fingerprint(); const std::filesystem::path& mod_runtime_effective_disc_path(); diff --git a/runtime/include/overlay_loader.h b/runtime/include/overlay_loader.h index 99f8eeb2..1b0b7be5 100644 --- a/runtime/include/overlay_loader.h +++ b/runtime/include/overlay_loader.h @@ -121,12 +121,6 @@ uint64_t overlay_loader_candidate_overflow(void); uint64_t overlay_loader_pair_aliases(void); int overlay_loader_dump_lazy_at(uint32_t addr, char *out, int cap); -/* Overlay CI wrapper early-return attribution (PSX_POST_LOAD_PROBE). */ -void overlay_loader_get_ci_skip_diag(uint64_t *unit, uint64_t *supp, - uint64_t *none, uint64_t *sr, - uint64_t *deliv, uint64_t *enter); -int overlay_loader_call_unit_depth(void); - #ifdef __cplusplus } #endif diff --git a/runtime/include/pst_wire.h b/runtime/include/pst_wire.h index 0eb16531..8f1f840d 100644 --- a/runtime/include/pst_wire.h +++ b/runtime/include/pst_wire.h @@ -131,11 +131,6 @@ static inline int pst_w_pod(PstW *w, const void *src, size_t nbytes, size_t elem const uint8_t *s = (const uint8_t *)src; if (!elem || (nbytes % elem) != 0) return 0; if (elem == 1) return pst_w_bytes(w, src, nbytes); -#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) - /* Host LE: wire bytes match native POD layout — skip per-element swizzle. */ - if (elem == 2 || elem == 4 || elem == 8) - return pst_w_bytes(w, src, nbytes); -#endif for (size_t off = 0; off < nbytes; off += elem) { if (elem == 2) { uint16_t v; @@ -160,10 +155,6 @@ static inline int pst_r_pod(PstR *r, void *dst, size_t nbytes, size_t elem) { uint8_t *d = (uint8_t *)dst; if (!elem || (nbytes % elem) != 0) return 0; if (elem == 1) return pst_r_bytes(r, dst, nbytes); -#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) - if (elem == 2 || elem == 4 || elem == 8) - return pst_r_bytes(r, dst, nbytes); -#endif for (size_t off = 0; off < nbytes; off += elem) { if (elem == 2) { uint16_t v; diff --git a/runtime/include/psx_cycles.h b/runtime/include/psx_cycles.h index 529c5583..d7926aa7 100644 --- a/runtime/include/psx_cycles.h +++ b/runtime/include/psx_cycles.h @@ -147,17 +147,9 @@ extern uint64_t g_idle_skip_cycles; extern uint32_t g_idle_skip_last_pc; extern uint32_t g_idle_skip_last_quantum; -/* Post-load probe cycle diagnostics (optional; main.cpp soft-load tooling). */ -extern int g_plp_cycle_diag; -extern uint64_t g_plp_adv_calls; -extern uint32_t g_plp_adv_max_chunk; -extern uint64_t g_plp_adv_sum; -extern uint64_t g_plp_svc_calls; - /* Save-state restore: re-anchor the deadline device model after psx_cycle_count - * is overwritten from a snapshot. Pass the live CPU so GTE/muldiv completion - * stamps and load-absorb give-back are rewound with the guest clock. */ -void psx_cycles_resync_after_restore(struct CPUState *cpu); + * is overwritten from a snapshot. */ +void psx_cycles_resync_after_restore(void); /* Soft rematch / session_reboot: zero the guest clock and deadline bookkeeping. * Soft-exit longjmps out of vblank (inside psx_devices_service_to_now) leave diff --git a/runtime/include/psx_keybinds.h b/runtime/include/psx_keybinds.h index 4f9d6884..c8db64ae 100644 --- a/runtime/include/psx_keybinds.h +++ b/runtime/include/psx_keybinds.h @@ -19,18 +19,15 @@ extern "C" { * This is the KEYBOARD map only. Game-controller (gamepad) button mapping is a * separate concern handled by input.ini (see main.cpp load_input_config). * - * Up to PSXKB_MAX_PLAYERS keyboard players (P1..P5). Every player ships with the - * same default keyboard map (the framework's historical P1 layout); "Reset to - * Defaults" restores that map for any slot. Simultaneous multi-keyboard play - * still needs distinct binds per slot — only one physical keyboard. + * Two players. Player 1 defaults reproduce the framework's historical hardcoded + * keyboard mapping (so out-of-the-box behaviour is unchanged); Player 2 is + * unbound by default (fill it in to drive a second keyboard player). * * The launcher's Controls page edits these live through the rebind API below * (get/set/reset/save) and persists to the same keybinds.ini; the runtime * re-reads the file at startup via psx_keybinds_init. */ -#define PSXKB_MAX_PLAYERS 5 - /* Button indices — stable order, matches the order keybinds.ini writes and the * order the launcher's rebind chips are laid out. Keep in sync with kButtons[] * in psx_keybinds.c. */ @@ -56,7 +53,8 @@ typedef struct { } PsxPlayerBinds; typedef struct { - PsxPlayerBinds player[PSXKB_MAX_PLAYERS]; /* [0]=P1 .. [4]=P5 */ + PsxPlayerBinds p1; + PsxPlayerBinds p2; } PsxKeyBinds; /* Initialize from /keybinds.ini. Generates a default file if one does @@ -67,7 +65,7 @@ void psx_keybinds_init(const char *exe_path); /* Read-only view of the current bindings. */ const PsxKeyBinds *psx_keybinds_get(void); -/* ── Runtime read helpers (keyboard -> PSX pad), player is 1..PSXKB_MAX_PLAYERS */ +/* ── Runtime read helpers (keyboard -> PSX pad), player is 1 or 2 ──────────── */ /* Build the 16-bit ACTIVE-LOW PSX button word for `player` from the SDL * keyboard state (bits per the standard PSX pad word; unbound inputs never @@ -87,13 +85,13 @@ int psx_keybinds_dpad_active(const uint8_t *keys, int player); /* ── Rebind API (launcher Controls page) ──────────────────────────────────── */ /* Buttons are indexed 0..PSX_KB_COUNT-1 (see PsxKeybindButton). Scancodes, not - * keycodes (keybinds.ini stores SDL scancode names). Players are 1-based. */ + * keycodes (keybinds.ini stores SDL scancode names). */ int psx_keybinds_button_count(void); const char *psx_keybinds_button_name(int button); /* "up".."rs_right" */ const char *psx_keybinds_button_label(int button); /* pretty, e.g. "Cross" */ SDL_Scancode psx_keybinds_get_button(int player, int button); void psx_keybinds_set_button(int player, int button, SDL_Scancode sc); -/* Reset one player's bindings to the built-in defaults (same map for every slot). */ +/* Reset one player's bindings to the built-in defaults (P2 = all unbound). */ void psx_keybinds_reset_player(int player); /* Persist the current bindings to keybinds.ini (path resolved by * psx_keybinds_init; call that first). */ diff --git a/runtime/include/psx_lobby_client.h b/runtime/include/psx_lobby_client.h index b98a98e8..1b7bfd7f 100644 --- a/runtime/include/psx_lobby_client.h +++ b/runtime/include/psx_lobby_client.h @@ -1,7 +1,6 @@ #ifndef PSX_LOBBY_CLIENT_H #define PSX_LOBBY_CLIENT_H -#include #include #ifdef __cplusplus @@ -13,8 +12,7 @@ extern "C" { #define PSX_LOBBY_VERSION_LEN 32 #define PSX_LOBBY_ENDPOINT_LEN 64 #define PSX_LOBBY_MAX_LIST 32 -#define PSX_LOBBY_MAX_MEMBERS 8 -#define PSX_LOBBY_MAX_LAN_EPS 4 +#define PSX_LOBBY_MAX_MEMBERS 4 #define PSX_LOBBY_LANG_LEN 16 #ifndef PSX_GAME_VERSION @@ -29,13 +27,6 @@ typedef struct PsxLobbyRow { int player_count; int max_slots; int has_password; - /* Host UDP endpoint from the server list (for one-shot latency probes). */ - char host_endpoint[PSX_LOBBY_ENDPOINT_LEN]; - /* Legacy hub lan_endpoints (compat). Prefer local UDP beacon by lobby_id. */ - char lan_endpoints[PSX_LOBBY_MAX_LAN_EPS][PSX_LOBBY_ENDPOINT_LEN]; - int lan_count; - /* Round-trip ms to a reachable candidate; -1 unknown / timed out. */ - int latency_ms; } PsxLobbyRow; typedef struct PsxLobbyMember { @@ -58,8 +49,6 @@ typedef struct PsxLobbyMatchCaps { int fast_boot; /* 0/1 */ int auto_skip_fmv; /* 0/1 */ int input_delay; /* recomp-net delay frames */ - int force_input_relay; /* 0/1 — server input relay (vs P2P) */ - int force_turn; /* 0/1 — ICE relay-only (Force TURN for UDP) */ char language[PSX_LOBBY_LANG_LEN]; } PsxLobbyMatchCaps; @@ -101,9 +90,6 @@ void psx_lobby_pump(void); void psx_lobby_set_game_identity(const char *game_name, const char *game_version); const char *psx_lobby_game_version(void); -/* Default max_slots for create (clamped 2..8, default 2). */ -void psx_lobby_set_max_slots(int max_slots); - void psx_lobby_request_list(void); int psx_lobby_list_count(void); int psx_lobby_list_get(int index, PsxLobbyRow *out); @@ -125,16 +111,8 @@ int psx_lobby_join(const char *lobby_id, const char *password, int psx_lobby_leave(void); -/* Host-only: remove the player in `slot` (not the host / self). */ -int psx_lobby_kick(int slot); - -/* Host-only: swap/move a seated player between slots (server broadcasts update). */ -int psx_lobby_move_member(int from_slot, int to_slot); - int psx_lobby_in_lobby(void); int psx_lobby_is_host(void); -/* Host's player_id from the last create/lobby_update (empty if unknown). */ -const char *psx_lobby_host_player_id(void); /* Filled after create/join/lobby_update; peer endpoints for PsxNetplayConfig. */ const PsxLobbyJoinInfo *psx_lobby_join_info(void); @@ -148,52 +126,6 @@ int psx_lobby_set_match_caps(const PsxLobbyMatchCaps *caps); int psx_lobby_member_count(void); int psx_lobby_member_get(int index, PsxLobbyMember *out); -/* Waiting-room peer RTT in ms for `slot`, or -1 if unknown. - * Host's own seat is always -1. Measured over UDP (rnet_rtt_probe) on the - * advertised game endpoints — not the lobby WebSocket. Guests also REPORT - * so the host UI updates when it cannot dial the guest yet. */ -int psx_lobby_member_latency_ms(int slot); - -/* True when member.player_id matches psx_lobby_host_player_id(). - * Prefer this over `slot == 0` — seats can move. */ -int psx_lobby_member_is_host(const PsxLobbyMember *member); - -/* - * ICE signaling relay (MotK WS op:signal). text is SDP/candidate (max 2047). - * send returns 0 if queued/written; poll returns 1 when an inbound signal was - * copied out (LOCAL_* types as emitted by the peer — remap to REMOTE_* before - * rnet_session_push_signal). - */ -int psx_lobby_send_signal(int type, int flag, const char *text); -int psx_lobby_poll_signal(int *type, int *flag, char *text, size_t text_cap); -/* Drop queued ICE SDP/candidates (soft-return / rematch hygiene). */ -void psx_lobby_clear_signals(void); -/* When 0, inbound ICE op:signal is discarded (lobby / post-match). Launch - * re-enables so early peer offers are kept until netplay drains them. */ -void psx_lobby_set_ice_signal_accept(int accept); - -/* - * Coturn / ICE credentials minted by the WS lobby - * (`get_turn_credentials` → `turn_credentials`). Valid until disconnect or TTL. - * Strings are stable until the next successful mint or disconnect — safe to - * pass into RNetIceConfig for juice_create. - */ -typedef struct PsxLobbyTurnCredentials { - int valid; /* 1 when ok mint cached and not expired */ - char stun_host[128]; - int stun_port; - char turn_host[128]; - int turn_port; - char username[192]; - char password[128]; - uint32_t ttl_secs; -} PsxLobbyTurnCredentials; - -/* Queue WS get_turn_credentials. Returns 0 if sent/queued. */ -int psx_lobby_request_turn_credentials(void); -/* Non-NULL; valid==0 when unavailable / expired / STUN-only. */ -const PsxLobbyTurnCredentials *psx_lobby_turn_credentials(void); - /* Local ready flag (from last lobby_update matching our player_id). */ int psx_lobby_local_ready(void); /* True when every seated player is ready and player_count >= 2. */ diff --git a/runtime/include/psx_netplay.h b/runtime/include/psx_netplay.h index c9632eea..8f8a0344 100644 --- a/runtime/include/psx_netplay.h +++ b/runtime/include/psx_netplay.h @@ -1,7 +1,6 @@ #ifndef PSX_NETPLAY_H #define PSX_NETPLAY_H -#include #include #ifdef __cplusplus @@ -9,8 +8,8 @@ extern "C" { #endif /* - * Delay-sync netplay facade over recomp-net (LAN UDP or MotK ICE). - * Online hosted lobbies use ICE + WS signaling; Direct IP / LAN stay on UDP. + * Delay-sync netplay facade over recomp-net (LAN peer UDP for now). + * Lobby UI / ICE signaling are later work — this layer is CLI/env driven. * * Lockstep contract (matches recomp-net host_integration.md): * wait_admit (publish pads for tick T) → guest runs frame T → @@ -21,11 +20,10 @@ extern "C" { * - Each peer stages one local device sample; recomp-net maps it onto * that peer's local_slot (slot 0 = sim P1, slot 1 = sim P2). Transport * host/client role is determined by peer_hostport, not by local_slot. - * - input_player selects which host PlayerInput to sample; -1 = auto + * - input_player selects which host PlayerInput (0/1) to sample; -1 = auto * (prefer g_players[local_slot] if assigned, else player 0). * - While active, publish / release_pads is the sole SIO writer. - * - Every session slot stays plugged for in-game N-player detect. - * - slot_count >= 3 enables SCPH-1070 multitap (sio_set_multitap). + * - Every session slot stays plugged for in-game 2P/VS detect. * * Pad blob (8 bytes): * [0..1] buttons LE u16 (PSX active-low) @@ -45,16 +43,9 @@ typedef struct PsxNetPad { typedef struct PsxNetplayConfig { int enabled; - int local_slot; /* 0 .. slot_count-1 */ - int slot_count; /* 2 .. PSX_MAX_PLAYERS (session pad count) */ - int player_count; /* seated players at launch (0 = use slot_count) */ - int input_player; /* host device index; -1 = auto */ + int local_slot; /* 0 or 1 */ + int input_player; /* 0/1 host device index; -1 = auto */ int input_delay; - int force_input_relay; /* 1 = lobby-server UDP input relay */ - int force_turn; /* 1 = ICE relay-only (Force TURN for UDP) */ - /* 0 = auto (MotK room → ICE, else LAN), 1 = force ICE, 2 = force LAN. - * Env PSX_NET_TRANSPORT=lan|ice overrides. */ - int transport; uint32_t session_id; char bind_hostport[64]; char peer_hostport[64]; @@ -65,25 +56,11 @@ void psx_netplay_apply_env(PsxNetplayConfig *cfg); int psx_netplay_active(void); int psx_netplay_is_running(void); -/* "ice" | "lan" | "none" */ -const char *psx_netplay_transport_name(void); -/* 1 when ICE agent reached FAILED (online path). */ -int psx_netplay_ice_failed(void); -/* Optional JSONL samples when PSX_NET_DIAG=1 (saves/netplay/net_diag.jsonl). */ -void psx_netplay_diag_tick(void); int psx_netplay_local_slot(void); -/* Resolved host player index used for local capture. */ +/* Resolved host player index (0/1) used for local capture. */ int psx_netplay_input_player(void); uint32_t psx_netplay_sim_tick(void); -/* - * Snapshot for diagnostic dumps (starvation_dump.jsonl meta, etc.). - * arch_out: "off" | "p2p" | "host_relay" | "server_relay" (never NULL when - * arch_cap > 0). Returns 1 when netplay is/was configured this run. - */ -int psx_netplay_diag_snapshot(char *arch_out, size_t arch_cap, - int *max_players_out, int *player_count_out); - int psx_netplay_start(const PsxNetplayConfig *cfg); void psx_netplay_shutdown(void); @@ -103,14 +80,9 @@ int psx_netplay_is_host(void); int psx_netplay_request_save(int slot); int psx_netplay_request_load(int slot); -/* 1 while a save/load/memcard probe, chunk transfer, or post-load ready owns - * the clock (long admit timeout, no peer-silence kick, FPS suppressed). */ +/* 1 while post-load apply/ready barrier owns the clock (no FPS / no present). */ int psx_netplay_in_load_barrier(void); -/* 1 once after a staged netplay load apply failed (stale .pst / mismatch). - * Clears. Caller should soft-exit to lobby — do not keep waiting on the barrier. */ -int psx_netplay_consume_load_apply_failed(void); - /* Stage local pad for the current sim tick. Ignored once that tick is latched. */ void psx_netplay_stage_local(const PsxNetPad *pad); @@ -123,48 +95,20 @@ int psx_netplay_input_desync(uint32_t *tick, uint32_t *local_hash, uint32_t *re /* 1 if peer sent BYE or went silent for ~timeout_ms (default 1500). */ int psx_netplay_peer_disconnected(uint32_t timeout_ms); -/* - * Ingress / lobby / INPUT retransmit without try_admit. Used while the - * delay-sync starvation latch holds for remote runway refill. - */ -void psx_netplay_pump(void); - /* * Pump + try_admit for the current sim tick. On success, publish has written * SIO and a finish_frame() is owed after the guest completes that tick. * Returns 1 if admitted, 0 if caller must keep polling (linking / wait). * Does NOT advance the session clock. - * - * After sustained admit misses, latches starvation (pump-only) until - * remote_lead >= D for a few frames, then arms a recovery catch-up boost. - * Env: PSX_NET_STARVATION_ENTER_FRAMES, EXIT_FRAMES, EXIT_HR_LEAD. */ int psx_netplay_poll_admit(void); /* Call after the guest finishes the admitted tick (vblank boundary). */ void psx_netplay_finish_frame(void); -/* highest_remote_wire - sim_tick (0 if inactive; can be negative). */ -int psx_netplay_remote_lead(void); -/* Session input delay frames (default 2 when inactive). */ -int psx_netplay_input_delay(void); - -/* - * Extra headroom for post-starvation / behind-peer catch-up - * (min(16, max(0, remote_lead - D, recovery_burst))). - * Host should skip wall-clock pace while this is > 0, then call - * psx_netplay_catchup_consume_frame() once per skipped pace. - */ -int psx_netplay_catchup_budget(void); -void psx_netplay_catchup_consume_frame(void); - /* Park the admit barrier until a peer datagram may be ready (or timeout). */ void psx_netplay_wait_recv(int timeout_ms); -/* Diagnostics for a stuck admit barrier (stall name, sim tick, remote lead). */ -void psx_netplay_admit_wait_info(char *stall_out, size_t stall_cap, - uint32_t *sim_tick_out, int *lead_out); - /* Normalize sticks (deadzone → center) for stabler cross-device blobs. */ void psx_netplay_normalize_pad(PsxNetPad *pad); diff --git a/runtime/include/savestate.h b/runtime/include/savestate.h index b45d86ff..ca1c9d23 100644 --- a/runtime/include/savestate.h +++ b/runtime/include/savestate.h @@ -45,11 +45,6 @@ int savestate_write_slot(int slot, const void* data, size_t size); /* 1 if the slot file exists and is non-empty. */ int savestate_slot_exists(int slot); -/* 1 if the slot .pst header matches this build's integrity key (BIOS/entry/ - * codegen). 0 + optional reason when missing or stale — use before netplay - * load probe so incompatible saves never enter the post-load barrier. */ -int savestate_slot_compatible(int slot, char* reason, size_t reason_cap); - /* Stage a save/load of slot [0..SAVESTATE_SLOTS-1]. Executed at the next safe * boundary by savestate_poll (called every block from psx_check_interrupts). * Safe to call from the SDL key handler or a debug-server command. @@ -63,20 +58,12 @@ int savestate_request_load(int slot); int savestate_request_save_protocol(int slot); int savestate_request_load_protocol(int slot); -/* Netplay LOAD transfer: stage an in-memory .pst (no disk write). Copied - * internally; applied by savestate_poll like a normal slot load. */ -int savestate_request_load_blob_protocol(const void* data, size_t size); - /* 1 while a staged save/load has not yet been consumed by savestate_poll. */ int savestate_pending(void); /* 1 once after a successful load restore (before scheduler longjmp). Clears. */ int savestate_take_load_completed(void); -/* 1 once after a staged load failed in savestate_poll (missing/mismatched). - * Clears. Netplay uses this to abort the load barrier instead of hanging. */ -int savestate_take_load_failed(void); - /* Frontend hook (main.cpp): restage VRAM present path after a successful load. */ void psx_frontend_on_savestate_loaded(void); diff --git a/runtime/include/sio.h b/runtime/include/sio.h index 587f599d..63bfbdc9 100644 --- a/runtime/include/sio.h +++ b/runtime/include/sio.h @@ -7,16 +7,6 @@ extern "C" { #endif -/* Logical pad count. Default 2 (port1=pad0, port2=pad1). With multitap - * (sio_set_multitap) and PSX_MAX_PLAYERS>=5: port1 hosts SCPH-1070 pads - * A–D as logical 0–3, port2 is logical pad 4. Absolute max for this pass. */ -#ifndef PSX_MAX_PLAYERS -#define PSX_MAX_PLAYERS 2 -#endif -#if PSX_MAX_PLAYERS < 1 || PSX_MAX_PLAYERS > 5 -#error "PSX_MAX_PLAYERS must be in 1..5" -#endif - /* SIO0 register base: 0x1F801040 */ #define SIO_BASE 0x1F801040 @@ -67,36 +57,14 @@ uint32_t sio_cycles_to_irq(uint32_t i_mask); uint64_t sio_get_advance_called(void); uint64_t sio_get_advance_with_work(void); -/* SCPH-1070 multitap. Off by default. When enabled (PSX_MAX_PLAYERS>=5): - * multitap_port==0 (console Port 1): tap on phys0 → logical pads 0–3, - * phys1 → logical 4. - * multitap_port==1 (console Port 2): phys0 → logical 0, tap on phys1 → - * logical pads 1–4 (Bomberman Party Edition and a few others). - * Bulk 0x80 responses follow the real TAP/REQ latch (psx-spx): REQ=1 in the - * third command byte arms the *next* transfer; empty tap slots are fine. */ -void sio_set_multitap(int enabled); -int sio_get_multitap(void); -/* phys_port: 0 = console Port 1, 1 = console Port 2. Default 0. */ -void sio_set_multitap_port(int phys_port); -int sio_get_multitap_port(void); -/* Tomba-only legacy pad config path ([controller] legacy_pad_config). */ -void sio_set_legacy_cfg(int enabled); -int sio_get_legacy_cfg(void); -/* 1 when multitap is armed and `logical_slot` is a tap pad (not the lone - * pad on the opposite console port). SCPH-1070 taps are treated as plain - * digital controllers (0x41) — DualShock/analog on a tap is not reliable - * across titles, so host input and SIO type requests are forced digital. */ -int sio_pad_on_multitap(int logical_slot); - /* Update pad button state. Buttons use PS1 convention: 0=pressed, 1=released. Bit layout: SELECT, L3, R3, START, UP, RIGHT, DOWN, LEFT, L2, R2, L1, R1, TRIANGLE, CIRCLE, CROSS, SQUARE - sio_set_pad_state targets logical pad 0; the _slot form targets - logical pad 0 .. PSX_MAX_PLAYERS-1. */ + sio_set_pad_state targets port 1 (slot 0); the _slot form targets either. */ void sio_set_pad_state(uint16_t buttons); void sio_set_pad_state_slot(int slot, uint16_t buttons); -/* Set the analog stick state + pad type for a logical pad. enabled selects the +/* Set the analog stick state + pad type for a slot. enabled selects the * emulated pad: 0 = digital (poll id 0x41), 1 = DualShock/analog (poll id * 0x73, with the four 0..255 stick axes appended; 0x80 = centred). */ void sio_set_pad_analog(int slot, int enabled, @@ -111,23 +79,22 @@ void sio_set_pad_analog(int slot, int enabled, void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry); void sio_request_pad_type(int slot, int analog); -/* Connect / disconnect a logical pad (0 .. PSX_MAX_PLAYERS-1). By default no - * pads are connected during initial BIOS boot. */ +/* Connect / disconnect a pad on a slot (0=port1, 1=port2). By default no pads + * are connected during initial BIOS boot. */ void sio_connect_pad(int slot); void sio_set_pad_connected(int slot, int connected); -/* Declare whether the pad on a logical slot is a config-capable DualShock (1) - * or a plain digital controller (0). A real digital controller (SCPH-1080, - * poll id 0x41) does NOT answer the config-mode commands (0x43/0x44/0x45/ - * 0x46/0x47/0x4C/0x4D/0x4F) — it returns hi-z / no ACK, so a game's pad - * driver classifies it as digital-only and just polls with 0x42. A DualShock - * answers them. Set from the per-player pad mode (DIGITAL => 0, ANALOG/HYBRID - * => 1) at boot/hotplug. Default is 1 (config-capable) so existing - * analog/hybrid behaviour is unchanged. */ +/* Declare whether the pad on a slot is a config-capable DualShock (1) or a + * plain digital controller (0). A real digital controller (SCPH-1080, poll id + * 0x41) does NOT answer the config-mode commands (0x43/0x44/0x45/0x46/0x47/ + * 0x4C/0x4D/0x4F) — it returns hi-z / no ACK, so a game's pad driver classifies + * it as digital-only and just polls with 0x42. A DualShock answers them. Set + * from the per-player pad mode (DIGITAL => 0, ANALOG/HYBRID => 1) at boot/ + * hotplug. Default is 1 (config-capable) so existing analog/hybrid behaviour is + * unchanged. */ void sio_set_pad_config_capable(int slot, int capable); -/* Return current pad button state (for debug server). _slot targets a logical - * pad 0 .. PSX_MAX_PLAYERS-1. */ +/* Return current pad button state (for debug server). _slot targets either. */ uint16_t sio_get_pad_buttons(void); uint16_t sio_get_pad_buttons_slot(int slot); @@ -139,7 +106,7 @@ uint16_t sio_peek_stat(void); uint16_t sio_peek_ctrl(void); uint8_t sio_peek_rx_data(void); -/* Debug accessors: is a logical pad connected, and is it in analog mode. */ +/* Debug accessors: is a pad connected on the slot, and is it in analog mode. */ int sio_get_pad_connected(int slot); int sio_get_pad_analog(int slot); void sio_get_pad_sticks(int slot, uint8_t out[4]); diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 381090dc..5a267016 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -280,10 +280,6 @@ if(PSX_NETPLAY AND NOT RECOMP_NET_ROOT) endif() if(PSX_NETPLAY AND RECOMP_NET_ROOT AND EXISTS "${RECOMP_NET_ROOT}/CMakeLists.txt") if(NOT TARGET recomp_net) - option(PSX_NET_ICE "Build recomp-net with ICE/libjuice for MotK online" ON) - if(PSX_NET_ICE) - set(RNET_ENABLE_ICE ON CACHE BOOL "Build libjuice ICE transport" FORCE) - endif() set(RNET_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) set(RNET_BUILD_TESTS OFF CACHE BOOL "" FORCE) add_subdirectory("${RECOMP_NET_ROOT}" "${CMAKE_BINARY_DIR}/recomp-net") @@ -490,7 +486,6 @@ function(psxrecomp_add_runtime_target target) LAUNCHER_BRAND EXE_NAME GAME_VERSION - MAX_PLAYERS ) # GAME_GENERATED_FULL_C is a list (not a single value): the split-TU build # writes the recompiled game as N full_NN.c shards instead of one @@ -717,10 +712,6 @@ function(psxrecomp_add_runtime_target target) target_link_libraries(${target} PRIVATE ${PSX_SDL_LIBRARIES}) endif() - # zlib: boot_state v4 savestate compression (RAM/VRAM/SPU blobs). - find_package(ZLIB REQUIRED) - target_link_libraries(${target} PRIVATE ZLIB::ZLIB) - # Build identity: stamp the psxrecomp commit into the binary so a crash report # can be correlated to an exact build (issue #1 user reports had no version). # Computed at configure time from the psxrecomp repo (this file's dir); empty @@ -742,22 +733,6 @@ function(psxrecomp_add_runtime_target target) endif() endif() - # Per-game netplay/local pad ceiling. Default 2 (MotK / dual-shock path). - # Games that need multitap N-player (e.g. Bomberman Party Edition) pass - # MAX_PLAYERS 5. Clamped to the framework absolute max of 5. - if(NOT PSXRT_MAX_PLAYERS) - if(DEFINED PSX_MAX_PLAYERS AND NOT PSX_MAX_PLAYERS STREQUAL "") - set(PSXRT_MAX_PLAYERS "${PSX_MAX_PLAYERS}") - else() - set(PSXRT_MAX_PLAYERS 2) - endif() - endif() - if(PSXRT_MAX_PLAYERS LESS 2 OR PSXRT_MAX_PLAYERS GREATER 5) - message(FATAL_ERROR - "MAX_PLAYERS must be in 2..5 (got ${PSXRT_MAX_PLAYERS})") - endif() - message(STATUS "psxrecomp ${target}: PSX_MAX_PLAYERS=${PSXRT_MAX_PLAYERS}") - target_compile_definitions(${target} PRIVATE DEFAULT_DEBUG_PORT=${PSXRT_DEBUG_PORT} PSX_DEFAULT_BIOS_PATH="${PSXRT_DEFAULT_BIOS_PATH}" @@ -768,7 +743,6 @@ function(psxrecomp_add_runtime_target target) PSX_WINDOW_TITLE="${PSXRT_WINDOW_TITLE}" PSX_BUILD_REV="${PSX_GIT_REV}" PSX_GAME_VERSION="${PSXRT_GAME_VERSION}" - PSX_MAX_PLAYERS=${PSXRT_MAX_PLAYERS} FMT_HEADER_ONLY=1 $<$:PSX_SDL3=1> $<$:SDL_MAIN_HANDLED> @@ -942,7 +916,7 @@ function(psxrecomp_add_runtime_target target) if(WIN32 OR MINGW) # opengl32: GL backend (gpu_gl_renderer.c). GL 1.x is exported directly # by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress. - target_link_libraries(${target} PRIVATE ws2_32 iphlpapi dbghelp comdlg32 opengl32) + target_link_libraries(${target} PRIVATE ws2_32 dbghelp comdlg32 opengl32) else() if(CMAKE_DL_LIBS) target_link_libraries(${target} PRIVATE ${CMAKE_DL_LIBS}) diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index eeb7a1e9..b4acb417 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -1,36 +1,11 @@ #include "boot_state.h" #include "overlay_api.h" /* PSX_OVERLAY_CODEGEN_HASH / _ABI_TAG / _CODEGEN_VER */ -#include "dirty_ram_interp.h" #include "gpu_render.h" /* gr_vram_transfer_in / gr_vram_transfer_out */ -#include "cpu_state.h" /* gte_canonicalize_cpu_state after CPU wire restore */ #include "psx_cycles.h" #include "pst_wire.h" #include #include #include -#include -#if defined(_WIN32) -# include -#else -# include -#endif - -static double boot_state_mono_ms(void) { -#if defined(_WIN32) - static LARGE_INTEGER freq; - LARGE_INTEGER c; - if (!freq.QuadPart) QueryPerformanceFrequency(&freq); - QueryPerformanceCounter(&c); - return (double)c.QuadPart * 1000.0 / (double)freq.QuadPart; -#else - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1.0e6; -#endif -} - -/* Compress payloads at/above this size (RAM/VRAM/SPU/dirty dominate I/O). */ -#define BOOT_STATE_ZLIB_MIN 256u #define RAM_SIZE (2u * 1024u * 1024u) #define SPAD_SIZE (1024u) @@ -41,6 +16,9 @@ static double boot_state_mono_ms(void) { /* ---- core accessors (existing runtime modules) ---- */ extern uint8_t* memory_get_ram_ptr(void); extern uint8_t* memory_get_scratchpad_ptr(void); +extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index); +extern uint32_t dirty_ram_get_bitmap_word_count(void); +extern void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count); extern uint32_t i_stat; extern uint32_t i_mask; extern uint64_t psx_cycle_count; @@ -69,9 +47,6 @@ extern int dma_snapshot_read(const uint8_t* p, uint32_t len); extern uint32_t sio_snapshot_bytes(void); extern void sio_snapshot_write(uint8_t* p); extern int sio_snapshot_read(const uint8_t* p, uint32_t len); -extern uint32_t mdec_snapshot_bytes(void); -extern void mdec_snapshot_write(uint8_t* p); -extern int mdec_snapshot_read(const uint8_t* p, uint32_t len); /* CPU regs wire: 32+3+32+32+32 LE u32 = 131 * 4 = 524 bytes (no padding). */ #define CPU_REGS_WIRE_BYTES (524u) @@ -105,44 +80,36 @@ static int write_header_le(FILE* f, const BootStateHeader* h) { return fwrite_all(f, buf, sizeof buf); } -static int write_section_raw(FILE* f, uint32_t tag, uint32_t flags, - const void* data, uint64_t len) { +static int read_header_le(FILE* f, BootStateHeader* h) { + uint8_t buf[BOOT_STATE_HEADER_WIRE_BYTES]; + PstR r; + if (fread(buf, 1, sizeof buf, f) != sizeof buf) return 0; + pst_r_init(&r, buf, sizeof buf); + memset(h, 0, sizeof *h); + if (!pst_r_u32(&r, &h->magic) || + !pst_r_u32(&r, &h->version) || + !pst_r_u32(&r, &h->bios_checksum) || + !pst_r_u32(&r, &h->entry_pc) || + !pst_r_u32(&r, &h->codegen_hash) || + !pst_r_i32(&r, &h->abi_tag) || + !pst_r_u32(&r, &h->codegen_ver) || + !pst_r_u32(&r, &h->section_count) || + !pst_r_u32(&r, &h->reserved)) + return 0; + return 1; +} + +static int write_section(FILE* f, uint32_t tag, const void* data, uint64_t len) { uint8_t hdr[16]; PstW w; pst_w_init(&w, hdr, sizeof hdr); - if (!pst_w_u32(&w, tag) || !pst_w_u32(&w, flags) || !pst_w_u64(&w, len)) + if (!pst_w_u32(&w, tag) || !pst_w_u32(&w, 0u) || !pst_w_u64(&w, len)) return 0; if (!fwrite_all(f, hdr, sizeof hdr)) return 0; if (len && !fwrite_all(f, data, (size_t)len)) return 0; return 1; } -/* Prefer zlib for large blobs (smaller disk + faster load on slow storage). - * Falls back to raw if compressBound/compress fails. */ -static int write_section(FILE* f, uint32_t tag, const void* data, uint64_t len) { - if (!data && len) return 0; - if (len >= BOOT_STATE_ZLIB_MIN && len <= 0xffffffffu) { - uLong bound = compressBound((uLong)len); - uint8_t* packed = (uint8_t*)malloc(4u + (size_t)bound); - if (packed) { - PstW lw; - uLong dest_len = bound; - pst_w_init(&lw, packed, 4); - if (pst_w_u32(&lw, (uint32_t)len) && - compress2(packed + 4, &dest_len, (const Bytef*)data, (uLong)len, - Z_BEST_SPEED) == Z_OK) { - uint64_t payload = 4u + (uint64_t)dest_len; - int ok = write_section_raw(f, tag, BOOT_STATE_SEC_ZLIB, - packed, payload); - free(packed); - return ok; - } - free(packed); - } - } - return write_section_raw(f, tag, 0u, data, len); -} - static int write_module_section(FILE* f, uint32_t tag, uint32_t (*bytes)(void), void (*write)(uint8_t*)) { @@ -211,7 +178,7 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH; h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG; h.codegen_ver = (uint32_t)PSX_OVERLAY_CODEGEN_VER; - h.section_count = 15; + h.section_count = 14; int ok = write_header_le(f, &h); @@ -241,8 +208,7 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, if (!vbuf) ok = 0; else { gr_vram_transfer_out(0, 0, VRAM_W, VRAM_H, vbuf); - /* VRAM is uint16 LE guest pixels — emit as LE u16 stream. - * pst_w_pod is a memcpy on LE hosts (see pst_wire.h). */ + /* VRAM is uint16 LE guest pixels — emit as LE u16 stream. */ uint8_t* wire = (uint8_t*)malloc(VRAM_SIZE); if (!wire) ok = 0; else { @@ -260,7 +226,6 @@ int boot_state_save(const CPUState* cpu, uint32_t bios_checksum, if (ok) ok = write_module_section(f, BS_SEC_CDROM, cdrom_snapshot_bytes, cdrom_snapshot_write); if (ok) ok = write_module_section(f, BS_SEC_DMA, dma_snapshot_bytes, dma_snapshot_write); if (ok) ok = write_module_section(f, BS_SEC_SIO, sio_snapshot_bytes, sio_snapshot_write); - if (ok) ok = write_module_section(f, BS_SEC_MDEC, mdec_snapshot_bytes, mdec_snapshot_write); if (ok) { uint32_t wc = dirty_ram_get_bitmap_word_count(); uint64_t nbytes = (uint64_t)wc * 4u; @@ -304,9 +269,6 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len, if (!pst_r_u32(&r, &cpu->gte_data[i])) return 0; for (int i = 0; i < 32; i++) if (!pst_r_u32(&r, &cpu->gte_ctrl[i])) return 0; - /* Architectural normalize + drop host-only projection provenance that - * belonged to the pre-load timeline (not part of the wire format). */ - gte_canonicalize_cpu_state(cpu); return 1; } case BS_SEC_RAM: @@ -363,27 +325,19 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len, case BS_SEC_GPU: return gpu_snapshot_read(p, len); case BS_SEC_VRAM: { + uint16_t* vbuf; + PstR r; if (len != VRAM_SIZE) return 0; -#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) - /* Wire == host layout: upload straight from the section buffer. */ - gr_vram_transfer_in(0, 0, VRAM_W, VRAM_H, (const uint16_t*)p); - return 1; -#else - { - uint16_t* vbuf; - PstR r; - vbuf = (uint16_t*)malloc(VRAM_SIZE); - if (!vbuf) return 0; - pst_r_init(&r, p, len); - if (!pst_r_pod(&r, vbuf, VRAM_SIZE, 2)) { - free(vbuf); - return 0; - } - gr_vram_transfer_in(0, 0, VRAM_W, VRAM_H, vbuf); + vbuf = (uint16_t*)malloc(VRAM_SIZE); + if (!vbuf) return 0; + pst_r_init(&r, p, len); + if (!pst_r_pod(&r, vbuf, VRAM_SIZE, 2)) { free(vbuf); - return 1; + return 0; } -#endif + gr_vram_transfer_in(0, 0, VRAM_W, VRAM_H, vbuf); + free(vbuf); + return 1; } case BS_SEC_SPU: return spu_snapshot_read(p, len); @@ -397,8 +351,6 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len, return dma_snapshot_read(p, len); case BS_SEC_SIO: return sio_snapshot_read(p, len); - case BS_SEC_MDEC: - return mdec_snapshot_read(p, len); case BS_SEC_DIRTY: { uint32_t wc; uint32_t* words; @@ -423,273 +375,58 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len, } } -static int boot_state_parse_header(const uint8_t* file, size_t file_len, - BootStateHeader* h_out) { - PstR hr; - if (!file || !h_out || file_len < BOOT_STATE_HEADER_WIRE_BYTES || - file_len > 64u * 1024u * 1024u) { - return 0; - } - pst_r_init(&hr, file, BOOT_STATE_HEADER_WIRE_BYTES); - memset(h_out, 0, sizeof(*h_out)); - if (!pst_r_u32(&hr, &h_out->magic) || - !pst_r_u32(&hr, &h_out->version) || - !pst_r_u32(&hr, &h_out->bios_checksum) || - !pst_r_u32(&hr, &h_out->entry_pc) || - !pst_r_u32(&hr, &h_out->codegen_hash) || - !pst_r_i32(&hr, &h_out->abi_tag) || - !pst_r_u32(&hr, &h_out->codegen_ver) || - !pst_r_u32(&hr, &h_out->section_count) || - !pst_r_u32(&hr, &h_out->reserved)) { - return 0; - } - return 1; -} - -static void boot_state_append_reason(char* reason, size_t reason_cap, - const char* part) { - size_t n; - if (!reason || reason_cap == 0 || !part || !part[0]) return; - n = strlen(reason); - if (n + 1 >= reason_cap) return; - if (n > 0) { - reason[n++] = ','; - reason[n] = '\0'; - if (n + 1 >= reason_cap) return; - } - snprintf(reason + n, reason_cap - n, "%s", part); -} +int boot_state_load(const char* path, uint32_t bios_checksum, + uint32_t entry_pc, CPUState* cpu) { + FILE* f = fopen(path, "rb"); + if (!f) return 0; -int boot_state_check_buffer(const uint8_t* file, size_t file_len, - uint32_t bios_checksum, uint32_t entry_pc, - char* reason, size_t reason_cap) { BootStateHeader h; - char part[96]; - - if (reason && reason_cap) - reason[0] = '\0'; - - if (!file || file_len < BOOT_STATE_HEADER_WIRE_BYTES) { - boot_state_append_reason(reason, reason_cap, "missing_or_truncated"); - return 0; - } - if (file_len > 64u * 1024u * 1024u) { - boot_state_append_reason(reason, reason_cap, "too_large"); - return 0; - } - if (!boot_state_parse_header(file, file_len, &h)) { - boot_state_append_reason(reason, reason_cap, "header_parse"); + if (!read_header_le(f, &h)) { fclose(f); return 0; } + + if (h.magic != BOOT_STATE_MAGIC || + h.version != BOOT_STATE_VERSION || + h.bios_checksum != bios_checksum || + h.entry_pc != entry_pc || + h.codegen_hash != (uint32_t)PSX_OVERLAY_CODEGEN_HASH || + h.abi_tag != (int32_t)PSX_OVERLAY_ABI_TAG || + h.codegen_ver != (uint32_t)PSX_OVERLAY_CODEGEN_VER) { + fclose(f); return 0; } - if (h.magic != BOOT_STATE_MAGIC) { - snprintf(part, sizeof(part), "magic=%08X(want %08X)", - (unsigned)h.magic, (unsigned)BOOT_STATE_MAGIC); - boot_state_append_reason(reason, reason_cap, part); - } - if (h.version < BOOT_STATE_VERSION_MIN_READ || - h.version > BOOT_STATE_VERSION) { - snprintf(part, sizeof(part), "version=%u(want %u..%u)", - (unsigned)h.version, (unsigned)BOOT_STATE_VERSION_MIN_READ, - (unsigned)BOOT_STATE_VERSION); - boot_state_append_reason(reason, reason_cap, part); - } - if (h.bios_checksum != bios_checksum) { - snprintf(part, sizeof(part), "bios=%08X(want %08X)", - (unsigned)h.bios_checksum, (unsigned)bios_checksum); - boot_state_append_reason(reason, reason_cap, part); - } - if (h.entry_pc != entry_pc) { - snprintf(part, sizeof(part), "entry=%08X(want %08X)", - (unsigned)h.entry_pc, (unsigned)entry_pc); - boot_state_append_reason(reason, reason_cap, part); - } - if (h.codegen_hash != (uint32_t)PSX_OVERLAY_CODEGEN_HASH) { - snprintf(part, sizeof(part), "codegen_hash=%08X(want %08X)", - (unsigned)h.codegen_hash, - (unsigned)PSX_OVERLAY_CODEGEN_HASH); - boot_state_append_reason(reason, reason_cap, part); - } - if (h.abi_tag != (int32_t)PSX_OVERLAY_ABI_TAG) { - snprintf(part, sizeof(part), "abi_tag=%d(want %d)", - (int)h.abi_tag, (int)PSX_OVERLAY_ABI_TAG); - boot_state_append_reason(reason, reason_cap, part); - } - if (h.codegen_ver != (uint32_t)PSX_OVERLAY_CODEGEN_VER) { - snprintf(part, sizeof(part), "codegen_ver=%u(want %u)", - (unsigned)h.codegen_ver, (unsigned)PSX_OVERLAY_CODEGEN_VER); - boot_state_append_reason(reason, reason_cap, part); - } - - if (reason && reason_cap && reason[0]) - return 0; - return 1; -} - -int boot_state_load_buffer(const uint8_t* file, size_t file_len, - uint32_t bios_checksum, uint32_t entry_pc, - CPUState* cpu) { - const uint8_t* cur; - const uint8_t* end; - BootStateHeader h; - char reject[256]; const uint32_t required = (1u< 64u * 1024u * 1024u || (uint64_t)(end - cur) < len) { + if (fread(shdr, 1, sizeof shdr, f) != sizeof shdr) { ok = 0; break; } + pst_r_init(&hr, shdr, sizeof shdr); + if (!pst_r_u32(&hr, &tag) || !pst_r_u32(&hr, &pad) || !pst_r_u64(&hr, &len)) { ok = 0; break; } - payload = cur; - cur += (size_t)len; - - if (h.version >= 4u && pad == BOOT_STATE_SEC_ZLIB) { - PstR lr; - uint32_t raw_len = 0; - uLong dest_len; - double t_inf; - if (len < 4u) { ok = 0; break; } - pst_r_init(&lr, payload, 4); - if (!pst_r_u32(&lr, &raw_len) || raw_len == 0 || - raw_len > 64u * 1024u * 1024u) { - ok = 0; break; - } - inflated = (uint8_t*)malloc(raw_len); - if (!inflated) { ok = 0; break; } - dest_len = (uLong)raw_len; - t_inf = boot_state_mono_ms(); - if (uncompress(inflated, &dest_len, payload + 4, - (uLong)(len - 4u)) != Z_OK || - dest_len != (uLong)raw_len) { - free(inflated); - ok = 0; - break; - } - inflate_ms += boot_state_mono_ms() - t_inf; - apply_ptr = inflated; - apply_len = raw_len; - } else if (pad != 0u) { - /* v3 requires pad==0; v4 unknown/extra flags are a hard reject. */ - ok = 0; - break; - } else { - if (len > 0xffffffffu) { ok = 0; break; } - apply_ptr = payload; - apply_len = (uint32_t)len; - } - - t_sec = boot_state_mono_ms(); - if (!apply_section(tag, apply_ptr, apply_len, cpu, entry_pc)) ok = 0; + if (len > 64u * 1024u * 1024u) { ok = 0; break; } + uint8_t* buf = (uint8_t*)malloc(len ? (size_t)len : 1); + if (!buf) { ok = 0; break; } + if (len && fread(buf, 1, (size_t)len, f) != (size_t)len) { free(buf); ok = 0; break; } + if (!apply_section(tag, buf, (uint32_t)len, cpu, entry_pc)) ok = 0; else if (tag < 32) seen |= (1u << tag); - { - double dt = boot_state_mono_ms() - t_sec; - if (tag == BS_SEC_RAM) apply_ram_ms += dt; - else if (tag == BS_SEC_VRAM) apply_vram_ms += dt; - else if (tag == BS_SEC_SPURAM) apply_spuram_ms += dt; - else apply_other_ms += dt; - } - free(inflated); + free(buf); } + fclose(f); if (!ok || (seen & required) != required) return 0; - - /* RAM was memcpy'd; force overlay revalidation before resume. */ - overlay_watch_invalidate_after_ram_restore(); - - { - const double total_ms = boot_state_mono_ms() - t0; - fprintf(stderr, - "savestate: load_timing read=0.0 inflate=%.1f " - "apply_ram=%.1f apply_vram=%.1f apply_spuram=%.1f " - "apply_other=%.1f total=%.1f ms (file=%zu)\n", - inflate_ms, - apply_ram_ms, apply_vram_ms, apply_spuram_ms, - apply_other_ms, total_ms, file_len); - } return 1; } -int boot_state_load(const char* path, uint32_t bios_checksum, - uint32_t entry_pc, CPUState* cpu) { - FILE* f = fopen(path, "rb"); - long sz; - uint8_t* file = NULL; - size_t file_len = 0; - int ok; - const double t0 = boot_state_mono_ms(); - double t_after_read; - - if (!f) { - fprintf(stderr, "boot_state: reject — missing %s\n", - path ? path : "(null)"); - return 0; - } - if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return 0; } - sz = ftell(f); - if (sz < (long)BOOT_STATE_HEADER_WIRE_BYTES || sz > 64L * 1024L * 1024L) { - fprintf(stderr, "boot_state: reject — bad size %ld for %s\n", - sz, path ? path : "(null)"); - fclose(f); - return 0; - } - if (fseek(f, 0, SEEK_SET) != 0) { fclose(f); return 0; } - file_len = (size_t)sz; - file = (uint8_t*)malloc(file_len); - if (!file) { fclose(f); return 0; } - if (fread(file, 1, file_len, f) != file_len) { - free(file); - fclose(f); - return 0; - } - fclose(f); - t_after_read = boot_state_mono_ms(); - (void)t0; - (void)t_after_read; - - ok = boot_state_load_buffer(file, file_len, bios_checksum, entry_pc, cpu); - free(file); - return ok; -} - void boot_state_set_capture(const char* path, uint32_t bios_checksum, uint32_t entry_pc) { strncpy(s_capture_path, path, sizeof(s_capture_path) - 1); diff --git a/runtime/src/cdrom.c b/runtime/src/cdrom.c index ca1e0122..eac0c6f5 100644 --- a/runtime/src/cdrom.c +++ b/runtime/src/cdrom.c @@ -2661,65 +2661,6 @@ int cdrom_data_read_active(void) { return reading && !xa_stream_active; } -/* Savestate post-load: authentic CD second-response delays (ReadTOC ~30M - * cycles, Init ~1.1M, far seeks, etc.) leave the restored frame on screen - * for up to ~1s+ of wall time. Clamp those timers so the next cdrom_advance - * delivers the IRQ; keep XA/FMV cadence untouched. */ -#define CDROM_SAVESTATE_DELAY_CAP 4096 -#define CDROM_SAVESTATE_BOOST_VBLANKS 180 /* ~3s at 60Hz */ - -static int s_savestate_cd_boost_vblanks = 0; - -static void cdrom_apply_savestate_delay_caps(int log_clamps) { - if (pending.pending && pending.delay > CDROM_SAVESTATE_DELAY_CAP) { - if (log_clamps) - fprintf(stderr, - "cdrom: savestate clamped pending cmd=0x%02X delay %d -> %d\n", - (unsigned)pending.cmd, pending.delay, - CDROM_SAVESTATE_DELAY_CAP); - pending.delay = CDROM_SAVESTATE_DELAY_CAP; - } - if (reading && !xa_stream_active && - read_delay > CDROM_SAVESTATE_DELAY_CAP) { - if (log_clamps) - fprintf(stderr, - "cdrom: savestate clamped read_delay %d -> %d\n", - read_delay, CDROM_SAVESTATE_DELAY_CAP); - read_delay = CDROM_SAVESTATE_DELAY_CAP; - s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay; - } - if (cdrom_irq_present_delay > CDROM_SAVESTATE_DELAY_CAP) - cdrom_irq_present_delay = CDROM_SAVESTATE_DELAY_CAP; -} - -void cdrom_accelerate_after_savestate(void) { - const char *probe = getenv("PSX_POST_LOAD_PROBE"); - const int log_probe = (probe && probe[0] == '1'); - s_savestate_cd_boost_vblanks = CDROM_SAVESTATE_BOOST_VBLANKS; - cdrom_apply_savestate_delay_caps(/*log_clamps*/log_probe); - if (log_probe) - fprintf(stderr, - "cdrom: savestate boost armed for %d vblanks (delay cap %d)\n", - CDROM_SAVESTATE_BOOST_VBLANKS, CDROM_SAVESTATE_DELAY_CAP); -} - -void cdrom_savestate_boost_vblank(void) { - if (s_savestate_cd_boost_vblanks <= 0) return; - cdrom_apply_savestate_delay_caps(/*log_clamps*/0); - s_savestate_cd_boost_vblanks--; -} - -int cdrom_savestate_cd_wait_active(void) { - if (s_savestate_cd_boost_vblanks <= 0) return 0; - if (pending.pending && pending.delay > 0) return 1; - if (reading && !xa_stream_active) return 1; - return 0; -} - -int cdrom_savestate_boost_vblanks_remaining(void) { - return s_savestate_cd_boost_vblanks; -} - /* ---- boot snapshot: complete CD-ROM controller FSM (see boot_state.h) ---- */ /* Every functional controller/drive/FIFO/IRQ-latch/XA-decode/timing global is * listed here exactly once. The X-macro guarantees bytes()/write()/read() can diff --git a/runtime/src/crash_trace.c b/runtime/src/crash_trace.c index 132a0104..8926cd2d 100644 --- a/runtime/src/crash_trace.c +++ b/runtime/src/crash_trace.c @@ -8,10 +8,6 @@ * - trap_crash * - TCP "post_mortem_dump" command (future) * - * Soft-exit (SIGINT / SIGTERM / SIGUSR1, plus Windows console Ctrl handlers) - * calls exit(0) so atexit / __gcov_exit / LLVM profile writers flush — required - * for PGO train scripts that stop the process with kill. - * * Mirrors the sibling SuperMarioWorldRecomp project's src/post_mortem.c. The file * is OVERWRITTEN on each dump (last-write-wins, single file per run); * this is not a log per CLAUDE.md §3 — it's a one-shot final state diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index b78d320c..56fecd2b 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -2521,17 +2521,13 @@ static void depth24_note_upload(uint32_t x, uint32_t w) { } uint32_t gpu_depth24_rgb_limit(uint32_t display_x, uint32_t crtc_w) { - if (!(display_depth & 1u) || crtc_w == 0u) + if (!(display_depth & 1u) || s_d24_upload_x1 == 0u || crtc_w == 0u) return crtc_w; - /* No uploads yet → treat as uncovered (present blanks until first blit). */ - if (s_d24_upload_x1 == 0u) - return 0u; uint32_t dx = display_x & 1023u; - if (s_d24_upload_x1 <= dx) return 0u; + if (s_d24_upload_x1 <= dx) return crtc_w; uint32_t hw = s_d24_upload_x1 - dx; uint32_t rgb = (hw * 2u) / 3u; - if (rgb == 0u) return 0u; - if (rgb >= crtc_w) return crtc_w; + if (rgb == 0u || rgb >= crtc_w) return crtc_w; return rgb; } @@ -2545,12 +2541,6 @@ int gpu_depth24_present_hold_tick(void) { return 1; } -void gpu_depth24_on_savestate_loaded(void) { - /* Hold skips Swap — after restore we want the restored VRAM visible now. - * Upload span / prev_h were restored from the GPU snap. */ - s_d24_present_hold = 0; -} - /* ---- Present-time screen-colour LUT (verified-enhancement, opt-in) ------- * * PRESENT-TIME ONLY. This sits on the 15-bit-scanout -> RGB888 conversion that @@ -4858,13 +4848,10 @@ static void gp1_display_mode(uint32_t val) { * bit 5: vertical interlace (0=off, 1=on) * bit 6: horizontal resolution 2 (0=normal, 1=368) * bit 7: "reverseflag" */ - uint32_t new_depth = (val >> 4) & 1; hres1 = val & 3; vres = (val >> 2) & 1; video_mode = (val >> 3) & 1; - if (new_depth != display_depth) - s_d24_upload_x1 = 0; /* rising/falling: drop stale coverage */ - display_depth = new_depth; + display_depth = (val >> 4) & 1; vertical_interlace = (val >> 5) & 1; /* GPUSTAT.13 holds the legacy constant 0 in progressive (see the vblank * field flip); clear it on the switch so a title that toggles interlace @@ -5005,8 +4992,6 @@ static int gpu_snap_emit(PstW *w) { WH(vram_write_col); WH(vram_write_row); WU(vram_write_remaining); WI(vram_read_active); WH(vram_read_x); WH(vram_read_y); WH(vram_read_w); WH(vram_read_h); WH(vram_read_col); WH(vram_read_row); - /* Depth24 present helpers (MotK FMV) — must resume with upload span. */ - WU(s_d24_upload_x1); WI(s_d24_present_hold); WU(s_d24_prev_disp_h); #undef WU #undef WI #undef WH @@ -5039,7 +5024,6 @@ static int gpu_snap_parse(PstR *r) { RH(vram_write_col); RH(vram_write_row); RU(vram_write_remaining); RI(vram_read_active); RH(vram_read_x); RH(vram_read_y); RH(vram_read_w); RH(vram_read_h); RH(vram_read_col); RH(vram_read_row); - RU(s_d24_upload_x1); RI(s_d24_present_hold); RU(s_d24_prev_disp_h); #undef RU #undef RI #undef RH diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index a780c4c0..ec8bc5f7 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -69,12 +69,12 @@ #include "gpu_gl_renderer.h" #include "latency_ring.h" -#include "psx_sdl.h" -#if defined(PSX_SDL3) -#include -#else -#include -#endif +#include "psx_sdl.h" +#if defined(PSX_SDL3) +#include +#else +#include +#endif #include #include #include @@ -315,12 +315,12 @@ static GLint s_present_uTex = -1, s_present_uUvRect = -1; static GLuint s_interp_prog = 0, s_interp_tex[3]; static GLsync s_interp_fence[3]; static GLsync s_interp_draw_fence = NULL; -static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; -static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; -static GLint s_interp_uBlendMode = -1; -static int s_interp_enabled = 0, s_interp_valid = 0; -static int s_interp_suspended = 0; -static int s_interp_blend_mode = 0; +static GLint s_interp_uPrev = -1, s_interp_uCurr = -1; +static GLint s_interp_uAlpha = -1, s_interp_uUvRect = -1; +static GLint s_interp_uBlendMode = -1; +static int s_interp_enabled = 0, s_interp_valid = 0; +static int s_interp_suspended = 0; +static int s_interp_blend_mode = 0; static int s_interp_prev = 0, s_interp_cur = 0; static int s_interp_w = 0, s_interp_h = 0, s_interp_linear = 0; static int s_interp_force_4_3 = 0, s_interp_source_path = -1; @@ -619,11 +619,6 @@ static int s_last_dx, s_last_dy, s_last_dw, s_last_dh; * (and FPS) keep advancing — especially on a 2nd+ load of the same slot. */ static int s_force_present_remaining = 0; -/* Post-load freeze probe (main.cpp): accumulate skip/swap/dirty marks. */ -static uint64_t s_probe_skip = 0; -static uint64_t s_probe_swap = 0; -static uint64_t s_probe_dirty_marks = 0; - static void present_dirty_rect(int x0, int y0, int x1, int y1, int set) { if (x0 < 0) x0 = 0; if (y0 < 0) y0 = 0; if (x1 >= VRAM_W) x1 = VRAM_W - 1; if (y1 >= VRAM_H) y1 = VRAM_H - 1; @@ -633,7 +628,6 @@ static void present_dirty_rect(int x0, int y0, int x1, int y1, int set) { for (int ty = y0 / PRES_TILE; ty <= y1 / PRES_TILE; ty++) { if (set) s_present_dirty[ty] |= mask; else s_present_dirty[ty] &= ~mask; } - if (set) s_probe_dirty_marks++; } static int present_dirty_test(int x0, int y0, int x1, int y1) { @@ -748,21 +742,21 @@ static const char *PRESENT_FS = "#version 330\n" "in vec2 v_uv; uniform sampler2D u_tex; out vec4 frag;\n" "void main(){ frag = texture(u_tex, v_uv); }\n"; -static const char *INTERP_FS = - "#version 330\n" - "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" - "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" - "void main(){\n" - " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" - " float alpha=u_alpha;\n" - " if(u_blend_mode==1){\n" - " vec3 d=abs(prev.rgb-curr.rgb);\n" - " float change=max(max(d.r,d.g),d.b);\n" - " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" - " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" - " }\n" - " frag=mix(prev,curr,alpha);\n" - "}\n"; +static const char *INTERP_FS = + "#version 330\n" + "in vec2 v_uv; uniform sampler2D u_prev; uniform sampler2D u_curr;\n" + "uniform float u_alpha; uniform int u_blend_mode; out vec4 frag;\n" + "void main(){\n" + " vec4 prev=texture(u_prev,v_uv), curr=texture(u_curr,v_uv);\n" + " float alpha=u_alpha;\n" + " if(u_blend_mode==1){\n" + " vec3 d=abs(prev.rgb-curr.rgb);\n" + " float change=max(max(d.r,d.g),d.b);\n" + " float safe_blend=1.0-smoothstep(0.08,0.20,change);\n" + " alpha=mix(step(0.5,u_alpha),u_alpha,safe_blend);\n" + " }\n" + " frag=mix(prev,curr,alpha);\n" + "}\n"; /* Geometry: position in VRAM pixels (draw offset already applied by gpu.c), * color rgb in 0..1, color a = mask bit (0/1). The clip transform is in @@ -2139,28 +2133,6 @@ static int depth24_is_fb_transfer(int w, int h) { return 0; } -/* Remember only the depth24 scanout band for clear-on-leave. Used after a - * full-VRAM savestate restage so texture pages stay in the FBO while the - * RGB888 movie band is still wiped when GP1 leaves 24-bit (avoids MotK - * rainbow/static from treating packed RGB as 1555). */ -static void depth24_mark_scanout_band(void) { - GpuDisplayInfo di; - int fb_w, fb_h, x0, y0, x1, y1; - if (!gpu_display_is_depth24()) return; - gpu_get_display_info(&di); - fb_w = (int)((di.width * 3u + 1u) / 2u); - fb_h = (int)di.height; - if (fb_w < 8) fb_w = 8; - if (fb_h < 1) fb_h = 1; - x0 = (int)(di.display_x & 1023u); - y0 = (int)(di.display_y & 511u); - x1 = x0 + fb_w - 1; - y1 = y0 + fb_h - 1; - if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; - if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; - rect_add(&s_d24_skip_fb, x0, y0, x1, y1); -} - static void depth24_clear_skipped_fb(void) { if (!s_raster_ok || !s_d24_skip_fb.set) return; flush_flat_batch(); @@ -2216,16 +2188,6 @@ static void glb_vram_transfer_in(int x,int y,int w,int h,const uint16_t *d){ sw_vram_transfer_in(x,y,w,h,d); depth24_upload_policy(); if (s_depth24_skip_up && depth24_is_fb_transfer(w, h)) { - /* Full-VRAM restore (boot_state): must stage into the FBO or every - * texture page outside the movie band is missing after FMV→menus. - * Only the scanout band is remembered for clear-on-leave. */ - if (w >= VRAM_W && h >= VRAM_H) { - up_add_transfer(x, y, w, h); - rect_clear(&s_d24_skip_fb); - depth24_mark_scanout_band(); - coh_record(GL_COH_UPLOAD, x, y, x + w - 1, y + h - 1); - return; - } int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1); rect_add(&s_d24_skip_fb, x0, y0, x0 + w - 1, y0 + h - 1); coh_record(GL_COH_UPLOAD, x, y, x+w-1, y+h-1); @@ -2511,11 +2473,11 @@ int gl_renderer_init_context(SDL_Window *win) { s_present_uTex = p_glGetUniformLocation(s_present_prog, "u_tex"); s_present_uUvRect = p_glGetUniformLocation(s_present_prog, "u_uv_rect"); s_interp_uPrev = p_glGetUniformLocation(s_interp_prog, "u_prev"); - s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); - s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); - s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect"); - s_interp_uBlendMode = - p_glGetUniformLocation(s_interp_prog, "u_blend_mode"); + s_interp_uCurr = p_glGetUniformLocation(s_interp_prog, "u_curr"); + s_interp_uAlpha = p_glGetUniformLocation(s_interp_prog, "u_alpha"); + s_interp_uUvRect = p_glGetUniformLocation(s_interp_prog, "u_uv_rect"); + s_interp_uBlendMode = + p_glGetUniformLocation(s_interp_prog, "u_blend_mode"); glGenTextures(3, s_interp_tex); for (int i = 0; i < 3; i++) { glBindTexture(GL_TEXTURE_2D, s_interp_tex[i]); @@ -2634,16 +2596,10 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea upload_present_tex(pixels, src_w, src_h, linear); p_glUseProgram(s_present_prog); p_glUniform1i(s_present_uTex, 0); if (crop) { - /* Cropped present keeps left-aligned content; still inset so linear - * AA does not blend the cut column with undefined border texels. */ - float u0 = (src_w > 0) ? (0.5f / (float)src_w) : 0.f; - float v0 = (src_h > 0) ? (0.5f / (float)src_h) : 0.f; - p_glUniform4f(s_present_uUvRect, u0, v0, uv_x1 - u0, 1.f - v0); - } else if (src_w > 0 && src_h > 0) { - /* Half-texel UV inset for both nearest and linear. Corner-mapped - * UV=1.0 grazes past the last texel (driver-dependent border sample); - * with GL_LINEAR that also blends an edge stripe into the image. - * Matches present_target_quad / MotK present UV edge-bleed fix. */ + p_glUniform4f(s_present_uUvRect, 0.f, 0.f, uv_x1, 1.f); + } else if (!linear && src_w > 0 && src_h > 0) { + /* Nearest: half-texel UV inset so UV=1.0 never grazes past the last + * column into undefined border samples on some drivers. */ float u0 = 0.5f / (float)src_w, v0 = 0.5f / (float)src_h; p_glUniform4f(s_present_uUvRect, u0, v0, 1.f - u0, 1.f - v0); } else { @@ -2655,7 +2611,6 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea latency_ring_mark(LAT_SWAP_BEGIN); SDL_GL_SwapWindow(s_win); latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; present_force_consumed(); s_last_present_path = GL_PRES_CPU; } @@ -2670,7 +2625,6 @@ void gl_renderer_present_blank(void) { latency_ring_mark(LAT_SWAP_BEGIN); SDL_GL_SwapWindow(s_win); latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; present_force_consumed(); s_last_present_path = GL_PRES_BLANK; } @@ -2688,45 +2642,6 @@ void gl_renderer_invalidate_present(void) { interp_reset_history(); } -void gl_renderer_restage_vram_after_savestate(void) { - if (!s_raster_ok || !s_vram) return; - /* Belt-and-suspenders after boot_state VRAM apply: force CPU mirror → FBO - * even if a depth24 skip swallowed the restore, then re-arm scanout-band - * clear so leaving FMV does not keep RGB888-as-1555 junk. */ - s_up_nrects = 0; - rect_clear(&s_d24_skip_fb); - s_depth24_skip_up = 0; - up_add_transfer(0, 0, VRAM_W, VRAM_H); - flush_cpu_upload(); - if (gpu_display_is_depth24()) { - s_depth24_skip_up = 1; - depth24_mark_scanout_band(); - } -} - -void gl_renderer_present_probe_reset(void) { - s_probe_skip = 0; - s_probe_swap = 0; - s_probe_dirty_marks = 0; -} - -void gl_renderer_present_probe_take(uint64_t *skip_delta, uint64_t *swap_delta, - uint64_t *dirty_mark_delta, - int *force_remaining) { - if (skip_delta) { *skip_delta = s_probe_skip; s_probe_skip = 0; } - if (swap_delta) { *swap_delta = s_probe_swap; s_probe_swap = 0; } - if (dirty_mark_delta) { - *dirty_mark_delta = s_probe_dirty_marks; - s_probe_dirty_marks = 0; - } - if (force_remaining) *force_remaining = s_force_present_remaining; -} - -int gl_renderer_present_rect_dirty(int disp_x, int disp_y, int w, int h) { - if (!s_raster_ok || w <= 0 || h <= 0) return 0; - return present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1); -} - void gl_renderer_flush_cpu_uploads(void) { if (!s_raster_ok) return; flush_flat_batch(); @@ -3338,13 +3253,13 @@ static void interp_reset_history(void) { if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex); } -void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz, - int blend_mode) { - double effective_hz = target_hz < 0.0 - ? -1.0 - : (target_hz >= 60.0 ? target_hz : host_hz); - int active = (enabled && - (effective_hz < 0.0 || effective_hz >= 50.0)) ? 1 : 0; +void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz, + int blend_mode) { + double effective_hz = target_hz < 0.0 + ? -1.0 + : (target_hz >= 60.0 ? target_hz : host_hz); + int active = (enabled && + (effective_hz < 0.0 || effective_hz >= 50.0)) ? 1 : 0; const char *diag = getenv("PSX_GL_INTERP_DIAG"); s_interp_diag = diag && diag[0] && diag[0] != '0'; if (active && !s_interp_ctx && s_ctx) { @@ -3372,20 +3287,20 @@ void gl_renderer_set_interpolation(int enabled, double host_hz, double target_hz if (s_interp_mutex) SDL_LockMutex(s_interp_mutex); if (active != s_interp_enabled) interp_reset_history_unlocked(); s_interp_enabled = active; - s_interp_host_hz = host_hz; - s_interp_target_hz = active ? effective_hz : 0.0; - s_interp_blend_mode = blend_mode == 1 ? 1 : 0; + s_interp_host_hz = host_hz; + s_interp_target_hz = active ? effective_hz : 0.0; + s_interp_blend_mode = blend_mode == 1 ? 1 : 0; if (s_interp_mutex) SDL_UnlockMutex(s_interp_mutex); - if (active && effective_hz < 0.0) - fprintf(stdout, "psxrecomp: GL frame interpolation enabled: uncapped " - "target on %.1f Hz display (%s blend)\n", host_hz, - s_interp_blend_mode ? "motion-adaptive" : "linear"); - else if (active) - fprintf(stdout, "psxrecomp: GL frame interpolation enabled: %.1f FPS " - "target on %.1f Hz display (%s blend)\n", effective_hz, host_hz, - s_interp_blend_mode ? "motion-adaptive" : "linear"); - else - fprintf(stdout, "psxrecomp: GL frame interpolation disabled (host %.1f Hz)\n", host_hz); + if (active && effective_hz < 0.0) + fprintf(stdout, "psxrecomp: GL frame interpolation enabled: uncapped " + "target on %.1f Hz display (%s blend)\n", host_hz, + s_interp_blend_mode ? "motion-adaptive" : "linear"); + else if (active) + fprintf(stdout, "psxrecomp: GL frame interpolation enabled: %.1f FPS " + "target on %.1f Hz display (%s blend)\n", effective_hz, host_hz, + s_interp_blend_mode ? "motion-adaptive" : "linear"); + else + fprintf(stdout, "psxrecomp: GL frame interpolation disabled (host %.1f Hz)\n", host_hz); } void gl_renderer_set_interpolation_suspended(int suspended) { @@ -3500,9 +3415,9 @@ static void interp_draw_quad(float alpha, int lx, int ly, int lw, int lh) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, s_interp_linear ? GL_LINEAR : GL_NEAREST); p_glUseProgram(s_interp_prog); p_glUniform1i(s_interp_uPrev, 0); - p_glUniform1i(s_interp_uCurr, 1); - p_glUniform1f(s_interp_uAlpha, alpha); - p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); + p_glUniform1i(s_interp_uCurr, 1); + p_glUniform1f(s_interp_uAlpha, alpha); + p_glUniform1i(s_interp_uBlendMode, s_interp_blend_mode); p_glUniform4f(s_interp_uUvRect, 0.f, 0.f, 1.f, 1.f); p_glBindVertexArray(s_interp_thread_vao); glDrawArrays(GL_TRIANGLES, 0, 3); @@ -3546,46 +3461,46 @@ static int interp_present(void) { return 1; } -static int interp_thread_main(void *opaque) { - (void)opaque; - if (SDL_GL_MakeCurrent(s_win, s_interp_ctx) != 0) return -1; - SDL_GL_SetSwapInterval(0); /* host-period scheduler owns cadence */ +static int interp_thread_main(void *opaque) { + (void)opaque; + if (SDL_GL_MakeCurrent(s_win, s_interp_ctx) != 0) return -1; + SDL_GL_SetSwapInterval(0); /* host-period scheduler owns cadence */ p_glGenVertexArrays(1, &s_interp_thread_vao); - uint64_t freq = SDL_GetPerformanceFrequency(); - uint64_t deadline = SDL_GetPerformanceCounter(); - uint64_t diag_start = deadline, diag_swaps = 0, diag_captures = 0; - - while (SDL_AtomicGet(&s_interp_thread_run)) { - SDL_LockMutex(s_interp_mutex); - double hz = s_interp_target_hz; - SDL_UnlockMutex(s_interp_mutex); - int uncapped = hz < 0.0; - uint64_t now = SDL_GetPerformanceCounter(); - if (!uncapped) { - if (hz < 50.0) hz = 60.0; - uint64_t period = (uint64_t)((double)freq / hz); - if (!period) period = 1; - deadline += period; - if (now > deadline + period * 4u) deadline = now + period; - for (;;) { - now = SDL_GetPerformanceCounter(); - if (now >= deadline) break; - uint64_t remain = deadline - now; - uint32_t ms = (uint32_t)((remain * 1000u) / - (freq ? freq : 1u)); - if (ms > 1) SDL_Delay(ms - 1); - } - while (SDL_GetPerformanceCounter() < deadline) {} - now = SDL_GetPerformanceCounter(); - } else { - deadline = now; - } - - SDL_LockMutex(s_interp_mutex); - int presented = 0; - if (SDL_AtomicGet(&s_interp_thread_run) && s_interp_enabled) - presented = interp_present(); - if (s_interp_diag && now - diag_start >= freq * 5u) { + uint64_t freq = SDL_GetPerformanceFrequency(); + uint64_t deadline = SDL_GetPerformanceCounter(); + uint64_t diag_start = deadline, diag_swaps = 0, diag_captures = 0; + + while (SDL_AtomicGet(&s_interp_thread_run)) { + SDL_LockMutex(s_interp_mutex); + double hz = s_interp_target_hz; + SDL_UnlockMutex(s_interp_mutex); + int uncapped = hz < 0.0; + uint64_t now = SDL_GetPerformanceCounter(); + if (!uncapped) { + if (hz < 50.0) hz = 60.0; + uint64_t period = (uint64_t)((double)freq / hz); + if (!period) period = 1; + deadline += period; + if (now > deadline + period * 4u) deadline = now + period; + for (;;) { + now = SDL_GetPerformanceCounter(); + if (now >= deadline) break; + uint64_t remain = deadline - now; + uint32_t ms = (uint32_t)((remain * 1000u) / + (freq ? freq : 1u)); + if (ms > 1) SDL_Delay(ms - 1); + } + while (SDL_GetPerformanceCounter() < deadline) {} + now = SDL_GetPerformanceCounter(); + } else { + deadline = now; + } + + SDL_LockMutex(s_interp_mutex); + int presented = 0; + if (SDL_AtomicGet(&s_interp_thread_run) && s_interp_enabled) + presented = interp_present(); + if (s_interp_diag && now - diag_start >= freq * 5u) { double seconds = (double)(now - diag_start) / (double)freq; fprintf(stdout, "psxrecomp: GL interpolation cadence: " "%.2f captures/s, %.2f presents/s\n", @@ -3595,10 +3510,10 @@ static int interp_thread_main(void *opaque) { diag_start = now; diag_captures = s_interp_captures; diag_swaps = s_interp_swaps; - } - SDL_UnlockMutex(s_interp_mutex); - if (uncapped && !presented) SDL_Delay(1); - } + } + SDL_UnlockMutex(s_interp_mutex); + if (uncapped && !presented) SDL_Delay(1); + } p_glBindVertexArray(0); SDL_GL_MakeCurrent(s_win, NULL); return 0; @@ -3639,7 +3554,6 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, s_last_dx == disp_x && s_last_dy == disp_y && s_last_dw == w && s_last_dh == h && !present_dirty_test(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1)) { - s_probe_skip++; gl_perf_present_enter(); gl_perf_present_exit(0); return; @@ -3677,7 +3591,6 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear, latency_ring_mark(LAT_SWAP_BEGIN); SDL_GL_SwapWindow(s_win); latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; gl_perf_present_exit(0); present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0); present_force_consumed(); @@ -3741,7 +3654,6 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) s_last_dx == disp_x && s_last_dy == disp_y && s_last_dw == g_wide_w && s_last_dh == disp_h && !present_dirty_test(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1)) { - s_probe_skip++; gl_perf_present_enter(); gl_perf_present_exit(1); return 1; @@ -3775,7 +3687,6 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear) latency_ring_mark(LAT_SWAP_BEGIN); SDL_GL_SwapWindow(s_win); latency_ring_mark(LAT_SWAP_END); - s_probe_swap++; gl_perf_present_exit(1); present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0); present_force_consumed(); diff --git a/runtime/src/gpu_sw_renderer.c b/runtime/src/gpu_sw_renderer.c index 51e6484c..86b8459c 100644 --- a/runtime/src/gpu_sw_renderer.c +++ b/runtime/src/gpu_sw_renderer.c @@ -1533,69 +1533,7 @@ uint16_t sw_vram_read(int x, int y) { /* Bulk VRAM transfers */ /* ------------------------------------------------------------------ */ -/* Rebuild the supersampled mirror from canonical VRAM after a bulk replace. - * Row-wise replication (memcpy) beats the per-texel nested loop used by the - * general transfer path — savestate restore hits full 1024×512 often. */ -static void hr_rebuild_from_vram(void) { - if (!g_hr || !g_vram) return; - const int s = g_scale; - if (s <= 1) return; - uint16_t *row = (uint16_t *)malloc((size_t)g_hr_w * sizeof(uint16_t)); - if (!row) { - /* Fall back to slow per-pixel replication if OOM (should not happen). */ - for (int py = 0; py < VRAM_HEIGHT; py++) { - for (int px = 0; px < VRAM_WIDTH; px++) { - uint16_t pixel = g_vram[py * VRAM_WIDTH + px]; - int bx = px * s, by = py * s; - for (int dy = 0; dy < s; dy++) { - uint16_t *dst = g_hr + (size_t)(by + dy) * (size_t)g_hr_w; - for (int dx = 0; dx < s; dx++) - dst[bx + dx] = pixel; - } - } - } - return; - } - for (int y = 0; y < VRAM_HEIGHT; y++) { - const uint16_t *src = g_vram + (size_t)y * VRAM_WIDTH; - for (int x = 0; x < VRAM_WIDTH; x++) { - uint16_t p = src[x]; - uint16_t *d = row + (size_t)x * (size_t)s; - for (int dx = 0; dx < s; dx++) - d[dx] = p; - } - for (int dy = 0; dy < s; dy++) { - memcpy(g_hr + ((size_t)(y * s + dy) * (size_t)g_hr_w), - row, (size_t)g_hr_w * sizeof(uint16_t)); - } - } - free(row); -} - void sw_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) { - if (!data || !g_vram || w <= 0 || h <= 0) return; - - /* Full-VRAM replace (savestate / boot_state): memcpy + one HR rebuild. - * The general path is a wrapped per-pixel loop that, with supersampling, - * does s² stores per guest texel — multi-second hitches on 2×/4×. */ - if (x == 0 && y == 0 && w == VRAM_WIDTH && h == VRAM_HEIGHT) { - memcpy(g_vram, data, (size_t)VRAM_WIDTH * (size_t)VRAM_HEIGHT * sizeof(uint16_t)); - if (g_hr) hr_rebuild_from_vram(); - return; - } - - /* Contiguous non-wrapping rect, no HR: memcpy each row. */ - x &= (VRAM_WIDTH - 1); - y &= (VRAM_HEIGHT - 1); - if (!g_hr && x + w <= VRAM_WIDTH && y + h <= VRAM_HEIGHT) { - for (int row = 0; row < h; row++) { - memcpy(g_vram + (size_t)(y + row) * VRAM_WIDTH + (size_t)x, - data + (size_t)row * (size_t)w, - (size_t)w * sizeof(uint16_t)); - } - return; - } - int idx = 0; int s = g_scale; for (int row = 0; row < h; row++) { @@ -1618,24 +1556,6 @@ void sw_vram_transfer_in(int x, int y, int w, int h, const uint16_t *data) { } void sw_vram_transfer_out(int x, int y, int w, int h, uint16_t *data) { - if (!data || !g_vram || w <= 0 || h <= 0) return; - - if (x == 0 && y == 0 && w == VRAM_WIDTH && h == VRAM_HEIGHT) { - memcpy(data, g_vram, (size_t)VRAM_WIDTH * (size_t)VRAM_HEIGHT * sizeof(uint16_t)); - return; - } - - x &= (VRAM_WIDTH - 1); - y &= (VRAM_HEIGHT - 1); - if (x + w <= VRAM_WIDTH && y + h <= VRAM_HEIGHT) { - for (int row = 0; row < h; row++) { - memcpy(data + (size_t)row * (size_t)w, - g_vram + (size_t)(y + row) * VRAM_WIDTH + (size_t)x, - (size_t)w * sizeof(uint16_t)); - } - return; - } - int idx = 0; for (int row = 0; row < h; row++) { int py = (y + row) & (VRAM_HEIGHT - 1); diff --git a/runtime/src/gpu_vk_renderer.c b/runtime/src/gpu_vk_renderer.c index 6ccc4bc7..5e9b4d6f 100644 --- a/runtime/src/gpu_vk_renderer.c +++ b/runtime/src/gpu_vk_renderer.c @@ -39,7 +39,6 @@ int vk_renderer_present_wide(int a,int b,int c,int d){(void)a;(void)b;(void)c;( void vk_renderer_present_cpu(const uint32_t*p,int w,int h,int l,int f){(void)p;(void)w;(void)h;(void)l;(void)f;} void vk_renderer_present_blank(void){} void vk_renderer_sync_cpu(void){} -void vk_renderer_restage_vram_after_savestate(void){} void vk_renderer_set_present_mode(int m){(void)m;} int vk_perf_json(char *out,int cap,int count){(void)count; return cap>2?snprintf(out,cap,"[]"):0;} const GpuRenderBackend *vk_backend_get(void) { return 0; } @@ -2260,24 +2259,6 @@ static int depth24_is_fb_transfer(int w, int h) { return 0; } -static void depth24_mark_scanout_band(void) { - GpuDisplayInfo di; - int fb_w, fb_h, x0, y0, x1, y1; - if (!gpu_display_is_depth24()) return; - gpu_get_display_info(&di); - fb_w = (int)((di.width * 3u + 1u) / 2u); - fb_h = (int)di.height; - if (fb_w < 8) fb_w = 8; - if (fb_h < 1) fb_h = 1; - x0 = (int)(di.display_x & 1023u); - y0 = (int)(di.display_y & 511u); - x1 = x0 + fb_w - 1; - y1 = y0 + fb_h - 1; - if (x1 > VRAM_W - 1) x1 = VRAM_W - 1; - if (y1 > VRAM_H - 1) y1 = VRAM_H - 1; - rect_add(&s_d24_skip_fb, x0, y0, x1, y1); -} - static void depth24_clear_skipped_fb(void) { /* GL scissor-clears the skipped FB union. VK keeps texture uploads that * landed during depth24; the FB bands are overwritten by the next 15-bit @@ -2303,32 +2284,12 @@ static void vkb_vram_transfer_in(int x, int y, int w, int h, const uint16_t *dat if (!s_ctx_ok) return; depth24_upload_policy(); if (s_depth24_skip_up && depth24_is_fb_transfer(w, h)) { - /* Full-VRAM savestate restore: stage FBO; mark scanout band only. */ - if (w >= VRAM_W && h >= VRAM_H) { - up_add_transfer(x, y, w, h); - rect_clear(&s_d24_skip_fb); - depth24_mark_scanout_band(); - return; - } int x0 = x & (VRAM_W - 1), y0 = y & (VRAM_H - 1); rect_add(&s_d24_skip_fb, x0, y0, x0 + w - 1, y0 + h - 1); return; } up_add_transfer(x, y, w, h); /* exact touched rects, incl. per-pixel wrap */ } - -void vk_renderer_restage_vram_after_savestate(void) { - if (!s_ctx_ok || !s_vram) return; - s_up_nrects = 0; - rect_clear(&s_d24_skip_fb); - s_depth24_skip_up = 0; - up_add_transfer(0, 0, VRAM_W, VRAM_H); - flush_cpu_upload(); - if (gpu_display_is_depth24()) { - s_depth24_skip_up = 1; - depth24_mark_scanout_band(); - } -} static void vkb_vram_transfer_out(int x, int y, int w, int h, uint16_t *data) { ensure_cpu(); /* sync GPU-rendered content down to the CPU mirror first */ for (int row = 0; row < h; row++) diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 368e019f..c267a87d 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -450,9 +450,6 @@ static uint32_t s_compiled_interrupt_resume_pc = 0; static uint32_t s_last_interrupt_check_pc = 0; static uint64_t s_last_interrupt_check_cycle = UINT64_MAX; -uint32_t psx_last_irq_check_pc(void) { return s_last_interrupt_check_pc; } -uint32_t psx_compiled_irq_resume_pc(void) { return s_compiled_interrupt_resume_pc; } - /* Deferred cooperative thread switch from nested exception delivery. * * A genuine in-exception ChangeThread (kind-30, escape site below) must be @@ -610,27 +607,6 @@ void interrupts_init(void) { g_vblank_raise_count = 0; last_sio_seq_seen = sio_get_seq(); last_sio_progress_cycle = psx_get_cycle_count(); - s_defer_switch_pending = 0; - s_defer_switch_target = 0; - s_defer_switch_from = 0; -} - -void interrupts_resync_after_restore(void) { - /* Absolute guest-cycle timestamps from the pre-load timeline are invalid - * once psx_cycle_count rewinds (typical: save → play N seconds → load). - * Leaving post_exception_cooldown_until in the future blocks every IRQ - * delivery — including VBlank — until the restored clock catches up, - * freezing the picture for those N seconds while host FPS stays at 60. */ - post_exception_cooldown_until = 0; - cycles_since_vblank = 0; - dispatch_count = 0; - in_exception = 0; - exception_nest_depth = 0; - last_sio_seq_seen = sio_get_seq(); - last_sio_progress_cycle = psx_get_cycle_count(); - s_defer_switch_pending = 0; - s_defer_switch_target = 0; - s_defer_switch_from = 0; } /* @@ -730,23 +706,6 @@ void psx_interrupt_delivery_diag(uint64_t *need_defer, uint64_t *need_irq, if (skip_nested) *skip_nested = s_skip_nested; } -/* Hot-path attribution for post-load freeze probe (see psx_interrupt_check_path_diag). */ -static uint64_t s_irq_path_entry; -static uint64_t s_irq_path_fast_sr; -static uint64_t s_irq_path_fast_none; -static uint64_t s_irq_path_eval; - -void psx_interrupt_check_path_diag(uint64_t *entry, uint64_t *fast_sr, - uint64_t *fast_none, uint64_t *mid, - uint64_t *eval, uint64_t *irq_deliv) { - if (entry) *entry = s_irq_path_entry; - if (fast_sr) *fast_sr = s_irq_path_fast_sr; - if (fast_none) *fast_none = s_irq_path_fast_none; - if (mid) *mid = total_checks; - if (eval) *eval = s_irq_path_eval; - if (irq_deliv) *irq_deliv = g_irq_deliver_count; -} - int psx_interrupt_delivery_needed(const CPUState* cpu) { if (s_defer_switch_pending) { s_need_defer++; return 1; } if ((i_stat & i_mask) == 0) { s_skip_none++; return 0; } @@ -781,8 +740,6 @@ void psx_check_interrupts(CPUState* cpu) { #define COSIM_IRQ_NOTE(kind_) cosim_irq_note(cpu, (kind_), COSIM_IRQ_TAKE_PC(), g_dirty_safe_resume_pc, s_compiled_interrupt_resume_pc, cpu->cop0[COP0_SR]) #endif - s_irq_path_entry++; - /* MotK VLC / FMV hot edge: sticky unmasked I_STAT (CD/VBlank) while * IEc or IM2 is clear — no architectural delivery possible. Skip the * mid-path bookkeeping / irq_deliver_eval that used to run every BB. @@ -792,7 +749,6 @@ void psx_check_interrupts(CPUState* cpu) { if (!in_exception && !s_defer_switch_pending && (i_stat & i_mask) != 0) { uint32_t sr = cpu->cop0[COP0_SR]; if (!(sr & 0x01u) || !(sr & (1u << 10))) { - s_irq_path_fast_sr++; if ((++s_fast_maintenance & 0x3FFFu) == 0) { extern void savestate_poll(CPUState* cpu, uint32_t resume_pc); savestate_poll(cpu, s_compiled_interrupt_resume_pc); @@ -839,7 +795,6 @@ void psx_check_interrupts(CPUState* cpu) { psx_idle_note_check(cpu, check_pc); } if ((i_stat & i_mask) == 0 && sw_pending == 0) { - s_irq_path_fast_none++; if ((++s_fast_maintenance & 0x3FFFu) == 0) { extern void savestate_poll(CPUState* cpu, uint32_t resume_pc); savestate_poll(cpu, s_compiled_interrupt_resume_pc); @@ -1001,7 +956,6 @@ void psx_check_interrupts(CPUState* cpu) { } irq_deliver_eval: - s_irq_path_eval++; /* Check if any interrupts are pending (INTC hardware or COP0 software). */ if ((i_stat & i_mask) == 0 && sw_pending == 0) { irq_record_outcome(EV_NONE, 0, 0); PSX_CHECK_INTERRUPTS_RETURN(); } /* Nested delivery (hardware semantics). Real R3000A has no 'in exception' diff --git a/runtime/src/load_accel.c b/runtime/src/load_accel.c index 3a43a141..d32dbfb7 100644 --- a/runtime/src/load_accel.c +++ b/runtime/src/load_accel.c @@ -260,8 +260,3 @@ void psx_vsync_query_hle_stats_json(char* buf, int cap) { (unsigned long long)s_extra_horizon_hits, (unsigned long long)s_extra_horizon_cycles); } - -void psx_vsync_query_hle_horizon_totals(uint64_t *hits, uint64_t *cycles) { - if (hits) *hits = s_horizon_hits + s_extra_horizon_hits; - if (cycles) *cycles = s_horizon_cycles + s_extra_horizon_cycles; -} diff --git a/runtime/src/lobby_ws/rnet_ws.c b/runtime/src/lobby_ws/rnet_ws.c index bb2b1f8c..6e037733 100644 --- a/runtime/src/lobby_ws/rnet_ws.c +++ b/runtime/src/lobby_ws/rnet_ws.c @@ -13,24 +13,6 @@ #include #endif -static int socket_would_block(void) -{ -#if defined(_WIN32) - return WSAGetLastError() == WSAEWOULDBLOCK; -#else - return errno == EAGAIN || errno == EWOULDBLOCK; -#endif -} - -static int socket_interrupted(void) -{ -#if defined(_WIN32) - return WSAGetLastError() == WSAEINTR; -#else - return errno == EINTR; -#endif -} - static const char *B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; @@ -107,7 +89,7 @@ static int send_all(int fd, const void *buf, size_t len) ssize_t n = send(fd, p + sent, len - sent, 0); #endif if (n < 0) { - if (socket_interrupted()) { + if (errno == EINTR) { continue; } return -1; @@ -195,7 +177,7 @@ int rnet_ws_read_text(int fd, char *buf, size_t cap, int *closed) return -1; } if (n < 0) { - if (socket_would_block()) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { return 0; } return -1; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 79b05dc7..181cddcc 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -6,6 +6,8 @@ */ #include "cpu_state.h" +#include "psx_bios_image.h" /* the active BIOS's self-description */ +#include "psx_bios_backend.h" /* routing to whichever BIOS was selected */ #include "psx_scheduler.h" /* psx_scheduler_run — deterministic TCB scheduler */ #include "parity_trace.h" /* general two-process control-flow parity ring */ #include "device_trace.h" /* general two-process device-event cycle ring */ @@ -15,7 +17,7 @@ #include "text_xlate.h" #include "boot_state.h" #include "bios_hle.h" -#include "psx_bios_backend.h" +#include "bios_hle_plan.h" #include "psx_cycles.h" #include "starvation_ring.h" #include "load_accel.h" @@ -27,7 +29,6 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "overlay_backend.h" #include "gpu.h" -#include "interrupts.h" #include "present_ring.h" #include "load_transition_ring.h" #include "gpu_sw_renderer.h" @@ -37,9 +38,6 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "frame_pacing.h" #include "latency_ring.h" #include "sio.h" -#ifndef PSX_MAX_PLAYERS -#define PSX_MAX_PLAYERS 2 -#endif #include "psx_netplay.h" #include "psx_lobby_client.h" #include "spu.h" @@ -59,14 +57,19 @@ extern "C" void psx_event_step_conservative_env_init(void); #include "freeze_heartbeat.h" #include "config_loader.h" #include "game_options.h" +#include "mod_plugins.h" +#include "mod_runtime.h" #include "crc32.h" #include "disc_identity.h" +#include "disc_path.h" +#include "bios_rom_alias.h" #include "iso_reader.h" /* text-image guard: extract the boot EXE from the disc */ +#include "launcher_device.h" /* recomp-ui controller-source round-trip */ #include "psx_keybinds.h" /* configurable keyboard->DualShock keybinds (keybinds.ini) */ +#include "psx_stick.h" /* radial SDL-stick -> DualShock response transform */ #if defined(RECOMP_LAUNCHER) #include "recomp_launcher.h" /* shared recomp-ui Dear ImGui launcher */ #include "launcher_profile.h" /* per-system variant profile (theme/caps bundle) */ -#include "launcher_boot_timing.h" /* PSX_LAUNCHER_BOOT_TIMING stamps */ #endif #include "psx_sdl.h" #include "psx_sdl_audio.h" @@ -75,7 +78,6 @@ extern "C" void psx_event_step_conservative_env_init(void); #endif #include #include -#include #include #include #include @@ -100,26 +102,17 @@ extern "C" void psx_event_step_conservative_env_init(void); #include #else #include -#include #include -#include #include #include #include -#include #include #include #endif #ifndef PSX_DEFAULT_BIOS_PATH -/* Compile-time fallback name only — not an implicit player choice. - * Runtime default with no explicit pick is bios/openbios.bin (see - * resolve_bios_for_runtime / docs/BIOS_SELECTION.md). */ #define PSX_DEFAULT_BIOS_PATH "bios/SCPH1001.BIN" #endif -#ifndef PSX_BUNDLED_BIOS_PATH -#define PSX_BUNDLED_BIOS_PATH "bios/openbios.bin" -#endif #ifndef PSX_DEFAULT_GAME_CONFIG_PATH #define PSX_DEFAULT_GAME_CONFIG_PATH "" #endif @@ -307,13 +300,10 @@ struct PlayerInput { * input), false => currently presenting a digital pad (D-pad was last). */ int mode = PSXRecompV4::PAD_MODE_HYBRID; bool hybrid_analog = false; - int deadzone = 3277; /* raw SDL axis units, ~10% default */ SDL_GameController* handle = nullptr; SDL_JoystickID instance = -1; }; -static PlayerInput g_players[PSX_MAX_PLAYERS]; -/* Offline SIO sample loop bound (from game.toml players; clamped). */ -static int g_offline_pad_count = 2; +static PlayerInput g_players[2]; /* ARGB8888 staging buffer. Sized for the active internal resolution: * 640*scale x 512*scale. Allocated once the supersampling scale is known * (sized for the native 640x512 when supersampling is off). */ @@ -358,61 +348,6 @@ static Smooth60State g_smooth_60_state; * survive soft-return and poison FMV/FPS after session_reboot). */ static bool s_disabled_frame_presented = false; static bool s_force_present_after_load = false; -/* After LOADED: optional freeze probe (PSX_POST_LOAD_PROBE=1). Off by default. */ -static int s_post_load_probe_enabled = -1; /* -1 = unread env */ -static int s_post_load_probe_left = 0; -static int s_post_load_probe_i = 0; -static uint64_t s_post_load_probe_gp00 = 0; -static uint64_t s_post_load_probe_skip_tot = 0; -static uint64_t s_post_load_probe_swap_tot = 0; -static uint64_t s_post_load_probe_dirty_tot = 0; -static uint64_t s_post_load_probe_gp0_tot = 0; -static uint64_t s_post_load_probe_vb_raise0 = 0; -static uint64_t s_post_load_probe_vb_deliv0 = 0; -static uint64_t s_post_load_probe_vb_ack0 = 0; -static uint64_t s_post_load_probe_dirty_blks0 = 0; -static uint64_t s_post_load_probe_chk_entry0 = 0; -static uint64_t s_post_load_probe_chk_fsr0 = 0; -static uint64_t s_post_load_probe_chk_fnone0 = 0; -static uint64_t s_post_load_probe_chk_mid0 = 0; -static uint64_t s_post_load_probe_chk_eval0 = 0; -static uint64_t s_post_load_probe_chk_deliv0 = 0; -static uint64_t s_post_load_probe_cyc0 = 0; -static uint64_t s_post_load_probe_ci_unit0 = 0; -static uint64_t s_post_load_probe_ci_supp0 = 0; -static uint64_t s_post_load_probe_ci_none0 = 0; -static uint64_t s_post_load_probe_ci_sr0 = 0; -static uint64_t s_post_load_probe_ci_deliv0 = 0; -static uint64_t s_post_load_probe_ci_enter0 = 0; -static uint64_t s_post_load_probe_adv_calls0 = 0; -static uint64_t s_post_load_probe_adv_sum0 = 0; -static uint64_t s_post_load_probe_svc0 = 0; -static uint64_t s_post_load_probe_dirty_insns0 = 0; -static uint64_t s_post_load_probe_dirty_pump0 = 0; -static uint64_t s_post_load_probe_stores0 = 0; -static uint64_t s_post_load_probe_idle_n0 = 0; -static uint64_t s_post_load_probe_idle_cyc0 = 0; -static uint64_t s_post_load_probe_hz_hits0 = 0; -static uint64_t s_post_load_probe_hz_cyc0 = 0; -static Uint64 s_post_load_probe_host_t0 = 0; -static int s_post_load_probe_stall_run = 0; -static int s_post_load_probe_in_stall = 0; -#define POST_LOAD_STALL_PC_CAP 8 -static uint32_t s_stall_pc[POST_LOAD_STALL_PC_CAP]; -static uint32_t s_stall_pc_n[POST_LOAD_STALL_PC_CAP]; -static int s_stall_pc_used = 0; -#define POST_LOAD_LIVE_PC_CAP 8 -static uint32_t s_live_pc[POST_LOAD_LIVE_PC_CAP]; -static uint32_t s_live_pc_n[POST_LOAD_LIVE_PC_CAP]; -static int s_live_pc_used = 0; - -static int post_load_probe_env_on(void) { - if (s_post_load_probe_enabled < 0) { - const char *e = std::getenv("PSX_POST_LOAD_PROBE"); - s_post_load_probe_enabled = (e && e[0] == '1') ? 1 : 0; - } - return s_post_load_probe_enabled; -} static Uint64 s_fps_last_time = 0; static uint64_t s_fps_last_frame = 0; static std::string s_fps_base_title; @@ -423,14 +358,16 @@ static int s_fmv_skip_present_skip = 0; static int s_netplay_depth24_present_skip = 0; static uint32_t s_fmv_skip_last_mdec = 0; static int s_fmv_skip_hold = 0; -/* MotK FMV cutover: after an idle gap, blank the first depth24+MDEC presents - * so one-frame RGB888 junk (stale VRAM) never reaches the window. */ -static int s_d24_prev_mdec = 0; -static int s_d24_saw_gap = 0; -static int s_d24_cutover_blank = 0; -/* Savestate restore → audio pump: re-anchor last_cycles (declared early so - * psx_frontend_on_savestate_loaded can set it). */ -static int g_audio_cycle_resync = 0; + +/* Lightweight title/stderr FPS telemetry is developer-facing and opt-in. + * Keep release window titles clean unless explicitly requested. */ +static bool fps_telemetry_enabled() { + static const bool enabled = [] { + const char* value = std::getenv("PSX_FPS_TELEMETRY"); + return value && value[0] && value[0] != '0'; + }(); + return enabled; +} static void smooth_60_reset(void) { g_smooth_60_state.previous_source.clear(); @@ -453,374 +390,9 @@ static void present_session_reset(void) { s_netplay_depth24_present_skip = 0; s_fmv_skip_last_mdec = 0; s_fmv_skip_hold = 0; - s_d24_prev_mdec = 0; - s_d24_saw_gap = 0; - s_d24_cutover_blank = 0; smooth_60_reset(); } -static void post_load_probe_stall_pc_note(uint32_t pc) { - if (!pc) return; - for (int i = 0; i < s_stall_pc_used; i++) { - if (s_stall_pc[i] == pc) { - if (s_stall_pc_n[i] < 0xffffffffu) s_stall_pc_n[i]++; - return; - } - } - if (s_stall_pc_used >= POST_LOAD_STALL_PC_CAP) return; - s_stall_pc[s_stall_pc_used] = pc; - s_stall_pc_n[s_stall_pc_used] = 1; - s_stall_pc_used++; -} - -static void post_load_probe_stall_pc_dump(const char *why) { - if (s_stall_pc_used <= 0) return; - std::fprintf(stderr, "post_load_probe stall_pcs (%s):", why); - for (int i = 0; i < s_stall_pc_used; i++) { - std::fprintf(stderr, " 0x%08X×%u", - (unsigned)s_stall_pc[i], (unsigned)s_stall_pc_n[i]); - } - std::fprintf(stderr, "\n"); -} - -static void post_load_probe_live_pc_note(uint32_t pc) { - if (!pc) return; - for (int i = 0; i < s_live_pc_used; i++) { - if (s_live_pc[i] == pc) { - if (s_live_pc_n[i] < 0xffffffffu) s_live_pc_n[i]++; - return; - } - } - if (s_live_pc_used >= POST_LOAD_LIVE_PC_CAP) return; - s_live_pc[s_live_pc_used] = pc; - s_live_pc_n[s_live_pc_used] = 1; - s_live_pc_used++; -} - -static void post_load_probe_live_pc_dump(const char *why) { - if (s_live_pc_used <= 0) return; - std::fprintf(stderr, "post_load_probe live_pcs (%s):", why); - for (int i = 0; i < s_live_pc_used; i++) { - std::fprintf(stderr, " 0x%08X×%u", - (unsigned)s_live_pc[i], (unsigned)s_live_pc_n[i]); - } - std::fprintf(stderr, "\n"); -} - -static void post_load_probe_arm(void) { - if (!post_load_probe_env_on()) { - s_post_load_probe_left = 0; - g_plp_cycle_diag = 0; - return; - } - extern uint64_t g_vblank_raise_count, g_vblank_deliver_count, g_vblank_ack_count; - extern uint64_t g_dirty_ram_blocks_run; - extern uint64_t g_dirty_ram_insns_run; - extern uint64_t g_dirty_pump_count; - extern uint64_t g_guest_store_count; - uint64_t e = 0, fsr = 0, fn = 0, mid = 0, ev = 0, id = 0; - psx_interrupt_check_path_diag(&e, &fsr, &fn, &mid, &ev, &id); - uint64_t ci_u = 0, ci_s = 0, ci_n = 0, ci_sr = 0, ci_d = 0, ci_e = 0; - overlay_loader_get_ci_skip_diag(&ci_u, &ci_s, &ci_n, &ci_sr, &ci_d, &ci_e); - uint64_t hz_h = 0, hz_c = 0; - psx_vsync_query_hle_horizon_totals(&hz_h, &hz_c); - g_plp_cycle_diag = 1; - g_plp_adv_calls = 0; - g_plp_adv_max_chunk = 0; - g_plp_adv_sum = 0; - g_plp_svc_calls = 0; - s_post_load_probe_left = 300; - s_post_load_probe_i = 0; - s_post_load_probe_gp00 = gpu_get_gp0_count(); - s_post_load_probe_skip_tot = 0; - s_post_load_probe_swap_tot = 0; - s_post_load_probe_dirty_tot = 0; - s_post_load_probe_gp0_tot = 0; - s_post_load_probe_vb_raise0 = g_vblank_raise_count; - s_post_load_probe_vb_deliv0 = g_vblank_deliver_count; - s_post_load_probe_vb_ack0 = g_vblank_ack_count; - s_post_load_probe_dirty_blks0 = g_dirty_ram_blocks_run; - s_post_load_probe_chk_entry0 = e; - s_post_load_probe_chk_fsr0 = fsr; - s_post_load_probe_chk_fnone0 = fn; - s_post_load_probe_chk_mid0 = mid; - s_post_load_probe_chk_eval0 = ev; - s_post_load_probe_chk_deliv0 = id; - s_post_load_probe_cyc0 = psx_get_cycle_count(); - s_post_load_probe_ci_unit0 = ci_u; - s_post_load_probe_ci_supp0 = ci_s; - s_post_load_probe_ci_none0 = ci_n; - s_post_load_probe_ci_sr0 = ci_sr; - s_post_load_probe_ci_deliv0 = ci_d; - s_post_load_probe_ci_enter0 = ci_e; - s_post_load_probe_adv_calls0 = 0; - s_post_load_probe_adv_sum0 = 0; - s_post_load_probe_svc0 = 0; - s_post_load_probe_dirty_insns0 = g_dirty_ram_insns_run; - s_post_load_probe_dirty_pump0 = g_dirty_pump_count; - s_post_load_probe_stores0 = g_guest_store_count; - s_post_load_probe_idle_n0 = g_idle_skip_count; - s_post_load_probe_idle_cyc0 = g_idle_skip_cycles; - s_post_load_probe_hz_hits0 = hz_h; - s_post_load_probe_hz_cyc0 = hz_c; - s_post_load_probe_host_t0 = SDL_GetPerformanceCounter(); - s_post_load_probe_stall_run = 0; - s_post_load_probe_in_stall = 0; - s_stall_pc_used = 0; - s_live_pc_used = 0; - gl_renderer_present_probe_reset(); - std::fprintf(stderr, - "savestate: post_load_probe armed (300 vblanks; " - "PSX_POST_LOAD_PROBE=1; live_pc/ci/adv diag on)\n"); -} - -static void post_load_probe_on_vblank(int turbo_active, int present_reached) { - if (s_post_load_probe_left <= 0) return; - s_post_load_probe_i++; - s_post_load_probe_left--; - - uint64_t skip = 0, swap = 0, dirty_marks = 0; - int force_left = 0; - gl_renderer_present_probe_take(&skip, &swap, &dirty_marks, &force_left); - s_post_load_probe_skip_tot += skip; - s_post_load_probe_swap_tot += swap; - s_post_load_probe_dirty_tot += dirty_marks; - - const uint64_t gp0_now = gpu_get_gp0_count(); - const uint64_t gp0_delta = gp0_now - s_post_load_probe_gp00; - s_post_load_probe_gp00 = gp0_now; - s_post_load_probe_gp0_tot += gp0_delta; - - extern uint64_t g_vblank_raise_count, g_vblank_deliver_count, g_vblank_ack_count; - extern uint64_t g_dirty_ram_blocks_run; - extern uint32_t i_stat, i_mask; - extern CPUState *debug_cpu_ptr; - const uint64_t vb_r = g_vblank_raise_count - s_post_load_probe_vb_raise0; - const uint64_t vb_d = g_vblank_deliver_count - s_post_load_probe_vb_deliv0; - const uint64_t vb_a = g_vblank_ack_count - s_post_load_probe_vb_ack0; - s_post_load_probe_vb_raise0 = g_vblank_raise_count; - s_post_load_probe_vb_deliv0 = g_vblank_deliver_count; - s_post_load_probe_vb_ack0 = g_vblank_ack_count; - const uint64_t dirty_blks = g_dirty_ram_blocks_run - s_post_load_probe_dirty_blks0; - s_post_load_probe_dirty_blks0 = g_dirty_ram_blocks_run; - - uint32_t tcb = 0, gp_a = 0, gp_b = 0, gp_reg = 0; - uint32_t sr = 0, cause = 0; - int iec = 0, im2 = 0; - if (debug_cpu_ptr) { - gp_reg = debug_cpu_ptr->gpr[28]; - tcb = psx_sched_current_tcb(debug_cpu_ptr); - sr = debug_cpu_ptr->cop0[12]; /* COP0 Status */ - cause = debug_cpu_ptr->cop0[13]; /* COP0 Cause */ - iec = (sr & 0x1u) ? 1 : 0; - im2 = (sr & (1u << 10)) ? 1 : 0; - /* func_8004FD14 frame-counter compare at 0x800501E8. */ - if (debug_cpu_ptr->read_word) { - gp_a = debug_cpu_ptr->read_word(gp_reg + 2552u); - gp_b = debug_cpu_ptr->read_word(gp_reg + 2632u); - } - } - /* Hot-path check_interrupts attribution (not delivery_needed — that only - * samples at present time and was misleading). */ - uint64_t chk_e = 0, chk_fsr = 0, chk_fn = 0, chk_mid = 0, chk_ev = 0, chk_id = 0; - psx_interrupt_check_path_diag(&chk_e, &chk_fsr, &chk_fn, &chk_mid, &chk_ev, &chk_id); - const uint64_t d_entry = chk_e - s_post_load_probe_chk_entry0; - const uint64_t d_fsr = chk_fsr - s_post_load_probe_chk_fsr0; - const uint64_t d_fnone = chk_fn - s_post_load_probe_chk_fnone0; - const uint64_t d_mid = chk_mid - s_post_load_probe_chk_mid0; - const uint64_t d_eval = chk_ev - s_post_load_probe_chk_eval0; - const uint64_t d_irqd = chk_id - s_post_load_probe_chk_deliv0; - s_post_load_probe_chk_entry0 = chk_e; - s_post_load_probe_chk_fsr0 = chk_fsr; - s_post_load_probe_chk_fnone0 = chk_fn; - s_post_load_probe_chk_mid0 = chk_mid; - s_post_load_probe_chk_eval0 = chk_ev; - s_post_load_probe_chk_deliv0 = chk_id; - const uint64_t cyc_now = psx_get_cycle_count(); - const uint64_t d_cyc = cyc_now - s_post_load_probe_cyc0; - s_post_load_probe_cyc0 = cyc_now; - - uint64_t ci_u = 0, ci_s = 0, ci_n = 0, ci_sr = 0, ci_d = 0, ci_e = 0; - overlay_loader_get_ci_skip_diag(&ci_u, &ci_s, &ci_n, &ci_sr, &ci_d, &ci_e); - const uint64_t d_ci_unit = ci_u - s_post_load_probe_ci_unit0; - const uint64_t d_ci_supp = ci_s - s_post_load_probe_ci_supp0; - const uint64_t d_ci_none = ci_n - s_post_load_probe_ci_none0; - const uint64_t d_ci_sr = ci_sr - s_post_load_probe_ci_sr0; - const uint64_t d_ci_deliv = ci_d - s_post_load_probe_ci_deliv0; - const uint64_t d_ci_enter = ci_e - s_post_load_probe_ci_enter0; - s_post_load_probe_ci_unit0 = ci_u; - s_post_load_probe_ci_supp0 = ci_s; - s_post_load_probe_ci_none0 = ci_n; - s_post_load_probe_ci_sr0 = ci_sr; - s_post_load_probe_ci_deliv0 = ci_d; - s_post_load_probe_ci_enter0 = ci_e; - - const uint64_t adv_calls = g_plp_adv_calls; - const uint64_t adv_sum = g_plp_adv_sum; - const uint32_t adv_max = g_plp_adv_max_chunk; - const uint64_t svc_calls = g_plp_svc_calls; - const uint64_t d_adv_calls = adv_calls - s_post_load_probe_adv_calls0; - const uint64_t d_adv_sum = adv_sum - s_post_load_probe_adv_sum0; - const uint64_t d_svc = svc_calls - s_post_load_probe_svc0; - s_post_load_probe_adv_calls0 = adv_calls; - s_post_load_probe_adv_sum0 = adv_sum; - s_post_load_probe_svc0 = svc_calls; - g_plp_adv_max_chunk = 0; /* per-frame max */ - - extern uint64_t g_dirty_ram_insns_run; - extern uint64_t g_dirty_pump_count; - extern uint64_t g_guest_store_count; - const uint64_t d_dirty_insns = g_dirty_ram_insns_run - s_post_load_probe_dirty_insns0; - const uint64_t d_dirty_pump = g_dirty_pump_count - s_post_load_probe_dirty_pump0; - const uint64_t d_stores = g_guest_store_count - s_post_load_probe_stores0; - s_post_load_probe_dirty_insns0 = g_dirty_ram_insns_run; - s_post_load_probe_dirty_pump0 = g_dirty_pump_count; - s_post_load_probe_stores0 = g_guest_store_count; - - const uint64_t d_idle_n = g_idle_skip_count - s_post_load_probe_idle_n0; - const uint64_t d_idle_cyc = g_idle_skip_cycles - s_post_load_probe_idle_cyc0; - s_post_load_probe_idle_n0 = g_idle_skip_count; - s_post_load_probe_idle_cyc0 = g_idle_skip_cycles; - - uint64_t hz_h = 0, hz_c = 0; - psx_vsync_query_hle_horizon_totals(&hz_h, &hz_c); - const uint64_t d_hz_hits = hz_h - s_post_load_probe_hz_hits0; - const uint64_t d_hz_cyc = hz_c - s_post_load_probe_hz_cyc0; - s_post_load_probe_hz_hits0 = hz_h; - s_post_load_probe_hz_cyc0 = hz_c; - - const Uint64 host_now = SDL_GetPerformanceCounter(); - const Uint64 host_freq = SDL_GetPerformanceFrequency(); - const double host_ms = (host_freq > 0) - ? (1000.0 * (double)(host_now - s_post_load_probe_host_t0) / - (double)host_freq) - : 0.0; - s_post_load_probe_host_t0 = host_now; - - GpuDisplayInfo di; - gpu_get_display_info(&di); - const int rect_dirty = (di.width > 0 && di.height > 0) - ? gl_renderer_present_rect_dirty((int)di.display_x, (int)di.display_y, - (int)di.width, (int)di.height) - : 0; - - CDROMDebugState cd; - cdrom_debug_snapshot(&cd); - const int cd_wait = cdrom_savestate_cd_wait_active(); - const int boost_left = cdrom_savestate_boost_vblanks_remaining(); - const int xa = cdrom_xa_stream_active(); - - const uint32_t irq_pc = psx_last_irq_check_pc(); - const uint32_t resume_pc = psx_compiled_irq_resume_pc(); - extern uint32_t g_debug_current_func_addr; - extern uint32_t g_debug_last_store_pc; - extern int g_psx_dispatch_depth; - const uint32_t func = g_debug_current_func_addr; - const uint32_t store_pc = g_debug_last_store_pc; - const uint32_t live_pc = debug_cpu_ptr ? debug_cpu_ptr->pc : 0u; - const uint32_t live_ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0u; - const int unit_depth = overlay_loader_call_unit_depth(); - const int disp_depth = g_psx_dispatch_depth; - int cooldown_left = 0; - int in_exc = 0; - psx_get_freeze_diag(NULL, NULL, &in_exc, &cooldown_left, NULL, NULL); - - const int stalled = (gp0_delta == 0); - if (stalled) { - s_post_load_probe_stall_run++; - s_post_load_probe_in_stall = 1; - post_load_probe_stall_pc_note(irq_pc ? irq_pc : resume_pc); - post_load_probe_stall_pc_note(func); - post_load_probe_live_pc_note(live_pc); - } else if (s_post_load_probe_in_stall) { - std::fprintf(stderr, - "post_load_probe STALL_END at #%d after %d vblanks " - "(irq_pc=0x%08X resume=0x%08X func=0x%08X " - "live=0x%08X ra=0x%08X unit=%d disp=%d)\n", - s_post_load_probe_i, s_post_load_probe_stall_run, - (unsigned)irq_pc, (unsigned)resume_pc, (unsigned)func, - (unsigned)live_pc, (unsigned)live_ra, - unit_depth, disp_depth); - post_load_probe_stall_pc_dump("end"); - post_load_probe_live_pc_dump("end"); - s_post_load_probe_in_stall = 0; - s_post_load_probe_stall_run = 0; - s_stall_pc_used = 0; - s_live_pc_used = 0; - } - - /* Dense samples during soft-stall; otherwise first 32 + every 15. */ - const int log_line = - stalled || - (s_post_load_probe_i <= 32) || - (s_post_load_probe_i % 15 == 0) || - (s_post_load_probe_left == 0); - if (log_line) { - std::fprintf(stderr, - "post_load_probe #%d: live=0x%08X ra=0x%08X " - "irq_pc=0x%08X resume=0x%08X func=0x%08X " - "store=0x%08X idle=0x%08X unit=%d disp=%d turbo=%d reached=%d " - "swap=%llu skip=%llu dirty_marks=%llu force=%d rect_dirty=%d " - "fb=%ux%u@(%u,%u) dis=%d d24=%d gp0=%llu " - "cd(pend=%d cmd=0x%02X dly=%d read=%d rdly=%d xa=%d wait=%d boost=%d) " - "exc=%d cool=%d istat=0x%X imask=0x%X " - "vb(r=%llu d=%llu a=%llu) tcb=0x%08X " - "gp9f8=%d gpA48=%d dirty_blks=%llu dins=%llu dpump=%llu stores=%llu " - "sr=0x%08X iec=%d im2=%d cause=0x%08X cyc=%llu host_ms=%.2f " - "chk(e=%llu fsr=%llu fn=%llu mid=%llu eval=%llu irq=%llu) " - "ci(unit=%llu supp=%llu none=%llu sr=%llu deliv=%llu enter=%llu) " - "adv(n=%llu sum=%llu max=%u svc=%llu) " - "idle_skip(n=%llu cyc=%llu) hz(n=%llu cyc=%llu)\n", - s_post_load_probe_i, - (unsigned)live_pc, (unsigned)live_ra, - (unsigned)irq_pc, (unsigned)resume_pc, (unsigned)func, - (unsigned)store_pc, (unsigned)g_idle_skip_last_pc, - unit_depth, disp_depth, - turbo_active, present_reached, - (unsigned long long)swap, (unsigned long long)skip, - (unsigned long long)dirty_marks, force_left, rect_dirty, - (unsigned)di.width, (unsigned)di.height, - (unsigned)di.display_x, (unsigned)di.display_y, - di.disabled ? 1 : 0, di.depth24 ? 1 : 0, - (unsigned long long)gp0_delta, - cd.pending_pending, (unsigned)cd.pending_cmd, cd.pending_delay, - cd.reading, cd.read_delay, xa, cd_wait, boost_left, - in_exc, cooldown_left, (unsigned)i_stat, (unsigned)i_mask, - (unsigned long long)vb_r, (unsigned long long)vb_d, - (unsigned long long)vb_a, (unsigned)tcb, - (int)gp_a, (int)gp_b, (unsigned long long)dirty_blks, - (unsigned long long)d_dirty_insns, (unsigned long long)d_dirty_pump, - (unsigned long long)d_stores, - (unsigned)sr, iec, im2, (unsigned)cause, - (unsigned long long)d_cyc, host_ms, - (unsigned long long)d_entry, (unsigned long long)d_fsr, - (unsigned long long)d_fnone, (unsigned long long)d_mid, - (unsigned long long)d_eval, (unsigned long long)d_irqd, - (unsigned long long)d_ci_unit, (unsigned long long)d_ci_supp, - (unsigned long long)d_ci_none, (unsigned long long)d_ci_sr, - (unsigned long long)d_ci_deliv, (unsigned long long)d_ci_enter, - (unsigned long long)d_adv_calls, (unsigned long long)d_adv_sum, - (unsigned)adv_max, (unsigned long long)d_svc, - (unsigned long long)d_idle_n, (unsigned long long)d_idle_cyc, - (unsigned long long)d_hz_hits, (unsigned long long)d_hz_cyc); - } - if (s_post_load_probe_left == 0) { - if (s_post_load_probe_in_stall) { - post_load_probe_stall_pc_dump("done-still-stalled"); - post_load_probe_live_pc_dump("done-still-stalled"); - } - g_plp_cycle_diag = 0; - std::fprintf(stderr, - "post_load_probe DONE: n=%d swap_tot=%llu skip_tot=%llu " - "dirty_tot=%llu gp0_tot=%llu\n", - s_post_load_probe_i, - (unsigned long long)s_post_load_probe_swap_tot, - (unsigned long long)s_post_load_probe_skip_tot, - (unsigned long long)s_post_load_probe_dirty_tot, - (unsigned long long)s_post_load_probe_gp0_tot); - } -} - /* Called from savestate_poll after a successful restore (before scheduler * longjmp). Clears present latches and forces the next vblank to show the * restored VRAM — including a blank if display was disabled in the snapshot. */ @@ -833,27 +405,11 @@ extern "C" void psx_frontend_on_savestate_loaded(void) { s_frame_pacer = FramePacer{ 0 }; s_fps_last_time = 0; s_fps_last_frame = 0; - /* Re-anchor guest-cycle→sample budgeting (pump clears queued PCM too). */ - g_audio_cycle_resync = 1; - /* Depth24 FMV: drop ephemeral present hold/cutover so restored VRAM shows. - * Upload span came back with the GPU snap; treat MDEC as already active if - * depth24 is on so we don't full-black the first post-load presents. */ - gpu_depth24_on_savestate_loaded(); - s_d24_cutover_blank = 0; - s_d24_saw_gap = 0; - s_d24_prev_mdec = gpu_display_is_depth24() ? 1 : 0; - /* Depth24 load skipped framebuffer-sized CPU→GPU uploads — including the - * full-VRAM boot_state blit — so restage the mirror into the FBO/image - * before present. Otherwise post-FMV menus miss texture pages. Safe - * no-ops when that backend was never brought up. */ - gl_renderer_restage_vram_after_savestate(); - vk_renderer_restage_vram_after_savestate(); /* GL present-dirty early-out can skip SwapWindow when the restored frame * matches the last swap (typical on 2nd+ load of the same slot). Invalidate * tiles + force several swaps so the window actually updates. Safe no-op * when the GL pipeline was never brought up. */ gl_renderer_invalidate_present(); - post_load_probe_arm(); } static uint64_t smooth_60_frame_hash(const uint32_t* pixels, size_t count) { @@ -984,7 +540,22 @@ static int g_low_latency_input = 1; static int g_video_vsync = 1; static int g_frame_interpolation = 0; static int g_frame_interpolation_fps = 0; +static int g_frame_interpolation_blend = + PSX_MOD_FRAME_INTERPOLATION_LINEAR; +static int g_frame_interpolation_blend_default = + PSX_MOD_FRAME_INTERPOLATION_LINEAR; +static int g_mod_controller_mode_override[2] = { -1, -1 }; +static_assert((int)PSX_MOD_CONTROLLER_HYBRID == + (int)PSXRecompV4::PAD_MODE_HYBRID); +static_assert((int)PSX_MOD_CONTROLLER_ANALOG == + (int)PSXRecompV4::PAD_MODE_ANALOG); +static_assert((int)PSX_MOD_CONTROLLER_DIGITAL == + (int)PSXRecompV4::PAD_MODE_DIGITAL); static double g_host_refresh_hz = 0.0; +static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / 59.94; +static double g_frame_period_ms = PSX_FRAME_PERIOD_MS; +static bool g_mod_native_vblank_rate = false; +static uint32_t g_mod_native_vblank_fps = 0; /* Map the configured tri-state fullscreen mode (g_fullscreen) to the SDL * window-fullscreen flag: used both to open the window in that mode and to @@ -1020,6 +591,164 @@ extern "C" void debug_get_fmv_config(int *auto_skip, uint32_t *total_table, * in config_loader.h). */ static int g_video_aspect_num = 4; static int g_video_aspect_den = 3; +/* Resize-driven widescreen. The user's fixed aspect is still used to shape the + * initial window; after the game window exists these values follow its live + * aspect, clamped to 4:3..the widest mode offered by the title. */ +static bool g_ws_adaptive_view = false; +static int g_ws_adaptive_max_num = 16; +static int g_ws_adaptive_max_den = 9; + +extern "C" int psx_mod_set_fixed_display_aspect( + uint32_t numerator, uint32_t denominator) { + if (numerator == 0 || denominator == 0 || + numerator > 99 || denominator > 99 || + 3u * numerator < 4u * denominator || + 9u * numerator > 32u * denominator) { + std::fprintf(stderr, + "psxrecomp: mod rejected invalid display aspect %u:%u\n", + (unsigned)numerator, (unsigned)denominator); + return 0; + } + g_video_aspect_num = (int)numerator; + g_video_aspect_den = (int)denominator; + g_ws_adaptive_view = false; + std::fprintf(stdout, "psxrecomp: mod selected fixed display aspect %u:%u\n", + (unsigned)numerator, (unsigned)denominator); + return 1; +} + +extern "C" int psx_mod_set_adaptive_display_aspect( + uint32_t max_numerator, uint32_t max_denominator) { + if (max_numerator == 0 || max_denominator == 0 || + max_numerator > 99 || max_denominator > 99 || + 3u * max_numerator < 4u * max_denominator || + 9u * max_numerator > 32u * max_denominator) { + std::fprintf(stderr, + "psxrecomp: mod rejected invalid adaptive display aspect %u:%u\n", + (unsigned)max_numerator, (unsigned)max_denominator); + return 0; + } + g_ws_adaptive_view = true; + g_ws_adaptive_max_num = (int)max_numerator; + g_ws_adaptive_max_den = (int)max_denominator; + std::fprintf(stdout, + "psxrecomp: mod selected adaptive display aspect " + "(initial %d:%d, range 4:3 through %u:%u)\n", + g_video_aspect_num, g_video_aspect_den, + (unsigned)max_numerator, (unsigned)max_denominator); + return 1; +} + +extern "C" int psx_mod_set_native_vblank_rate( + uint32_t frames_per_second) { + if (frames_per_second != 0 && + (frames_per_second < 60 || frames_per_second > 1000)) { + std::fprintf(stderr, + "psxrecomp: mod rejected invalid native VBlank rate %u FPS\n", + (unsigned)frames_per_second); + return 0; + } + g_mod_native_vblank_rate = true; + g_mod_native_vblank_fps = frames_per_second; + g_frame_period_ms = frames_per_second + ? 1000.0 / (double)frames_per_second + : 0.0; + /* + * Above the physical panel rate (and in uncapped mode), swap-interval + * blocking would silently replace the requested guest cadence with the + * display cadence. The frame pacer owns fixed-rate timing here. + */ + if (frames_per_second == 0 || frames_per_second > 60) + g_video_vsync = 0; + if (frames_per_second) { + std::fprintf(stdout, + "psxrecomp: mod selected native guest VBlank pacing at %u FPS " + "(%.4f ms/frame)\n", + (unsigned)frames_per_second, g_frame_period_ms); + } else { + std::fprintf(stdout, + "psxrecomp: mod selected uncapped native guest VBlank pacing\n"); + } + return 1; +} + +extern "C" int psx_mod_set_frame_interpolation( + uint32_t frames_per_second) { + if (frames_per_second != 0 && + (frames_per_second < 60 || frames_per_second > 1000)) { + std::fprintf(stderr, + "psxrecomp: mod rejected invalid interpolated frame rate %u FPS\n", + (unsigned)frames_per_second); + return 0; + } + + /* + * Interpolation is a presentation feature of the OpenGL renderer. Mods are + * activated before the game window and renderer are created, so selecting + * it here is deterministic and does not require a live backend switch. + */ + g_video_renderer = 1; + g_video_vsync = 0; + g_frame_interpolation = 1; + g_frame_interpolation_fps = + frames_per_second ? (int)frames_per_second : 0; + + if (frames_per_second) { + std::fprintf(stdout, + "psxrecomp: mod selected presentation-only interpolation at " + "%u FPS; guest timing remains stock\n", + (unsigned)frames_per_second); + } else { + std::fprintf(stdout, + "psxrecomp: mod selected display-refresh presentation-only " + "interpolation; guest timing remains stock\n"); + } + return 1; +} + +extern "C" int psx_mod_set_frame_interpolation_blend( + uint32_t blend_mode) { + if (blend_mode != PSX_MOD_FRAME_INTERPOLATION_LINEAR && + blend_mode != PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE) { + std::fprintf(stderr, + "psxrecomp: mod rejected invalid frame-interpolation blend %u\n", + (unsigned)blend_mode); + return 0; + } + g_frame_interpolation_blend = (int)blend_mode; + std::fprintf(stdout, "psxrecomp: frame-interpolation blend = %s\n", + blend_mode == PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE + ? "motion-adaptive clarity" : "linear crossfade"); + return 1; +} + +extern "C" int psx_mod_set_auto_skip_fmv(int enabled) { + if (enabled != 0 && enabled != 1) { + std::fprintf(stderr, + "psxrecomp: mod rejected invalid auto-skip-FMV value %d\n", + enabled); + return 0; + } + g_auto_skip_fmv = enabled; + std::fprintf(stdout, "psxrecomp: mod %s automatic FMV skipping\n", + enabled ? "enabled" : "disabled"); + return 1; +} + +extern "C" int psx_mod_set_controller_mode_override( + uint32_t player, uint32_t controller_mode) { + if (player >= 2 || + controller_mode > (uint32_t)PSXRecompV4::PAD_MODE_DIGITAL) { + std::fprintf(stderr, + "psxrecomp: mod rejected controller-mode override " + "(player %u, mode %u)\n", + (unsigned)player, (unsigned)controller_mode); + return 0; + } + g_mod_controller_mode_override[player] = (int)controller_mode; + return 1; +} + /* [widescreen] per-game hooks (see config_loader.h): anchor scratch addr for * tagged sprite prims + HUD SPRT center-squash. Inert at 0/false. */ static uint32_t g_ws_anchor_addr = 0; @@ -1057,6 +786,53 @@ static void clamp_window_aspect(int* w, int* h, int num, int den) { *h = width * den / num; } +static int aspect_gcd(int a, int b) { + while (b) { int t = a % b; a = b; b = t; } + return a > 0 ? a : 1; +} + +/* Follow the host window without feeding its absolute pixel size into guest + * rendering. Only the ratio matters: gpu_ws_configure derives the PSX-native + * sidecar width from it, just as the fixed 16:9/21:9 modes do. */ +static void update_adaptive_widescreen() { + if (!g_ws_adaptive_view || !sdl_window) return; + + int width = 0, height = 0; + SDL_GetWindowSize(sdl_window, &width, &height); + if (width <= 0 || height <= 0) return; + + int num = width, den = height; + if ((int64_t)width * 3 <= (int64_t)height * 4) { + num = 4; den = 3; + } else if ((int64_t)width * g_ws_adaptive_max_den >= + (int64_t)height * g_ws_adaptive_max_num) { + num = g_ws_adaptive_max_num; + den = g_ws_adaptive_max_den; + } else { + int divisor = aspect_gcd(num, den); + num /= divisor; + den /= divisor; + } + if (num == g_video_aspect_num && den == g_video_aspect_den) return; + + g_video_aspect_num = num; + g_video_aspect_den = den; + gl_renderer_set_display_aspect(num, den); + if (sdl_renderer) { + g_logical_w = 480 * num * g_video_scale / den; + SDL_RenderSetLogicalSize(sdl_renderer, g_logical_w, 480 * g_video_scale); + } + + if (g_ws_engaged) { + const bool wide = num * 3 != den * 4; + const int mode = wide ? (g_ws_native_wide ? 2 : 1) : 0; + gte_set_display_aspect(mode == 1 ? num : 4, + mode == 1 ? den : 3); + gpu_ws_configure(num, den, g_ws_anchor_addr, + g_ws_hud_sprt ? 1 : 0, mode); + } +} + /* SDL GL attributes are global inputs to the next context creation. Set the * runtime requirements immediately before every GL window, because launcher * teardown resets them and macOS otherwise supplies a legacy 2.1 context. */ @@ -1124,6 +900,10 @@ static int16_t sdl_audio_buf[2048 * 2]; * thread. */ static rab_bridge s_drc; static bool s_drc_ready = false; +/* Per-game bridge fill target. 180 ms remains the compatibility default; + * [audio].buffer_ms may lower it for titles whose production cadence has been + * validated with less reserve. */ +static double g_audio_buffer_ms = 180.0; /* Observability + A/B. PSXRECOMP_AUDIO_LEGACY=1 keeps the historical push model * (SDL_QueueAudio, no bridge) so the underrun baseline can be measured against @@ -1142,7 +922,9 @@ static uint64_t g_legacy_underruns = 0; int g_audio_unmute_resync = 0; /* Actual device rate the host opened at (bridge mode may differ from 44100; * the T3 tap ring runs at this rate and its WAV dump must say so). */ -extern "C" int g_audio_host_rate = 44100; +extern "C" { +int g_audio_host_rate = 44100; +} static void sdl_drc_callback(void* /*user*/, Uint8* stream, int len) { if (!s_drc_ready) { std::memset(stream, 0, (size_t)len); return; } @@ -1270,13 +1052,25 @@ static void launcher_info(const char* title, const std::string& msg) { * config loads, before any interactive file resolution. */ static std::string s_picker_game_name = "PSXRecomp"; +/* BIOS selection state (docs/BIOS_SELECTION.md). s_openbios_allowed is the + * game's [runtime] openbios; s_bundled_bios_rel is where the shipped + * redistributable image lives relative to the executable. */ +static bool s_openbios_allowed = true; +#ifdef PSX_BUNDLED_BIOS_PATH +static std::string s_bundled_bios_rel = PSX_BUNDLED_BIOS_PATH; +#else +static std::string s_bundled_bios_rel = "bios/openbios.bin"; +#endif + static bool pick_runtime_file(const char* title, const char* filter, - std::filesystem::path& out) { + std::filesystem::path& out, const char* cli_flag) { // Headless: never open an interactive file dialog (it blocks). Fail the // resolve so boot aborts cleanly with the stderr message the caller printed. if (g_headless) { - std::fprintf(stderr, "psxrecomp: headless — cannot prompt for '%s'; " - "supply it via game.toml / --disc / --bios.\n", title); + std::fprintf(stderr, + "psxrecomp: headless — cannot prompt for '%s'.\n" + " Supply it on the command line: %s (or set it in game.toml).\n", + title, cli_flag); return false; } #ifdef _WIN32 @@ -1299,7 +1093,16 @@ static bool pick_runtime_file(const char* title, const char* filter, #else (void)filter; (void)out; - std::fprintf(stderr, "psxrecomp: %s requires a command-line path on this platform.\n", title); + // No native GUI file picker on this platform (macOS/Linux): the file must be + // named on the command line. Tell the user the exact flag + a full example + // instead of a dead-end "requires a command-line path". + std::fprintf(stderr, + "psxrecomp: no graphical file picker on this platform — '%s'\n" + " must be supplied on the command line: %s \n" + " Example: ./ --game game.toml " + "--bios /path/to/SCPH1001.BIN --disc /path/to/game.cue\n" + " (or set the path in game.toml). See the game's README, \"Building From Source\".\n", + title, cli_flag); return false; #endif } @@ -1376,37 +1179,24 @@ static bool validate_disc_for_launch(const std::filesystem::path& path, return true; } +// Canonicalize whichever member of a dump the user pointed us at. Picking the +// .cue and picking the .bin must both mount and both verify — see +// runtime/include/disc_path.h for the preference rules. This used to swap a +// .cue for its same-named .bin unconditionally, which silently discarded the +// cue's table of contents (and with it every CD-DA track and pregap) on +// single-file multi-track dumps. +// Pure: no dialog here. Every caller feeds the result to +// validate_disc_for_launch(), whose detail text already carries the resolver's +// note (identify_disc surfaces it), so a broken dump is reported exactly once +// instead of popping a modal on every boot. static std::filesystem::path normalize_disc_path_for_launch(const std::filesystem::path& path) { - namespace fs = std::filesystem; - std::error_code ec; - fs::path p = fs::absolute(path, ec); - if (ec) p = path; - - if (uppercase_ascii(p.extension().string()) == ".CUE") { - fs::path bin = p; - bin.replace_extension(".bin"); - if (fs::exists(bin, ec)) { - fs::path abs = fs::absolute(bin, ec); - return ec ? bin : abs; - } - ec.clear(); - bin.replace_extension(".BIN"); - if (fs::exists(bin, ec)) { - fs::path abs = fs::absolute(bin, ec); - return ec ? bin : abs; - } - } - - return p; + return PSXRecompV4::resolve_disc_path(path).mount; } -/* BIOS selection state (docs/BIOS_SELECTION.md). s_openbios_allowed is the - * game's [runtime] openbios; s_bundled_bios_rel is where the shipped - * redistributable image lives relative to the executable. */ -static bool s_openbios_allowed = true; -static std::string s_bundled_bios_rel = PSX_BUNDLED_BIOS_PATH; - -/* Identity-match a file against a linked backend. Size+CRC must agree — +/* Which compiled-in backend, if any, was recompiled from THIS file? + * + * The chosen file is only ever DATA: the code that executes is the statically + * recompiled C of one specific image, so running it against another image's * bytes is a guaranteed wild jump. Identity therefore decides WHICH backend * may run, not merely whether to warn. Null = no linked image matches. */ @@ -1486,6 +1276,11 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested, /* 1. An explicit choice: --bios, else a remembered pick. */ std::filesystem::path chosen; if (requested_is_explicit && requested && requested[0]) { + /* --bios, or a BIOS named in settings.toml. NOT the compile-time + * default: that is a build constant, not something the player asked + * for, and treating it as a choice made a stray file near the exe + * produce a "BIOS Mismatch" dialog before the bundled image was even + * considered. */ chosen = resolve_bios_path(requested, argv0); } else { std::filesystem::path cached = read_cached_path(argv0, "bios.cfg"); @@ -1493,6 +1288,8 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested, } if (!chosen.empty() && std::filesystem::exists(chosen)) { if (validate_bios_for_launch(chosen)) return chosen; /* activates it */ + /* Mismatched. With OpenBIOS available this is recoverable, so say so + * and carry on rather than dead-ending on a file they chose once. */ if (!(openbios_allowed && bundled)) return {}; } @@ -1521,18 +1318,16 @@ static std::filesystem::path resolve_bios_for_runtime(const char* requested, "Step 1 of 2 — PlayStation BIOS\n\n" "In the next window, select your PlayStation BIOS dump. This build " "requires the exact image it was compiled from: " + accepted + ". " - "Usually named SCPH1001.BIN and exactly 512 KB. Dump from your own " - "console or otherwise legally obtain it.\n\n" - "(This is NOT the game disc — that is asked for next.)"); + "You must dump it from your own console or otherwise legally obtain " + "it.\n\n(This is NOT the game disc — that is asked for next.)"); std::string bios_title = - s_picker_game_name + " — Step 1 of 2: select PlayStation BIOS (" + - accepted + ")"; + s_picker_game_name + " — Step 1 of 2: select PlayStation BIOS (" + accepted + ")"; for (;;) { std::filesystem::path picked; if (!pick_runtime_file( bios_title.c_str(), "PlayStation BIOS (*.bin)\0*.bin\0All Files (*.*)\0*.*\0", - picked)) { + picked, "--bios")) { return {}; } if (validate_bios_for_launch(picked)) { @@ -1591,7 +1386,7 @@ static std::filesystem::path resolve_disc_for_runtime(const std::filesystem::pat if (!pick_runtime_file( disc_title.c_str(), "PS1 Disc Images (*.cue;*.bin;*.iso)\0*.cue;*.bin;*.iso\0All Files (*.*)\0*.*\0", - picked)) { + picked, "--disc")) { return {}; } picked = normalize_disc_path_for_launch(picked); @@ -1605,17 +1400,31 @@ static std::filesystem::path resolve_disc_for_runtime(const std::filesystem::pat static std::filesystem::path resolve_bios_path(const char* requested, const char* argv0) { namespace fs = std::filesystem; std::error_code ec; - if (!requested || !requested[0]) return {}; fs::path p(requested); if (fs::exists(p, ec)) { fs::path abs = fs::absolute(p, ec); return ec ? p : abs; } + // Either BIOS filename convention is acceptable: a dump folder holding + // "US-PSX-SCPH1001.BIN" satisfies a request for "SCPH1001.BIN" and vice + // versa (see recompiler/include/bios_rom_alias.h). + if (fs::path aliased = PSXRecompV4::resolve_bios_rom(p); aliased != p) { + fs::path abs = fs::absolute(aliased, ec); + return ec ? aliased : abs; + } if (p.is_absolute()) return p; // Anchor on the exe directory — never cwd (see exe_dir_from_argv). fs::path found = find_upward(exe_dir_from_argv(argv0), p); if (!found.empty()) return found / p; + // Same walk, accepting the other naming convention at each rung: the + // literal name is absent but a region-qualified sibling may be present. + for (fs::path dir = fs::absolute(exe_dir_from_argv(argv0), ec); + !dir.empty(); dir = dir.parent_path()) { + const fs::path aliased = PSXRecompV4::resolve_bios_rom(dir / p); + if (aliased != dir / p && fs::exists(aliased, ec)) return aliased; + if (!dir.has_parent_path() || dir.parent_path() == dir) break; + } // Dev-checkout rung: game projects keep the framework at // /psxrecomp-v4 (junction/worktree), so a relative default like @@ -1685,6 +1494,11 @@ static void shutdown_runtime(void) { * off-thread JIT worker here; the worker no longer exists.) */ psx_netplay_shutdown(); memcard_flush_all(); + /* Stop and join the active external compiler before capture/debug teardown. + * Otherwise closing the window can leave cmd/python/gcc running against + * cache and capture files while the main thread performs synchronous + * shutdown work, making the window appear frozen until that tree exits. */ + autocompile_shutdown(); overlay_autocapture_shutdown(); overlay_capture_wait_pending(); overlay_capture_write_json(); @@ -1725,11 +1539,6 @@ static void teardown_game_session_keep_lobby(void) { if (sdl_pixel_buf) { std::free(sdl_pixel_buf); sdl_pixel_buf = nullptr; } psx_lobby_set_ready(0); psx_lobby_clear_launch_pending(); -#if defined(PSX_HAS_LOBBY_CLIENT) - /* Drop prior-match ICE SDP/candidates; ignore new ones until next launch. */ - psx_lobby_clear_signals(); - psx_lobby_set_ice_signal_accept(0); -#endif psx_clear_return_to_lobby(); } @@ -1810,16 +1619,6 @@ static void sdl_audio_pump(bool discard_output = false) { static uint64_t last_cycles = 0; static uint64_t cycle_carry = 0; const uint64_t now_cycles = psx_cycle_count; - if (g_audio_cycle_resync) { - last_cycles = now_cycles; - cycle_carry = 0; - g_audio_cycle_resync = 0; - if (legacy) - psx_sdl_audio_clear(sdl_audio_device); - else - g_audio_unmute_resync = 1; /* skip mute-drain underrun reports */ - return; - } if (last_cycles == 0) last_cycles = now_cycles; uint64_t delta = (now_cycles - last_cycles) + cycle_carry; last_cycles = now_cycles; @@ -1854,7 +1653,8 @@ static void sdl_audio_pump(bool discard_output = false) { if (legacy) { /* T3 tap: the exact post-fade bytes handed to the host audio queue. */ audio_trace_pcm(AUDIO_TAP_HOST, sdl_audio_buf, frames); - psx_sdl_audio_queue(sdl_audio_device, sdl_audio_buf, (uint32_t)frames * bytes_per_frame); + psx_sdl_audio_queue(sdl_audio_device, sdl_audio_buf, + (uint32_t)frames * bytes_per_frame); had_audio = 1; } else { /* Hand to the bridge (band-limited resample + DRC) instead of @@ -1882,12 +1682,14 @@ static void sdl_audio_pump(bool discard_output = false) { * across the first ~50 ms of samples (sdl_audio_pump above). */ /* Bridge/legacy output health, surfaced through the audio_stats TCP command * (debug_server.c) — no stderr probe; rule 3. */ -extern "C" int psx_audio_out_stats(double *fill_ms, uint64_t *underruns, +extern "C" int psx_audio_out_stats(double *fill_ms, double *target_ms, + uint64_t *underruns, uint64_t *overflow_drops, double *correction, int *legacy, int *host_rate) { *legacy = audio_legacy_mode() ? 1 : 0; *host_rate = g_audio_host_rate; + *target_ms = g_audio_buffer_ms; if (*legacy || !s_drc_ready) { *fill_ms = sdl_audio_device ? (double)psx_sdl_audio_queued_size(sdl_audio_device) @@ -2140,11 +1942,11 @@ static void runtime_perf_diag_tick() { RuntimePerfSnapshot current = runtime_perf_snapshot(now); AudioTraceStats audio; audio_trace_get_stats(&audio); - double fill_ms = 0.0, correction = 0.0; + double fill_ms = 0.0, target_ms = 0.0, correction = 0.0; uint64_t underruns = 0, overflows = 0; int legacy = 0, host_rate = 0; - psx_audio_out_stats(&fill_ms, &underruns, &overflows, &correction, - &legacy, &host_rate); + psx_audio_out_stats(&fill_ms, &target_ms, &underruns, &overflows, + &correction, &legacy, &host_rate); uint64_t up[6] = {0}; gl_renderer_runtime_diag(up); uint64_t overlay_load_us = 0, overlay_load_max_us = 0, overlay_load_last_us = 0; @@ -2165,7 +1967,7 @@ static void runtime_perf_diag_tick() { (double)g_runtime_perf.frequency; std::fprintf(stdout, "psxrecomp: runtime cadence: guest=%.2f Hz, spu=%.1f Hz, " - "audio_fill=%.1f ms, underruns=+%llu, overflows=+%llu, corr=%+.5f; " + "audio_fill=%.1f/%.1f ms, underruns=+%llu, overflows=+%llu, corr=%+.5f; " "GL upload=%.1f calls/s %.1f rect/s %.2f Mpix/s, " "cpu=%.1f tex=%.1f draw=%.1f ms/s; " "work guest=%.1f pacer=%.1f autocapture=%.1f provider_poll=%.1f ms/s, " @@ -2177,7 +1979,7 @@ static void runtime_perf_diag_tick() { "capture triggers=+%u overlays=+%d last_dispatch_delta=%llu\n", (double)(current.frame - last.frame) / dt, (double)(audio.tap_frames[AUDIO_TAP_SPU_OUT] - last_spu) / dt, - fill_ms, (unsigned long long)(underruns - last_underruns), + fill_ms, target_ms, (unsigned long long)(underruns - last_underruns), (unsigned long long)(overflows - last_overflows), correction, (double)(up[0] - last_up[0]) / dt, (double)(up[1] - last_up[1]) / dt, @@ -2245,7 +2047,8 @@ static void sdl_audio_update(int hard_mute_active, int turbo_sink_active) { audio_trace_event(AUDIO_EV_MUTE, (uint32_t)tail, 0); if (audio_legacy_mode()) { audio_trace_pcm(AUDIO_TAP_HOST, sdl_audio_buf, tail); - psx_sdl_audio_queue(sdl_audio_device, sdl_audio_buf, (uint32_t)tail * sizeof(int16_t) * 2u); + psx_sdl_audio_queue(sdl_audio_device, sdl_audio_buf, + (uint32_t)tail * sizeof(int16_t) * 2u); } else if (s_drc_ready) { psx_sdl_audio_lock(sdl_audio_device); rab_push(&s_drc, sdl_audio_buf, tail); @@ -2327,9 +2130,8 @@ struct PsxButtonMap { }; static int controller_device_index = 0; -/* Default ~10% of SDL axis range (32767). Overridden per-player via settings. */ -static int controller_deadzone = 3277; -static constexpr int kDefaultDeadzoneRaw = 3277; +static int controller_deadzone = 12000; +static int controller_anti_deadzone = 0; static std::array controller_map = {{ { PAD_UP, "up", {} }, { PAD_DOWN, "down", {} }, @@ -2580,7 +2382,7 @@ static std::string default_input_ini_text(void) { "[controller]\n" "enabled = true\n" "device = 0\n" - "deadzone = 3277\n" + "deadzone = 12000\n" "\n" "[mapping]\n" "up = dpup,lefty-\n" @@ -2671,23 +2473,15 @@ static void close_player(PlayerInput& p) { } static void close_controller(void) { - for (int s = 0; s < PSX_MAX_PLAYERS; s++) - close_player(g_players[s]); -} - -static int device_claimed_by_other(int self_slot, SDL_JoystickID inst) { - for (int o = 0; o < PSX_MAX_PLAYERS; o++) { - if (o == self_slot) continue; - if (g_players[o].handle && g_players[o].instance == inst) return 1; - } - return 0; + close_player(g_players[0]); + close_player(g_players[1]); } /* Open the SDL controller whose GUID matches p.guid. If no exact GUID match * exists (e.g. a different physical unit of the same model, or Steam's virtual * pad at an unpredictable slot), fall back to the first controller not already - * claimed by another player. */ -static void open_player(PlayerInput& p, int self_slot) { + * claimed by the other player. */ +static void open_player(PlayerInput& p, const PlayerInput& other) { if (p.kind != 2 || p.handle) return; int chosen = -1, fallback = -1; @@ -2697,9 +2491,9 @@ static void open_player(PlayerInput& p, int self_slot) { SDL_JoystickGUID g = SDL_JoystickGetDeviceGUID(i); char buf[40] = {0}; SDL_JoystickGetGUIDString(g, buf, sizeof(buf)); - /* Skip a device already opened by another player. */ + /* Skip a device already opened by the other player. */ SDL_JoystickID inst = SDL_JoystickGetDeviceInstanceID(i); - if (device_claimed_by_other(self_slot, inst)) continue; + if (other.handle && other.instance == inst) continue; if (p.guid[0] && std::strcmp(buf, p.guid) == 0) { chosen = i; break; } if (fallback < 0) fallback = i; } @@ -2727,43 +2521,24 @@ static int pad_mode_boot_analog(int mode) { mode == PSXRecompV4::PAD_MODE_HYBRID) ? 1 : 0; } -/* Keyboard has no DualShock sticks/config handshake — always present as a - * plain digital pad (SCPH-1080). Leaving keyboard slots in ANALOG/HYBRID made - * P2–P5 show as connected while games that expect digital multitap pads never - * saw usable button input. */ -static int effective_player_mode(const PlayerInput& p) { - if (p.kind == 1) return (int)PSXRecompV4::PAD_MODE_DIGITAL; - return p.mode; -} - -/* Pad mode for the SIO seat this sample will drive. Multitap taps are always - * plain digital (0x41) — DualShock on a SCPH-1070 tap is not reliable. */ -static int effective_player_mode_for_sio(const PlayerInput& p, int sio_slot) { - if (sio_pad_on_multitap(sio_slot)) - return (int)PSXRecompV4::PAD_MODE_DIGITAL; - return effective_player_mode(p); -} - /* Open/close SDL handles so they match g_players, and (re)assert each slot's * PSX connection + pad type. Safe to call repeatedly (hotplug, boot). * While delay-sync netplay is active, SIO connection/type are owned by * psx_netplay (session slots stay plugged); only refresh host SDL handles. */ static void refresh_player_devices(void) { const int netplay = psx_netplay_active(); - for (int s = 0; s < PSX_MAX_PLAYERS; s++) { + for (int s = 0; s < 2; s++) { PlayerInput& p = g_players[s]; if (p.kind != 2) close_player(p); /* keyboard/none: no handle */ - else open_player(p, s); + else open_player(p, g_players[s ^ 1]); if (netplay) continue; - const int mode = effective_player_mode_for_sio(p, s); sio_set_pad_connected(s, p.kind != 0 ? 1 : 0); - sio_set_pad_analog(s, pad_mode_boot_analog(mode), 0x80, 0x80, 0x80, 0x80); + sio_set_pad_analog(s, pad_mode_boot_analog(p.mode), 0x80, 0x80, 0x80, 0x80); /* DIGITAL mode == a plain digital controller that ignores the DualShock * config-mode commands (real SCPH-1080 behaviour); ANALOG/HYBRID == a * config-capable DualShock. A digital pad that wrongly answered 0x43 - * sent Tomba 2's pad driver down the config path -> phantom 0x00 reads. - * Multitap taps are always digital (see sio_pad_on_multitap). */ - sio_set_pad_config_capable(s, mode != PSXRecompV4::PAD_MODE_DIGITAL); + * sent Tomba 2's pad driver down the config path -> phantom 0x00 reads. */ + sio_set_pad_config_capable(s, p.mode != PSXRecompV4::PAD_MODE_DIGITAL); } } @@ -2796,10 +2571,8 @@ static void set_player_device(PlayerInput& p, const std::string& dev, int mode) * component became a D-Pad bit. Analog stick bytes still use radial rescale * in axes_to_pad_pair; this path is digital buttons only. Triggers share the * same per-axis threshold. */ -static bool controller_source_pressed_h(SDL_GameController* h, const ControllerSource& source, - int deadzone_raw) { +static bool controller_source_pressed_h(SDL_GameController* h, const ControllerSource& source) { if (!h) return false; - const int dz = deadzone_raw > 0 ? deadzone_raw : controller_deadzone; switch (source.kind) { case ControllerSource::Kind::Button: @@ -2809,8 +2582,8 @@ static bool controller_source_pressed_h(SDL_GameController* h, const ControllerS case ControllerSource::Kind::AxisNegative: { const int16_t v = SDL_GameControllerGetAxis(h, (SDL_GameControllerAxis)source.id); if (source.kind == ControllerSource::Kind::AxisPositive) - return v > dz; - return v < -dz; + return v > controller_deadzone; + return v < -controller_deadzone; } case ControllerSource::Kind::None: default: @@ -2849,14 +2622,13 @@ static bool source_is_stick_axis(const ControllerSource& s) { * left/right analog-stick axes do NOT contribute button bits — see * source_is_stick_axis above. Digital mode passes false, so the stick still * folds onto the D-pad (its only outlet there). */ -static uint16_t controller_pad_buttons(SDL_GameController* h, bool suppress_stick_axes, - int deadzone_raw) { +static uint16_t controller_pad_buttons(SDL_GameController* h, bool suppress_stick_axes) { uint16_t buttons = 0xFFFF; /* all released */ if (!h) return buttons; for (const auto& entry : controller_map) { for (const auto& source : entry.sources) { if (suppress_stick_axes && source_is_stick_axis(source)) continue; - if (controller_source_pressed_h(h, source, deadzone_raw)) { + if (controller_source_pressed_h(h, source)) { buttons &= (uint16_t)~entry.bit; break; } @@ -2865,41 +2637,17 @@ static uint16_t controller_pad_buttons(SDL_GameController* h, bool suppress_stic return buttons; } -/* Radial deadzone: process a stick's X and Y together so the dead region is a - * small CIRCLE, not a per-axis square. Travel within controller_deadzone reads - * centred (0x80/0x80); beyond it the vector MAGNITUDE is rescaled to the full - * range along the stick's true direction, so diagonals are preserved (a - * per-axis deadzone snaps diagonals toward the cardinals and feels notchy — - * bad for directional analog like Ape Escape's net swing). The magnitude is - * capped at 32767 before rescale so a full-diagonal push (raw mag ~46341) maps - * to ~0x9E/0x9E per axis — the circular gate a real DualShock stick reports, - * not 0xFF/0xFF. At dz==0 it reduces to a plain magnitude-preserving map. */ -static void axes_to_pad_pair(int16_t vx, int16_t vy, uint8_t* obx, uint8_t* oby, - int deadzone_raw) { - const double dz = (double)(deadzone_raw > 0 ? deadzone_raw : controller_deadzone); - double x = vx, y = vy; - double mag = std::sqrt(x * x + y * y); /* 0 .. ~46341 */ - if (mag <= dz || mag <= 0.0) { *obx = 0x80; *oby = 0x80; return; } - double capped = mag > 32767.0 ? 32767.0 : mag; - double range = 32767.0 - dz; - if (range < 1.0) range = 1.0; - double newmag = (capped - dz) * 32767.0 / range; /* 0 .. 32767 */ - double scale = newmag / mag; /* along true direction */ - int sx = (int)std::lround(x * scale); - int sy = (int)std::lround(y * scale); - if (sx > 32767) sx = 32767; else if (sx < -32768) sx = -32768; - if (sy > 32767) sy = 32767; else if (sy < -32768) sy = -32768; - int bx = (sx + 32768) >> 8; - int by = (sy + 32768) >> 8; - *obx = (uint8_t)(bx < 0 ? 0 : (bx > 255 ? 255 : bx)); - *oby = (uint8_t)(by < 0 ? 0 : (by > 255 ? 255 : by)); +static void axes_to_pad_pair(int16_t vx, int16_t vy, uint8_t* obx, uint8_t* oby) { + psx_stick_to_dualshock(vx, vy, + controller_deadzone, controller_anti_deadzone, + obx, oby); } /* Buttons for a player's selected device (0xFFFF = none pressed). `player` is - * 1..5 — selects which keybinds.ini section drives a keyboard port. */ + * 1 or 2 — selects which keybinds.ini section drives a keyboard port. */ static uint16_t pad_buttons_for(const PlayerInput& p, int player, bool suppress_stick_axes) { if (p.kind == 1) return pad_from_keyboard(player); - if (p.kind == 2) return controller_pad_buttons(p.handle, suppress_stick_axes, p.deadzone); + if (p.kind == 2) return controller_pad_buttons(p.handle, suppress_stick_axes); return 0xFFFF; } @@ -2929,10 +2677,10 @@ static void pad_sticks_for(const PlayerInput& p, int player, uint8_t out[4], boo if (p.kind == 2 && p.handle) { axes_to_pad_pair(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTX), SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTY), - &out[0], &out[1], p.deadzone); + &out[0], &out[1]); axes_to_pad_pair(SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_RIGHTX), SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_RIGHTY), - &out[2], &out[3], p.deadzone); + &out[2], &out[3]); if (fold_dpad) { if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_LEFT)) out[0] = 0x00; if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) out[0] = 0xFF; @@ -2947,22 +2695,50 @@ static void pad_sticks_for(const PlayerInput& p, int player, uint8_t out[4], boo * player has reached for analog. hybrid_dpad_active: any D-pad direction (or, * for the keyboard, an arrow key) is held — the player wants classic digital. * The keyboard has no analog stick, so a keyboard player stays digital. */ -static bool hybrid_stick_active(const PlayerInput& p) { - if (p.kind != 2 || !p.handle) return false; - const double lx = SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTX); - const double ly = SDL_GameControllerGetAxis(p.handle, SDL_CONTROLLER_AXIS_LEFTY); - const double dz = (double)(p.deadzone > 0 ? p.deadzone : controller_deadzone); - return std::sqrt(lx * lx + ly * ly) > dz; +static bool controller_stick_active(SDL_GameController* handle) { + if (!handle) return false; + const double lx = + SDL_GameControllerGetAxis(handle, SDL_CONTROLLER_AXIS_LEFTX); + const double ly = + SDL_GameControllerGetAxis(handle, SDL_CONTROLLER_AXIS_LEFTY); + return std::sqrt(lx * lx + ly * ly) > (double)controller_deadzone; } -static bool hybrid_dpad_active(const PlayerInput& p, int player, bool kb_always) { - if (p.kind == 2 && p.handle) { - if (SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_LEFT) || - SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) || - SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_UP) || - SDL_GameControllerGetButton(p.handle, SDL_CONTROLLER_BUTTON_DPAD_DOWN)) - return true; +static bool controller_dpad_active(SDL_GameController* handle) { + return handle && + (SDL_GameControllerGetButton(handle, SDL_CONTROLLER_BUTTON_DPAD_LEFT) || + SDL_GameControllerGetButton(handle, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) || + SDL_GameControllerGetButton(handle, SDL_CONTROLLER_BUTTON_DPAD_UP) || + SDL_GameControllerGetButton(handle, SDL_CONTROLLER_BUTTON_DPAD_DOWN)); +} +static bool hybrid_stick_active(const PlayerInput& p, bool dev_any) { + if (p.kind == 2 && controller_stick_active(p.handle)) return true; + if (dev_any) { + const int n = SDL_NumJoysticks(); + for (int i = 0; i < n; i++) { + if (!SDL_IsGameController(i)) continue; + const SDL_JoystickID inst = SDL_JoystickGetDeviceInstanceID(i); + SDL_GameController* handle = + SDL_GameControllerFromInstanceID(inst); + if (!handle) handle = SDL_GameControllerOpen(i); + if (controller_stick_active(handle)) return true; + } } - if (p.kind == 1 || kb_always) { + return false; +} +static bool hybrid_dpad_active(const PlayerInput& p, int player, bool dev_any) { + if (p.kind == 2 && controller_dpad_active(p.handle)) return true; + if (dev_any) { + const int n = SDL_NumJoysticks(); + for (int i = 0; i < n; i++) { + if (!SDL_IsGameController(i)) continue; + const SDL_JoystickID inst = SDL_JoystickGetDeviceInstanceID(i); + SDL_GameController* handle = + SDL_GameControllerFromInstanceID(inst); + if (!handle) handle = SDL_GameControllerOpen(i); + if (controller_dpad_active(handle)) return true; + } + } + if (p.kind == 1 || dev_any) { const Uint8* keys = SDL_GetKeyboardState(NULL); if (psx_keybinds_dpad_active(keys, player)) return true; } @@ -2986,16 +2762,13 @@ static bool hybrid_dpad_active(const PlayerInput& p, int player, bool kb_always) * pad TYPE is left unchanged: a launcher-assigned analog DualShock still presents * as analog (so the game's analog input path / SIO handshake cadence is preserved * exactly), and merged sources only contribute button/stick STATE, never a type - * downgrade. - * - * Default OFF: each player slot accepts input only from its launcher-assigned - * device (keyboard XOR that controller). Opt in with PSX_DEV_INPUT=1 for the - * old "any device drives P1" workflow. */ + * downgrade. Controlled by PSX_DEV_INPUT (default ON for the dev workflow); set + * PSX_DEV_INPUT=0 to restore strict single-device-per-port routing. */ static bool dev_any_input_enabled() { static int cached = -1; if (cached < 0) { const char* e = std::getenv("PSX_DEV_INPUT"); - cached = (e && (e[0] == '1' || e[0] == 'y' || e[0] == 'Y' || e[0] == 't' || e[0] == 'T')) ? 1 : 0; + cached = (e && (e[0] == '0' || e[0] == 'n' || e[0] == 'N' || e[0] == 'f' || e[0] == 'F')) ? 0 : 1; } return cached != 0; } @@ -3014,7 +2787,7 @@ static uint16_t dev_all_controllers_buttons(bool suppress_stick_axes) { SDL_JoystickID inst = SDL_JoystickGetDeviceInstanceID(i); SDL_GameController* h = SDL_GameControllerFromInstanceID(inst); if (!h) h = SDL_GameControllerOpen(i); /* open once; SDL keeps it */ - if (h) btn &= controller_pad_buttons(h, suppress_stick_axes, controller_deadzone); + if (h) btn &= controller_pad_buttons(h, suppress_stick_axes); } return btn; } @@ -3033,11 +2806,9 @@ static void dev_any_controller_sticks(uint8_t st[4]) { if (!h) continue; uint8_t lx, ly, rx, ry; axes_to_pad_pair(SDL_GameControllerGetAxis(h, SDL_CONTROLLER_AXIS_LEFTX), - SDL_GameControllerGetAxis(h, SDL_CONTROLLER_AXIS_LEFTY), - &lx, &ly, controller_deadzone); + SDL_GameControllerGetAxis(h, SDL_CONTROLLER_AXIS_LEFTY), &lx, &ly); axes_to_pad_pair(SDL_GameControllerGetAxis(h, SDL_CONTROLLER_AXIS_RIGHTX), - SDL_GameControllerGetAxis(h, SDL_CONTROLLER_AXIS_RIGHTY), - &rx, &ry, controller_deadzone); + SDL_GameControllerGetAxis(h, SDL_CONTROLLER_AXIS_RIGHTY), &rx, &ry); if (lx != 0x80 || ly != 0x80) { st[0] = lx; st[1] = ly; } if (rx != 0x80 || ry != 0x80) { st[2] = rx; st[3] = ry; } } @@ -3066,10 +2837,10 @@ static void apply_input_override_to_sio(int override_word) { st[2] != 0x80 || st[3] != 0x80); const bool dpad_live = ((uint16_t)~w & 0x00F0u) != 0; /* up/right/down/left */ - int mode; - if (p.kind != 0) mode = effective_player_mode(p); - else if (dev_any_input_enabled()) mode = (int)PSXRecompV4::PAD_MODE_HYBRID; - else mode = (int)PSXRecompV4::PAD_MODE_DIGITAL; + /* Debug injection follows the game's resolved mode even when P1 has no + * assigned device. This keeps dev-any-input from manufacturing Hybrid for + * a title that deliberately exposes only Analog / D-Pad. */ + const int mode = p.mode; int eff_analog; if (mode == (int)PSXRecompV4::PAD_MODE_DIGITAL) { @@ -3096,9 +2867,10 @@ static int capture_pad_slot(int s, PsxNetPad* out) { out->connected = 0; PlayerInput& p = g_players[s]; - const int player = s + 1; /* keybinds.ini section (1..5) */ - /* Opt-in dev merge: P1 is driven by the keyboard AND every connected - * controller (PSX_DEV_INPUT=1). Default is strict per-slot routing. */ + const int player = s + 1; /* keybinds.ini section (1|2) */ + /* Dev input: P1 is driven by the keyboard AND every connected controller, + * so a tester can navigate from whatever is plugged in (P2 keeps strict + * per-port routing). */ const bool dev_here = (dev_any_input_enabled() && s == 0); if (p.kind == 0 && !dev_here) return 0; /* no device in this port */ @@ -3106,22 +2878,18 @@ static int capture_pad_slot(int s, PsxNetPad* out) { * state gates how the left stick is read for BOTH the button word and the * analog axes below. An assigned device keeps its configured mode (a * launcher-selected analog DualShock stays analog, so its input path / SIO - * handshake cadence is preserved exactly). Keyboard is always digital. - * Multitap taps are forced digital. A P1 with no assigned device but - * dev-any-input on presents as HYBRID. */ - int mode; - if (sio_pad_on_multitap(s)) - mode = (int)PSXRecompV4::PAD_MODE_DIGITAL; - else if (p.kind != 0) mode = effective_player_mode(p); - else if (dev_here) mode = (int)PSXRecompV4::PAD_MODE_HYBRID; - else mode = (int)PSXRecompV4::PAD_MODE_DIGITAL; + * handshake cadence is preserved exactly). A P1 with no assigned device + * keeps the game's resolved mode while dev-any-input merges the keyboard and + * all connected controllers. This is important for games that deliberately + * do not support Hybrid. */ + const int mode = p.mode; int eff_analog; if (mode == PSXRecompV4::PAD_MODE_DIGITAL) { eff_analog = 0; } else if (mode == PSXRecompV4::PAD_MODE_ANALOG) { eff_analog = 1; } else { /* HYBRID */ - if (hybrid_stick_active(p)) p.hybrid_analog = true; + if (hybrid_stick_active(p, dev_here)) p.hybrid_analog = true; else if (hybrid_dpad_active(p, player, dev_here)) p.hybrid_analog = false; eff_analog = p.hybrid_analog ? 1 : 0; } @@ -3171,9 +2939,8 @@ static int capture_pad_slot(int s, PsxNetPad* out) { /* Netplay-only capture: assigned PlayerInput for this slot only. Never merges * keyboard-all / all-controllers (dev_any_input). Same pad-mode / stick rules - * as capture_pad_slot otherwise. present_sio_slot is the delay-sync seat this - * blob will publish to (may differ from host card `s` on guests). */ -static int capture_pad_slot_exclusive(int s, PsxNetPad* out, int present_sio_slot) { + * as capture_pad_slot otherwise. */ +static int capture_pad_slot_exclusive(int s, PsxNetPad* out) { if (!out) return 0; out->buttons = 0xFFFFu; out->lx = out->ly = out->rx = out->ry = 0x80u; @@ -3181,19 +2948,18 @@ static int capture_pad_slot_exclusive(int s, PsxNetPad* out, int present_sio_slo out->connected = 0; PlayerInput& p = g_players[s]; - const int player = s + 1; /* keybinds.ini section (1..5) */ + const int player = s + 1; /* keybinds.ini section (1|2) */ const bool dev_here = false; if (p.kind == 0) return 0; /* no device in this port */ - const int sio_slot = (present_sio_slot >= 0) ? present_sio_slot : s; - int mode = effective_player_mode_for_sio(p, sio_slot); + int mode = p.mode; int eff_analog; if (mode == PSXRecompV4::PAD_MODE_DIGITAL) { eff_analog = 0; } else if (mode == PSXRecompV4::PAD_MODE_ANALOG) { eff_analog = 1; } else { /* HYBRID */ - if (hybrid_stick_active(p)) p.hybrid_analog = true; + if (hybrid_stick_active(p, dev_here)) p.hybrid_analog = true; else if (hybrid_dpad_active(p, player, dev_here)) p.hybrid_analog = false; eff_analog = p.hybrid_analog ? 1 : 0; } @@ -3216,8 +2982,6 @@ static int capture_pad_slot_exclusive(int s, PsxNetPad* out, int present_sio_slo } static void apply_pad_slot_to_sio(int s, const PsxNetPad& pad) { - if (sio_pad_on_multitap(s)) - sio_set_pad_config_capable(s, 0); sio_set_pad_state_slot(s, pad.buttons); sio_set_pad_sticks(s, pad.lx, pad.ly, pad.rx, pad.ry); sio_request_pad_type(s, pad.analog ? 1 : 0); @@ -3225,42 +2989,22 @@ static void apply_pad_slot_to_sio(int s, const PsxNetPad& pad) { /* Local human pad for delay-sync: sample the host PlayerInput selected for * this peer (see --net-input-player / auto), then recomp-net maps that blob - * onto local_slot (lobby seat → sim P1/P2/…). Never writes SIO. Exclusive + * onto local_slot (host→sim P1, guest→sim P2). Never writes SIO. Exclusive * capture — no keyboard-all / all-controllers merge — so peers hash-agree. */ static void capture_local_human_pad(PsxNetPad* out) { int idx = psx_netplay_input_player(); - if (idx < 0 || idx >= PSX_MAX_PLAYERS) idx = 0; - /* Present as the lobby seat (multitap taps → digital), not the host card. */ - const int seat = psx_netplay_local_slot(); - const int present = (seat >= 0) ? seat : idx; - if (capture_pad_slot_exclusive(idx, out, present)) { - out->connected = 1; - psx_netplay_normalize_pad(out); - return; - } - /* Fallbacks: NETPLAY/P1 card, lobby seat card, then any assigned device. */ - if (idx != 0 && capture_pad_slot_exclusive(0, out, present)) { - out->connected = 1; - psx_netplay_normalize_pad(out); - return; - } - if (seat >= 0 && seat < PSX_MAX_PLAYERS && seat != idx && seat != 0 && - capture_pad_slot_exclusive(seat, out, present)) { - out->connected = 1; - psx_netplay_normalize_pad(out); - return; - } - for (int s = 0; s < PSX_MAX_PLAYERS; ++s) { - if (s == idx || s == 0 || s == seat) continue; - if (capture_pad_slot_exclusive(s, out, present)) { + if (idx < 0 || idx > 1) idx = 0; + if (!capture_pad_slot_exclusive(idx, out)) { + /* Fallback: if auto picked empty P2, try P1 (two-machine guest). */ + if (idx != 0 && capture_pad_slot_exclusive(0, out)) { out->connected = 1; psx_netplay_normalize_pad(out); return; } + out->buttons = 0xFFFFu; + out->lx = out->ly = out->rx = out->ry = 0x80u; + out->analog = 1; } - out->buttons = 0xFFFFu; - out->lx = out->ly = out->rx = out->ry = 0x80u; - out->analog = 1; out->connected = 1; psx_netplay_normalize_pad(out); } @@ -3278,10 +3022,7 @@ static void capture_override_pad(int override_word, PsxNetPad* out) { st[2] != 0x80 || st[3] != 0x80); const bool dpad_live = ((uint16_t)~w & 0x00F0u) != 0; - int mode; - if (p.kind != 0) mode = effective_player_mode(p); - else if (dev_any_input_enabled()) mode = (int)PSXRecompV4::PAD_MODE_HYBRID; - else mode = (int)PSXRecompV4::PAD_MODE_DIGITAL; + const int mode = p.mode; int eff_analog; if (mode == (int)PSXRecompV4::PAD_MODE_DIGITAL) { @@ -3322,13 +3063,7 @@ static int netplay_timing_on(void) { * sample per sim tick; stalls on INPUT_CONFIRM desync. */ static void netplay_barrier_admit(int override) { if (!psx_netplay_active()) return; - /* Launcher/game window teardown can leave a queued SDL_QUIT; draining it - * prevents an instant soft-return before the first frame. */ - SDL_PumpEvents(); - SDL_FlushEvent(SDL_QUIT); static int desync_logged = 0; - const Uint64 barrier_t0 = SDL_GetTicks64(); - Uint64 last_stall_log_ms = barrier_t0; const uint64_t admit_t0 = netplay_timing_on() ? SDL_GetPerformanceCounter() : 0; /* Guest quantum = time since previous admit returned (fiber ran). */ @@ -3338,7 +3073,6 @@ static void netplay_barrier_admit(int override) { } for (;;) { uint32_t dt = 0, lh = 0, rh = 0; - const Uint64 now_ms = SDL_GetTicks64(); /* Load apply/ready suppresses INPUT (and can sit silent for seconds on * a hash-match .pst). timeout=0 still honors BYE (peer_gone) but does * not treat rx silence as disconnect — that was kicking both peers to @@ -3348,51 +3082,6 @@ static void netplay_barrier_admit(int override) { netplay_soft_exit("netplay_peer_disconnect"); if (psx_return_to_lobby_requested()) return; } - /* Staged .pst rejected (stale codegen / BIOS / missing) — do not wait - * out the 90s load barrier with stall=load_apply_done. */ - if (psx_netplay_consume_load_apply_failed()) { - netplay_soft_exit("netplay_load_failed"); - if (psx_return_to_lobby_requested()) return; - } - /* Mutual INPUT/CONFIRM stall still refreshes last_peer_rx — detect - * "no sim progress" separately (common rematch + TURN loss mode). - * Save/load/memcard probe+chunk xfer uses a longer budget (TURN + - * ~1.4MB .pst). The old 20s admit timeout killed SAVE mid-transfer. */ - if (psx_netplay_in_load_barrier() && now_ms - barrier_t0 >= 90000u) { - char stall[96]; - uint32_t sim = 0; - int lead = 0; - psx_netplay_admit_wait_info(stall, sizeof(stall), &sim, &lead); - std::fprintf(stderr, - "psxrecomp: netplay state barrier timeout sim=%u " - "stall=%s lead=%d — returning to lobby\n", - (unsigned)sim, stall[0] ? stall : "?", lead); - netplay_soft_exit("netplay_load_stall"); - if (psx_return_to_lobby_requested()) return; - } else if (!psx_netplay_in_load_barrier() && - now_ms - barrier_t0 >= 20000u) { - char stall[64]; - uint32_t sim = 0; - int lead = 0; - psx_netplay_admit_wait_info(stall, sizeof(stall), &sim, &lead); - std::fprintf(stderr, - "psxrecomp: netplay admit stall timeout sim=%u " - "stall=%s lead=%d — returning to lobby\n", - (unsigned)sim, stall[0] ? stall : "?", lead); - netplay_soft_exit("netplay_admit_stall"); - if (psx_return_to_lobby_requested()) return; - } else if (now_ms - last_stall_log_ms >= 2000u) { - char stall[96]; - uint32_t sim = 0; - int lead = 0; - psx_netplay_admit_wait_info(stall, sizeof(stall), &sim, &lead); - std::fprintf(stderr, - "psxrecomp: netplay admit waiting sim=%u stall=%s " - "lead=%d (%llums)\n", - (unsigned)sim, stall[0] ? stall : "?", lead, - (unsigned long long)(now_ms - barrier_t0)); - last_stall_log_ms = now_ms; - } psx_lobby_pump(); if (psx_netplay_input_desync(&dt, &lh, &rh)) { if (!desync_logged) { @@ -3440,16 +3129,14 @@ static void netplay_barrier_admit(int override) { netplay_soft_exit("sdl_window_close"); if (psx_return_to_lobby_requested()) return; } - if (ev.type == SDL_KEYDOWN) { + if (ev.type == SDL_KEYDOWN && #if defined(PSX_SDL3) - const SDL_Keycode key = ev.key.key; + ev.key.key == SDLK_ESCAPE) { #else - const SDL_Keycode key = ev.key.keysym.sym; + ev.key.keysym.sym == SDLK_ESCAPE) { #endif - if (key == SDLK_ESCAPE) { - netplay_soft_exit("netplay_barrier_escape"); - if (psx_return_to_lobby_requested()) return; - } + netplay_soft_exit("netplay_barrier_escape"); + if (psx_return_to_lobby_requested()) return; } if (ev.type == SDL_CONTROLLERDEVICEADDED || ev.type == SDL_CONTROLLERDEVICEREMOVED) { @@ -3473,10 +3160,7 @@ static void sample_pad_into_sio(int override) { apply_input_override_to_sio(override); return; } - int n = g_offline_pad_count; - if (n < 1) n = 1; - if (n > PSX_MAX_PLAYERS) n = PSX_MAX_PLAYERS; - for (int s = 0; s < n; s++) { + for (int s = 0; s < 2; s++) { PsxNetPad pad; if (!capture_pad_slot(s, &pad)) continue; /* no device in this port */ /* Push sticks every frame; request the pad type (digital/analog) through @@ -3521,12 +3205,10 @@ static void sample_headless_pad_into_sio(int override) { * do not fight — a fixed 59.94 pacer against a 60.00 Hz panel makes rendered * frames land on an uneven vblank count (a 2/3/1 beat) that reads as * moving-object judder/flicker. See g_frame_period_ms. */ -static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / 59.94; /* Live pacer period (ms). Defaults to the PSX rate; set to the host refresh * period when the panel is within ~2% of 60 Hz so 30fps content pads evenly to * two host refreshes. Left at the PSX rate on non-~60Hz panels to avoid running * the sim at the wrong speed. */ -static double g_frame_period_ms = PSX_FRAME_PERIOD_MS; /* ── Host-stack-usage profile (RECURSION_BUG.md §17) ────────────────────────── * The decisive instrument for the long-run freeze. The guest call graph mirrors @@ -3676,52 +3358,17 @@ static void load_transition_note(int read_active, int load_active, prev_turbo = turbo_active; } -/* Tick MotK-style depth24 cutover state once per present. Arm a short full- - * frame blank when MDEC returns after an idle gap (or after leaving depth24). */ -static void depth24_cutover_tick(int depth24) { - const int mdec_on = depth24 && mdec_recently_active(3); - if (!depth24) { - s_d24_prev_mdec = 0; - s_d24_cutover_blank = 0; - s_d24_saw_gap = 1; /* next depth24+MDEC is a fresh movie cutover */ - return; - } - if (!mdec_on) - s_d24_saw_gap = 1; - if (s_d24_saw_gap && mdec_on && !s_d24_prev_mdec) { - /* Hide the transitional present(s) that still show stale RGB888 junk. */ - s_d24_cutover_blank = 2; - s_d24_saw_gap = 0; - } - s_d24_prev_mdec = mdec_on; -} - -/* Depth24 FMV: CRTC width stays full (e.g. MotK 512) while MDEC uploads may - * not cover the right side yet — leftover VRAM read as RGB888 flashes junk. - * Present width is never shrunk; black-fill only the trailing uncovered cols. +/* Depth24 FMV: trailing RGB columns can be stale while CRTC width stays full + * (MotK 512). Present width is never shrunk — cropping the GL/SDL rect left a + * flickering black pillar when upload coverage varied. * - * Match origin/master's span policy: when upload span is unknown (lim==0), - * only blank the last ~8 columns. Blanking [0..w) on lim==0 (an older tip - * path) turned MotK FMV into a permanent black screen after GP1(07h) hold - * reset the span — hold ticks skip Swap, then the next present saw lim=0 - * and wiped the whole frame every vblank. - * - * Optional short cutover blank (tip): full-frame black for 1–2 presents when - * MDEC returns after an idle gap, hiding one-frame transitional junk. */ + * Always black-fill the last 8 RGB cols. Also blank beyond the tracked FB A0 + * span when it falls short of the CRTC (movie cut / partial cover). Do NOT + * replicate the last good column — that smeared MotK's starfield into an + * 8-wide streak. */ static void depth24_fix_trailing_margin(uint32_t *buf, uint32_t w, uint32_t h, uint32_t display_x) { if (!buf || w < 8u || h == 0u) return; - - if (s_d24_cutover_blank > 0) { - s_d24_cutover_blank--; - const uint32_t n = w * h; - for (uint32_t i = 0; i < n; i++) - buf[i] = 0xFF000000u; - return; - } - - /* Default: last 8 columns. If the upload span is known and ends earlier - * inside that margin, start blanking from the span edge instead. */ uint32_t start = w - 8u; uint32_t lim = gpu_depth24_rgb_limit(display_x, w); if (lim > 0u && lim < w && lim < start) @@ -3734,16 +3381,6 @@ static void depth24_fix_trailing_margin(uint32_t *buf, uint32_t w, uint32_t h, /* Called from gpu_vblank_tick() at each simulated vblank. */ static void sdl_vblank_present(void) { - int probe_turbo = 0; - int probe_reached = 0; - struct PostLoadProbeScope { - int *turbo; - int *reached; - ~PostLoadProbeScope() { - post_load_probe_on_vblank(*turbo, *reached); - } - } probe_scope{&probe_turbo, &probe_reached}; - #ifndef PSX_NO_DEBUG_TOOLS debug_server_set_fmv_quiet(mdec_recently_active(2)); /* Debug server: pause gate, poll commands, record frame, check watchpoints. */ @@ -3770,14 +3407,14 @@ static void sdl_vblank_present(void) { * than presents so turbo and skipped-frame modes still report game speed. * Skip during netplay post-load barrier — admit is stalled and the window * is not updating, so a climbing FPS line is misleading. */ - if (!psx_netplay_in_load_barrier()) { + if (fps_telemetry_enabled() && !psx_netplay_in_load_barrier()) { extern uint64_t s_frame_count; const Uint64 now = SDL_GetPerformanceCounter(); const Uint64 frequency = SDL_GetPerformanceFrequency(); if (!s_fps_last_time) { s_fps_last_time = now; s_fps_last_frame = s_frame_count; - if (sdl_window) { + if (sdl_window && s_fps_base_title.empty()) { const char *title = SDL_GetWindowTitle(sdl_window); if (title) s_fps_base_title = title; } @@ -3853,15 +3490,13 @@ static void sdl_vblank_present(void) { } else if (ev.type == SDL_CONTROLLERDEVICEADDED) { refresh_player_devices(); } else if (ev.type == SDL_CONTROLLERDEVICEREMOVED) { - bool ours = false; - for (int s = 0; s < PSX_MAX_PLAYERS; s++) { #if defined(PSX_SDL3) - if (ev.gdevice.which == g_players[s].instance) { ours = true; break; } + if (ev.gdevice.which == g_players[0].instance || + ev.gdevice.which == g_players[1].instance) { #else - if (ev.cdevice.which == g_players[s].instance) { ours = true; break; } + if (ev.cdevice.which == g_players[0].instance || + ev.cdevice.which == g_players[1].instance) { #endif - } - if (ours) { close_controller(); refresh_player_devices(); } @@ -3951,62 +3586,30 @@ static void sdl_vblank_present(void) { if (psx_return_to_lobby_requested()) return; netplay_barrier_admit(override_); if (skip_pace_ || psx_return_to_lobby_requested()) return; - /* Post-starvation / behind-peer catch-up: skip wall pace so admits - * can burn down remote tip (mirrors snes_host_catchup_budget). - * Never unpace during depth24 FMV — catch-up would race XA/video - * ahead of wall clock (~90fps) and make movies play too fast. */ - if (!gpu_display_is_depth24() && psx_netplay_catchup_budget() > 0) { - psx_netplay_catchup_consume_frame(); - return; - } uint64_t perf_start = runtime_perf_section_begin(); - frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); + if (g_frame_period_ms > 0.0) + frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks); latency_ring_mark(LAT_PACED); } } netplay_tail(override); - /* Turbo-active / multitap arming share game-started detection. */ - extern int fntrace_is_game_started(void); - if (psx_netplay_active()) { psx_netplay_finish_frame(); + } else if (g_headless) { + sample_headless_pad_into_sio(override); } else { - /* Offline N-pad: enable multitap only once the game EXE is running. - * Port comes from game.toml [controller] multitap_port (default Port 1; - * BPE uses Port 2). Empty tap slots are OK — P1 may be the standalone - * pad on the other port. */ - if (g_offline_pad_count >= 3 && fntrace_is_game_started() && - !sio_get_multitap()) { - sio_set_multitap(1); - /* Tap seats drop to plain digital as soon as the tap is live. */ - for (int s = 0; s < PSX_MAX_PLAYERS; ++s) { - if (!sio_pad_on_multitap(s)) continue; - sio_set_pad_config_capable(s, 0); - sio_set_pad_analog(s, 0, 0x80, 0x80, 0x80, 0x80); - } - std::fprintf(stdout, - "psxrecomp: multitap armed (console Port %d; " - "tap pads forced digital)\n", - sio_get_multitap_port() + 1); - } - if (g_headless) - sample_headless_pad_into_sio(override); - else - sample_pad_into_sio(override); + sample_pad_into_sio(override); } /* Latency ring: open this present cycle's slot, stamping when input was * sampled into SIO. Always-on; queried via the debug server "latency". */ latency_ring_frame_begin(); - /* Post-savestate CD delay boost (ReadTOC/seek clamp window). */ - cdrom_savestate_boost_vblank(); - /* Turbo-active test shared by the pacing/present gate below. */ int turbo_loads_active = 0; - int logical_load_active = fntrace_is_game_started() && - (cdrom_load_in_progress() || cdrom_savestate_cd_wait_active()); + extern int fntrace_is_game_started(void); + int logical_load_active = fntrace_is_game_started() && cdrom_load_in_progress(); int load_run_value = 0; static int load_run = 0; static int release_run = 0; @@ -4037,7 +3640,6 @@ static void sdl_vblank_present(void) { * old fast_boot snapshot restore; all guest timing is authentic. */ if (psx_bios_hle_boot_turbo_active()) turbo_loads_active = 1; - probe_turbo = turbo_loads_active; load_transition_note(cdrom_data_read_active(), logical_load_active, turbo_loads_active, load_run_value); @@ -4158,12 +3760,12 @@ static void sdl_vblank_present(void) { } } - /* Netplay FMV: skip present every other depth24 vblank to cut GPU cost - * (admit + wall pace still run in the RAII tail every tick). Do NOT skip - * pace here — that let MotK movies run ~90fps once decode was fast enough. */ + /* Netplay FMV: skip present every other depth24 vblank (admit still runs + * in the RAII tail). Present-first frame, then alternate. */ if (psx_netplay_active() && gpu_display_is_depth24()) { if (s_netplay_depth24_present_skip) { s_netplay_depth24_present_skip = 0; + netplay_tail.skip_pace(); return; } s_netplay_depth24_present_skip = 1; @@ -4175,7 +3777,8 @@ static void sdl_vblank_present(void) { * AFTER present so Swap overlaps the peer's guest quantum. */ if (!psx_netplay_active()) { uint64_t perf_start = runtime_perf_section_begin(); - frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); + if (g_frame_period_ms > 0.0) + frame_pacer_wait(&s_frame_pacer, g_frame_period_ms); runtime_perf_section_end(perf_start, &g_runtime_perf.pacer_ticks); latency_ring_mark(LAT_PACED); @@ -4194,11 +3797,16 @@ static void sdl_vblank_present(void) { /* Mod hooks. Run after all normal input sampling. */ mod_call_frame_hooks(); + /* Resize-driven mode updates the pending aspect during BIOS boot too, but + * the actual wide compositor remains disengaged until game entry. */ + update_adaptive_widescreen(); + + /* Resize-driven mode updates the pending aspect during BIOS boot too, but + * the actual wide compositor remains disengaged until game entry. */ + update_adaptive_widescreen(); + /* Depth24 GP1(07h) retarget (MotK intro→crawl): keep the prior Swap for a - * few vblanks so stale trailing VRAM never flashes on the right edge. - * Must tick every present — gpu.c arms s_d24_present_hold and also - * freezes upload-span tracking while hold > 0; without this call the hold - * sticks and depth24_fix_trailing_margin blanks the whole FMV forever. */ + * few vblanks so stale trailing VRAM never flashes on the right edge. */ if (gpu_depth24_present_hold_tick()) return; @@ -4207,7 +3815,8 @@ static void sdl_vblank_present(void) { extern int fntrace_is_game_started(void); if (fntrace_is_game_started()) { g_ws_engaged = true; - int mode = g_ws_native_wide ? 2 : 1; + const bool wide = g_video_aspect_num * 3 != g_video_aspect_den * 4; + int mode = wide ? (g_ws_native_wide ? 2 : 1) : 0; /* Native-wide: GTE drawn un-squashed — feed it the 4:3 ratio * (identity squash). Squash mode: feed the real wide aspect. */ gte_set_display_aspect(mode == 1 ? g_video_aspect_num : 4, @@ -4218,7 +3827,6 @@ static void sdl_vblank_present(void) { } /* ---- Display from our VRAM ---- */ - probe_reached = 1; uint32_t w = 0, h = 0; uint32_t present_w = 0; /* display width actually presented (w + native-wide EXTRA) */ int active_scale = 1; /* hi-res mirror used only for 15-bit display */ @@ -4232,7 +3840,6 @@ static void sdl_vblank_present(void) { GpuDisplayInfo di; gpu_get_display_info(&di); depth24_frame = di.depth24 != 0; - depth24_cutover_tick(depth24_frame ? 1 : 0); if (di.disabled || di.width == 0 || di.height == 0) { smooth_60_present(nullptr, 0, 0, false); present_ring_commit(PRES_PATH_BLANK, (uint16_t)di.width, @@ -4343,8 +3950,8 @@ static void sdl_vblank_present(void) { for (uint32_t x = 0; x < present_w; x++) sdl_pixel_buf[y * present_w + x] = gpu_display_pixel_argb(&di, x, y); - /* Trailing / cutover blank: black-fill uncovered RGB cols - * inside the full-width buffer — never shrink present width. */ + /* Trailing margin: blank junk cols inside the full-width + * buffer — never shrink present width. */ depth24_fix_trailing_margin(sdl_pixel_buf, present_w, h, di.display_x); vk_renderer_present_cpu(sdl_pixel_buf, (int)present_w, (int)h, @@ -4408,9 +4015,9 @@ static void sdl_vblank_present(void) { } } - /* Depth24 trailing / cutover blank: MotK CRTC is 512 RGB but uploads - * may not cover the right side yet (colorful junk flash). Fix pixels - * in-place at full present_w — never crop the GL/SDL draw width. */ + /* Depth24 trailing margin: MotK CRTC is 512 RGB but trailing cols + * can be stale. Fix pixels in-place at full present_w — never crop the + * GL/SDL draw width (that caused a flickering black pillar). */ if (di.depth24 && active_scale == 1 && !wide_present) depth24_fix_trailing_margin(sdl_pixel_buf, present_w, h, di.display_x); @@ -4491,8 +4098,10 @@ static void sdl_vblank_present(void) { int dst_w = pin_43 ? 640 * g_video_scale : g_logical_w; int dst_h = 480 * g_video_scale; SDL_Rect dst = { (g_logical_w - dst_w) / 2, 0, dst_w, dst_h }; - /* Match GL: short display bands letterbox inside the 4:3 rect. */ - if (pin_43 && h > 0 && h < 240) { + /* Match GL: short display bands letterbox inside the 4:3 rect — FMV only + * (depth24), and only genuinely windowed bands (<80% of the field; a + * native short display mode like 216/224 fills the rect). */ + if (pin_43 && depth24_frame && h > 0 && h < 192) { int content_h = (dst_h * (int)h) / 240; if (content_h < 1) content_h = 1; dst.y = (dst_h - content_h) / 2; @@ -4540,119 +4149,6 @@ namespace { std::string g_lnch_expected_serial; uint32_t g_lnch_expected_crc = 0; bool g_lnch_has_crc = false; - const char* g_lnch_argv0 = nullptr; - - int ae_bios_verify(const char* bios_path, RecompLauncherCBiosVerify* out) { - if (!out) return 0; - std::memset(out, 0, sizeof(*out)); - /* Empty path = use bundled OpenBIOS when this title allows it. */ - if (!bios_path || !bios_path[0]) { - if (s_openbios_allowed && psx_bios_bundled()) { - out->ok = 1; - std::snprintf(out->detail, sizeof(out->detail), - "Using bundled OpenBIOS."); - return 1; - } - std::snprintf(out->detail, sizeof(out->detail), - "PlayStation BIOS required (SCPH1001.BIN)."); - return 1; - } - std::ifstream f(bios_path, std::ios::binary | std::ios::ate); - if (!f.is_open()) { - std::snprintf(out->detail, sizeof(out->detail), "BIOS file not found."); - return 1; - } - const std::streamoff size = f.tellg(); - if (size != 512 * 1024) { - std::snprintf(out->detail, sizeof(out->detail), - "BIOS must be exactly 512 KiB (got %lld). Use SCPH1001.BIN.", - (long long)size); - return 1; - } - std::vector data((size_t)size); - if (!read_at(f, 0, data.data(), data.size())) { - std::snprintf(out->detail, sizeof(out->detail), "Failed to read BIOS file."); - return 1; - } - const uint32_t crc = crc32_compute(data.data(), data.size()); - out->ok = 1; - if (crc != 0x37157331u) { - out->warn = 1; - std::snprintf(out->detail, sizeof(out->detail), - "CRC32 %08X (validated dump is SCPH1001 CRC32 37157331). " - "Boot may still work.", - crc); - } else { - std::snprintf(out->detail, sizeof(out->detail), "SCPH1001.BIN (CRC OK)."); - } - return 1; - } - - int ae_prepare_disc(const char* source_path, char* out_disc_path, size_t out_cap, - char* err_msg, size_t err_cap) { - if (!source_path || !source_path[0] || !out_disc_path || out_cap == 0) return 0; - out_disc_path[0] = '\0'; - if (err_msg && err_cap) err_msg[0] = '\0'; - namespace fs = std::filesystem; - std::error_code ec; - if (!fs::is_regular_file(source_path, ec)) { - if (err_msg && err_cap) - std::snprintf(err_msg, err_cap, "Source dump not found."); - return 0; - } - const fs::path exe_dir = exe_dir_from_argv(g_lnch_argv0 ? g_lnch_argv0 : ""); - const fs::path root = find_upward(exe_dir, "tools/prepare_disc.py"); - if (root.empty()) { - if (err_msg && err_cap) - std::snprintf(err_msg, err_cap, - "tools/prepare_disc.py not found near the executable."); - return 0; - } - const fs::path script = root / "tools" / "prepare_disc.py"; - const fs::path out_dir = root / "motk"; - fs::create_directories(out_dir, ec); - /* Prefer python3; fall back to python (Windows). */ - const char* py = "python3"; -#if defined(_WIN32) - /* On Windows `python` is the usual launcher; python3 may be absent. */ - py = "python"; -#endif - std::string cmd = std::string(py) + " \"" + script.string() + "\" \"" + - source_path + "\" --out-dir \"" + out_dir.string() + "\""; -#if !defined(_WIN32) - /* If python3 missing, retry with python. */ - int rc = std::system(cmd.c_str()); - if (rc != 0) { - cmd = std::string("python \"") + script.string() + "\" \"" + source_path + - "\" --out-dir \"" + out_dir.string() + "\""; - rc = std::system(cmd.c_str()); - } -#else - int rc = std::system(cmd.c_str()); -#endif - if (rc != 0) { - if (err_msg && err_cap) - std::snprintf(err_msg, err_cap, - "prepare_disc.py failed (exit %d). Check the dump is " - "2448 bytes/sector.", - rc); - return 0; - } - const fs::path cue = - out_dir / "Star Wars - Masters of Teras Kasi (USA).cue"; - const fs::path bin = - out_dir / "Star Wars - Masters of Teras Kasi (USA).bin"; - fs::path playable = fs::exists(cue, ec) ? cue : bin; - if (!fs::exists(playable, ec)) { - if (err_msg && err_cap) - std::snprintf(err_msg, err_cap, - "prepare_disc finished but no .cue/.bin was written."); - return 0; - } - playable = normalize_disc_path_for_launch(playable); - std::snprintf(out_disc_path, out_cap, "%s", playable.string().c_str()); - return 1; - } int ae_disc_verify(const char* disc_path, RecompLauncherCDiscVerify* out) { if (!disc_path || !disc_path[0] || !out) return 0; @@ -4683,400 +4179,50 @@ namespace { } std::string g_lnch_netplay_game_name; - int g_lnch_game_players = 2; /* from game.toml; lobby max_slots default */ std::filesystem::path g_lnch_settings_path; std::string g_lnch_lobby_url; RecompLauncherCNetplayLaunch g_lnch_pending_direct_launch{}; - int g_lnch_lobby_input_delay = 2; - int g_lnch_force_input_relay = 0; - /* Default on: CGNAT-safe relay-only ICE (BattleShip-style online path). */ - int g_lnch_force_turn = 1; - int g_lnch_host_max_slots = 2; - - /* Delay-sync READY/START waits for every seat in slot_count. Use seated - * players (not lobby max_slots) so a 3/5 room can start. Sparse seats - * (moved) still need width covering the highest occupied index. */ - static int ae_np_session_slot_count(int player_count, int max_slots, - int local_slot, int game_fallback) { - int slots = player_count >= 2 ? player_count - : (max_slots >= 2 ? max_slots - : (game_fallback >= 2 ? game_fallback : 2)); - if (local_slot + 1 > slots) slots = local_slot + 1; - if (slots < 2) slots = 2; - if (slots > PSX_MAX_PLAYERS) slots = PSX_MAX_PLAYERS; - return slots; - } bool g_lnch_hosting_lan = false; bool g_lnch_joined_lan = false; - /* Join Direct / cross-machine: membership via UDP, not the local file. */ - bool g_lnch_remote_lan = false; std::string g_lnch_lan_endpoint; - uint32_t g_lnch_lan_session_id = 1; + std::string g_lnch_lan_guest_bind; - static constexpr int kAeLanMaxSlots = RECOMP_LAUNCHER_NETPLAY_MAX_MEMBERS; struct AeLanLobbyState { std::string name; std::string game; std::string endpoint; std::string host_name; - std::string joiner_name; /* legacy / first guest mirror */ + std::string joiner_name; std::string password; bool started = false; int host_slot = 0; - int max_slots = 2; - uint32_t session_id = 1; - std::string slot_name[kAeLanMaxSlots]; - std::string slot_id[kAeLanMaxSlots]; /* stable per-seat player id */ }; - AeLanLobbyState g_lnch_remote_lan_state{}; - int g_lnch_lan_my_slot = -1; - std::string g_lnch_lan_player_id; /* local process identity for LAN seats */ - -#ifdef _WIN32 - using AeLanSock = SOCKET; - static constexpr AeLanSock kAeLanSockInvalid = INVALID_SOCKET; -#else - using AeLanSock = int; - static constexpr AeLanSock kAeLanSockInvalid = -1; -#endif - AeLanSock g_lnch_lan_udp = kAeLanSockInvalid; - sockaddr_in g_lnch_lan_peers[kAeLanMaxSlots]{}; - bool g_lnch_lan_peer_ok[kAeLanMaxSlots]{}; - uint32_t g_lnch_lan_join_pulse_ms = 0; - /* LAN list latency from last Refresh probe; -1 unknown. */ - int g_lnch_lan_latency_ms = -1; std::filesystem::path ae_np_lan_file() { return std::filesystem::current_path() / "netplay_lan_lobby.txt"; } - static void ae_np_lan_sock_close(AeLanSock* s) { - if (!s || *s == kAeLanSockInvalid) return; -#ifdef _WIN32 - closesocket(*s); -#else - close(*s); -#endif - *s = kAeLanSockInvalid; - } - - static void ae_np_lan_udp_close(void) { - ae_np_lan_sock_close(&g_lnch_lan_udp); - for (int i = 0; i < kAeLanMaxSlots; ++i) - g_lnch_lan_peer_ok[i] = false; - } - - static void ae_np_lan_sync_legacy_names(AeLanLobbyState& state) { - if (state.max_slots < 2) state.max_slots = 2; - if (state.max_slots > kAeLanMaxSlots) state.max_slots = kAeLanMaxSlots; - if (state.host_slot < 0 || state.host_slot >= state.max_slots) - state.host_slot = 0; - if (!state.slot_name[state.host_slot].empty()) - state.host_name = state.slot_name[state.host_slot]; - else if (!state.host_name.empty()) - state.slot_name[state.host_slot] = state.host_name; - state.joiner_name.clear(); - for (int i = 0; i < state.max_slots; ++i) { - if (i == state.host_slot) continue; - if (!state.slot_name[i].empty()) { - state.joiner_name = state.slot_name[i]; - break; - } - } + bool ae_np_read_lan_state(AeLanLobbyState* state) { + if (!state) return false; + std::ifstream f(ae_np_lan_file()); + if (!f) return false; + std::string started, host_slot; + std::getline(f, state->name); + std::getline(f, state->game); + std::getline(f, state->endpoint); + std::getline(f, state->host_name); + std::getline(f, state->joiner_name); + std::getline(f, started); + std::getline(f, host_slot); + std::getline(f, state->password); + state->started = started == "1"; + state->host_slot = host_slot == "1" ? 1 : 0; + return !state->endpoint.empty(); } - static int ae_np_lan_occupied(const AeLanLobbyState& state) { - int n = 0; - for (int i = 0; i < state.max_slots && i < kAeLanMaxSlots; ++i) - if (!state.slot_name[i].empty()) ++n; - return n; - } - - static int ae_np_lan_find_free_slot(const AeLanLobbyState& state) { - for (int i = 0; i < state.max_slots && i < kAeLanMaxSlots; ++i) { - if (i == state.host_slot) continue; - if (state.slot_name[i].empty()) return i; - } - return -1; - } - - /* Guest seat by display name. Never match host_slot — same-machine - * instances often share settings.toml player_name, and reclaiming the - * host seat made JOIN "succeed" with no visible joiner. */ - static int ae_np_lan_find_guest_slot_by_name(const AeLanLobbyState& state, - const char* name) { - if (!name || !name[0]) return -1; - for (int i = 0; i < state.max_slots && i < kAeLanMaxSlots; ++i) { - if (i == state.host_slot) continue; - if (state.slot_name[i] == name) return i; - } - return -1; - } - - static int ae_np_lan_find_slot_by_id(const AeLanLobbyState& state, - const char* player_id) { - if (!player_id || !player_id[0]) return -1; - for (int i = 0; i < state.max_slots && i < kAeLanMaxSlots; ++i) { - if (state.slot_id[i] == player_id) return i; - } - return -1; - } - - /* Stable per-process LAN identity (prefer WS player_id when connected). */ - static const char* ae_np_lan_local_player_id(void) { - if (!g_lnch_lan_player_id.empty()) return g_lnch_lan_player_id.c_str(); - const char* ws = psx_lobby_player_id(); - if (ws && ws[0]) { - g_lnch_lan_player_id = ws; - return g_lnch_lan_player_id.c_str(); - } - unsigned char b[16]; - bool ok = false; -#if !defined(_WIN32) - FILE* ur = std::fopen("/dev/urandom", "rb"); - if (ur) { - ok = std::fread(b, 1, sizeof(b), ur) == sizeof(b); - std::fclose(ur); - } -#endif - if (!ok) { - uint64_t t = (uint64_t)SDL_GetTicks() - ^ ((uint64_t)(uintptr_t)&g_lnch_lan_player_id << 32) - ^ ((uint64_t)SDL_GetPerformanceCounter() << 17); - for (size_t i = 0; i < sizeof(b); ++i) { - t = t * 6364136223846793005ull + 1ull; - b[i] = (unsigned char)(t >> 56); - } - } - char hex[40]; - std::snprintf(hex, sizeof(hex), - "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", - b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], - b[10], b[11], b[12], b[13], b[14], b[15]); - g_lnch_lan_player_id = hex; - return g_lnch_lan_player_id.c_str(); - } - - /* Exact-name uniqueness among seats: Alex, Alex (2), Alex (3), … */ - static std::string ae_np_lan_unique_display_name(const AeLanLobbyState& state, - const char* requested, - int skip_slot) { - std::string base = (requested && requested[0]) ? requested : "Player"; - auto taken = [&](const std::string& n) { - for (int i = 0; i < state.max_slots && i < kAeLanMaxSlots; ++i) { - if (i == skip_slot) continue; - if (state.slot_name[i] == n) return true; - } - return false; - }; - if (!taken(base)) return base; - for (int n = 2; n <= 64; ++n) { - char cand[96]; - std::snprintf(cand, sizeof(cand), "%s (%d)", base.c_str(), n); - if (!taken(cand)) return cand; - } - return base + " (" + std::string(ae_np_lan_local_player_id()).substr(0, 8) + ")"; - } - - /* Seat or reconnect a guest by player_id; uniquify display name on insert. */ - static int ae_np_lan_seat_guest(AeLanLobbyState& st, const char* player_id, - const char* requested_name) { - if (!player_id || !player_id[0]) return -1; - int slot = ae_np_lan_find_slot_by_id(st, player_id); - if (slot >= 0) { - if (slot == st.host_slot) return -1; - st.slot_name[slot] = - ae_np_lan_unique_display_name(st, requested_name, slot); - return slot; - } - slot = ae_np_lan_find_free_slot(st); - if (slot < 0) return -1; - st.slot_id[slot] = player_id; - st.slot_name[slot] = - ae_np_lan_unique_display_name(st, requested_name, -1); - return slot; - } - - static void ae_np_lan_clear_peer_slot(int slot) { - if (slot < 0 || slot >= kAeLanMaxSlots) return; - g_lnch_lan_peer_ok[slot] = false; - } - - static void ae_np_lan_set_peer_slot(int slot, const sockaddr_in& addr) { - if (slot < 0 || slot >= kAeLanMaxSlots) return; - g_lnch_lan_peers[slot] = addr; - g_lnch_lan_peer_ok[slot] = true; - } - - static int ae_np_lan_endpoint_port(const std::string& endpoint) { - const size_t colon = endpoint.rfind(':'); - if (colon == std::string::npos) return 7777; - const int p = std::atoi(endpoint.c_str() + colon + 1); - return (p > 0 && p <= 65535) ? p : 7777; - } - - static bool ae_np_lan_endpoint_host(const std::string& endpoint, char* host, - size_t host_len) { - if (!host || host_len == 0) return false; - const size_t colon = endpoint.rfind(':'); - if (colon == std::string::npos || colon == 0) return false; - if (colon >= host_len) return false; - std::memcpy(host, endpoint.data(), colon); - host[colon] = '\0'; - return host[0] != '\0'; - } - - static bool ae_np_lan_set_nonblock(AeLanSock s) { -#ifdef _WIN32 - u_long mode = 1; - return ioctlsocket(s, FIONBIO, &mode) == 0; -#else - const int fl = fcntl(s, F_GETFL, 0); - return fl >= 0 && fcntl(s, F_SETFL, fl | O_NONBLOCK) == 0; -#endif - } - - /* Exclusive bind probe (no SO_REUSEADDR) so a busy port is detected. */ - static bool ae_np_udp_port_available(int port) { - if (port <= 0 || port > 65535) return false; -#ifdef _WIN32 - WSADATA wsa; - WSAStartup(MAKEWORD(2, 2), &wsa); -#endif - AeLanSock s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (s == kAeLanSockInvalid) return false; - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons((uint16_t)port); - const bool ok = (bind(s, (sockaddr*)&addr, sizeof(addr)) == 0); - ae_np_lan_sock_close(&s); - return ok; - } - - /* Online create: try preferred, then the next few ports. */ - static int ae_np_find_free_udp_port(int preferred) { - if (preferred <= 0 || preferred > 65535) preferred = 7777; - for (int i = 0; i < 32; ++i) { - const int p = preferred + i; - if (p > 65535) break; - if (ae_np_udp_port_available(p)) return p; - } - return -1; - } - - static bool ae_np_endpoint_replace_port(char* endpoint, size_t cap, int port) { - if (!endpoint || cap < 4 || port <= 0 || port > 65535) return false; - char host[64]; - if (!ae_np_lan_endpoint_host(endpoint, host, sizeof(host))) { - if (endpoint[0] == ':' || !endpoint[0]) - std::snprintf(host, sizeof(host), "0.0.0.0"); - else - return false; - } - const int n = std::snprintf(endpoint, cap, "%s:%d", host, port); - return n > 0 && (size_t)n < cap; - } - - static bool ae_np_lan_udp_ensure(bool bind_port, int port) { -#ifdef _WIN32 - WSADATA wsa; - WSAStartup(MAKEWORD(2, 2), &wsa); -#endif - if (g_lnch_lan_udp == kAeLanSockInvalid) { - g_lnch_lan_udp = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (g_lnch_lan_udp == kAeLanSockInvalid) return false; - if (!ae_np_lan_set_nonblock(g_lnch_lan_udp)) { - ae_np_lan_udp_close(); - return false; - } - /* Do not SO_REUSEADDR on the host lobby port — a second host on the - * same port must fail so we can surface "port in use". */ - if (bind_port) { - sockaddr_in addr{}; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons((uint16_t)port); - if (bind(g_lnch_lan_udp, (sockaddr*)&addr, sizeof(addr)) != 0) { - ae_np_lan_udp_close(); - return false; - } - } - } - return true; - } - - static void ae_np_lan_udp_sendto(const sockaddr_in& to, const char* msg) { - if (g_lnch_lan_udp == kAeLanSockInvalid || !msg) return; - const int n = (int)std::strlen(msg); -#ifdef _WIN32 - sendto(g_lnch_lan_udp, msg, n, 0, (const sockaddr*)&to, sizeof(to)); -#else - sendto(g_lnch_lan_udp, msg, (size_t)n, 0, (const sockaddr*)&to, sizeof(to)); -#endif - } - - bool ae_np_read_lan_state(AeLanLobbyState* state) { - if (!state) return false; - if (g_lnch_remote_lan) { - *state = g_lnch_remote_lan_state; - ae_np_lan_sync_legacy_names(*state); - return !state->endpoint.empty(); - } - std::ifstream f(ae_np_lan_file()); - if (!f) return false; - std::string started, host_slot, session, max_slots_s; - std::getline(f, state->name); - std::getline(f, state->game); - std::getline(f, state->endpoint); - std::getline(f, state->host_name); - std::getline(f, state->joiner_name); - std::getline(f, started); - std::getline(f, host_slot); - std::getline(f, state->password); - std::getline(f, session); - state->started = started == "1"; - state->host_slot = std::atoi(host_slot.c_str()); - if (state->host_slot < 0) state->host_slot = 0; - state->session_id = 1; - if (!session.empty()) { - const unsigned v = (unsigned)std::strtoul(session.c_str(), nullptr, 10); - if (v) state->session_id = (uint32_t)v; - } - state->max_slots = 2; - for (int i = 0; i < kAeLanMaxSlots; ++i) { - state->slot_name[i].clear(); - state->slot_id[i].clear(); - } - if (std::getline(f, max_slots_s)) { - int ms = std::atoi(max_slots_s.c_str()); - if (ms >= 2 && ms <= kAeLanMaxSlots) state->max_slots = ms; - for (int i = 0; i < state->max_slots; ++i) - std::getline(f, state->slot_name[i]); - for (int i = 0; i < state->max_slots; ++i) { - if (!std::getline(f, state->slot_id[i])) - state->slot_id[i].clear(); - } - } else { - /* Legacy 2P file: host + single joiner. */ - state->slot_name[state->host_slot == 1 ? 1 : 0] = state->host_name; - const int guest = state->host_slot == 1 ? 0 : 1; - state->slot_name[guest] = state->joiner_name; - } - ae_np_lan_sync_legacy_names(*state); - return !state->endpoint.empty(); - } - - bool ae_np_write_lan_state(const AeLanLobbyState& state_in) { - AeLanLobbyState state = state_in; - ae_np_lan_sync_legacy_names(state); - if (g_lnch_remote_lan) { - g_lnch_remote_lan_state = state; - return true; - } + bool ae_np_write_lan_state(const AeLanLobbyState& state) { std::ofstream f(ae_np_lan_file(), std::ios::trunc); if (!f) return false; - const uint32_t sid = state.session_id ? state.session_id : 1u; f << state.name << "\n" << state.game << "\n" << state.endpoint << "\n" @@ -5084,52 +4230,12 @@ namespace { << state.joiner_name << "\n" << (state.started ? "1" : "0") << "\n" << state.host_slot << "\n" - << state.password << "\n" - << sid << "\n" - << state.max_slots << "\n"; - for (int i = 0; i < state.max_slots && i < kAeLanMaxSlots; ++i) - f << state.slot_name[i] << "\n"; - for (int i = 0; i < state.max_slots && i < kAeLanMaxSlots; ++i) - f << state.slot_id[i] << "\n"; + << state.password << "\n"; return (bool)f; } - static void ae_np_lan_send_update_to_peers(const AeLanLobbyState& state_in) { - AeLanLobbyState state = state_in; - ae_np_lan_sync_legacy_names(state); - char msg[1536]; - int off = std::snprintf(msg, sizeof(msg), - "MOTK3 UPDATE\n%d\n%d\n%d\n%u\n", - state.max_slots, state.host_slot, - state.started ? 1 : 0, - (unsigned)(state.session_id ? state.session_id : 1u)); - for (int i = 0; i < state.max_slots && i < kAeLanMaxSlots && off > 0 && - off < (int)sizeof(msg) - 128; ++i) { - off += std::snprintf(msg + off, sizeof(msg) - (size_t)off, "%s\n%s\n", - state.slot_id[i].c_str(), - state.slot_name[i].c_str()); - } - if (off <= 0) return; - for (int i = 0; i < kAeLanMaxSlots; ++i) { - if (!g_lnch_lan_peer_ok[i]) continue; - ae_np_lan_udp_sendto(g_lnch_lan_peers[i], msg); - } - } - - static void ae_np_lan_atexit_cleanup(void) { - if (!g_lnch_hosting_lan) return; - std::error_code ec; - std::filesystem::remove(ae_np_lan_file(), ec); - g_lnch_hosting_lan = false; - } - - /* Returns false if the lobby UDP port cannot be bound (in use). */ - bool ae_np_write_lan_lobby(const char* name, const char* endpoint, - const char* password, int max_slots) { - ae_np_lan_udp_close(); - g_lnch_remote_lan = false; - g_lnch_remote_lan_state = {}; - g_lnch_lan_my_slot = 0; + void ae_np_write_lan_lobby(const char* name, const char* endpoint, + const char* password) { AeLanLobbyState state; state.name = name && name[0] ? name : "LAN Lobby"; state.game = g_lnch_netplay_game_name.empty() ? "PSX" : g_lnch_netplay_game_name; @@ -5137,31 +4243,11 @@ namespace { state.host_name = psx_lobby_display_name(); if (state.host_name.empty()) state.host_name = "Host"; state.password = password ? password : ""; - if (max_slots < 2) max_slots = 2; - if (max_slots > kAeLanMaxSlots) max_slots = kAeLanMaxSlots; - state.max_slots = max_slots; - state.host_slot = 0; - state.slot_name[0] = state.host_name; - state.slot_id[0] = ae_np_lan_local_player_id(); - const int port = ae_np_lan_endpoint_port(state.endpoint); - if (!ae_np_udp_port_available(port) || - !ae_np_lan_udp_ensure(true, port)) { - ae_np_lan_udp_close(); - return false; - } - if (!ae_np_write_lan_state(state)) { - ae_np_lan_udp_close(); - return false; - } + ae_np_write_lan_state(state); g_lnch_hosting_lan = true; g_lnch_joined_lan = false; g_lnch_lan_endpoint = state.endpoint; - static bool atexit_hooked = false; - if (!atexit_hooked) { - std::atexit(ae_np_lan_atexit_cleanup); - atexit_hooked = true; - } - return true; + g_lnch_lan_guest_bind.clear(); } int ae_np_read_lan_lobby(RecompLauncherCNetplayLobby* out) { @@ -5173,326 +4259,12 @@ namespace { state.name.empty() ? "Lobby" : state.name.c_str()); std::snprintf(out->game_name, sizeof(out->game_name), "%s", state.game.empty() ? "PSX" : state.game.c_str()); - out->player_count = ae_np_lan_occupied(state); - out->max_slots = state.max_slots >= 2 ? state.max_slots - : (g_lnch_host_max_slots >= 2 ? g_lnch_host_max_slots - : (g_lnch_game_players >= 2 ? g_lnch_game_players : 2)); - if (out->max_slots > PSX_MAX_PLAYERS) out->max_slots = PSX_MAX_PLAYERS; - if (out->max_slots > kAeLanMaxSlots) out->max_slots = kAeLanMaxSlots; + out->player_count = state.joiner_name.empty() ? 1 : 2; + out->max_slots = 2; out->has_password = state.password.empty() ? 0 : 1; - out->latency_ms = g_lnch_hosting_lan ? 0 : g_lnch_lan_latency_ms; return 1; } - static bool ae_np_remote_has_same_name_as_lan(const char* lan_display_name) { - static constexpr const char kLanPrefix[] = "LAN - "; - const char* base = lan_display_name; - if (base && std::strncmp(base, kLanPrefix, sizeof(kLanPrefix) - 1) == 0) - base += sizeof(kLanPrefix) - 1; - if (!base || !base[0]) return false; - for (int i = 0; i < psx_lobby_list_count(); ++i) { - PsxLobbyRow row{}; - if (!psx_lobby_list_get(i, &row)) continue; - if (std::strcmp(row.name, base) == 0) return true; - } - return false; - } - - static bool ae_np_read_lan_file_state(AeLanLobbyState* state) { - if (!state) return false; - const bool prior = g_lnch_remote_lan; - g_lnch_remote_lan = false; - const bool ok = ae_np_read_lan_state(state); - g_lnch_remote_lan = prior; - return ok; - } - - /* Probe LAN host; returns RTT ms, or -1 on timeout/failure. */ - static int ae_np_lan_probe_rtt_ms(const std::string& endpoint, uint32_t timeout_ms) { - char host[64]; - if (!ae_np_lan_endpoint_host(endpoint, host, sizeof(host))) return -1; -#ifdef _WIN32 - WSADATA wsa; - WSAStartup(MAKEWORD(2, 2), &wsa); -#endif - AeLanSock s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (s == kAeLanSockInvalid) return -1; - if (!ae_np_lan_set_nonblock(s)) { - ae_np_lan_sock_close(&s); - return -1; - } - sockaddr_in to{}; - to.sin_family = AF_INET; - to.sin_port = htons((uint16_t)ae_np_lan_endpoint_port(endpoint)); - if (inet_pton(AF_INET, host, &to.sin_addr) != 1) { - ae_np_lan_sock_close(&s); - return -1; - } - const char ping[] = "MOTK1 PING\n"; - const uint32_t t0 = SDL_GetTicks(); -#ifdef _WIN32 - sendto(s, ping, (int)sizeof(ping) - 1, 0, (const sockaddr*)&to, sizeof(to)); -#else - sendto(s, ping, sizeof(ping) - 1, 0, (const sockaddr*)&to, sizeof(to)); -#endif - const uint32_t deadline = t0 + timeout_ms; - int rtt = -1; - while ((int32_t)(deadline - SDL_GetTicks()) > 0) { - char buf[64]; - sockaddr_in from{}; -#ifdef _WIN32 - int fromlen = (int)sizeof(from); - const int n = recvfrom(s, buf, (int)sizeof(buf) - 1, 0, - (sockaddr*)&from, &fromlen); -#else - socklen_t fromlen = sizeof(from); - const int n = (int)recvfrom(s, buf, sizeof(buf) - 1, 0, - (sockaddr*)&from, &fromlen); -#endif - if (n > 0) { - buf[n] = '\0'; - if (std::strncmp(buf, "MOTK1 PONG", 10) == 0) { - rtt = (int)(SDL_GetTicks() - t0); - break; - } - } - SDL_Delay(5); - } - ae_np_lan_sock_close(&s); - return rtt; - } - - static bool ae_np_lan_probe_host_ms(const std::string& endpoint, uint32_t timeout_ms) { - return ae_np_lan_probe_rtt_ms(endpoint, timeout_ms) >= 0; - } - - static bool ae_np_lan_probe_host(const std::string& endpoint) { - return ae_np_lan_probe_host_ms(endpoint, 200u); - } - - - static int ae_np_lan_parse_motk2_update(char* body, AeLanLobbyState* out) { - if (!body || !out) return -1; - char* lines[8] = {}; - char* p = body; - for (int i = 0; i < 4; ++i) { - lines[i] = p; - char* nl = std::strchr(p, '\n'); - if (!nl) return -1; - *nl = '\0'; - p = nl + 1; - } - int max_slots = std::atoi(lines[0]); - int host_slot = std::atoi(lines[1]); - int started = std::atoi(lines[2]); - unsigned sid = (unsigned)std::strtoul(lines[3], nullptr, 10); - if (max_slots < 2) max_slots = 2; - if (max_slots > kAeLanMaxSlots) max_slots = kAeLanMaxSlots; - if (host_slot < 0 || host_slot >= max_slots) host_slot = 0; - out->max_slots = max_slots; - out->host_slot = host_slot; - out->started = started != 0; - out->session_id = sid ? (uint32_t)sid : 1u; - for (int i = 0; i < kAeLanMaxSlots; ++i) { - out->slot_name[i].clear(); - out->slot_id[i].clear(); - } - for (int i = 0; i < max_slots; ++i) { - char* nl = std::strchr(p, '\n'); - if (nl) { - *nl = '\0'; - out->slot_name[i] = p; - p = nl + 1; - } else { - out->slot_name[i] = p; - p = p + std::strlen(p); - } - } - ae_np_lan_sync_legacy_names(*out); - return 0; - } - - /* MOTK3 UPDATE: header + (player_id, display_name) per slot. */ - static int ae_np_lan_parse_motk3_update(char* body, AeLanLobbyState* out) { - if (!body || !out) return -1; - char* lines[8] = {}; - char* p = body; - for (int i = 0; i < 4; ++i) { - lines[i] = p; - char* nl = std::strchr(p, '\n'); - if (!nl) return -1; - *nl = '\0'; - p = nl + 1; - } - int max_slots = std::atoi(lines[0]); - int host_slot = std::atoi(lines[1]); - int started = std::atoi(lines[2]); - unsigned sid = (unsigned)std::strtoul(lines[3], nullptr, 10); - if (max_slots < 2) max_slots = 2; - if (max_slots > kAeLanMaxSlots) max_slots = kAeLanMaxSlots; - if (host_slot < 0 || host_slot >= max_slots) host_slot = 0; - out->max_slots = max_slots; - out->host_slot = host_slot; - out->started = started != 0; - out->session_id = sid ? (uint32_t)sid : 1u; - for (int i = 0; i < kAeLanMaxSlots; ++i) { - out->slot_name[i].clear(); - out->slot_id[i].clear(); - } - for (int i = 0; i < max_slots; ++i) { - char* nl = std::strchr(p, '\n'); - if (!nl) return -1; - *nl = '\0'; - out->slot_id[i] = p; - p = nl + 1; - nl = std::strchr(p, '\n'); - if (nl) { - *nl = '\0'; - out->slot_name[i] = p; - p = nl + 1; - } else { - out->slot_name[i] = p; - p = p + std::strlen(p); - } - } - ae_np_lan_sync_legacy_names(*out); - return 0; - } - - /* Send JOIN and wait for UPDATE / ERR. Returns 0, -1 full, -2 password, -3 timeout. */ - static int ae_np_lan_wait_join_ack(const std::string& endpoint, const char* password, - AeLanLobbyState* out) { - if (!out) return -1; - char host[64]; - if (!ae_np_lan_endpoint_host(endpoint, host, sizeof(host))) return -3; - if (!ae_np_lan_udp_ensure(false, 0)) return -3; - sockaddr_in to{}; - to.sin_family = AF_INET; - to.sin_port = htons((uint16_t)ae_np_lan_endpoint_port(endpoint)); - if (inet_pton(AF_INET, host, &to.sin_addr) != 1) return -3; - - std::string me = psx_lobby_display_name(); - if (me.empty()) me = "Player"; - const char* my_id = ae_np_lan_local_player_id(); - char msg[384]; - std::snprintf(msg, sizeof(msg), "MOTK3 JOIN\n%s\n%s\n%s\n", my_id, me.c_str(), - password ? password : ""); - ae_np_lan_udp_sendto(to, msg); - - const uint32_t deadline = SDL_GetTicks() + 1000u; - while ((int32_t)(deadline - SDL_GetTicks()) > 0) { - char buf[1536]; - sockaddr_in from{}; -#ifdef _WIN32 - int fromlen = (int)sizeof(from); - const int n = recvfrom(g_lnch_lan_udp, buf, (int)sizeof(buf) - 1, 0, - (sockaddr*)&from, &fromlen); -#else - socklen_t fromlen = sizeof(from); - const int n = (int)recvfrom(g_lnch_lan_udp, buf, sizeof(buf) - 1, 0, - (sockaddr*)&from, &fromlen); -#endif - if (n <= 0) { - SDL_Delay(5); - continue; - } - buf[n] = '\0'; - if (std::strncmp(buf, "MOTK1 ERR\n", 10) == 0) { - const char* code = buf + 10; - if (std::strncmp(code, "bad_password", 12) == 0) return -2; - return -1; - } - if (std::strncmp(buf, "MOTK3 UPDATE\n", 13) == 0) { - if (ae_np_lan_parse_motk3_update(buf + 13, out) != 0) continue; - out->endpoint = endpoint; - const int my_slot = ae_np_lan_find_slot_by_id(*out, my_id); - if (my_slot < 0) return -1; - g_lnch_lan_my_slot = my_slot; - return 0; - } - if (std::strncmp(buf, "MOTK2 UPDATE\n", 13) == 0) { - if (ae_np_lan_parse_motk2_update(buf + 13, out) != 0) continue; - out->endpoint = endpoint; - /* Legacy host: uniquified name may differ from requested. */ - int my_slot = ae_np_lan_find_guest_slot_by_name(*out, me.c_str()); - if (my_slot < 0) { - for (int i = 0; i < out->max_slots; ++i) { - if (i == out->host_slot) continue; - if (!out->slot_name[i].empty() && - out->slot_name[i].rfind(me, 0) == 0) { - my_slot = i; - break; - } - } - } - if (my_slot < 0) return -1; - g_lnch_lan_my_slot = my_slot; - return 0; - } - if (std::strncmp(buf, "MOTK1 UPDATE\n", 13) == 0) { - char* p = buf + 13; - char* lines[4] = {}; - for (int i = 0; i < 4; ++i) { - lines[i] = p; - char* nl = std::strchr(p, '\n'); - if (!nl) break; - *nl = '\0'; - p = nl + 1; - } - if (!lines[0] || !lines[1] || !lines[2] || !lines[3]) continue; - out->host_name = lines[0]; - out->joiner_name = lines[1]; - out->host_slot = (std::atoi(lines[2]) == 1) ? 1 : 0; - out->started = std::atoi(lines[3]) != 0; - out->max_slots = 2; - out->slot_name[0].clear(); - out->slot_name[1].clear(); - out->slot_name[out->host_slot] = out->host_name; - out->slot_name[1 - out->host_slot] = out->joiner_name; - out->endpoint = endpoint; - if (out->joiner_name != me) return -1; - g_lnch_lan_my_slot = 1 - out->host_slot; - return 0; - } - SDL_Delay(5); - } - return -3; - } - - /* Drop orphan LAN registry files (host crashed / unreachable). - * Never delete merely because started=1: host closes lobby UDP before - * delay-sync bind, so PING fails mid-launch and a delete races the joiner - * reading session_id/started from the file. Started lobbies are already - * hidden by ae_np_lan_list_visible(). */ - static void ae_np_lan_rescan(void) { - if (g_lnch_hosting_lan) { - g_lnch_lan_latency_ms = 0; - return; - } - if (g_lnch_joined_lan) return; - AeLanLobbyState st; - if (!ae_np_read_lan_file_state(&st)) { - g_lnch_lan_latency_ms = -1; - return; - } - if (st.started) return; - g_lnch_lan_latency_ms = ae_np_lan_probe_rtt_ms(st.endpoint, 200u); - if (g_lnch_lan_latency_ms < 0) { - std::error_code ec; - std::filesystem::remove(ae_np_lan_file(), ec); - } - } - - static bool ae_np_lan_list_visible(void) { - if (g_lnch_hosting_lan) { - AeLanLobbyState st; - return ae_np_read_lan_file_state(&st) && !st.started; - } - AeLanLobbyState st; - if (!ae_np_read_lan_file_state(&st) || st.started) return false; - return true; - } - PsxLobbyMatchCaps ae_netplay_caps_from_settings(const RecompLauncherCSettings* s) { PsxLobbyMatchCaps caps{}; caps.valid = 1; @@ -5503,94 +4275,10 @@ namespace { } caps.turbo_loads = s ? (s->turbo_loads != 0) : 0; caps.auto_skip_fmv = s ? (s->auto_skip_fmv != 0) : 0; - caps.input_delay = g_lnch_lobby_input_delay; - if (caps.input_delay < 2) caps.input_delay = 2; - if (caps.input_delay > 20) caps.input_delay = 20; - caps.force_input_relay = g_lnch_force_input_relay != 0; - caps.force_turn = g_lnch_force_turn != 0; + caps.input_delay = 2; return caps; } - static void ae_np_push_match_caps(const RecompLauncherCSettings* settings) { - if (!psx_lobby_in_lobby() || !psx_lobby_is_host()) return; - const PsxLobbyMatchCaps* cur = psx_lobby_match_caps(); - PsxLobbyMatchCaps caps = (cur && cur->valid) - ? *cur - : ae_netplay_caps_from_settings(settings); - caps.valid = 1; - caps.input_delay = g_lnch_lobby_input_delay; - if (caps.input_delay < 2) caps.input_delay = 2; - if (caps.input_delay > 20) caps.input_delay = 20; - caps.force_input_relay = g_lnch_force_input_relay != 0; - caps.force_turn = g_lnch_force_turn != 0; - (void)psx_lobby_set_match_caps(&caps); - } - - int ae_np_input_delay_get(void*) { - /* Online guests show host-authoritative match_caps. */ - if (!g_lnch_hosting_lan && !g_lnch_joined_lan) { - const PsxLobbyMatchCaps* caps = psx_lobby_match_caps(); - if (caps && caps->valid) { - int d = caps->input_delay; - if (d < 2) d = 2; - if (d > 20) d = 20; - return d; - } - } - return g_lnch_lobby_input_delay; - } - int ae_np_input_delay_set(void*, int delay_frames) { - if (delay_frames < 2) delay_frames = 2; - if (delay_frames > 20) delay_frames = 20; - g_lnch_lobby_input_delay = delay_frames; - ae_np_push_match_caps(nullptr); - return 0; - } - int ae_np_force_input_relay_get(void*) { - if (!g_lnch_hosting_lan && !g_lnch_joined_lan) { - const PsxLobbyMatchCaps* caps = psx_lobby_match_caps(); - if (caps && caps->valid) - return caps->force_input_relay ? 1 : 0; - } - return g_lnch_force_input_relay; - } - int ae_np_force_input_relay_set(void*, int force) { - g_lnch_force_input_relay = force ? 1 : 0; - ae_np_push_match_caps(nullptr); - return 0; - } - int ae_np_force_turn_get(void*) { - if (!g_lnch_hosting_lan && !g_lnch_joined_lan) { - const PsxLobbyMatchCaps* caps = psx_lobby_match_caps(); - if (caps && caps->valid) - return caps->force_turn ? 1 : 0; - } - return g_lnch_force_turn; - } - int ae_np_force_turn_set(void*, int force) { - if (g_lnch_hosting_lan || g_lnch_joined_lan) - return 0; /* LAN/Direct IP does not use ICE TURN */ - g_lnch_force_turn = force ? 1 : 0; - ae_np_push_match_caps(nullptr); - return 0; - } - - /* Seat ceiling for the active room (listing / LOBBY UI). 0 if unknown. */ - int ae_np_lobby_max_slots(void*) { - if (g_lnch_hosting_lan || g_lnch_joined_lan) { - AeLanLobbyState state; - if (ae_np_read_lan_state(&state) && state.max_slots >= 2) - return state.max_slots; - return g_lnch_host_max_slots >= 2 ? g_lnch_host_max_slots : 0; - } - if (psx_lobby_in_lobby()) { - const PsxLobbyJoinInfo* ji = psx_lobby_join_info(); - if (ji && ji->max_slots >= 2) return ji->max_slots; - return g_lnch_host_max_slots >= 2 ? g_lnch_host_max_slots : 0; - } - return 0; - } - const char* ae_np_default_url(void*) { return g_lnch_lobby_url.empty() ? psx_lobby_default_url() : g_lnch_lobby_url.c_str(); } @@ -5618,7 +4306,6 @@ namespace { int ae_np_connect(void*) { psx_lobby_set_game_identity(g_lnch_netplay_game_name.c_str(), PSX_GAME_VERSION); - psx_lobby_set_max_slots(g_lnch_game_players); return psx_lobby_connect(ae_np_default_url(nullptr)); } @@ -5626,294 +4313,8 @@ namespace { return psx_lobby_connected(); } - static void ae_np_lan_udp_pump(void) { - if (!g_lnch_hosting_lan && !(g_lnch_joined_lan && g_lnch_remote_lan)) - return; - - if (g_lnch_hosting_lan) { - AeLanLobbyState st; - if (!ae_np_read_lan_state(&st)) return; - if (g_lnch_lan_udp == kAeLanSockInvalid) - (void)ae_np_lan_udp_ensure(true, ae_np_lan_endpoint_port(st.endpoint)); - } else if (g_lnch_remote_lan) { - if (g_lnch_lan_udp == kAeLanSockInvalid) - (void)ae_np_lan_udp_ensure(false, 0); - const uint32_t now = SDL_GetTicks(); - if (g_lnch_lan_udp != kAeLanSockInvalid && - now - g_lnch_lan_join_pulse_ms >= 400u) { - g_lnch_lan_join_pulse_ms = now; - char host[64]; - if (ae_np_lan_endpoint_host(g_lnch_lan_endpoint, host, sizeof(host))) { - sockaddr_in to{}; - to.sin_family = AF_INET; - to.sin_port = htons((uint16_t)ae_np_lan_endpoint_port(g_lnch_lan_endpoint)); - if (inet_pton(AF_INET, host, &to.sin_addr) == 1) { - std::string me = psx_lobby_display_name(); - if (me.empty()) me = "Player"; - char msg[384]; - std::snprintf(msg, sizeof(msg), "MOTK3 JOIN\n%s\n%s\n%s\n", - ae_np_lan_local_player_id(), me.c_str(), - g_lnch_remote_lan_state.password.c_str()); - ae_np_lan_udp_sendto(to, msg); - } - } - } - } - - if (g_lnch_lan_udp == kAeLanSockInvalid) return; - - for (;;) { - char buf[1536]; - sockaddr_in from{}; -#ifdef _WIN32 - int fromlen = (int)sizeof(from); - const int n = recvfrom(g_lnch_lan_udp, buf, (int)sizeof(buf) - 1, 0, - (sockaddr*)&from, &fromlen); -#else - socklen_t fromlen = sizeof(from); - const int n = (int)recvfrom(g_lnch_lan_udp, buf, sizeof(buf) - 1, 0, - (sockaddr*)&from, &fromlen); -#endif - if (n <= 0) break; - buf[n] = '\0'; - - if (std::strncmp(buf, "MOTK1 PING", 10) == 0 && g_lnch_hosting_lan) { - ae_np_lan_udp_sendto(from, "MOTK1 PONG\n"); - continue; - } - - if (std::strncmp(buf, "MOTK3 JOIN\n", 11) == 0 && g_lnch_hosting_lan) { - char* p = buf + 11; - char* nl = std::strchr(p, '\n'); - if (!nl) continue; - *nl = '\0'; - const char* player_id = p; - p = nl + 1; - nl = std::strchr(p, '\n'); - if (!nl) continue; - *nl = '\0'; - const char* name = p; - p = nl + 1; - nl = std::strchr(p, '\n'); - if (nl) *nl = '\0'; - const char* pass = p; - AeLanLobbyState st; - if (!ae_np_read_lan_state(&st)) continue; - if (st.password != pass) { - ae_np_lan_udp_sendto(from, "MOTK1 ERR\nbad_password\n"); - continue; - } - const int slot = ae_np_lan_seat_guest(st, player_id, name); - if (slot < 0) { - ae_np_lan_udp_sendto(from, "MOTK1 ERR\nfull\n"); - continue; - } - st.started = false; - ae_np_lan_sync_legacy_names(st); - if (!ae_np_write_lan_state(st)) continue; - ae_np_lan_set_peer_slot(slot, from); - ae_np_lan_send_update_to_peers(st); - continue; - } - - if (std::strncmp(buf, "MOTK1 JOIN\n", 11) == 0 && g_lnch_hosting_lan) { - char* p = buf + 11; - char* nl = std::strchr(p, '\n'); - if (!nl) continue; - *nl = '\0'; - const char* name = p; - p = nl + 1; - nl = std::strchr(p, '\n'); - if (nl) *nl = '\0'; - const char* pass = p; - AeLanLobbyState st; - if (!ae_np_read_lan_state(&st)) continue; - if (st.password != pass) { - ae_np_lan_udp_sendto(from, "MOTK1 ERR\nbad_password\n"); - continue; - } - /* Legacy JOIN: synthesize an id from peer addr so same-name - * clients still get distinct seats + (2)/(3) labels. */ - char synth_id[48]; - std::snprintf(synth_id, sizeof(synth_id), "motk1-%08x-%04x", - (unsigned)ntohl(from.sin_addr.s_addr), - (unsigned)ntohs(from.sin_port)); - const int slot = ae_np_lan_seat_guest(st, synth_id, name); - if (slot < 0) { - ae_np_lan_udp_sendto(from, "MOTK1 ERR\nfull\n"); - continue; - } - st.started = false; - ae_np_lan_sync_legacy_names(st); - if (!ae_np_write_lan_state(st)) continue; - ae_np_lan_set_peer_slot(slot, from); - ae_np_lan_send_update_to_peers(st); - continue; - } - - if ((std::strncmp(buf, "MOTK3 LEAVE\n", 12) == 0 || - std::strncmp(buf, "MOTK1 LEAVE\n", 12) == 0) && - g_lnch_hosting_lan) { - const bool by_id = std::strncmp(buf, "MOTK3 LEAVE\n", 12) == 0; - char* p = buf + 12; - char* nl = std::strchr(p, '\n'); - if (nl) *nl = '\0'; - const char* leave_key = (p && p[0]) ? p : nullptr; - AeLanLobbyState st; - if (!ae_np_read_lan_state(&st)) continue; - int cleared = -1; - if (leave_key) { - if (by_id) { - cleared = ae_np_lan_find_slot_by_id(st, leave_key); - if (cleared == st.host_slot) cleared = -1; - } else { - for (int i = 0; i < st.max_slots; ++i) { - if (i == st.host_slot) continue; - if (st.slot_name[i] == leave_key) { - cleared = i; - break; - } - } - } - if (cleared >= 0) { - st.slot_name[cleared].clear(); - st.slot_id[cleared].clear(); - } - } else { - /* Legacy leave without name: drop first guest. */ - for (int i = 0; i < st.max_slots; ++i) { - if (i == st.host_slot) continue; - if (!st.slot_name[i].empty()) { - st.slot_name[i].clear(); - st.slot_id[i].clear(); - cleared = i; - break; - } - } - } - st.started = false; - ae_np_lan_sync_legacy_names(st); - ae_np_write_lan_state(st); - if (cleared >= 0) ae_np_lan_clear_peer_slot(cleared); - ae_np_lan_send_update_to_peers(st); - continue; - } - - if (!g_lnch_remote_lan) continue; - - if (std::strncmp(buf, "MOTK3 UPDATE\n", 13) == 0) { - AeLanLobbyState st = g_lnch_remote_lan_state; - if (ae_np_lan_parse_motk3_update(buf + 13, &st) != 0) continue; - st.endpoint = g_lnch_lan_endpoint; - g_lnch_remote_lan_state = st; - const int my_slot = - ae_np_lan_find_slot_by_id(st, ae_np_lan_local_player_id()); - if (my_slot < 0) { - g_lnch_joined_lan = false; - g_lnch_remote_lan = false; - g_lnch_lan_endpoint.clear(); - g_lnch_lan_my_slot = -1; - ae_np_lan_udp_close(); - } else { - g_lnch_lan_my_slot = my_slot; - } - continue; - } - - if (std::strncmp(buf, "MOTK2 UPDATE\n", 13) == 0) { - AeLanLobbyState st = g_lnch_remote_lan_state; - if (ae_np_lan_parse_motk2_update(buf + 13, &st) != 0) continue; - st.endpoint = g_lnch_lan_endpoint; - g_lnch_remote_lan_state = st; - if (g_lnch_lan_my_slot >= 0 && - g_lnch_lan_my_slot < st.max_slots && - !st.slot_name[g_lnch_lan_my_slot].empty()) { - /* keep assigned slot */ - } else { - std::string me = psx_lobby_display_name(); - if (me.empty()) me = "Player"; - const int my_slot = - ae_np_lan_find_guest_slot_by_name(st, me.c_str()); - if (my_slot < 0) { - g_lnch_joined_lan = false; - g_lnch_remote_lan = false; - g_lnch_lan_endpoint.clear(); - g_lnch_lan_my_slot = -1; - ae_np_lan_udp_close(); - } else { - g_lnch_lan_my_slot = my_slot; - } - } - continue; - } - if (std::strncmp(buf, "MOTK1 UPDATE\n", 13) == 0) { - char* p = buf + 13; - char* lines[4] = {}; - for (int i = 0; i < 4; ++i) { - lines[i] = p; - char* nl = std::strchr(p, '\n'); - if (!nl) break; - *nl = '\0'; - p = nl + 1; - } - if (!lines[0] || !lines[1] || !lines[2] || !lines[3]) continue; - g_lnch_remote_lan_state.host_name = lines[0]; - g_lnch_remote_lan_state.joiner_name = lines[1]; - g_lnch_remote_lan_state.host_slot = (std::atoi(lines[2]) == 1) ? 1 : 0; - g_lnch_remote_lan_state.started = std::atoi(lines[3]) != 0; - g_lnch_remote_lan_state.max_slots = 2; - g_lnch_remote_lan_state.slot_name[0].clear(); - g_lnch_remote_lan_state.slot_name[1].clear(); - g_lnch_remote_lan_state.slot_name[g_lnch_remote_lan_state.host_slot] = - g_lnch_remote_lan_state.host_name; - g_lnch_remote_lan_state.slot_name[1 - g_lnch_remote_lan_state.host_slot] = - g_lnch_remote_lan_state.joiner_name; - std::string me = psx_lobby_display_name(); - if (me.empty()) me = "Player"; - if (g_lnch_remote_lan_state.joiner_name != me) { - g_lnch_joined_lan = false; - g_lnch_remote_lan = false; - g_lnch_lan_endpoint.clear(); - g_lnch_lan_my_slot = -1; - ae_np_lan_udp_close(); - } else { - g_lnch_lan_my_slot = 1 - g_lnch_remote_lan_state.host_slot; - } - continue; - } - if (std::strncmp(buf, "MOTK1 START\n", 12) == 0) { - g_lnch_remote_lan_state.started = true; - const char* sid = buf + 12; - if (*sid) { - const unsigned v = (unsigned)std::strtoul(sid, nullptr, 10); - if (v) { - g_lnch_lan_session_id = (uint32_t)v; - g_lnch_remote_lan_state.session_id = (uint32_t)v; - } - } - continue; - } - if (std::strncmp(buf, "MOTK1 KICK\n", 11) == 0 || - std::strncmp(buf, "MOTK1 ERR\n", 10) == 0) { - g_lnch_joined_lan = false; - g_lnch_remote_lan = false; - g_lnch_lan_endpoint.clear(); - g_lnch_remote_lan_state = {}; - ae_np_lan_udp_close(); - } - } - } - void ae_np_pump(void*) { psx_lobby_pump(); - ae_np_lan_udp_pump(); - /* Lobby UI has no Ready toggle; production WS still requires every - * seated player ready before start. Keep seats ready while in-room - * (including after soft-return rematch clears ready). */ - if (!g_lnch_hosting_lan && !g_lnch_joined_lan && - psx_lobby_in_lobby() && !psx_lobby_local_ready()) { - (void)psx_lobby_set_ready(1); - } } void ae_np_set_player_name(void*, const char* name) { @@ -5926,19 +4327,19 @@ namespace { } void ae_np_request_list(void*) { - ae_np_lan_rescan(); psx_lobby_request_list(); } int ae_np_list_count(void*) { - return psx_lobby_list_count() + (ae_np_lan_list_visible() ? 1 : 0); + RecompLauncherCNetplayLobby lan{}; + return psx_lobby_list_count() + (ae_np_read_lan_lobby(&lan) ? 1 : 0); } int ae_np_list_get(void*, int index, RecompLauncherCNetplayLobby* out) { if (!out) return 0; const int remote_count = psx_lobby_list_count(); if (index >= remote_count) - return ae_np_lan_list_visible() ? ae_np_read_lan_lobby(out) : 0; + return ae_np_read_lan_lobby(out); PsxLobbyRow row{}; if (!psx_lobby_list_get(index, &row)) return 0; std::snprintf(out->lobby_id, sizeof(out->lobby_id), "%s", row.lobby_id); @@ -5948,96 +4349,10 @@ namespace { out->player_count = row.player_count; out->max_slots = row.max_slots; out->has_password = row.has_password; - out->latency_ms = row.latency_ms; return 1; } - int ae_np_external_ip(void*, char* out, size_t out_len) { - if (!out || out_len == 0) return 0; -#ifdef _WIN32 - WSADATA wsa; - WSAStartup(MAKEWORD(2, 2), &wsa); -#endif - addrinfo hints{}; - hints.ai_family = AF_INET; - hints.ai_socktype = SOCK_STREAM; - addrinfo* res = nullptr; - if (getaddrinfo("api.ipify.org", "80", &hints, &res) != 0 || !res) - return 0; -#ifdef _WIN32 - SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (s == INVALID_SOCKET) { - freeaddrinfo(res); - return 0; - } - DWORD timeout_ms = 3000; - setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout_ms, - sizeof(timeout_ms)); - setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, (const char*)&timeout_ms, - sizeof(timeout_ms)); -#else - int s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - if (s < 0) { - freeaddrinfo(res); - return 0; - } - timeval tv{}; - tv.tv_sec = 3; - tv.tv_usec = 0; - setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); - setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); -#endif - int ok = 0; -#ifdef _WIN32 - const int connected = - connect(s, res->ai_addr, (int)res->ai_addrlen) != SOCKET_ERROR; -#else - const int connected = - connect(s, res->ai_addr, (socklen_t)res->ai_addrlen) == 0; -#endif - if (connected) { - const char req[] = - "GET / HTTP/1.1\r\n" - "Host: api.ipify.org\r\n" - "User-Agent: psxrecomp-netplay/1.0\r\n" - "Connection: close\r\n\r\n"; -#ifdef _WIN32 - (void)send(s, req, (int)strlen(req), 0); - char resp[1024]; - int n = recv(s, resp, sizeof(resp) - 1, 0); -#else - (void)send(s, req, strlen(req), 0); - char resp[1024]; - ssize_t n = recv(s, resp, sizeof(resp) - 1, 0); -#endif - if (n > 0) { - resp[n] = '\0'; - char* body = strstr(resp, "\r\n\r\n"); - body = body ? body + 4 : resp; - char ip[64] = {}; - int j = 0; - for (int i = 0; body[i] && j < (int)sizeof(ip) - 1; ++i) { - if ((body[i] >= '0' && body[i] <= '9') || body[i] == '.') - ip[j++] = body[i]; - else if (j > 0) - break; - } - if (j > 0) { - std::snprintf(out, out_len, "%s", ip); - ok = 1; - } - } - } -#ifdef _WIN32 - closesocket(s); -#else - close(s); -#endif - freeaddrinfo(res); - return ok; - } - - /* Collect non-loopback IPv4 addresses for local_address_get. */ + /* Collect non-loopback IPv4 addresses for Host Lobby IP dropdown. */ static void ae_np_collect_local_addresses( std::vector* out) { if (!out) return; @@ -6129,115 +4444,94 @@ namespace { return 1; } - /* LAN/Direct IP rooms own membership via the local file registry. Server - * lobbies use WebSocket lobby_update. Never mix: LAN mode wins if set. */ - static bool ae_np_use_lan_members(void) { - return g_lnch_hosting_lan || g_lnch_joined_lan; - } - - static bool ae_np_use_ws_members(void) { - return !ae_np_use_lan_members() && psx_lobby_in_lobby() != 0; - } - - /* Joiner was cleared / file gone → treat as kicked or host left. */ - static void ae_np_poll_lan_joiner_still_seated(void) { - if (!g_lnch_joined_lan) return; - if (g_lnch_remote_lan) { - /* Remote seat cleared by UDP KICK/ERR/UPDATE in pump. */ - return; - } - AeLanLobbyState state; - if (!ae_np_read_lan_state(&state)) { - g_lnch_joined_lan = false; - g_lnch_lan_endpoint.clear(); - g_lnch_lan_my_slot = -1; - return; + int ae_np_external_ip(void*, char* out, size_t out_len) { + if (!out || out_len == 0) return 0; +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); + addrinfo hints{}; + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + addrinfo* res = nullptr; + if (getaddrinfo("api.ipify.org", "80", &hints, &res) != 0 || !res) + return 0; + SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + if (s == INVALID_SOCKET) { + freeaddrinfo(res); + return 0; } - bool seated = false; - const int by_id = - ae_np_lan_find_slot_by_id(state, ae_np_lan_local_player_id()); - if (by_id >= 0 && by_id != state.host_slot) { - seated = true; - g_lnch_lan_my_slot = by_id; - } else if (g_lnch_lan_my_slot >= 0 && - g_lnch_lan_my_slot < state.max_slots && - g_lnch_lan_my_slot != state.host_slot && - !state.slot_name[g_lnch_lan_my_slot].empty()) { - seated = true; - } else { - std::string me = psx_lobby_display_name(); - if (me.empty()) me = "Player"; - const int slot = ae_np_lan_find_guest_slot_by_name(state, me.c_str()); - if (slot >= 0) { - seated = true; - g_lnch_lan_my_slot = slot; + int ok = 0; + if (connect(s, res->ai_addr, (int)res->ai_addrlen) != SOCKET_ERROR) { + const char req[] = + "GET / HTTP/1.1\r\n" + "Host: api.ipify.org\r\n" + "Connection: close\r\n\r\n"; + (void)send(s, req, (int)strlen(req), 0); + char resp[1024]; + int n = recv(s, resp, sizeof(resp) - 1, 0); + if (n > 0) { + resp[n] = '\0'; + char* body = strstr(resp, "\r\n\r\n"); + body = body ? body + 4 : resp; + char ip[64] = {}; + int j = 0; + for (int i = 0; body[i] && j < (int)sizeof(ip) - 1; ++i) { + if ((body[i] >= '0' && body[i] <= '9') || body[i] == '.') + ip[j++] = body[i]; + else if (j > 0) + break; + } + if (j > 0) { + std::snprintf(out, out_len, "%s", ip); + ok = 1; + } } } - if (!seated) { - g_lnch_joined_lan = false; - g_lnch_lan_endpoint.clear(); - g_lnch_lan_my_slot = -1; - } + closesocket(s); + freeaddrinfo(res); + return ok; +#else + std::snprintf(out, out_len, "Unavailable"); + return 0; +#endif } - /* host_endpoint is in/out (capacity >= 64). Online may rewrite the UDP - * port when the requested one is busy. Returns 0 ok, -4 port unavailable. */ + /* host_endpoint is in/out (capacity >= 64). recomp-ui applies UDP port + * policy before calling. lan_only: local registry only; else lobby server. */ int ae_np_create(void*, const char* lobby_name, char* host_endpoint, const char* password, const RecompLauncherCSettings* settings, int lan_only, int max_slots) { - int game_max = g_lnch_game_players >= 2 ? g_lnch_game_players : 2; - if (game_max > PSX_MAX_PLAYERS) game_max = PSX_MAX_PLAYERS; - if (game_max > 8) game_max = 8; - if (max_slots < 2) max_slots = 2; - if (max_slots > game_max) max_slots = game_max; - /* Lobby + delay-sync ceiling (party games up to 8). */ - if (max_slots > RECOMP_LAUNCHER_NETPLAY_MAX_MEMBERS) - max_slots = RECOMP_LAUNCHER_NETPLAY_MAX_MEMBERS; - g_lnch_host_max_slots = max_slots; + /* The restored two-peer PSX transport does not consume the launcher's + * newer seat-count hint yet. The game profile still caps PSX sessions + * to the supported player count. */ + (void)max_slots; PsxLobbyMatchCaps caps = ae_netplay_caps_from_settings(settings); char endpoint[96]; if (host_endpoint && host_endpoint[0]) std::snprintf(endpoint, sizeof(endpoint), "%s", host_endpoint); else std::snprintf(endpoint, sizeof(endpoint), "0.0.0.0:7777"); - const int want_port = ae_np_lan_endpoint_port(endpoint); - /* LAN/Direct IP only: local registry + UDP membership (no lobby WS). - * Online create always uses psx_lobby_create — even when the UI passes - * a concrete LAN IPv4 for MotK-style host_bind rewrite. */ if (lan_only) { - /* LAN/Direct IP: exact port required — fail if busy. */ if (psx_lobby_in_lobby()) (void)psx_lobby_leave(); - if (!ae_np_write_lan_lobby(lobby_name, endpoint, password, max_slots)) - return -4; + ae_np_write_lan_lobby(lobby_name, endpoint, password); if (host_endpoint) std::snprintf(host_endpoint, 96, "%s", endpoint); return 0; } - /* Online: auto-pick a free UDP port starting at the requested one. */ - const int free_port = ae_np_find_free_udp_port(want_port); - if (free_port < 0) return -4; - if (free_port != want_port && - !ae_np_endpoint_replace_port(endpoint, sizeof(endpoint), free_port)) { - return -4; - } - if (host_endpoint) - std::snprintf(host_endpoint, 96, "%s", endpoint); - if (g_lnch_hosting_lan) { std::error_code ec; std::filesystem::remove(ae_np_lan_file(), ec); + g_lnch_hosting_lan = false; } - ae_np_lan_udp_close(); - g_lnch_hosting_lan = false; g_lnch_joined_lan = false; - g_lnch_remote_lan = false; - g_lnch_remote_lan_state = {}; g_lnch_lan_endpoint.clear(); - psx_lobby_set_max_slots(max_slots); + g_lnch_lan_guest_bind.clear(); + if (host_endpoint) + std::snprintf(host_endpoint, 96, "%s", endpoint); return psx_lobby_create(lobby_name && lobby_name[0] ? lobby_name : "Netplay Lobby", g_lnch_netplay_game_name.c_str(), PSX_GAME_VERSION, password ? password : "", endpoint, &caps); @@ -6259,277 +4553,84 @@ namespace { std::snprintf(guest_bind, 64, "%s", bind_buf); } if (lobby_id && strncmp(lobby_id, "lan:", 4) == 0) { - const char* endpoint = lobby_id + 4; - if (!endpoint[0]) return -1; - if (psx_lobby_in_lobby()) - (void)psx_lobby_leave(); - - /* Must be a live LAN/Direct IP host (UDP PONG). Online-only hosts - * never answer — refuse so we don't open a fake local room. */ - if (!ae_np_lan_probe_host_ms(endpoint, 750u)) - return -3; - - /* Peek the on-disk registry (ignore any prior remote seat). */ - const bool prior_remote = g_lnch_remote_lan; - g_lnch_remote_lan = false; - AeLanLobbyState file{}; - const bool have_file = ae_np_read_lan_file_state(&file); - g_lnch_remote_lan = prior_remote; - /* Same-machine / shared cwd: claim the local LAN file when the - * endpoint matches. */ - if (have_file && !g_lnch_hosting_lan && file.endpoint == endpoint) { - if (file.password != (password ? password : "")) return -2; - std::string me = psx_lobby_display_name(); - if (me.empty()) me = "Player"; - const int slot = - ae_np_lan_seat_guest(file, ae_np_lan_local_player_id(), me.c_str()); - if (slot < 0) return -1; - file.started = false; - ae_np_lan_sync_legacy_names(file); - g_lnch_remote_lan = false; - g_lnch_remote_lan_state = {}; - if (!ae_np_write_lan_state(file)) return -1; - g_lnch_hosting_lan = false; - g_lnch_joined_lan = true; - g_lnch_lan_my_slot = slot; - g_lnch_lan_endpoint = file.endpoint; - return 0; - } - - /* Cross-machine Join Direct: JOIN must be acked before we seat. */ - ae_np_lan_udp_close(); - AeLanLobbyState seated{}; - seated.name = "Direct"; - seated.game = - g_lnch_netplay_game_name.empty() ? "PSX" : g_lnch_netplay_game_name; - seated.endpoint = endpoint; - seated.password = password ? password : ""; - const int ack = ae_np_lan_wait_join_ack(endpoint, password, &seated); - if (ack != 0) { - ae_np_lan_udp_close(); - g_lnch_joined_lan = false; - g_lnch_remote_lan = false; - g_lnch_remote_lan_state = {}; - g_lnch_lan_endpoint.clear(); - return ack; - } - g_lnch_remote_lan = true; - g_lnch_remote_lan_state = seated; - ae_np_lan_sync_legacy_names(g_lnch_remote_lan_state); + AeLanLobbyState state; + if (!ae_np_read_lan_state(&state) || !state.joiner_name.empty()) return -1; + if (state.password != (password ? password : "")) return -2; + state.joiner_name = psx_lobby_display_name(); + if (state.joiner_name.empty()) state.joiner_name = "Player"; + state.started = false; + if (!ae_np_write_lan_state(state)) return -1; g_lnch_hosting_lan = false; g_lnch_joined_lan = true; - g_lnch_lan_endpoint = endpoint; - g_lnch_lan_join_pulse_ms = SDL_GetTicks(); + g_lnch_lan_endpoint = state.endpoint; + g_lnch_lan_guest_bind = bind; return 0; } - /* Server join: leave any stale LAN-file room mode so membership follows WS. */ - ae_np_lan_udp_close(); - g_lnch_hosting_lan = false; - g_lnch_joined_lan = false; - g_lnch_remote_lan = false; - g_lnch_remote_lan_state = {}; - g_lnch_lan_endpoint.clear(); return psx_lobby_join(lobby_id, password ? password : "", bind); } - const char* ae_np_last_error(void*) { - const PsxLobbyJoinInfo* ji = psx_lobby_join_info(); - return (ji && ji->last_error[0]) ? ji->last_error : nullptr; - } - - void ae_np_clear_last_error(void*) { - PsxLobbyJoinInfo* ji = const_cast(psx_lobby_join_info()); - if (ji) ji->last_error[0] = '\0'; - } - int ae_np_leave(void*) { if (g_lnch_hosting_lan) { - for (int i = 0; i < kAeLanMaxSlots; ++i) { - if (g_lnch_lan_peer_ok[i]) - ae_np_lan_udp_sendto(g_lnch_lan_peers[i], "MOTK1 KICK\n"); - } std::error_code ec; std::filesystem::remove(ae_np_lan_file(), ec); g_lnch_hosting_lan = false; } else if (g_lnch_joined_lan) { - std::string me = psx_lobby_display_name(); - if (me.empty()) me = "Player"; - if (g_lnch_remote_lan) { - char host[64]; - if (ae_np_lan_endpoint_host(g_lnch_lan_endpoint, host, sizeof(host)) && - g_lnch_lan_udp != kAeLanSockInvalid) { - sockaddr_in to{}; - to.sin_family = AF_INET; - to.sin_port = - htons((uint16_t)ae_np_lan_endpoint_port(g_lnch_lan_endpoint)); - if (inet_pton(AF_INET, host, &to.sin_addr) == 1) { - char leave_msg[160]; - std::snprintf(leave_msg, sizeof(leave_msg), - "MOTK3 LEAVE\n%s\n", - ae_np_lan_local_player_id()); - ae_np_lan_udp_sendto(to, leave_msg); - } - } - } else { - AeLanLobbyState state; - if (ae_np_read_lan_state(&state)) { - int slot = ae_np_lan_find_slot_by_id(state, ae_np_lan_local_player_id()); - if (slot < 0 && g_lnch_lan_my_slot >= 0 && - g_lnch_lan_my_slot < state.max_slots && - g_lnch_lan_my_slot != state.host_slot) { - slot = g_lnch_lan_my_slot; - } - if (slot < 0) - slot = ae_np_lan_find_guest_slot_by_name(state, me.c_str()); - if (slot >= 0) { - state.slot_name[slot].clear(); - state.slot_id[slot].clear(); - } - state.started = false; - ae_np_lan_sync_legacy_names(state); - ae_np_write_lan_state(state); - } + AeLanLobbyState state; + if (ae_np_read_lan_state(&state)) { + state.joiner_name.clear(); + state.started = false; + ae_np_write_lan_state(state); } } - ae_np_lan_udp_close(); g_lnch_joined_lan = false; - g_lnch_remote_lan = false; - g_lnch_remote_lan_state = {}; g_lnch_lan_endpoint.clear(); - g_lnch_lan_my_slot = -1; + g_lnch_lan_guest_bind.clear(); g_lnch_pending_direct_launch = {}; return psx_lobby_leave(); } int ae_np_in_lobby(void*) { - ae_np_poll_lan_joiner_still_seated(); - if (g_lnch_hosting_lan) { - AeLanLobbyState state; - return ae_np_read_lan_state(&state) ? 1 : 0; - } - if (g_lnch_joined_lan) return 1; - return psx_lobby_in_lobby(); + return g_lnch_hosting_lan || g_lnch_joined_lan || psx_lobby_in_lobby(); } int ae_np_is_host(void*) { - if (ae_np_use_lan_members()) return g_lnch_hosting_lan ? 1 : 0; - if (ae_np_use_ws_members()) return psx_lobby_is_host(); + if (g_lnch_hosting_lan || g_lnch_joined_lan) return g_lnch_hosting_lan ? 1 : 0; return psx_lobby_is_host(); } int ae_np_member_count(void*) { - if (ae_np_use_ws_members()) { - const int n = psx_lobby_member_count(); - return n > 0 ? n : 1; - } - if (ae_np_use_lan_members()) { - AeLanLobbyState state; - if (!ae_np_read_lan_state(&state)) return 1; - const int n = ae_np_lan_occupied(state); - return n > 0 ? n : 1; - } + if (g_lnch_hosting_lan || g_lnch_joined_lan) return 2; return psx_lobby_member_count(); } int ae_np_member_get(void*, int index, RecompLauncherCNetplayMember* out) { if (!out) return 0; - if (ae_np_use_ws_members()) { - PsxLobbyMember mem{}; - if (!psx_lobby_member_get(index, &mem)) return 0; - out->slot = mem.slot; - std::snprintf(out->display_name, sizeof(out->display_name), "%s", - mem.display_name); - out->ready = mem.ready; - const char* host_id = psx_lobby_host_player_id(); - /* Prefer host_player_id; slot-0 fallback only when unknown (never - * mark a guest as host after a seat swap). */ - if (host_id && host_id[0] && mem.player_id[0]) - out->is_host = (std::strcmp(host_id, mem.player_id) == 0) ? 1 : 0; - else - out->is_host = (mem.slot == 0) ? 1 : 0; - out->latency_ms = psx_lobby_member_latency_ms(mem.slot); - return 1; - } - if (ae_np_use_lan_members()) { + if (g_lnch_hosting_lan || g_lnch_joined_lan) { + if (index < 0 || index > 1) return 0; AeLanLobbyState state; if (!ae_np_read_lan_state(&state)) return 0; - int seen = 0; - for (int slot = 0; slot < state.max_slots; ++slot) { - if (state.slot_name[slot].empty()) continue; - if (seen == index) { - out->slot = slot; - std::snprintf(out->display_name, sizeof(out->display_name), "%s", - state.slot_name[slot].c_str()); - out->ready = 1; - out->is_host = (slot == state.host_slot) ? 1 : 0; - out->latency_ms = -1; - return 1; - } - ++seen; - } - return 0; + const bool host = index == 0; + out->slot = host ? state.host_slot : 1 - state.host_slot; + const std::string& name = host ? state.host_name : state.joiner_name; + std::snprintf(out->display_name, sizeof(out->display_name), "%s", name.c_str()); + out->ready = !name.empty(); + out->is_host = host ? 1 : 0; + return 1; } PsxLobbyMember mem{}; if (!psx_lobby_member_get(index, &mem)) return 0; out->slot = mem.slot; std::snprintf(out->display_name, sizeof(out->display_name), "%s", mem.display_name); out->ready = mem.ready; - const char* host_id = psx_lobby_host_player_id(); - if (host_id && host_id[0] && mem.player_id[0]) - out->is_host = (std::strcmp(host_id, mem.player_id) == 0) ? 1 : 0; - else - out->is_host = (mem.slot == 0) ? 1 : 0; - out->latency_ms = psx_lobby_member_latency_ms(mem.slot); + out->is_host = mem.slot == 0; return 1; } int ae_np_move_member(void*, int from_slot, int to_slot) { - if (from_slot < 0 || to_slot < 0 || from_slot == to_slot) return -1; - if (g_lnch_hosting_lan) { - AeLanLobbyState state; - if (!ae_np_read_lan_state(&state)) return -1; - if (from_slot < 0 || to_slot < 0 || - from_slot >= state.max_slots || to_slot >= state.max_slots) - return -1; - std::swap(state.slot_name[from_slot], state.slot_name[to_slot]); - std::swap(state.slot_id[from_slot], state.slot_id[to_slot]); - if (state.host_slot == from_slot) state.host_slot = to_slot; - else if (state.host_slot == to_slot) state.host_slot = from_slot; - /* Swap peer bindings with seats. */ - if (from_slot < kAeLanMaxSlots && to_slot < kAeLanMaxSlots) { - std::swap(g_lnch_lan_peers[from_slot], g_lnch_lan_peers[to_slot]); - std::swap(g_lnch_lan_peer_ok[from_slot], g_lnch_lan_peer_ok[to_slot]); - } - state.started = false; - ae_np_lan_sync_legacy_names(state); - if (!ae_np_write_lan_state(state)) return -1; - ae_np_lan_send_update_to_peers(state); - return 0; - } - if (ae_np_use_ws_members() && psx_lobby_is_host()) - return psx_lobby_move_member(from_slot, to_slot); - return -1; - } - - int ae_np_kick_member(void*, int slot) { - if (g_lnch_hosting_lan) { - AeLanLobbyState state; - if (!ae_np_read_lan_state(&state)) return -1; - if (slot < 0 || slot >= state.max_slots || slot == state.host_slot) - return -1; - if (state.slot_name[slot].empty()) return -1; - state.slot_name[slot].clear(); - state.slot_id[slot].clear(); - state.started = false; - ae_np_lan_sync_legacy_names(state); - if (!ae_np_write_lan_state(state)) return -1; - if (slot < kAeLanMaxSlots && g_lnch_lan_peer_ok[slot]) { - ae_np_lan_udp_sendto(g_lnch_lan_peers[slot], "MOTK1 KICK\n"); - ae_np_lan_clear_peer_slot(slot); - } - ae_np_lan_send_update_to_peers(state); - return 0; - } - if (ae_np_use_ws_members() && psx_lobby_is_host()) - return psx_lobby_kick(slot); - return -1; + if (!g_lnch_hosting_lan || from_slot < 0 || from_slot > 1 || + to_slot < 0 || to_slot > 1 || from_slot == to_slot) return -1; + AeLanLobbyState state; + if (!ae_np_read_lan_state(&state)) return -1; + state.host_slot = 1 - state.host_slot; + state.started = false; + return ae_np_write_lan_state(state) ? 0 : -1; } int ae_np_local_ready(void*) { return psx_lobby_local_ready(); } @@ -6539,26 +4640,10 @@ namespace { int ae_np_request_start(void*, const RecompLauncherCSettings* settings) { if (g_lnch_hosting_lan) { AeLanLobbyState state; - if (!ae_np_read_lan_state(&state) || ae_np_lan_occupied(state) < 2) - return -1; + if (!ae_np_read_lan_state(&state) || state.joiner_name.empty()) return -1; state.started = true; - state.session_id += 1u; - if (state.session_id == 0) state.session_id = 1; - g_lnch_lan_session_id = state.session_id; - if (!ae_np_write_lan_state(state)) return -1; - ae_np_lan_send_update_to_peers(state); - char start_msg[64]; - std::snprintf(start_msg, sizeof(start_msg), "MOTK1 START\n%u\n", - (unsigned)state.session_id); - for (int i = 0; i < kAeLanMaxSlots; ++i) { - if (g_lnch_lan_peer_ok[i]) - ae_np_lan_udp_sendto(g_lnch_lan_peers[i], start_msg); - } - return 0; + return ae_np_write_lan_state(state) ? 0 : -1; } - if (!psx_lobby_is_host()) return -1; - /* Ensure host seat is ready even if pump hasn't run since rematch. */ - (void)psx_lobby_set_ready(1); PsxLobbyMatchCaps caps = ae_netplay_caps_from_settings(settings); return psx_lobby_request_start(&caps); } @@ -6568,32 +4653,13 @@ namespace { !g_lnch_pending_direct_launch.enabled) { AeLanLobbyState state; if (ae_np_read_lan_state(&state) && state.started) { - /* Free the lobby UDP port before delay-sync binds it. */ - ae_np_lan_udp_close(); - g_lnch_lan_session_id = state.session_id ? state.session_id : 1u; g_lnch_pending_direct_launch = {}; g_lnch_pending_direct_launch.enabled = 1; - { - int local_slot = g_lnch_hosting_lan ? state.host_slot : g_lnch_lan_my_slot; - if (local_slot < 0) - local_slot = g_lnch_hosting_lan ? state.host_slot : 1; - g_lnch_pending_direct_launch.local_slot = local_slot; - } - /* -1 => resolve at netplay start (prefer NETPLAY/P1 card). */ - g_lnch_pending_direct_launch.input_player = -1; - g_lnch_pending_direct_launch.session_id = g_lnch_lan_session_id; - g_lnch_pending_direct_launch.input_delay = g_lnch_lobby_input_delay; - g_lnch_pending_direct_launch.max_slots = - state.max_slots >= 2 ? state.max_slots - : (g_lnch_host_max_slots >= 2 ? g_lnch_host_max_slots - : (g_lnch_game_players >= 2 ? g_lnch_game_players : 2)); - if (g_lnch_pending_direct_launch.max_slots > PSX_MAX_PLAYERS) - g_lnch_pending_direct_launch.max_slots = PSX_MAX_PLAYERS; - if (g_lnch_pending_direct_launch.max_slots > kAeLanMaxSlots) - g_lnch_pending_direct_launch.max_slots = kAeLanMaxSlots; - g_lnch_pending_direct_launch.force_input_relay = 0; - g_lnch_pending_direct_launch.force_turn = 0; - g_lnch_pending_direct_launch.player_count = ae_np_lan_occupied(state); + g_lnch_pending_direct_launch.local_slot = g_lnch_hosting_lan + ? state.host_slot : 1 - state.host_slot; + g_lnch_pending_direct_launch.input_player = 0; + g_lnch_pending_direct_launch.session_id = 1; + g_lnch_pending_direct_launch.input_delay = 2; if (g_lnch_hosting_lan) { const size_t colon = state.endpoint.rfind(':'); const char* port = colon == std::string::npos @@ -6601,11 +4667,11 @@ namespace { std::snprintf(g_lnch_pending_direct_launch.bind_hostport, sizeof(g_lnch_pending_direct_launch.bind_hostport), "0.0.0.0:%s", port); - /* 3+ host-as-relay: empty peer → lan_hub. 2P: accept-first. */ - g_lnch_pending_direct_launch.peer_hostport[0] = '\0'; } else { std::snprintf(g_lnch_pending_direct_launch.bind_hostport, - sizeof(g_lnch_pending_direct_launch.bind_hostport), "0.0.0.0:0"); + sizeof(g_lnch_pending_direct_launch.bind_hostport), "%s", + g_lnch_lan_guest_bind.empty() + ? "0.0.0.0:0" : g_lnch_lan_guest_bind.c_str()); std::snprintf(g_lnch_pending_direct_launch.peer_hostport, sizeof(g_lnch_pending_direct_launch.peer_hostport), "%s", state.endpoint.c_str()); @@ -6619,35 +4685,6 @@ namespace { psx_lobby_clear_launch_pending(); } - /* After a match soft-exit: keep seats, clear started/ready, rebind LAN UDP. */ - void ae_np_prepare_lobby_rematch(void) { - g_lnch_pending_direct_launch = {}; - psx_lobby_set_ready(0); - psx_lobby_clear_launch_pending(); - psx_lobby_clear_signals(); - psx_lobby_set_ice_signal_accept(0); - if (!(g_lnch_hosting_lan || g_lnch_joined_lan)) return; - AeLanLobbyState st; - if (ae_np_read_lan_state(&st)) { - st.started = false; - (void)ae_np_write_lan_state(st); - } - if (g_lnch_hosting_lan && !g_lnch_lan_endpoint.empty()) { - ae_np_lan_udp_close(); - (void)ae_np_lan_udp_ensure(true, ae_np_lan_endpoint_port(g_lnch_lan_endpoint)); - if (ae_np_read_lan_state(&st)) - ae_np_lan_send_update_to_peers(st); - } else if (g_lnch_remote_lan) { - ae_np_lan_udp_close(); - (void)ae_np_lan_udp_ensure(false, 0); - g_lnch_lan_join_pulse_ms = 0; - } - } - - const char* ae_np_lan_endpoint_cstr(void) { - return g_lnch_lan_endpoint.empty() ? nullptr : g_lnch_lan_endpoint.c_str(); - } - int ae_np_fill_launch(void*, RecompLauncherCNetplayLaunch* out) { if (!out) return 0; if (g_lnch_pending_direct_launch.enabled) { @@ -6657,42 +4694,26 @@ namespace { const PsxLobbyJoinInfo* ji = psx_lobby_join_info(); if (!ji || !ji->ok) return 0; const PsxLobbyMatchCaps* caps = psx_lobby_match_caps(); - /* Host match_caps are required online — do not silently default D/relay. */ - if (!caps || !caps->valid) return 0; out->enabled = 1; out->local_slot = ji->local_slot; - /* -1 => resolve at netplay start (prefer NETPLAY/P1 card). */ - out->input_player = -1; + out->input_player = 0; std::snprintf(out->bind_hostport, sizeof(out->bind_hostport), "%s", ji->bind_hostport); std::snprintf(out->peer_hostport, sizeof(out->peer_hostport), "%s", ji->peer_hostport); out->session_id = ji->session_id; - out->input_delay = caps->input_delay; - out->max_slots = ji->max_slots >= 2 ? ji->max_slots - : (g_lnch_game_players >= 2 ? g_lnch_game_players : 2); - if (out->max_slots > PSX_MAX_PLAYERS) out->max_slots = PSX_MAX_PLAYERS; - /* Seated width for delay-sync (not lobby ceiling). Bump for sparse - * seat indices so a moved player at slot N still fits. */ - { - int seated = ji->player_count > 0 ? ji->player_count : 0; - int high = ji->local_slot; - const int mc = psx_lobby_member_count(); - for (int i = 0; i < mc; ++i) { - PsxLobbyMember mem{}; - if (!psx_lobby_member_get(i, &mem)) continue; - if (mem.slot > high) high = mem.slot; - } - if (high + 1 > seated) seated = high + 1; - if (seated < 2) seated = out->max_slots; - if (seated > out->max_slots) seated = out->max_slots; - out->player_count = seated; - } - out->force_input_relay = - (caps && caps->valid && caps->force_input_relay) ? 1 : 0; - out->force_turn = - (caps && caps->valid && caps->force_turn) ? 1 : 0; + out->input_delay = (caps && caps->valid) ? caps->input_delay : 2; return 1; } + const char* ae_np_last_error(void*) { + const PsxLobbyJoinInfo* ji = psx_lobby_join_info(); + return (ji && ji->last_error[0]) ? ji->last_error : nullptr; + } + + void ae_np_clear_last_error(void*) { + PsxLobbyJoinInfo* ji = const_cast(psx_lobby_join_info()); + if (ji) ji->last_error[0] = '\0'; + } + RecompLauncherCNetplayCallbacks g_lnch_netplay_callbacks = { nullptr, ae_np_default_url, @@ -6723,16 +4744,9 @@ namespace { ae_np_clear_launch_pending, ae_np_fill_launch, ae_np_local_address_get, - ae_np_kick_member, + nullptr, /* kick_member — optional */ ae_np_last_error, ae_np_clear_last_error, - ae_np_input_delay_get, - ae_np_input_delay_set, - ae_np_force_input_relay_get, - ae_np_force_input_relay_set, - ae_np_lobby_max_slots, - ae_np_force_turn_get, - ae_np_force_turn_set, }; } // namespace #endif @@ -6743,16 +4757,10 @@ int main(int argc, char** argv) { std::setvbuf(stderr, nullptr, _IOLBF, 0); std::fprintf(stderr, "psxrecomp: main() entered\n"); std::fflush(stderr); -#if defined(RECOMP_LAUNCHER) - launcher_boot_timing_mark("host:main_enter"); -#endif /* Install crash handlers early so they catch issues during init too. * Writes psx_last_run_report.json on signal/SEH/atexit/fail-fast. */ psx_crash_trace_install_handlers(); -#if defined(RECOMP_LAUNCHER) - launcher_boot_timing_mark("host:crash_handlers"); -#endif const char* bios_path = PSX_DEFAULT_BIOS_PATH; const char* game_config_path = nullptr; @@ -6854,6 +4862,7 @@ int main(int argc, char** argv) { bios_path = argv[i]; bios_from_cli = true; bios_explicit = true; + bios_explicit = true; } else { std::fprintf(stderr, "psxrecomp: ignoring unexpected positional argument after BIOS selection: %s\n", @@ -6898,33 +4907,36 @@ int main(int argc, char** argv) { bool memcard1_enabled = true; bool memcard2_enabled = true; /* [controller] device routing (defaults: P1 keyboard/digital, P2 none). */ - /* Dev builds default Player 1 to the first connected controller ("auto"). - * Strict per-slot routing is the default (PSX_DEV_INPUT off): only the - * assigned device drives each port. Opt in with PSX_DEV_INPUT=1 to merge - * keyboard + every controller onto P1 for quick testing. Release keeps - * "keyboard" until the launcher assigns devices. */ -std::string player_device[PSX_MAX_PLAYERS]; - int player_mode[PSX_MAX_PLAYERS]; - int player_deadzone[PSX_MAX_PLAYERS]; - int ctrl_locked_mode[PSX_MAX_PLAYERS]; - for (int i = 0; i < PSX_MAX_PLAYERS; ++i) { + /* Dev builds default Player 1 to the first connected controller ("auto"): + * combined with dev-any-input (dev_any_input_enabled(), default ON) the + * selected controller, EVERY other plugged-in controller, AND the keyboard + * all drive P1 with no launcher setup. If no controller is present, "auto" + * opens nothing and the keyboard/any-controller merge still drives P1. + * Release keeps "keyboard" (the launcher assigns devices). */ #if defined(PSX_DEBUG_TOOLS) - player_device[i] = (i == 0) ? "auto" : "none"; + std::string p1_device = "auto"; #else - player_device[i] = (i == 0) ? "keyboard" : "none"; + std::string p1_device = "keyboard"; #endif - player_mode[i] = PSXRecompV4::PAD_MODE_HYBRID; - player_deadzone[i] = kDefaultDeadzoneRaw; - ctrl_locked_mode[i] = PSXRecompV4::PAD_MODE_HYBRID; - } + std::string p2_device = "none"; + int p1_mode = PSXRecompV4::PAD_MODE_HYBRID; + int p2_mode = PSXRecompV4::PAD_MODE_HYBRID; bool ctrl_allow_hybrid = true; /* game.toml [controller] allow_hybrid; false hides Hybrid in the launcher */ bool ctrl_lock_mode = false; /* game.toml [controller] lock_mode; true hides the whole pad-mode selector */ bool ctrl_lock_device = false; /* game.toml [controller] lock_device; true hides the Player controller cards entirely */ + /* The game-DECLARED port modes, captured at game.toml load and immune to the + * settings.toml overrides below. Under lock_mode these are the only valid + * modes (the game supports exactly one pad type), so they are what the + * runtime clamps to and what the launcher locks its selector to. */ + int ctrl_locked_p1_mode = PSXRecompV4::PAD_MODE_HYBRID; + int ctrl_locked_p2_mode = PSXRecompV4::PAD_MODE_HYBRID; bool ws_offered = true; /* game.toml [widescreen] offer; false hides the launcher toggle + clamps 4:3 */ bool ws_ultrawide_offered = false; + bool ws_adaptive_view_supported = false; + bool frame_interpolation_offered = true; + bool skip_fmv_offered = true; bool vulkan_offered = false; /* game.toml [video] offer_vulkan; developer opt-in for launcher visibility */ - /* Legacy single deadzone (<0 => keep per-slot / input.ini defaults). */ - int resolved_deadzone = -1; + int resolved_deadzone = -1; /* <0 => keep input.ini/runtime default (12000) */ /* Localization: the effective language (game.toml default -> settings.toml -> * launcher choice), applied to the translation layer AFTER the launcher runs. * lang_menu_options drives the launcher's "Localization" dropdown (empty => @@ -6945,27 +4957,19 @@ std::string player_device[PSX_MAX_PLAYERS]; std::vector warm_cd_routes; uint32_t game_entry_pc = 0; bool fast_boot = false; /* DEPRECATED alias: HLE boot-skip only */ - bool bios_hle = false; /* HLE kernel-service tier (bios_hle.c) */ + /* REQUESTED intent, never the granted result: "run the HLE tier" as asked + * for by game.toml / settings.toml / a netplay seed / env. What each of the + * tier's two axes actually gets is derived per BIOS image by + * psx_bios_hle_plan() at bring-up and must NOT be written back here — the + * session_reboot path re-enters that block, and folding a refusal back into + * the request would make a rematch differ from the first launch. */ + bool bios_hle_requested = false; /* [runtime] bios_hle (bios_hle.c) */ bool bios_hle_keep_intro = false; /* Text-image guard source, captured at config load; armed after the disc * path is resolved (arm_text_image_guard). */ std::string text_guard_exe_path; uint32_t text_guard_load_addr = 0; - /* Overlay cache init is deferred until after the launcher window so ABI - * preflight / resident DLL loads do not delay first paint. */ - bool deferred_overlay_cache = false; - std::filesystem::path deferred_overlay_project_root; - std::vector deferred_overlay_native_block; - uint32_t deferred_overlay_config_hash = 0; - std::string deferred_overlay_backend; - bool deferred_has_overlay_ac = false; - std::string deferred_overlay_ac; - bool deferred_has_overlay_ac_tcc = false; - std::string deferred_overlay_ac_tcc; - bool deferred_overlay_capture_history = false; - std::string deferred_overlay_capture_persist_dir; - if (game_config_path) { try { const auto gc = PSXRecompV4::load_game_config(game_config_path); @@ -6973,9 +4977,6 @@ std::string player_device[PSX_MAX_PLAYERS]; game_id = gc.id; game_region = gc.region; game_players = gc.players; - g_offline_pad_count = game_players > 0 ? game_players : 1; - if (g_offline_pad_count > PSX_MAX_PLAYERS) - g_offline_pad_count = PSX_MAX_PLAYERS; game_has_disc_crc = gc.has_disc_crc; game_disc_crc = gc.disc_crc; if (!gc.discs.empty()) resolved_disc = gc.discs.front(); @@ -7037,6 +5038,7 @@ std::string player_device[PSX_MAX_PLAYERS]; g_fmv_skip_no_xa_hold = gc.runtime.video_fmv_skip_no_xa_hold; g_ws_anchor_addr = gc.ws_sprite_anchor_addr; g_ws_hud_sprt = gc.ws_hud_sprt_squash; + gpu_ws_set_auto_ui_squash(gc.ws_auto_ui_squash ? 1 : 0); /* [widescreen] full_2d — opt a pure-2D sprite game (MMX6) into the * widescreen present path. Applied to the GPU layer up front so the * ws engage at game entry classifies every frame as gameplay. */ @@ -7087,6 +5089,8 @@ std::string player_device[PSX_MAX_PLAYERS]; * cleanup of synthetic native-wide margins. */ gpu_ws_set_clear_reveal(gc.ws_clear_reveal ? 1 : 0); gpu_ws_set_cull_guard_pixels(gc.ws_cull_guard_pixels); + gpu_ws_set_activation_guard_pixels( + gc.ws_cull_activation_guard_pixels); gpu_ws_set_explicit_cull_sites( gc.ws_cull_bias_sites.data(), (int)gc.ws_cull_bias_sites.size(), gc.ws_cull_slti_sites.data(), (int)gc.ws_cull_slti_sites.size(), @@ -7101,6 +5105,74 @@ std::string player_device[PSX_MAX_PLAYERS]; gc.ws_cull_plane_nx_sites.data(), (int)gc.ws_cull_plane_nx_sites.size()); gpu_ws_set_xclip_load_sites( gc.ws_cull_xclip_load_sites.data(), (int)gc.ws_cull_xclip_load_sites.size()); + { + std::vector addresses, expected, results; + addresses.reserve(gc.ws_cull_keep_sites.size()); + expected.reserve(gc.ws_cull_keep_sites.size()); + results.reserve(gc.ws_cull_keep_sites.size()); + for (const auto& site : gc.ws_cull_keep_sites) { + addresses.push_back(site.address); + expected.push_back(site.expected); + results.push_back(site.result); + } + gpu_ws_set_cull_keep_sites( + addresses.data(), expected.data(), results.data(), + (int)addresses.size()); + } + { + std::vector addresses, expected; + addresses.reserve(gc.ws_cull_angle_sites.size()); + expected.reserve(gc.ws_cull_angle_sites.size()); + for (const auto& site : gc.ws_cull_angle_sites) { + addresses.push_back(site.address); + expected.push_back(site.expected); + } + gpu_ws_set_angle_sites( + addresses.data(), expected.data(), (int)addresses.size()); + } + { + std::vector addresses, expected, thresholds; + std::vector object_regs, x_regs, z_regs, y_regs; + std::vector queue_guards; + addresses.reserve(gc.ws_aspect_cone.sites.size()); + expected.reserve(gc.ws_aspect_cone.sites.size()); + thresholds.reserve(gc.ws_aspect_cone.sites.size()); + object_regs.reserve(gc.ws_aspect_cone.sites.size()); + x_regs.reserve(gc.ws_aspect_cone.sites.size()); + z_regs.reserve(gc.ws_aspect_cone.sites.size()); + y_regs.reserve(gc.ws_aspect_cone.sites.size()); + queue_guards.reserve(gc.ws_aspect_cone.sites.size()); + const auto effective_reg = [](uint32_t site_reg, + uint32_t default_reg) { + return site_reg == 0xFFFFFFFFu + ? default_reg : site_reg; + }; + for (const auto& site : gc.ws_aspect_cone.sites) { + addresses.push_back(site.address); + expected.push_back(site.expected); + thresholds.push_back(site.cosine_threshold); + object_regs.push_back(effective_reg( + site.object_reg, gc.ws_aspect_cone.object_reg)); + x_regs.push_back(effective_reg( + site.x_reg, gc.ws_aspect_cone.x_reg)); + z_regs.push_back(effective_reg( + site.z_reg, gc.ws_aspect_cone.z_reg)); + y_regs.push_back(effective_reg( + site.y_reg, gc.ws_aspect_cone.y_reg)); + queue_guards.push_back(site.queue_guard ? 1u : 0u); + } + gpu_ws_set_aspect_cone( + addresses.data(), expected.data(), thresholds.data(), + object_regs.data(), x_regs.data(), z_regs.data(), + y_regs.data(), queue_guards.data(), (int)addresses.size(), + gc.ws_aspect_cone.forward_addr, + gc.ws_aspect_cone.object_type_offset, + gc.ws_aspect_cone.hysteresis_pixels, + gc.ws_aspect_cone.queue_reserve, + gc.ws_aspect_cone.queue_count_addrs.data(), + gc.ws_aspect_cone.queue_capacities.data(), + gc.ws_aspect_cone.queue_type_masks.data()); + } gte_ws_configure_dome_sites( gc.ws_dome_call_sites.data(), (int)gc.ws_dome_call_sites.size()); /* [widescreen.cull] per-game gates + signature immediates for the @@ -7113,6 +5185,10 @@ std::string player_device[PSX_MAX_PLAYERS]; gc.ws_cull_h_imms.data(), (int)gc.ws_cull_h_imms.size()); ws_offered = gc.ws_offered; ws_ultrawide_offered = gc.ws_ultrawide_offered; + ws_adaptive_view_supported = gc.ws_adaptive_view; + frame_interpolation_offered = + gc.runtime.video_offer_frame_interpolation; + skip_fmv_offered = gc.runtime.video_offer_skip_fmv; vulkan_offered = gc.vulkan_offered; /* Register the [widescreen.backdrop] store PCs so the dirty-RAM * interpreter applies the backdrop screenX squash on the interp @@ -7122,47 +5198,27 @@ std::string player_device[PSX_MAX_PLAYERS]; psx_ws_set_backdrop_sites(gc.ws_backdrop_x_sites.data(), (int)gc.ws_backdrop_x_sites.size()); g_audio_spu_hq = gc.runtime.audio_spu_hq; + g_audio_buffer_ms = (double)gc.runtime.audio_buffer_ms; g_auto_skip_fmv = gc.runtime.video_auto_skip_fmv ? 1 : 0; /* [controller] game-declared input defaults (settings.toml/launcher * still override below). */ if (gc.runtime.has_default_mode) { - for (int i = 0; i < PSX_MAX_PLAYERS; ++i) { - player_mode[i] = (i == 0) ? gc.runtime.default_p1_mode - : gc.runtime.default_p2_mode; - /* Beyond P2, reuse default_mode (same as P1 when set via default_mode). */ - if (i >= 2) player_mode[i] = gc.runtime.default_p1_mode; - } + p1_mode = gc.runtime.default_p1_mode; + p2_mode = gc.runtime.default_p2_mode; } - for (int i = 0; i < PSX_MAX_PLAYERS; ++i) - ctrl_locked_mode[i] = player_mode[i]; + ctrl_locked_p1_mode = gc.runtime.default_p1_mode; + ctrl_locked_p2_mode = gc.runtime.default_p2_mode; ctrl_allow_hybrid = gc.runtime.controller_allow_hybrid; ctrl_lock_mode = gc.runtime.controller_lock_mode; ctrl_lock_device = gc.runtime.controller_lock_device; - if (gc.runtime.has_deadzone) { - resolved_deadzone = gc.runtime.deadzone; - for (int i = 0; i < PSX_MAX_PLAYERS; ++i) - player_deadzone[i] = gc.runtime.deadzone; - } - /* Console port for SCPH-1070 when offline/netplay arms multitap. - * Most titles use Port 1; Bomberman Party Edition needs Port 2. */ - if (gc.runtime.has_multitap_port) { - const int phys = (gc.runtime.multitap_port == 2) ? 1 : 0; - sio_set_multitap_port(phys); - std::fprintf(stdout, - "psxrecomp: multitap on console Port %d\n", - gc.runtime.multitap_port); - } - /* LEGACY per-game pad-config opt-in (default modern). Only Tomba sets - * it, so its launcher Hybrid mode's analog<->digital flip doesn't make - * libpad manufacture a 1-frame "pad unplugged". sio_init() does not - * touch this flag, so applying it here (config-load time) is stable. - * Full history + removal plan: psxrecomp sio.c g_pad_legacy_cfg. */ - sio_set_legacy_cfg(gc.runtime.legacy_pad_config ? 1 : 0); + if (gc.runtime.has_deadzone) resolved_deadzone = gc.runtime.deadzone; + if (gc.runtime.has_anti_deadzone) + controller_anti_deadzone = gc.runtime.anti_deadzone; { const char *e = std::getenv("PSX_GL_FORCE_CPU_PRESENT"); if (e && e[0] && e[0] != '0') g_gl_fbo_present = 0; } game_entry_pc = gc.entry_pc; fast_boot = gc.runtime.fast_boot; - bios_hle = gc.runtime.bios_hle; + bios_hle_requested = gc.runtime.bios_hle; bios_hle_keep_intro = gc.runtime.bios_hle_keep_intro; /* Developer compatibility finding, applied before BIOS selection. * Not exposed to settings.toml on purpose — see BIOS_SELECTION.md. */ @@ -7195,22 +5251,182 @@ std::string player_device[PSX_MAX_PLAYERS]; "psxrecomp: overlay_region_floor = 0x%05X (game text end)\n", g_overlay_region_floor); } - /* Overlay DLL cache (Layer A): stash config now; heavy init - * (cache scan / ABI preflight / resident LoadLibrary) runs after - * the launcher window so first UI paint is not blocked. */ + /* Overlay DLL cache (Layer A). Off unless enabled in [runtime]; + * when on, capture overlay bytes and scan cache// for + * precompiled overlay DLLs. */ if (gc.runtime.overlay_cache) { - deferred_overlay_cache = true; - deferred_overlay_project_root = gc.project_root; - deferred_overlay_native_block = gc.runtime.overlay_native_block; - deferred_overlay_config_hash = + std::filesystem::path exe_dir = exe_dir_from_argv(argv[0]); + std::string cache_dir = (exe_dir / "cache").string(); + std::filesystem::path captures_path = + resolve_overlay_capture_path(gc.project_root, exe_dir, game_id); + if (!overlay_capture_set_path(captures_path.string().c_str())) { + captures_path = exe_dir / "overlay_captures.json"; + if (!overlay_capture_set_path(captures_path.string().c_str())) + throw std::runtime_error("overlay capture path exceeds runtime limit"); + } + overlay_capture_set_enabled(1); + std::fprintf(stdout, + "psxrecomp: additive overlay capture store = %s (+ .d history)\n", + captures_path.string().c_str()); + std::string capture_persist_dir; + if (gc.runtime.overlay_capture_history && + !gc.runtime.overlay_capture_persist_dir.empty()) { + std::filesystem::path persist = + gc.project_root / gc.runtime.overlay_capture_persist_dir; + std::error_code persist_ec; + std::filesystem::create_directories(persist, persist_ec); + if (persist_ec) { + std::fprintf(stderr, + "psxrecomp: cannot create overlay capture history %s: %s\n", + persist.string().c_str(), persist_ec.message().c_str()); + } else { + capture_persist_dir = persist.string(); + } + } + overlay_capture_configure_history( + gc.runtime.overlay_capture_history ? 1 : 0, + capture_persist_dir.empty() ? nullptr : + capture_persist_dir.c_str(), + game_id.c_str()); + const uint32_t overlay_config_hash = PSXRecompV4::overlay_codegen_config_hash(gc); - deferred_overlay_backend = gc.runtime.overlay_backend; - deferred_has_overlay_ac = gc.runtime.has_overlay_autocompile_cmd; - deferred_overlay_ac = gc.runtime.overlay_autocompile_cmd; - deferred_has_overlay_ac_tcc = gc.runtime.has_overlay_autocompile_cmd_tcc; - deferred_overlay_ac_tcc = gc.runtime.overlay_autocompile_cmd_tcc; - deferred_overlay_capture_history = gc.runtime.overlay_capture_history; - deferred_overlay_capture_persist_dir = gc.runtime.overlay_capture_persist_dir; + overlay_loader_init(cache_dir.c_str(), game_id.c_str(), + overlay_config_hash); + std::fprintf(stdout, + "psxrecomp: overlay codegen config hash = %08x\n", + (unsigned)overlay_config_hash); + for (uint32_t addr : gc.runtime.overlay_native_block) { + overlay_loader_native_block_add(addr); + } + if (!gc.runtime.overlay_native_block.empty()) { + std::fprintf(stdout, + "psxrecomp: overlay native blocklist seeded with %zu entr%s\n", + gc.runtime.overlay_native_block.size(), + gc.runtime.overlay_native_block.size() == 1 ? "y" : "ies"); + } + /* Scoped pre-DMA journaling and the shutdown snapshot remain + * active whenever the cache is on, including toolchain-less + * production machines. The periodic pressure trigger exists to + * feed a live compiler; leave it off when no provider command + * exists, otherwise it rewrites manifests every cooldown while + * being unable to reduce interpreter residency. */ + overlay_autocapture_set_enabled(0); + /* Resolve the overlay tier first so we wire the RIGHT compiler's + * autocompile command. gcc is "available" only when a gcc cmd is + * configured AND a gcc toolchain is actually reachable (a real + * dev/production box). auto => gcc if so, else tcc; auto-no-gcc => + * tcc even with gcc present (simulate a toolchain-less user box). + * env PSX_OVERLAY_BACKEND overrides. Tiers: static > gcc > tcc > + * interp. */ + const char *cfg_backend = gc.runtime.overlay_backend.empty() + ? nullptr : gc.runtime.overlay_backend.c_str(); + int gcc_avail = gc.runtime.has_overlay_autocompile_cmd + && autocompile_toolchain_available(); + OverlayBackend eff = overlay_backend_resolve(cfg_backend, gcc_avail); + /* gcc and tcc run the IDENTICAL recompiler->C->DLL->load pipeline; + * only the compiler binary differs. Wire the autocompile spawn with + * the command for the resolved tier (tcc cmd for the tcc tier, gcc + * cmd otherwise). gcc shards already on disk still LOAD either way + * (the loader is compiler-blind), so a tcc box uses shipped gcc + * shards first and fills the rest with tcc. */ + std::string built_tcc_cmd; /* runtime-constructed bundled tcc cmd */ + std::string env_ac_cmd; + const std::string *ac_cmd = nullptr; + if (eff == OVERLAY_BACKEND_TCC) { + if (gc.runtime.has_overlay_autocompile_cmd_tcc) { + ac_cmd = &gc.runtime.overlay_autocompile_cmd_tcc; /* explicit override (dev) */ + } else { + /* PRODUCTION: construct the tcc autocompile cmd from the + * self-contained toolchain bundled beside the exe + * (/overlay_toolchain/ = embedded python + tcc + + * recompiler + compile_overlays.py + runtime headers). No + * system python or gcc required. */ + extern int g_psx_cps_mode; + std::filesystem::path xd = exe_dir_from_argv(argv[0]); + std::filesystem::path tk = xd / "overlay_toolchain"; + std::filesystem::path py = tk / "python" / "python.exe"; + if (std::filesystem::exists(py)) { + auto cmd_quote = [](const std::string& s) { + return std::string("\"") + s + "\""; + }; + built_tcc_cmd = + cmd_quote(py.string()) + " " + + cmd_quote((tk / "compile_overlays.py").string()) + + " --captures " + cmd_quote(captures_path.string()) + + " --game-toml " + cmd_quote(std::string( + game_config_path ? game_config_path : "game.toml")) + + " --recompiler " + cmd_quote((tk / "psxrecomp-game.exe").string()) + + " --runtime-include " + cmd_quote((tk / "include").string()) + + " --out-dir " + cmd_quote((xd / "cache").string()) + + (g_psx_cps_mode ? " --cps" : "") + + " --compiler tcc --tcc " + + cmd_quote((tk / "tcc" / "tcc.exe").string()); + ac_cmd = &built_tcc_cmd; + std::fprintf(stdout, + "psxrecomp: tcc tier using bundled toolchain (%s)\n", + tk.string().c_str()); + } else { + std::fprintf(stdout, + "psxrecomp: tcc tier active but no bundled toolchain at %s " + "(overlay gaps -> interpreter)\n", tk.string().c_str()); + } + } + } else { + if (gc.runtime.has_overlay_autocompile_cmd) + ac_cmd = &gc.runtime.overlay_autocompile_cmd; + } + /* A developer may run a game config from one checkout against a + * runtime/recompiler built in another worktree. Let the launch + * pin the producer command to that exact worktree so the baked + * codegen hash, additive-capture reader, and runtime headers + * cannot silently drift through a game-repo junction. */ + if (const char *e = std::getenv("PSX_OVERLAY_AUTOCOMPILE_CMD")) { + if (e[0]) { + env_ac_cmd = e; + ac_cmd = &env_ac_cmd; + std::fprintf(stdout, + "psxrecomp: overlay autocompile command overridden by environment\n"); + } + } + if (const char *e = std::getenv("PSX_OVERLAY_AUTOCOMPILE_OFF")) { + if (e[0] && e[0] != '0') { + ac_cmd = nullptr; + std::fprintf(stdout, + "psxrecomp: overlay autocompile disabled by environment\n"); + } + } + if (ac_cmd) { + /* Pin the compile's WRITE cache + READ captures to the SAME + * canonical locations the loader uses (cache_dir = /cache, + * /overlay_captures.json — set above). The framework owns + * the cache location; no game.toml --out-dir/--captures can make + * the write drift from the read. Single source of truth, all + * games, dev or prod. */ + autocompile_set_cache_paths(cache_dir.c_str(), + captures_path.string().c_str()); + std::string ac_cwd = gc.project_root.string(); + if (const char *e = std::getenv("PSX_OVERLAY_AUTOCOMPILE_CWD")) { + if (e[0]) ac_cwd = e; + } + autocompile_configure(ac_cmd->c_str(), ac_cwd.c_str()); + overlay_autocapture_set_enabled(1); + /* cwd is printed because every relative path in + * overlay_autocompile_cmd resolves against it, and it is + * derived from game.toml's detected project root. Staging a + * game.toml somewhere that is not the real project root + * therefore repoints the whole compile command, and the only + * symptom is that nothing ever goes native and frame times + * are 10-30x worse. Printing the root makes that a one-line + * diagnosis instead of a TCP query against + * autocompile_status. */ + std::fprintf(stdout, + "psxrecomp: overlay autocompile enabled (%s); cache=%s; captures=%s; cwd=%s\n", + overlay_backend_name(eff), cache_dir.c_str(), + captures_path.string().c_str(), ac_cwd.c_str()); + } + code_provider_init(cfg_backend, gcc_avail); + /* (sljit removed 2026-07-15: overlay_loader_apply_live_policy was + * called here once the backend resolved.) */ } std::fprintf(stdout, "psxrecomp: loaded game config %s (%s, %s)\n", game_config_path, game_name.c_str(), game_id.c_str()); @@ -7220,9 +5436,6 @@ std::string player_device[PSX_MAX_PLAYERS]; return 1; } } -#if defined(RECOMP_LAUNCHER) - launcher_boot_timing_mark("host:game_config_done"); -#endif if (!game_name.empty()) s_picker_game_name = game_name; @@ -7289,14 +5502,36 @@ std::string player_device[PSX_MAX_PLAYERS]; if (us.has_screen_kind) g_video_screen = us.screen_kind; if (us.has_auto_skip_fmv) g_auto_skip_fmv = us.auto_skip_fmv ? 1 : 0; if (us.has_turbo_loads) g_turbo_loads_enabled = us.turbo_loads ? 1 : 0; - if (us.has_fast_boot) fast_boot = us.fast_boot; - if (us.has_bios_hle) bios_hle = us.bios_hle; + /* fast_boot is deliberately NOT restored from settings.toml: the + * launcher exposes no control for it, so the persisted row can only + * echo whatever game.toml said on some EARLIER run — a write-only + * latch that silently overrode a later config change (flipping + * game.toml fast_boot=false still skipped the boot animation). + * Config + env own it until a launcher toggle exists; if one is + * added, restore this line together with the UI. The seed write + * below still persists the effective value for inspection. */ + if (us.has_bios_hle) bios_hle_requested = us.bios_hle; if (us.has_fullscreen) g_fullscreen = us.fullscreen; if (us.has_aspect_ratio) { g_video_aspect_num = us.aspect_num; g_video_aspect_den = us.aspect_den; } + if (us.has_adaptive_view) g_ws_adaptive_view = us.adaptive_view; if (us.has_spu_hq) g_audio_spu_hq = us.spu_hq; + /* Bundled BIOS: ignore any persisted bios_path (same clamp-as-well- + * as-hide treatment as lock_mode / ws_offered below) so a stale + * settings.toml or launcher-less build can never point a bundled + * build at a foreign image. The identity gate is the backstop. */ + /* A non-empty BIOS in settings.toml is the player's choice, made in + * the launcher's SYSTEM row, and is honoured on every build — that row + * is the only way to opt into a retail image now that bundled builds + * no longer prompt. An EMPTY value is equally meaningful: it is the + * launcher's Clear, i.e. "go back to the bundled BIOS", so it must NOT + * be adopted as a path. + * + * The identity gate is what protects a bundled build from a stale or + * foreign path here — previously this was guarded by refusing to read + * the setting at all, which would now silently ignore the player. */ if (us.has_bios_path && !bios_from_cli && !us.bios_path.empty()) { settings_bios_storage = us.bios_path.string(); bios_path = settings_bios_storage.c_str(); @@ -7310,16 +5545,11 @@ std::string player_device[PSX_MAX_PLAYERS]; if (us.has_memcard1_enabled) memcard1_enabled = us.memcard1_enabled; if (us.has_memcard2_enabled) memcard2_enabled = us.memcard2_enabled; if (us.has_language) resolved_language = us.language; - { - const int n = std::min(PSX_MAX_PLAYERS, - PSXRecompV4::UserSettings::kMaxControllerPlayers); - for (int i = 0; i < n; ++i) { - if (us.has_p_device[i]) player_device[i] = us.p_device[i]; - if (us.has_p_mode[i]) player_mode[i] = us.p_mode[i]; - if (us.has_p_deadzone[i]) player_deadzone[i] = us.p_deadzone[i]; - } - if (us.has_deadzone) resolved_deadzone = us.deadzone; - } + if (us.has_p1_device) p1_device = us.p1_device; + if (us.has_p2_device) p2_device = us.p2_device; + if (us.has_p1_mode) p1_mode = us.p1_mode; + if (us.has_p2_mode) p2_mode = us.p2_mode; + if (us.has_deadzone) resolved_deadzone = us.deadzone; if (us.has_low_latency_input) g_low_latency_input = us.low_latency_input ? 1 : 0; if (us.has_vsync) g_video_vsync = us.vsync; if (us.has_frame_interpolation) @@ -7328,6 +5558,14 @@ std::string player_device[PSX_MAX_PLAYERS]; g_frame_interpolation_fps = us.frame_interpolation_fps; } + /* The launcher must inspect the same explicit disc override that the + * runtime will validate and mount. Applying --disc only in the later + * resolve_disc_for_runtime() pass left the launcher showing game.toml's + * path, which may not exist in a build/worktree even though the CLI path + * is valid. */ + if (disc_override_path && disc_override_path[0]) + resolved_disc = normalize_disc_path_for_launch(disc_override_path); + /* lock_mode: the game supports exactly ONE pad type (e.g. X4 / Tomba 2 are * digital-only — X4's pre-DualShock libpad silently discards input from a * pad answering id 0x73). The launcher hides its selector for such games, @@ -7338,8 +5576,44 @@ std::string player_device[PSX_MAX_PLAYERS]; * config/settings source has been applied, so a locked game can never boot * a pad type it doesn't support. */ if (ctrl_lock_mode) { - for (int i = 0; i < PSX_MAX_PLAYERS; ++i) - player_mode[i] = ctrl_locked_mode[i]; + p1_mode = ctrl_locked_p1_mode; + p2_mode = ctrl_locked_p2_mode; + } + /* allow_hybrid=false removes Hybrid from the game's supported controller + * modes. Clamp an old persisted Hybrid value here as well as hiding it in + * recomp-ui, so launcher-less builds cannot revive an unsupported mode. + * Prefer each port's game-declared default; malformed/legacy configs that + * also default to Hybrid fall back to Analog, matching recomp-ui. */ + if (!ctrl_allow_hybrid) { + const int fallback_p1 = + ctrl_locked_p1_mode == PSXRecompV4::PAD_MODE_HYBRID + ? PSXRecompV4::PAD_MODE_ANALOG : ctrl_locked_p1_mode; + const int fallback_p2 = + ctrl_locked_p2_mode == PSXRecompV4::PAD_MODE_HYBRID + ? PSXRecompV4::PAD_MODE_ANALOG : ctrl_locked_p2_mode; + if (p1_mode == PSXRecompV4::PAD_MODE_HYBRID) p1_mode = fallback_p1; + if (p2_mode == PSXRecompV4::PAD_MODE_HYBRID) p2_mode = fallback_p2; + } + + /* A game may migrate Skip FMVs from generic Settings into its mod catalog. + * Clamp stale settings before seeding recomp-ui; an enabled activation + * plugin applies the feature after the final mod-plan commit. */ + if (!skip_fmv_offered && g_auto_skip_fmv) { + std::fprintf(stdout, + "psxrecomp: Skip FMVs is mod-owned for this title; " + "ignoring the legacy Settings value\n"); + g_auto_skip_fmv = 0; + } + /* Treat presentation interpolation the same way when a title moves it to + * Mods. Both the boolean and target rate are cleared so launcher-less + * starts cannot revive an old generic Settings selection. */ + if (!frame_interpolation_offered && + (g_frame_interpolation || g_frame_interpolation_fps)) { + std::fprintf(stdout, + "psxrecomp: Frame interpolation is mod-owned for this title; " + "ignoring the legacy Settings value\n"); + g_frame_interpolation = 0; + g_frame_interpolation_fps = 0; } /* [widescreen] offer=false: this title's widescreen is unported/unvalidated, @@ -7360,10 +5634,14 @@ std::string player_device[PSX_MAX_PLAYERS]; g_video_aspect_num = ws_offered ? 16 : 4; g_video_aspect_den = ws_offered ? 9 : 3; } + if (!ws_adaptive_view_supported) g_ws_adaptive_view = false; + g_ws_adaptive_max_num = ws_ultrawide_offered ? 21 : 16; + g_ws_adaptive_max_den = 9; /* Latency knobs: env overrides win over config (for A/B measurement). * PSX_LOW_LATENCY_INPUT=0/1 ; PSX_VSYNC=1(vsync)/0(immediate)/-1(adaptive); - * PSX_FRAME_INTERPOLATION=0/1; PSX_FRAME_INTERPOLATION_FPS=0|90+. */ + * PSX_FRAME_INTERPOLATION=0/1; PSX_FRAME_INTERPOLATION_FPS=0|60+; + * PSX_FRAME_INTERPOLATION_BLEND=linear|adaptive. */ if (const char *e = std::getenv("PSX_LOW_LATENCY_INPUT")) g_low_latency_input = atoi(e) ? 1 : 0; if (const char *e = std::getenv("PSX_VSYNC")) g_video_vsync = atoi(e); if (const char *e = std::getenv("PSX_SMOOTH_60FPS")) @@ -7372,7 +5650,18 @@ std::string player_device[PSX_MAX_PLAYERS]; g_frame_interpolation = atoi(e) ? 1 : 0; if (const char *e = std::getenv("PSX_FRAME_INTERPOLATION_FPS")) { int fps = atoi(e); - if (fps == 0 || fps >= 90) g_frame_interpolation_fps = fps; + if (fps == 0 || fps >= 60) g_frame_interpolation_fps = fps; + } + if (const char *e = std::getenv("PSX_FRAME_INTERPOLATION_BLEND")) { + if (strcmp(e, "adaptive") == 0 || strcmp(e, "clarity") == 0 || + strcmp(e, "1") == 0) { + g_frame_interpolation_blend_default = + PSX_MOD_FRAME_INTERPOLATION_MOTION_ADAPTIVE; + } else if (strcmp(e, "linear") == 0 || strcmp(e, "0") == 0) { + g_frame_interpolation_blend_default = + PSX_MOD_FRAME_INTERPOLATION_LINEAR; + } + g_frame_interpolation_blend = g_frame_interpolation_blend_default; } /* Apply writable-state isolation before game-options and launcher setup, @@ -7441,156 +5730,22 @@ std::string player_device[PSX_MAX_PLAYERS]; } } -#if defined(RECOMP_LAUNCHER) - launcher_boot_timing_mark("host:pre_overlay_worker"); -#endif - /* Overlay cache: run ABI preflight / resident DLL loads on a worker so the - * launcher can open immediately and init overlaps with UI time. Join before - * guest boot. When the launcher is skipped, the join still runs below. */ - std::thread overlay_init_thread; - std::exception_ptr overlay_init_exc; - auto run_deferred_overlay_init = [&]() { - std::filesystem::path exe_dir = exe_dir_from_argv(argv[0]); - std::string cache_dir = (exe_dir / "cache").string(); - std::filesystem::path captures_path = - resolve_overlay_capture_path(deferred_overlay_project_root, exe_dir, game_id); - if (!overlay_capture_set_path(captures_path.string().c_str())) { - captures_path = exe_dir / "overlay_captures.json"; - if (!overlay_capture_set_path(captures_path.string().c_str())) { - throw std::runtime_error( - "overlay capture path exceeds runtime limit"); - } - } - overlay_capture_set_enabled(1); - std::fprintf(stdout, - "psxrecomp: additive overlay capture store = %s (+ .d history)\n", - captures_path.string().c_str()); - std::string capture_persist_dir; - if (deferred_overlay_capture_history && - !deferred_overlay_capture_persist_dir.empty()) { - std::filesystem::path persist = - deferred_overlay_project_root / deferred_overlay_capture_persist_dir; - std::error_code persist_ec; - std::filesystem::create_directories(persist, persist_ec); - if (persist_ec) { - std::fprintf(stderr, - "psxrecomp: cannot create overlay capture history %s: %s\n", - persist.string().c_str(), persist_ec.message().c_str()); - } else { - capture_persist_dir = persist.string(); - } - } - overlay_capture_configure_history( - deferred_overlay_capture_history ? 1 : 0, - capture_persist_dir.empty() ? nullptr : - capture_persist_dir.c_str(), - game_id.c_str()); - overlay_loader_init(cache_dir.c_str(), game_id.c_str(), - deferred_overlay_config_hash); - for (uint32_t addr : deferred_overlay_native_block) { - overlay_loader_native_block_add(addr); - } - if (!deferred_overlay_native_block.empty()) { - std::fprintf(stdout, - "psxrecomp: overlay native blocklist seeded with %zu entr%s\n", - deferred_overlay_native_block.size(), - deferred_overlay_native_block.size() == 1 ? "y" : "ies"); - } - overlay_autocapture_set_enabled(0); - const char *cfg_backend = deferred_overlay_backend.empty() - ? nullptr : deferred_overlay_backend.c_str(); - int gcc_avail = deferred_has_overlay_ac - && autocompile_toolchain_available(); - OverlayBackend eff = overlay_backend_resolve(cfg_backend, gcc_avail); - std::string built_tcc_cmd; - std::string env_ac_cmd; - const std::string *ac_cmd = nullptr; - if (eff == OVERLAY_BACKEND_TCC) { - if (deferred_has_overlay_ac_tcc) { - ac_cmd = &deferred_overlay_ac_tcc; - } else { - extern int g_psx_cps_mode; - std::filesystem::path xd = exe_dir_from_argv(argv[0]); - std::filesystem::path tk = xd / "overlay_toolchain"; - std::filesystem::path py = tk / "python" / "python.exe"; - if (std::filesystem::exists(py)) { - auto cmd_quote = [](const std::string& s) { - return std::string("\"") + s + "\""; - }; - built_tcc_cmd = - cmd_quote(py.string()) + " " + - cmd_quote((tk / "compile_overlays.py").string()) + - " --captures " + cmd_quote(captures_path.string()) + - " --game-toml " + cmd_quote(std::string( - game_config_path ? game_config_path : "game.toml")) + - " --recompiler " + cmd_quote((tk / "psxrecomp-game.exe").string()) + - " --runtime-include " + cmd_quote((tk / "include").string()) + - " --out-dir " + cmd_quote((xd / "cache").string()) + - (g_psx_cps_mode ? " --cps" : "") + - " --compiler tcc --tcc " + - cmd_quote((tk / "tcc" / "tcc.exe").string()); - ac_cmd = &built_tcc_cmd; - std::fprintf(stdout, - "psxrecomp: tcc tier using bundled toolchain (%s)\n", - tk.string().c_str()); - } else { - std::fprintf(stdout, - "psxrecomp: tcc tier active but no bundled toolchain at %s " - "(overlay gaps -> interpreter)\n", tk.string().c_str()); - } - } - } else { - if (deferred_has_overlay_ac) - ac_cmd = &deferred_overlay_ac; - } - if (const char *e = std::getenv("PSX_OVERLAY_AUTOCOMPILE_CMD")) { - if (e[0]) { - env_ac_cmd = e; - ac_cmd = &env_ac_cmd; - std::fprintf(stdout, - "psxrecomp: overlay autocompile command overridden by environment\n"); - } - } - if (const char *e = std::getenv("PSX_OVERLAY_AUTOCOMPILE_OFF")) { - if (e[0] && e[0] != '0') { - ac_cmd = nullptr; - std::fprintf(stdout, - "psxrecomp: overlay autocompile disabled by environment\n"); - } - } - if (ac_cmd) { - autocompile_set_cache_paths(cache_dir.c_str(), - captures_path.string().c_str()); - std::string ac_cwd = deferred_overlay_project_root.string(); - if (const char *e = std::getenv("PSX_OVERLAY_AUTOCOMPILE_CWD")) { - if (e[0]) ac_cwd = e; - } - autocompile_configure(ac_cmd->c_str(), ac_cwd.c_str()); - overlay_autocapture_set_enabled(1); - std::fprintf(stdout, - "psxrecomp: overlay autocompile enabled (%s); cache=%s; captures=%s\n", - overlay_backend_name(eff), cache_dir.c_str(), - captures_path.string().c_str()); + { + std::string mod_error; + if (!PSXRecompV4::mod_runtime_initialize( + exe_dir_from_argv(argv[0]) / "mods", game_id, + game_entry_pc, text_guard_exe_path, &mod_error)) { + std::fprintf(stderr, "psxrecomp: mods unavailable: %s\n", + mod_error.c_str()); } - code_provider_init(cfg_backend, gcc_avail); - }; - - if (deferred_overlay_cache) { - overlay_init_thread = std::thread([&]() { - try { - run_deferred_overlay_init(); - } catch (...) { - overlay_init_exc = std::current_exception(); - } - }); } #if defined(RECOMP_LAUNCHER) /* Integrated recomp-ui launcher: shown in its own GL window before the emulator * boots. Seeded with the effective settings (game.toml ∪ settings.toml); * on LAUNCH the user's choices are persisted to settings.toml and applied. - * The launcher window/GL context is destroyed afterward, but SDL subsystems - * stay initialized so the game window can open without a second SDL_Init. + * The launcher window/context is fully torn down before the emulator's own + * window is created, so the emulator boot path below is untouched. * * Skip the GUI (boot straight in) when ANY of: PSX_NO_LAUNCHER=1 env, * --no-launcher, or the persisted [launcher] skip_launcher setting — unless @@ -7600,10 +5755,7 @@ std::string player_device[PSX_MAX_PLAYERS]; force_launcher || (!std::getenv("PSX_NO_LAUNCHER") && !force_no_launcher && !skip_launcher_setting); if (want_launcher) { - launcher_boot_timing_mark("host:before_sdl_init"); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) == 0) { - launcher_boot_timing_mark("host:after_sdl_init"); - recomp_launcher_set_preserve_sdl(1); int lr = 2; /* 0 = launch, 1 = quit, 2 = unavailable */ PSXRecompV4::UserSettings seed; seed.renderer = g_video_renderer; seed.has_renderer = true; @@ -7611,17 +5763,21 @@ std::string player_device[PSX_MAX_PLAYERS]; seed.antialiasing = g_video_aa; seed.has_antialiasing = true; seed.texture_filter = g_video_texfilter; seed.has_texture_filter = true; seed.screen_kind = g_video_screen; seed.has_screen_kind = true; - seed.auto_skip_fmv = (g_auto_skip_fmv != 0); seed.has_auto_skip_fmv = true; + seed.auto_skip_fmv = (g_auto_skip_fmv != 0); + seed.has_auto_skip_fmv = skip_fmv_offered; seed.turbo_loads = (g_turbo_loads_enabled != 0); seed.has_turbo_loads = true; seed.fast_boot = fast_boot; seed.has_fast_boot = true; - seed.bios_hle = bios_hle; seed.has_bios_hle = true; + seed.bios_hle = bios_hle_requested; seed.has_bios_hle = true; seed.fullscreen = g_fullscreen; seed.has_fullscreen = true; seed.frame_interpolation = (g_frame_interpolation != 0); - seed.has_frame_interpolation = true; + seed.has_frame_interpolation = frame_interpolation_offered; seed.frame_interpolation_fps = g_frame_interpolation_fps; - seed.has_frame_interpolation_fps = true; + seed.has_frame_interpolation_fps = frame_interpolation_offered; seed.aspect_num = g_video_aspect_num; - seed.aspect_den = g_video_aspect_den; seed.has_aspect_ratio = true; + seed.aspect_den = g_video_aspect_den; + seed.has_aspect_ratio = ws_offered; + seed.adaptive_view = g_ws_adaptive_view; + seed.has_adaptive_view = ws_offered; seed.spu_hq = g_audio_spu_hq; seed.has_spu_hq = true; seed.skip_launcher = skip_launcher_setting; seed.has_skip_launcher = true; if (has_netplay_player_name) { @@ -7632,10 +5788,7 @@ std::string player_device[PSX_MAX_PLAYERS]; seed.netplay_lobby_url = g_lnch_lobby_url; seed.has_netplay_lobby_url = true; } - if (bios_explicit && bios_path && bios_path[0]) { - seed.bios_path = bios_path; - seed.has_bios_path = true; - } + if (bios_path && bios_path[0]) { seed.bios_path = bios_path; seed.has_bios_path = true; } if (!resolved_disc.empty()) { seed.disc_path = resolved_disc; seed.has_disc_path = true; } seed.memcard_dir = memcard_dir; seed.has_memcard_dir = true; seed.memcard1_enabled = memcard1_enabled; seed.has_memcard1_enabled = true; @@ -7643,29 +5796,17 @@ std::string player_device[PSX_MAX_PLAYERS]; if (!memcard1_path.empty()) { seed.memcard1_path = memcard1_path; seed.has_memcard1_path = true; } if (!memcard2_path.empty()) { seed.memcard2_path = memcard2_path; seed.has_memcard2_path = true; } seed.language = resolved_language; seed.has_language = true; - { - const int n = std::min(PSX_MAX_PLAYERS, - PSXRecompV4::UserSettings::kMaxControllerPlayers); - for (int i = 0; i < n; ++i) { - seed.p_device[i] = player_device[i]; - seed.has_p_device[i] = true; - seed.p_mode[i] = player_mode[i]; - seed.has_p_mode[i] = true; - seed.p_deadzone[i] = player_deadzone[i]; - seed.has_p_deadzone[i] = true; - } - seed.deadzone = player_deadzone[0]; - seed.has_deadzone = true; - } + seed.p1_device = p1_device; seed.has_p1_device = true; + seed.p2_device = p2_device; seed.has_p2_device = true; + seed.p1_mode = p1_mode; seed.has_p1_mode = true; + seed.p2_mode = p2_mode; seed.has_p2_mode = true; + seed.deadzone = resolved_deadzone >= 0 ? resolved_deadzone : 12000; + seed.has_deadzone = true; seed.window_width = g_video_win_w; seed.has_window_width = true; - /* recomp-ui creates + owns its SDL2/GL window internally, so there + /* recomp-ui creates + owns its SDL/GL window internally, so there * is no launcher window/context to manage here. */ std::string assets_dir_str = exe_dir_from_argv(argv[0]).string(); - /* Same path the runtime's psx_keybinds_init(argv0) reads — keep the - * launcher Controls page and in-game keyboard map on one file. */ - static std::string s_rui_keybinds_path; - s_rui_keybinds_path = (exe_dir_from_argv(argv[0]) / "keybinds.ini").string(); std::string rui_initial_disc = resolved_disc.string(); std::string rui_title = (game_name.empty() ? std::string("PSX") : game_name) + " - Launcher"; @@ -7681,33 +5822,39 @@ std::string player_device[PSX_MAX_PLAYERS]; ls.enable_audio = 1; ls.audio_freq = 44100; ls.volume = 100; + ls.player_src[0] = PSXRecompV4::launcher_source_from_device(p1_device); + ls.player_src[1] = PSXRecompV4::launcher_source_from_device(p2_device); +#if defined(RECOMP_LAUNCHER_HAS_PLAYER_GAMEPAD_GUID) + if (ls.player_src[0] == 2) { + std::snprintf(ls.player_gamepad_guid[0], + sizeof(ls.player_gamepad_guid[0]), "%s", + p1_device.c_str()); + } + if (ls.player_src[1] == 2) { + std::snprintf(ls.player_gamepad_guid[1], + sizeof(ls.player_gamepad_guid[1]), "%s", + p2_device.c_str()); + } +#endif { - const int n = std::min(PSX_MAX_PLAYERS, RECOMP_LAUNCHER_MAX_PLAYERS); - for (int i = 0; i < n; ++i) { - const std::string& d = player_device[i]; - ls.player_src[i] = (d == "keyboard") ? 1 - : (d == "none" || d.empty()) ? 0 : 2; - ls.deadzone[i] = (player_deadzone[i] * 100 + 16383) / 32767; - ls.pad_mode[i] = (ls.player_src[i] == 1) - ? PSXRecompV4::PAD_MODE_DIGITAL - : player_mode[i]; - ls.player_gamepad_guid[i][0] = '\0'; - if (ls.player_src[i] == 2 && !d.empty() && d != "auto" && - d != "gamepad" && d != "controller") { - std::snprintf(ls.player_gamepad_guid[i], - sizeof(ls.player_gamepad_guid[i]), "%s", - d.c_str()); - } - } + int rui_deadzone_pct = seed.deadzone * 100 / 32767; + ls.deadzone[0] = rui_deadzone_pct; + ls.deadzone[1] = rui_deadzone_pct; } ls.skip_launcher = seed.skip_launcher ? 1 : 0; ls.msu1_enabled = 0; ls.msu1_dir[0] = '\0'; std::snprintf(ls.netplay_player_name, sizeof(ls.netplay_player_name), "%s", has_netplay_player_name ? netplay_player_name.c_str() : ""); - /* aspect_index: 0 = 4:3, 1 = 16:9, 2 = 21:9 (see RecompLauncherCSettings). */ - ls.aspect_index = (seed.aspect_num * 9 == seed.aspect_den * 21) ? 2 : - (seed.aspect_num == 16 && seed.aspect_den == 9) ? 1 : 0; + ls.pad_mode[0] = seed.p1_mode; + ls.pad_mode[1] = seed.p2_mode; + /* Adaptive is part of the aspect vocabulary, not a second toggle. + * It follows 21:9 when ultrawide is offered, otherwise 16:9. */ + const int adaptive_aspect_index = ws_ultrawide_offered ? 3 : 2; + ls.aspect_index = seed.adaptive_view ? adaptive_aspect_index : + (seed.aspect_num * 9 == seed.aspect_den * 21) ? 2 : + (seed.aspect_num == 16 && seed.aspect_den == 9) ? 1 : 0; + ls.adaptive_view = seed.adaptive_view ? 1 : 0; /* ---- deeper PSX-style settings (capability-gated via launcher_profile * below). Sourced 1:1 from PSXRecompV4::UserSettings (config_loader.h). */ @@ -7775,15 +5922,34 @@ std::string player_device[PSX_MAX_PLAYERS]; "OpenGL (Recommended)", "Vulkan" }; + static const char* const kAdaptiveAspectLabels[] = { + "4:3 (Native)", + "16:9 (Widescreen)", + "21:9 (Ultrawide)", + "Adaptive" + }; + static const char* const kAdaptiveAspectLabelsNoUltrawide[] = { + "4:3 (Native)", + "16:9 (Widescreen)", + "Adaptive" + }; /* System identity + full PS1 settings-surface capability set (theme, * platform label, rom_noun, pad-mode support, aspect mask base, and * all has_* deep-settings flags) — one profile call keeps them from * drifting apart across PSX titles. Per-game specifics below override * only what the profile can't know. */ launcher_profile_apply("psx", &gi); + /* Show the SYSTEM module's BIOS row whenever this build can + * actually use a player-supplied image — i.e. some linked backend + * is not the bundled one. Bundled builds used to hide the row + * entirely, which under the current model would strand the player: + * with OpenBIOS AND a retail image linked, choosing retail (and + * clearing back to bundled) is exactly what the row is for. + * Asks the registry rather than psx_bios_image, which is not + * populated until a backend is activated. */ + gi.has_bios = psx_bios_has_selectable(); gi.name = game_name.empty() ? nullptr : game_name.c_str(); gi.region = rui_region.empty() ? nullptr : rui_region.c_str(); - gi.keybinds_path = s_rui_keybinds_path.c_str(); gi.has_expected_crc = 0; /* the launcher's simple file-CRC doesn't fit PSX multi-track discs — skip verification */ gi.num_known_sha256 = 0; @@ -7795,11 +5961,25 @@ std::string player_device[PSX_MAX_PLAYERS]; * [controller]/[widescreen] via GameConfig. PSX always has pad modes. */ gi.pad_mode_selectable = ctrl_lock_mode ? 0 : 1; gi.allow_hybrid = ctrl_allow_hybrid ? 1 : 0; - gi.locked_pad_mode = ctrl_locked_mode[0]; /* game-declared default_mode */ + gi.locked_pad_mode = p1_mode; /* force the game's declared mode (default_mode) */ gi.lock_device = ctrl_lock_device ? 1 : 0; - gi.aspect_mask = 0x1 | (ws_offered ? 0x2 : 0) | (ws_ultrawide_offered ? 0x4 : 0); + /* No aspect capability means recomp-ui omits View mode entirely. + * Games still using the legacy Settings path advertise Native plus + * their offered wide modes; games migrating widescreen into Mods + * set [widescreen] offer=false and let a plugin own activation. */ + gi.aspect_mask = ws_offered + ? (0x1 | 0x2 | (ws_ultrawide_offered ? 0x4 : 0)) + : 0; + gi.has_frame_interp = frame_interpolation_offered ? 1 : 0; + if (ws_offered && ws_adaptive_view_supported) { + gi.aspect_labels = ws_ultrawide_offered + ? kAdaptiveAspectLabels : kAdaptiveAspectLabelsNoUltrawide; + gi.num_aspect_labels = ws_ultrawide_offered ? 4 : 3; + gi.aspect_experimental = 1; + } gi.renderer_labels = kPsxRendererLabels; gi.num_renderers = vulkan_offered ? 3 : 2; + gi.has_skip_fmv = skip_fmv_offered ? 1 : 0; /* Localization menu: shown only when the game declares languages. */ if (!rui_lang_labels.empty()) { gi.language_labels = rui_lang_labels.data(); @@ -7815,67 +5995,19 @@ std::string player_device[PSX_MAX_PLAYERS]; g_lnch_expected_serial = game_id; g_lnch_expected_crc = game_disc_crc; g_lnch_has_crc = game_has_disc_crc; - g_lnch_argv0 = argv[0]; gi.disc_verify = ae_disc_verify; gi.memcard_inspect = ae_memcard_inspect; - gi.bios_verify = ae_bios_verify; - /* MotK ships tools/prepare_disc.py (2448→2352). Offer it in the - * first-run wizard so players need not run the script by hand. */ - { - const auto root = find_upward(exe_dir_from_argv(argv[0]), - "tools/prepare_disc.py"); - if (!root.empty()) { - gi.prepare_disc = ae_prepare_disc; - gi.prepare_disc_label = "Convert 2448-byte dump…"; - gi.prepare_disc_note = - "If you have a 2448-byte/sector MotK ISO dump, convert it " - "to a MODE2/2352 .bin/.cue under motk/ (same as " - "tools/prepare_disc.py)."; - } - } - /* Quiet first-run detection: missing/unreadable BIOS or disc opens - * the setup wizard inside recomp-ui (cross-platform file pickers). */ - { - bool bios_ok = false; - RecompLauncherCBiosVerify bv{}; - if (ls.bios_path[0]) { - if (ae_bios_verify(ls.bios_path, &bv) && bv.ok) bios_ok = true; - else ls.bios_path[0] = '\0'; - } - if (!bios_ok) { - if (ae_bios_verify("", &bv) && bv.ok) bios_ok = true; - } - bool disc_ok = false; - if (!rui_initial_disc.empty()) { - std::error_code ec; - if (std::filesystem::exists(rui_initial_disc, ec)) { - const DiscValidation dv = - validate_disc_image(rui_initial_disc, game_id); - disc_ok = dv.opened && dv.has_header; - } - if (!disc_ok) rui_initial_disc.clear(); - } - gi.needs_setup = (!bios_ok || !disc_ok) ? 1 : 0; - } - launcher_boot_timing_mark("host:setup_checks_done"); + gi.mods = PSXRecompV4::mod_runtime_launcher_provider(); #if defined(PSX_HAS_RECOMP_NET) && defined(PSX_HAS_LOBBY_CLIENT) g_lnch_netplay_game_name = game_name.empty() ? "PSX" : game_name; - g_lnch_game_players = game_players; - g_offline_pad_count = game_players > 0 ? game_players : 1; - if (g_offline_pad_count > PSX_MAX_PLAYERS) - g_offline_pad_count = PSX_MAX_PLAYERS; - psx_lobby_set_max_slots(game_players); - gi.netplay_supported = - (game_players >= 2 && game_players <= PSX_MAX_PLAYERS) ? 1 : 0; + gi.netplay_supported = (game_players == 2) ? 1 : 0; gi.netplay = &g_lnch_netplay_callbacks; #endif char rui_out_disc[1024] = {0}; - launcher_boot_timing_mark("host:before_run_window"); int rui_rc = recomp_launcher_run_window( rui_title.c_str(), &ls, &gi, assets_dir_str.c_str(), rui_initial_disc.c_str(), rui_out_disc, sizeof(rui_out_disc)); - launcher_boot_timing_mark("host:after_run_window"); lr = rui_rc; @@ -7888,53 +6020,54 @@ std::string player_device[PSX_MAX_PLAYERS]; } seed.fullscreen = ls.fullscreen; seed.has_fullscreen = true; seed.skip_launcher = ls.skip_launcher != 0; seed.has_skip_launcher = true; - /* aspect_index round-trips 0/1/2 -> 4:3 / 16:9 / 21:9, superseding the - * legacy ls.widescreen bool (still set above for older callers). */ - switch (ls.aspect_index) { - case 2: seed.aspect_num = 21; seed.aspect_den = 9; break; - case 1: seed.aspect_num = 16; seed.aspect_den = 9; break; - default: seed.aspect_num = 4; seed.aspect_den = 3; break; + /* Adaptive preserves the previous fixed aspect as the initial + * window shape. Selecting a fixed entry turns adaptation off. */ + if (ws_offered) { + if (ws_adaptive_view_supported && + ls.aspect_index == adaptive_aspect_index) { + seed.adaptive_view = true; + } else { + seed.adaptive_view = false; + switch (ls.aspect_index) { + case 2: seed.aspect_num = 21; seed.aspect_den = 9; break; + case 1: seed.aspect_num = 16; seed.aspect_den = 9; break; + default: seed.aspect_num = 4; seed.aspect_den = 3; break; + } + } + seed.has_aspect_ratio = true; + seed.has_adaptive_view = true; + } else { + /* A mod-owned widescreen path starts from an unpersisted + * 4:3 baseline. Its activation plugin selects the actual + * fixed/adaptive aspect after the launcher commits Mods. */ + seed.adaptive_view = false; + seed.aspect_num = 4; + seed.aspect_den = 3; + seed.has_aspect_ratio = false; + seed.has_adaptive_view = false; } - seed.has_aspect_ratio = true; /* has_texture_filter is on (PSX profile) so the launcher edits * ls.texture_filter directly (0=nearest,1=bilinear); ls.linear_filter * is the legacy fallback field for consoles without the cap and is * left unused here. */ seed.texture_filter = ls.texture_filter ? 1 : 0; seed.has_texture_filter = true; - { - const int n = std::min(PSX_MAX_PLAYERS, RECOMP_LAUNCHER_MAX_PLAYERS); - const int un = std::min(n, PSXRecompV4::UserSettings::kMaxControllerPlayers); - for (int i = 0; i < n; ++i) { - if (ls.player_src[i] == 1) { - player_device[i] = "keyboard"; - /* Keyboard is always a digital pad at runtime. */ - player_mode[i] = PSXRecompV4::PAD_MODE_DIGITAL; - } else if (ls.player_src[i] == 0) { - player_device[i] = "none"; - player_mode[i] = ls.pad_mode[i]; - } else if (ls.player_gamepad_guid[i][0]) { - player_device[i] = ls.player_gamepad_guid[i]; - player_mode[i] = ls.pad_mode[i]; - } else if (player_device[i] == "none" || - player_device[i] == "keyboard") { - player_device[i] = "gamepad"; - player_mode[i] = ls.pad_mode[i]; - } else { - player_mode[i] = ls.pad_mode[i]; - } - player_deadzone[i] = ls.deadzone[i] * 32767 / 100; - if (i < un) { - seed.p_device[i] = player_device[i]; - seed.has_p_device[i] = true; - seed.p_mode[i] = player_mode[i]; - seed.has_p_mode[i] = true; - seed.p_deadzone[i] = player_deadzone[i]; - seed.has_p_deadzone[i] = true; - } - } - seed.deadzone = player_deadzone[0]; - seed.has_deadzone = true; - } + std::string p1_launcher_device = p1_device; + std::string p2_launcher_device = p2_device; +#if defined(RECOMP_LAUNCHER_HAS_PLAYER_GAMEPAD_GUID) + if (ls.player_gamepad_guid[0][0]) + p1_launcher_device = ls.player_gamepad_guid[0]; + if (ls.player_gamepad_guid[1][0]) + p2_launcher_device = ls.player_gamepad_guid[1]; +#endif + p1_device = PSXRecompV4::launcher_device_from_source( + ls.player_src[0], p1_launcher_device); + p2_device = PSXRecompV4::launcher_device_from_source( + ls.player_src[1], p2_launcher_device); + seed.p1_device = p1_device; seed.has_p1_device = true; + seed.p2_device = p2_device; seed.has_p2_device = true; + seed.deadzone = ls.deadzone[0] * 32767 / 100; seed.has_deadzone = true; + seed.p1_mode = ls.pad_mode[0]; seed.has_p1_mode = true; + seed.p2_mode = ls.pad_mode[1]; seed.has_p2_mode = true; /* ---- deeper PSX-style settings write-back (mirrors the seed * fields above), all gated on by the "psx" launcher_profile caps. */ @@ -7943,17 +6076,21 @@ std::string player_device[PSX_MAX_PLAYERS]; seed.supersampling = ls.supersampling; seed.has_supersampling = true; seed.antialiasing = ls.antialiasing != 0; seed.has_antialiasing = true; seed.screen_kind = ls.screen_kind; seed.has_screen_kind = true; - seed.frame_interpolation = ls.frame_interp != 0; seed.has_frame_interpolation = true; - seed.frame_interpolation_fps = ls.frame_interp_fps; seed.has_frame_interpolation_fps = true; + seed.frame_interpolation = ls.frame_interp != 0; + seed.has_frame_interpolation = frame_interpolation_offered; + seed.frame_interpolation_fps = ls.frame_interp_fps; + seed.has_frame_interpolation_fps = frame_interpolation_offered; seed.spu_hq = ls.spu_hq != 0; seed.has_spu_hq = true; - seed.auto_skip_fmv = ls.auto_skip_fmv != 0; seed.has_auto_skip_fmv = true; + seed.auto_skip_fmv = ls.auto_skip_fmv != 0; + seed.has_auto_skip_fmv = skip_fmv_offered; seed.turbo_loads = ls.turbo_loads != 0; seed.has_turbo_loads = true; - if (ls.bios_path[0]) { + /* Bundled-BIOS builds ignore any launcher-supplied path (the + * picker is hidden, but a stale settings file could still + * carry one) — resolve_bios_for_runtime would reject it and + * the identity gate makes it meaningless. */ + if (ls.bios_path[0] && !psx_bios_image.image_bundled) { seed.bios_path = ls.bios_path; seed.has_bios_path = true; - } else { - seed.bios_path.clear(); - seed.has_bios_path = false; } /* Memory-card slots: enable flags + any Browse/New paths. */ seed.memcard1_enabled = ls.memcard_enabled[0] != 0; seed.has_memcard1_enabled = true; @@ -7978,8 +6115,10 @@ std::string player_device[PSX_MAX_PLAYERS]; seed.has_aspect_ratio = true; seed.turbo_loads = caps->turbo_loads != 0; seed.has_turbo_loads = true; - seed.auto_skip_fmv = caps->auto_skip_fmv != 0; - seed.has_auto_skip_fmv = true; + if (skip_fmv_offered) { + seed.auto_skip_fmv = caps->auto_skip_fmv != 0; + seed.has_auto_skip_fmv = true; + } if (caps->language[0]) { seed.language = caps->language; seed.has_language = true; @@ -7991,9 +6130,6 @@ std::string player_device[PSX_MAX_PLAYERS]; if (lr == 1) { std::fprintf(stdout, "psxrecomp: launcher closed; exiting.\n"); - if (overlay_init_thread.joinable()) - overlay_init_thread.join(); - SDL_Quit(); return 0; } if (lr == 0) { @@ -8003,23 +6139,15 @@ std::string player_device[PSX_MAX_PLAYERS]; net_cfg.input_player = ls.netplay_launch.input_player; net_cfg.session_id = ls.netplay_launch.session_id; net_cfg.input_delay = ls.netplay_launch.input_delay; - net_cfg.force_input_relay = ls.netplay_launch.force_input_relay ? 1 : 0; - net_cfg.force_turn = ls.netplay_launch.force_turn ? 1 : 0; - net_cfg.player_count = ls.netplay_launch.player_count; - net_cfg.slot_count = ae_np_session_slot_count( - ls.netplay_launch.player_count, ls.netplay_launch.max_slots, - ls.netplay_launch.local_slot, game_players); - if (net_cfg.player_count <= 0) - net_cfg.player_count = net_cfg.slot_count; std::snprintf(net_cfg.bind_hostport, sizeof(net_cfg.bind_hostport), "%s", ls.netplay_launch.bind_hostport); std::snprintf(net_cfg.peer_hostport, sizeof(net_cfg.peer_hostport), "%s", ls.netplay_launch.peer_hostport); g_netplay_from_lobby = 1; std::fprintf(stdout, - "psxrecomp: launcher netplay slot=%d slots=%d bind=%s peer=%s session=%u\n", - net_cfg.local_slot, net_cfg.slot_count, net_cfg.bind_hostport, - net_cfg.peer_hostport, (unsigned)net_cfg.session_id); + "psxrecomp: launcher netplay slot=%d bind=%s peer=%s session=%u\n", + net_cfg.local_slot, net_cfg.bind_hostport, net_cfg.peer_hostport, + (unsigned)net_cfg.session_id); std::fflush(stdout); } else { g_netplay_from_lobby = 0; @@ -8029,48 +6157,37 @@ std::string player_device[PSX_MAX_PLAYERS]; g_video_aa = seed.antialiasing; g_video_texfilter = seed.texture_filter; g_video_screen = seed.screen_kind; - g_auto_skip_fmv = seed.auto_skip_fmv ? 1 : 0; + g_auto_skip_fmv = skip_fmv_offered && seed.auto_skip_fmv ? 1 : 0; g_turbo_loads_enabled = seed.turbo_loads ? 1 : 0; fast_boot = seed.fast_boot; - bios_hle = seed.bios_hle; + bios_hle_requested = seed.bios_hle; g_fullscreen = seed.fullscreen; - g_frame_interpolation = seed.frame_interpolation ? 1 : 0; - g_frame_interpolation_fps = seed.frame_interpolation_fps; - g_video_aspect_num = seed.aspect_num; - g_video_aspect_den = seed.aspect_den; + g_frame_interpolation = + frame_interpolation_offered && seed.frame_interpolation ? 1 : 0; + g_frame_interpolation_fps = + frame_interpolation_offered + ? seed.frame_interpolation_fps : 0; + g_video_aspect_num = ws_offered ? seed.aspect_num : 4; + g_video_aspect_den = ws_offered ? seed.aspect_den : 3; + g_ws_adaptive_view = ws_offered && seed.adaptive_view; g_audio_spu_hq = seed.spu_hq; skip_launcher_setting = seed.skip_launcher; if (seed.has_bios_path) { settings_bios_storage = seed.bios_path.string(); bios_path = settings_bios_storage.c_str(); - bios_explicit = true; - write_cached_path(argv[0], "bios.cfg", seed.bios_path); - } else if (!bios_from_cli) { - /* Cleared to bundled OpenBIOS — drop any cached retail pick. */ - bios_explicit = false; - std::error_code ec; - std::filesystem::remove(sidecar_cfg_path(argv[0], "bios.cfg"), ec); } if (seed.has_disc_path) { seed.disc_path = normalize_disc_path_for_launch(seed.disc_path); resolved_disc = seed.disc_path; - write_cached_path(argv[0], "disc.cfg", resolved_disc); } memcard1_enabled = seed.memcard1_enabled; memcard2_enabled = seed.memcard2_enabled; if (seed.has_memcard1_path) memcard1_path = seed.memcard1_path; if (seed.has_memcard2_path) memcard2_path = seed.memcard2_path; if (seed.has_language) resolved_language = seed.language; - { - const int n = std::min(PSX_MAX_PLAYERS, - PSXRecompV4::UserSettings::kMaxControllerPlayers); - for (int i = 0; i < n; ++i) { - if (seed.has_p_device[i]) player_device[i] = seed.p_device[i]; - if (seed.has_p_mode[i]) player_mode[i] = seed.p_mode[i]; - if (seed.has_p_deadzone[i]) player_deadzone[i] = seed.p_deadzone[i]; - } - if (seed.has_deadzone) resolved_deadzone = seed.deadzone; - } + p1_device = seed.p1_device; p2_device = seed.p2_device; + p1_mode = seed.p1_mode; p2_mode = seed.p2_mode; + if (seed.has_deadzone) resolved_deadzone = seed.deadzone; g_video_win_w = seed.window_width; /* Persist the user's choices next to the exe. */ PSXRecompV4::save_user_settings( @@ -8080,19 +6197,37 @@ std::string player_device[PSX_MAX_PLAYERS]; } #endif - if (overlay_init_thread.joinable()) { - overlay_init_thread.join(); - if (overlay_init_exc) { - try { - std::rethrow_exception(overlay_init_exc); - } catch (const std::exception& ex) { - std::fprintf(stderr, "psxrecomp: overlay cache init failed: %s\n", - ex.what()); - return 1; - } + /* Resolve the actual stock image before mod resolution. In particular, + * --disc is a late CLI override and must participate in target hashing; + * the launcher already stores its imported stock path in resolved_disc. */ + if (game_config_path || disc_override_path || !resolved_disc.empty()) { + resolved_disc = + resolve_disc_for_runtime(resolved_disc, disc_override_path, game_id, argv[0]); + if (game_config_path && resolved_disc.empty()) { + std::fprintf(stderr, "psxrecomp: no disc image selected; exiting.\n"); + return 1; } } + { + std::string mod_error; + if (!PSXRecompV4::mod_runtime_commit(resolved_disc, &mod_error)) { + std::fprintf(stderr, "psxrecomp: cannot launch with selected mods: %s\n", + mod_error.c_str()); + return 1; + } + } + /* Activation callbacks are re-run after every launcher session. Clear + * game-owned controller overrides first so disabling a package cannot + * leave its prior mode latched across a soft return to the launcher. */ + g_mod_controller_mode_override[0] = -1; + g_mod_controller_mode_override[1] = -1; + g_frame_interpolation_blend = g_frame_interpolation_blend_default; + mod_runtime_activate_plugins(); + if (g_mod_controller_mode_override[0] >= 0) + p1_mode = g_mod_controller_mode_override[0]; + if (g_mod_controller_mode_override[1] >= 0) + p2_mode = g_mod_controller_mode_override[1]; /* Re-apply the resolved language to the translation layer. text_xlate_init * (at config load) only saw the game.toml default; this folds in the @@ -8120,19 +6255,19 @@ std::string player_device[PSX_MAX_PLAYERS]; std::fprintf(stderr, "psxrecomp: no BIOS selected; exiting.\n"); return 1; } - if (game_config_path || disc_override_path || !resolved_disc.empty()) { - resolved_disc = resolve_disc_for_runtime(resolved_disc, disc_override_path, game_id, argv[0]); - if (game_config_path && resolved_disc.empty()) { - std::fprintf(stderr, "psxrecomp: no disc image selected; exiting.\n"); - return 1; - } - } - /* memcard_dir was resolved to its default before the launcher (above). */ std::string bios_path_str = resolved_bios.string(); std::string memcard_dir_str = memcard_dir.string(); - std::string disc_path_str = resolved_disc.string(); + const std::filesystem::path& mod_disc = + PSXRecompV4::mod_runtime_effective_disc_path(); + std::string disc_path_str = + (mod_disc.empty() ? resolved_disc : mod_disc).string(); + if (!mod_disc.empty()) { + std::fprintf(stdout, + "psxrecomp: stock disc remains %s; mounting private mod cache %s\n", + resolved_disc.string().c_str(), mod_disc.string().c_str()); + } session_reboot: /* Rematch after lobby soft-return re-enters here with updated net_cfg. */ @@ -8187,17 +6322,18 @@ std::string player_device[PSX_MAX_PLAYERS]; * this aspect; native-wide fills it with a genuinely wider frame (no * stretch), squash mode stretches the 4:3 frame into it. */ gl_renderer_set_display_aspect(g_video_aspect_num, g_video_aspect_den); - if (g_video_aspect_num * 3 != g_video_aspect_den * 4) { + if (g_video_aspect_num * 3 != g_video_aspect_den * 4 || g_ws_adaptive_view) { /* Hold widescreen off through the BIOS boot (authentic 4:3 logos); * the per-frame present path engages it at game entry. */ g_ws_engaged = false; std::fprintf(stdout, - "psxrecomp: widescreen %d:%d (%s%s%s; engages at game entry)\n", + "psxrecomp: widescreen %d:%d (%s%s%s%s; engages at game entry)\n", g_video_aspect_num, g_video_aspect_den, g_ws_native_wide ? "native-wide, present 1:1" : "GTE X-squash + stretched present", g_ws_anchor_addr ? " + sprite tags" : "", - g_ws_hud_sprt ? " + HUD squash" : ""); + g_ws_hud_sprt ? " + HUD squash" : "", + g_ws_adaptive_view ? " + adaptive view" : ""); } /* Present-time screen-colour model (verified-enhancement LUT). Default raw * is byte-identical; PSX_SCREEN env overrides this at scanout. */ @@ -8221,22 +6357,14 @@ std::string player_device[PSX_MAX_PLAYERS]; * SDL controller handles are opened later (after SDL_Init); here we only * set the PSX-visible connection + pad type so the BIOS sees the right * ports during early boot. */ - for (int s = 0; s < PSX_MAX_PLAYERS; ++s) { - set_player_device(g_players[s], player_device[s], player_mode[s]); - g_players[s].deadzone = player_deadzone[s]; - } - /* Multitap stays OFF through BIOS boot: SCPH-1070 on port 1 breaks shell / - * LoadExe pad bring-up for titles that expect a lone digital pad. Offline - * 3+ player builds arm it after game entry (see vblank path); netplay arms - * it from psx_netplay when slot_count >= 3. */ - for (int s = 0; s < PSX_MAX_PLAYERS; s++) { + set_player_device(g_players[0], p1_device, p1_mode); + set_player_device(g_players[1], p2_device, p2_mode); + for (int s = 0; s < 2; s++) { /* Dev-any-input keeps P1 connected even with no assigned controller so the * keyboard / any plugged-in controller can drive port 1 standalone. */ const bool dev_p1 = (dev_any_input_enabled() && s == 0); - const int mode = effective_player_mode_for_sio(g_players[s], s); sio_set_pad_connected(s, (g_players[s].kind != 0 || dev_p1) ? 1 : 0); - sio_set_pad_analog(s, pad_mode_boot_analog(mode), 0x80, 0x80, 0x80, 0x80); - sio_set_pad_config_capable(s, mode != PSXRecompV4::PAD_MODE_DIGITAL); + sio_set_pad_analog(s, pad_mode_boot_analog(g_players[s].mode), 0x80, 0x80, 0x80, 0x80); } /* SPU float-shadow gate must be set before spu_init() (which runs * spu_shadow_reset()). Default OFF; PSX_AUDIO_SHADOW env overrides. */ @@ -8245,6 +6373,29 @@ std::string player_device[PSX_MAX_PLAYERS]; std::fprintf(stdout, "psxrecomp: SPU float-shadow enabled (verified-enhancement)\n"); spu_init(); cdrom_init(disc_path_str.empty() ? NULL : disc_path_str.c_str()); + + /* A disc was requested but nothing mounted. cdrom_init() is non-fatal here + * (BIOS-only targets run with an empty drive on purpose), so without this + * check the game would boot into an empty drive and render NOTHING -- 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 shows a green "Disc verified" badge. Report the actual + * failure instead of leaving the player staring at black. */ + if (!disc_path_str.empty() && !cdrom_has_disc()) { + const PSXRecompV4::DiscPathResolution r = + PSXRecompV4::resolve_disc_path(disc_path_str); + std::string detail = + "The disc image was found and verified, but the CD-ROM drive could " + "not mount it, so the game would boot with an empty drive.\n\n" + "Selected:\n" + disc_path_str; + if (r.mount != r.picked) detail += "\nMounted as:\n" + r.mount.string(); + if (!r.note.empty()) detail += "\n\n" + r.note; + detail += "\n\nIf this is a .cue, check that every FILE line it names " + "exists next to it; selecting the .bin directly also works."; + launcher_warning("Disc Could Not Be Mounted", detail); + return 1; + } for (const auto& route : warm_cd_routes) { cdrom_register_warm_route(route.arm_lba, route.lbas.data(), (int)route.lbas.size(), @@ -8274,6 +6425,7 @@ std::string player_device[PSX_MAX_PLAYERS]; if (game_config_path) arm_text_image_guard(text_guard_exe_path, text_guard_load_addr, disc_path_str); + mod_runtime_enable_disc_patches(); { int divisor = 1; /* default: authentic 1x timing */ if (disc_speed == "instant") divisor = 0; @@ -8355,15 +6507,6 @@ std::string player_device[PSX_MAX_PLAYERS]; * load_input_config has read input.ini. */ if (resolved_deadzone >= 0) controller_deadzone = std::max(0, std::min(32767, resolved_deadzone)); - else - controller_deadzone = kDefaultDeadzoneRaw; - for (int s = 0; s < PSX_MAX_PLAYERS; ++s) { - if (player_deadzone[s] >= 0) - g_players[s].deadzone = std::max(0, std::min(32767, player_deadzone[s])); - else - g_players[s].deadzone = controller_deadzone; - } - controller_deadzone = g_players[0].deadzone; refresh_player_devices(); /* open SDL handles to match the player config */ #ifndef PSX_SDL_NO_AUDIO audio_trace_init(); @@ -8385,6 +6528,7 @@ std::string player_device[PSX_MAX_PLAYERS]; cfg.channels = 2; cfg.source_rate = 44100.0; /* SPU render rate */ cfg.host_rate = (double)have.freq; /* actual device rate */ + cfg.target_ms = g_audio_buffer_ms; /* per-game cushion */ if (rab_init(&s_drc, &cfg) == 0) s_drc_ready = true; } g_audio_host_rate = have.freq; @@ -8436,13 +6580,24 @@ std::string player_device[PSX_MAX_PLAYERS]; if (disp_idx >= 0 && SDL_GetCurrentDisplayMode(disp_idx, &dm) == 0 && dm.refresh_rate > 0) { double host_hz = (double)dm.refresh_rate; g_host_refresh_hz = host_hz; - if (host_hz >= 58.8 && host_hz <= 61.2) { + if (g_mod_native_vblank_rate) { + if (g_mod_native_vblank_fps) { + std::printf("psxrecomp: native VBlank mod owns pacing at " + "%u FPS; ignoring %.2f Hz panel sync\n", + (unsigned)g_mod_native_vblank_fps, + host_hz); + } else { + std::printf("psxrecomp: native VBlank mod owns uncapped " + "pacing; ignoring %.2f Hz panel sync\n", + host_hz); + } + } else if (host_hz >= 58.8 && host_hz <= 61.2) { g_frame_period_ms = 1000.0 / host_hz; - std::printf("psxrecomp: sync-to-host-refresh: pacing to %d Hz panel " - "(%.4f ms/frame)\n", dm.refresh_rate, g_frame_period_ms); + std::printf("psxrecomp: sync-to-host-refresh: pacing to %.2f Hz panel " + "(%.4f ms/frame)\n", host_hz, g_frame_period_ms); } else { - std::printf("psxrecomp: host panel %d Hz not ~60 Hz; keeping PSX " - "59.94 Hz pacing\n", dm.refresh_rate); + std::printf("psxrecomp: host panel %.2f Hz not ~60 Hz; keeping PSX " + "59.94 Hz pacing\n", host_hz); } } } @@ -8463,7 +6618,7 @@ std::string player_device[PSX_MAX_PLAYERS]; g_video_scale = gr_scale(); gl_renderer_set_interpolation(g_frame_interpolation, g_host_refresh_hz, (double)g_frame_interpolation_fps, - /*blend_mode*/ 0); + g_frame_interpolation_blend); } /* Vulkan backend: create the instance/device/swapchain on the * SDL_WINDOW_VULKAN window. On failure, fall back to software (vkb_init @@ -8576,30 +6731,15 @@ std::string player_device[PSX_MAX_PLAYERS]; return 1; } /* Resolve which host PlayerInput feeds this peer's net sample. - * Auto (-1): always prefer dashboard P1 ("PLAYER N / NETPLAY") — that - * pad is published as lobby local_slot. Seat-card P2/P3… are only used - * when P1 is empty (legacy same-PC layout). Else sole assigned / P1. */ - if (net_cfg.input_player < 0 || net_cfg.input_player >= PSX_MAX_PLAYERS) { - const int seat = net_cfg.local_slot; - int sole = -1, n_assigned = 0; - for (int i = 0; i < PSX_MAX_PLAYERS; ++i) { - if (g_players[i].kind == 0) continue; - ++n_assigned; - sole = i; - } - if (g_players[0].kind != 0) - net_cfg.input_player = 0; - else if (seat >= 0 && seat < PSX_MAX_PLAYERS && g_players[seat].kind != 0) - net_cfg.input_player = seat; - else if (n_assigned == 1) - net_cfg.input_player = sole; + * Auto: prefer g_players[local_slot] when assigned (same-PC: host + * C40 on P1 + guest keyboard on P2); else player 0 (two-machine). */ + if (net_cfg.input_player != 0 && net_cfg.input_player != 1) { + const int prefer = (net_cfg.local_slot == 1) ? 1 : 0; + if (prefer == 1 && g_players[1].kind != 0) + net_cfg.input_player = 1; else net_cfg.input_player = 0; } - if (net_cfg.slot_count < 2) - net_cfg.slot_count = game_players >= 2 ? game_players : 2; - if (net_cfg.slot_count > PSX_MAX_PLAYERS) - net_cfg.slot_count = PSX_MAX_PLAYERS; const int nrc = psx_netplay_start(&net_cfg); if (nrc != 0) { std::fprintf(stderr, @@ -8608,14 +6748,10 @@ std::string player_device[PSX_MAX_PLAYERS]; nrc, net_cfg.local_slot, net_cfg.bind_hostport, net_cfg.peer_hostport); return 1; } - std::printf("psxrecomp: netplay transport=%s slot=%d input_player=%d delay=%d " - "force_turn=%d bind=%s peer=%s session=%u\n", - psx_netplay_transport_name(), + std::printf("psxrecomp: netplay LAN slot=%d input_player=%d delay=%d " + "bind=%s peer=%s session=%u\n", net_cfg.local_slot, net_cfg.input_player, net_cfg.input_delay, - net_cfg.force_turn ? 1 : 0, - net_cfg.bind_hostport, - (std::strcmp(psx_netplay_transport_name(), "ice") == 0) - ? "(ice)" : net_cfg.peer_hostport, + net_cfg.bind_hostport, net_cfg.peer_hostport, (unsigned)net_cfg.session_id); } @@ -8649,20 +6785,57 @@ std::string player_device[PSX_MAX_PLAYERS]; * game EXE load still run in the real recompiled BIOS, unpaced) — this * REPLACES the old fast_boot snapshot restore, and fast_boot=true now * aliases the boot-skip alone. Env overrides: PSX_BIOS_HLE / - * PSX_BIOS_HLE_KEEP_INTRO ('0' = off, anything else = on). */ + * PSX_BIOS_HLE_KEEP_INTRO ('0' = off, anything else = on). + * + * The two axes are decided in ONE pure place — psx_bios_hle_plan(), + * runtime/src/bios_hle_plan.c — because they have DIFFERENT per-image + * requirements and conflating them broke boot-skip on OpenBIOS: call-HLE + * needs deliver_event_ret, the boot-skip needs only shell_entry_phys, so + * refusing the former must not silently cancel the latter. See that + * header for the full rationale and the bug it fixes. */ { if (const char* e = std::getenv("PSX_BIOS_HLE")) - bios_hle = (e[0] && e[0] != '0'); + bios_hle_requested = (e[0] && e[0] != '0'); if (const char* e = std::getenv("PSX_BIOS_HLE_KEEP_INTRO")) bios_hle_keep_intro = (e[0] && e[0] != '0'); - const bool boot_skip = - (bios_hle && !bios_hle_keep_intro) || fast_boot; - psx_bios_hle_configure(bios_hle ? 1 : 0, - (boot_skip && game_entry_pc != 0) ? 1 : 0); + + PsxBiosHleRequest req; + req.bios_hle = bios_hle_requested ? 1 : 0; + req.keep_intro = bios_hle_keep_intro ? 1 : 0; + req.fast_boot = fast_boot ? 1 : 0; + req.have_deliver_event_ret = (psx_bios_image.deliver_event_ret != 0); + req.have_shell_entry = (psx_bios_image.shell_entry_phys != 0); + req.have_game_entry = (game_entry_pc != 0); + const PsxBiosHlePlan plan = psx_bios_hle_plan(req); + + /* Call-HLE is a per-image capability, not just a preference: its kernel + * service semantics were validated against the SCPH1001 kernel, and an + * image that exports no DeliverEvent anchor (psx_bios_image + * deliver_event_ret == 0, e.g. OpenBIOS until validated) declares that + * axis STRUCTURALLY UNAVAILABLE. Config/env cannot turn it on for such + * a BIOS — servicing B0 events with mismatched semantics wedges the + * guest in event waits (OpenBIOS bring-up: the shell menu hung in a + * CD-event wait under HLE, booted the game under LLE). */ + if (plan.call_hle_denied) { + std::fprintf(stdout, + "psxrecomp: bios_hle kernel-call tier unavailable on %s (no " + "DeliverEvent anchor); kernel calls stay LLE\n", + psx_bios_image.image_id); + } + /* Boot-skip refusal is only possible on an image with no shell entry at + * all. Say so rather than quietly playing the intro. */ + if (plan.boot_skip_denied) { + std::fprintf(stdout, + "psxrecomp: BIOS boot-skip unavailable on %s (no shell entry " + "anchor); playing the real intro\n", + psx_bios_image.image_id); + } + psx_bios_hle_configure(plan.call_hle, plan.boot_skip); std::fprintf(stdout, "psxrecomp: bios_backend=%s bios_boot=%s\n", psx_bios_hle_backend_name(), psx_bios_hle_boot_skip_enabled() - ? "HLE (shell skipped)" : "LLE (real intro)"); + ? "skip to game (shell skipped)" + : "real intro"); } /* R3000A reset state. */ @@ -8848,8 +7021,6 @@ std::string player_device[PSX_MAX_PLAYERS]; /* Delay-sync: do not free-run boot while HELLO/START is in flight. * Park until tick 0 pads are published, then enter the guest. */ if (psx_netplay_active()) { - SDL_PumpEvents(); - SDL_FlushEvent(SDL_QUIT); std::printf("psxrecomp: netplay waiting for peer START + tick-0 admit…\n"); std::fflush(stdout); netplay_barrier_admit(-1); @@ -8936,125 +7107,9 @@ std::string player_device[PSX_MAX_PLAYERS]; return 0; soft_return_lobby: - /* Netplay soft-exit: tear down the match, keep the lobby seat, and reopen - * the launcher on the LOBBY room so every peer can rematch. */ + /* Netplay soft-exit: tear down the match window. Lobby resume will be + * restored through recomp-ui. */ teardown_game_session_keep_lobby(); -#if defined(RECOMP_LAUNCHER) && defined(PSX_HAS_LOBBY_CLIENT) - ae_np_prepare_lobby_rematch(); - { - std::string assets_dir_str = exe_dir_from_argv(argv[0]).string(); - std::string rui_title = (game_name.empty() ? std::string("PSX") : game_name) - + " - Launcher"; - std::string rui_initial_disc = disc_path_str; - - RecompLauncherCSettings ls{}; - ls.output_method = 2; - ls.window_scale = std::max(1, std::min(4, g_video_win_w / 320)); - ls.fullscreen = g_fullscreen ? 1 : 0; - ls.enable_audio = 1; - ls.audio_freq = 44100; - ls.volume = 100; - ls.window_width = g_video_win_w; - ls.renderer = g_video_renderer; - ls.supersampling = g_video_scale; - ls.antialiasing = g_video_aa ? 1 : 0; - ls.texture_filter = g_video_texfilter; - ls.screen_kind = g_video_screen; - ls.frame_interp = g_frame_interpolation ? 1 : 0; - ls.frame_interp_fps = g_frame_interpolation_fps; - ls.spu_hq = g_audio_spu_hq ? 1 : 0; - ls.auto_skip_fmv = g_auto_skip_fmv ? 1 : 0; - ls.turbo_loads = g_turbo_loads_enabled ? 1 : 0; - ls.aspect_index = (g_video_aspect_num * 9 == g_video_aspect_den * 21) ? 2 - : (g_video_aspect_num == 16 && g_video_aspect_den == 9) ? 1 : 0; - std::snprintf(ls.netplay_player_name, sizeof(ls.netplay_player_name), "%s", - psx_lobby_display_name()); - std::snprintf(ls.bios_path, sizeof(ls.bios_path), "%s", bios_path_str.c_str()); - - RecompLauncherCGameInfo gi{}; - launcher_profile_apply("psx", &gi); - gi.name = game_name.empty() ? nullptr : game_name.c_str(); - gi.num_players = game_players; - g_lnch_game_players = game_players; - psx_lobby_set_max_slots(game_players); - gi.netplay_supported = - (game_players >= 2 && game_players <= PSX_MAX_PLAYERS) ? 1 : 0; - gi.netplay = &g_lnch_netplay_callbacks; - gi.resume_netplay_room = 1; - gi.resume_netplay_endpoint = ae_np_lan_endpoint_cstr(); - gi.disc_verify = ae_disc_verify; - gi.memcard_inspect = ae_memcard_inspect; - - char rui_out_disc[1024] = {0}; - recomp_launcher_set_preserve_sdl(1); - const int rui_rc = recomp_launcher_run_window( - rui_title.c_str(), &ls, &gi, assets_dir_str.c_str(), - rui_initial_disc.c_str(), rui_out_disc, sizeof(rui_out_disc)); - - if (rui_rc == 1) { - /* User closed the launcher — leave the lobby and exit. */ - if (g_lnch_netplay_callbacks.leave) - (void)g_lnch_netplay_callbacks.leave(g_lnch_netplay_callbacks.ctx); - else - (void)psx_lobby_leave(); - psx_lobby_disconnect(); - SDL_Quit(); - return 0; - } - - if (rui_rc == 0) { - if (rui_out_disc[0]) { - resolved_disc = normalize_disc_path_for_launch(rui_out_disc); - disc_path_str = resolved_disc.string(); - } - if (ls.netplay_launch.enabled) { - net_cfg = {}; - net_cfg.enabled = 1; - net_cfg.local_slot = ls.netplay_launch.local_slot; - net_cfg.input_player = ls.netplay_launch.input_player; - net_cfg.session_id = ls.netplay_launch.session_id; - net_cfg.input_delay = ls.netplay_launch.input_delay; - net_cfg.force_input_relay = ls.netplay_launch.force_input_relay ? 1 : 0; - net_cfg.force_turn = ls.netplay_launch.force_turn ? 1 : 0; - net_cfg.player_count = ls.netplay_launch.player_count; - net_cfg.slot_count = ae_np_session_slot_count( - ls.netplay_launch.player_count, ls.netplay_launch.max_slots, - ls.netplay_launch.local_slot, game_players); - if (net_cfg.player_count <= 0) - net_cfg.player_count = net_cfg.slot_count; - std::snprintf(net_cfg.bind_hostport, sizeof(net_cfg.bind_hostport), "%s", - ls.netplay_launch.bind_hostport); - std::snprintf(net_cfg.peer_hostport, sizeof(net_cfg.peer_hostport), "%s", - ls.netplay_launch.peer_hostport); - g_netplay_from_lobby = 1; - } else { - net_cfg = {}; - g_netplay_from_lobby = 0; - } - g_video_renderer = ls.renderer; - g_video_scale = ls.supersampling; - g_video_aa = ls.antialiasing; - g_video_texfilter = ls.texture_filter; - g_video_screen = ls.screen_kind; - g_auto_skip_fmv = ls.auto_skip_fmv ? 1 : 0; - g_turbo_loads_enabled = ls.turbo_loads ? 1 : 0; - g_fullscreen = ls.fullscreen != 0; - g_frame_interpolation = ls.frame_interp ? 1 : 0; - g_frame_interpolation_fps = ls.frame_interp_fps; - g_audio_spu_hq = ls.spu_hq != 0; - switch (ls.aspect_index) { - case 2: g_video_aspect_num = 21; g_video_aspect_den = 9; break; - case 1: g_video_aspect_num = 16; g_video_aspect_den = 9; break; - default: g_video_aspect_num = 4; g_video_aspect_den = 3; break; - } - g_video_win_w = ls.window_width > 0 ? ls.window_width : g_video_win_w; - std::printf("psxrecomp: rematch from lobby (netplay=%d)\n", - net_cfg.enabled ? 1 : 0); - std::fflush(stdout); - goto session_reboot; - } - } -#endif psx_lobby_disconnect(); SDL_Quit(); return 0; diff --git a/runtime/src/mdec.c b/runtime/src/mdec.c index 64526476..d9b7ae6e 100644 --- a/runtime/src/mdec.c +++ b/runtime/src/mdec.c @@ -1,5 +1,4 @@ #include "mdec.h" -#include "pst_wire.h" #include #include @@ -757,140 +756,3 @@ void mdec_debug_dma_out_end(uint32_t addr, uint32_t words) { (void)addr; trace_event(MDEC_EVT_DMA_OUT_END, words); } - -/* ---- boot_state snapshot (variable-length input/output FIFOs) ------------ */ -#define MDEC_SNAP_VER 1u -#define MDEC_SNAP_INPUT_MAX (4u * 1024u * 1024u) /* halfwords */ -#define MDEC_SNAP_OUTPUT_MAX (8u * 1024u * 1024u) /* bytes */ - -static uint32_t mdec_snap_fixed_bytes(void) { - /* ver + scalars + tables + counts + last_color_age */ - return 4u + /* ver */ - 4u * 14u + /* u32 scalars */ - 1u * 8u + /* u8 flags */ - 64u + 64u + /* y/uv quant */ - 64u * 2u + /* scale i16 */ - 4u + 4u + /* input_count, output_size */ - 8u; /* last_color_age */ -} - -uint32_t mdec_snapshot_bytes(void) { - uint64_t n = (uint64_t)mdec_snap_fixed_bytes() + - (uint64_t)mdec.input_count * 2u + - (uint64_t)mdec.output_size; - if (n > 0xffffffffu) return 0; - return (uint32_t)n; -} - -void mdec_snapshot_write(uint8_t *p) { - PstW w; - uint32_t n = mdec_snapshot_bytes(); - uint64_t age; - if (!p || n == 0) return; - pst_w_init(&w, p, n); - (void)pst_w_u32(&w, MDEC_SNAP_VER); - (void)pst_w_u32(&w, mdec.command); - (void)pst_w_u32(&w, mdec.expected_halfwords); - (void)pst_w_u32(&w, mdec.last_status); - (void)pst_w_u32(&w, mdec.decode_macroblocks); - (void)pst_w_u32(&w, mdec.decode_blocks); - (void)pst_w_u32(&w, mdec.decode_stop_reason); - (void)pst_w_u32(&w, mdec.decode_input_pos); - (void)pst_w_u32(&w, mdec.decode_input_end); - (void)pst_w_u32(&w, mdec.dma_in_words); - (void)pst_w_u32(&w, mdec.dma_out_words); - (void)pst_w_u32(&w, mdec.dma_read_underflows); - (void)pst_w_u32(&w, mdec.output_pos); - (void)pst_w_u32(&w, 0u); /* reserved */ - (void)pst_w_u32(&w, 0u); /* reserved */ - (void)pst_w_u8(&w, mdec.output_bit15); - (void)pst_w_u8(&w, mdec.output_signed); - (void)pst_w_u8(&w, mdec.output_depth); - (void)pst_w_u8(&w, mdec.current_block); - (void)pst_w_u8(&w, mdec.busy); - (void)pst_w_u8(&w, mdec.input_full); - (void)pst_w_u8(&w, mdec.enable_dma_in); - (void)pst_w_u8(&w, mdec.enable_dma_out); - (void)pst_w_bytes(&w, mdec.y_quant, 64u); - (void)pst_w_bytes(&w, mdec.uv_quant, 64u); - for (int i = 0; i < 64; i++) - (void)pst_w_i16(&w, mdec.scale[i]); - (void)pst_w_u32(&w, mdec.input_count); - (void)pst_w_u32(&w, mdec.output_size); - if (s_frame_count >= mdec_last_color_decode_frame) - age = s_frame_count - mdec_last_color_decode_frame; - else - age = 1000ull; - (void)pst_w_u64(&w, age); - for (uint32_t i = 0; i < mdec.input_count; i++) - (void)pst_w_u16(&w, mdec.input ? mdec.input[i] : 0u); - if (mdec.output_size && mdec.output) - (void)pst_w_bytes(&w, mdec.output, mdec.output_size); -} - -int mdec_snapshot_read(const uint8_t *p, uint32_t len) { - PstR r; - uint32_t ver = 0, input_count = 0, output_size = 0, reserved; - uint64_t age = 1000ull; - int16_t s16; - if (!p || len < mdec_snap_fixed_bytes()) return 0; - pst_r_init(&r, p, len); - if (!pst_r_u32(&r, &ver) || ver != MDEC_SNAP_VER) return 0; - if (!pst_r_u32(&r, &mdec.command) || - !pst_r_u32(&r, &mdec.expected_halfwords) || - !pst_r_u32(&r, &mdec.last_status) || - !pst_r_u32(&r, &mdec.decode_macroblocks) || - !pst_r_u32(&r, &mdec.decode_blocks) || - !pst_r_u32(&r, &mdec.decode_stop_reason) || - !pst_r_u32(&r, &mdec.decode_input_pos) || - !pst_r_u32(&r, &mdec.decode_input_end) || - !pst_r_u32(&r, &mdec.dma_in_words) || - !pst_r_u32(&r, &mdec.dma_out_words) || - !pst_r_u32(&r, &mdec.dma_read_underflows) || - !pst_r_u32(&r, &mdec.output_pos) || - !pst_r_u32(&r, &reserved) || - !pst_r_u32(&r, &reserved)) - return 0; - if (!pst_r_u8(&r, &mdec.output_bit15) || - !pst_r_u8(&r, &mdec.output_signed) || - !pst_r_u8(&r, &mdec.output_depth) || - !pst_r_u8(&r, &mdec.current_block) || - !pst_r_u8(&r, &mdec.busy) || - !pst_r_u8(&r, &mdec.input_full) || - !pst_r_u8(&r, &mdec.enable_dma_in) || - !pst_r_u8(&r, &mdec.enable_dma_out)) - return 0; - if (!pst_r_bytes(&r, mdec.y_quant, 64u) || - !pst_r_bytes(&r, mdec.uv_quant, 64u)) - return 0; - for (int i = 0; i < 64; i++) { - if (!pst_r_i16(&r, &s16)) return 0; - mdec.scale[i] = s16; - } - if (!pst_r_u32(&r, &input_count) || !pst_r_u32(&r, &output_size) || - !pst_r_u64(&r, &age)) - return 0; - if (input_count > MDEC_SNAP_INPUT_MAX || output_size > MDEC_SNAP_OUTPUT_MAX) - return 0; - if (mdec.output_pos > output_size) return 0; - if ((size_t)(r.end - r.p) < - (size_t)input_count * 2u + (size_t)output_size) - return 0; - if (!ensure_input_capacity(input_count ? input_count : 1u)) return 0; - if (!ensure_output_capacity(output_size ? output_size : 1u)) return 0; - mdec.input_count = input_count; - mdec.output_size = output_size; - for (uint32_t i = 0; i < input_count; i++) { - uint16_t hw; - if (!pst_r_u16(&r, &hw)) return 0; - mdec.input[i] = hw; - } - if (output_size && !pst_r_bytes(&r, mdec.output, output_size)) - return 0; - if (age > 100000ull) age = 100000ull; - if (age >= s_frame_count) - mdec_last_color_decode_frame = 0; - else - mdec_last_color_decode_frame = s_frame_count - age; - return 1; -} diff --git a/runtime/src/memcard.c b/runtime/src/memcard.c index 215f17fc..c0f95005 100644 --- a/runtime/src/memcard.c +++ b/runtime/src/memcard.c @@ -83,37 +83,12 @@ static void memcard_format(uint8_t *data) { memcpy(&data[63 * 128], &data[0], 128); } -/* mkdir -p: create each path component (guest sandbox is /netplay). */ static void memcard_ensure_dir(const char* dir) { - char tmp[512]; - size_t len; - size_t i; if (!dir || !dir[0]) return; - strncpy(tmp, dir, sizeof(tmp) - 1); - tmp[sizeof(tmp) - 1] = '\0'; - len = strlen(tmp); - while (len > 1 && (tmp[len - 1] == '/' || tmp[len - 1] == '\\')) { - tmp[--len] = '\0'; - } - for (i = 1; i < len; i++) { - if (tmp[i] == '/' || tmp[i] == '\\') { -#ifdef _WIN32 - if (i == 2 && tmp[1] == ':') - continue; -#endif - tmp[i] = '\0'; -#ifdef _WIN32 - (void)_mkdir(tmp); -#else - (void)mkdir(tmp, 0755); -#endif - tmp[i] = '/'; - } - } #ifdef _WIN32 - (void)_mkdir(tmp); + (void)_mkdir(dir); #else - (void)mkdir(tmp, 0755); + (void)mkdir(dir, 0755); #endif } diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 41849963..2a4443f9 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -578,9 +578,6 @@ void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count) { if (count > DIRTY_RAM_BITMAP_WORDS) count = DIRTY_RAM_BITMAP_WORDS; for (uint32_t i = 0; i < count; i++) dirty_ram_bitmap[i] = words[i]; - /* Bitmap replace bypasses clean→dirty transitions; bump so interpreter - * site caches keyed on g_dirty_ram_code_gen cannot survive a restore. */ - g_dirty_ram_code_gen++; } /* ---- Inc3: watched overlay pages + per-page generation counters --------- @@ -634,17 +631,6 @@ uint32_t overlay_watch_pagegen_sum(uint32_t phys, uint32_t len) { return sum; } -/* Savestate restores RAM via memcpy and never hits the store chokepoint that - * bumps overlay_page_gen. Without this, ENTRY_VALID overlays keep the gen-gated - * fast path and run native code against restored bytes they were not validated - * for — hang / freeze after the restored frame presents. */ -void overlay_watch_invalidate_after_ram_restore(void) { - for (uint32_t pg = 0; pg < DIRTY_RAM_PAGE_COUNT; pg++) - overlay_page_gen[pg]++; - extern void overlay_loader_note_code_write(void); - overlay_loader_note_code_write(); -} - static inline void overlay_watch_note_write(uint32_t phys, uint32_t size) { uint32_t pg = phys >> DIRTY_RAM_PAGE_SHIFT; if (pg >= DIRTY_RAM_PAGE_COUNT) return; diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 5df9a0ec..0adc09ad 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -763,17 +763,6 @@ int provider_commit(void*, const char* image_path) { return 1; } -int provider_commit_netplay(void*, const char* image_path) { - (void)image_path; - std::string error; - if (!mod_runtime_clear_for_netplay(&error)) { - set_error(error); - return 0; - } - state().error.clear(); - return 1; -} - const char* provider_error(void*) { return state().error.c_str(); } @@ -801,9 +790,6 @@ RecompLauncherCModProvider provider = { provider_feature_set_option, provider_diagnostic_count, provider_diagnostic_get, - nullptr, /* archive_extension — PSX defaults */ - nullptr, /* archive_description */ - provider_commit_netplay, }; #endif @@ -850,28 +836,6 @@ bool mod_runtime_initialize(const std::filesystem::path& root, return true; } -bool mod_runtime_clear_for_netplay(std::string* error) { - RuntimeMods& s = state(); - if (!s.initialized) { - if (error) error->clear(); - return true; - } - s.plan = {}; - s.validation = {}; - s.raw_disc_index.clear(); - s.user_disc_index.clear(); - s.raw_overlay_index.clear(); - s.user_overlay_index.clear(); - s.effective_disc_path.clear(); - s.main_applied = false; - s.disc_enabled = false; - s.disc_guard_failed = false; - s.error.clear(); - if (error) error->clear(); - std::fprintf(stdout, "psxrecomp: mods cleared for netplay (vanilla session)\n"); - return true; -} - bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* error) { RuntimeMods& s = state(); if (!s.initialized) return true; diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 32908fca..d5d1f381 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -2042,28 +2042,6 @@ void overlay_loader_get_irq_suppress(int *mode, uint32_t *rl, uint64_t *supp) { if (supp) *supp = s_irq_suppressed; } -/* Overlay CI-wrapper attribution (post-load freeze): early returns never enter - * psx_check_interrupts, so s_irq_path_entry stays flat while cycles still - * advance inside native overlay / call-unit regions. */ -static uint64_t s_ci_skip_unit; -static uint64_t s_ci_skip_supp; -static uint64_t s_ci_skip_none; -static uint64_t s_ci_skip_sr; -static uint64_t s_ci_skip_deliv; -static uint64_t s_ci_enter; - -void overlay_loader_get_ci_skip_diag(uint64_t *unit, uint64_t *supp, - uint64_t *none, uint64_t *sr, - uint64_t *deliv, uint64_t *enter) { - if (unit) *unit = s_ci_skip_unit; - if (supp) *supp = s_ci_skip_supp; - if (none) *none = s_ci_skip_none; - if (sr) *sr = s_ci_skip_sr; - if (deliv) *deliv = s_ci_skip_deliv; - if (enter) *enter = s_ci_enter; -} -int overlay_loader_call_unit_depth(void) { return g_call_unit_depth; } - static int overlay_irq_suppressed_now(void) { /* Differential replay (and its authoritative interpreter pass) is atomic. * Never let a previously armed rate-limit punch a real IRQ into a shadow. */ @@ -2094,19 +2072,15 @@ static int overlay_irq_suppressed_now(void) { static void overlay_ci_wrapper(CPUState *cpu) { /* Defer while inside a nested call unit — a callee must not interrupt * mid-call (static-call atomicity). See g_call_unit_depth. */ - if (g_call_unit_depth > 0) { s_ci_skip_unit++; return; } - if (overlay_irq_suppressed_now()) { s_ci_skip_supp++; return; } + if (g_call_unit_depth > 0) return; + if (overlay_irq_suppressed_now()) return; /* psx_advance_cycles() has already raised every device edge due at this * block. Avoid entering the full scheduler/diagnostic path when COP0 could * not take the IRQ anyway. FMV polling loops can execute this edge millions * of times while an INTC bit is pending but IEc is deliberately clear. */ - if ((i_stat & i_mask) == 0) { s_ci_skip_none++; return; } - if ((cpu->cop0[12] & ((1u << 10) | 1u)) != ((1u << 10) | 1u)) { - s_ci_skip_sr++; - return; - } - if (!psx_interrupt_delivery_needed(cpu)) { s_ci_skip_deliv++; return; } - s_ci_enter++; + if ((i_stat & i_mask) == 0) return; + if ((cpu->cop0[12] & ((1u << 10) | 1u)) != ((1u << 10) | 1u)) return; + if (!psx_interrupt_delivery_needed(cpu)) return; if (s_irq_defer_cdrom && (i_stat & (1u << IRQ_CDROM))) { uint32_t saved_cd = i_stat & (1u << IRQ_CDROM); i_stat &= ~(1u << IRQ_CDROM); @@ -2134,15 +2108,11 @@ static void overlay_ci_at_wrapper(CPUState *cpu, uint32_t resume_pc) { /* Defer while inside a nested call unit (see g_call_unit_depth): suspending * here would save resume_pc at the callee's block leader while the enclosing * dirty caller expects an atomic unit — the resume-desync bug. */ - if (g_call_unit_depth > 0) { s_ci_skip_unit++; return; } - if (overlay_irq_suppressed_now()) { s_ci_skip_supp++; return; } - if ((i_stat & i_mask) == 0) { s_ci_skip_none++; return; } - if ((cpu->cop0[12] & ((1u << 10) | 1u)) != ((1u << 10) | 1u)) { - s_ci_skip_sr++; - return; - } - if (!psx_interrupt_delivery_needed(cpu)) { s_ci_skip_deliv++; return; } - s_ci_enter++; + if (g_call_unit_depth > 0) return; + if (overlay_irq_suppressed_now()) return; + if ((i_stat & i_mask) == 0) return; + if ((cpu->cop0[12] & ((1u << 10) | 1u)) != ((1u << 10) | 1u)) return; + if (!psx_interrupt_delivery_needed(cpu)) return; extern int g_idle_note_suppress; int suppress_idle_note = overlay_idle_note_is_internal_or_return(cpu, resume_pc); if (suppress_idle_note) g_idle_note_suppress++; diff --git a/runtime/src/psx_cycles.c b/runtime/src/psx_cycles.c index 0ceeeab2..72cbe9b4 100644 --- a/runtime/src/psx_cycles.c +++ b/runtime/src/psx_cycles.c @@ -3,7 +3,6 @@ #include "psx_cycles.h" #include "cpu_state.h" #include -#include #if defined(_MSC_VER) #include /* MSVC intrinsics: _BitScanReverse (no __builtin_clz) */ #endif @@ -13,9 +12,6 @@ #include "sio.h" #include "starvation_ring.h" #include "timers.h" -#if defined(PSX_HAS_RECOMP_NET) -#include "psx_netplay.h" -#endif #ifdef PSX_COSIM #include "cosim_state.h" #endif @@ -111,11 +107,6 @@ static uint64_t s_devices_synced_cycle = 0; /* devices are advanced up to here /* Exported for the inlined psx_advance_cycles / psx_cyc_charge hot path. */ uint64_t psx_next_service_cycle = 0; /* absolute; 0 = dirty, recompute */ int psx_in_device_service = 0; /* re-entrancy guard */ -int g_plp_cycle_diag = 0; -uint64_t g_plp_adv_calls = 0; -uint32_t g_plp_adv_max_chunk = 0; -uint64_t g_plp_adv_sum = 0; -uint64_t g_plp_svc_calls = 0; #define s_next_service_cycle psx_next_service_cycle #define s_in_device_service psx_in_device_service static uint64_t s_next_watchdog = 0; @@ -167,7 +158,6 @@ static void psx_devices_recompute_deadline(void) { void psx_devices_service_to_now(void) { if (s_in_device_service) return; /* device code charged cycles: absorb */ - if (g_plp_cycle_diag) g_plp_svc_calls++; g_psx_cycle_fast_limit = 0; s_in_device_service = 1; uint64_t target = psx_cycle_count; @@ -208,11 +198,6 @@ void psx_devices_service_to_now(void) { if (target >= s_next_watchdog) { s_next_watchdog = target + 65536ull; psx_cycles_watchdog_fire(); -#if defined(PSX_HAS_RECOMP_NET) - /* INPUT/CONFIRM retransmit during BIOS free-run (before vblank admit). */ - if (psx_netplay_active()) - psx_netplay_pump(); -#endif } if (target >= s_next_pc_sample) { s_next_pc_sample = target + 1048576ull; @@ -315,19 +300,6 @@ void psx_advance_cycles_slow(uint32_t cycles) { psx_cycles_pc_sample_fire(); } #endif -#if defined(PSX_HAS_RECOMP_NET) - /* Independent of starvation ring (off in PSX_NO_DEBUG_TOOLS release). - * Retransmit INPUT/CONFIRM during BIOS free-run before first vblank. */ - { - static uint32_t s_net_pump_throttle; - s_net_pump_throttle += cycles; - if (s_net_pump_throttle >= 65536u) { - s_net_pump_throttle = 0; - if (psx_netplay_active()) - psx_netplay_pump(); - } - } -#endif } uint64_t psx_get_cycle_count(void) { @@ -546,35 +518,13 @@ void psx_idle_note_check(CPUState *cpu, uint32_t check_pc) { * the deadline-model bookkeeping (synced position + next deadline) is stale and * would try to replay a bogus gap. Re-anchor devices at the restored cycle and * force a fresh deadline on the next charge. */ -void psx_cycles_resync_after_restore(CPUState *cpu) { +void psx_cycles_resync_after_restore(void) { g_psx_cyc_batch = 0; g_psx_cyc_batch_limit = 0; g_psx_cyc_bb_defer = 0; s_devices_synced_cycle = psx_cycle_count; psx_next_service_cycle = 0; /* recompute on next charge */ psx_in_device_service = 0; - /* Idle-skip detector latches absolute cycle/store counters from the - * pre-load timeline; drop them so a rewound clock cannot false-train. */ - s_idle_pc = 0; - s_idle_streak = 0; - s_idle_have_snap = 0; - s_idle_progress_reg = -2; - s_idle_last_cycle = psx_cycle_count; - /* GTE/muldiv completion deadlines and load-absorb give-back are host-only - * absolute cycle stamps (not in BS_SEC_CPU). After a warm load they still - * hold the pre-load live timeline; the next psx_gte_stall / muldiv_stall - * would then advance (live_ts - restored_cycle) in one shot — tens of - * millions of cycles / N nested presents with zero IRQ checks (MotK - * transform CTC2 path). Anchor them at the restored clock. */ - if (cpu) { - cpu->gte_ts_done = psx_cycle_count; - cpu->muldiv_ts_done = psx_cycle_count; - memset(cpu->read_absorb, 0, sizeof(cpu->read_absorb)); - cpu->read_absorb_which = 0; - cpu->read_fudge = 0x20u; /* no committed predecessor load */ - cpu->ld_which_t = 0x20u; /* no pending load dest */ - cpu->ld_absorb = 0; - } } void psx_cycles_reset_for_boot(void) { diff --git a/runtime/src/psx_keybinds.c b/runtime/src/psx_keybinds.c index 3d2458ef..5e19efa9 100644 --- a/runtime/src/psx_keybinds.c +++ b/runtime/src/psx_keybinds.c @@ -9,7 +9,6 @@ #include "psx_keybinds.h" #include -#include #include #include #include @@ -35,37 +34,45 @@ /* ── Defaults ─────────────────────────────────────────────────────────────── */ /* - * Every player slot uses the framework's historical hardcoded keyboard mapping - * (pad_from_keyboard / pad_sticks_for in main.cpp): + * Player 1 reproduces the framework's historical hardcoded keyboard mapping + * (pad_from_keyboard / pad_sticks_for in main.cpp), so shipping keybinds.ini + * with defaults changes nothing until the user edits it: * D-pad: Arrow keys Start: Return Select: Right Shift * Cross: X Circle: S Square: Z Triangle: A - * L1: Q R1: W L2: E R2: R L3: T R3: Y - * Left analog stick: Arrow keys - * Right analog stick: unbound - * Simultaneous multi-keyboard play still requires distinct binds per slot. + * L1: Q R1: W L2: E R2: R L3: T R3: Y (stick clicks continue the + * shoulder row; DualShock-only games like Ape Escape require L3/R3) + * Left analog stick: Arrow keys (matches the old keyboard analog path) + * Right analog stick: unbound (the keyboard never drove it before) + * Player 2 is fully unbound (add binds to enable a 2nd keyboard player). */ -#define PSXKB_PLAYER_DEFAULTS { \ - .up = SDL_SCANCODE_UP, .down = SDL_SCANCODE_DOWN, \ - .left = SDL_SCANCODE_LEFT, .right = SDL_SCANCODE_RIGHT, \ - .cross = SDL_SCANCODE_X, .circle = SDL_SCANCODE_S, \ - .square = SDL_SCANCODE_Z, .triangle = SDL_SCANCODE_A, \ - .l1 = SDL_SCANCODE_Q, .r1 = SDL_SCANCODE_W, \ - .l2 = SDL_SCANCODE_E, .r2 = SDL_SCANCODE_R, \ - .l3 = SDL_SCANCODE_T, .r3 = SDL_SCANCODE_Y, \ - .start = SDL_SCANCODE_RETURN, .select = SDL_SCANCODE_RSHIFT, \ - .ls_up = SDL_SCANCODE_UP, .ls_down = SDL_SCANCODE_DOWN, \ - .ls_left = SDL_SCANCODE_LEFT, .ls_right = SDL_SCANCODE_RIGHT, \ - .rs_up = SDL_SCANCODE_UNKNOWN, .rs_down = SDL_SCANCODE_UNKNOWN, \ - .rs_left = SDL_SCANCODE_UNKNOWN, .rs_right = SDL_SCANCODE_UNKNOWN, \ -} - #define PSXKB_DEFAULTS { \ - .player = { \ - PSXKB_PLAYER_DEFAULTS, \ - PSXKB_PLAYER_DEFAULTS, \ - PSXKB_PLAYER_DEFAULTS, \ - PSXKB_PLAYER_DEFAULTS, \ - PSXKB_PLAYER_DEFAULTS, \ + .p1 = { \ + .up = SDL_SCANCODE_UP, .down = SDL_SCANCODE_DOWN, \ + .left = SDL_SCANCODE_LEFT, .right = SDL_SCANCODE_RIGHT, \ + .cross = SDL_SCANCODE_X, .circle = SDL_SCANCODE_S, \ + .square = SDL_SCANCODE_Z, .triangle = SDL_SCANCODE_A, \ + .l1 = SDL_SCANCODE_Q, .r1 = SDL_SCANCODE_W, \ + .l2 = SDL_SCANCODE_E, .r2 = SDL_SCANCODE_R, \ + .l3 = SDL_SCANCODE_T, .r3 = SDL_SCANCODE_Y, \ + .start = SDL_SCANCODE_RETURN, .select = SDL_SCANCODE_RSHIFT, \ + .ls_up = SDL_SCANCODE_UP, .ls_down = SDL_SCANCODE_DOWN, \ + .ls_left = SDL_SCANCODE_LEFT, .ls_right = SDL_SCANCODE_RIGHT, \ + .rs_up = SDL_SCANCODE_UNKNOWN, .rs_down = SDL_SCANCODE_UNKNOWN, \ + .rs_left = SDL_SCANCODE_UNKNOWN, .rs_right = SDL_SCANCODE_UNKNOWN, \ + }, \ + .p2 = { \ + .up = SDL_SCANCODE_UNKNOWN, .down = SDL_SCANCODE_UNKNOWN, \ + .left = SDL_SCANCODE_UNKNOWN, .right = SDL_SCANCODE_UNKNOWN, \ + .cross = SDL_SCANCODE_UNKNOWN, .circle = SDL_SCANCODE_UNKNOWN, \ + .square = SDL_SCANCODE_UNKNOWN, .triangle = SDL_SCANCODE_UNKNOWN, \ + .l1 = SDL_SCANCODE_UNKNOWN, .r1 = SDL_SCANCODE_UNKNOWN, \ + .l2 = SDL_SCANCODE_UNKNOWN, .r2 = SDL_SCANCODE_UNKNOWN, \ + .l3 = SDL_SCANCODE_UNKNOWN, .r3 = SDL_SCANCODE_UNKNOWN, \ + .start = SDL_SCANCODE_UNKNOWN, .select = SDL_SCANCODE_UNKNOWN, \ + .ls_up = SDL_SCANCODE_UNKNOWN, .ls_down = SDL_SCANCODE_UNKNOWN, \ + .ls_left = SDL_SCANCODE_UNKNOWN, .ls_right = SDL_SCANCODE_UNKNOWN, \ + .rs_up = SDL_SCANCODE_UNKNOWN, .rs_down = SDL_SCANCODE_UNKNOWN, \ + .rs_left = SDL_SCANCODE_UNKNOWN, .rs_right = SDL_SCANCODE_UNKNOWN, \ }, \ } @@ -210,35 +217,15 @@ static void write_ini(const char *path) { "# l3/r3 (stick clicks), start/select. ls_* / rs_* are the left/right\n" "# analog-stick DIRECTIONS driven from the keyboard (analog pad modes).\n" "#\n" - "# Every player slot defaults to the same keyboard map. Rebind per slot\n" - "# for simultaneous multi-keyboard play (route a port to \"Keyboard\").\n" + "# Player 2 is unbound by default — fill in keys to enable a second\n" + "# keyboard player (route a port to \"Keyboard\" in the launcher).\n" "\n"); - for (int p = 0; p < PSXKB_MAX_PLAYERS; ++p) { - char section[16]; - snprintf(section, sizeof(section), "player%d", p + 1); - write_player_section(f, section, &s_binds.player[p]); - } + write_player_section(f, "player1", &s_binds.p1); + write_player_section(f, "player2", &s_binds.p2); fclose(f); printf("[Keybinds] Wrote %s\n", path); } -static int player_all_unbound(const PsxPlayerBinds *pb) { - for (int i = 0; i < PSXKB_N; i++) { - SDL_Scancode sc = *(const SDL_Scancode *)((const char *)pb + s_buttons[i].offset); - if (sc != SDL_SCANCODE_UNKNOWN) return 0; - } - return 1; -} - -/* Older keybinds.ini left P2+ fully unbound. Promote empty slots to the shared - * default map so every player can Reset / use keyboard with the P1 layout. */ -static void promote_empty_players(void) { - for (int p = 0; p < PSXKB_MAX_PLAYERS; ++p) { - if (player_all_unbound(&s_binds.player[p])) - s_binds.player[p] = s_default_binds.player[0]; - } -} - static void load_ini(const char *path) { FILE *f = fopen(path, "r"); if (!f) return; @@ -252,11 +239,8 @@ static void load_ini(const char *path) { if (end) *end = '\0'; const char *section = line + 1; current = NULL; - if (!strncmp(section, "player", 6)) { - int n = atoi(section + 6); - if (n >= 1 && n <= PSXKB_MAX_PLAYERS) - current = &s_binds.player[n - 1]; - } + if (!strcmp(section, "player1")) current = &s_binds.p1; + else if (!strcmp(section, "player2")) current = &s_binds.p2; continue; } char *eq = strchr(line, '='); @@ -274,7 +258,6 @@ static void load_ini(const char *path) { } } fclose(f); - promote_empty_players(); printf("[Keybinds] Loaded %s\n", path); } @@ -290,12 +273,10 @@ void psx_keybinds_init(const char *exe_path) { const PsxKeyBinds *psx_keybinds_get(void) { return &s_binds; } static const PsxPlayerBinds *player_binds_c(int player) { - if (player < 1 || player > PSXKB_MAX_PLAYERS) return &s_binds.player[0]; - return &s_binds.player[player - 1]; + return (player == 2) ? &s_binds.p2 : &s_binds.p1; } static PsxPlayerBinds *player_binds(int player) { - if (player < 1 || player > PSXKB_MAX_PLAYERS) return &s_binds.player[0]; - return &s_binds.player[player - 1]; + return (player == 2) ? &s_binds.p2 : &s_binds.p1; } /* Is the scancode at button-def index i currently held for this player? */ @@ -305,7 +286,7 @@ static int held(const uint8_t *keys, const PsxPlayerBinds *pb, int i) { } uint16_t psx_keybinds_pad_word(const uint8_t *keys, int player) { - if (!keys || player < 1 || player > PSXKB_MAX_PLAYERS) return 0xFFFF; + if (!keys) return 0xFFFF; const PsxPlayerBinds *pb = player_binds_c(player); uint16_t b = 0xFFFF; /* active-low: all released */ for (int i = 0; i < PSXKB_N; i++) { @@ -316,7 +297,7 @@ uint16_t psx_keybinds_pad_word(const uint8_t *keys, int player) { } void psx_keybinds_sticks(const uint8_t *keys, int player, uint8_t out[4]) { - if (!keys || !out || player < 1 || player > PSXKB_MAX_PLAYERS) return; + if (!keys || !out) return; const PsxPlayerBinds *pb = player_binds_c(player); if (held(keys, pb, PSX_KB_LS_LEFT)) out[0] = 0x00; if (held(keys, pb, PSX_KB_LS_RIGHT)) out[0] = 0xFF; @@ -329,7 +310,7 @@ void psx_keybinds_sticks(const uint8_t *keys, int player, uint8_t out[4]) { } int psx_keybinds_dpad_active(const uint8_t *keys, int player) { - if (!keys || player < 1 || player > PSXKB_MAX_PLAYERS) return 0; + if (!keys) return 0; const PsxPlayerBinds *pb = player_binds_c(player); return held(keys, pb, PSX_KB_UP) || held(keys, pb, PSX_KB_DOWN) || held(keys, pb, PSX_KB_LEFT) || held(keys, pb, PSX_KB_RIGHT); @@ -350,19 +331,16 @@ const char *psx_keybinds_button_label(int button) { SDL_Scancode psx_keybinds_get_button(int player, int button) { if (button < 0 || button >= PSXKB_N) return SDL_SCANCODE_UNKNOWN; - if (player < 1 || player > PSXKB_MAX_PLAYERS) return SDL_SCANCODE_UNKNOWN; return *(SDL_Scancode *)((char *)player_binds(player) + s_buttons[button].offset); } void psx_keybinds_set_button(int player, int button, SDL_Scancode sc) { if (button < 0 || button >= PSXKB_N) return; - if (player < 1 || player > PSXKB_MAX_PLAYERS) return; *(SDL_Scancode *)((char *)player_binds(player) + s_buttons[button].offset) = sc; } void psx_keybinds_reset_player(int player) { - if (player < 1 || player > PSXKB_MAX_PLAYERS) return; - *player_binds(player) = s_default_binds.player[0]; + *player_binds(player) = (player == 2) ? s_default_binds.p2 : s_default_binds.p1; } void psx_keybinds_save(void) { diff --git a/runtime/src/psx_lobby_client.c b/runtime/src/psx_lobby_client.c index d512f922..e0dfdc7e 100644 --- a/runtime/src/psx_lobby_client.c +++ b/runtime/src/psx_lobby_client.c @@ -25,19 +25,14 @@ int psx_lobby_list_count(void) { return 0; } int psx_lobby_list_get(int index, PsxLobbyRow *out) { (void)index; (void)out; return 0; } void psx_lobby_set_game_identity(const char *a, const char *b) { (void)a; (void)b; } const char *psx_lobby_game_version(void) { return PSX_GAME_VERSION; } -void psx_lobby_set_max_slots(int max_slots) { (void)max_slots; } int psx_lobby_create(const char *a, const char *b, const char *c, const char *d, const char *e, const PsxLobbyMatchCaps *f) { (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; return -1; } int psx_lobby_join(const char *a, const char *b, const char *c) { (void)a; (void)b; (void)c; return -1; } int psx_lobby_leave(void) { return -1; } -int psx_lobby_kick(int slot) { (void)slot; return -1; } -int psx_lobby_move_member(int from_slot, int to_slot) -{ (void)from_slot; (void)to_slot; return -1; } int psx_lobby_in_lobby(void) { return 0; } int psx_lobby_is_host(void) { return 0; } -const char *psx_lobby_host_player_id(void) { return ""; } const PsxLobbyJoinInfo *psx_lobby_join_info(void) { static PsxLobbyJoinInfo z; @@ -51,35 +46,6 @@ const PsxLobbyMatchCaps *psx_lobby_match_caps(void) int psx_lobby_set_match_caps(const PsxLobbyMatchCaps *c) { (void)c; return -1; } int psx_lobby_member_count(void) { return 0; } int psx_lobby_member_get(int index, PsxLobbyMember *out) { (void)index; (void)out; return 0; } -int psx_lobby_member_latency_ms(int slot) { (void)slot; return -1; } -int psx_lobby_member_is_host(const PsxLobbyMember *member) -{ - (void)member; - return 0; -} -int psx_lobby_send_signal(int type, int flag, const char *text) -{ - (void)type; - (void)flag; - (void)text; - return -1; -} -int psx_lobby_poll_signal(int *type, int *flag, char *text, size_t text_cap) -{ - (void)type; - (void)flag; - (void)text; - (void)text_cap; - return 0; -} -void psx_lobby_clear_signals(void) {} -void psx_lobby_set_ice_signal_accept(int accept) { (void)accept; } -int psx_lobby_request_turn_credentials(void) { return -1; } -const PsxLobbyTurnCredentials *psx_lobby_turn_credentials(void) -{ - static PsxLobbyTurnCredentials z; - return &z; -} int psx_lobby_local_ready(void) { return 0; } int psx_lobby_all_ready(void) { return 0; } int psx_lobby_set_ready(int ready) { (void)ready; return -1; } @@ -91,9 +57,6 @@ void psx_lobby_clear_launch_pending(void) {} #include "rnet_ws.h" #include "rnet_sha1.h" -#include "recomp_net/address.h" -#include "recomp_net/lan_beacon.h" -#include "recomp_net/rtt_probe.h" #if defined(_WIN32) #include @@ -108,17 +71,6 @@ void psx_lobby_clear_launch_pending(void) {} #include #endif -/* Winsock sets WSAGetLastError(), not errno — bare errno checks drop the - * non-blocking WS handshake on Windows (list/create look permanently dead). */ -static int socket_would_block(void) -{ -#if defined(_WIN32) - return WSAGetLastError() == WSAEWOULDBLOCK; -#else - return errno == EAGAIN || errno == EWOULDBLOCK; -#endif -} - typedef struct { int fd; int connected; @@ -137,7 +89,6 @@ typedef struct { int list_count; int in_lobby; int is_host; - char host_player_id[PSX_LOBBY_ID_LEN]; char my_bind[PSX_LOBBY_ENDPOINT_LEN]; char filter_game_name[PSX_LOBBY_NAME_LEN]; char filter_game_version[PSX_LOBBY_VERSION_LEN]; @@ -150,24 +101,6 @@ typedef struct { PsxLobbyMatchCaps match_caps; char pending_tx[8][2048]; int pending_n; - /* Inbound ICE signals (WS op:signal). */ - struct { - int type; - int flag; - char text[2048]; - } sig_q[32]; - int sig_head; - int sig_tail; - int sig_count; - /* 0 while in lobby / after soft-return — drop stale ICE; launch sets 1. */ - int ice_signal_accept; - /* Coturn mint from WS get_turn_credentials. */ - PsxLobbyTurnCredentials turn; - time_t turn_received_at; - int turn_request_pending; - /* Waiting-room latency (ms) keyed by pad slot; -1 = unknown. */ - int member_rtt_ms[PSX_LOBBY_MAX_MEMBERS]; - uint64_t rtt_next_ping_ms; } LobbyClient; static LobbyClient g_lc = { @@ -175,600 +108,6 @@ static LobbyClient g_lc = { .filter_game_version = PSX_GAME_VERSION, }; -enum { - PSX_LOBBY_SIG_RTT_PING = 100, - PSX_LOBBY_SIG_RTT_PONG = 101, - PSX_LOBBY_SIG_RTT_REPORT = 102 -}; - -static uint64_t lobby_mono_ms(void) -{ -#if defined(CLOCK_MONOTONIC) - struct timespec ts; - if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) - return (uint64_t)ts.tv_sec * 1000ull + - (uint64_t)ts.tv_nsec / 1000000ull; -#endif - return (uint64_t)time(NULL) * 1000ull; -} - -/* Defined later; used by waiting-room RTT signal handling. */ -int psx_lobby_send_signal(int type, int flag, const char *text); -static int endpoint_port_is_zero(const char *ep); -static int using_server_input_relay(const PsxLobbyJoinInfo *j); -static void queue_send(const char *msg); -static void flush_pending(void); -static void lobby_rtt_close(void); - -static RNetRttProbe *g_rtt_probe; - -/* One-shot list latency: burst-ping LAN + public candidates after lobby_list. */ -#define PSX_LOBBY_MAX_PROBE_PEND (PSX_LOBBY_MAX_LIST * (PSX_LOBBY_MAX_LAN_EPS + 1)) -static RNetRttProbe *g_list_rtt_probe; -static int g_list_rtt_active; -static int g_list_rtt_on_next_list; /* set by request_list / Refresh */ -static uint64_t g_list_rtt_deadline_ms; -static unsigned long long g_list_rtt_sent_ts[PSX_LOBBY_MAX_PROBE_PEND]; -static int g_list_rtt_lobby_idx[PSX_LOBBY_MAX_PROBE_PEND]; -static int g_list_rtt_pend_active[PSX_LOBBY_MAX_PROBE_PEND]; -static int g_list_rtt_pend_count; - -/* Local UDP broadcast discovery — LAN endpoint never goes to the hub. */ -static RNetLanBeacon *g_lan_beacon_pub; -static RNetLanBeacon *g_lan_beacon_listen; - -/* Host STUN advertise → set_host_endpoint for list / pre-join RTT. */ -enum { - HOST_ADV_IDLE = 0, - HOST_ADV_WAIT_TURN, - HOST_ADV_DONE -}; -static int g_host_adv_state; -static uint64_t g_host_adv_deadline_ms; - -static void lobby_host_advertise_reset(void) -{ - g_host_adv_state = HOST_ADV_IDLE; - g_host_adv_deadline_ms = 0; -} - -static int host_endpoint_is_loopback(const char *ep) -{ - if (!ep || !ep[0]) - return 0; - if (strncmp(ep, "127.", 4) == 0) - return 1; - if (strncmp(ep, "::1:", 4) == 0 || strcmp(ep, "::1") == 0) - return 1; - if (strncmp(ep, "localhost:", 10) == 0 || strcmp(ep, "localhost") == 0) - return 1; - return 0; -} - -static int endpoint_host_port(const char *ep, char *host, size_t host_cap, int *port_out) -{ - const char *colon; - size_t n; - if (!ep || !ep[0] || !host || host_cap == 0 || !port_out) - return 0; - colon = strrchr(ep, ':'); - if (!colon || colon == ep || !colon[1]) - return 0; - n = (size_t)(colon - ep); - if (n + 1 > host_cap) - n = host_cap - 1; - memcpy(host, ep, n); - host[n] = '\0'; - *port_out = (int)strtoul(colon + 1, NULL, 10); - return *port_out > 0 && *port_out <= 65535; -} - -static int parse_ipv4_dotted(const char *host, unsigned *o) -{ - unsigned a, b, c, d; - char extra; - if (!host || !o) - return 0; - if (sscanf(host, "%u.%u.%u.%u%c", &a, &b, &c, &d, &extra) != 4) - return 0; - if (a > 255 || b > 255 || c > 255 || d > 255) - return 0; - o[0] = a; - o[1] = b; - o[2] = c; - o[3] = d; - return 1; -} - -static int ipv4_is_rfc1918(const unsigned o[4]) -{ - if (!o) - return 0; - if (o[0] == 10) - return 1; - if (o[0] == 172 && o[1] >= 16 && o[1] <= 31) - return 1; - if (o[0] == 192 && o[1] == 168) - return 1; - return 0; -} - -static int ipv4_is_link_local(const unsigned o[4]) -{ - return o && o[0] == 169 && o[1] == 254; -} - -/* Public WAN IPv4 suitable for lobby-list host_endpoint (not LAN/loopback). */ -static int endpoint_is_public_ipv4(const char *ep) -{ - char host[128]; - unsigned o[4]; - int port = 0; - if (!endpoint_host_port(ep, host, sizeof(host), &port)) - return 0; - if (!parse_ipv4_dotted(host, o)) - return 0; - if (o[0] == 0 || o[0] == 127 || o[0] >= 224) - return 0; - if (ipv4_is_rfc1918(o) || ipv4_is_link_local(o)) - return 0; - return 1; -} - -static void lobby_rtt_ensure(void); -static void lobby_list_rtt_start(int force_all); - -/* /24 heuristic — good enough for typical home/small office LANs. */ -static int ipv4_same_lan24(const unsigned a[4], const unsigned b[4]) -{ - return a && b && a[0] == b[0] && a[1] == b[1] && a[2] == b[2]; -} - -static int my_bind_port(void) -{ - char host[128]; - int port = 7777; - if (g_lc.my_bind[0] && endpoint_host_port(g_lc.my_bind, host, sizeof(host), &port)) - return port; - return 7777; -} - -/* Single LAN advertise candidate: the Host Lobby "Advertised IP" / my_bind NIC. - * Do not enumerate every local interface — only the menu selection. */ -static int collect_host_lan_endpoints(char out[][PSX_LOBBY_ENDPOINT_LEN], int max_out) -{ - char bind_host[128]; - unsigned bind_o[4]; - int port = my_bind_port(); - - if (!out || max_out <= 0) - return 0; - bind_host[0] = '\0'; - if (!g_lc.my_bind[0] || - !endpoint_host_port(g_lc.my_bind, bind_host, sizeof(bind_host), &port) || - !parse_ipv4_dotted(bind_host, bind_o) || !ipv4_is_rfc1918(bind_o) || - strcmp(bind_host, "0.0.0.0") == 0) - return 0; - snprintf(out[0], PSX_LOBBY_ENDPOINT_LEN, "%s:%d", bind_host, port); - return 1; -} - -/* Discover a public UDP mapping for the game port. Coturn on the same LAN can - * return RFC1918 — reject those and fall back to any:/port + public STUN. */ -static int lobby_host_stun_public(char *out, size_t out_len) -{ - RNetExternalIpv4Config stun; - char any_bind[64]; - char endpoint[RNET_ENDPOINT_TEXT_MAX]; - int port = my_bind_port(); - int attempt; - int last_rc = RNET_EXTERNAL_IPV4_ERR_ARGUMENT; - - if (!out || out_len == 0) - return -1; - out[0] = '\0'; - snprintf(any_bind, sizeof(any_bind), "0.0.0.0:%d", port); - - for (attempt = 0; attempt < 4; ++attempt) { - const char *bind_hp; - endpoint[0] = '\0'; - rnet_external_ipv4_config_init(&stun); - if (attempt < 2 && g_lc.turn.valid && g_lc.turn.stun_host[0]) { - stun.stun_host = g_lc.turn.stun_host; - stun.stun_port = (unsigned short)g_lc.turn.stun_port; - } - /* attempt 0: coturn + my_bind, 1: coturn + any, 2: default + my_bind, - * 3: default + any */ - bind_hp = (attempt & 1) ? any_bind - : (g_lc.my_bind[0] ? g_lc.my_bind : any_bind); - last_rc = rnet_external_udp_endpoint_discover(&stun, bind_hp, endpoint, - sizeof(endpoint)); - if (last_rc != RNET_EXTERNAL_IPV4_OK || !endpoint[0]) - continue; - if (!endpoint_is_public_ipv4(endpoint)) { - fprintf(stderr, - "psx_lobby: STUN mapped private %s (bind=%s stun=%s) — " - "retrying\n", - endpoint, bind_hp, - stun.stun_host ? stun.stun_host : "(default)"); - continue; - } - strncpy(out, endpoint, out_len - 1); - out[out_len - 1] = '\0'; - return 0; - } - return last_rc != 0 ? last_rc : -1; -} - -static void lobby_lan_beacon_close_all(void) -{ - rnet_lan_beacon_close(&g_lan_beacon_pub); - rnet_lan_beacon_close(&g_lan_beacon_listen); -} - -static void lobby_lan_beacon_publish_update(void) -{ - char lan[PSX_LOBBY_MAX_LAN_EPS][PSX_LOBBY_ENDPOINT_LEN]; - int lan_n; - if (!g_lc.is_host || !g_lc.in_lobby || g_lc.launch_pending) - return; - if (!g_lc.join.lobby_id[0]) - return; - lan_n = collect_host_lan_endpoints(lan, PSX_LOBBY_MAX_LAN_EPS); - if (lan_n <= 0) { - rnet_lan_beacon_close(&g_lan_beacon_pub); - return; - } - if (!g_lan_beacon_pub && - rnet_lan_beacon_publish_open(&g_lan_beacon_pub, 0) != 0) - return; - if (rnet_lan_beacon_publish_set(g_lan_beacon_pub, g_lc.join.lobby_id, lan[0], - g_lc.filter_game_name) != 0) { - rnet_lan_beacon_close(&g_lan_beacon_pub); - return; - } - fprintf(stderr, "psx_lobby: LAN beacon publish %s → %s\n", - g_lc.join.lobby_id, lan[0]); -} - -static void lobby_lan_beacon_tick(void) -{ - if (g_lc.is_host && g_lc.in_lobby && !g_lc.launch_pending) { - if (!g_lan_beacon_pub) - lobby_lan_beacon_publish_update(); - if (g_lan_beacon_pub) - (void)rnet_lan_beacon_publish_tick(g_lan_beacon_pub); - } else if (g_lan_beacon_pub) { - rnet_lan_beacon_close(&g_lan_beacon_pub); - } - - /* Guests (and hosts browsing after leave) listen for same-LAN announces. */ - if (!g_lc.is_host || !g_lc.in_lobby) { - int updated = 0; - if (!g_lan_beacon_listen) - (void)rnet_lan_beacon_listen_open(&g_lan_beacon_listen, 0); - if (g_lan_beacon_listen) - updated = rnet_lan_beacon_listen_pump(g_lan_beacon_listen); - /* New beacon → re-probe list rows still missing latency. */ - if (updated > 0 && g_lc.list_count > 0 && !g_list_rtt_active) { - int i; - int need = 0; - for (i = 0; i < g_lc.list_count; ++i) { - if (g_lc.list[i].latency_ms < 0) { - need = 1; - break; - } - } - if (need) - lobby_list_rtt_start(0); - } - } -} - -static void lobby_host_advertise_tick(void) -{ - char endpoint[RNET_ENDPOINT_TEXT_MAX]; - char msg[384]; - int rc; - - if (g_host_adv_state != HOST_ADV_WAIT_TURN) - return; - if (!g_lc.is_host || !g_lc.in_lobby || g_lc.launch_pending) { - lobby_host_advertise_reset(); - return; - } - if (using_server_input_relay(&g_lc.join)) { - g_host_adv_state = HOST_ADV_DONE; - return; - } - /* Prefer Coturn STUN from turn_credentials; don't block forever. */ - if (!g_lc.turn.valid && lobby_mono_ms() < g_host_adv_deadline_ms) - return; - if (!g_lc.my_bind[0]) { - g_host_adv_state = HOST_ADV_DONE; - return; - } - - /* Free the game UDP port for an exclusive STUN bind (skip on loopback hub). */ - if (!host_endpoint_is_loopback(g_lc.join.host_endpoint)) { - lobby_rtt_close(); - endpoint[0] = '\0'; - rc = lobby_host_stun_public(endpoint, sizeof(endpoint)); - if (rc == 0 && endpoint[0]) { - strncpy(g_lc.join.host_endpoint, endpoint, - sizeof(g_lc.join.host_endpoint) - 1); - g_lc.join.host_endpoint[sizeof(g_lc.join.host_endpoint) - 1] = '\0'; - } else { - fprintf(stderr, - "psx_lobby: STUN advertise failed (%d) — keeping %s%s\n", rc, - g_lc.join.host_endpoint[0] ? g_lc.join.host_endpoint - : "(none)", - endpoint_is_public_ipv4(g_lc.join.host_endpoint) - ? "" - : " (use LAN beacon for local list RTT)"); - } - /* Answer list/waiting-room PINGs again as soon as STUN frees the port. */ - lobby_rtt_ensure(); - } - - /* Private IPs stay on the LAN beacon only — never on the hub list. */ - lobby_lan_beacon_publish_update(); - - g_host_adv_state = HOST_ADV_DONE; - if (!g_lc.join.host_endpoint[0]) - return; - snprintf(msg, sizeof(msg), - "{\"op\":\"set_host_endpoint\",\"host_endpoint\":\"%s\"}", - g_lc.join.host_endpoint); - queue_send(msg); - flush_pending(); - fprintf(stderr, "psx_lobby: advertised host_endpoint=%s (LAN via local beacon)\n", - g_lc.join.host_endpoint); -} - -static void lobby_rtt_close(void) -{ - rnet_rtt_probe_close(&g_rtt_probe); -} - -static void lobby_list_rtt_close(void) -{ - rnet_rtt_probe_close(&g_list_rtt_probe); - g_list_rtt_active = 0; - g_list_rtt_pend_count = 0; - memset(g_list_rtt_pend_active, 0, sizeof(g_list_rtt_pend_active)); - memset(g_list_rtt_sent_ts, 0, sizeof(g_list_rtt_sent_ts)); - memset(g_list_rtt_lobby_idx, 0, sizeof(g_list_rtt_lobby_idx)); -} - -static int collect_local_rfc1918(unsigned out[][4], int max_out) -{ - RNetIpv4Address addrs[16]; - int n; - int i; - int count = 0; - - if (!out || max_out <= 0) - return 0; - n = rnet_ipv4_enumerate(addrs, sizeof(addrs) / sizeof(addrs[0])); - if (n < 0) - n = 0; - if (n > (int)(sizeof(addrs) / sizeof(addrs[0]))) - n = (int)(sizeof(addrs) / sizeof(addrs[0])); - for (i = 0; i < n && count < max_out; ++i) { - unsigned o[4]; - if (!parse_ipv4_dotted(addrs[i].address, o) || !ipv4_is_rfc1918(o)) - continue; - memcpy(out[count], o, sizeof(o)); - ++count; - } - return count; -} - -static int cand_already(char cands[][PSX_LOBBY_ENDPOINT_LEN], int n, const char *ep) -{ - int i; - for (i = 0; i < n; ++i) { - if (strcmp(cands[i], ep) == 0) - return 1; - } - return 0; -} - -/* Prefer local beacon LAN, then legacy server lan_endpoints, then public. */ -static int lobby_row_build_candidates(const PsxLobbyRow *row, - char cands[][PSX_LOBBY_ENDPOINT_LEN], - int max_cands) -{ - unsigned local[8][4]; - int local_n; - int n = 0; - int i; - int pass; - char beacon_ep[PSX_LOBBY_ENDPOINT_LEN]; - - if (!row || !cands || max_cands <= 0) - return 0; - - beacon_ep[0] = '\0'; - if (g_lan_beacon_listen && row->lobby_id[0] && - rnet_lan_beacon_lookup(g_lan_beacon_listen, row->lobby_id, beacon_ep, - sizeof(beacon_ep)) && - beacon_ep[0] && !endpoint_port_is_zero(beacon_ep)) { - strncpy(cands[n], beacon_ep, PSX_LOBBY_ENDPOINT_LEN - 1); - cands[n][PSX_LOBBY_ENDPOINT_LEN - 1] = '\0'; - ++n; - } - - local_n = collect_local_rfc1918(local, 8); - - for (pass = 0; pass < 2; ++pass) { - for (i = 0; i < row->lan_count && n < max_cands; ++i) { - char host[64]; - int port = 0; - unsigned o[4]; - int same = 0; - int j; - if (!row->lan_endpoints[i][0] || - endpoint_port_is_zero(row->lan_endpoints[i])) - continue; - if (!endpoint_host_port(row->lan_endpoints[i], host, sizeof(host), &port) || - !parse_ipv4_dotted(host, o) || !ipv4_is_rfc1918(o)) - continue; - for (j = 0; j < local_n; ++j) { - if (ipv4_same_lan24(local[j], o)) { - same = 1; - break; - } - } - if (pass == 0 && !same) - continue; - if (pass == 1 && same) - continue; /* already added */ - if (cand_already(cands, n, row->lan_endpoints[i])) - continue; - strncpy(cands[n], row->lan_endpoints[i], PSX_LOBBY_ENDPOINT_LEN - 1); - cands[n][PSX_LOBBY_ENDPOINT_LEN - 1] = '\0'; - ++n; - } - } - if (n < max_cands && row->host_endpoint[0] && - !endpoint_port_is_zero(row->host_endpoint) && - !cand_already(cands, n, row->host_endpoint)) { - strncpy(cands[n], row->host_endpoint, PSX_LOBBY_ENDPOINT_LEN - 1); - cands[n][PSX_LOBBY_ENDPOINT_LEN - 1] = '\0'; - ++n; - } - return n; -} - -/* force_all: Refresh — re-probe every row. Otherwise only rows with unknown RTT. */ -static void lobby_list_rtt_start(int force_all) -{ - int i; - - lobby_list_rtt_close(); - if (g_lc.list_count <= 0) - return; - /* Drain local beacons before building candidates (may beat WS list). */ - if (!g_lan_beacon_listen) - (void)rnet_lan_beacon_listen_open(&g_lan_beacon_listen, 0); - if (g_lan_beacon_listen) - (void)rnet_lan_beacon_listen_pump(g_lan_beacon_listen); - if (rnet_rtt_probe_open(&g_list_rtt_probe, NULL) != 0) - return; - - for (i = 0; i < g_lc.list_count; ++i) { - char cands[PSX_LOBBY_MAX_LAN_EPS + 1][PSX_LOBBY_ENDPOINT_LEN]; - int cn; - int c; - if (force_all) - g_lc.list[i].latency_ms = -1; - if (g_lc.list[i].latency_ms >= 0) - continue; - cn = lobby_row_build_candidates(&g_lc.list[i], cands, - PSX_LOBBY_MAX_LAN_EPS + 1); - for (c = 0; c < cn && g_list_rtt_pend_count < PSX_LOBBY_MAX_PROBE_PEND; ++c) { - unsigned long long sent = 0; - int slot = g_list_rtt_pend_count; - if (rnet_rtt_probe_set_peer(g_list_rtt_probe, cands[c]) != 0) - continue; - if (rnet_rtt_probe_ping_ts(g_list_rtt_probe, &sent) != 0) - continue; - g_list_rtt_sent_ts[slot] = sent; - g_list_rtt_lobby_idx[slot] = i; - g_list_rtt_pend_active[slot] = 1; - ++g_list_rtt_pend_count; - } - } - - if (g_list_rtt_pend_count <= 0) { - lobby_list_rtt_close(); - return; - } - g_list_rtt_active = 1; - /* STUN advertise briefly drops the host answer sock; give guests time. */ - g_list_rtt_deadline_ms = lobby_mono_ms() + 1500ull; -} - -static void lobby_list_rtt_tick(void) -{ - int remaining; - - if (!g_list_rtt_active || !g_list_rtt_probe) - return; - - for (;;) { - int ms = 0; - unsigned long long echo = 0; - int p; - int got = rnet_rtt_probe_pump_ex(g_list_rtt_probe, &ms, &echo); - if (got != 1) - break; - for (p = 0; p < g_list_rtt_pend_count; ++p) { - int li; - int q; - if (!g_list_rtt_pend_active[p] || g_list_rtt_sent_ts[p] != echo) - continue; - li = g_list_rtt_lobby_idx[p]; - if (li >= 0 && li < g_lc.list_count && g_lc.list[li].latency_ms < 0) - g_lc.list[li].latency_ms = ms; - /* Drop remaining candidates for this lobby. */ - for (q = 0; q < g_list_rtt_pend_count; ++q) { - if (g_list_rtt_lobby_idx[q] == li) - g_list_rtt_pend_active[q] = 0; - } - break; - } - } - - remaining = 0; - { - int p; - for (p = 0; p < g_list_rtt_pend_count; ++p) { - if (g_list_rtt_pend_active[p]) - ++remaining; - } - } - if (remaining <= 0 || lobby_mono_ms() >= g_list_rtt_deadline_ms) - lobby_list_rtt_close(); -} - -static void member_rtt_clear(void) -{ - int i; - for (i = 0; i < PSX_LOBBY_MAX_MEMBERS; ++i) - g_lc.member_rtt_ms[i] = -1; - g_lc.rtt_next_ping_ms = 0; -} - -static int member_slot_for_player(const char *player_id) -{ - int i; - if (!player_id || !player_id[0]) - return -1; - for (i = 0; i < g_lc.member_count; ++i) { - if (strcmp(g_lc.members[i].player_id, player_id) == 0) - return g_lc.members[i].slot; - } - return -1; -} - -static int local_member_slot(void) -{ - return member_slot_for_player(g_lc.player_id); -} - -/* Default max_slots for create (clamped 2..8). */ -static int g_lobby_max_slots = 2; - -void psx_lobby_set_max_slots(int max_slots) -{ - if (max_slots < 2) max_slots = 2; - if (max_slots > 8) max_slots = 8; - g_lobby_max_slots = max_slots; -} - static const char *effective_game_version(const char *override_ver) { if (override_ver && override_ver[0]) { @@ -790,24 +129,6 @@ static int list_filter_version_strict(void) } static void queue_send(const char *json); -static void clear_turn_credentials(void); -static int queue_turn_credentials_request(void); - -static void clear_turn_credentials(void) -{ - memset(&g_lc.turn, 0, sizeof(g_lc.turn)); - g_lc.turn_received_at = 0; - g_lc.turn_request_pending = 0; -} - -static int queue_turn_credentials_request(void) -{ - if (!psx_lobby_connected()) - return -1; - queue_send("{\"op\":\"get_turn_credentials\"}"); - g_lc.turn_request_pending = 1; - return 0; -} static void queue_list_request(void) { @@ -917,17 +238,6 @@ static const char *json_get_str(const char *json, const char *key, char *out, si while (*p && *p != '"' && o + 1 < cap) { if (*p == '\\' && p[1]) { ++p; - switch (*p) { - case 'n': out[o++] = '\n'; break; - case 'r': out[o++] = '\r'; break; - case 't': out[o++] = '\t'; break; - case '"': out[o++] = '"'; break; - case '\\': out[o++] = '\\'; break; - case '/': out[o++] = '/'; break; - default: out[o++] = *p; break; - } - ++p; - continue; } out[o++] = *p++; } @@ -935,128 +245,6 @@ static const char *json_get_str(const char *json, const char *key, char *out, si return out; } -/* Parse JSON string array values for key into out[0..max_out). Returns count. */ -static int json_parse_str_array(const char *json, const char *key, - char out[][PSX_LOBBY_ENDPOINT_LEN], int max_out) -{ - char pat[80]; - const char *p; - int n = 0; - - if (!json || !key || !out || max_out <= 0) - return 0; - snprintf(pat, sizeof(pat), "\"%s\"", key); - p = strstr(json, pat); - if (!p) - return 0; - p = strchr(p + strlen(pat), '['); - if (!p) - return 0; - ++p; - while (*p && n < max_out) { - size_t o = 0; - while (*p && (isspace((unsigned char)*p) || *p == ',')) - ++p; - if (*p == ']') - break; - if (*p != '"') - break; - ++p; - while (*p && *p != '"' && o + 1 < PSX_LOBBY_ENDPOINT_LEN) - out[n][o++] = *p++; - out[n][o] = '\0'; - if (*p == '"') - ++p; - if (out[n][0]) - ++n; - } - return n; -} - -static void lobby_row_lan_fingerprint(const PsxLobbyRow *row, char *out, size_t cap) -{ - size_t o = 0; - int i; - if (!out || cap == 0) - return; - out[0] = '\0'; - if (!row) - return; - for (i = 0; i < row->lan_count; ++i) { - int wrote; - if (!row->lan_endpoints[i][0]) - continue; - wrote = snprintf(out + o, cap > o ? cap - o : 0, "%s%s", o ? "|" : "", - row->lan_endpoints[i]); - if (wrote < 0 || (size_t)wrote >= (cap > o ? cap - o : 0)) - break; - o += (size_t)wrote; - } -} - -static size_t json_escape(const char *in, char *out, size_t cap) -{ - size_t o = 0; - if (!in || !out || cap == 0) return 0; - while (*in && o + 2 < cap) { - unsigned char c = (unsigned char)*in++; - if (c == '"' || c == '\\') { - if (o + 3 >= cap) break; - out[o++] = '\\'; - out[o++] = (char)c; - } else if (c == '\n') { - if (o + 3 >= cap) break; - out[o++] = '\\'; - out[o++] = 'n'; - } else if (c == '\r') { - if (o + 3 >= cap) break; - out[o++] = '\\'; - out[o++] = 'r'; - } else if (c == '\t') { - if (o + 3 >= cap) break; - out[o++] = '\\'; - out[o++] = 't'; - } else if (c < 0x20) { - continue; - } else { - out[o++] = (char)c; - } - } - out[o] = '\0'; - return o; -} - -static void enqueue_signal(int type, int flag, const char *text) -{ - int i; - if (!g_lc.ice_signal_accept) - return; - if (g_lc.sig_count >= (int)(sizeof(g_lc.sig_q) / sizeof(g_lc.sig_q[0]))) { - g_lc.sig_head = (g_lc.sig_head + 1) % (int)(sizeof(g_lc.sig_q) / sizeof(g_lc.sig_q[0])); - g_lc.sig_count--; - } - i = g_lc.sig_tail; - g_lc.sig_q[i].type = type; - g_lc.sig_q[i].flag = flag; - g_lc.sig_q[i].text[0] = '\0'; - if (text) - strncpy(g_lc.sig_q[i].text, text, sizeof(g_lc.sig_q[i].text) - 1); - g_lc.sig_tail = (g_lc.sig_tail + 1) % (int)(sizeof(g_lc.sig_q) / sizeof(g_lc.sig_q[0])); - g_lc.sig_count++; -} - -void psx_lobby_clear_signals(void) -{ - g_lc.sig_head = 0; - g_lc.sig_tail = 0; - g_lc.sig_count = 0; -} - -void psx_lobby_set_ice_signal_accept(int accept) -{ - g_lc.ice_signal_accept = accept ? 1 : 0; -} - static int json_get_int(const char *json, const char *key, int def) { char pat[80]; @@ -1140,8 +328,6 @@ static void parse_match_caps_object(const char *obj, PsxLobbyMatchCaps *out) out->input_delay = json_get_int(obj, "input_delay", 2); if (out->input_delay < 0) out->input_delay = 0; if (out->input_delay > 16) out->input_delay = 16; - out->force_input_relay = json_get_bool(obj, "force_input_relay", 0); - out->force_turn = json_get_bool(obj, "force_turn", 0); json_get_str(obj, "language", out->language, sizeof(out->language)); out->valid = 1; } @@ -1170,17 +356,13 @@ static int append_match_caps_json(char *dst, size_t dst_cap, const PsxLobbyMatch return snprintf(dst, dst_cap, ",\"match_caps\":{\"v\":1,\"aspect_num\":%d,\"aspect_den\":%d," "\"turbo_loads\":%s,\"bios_hle\":%s,\"fast_boot\":%s," - "\"auto_skip_fmv\":%s,\"input_delay\":%d,\"force_input_relay\":%s," - "\"force_turn\":%s,\"language\":\"%s\"}", + "\"auto_skip_fmv\":%s,\"input_delay\":%d,\"language\":\"%s\"}", caps->aspect_num, caps->aspect_den, caps->turbo_loads ? "true" : "false", caps->bios_hle ? "true" : "false", caps->fast_boot ? "true" : "false", caps->auto_skip_fmv ? "true" : "false", - caps->input_delay, - caps->force_input_relay ? "true" : "false", - caps->force_turn ? "true" : "false", - lang); + caps->input_delay, lang); } static void queue_send(const char *json) @@ -1214,72 +396,21 @@ static int endpoint_port_is_zero(const char *ep) return (int)strtoul(colon + 1, NULL, 10) == 0; } -/* Prefer a usable host:port among candidates (skip empty / :0). */ -static void copy_first_usable_endpoint(char *dst, size_t dst_len, const char *a, - const char *b, const char *c) -{ - const char *cands[3]; - int i; - if (!dst || dst_len == 0) return; - dst[0] = '\0'; - cands[0] = a; - cands[1] = b; - cands[2] = c; - for (i = 0; i < 3; ++i) { - if (cands[i] && cands[i][0] && !endpoint_port_is_zero(cands[i])) { - strncpy(dst, cands[i], dst_len - 1); - dst[dst_len - 1] = '\0'; - return; - } - } -} - -static int using_server_input_relay(const PsxLobbyJoinInfo *j) -{ - if (g_lc.match_caps.valid && g_lc.match_caps.force_input_relay) - return 1; - /* Server rewrote both endpoints to the same relay advertise address. */ - if (j && j->host_endpoint[0] && j->guest_endpoint[0] && - !endpoint_port_is_zero(j->host_endpoint) && - !endpoint_port_is_zero(j->guest_endpoint) && - strcmp(j->host_endpoint, j->guest_endpoint) == 0 && - (!g_lc.my_bind[0] || strcmp(j->host_endpoint, g_lc.my_bind) != 0)) - return 1; - return 0; -} - static void fill_peer_bind_from_join(void) { PsxLobbyJoinInfo *j = &g_lc.join; - const int force_relay = using_server_input_relay(j); - const int seats = j->player_count >= 2 ? j->player_count : j->max_slots; - const int host_hub = (g_lc.is_host && seats >= 3 && !force_relay) ? 1 : 0; memset(j->bind_hostport, 0, sizeof(j->bind_hostport)); memset(j->peer_hostport, 0, sizeof(j->peer_hostport)); - if (force_relay) { - /* Everyone dials the lobby-server UDP relay — ephemeral local bind - * (same as LAN guests) so same-PC multi-instance doesn't collide. */ - strncpy(j->bind_hostport, "0.0.0.0:0", sizeof(j->bind_hostport) - 1); - copy_first_usable_endpoint(j->peer_hostport, sizeof(j->peer_hostport), - j->host_endpoint, j->guest_endpoint, NULL); - } else if (g_lc.is_host) { + if (g_lc.is_host) { strncpy(j->bind_hostport, g_lc.my_bind, sizeof(j->bind_hostport) - 1); - if (!host_hub) { - /* 2P P2P: dial guest when they advertised a fixed port. Online - * guests often join with :0 — leave peer empty (accept-first). */ - if (j->guest_endpoint[0] && !endpoint_port_is_zero(j->guest_endpoint)) - strncpy(j->peer_hostport, j->guest_endpoint, sizeof(j->peer_hostport) - 1); - } - /* host_hub: peer stays empty → rnet_session_start_lan_hub */ + /* Online guests join with 0.0.0.0:0 (ephemeral). The lobby rewrites + * that to peer_ip:0, which rnet rejects as a dial target. Leave peer + * empty so the host learns the guest from the first HELLO (guest + * dials host_endpoint). Fixed guest ports still dial normally. */ + if (j->guest_endpoint[0] && !endpoint_port_is_zero(j->guest_endpoint)) + strncpy(j->peer_hostport, j->guest_endpoint, sizeof(j->peer_hostport) - 1); } else { - /* Guests dialing 3+ host hub: ephemeral local UDP (join only probes - * 7778+ and does not hold the socket). 2P P2P keeps the advertised - * fixed guest_bind so the host can dial. */ - if (seats >= 3) { - strncpy(j->bind_hostport, "0.0.0.0:0", sizeof(j->bind_hostport) - 1); - } else { - strncpy(j->bind_hostport, g_lc.my_bind, sizeof(j->bind_hostport) - 1); - } + strncpy(j->bind_hostport, g_lc.my_bind, sizeof(j->bind_hostport) - 1); strncpy(j->peer_hostport, j->host_endpoint, sizeof(j->peer_hostport) - 1); } j->bind_hostport[sizeof(j->bind_hostport) - 1] = '\0'; @@ -1341,7 +472,6 @@ static void parse_slots_array(const char *json) if (g_lc.player_id[0] && strcmp(g_lc.members[n].player_id, g_lc.player_id) == 0) { g_lc.local_ready = g_lc.members[n].ready; - g_lc.join.local_slot = g_lc.members[n].slot; } ++n; p = end; @@ -1351,17 +481,6 @@ static void parse_slots_array(const char *json) g_lc.member_count = n; } -static void ingest_host_player_id(const char *json) -{ - char host_id[PSX_LOBBY_ID_LEN]; - host_id[0] = '\0'; - json_get_str(json, "host_player_id", host_id, sizeof(host_id)); - if (host_id[0]) { - strncpy(g_lc.host_player_id, host_id, sizeof(g_lc.host_player_id) - 1); - g_lc.host_player_id[sizeof(g_lc.host_player_id) - 1] = '\0'; - } -} - static void handle_server_json(const char *json); /* Parse complete unmasked server text frames from ws_pending; leave remainder. */ @@ -1419,75 +538,11 @@ static void handle_server_json(const char *json) queue_send(msg); } queue_list_request(); - /* Prefetch Coturn creds for ICE (no-op reply if server lacks COTURN_*). */ - (void)queue_turn_credentials_request(); - return; - } - if (strcmp(op, "turn_credentials") == 0) { - int ok = json_get_bool(json, "ok", 0); - g_lc.turn_request_pending = 0; - memset(&g_lc.turn, 0, sizeof(g_lc.turn)); - g_lc.turn_received_at = 0; - if (!ok) { - char err[64]; - json_get_str(json, "error", err, sizeof(err)); - fprintf(stderr, - "psx_lobby: turn_credentials failed (%s) — online ICE " - "requires Coturn (or PSX_NET_TURN_* / " - "PSX_NET_ALLOW_STUN_ONLY=1)\n", - err[0] ? err : "unknown"); - return; - } - json_get_str(json, "stun_host", g_lc.turn.stun_host, - sizeof(g_lc.turn.stun_host)); - json_get_str(json, "turn_host", g_lc.turn.turn_host, - sizeof(g_lc.turn.turn_host)); - json_get_str(json, "username", g_lc.turn.username, - sizeof(g_lc.turn.username)); - json_get_str(json, "password", g_lc.turn.password, - sizeof(g_lc.turn.password)); - g_lc.turn.stun_port = json_get_int(json, "stun_port", 3478); - g_lc.turn.turn_port = json_get_int(json, "turn_port", 3478); - g_lc.turn.ttl_secs = (uint32_t)json_get_int(json, "ttl_secs", 86400); - if (g_lc.turn.turn_host[0] && g_lc.turn.username[0] && - g_lc.turn.password[0]) { - g_lc.turn.valid = 1; - g_lc.turn_received_at = time(NULL); - fprintf(stderr, - "psx_lobby: turn_credentials ok stun=%s:%d turn=%s:%d " - "user=%s ttl=%us\n", - g_lc.turn.stun_host[0] ? g_lc.turn.stun_host : "(none)", - g_lc.turn.stun_port, - g_lc.turn.turn_host, g_lc.turn.turn_port, - g_lc.turn.username, (unsigned)g_lc.turn.ttl_secs); - } else { - fprintf(stderr, - "psx_lobby: turn_credentials ok but incomplete fields\n"); - } return; } if (strcmp(op, "lobby_list") == 0) { const char *p = strstr(json, "\"lobbies\""); int n = 0; - /* Keep prior RTTs across server list pushes; Refresh re-probes. - * Invalidate when host_endpoint or lan_endpoints change. */ - char prev_ids[PSX_LOBBY_MAX_LIST][PSX_LOBBY_ID_LEN]; - char prev_eps[PSX_LOBBY_MAX_LIST][PSX_LOBBY_ENDPOINT_LEN]; - char prev_lan[PSX_LOBBY_MAX_LIST][256]; - int prev_ms[PSX_LOBBY_MAX_LIST]; - int prev_n = g_lc.list_count; - int i; - int want_probe = g_list_rtt_on_next_list; - g_list_rtt_on_next_list = 0; - for (i = 0; i < prev_n && i < PSX_LOBBY_MAX_LIST; ++i) { - strncpy(prev_ids[i], g_lc.list[i].lobby_id, PSX_LOBBY_ID_LEN - 1); - prev_ids[i][PSX_LOBBY_ID_LEN - 1] = '\0'; - strncpy(prev_eps[i], g_lc.list[i].host_endpoint, - PSX_LOBBY_ENDPOINT_LEN - 1); - prev_eps[i][PSX_LOBBY_ENDPOINT_LEN - 1] = '\0'; - lobby_row_lan_fingerprint(&g_lc.list[i], prev_lan[i], sizeof(prev_lan[i])); - prev_ms[i] = g_lc.list[i].latency_ms; - } g_lc.list_count = 0; if (!p) { return; @@ -1502,10 +557,6 @@ static void handle_server_json(const char *json) while (*p && *p != '{') { if (*p == ']') { g_lc.list_count = n; - if (want_probe) - lobby_list_rtt_start(1); - else if (n > 0 && !g_list_rtt_active) - lobby_list_rtt_start(0); return; } ++p; @@ -1526,26 +577,22 @@ static void handle_server_json(const char *json) ++end; } while (*end && depth > 0); { - char chunk[1536]; - char lan_fp[256]; + char chunk[1024]; size_t len = (size_t)(end - obj); if (len >= sizeof(chunk)) { len = sizeof(chunk) - 1; } memcpy(chunk, obj, len); chunk[len] = '\0'; - memset(&g_lc.list[n], 0, sizeof(g_lc.list[n])); - g_lc.list[n].latency_ms = -1; json_get_str(chunk, "lobby_id", g_lc.list[n].lobby_id, sizeof(g_lc.list[n].lobby_id)); json_get_str(chunk, "name", g_lc.list[n].name, sizeof(g_lc.list[n].name)); json_get_str(chunk, "game_name", g_lc.list[n].game_name, sizeof(g_lc.list[n].game_name)); json_get_str(chunk, "game_version", g_lc.list[n].game_version, sizeof(g_lc.list[n].game_version)); - /* Missing version stays empty for filter decisions; display - * defaults to "dev" only after accept. Old servers omitted - * game_version — rewriting to "dev" before the strict pin - * hid those rows from release clients. */ - const int has_game_version = g_lc.list[n].game_version[0] != '\0'; + if (!g_lc.list[n].game_version[0]) { + strncpy(g_lc.list[n].game_version, "dev", + sizeof(g_lc.list[n].game_version) - 1); + } /* Drop lobbies that don't match our title (broadcast list * is unfiltered). Release builds also pin game_version; * "dev" keeps other versions visible for testing. */ @@ -1554,7 +601,7 @@ static void handle_server_json(const char *json) p = end; continue; } - if (list_filter_version_strict() && has_game_version) { + if (list_filter_version_strict()) { const char *want_ver = effective_game_version(NULL); if (want_ver && want_ver[0] && strcmp(g_lc.list[n].game_version, want_ver) != 0) { @@ -1562,52 +609,15 @@ static void handle_server_json(const char *json) continue; } } - if (!has_game_version) { - strncpy(g_lc.list[n].game_version, "dev", - sizeof(g_lc.list[n].game_version) - 1); - } g_lc.list[n].player_count = json_get_int(chunk, "player_count", 0); g_lc.list[n].max_slots = json_get_int(chunk, "max_slots", 2); g_lc.list[n].has_password = json_get_bool(chunk, "has_password", 0); - json_get_str(chunk, "host_endpoint", g_lc.list[n].host_endpoint, - sizeof(g_lc.list[n].host_endpoint)); - g_lc.list[n].lan_count = json_parse_str_array( - chunk, "lan_endpoints", g_lc.list[n].lan_endpoints, - PSX_LOBBY_MAX_LAN_EPS); - lobby_row_lan_fingerprint(&g_lc.list[n], lan_fp, sizeof(lan_fp)); - if (!want_probe) { - for (i = 0; i < prev_n; ++i) { - if (prev_ids[i][0] && - strcmp(prev_ids[i], g_lc.list[n].lobby_id) == 0 && - strcmp(prev_eps[i], g_lc.list[n].host_endpoint) == 0 && - strcmp(prev_lan[i], lan_fp) == 0) { - g_lc.list[n].latency_ms = prev_ms[i]; - break; - } - } - } ++n; p = end; } } } g_lc.list_count = n; - if (want_probe) - lobby_list_rtt_start(1); - else { - int need = 0; - for (i = 0; i < n; ++i) { - if (g_lc.list[i].latency_ms < 0 && - (g_lc.list[i].host_endpoint[0] || g_lc.list[i].lan_count > 0)) { - need = 1; - break; - } - } - /* Restart even if a prior burst is mid-flight — advertise may have - * just published a public host_endpoint / LAN candidate. */ - if (need) - lobby_list_rtt_start(0); - } return; } if (strcmp(op, "created") == 0) { @@ -1616,20 +626,14 @@ static void handle_server_json(const char *json) g_lc.join.ok = 1; g_lc.launch_pending = 0; g_lc.all_ready = 0; - member_rtt_clear(); json_get_str(json, "lobby_id", g_lc.join.lobby_id, sizeof(g_lc.join.lobby_id)); g_lc.join.session_id = (uint32_t)json_get_int(json, "session_id", 1); g_lc.join.local_slot = json_get_int(json, "local_slot", 0); json_get_str(json, "host_endpoint", g_lc.join.host_endpoint, sizeof(g_lc.join.host_endpoint)); json_get_str(json, "guest_endpoint", g_lc.join.guest_endpoint, sizeof(g_lc.join.guest_endpoint)); g_lc.join.player_count = 1; - g_lc.join.max_slots = json_get_int(json, "max_slots", g_lobby_max_slots); + g_lc.join.max_slots = 2; g_lc.join.last_error[0] = '\0'; - if (g_lc.player_id[0]) { - strncpy(g_lc.host_player_id, g_lc.player_id, sizeof(g_lc.host_player_id) - 1); - g_lc.host_player_id[sizeof(g_lc.host_player_id) - 1] = '\0'; - } - ingest_host_player_id(json); ingest_match_caps_from_json(json); fill_peer_bind_from_join(); parse_slots_array(json); @@ -1642,13 +646,6 @@ static void handle_server_json(const char *json) g_lc.member_count = 1; g_lc.local_ready = 0; } - /* After create: LAN beacon immediately; STUN for public host_endpoint. */ - g_host_adv_state = HOST_ADV_WAIT_TURN; - g_host_adv_deadline_ms = lobby_mono_ms() + 500ull; - lobby_lan_beacon_publish_update(); - return; - } - if (strcmp(op, "host_endpoint_ok") == 0) { return; } if (strcmp(op, "joined") == 0) { @@ -1657,158 +654,68 @@ static void handle_server_json(const char *json) g_lc.join.ok = 1; g_lc.launch_pending = 0; g_lc.all_ready = 0; - member_rtt_clear(); json_get_str(json, "lobby_id", g_lc.join.lobby_id, sizeof(g_lc.join.lobby_id)); g_lc.join.session_id = (uint32_t)json_get_int(json, "session_id", 1); g_lc.join.local_slot = json_get_int(json, "local_slot", 1); json_get_str(json, "host_endpoint", g_lc.join.host_endpoint, sizeof(g_lc.join.host_endpoint)); json_get_str(json, "guest_endpoint", g_lc.join.guest_endpoint, sizeof(g_lc.join.guest_endpoint)); - g_lc.join.player_count = json_get_int(json, "player_count", 2); - g_lc.join.max_slots = json_get_int(json, "max_slots", 2); + g_lc.join.player_count = 2; + g_lc.join.max_slots = 2; g_lc.join.last_error[0] = '\0'; - ingest_host_player_id(json); ingest_match_caps_from_json(json); fill_peer_bind_from_join(); - /* Prefer slots on joined when present; lobby_update usually follows. */ - parse_slots_array(json); return; } if (strcmp(op, "lobby_update") == 0) { - g_lc.in_lobby = 1; json_get_str(json, "host_endpoint", g_lc.join.host_endpoint, sizeof(g_lc.join.host_endpoint)); json_get_str(json, "guest_endpoint", g_lc.join.guest_endpoint, sizeof(g_lc.join.guest_endpoint)); g_lc.join.player_count = json_get_int(json, "player_count", g_lc.join.player_count); g_lc.join.max_slots = json_get_int(json, "max_slots", g_lc.join.max_slots); g_lc.join.session_id = (uint32_t)json_get_int(json, "session_id", (int)g_lc.join.session_id); g_lc.all_ready = json_get_bool(json, "all_ready", 0); - ingest_host_player_id(json); - if (g_lc.host_player_id[0] && g_lc.player_id[0]) { - g_lc.is_host = (strcmp(g_lc.host_player_id, g_lc.player_id) == 0); - } ingest_match_caps_from_json(json); fill_peer_bind_from_join(); parse_slots_array(json); return; } if (strcmp(op, "launch") == 0) { - char relay_endpoint[PSX_LOBBY_ENDPOINT_LEN]; json_get_str(json, "host_endpoint", g_lc.join.host_endpoint, sizeof(g_lc.join.host_endpoint)); json_get_str(json, "guest_endpoint", g_lc.join.guest_endpoint, sizeof(g_lc.join.guest_endpoint)); - relay_endpoint[0] = '\0'; - json_get_str(json, "relay_endpoint", relay_endpoint, sizeof(relay_endpoint)); g_lc.join.player_count = json_get_int(json, "player_count", g_lc.join.player_count); g_lc.join.max_slots = json_get_int(json, "max_slots", g_lc.join.max_slots); g_lc.join.session_id = (uint32_t)json_get_int(json, "session_id", (int)g_lc.join.session_id); ingest_match_caps_from_json(json); - /* Prefer explicit relay_endpoint when the server opened input relay. - * Apply after caps ingest: omitted force_input_relay must not leave - * hosts on the hub path while guests dial the relay. */ - if (relay_endpoint[0] && !endpoint_port_is_zero(relay_endpoint)) { - strncpy(g_lc.join.host_endpoint, relay_endpoint, - sizeof(g_lc.join.host_endpoint) - 1); - g_lc.join.host_endpoint[sizeof(g_lc.join.host_endpoint) - 1] = '\0'; - strncpy(g_lc.join.guest_endpoint, relay_endpoint, - sizeof(g_lc.join.guest_endpoint) - 1); - g_lc.join.guest_endpoint[sizeof(g_lc.join.guest_endpoint) - 1] = '\0'; - if (!g_lc.match_caps.valid) - g_lc.match_caps.valid = 1; - g_lc.match_caps.force_input_relay = 1; - } fill_peer_bind_from_join(); parse_slots_array(json); - /* Guest must know host:port (or relay). Host may leave peer empty for - * accept-first / host-as-relay. Server relay: every peer dials relay. */ - { - const int force_relay = using_server_input_relay(&g_lc.join); - const int seats = g_lc.join.player_count >= 2 ? g_lc.join.player_count - : g_lc.join.max_slots; - const int host_hub = - (g_lc.is_host && seats >= 3 && !force_relay) ? 1 : 0; - const int peer_bad = !g_lc.join.peer_hostport[0] || - endpoint_port_is_zero(g_lc.join.peer_hostport); - if (force_relay) { - if (peer_bad) { - strncpy(g_lc.join.last_error, "missing_endpoints", - sizeof(g_lc.join.last_error) - 1); - g_lc.launch_pending = 0; - return; - } - } else if (!g_lc.join.host_endpoint[0] || - (g_lc.is_host && !host_hub && !g_lc.join.guest_endpoint[0]) || - (!g_lc.is_host && peer_bad)) { - strncpy(g_lc.join.last_error, "missing_endpoints", - sizeof(g_lc.join.last_error) - 1); - g_lc.launch_pending = 0; - return; - } + /* Guest must know host:port. Host may leave peer empty (accept-first) + * when the guest advertised an ephemeral :0 bind. */ + if (!g_lc.join.host_endpoint[0] || + (g_lc.is_host && !g_lc.join.guest_endpoint[0]) || + (!g_lc.is_host && (!g_lc.join.peer_hostport[0] || + endpoint_port_is_zero(g_lc.join.peer_hostport)))) { + strncpy(g_lc.join.last_error, "missing_endpoints", + sizeof(g_lc.join.last_error) - 1); + g_lc.launch_pending = 0; + return; } g_lc.join.last_error[0] = '\0'; g_lc.launch_pending = 1; - /* Accept ICE for this match; queue was idle (accept=0) during lobby. */ - g_lc.ice_signal_accept = 1; - lobby_rtt_close(); /* free game UDP port for the session bind */ - rnet_lan_beacon_close(&g_lan_beacon_pub); - lobby_host_advertise_reset(); - return; - } - if (strcmp(op, "signal") == 0) { - char text_buf[2048]; - char from[PSX_LOBBY_ID_LEN]; - int type = json_get_int(json, "type", 0); - int flag = json_get_int(json, "flag", 0); - text_buf[0] = '\0'; - from[0] = '\0'; - json_get_str(json, "text", text_buf, sizeof(text_buf)); - json_get_str(json, "from_player_id", from, sizeof(from)); - /* Legacy WS RTT_PING/PONG ignored — waiting-room latency uses UDP - * rnet_rtt_probe (peer path). REPORT still accepted from peers. */ - if (type == PSX_LOBBY_SIG_RTT_PING || type == PSX_LOBBY_SIG_RTT_PONG) - return; - if (type == PSX_LOBBY_SIG_RTT_REPORT) { - int slot = member_slot_for_player(from); - int ms = (int)strtol(text_buf, NULL, 10); - if (slot >= 0 && slot < PSX_LOBBY_MAX_MEMBERS && ms >= 0 && ms <= 60000) - g_lc.member_rtt_ms[slot] = ms; - return; - } - enqueue_signal(type, flag, text_buf); - (void)flag; return; } if (strcmp(op, "error") == 0) { - char code[64]; - json_get_str(json, "code", code, sizeof(code)); - strncpy(g_lc.join.last_error, code, sizeof(g_lc.join.last_error) - 1); - g_lc.join.last_error[sizeof(g_lc.join.last_error) - 1] = '\0'; - /* Create/join failures are fatal to the seat. In-lobby ops (kick/move - * on an older server, not_host, …) must not clear join.ok or the room - * looks abandoned after a rejected host action. */ - if (!g_lc.in_lobby || - strcmp(code, "bad_password") == 0 || - strcmp(code, "full") == 0 || - strcmp(code, "gone") == 0 || - strcmp(code, "already_in_lobby") == 0 || - strcmp(code, "lobby_limit") == 0 || - strcmp(code, "version_mismatch") == 0 || - strcmp(code, "game_mismatch") == 0) { - g_lc.join.ok = 0; - } + json_get_str(json, "code", g_lc.join.last_error, sizeof(g_lc.join.last_error)); + g_lc.join.ok = 0; return; } - if (strcmp(op, "lobby_closed") == 0 || strcmp(op, "left") == 0 || - strcmp(op, "kicked") == 0) { - lobby_rtt_close(); - lobby_host_advertise_reset(); + if (strcmp(op, "lobby_closed") == 0 || strcmp(op, "left") == 0) { g_lc.in_lobby = 0; g_lc.is_host = 0; - g_lc.host_player_id[0] = '\0'; g_lc.member_count = 0; g_lc.local_ready = 0; g_lc.all_ready = 0; g_lc.launch_pending = 0; memset(&g_lc.join, 0, sizeof(g_lc.join)); match_caps_clear(&g_lc.match_caps); - member_rtt_clear(); return; } } @@ -1917,11 +824,6 @@ int psx_lobby_connect(const char *ws_url) void psx_lobby_disconnect(void) { - lobby_rtt_close(); - lobby_list_rtt_close(); - lobby_lan_beacon_close_all(); - lobby_host_advertise_reset(); - g_list_rtt_on_next_list = 0; if (g_lc.fd >= 0) { close(g_lc.fd); } @@ -1931,7 +833,6 @@ void psx_lobby_disconnect(void) memset(&g_lc, 0, sizeof(g_lc)); g_lc.fd = -1; strncpy(g_lc.display_name, dname, sizeof(g_lc.display_name) - 1); - member_rtt_clear(); } } @@ -1959,91 +860,6 @@ const char *psx_lobby_player_id(void) return g_lc.player_id; } -static int first_guest_member_slot(void) -{ - int i; - for (i = 0; i < g_lc.member_count; ++i) { - if (!psx_lobby_member_is_host(&g_lc.members[i])) - return g_lc.members[i].slot; - } - return -1; -} - -/* UDP peer RTT for the waiting-room latency column (not WS signal RTT). */ -static void lobby_rtt_ensure(void) -{ - const char *bind; - const char *peer; - - if (!g_lc.in_lobby || g_lc.launch_pending || using_server_input_relay(&g_lc.join)) { - lobby_rtt_close(); - return; - } - - if (!g_rtt_probe) { - bind = g_lc.my_bind[0] ? g_lc.my_bind : NULL; - /* 3+ guests use ephemeral session binds; probe the same way. */ - if (!g_lc.is_host && g_lc.join.max_slots >= 3) - bind = NULL; - if (rnet_rtt_probe_open(&g_rtt_probe, bind) != 0) - return; - } - - peer = NULL; - if (g_lc.join.peer_hostport[0] && !endpoint_port_is_zero(g_lc.join.peer_hostport)) - peer = g_lc.join.peer_hostport; - else if (!g_lc.is_host && g_lc.join.host_endpoint[0] && - !endpoint_port_is_zero(g_lc.join.host_endpoint)) - peer = g_lc.join.host_endpoint; - if (peer) - (void)rnet_rtt_probe_set_peer(g_rtt_probe, peer); -} - -static void lobby_rtt_tick(void) -{ - int ms = 0; - int got; - - if (!g_lc.in_lobby || g_lc.launch_pending) { - lobby_rtt_close(); - return; - } - if (using_server_input_relay(&g_lc.join)) { - lobby_rtt_close(); - return; - } - - lobby_rtt_ensure(); - if (!g_rtt_probe) - return; - - got = rnet_rtt_probe_pump(g_rtt_probe, &ms); - if (got == 1) { - if (g_lc.is_host) { - int slot = first_guest_member_slot(); - if (slot >= 0) - g_lc.member_rtt_ms[slot] = ms; - } else { - int slot = local_member_slot(); - if (slot >= 0) - g_lc.member_rtt_ms[slot] = ms; - { - char report[32]; - snprintf(report, sizeof(report), "%d", ms); - (void)psx_lobby_send_signal(PSX_LOBBY_SIG_RTT_REPORT, 0, report); - } - } - } - - { - uint64_t now = lobby_mono_ms(); - if (now >= g_lc.rtt_next_ping_ms && rnet_rtt_probe_peer_known(g_rtt_probe)) { - (void)rnet_rtt_probe_ping(g_rtt_probe); - g_lc.rtt_next_ping_ms = now + 2500ull; - } - } -} - void psx_lobby_pump(void) { char buf[4096]; @@ -2058,7 +874,7 @@ void psx_lobby_pump(void) if (!g_lc.handshake_done) { n = recv(g_lc.fd, buf, sizeof(buf), 0); if (n < 0) { - if (socket_would_block()) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { return; } psx_lobby_disconnect(); @@ -2101,43 +917,44 @@ void psx_lobby_pump(void) } flush_pending(); drain_ws_pending(); - /* Non-blocking recv into ws_pending + frame parse. Avoid MSG_PEEK / - * MSG_WAITALL / temporary blocking — those break on Windows MinGW when - * the socket stays O_NONBLOCK (list/create never see welcome/created). */ for (;;) { - size_t available = sizeof(g_lc.ws_pending) - g_lc.ws_pending_len; - if (available == 0) { - psx_lobby_disconnect(); - return; - } - n = recv(g_lc.fd, - (char *)g_lc.ws_pending + g_lc.ws_pending_len, - (int)available, 0); - if (n < 0) { - if (socket_would_block()) { - break; + int closed = 0; + int fl; +#if !defined(_WIN32) + fl = fcntl(g_lc.fd, F_GETFL, 0); + /* Non-blocking peek: if no data, EAGAIN from first recv inside read */ +#endif + { + uint8_t peek[1]; + n = recv(g_lc.fd, (char *)peek, 1, MSG_PEEK); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + break; + } + psx_lobby_disconnect(); + return; + } + if (n == 0) { + psx_lobby_disconnect(); + return; } - psx_lobby_disconnect(); - return; } - if (n == 0) { +#if !defined(_WIN32) + fcntl(g_lc.fd, F_SETFL, fl & ~O_NONBLOCK); +#endif + n = rnet_ws_read_text(g_lc.fd, buf, sizeof(buf), &closed); +#if !defined(_WIN32) + fcntl(g_lc.fd, F_SETFL, fl | O_NONBLOCK); +#endif + if (closed || n < 0) { psx_lobby_disconnect(); return; } - g_lc.ws_pending_len += (size_t)n; - drain_ws_pending(); - if (!psx_lobby_connected()) { + if (n == 0) { break; } + handle_server_json(buf); } - /* Host STUN advertise (may briefly close the waiting-room RTT sock). */ - lobby_host_advertise_tick(); - /* Local UDP broadcast: host announce / guest cache for list RTT. */ - lobby_lan_beacon_tick(); - /* Peer-path UDP latency for the lobby seat table. */ - lobby_rtt_tick(); - /* One-shot list latency after Refresh / lobby_list. */ - lobby_list_rtt_tick(); } void psx_lobby_set_game_identity(const char *game_name, const char *game_version) @@ -2165,7 +982,6 @@ const char *psx_lobby_game_version(void) void psx_lobby_request_list(void) { - g_list_rtt_on_next_list = 1; queue_list_request(); flush_pending(); } @@ -2212,9 +1028,9 @@ int psx_lobby_create(const char *name, const char *game_name, const char *game_v } n = snprintf(msg, sizeof(msg), "{\"op\":\"create\",\"name\":\"%s\",\"game_name\":\"%s\",\"game_version\":\"%s\"," - "\"password\":\"%s\",\"max_slots\":%d,\"host_bind\":\"%s\",\"display_name\":\"%s\"%s}", + "\"password\":\"%s\",\"max_slots\":2,\"host_bind\":\"%s\",\"display_name\":\"%s\"%s}", name && name[0] ? name : "Lobby", gn, gv, - password ? password : "", g_lobby_max_slots, g_lc.my_bind, + password ? password : "", g_lc.my_bind, g_lc.display_name[0] ? g_lc.display_name : "Host", caps_json); if (n < 0 || (size_t)n >= sizeof(msg)) return -1; queue_send(msg); @@ -2250,53 +1066,13 @@ int psx_lobby_leave(void) { queue_send("{\"op\":\"leave\"}"); flush_pending(); - lobby_rtt_close(); - lobby_host_advertise_reset(); g_lc.in_lobby = 0; g_lc.is_host = 0; - g_lc.host_player_id[0] = '\0'; g_lc.member_count = 0; g_lc.local_ready = 0; g_lc.all_ready = 0; g_lc.launch_pending = 0; - g_lc.ice_signal_accept = 0; - psx_lobby_clear_signals(); match_caps_clear(&g_lc.match_caps); - member_rtt_clear(); - return 0; -} - -int psx_lobby_kick(int slot) -{ - char msg[64]; - if (!psx_lobby_connected() || !g_lc.in_lobby || !g_lc.is_host) { - return -1; - } - if (slot < 0 || slot >= PSX_LOBBY_MAX_MEMBERS) { - return -1; - } - snprintf(msg, sizeof(msg), "{\"op\":\"kick\",\"slot\":%d}", slot); - queue_send(msg); - flush_pending(); - return 0; -} - -int psx_lobby_move_member(int from_slot, int to_slot) -{ - char msg[96]; - if (!psx_lobby_connected() || !g_lc.in_lobby || !g_lc.is_host) { - return -1; - } - if (from_slot < 0 || from_slot >= PSX_LOBBY_MAX_MEMBERS || - to_slot < 0 || to_slot >= PSX_LOBBY_MAX_MEMBERS || - from_slot == to_slot) { - return -1; - } - snprintf(msg, sizeof(msg), - "{\"op\":\"move\",\"from_slot\":%d,\"to_slot\":%d}", - from_slot, to_slot); - queue_send(msg); - flush_pending(); return 0; } @@ -2310,11 +1086,6 @@ int psx_lobby_is_host(void) return g_lc.is_host; } -const char *psx_lobby_host_player_id(void) -{ - return g_lc.host_player_id; -} - const PsxLobbyJoinInfo *psx_lobby_join_info(void) { return &g_lc.join; @@ -2357,30 +1128,6 @@ int psx_lobby_member_get(int index, PsxLobbyMember *out) return 1; } -int psx_lobby_member_latency_ms(int slot) -{ - if (slot < 0 || slot >= PSX_LOBBY_MAX_MEMBERS) - return -1; - if (g_lc.host_player_id[0]) { - int i; - for (i = 0; i < g_lc.member_count; ++i) { - if (g_lc.members[i].slot == slot && - strcmp(g_lc.members[i].player_id, g_lc.host_player_id) == 0) - return -1; /* host row */ - } - } - return g_lc.member_rtt_ms[slot]; -} - -int psx_lobby_member_is_host(const PsxLobbyMember *member) -{ - const char *host_id; - if (!member || !member->player_id[0]) - return 0; - host_id = psx_lobby_host_player_id(); - return host_id && host_id[0] && strcmp(member->player_id, host_id) == 0; -} - int psx_lobby_local_ready(void) { return g_lc.local_ready; @@ -2433,72 +1180,4 @@ void psx_lobby_clear_launch_pending(void) g_lc.launch_pending = 0; } -int psx_lobby_send_signal(int type, int flag, const char *text) -{ - char esc[4096]; - char msg[4608]; - const char *lid; - if (!psx_lobby_connected() || !g_lc.in_lobby) { - return -1; - } - lid = g_lc.join.lobby_id[0] ? g_lc.join.lobby_id : ""; - json_escape(text ? text : "", esc, sizeof(esc)); - snprintf(msg, sizeof(msg), - "{\"op\":\"signal\",\"lobby_id\":\"%s\",\"to_player_id\":\"\"," - "\"type\":%d,\"flag\":%d,\"text\":\"%s\"}", - lid, type, flag, esc); - /* Write immediately — ICE candidates arrive in bursts larger than pending_tx. */ - if (g_lc.handshake_done && g_lc.fd >= 0) { - if (rnet_ws_write_text(g_lc.fd, msg, 1) < 0) - return -1; - return 0; - } - queue_send(msg); - return 0; -} - -int psx_lobby_poll_signal(int *type, int *flag, char *text, size_t text_cap) -{ - int i; - if (g_lc.sig_count <= 0) { - return 0; - } - i = g_lc.sig_head; - if (type) *type = g_lc.sig_q[i].type; - if (flag) *flag = g_lc.sig_q[i].flag; - if (text && text_cap) { - strncpy(text, g_lc.sig_q[i].text, text_cap - 1); - text[text_cap - 1] = '\0'; - } - g_lc.sig_head = (g_lc.sig_head + 1) % (int)(sizeof(g_lc.sig_q) / sizeof(g_lc.sig_q[0])); - g_lc.sig_count--; - return 1; -} - -int psx_lobby_request_turn_credentials(void) -{ - if (!psx_lobby_connected()) - return -1; - if (g_lc.turn.valid && g_lc.turn_received_at > 0 && g_lc.turn.ttl_secs > 0) { - time_t now = time(NULL); - if (now >= g_lc.turn_received_at && - (uint32_t)(now - g_lc.turn_received_at) + 60u < g_lc.turn.ttl_secs) { - return 0; /* still fresh (60s skew margin) */ - } - } - return queue_turn_credentials_request(); -} - -const PsxLobbyTurnCredentials *psx_lobby_turn_credentials(void) -{ - if (g_lc.turn.valid && g_lc.turn_received_at > 0 && g_lc.turn.ttl_secs > 0) { - time_t now = time(NULL); - if (now < g_lc.turn_received_at || - (uint32_t)(now - g_lc.turn_received_at) >= g_lc.turn.ttl_secs) { - clear_turn_credentials(); - } - } - return &g_lc.turn; -} - #endif /* PSX_HAS_LOBBY_CLIENT */ diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index f2910cc8..e9089fbe 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -11,62 +11,23 @@ #include #include #include -#include - -#if defined(_WIN32) -#include -#include -#else -#include -#include -#endif #if defined(__linux__) #include +#include #endif #if defined(PSX_HAS_RECOMP_NET) #include "recomp_net/recomp_net.h" -#if defined(PSX_HAS_LOBBY_CLIENT) -#include "psx_lobby_client.h" -#endif #endif -#ifndef PSX_MAX_PLAYERS -#define PSX_MAX_PLAYERS 2 -#endif - -/* Session pad count mirrored for release_pads (available without recomp-net). */ -static int g_np_slot_count = 2; - -/* Persists across shutdown so starvation dumps still see last session topology. */ -static char g_np_diag_arch[24] = "off"; -static int g_np_diag_max_players = 0; -static int g_np_diag_player_count = 0; -static int g_np_diag_configured = 0; - -int psx_netplay_diag_snapshot(char *arch_out, size_t arch_cap, - int *max_players_out, int *player_count_out) -{ - if (arch_out && arch_cap) - snprintf(arch_out, arch_cap, "%s", g_np_diag_arch); - if (max_players_out) *max_players_out = g_np_diag_max_players; - if (player_count_out) *player_count_out = g_np_diag_player_count; - return g_np_diag_configured; -} - void psx_netplay_config_defaults(PsxNetplayConfig *cfg) { if (!cfg) return; memset(cfg, 0, sizeof(*cfg)); cfg->local_slot = 0; - cfg->slot_count = 2; - cfg->player_count = 0; cfg->input_player = -1; cfg->input_delay = 2; - cfg->force_input_relay = 0; - cfg->force_turn = 0; - cfg->transport = 0; cfg->session_id = 1; strncpy(cfg->bind_hostport, "0.0.0.0:7777", sizeof(cfg->bind_hostport) - 1); cfg->peer_hostport[0] = '\0'; @@ -87,8 +48,6 @@ void psx_netplay_apply_env(PsxNetplayConfig *cfg) if (v && v[0] && v[0] != '0') cfg->enabled = 1; v = getenv("PSX_NET_SLOT"); if (v && v[0]) cfg->local_slot = (int)strtol(v, NULL, 10); - v = getenv("PSX_NET_SLOTS"); - if (v && v[0]) cfg->slot_count = (int)strtol(v, NULL, 10); v = getenv("PSX_NET_INPUT_PLAYER"); if (v && v[0]) cfg->input_player = (int)strtol(v, NULL, 10); v = getenv("PSX_NET_DELAY"); @@ -104,16 +63,6 @@ void psx_netplay_apply_env(PsxNetplayConfig *cfg) strncpy(cfg->peer_hostport, v, sizeof(cfg->peer_hostport) - 1); cfg->peer_hostport[sizeof(cfg->peer_hostport) - 1] = '\0'; } - v = getenv("PSX_NET_TRANSPORT"); - if (v && v[0]) { - if (strcmp(v, "ice") == 0 || strcmp(v, "ICE") == 0) - cfg->transport = 1; - else if (strcmp(v, "lan") == 0 || strcmp(v, "LAN") == 0) - cfg->transport = 2; - } - v = getenv("PSX_NET_FORCE_TURN"); - if (v && v[0] && v[0] != '0') - cfg->force_turn = 1; } void psx_netplay_normalize_pad(PsxNetPad *pad) @@ -134,40 +83,28 @@ static void force_session_pads_connected(int slot_count) { int i; if (slot_count < 2) slot_count = 2; - if (slot_count > PSX_MAX_PLAYERS) slot_count = PSX_MAX_PLAYERS; - if (slot_count >= 3) - sio_set_multitap(1); - else - sio_set_multitap(0); + if (slot_count > 2) slot_count = 2; for (i = 0; i < slot_count; ++i) { sio_connect_pad(i); - /* Multitap taps are plain digital (sio clamps); lone port pad may be DS. */ - sio_set_pad_config_capable(i, sio_pad_on_multitap(i) ? 0 : 1); + sio_set_pad_config_capable(i, 1); } } void psx_netplay_release_pads(void) { - int i; - int n = g_np_slot_count; - if (n < 2) n = 2; - if (n > PSX_MAX_PLAYERS) n = PSX_MAX_PLAYERS; - force_session_pads_connected(n); - for (i = 0; i < n; ++i) { - sio_set_pad_state_slot(i, 0xFFFFu); - sio_set_pad_sticks(i, 0x80, 0x80, 0x80, 0x80); - /* Tap slots stay digital; standalone port may request DualShock. */ - sio_request_pad_type(i, sio_pad_on_multitap(i) ? 0 : 1); - } + force_session_pads_connected(2); + sio_set_pad_state_slot(0, 0xFFFFu); + sio_set_pad_state_slot(1, 0xFFFFu); + sio_set_pad_sticks(0, 0x80, 0x80, 0x80, 0x80); + sio_set_pad_sticks(1, 0x80, 0x80, 0x80, 0x80); + sio_request_pad_type(0, 1); + sio_request_pad_type(1, 1); } #if !defined(PSX_HAS_RECOMP_NET) int psx_netplay_active(void) { return 0; } int psx_netplay_is_running(void) { return 0; } -const char *psx_netplay_transport_name(void) { return "none"; } -int psx_netplay_ice_failed(void) { return 0; } -void psx_netplay_diag_tick(void) {} int psx_netplay_local_slot(void) { return -1; } int psx_netplay_input_player(void) { return 0; } uint32_t psx_netplay_sim_tick(void) { return 0; } @@ -196,30 +133,13 @@ int psx_netplay_is_host(void) { return 0; } int psx_netplay_request_save(int slot) { (void)slot; return 0; } int psx_netplay_request_load(int slot) { (void)slot; return 0; } int psx_netplay_in_load_barrier(void) { return 0; } -int psx_netplay_consume_load_apply_failed(void) { return 0; } -void psx_netplay_pump(void) {} int psx_netplay_poll_admit(void) { return 1; } void psx_netplay_finish_frame(void) {} -int psx_netplay_remote_lead(void) { return 0; } -int psx_netplay_input_delay(void) { return 2; } -int psx_netplay_catchup_budget(void) { return 0; } -void psx_netplay_catchup_consume_frame(void) {} void psx_netplay_wait_recv(int timeout_ms) { (void)timeout_ms; } -void psx_netplay_admit_wait_info(char *stall_out, size_t stall_cap, - uint32_t *sim_tick_out, int *lead_out) -{ - if (stall_out && stall_cap) { - stall_out[0] = '\0'; - if (stall_cap > 1) - strncpy(stall_out, "off", stall_cap - 1); - } - if (sim_tick_out) *sim_tick_out = 0; - if (lead_out) *lead_out = 0; -} #else /* PSX_HAS_RECOMP_NET */ -#define NP_SANDBOX_FALLBACK "saves/netplay" +#define NP_SANDBOX_DIR "saves/netplay" #define NP_MC_BLOB_BYTES (4u + (size_t)MEMCARD_SIZE * 2u) /* LOAD probe size==0 + this crc = post-load ready rendezvous (not SAVE coord). */ #define NP_LOAD_READY_CRC 0x4C4F4144u /* 'LOAD' */ @@ -244,7 +164,7 @@ typedef struct { int active; int slot_count; int local_slot; - int input_player; /* resolved host PlayerInput index */ + int input_player; /* resolved 0/1 */ int needs_advance; int latched_for_tick; /* 1 if staged pad frozen for current sim_tick */ uint32_t latched_sim_tick; @@ -261,72 +181,16 @@ typedef struct { int mc_sync_done; int mc_sync_sent; int local_save_staged; - int local_save_acked; /* guest: coord reply already sent */ - uint32_t save_target_tick; /* both peers save during this sim_tick */ int load_applied_local; int load_ready_replied; /* READY exchanged; synced; stay LOAD_READY until admit */ int load_sync_done; /* hard_resync+prime once at mutual ready */ - int load_apply_failed; /* sticky: staged apply rejected — soft-exit */ - /* Transport / ICE / diag (MotK online path). */ - int use_ice; - int ice_has_turn; - int force_input_relay; - int is_host; - int input_delay; - uint32_t session_id; - uint32_t frames_finished; - uint32_t diag_session; - unsigned ice_stun_port; - unsigned ice_turn_port; - char ice_stun_host[128]; - char ice_turn_host[128]; - char ice_turn_user[192]; - char ice_turn_pass[128]; - char ice_bind_addr[64]; - char bind_hostport[64]; - char peer_hostport[64]; - char match_mode[32]; - char lobby_server[256]; - char lobby_id[64]; } NetplayState; static NetplayState g_np; -static FILE *g_diag_file; -static uint32_t g_diag_file_session; -static int g_diag_summary_written; -static uint32_t g_diag_last_write_ms; -static int g_diag_mkdir_done; - -static void np_sleep_ms(unsigned ms) -{ -#if defined(_WIN32) - Sleep(ms); -#else - usleep(ms * 1000u); -#endif -} - -static uint32_t np_mono_ms(void) -{ -#if defined(CLOCK_MONOTONIC) - struct timespec ts; - if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) - return (uint32_t)((uint64_t)ts.tv_sec * 1000ull + - (uint64_t)ts.tv_nsec / 1000000ull); -#endif -#if defined(_WIN32) - return (uint32_t)GetTickCount64(); -#else - return (uint32_t)((uint64_t)time(NULL) * 1000ull); -#endif -} - static void np_enter_load_ready(int slot); static void np_commit_load_sync(void); static void np_begin_load_apply(int slot); -static void np_starv_reset(void); -static void np_maybe_stage_target_save(void); static int np_file_crc(const uint8_t *data, size_t size, uint32_t *crc_out) { @@ -411,7 +275,6 @@ static void np_enter_guest_sandbox(void) const char *p0 = NULL; const char *p1 = NULL; uint32_t bios = 0, entry = 0; - char sandbox[560]; savestate_get_integrity(&bios, &entry); g_np.bios_checksum = bios; @@ -423,24 +286,9 @@ static void np_enter_guest_sandbox(void) if (p0) strncpy(g_np.personal_mc0, p0, sizeof(g_np.personal_mc0) - 1); if (p1) strncpy(g_np.personal_mc1, p1, sizeof(g_np.personal_mc1) - 1); - /* Prefer /netplay (absolute, next to the binary) so CWD does - * not matter. Relative "saves/netplay" only as a last-resort fallback. */ - if (g_np.personal_save_dir[0]) { - size_t n = strlen(g_np.personal_save_dir); - while (n > 0 && (g_np.personal_save_dir[n - 1] == '/' || - g_np.personal_save_dir[n - 1] == '\\')) { - g_np.personal_save_dir[--n] = '\0'; - } - snprintf(sandbox, sizeof(sandbox), "%s/netplay", g_np.personal_save_dir); - } else { - snprintf(sandbox, sizeof(sandbox), "%s", NP_SANDBOX_FALLBACK); - } - - savestate_configure(sandbox, bios, entry); - (void)memcard_rebind_dir(sandbox); + savestate_configure(NP_SANDBOX_DIR, bios, entry); + (void)memcard_rebind_dir(NP_SANDBOX_DIR); g_np.guest_sandbox = 1; - printf("psxrecomp: netplay guest sandbox -> %s\n", sandbox); - fflush(stdout); } static void np_leave_guest_sandbox(void) @@ -519,27 +367,23 @@ static void np_apply_ready_state(void) return; } - /* LOAD transfer (hash miss): guest stages the wire blob in memory (no disk - * dependency — relative sandbox/CWD issues used to fail write_slot here). - * Both peers request apply so host cannot restore before guest has bytes. */ + /* LOAD transfer (hash miss): guest writes sandbox, both enter apply barrier. */ if (g_np.local_slot != 0) { - if (!savestate_request_load_blob_protocol(data, size)) { - printf("psxrecomp: netplay guest load slot=%u — blob stage failed " - "(%zu bytes, sandbox='%s')\n", - (unsigned)slot, size, savestate_dir()); - fflush(stdout); + if (!savestate_write_slot((int)slot, data, size)) { rnet_session_state_finish(g_np.session, 0); g_np.xfer = NP_XFER_NONE; return; } - /* Best-effort mirror to sandbox for hash-probe hits on rematch. */ - if (!savestate_write_slot((int)slot, data, size)) { - printf("psxrecomp: netplay guest load slot=%u — sandbox mirror " - "failed (in-memory apply continues)\n", - (unsigned)slot); - fflush(stdout); + { + uint32_t got_sz = 0, got_crc = 0; + if (!np_slot_crc((int)slot, &got_sz, &got_crc) || + got_sz != (uint32_t)size || + got_crc != rnet_checksum((const rnet_u8 *)data, size)) { + rnet_session_state_finish(g_np.session, 0); + g_np.xfer = NP_XFER_NONE; + return; + } } - } else { (void)savestate_request_load_protocol((int)slot); } rnet_session_state_finish(g_np.session, 0); @@ -587,30 +431,24 @@ static void np_guest_handle_probe(void) } if (size == 0) { - /* SAVE coord: crc carries the shared target sim_tick. Both peers - * stage the write when sim reaches that tick (see - * np_maybe_stage_target_save) so CRCs match and skip transfer. */ - if (g_np.xfer != NP_XFER_SAVE_COORD) { - g_np.xfer = NP_XFER_SAVE_COORD; - g_np.xfer_slot = (int)slot; - g_np.save_target_tick = crc; - g_np.local_save_staged = 0; - g_np.local_save_acked = 0; - printf("psxrecomp: netplay guest save slot=%u — armed target " - "sim=%u\n", - (unsigned)slot, (unsigned)crc); - fflush(stdout); + /* SAVE coordinate local write (admit is not stalled for size==0). */ + if (!g_np.local_save_staged) { + if (savestate_request_save_protocol((int)slot)) { + g_np.local_save_staged = 1; + printf("psxrecomp: netplay guest save slot=%u — writing sandbox…\n", + (unsigned)slot); + fflush(stdout); + } else { + (void)rnet_session_state_probe_reply(g_np.session, 0); + return; + } } if (savestate_pending()) return; - if (!g_np.local_save_staged || !savestate_slot_exists((int)slot)) return; - if (!g_np.local_save_acked) { - g_np.local_save_acked = 1; - (void)rnet_session_state_probe_reply(g_np.session, 1); - printf("psxrecomp: netplay guest save slot=%u — local write done " - "@ target sim (frozen until hash probe)\n", - (unsigned)slot); - fflush(stdout); - } + if (!savestate_slot_exists((int)slot)) return; + g_np.local_save_staged = 0; + (void)rnet_session_state_probe_reply(g_np.session, 1); + printf("psxrecomp: netplay guest save slot=%u — local write done\n", (unsigned)slot); + fflush(stdout); return; } @@ -624,53 +462,18 @@ static void np_guest_handle_probe(void) { uint32_t local_sz = 0, local_crc = 0; - char reason[192]; match = np_slot_crc((int)slot, &local_sz, &local_crc) && local_sz == size && local_crc == crc; - /* CRC match of a stale .pst (wrong codegen) is not loadable — ask the - * host to transfer. Host also refuses probe start if its own slot is - * stale, so this mainly covers guest-sandbox drift. */ - if (match && op == RNET_STATE_OP_LOAD && - !savestate_slot_compatible((int)slot, reason, sizeof(reason))) { - printf("psxrecomp: netplay guest load slot=%u — hash matched but " - "unloadable (%s); requesting transfer\n", - (unsigned)slot, reason[0] ? reason : "incompatible"); - fflush(stdout); - match = 0; - } (void)rnet_session_state_probe_reply(g_np.session, match); - if (op == RNET_STATE_OP_SAVE) { - if (match) { - g_np.xfer = NP_XFER_NONE; - printf("psxrecomp: netplay guest save slot=%u — hashes match, " - "skip transfer\n", + if (match && op == RNET_STATE_OP_LOAD) { + if (g_np.xfer != NP_XFER_LOAD_APPLYING && g_np.xfer != NP_XFER_LOAD_READY) { + (void)savestate_request_load_protocol((int)slot); + np_begin_load_apply((int)slot); + printf("psxrecomp: netplay guest load slot=%u — hashes match, applying…\n", (unsigned)slot); fflush(stdout); } else { - /* Host will chunk the authoritative .pst — stay parked. */ - g_np.xfer = NP_XFER_SAVE_SEND; - g_np.xfer_slot = (int)slot; - } - } else if (op == RNET_STATE_OP_LOAD) { - if (match) { - if (g_np.xfer != NP_XFER_LOAD_APPLYING && - g_np.xfer != NP_XFER_LOAD_READY) { - (void)savestate_request_load_protocol((int)slot); - np_begin_load_apply((int)slot); - printf("psxrecomp: netplay guest load slot=%u — hashes match, " - "applying…\n", - (unsigned)slot); - fflush(stdout); - } - } else { - /* Must mark LOAD_SEND or guest keeps the 20s admit timeout and - * BYEs the host mid-TURN transfer. */ - g_np.xfer = NP_XFER_LOAD_SEND; - g_np.xfer_slot = (int)slot; - printf("psxrecomp: netplay guest load slot=%u — hash miss, " - "waiting for transfer…\n", - (unsigned)slot); - fflush(stdout); + /* Retransmit of hash probe — already staging/applying. */ } } } @@ -711,11 +514,8 @@ static void np_host_drive_xfer(void) return; case NP_XFER_SAVE_COORD: - /* Host + guest both stage at save_target_tick; wait for local write - * and guest ACK before hashing. */ if (savestate_pending()) return; - if (!g_np.local_save_staged || !savestate_slot_exists(g_np.xfer_slot)) - return; + if (!savestate_slot_exists(g_np.xfer_slot)) return; if (!rnet_session_state_probe_take_reply(g_np.session, &match)) return; rnet_session_state_probe_finish(g_np.session); @@ -779,10 +579,7 @@ static void np_host_drive_xfer(void) g_np.xfer = NP_XFER_NONE; return; } - /* Do not stage savestate_request_load here — host would apply during - * SEND, enter LOAD_READY, and suppress INPUT before the guest can - * admit frames for its own savestate_poll (deadlock). Both peers - * stage in np_apply_ready_state when the transfer completes. */ + (void)savestate_request_load_protocol(g_np.xfer_slot); g_np.load_applied_local = 0; g_np.load_sync_done = 0; if (rnet_session_state_begin(g_np.session, RNET_STATE_OP_LOAD, (rnet_u8)g_np.xfer_slot, buf, @@ -834,53 +631,22 @@ static void np_host_drive_xfer(void) static void np_prime_after_hard_resync(void) { uint8_t bytes[PSX_NETPLAY_PAD_BYTES]; - PsxNetPad pad; - - /* Prime delay prefix with the current local hold (not forced neutral) so the - * first D play frames continue what the player is already pressing. Each - * peer only primes its own slot — lockstep stays valid. Tip latency for - * *changes* remains D; we just avoid a post-load dead zone of released pads. */ - memset(&pad, 0, sizeof(pad)); - pad.buttons = 0xFFFFu; - pad.lx = pad.ly = pad.rx = pad.ry = 0x80u; - pad.analog = 1; - pad.connected = 1; - if (g_np.staged_valid) - pad = g_np.staged; - pad.connected = 1; - psx_netplay_normalize_pad(&pad); - - bytes[0] = (uint8_t)(pad.buttons & 0xFFu); - bytes[1] = (uint8_t)((pad.buttons >> 8) & 0xFFu); - bytes[2] = pad.lx; - bytes[3] = pad.ly; - bytes[4] = pad.rx; - bytes[5] = pad.ry; - bytes[6] = pad.analog ? 1u : 0u; + /* Released digital + centered sticks — delay tip only; real pads resume after. */ + bytes[0] = 0xFFu; + bytes[1] = 0xFFu; + bytes[2] = bytes[3] = bytes[4] = bytes[5] = 0x80u; + bytes[6] = 1u; bytes[7] = 1u; rnet_session_prime_delay_inputs(g_np.session, bytes, (rnet_u16)PSX_NETPLAY_PAD_BYTES); - - /* Keep staged matching the prime so the first tip sample is not a sudden - * release while [0..D) still holds the live pad. */ - g_np.staged = pad; - g_np.staged_valid = 1; } /* Stage restore. Keep INPUT flowing so try_admit can still run guest cycles - * for savestate_poll — suppress only at mutual ready (np_commit_load_sync). - * Ready probe must also leave INPUT unstalled (recomp-net size==0 LOAD). */ + * for savestate_poll — suppress starts at enter_load_ready / hard_resync. */ static void np_begin_load_apply(int slot) { - /* Transfer admit failures (state_xfer) often latch starvation; lead can sit - * at D-1 after ICE xfer and would block the only frame savestate_poll needs. */ - np_starv_reset(); g_np.xfer = NP_XFER_LOAD_APPLYING; g_np.load_applied_local = 0; g_np.load_sync_done = 0; - g_np.load_ready_replied = 0; - g_np.needs_advance = 0; - g_np.latched_for_tick = 0; - g_np.staged_valid = 0; g_np.xfer_slot = slot; } @@ -889,22 +655,18 @@ static void np_commit_load_sync(void) { if (g_np.load_sync_done || !g_np.session) return; - /* Suppress empty tips only for the hard_resync→prime window. */ - rnet_session_set_input_send_suppress(g_np.session, 1); rnet_session_hard_resync(g_np.session); np_prime_after_hard_resync(); /* clears suppress + emits fresh tip */ g_np.load_sync_done = 1; g_np.needs_advance = 0; g_np.latched_for_tick = 0; - /* staged_valid left set by prime — tip must match delay-prefix hold. */ + g_np.staged_valid = 0; } static void np_enter_load_ready(int slot) { /* Do not hard_resync/prime here — the later-applying peer would clear the - * earlier peer's tip and stall resume. Sync runs at mutual ready. - * Do not suppress INPUT here either: the first peer to finish apply must - * keep sending pads so the other can still admit frames for savestate_poll. */ + * earlier peer's tip and stall resume. Sync runs at mutual ready. */ g_np.load_applied_local = 1; g_np.load_ready_replied = 0; g_np.load_sync_done = 0; @@ -913,6 +675,9 @@ static void np_enter_load_ready(int slot) g_np.staged_valid = 0; g_np.xfer = NP_XFER_LOAD_READY; g_np.xfer_slot = slot; + /* Stop pre-resync tips until hard_resync + prime (avoids tick%128 clobber). */ + if (g_np.session) + rnet_session_set_input_send_suppress(g_np.session, 1); } /* After both peers stage a load: run until restore completes, then rendezvous. @@ -923,23 +688,6 @@ static void np_drive_load_barrier(void) return; if (savestate_pending()) return; - if (savestate_take_load_failed()) { - /* Stale/mismatched .pst: do not sit in load_apply_done forever. */ - printf("psxrecomp: netplay load slot=%d — apply failed " - "(incompatible or missing .pst) — aborting barrier\n", - g_np.xfer_slot); - fflush(stdout); - if (g_np.session) - rnet_session_state_finish(g_np.session, 0); - g_np.xfer = NP_XFER_NONE; - g_np.load_applied_local = 0; - g_np.load_ready_replied = 0; - g_np.load_sync_done = 0; - g_np.load_apply_failed = 1; - if (g_np.session) - rnet_session_set_input_send_suppress(g_np.session, 0); - return; - } if (!g_np.load_applied_local && !savestate_take_load_completed()) return; @@ -1023,16 +771,12 @@ static void decode_pad(const RNetInputSample *in, PsxNetPad *pad) static void apply_pad_slot(int slot, const PsxNetPad *pad) { - if (slot < 0 || slot >= g_np.slot_count || slot >= PSX_MAX_PLAYERS || !pad) return; - const int on_tap = sio_pad_on_multitap(slot); + if (slot < 0 || slot > 1 || !pad) return; sio_set_pad_connected(slot, 1); - sio_set_pad_config_capable(slot, on_tap ? 0 : 1); + sio_set_pad_config_capable(slot, 1); sio_set_pad_state_slot(slot, pad->buttons); - if (on_tap) - sio_set_pad_sticks(slot, 0x80, 0x80, 0x80, 0x80); - else - sio_set_pad_sticks(slot, pad->lx, pad->ly, pad->rx, pad->ry); - sio_request_pad_type(slot, (!on_tap && pad->analog) ? 1 : 0); + sio_set_pad_sticks(slot, pad->lx, pad->ly, pad->rx, pad->ry); + sio_request_pad_type(slot, pad->analog ? 1 : 0); } static void host_sample_local(rnet_u32 tick, RNetInputSample *out, void *ctx) @@ -1052,15 +796,11 @@ static void host_sample_local(rnet_u32 tick, RNetInputSample *out, void *ctx) static void host_publish(rnet_u32 tick, const RNetInputSample *by_slot, int slots, void *ctx) { int i; - int n; (void)tick; (void)ctx; if (!by_slot || slots <= 0) return; - n = g_np.slot_count; - if (n > slots) n = slots; - if (n > PSX_MAX_PLAYERS) n = PSX_MAX_PLAYERS; - force_session_pads_connected(n); - for (i = 0; i < n; ++i) { + force_session_pads_connected(slots); + for (i = 0; i < slots && i < 2; ++i) { PsxNetPad pad; decode_pad(&by_slot[i], &pad); apply_pad_slot(i, &pad); @@ -1077,23 +817,6 @@ int psx_netplay_is_running(void) return psx_netplay_active() && rnet_session_is_running(g_np.session); } -const char *psx_netplay_transport_name(void) -{ - if (!psx_netplay_active()) return "none"; - return g_np.use_ice ? "ice" : "lan"; -} - -int psx_netplay_ice_failed(void) -{ -#if defined(RNET_ENABLE_ICE) - if (!psx_netplay_active() || !g_np.use_ice) - return 0; - return rnet_session_ice_state(g_np.session) == RNET_ICE_STATE_FAILED; -#else - return 0; -#endif -} - int psx_netplay_local_slot(void) { return psx_netplay_active() ? g_np.local_slot : -1; @@ -1154,28 +877,10 @@ int psx_netplay_input_desync(uint32_t *tick, uint32_t *local_hash, uint32_t *rem int psx_netplay_peer_disconnected(uint32_t timeout_ms) { if (!psx_netplay_active()) return 0; - /* timeout_ms == 0: BYE / peer_gone only (no silence timeout). Used during - * load barriers where INPUT is suppressed for seconds. */ + if (timeout_ms == 0) timeout_ms = 1500u; return rnet_session_peer_disconnected(g_np.session, (rnet_u64)timeout_ms); } -static void np_diag_capture(const PsxNetplayConfig *cfg, int slots) -{ - const char *arch = "p2p"; - int players; - if (!cfg) return; - if (cfg->force_input_relay) - arch = "server_relay"; - else if (slots >= 3) - arch = "host_relay"; - players = cfg->player_count > 0 ? cfg->player_count : slots; - if (players < 1) players = slots; - snprintf(g_np_diag_arch, sizeof(g_np_diag_arch), "%s", arch); - g_np_diag_max_players = slots; - g_np_diag_player_count = players; - g_np_diag_configured = 1; -} - #if defined(__linux__) static int peer_is_loopback(const char *peer_hostport) { @@ -1211,336 +916,40 @@ static void pin_localhost_peer_cpus(int local_slot) } #endif -#if defined(PSX_HAS_LOBBY_CLIENT) && defined(RNET_ENABLE_ICE) -static void host_on_signal(const RNetSignal *msg, void *ctx) -{ - (void)ctx; - if (!msg) return; - (void)psx_lobby_send_signal((int)msg->type, (int)msg->flag, msg->text); -} - -static void drain_lobby_signals(void) -{ - int type = 0, flag = 0; - char text[2048]; - if (!g_np.session) return; - while (psx_lobby_poll_signal(&type, &flag, text, sizeof(text))) { - RNetSignal sig; - memset(&sig, 0, sizeof(sig)); - /* Peers emit LOCAL_*; push_signal expects REMOTE_* for SDP/candidates. */ - if (type == (int)RNET_SIGNAL_LOCAL_SDP) - type = (int)RNET_SIGNAL_REMOTE_SDP; - else if (type == (int)RNET_SIGNAL_LOCAL_CANDIDATE) - type = (int)RNET_SIGNAL_REMOTE_CANDIDATE; - sig.type = (RNetSignalType)type; - sig.flag = (rnet_u8)(flag & 0xFF); - strncpy(sig.text, text, sizeof(sig.text) - 1); - rnet_session_push_signal(g_np.session, &sig); - } -} -#else -static void drain_lobby_signals(void) {} -#endif - -static int resolve_use_ice(const PsxNetplayConfig *cfg) -{ - int in_motk_room = 0; - - if (cfg->transport == 2) return 0; /* force LAN */ -#if defined(PSX_HAS_LOBBY_CLIENT) - in_motk_room = psx_lobby_connected() && psx_lobby_in_lobby(); -#endif - - /* Server UDP pad relay: dial relay_endpoint with LAN transport (not ICE). - * MotK previously always preferred ICE and ignored the relay rewrite. */ - if (cfg->force_input_relay) { - if (!cfg->peer_hostport || !cfg->peer_hostport[0]) { - fprintf(stderr, - "psx_netplay: force_input_relay set but peer/relay " - "endpoint empty\n"); - return -1; - } - fprintf(stderr, - "psx_netplay: server input relay — LAN transport to %s\n", - cfg->peer_hostport); - return 0; - } - -#if defined(RNET_ENABLE_ICE) && defined(PSX_HAS_LOBBY_CLIENT) - if (cfg->transport == 1) { - if (!in_motk_room) { - fprintf(stderr, - "psx_netplay: ICE requested but MotK lobby not connected\n"); - return -1; - } - return 1; - } - /* Auto: hosted MotK room always uses ICE. Do not demote to LAN when the - * lobby rewrites 0.0.0.0 binds to a private TCP peer IP (often wrong). - * Direct IP / LAN file lobby (no MotK seat) stays on LAN UDP. */ - if (in_motk_room) - return 1; - return 0; -#else - { - int online_requested = cfg->transport == 1 || - (cfg->transport == 0 && in_motk_room); - if (online_requested) { - fprintf(stderr, - "psx_netplay: hosted lobby requires ICE, but ICE is not " - "available in this build (configure with PSX_NET_ICE=ON / " - "RNET_ENABLE_ICE=ON)\n"); - return -1; - } - } - return 0; -#endif -} - - int psx_netplay_start(const PsxNetplayConfig *cfg) { RNetConfig rcfg; RNetHostVTable host; int in_player; - int slots; - int local; - int use_ice; if (!cfg || !cfg->enabled) return -1; if (g_np.session) psx_netplay_shutdown(); - slots = cfg->slot_count; - if (slots < 2) slots = 2; - if (slots > PSX_MAX_PLAYERS) slots = PSX_MAX_PLAYERS; - if (slots > RNET_MAX_SLOTS) slots = RNET_MAX_SLOTS; - - local = cfg->local_slot; - if (local < 0) local = 0; - if (local >= slots) local = slots - 1; - rnet_config_init_defaults(&rcfg); - rcfg.slot_count = (rnet_u8)slots; - rcfg.local_slot = (rnet_u8)local; - /* Max delay 20 matches RNET_MAX_BUNDLE 21 (neutral prefix + tip). */ - rcfg.input_delay = (rnet_u8)(cfg->input_delay < 0 ? 0 - : (cfg->input_delay > 20 ? 20 : cfg->input_delay)); + rcfg.slot_count = 2; + rcfg.local_slot = (rnet_u8)(cfg->local_slot < 0 ? 0 : (cfg->local_slot > 1 ? 1 : cfg->local_slot)); + rcfg.input_delay = (rnet_u8)(cfg->input_delay < 0 ? 0 : (cfg->input_delay > 16 ? 16 : cfg->input_delay)); rcfg.session_id = cfg->session_id ? cfg->session_id : 1u; - /* Host resolves auto (-1) before start; accept 0..PSX_MAX_PLAYERS-1. */ - in_player = cfg->input_player; - if (in_player < 0 || in_player >= PSX_MAX_PLAYERS) in_player = 0; - - use_ice = resolve_use_ice(cfg); - if (use_ice < 0) - return -4; + /* Host resolves auto (-1) before start; accept only 0/1 here. */ + in_player = (cfg->input_player == 1) ? 1 : 0; memset(&host, 0, sizeof(host)); host.sample_local = host_sample_local; host.publish = host_publish; host.ctx = &g_np; -#if defined(PSX_HAS_LOBBY_CLIENT) && defined(RNET_ENABLE_ICE) - if (use_ice) - host.on_signal = host_on_signal; -#endif g_np.session = rnet_session_create(&rcfg, &host); if (!g_np.session) return -2; - - if (use_ice) { -#if defined(RNET_ENABLE_ICE) - RNetIceConfig ice; - RNetIpv4Address addrs[8]; - int naddr; - const char *env_turn_host = getenv("PSX_NET_TURN_HOST"); - const char *env_turn_user = getenv("PSX_NET_TURN_USER"); - const char *env_turn_pass = getenv("PSX_NET_TURN_PASS"); - const char *env_stun = getenv("PSX_NET_STUN_HOST"); - - g_np.ice_has_turn = 0; - g_np.ice_stun_host[0] = '\0'; - g_np.ice_turn_host[0] = '\0'; - g_np.ice_turn_user[0] = '\0'; - g_np.ice_turn_pass[0] = '\0'; - g_np.ice_bind_addr[0] = '\0'; - - rnet_ice_config_init_defaults(&ice); - ice.controlling = (rcfg.local_slot == 0) ? 1u : 0u; - - naddr = rnet_ipv4_enumerate(addrs, sizeof(addrs) / sizeof(addrs[0])); - if (naddr > 0 && addrs[0].address[0]) { - snprintf(g_np.ice_bind_addr, sizeof(g_np.ice_bind_addr), "%s", - addrs[0].address); - ice.bind_address = g_np.ice_bind_addr; - } - -#if defined(PSX_HAS_LOBBY_CLIENT) - /* Prefer TURN prefetched at WS welcome; re-request and wait if stale. */ - if (psx_lobby_connected()) { - int i; - const PsxLobbyTurnCredentials *tc = psx_lobby_turn_credentials(); - if (!tc || !tc->valid) { - (void)psx_lobby_request_turn_credentials(); - for (i = 0; i < 200; ++i) { /* up to ~2s */ - tc = psx_lobby_turn_credentials(); - if (tc && tc->valid) - break; - psx_lobby_pump(); - np_sleep_ms(10); - } - } - } - { - const PsxLobbyTurnCredentials *tc = psx_lobby_turn_credentials(); - if (tc && tc->valid) { - if (tc->stun_host[0]) { - snprintf(g_np.ice_stun_host, sizeof(g_np.ice_stun_host), - "%s", tc->stun_host); - ice.stun_host = g_np.ice_stun_host; - ice.stun_port = (rnet_u16)(tc->stun_port > 0 ? tc->stun_port - : 3478); - } - snprintf(g_np.ice_turn_host, sizeof(g_np.ice_turn_host), "%s", - tc->turn_host); - snprintf(g_np.ice_turn_user, sizeof(g_np.ice_turn_user), "%s", - tc->username); - snprintf(g_np.ice_turn_pass, sizeof(g_np.ice_turn_pass), "%s", - tc->password); - ice.turn_host = g_np.ice_turn_host; - ice.turn_user = g_np.ice_turn_user; - ice.turn_pass = g_np.ice_turn_pass; - ice.turn_port = (rnet_u16)(tc->turn_port > 0 ? tc->turn_port - : 3478); - g_np.ice_has_turn = 1; - } - } -#endif - if (env_stun && env_stun[0]) { - snprintf(g_np.ice_stun_host, sizeof(g_np.ice_stun_host), "%s", - env_stun); - ice.stun_host = g_np.ice_stun_host; - ice.stun_port = (rnet_u16)env_u("PSX_NET_STUN_PORT", ice.stun_port - ? ice.stun_port - : 3478); - } - if (env_turn_host && env_turn_host[0] && env_turn_user && - env_turn_user[0] && env_turn_pass && env_turn_pass[0]) { - snprintf(g_np.ice_turn_host, sizeof(g_np.ice_turn_host), "%s", - env_turn_host); - snprintf(g_np.ice_turn_user, sizeof(g_np.ice_turn_user), "%s", - env_turn_user); - snprintf(g_np.ice_turn_pass, sizeof(g_np.ice_turn_pass), "%s", - env_turn_pass); - ice.turn_host = g_np.ice_turn_host; - ice.turn_user = g_np.ice_turn_user; - ice.turn_pass = g_np.ice_turn_pass; - ice.turn_port = (rnet_u16)env_u("PSX_NET_TURN_PORT", 3478); - g_np.ice_has_turn = 1; - } - - if (!g_np.ice_stun_host[0] && ice.stun_host && ice.stun_host[0]) { - snprintf(g_np.ice_stun_host, sizeof(g_np.ice_stun_host), "%s", - ice.stun_host); - } - g_np.ice_stun_port = ice.stun_port ? (unsigned)ice.stun_port : 19302u; - g_np.ice_turn_port = ice.turn_port ? (unsigned)ice.turn_port : 0u; - - if (g_np.ice_has_turn) { - fprintf(stderr, - "psx_netplay: ICE stun=%s:%u turn=%s:%u user=%s bind=%s\n", - ice.stun_host ? ice.stun_host : "(default)", - (unsigned)ice.stun_port, - ice.turn_host, (unsigned)ice.turn_port, ice.turn_user, - ice.bind_address ? ice.bind_address : "(any)"); - } else { - const char *allow_stun = getenv("PSX_NET_ALLOW_STUN_ONLY"); - fprintf(stderr, - "psx_netplay: ICE STUN-only (no TURN) stun=%s:%u " - "bind=%s — online MotK requires Coturn " - "(lobby get_turn_credentials or PSX_NET_TURN_*); set " - "PSX_NET_ALLOW_STUN_ONLY=1 to override\n", - ice.stun_host ? ice.stun_host : "(default)", - (unsigned)ice.stun_port, - ice.bind_address ? ice.bind_address : "(any)"); - /* BattleShip-style: refuse WAN ICE without TURN (CGNAT hangs). */ - if (!allow_stun || !allow_stun[0] || allow_stun[0] == '0') { - rnet_session_destroy(g_np.session); - g_np.session = NULL; - return -4; - } - } - - { - /* Online default is Force TURN (match_caps / UI); env overrides. */ - int force_turn = cfg->force_turn ? 1 : 0; - const char *ft = getenv("PSX_NET_FORCE_TURN"); - if (ft && ft[0] && ft[0] != '0') - force_turn = 1; - else if (ft && ft[0] == '0') - force_turn = 0; - if (force_turn && !g_np.ice_has_turn) { - fprintf(stderr, - "psx_netplay: FORCE_TURN requires Coturn credentials " - "(lobby get_turn_credentials or PSX_NET_TURN_*)\n"); - rnet_session_destroy(g_np.session); - g_np.session = NULL; - return -4; - } - if (force_turn) { - ice.force_relay = 1; - fprintf(stderr, - "psx_netplay: FORCE_TURN — ICE will use relay-only " - "candidates (host match_caps / all peers)\n"); - } - } - - if (rnet_session_start_ice(g_np.session, &ice) != 0) { - fprintf(stderr, - "psx_netplay: start_ice failed; refusing unsafe LAN " - "fallback for an online lobby\n"); - rnet_session_destroy(g_np.session); - g_np.session = NULL; - return -4; - } -#else - fprintf(stderr, "psx_netplay: ICE requested but not built\n"); + if (rnet_session_start_lan(g_np.session, cfg->bind_hostport, cfg->peer_hostport) != 0) { rnet_session_destroy(g_np.session); g_np.session = NULL; - return -4; -#endif - } - - if (!use_ice) { - /* Host-as-relay: slot 0 with 3+ seats and no dial peer. */ -#if PSX_MAX_PLAYERS >= 3 - const int peer_empty = - !cfg->peer_hostport || !cfg->peer_hostport[0]; - const int use_hub = (local == 0 && slots >= 3 && peer_empty); - const int rc = use_hub - ? rnet_session_start_lan_hub(g_np.session, cfg->bind_hostport) - : rnet_session_start_lan(g_np.session, cfg->bind_hostport, - cfg->peer_hostport); -#else - const int rc = rnet_session_start_lan(g_np.session, cfg->bind_hostport, - cfg->peer_hostport); -#endif - if (rc != 0) { - rnet_session_destroy(g_np.session); - g_np.session = NULL; - return -3; - } + return -3; } - np_diag_capture(cfg, slots); g_np.active = 1; - g_np.use_ice = use_ice ? 1 : 0; g_np.slot_count = (int)rcfg.slot_count; - g_np_slot_count = g_np.slot_count; g_np.local_slot = (int)rcfg.local_slot; g_np.input_player = in_player; - if (g_np.slot_count >= 3) - sio_set_multitap(1); - else - sio_set_multitap(0); g_np.staged_valid = 0; g_np.needs_advance = 0; g_np.latched_for_tick = 0; @@ -1552,53 +961,13 @@ int psx_netplay_start(const PsxNetplayConfig *cfg) g_np.local_save_staged = 0; g_np.load_applied_local = 0; g_np.guest_sandbox = 0; - g_np.force_input_relay = cfg->force_input_relay ? 1 : 0; - g_np.input_delay = (int)rcfg.input_delay; - g_np.session_id = rcfg.session_id; - g_np.is_host = (g_np.local_slot == 0) ? 1 : 0; - g_np.frames_finished = 0; - g_np.diag_session++; - g_diag_summary_written = 0; - if (g_diag_file) { - fclose(g_diag_file); - g_diag_file = NULL; - } - g_diag_file_session = 0; - g_diag_last_write_ms = 0; - snprintf(g_np.bind_hostport, sizeof(g_np.bind_hostport), "%s", - cfg->bind_hostport); - snprintf(g_np.peer_hostport, sizeof(g_np.peer_hostport), "%s", - cfg->peer_hostport); - g_np.lobby_server[0] = '\0'; - g_np.lobby_id[0] = '\0'; -#if defined(PSX_HAS_LOBBY_CLIENT) - if (use_ice && psx_lobby_connected() && psx_lobby_in_lobby()) { - const PsxLobbyJoinInfo *ji = psx_lobby_join_info(); - snprintf(g_np.match_mode, sizeof(g_np.match_mode), "hosted_lobby"); - snprintf(g_np.lobby_server, sizeof(g_np.lobby_server), "%s", - psx_lobby_default_url()); - if (ji && ji->lobby_id[0]) - snprintf(g_np.lobby_id, sizeof(g_np.lobby_id), "%s", ji->lobby_id); - g_np.is_host = psx_lobby_is_host() ? 1 : 0; - } else -#endif - { - snprintf(g_np.match_mode, sizeof(g_np.match_mode), "direct_ip"); - } #if defined(__linux__) - if (!use_ice && peer_is_loopback(cfg->peer_hostport)) + if (peer_is_loopback(cfg->peer_hostport)) pin_localhost_peer_cpus(g_np.local_slot); #endif psx_netplay_release_pads(); - fprintf(stderr, - "psx_netplay: started transport=%s slot=%d input_player=%d session=%u " - "delay=%u force_input_relay=%d force_turn=%d bind=%s peer=%s\n", - use_ice ? "ice" : "lan", g_np.local_slot, g_np.input_player, - (unsigned)rcfg.session_id, (unsigned)rcfg.input_delay, - g_np.force_input_relay, cfg->force_turn ? 1 : 0, cfg->bind_hostport, - use_ice ? "(ice)" : cfg->peer_hostport); return 0; } @@ -1609,68 +978,8 @@ void psx_netplay_bind_guest_saves(void) np_enter_guest_sandbox(); } -/* Delay-sync starvation hold (lockstep-safe; mirrors snes_host_barrier_admit). */ -#define PSX_STARVATION_ENTER_DEFAULT 4 -#define PSX_STARVATION_EXIT_DEFAULT 3 -#define PSX_STARVATION_EXIT_HR_LEAD_DEFAULT 0 -#define PSX_STARVATION_GRACE_TICKS 60 -/* Default 0: after starvation clears, resume ~1 sim/wall frame and let - * remote_lead rebuild toward D instead of a turbo recovery burst. - * Override: PSX_NET_STARVATION_RECOVERY_BURST / PSX_NET_CATCHUP_CAP. */ -#define PSX_STARVATION_RECOVERY_BURST_DEFAULT 0 -#define PSX_CATCHUP_CAP_DEFAULT 0 - -static struct { - int latched; - int enter_run; - int exit_run; - int recovery_amount; - int latch_logged; - int just_cleared; -} g_starv; - -/* Defined below poll_admit; used by the starvation runway check. */ -int psx_netplay_remote_lead(void); -int psx_netplay_input_delay(void); - -static int np_starv_env_int(const char *name, int def) -{ - const char *v = getenv(name); - long n; - char *end; - if (!v || !v[0]) - return def; - n = strtol(v, &end, 10); - if (end == v || *end != '\0' || n < 0 || n > 64) - return def; - return (int)n; -} - -static void np_starv_reset(void) -{ - memset(&g_starv, 0, sizeof(g_starv)); -} - -static int np_starv_runway_ok(void) -{ - int lead = psx_netplay_remote_lead(); - int delay = psx_netplay_input_delay(); - int hr_lead = np_starv_env_int("PSX_NET_STARVATION_EXIT_HR_LEAD", - PSX_STARVATION_EXIT_HR_LEAD_DEFAULT); - if (delay < 0) - delay = 0; - return lead >= delay + hr_lead; -} - void psx_netplay_shutdown(void) { - if (g_diag_file) { - fclose(g_diag_file); - g_diag_file = NULL; - } - g_diag_file_session = 0; - g_diag_summary_written = 0; - g_diag_last_write_ms = 0; if (g_np.session) { (void)rnet_session_send_bye(g_np.session); rnet_session_destroy(g_np.session); @@ -1678,7 +987,6 @@ void psx_netplay_shutdown(void) } np_leave_guest_sandbox(); memset(&g_np, 0, sizeof(g_np)); - np_starv_reset(); } int psx_netplay_is_host(void) @@ -1688,11 +996,11 @@ int psx_netplay_is_host(void) int psx_netplay_request_save(int slot) { - uint32_t sim; - uint32_t delay; - uint32_t target; if (!psx_netplay_active() || !rnet_session_is_running(g_np.session)) return 0; + printf("psxrecomp: netplay savestates are disabled\n"); + fflush(stdout); + return 1; if (g_np.local_slot != 0) return 1; /* guest: host-only; ignore */ if (np_xfer_busy() || !g_np.mc_sync_done) @@ -1700,24 +1008,15 @@ int psx_netplay_request_save(int slot) if (slot < 0) slot = 0; if (slot >= SAVESTATE_SLOTS) slot = SAVESTATE_SLOTS - 1; - /* Agree a future sim_tick so TURN/coord latency cannot make the host - * write tick T while the guest still writes T+k (CRC miss → transfer). - * crc field of size==0 probe carries the target tick. */ - sim = rnet_session_sim_tick(g_np.session); - delay = (uint32_t)psx_netplay_input_delay(); - if (delay < 1u) delay = 1u; - target = sim + delay + 2u; - if (rnet_session_state_probe(g_np.session, RNET_STATE_OP_SAVE, (rnet_u8)slot, 0, - target) != 0) + if (!savestate_request_save_protocol(slot)) + return 1; + /* Coord probe (size=0) does not stall admit — both peers must keep + * running until savestate_poll writes the .pst, then hash-probe stalls. */ + if (rnet_session_state_probe(g_np.session, RNET_STATE_OP_SAVE, (rnet_u8)slot, 0, 0) != 0) return 1; g_np.xfer = NP_XFER_SAVE_COORD; g_np.xfer_slot = slot; - g_np.save_target_tick = target; - g_np.local_save_staged = 0; - g_np.local_save_acked = 0; - printf("psxrecomp: netplay save slot=%d — coordinating local writes " - "(target sim=%u, now=%u)…\n", - slot, (unsigned)target, (unsigned)sim); + printf("psxrecomp: netplay save slot=%d — coordinating local writes…\n", slot); fflush(stdout); return 1; } @@ -1725,22 +1024,17 @@ int psx_netplay_request_save(int slot) int psx_netplay_request_load(int slot) { uint32_t size = 0, crc = 0; - char reason[192]; if (!psx_netplay_active() || !rnet_session_is_running(g_np.session)) return 0; + printf("psxrecomp: netplay savestates are disabled\n"); + fflush(stdout); + return 1; if (g_np.local_slot != 0) return 1; if (np_xfer_busy() || !g_np.mc_sync_done) return 1; if (slot < 0) slot = 0; if (slot >= SAVESTATE_SLOTS) slot = SAVESTATE_SLOTS - 1; - if (!savestate_slot_compatible(slot, reason, sizeof(reason))) { - printf("psxrecomp: netplay load slot=%d refused — %s " - "(resave with this build: Shift+F%d)\n", - slot, reason[0] ? reason : "incompatible", slot + 1); - fflush(stdout); - return 1; - } if (!np_slot_crc(slot, &size, &crc)) return 1; if (rnet_session_state_probe(g_np.session, RNET_STATE_OP_LOAD, (rnet_u8)slot, size, crc) != 0) @@ -1748,7 +1042,6 @@ int psx_netplay_request_load(int slot) g_np.xfer = NP_XFER_LOAD_PROBE; g_np.xfer_slot = slot; g_np.load_applied_local = 0; - g_np.load_apply_failed = 0; printf("psxrecomp: netplay load slot=%d — hash probe (%u bytes)\n", slot, (unsigned)size); fflush(stdout); return 1; @@ -1758,346 +1051,26 @@ int psx_netplay_in_load_barrier(void) { if (!psx_netplay_active()) return 0; - /* Any save/load/memcard sync phase — TURN chunk xfers of ~1.4MB need the - * 90s budget (20s admit stall was killing SAVE mid-transfer). */ - return (g_np.xfer != NP_XFER_NONE) ? 1 : 0; + return (g_np.xfer == NP_XFER_LOAD_APPLYING || g_np.xfer == NP_XFER_LOAD_READY) ? 1 : 0; } -int psx_netplay_consume_load_apply_failed(void) -{ - int v = g_np.load_apply_failed; - g_np.load_apply_failed = 0; - return v; -} - - -static int np_diag_enabled(void) -{ - static int cached = -1; - if (cached < 0) { - const char *v = getenv("PSX_NET_DIAG"); - cached = (v && v[0] && v[0] != '0') ? 1 : 0; - } - return cached; -} - -/* Verbose delay-sync starvation latch/clear spam. Off by default — the latch - * can toggle every few frames under jitter and floods stderr. Enable with - * PSX_NET_DELAY_SYNC_DIAG=1 (alias: PSX_NET_STARVATION_DIAG=1). */ -static int np_delay_sync_diag_enabled(void) -{ - static int cached = -1; - if (cached < 0) { - const char *v = getenv("PSX_NET_DELAY_SYNC_DIAG"); - if (!v || !v[0]) - v = getenv("PSX_NET_STARVATION_DIAG"); - cached = (v && v[0] && v[0] != '0') ? 1 : 0; - } - return cached; -} - -static unsigned np_diag_interval_ms(void) -{ - static unsigned cached = 0; - unsigned hz; - if (cached) - return cached; - hz = env_u("PSX_NET_DIAG_HZ", 2); - if (hz < 1) hz = 1; - if (hz > 30) hz = 30; - cached = 1000u / hz; - if (cached < 1) cached = 1; - return cached; -} - -static void np_diag_escape(char *out, size_t out_len, const char *in) -{ - size_t oi = 0; - if (!out || out_len == 0) - return; - out[0] = '\0'; - if (!in) - return; - for (; *in && oi + 2 < out_len; ++in) { - char c = *in; - if (c == '"' || c == '\\') { - if (oi + 3 >= out_len) - break; - out[oi++] = '\\'; - out[oi++] = c; - } else if ((unsigned char)c < 0x20) { - /* skip */ - } else { - out[oi++] = c; - } - } - out[oi] = '\0'; -} - -static const char *np_diag_ice_path(const RNetSessionStats *st) -{ - if (!g_np.use_ice) - return "lan"; - if (!st) - return "pending"; - if (st->ice_state == RNET_ICE_STATE_FAILED) - return "failed"; - if (st->ice_path[0]) - return st->ice_path; - if (st->ice_state == RNET_ICE_STATE_COMPLETED || - st->ice_state == RNET_ICE_STATE_CONNECTED) - return "unknown"; - return "pending"; -} - -static const char *np_diag_ice_nat(const char *path) -{ - if (!g_np.use_ice) - return "lan"; - if (!path || !path[0] || strcmp(path, "pending") == 0) - return "pending"; - if (strcmp(path, "failed") == 0) - return "failed"; - if (strcmp(path, "relay") == 0) - return "turn"; - if (strcmp(path, "srflx") == 0 || strcmp(path, "prflx") == 0) - return "stun"; - if (strcmp(path, "host") == 0) - return "host"; - return "unknown"; -} - -static int np_diag_path_ready(const RNetSessionStats *st) -{ - if (!g_np.use_ice) - return 1; - if (!st) - return 0; - if (st->ice_state == RNET_ICE_STATE_FAILED) - return 1; - if (st->ice_path[0] && strcmp(st->ice_path, "pending") != 0 && - strcmp(st->ice_path, "unknown") != 0) - return 1; - if (st->ice_state == RNET_ICE_STATE_COMPLETED || - st->ice_state == RNET_ICE_STATE_CONNECTED) - return 1; - return 0; -} - -static void np_diag_write_summary(FILE *f, const RNetSessionStats *st, uint32_t now) -{ - char server_esc[280]; - char lobby_esc[80]; - char bind_esc[80]; - char peer_esc[80]; - char stun_esc[140]; - char turn_esc[140]; - char ice_local_esc[120]; - char ice_remote_esc[120]; - const char *path = np_diag_ice_path(st); - const char *nat = np_diag_ice_nat(path); - const char *ice_state = - st ? rnet_ice_state_name(st->ice_state) : "idle"; - - np_diag_escape(server_esc, sizeof(server_esc), g_np.lobby_server); - np_diag_escape(lobby_esc, sizeof(lobby_esc), g_np.lobby_id); - np_diag_escape(bind_esc, sizeof(bind_esc), g_np.bind_hostport); - np_diag_escape(peer_esc, sizeof(peer_esc), g_np.peer_hostport); - np_diag_escape(stun_esc, sizeof(stun_esc), g_np.ice_stun_host); - np_diag_escape(turn_esc, sizeof(turn_esc), g_np.ice_turn_host); - np_diag_escape(ice_local_esc, sizeof(ice_local_esc), - st ? st->ice_local : ""); - np_diag_escape(ice_remote_esc, sizeof(ice_remote_esc), - st ? st->ice_remote : ""); - - fprintf(f, - "{\"type\":\"summary\",\"t_ms\":%u,\"match\":\"%s\"," - "\"lobby_server\":\"%s\",\"lobby_id\":\"%s\",\"is_host\":%d," - "\"slot\":%d,\"session_id\":%u,\"input_delay\":%d," - "\"force_input_relay\":%d," - "\"transport\":\"%s\",\"bind\":\"%s\",\"peer\":\"%s\"," - "\"turn_configured\":%d,\"stun_host\":\"%s\",\"stun_port\":%u," - "\"turn_host\":\"%s\",\"turn_port\":%u,\"ice_state\":\"%s\"," - "\"ice_path\":\"%s\",\"ice_nat\":\"%s\"," - "\"ice_local\":\"%s\",\"ice_remote\":\"%s\"}\n", - (unsigned)now, g_np.match_mode[0] ? g_np.match_mode : "unknown", - server_esc, lobby_esc, g_np.is_host, g_np.local_slot, - (unsigned)g_np.session_id, g_np.input_delay, g_np.force_input_relay, - g_np.use_ice ? "ice" : "lan", bind_esc, peer_esc, - g_np.ice_has_turn ? 1 : 0, stun_esc, g_np.ice_stun_port, turn_esc, - g_np.ice_turn_port, ice_state ? ice_state : "idle", path, nat, - ice_local_esc, ice_remote_esc); -} - -void psx_netplay_diag_tick(void) -{ - RNetSessionStats st; - uint32_t now; - const char *transport; - const char *ice_state; - const char *path; - - if (!np_diag_enabled() || !psx_netplay_active() || !g_np.session) - return; - - rnet_session_get_stats(g_np.session, &st); - - if (!g_diag_summary_written && !np_diag_path_ready(&st)) - return; - - now = np_mono_ms(); - if (g_diag_last_write_ms && - (uint32_t)(now - g_diag_last_write_ms) < np_diag_interval_ms() && - g_diag_summary_written) - return; - g_diag_last_write_ms = now ? now : 1u; - - if (!g_diag_mkdir_done) { - g_diag_mkdir_done = 1; -#ifdef _WIN32 - _mkdir("saves"); - _mkdir("saves\\netplay"); -#else - mkdir("saves", 0755); - mkdir("saves/netplay", 0755); -#endif - } - - if (!g_diag_file || g_diag_file_session != g_np.diag_session) { - char pathbuf[64]; - if (g_diag_file) { - fclose(g_diag_file); - g_diag_file = NULL; - } - snprintf(pathbuf, sizeof(pathbuf), "saves/netplay/net_diag.jsonl"); - g_diag_file = fopen(pathbuf, "wb"); - if (!g_diag_file) - return; - setvbuf(g_diag_file, NULL, _IOLBF, 0); - g_diag_file_session = g_np.diag_session; - g_diag_summary_written = 0; - fprintf(stderr, "psx_netplay: diag writing %s " - "(PSX_NET_DIAG_HZ interval %ums)\n", - pathbuf, np_diag_interval_ms()); - } - - if (!g_diag_summary_written) { - np_diag_write_summary(g_diag_file, &st, now); - g_diag_summary_written = 1; - } - - { - char ice_local_esc[120]; - char ice_remote_esc[120]; - const char *stall = rnet_admit_stall_name(st.last_stall); - int using_turn_path = (strcmp(np_diag_ice_path(&st), "relay") == 0) ? 1 : 0; - - transport = psx_netplay_transport_name(); - ice_state = rnet_ice_state_name(st.ice_state); - path = np_diag_ice_path(&st); - np_diag_escape(ice_local_esc, sizeof(ice_local_esc), st.ice_local); - np_diag_escape(ice_remote_esc, sizeof(ice_remote_esc), st.ice_remote); - - fprintf(g_diag_file, - "{\"t_ms\":%u,\"slot\":%d,\"transport\":\"%s\",\"ice_state\":\"%s\"," - "\"ice_path\":\"%s\",\"ice_nat\":\"%s\",\"turn\":%d," - "\"ice_local\":\"%s\",\"ice_remote\":\"%s\"," - "\"running\":%d,\"sim_tick\":%u,\"frames_finished\":%u," - "\"delay\":%u,\"stall\":\"%s\"," - "\"stall_ms\":%u,\"stall_max_ms\":%u,\"stall_streaks\":%u," - "\"consec_stalls\":%u,\"admit_ok\":%u,\"remote_lead\":%d," - "\"remote_wire\":%u,\"peer_rx_age_ms\":%llu,\"peer_gone\":%d," - "\"desync\":%d,\"desync_tick\":%u,\"state_busy\":%d,\"state_op\":%u," - "\"pkts_rx\":%u,\"input_sends\":%u}\n", - (unsigned)now, g_np.local_slot, transport ? transport : "none", - ice_state ? ice_state : "idle", path, np_diag_ice_nat(path), - using_turn_path, ice_local_esc, ice_remote_esc, st.is_running, - (unsigned)st.sim_tick, (unsigned)g_np.frames_finished, - (unsigned)st.delay, stall ? stall : "unknown", - (unsigned)st.last_admit_wait_ms, (unsigned)st.max_admit_wait_ms, - (unsigned)st.stall_streaks, (unsigned)st.consecutive_stalls, - (unsigned)st.admit_ok_count, st.remote_lead, - (unsigned)st.highest_remote_wire, - (unsigned long long)st.last_peer_rx_age_ms, st.peer_gone, - st.input_desync, (unsigned)st.desync_tick, st.state_busy, - (unsigned)st.state_op, (unsigned)st.packets_rx, - (unsigned)st.input_bundle_sends); - } -} - -/* Stage the coord save once sim_tick reaches the agreed target. */ -static void np_maybe_stage_target_save(void) +int psx_netplay_poll_admit(void) { - uint32_t sim; - if (g_np.xfer != NP_XFER_SAVE_COORD || g_np.local_save_staged) - return; - if (!g_np.session || !rnet_session_is_running(g_np.session)) - return; - sim = rnet_session_sim_tick(g_np.session); - if (sim < g_np.save_target_tick) - return; - if (!savestate_request_save_protocol(g_np.xfer_slot)) - return; - g_np.local_save_staged = 1; - printf("psxrecomp: netplay %s save slot=%d — staging @ sim=%u (target=%u)\n", - g_np.local_slot == 0 ? "host" : "guest", g_np.xfer_slot, - (unsigned)sim, (unsigned)g_np.save_target_tick); - fflush(stdout); -} + rnet_u32 sim; + if (!psx_netplay_active()) return 1; -static void np_pump_session(void) -{ -#if defined(PSX_HAS_LOBBY_CLIENT) - if (g_np.use_ice || psx_lobby_connected()) - psx_lobby_pump(); -#endif - drain_lobby_signals(); rnet_session_pump(g_np.session); np_guest_handle_probe(); - np_maybe_stage_target_save(); np_apply_ready_state(); np_drive_load_barrier(); np_host_drive_xfer(); - if (rnet_session_is_running(g_np.session)) - np_maybe_start_mc_sync(); -} - -void psx_netplay_pump(void) -{ - if (!psx_netplay_active()) - return; - np_pump_session(); - psx_netplay_diag_tick(); -} - -static int np_try_admit_gameplay(void) -{ - rnet_u32 sim = rnet_session_sim_tick(g_np.session); - if (rnet_session_try_admit(g_np.session, sim)) { - g_np.needs_advance = 1; - return 1; - } - force_session_pads_connected(g_np.slot_count); - return 0; -} - -int psx_netplay_poll_admit(void) -{ - rnet_u32 sim; - int enter_need; - int exit_need; - - if (!psx_netplay_active()) return 1; - - np_pump_session(); if (!rnet_session_is_running(g_np.session)) { psx_netplay_release_pads(); - np_starv_reset(); - psx_netplay_diag_tick(); return 0; } + np_maybe_start_mc_sync(); /* Both peers stall until initial memcard hash-agree / transfer finishes. */ if (!g_np.mc_sync_done) return 0; @@ -2108,14 +1081,6 @@ int psx_netplay_poll_admit(void) if (g_np.xfer == NP_XFER_LOAD_APPLYING && !savestate_pending()) return 0; - /* Staged load must run guest cycles — bypass starvation latch. ICE xfer - * often leaves lead=D-1 and would otherwise block try_admit forever. */ - if (g_np.xfer == NP_XFER_LOAD_APPLYING && savestate_pending()) { - if (g_np.needs_advance) - return 1; - return np_try_admit_gameplay(); - } - /* Both peers: after mutual ready + sync, stay in LOAD_READY until try_admit * succeeds (fresh tip exchange + INPUT_CONFIRM). Dropping the barrier early * on the host let it spin on confirm with FPS/present already "live". */ @@ -2143,110 +1108,11 @@ int psx_netplay_poll_admit(void) if (g_np.needs_advance) return 1; sim = rnet_session_sim_tick(g_np.session); - enter_need = np_starv_env_int("PSX_NET_STARVATION_ENTER_FRAMES", - PSX_STARVATION_ENTER_DEFAULT); - exit_need = np_starv_env_int("PSX_NET_STARVATION_EXIT_FRAMES", - PSX_STARVATION_EXIT_DEFAULT); - - /* SAVE coord: run admit until both reach save_target_tick and flush the - * staged write. After the local .pst exists, freeze (host also freezes - * unless the guest tip is behind and still needs catch-up admits). */ - if (g_np.xfer == NP_XFER_SAVE_COORD) { - g_starv.enter_run = 0; - g_starv.exit_run = 0; - g_starv.latched = 0; - g_starv.just_cleared = 0; - np_maybe_stage_target_save(); - if (!g_np.local_save_staged || savestate_pending()) - return np_try_admit_gameplay(); - if (g_np.local_slot != 0) - return 0; /* guest saved — wait for hash probe */ - /* Host saved: freeze for same-tick match. If guest is still behind - * the target, keep admitting so it can catch up and write. */ - if (psx_netplay_remote_lead() < 0) - return np_try_admit_gameplay(); - return 0; - } - - if (g_np.xfer == NP_XFER_LOAD_PROBE || g_np.xfer == NP_XFER_LOAD_SEND || - g_np.xfer == NP_XFER_SAVE_PROBE || g_np.xfer == NP_XFER_SAVE_SEND || - g_np.xfer == NP_XFER_MC_PROBE || g_np.xfer == NP_XFER_MC_SEND) { - g_starv.enter_run = 0; - g_starv.exit_run = 0; - g_starv.latched = 0; - g_starv.just_cleared = 0; - return np_try_admit_gameplay(); - } - - /* Startup grace: do not latch before the delay rings warm up. */ - if (sim < (rnet_u32)PSX_STARVATION_GRACE_TICKS) { - g_starv.enter_run = 0; - g_starv.exit_run = 0; - g_starv.latched = 0; - g_starv.just_cleared = 0; - return np_try_admit_gameplay(); - } - - if (g_starv.latched) { - /* Pump already ran; hold try_admit until remote tip refills. */ - if (np_starv_runway_ok()) { - g_starv.exit_run++; - if (g_starv.exit_run >= exit_need) { - g_starv.latched = 0; - g_starv.exit_run = 0; - g_starv.latch_logged = 0; - g_starv.just_cleared = 1; - } else { - return 0; - } - } else { - g_starv.exit_run = 0; - return 0; - } - } - - if (np_try_admit_gameplay()) { - g_starv.enter_run = 0; - if (g_starv.just_cleared) { - int burst = np_starv_env_int("PSX_NET_STARVATION_RECOVERY_BURST", - PSX_STARVATION_RECOVERY_BURST_DEFAULT); - g_starv.just_cleared = 0; - g_starv.recovery_amount = burst; - if (np_delay_sync_diag_enabled()) { - if (burst > 0) { - fprintf(stderr, - "psxrecomp: delay_sync_starvation cleared sim=%u lead=%d " - "D=%d — recovery burst %d\n", - (unsigned)psx_netplay_sim_tick(), psx_netplay_remote_lead(), - psx_netplay_input_delay(), burst); - } else { - fprintf(stderr, - "psxrecomp: delay_sync_starvation cleared sim=%u lead=%d " - "D=%d — resume 1:1 (rebuild input buffer)\n", - (unsigned)psx_netplay_sim_tick(), psx_netplay_remote_lead(), - psx_netplay_input_delay()); - } - } - } + if (rnet_session_try_admit(g_np.session, sim)) { + g_np.needs_advance = 1; return 1; } - - g_starv.just_cleared = 0; - g_starv.enter_run++; - if (g_starv.enter_run >= enter_need) { - g_starv.latched = 1; - g_starv.enter_run = 0; - if (!g_starv.latch_logged) { - if (np_delay_sync_diag_enabled()) { - fprintf(stderr, - "psxrecomp: delay_sync_starvation latched sim=%u lead=%d " - "D=%d (enter=%d)\n", - (unsigned)psx_netplay_sim_tick(), psx_netplay_remote_lead(), - psx_netplay_input_delay(), enter_need); - } - g_starv.latch_logged = 1; - } - } + force_session_pads_connected(g_np.slot_count); return 0; } @@ -2257,62 +1123,6 @@ void psx_netplay_finish_frame(void) rnet_session_advance(g_np.session); g_np.needs_advance = 0; g_np.latched_for_tick = 0; - g_np.frames_finished++; -} - -int psx_netplay_remote_lead(void) -{ - RNetSessionStats st; - if (!psx_netplay_active()) - return 0; - memset(&st, 0, sizeof(st)); - rnet_session_get_stats(g_np.session, &st); - return st.remote_lead; -} - -int psx_netplay_input_delay(void) -{ - RNetSessionStats st; - if (!psx_netplay_active()) - return 2; - memset(&st, 0, sizeof(st)); - rnet_session_get_stats(g_np.session, &st); - return st.delay > 0 ? (int)st.delay : 2; -} - -int psx_netplay_catchup_budget(void) -{ - int lead; - int delay; - int extra; - int budget; - int cap; - - if (!psx_netplay_active()) - return 0; - cap = np_starv_env_int("PSX_NET_CATCHUP_CAP", PSX_CATCHUP_CAP_DEFAULT); - if (cap <= 0 && g_starv.recovery_amount <= 0) - return 0; - lead = psx_netplay_remote_lead(); - delay = psx_netplay_input_delay(); - if (delay < 0) - delay = 0; - /* Only spend surplus above D; keep the delay runway intact. */ - extra = lead - delay; - if (extra < 0) - extra = 0; - budget = extra; - if (g_starv.recovery_amount > budget) - budget = g_starv.recovery_amount; - if (budget > cap) - budget = cap; - return budget; -} - -void psx_netplay_catchup_consume_frame(void) -{ - if (g_starv.recovery_amount > 0) - g_starv.recovery_amount--; } void psx_netplay_wait_recv(int timeout_ms) @@ -2321,89 +1131,4 @@ void psx_netplay_wait_recv(int timeout_ms) (void)rnet_session_wait_recv(g_np.session, timeout_ms); } -void psx_netplay_admit_wait_info(char *stall_out, size_t stall_cap, - uint32_t *sim_tick_out, int *lead_out) -{ - RNetSessionStats st; - const char *name = "inactive"; - char phase[96]; - memset(&st, 0, sizeof(st)); - phase[0] = '\0'; - if (psx_netplay_active() && g_np.session) { - rnet_session_get_stats(g_np.session, &st); - name = rnet_admit_stall_name(st.last_stall); - if (!name || !name[0]) - name = "unknown"; - /* LOAD_READY never calls try_admit, so last_stall stays "ok" — surface - * the app barrier phase (+ transfer progress) instead. */ - switch (g_np.xfer) { - case NP_XFER_SAVE_COORD: - snprintf(phase, sizeof(phase), "save_coord"); - break; - case NP_XFER_SAVE_PROBE: - snprintf(phase, sizeof(phase), "save_probe"); - break; - case NP_XFER_SAVE_SEND: - if (st.state_bytes_total > 0) - snprintf(phase, sizeof(phase), "save_xfer_%u/%u", - (unsigned)st.state_bytes_acked, (unsigned)st.state_bytes_total); - else - snprintf(phase, sizeof(phase), "save_xfer"); - break; - case NP_XFER_MC_PROBE: - snprintf(phase, sizeof(phase), "mc_probe"); - break; - case NP_XFER_MC_SEND: - if (st.state_bytes_total > 0) - snprintf(phase, sizeof(phase), "mc_xfer_%u/%u", - (unsigned)st.state_bytes_acked, (unsigned)st.state_bytes_total); - else - snprintf(phase, sizeof(phase), "mc_xfer"); - break; - case NP_XFER_LOAD_PROBE: - snprintf(phase, sizeof(phase), "load_probe"); - break; - case NP_XFER_LOAD_SEND: - if (st.state_bytes_total > 0) - snprintf(phase, sizeof(phase), "load_xfer_%u/%u", - (unsigned)st.state_bytes_acked, (unsigned)st.state_bytes_total); - else - snprintf(phase, sizeof(phase), "load_xfer"); - break; - case NP_XFER_LOAD_APPLYING: - if (savestate_pending()) { - if (g_starv.latched) - snprintf(phase, sizeof(phase), "load_applying+starv_%s", name); - else - snprintf(phase, sizeof(phase), "load_applying+%s", name); - } else { - snprintf(phase, sizeof(phase), "load_apply_done+%s", name); - } - break; - case NP_XFER_LOAD_READY: - if (g_np.load_ready_replied) - snprintf(phase, sizeof(phase), "load_ready_admit+%s", name); - else if (g_np.load_applied_local) - snprintf(phase, sizeof(phase), "load_ready_wait_peer+%s", name); - else - snprintf(phase, sizeof(phase), "load_ready+%s", name); - break; - default: - break; - } - } - if (stall_out && stall_cap) { - if (phase[0]) - snprintf(stall_out, stall_cap, "%s", phase); - else { - strncpy(stall_out, name, stall_cap - 1); - stall_out[stall_cap - 1] = '\0'; - } - } - if (sim_tick_out) - *sim_tick_out = st.sim_tick; - if (lead_out) - *lead_out = st.remote_lead; -} - #endif /* PSX_HAS_RECOMP_NET */ diff --git a/runtime/src/savestate.c b/runtime/src/savestate.c index 0f00f63a..b04e4ac0 100644 --- a/runtime/src/savestate.c +++ b/runtime/src/savestate.c @@ -7,37 +7,18 @@ #include "savestate.h" #include "boot_state.h" -#include "cdrom.h" -#include "interrupts.h" #include "psx_cycles.h" #include "psx_netplay.h" #include "psx_scheduler.h" -#include #include #include #include #ifdef _WIN32 #include -#include #else #include -#include #endif -static double savestate_mono_ms(void) { -#ifdef _WIN32 - static LARGE_INTEGER freq; - LARGE_INTEGER c; - if (!freq.QuadPart) QueryPerformanceFrequency(&freq); - QueryPerformanceCounter(&c); - return (double)c.QuadPart * 1000.0 / (double)freq.QuadPart; -#else - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1.0e6; -#endif -} - static char s_dir[512]; static uint32_t s_bios_checksum; static uint32_t s_entry_pc; @@ -45,54 +26,23 @@ static int s_configured = 0; static int s_save_pending = -1; /* slot, or -1 */ static int s_load_pending = -1; static int s_load_completed = 0; -static int s_load_failed = 0; -static uint8_t *s_load_blob = NULL; /* optional in-memory .pst for netplay */ -static size_t s_load_blob_len = 0; +static uint64_t s_load_cooldown_until_frame = 0; +static int s_load_cooldown_notice = 0; extern int psx_hle_scheduler_enabled(void); +extern uint64_t s_frame_count; + +#define SAVESTATE_LOAD_COOLDOWN_FRAMES 60u -/* Create each path component (mkdir -p). Single-level mkdir fails for - * "saves/netplay" when parent "saves" is missing. */ static void ensure_dir(const char* dir) { - char tmp[512]; - size_t len; - size_t i; if (!dir || !dir[0]) return; - strncpy(tmp, dir, sizeof(tmp) - 1); - tmp[sizeof(tmp) - 1] = '\0'; - len = strlen(tmp); - while (len > 1 && (tmp[len - 1] == '/' || tmp[len - 1] == '\\')) { - tmp[--len] = '\0'; - } - for (i = 1; i < len; i++) { - if (tmp[i] == '/' || tmp[i] == '\\') { -#ifdef _WIN32 - /* Keep drive prefix "C:" intact — do not mkdir("C:"). */ - if (i == 2 && tmp[1] == ':') - continue; -#endif - tmp[i] = '\0'; -#ifdef _WIN32 - (void)_mkdir(tmp); -#else - (void)mkdir(tmp, 0755); -#endif - tmp[i] = '/'; - } - } #ifdef _WIN32 - (void)_mkdir(tmp); + (void)_mkdir(dir); #else - (void)mkdir(tmp, 0755); + (void)mkdir(dir, 0755); #endif } -static void clear_load_blob(void) { - free(s_load_blob); - s_load_blob = NULL; - s_load_blob_len = 0; -} - void savestate_configure(const char* dir, uint32_t bios_checksum, uint32_t entry_pc) { if (dir && dir[0]) { strncpy(s_dir, dir, sizeof(s_dir) - 1); @@ -138,28 +88,6 @@ int savestate_slot_exists(int slot) { return sz > 0; } -int savestate_slot_compatible(int slot, char* reason, size_t reason_cap) { - uint8_t* data = NULL; - size_t size = 0; - int ok; - if (reason && reason_cap) - reason[0] = '\0'; - if (!s_configured) { - if (reason && reason_cap) - snprintf(reason, reason_cap, "not_configured"); - return 0; - } - if (!savestate_read_slot(slot, &data, &size) || !data) { - if (reason && reason_cap) - snprintf(reason, reason_cap, "missing"); - return 0; - } - ok = boot_state_check_buffer(data, size, s_bios_checksum, s_entry_pc, - reason, reason_cap); - free(data); - return ok; -} - int savestate_read_slot(int slot, uint8_t** data_out, size_t* size_out) { char path[600]; FILE* f; @@ -191,50 +119,26 @@ int savestate_read_slot(int slot, uint8_t** data_out, size_t* size_out) { int savestate_write_slot(int slot, const void* data, size_t size) { char path[600]; FILE* f; - size_t wrote; if (!data || size == 0) return 0; - if (!savestate_slot_path(slot, path, sizeof(path))) { - fprintf(stderr, - "savestate: write_slot=%d failed (not configured / bad slot) " - "dir='%s' configured=%d\n", - slot, s_dir, s_configured); - return 0; - } + if (!savestate_slot_path(slot, path, sizeof(path))) return 0; ensure_dir(s_dir); f = fopen(path, "wb"); - if (!f) { - fprintf(stderr, "savestate: write_slot fopen('%s') failed: %s\n", - path, strerror(errno)); - return 0; - } - wrote = fwrite(data, 1, size, f); - if (wrote != size) { - fprintf(stderr, - "savestate: write_slot fwrite('%s') %zu/%zu failed: %s\n", - path, wrote, size, strerror(errno)); + if (!f) return 0; + if (fwrite(data, 1, size, f) != size) { fclose(f); remove(path); return 0; } if (fflush(f) != 0 || fclose(f) != 0) { - fprintf(stderr, "savestate: write_slot flush/close('%s') failed: %s\n", - path, strerror(errno)); remove(path); return 0; } return 1; } -/* User APIs during netplay: guests cannot initiate; host must use - * psx_netplay_request_* so peers hash-probe and sync over STATE_*. */ -static int netplay_user_blocked(void) { +static int netplay_savestate_blocked(void) { if (!psx_netplay_active()) return 0; - if (!psx_netplay_is_host()) { - fprintf(stderr, "savestate: netplay guest cannot save/load (host-only)\n"); - return 1; - } - fprintf(stderr, - "savestate: during netplay use host Shift+F / F (synced path)\n"); + fprintf(stderr, "savestate: disabled during netplay\n"); return 1; } @@ -248,6 +152,13 @@ static int request_save_inner(int slot) { static int request_load_inner(int slot) { if (!s_configured) { fprintf(stderr, "savestate: not configured\n"); return 0; } if (slot < 0 || slot >= SAVESTATE_SLOTS) return 0; + if (s_frame_count < s_load_cooldown_until_frame) { + if (!s_load_cooldown_notice) { + fprintf(stderr, "savestate: load ignored during restore cooldown\n"); + s_load_cooldown_notice = 1; + } + return 1; + } if (!psx_hle_scheduler_enabled()) { /* LLE (host-fiber) mode: the restore longjmp target lives on the * scheduler fiber; cross-fiber unwind is unsafe. HLE is the default. */ @@ -255,60 +166,30 @@ static int request_load_inner(int slot) { "PSX_HLE_SCHEDULER=0 run cannot load states.\n"); return 0; } - s_load_failed = 0; - s_load_completed = 0; s_load_pending = slot; return 1; } int savestate_request_save(int slot) { - if (netplay_user_blocked()) return 0; + if (netplay_savestate_blocked()) return 0; return request_save_inner(slot); } int savestate_request_load(int slot) { - if (netplay_user_blocked()) return 0; + if (netplay_savestate_blocked()) return 0; return request_load_inner(slot); } int savestate_request_save_protocol(int slot) { - /* Follow-host sync: guests must write the host-authoritative .pst. */ + if (netplay_savestate_blocked()) return 0; return request_save_inner(slot); } int savestate_request_load_protocol(int slot) { - /* Follow-host sync: guests must apply the host-authoritative .pst. */ - clear_load_blob(); + if (netplay_savestate_blocked()) return 0; return request_load_inner(slot); } -int savestate_request_load_blob_protocol(const void* data, size_t size) { - uint8_t* copy; - if (!s_configured) { - fprintf(stderr, "savestate: load_blob — not configured\n"); - return 0; - } - if (!data || size == 0 || size > 64u * 1024u * 1024u) - return 0; - if (!psx_hle_scheduler_enabled()) { - fprintf(stderr, "savestate: load_blob requires the HLE scheduler\n"); - return 0; - } - copy = (uint8_t*)malloc(size); - if (!copy) { - fprintf(stderr, "savestate: load_blob malloc(%zu) failed\n", size); - return 0; - } - memcpy(copy, data, size); - clear_load_blob(); - s_load_blob = copy; - s_load_blob_len = size; - s_load_failed = 0; - s_load_completed = 0; - s_load_pending = 0; /* non-negative: poll will prefer the blob */ - return 1; -} - int savestate_pending(void) { return (s_save_pending >= 0 || s_load_pending >= 0) ? 1 : 0; } @@ -319,12 +200,6 @@ int savestate_take_load_completed(void) { return v; } -int savestate_take_load_failed(void) { - int v = s_load_failed; - s_load_failed = 0; - return v; -} - void savestate_poll(CPUState* cpu, uint32_t resume_pc) { if (s_save_pending < 0 && s_load_pending < 0) return; /* hot path: nothing staged */ @@ -345,64 +220,27 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) { if (s_load_pending >= 0) { int slot = s_load_pending; - int loaded = 0; s_load_pending = -1; char path[600]; - const double t_load0 = savestate_mono_ms(); - double t_after_boot = t_load0; - double t_after_frontend = t_load0; - path[0] = '\0'; - if (s_load_blob && s_load_blob_len > 0) { - const size_t blob_len = s_load_blob_len; - loaded = boot_state_load_buffer(s_load_blob, blob_len, - s_bios_checksum, s_entry_pc, cpu); - clear_load_blob(); - if (!loaded) { - fprintf(stderr, - "savestate: LOAD FAILED blob (%zu bytes, entry=%08X)\n", - blob_len, (unsigned)s_entry_pc); - s_load_failed = 1; - } - } else if (savestate_slot_path(slot, path, sizeof(path))) { - loaded = boot_state_load(path, s_bios_checksum, s_entry_pc, cpu); - if (!loaded) { - fprintf(stderr, - "savestate: LOAD FAILED slot %d %s\n", - slot, path); - s_load_failed = 1; - } - } else { - fprintf(stderr, "savestate: LOAD FAILED slot %d (no path)\n", slot); - s_load_failed = 1; - } - if (loaded) { - t_after_boot = savestate_mono_ms(); - psx_cycles_resync_after_restore(cpu); - /* Drop absolute-cycle IRQ cooldowns / VBlank phase from the - * pre-load host timeline (cycle rewind would otherwise blackout - * VBlank delivery for however long the user played past the save). */ - interrupts_resync_after_restore(); - /* Collapse restored / imminent CD second-response debt (ReadTOC, - * Init, seeks) so the picture does not freeze for ~1s after the - * restored frame presents. */ - cdrom_accelerate_after_savestate(); + if (!savestate_slot_path(slot, path, sizeof(path))) return; + if (boot_state_load(path, s_bios_checksum, s_entry_pc, cpu)) { + psx_cycles_resync_after_restore(); + s_load_cooldown_until_frame = + s_frame_count + SAVESTATE_LOAD_COOLDOWN_FRAMES; + s_load_cooldown_notice = 0; + fprintf(stderr, "savestate: LOADED slot %d -> resuming pc=0x%08X\n", + slot, (unsigned)cpu->pc); /* Netplay post-load barrier observes this before the longjmp. */ s_load_completed = 1; /* Restage FBO/present latch so the restored frame is visible * immediately (avoids disabled-display blank latch + stale smooth). */ psx_frontend_on_savestate_loaded(); - t_after_frontend = savestate_mono_ms(); - fprintf(stderr, - "savestate: LOADED slot %d -> resuming pc=0x%08X " - "(boot=%.1f frontend=%.1f poll_total=%.1f ms)%s\n", - slot, (unsigned)cpu->pc, - t_after_boot - t_load0, - t_after_frontend - t_after_boot, - t_after_frontend - t_load0, - path[0] ? "" : " [blob]"); /* Unwind to the scheduler and re-dispatch the restored PC. Never * returns; abandons the suspended CPS frames on the current stack. */ psx_scheduler_resume_at(cpu->pc); + } else { + fprintf(stderr, "savestate: LOAD FAILED slot %d (missing/mismatched) %s\n", + slot, path); } } } diff --git a/runtime/src/sio.c b/runtime/src/sio.c index 1e57e8a6..31baf504 100644 --- a/runtime/src/sio.c +++ b/runtime/src/sio.c @@ -42,34 +42,27 @@ static void sio_debug_poll_maybe(void) { } } -/* Pad state: 0=pressed, 1=released (PS1 convention). Indexed by LOGICAL pad - * 0 .. PSX_MAX_PLAYERS-1 (not physical SIO slot). */ -static uint16_t pad_buttons[PSX_MAX_PLAYERS] = { [0 ... PSX_MAX_PLAYERS - 1] = 0xFFFF }; +/* Pad state: 0=pressed, 1=released (PS1 convention). Per slot (port). */ +static uint16_t pad_buttons[2] = { 0xFFFF, 0xFFFF }; /* all released */ -/* Per-logical-pad type + analog stick state. analog: 0=digital pad (poll id +/* Per-slot pad type + analog stick state. analog: 0=digital pad (poll id * 0x41), 1=DualShock/analog (poll id 0x73). Sticks are 0..255, 0x80 centred. */ -static uint8_t pad_analog[PSX_MAX_PLAYERS]; -static uint8_t pad_stick[PSX_MAX_PLAYERS][4] = { - [0 ... PSX_MAX_PLAYERS - 1] = { 0x80, 0x80, 0x80, 0x80 } -}; /* lx,ly,rx,ry */ - -/* Analog-mode lock, per logical pad. A real DualShock's config command 0x44 - * 0x..02/0x03 locks/unlocks the mode (dualshock.cpp:714-725); a locked pad - * ignores the physical analog button (dualshock.cpp:203). We emulate the - * analog button via the host hybrid heuristic (pad_type_req), so when a game - * LOCKS the mode the hybrid auto-flip must not override it — else the type - * flips underneath a game that pinned DualShock, the exact desync the - * deferred-request machinery cannot otherwise prevent. */ -static uint8_t analog_mode_locked[PSX_MAX_PLAYERS]; - -/* Which logical pads have devices connected (bit i = pad i). Fits 5 pads. */ +static uint8_t pad_analog[2] = { 0, 0 }; +static uint8_t pad_stick[2][4] = { { 0x80, 0x80, 0x80, 0x80 }, + { 0x80, 0x80, 0x80, 0x80 } }; /* lx,ly,rx,ry */ + +/* Analog-mode lock, per slot. A real DualShock's config command 0x44 0x..02/0x03 + * locks/unlocks the mode (dualshock.cpp:714-725); a locked pad ignores the + * physical analog button (dualshock.cpp:203). We emulate the analog button via + * the host hybrid heuristic (pad_type_req), so when a game LOCKS the mode the + * hybrid auto-flip must not override it — else the type flips underneath a game + * that pinned DualShock, the exact desync the deferred-request machinery cannot + * otherwise prevent. */ +static uint8_t analog_mode_locked[2] = { 0, 0 }; + +/* Which slots have devices connected */ static uint8_t pad_connected = 0; -/* Host-side SCPH-1070 enable. Only meaningful when PSX_MAX_PLAYERS >= 5. */ -static int sio_multitap_enabled = 0; -/* Physical SIO port hosting the tap: 0 = console Port 1, 1 = Port 2. */ -static int sio_multitap_port = 0; - /* Pad communication state machine */ typedef enum { PAD_IDLE, @@ -77,51 +70,33 @@ typedef enum { PAD_SEND_RESPONSE, /* sending command response bytes */ } PadState; -/* Multitap 0x42 bulk: ID(0x80)+0x5A + 4×8 pad status bytes. */ -#define PAD_RESPONSE_MAX 34 - -/* What the next 0x42 on the multitap port returns (psx-spx TAP/REQ latch). */ -typedef enum { - MTAP_NEXT_SLOT_A = 0, - MTAP_NEXT_BULK, - MTAP_NEXT_GARBAGE, -} MtapNextMode; - static PadState pad_state = PAD_IDLE; -static int selected_slot = 0; /* physical SIO slot (CTRL bit13): 0 or 1 */ -static int pad_active_logical = 0; /* logical pad for single-pad / config cmds */ -static uint8_t pad_response[PAD_RESPONSE_MAX]; +static int selected_slot = 0; +static uint8_t pad_response[8]; static uint8_t pad_response_len = 0; static uint8_t pad_response_idx = 0; static uint8_t pad_current_cmd = 0; -/* Address byte that opened this pad txn (01h=Slot A / bulk, 02h..04h=B..D). */ -static uint8_t pad_mtap_addr = 0x01; -static int mtap_next_mode = MTAP_NEXT_SLOT_A; -static int mtap_req_this = 0; /* TAP==1 seen on current 0x42 txn */ -static int mtap_returned = MTAP_NEXT_SLOT_A; /* what this txn returned */ -/* DualShock config-mode latch, per logical pad. A real controller only answers - * the config commands (0x44/0x45/0x46/0x47/0x4C/0x4D/0x4F) and reports the - * config ID 0xF3 while it is IN config mode; outside config it reports its - * normal ID (0x41 digital / 0x73 analog) and ignores config commands. Config is - * entered/exited by command 0x43 with the data byte 0x01(enter)/0x00(exit). - * Faking "always in config" (constant 0xF3) wedges games that probe the pad - * type via 0x43 before polling — e.g. Mega Man X6 loops 01 43 00 00 forever - * and never reaches 0x42. (MMX6 ISSUES.md #2.) */ -static uint8_t pad_in_config[PSX_MAX_PLAYERS]; - -/* Whether the pad on a logical slot is a config-capable DualShock (1) or a - * plain digital controller (0). A real SCPH-1080 digital pad (poll id 0x41) - * does NOT answer the config-mode commands (0x43/0x44/.../0x4F): it returns - * hi-z and the transaction ends. A game's pad driver that probes with 0x43 to - * detect a DualShock therefore classifies a digital pad as digital-only and - * just polls it with 0x42. Tomba 2's driver probes this way every frame; when - * the SM (wrongly) answered 0x43 for its digital pad it went down the - * DualShock config path and read the 0x00 config-response bytes as buttons -> - * phantom "all pressed" input. Default 1 keeps analog/hybrid pads unchanged; - * main.cpp sets 0 for PAD_MODE_DIGITAL. */ -static uint8_t pad_supports_config[PSX_MAX_PLAYERS] = { - [0 ... PSX_MAX_PLAYERS - 1] = 1 -}; +/* DualShock config-mode latch, per slot. A real controller only answers the + * config commands (0x44/0x45/0x46/0x47/0x4C/0x4D/0x4F) and reports the config + * ID 0xF3 while it is IN config mode; outside config it reports its normal ID + * (0x41 digital / 0x73 analog) and ignores config commands. Config is entered/ + * exited by command 0x43 with the data byte 0x01(enter)/0x00(exit). Faking + * "always in config" (constant 0xF3) wedges games that probe the pad type via + * 0x43 before polling — e.g. Mega Man X6 loops 01 43 00 00 forever and never + * reaches 0x42. (MMX6 ISSUES.md #2.) */ +static uint8_t pad_in_config[2] = { 0, 0 }; + +/* Whether the pad on a slot is a config-capable DualShock (1) or a plain + * digital controller (0). A real SCPH-1080 digital pad (poll id 0x41) does NOT + * answer the config-mode commands (0x43/0x44/.../0x4F): it returns hi-z and the + * transaction ends. A game's pad driver that probes with 0x43 to detect a + * DualShock therefore classifies a digital pad as digital-only and just polls + * it with 0x42. Tomba 2's driver probes this way every frame; when the SM + * (wrongly) answered 0x43 for its digital pad it went down the DualShock config + * path and read the 0x00 config-response bytes as buttons -> phantom "all + * pressed" input. Default 1 keeps analog/hybrid pads unchanged; main.cpp sets 0 + * for PAD_MODE_DIGITAL. */ +static uint8_t pad_supports_config[2] = { 1, 1 }; /* Coherent-DualShock model (Tomba phantom-input fix). A real controller never * changes its reported type (0x41 digital <-> 0x73 analog) in the middle of a @@ -134,103 +109,7 @@ static uint8_t pad_supports_config[PSX_MAX_PLAYERS] = { * host REQUESTS a type via pad_type_req[] and the change is applied atomically * only when the bus is idle (PAD_IDLE) and the pad is NOT in config mode. A * request raised during config is held until config exits. -1 = no request. */ -static int8_t pad_type_req[PSX_MAX_PLAYERS] = { - [0 ... PSX_MAX_PLAYERS - 1] = -1 -}; - -/* ---- Logical pad ↔ physical SIO port mapping ---- - * - * Multitap off (default / PSX_MAX_PLAYERS==2): - * physical 0 → logical 0, physical 1 → logical 1 - * Multitap on Port 1 (sio_multitap_port==0): - * physical 0 → multitap (pads A–D = logical 0–3; Slot A path = 0) - * physical 1 → logical 4 - * Multitap on Port 2 (sio_multitap_port==1): - * physical 0 → logical 0 - * physical 1 → multitap (pads A–D = logical 1–4; Slot A path = 1) - */ -static int sio_multitap_active(void) { -#if PSX_MAX_PLAYERS >= 5 - return sio_multitap_enabled; -#else - return 0; -#endif -} - -static int mtap_slot_a_logical(void) { - return (sio_multitap_port == 0) ? 0 : 1; -} - -static int mtap_standalone_logical(void) { - return (sio_multitap_port == 0) ? 4 : 0; -} - -static int pad_logical_for_port(int phys_port) { - if (phys_port < 0 || phys_port > 1) return -1; - if (sio_multitap_active()) { - if (phys_port == sio_multitap_port) - return mtap_slot_a_logical(); - return mtap_standalone_logical(); - } - return phys_port; -} - -/* Physical port answers when a device is present. Multitap itself is present - * whenever enabled (individual tap slots may still be empty). */ -static int pad_port_has_device(int phys_port) { - if (phys_port < 0 || phys_port > 1) return 0; - if (sio_multitap_active()) { - if (phys_port == sio_multitap_port) return 1; - return (pad_connected & (1u << mtap_standalone_logical())) ? 1 : 0; - } - return (pad_connected & (1u << phys_port)) ? 1 : 0; -} - -static int selected_is_mtap_port(void) { - return sio_multitap_active() && selected_slot == sio_multitap_port; -} - -/* After a completed 0x42 on the multitap port, arm the next response mode - * from the REQ bit seen this transfer and what we just returned (psx-spx). */ -static void mtap_finish_42(void) { - if (!selected_is_mtap_port() || pad_current_cmd != 0x42 || pad_mtap_addr != 0x01) - return; - if (!mtap_req_this) { - mtap_next_mode = MTAP_NEXT_SLOT_A; - } else if (mtap_returned == MTAP_NEXT_SLOT_A) { - mtap_next_mode = MTAP_NEXT_BULK; - } else if (mtap_returned == MTAP_NEXT_BULK) { - mtap_next_mode = MTAP_NEXT_GARBAGE; - } else { - mtap_next_mode = MTAP_NEXT_BULK; - } -} - -/* Fill 8-byte per-pad status block used in multitap bulk 0x42 responses. - * Disconnected → all 0xFF. Digital → 0x41 0x5A btnL btnH + 0xFF pad. - * Analog/config → 0x73/0xF3 0x5A btn + stick bytes. */ -static void pad_fill_status8(int logical, uint8_t out[8]) { - if (logical < 0 || logical >= PSX_MAX_PLAYERS || - !(pad_connected & (1u << logical))) { - memset(out, 0xFF, 8); - return; - } - const uint8_t id = pad_in_config[logical] ? 0xF3 - : (pad_analog[logical] ? 0x73 : 0x41); - const uint16_t btn = pad_buttons[logical]; - out[0] = id; - out[1] = 0x5A; - out[2] = (uint8_t)(btn & 0xFF); - out[3] = (uint8_t)(btn >> 8); - if (pad_analog[logical] || pad_in_config[logical]) { - out[4] = pad_stick[logical][2]; /* right X */ - out[5] = pad_stick[logical][3]; /* right Y */ - out[6] = pad_stick[logical][0]; /* left X */ - out[7] = pad_stick[logical][1]; /* left Y */ - } else { - out[4] = out[5] = out[6] = out[7] = 0xFF; - } -} +static int8_t pad_type_req[2] = { -1, -1 }; /* Memory card SIO state machine */ typedef enum { @@ -663,19 +542,11 @@ void sio_init(void) { pad_response_len = 0; pad_response_idx = 0; pad_current_cmd = 0; - pad_active_logical = 0; - for (int i = 0; i < PSX_MAX_PLAYERS; i++) { - pad_buttons[i] = 0xFFFF; - pad_analog[i] = 0; - pad_stick[i][0] = pad_stick[i][1] = pad_stick[i][2] = pad_stick[i][3] = 0x80; - pad_in_config[i] = 0; - pad_type_req[i] = -1; - analog_mode_locked[i] = 0; - pad_supports_config[i] = 1; - } + pad_buttons[0] = pad_buttons[1] = 0xFFFF; + pad_in_config[0] = pad_in_config[1] = 0; /* clear stale config latch on reset */ + pad_type_req[0] = pad_type_req[1] = -1; /* no pending host type change */ + analog_mode_locked[0] = analog_mode_locked[1] = 0; /* unlocked on reset */ pad_connected = 0; - /* Multitap enable/port are host preferences — leave them alone across - * sio_init so a soft reset does not drop the tap configuration. */ mc_state = MC_IDLE; for (int i = 0; i < 2; i++) { mc_slots[i].state = MC_IDLE; @@ -731,70 +602,19 @@ uint32_t sio_cycles_to_irq(uint32_t i_mask) { return best; } -void sio_set_multitap(int enabled) { -#if PSX_MAX_PLAYERS >= 5 - sio_multitap_enabled = enabled ? 1 : 0; - if (!enabled) { - mtap_next_mode = MTAP_NEXT_SLOT_A; - mtap_req_this = 0; - mtap_returned = MTAP_NEXT_SLOT_A; - } -#else - (void)enabled; - sio_multitap_enabled = 0; -#endif -} - -int sio_get_multitap(void) { - return sio_multitap_active(); -} - -void sio_set_multitap_port(int phys_port) { -#if PSX_MAX_PLAYERS >= 5 - sio_multitap_port = (phys_port == 1) ? 1 : 0; - mtap_next_mode = MTAP_NEXT_SLOT_A; - mtap_req_this = 0; - mtap_returned = MTAP_NEXT_SLOT_A; -#else - (void)phys_port; - sio_multitap_port = 0; -#endif -} - -int sio_get_multitap_port(void) { -#if PSX_MAX_PLAYERS >= 5 - return sio_multitap_port; -#else - return 0; -#endif -} - -int sio_pad_on_multitap(int logical_slot) { -#if PSX_MAX_PLAYERS >= 5 - if (!sio_multitap_active()) return 0; - if (logical_slot < 0 || logical_slot >= PSX_MAX_PLAYERS) return 0; - /* Opposite-port lone pad may stay DualShock; every tap slot is digital. */ - return (logical_slot == mtap_standalone_logical()) ? 0 : 1; -#else - (void)logical_slot; - return 0; -#endif -} - void sio_connect_pad(int slot) { - if (slot >= 0 && slot < PSX_MAX_PLAYERS) - pad_connected |= (uint8_t)(1u << slot); + if (slot >= 0 && slot <= 1) + pad_connected |= (1 << slot); } void sio_set_pad_connected(int slot, int connected) { - if (slot < 0 || slot >= PSX_MAX_PLAYERS) return; - if (connected) pad_connected |= (uint8_t)(1u << slot); - else pad_connected &= (uint8_t)~(1u << slot); + if (slot < 0 || slot > 1) return; + if (connected) pad_connected |= (uint8_t)(1 << slot); + else pad_connected &= (uint8_t)~(1 << slot); } void sio_set_pad_config_capable(int slot, int capable) { - if (slot < 0 || slot >= PSX_MAX_PLAYERS) return; - if (sio_pad_on_multitap(slot)) capable = 0; + if (slot < 0 || slot > 1) return; pad_supports_config[slot] = capable ? 1 : 0; /* A plain digital pad can never be in config mode; clear any stale latch so * the next poll reports the digital id (0x41), not the config id (0xF3). */ @@ -806,7 +626,7 @@ void sio_set_pad_state(uint16_t buttons) { } void sio_set_pad_state_slot(int slot, uint16_t buttons) { - if (slot >= 0 && slot < PSX_MAX_PLAYERS) pad_buttons[slot] = buttons; + if (slot >= 0 && slot <= 1) pad_buttons[slot] = buttons; } /* Direct set of pad type + sticks. Used at boot/hotplug (refresh_player_devices) @@ -816,11 +636,7 @@ void sio_set_pad_state_slot(int slot, uint16_t buttons) { * coherently (see pad_type_req[] above). */ void sio_set_pad_analog(int slot, int enabled, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry) { - if (slot < 0 || slot >= PSX_MAX_PLAYERS) return; - if (sio_pad_on_multitap(slot)) { - enabled = 0; - lx = ly = rx = ry = 0x80; - } + if (slot < 0 || slot > 1) return; pad_analog[slot] = enabled ? 1 : 0; pad_type_req[slot] = -1; /* explicit set supersedes any pending request */ pad_stick[slot][0] = lx; pad_stick[slot][1] = ly; @@ -829,7 +645,7 @@ void sio_set_pad_analog(int slot, int enabled, /* Per-frame stick update (does not touch the reported pad type). */ void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry) { - if (slot < 0 || slot >= PSX_MAX_PLAYERS) return; + if (slot < 0 || slot > 1) return; pad_stick[slot][0] = lx; pad_stick[slot][1] = ly; pad_stick[slot][2] = rx; pad_stick[slot][3] = ry; } @@ -838,8 +654,7 @@ void sio_set_pad_sticks(int slot, uint8_t lx, uint8_t ly, uint8_t rx, uint8_t ry * deferred and applied atomically at the next idle, non-config boundary, so it * can never split a poll or a config handshake. A no-op if already that type. */ void sio_request_pad_type(int slot, int analog) { - if (slot < 0 || slot >= PSX_MAX_PLAYERS) return; - if (sio_pad_on_multitap(slot)) analog = 0; + if (slot < 0 || slot > 1) return; int want = analog ? 1 : 0; pad_type_req[slot] = (pad_analog[slot] == want) ? -1 : (int8_t)want; } @@ -849,21 +664,21 @@ uint16_t sio_get_pad_buttons(void) { } uint16_t sio_get_pad_buttons_slot(int slot) { - return (slot >= 0 && slot < PSX_MAX_PLAYERS) ? pad_buttons[slot] : 0xFFFF; + return (slot >= 0 && slot <= 1) ? pad_buttons[slot] : 0xFFFF; } int sio_get_pad_connected(int slot) { - if (slot < 0 || slot >= PSX_MAX_PLAYERS) return 0; - return (pad_connected & (1u << slot)) ? 1 : 0; + if (slot < 0 || slot > 1) return 0; + return (pad_connected & (1 << slot)) ? 1 : 0; } int sio_get_pad_analog(int slot) { - return (slot >= 0 && slot < PSX_MAX_PLAYERS) ? pad_analog[slot] : 0; + return (slot >= 0 && slot <= 1) ? pad_analog[slot] : 0; } void sio_get_pad_sticks(int slot, uint8_t out[4]) { if (!out) return; - if (slot < 0 || slot >= PSX_MAX_PLAYERS) { + if (slot < 0 || slot > 1) { out[0] = out[1] = out[2] = out[3] = 0x80; return; } @@ -871,69 +686,6 @@ void sio_get_pad_sticks(int slot, uint8_t out[4]) { out[2] = pad_stick[slot][2]; out[3] = pad_stick[slot][3]; } -/* ── LEGACY pad-config compatibility (Tomba "Hybrid" controller) ───────────── - * - * Why this exists, and why it is explicitly LEGACY: - * - * This is the descendant of our FIRST controller implementation. It was built - * for Tomba, to reproduce the seamless analog/digital feel of Tomba: Special - * Edition — the launcher "Hybrid" mode flips the emulated pad's reported TYPE - * between digital (poll id 0x41) and DualShock/analog (poll id 0x73) as the - * player moves between the d-pad and the stick. In that first implementation - * the SIO pad answered the DualShock config-mode commands trivially (it always - * reported the config id 0xF3), and Tomba's Hybrid flip worked. - * - * We then matured the pad against Mega Man X6. MMX6's libpad probes the pad - * type via config mode (01 43 00 00 ...) BEFORE it ever polls, and the trivial - * "always 0xF3" answer WEDGED it: it looped the probe forever, never reached - * the 0x42 poll, and had no input. The fix (commit 98aa688) was a REAL - * DualShock config-mode state machine — report 0xF3 only while actually in - * config, track 0x43 enter/exit, answer the capability queries (0x45/0x46/ - * 0x47/0x4C) like the real pad. That "modern" SM is the correct behaviour, is - * what MMX6 and every other title needs, and is the default. - * - * But post-MMX6, under the modern SM, Tomba's Hybrid flip regressed. Any type - * change makes libpad re-run findpad / re-detect the pad; under the modern SM - * that re-detect manufactures a one-frame "pad unplugged" (buf[0] = 0xFF). - * Tomba reads it as a controller disconnect and unpauses the menu / drops - * input; MMX6 reads the held direction as released-then-re-pressed and fires a - * phantom dash. We could NOT, with the modern SM, keep the flip benign. Rather - * than block Tomba's Hybrid feature, we kept the original behaviour available - * as a per-game opt-in — this flag. - * - * What the flag does: - * g_pad_legacy_cfg == 0 (default) -> modern DualShock config state machine. - * Required by MMX6; correct for every title. - * g_pad_legacy_cfg != 0 -> the pre-98aa688 behaviour: config commands - * always answer the config id 0xF3, with no - * enter/exit tracking. Tomba's libpad was - * written against exactly this, so its Hybrid - * flip re-detect is benign. - * - * Driven per-game by [controller] legacy_pad_config in game.toml (applied via - * sio_set_legacy_cfg() at boot). ONLY Tomba opts in. Because the default is 0, - * the modern path in pad_process_byte() below is byte-for-byte unchanged when - * the flag is off — no other title is affected. The `pad_cfg` debug command can - * also flip it live for A/B testing. - * - * THIS IS LEGACY — remove it once the behavioural mechanism is right. A real - * DualShock tolerates unlimited analog-button presses (type changes) with no - * disconnect, which proves the Hybrid flip CAN be benign under a correct state - * machine for every game, with no per-game compatibility branch. When that - * findpad/re-detect refactor lands, DELETE this whole legacy feature set — this - * flag, the g_pad_legacy_cfg-gated branches in pad_process_byte(), the - * legacy_pad_config config field, and the per-game game.toml opt-in — and let - * Tomba ride the modern SM like everything else. */ -volatile int g_pad_legacy_cfg = 0; -int sio_get_legacy_cfg(void) { return g_pad_legacy_cfg; } -void sio_set_legacy_cfg(int v) { - g_pad_legacy_cfg = v ? 1 : 0; - /* Clear any in-flight config latch so a mid-session toggle can't carry a - * stale 0xF3/8-byte poll into the other mode's dispatch. */ - for (int s = 0; s < PSX_MAX_PLAYERS; s++) - pad_in_config[s] = 0; -} - static void pad_process_byte(uint8_t tx_byte) { /* Apply any pending host type change (the emulated analog button) ONLY while * the bus is idle and the pad is not in config mode. This guarantees the @@ -941,7 +693,7 @@ static void pad_process_byte(uint8_t tx_byte) { * handshake — a hybrid stick/d-pad flip can never desync the game's driver * mid-transaction. A request raised during config stays pending until exit. */ if (pad_state == PAD_IDLE) { - for (int s = 0; s < PSX_MAX_PLAYERS; s++) { + for (int s = 0; s < 2; s++) { /* A game-LOCKED analog mode (0x44 ..03) ignores the physical analog * button — and our hybrid auto-flip IS that button — so a locked slot * drops the pending host request instead of applying it. */ @@ -953,17 +705,7 @@ static void pad_process_byte(uint8_t tx_byte) { } switch (pad_state) { case PAD_IDLE: - /* Standard address 01h selects Slot A (or the standalone pad). With a - * multitap, 02h..04h select pads B–D on that port (psx-spx method 2). */ - if (tx_byte == 0x01 && pad_port_has_device(selected_slot)) { - pad_active_logical = pad_logical_for_port(selected_slot); - pad_mtap_addr = 0x01; - pad_state = PAD_WAIT_ACCESS; - sio_rx_data = 0xFF; - sio_stat |= SIO_STAT_ACK; - } else if (selected_is_mtap_port() && tx_byte >= 0x02 && tx_byte <= 0x04) { - pad_active_logical = mtap_slot_a_logical() + (int)(tx_byte - 1); - pad_mtap_addr = tx_byte; + if (tx_byte == 0x01 && (pad_connected & (1 << selected_slot))) { pad_state = PAD_WAIT_ACCESS; sio_rx_data = 0xFF; sio_stat |= SIO_STAT_ACK; @@ -975,80 +717,31 @@ static void pad_process_byte(uint8_t tx_byte) { case PAD_WAIT_ACCESS: pad_current_cmd = tx_byte; pad_response_idx = 1; - mtap_req_this = 0; - /* SCPH-1070 method 1: only when address was 01h AND a prior transfer - * latched REQ=1. Otherwise Slot A (or garbage) — never force bulk on - * every 0x42 (that breaks P1 when the tap is present with one pad). */ - if (selected_is_mtap_port() && pad_mtap_addr == 0x01 && tx_byte == 0x42 && - mtap_next_mode == MTAP_NEXT_BULK) { - const int base = mtap_slot_a_logical(); - pad_response[0] = 0x80; - pad_response[1] = 0x5A; - for (int i = 0; i < 4; i++) - pad_fill_status8(base + i, &pad_response[2 + i * 8]); - pad_response_len = PAD_RESPONSE_MAX; - mtap_returned = MTAP_NEXT_BULK; - pad_state = PAD_SEND_RESPONSE; - sio_rx_data = pad_response[0]; - sio_stat |= SIO_STAT_ACK; - break; - } - if (selected_is_mtap_port() && pad_mtap_addr == 0x01 && tx_byte == 0x42 && - mtap_next_mode == MTAP_NEXT_GARBAGE) { - /* HiZ,80h,5Ah,LSB(Slot A id) then abort (psx-spx). */ - const int a = mtap_slot_a_logical(); - const uint8_t id = (!(pad_connected & (1u << a))) ? 0xFFu - : (pad_in_config[a] ? 0xF3u - : (pad_analog[a] ? 0x73u : 0x41u)); - pad_response[0] = 0x80; - pad_response[1] = 0x5A; - pad_response[2] = id; - pad_response_len = 3; - mtap_returned = MTAP_NEXT_GARBAGE; - pad_state = PAD_SEND_RESPONSE; - sio_rx_data = pad_response[0]; - sio_stat |= SIO_STAT_ACK; - break; - } - if (selected_is_mtap_port() && pad_mtap_addr == 0x01 && tx_byte == 0x42) - mtap_returned = MTAP_NEXT_SLOT_A; - /* Single-pad path (standalone port / Slot A / method-2 pad / non-0x42). */ - { - const int lp = pad_active_logical; - if (lp < 0 || lp >= PSX_MAX_PLAYERS || !(pad_connected & (1u << lp))) { - /* No pad on this logical slot (e.g. empty multitap A during a - * non-bulk command): hi-z, end transaction. */ - pad_state = PAD_IDLE; - pad_response_len = 0; - pad_response_idx = 0; - pad_current_cmd = 0; - sio_rx_data = 0xFF; - break; - } /* Controller ID reported as the first response byte. Real hardware * reports the config ID (0xF3) ONLY while in config mode; otherwise the * normal mode ID (0x41 digital / 0x73 analog). */ - const uint8_t cur_id = pad_in_config[lp] ? 0xF3 - : (pad_analog[lp] ? 0x73 : 0x41); + { + const uint8_t cur_id = pad_in_config[selected_slot] ? 0xF3 + : (pad_analog[selected_slot] ? 0x73 : 0x41); /* A plain digital controller (SCPH-1080) answers ONLY the 0x42 poll; it * ignores every config-mode command (returns hi-z, no ACK). A driver * that probes with 0x43 to detect a DualShock then classifies it as * digital-only and just polls. Gate all config branches on this so a * digital-mode pad behaves like real hardware (see pad_supports_config). */ - const int ds = pad_supports_config[lp]; + const int ds = pad_supports_config[selected_slot]; if (tx_byte == 0x42) { /* Read poll. Analog (or in-config) uses the 8-byte format with the * four stick axes; a plain digital pad uses the 4-byte format. */ - const uint16_t btn = pad_buttons[lp]; + const uint16_t btn = pad_buttons[selected_slot]; pad_response[0] = cur_id; pad_response[1] = 0x5A; pad_response[2] = (uint8_t)(btn & 0xFF); pad_response[3] = (uint8_t)(btn >> 8); - if (pad_analog[lp] || pad_in_config[lp]) { - pad_response[4] = pad_stick[lp][2]; /* right X */ - pad_response[5] = pad_stick[lp][3]; /* right Y */ - pad_response[6] = pad_stick[lp][0]; /* left X */ - pad_response[7] = pad_stick[lp][1]; /* left Y */ + if (pad_analog[selected_slot] || pad_in_config[selected_slot]) { + pad_response[4] = pad_stick[selected_slot][2]; /* right X */ + pad_response[5] = pad_stick[selected_slot][3]; /* right Y */ + pad_response[6] = pad_stick[selected_slot][0]; /* left X */ + pad_response[7] = pad_stick[selected_slot][1]; /* left Y */ pad_response_len = 8; } else { pad_response_len = 4; @@ -1060,17 +753,9 @@ static void pad_process_byte(uint8_t tx_byte) { /* Enter/exit config mode. The ID byte reflects the CURRENT mode; the * enter(0x01)/exit(0x00) flag is the second data byte, latched in * PAD_SEND_RESPONSE so it takes effect after this transaction. */ - const uint16_t btn = pad_buttons[lp]; + const uint16_t btn = pad_buttons[selected_slot]; pad_response[1] = 0x5A; - if (g_pad_legacy_cfg) { - /* LEGACY (pre-98aa688): always config ID 0xF3, zero frame, no - * enter/exit tracking. */ - pad_response[0] = 0xF3; - pad_response[2] = 0x00; pad_response[3] = 0x00; - pad_response[4] = 0x00; pad_response[5] = 0x00; - pad_response[6] = 0x00; pad_response[7] = 0x00; - pad_response_len = 8; - } else if (!pad_in_config[lp]) { + if (!pad_in_config[selected_slot]) { /* ENTER attempt (normal mode): a real DualShock transmits the LIVE * poll frame here — identical framing to 0x42 (dualshock.cpp:471-490) * — and only latches config entry from the 0x01 data byte AFTERWARD. @@ -1081,11 +766,11 @@ static void pad_process_byte(uint8_t tx_byte) { pad_response[0] = cur_id; pad_response[2] = (uint8_t)(btn & 0xFF); pad_response[3] = (uint8_t)(btn >> 8); - if (pad_analog[lp]) { - pad_response[4] = pad_stick[lp][2]; /* right X */ - pad_response[5] = pad_stick[lp][3]; /* right Y */ - pad_response[6] = pad_stick[lp][0]; /* left X */ - pad_response[7] = pad_stick[lp][1]; /* left Y */ + if (pad_analog[selected_slot]) { + pad_response[4] = pad_stick[selected_slot][2]; /* right X */ + pad_response[5] = pad_stick[selected_slot][3]; /* right Y */ + pad_response[6] = pad_stick[selected_slot][0]; /* left X */ + pad_response[7] = pad_stick[selected_slot][1]; /* left Y */ pad_response_len = 8; } else { pad_response_len = 4; @@ -1102,32 +787,7 @@ static void pad_process_byte(uint8_t tx_byte) { pad_state = PAD_SEND_RESPONSE; sio_rx_data = pad_response[0]; sio_stat |= SIO_STAT_ACK; - } else if (ds && g_pad_legacy_cfg && - (tx_byte == 0x45 || tx_byte == 0x46 || tx_byte == 0x47 || - tx_byte == 0x4C || tx_byte == 0x4D)) { - /* LEGACY config answers (pre-98aa688): canned 0xF3 responses given - * UNCONDITIONALLY (no config-mode gating). 0x44/0x4F had no handler - * then, so they fall through to the hi-z "no response" else below. */ - static const uint8_t r_45[8] = { 0xF3,0x5A,0x03,0x02,0x01,0x02,0x01,0x00 }; - static const uint8_t r_46[8] = { 0xF3,0x5A,0x00,0x00,0x01,0x02,0x00,0x0A }; - static const uint8_t r_47[8] = { 0xF3,0x5A,0x00,0x00,0x02,0x00,0x01,0x00 }; - static const uint8_t r_4c[8] = { 0xF3,0x5A,0x00,0x00,0x00,0x04,0x00,0x00 }; - static const uint8_t r_4d[8] = { 0xF3,0x5A,0x00,0x00,0x00,0x00,0x00,0x00 }; - const uint8_t *r = r_4d; - if (tx_byte == 0x45) r = r_45; - else if (tx_byte == 0x46) r = r_46; - else if (tx_byte == 0x47) r = r_47; - else if (tx_byte == 0x4C) r = r_4c; - memcpy(pad_response, r, 8); - /* 0x45 status byte must report the LIVE analog mode, not a fixed - * analog-on (dualshock.cpp:743) — see fix below for the modern path. */ - if (tx_byte == 0x45) - pad_response[3] = pad_analog[lp] ? 0x01 : 0x00; - pad_response_len = 8; - pad_state = PAD_SEND_RESPONSE; - sio_rx_data = pad_response[0]; - sio_stat |= SIO_STAT_ACK; - } else if (ds && !g_pad_legacy_cfg && pad_in_config[lp] && + } else if (ds && pad_in_config[selected_slot] && (tx_byte == 0x44 || tx_byte == 0x45 || tx_byte == 0x46 || tx_byte == 0x47 || tx_byte == 0x4C || tx_byte == 0x4D || tx_byte == 0x4F)) { @@ -1152,7 +812,7 @@ static void pad_process_byte(uint8_t tx_byte) { * driver mis-parse the poll frame length → off-by-frame garbage buttons * (axis5_sio_controller.md D8). */ if (tx_byte == 0x45) - pad_response[3] = pad_analog[lp] ? 0x01 : 0x00; + pad_response[3] = pad_analog[selected_slot] ? 0x01 : 0x00; pad_response_len = 8; pad_state = PAD_SEND_RESPONSE; sio_rx_data = pad_response[0]; @@ -1170,50 +830,40 @@ static void pad_process_byte(uint8_t tx_byte) { break; case PAD_SEND_RESPONSE: - /* TAP/REQ (third command byte, paired with idhi/5Ah at idx==1): does not - * change *this* response; it arms the next 0x42 on the multitap port. */ - if (selected_is_mtap_port() && pad_current_cmd == 0x42 && - pad_mtap_addr == 0x01 && pad_response_idx == 1) - mtap_req_this = (tx_byte == 0x01) ? 1 : 0; /* For 0x43 (enter/exit config), the data byte selecting enter(0x01)/ * exit(0x00) arrives paired with response index 2. Latch the new config * state; it takes effect from the next transaction (the ID byte already * reported the mode that was current at the start of this one). */ - if (!g_pad_legacy_cfg && pad_current_cmd == 0x43 && pad_response_idx == 2 && - pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS) - pad_in_config[pad_active_logical] = (tx_byte == 0x01) ? 1 : 0; + if (pad_current_cmd == 0x43 && pad_response_idx == 2) + pad_in_config[selected_slot] = (tx_byte == 0x01) ? 1 : 0; /* 0x44 set-mode (game owns the analog/digital mode): the mode byte rides * in the same slot as 0x43's enter/exit flag (data position 3). 0x01 => * analog (0x73), 0x00 => digital (0x41). Honouring it makes the pad * coherent — the type the game just selected is the type it then polls, * instead of the host hybrid silently winning. Drop any stale host * request so it can't immediately undo the game's choice. */ - if (!g_pad_legacy_cfg && pad_current_cmd == 0x44 && pad_response_idx == 2 && - pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS) { - pad_analog[pad_active_logical] = (tx_byte == 0x01) ? 1 : 0; - pad_type_req[pad_active_logical] = -1; + if (pad_current_cmd == 0x44 && pad_response_idx == 2) { + pad_analog[selected_slot] = (tx_byte == 0x01) ? 1 : 0; + pad_type_req[selected_slot] = -1; } /* 0x44 lock byte (data position 4, the byte after the mode byte): 0x03 => * lock analog mode, 0x02 => unlock (dualshock.cpp:714-725). A locked slot * ignores the host hybrid auto-flip (see analog_mode_locked). */ - if (!g_pad_legacy_cfg && pad_current_cmd == 0x44 && pad_response_idx == 3 && - pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS) { - if (tx_byte == 0x03) analog_mode_locked[pad_active_logical] = 1; - else if (tx_byte == 0x02) analog_mode_locked[pad_active_logical] = 0; + if (pad_current_cmd == 0x44 && pad_response_idx == 3) { + if (tx_byte == 0x03) analog_mode_locked[selected_slot] = 1; + else if (tx_byte == 0x02) analog_mode_locked[selected_slot] = 0; } if (pad_response_idx < pad_response_len) { sio_rx_data = pad_response[pad_response_idx++]; if (pad_response_idx < pad_response_len) { sio_stat |= SIO_STAT_ACK; } else { - mtap_finish_42(); pad_state = PAD_IDLE; pad_response_len = 0; pad_response_idx = 0; pad_current_cmd = 0; } } else { - mtap_finish_42(); pad_state = PAD_IDLE; pad_response_len = 0; pad_response_idx = 0; @@ -2479,19 +2129,13 @@ static int sio_snap_emit(PstW *w) { !pst_w_u16(w, sio_stat) || !pst_w_u16(w, sio_mode) || !pst_w_u16(w, sio_ctrl) || !pst_w_u16(w, sio_baud)) return 0; - /* Pad arrays sized by PSX_MAX_PLAYERS. Default MAX=2 keeps the historical - * 2-pad snap layout byte-identical. pad_response runtime buffer is larger - * for multitap bulk (34); snap still stores the first 8 bytes. */ - if (!pst_w_bytes(w, pad_analog, PSX_MAX_PLAYERS) || !pst_w_u8(w, pad_connected) || + if (!pst_w_bytes(w, pad_analog, 2) || !pst_w_u8(w, pad_connected) || !pst_w_u32(w, (uint32_t)pad_state) || !pst_w_i32(w, (int32_t)selected_slot) || !pst_w_bytes(w, pad_response, 8) || !pst_w_u8(w, pad_response_len) || !pst_w_u8(w, pad_response_idx) || !pst_w_u8(w, pad_current_cmd) || - !pst_w_bytes(w, pad_in_config, PSX_MAX_PLAYERS)) + !pst_w_bytes(w, pad_in_config, 2) || + !pst_w_i16(w, (int16_t)pad_type_req[0]) || !pst_w_i16(w, (int16_t)pad_type_req[1])) return 0; - for (int s = 0; s < PSX_MAX_PLAYERS; s++) { - if (!pst_w_i16(w, (int16_t)pad_type_req[s])) - return 0; - } if (!pst_w_u32(w, (uint32_t)mc_state) || !pst_w_i32(w, (int32_t)mc_slot) || !pst_w_u8(w, mc_cmd) || !pst_w_u16(w, mc_sector) || !pst_w_u8(w, mc_sector_msb) || !pst_w_u8(w, mc_sector_lsb) || @@ -2528,33 +2172,22 @@ static int sio_snap_emit(PstW *w) { static int sio_snap_parse(PstR *r) { uint32_t u; int32_t i; - int16_t tr; + int16_t tr0, tr1; if (!pst_r_u8(r, &sio_tx_data) || !pst_r_u8(r, &sio_rx_data) || !pst_r_u16(r, &sio_stat) || !pst_r_u16(r, &sio_mode) || !pst_r_u16(r, &sio_ctrl) || !pst_r_u16(r, &sio_baud)) return 0; - if (!pst_r_bytes(r, pad_analog, PSX_MAX_PLAYERS) || !pst_r_u8(r, &pad_connected) || + if (!pst_r_bytes(r, pad_analog, 2) || !pst_r_u8(r, &pad_connected) || !pst_r_u32(r, &u) || !pst_r_i32(r, &i) || !pst_r_bytes(r, pad_response, 8) || !pst_r_u8(r, &pad_response_len) || !pst_r_u8(r, &pad_response_idx) || !pst_r_u8(r, &pad_current_cmd) || - !pst_r_bytes(r, pad_in_config, PSX_MAX_PLAYERS)) + !pst_r_bytes(r, pad_in_config, 2) || + !pst_r_i16(r, &tr0) || !pst_r_i16(r, &tr1)) return 0; pad_state = (PadState)u; selected_slot = (int)i; - pad_active_logical = pad_logical_for_port(selected_slot); - /* Multitap bulk responses are 34 bytes; snap only stores 8. Abort an - * in-flight bulk restore rather than feed a truncated frame. */ - if (pad_response_len > 8) { - pad_state = PAD_IDLE; - pad_response_len = 0; - pad_response_idx = 0; - pad_current_cmd = 0; - } - for (int s = 0; s < PSX_MAX_PLAYERS; s++) { - if (!pst_r_i16(r, &tr)) - return 0; - pad_type_req[s] = (int8_t)tr; - } + pad_type_req[0] = (int8_t)tr0; + pad_type_req[1] = (int8_t)tr1; if (!pst_r_u32(r, &u) || !pst_r_i32(r, &i) || !pst_r_u8(r, &mc_cmd) || !pst_r_u16(r, &mc_sector) || !pst_r_u8(r, &mc_sector_msb) || !pst_r_u8(r, &mc_sector_lsb) || !pst_r_bytes(r, mc_data, 128)) diff --git a/runtime/src/starvation_ring.c b/runtime/src/starvation_ring.c index d4274f68..1a0bced4 100644 --- a/runtime/src/starvation_ring.c +++ b/runtime/src/starvation_ring.c @@ -11,7 +11,6 @@ #include "starvation_ring.h" #include "psx_cycles.h" -#include "psx_netplay.h" #include #include #include @@ -191,26 +190,18 @@ void starvation_ring_dump(const char *path) { if (!f) return; uint64_t total = s_seq; uint64_t avail = total < STARVATION_RING_CAP ? total : STARVATION_RING_CAP; - { - char net_arch[24]; - int max_players = 0, player_count = 0; - (void)psx_netplay_diag_snapshot(net_arch, sizeof(net_arch), - &max_players, &player_count); - fprintf(f, "{\"meta\":{\"total\":%llu,\"shown\":%llu," - "\"last_heartbeat_us\":%llu,\"now_us\":%llu," - "\"psx_cycle_count\":%llu," - "\"current_func\":\"0x%08X\",\"last_store_pc\":\"0x%08X\"," - "\"in_exception\":%u,\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\"," - "\"net_arch\":\"%s\",\"max_players\":%d,\"player_count\":%d}}\n", - (unsigned long long)total, (unsigned long long)avail, - (unsigned long long)s_last_heartbeat_us, - (unsigned long long)host_us_now(), - (unsigned long long)psx_get_cycle_count(), - g_debug_current_func_addr, g_debug_last_store_pc, - (unsigned)psx_get_in_exception(), - i_stat, i_mask, - net_arch, max_players, player_count); - } + fprintf(f, "{\"meta\":{\"total\":%llu,\"shown\":%llu," + "\"last_heartbeat_us\":%llu,\"now_us\":%llu," + "\"psx_cycle_count\":%llu," + "\"current_func\":\"0x%08X\",\"last_store_pc\":\"0x%08X\"," + "\"in_exception\":%u,\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\"}}\n", + (unsigned long long)total, (unsigned long long)avail, + (unsigned long long)s_last_heartbeat_us, + (unsigned long long)host_us_now(), + (unsigned long long)psx_get_cycle_count(), + g_debug_current_func_addr, g_debug_last_store_pc, + (unsigned)psx_get_in_exception(), + i_stat, i_mask); uint64_t start = total > avail ? total - avail : 0; for (uint64_t i = start; i < total; i++) { StarvationEntry *e = &s_ring[i & (STARVATION_RING_CAP - 1)]; diff --git a/runtime/tests/test_mod_controller_override.py b/runtime/tests/test_mod_controller_override.py index 9e71aacd..3678a91f 100644 --- a/runtime/tests/test_mod_controller_override.py +++ b/runtime/tests/test_mod_controller_override.py @@ -19,15 +19,18 @@ assert symbol in HEADER, f"missing trusted-plugin controller API: {symbol}" reset = """g_mod_controller_mode_override[0] = -1; - g_mod_controller_mode_override[1] = -1; - mod_runtime_activate_plugins();""" + g_mod_controller_mode_override[1] = -1;""" +activate = "mod_runtime_activate_plugins();" apply = """if (g_mod_controller_mode_override[0] >= 0) p1_mode = g_mod_controller_mode_override[0]; if (g_mod_controller_mode_override[1] >= 0) p2_mode = g_mod_controller_mode_override[1];""" assert reset in MAIN, "controller overrides must reset before every activation pass" +assert activate in MAIN, "plugins must activate after controller overrides reset" assert apply in MAIN, "controller overrides must replace the resolved launch modes" -assert MAIN.index(reset) < MAIN.index(apply), "reset/activate must precede application" +assert MAIN.index(reset) < MAIN.index(activate) < MAIN.index(apply), ( + "reset/activate must precede controller override application" +) print("mod controller override lifecycle guard passed")