Revert "Merge pull request #87 from mstan/feat/ui-5p"

This reverts commit e01347cbca, reversing
changes made to 60cb60eee6.
This commit is contained in:
Matthew Stanley
2026-07-31 15:37:57 -07:00
parent c03fcf4746
commit 987c6eeced
48 changed files with 1941 additions and 8565 deletions
-205
View File
@@ -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.
-4
View File
@@ -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.
+44 -93
View File
@@ -617,21 +617,6 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
rt.deadzone = static_cast<int>(n);
rt.has_deadzone = true;
}
if (ct.contains("multitap_port")) {
const auto n = toml::find<int64_t>(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<int>(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<bool>(ct, "legacy_pad_config");
}
if (ct.contains("anti_deadzone")) {
const auto n = toml::find<int64_t>(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<std::string>(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<bool>(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<std::string>(ct, kModeKeys[i]), PAD_MODE_HYBRID);
s.has_p_mode[i] = true;
});
if (ct.contains(kDzKeys[i])) try_get([&]{
const auto n = toml::find<int64_t>(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<std::string>(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<std::string>(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<bool>(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<bool>(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<std::string>(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<std::string>(ct, "p2_mode"), PAD_MODE_HYBRID);
s.has_p2_mode = true;
});
if (ct.contains("deadzone")) try_get([&]{
const auto n = toml::find<int64_t>(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) {
+13 -36
View File
@@ -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)
// "<GUID>" — 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.
+4 -21
View File
@@ -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
+4 -23
View File
@@ -1,7 +1,6 @@
#ifndef PSX_BOOT_STATE_H
#define PSX_BOOT_STATE_H
#include <stddef.h>
#include <stdint.h>
#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,
-12
View File
@@ -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);
-10
View File
@@ -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. */
+2 -6
View File
@@ -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);
-15
View File
@@ -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.
-3
View File
@@ -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). */
-22
View File
@@ -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). */
-2
View File
@@ -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
}
-5
View File
@@ -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
-3
View File
@@ -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();
-6
View File
@@ -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
-9
View File
@@ -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;
+2 -10
View File
@@ -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
+8 -10
View File
@@ -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 <exe_dir>/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). */
+1 -69
View File
@@ -1,7 +1,6 @@
#ifndef PSX_LOBBY_CLIENT_H
#define PSX_LOBBY_CLIENT_H
#include <stddef.h>
#include <stdint.h>
#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. */
+8 -64
View File
@@ -1,7 +1,6 @@
#ifndef PSX_NETPLAY_H
#define PSX_NETPLAY_H
#include <stddef.h>
#include <stdint.h>
#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);
-13
View File
@@ -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);
+14 -47
View File
@@ -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]);
+1 -27
View File
@@ -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
$<$<BOOL:${PSX_SDL3}>:PSX_SDL3=1>
$<$<CXX_COMPILER_ID:MSVC>: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})
+62 -325
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h>
#if defined(_WIN32)
# include <windows.h>
#else
# include <time.h>
#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;
}
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;
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_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 (!read_header_le(f, &h)) { fclose(f); return 0; }
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 (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<<BS_SEC_CPU)|(1u<<BS_SEC_RAM)|(1u<<BS_SEC_SPAD)|(1u<<BS_SEC_IRQ)|
(1u<<BS_SEC_TIMER)|(1u<<BS_SEC_CLOCK)|(1u<<BS_SEC_GPU)|(1u<<BS_SEC_VRAM)|
(1u<<BS_SEC_SPU)|(1u<<BS_SEC_SPURAM)|(1u<<BS_SEC_CDROM)|(1u<<BS_SEC_DMA)|
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_MDEC)|(1u<<BS_SEC_DIRTY);
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_DIRTY);
uint32_t seen = 0;
int ok = 1;
const double t0 = boot_state_mono_ms();
double inflate_ms = 0.0;
double apply_ram_ms = 0.0;
double apply_vram_ms = 0.0;
double apply_spuram_ms = 0.0;
double apply_other_ms = 0.0;
if (!boot_state_check_buffer(file, file_len, bios_checksum, entry_pc,
reject, sizeof(reject))) {
fprintf(stderr, "boot_state: reject — %s\n",
reject[0] ? reject : "unknown");
return 0;
}
if (!boot_state_parse_header(file, file_len, &h))
return 0;
cur = file + BOOT_STATE_HEADER_WIRE_BYTES;
end = file + file_len;
for (uint32_t i = 0; ok && i < h.section_count; i++) {
PstR sh;
uint8_t shdr[16];
PstR hr;
uint32_t tag = 0, pad = 0;
uint64_t len = 0;
const uint8_t* payload;
uint8_t* inflated = NULL;
const uint8_t* apply_ptr;
uint32_t apply_len;
double t_sec;
if ((size_t)(end - cur) < 16u) { ok = 0; break; }
pst_r_init(&sh, cur, 16);
if (!pst_r_u32(&sh, &tag) || !pst_r_u32(&sh, &pad) || !pst_r_u64(&sh, &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;
}
cur += 16;
if (len > 64u * 1024u * 1024u || (uint64_t)(end - cur) < 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);
-59
View File
@@ -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
-4
View File
@@ -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
+4 -20
View File
@@ -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
+102 -191
View File
@@ -69,12 +69,12 @@
#include "gpu_gl_renderer.h"
#include "latency_ring.h"
#include "psx_sdl.h"
#if defined(PSX_SDL3)
#include <SDL3/SDL_opengl.h>
#else
#include <SDL_opengl.h>
#endif
#include "psx_sdl.h"
#if defined(PSX_SDL3)
#include <SDL3/SDL_opengl.h>
#else
#include <SDL_opengl.h>
#endif
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
@@ -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();
-80
View File
@@ -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);
-39
View File
@@ -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++)
-46
View File
@@ -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'
-5
View File
@@ -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;
}
+2 -20
View File
@@ -13,24 +13,6 @@
#include <sys/socket.h>
#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;
+1302 -3247
View File
File diff suppressed because it is too large Load Diff
-138
View File
@@ -1,5 +1,4 @@
#include "mdec.h"
#include "pst_wire.h"
#include <stdint.h>
#include <stdlib.h>
@@ -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;
}
+2 -27
View File
@@ -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 <memcard>/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);
(void)_mkdir(dir);
#else
(void)mkdir(tmp, 0755);
#endif
tmp[i] = '/';
}
}
#ifdef _WIN32
(void)_mkdir(tmp);
#else
(void)mkdir(tmp, 0755);
(void)mkdir(dir, 0755);
#endif
}
-14
View File
@@ -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;
-36
View File
@@ -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;
+10 -40
View File
@@ -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++;
+1 -51
View File
@@ -3,7 +3,6 @@
#include "psx_cycles.h"
#include "cpu_state.h"
#include <stdlib.h>
#include <string.h>
#if defined(_MSC_VER)
#include <intrin.h> /* 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) {
+47 -69
View File
@@ -9,7 +9,6 @@
#include "psx_keybinds.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stddef.h>
@@ -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) {
File diff suppressed because it is too large Load Diff
+97 -1372
View File
File diff suppressed because it is too large Load Diff
+34 -196
View File
@@ -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 <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <direct.h>
#include <windows.h>
#else
#include <sys/stat.h>
#include <time.h>
#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,52 +26,21 @@ 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);
(void)_mkdir(dir);
#else
(void)mkdir(tmp, 0755);
(void)mkdir(dir, 0755);
#endif
tmp[i] = '/';
}
}
#ifdef _WIN32
(void)_mkdir(tmp);
#else
(void)mkdir(tmp, 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) {
@@ -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);
}
}
}
+95 -462
View File
@@ -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 */
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 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];
/* 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 logical pads have devices connected (bit i = pad i). Fits 5 pads. */
/* 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];
/* 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 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
};
/* 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))
+12 -21
View File
@@ -11,7 +11,6 @@
#include "starvation_ring.h"
#include "psx_cycles.h"
#include "psx_netplay.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -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)];
@@ -19,15 +19,18 @@ for symbol in (
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")