mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
+1
-1
Submodule lib/recomp-net updated: add67b44bc...a1d77a32dd
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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). */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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,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++)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
+1385
-64
File diff suppressed because it is too large
Load Diff
+1372
-97
File diff suppressed because it is too large
Load Diff
+197
-35
@@ -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
@@ -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))
|
||||
|
||||
@@ -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)];
|
||||
|
||||
Reference in New Issue
Block a user