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

Feat/UI 5p
This commit is contained in:
Alex Vanderveen
2026-07-29 02:48:42 -04:00
committed by GitHub
47 changed files with 8548 additions and 1934 deletions
+205
View File
@@ -0,0 +1,205 @@
# 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,3 +40,7 @@ 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.
+93 -44
View File
@@ -617,6 +617,21 @@ 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)
@@ -2077,39 +2092,56 @@ UserSettings load_user_settings(const fs::path& path) {
}
if (doc.contains("controller")) {
const toml::value& ct = toml::find(doc, "controller");
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;
});
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("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; }
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;
}
}
}
});
}
return s;
@@ -2205,19 +2237,36 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
f << "enable2 = " << (s.memcard2_enabled ? "true" : "false") << "\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";
{
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_language) {
+36 -13
View File
@@ -454,6 +454,22 @@ 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.
@@ -991,22 +1007,29 @@ 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. device is one of:
// [controller] — per-player input device + pad type + deadzone.
// 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)
// 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;
// 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;
// Localization: the launcher's chosen language code (feeds RuntimeConfig
// .language / g_lang). "off"/"jp"/"" = untranslated native game. Persisted to
// settings.toml [localization].language.
+23 -4
View File
@@ -1,6 +1,7 @@
#ifndef PSX_BOOT_STATE_H
#define PSX_BOOT_STATE_H
#include <stddef.h>
#include <stdint.h>
#include "cpu_state.h"
@@ -36,8 +37,13 @@ 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). */
#define BOOT_STATE_VERSION 3u
* 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
/*
* On-disk header (v3): nine little-endian uint32 fields at offset 0 (36 bytes),
@@ -61,11 +67,12 @@ typedef struct {
#define BOOT_STATE_HEADER_WIRE_BYTES 36u
/*
* Section stream (v3): section_count records, each laid out as
* Section stream (v3/v4): section_count records, each laid out as
* uint32_t tag; LE (one of BS_SEC_*)
* uint32_t pad; LE 0
* uint32_t pad; LE flags (v3: 0; v4: BOOT_STATE_SEC_ZLIB optional)
* 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.
*/
@@ -84,6 +91,7 @@ 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. */
@@ -95,6 +103,17 @@ 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,6 +79,18 @@ 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,10 +117,20 @@ 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. */
+6 -2
View File
@@ -47,14 +47,18 @@ 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 crtc_w when unknown / full coverage. Present uses this to blank a
* trailing margin without shrinking the CRTC-derived width globally. */
* 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). */
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,6 +65,21 @@ 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,6 +45,9 @@ 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,6 +24,12 @@ 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
@@ -43,6 +49,17 @@ 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);
@@ -70,6 +87,11 @@ 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,6 +31,8 @@ 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,6 +79,11 @@ 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,6 +18,9 @@ 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,6 +121,12 @@ 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,6 +131,11 @@ 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;
@@ -155,6 +160,10 @@ 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;
+10 -2
View File
@@ -147,9 +147,17 @@ 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. */
void psx_cycles_resync_after_restore(void);
* 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);
/* Soft rematch / session_reboot: zero the guest clock and deadline bookkeeping.
* Soft-exit longjmps out of vblank (inside psx_devices_service_to_now) leave
+10 -8
View File
@@ -19,15 +19,18 @@ 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).
*
* 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).
* 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.
*
* 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. */
@@ -53,8 +56,7 @@ typedef struct {
} PsxPlayerBinds;
typedef struct {
PsxPlayerBinds p1;
PsxPlayerBinds p2;
PsxPlayerBinds player[PSXKB_MAX_PLAYERS]; /* [0]=P1 .. [4]=P5 */
} PsxKeyBinds;
/* Initialize from <exe_dir>/keybinds.ini. Generates a default file if one does
@@ -65,7 +67,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 or 2 ──────────── */
/* ── Runtime read helpers (keyboard -> PSX pad), player is 1..PSXKB_MAX_PLAYERS */
/* 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
@@ -85,13 +87,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). */
* keycodes (keybinds.ini stores SDL scancode names). Players are 1-based. */
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 (P2 = all unbound). */
/* Reset one player's bindings to the built-in defaults (same map for every slot). */
void psx_keybinds_reset_player(int player);
/* Persist the current bindings to keybinds.ini (path resolved by
* psx_keybinds_init; call that first). */
+69 -1
View File
@@ -1,6 +1,7 @@
#ifndef PSX_LOBBY_CLIENT_H
#define PSX_LOBBY_CLIENT_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
@@ -12,7 +13,8 @@ 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 4
#define PSX_LOBBY_MAX_MEMBERS 8
#define PSX_LOBBY_MAX_LAN_EPS 4
#define PSX_LOBBY_LANG_LEN 16
#ifndef PSX_GAME_VERSION
@@ -27,6 +29,13 @@ 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 {
@@ -49,6 +58,8 @@ 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;
@@ -90,6 +101,9 @@ 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);
@@ -111,8 +125,16 @@ 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);
@@ -126,6 +148,52 @@ 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. */
+64 -8
View File
@@ -1,6 +1,7 @@
#ifndef PSX_NETPLAY_H
#define PSX_NETPLAY_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
@@ -8,8 +9,8 @@ extern "C" {
#endif
/*
* 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.
* 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.
*
* Lockstep contract (matches recomp-net host_integration.md):
* wait_admit (publish pads for tick T) → guest runs frame T →
@@ -20,10 +21,11 @@ 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 (0/1) to sample; -1 = auto
* - input_player selects which host PlayerInput 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 2P/VS detect.
* - Every session slot stays plugged for in-game N-player detect.
* - slot_count >= 3 enables SCPH-1070 multitap (sio_set_multitap).
*
* Pad blob (8 bytes):
* [0..1] buttons LE u16 (PSX active-low)
@@ -43,9 +45,16 @@ typedef struct PsxNetPad {
typedef struct PsxNetplayConfig {
int enabled;
int local_slot; /* 0 or 1 */
int input_player; /* 0/1 host device index; -1 = auto */
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 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];
@@ -56,11 +65,25 @@ 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 (0/1) used for local capture. */
/* Resolved host player index 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);
@@ -80,9 +103,14 @@ int psx_netplay_is_host(void);
int psx_netplay_request_save(int slot);
int psx_netplay_request_load(int slot);
/* 1 while post-load apply/ready barrier owns the clock (no FPS / no present). */
/* 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). */
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);
@@ -95,20 +123,48 @@ 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,6 +45,11 @@ 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.
@@ -58,12 +63,20 @@ 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);
+47 -14
View File
@@ -7,6 +7,16 @@
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
@@ -57,14 +67,36 @@ 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 port 1 (slot 0); the _slot form targets either. */
sio_set_pad_state targets logical pad 0; the _slot form targets
logical pad 0 .. PSX_MAX_PLAYERS-1. */
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 slot. enabled selects the
/* Set the analog stick state + pad type for a logical pad. 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,
@@ -79,22 +111,23 @@ 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 pad on a slot (0=port1, 1=port2). By default no pads
* are connected during initial BIOS boot. */
/* Connect / disconnect a logical pad (0 .. PSX_MAX_PLAYERS-1). 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 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 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. */
void sio_set_pad_config_capable(int slot, int capable);
/* Return current pad button state (for debug server). _slot targets either. */
/* Return current pad button state (for debug server). _slot targets a logical
* pad 0 .. PSX_MAX_PLAYERS-1. */
uint16_t sio_get_pad_buttons(void);
uint16_t sio_get_pad_buttons_slot(int slot);
@@ -106,7 +139,7 @@ uint16_t sio_peek_stat(void);
uint16_t sio_peek_ctrl(void);
uint8_t sio_peek_rx_data(void);
/* Debug accessors: is a pad connected on the slot, and is it in analog mode. */
/* Debug accessors: is a logical pad connected, 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]);
+27 -1
View File
@@ -280,6 +280,10 @@ 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")
@@ -486,6 +490,7 @@ 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
@@ -712,6 +717,10 @@ 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
@@ -733,6 +742,22 @@ 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}"
@@ -743,6 +768,7 @@ 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>
@@ -916,7 +942,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 dbghelp comdlg32 opengl32)
target_link_libraries(${target} PRIVATE ws2_32 iphlpapi dbghelp comdlg32 opengl32)
else()
if(CMAKE_DL_LIBS)
target_link_libraries(${target} PRIVATE ${CMAKE_DL_LIBS})
+326 -63
View File
@@ -1,11 +1,36 @@
#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)
@@ -16,9 +41,6 @@
/* ---- 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;
@@ -47,6 +69,9 @@ 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)
@@ -80,36 +105,44 @@ static int write_header_le(FILE* f, const BootStateHeader* h) {
return fwrite_all(f, buf, sizeof buf);
}
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) {
static int write_section_raw(FILE* f, uint32_t tag, uint32_t flags,
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, 0u) || !pst_w_u64(&w, len))
if (!pst_w_u32(&w, tag) || !pst_w_u32(&w, flags) || !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*)) {
@@ -178,7 +211,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 = 14;
h.section_count = 15;
int ok = write_header_le(f, &h);
@@ -208,7 +241,8 @@ 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. */
/* VRAM is uint16 LE guest pixels — emit as LE u16 stream.
* pst_w_pod is a memcpy on LE hosts (see pst_wire.h). */
uint8_t* wire = (uint8_t*)malloc(VRAM_SIZE);
if (!wire) ok = 0;
else {
@@ -226,6 +260,7 @@ 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;
@@ -269,6 +304,9 @@ 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:
@@ -325,19 +363,27 @@ 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;
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);
free(vbuf);
#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);
free(vbuf);
return 1;
}
#endif
}
case BS_SEC_SPU:
return spu_snapshot_read(p, len);
@@ -351,6 +397,8 @@ 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;
@@ -375,58 +423,273 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len,
}
}
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 int boot_state_parse_header(const uint8_t* file, size_t file_len,
BootStateHeader* h_out) {
PstR hr;
if (!file || !h_out || file_len < BOOT_STATE_HEADER_WIRE_BYTES ||
file_len > 64u * 1024u * 1024u) {
return 0;
}
pst_r_init(&hr, file, BOOT_STATE_HEADER_WIRE_BYTES);
memset(h_out, 0, sizeof(*h_out));
if (!pst_r_u32(&hr, &h_out->magic) ||
!pst_r_u32(&hr, &h_out->version) ||
!pst_r_u32(&hr, &h_out->bios_checksum) ||
!pst_r_u32(&hr, &h_out->entry_pc) ||
!pst_r_u32(&hr, &h_out->codegen_hash) ||
!pst_r_i32(&hr, &h_out->abi_tag) ||
!pst_r_u32(&hr, &h_out->codegen_ver) ||
!pst_r_u32(&hr, &h_out->section_count) ||
!pst_r_u32(&hr, &h_out->reserved)) {
return 0;
}
return 1;
}
static void boot_state_append_reason(char* reason, size_t reason_cap,
const char* part) {
size_t n;
if (!reason || reason_cap == 0 || !part || !part[0]) return;
n = strlen(reason);
if (n + 1 >= reason_cap) return;
if (n > 0) {
reason[n++] = ',';
reason[n] = '\0';
if (n + 1 >= reason_cap) return;
}
snprintf(reason + n, reason_cap - n, "%s", part);
}
int boot_state_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;
if (!read_header_le(f, &h)) { fclose(f); return 0; }
char part[96];
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);
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");
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_DIRTY);
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_MDEC)|(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++) {
uint8_t shdr[16];
PstR hr;
PstR sh;
uint32_t tag = 0, pad = 0;
uint64_t len = 0;
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)) {
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)) {
ok = 0; break;
}
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;
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;
else if (tag < 32) seen |= (1u << tag);
free(buf);
{
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);
}
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,6 +2661,65 @@ 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,6 +8,10 @@
* - 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
+20 -4
View File
@@ -2521,13 +2521,17 @@ 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) || s_d24_upload_x1 == 0u || crtc_w == 0u)
if (!(display_depth & 1u) || 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 crtc_w;
if (s_d24_upload_x1 <= dx) return 0u;
uint32_t hw = s_d24_upload_x1 - dx;
uint32_t rgb = (hw * 2u) / 3u;
if (rgb == 0u || rgb >= crtc_w) return crtc_w;
if (rgb == 0u) return 0u;
if (rgb >= crtc_w) return crtc_w;
return rgb;
}
@@ -2541,6 +2545,12 @@ 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
@@ -4848,10 +4858,13 @@ 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;
display_depth = (val >> 4) & 1;
if (new_depth != display_depth)
s_d24_upload_x1 = 0; /* rising/falling: drop stale coverage */
display_depth = new_depth;
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
@@ -4992,6 +5005,8 @@ 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
@@ -5024,6 +5039,7 @@ 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
+191 -102
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,6 +619,11 @@ 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;
@@ -628,6 +633,7 @@ 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) {
@@ -742,21 +748,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
@@ -2133,6 +2139,28 @@ 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();
@@ -2188,6 +2216,16 @@ 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);
@@ -2473,11 +2511,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]);
@@ -2596,10 +2634,16 @@ 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) {
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. */
/* 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. */
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 {
@@ -2611,6 +2655,7 @@ 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;
}
@@ -2625,6 +2670,7 @@ 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;
}
@@ -2642,6 +2688,45 @@ 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();
@@ -3253,13 +3338,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) {
@@ -3287,20 +3372,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) {
@@ -3415,9 +3500,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);
@@ -3461,46 +3546,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",
@@ -3510,10 +3595,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;
@@ -3554,6 +3639,7 @@ 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;
@@ -3591,6 +3677,7 @@ 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();
@@ -3654,6 +3741,7 @@ 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;
@@ -3687,6 +3775,7 @@ 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,7 +1533,69 @@ 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++) {
@@ -1556,6 +1618,24 @@ 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,6 +39,7 @@ 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; }
@@ -2259,6 +2260,24 @@ 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
@@ -2284,12 +2303,32 @@ 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,6 +450,9 @@ 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
@@ -607,6 +610,27 @@ 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;
}
/*
@@ -706,6 +730,23 @@ 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; }
@@ -740,6 +781,8 @@ 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.
@@ -749,6 +792,7 @@ 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);
@@ -795,6 +839,7 @@ 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);
@@ -956,6 +1001,7 @@ 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,3 +260,8 @@ 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;
}
+20 -2
View File
@@ -13,6 +13,24 @@
#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+/";
@@ -89,7 +107,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 (errno == EINTR) {
if (socket_interrupted()) {
continue;
}
return -1;
@@ -177,7 +195,7 @@ int rnet_ws_read_text(int fd, char *buf, size_t cap, int *closed)
return -1;
}
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
if (socket_would_block()) {
return 0;
}
return -1;
+3251 -1302
View File
File diff suppressed because it is too large Load Diff
+138
View File
@@ -1,4 +1,5 @@
#include "mdec.h"
#include "pst_wire.h"
#include <stdint.h>
#include <stdlib.h>
@@ -756,3 +757,140 @@ 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;
}
+27 -2
View File
@@ -83,12 +83,37 @@ 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
(void)_mkdir(dir);
if (i == 2 && tmp[1] == ':')
continue;
#endif
tmp[i] = '\0';
#ifdef _WIN32
(void)_mkdir(tmp);
#else
(void)mkdir(dir, 0755);
(void)mkdir(tmp, 0755);
#endif
tmp[i] = '/';
}
}
#ifdef _WIN32
(void)_mkdir(tmp);
#else
(void)mkdir(tmp, 0755);
#endif
}
+14
View File
@@ -578,6 +578,9 @@ 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 ---------
@@ -631,6 +634,17 @@ 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,6 +763,17 @@ 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();
}
@@ -790,6 +801,9 @@ RecompLauncherCModProvider provider = {
provider_feature_set_option,
provider_diagnostic_count,
provider_diagnostic_get,
nullptr, /* archive_extension — PSX defaults */
nullptr, /* archive_description */
provider_commit_netplay,
};
#endif
@@ -836,6 +850,28 @@ 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;
+40 -10
View File
@@ -2042,6 +2042,28 @@ 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. */
@@ -2072,15 +2094,19 @@ 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) return;
if (overlay_irq_suppressed_now()) return;
if (g_call_unit_depth > 0) { s_ci_skip_unit++; return; }
if (overlay_irq_suppressed_now()) { s_ci_skip_supp++; 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) return;
if ((cpu->cop0[12] & ((1u << 10) | 1u)) != ((1u << 10) | 1u)) return;
if (!psx_interrupt_delivery_needed(cpu)) 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 (s_irq_defer_cdrom && (i_stat & (1u << IRQ_CDROM))) {
uint32_t saved_cd = i_stat & (1u << IRQ_CDROM);
i_stat &= ~(1u << IRQ_CDROM);
@@ -2108,11 +2134,15 @@ 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) 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;
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++;
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++;
+51 -1
View File
@@ -3,6 +3,7 @@
#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
@@ -12,6 +13,9 @@
#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
@@ -107,6 +111,11 @@ 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;
@@ -158,6 +167,7 @@ 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;
@@ -198,6 +208,11 @@ 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;
@@ -300,6 +315,19 @@ 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) {
@@ -518,13 +546,35 @@ 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(void) {
void psx_cycles_resync_after_restore(CPUState *cpu) {
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) {
+69 -47
View File
@@ -9,6 +9,7 @@
#include "psx_keybinds.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <stddef.h>
@@ -34,45 +35,37 @@
/* ── Defaults ─────────────────────────────────────────────────────────────── */
/*
* 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:
* Every player slot uses the framework's historical hardcoded keyboard mapping
* (pad_from_keyboard / pad_sticks_for in main.cpp):
* 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 (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).
* 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.
*/
#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 { \
.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, \
.player = { \
PSXKB_PLAYER_DEFAULTS, \
PSXKB_PLAYER_DEFAULTS, \
PSXKB_PLAYER_DEFAULTS, \
PSXKB_PLAYER_DEFAULTS, \
PSXKB_PLAYER_DEFAULTS, \
}, \
}
@@ -217,15 +210,35 @@ 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"
"# 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"
"# Every player slot defaults to the same keyboard map. Rebind per slot\n"
"# for simultaneous multi-keyboard play (route a port to \"Keyboard\").\n"
"\n");
write_player_section(f, "player1", &s_binds.p1);
write_player_section(f, "player2", &s_binds.p2);
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]);
}
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;
@@ -239,8 +252,11 @@ static void load_ini(const char *path) {
if (end) *end = '\0';
const char *section = line + 1;
current = NULL;
if (!strcmp(section, "player1")) current = &s_binds.p1;
else if (!strcmp(section, "player2")) current = &s_binds.p2;
if (!strncmp(section, "player", 6)) {
int n = atoi(section + 6);
if (n >= 1 && n <= PSXKB_MAX_PLAYERS)
current = &s_binds.player[n - 1];
}
continue;
}
char *eq = strchr(line, '=');
@@ -258,6 +274,7 @@ static void load_ini(const char *path) {
}
}
fclose(f);
promote_empty_players();
printf("[Keybinds] Loaded %s\n", path);
}
@@ -273,10 +290,12 @@ 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) {
return (player == 2) ? &s_binds.p2 : &s_binds.p1;
if (player < 1 || player > PSXKB_MAX_PLAYERS) return &s_binds.player[0];
return &s_binds.player[player - 1];
}
static PsxPlayerBinds *player_binds(int player) {
return (player == 2) ? &s_binds.p2 : &s_binds.p1;
if (player < 1 || player > PSXKB_MAX_PLAYERS) return &s_binds.player[0];
return &s_binds.player[player - 1];
}
/* Is the scancode at button-def index i currently held for this player? */
@@ -286,7 +305,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) return 0xFFFF;
if (!keys || player < 1 || player > PSXKB_MAX_PLAYERS) 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++) {
@@ -297,7 +316,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) return;
if (!keys || !out || player < 1 || player > PSXKB_MAX_PLAYERS) 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;
@@ -310,7 +329,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) return 0;
if (!keys || player < 1 || player > PSXKB_MAX_PLAYERS) 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);
@@ -331,16 +350,19 @@ 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) {
*player_binds(player) = (player == 2) ? s_default_binds.p2 : s_default_binds.p1;
if (player < 1 || player > PSXKB_MAX_PLAYERS) return;
*player_binds(player) = s_default_binds.player[0];
}
void psx_keybinds_save(void) {
File diff suppressed because it is too large Load Diff
+1372 -97
View File
File diff suppressed because it is too large Load Diff
+197 -35
View File
@@ -7,18 +7,37 @@
#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;
@@ -26,21 +45,52 @@ 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 uint64_t s_load_cooldown_until_frame = 0;
static int s_load_cooldown_notice = 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;
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
(void)_mkdir(dir);
#else
(void)mkdir(dir, 0755);
/* Keep drive prefix "C:" intact — do not mkdir("C:"). */
if (i == 2 && tmp[1] == ':')
continue;
#endif
tmp[i] = '\0';
#ifdef _WIN32
(void)_mkdir(tmp);
#else
(void)mkdir(tmp, 0755);
#endif
tmp[i] = '/';
}
}
#ifdef _WIN32
(void)_mkdir(tmp);
#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) {
@@ -88,6 +138,28 @@ 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;
@@ -119,26 +191,50 @@ 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))) 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;
}
ensure_dir(s_dir);
f = fopen(path, "wb");
if (!f) return 0;
if (fwrite(data, 1, size, f) != size) {
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));
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;
}
static int netplay_savestate_blocked(void) {
/* 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) {
if (!psx_netplay_active()) return 0;
fprintf(stderr, "savestate: disabled during netplay\n");
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");
return 1;
}
@@ -152,13 +248,6 @@ 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. */
@@ -166,30 +255,60 @@ 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_savestate_blocked()) return 0;
if (netplay_user_blocked()) return 0;
return request_save_inner(slot);
}
int savestate_request_load(int slot) {
if (netplay_savestate_blocked()) return 0;
if (netplay_user_blocked()) return 0;
return request_load_inner(slot);
}
int savestate_request_save_protocol(int slot) {
if (netplay_savestate_blocked()) return 0;
/* Follow-host sync: guests must write the host-authoritative .pst. */
return request_save_inner(slot);
}
int savestate_request_load_protocol(int slot) {
if (netplay_savestate_blocked()) return 0;
/* Follow-host sync: guests must apply the host-authoritative .pst. */
clear_load_blob();
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;
}
@@ -200,6 +319,12 @@ 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 */
@@ -220,27 +345,64 @@ 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];
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);
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();
/* 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);
}
}
}
+462 -95
View File
@@ -42,27 +42,34 @@ static void sio_debug_poll_maybe(void) {
}
}
/* Pad state: 0=pressed, 1=released (PS1 convention). Per slot (port). */
static uint16_t pad_buttons[2] = { 0xFFFF, 0xFFFF }; /* all released */
/* 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 };
/* Per-slot pad type + analog stick state. analog: 0=digital pad (poll id
/* Per-logical-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[2] = { 0, 0 };
static uint8_t pad_stick[2][4] = { { 0x80, 0x80, 0x80, 0x80 },
{ 0x80, 0x80, 0x80, 0x80 } }; /* lx,ly,rx,ry */
static uint8_t pad_analog[PSX_MAX_PLAYERS];
static uint8_t pad_stick[PSX_MAX_PLAYERS][4] = {
[0 ... PSX_MAX_PLAYERS - 1] = { 0x80, 0x80, 0x80, 0x80 }
}; /* lx,ly,rx,ry */
/* Analog-mode lock, per 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 };
/* Analog-mode lock, per logical pad. A real DualShock's config command 0x44
* 0x..02/0x03 locks/unlocks the mode (dualshock.cpp:714-725); a locked pad
* ignores the physical analog button (dualshock.cpp:203). We emulate the
* analog button via the host hybrid heuristic (pad_type_req), so when a game
* LOCKS the mode the hybrid auto-flip must not override it — else the type
* flips underneath a game that pinned DualShock, the exact desync the
* deferred-request machinery cannot otherwise prevent. */
static uint8_t analog_mode_locked[PSX_MAX_PLAYERS];
/* Which slots have devices connected */
/* Which logical pads have devices connected (bit i = pad i). Fits 5 pads. */
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,
@@ -70,33 +77,51 @@ 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;
static uint8_t pad_response[8];
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 uint8_t pad_response_len = 0;
static uint8_t pad_response_idx = 0;
static uint8_t pad_current_cmd = 0;
/* 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 };
/* Address byte that opened this pad txn (01h=Slot A / bulk, 02h..04h=B..D). */
static uint8_t pad_mtap_addr = 0x01;
static int mtap_next_mode = MTAP_NEXT_SLOT_A;
static int mtap_req_this = 0; /* TAP==1 seen on current 0x42 txn */
static int mtap_returned = MTAP_NEXT_SLOT_A; /* what this txn returned */
/* DualShock config-mode latch, per logical pad. A real controller only answers
* the config commands (0x44/0x45/0x46/0x47/0x4C/0x4D/0x4F) and reports the
* config ID 0xF3 while it is IN config mode; outside config it reports its
* normal ID (0x41 digital / 0x73 analog) and ignores config commands. Config is
* entered/exited by command 0x43 with the data byte 0x01(enter)/0x00(exit).
* Faking "always in config" (constant 0xF3) wedges games that probe the pad
* type via 0x43 before polling — e.g. Mega Man X6 loops 01 43 00 00 forever
* and never reaches 0x42. (MMX6 ISSUES.md #2.) */
static uint8_t pad_in_config[PSX_MAX_PLAYERS];
/* Whether the pad on a 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 };
/* 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
};
/* Coherent-DualShock model (Tomba phantom-input fix). A real controller never
* changes its reported type (0x41 digital <-> 0x73 analog) in the middle of a
@@ -109,7 +134,103 @@ static uint8_t pad_supports_config[2] = { 1, 1 };
* 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[2] = { -1, -1 };
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;
}
}
/* Memory card SIO state machine */
typedef enum {
@@ -542,11 +663,19 @@ void sio_init(void) {
pad_response_len = 0;
pad_response_idx = 0;
pad_current_cmd = 0;
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_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_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;
@@ -602,19 +731,70 @@ 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 <= 1)
pad_connected |= (1 << slot);
if (slot >= 0 && slot < PSX_MAX_PLAYERS)
pad_connected |= (uint8_t)(1u << slot);
}
void sio_set_pad_connected(int slot, int connected) {
if (slot < 0 || slot > 1) return;
if (connected) pad_connected |= (uint8_t)(1 << slot);
else pad_connected &= (uint8_t)~(1 << slot);
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (connected) pad_connected |= (uint8_t)(1u << slot);
else pad_connected &= (uint8_t)~(1u << slot);
}
void sio_set_pad_config_capable(int slot, int capable) {
if (slot < 0 || slot > 1) return;
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_pad_on_multitap(slot)) capable = 0;
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). */
@@ -626,7 +806,7 @@ void sio_set_pad_state(uint16_t buttons) {
}
void sio_set_pad_state_slot(int slot, uint16_t buttons) {
if (slot >= 0 && slot <= 1) pad_buttons[slot] = buttons;
if (slot >= 0 && slot < PSX_MAX_PLAYERS) pad_buttons[slot] = buttons;
}
/* Direct set of pad type + sticks. Used at boot/hotplug (refresh_player_devices)
@@ -636,7 +816,11 @@ 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 > 1) return;
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_pad_on_multitap(slot)) {
enabled = 0;
lx = ly = rx = ry = 0x80;
}
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;
@@ -645,7 +829,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 > 1) return;
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
pad_stick[slot][0] = lx; pad_stick[slot][1] = ly;
pad_stick[slot][2] = rx; pad_stick[slot][3] = ry;
}
@@ -654,7 +838,8 @@ 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 > 1) return;
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
if (sio_pad_on_multitap(slot)) analog = 0;
int want = analog ? 1 : 0;
pad_type_req[slot] = (pad_analog[slot] == want) ? -1 : (int8_t)want;
}
@@ -664,21 +849,21 @@ uint16_t sio_get_pad_buttons(void) {
}
uint16_t sio_get_pad_buttons_slot(int slot) {
return (slot >= 0 && slot <= 1) ? pad_buttons[slot] : 0xFFFF;
return (slot >= 0 && slot < PSX_MAX_PLAYERS) ? pad_buttons[slot] : 0xFFFF;
}
int sio_get_pad_connected(int slot) {
if (slot < 0 || slot > 1) return 0;
return (pad_connected & (1 << slot)) ? 1 : 0;
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return 0;
return (pad_connected & (1u << slot)) ? 1 : 0;
}
int sio_get_pad_analog(int slot) {
return (slot >= 0 && slot <= 1) ? pad_analog[slot] : 0;
return (slot >= 0 && slot < PSX_MAX_PLAYERS) ? pad_analog[slot] : 0;
}
void sio_get_pad_sticks(int slot, uint8_t out[4]) {
if (!out) return;
if (slot < 0 || slot > 1) {
if (slot < 0 || slot >= PSX_MAX_PLAYERS) {
out[0] = out[1] = out[2] = out[3] = 0x80;
return;
}
@@ -686,6 +871,69 @@ 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
@@ -693,7 +941,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 < 2; s++) {
for (int s = 0; s < PSX_MAX_PLAYERS; 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. */
@@ -705,7 +953,17 @@ static void pad_process_byte(uint8_t tx_byte) {
}
switch (pad_state) {
case PAD_IDLE:
if (tx_byte == 0x01 && (pad_connected & (1 << selected_slot))) {
/* 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;
pad_state = PAD_WAIT_ACCESS;
sio_rx_data = 0xFF;
sio_stat |= SIO_STAT_ACK;
@@ -717,31 +975,80 @@ 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[selected_slot] ? 0xF3
: (pad_analog[selected_slot] ? 0x73 : 0x41);
const uint8_t cur_id = pad_in_config[lp] ? 0xF3
: (pad_analog[lp] ? 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[selected_slot];
const int ds = pad_supports_config[lp];
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[selected_slot];
const uint16_t btn = pad_buttons[lp];
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[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 */
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 */
pad_response_len = 8;
} else {
pad_response_len = 4;
@@ -753,9 +1060,17 @@ 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[selected_slot];
const uint16_t btn = pad_buttons[lp];
pad_response[1] = 0x5A;
if (!pad_in_config[selected_slot]) {
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]) {
/* 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.
@@ -766,11 +1081,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[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 */
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 */
pad_response_len = 8;
} else {
pad_response_len = 4;
@@ -787,7 +1102,32 @@ 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 && pad_in_config[selected_slot] &&
} 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] &&
(tx_byte == 0x44 || tx_byte == 0x45 || tx_byte == 0x46 ||
tx_byte == 0x47 || tx_byte == 0x4C || tx_byte == 0x4D ||
tx_byte == 0x4F)) {
@@ -812,7 +1152,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[selected_slot] ? 0x01 : 0x00;
pad_response[3] = pad_analog[lp] ? 0x01 : 0x00;
pad_response_len = 8;
pad_state = PAD_SEND_RESPONSE;
sio_rx_data = pad_response[0];
@@ -830,40 +1170,50 @@ 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 (pad_current_cmd == 0x43 && pad_response_idx == 2)
pad_in_config[selected_slot] = (tx_byte == 0x01) ? 1 : 0;
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;
/* 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 (pad_current_cmd == 0x44 && pad_response_idx == 2) {
pad_analog[selected_slot] = (tx_byte == 0x01) ? 1 : 0;
pad_type_req[selected_slot] = -1;
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;
}
/* 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 (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 (!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_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;
@@ -2129,13 +2479,19 @@ 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;
if (!pst_w_bytes(w, pad_analog, 2) || !pst_w_u8(w, pad_connected) ||
/* 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) ||
!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, 2) ||
!pst_w_i16(w, (int16_t)pad_type_req[0]) || !pst_w_i16(w, (int16_t)pad_type_req[1]))
!pst_w_bytes(w, pad_in_config, PSX_MAX_PLAYERS))
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) ||
@@ -2172,22 +2528,33 @@ static int sio_snap_emit(PstW *w) {
static int sio_snap_parse(PstR *r) {
uint32_t u;
int32_t i;
int16_t tr0, tr1;
int16_t tr;
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, 2) || !pst_r_u8(r, &pad_connected) ||
if (!pst_r_bytes(r, pad_analog, PSX_MAX_PLAYERS) || !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, 2) ||
!pst_r_i16(r, &tr0) || !pst_r_i16(r, &tr1))
!pst_r_bytes(r, pad_in_config, PSX_MAX_PLAYERS))
return 0;
pad_state = (PadState)u;
selected_slot = (int)i;
pad_type_req[0] = (int8_t)tr0;
pad_type_req[1] = (int8_t)tr1;
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;
}
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))
+21 -12
View File
@@ -11,6 +11,7 @@
#include "starvation_ring.h"
#include "psx_cycles.h"
#include "psx_netplay.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -190,18 +191,26 @@ 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;
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);
{
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);
}
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)];