load: add opt-in event horizon, warm routes, and audio sink

This commit is contained in:
Matthew Stanley
2026-07-14 22:44:31 -07:00
parent 12934ed2c5
commit 64b084e2a7
22 changed files with 1351 additions and 84 deletions
+35 -15
View File
@@ -301,7 +301,7 @@ turbo), the risky axis is guest time.
**Prioritized burndown (most agnostic + most likely beneficial first):**
- [ ] **L1.0 — E0 `load_probe_v2`: load-window decomposition on Tomba 1.**
- [x] **L1.0 — E0 `load_probe_v2`: load-window decomposition on Tomba 1.**
100% agnostic, zero risk, prices every bet below. Split a real pig-load
window into guest time (seek / sector cadence / per-sector processing /
explicit waits) and host time (native code / decompressors / interp /
@@ -316,32 +316,52 @@ turbo), the risky axis is guest time.
(b) audio at the HOST SINK only (drop excess samples, crossfade on
exit; never guest state); (c) root-cause MMX5 dev-tools+turbo 0xE10
boot wedge (foundation timing bug).
- [ ] **L1.2 — Event-horizon acceleration + batched device ticking.**
- [x] **L1.2 — Event-horizon acceleration + batched device ticking.**
Provably side-effect-free poll/idle regions jump to the next scheduled
observable event with exact cycle credit + identical event ordering;
devices advance to deadlines instead of per-block ticks. Attacks the
~2x ceiling directly; class-level, all titles inherit. Gate set from
L1.0's poll/idle share. Kill: <10% gain or ONE event-order divergence.
- [ ] **L1.3 — Load-path overlay coverage.** Only if L1.0 shows interp
share >~10% in-window; coverage-capture the load path, reshard.
- [ ] **L1.4 — Data shards (spike/tomba-load-shards): verify-only SHADOW
L1.0's poll/idle share. Shipped: deadline-based device servicing, six Tomba
wait sites, and proof-gated generic idle skipping. Cross-game validation is
queued at the end. Kill: <10% gain or ONE event-order divergence.
- [x] **L1.3 — Load-path overlay coverage (killed by gate).** L1.0 measured
zero in-window interpreter instructions, so there is no coverage win to buy.
- [x] **L1.4 — Data shards (rejected/quarantined): verify-only SHADOW
mode, then replay.** Gated on L1.0 (decomp ≥~20-25% host share).
Correctness bar: temporal write visibility — replay sound only if
IRQs-off across the window OR duration < next observable event.
- [ ] **L1.5 — Authentic drive backlog (never-early + catch-up).**
Correctness work, not acceleration. Probe deadline-vs-exposure deltas
first; prereq = resolve the CD-model split (live tree Ape
direct-delivery vs master model).
- [ ] **L1.6 — Seek-only latency probe.** Bounded (seeks × ~250ms), NOT
proven safe, per-game opt-in at best. Kill: seek share <10% of window.
- [x] **L1.5 — Authentic drive backlog (killed as acceleration).** Passive
deadline-vs-exposure probe measured 1,304/1,304 data sectors available on
their exact scheduled cycle, then the intentional fixed 5,000-cycle INT1
presentation delay. Zero early/late sectors, holds, pending/lost INT1s, or
overwrites: there is no artificial lateness for backlog/catch-up to remove.
- [x] **L1.6 — Seek-only latency probe (killed on Tomba).** The measured
New Game window issued zero seek commands. Read-start latency was only
7,676,928 / 298,130,657 cycles (2.57%); pause latency was 8.11% but is an
authentic CPU-visible ordering contract, not a safe seek-speedup target.
- [ ] **L1.7 — Phase-2 doors (open only with cause):** per-title read
speedup with XA/CDDA/MDEC exclusions; decompressor HLE (only via L1.4
failing for a named reason); load-transition state cache (the only
true near-zero; needs thousands-of-frames differential validation).
Method, every experiment: measure first via always-on rings; flag-gated
default-off; one per session; kill criterion written before code; corpus
gate ≥2 titles (Tomba + MMX6 minimum) before any default flips.
**Live status / decision ledger (Tomba 1, 2026-07-14):**
| Item | Verdict | Evidence / measured result |
|---|---|---|
| L1.0 decomposition | DONE | 761 sectors, ~9.5 s baseline window; zero in-window interp; host/static execution dominated. |
| L1.1 turbo hardening | IMPLEMENTED ON TOMBA; CORPUS GATE OPEN | SDL pump remains before every turbo return; 4-frame engage + 6-frame release debounce passed live QA. Opt-in host-audio sink advances canonical SPU state while discarding only accelerated host output; Tomba listening QA passed after 1,100,752 discarded SPU frames. |
| L1.2 event horizon | IMPLEMENTED; CORPUS VALIDATION QUEUED | Production cycle advancement already batches device service at event/MMIO deadlines. Six configured PsyQ CD-wait sites delivered ~27% + ~9% stages. Generic idle-loop skip then cut warm bursts 0.53->0.34 s and 2.19->1.73 s, with 4,599 skips / 765M guest cycles and zero CD overwrites. Strictly per-game opt-in. |
| L1.3 overlay coverage | KILLED | Interpreter share was zero in the measured load window. |
| L1.4 asset/data replay | REJECTED FOR NOW | `FUN_8003EF50` replay produced title/game texture corruption despite zero verifier failures: v1 temporal verifier is unsound. Data shards default off and artifacts removed. |
| Configurable warm CD routes (L1.7 read-speed branch) | ACCEPTED, STRICTLY PER-GAME OPT-IN | Framework accepts up to 16 strict LBA routes with mismatch fallback and consumer-paced IRQ/DMA. Only data-read cadence accelerates; XA/CDDA, seek, and motor timing remain authentic. Tomba multi-route regression: 3 matches, 1,944 accelerated sectors, zero overwrites. Legacy singular config is deprecated. |
| L1.5 authentic backlog | DONE / KILLED AS ACCELERATION | 1,304/1,304 sectors exact-deadline; INT1 exposure exactly +5,000 cycles; zero holds, pending/lost, or overwrites. |
| L1.6 seek-only probe | DONE / KILLED ON TOMBA | Automated New Game: 0 seeks across 792 data sectors. Read-start latency was 2.57% of the data span, below the 10% gate. Pause was 8.11% but remains authentic because early completion is a known race/wedge class. |
| L1.7 state cache / broader HLE | DEFERRED | User excludes savestates; decompressor replay failed correctness. |
Method, every experiment: measure first via always-on rings; start flag-gated;
one per session; kill criterion written before code. `idle_skip`, warm CD routes,
and the turbo host-audio sink are all strictly per-game opt-in. MMX6 remains the
next corpus validation.
---
+57 -4
View File
@@ -61,7 +61,8 @@ Three levers, in ascending order of danger:
## 2. Prior-attempt ledger (do not re-litigate; verbatim outcomes)
1. **turbo_loads — SHIPPED, production path.** Engage = sustained
`cdrom_load_in_progress()` + game-started, 20-frame hysteresis
`cdrom_load_in_progress()` + game-started, 4-frame entry debounce +
6-frame release debounce
(`main.cpp:2062-2084`); presents 1-in-30; audio deliberately NOT
muted (queue capped). ~2x wall on Tomba loads with a healthy overlay
cache. NOTE (survey 2026-07-13): the primary SDL event pump now runs
@@ -87,15 +88,44 @@ Three levers, in ascending order of danger:
green-thread yield bookkeeping; ONE skipped switch is eventually
fatal. Kept: the `[instant_loads]` inert scaffold (load-grid
detector, window plumbing, TCP counters) and the measure-first method.
5. **Data shards — BUILT on `spike/tomba-load-shards` (2a4e574), never
regen'd/run.** Memoized pure-function replay: read-set/write-set/
5. **Data shards — REJECTED/QUARANTINED after live Tomba test.** Memoized
pure-function replay: read-set/write-set/
reg-out/cycle-cost capture at emitter-hooked funcs, byte-verified
replay, recorded cycles credited in slices through the normal
advance+IRQ path. Purity poisons: MMIO, DMA write, thread switch,
4MiB budget, staleness. Persists per-shard `.dss`. Known v1 fidelity
deltas (documented in DATA_SHARDS.md): ISR cycles double-credited;
IRQ arriving mid-credit observes the completed write-set; GTE/COP0
not captured. §5 adds ChatGPT's sharper version of that last flaw.
not captured. Live replay of `FUN_8003EF50` corrupted title and gameplay
textures despite zero verifier failures, proving the v1 temporal verifier
unsound. Runtime default is off and derived shards were removed.
6. **Generic idle-loop skip — SHIPPED, strictly per-game opt-in.** Requires
repeated same-PC interrupt boundaries with no stores, no MMIO, and stable
registers; advances to the next device deadline in whole loop quanta.
Tomba live A/B: 4,599 skips / 765M guest cycles, warm main burst
2.19 s -> 1.73 s, zero CD overwrites. Opt in per game with
`[runtime] idle_skip=true`, per process with `PSX_IDLE_SKIP=1`, or live
through the `idle_skip` debug command. Known accepted cosmetic tradeoff:
an occasional partial texture frame may flash under aggressive load turbo
and resolves on the following complete frame.
7. **Strict warm CD routes — SHIPPED, per-game opt-in only.** The reusable
`[[runtime.warm_cd_routes]]` schema supports up to 16 exact LBA sequences.
A mismatch immediately restores normal timing. Only non-XA data-read cadence
changes; XA/CDDA, seek, motor, IRQ, DMA, callback, and decompression paths
remain authoritative. Tomba regression: three route matches, 1,944 accelerated
sectors, zero response overwrites. The legacy `[runtime.warm_cd_route]`
singular table is deprecated but remains readable with a warning.
8. **Turbo host-audio sink — SHIPPED, per-game opt-in only.** With
`[runtime] turbo_audio_sink=true`, the canonical SPU still renders every
guest-time sample during an active turbo load, preserving voice and CD state,
but the accelerated buffers stop before SDL playback. Normal playback fades
back in from the current guest state. Tomba listening QA passed with
1,100,752 sink-discarded SPU frames, two accelerated load-route matches, and
zero CD response overwrites. This does not revive the rejected mute/freeze
model, which paused voice state and produced audible cuts/restarts.
Also relevant, shipped: FMV auto-skip (unpaced + muted + game-native
teardown), HLE boot shell-skip + boot-turbo window (`bios_hle.c`),
@@ -223,6 +253,14 @@ has NO acceleration value and is pure drive-model correctness work.
Prereq: resolve the CD-model tree split (Ape direct-delivery vs master
model) — pick ONE canonical model.
**Tomba verdict (2026-07-14): KILLED as acceleration.** The passive
`cdrom_timing` ring observed 1,304 data sectors: every buffer fill landed on
its exact scheduled cycle (zero early, zero late), and every exposed INT1 used
the intentional fixed +5,000-cycle controller-presentation latency. There were
zero read holds, pending/lost INT1s, or response overwrites. The current model
already maintains the continuous physical timeline; no catch-up work can make
this load faster.
### E6 — `cd_seek_speedup` (Lever B, bounded, per-game opt-in)
Probe first: seek/pause/spin share of window guest time. Upper bound =
number_of_seeks x ~250ms — a seek-only hack cannot touch a sequential
@@ -232,11 +270,26 @@ some titles, broke others). `pause_complete_delay_cycles` is a known
CPU-visible ordering contract (Ape memcard wedge) — stays authentic.
Kill: seek share <10% or <~250ms, or any callback/VBlank divergence.
**Tomba verdict (2026-07-14): KILLED by the gate.** An automated, authentic
New Game sample covered 792 data sectors over 298,130,657 guest cycles and
issued zero seek commands. Seventeen read-start delays totalled 7,676,928
cycles (2.57% of the data span), below the 10% threshold. Seventeen pause
delays totalled 24,167,597 cycles (8.11%), but pause completion is the
CPU-visible ordering contract explicitly excluded above. The combined
31,844,525-cycle figure is only a theoretical upper bound: it includes that
unsafe pause term and assumes impossible zero-cost, non-overlapping removal.
No seek-speedup implementation is justified for this path.
### E7 — `cd_read_speedup_exp` (Lever B, Phase-2 only)
Per-title, with explicit XA/CDDA/MDEC and timing-sensitive exclusions
(the DuckStation trait architecture). First unexplained wedge ⇒
permanently non-default. Never foundation.
**Implemented scope (2026-07-14):** strict, config-declared warm routes only.
This is not automatic global read acceleration and remains absent unless a game
opts in. Multiple routes share the same fail-closed matcher and consumer-paced
delivery path; XA/CDDA and non-read drive timing are excluded.
### E8 — Decompressor HLE (only via E4's failure)
Entered only if E0 shows decompression host-hot AND shards fail for an
identifiable reason (low reuse, expensive verify, interp-resident).
+33
View File
@@ -106,6 +106,39 @@ controller = "digital" # "digital" or "dualshock"
memcard_dir = "." # memcard files location, relative to project root
```
### Opt-in warm CD routes
Per-game read acceleration is disabled unless the game explicitly declares
one or more strict routes. Each route arms on one `SetLoc`, then requires every
later file start to match `lbas` in order. A mismatch immediately restores the
normal configured disc timing. Only data-read cadence is accelerated; XA/CDDA,
seek, and motor timing remain authentic.
```toml
[[runtime.warm_cd_routes]]
arm_lba = 95947
lbas = [298, 299, 306]
instant_max_per_frame = 32
```
Up to 16 routes may be declared, with 1–64 LBAs each. The old singular
`[runtime.warm_cd_route]` table is deprecated and emits a warning when loaded;
it remains readable for compatibility. This enhancement is intentionally
opt-in and must not acquire a global default.
The other load-time accelerators are likewise opt-in:
```toml
[runtime]
turbo_loads = true
idle_skip = true
turbo_audio_sink = true
```
`turbo_audio_sink` is meaningful only while `turbo_loads` is active. It keeps
the guest SPU timeline advancing but discards accelerated samples before host
playback, then fades normal output back in.
Reserved future fields:
- `default_disc_path` — game runtimes can pre-mount a disc
- `default_game_root` — for sibling-junction setups
+9
View File
@@ -5,6 +5,8 @@
#include <vector>
#include <sstream>
#include <set>
#include <map>
#include <array>
#include "ps1_exe_parser.h"
#include "function_analysis.h"
#include "control_flow.h"
@@ -56,6 +58,13 @@ struct CodeGenConfig {
// See docs/DATA_SHARDS.md. Empty = no hooks (default).
std::set<uint32_t> data_shard_funcs;
// [load_accel.vsync_query] verified PsyQ VSync functions whose mode=-1
// path may bypass its unused GPUSTAT/Timer1 reads. The map value is the
// guest RAM VBlank counter returned by that query path. Empty = inert.
// value = {VBlank counter, GPUSTAT-pointer global, Timer1-pointer global,
// cached Timer1 global}
std::map<uint32_t, std::array<uint32_t, 4>> vsync_query_hle_funcs;
// MMX6-class 2D tile-ring stage initializer. Emit a reveal-invalidation
// callback at entry so host-only wide pixels cannot survive a stage swap.
uint32_t ws_bg2d_init_func = 0;
+13
View File
@@ -2196,6 +2196,18 @@ GeneratedFunction CodeGenerator::generate_function(
body_ss << config_.indent
<< fmt::format("debug_server_log_call_entry(0x{:08X}u);\n",
func.start_addr);
{
const auto vq = config_.vsync_query_hle_funcs.find(func.start_addr);
if (vq != config_.vsync_query_hle_funcs.end()) {
body_ss << config_.indent
<< fmt::format(
"if (psx_vsync_query_hle_enter(cpu, 0x{:08X}u, 0x{:08X}u, "
"0x{:08X}u, 0x{:08X}u, 0x{:08X}u)) return;"
" /* load accel: cycle-faithful VSync(-1) query */\n",
func.start_addr, vq->second[0], vq->second[1],
vq->second[2], vq->second[3]);
}
}
if (config_.data_shard_funcs.count(func.start_addr)) {
// Data-shard hook (docs/DATA_SHARDS.md): on a verified shard hit the
// runtime applies the recorded effect, credits the recorded cycles,
@@ -2650,6 +2662,7 @@ std::string CodeGenerator::generate_file(
ss << "#endif\n";
ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n";
ss << "extern void psx_datashard_ret(CPUState* cpu); /* data-shard capture finalize */\n";
ss << "extern int psx_vsync_query_hle_enter(CPUState* cpu, uint32_t func, uint32_t counter_addr, uint32_t gpustat_ptr_addr, uint32_t timer1_ptr_addr, uint32_t timer1_cache_addr); /* load_accel.c */\n";
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
ss << "extern void psx_ws_mmx6_bg_stage_init(void); /* ws 2D stage reveal invalidation (gpu.c) */\n";
ss << "extern int psx_ws_x_margin(void); /* widescreen cull-margin term (gpu.c) */\n";
+138
View File
@@ -3,6 +3,7 @@
#include "config_loader.h"
#include <cctype>
#include <cstdio>
#include <fstream>
#include <stdexcept>
#include <string>
@@ -149,6 +150,74 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
rt.instant_max_per_frame = static_cast<int>(n);
rt.has_instant_max_per_frame = true;
}
auto parse_warm_cd_route = [&](const toml::value& route,
const std::string& label) {
if (!route.contains("arm_lba") || !route.contains("lbas")) {
throw std::runtime_error(
label + " arm_lba and lbas are required");
}
const auto arm = toml::find<int64_t>(route, "arm_lba");
if (arm < 0 || arm > 0x7FFFFFFFll) {
throw std::runtime_error(fmt::format(
"{} arm_lba out of range: {}", label, arm));
}
RuntimeConfig::WarmCdRoute parsed;
parsed.arm_lba = static_cast<int>(arm);
const auto& lbas = toml::find<std::vector<int64_t>>(route, "lbas");
if (lbas.empty() || lbas.size() > 64) {
throw std::runtime_error(
label + " lbas must contain 1..64 entries");
}
for (const auto lba : lbas) {
if (lba < 0 || lba > 0x7FFFFFFFll) {
throw std::runtime_error(fmt::format(
"{} LBA out of range: {}", label, lba));
}
parsed.lbas.push_back(static_cast<int>(lba));
}
if (route.contains("instant_max_per_frame")) {
const auto n = toml::find<int64_t>(route, "instant_max_per_frame");
if (n < 1 || n > 4096) {
throw std::runtime_error(fmt::format(
"{} instant_max_per_frame out of range (1..4096): {}",
label, n));
}
parsed.instant_max_per_frame = static_cast<int>(n);
}
for (const auto& existing : rt.warm_cd_routes) {
if (existing.arm_lba == parsed.arm_lba &&
existing.lbas.front() == parsed.lbas.front()) {
throw std::runtime_error(fmt::format(
"{} duplicates route arm {} / first LBA {}", label,
parsed.arm_lba, parsed.lbas.front()));
}
}
rt.warm_cd_routes.push_back(std::move(parsed));
};
if (runtime.contains("warm_cd_route")) {
std::fprintf(stderr,
"psxrecomp: warning: [runtime.warm_cd_route] is deprecated; "
"use [[runtime.warm_cd_routes]]\n");
parse_warm_cd_route(toml::find(runtime, "warm_cd_route"),
"[runtime.warm_cd_route]");
}
if (runtime.contains("warm_cd_routes")) {
const auto& routes = toml::find(runtime, "warm_cd_routes");
if (!routes.is_array() || routes.as_array().empty() ||
routes.as_array().size() > 16) {
throw std::runtime_error(
"[[runtime.warm_cd_routes]] must contain 1..16 routes");
}
size_t route_index = 0;
for (const auto& route : routes.as_array()) {
if (!route.is_table()) {
throw std::runtime_error(
"[[runtime.warm_cd_routes]] entries must be tables");
}
parse_warm_cd_route(route, fmt::format(
"[[runtime.warm_cd_routes]] entry {}", route_index++));
}
}
if (runtime.contains("fast_boot")) {
rt.fast_boot = toml::find<bool>(runtime, "fast_boot");
}
@@ -167,6 +236,12 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
if (runtime.contains("turbo_loads")) {
rt.turbo_loads = toml::find<bool>(runtime, "turbo_loads");
}
if (runtime.contains("turbo_audio_sink")) {
rt.turbo_audio_sink = toml::find<bool>(runtime, "turbo_audio_sink");
}
if (runtime.contains("idle_skip")) {
rt.idle_skip = toml::find<bool>(runtime, "idle_skip");
}
if (runtime.contains("overlay_autocompile_cmd")) {
rt.overlay_autocompile_cmd =
toml::find<std::string>(runtime, "overlay_autocompile_cmd");
@@ -735,6 +810,62 @@ GameConfig load_game_config(const fs::path& config_path_in) {
data_shard_funcs.push_back(parse_hex(a, "data_shards.funcs"));
}
}
uint32_t vsync_query_func = 0;
uint32_t vsync_counter_addr = 0;
uint32_t vsync_gpustat_ptr_addr = 0;
uint32_t vsync_timer1_ptr_addr = 0;
uint32_t vsync_timer1_cache_addr = 0;
std::vector<uint32_t> vsync_event_horizon_sites;
std::vector<uint32_t> vsync_event_horizon_extra_sites;
if (cfg.contains("load_accel")) {
const toml::value& lav = toml::find(cfg, "load_accel");
if (lav.contains("vsync_query")) {
const toml::value& vq = toml::find(lav, "vsync_query");
const bool has_func = vq.contains("func");
const bool has_counter = vq.contains("counter_addr");
const bool has_gpu_ptr = vq.contains("gpustat_ptr_addr");
const bool has_timer_ptr = vq.contains("timer1_ptr_addr");
const bool has_timer_cache = vq.contains("timer1_cache_addr");
const int present = (int)has_func + (int)has_counter + (int)has_gpu_ptr +
(int)has_timer_ptr + (int)has_timer_cache;
if (present != 0 && present != 5)
throw std::runtime_error(fmt::format(
"{}: [load_accel.vsync_query] func, counter_addr, gpustat_ptr_addr, "
"timer1_ptr_addr, and timer1_cache_addr must be set together",
config_path.string()));
if (has_func) {
vsync_query_func = parse_hex(
toml::find<std::string>(vq, "func"),
"load_accel.vsync_query.func");
vsync_counter_addr = parse_hex(
toml::find<std::string>(vq, "counter_addr"),
"load_accel.vsync_query.counter_addr");
vsync_gpustat_ptr_addr = parse_hex(
toml::find<std::string>(vq, "gpustat_ptr_addr"),
"load_accel.vsync_query.gpustat_ptr_addr");
vsync_timer1_ptr_addr = parse_hex(
toml::find<std::string>(vq, "timer1_ptr_addr"),
"load_accel.vsync_query.timer1_ptr_addr");
vsync_timer1_cache_addr = parse_hex(
toml::find<std::string>(vq, "timer1_cache_addr"),
"load_accel.vsync_query.timer1_cache_addr");
if (vq.contains("event_horizon_sites")) {
const auto& arr = toml::find<std::vector<std::string>>(
vq, "event_horizon_sites");
for (const auto& a : arr)
vsync_event_horizon_sites.push_back(parse_hex(
a, "load_accel.vsync_query.event_horizon_sites"));
}
if (vq.contains("event_horizon_extra_sites")) {
const auto& arr = toml::find<std::vector<std::string>>(
vq, "event_horizon_extra_sites");
for (const auto& a : arr)
vsync_event_horizon_extra_sites.push_back(parse_hex(
a, "load_accel.vsync_query.event_horizon_extra_sites"));
}
}
}
}
if (cfg.contains("widescreen")) {
const toml::value& ws = toml::find(cfg, "widescreen");
if (ws.contains("sprite_tag_funcs")) {
@@ -955,6 +1086,13 @@ GameConfig load_game_config(const fs::path& config_path_in) {
/*ws_sprite_anchor_addr*/ ws_sprite_anchor_addr,
/*ws_hud_sprt_squash*/ ws_hud_sprt_squash,
/*data_shard_funcs*/ data_shard_funcs,
/*vsync_query_func*/ vsync_query_func,
/*vsync_counter_addr*/ vsync_counter_addr,
/*vsync_gpustat_ptr_addr*/ vsync_gpustat_ptr_addr,
/*vsync_timer1_ptr_addr*/ vsync_timer1_ptr_addr,
/*vsync_timer1_cache_addr*/ vsync_timer1_cache_addr,
/*vsync_event_horizon_sites*/ vsync_event_horizon_sites,
/*vsync_event_horizon_extra_sites*/ vsync_event_horizon_extra_sites,
/*ws_cull_bias_sites*/ ws_cull_bias_sites,
/*ws_cull_range_sites*/ ws_cull_range_sites,
/*ws_cull_a1_sites*/ ws_cull_a1_sites,
+41
View File
@@ -84,6 +84,19 @@ struct RuntimeConfig {
bool has_instant_max_per_frame = false;
int instant_max_per_frame = 0;
// Optional, game-specific warm-load route accelerators. A matching SetLoc
// at arm_lba followed by the exact ordered lbas sequence switches only
// those data reads to the bounded instant cadence. Any sequence mismatch
// fails closed to the configured disc_speed. The parser accepts the legacy
// [runtime.warm_cd_route] table and the reusable
// [[runtime.warm_cd_routes]] array-of-tables.
struct WarmCdRoute {
int arm_lba = -1;
std::vector<int> lbas;
int instant_max_per_frame = 32;
};
std::vector<WarmCdRoute> warm_cd_routes;
// fast_boot: DEPRECATED alias for the HLE boot shell-skip (see bios_hle
// below). The old mechanism (snapshot BIOS state at first handoff, restore
// on later launches) is gone; fast_boot=true now skips only the BIOS shell
@@ -125,6 +138,18 @@ struct RuntimeConfig {
// load wall-time. Streaming titles (e.g. Crash) must leave this off.
bool turbo_loads = false;
// turbo_audio_sink: while turbo_loads is actively running unpaced, keep
// rendering the exact guest-time SPU sample budget (so voice/CD state
// advances) but discard those samples before the host playback queue.
// Opt-in while the experiment is under live audio QA.
bool turbo_audio_sink = false;
// idle_skip: proof-gated fast-forward through repeated CPU polling
// loops with no stores/MMIO and stable register state. Guest time and
// device events still advance exactly. Opt-in per game; the idle_skip
// debug command and PSX_IDLE_SKIP environment variable support live A/B.
bool idle_skip = false;
// overlay_autocompile_cmd: variant-capture automation (step 2.8). A
// shell command (run via cmd.exe /C, cwd = project root) that compiles
// overlay_captures.json into the cache — normally the project's
@@ -417,6 +442,22 @@ struct GameConfig {
// out to be impure only costs a poisoned capture, never a wrong replay.
std::vector<uint32_t> data_shard_funcs;
// [load_accel.vsync_query] opt-in for a byte-verified PsyQ VSync(mode)
// implementation. mode=-1 returns vsync_counter_addr while bypassing two
// unused MMIO reads; every other mode executes the original function.
uint32_t vsync_query_func = 0;
uint32_t vsync_counter_addr = 0;
uint32_t vsync_gpustat_ptr_addr = 0;
uint32_t vsync_timer1_ptr_addr = 0;
uint32_t vsync_timer1_cache_addr = 0;
// Return addresses of verified CD wait-loop VSync(-1) calls. At these
// sites only, the runtime may advance guest time to the next deliverable
// device event while a sustained CD load is active.
std::vector<uint32_t> vsync_event_horizon_sites;
// Separately togglable second tier for additional verified loops, allowing
// per-feature A/B without disabling the accepted base site set.
std::vector<uint32_t> vsync_event_horizon_extra_sites;
// Cull-margin widening. The game's per-object draw classifier compares
// (objX - camX + BIAS) against a RANGE derived from the 4:3 screen width;
// the GTE squash shows ~33% more world, so the fixed margin collapses and
+6
View File
@@ -121,6 +121,7 @@ int main(int argc, char** argv) {
bool overlay_mode = false;
std::set<uint32_t> ws_tag_funcs; // [widescreen] sprite_tag_funcs
std::set<uint32_t> ds_funcs; // [data_shards] funcs
std::map<uint32_t, std::array<uint32_t, 4>> vsync_query_hle_funcs;
std::set<uint32_t> ws_cull_bias, ws_cull_range, ws_cull_a1; // [widescreen.cull]
std::set<uint32_t> ws_cull_screen_x; // [widescreen.cull] screen_x_sites
std::set<uint32_t> ws_cull_slti; // [widescreen.cull] slti_sites
@@ -149,6 +150,10 @@ int main(int argc, char** argv) {
ws_tag_funcs.insert(cfg.ws_sprite_tag_funcs.begin(),
cfg.ws_sprite_tag_funcs.end());
ds_funcs.insert(cfg.data_shard_funcs.begin(), cfg.data_shard_funcs.end());
if (cfg.vsync_query_func)
vsync_query_hle_funcs[cfg.vsync_query_func] = {
cfg.vsync_counter_addr, cfg.vsync_gpustat_ptr_addr,
cfg.vsync_timer1_ptr_addr, cfg.vsync_timer1_cache_addr };
ws_cull_bias.insert(cfg.ws_cull_bias_sites.begin(), cfg.ws_cull_bias_sites.end());
ws_cull_range.insert(cfg.ws_cull_range_sites.begin(), cfg.ws_cull_range_sites.end());
ws_cull_a1.insert(cfg.ws_cull_a1_sites.begin(), cfg.ws_cull_a1_sites.end());
@@ -780,6 +785,7 @@ int main(int argc, char** argv) {
codegen_config.overlay_mode = overlay_mode;
codegen_config.ws_sprite_tag_funcs = ws_tag_funcs;
codegen_config.data_shard_funcs = ds_funcs;
codegen_config.vsync_query_hle_funcs = vsync_query_hle_funcs;
codegen_config.ws_bg2d_init_func = ws_bg2d_init_func;
codegen_config.ws_cull_bias_sites = ws_cull_bias;
codegen_config.ws_cull_range_sites = ws_cull_range;
+1
View File
@@ -59,6 +59,7 @@ enum {
b=(stage<<28)|(start_frame<<14)|run_len.
stage 0=post-ADPCM-decode (native rate),
stage 1=post-resample+volume (44100). */
AUDIO_EV_SINK_DROP = 10, /* turbo host sink; a=guest SPU frames discarded */
};
typedef struct {
+17
View File
@@ -29,6 +29,20 @@ int cdrom_get_setloc_lba(void);
void cdrom_set_instant_rate(int per_frame);
int cdrom_get_instant_rate(void);
/* Strict, config-driven warm-load route accelerator. Up to 16 routes may be
* registered after cdrom_init. A route arms only after arm_lba and activates
* only if the following SetLoc values match lbas in order. Any mismatch
* restores normal disc timing. Only non-XA read cadence is accelerated. */
void cdrom_register_warm_route(int arm_lba, const int* lbas, int count,
int instant_max_per_frame);
void cdrom_warm_route_set_enabled(int enabled);
void cdrom_warm_route_stats_json(char* out, int cap);
/* Passive physical-deadline -> buffer -> INT1 exposure telemetry (L1.5).
* Diagnostics only: recording never changes CD scheduling or delivery. */
void cdrom_timing_reset(void);
void cdrom_timing_stats_json(char* out, int cap);
/* FMV auto-skip detection: cdrom_xa_stream_active() lets the frontend detect
* that streaming XA (FMV/CDDA) is in progress. The skip itself is done by the
* frontend via uncapped pacing (it does NOT alter CD timing — flooding XA
@@ -52,6 +66,9 @@ uint32_t cdrom_get_burst_total(void);
* data sector delivered within the burst-gap window). XA streaming is never
* a load. Drives turbo-through-loads (step 4). */
int cdrom_load_in_progress(void);
/* Physical non-XA data-read command state, without the logical load gap
* bridge used by cdrom_load_in_progress(). Diagnostics only. */
int cdrom_data_read_active(void);
/* MMIO read/write (0x1F801800-0x1F801803) */
uint32_t cdrom_read(uint32_t addr);
+27
View File
@@ -0,0 +1,27 @@
/* load_accel.h -- opt-in, verified load-time acceleration primitives. */
#ifndef PSXRECOMP_LOAD_ACCEL_H
#define PSXRECOMP_LOAD_ACCEL_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct CPUState;
int psx_vsync_query_hle_enter(struct CPUState* cpu, uint32_t func,
uint32_t counter_addr, uint32_t gpustat_ptr_addr,
uint32_t timer1_ptr_addr,
uint32_t timer1_cache_addr);
void psx_vsync_query_hle_set_enabled(int on);
void psx_vsync_query_hle_set_horizon_enabled(int on);
void psx_vsync_query_hle_set_extra_horizon_enabled(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);
#ifdef __cplusplus
}
#endif
#endif
+29
View File
@@ -0,0 +1,29 @@
#ifndef PSXRECOMP_LOAD_TRANSITION_RING_H
#define PSXRECOMP_LOAD_TRANSITION_RING_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Host-side load/pacing edge telemetry. An entry is appended whenever the
* physical CD read state, bridged logical-load predicate, or turbo state
* changes. This is deliberately observational: it does not alter pacing. */
typedef struct {
uint32_t frame;
uint32_t host_ms;
uint16_t load_run;
uint8_t read_active;
uint8_t load_active;
uint8_t turbo_active;
} LoadTransitionEntry;
uint64_t load_transition_total(void);
int load_transition_get(uint64_t seq, LoadTransitionEntry *out);
#ifdef __cplusplus
}
#endif
#endif /* PSXRECOMP_LOAD_TRANSITION_RING_H */
+6
View File
@@ -88,6 +88,12 @@ extern uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr, uint32_t rt, uin
extern uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
extern uint8_t psx_cyc_load_byte(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask);
/* Charge the exact timing/pipeline effects of a 32-bit CPU load without
* invoking the memory handler. Enhancement HLE may use this only when static
* analysis proves the loaded value and read side effects are unobservable. */
extern void psx_cyc_load_word_timing_only(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask);
/* LWC2 (GTE load) ReadMemory timing only (completion +1, NO LDWhich arm — the dest
* is a GTE register). Call AFTER psx_cyc_step(cpu,0) (§1+DO_LDS) and psx_gte_stall.
* Returns the raw 32-bit value. */
+1
View File
@@ -191,6 +191,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/starvation_ring.c
${PSXRECOMP_ROOT}/runtime/src/latency_ring.c
${PSXRECOMP_ROOT}/runtime/src/data_shards.c
${PSXRECOMP_ROOT}/runtime/src/load_accel.c
${PSXRECOMP_ROOT}/runtime/src/card_read_summary.c
${PSXRECOMP_ROOT}/runtime/src/card_data_writes.c
${PSXRECOMP_ROOT}/runtime/src/overlay_capture.c
+437 -7
View File
@@ -15,6 +15,8 @@
#include "spu.h"
#include "event_ring.h"
#include "audio_trace.h"
#include "psx_cycles.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#ifndef _WIN32
@@ -166,6 +168,43 @@ static int read_min, read_sec, read_sect;
static uint8_t mode_reg;
static uint8_t read_cmd;
static int read_delay;
/* ---- Sector deadline/exposure telemetry (L1.5, passive) -----------------
* One record per physical sector deadline. It separates drive scheduling
* lateness (buffer_cycle - due_cycle) from guest/controller backpressure
* (IRQ arm/presentation after the buffer already exists). */
#define CD_TIMING_CAP 4096u
#define CDT_DATA 0x01u
#define CDT_DMA 0x02u
#define CDT_PENDED 0x04u
#define CDT_LOST 0x08u
#define CDT_IRQ_ARMED 0x10u
#define CDT_INTC 0x20u
typedef struct {
uint64_t seq;
uint64_t due_cycle;
uint64_t buffer_cycle;
uint64_t irq_arm_cycle;
uint64_t intc_cycle;
uint32_t frame;
int32_t lba;
uint8_t flags;
} CdTimingRecord;
static CdTimingRecord s_cd_timing[CD_TIMING_CAP];
static uint64_t s_cd_timing_total;
static uint64_t s_cd_timing_reset_seq;
static uint64_t s_cd_timing_next_due;
static uint64_t s_cd_timing_irq_seq = UINT64_MAX;
static uint64_t s_cd_timing_pending_seq = UINT64_MAX;
static uint64_t s_cd_timing_stream_starts;
static uint64_t s_cd_timing_reset_cycle;
static uint64_t s_cd_probe_read_start_count, s_cd_probe_read_start_cycles;
static uint64_t s_cd_probe_pause_count, s_cd_probe_pause_cycles;
static uint64_t s_cd_probe_seek_count, s_cd_probe_seek_cycles;
static uint64_t s_cd_probe_motor_count, s_cd_probe_motor_cycles;
static uint64_t s_cd_probe_stop_count, s_cd_probe_stop_cycles;
static void cd_timing_note_intc(void);
static uint8_t filter_file;
static uint8_t filter_channel;
static uint8_t cd_muted;
@@ -269,6 +308,157 @@ uint32_t g_cd_overwrite_last_frame = 0;
* effective ceiling is VBLANK_CYCLES_NTSC/CDROM_MIN_DELAY ≈ 1128/frame. */
static int g_instant_max_per_frame = CDROM_INSTANT_MAX_PER_FRAME_DEFAULT;
/* Config-driven, game-specific warm-load routes. These deliberately change
* only non-XA read cadence: the normal command, seek/motor timing, IRQ,
* callback, DMA, and guest decompression paths remain authoritative. */
#define CDROM_WARM_ROUTE_MAX 64
#define CDROM_WARM_ROUTES_MAX 16
typedef struct CDROMWarmRoute {
int arm_lba;
int lbas[CDROM_WARM_ROUTE_MAX];
int count;
int rate;
} CDROMWarmRoute;
static CDROMWarmRoute s_warm_routes[CDROM_WARM_ROUTES_MAX];
static int s_warm_routes_count;
static int s_warm_route_configured;
static int s_warm_route_enabled;
static int s_warm_route_armed;
static int s_warm_route_armed_lba = -1;
static int s_warm_route_active;
static int s_warm_route_active_index = -1;
static int s_warm_route_next;
static int s_warm_route_last_lba = -1;
static uint64_t s_warm_route_matches;
static uint64_t s_warm_route_mismatches;
static uint64_t s_warm_route_sectors;
static uint64_t s_warm_route_consumer_waits;
static uint64_t s_warm_route_consumer_wait_cycles;
void cdrom_register_warm_route(int arm_lba, const int* lbas, int count,
int instant_max_per_frame) {
if (!lbas || count < 1 || count > CDROM_WARM_ROUTE_MAX || arm_lba < 0 ||
s_warm_routes_count >= CDROM_WARM_ROUTES_MAX) return;
if (instant_max_per_frame < 1) instant_max_per_frame = 1;
if (instant_max_per_frame > 4096) instant_max_per_frame = 4096;
CDROMWarmRoute *route = &s_warm_routes[s_warm_routes_count++];
memcpy(route->lbas, lbas, (size_t)count * sizeof(lbas[0]));
route->arm_lba = arm_lba;
route->count = count;
route->rate = instant_max_per_frame;
s_warm_route_configured = 1;
s_warm_route_enabled = 1;
s_warm_route_armed = 0;
s_warm_route_armed_lba = -1;
s_warm_route_active = 0;
s_warm_route_active_index = -1;
s_warm_route_next = 0;
s_warm_route_last_lba = -1;
}
void cdrom_warm_route_set_enabled(int enabled) {
s_warm_route_enabled = enabled ? 1 : 0;
if (!s_warm_route_enabled) {
s_warm_route_armed = 0;
s_warm_route_armed_lba = -1;
s_warm_route_active = 0;
s_warm_route_active_index = -1;
s_warm_route_next = 0;
s_warm_route_last_lba = -1;
}
}
static const CDROMWarmRoute* warm_route_current(void) {
if (!s_warm_route_active || s_warm_route_active_index < 0 ||
s_warm_route_active_index >= s_warm_routes_count) return NULL;
return &s_warm_routes[s_warm_route_active_index];
}
void cdrom_warm_route_stats_json(char* out, int cap) {
if (!out || cap <= 0) return;
int state_index = s_warm_route_active ? s_warm_route_active_index : -1;
const CDROMWarmRoute *route = state_index >= 0
? &s_warm_routes[state_index]
: (s_warm_routes_count > 0 ? &s_warm_routes[0] : NULL);
snprintf(out, (size_t)cap,
"\"configured\":%d,\"enabled\":%d,\"armed\":%d,"
"\"active\":%d,\"routes\":%d,\"active_route\":%d,"
"\"arm_lba\":%d,\"route_entries\":%d,"
"\"next_entry\":%d,\"rate\":%d,\"matches\":%llu,"
"\"mismatches\":%llu,\"sectors\":%llu,"
"\"consumer_waits\":%llu,\"consumer_wait_cycles\":%llu",
s_warm_route_configured, s_warm_route_enabled,
s_warm_route_armed, s_warm_route_active, s_warm_routes_count,
state_index, route ? route->arm_lba : -1,
route ? route->count : 0, s_warm_route_next,
route ? route->rate : 0,
(unsigned long long)s_warm_route_matches,
(unsigned long long)s_warm_route_mismatches,
(unsigned long long)s_warm_route_sectors,
(unsigned long long)s_warm_route_consumer_waits,
(unsigned long long)s_warm_route_consumer_wait_cycles);
}
static void warm_route_on_setloc(int lba) {
if (!s_warm_route_configured || !s_warm_route_enabled) return;
for (int i = 0; i < s_warm_routes_count; i++) {
if (lba == s_warm_routes[i].arm_lba) {
s_warm_route_armed = 1;
s_warm_route_armed_lba = lba;
s_warm_route_active = 0;
s_warm_route_active_index = -1;
s_warm_route_next = 0;
s_warm_route_last_lba = lba;
return;
}
}
if (!s_warm_route_active) {
if (s_warm_route_armed) {
int match = -1;
for (int i = 0; i < s_warm_routes_count; i++) {
if (s_warm_routes[i].arm_lba == s_warm_route_armed_lba &&
lba == s_warm_routes[i].lbas[0]) {
match = i;
break;
}
}
s_warm_route_armed = 0;
s_warm_route_armed_lba = -1;
if (match >= 0) {
s_warm_route_active = 1;
s_warm_route_active_index = match;
s_warm_route_next = 1;
s_warm_route_last_lba = lba;
s_warm_route_matches++;
} else {
s_warm_route_mismatches++;
}
}
return;
}
const CDROMWarmRoute *route = warm_route_current();
/* Repeated SetLoc for the current file is harmless. Every transition to
* a new file must match the captured order exactly. */
if (lba == s_warm_route_last_lba) return;
if (route && s_warm_route_next < route->count &&
lba == route->lbas[s_warm_route_next]) {
s_warm_route_last_lba = lba;
s_warm_route_next++;
return;
}
s_warm_route_active = 0;
s_warm_route_active_index = -1;
s_warm_route_armed = 0;
s_warm_route_armed_lba = -1;
s_warm_route_next = 0;
s_warm_route_last_lba = -1;
s_warm_route_mismatches++;
}
void cdrom_set_instant_rate(int per_frame) {
if (per_frame < 1) per_frame = 1;
if (per_frame > 4096) per_frame = 4096;
@@ -281,6 +471,13 @@ static int instant_period(void) {
return p < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : p;
}
static int warm_route_period(void) {
const CDROMWarmRoute *route = warm_route_current();
int rate = route ? route->rate : CDROM_INSTANT_MAX_PER_FRAME_DEFAULT;
int p = VBLANK_CYCLES_NTSC / rate;
return p < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : p;
}
static int apply_speed(int delay) {
/* XA streaming (FMV / CDDA background music): preserve authentic timing.
* FMVs interleave XA audio + MDEC video — speeding up sector delivery
@@ -291,6 +488,15 @@ static int apply_speed(int delay) {
return d < CDROM_MIN_DELAY ? CDROM_MIN_DELAY : d;
}
static int apply_read_speed(int delay) {
/* A route is an explicit DATA-read allowlist, never a blanket drive-speed
* change. XA filter/ADPCM modes are rejected before the first streaming
* sector can set xa_stream_active; established XA remains authentic too. */
if (xa_stream_active || (mode_reg & 0x48u)) return delay;
if (s_warm_route_active) return warm_route_period();
return apply_speed(delay);
}
/* ---- CD load-burst ring (always-on; CLAUDE.md ring-buffer rule) ----------
* A "burst" is a run of delivered sectors with no gap longer than
* CD_BURST_GAP_FRAMES — i.e. one load. Records make "load duration" a
@@ -326,8 +532,10 @@ static void burst_note_sector(void) {
b->end_frame = f;
b->end_ms = ms;
b->sectors++;
b->rate = (uint32_t)g_instant_max_per_frame;
b->divisor = (uint32_t)g_disc_speed_divisor;
const CDROMWarmRoute *route = warm_route_current();
b->rate = (uint32_t)(route ? route->rate : g_instant_max_per_frame);
b->divisor = (uint32_t)(s_warm_route_active ? 0
: g_disc_speed_divisor);
}
/* Copy out the most recent `max` bursts, newest first. Returns count. */
@@ -348,7 +556,7 @@ static int sector_delay_cycles(void) {
int base = (mode_reg & 0x80)
? (CDROM_SINGLE_SPEED_SECTOR_CYCLES / 2)
: CDROM_SINGLE_SPEED_SECTOR_CYCLES;
return apply_speed(base);
return apply_read_speed(base);
}
static int initial_read_delay_cycles(void) {
@@ -358,7 +566,7 @@ static int initial_read_delay_cycles(void) {
int base = (mode_reg & 0x80)
? CDROM_SINGLE_SPEED_SECTOR_CYCLES
: (CDROM_SINGLE_SPEED_SECTOR_CYCLES * 2);
return apply_speed(base);
return apply_read_speed(base);
}
static int seek_complete_delay_cycles(void) {
@@ -584,6 +792,7 @@ static void present_cdrom_irq(void) {
cdrom_intc_latched_generation = cdrom_irq_generation;
event_ring_record(EV_ISTAT_RAISE, 2 /* IRQ_CDROM bit */);
trace_cdrom('F', 0, irq_flag, 0);
if (irq_flag == CDIRQ_DATA_READY) cd_timing_note_intc();
}
}
@@ -608,6 +817,158 @@ static int msf_to_lba(int m, int s, int f) {
return (m * 60 + s) * 75 + f - 150;
}
static CdTimingRecord *cd_timing_lookup(uint64_t seq) {
if (seq == UINT64_MAX || seq >= s_cd_timing_total ||
s_cd_timing_total - seq > CD_TIMING_CAP) return NULL;
CdTimingRecord *r = &s_cd_timing[seq & (CD_TIMING_CAP - 1u)];
return r->seq == seq ? r : NULL;
}
static uint64_t cd_timing_begin_sector(int lba) {
uint64_t seq = s_cd_timing_total++;
CdTimingRecord *r = &s_cd_timing[seq & (CD_TIMING_CAP - 1u)];
memset(r, 0, sizeof(*r));
r->seq = seq;
r->due_cycle = s_cd_timing_next_due ? s_cd_timing_next_due : psx_cycle_count;
r->buffer_cycle = psx_cycle_count;
r->frame = (uint32_t)s_frame_count;
r->lba = lba;
return seq;
}
static void cd_timing_flag(uint64_t seq, uint8_t flags) {
CdTimingRecord *r = cd_timing_lookup(seq);
if (r) r->flags |= flags;
}
static void cd_timing_arm_irq(uint64_t seq) {
CdTimingRecord *r = cd_timing_lookup(seq);
if (!r) return;
r->flags |= CDT_IRQ_ARMED;
r->irq_arm_cycle = psx_cycle_count;
s_cd_timing_irq_seq = seq;
}
static void cd_timing_note_intc(void) {
CdTimingRecord *r = cd_timing_lookup(s_cd_timing_irq_seq);
if (!r || (r->flags & CDT_INTC)) return;
r->flags |= CDT_INTC;
r->intc_cycle = psx_cycle_count;
}
void cdrom_timing_reset(void) {
s_cd_timing_reset_seq = s_cd_timing_total;
s_cd_timing_stream_starts = 0;
s_cd_timing_reset_cycle = psx_cycle_count;
s_cd_probe_read_start_count = s_cd_probe_read_start_cycles = 0;
s_cd_probe_pause_count = s_cd_probe_pause_cycles = 0;
s_cd_probe_seek_count = s_cd_probe_seek_cycles = 0;
s_cd_probe_motor_count = s_cd_probe_motor_cycles = 0;
s_cd_probe_stop_count = s_cd_probe_stop_cycles = 0;
}
void cdrom_timing_stats_json(char *out, int cap) {
if (!out || cap <= 0) return;
uint64_t start = s_cd_timing_reset_seq;
uint64_t dropped = 0;
if (s_cd_timing_total - start > CD_TIMING_CAP) {
dropped = s_cd_timing_total - start - CD_TIMING_CAP;
start = s_cd_timing_total - CD_TIMING_CAP;
}
uint64_t data = 0, exact = 0, early = 0, late = 0;
uint64_t late_sum = 0, late_max = 0;
uint64_t armed = 0, arm_sum = 0, arm_max = 0;
uint64_t exposed = 0, exposure_sum = 0, exposure_max = 0;
uint64_t pended = 0, lost = 0, dma = 0, over_sector = 0;
uint64_t first_due = UINT64_MAX, last_buffer = 0;
uint64_t bins[5] = {0, 0, 0, 0, 0};
for (uint64_t seq = start; seq < s_cd_timing_total; seq++) {
CdTimingRecord *r = cd_timing_lookup(seq);
if (!r || !(r->flags & CDT_DATA)) continue;
data++;
if (r->due_cycle < first_due) first_due = r->due_cycle;
if (r->buffer_cycle > last_buffer) last_buffer = r->buffer_cycle;
if (r->buffer_cycle < r->due_cycle) {
early++;
} else {
uint64_t d = r->buffer_cycle - r->due_cycle;
if (d == 0) { exact++; bins[0]++; }
else {
late++; late_sum += d; if (d > late_max) late_max = d;
if (d <= 64u) bins[1]++;
else if (d <= 1024u) bins[2]++;
else if (d <= 5000u) bins[3]++;
else bins[4]++;
}
}
if (r->flags & CDT_IRQ_ARMED) {
uint64_t d = r->irq_arm_cycle - r->buffer_cycle;
armed++; arm_sum += d; if (d > arm_max) arm_max = d;
}
if (r->flags & CDT_INTC) {
uint64_t d = r->intc_cycle - r->buffer_cycle;
exposed++; exposure_sum += d;
if (d > exposure_max) exposure_max = d;
if (d > CDROM_SINGLE_SPEED_SECTOR_CYCLES) over_sector++;
}
if (r->flags & CDT_PENDED) pended++;
if (r->flags & CDT_LOST) lost++;
if (r->flags & CDT_DMA) dma++;
}
snprintf(out, (size_t)cap,
"\"stream_starts\":%llu,\"records\":%llu,\"dropped\":%llu,"
"\"data_sectors\":%llu,\"schedule_exact\":%llu,"
"\"schedule_early\":%llu,\"schedule_late\":%llu,"
"\"late_cycles_avg\":%llu,\"late_cycles_max\":%llu,"
"\"late_bins\":[%llu,%llu,%llu,%llu,%llu],"
"\"irq_armed\":%llu,\"arm_delay_avg\":%llu,"
"\"arm_delay_max\":%llu,\"intc_exposed\":%llu,"
"\"exposure_delay_avg\":%llu,\"exposure_delay_max\":%llu,"
"\"exposure_over_1x_sector\":%llu,\"pended\":%llu,"
"\"lost\":%llu,\"dma_refills\":%llu,"
"\"data_span_cycles\":%llu,\"window_cycles\":%llu,"
"\"read_start_count\":%llu,\"read_start_cycles\":%llu,"
"\"pause_count\":%llu,\"pause_cycles\":%llu,"
"\"seek_count\":%llu,\"seek_cycles\":%llu,"
"\"motor_count\":%llu,\"motor_cycles\":%llu,"
"\"stop_count\":%llu,\"stop_cycles\":%llu,"
"\"latency_upper_cycles\":%llu",
(unsigned long long)s_cd_timing_stream_starts,
(unsigned long long)(s_cd_timing_total - s_cd_timing_reset_seq),
(unsigned long long)dropped, (unsigned long long)data,
(unsigned long long)exact, (unsigned long long)early,
(unsigned long long)late,
(unsigned long long)(late ? late_sum / late : 0),
(unsigned long long)late_max,
(unsigned long long)bins[0], (unsigned long long)bins[1],
(unsigned long long)bins[2], (unsigned long long)bins[3],
(unsigned long long)bins[4], (unsigned long long)armed,
(unsigned long long)(armed ? arm_sum / armed : 0),
(unsigned long long)arm_max, (unsigned long long)exposed,
(unsigned long long)(exposed ? exposure_sum / exposed : 0),
(unsigned long long)exposure_max,
(unsigned long long)over_sector, (unsigned long long)pended,
(unsigned long long)lost, (unsigned long long)dma,
(unsigned long long)(first_due != UINT64_MAX && last_buffer >= first_due
? last_buffer - first_due : 0),
(unsigned long long)(psx_cycle_count - s_cd_timing_reset_cycle),
(unsigned long long)s_cd_probe_read_start_count,
(unsigned long long)s_cd_probe_read_start_cycles,
(unsigned long long)s_cd_probe_pause_count,
(unsigned long long)s_cd_probe_pause_cycles,
(unsigned long long)s_cd_probe_seek_count,
(unsigned long long)s_cd_probe_seek_cycles,
(unsigned long long)s_cd_probe_motor_count,
(unsigned long long)s_cd_probe_motor_cycles,
(unsigned long long)s_cd_probe_stop_count,
(unsigned long long)s_cd_probe_stop_cycles,
(unsigned long long)(s_cd_probe_read_start_cycles +
s_cd_probe_pause_cycles +
s_cd_probe_seek_cycles +
s_cd_probe_motor_cycles +
s_cd_probe_stop_cycles));
}
static uint8_t bin_to_bcd(int val) {
return (uint8_t)(((val / 10) << 4) | (val % 10));
}
@@ -911,6 +1272,7 @@ static int read_sector_at(int min, int sec, int sect) {
if (delivery.data_delivered) {
memcpy(last_sector_buffer, sector_buffer, (size_t)sector_size);
burst_note_sector();
if (s_warm_route_active) s_warm_route_sectors++;
} else {
uint32_t copy_size = history_size < SECTOR_BUFFER_SIZE ? (uint32_t)history_size
: SECTOR_BUFFER_SIZE;
@@ -974,6 +1336,7 @@ static uint64_t s_int1_lost; /* pended INT1s replaced unseen */
static void cdrom_clear_pending_dataready(void) {
pending_dataready = 0;
pending_dataready_stat = 0;
s_cd_timing_pending_seq = UINT64_MAX;
}
static void start_read_stream(uint8_t cmd) {
@@ -990,6 +1353,10 @@ static void start_read_stream(uint8_t cmd) {
read_sect = seek_sect;
read_cmd = cmd;
read_delay = initial_read_delay_cycles();
s_cd_probe_read_start_count++;
s_cd_probe_read_start_cycles += (uint64_t)read_delay;
s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay;
s_cd_timing_stream_starts++;
reading = 1;
stat_reg |= CDSTAT_READ;
/* ENQUEUE: sector-read stream scheduled (due in read_delay cycles). A
@@ -1002,6 +1369,7 @@ static void stop_read_stream(void) {
reading = 0;
read_cmd = 0;
read_delay = 0;
s_cd_timing_next_due = 0;
cdrom_clear_pending_dataready();
}
@@ -1148,6 +1516,7 @@ static void exec_command(uint8_t cmd) {
seek_sect = bcd_to_bin(param_fifo[2]);
s_setloc_lba = msf_to_lba(seek_min, seek_sec, seek_sect);
setloc_seek_far = (abs(current_lba - s_setloc_lba) > 16) ? 1 : 0;
warm_route_on_setloc(s_setloc_lba);
}
response_push(stat_reg);
set_irq(CDIRQ_ACK);
@@ -1186,6 +1555,8 @@ static void exec_command(uint8_t cmd) {
pending.cmd = 0x07;
pending.pending = 1;
pending.delay = apply_speed(30000); /* motor spin-up */
s_cd_probe_motor_count++;
s_cd_probe_motor_cycles += (uint64_t)pending.delay;
pending.phase = 1;
break;
@@ -1208,6 +1579,8 @@ static void exec_command(uint8_t cmd) {
pending.cmd = 0x08;
pending.pending = 1;
pending.delay = apply_speed(30000); /* motor spin-down */
s_cd_probe_stop_count++;
s_cd_probe_stop_cycles += (uint64_t)pending.delay;
pending.phase = 1;
break;
@@ -1222,6 +1595,8 @@ static void exec_command(uint8_t cmd) {
pending.cmd = 0x09;
pending.pending = 1;
pending.delay = complete_delay;
s_cd_probe_pause_count++;
s_cd_probe_pause_cycles += (uint64_t)complete_delay;
pending.phase = 1;
break;
}
@@ -1408,6 +1783,8 @@ static void exec_command(uint8_t cmd) {
pending.cmd = cmd; /* 0x15 or 0x16 — completed in process_pending */
pending.pending = 1;
pending.delay = seek_complete_delay_cycles();
s_cd_probe_seek_count++;
s_cd_probe_seek_cycles += (uint64_t)pending.delay;
pending.phase = 1;
break;
@@ -1590,9 +1967,30 @@ static void process_pending(uint32_t cycles) {
static uint64_t s_read_hold_cycles;
static uint64_t s_read_hold_events;
/* Route-only HLE producer/consumer handshake. Do not overwrite a cached
* sector or data-ready callback: make the next sector eligible only after the
* guest consumes the FIFO and acknowledges the previous notification.
* Multi-sector DMA may refill while its original data-ready IRQ is active. */
static int warm_route_consumer_blocked(void) {
if (!s_warm_route_active) return 0;
/* Tomba's raw-sector path intentionally consumes 12 header + 2048 data
* bytes from a 2340-byte FIFO and leaves the final 280 bytes unread. The
* IRQ ack (plus an inactive DMA channel), not sector_available, is the
* authoritative consumer-complete handshake. */
if (pending_dataready) return 1;
if (irq_flag != 0 && !dma_cdrom_transfer_active()) return 1;
return 0;
}
static void process_read_stream(uint32_t cycles) {
if (!reading) return;
if (warm_route_consumer_blocked()) {
s_warm_route_consumer_waits++;
s_warm_route_consumer_wait_cycles += cycles;
return;
}
/* Disc time NEVER pauses while the drive reads (Beetle cdc.cpp
* HandleSectorRead: the sector pipeline advances on cycle deadlines
* regardless of INT ack state). XA-ADPCM realtime audio flows to the
@@ -1603,29 +2001,38 @@ static void process_read_stream(uint32_t cycles) {
}
if (read_delay <= 0) {
uint64_t timing_seq = cd_timing_begin_sector(
msf_to_lba(read_min, read_sec, read_sect));
if (irq_flag == 0) {
if (sector_available) {
trace_cdrom('O', 0, (uint32_t)sector_read_pos, 0);
}
deliver_read_sector();
if (deliver_read_sector()) {
cd_timing_flag(timing_seq, CDT_DATA);
cd_timing_arm_irq(timing_seq);
}
} else if (dma_cdrom_transfer_active()) {
/* Active multi-sector CD DMA with the data-ready INT still
* asserted: refill the buffer so the DMA keeps draining, no new
* INT (the historical shape; games start the DMA inside the
* data-ready callback before acking). */
deliver_read_sector_without_irq();
if (deliver_read_sector_without_irq())
cd_timing_flag(timing_seq, CDT_DATA | CDT_DMA);
} else {
/* Guest hasn't acked the previous INT yet. Read the sector on
* schedule (XA audio + buffer overwrite happen inside), and
* pend its data-ready INT1 one deep. */
int delivered = deliver_read_sector_without_irq();
if (delivered) {
cd_timing_flag(timing_seq, CDT_DATA | CDT_PENDED);
if (pending_dataready) {
s_int1_lost++;
trace_cdrom('P', 0, (uint32_t)last_sector_lba, 0);
cd_timing_flag(s_cd_timing_pending_seq, CDT_LOST);
}
pending_dataready = 1;
pending_dataready_stat = stat_reg;
s_cd_timing_pending_seq = timing_seq;
s_int1_pended++;
}
}
@@ -1635,6 +2042,7 @@ static void process_read_stream(uint32_t cycles) {
if (read_delay <= 0) {
read_delay = sector_delay_cycles();
}
s_cd_timing_next_due = psx_cycle_count + (uint64_t)read_delay;
}
}
@@ -1643,11 +2051,14 @@ static void process_read_stream(uint32_t cycles) {
* register clears). Called from the irq_flag ack write. */
static void present_pending_dataready(void) {
if (!pending_dataready || irq_flag != 0) return;
uint64_t timing_seq = s_cd_timing_pending_seq;
pending_dataready = 0;
s_cd_timing_pending_seq = UINT64_MAX;
response_clear();
response_push(pending_dataready_stat);
set_irq(CDIRQ_DATA_READY);
fire_cdrom_irq();
cd_timing_arm_irq(timing_seq);
s_dataready_fires++;
}
@@ -1692,6 +2103,20 @@ void cdrom_init(const char* cue_path) {
memset(&queued_cmd, 0, sizeof(queued_cmd));
seek_min = seek_sec = seek_sect = 0;
setloc_seek_far = 0;
s_warm_routes_count = 0;
s_warm_route_configured = 0;
s_warm_route_enabled = 0;
s_warm_route_armed = 0;
s_warm_route_armed_lba = -1;
s_warm_route_active = 0;
s_warm_route_active_index = -1;
s_warm_route_next = 0;
s_warm_route_last_lba = -1;
s_warm_route_matches = 0;
s_warm_route_mismatches = 0;
s_warm_route_sectors = 0;
s_warm_route_consumer_waits = 0;
s_warm_route_consumer_wait_cycles = 0;
cdrom_debug_clear_sector_history();
if (cue_path) {
@@ -1849,7 +2274,8 @@ uint32_t cdrom_cycles_to_irq(uint32_t i_mask) {
if (pending.pending && pending.delay > 0 && (uint32_t)pending.delay < best)
best = (uint32_t)pending.delay;
/* Active sector read: next data-ready in read_delay cycles. */
if (reading && read_delay > 0 && (uint32_t)read_delay < best)
if (reading && !warm_route_consumer_blocked() &&
read_delay > 0 && (uint32_t)read_delay < best)
best = (uint32_t)read_delay;
return best;
}
@@ -2039,6 +2465,10 @@ int cdrom_load_in_progress(void) {
return 0;
}
int cdrom_data_read_active(void) {
return reading && !xa_stream_active;
}
/* ---- 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
+5 -1
View File
@@ -82,7 +82,11 @@ static DsShard* s_buckets[DS_KEY_BUCKETS];
static int s_loaded_keys[256]; static int s_loaded_n = 0; /* keys with disk load done */
volatile int g_ds_recording = 0;
static int s_enabled = 1;
/* Fail closed. A configured hook is only a candidate boundary; replay must be
* enabled explicitly after capture/verification for the current experiment.
* This prevents an unsafe persisted candidate from mutating boot assets before
* the debug/control plane is available to disable it. */
static int s_enabled = 0;
/* ---- recorder state ---- */
static CPUState* s_arm_cpu = 0;
+89 -1
View File
@@ -27,6 +27,7 @@
#include "dma.h"
#include "gpu.h"
#include "present_ring.h"
#include "load_transition_ring.h"
#include "cdrom.h"
#include "sio.h"
#include "memcard.h"
@@ -5969,7 +5970,7 @@ static void handle_audio_events(int id, const char *json)
uint64_t total = audio_trace_events_total();
static const char *kind_names[] = {
"?", "REG", "RENDER", "SKIP", "UNDERRUN",
"MUTE", "UNMUTE", "CD_PUSH", "DMA", "XA_ZERO"
"MUTE", "UNMUTE", "CD_PUSH", "DMA", "XA_ZERO", "SINK_DROP"
};
size_t cap = 256u + (size_t)got * 192u;
@@ -10516,6 +10517,46 @@ static void handle_turbo_loads(int id, const char *json)
(unsigned long long)g_turbo_loads_frames);
}
/* turbo_audio_sink: opt-in host-output discard while turbo loads run. Guest
* SPU state continues advancing; only accelerated playback samples are sunk. */
static void handle_turbo_audio_sink(int id, const char *json)
{
extern int g_turbo_audio_sink_enabled;
extern int g_turbo_audio_sink_active;
extern uint64_t g_turbo_audio_sink_frames;
int n = json_get_int(json, "n", -1);
if (n == 0 || n == 1) g_turbo_audio_sink_enabled = n;
send_fmt("{\"id\":%d,\"ok\":true,\"enabled\":%d,\"active\":%d,"
"\"discarded_spu_frames\":%llu}\n", id,
g_turbo_audio_sink_enabled, g_turbo_audio_sink_active,
(unsigned long long)g_turbo_audio_sink_frames);
}
/* load_transitions: state-edge log for physical reads, the bridged logical
* load predicate, and host-side turbo pacing. Newest entries are returned
* last so adjacent edges can be read as a timeline. */
static void handle_load_transitions(int id, const char *json)
{
int count = json_get_int(json, "count", 128);
if (count < 1) count = 1;
if (count > 512) count = 512;
uint64_t total = load_transition_total();
uint64_t start = total > (uint64_t)count ? total - (uint64_t)count : 0;
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"edges\":[",
id, (unsigned long long)total);
int emitted = 0;
for (uint64_t seq = start; seq < total; seq++) {
LoadTransitionEntry e;
if (!load_transition_get(seq, &e)) continue;
send_fmt("%s{\"seq\":%llu,\"frame\":%u,\"host_ms\":%u,"
"\"read\":%u,\"load\":%u,\"turbo\":%u,\"load_run\":%u}",
emitted++ ? "," : "", (unsigned long long)seq, e.frame,
e.host_ms, e.read_active, e.load_active, e.turbo_active,
e.load_run);
}
send_fmt("]}\n");
}
/* cdrom_bursts: dump the always-on CD load-burst ring, newest first. Each
* record is one gap-separated run of delivered data sectors — i.e. one load.
* Param "count" (optional, default 32, max 128). */
@@ -10542,6 +10583,18 @@ static void handle_cdrom_bursts(int id, const char *json)
send_fmt("]}\n");
}
/* cdrom_timing: passive L1.5 physical deadline -> buffer -> INT1 exposure
* summary. `reset:1` starts a fresh measurement window without touching the
* controller or its schedules. late_bins are exact, 1..64, 65..1024,
* 1025..5000, and >5000 guest cycles. */
static void handle_cdrom_timing(int id, const char *json)
{
if (json_get_int(json, "reset", 0) == 1) cdrom_timing_reset();
char stats[2048];
cdrom_timing_stats_json(stats, (int)sizeof(stats));
send_fmt("{\"id\":%d,\"ok\":true,%s}\n", id, stats);
}
/* autocompile_status: variant-capture automation state — autocapture
* enable/trigger counters + the background compile's state and output tail
* (in-memory ring; no log files). */
@@ -12028,10 +12081,42 @@ static void handle_data_shards(int id, const char *json)
send_fmt("{\"id\":%d,\"ok\":true,%s}", id, body);
}
/* vsync_query_hle: cycle-faithful VSync(-1) query acceleration counters. */
static void handle_vsync_query_hle(int id, const char *json)
{
extern void psx_vsync_query_hle_stats_json(char* buf, int cap);
extern void psx_vsync_query_hle_set_enabled(int on);
extern void psx_vsync_query_hle_set_horizon_enabled(int on);
extern void psx_vsync_query_hle_set_extra_horizon_enabled(int on);
int en = json_get_int(json, "enable", -1);
int horizon = json_get_int(json, "horizon", -1);
int extra = json_get_int(json, "extra", -1);
if (en == 0 || en == 1) psx_vsync_query_hle_set_enabled(en);
if (horizon == 0 || horizon == 1)
psx_vsync_query_hle_set_horizon_enabled(horizon);
if (extra == 0 || extra == 1)
psx_vsync_query_hle_set_extra_horizon_enabled(extra);
char body[512];
psx_vsync_query_hle_stats_json(body, sizeof(body));
send_fmt("{\"id\":%d,\"ok\":true,%s}", id, body);
}
/* warm_cd_route: live A/B toggle and fail-closed route counters. */
static void handle_warm_cd_route(int id, const char *json)
{
int en = json_get_int(json, "enable", -1);
if (en == 0 || en == 1) cdrom_warm_route_set_enabled(en);
char body[512];
cdrom_warm_route_stats_json(body, sizeof(body));
send_fmt("{\"id\":%d,\"ok\":true,%s}\n", id, body);
}
static const CmdEntry s_commands[] = {
{ "phase_profile", handle_phase_profile },
{ "starv_ring", handle_starv_ring },
{ "data_shards", handle_data_shards },
{ "vsync_query_hle", handle_vsync_query_hle },
{ "warm_cd_route", handle_warm_cd_route },
{ "phase_hot", handle_phase_hot },
{ "idle_skip", handle_idle_skip },
{ "lockstep", handle_lockstep },
@@ -12302,7 +12387,10 @@ static const CmdEntry s_commands[] = {
{ "cdrom_instant_rate", handle_cdrom_instant_rate },
{ "cd_overwrite", handle_cd_overwrite },
{ "cdrom_bursts", handle_cdrom_bursts },
{ "cdrom_timing", handle_cdrom_timing },
{ "turbo_loads", handle_turbo_loads },
{ "turbo_audio_sink", handle_turbo_audio_sink },
{ "load_transitions", handle_load_transitions },
{ "autocompile_status", handle_autocompile_status },
{ "sljit_status", handle_sljit_status },
{ "sljit_async", handle_sljit_async },
+228
View File
@@ -0,0 +1,228 @@
/* load_accel.c -- verified enhancement-phase load-time acceleration.
*
* PsyQ's VSync(-1) is used as a timeout clock by CD_sync/CD_ready loops. The
* library routine returns its RAM VBlank counter, but its common prologue first
* reads GPUSTAT and Timer1 even though neither value reaches the mode=-1 result.
* Those reads are architecturally side-effect-free yet expensive in the host
* runtime because every MMIO access catches all devices up to the current cycle.
*
* This helper reproduces the verified Tomba/PsyQ query path instruction by
* instruction: load-delay timing, stack traffic, I-cache fetches, and all three
* interrupt checkpoints. Only the two unused MMIO handlers are suppressed.
* Non-query calls, precise diagnostics, and cosim always use the original LLE.
* Config-verified CD wait-loop call sites may additionally advance guest time
* to the next deliverable device event. That elides empty host-side polling
* while preserving the device clock, event order, and subsequent IRQ delivery.
*/
#include "load_accel.h"
#include "cdrom.h"
#include "cpu_state.h"
#include "interrupts.h"
#include "psx_cyc.h"
#include <stdio.h>
extern uint32_t g_debug_last_store_pc;
extern int g_precise_mode;
static int s_enabled = 1;
static int s_horizon_enabled = 1;
static int s_extra_horizon_enabled = 1;
static uint64_t s_calls;
static uint64_t s_handled;
static uint64_t s_fallback_mode;
static uint64_t s_fallback_diagnostic;
static uint64_t s_fallback_layout;
static uint64_t s_horizon_hits;
static uint64_t s_horizon_cycles;
static uint32_t s_horizon_sites[16];
static uint32_t s_horizon_site_count;
static uint64_t s_extra_horizon_hits;
static uint64_t s_extra_horizon_cycles;
static uint32_t s_extra_horizon_sites[16];
static uint32_t s_extra_horizon_site_count;
#ifdef PSX_ENABLE_BLOCK_CYCLES
#define VQ_STEP(cpu_, mask_) psx_cyc_step((cpu_), (mask_))
#else
#define VQ_STEP(cpu_, mask_) do { (void)(cpu_); (void)(mask_); } while (0)
#endif
void psx_vsync_query_hle_set_enabled(int on) { s_enabled = on ? 1 : 0; }
void psx_vsync_query_hle_set_horizon_enabled(int on) {
s_horizon_enabled = on ? 1 : 0;
}
void psx_vsync_query_hle_set_extra_horizon_enabled(int on) {
s_extra_horizon_enabled = on ? 1 : 0;
}
void psx_vsync_query_hle_add_event_horizon_site(uint32_t return_pc) {
if (return_pc == 0u) return;
for (uint32_t i = 0; i < s_horizon_site_count; i++)
if (s_horizon_sites[i] == return_pc) return;
if (s_horizon_site_count <
(uint32_t)(sizeof(s_horizon_sites) / sizeof(s_horizon_sites[0])))
s_horizon_sites[s_horizon_site_count++] = return_pc;
}
void psx_vsync_query_hle_add_extra_event_horizon_site(uint32_t return_pc) {
if (return_pc == 0u) return;
for (uint32_t i = 0; i < s_extra_horizon_site_count; i++)
if (s_extra_horizon_sites[i] == return_pc) return;
if (s_extra_horizon_site_count <
(uint32_t)(sizeof(s_extra_horizon_sites) /
sizeof(s_extra_horizon_sites[0])))
s_extra_horizon_sites[s_extra_horizon_site_count++] = return_pc;
}
static int is_event_horizon_site(uint32_t return_pc) {
for (uint32_t i = 0; i < s_horizon_site_count; i++)
if (s_horizon_sites[i] == return_pc) return 1;
for (uint32_t i = 0; i < s_extra_horizon_site_count; i++)
if (s_extra_horizon_sites[i] == return_pc) return 2;
return 0;
}
int psx_vsync_query_hle_enter(CPUState* cpu, uint32_t func,
uint32_t counter_addr, uint32_t gpustat_ptr_addr,
uint32_t timer1_ptr_addr,
uint32_t timer1_cache_addr) {
s_calls++;
if (!s_enabled || cpu->gpr[4] != 0xFFFFFFFFu) {
s_fallback_mode++;
return 0;
}
#ifdef PSX_COSIM
s_fallback_diagnostic++;
return 0;
#else
if (g_precise_mode) {
s_fallback_diagnostic++;
return 0;
}
#endif
/* Fail closed if the verified PsyQ globals were repointed at runtime. */
if (cpu->read_word(gpustat_ptr_addr) != 0x1F801814u ||
cpu->read_word(timer1_ptr_addr) != 0x1F801110u) {
s_fallback_layout++;
return 0;
}
s_handled++;
/* These sites were statically verified as the clock sample inside a CD
* wait loop. While a sustained read is active, nothing useful happens
* between samples until a deliverable device event. Advance the same
* guest clock/device model to that boundary in one host operation; the
* original VSync path below then reaches its normal interrupt checkpoint
* and returns the post-event VBlank counter. A defensive one-VBlank-ish
* cap prevents a malformed per-game site list from making a large jump. */
int horizon_kind = is_event_horizon_site(cpu->gpr[31]);
if (s_horizon_enabled &&
(horizon_kind == 1 ||
(horizon_kind == 2 && s_extra_horizon_enabled)) &&
cdrom_load_in_progress()) {
uint32_t dist = cycles_to_next_event();
if (dist > 64u && dist != 0xFFFFFFFFu && dist <= 1200000u) {
psx_advance_cycles(dist);
if (horizon_kind == 2) {
s_extra_horizon_hits++;
s_extra_horizon_cycles += dist;
} else {
s_horizon_hits++;
s_horizon_cycles += dist;
}
}
}
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, func + 0x00u);
#endif
VQ_STEP(cpu, 0x4u); cpu->gpr[2] = 0x80090000u; /* +00 lui v0 */
cpu->gpr[2] = psx_cyc_load_word(cpu, gpustat_ptr_addr, 2u, 0x4u); /* +04 lw */
VQ_STEP(cpu, 0x8u); cpu->gpr[3] = 0x80090000u; /* +08 lui v1 */
cpu->gpr[3] = psx_cyc_load_word(cpu, timer1_ptr_addr, 3u, 0x8u); /* +0C lw */
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, func + 0x10u);
#endif
VQ_STEP(cpu, 0x20000000u); cpu->gpr[29] -= 32u; /* +10 addiu sp */
VQ_STEP(cpu, 0xA0000000u); g_debug_last_store_pc = func + 0x14u;
cpu->write_word(cpu->gpr[29] + 24u, cpu->gpr[31]); /* +14 sw ra */
VQ_STEP(cpu, 0x20020000u); g_debug_last_store_pc = func + 0x18u;
cpu->write_word(cpu->gpr[29] + 20u, cpu->gpr[17]); /* +18 sw s1 */
VQ_STEP(cpu, 0x20010000u); g_debug_last_store_pc = func + 0x1Cu;
cpu->write_word(cpu->gpr[29] + 16u, cpu->gpr[16]); /* +1C sw s0 */
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, func + 0x20u);
#endif
psx_cyc_load_word_timing_only(cpu, 0x1F801814u, 16u, 0x4u); /* +20 GPUSTAT */
cpu->gpr[16] = 0u; /* value unused */
psx_cyc_load_word_timing_only(cpu, 0x1F801110u, 2u, 0x8u); /* +24 Timer1 */
cpu->gpr[2] = 0u; /* value unused */
VQ_STEP(cpu, 0x8u); cpu->gpr[3] = 0x80090000u; /* +28 lui v1 */
cpu->gpr[3] = psx_cyc_load_word(cpu, timer1_cache_addr, 3u, 0x8u); /* +2C */
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, func + 0x30u);
#endif
VQ_STEP(cpu, 0x1u); /* +30 nop */
VQ_STEP(cpu, 0xCu); cpu->gpr[2] -= cpu->gpr[3]; /* +34 subu */
VQ_STEP(cpu, 0x10u); /* +38 bgez a0 */
VQ_STEP(cpu, 0x20004u); cpu->gpr[17] = cpu->gpr[2] & 0xFFFFu; /* +3C */
psx_check_interrupts_at(cpu, func + 0x40u);
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, func + 0x40u);
#endif
VQ_STEP(cpu, 0x4u); cpu->gpr[2] = 0x80090000u; /* +40 lui v0 */
cpu->gpr[2] = psx_cyc_load_word(cpu, counter_addr, 2u, 0x4u); /* +44 counter */
VQ_STEP(cpu, 0x0u); /* +48 j epilogue */
VQ_STEP(cpu, 0x1u); /* +4C nop */
psx_check_interrupts_at(cpu, func + 0x130u);
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, func + 0x130u);
#endif
cpu->gpr[31] = psx_cyc_load_word(cpu, cpu->gpr[29] + 24u, 31u, 0x20000000u);
cpu->gpr[17] = psx_cyc_load_word(cpu, cpu->gpr[29] + 20u, 17u, 0x20000000u);
cpu->gpr[16] = psx_cyc_load_word(cpu, cpu->gpr[29] + 16u, 16u, 0x20000000u);
VQ_STEP(cpu, 0x20000000u); cpu->gpr[29] += 32u;
#ifdef PSX_ENABLE_BLOCK_CYCLES
psx_icache_fetch(cpu, func + 0x140u);
#endif
{
uint32_t ra = cpu->gpr[31];
VQ_STEP(cpu, 0x80000001u); /* jr ra */
VQ_STEP(cpu, 0x1u); /* delay nop */
psx_check_interrupts_at(cpu, ra);
cpu->pc = ra;
}
return 1;
}
void psx_vsync_query_hle_stats_json(char* buf, int cap) {
snprintf(buf, (size_t)cap,
"\"enabled\":%d,\"horizon_enabled\":%d,"
"\"extra_horizon_enabled\":%d,"
"\"calls\":%llu,\"handled\":%llu,"
"\"fallback_mode\":%llu,\"fallback_diagnostic\":%llu,"
"\"fallback_layout\":%llu,\"horizon_sites\":%u,"
"\"horizon_hits\":%llu,\"horizon_cycles\":%llu,"
"\"extra_horizon_sites\":%u,\"extra_horizon_hits\":%llu,"
"\"extra_horizon_cycles\":%llu",
s_enabled, s_horizon_enabled, s_extra_horizon_enabled,
(unsigned long long)s_calls,
(unsigned long long)s_handled,
(unsigned long long)s_fallback_mode,
(unsigned long long)s_fallback_diagnostic,
(unsigned long long)s_fallback_layout, s_horizon_site_count,
(unsigned long long)s_horizon_hits,
(unsigned long long)s_horizon_cycles,
s_extra_horizon_site_count,
(unsigned long long)s_extra_horizon_hits,
(unsigned long long)s_extra_horizon_cycles);
}
+143 -33
View File
@@ -15,6 +15,7 @@
#include "text_xlate.h"
#include "boot_state.h"
#include "bios_hle.h"
#include "load_accel.h"
#include "savestate.h"
#include "overlay_capture.h"
#include "overlay_loader.h"
@@ -24,6 +25,7 @@
#include "overlay_backend.h"
#include "gpu.h"
#include "present_ring.h"
#include "load_transition_ring.h"
#include "gpu_sw_renderer.h"
#include "gpu_render.h"
#include "gpu_gl_renderer.h"
@@ -395,14 +397,18 @@ int g_present_vsync_disabled = 0; /* 1 once self-heal tripped */
extern "C" {
int g_turbo_loads_enabled = 0;
uint64_t g_turbo_loads_frames = 0; /* vblanks run unpaced (observability) */
int g_turbo_audio_sink_enabled = 0;
int g_turbo_audio_sink_active = 0;
uint64_t g_turbo_audio_sink_frames = 0; /* guest SPU frames advanced, not queued */
}
/* Engage turbo only after a load has been continuously in progress for this many
* frames. Filters brief incidental reads (e.g. a boss/stage-select screen that
* streams XA music and reads a few preview sectors as you hover): each 1-2 frame
* blip would otherwise flip turbo on, muting the music for the audio hangover and
* stuttering the frame rate. Real loads hold for hundreds of frames, so they
* lose only this brief authentic-paced prefix. */
#define TURBO_LOADS_ENGAGE_FRAMES 20
/* The CD predicate already excludes XA and holds across ordinary inter-file
* gaps. A short engage debounce rejects a one-frame controller blip without
* leaving a visible authentic-paced prefix on every real load. Once engaged,
* a symmetric release debounce rides through tiny late-sector/IRQ gaps. This
* changes host presentation/pacing only; guest cycles and input sampling keep
* advancing normally. */
#define TURBO_LOADS_ENGAGE_FRAMES 4
#define TURBO_LOADS_RELEASE_FRAMES 6
static SDL_AudioDeviceID sdl_audio_device;
static int16_t sdl_audio_buf[2048 * 2];
@@ -837,14 +843,17 @@ static void sdl_audio_gain_ramp(int16_t* buf, int frames, float g0, float g1) {
static const int sdl_audio_fade_samples = 44100 * 40 / 1000; /* 40 ms */
static int sdl_audio_fadein_left = 0;
static void sdl_audio_pump(void) {
static void sdl_audio_pump(bool discard_output = false) {
if (!sdl_audio_device) return;
const uint32_t bytes_per_frame = sizeof(int16_t) * 2u;
const bool legacy = audio_legacy_mode();
static int had_audio = 0;
uint32_t queued = 0; /* RENDER event b: bytes (legacy) / fill ms (bridge) */
if (legacy) {
if (discard_output) {
/* Host-only sink: skip all queue/bridge interaction, but continue to
* the guest-cycle sample budget and spu_render below. */
} else if (legacy) {
/* Historical push model + baseline measurement: a drained (==0) queue
* means the device was silence-filling since the last pump = a gap.
* Only meaningful after the first audio has been queued. */
@@ -910,6 +919,11 @@ static void sdl_audio_pump(void) {
audio_trace_event(AUDIO_EV_RENDER, (uint32_t)frames, queued);
spu_render(sdl_audio_buf, frames);
if (discard_output) {
g_turbo_audio_sink_frames += (uint64_t)frames;
audio_trace_event(AUDIO_EV_SINK_DROP, (uint32_t)frames, 0);
return;
}
if (sdl_audio_fadein_left > 0) {
const float g0 = 1.0f - (float)sdl_audio_fadein_left
/ (float)sdl_audio_fade_samples;
@@ -1049,7 +1063,7 @@ static void runtime_perf_diag_tick() {
for (int i = 0; i < 6; i++) last_up[i] = up[i];
}
static void sdl_audio_update(int turbo_active) {
static void sdl_audio_update(int hard_mute_active, int turbo_sink_active) {
if (!sdl_audio_device) return;
{ /* Tag audio events with the vblank frame counter. */
extern uint64_t s_frame_count;
@@ -1058,8 +1072,9 @@ static void sdl_audio_update(int turbo_active) {
const int HANGOVER_FRAMES = 8; /* ~133 ms at 60 fps */
static int muted = 0;
static int hangover = 0;
static int sink_was_active = 0;
if (turbo_active) {
if (hard_mute_active) {
if (!muted) {
int tail = sdl_audio_fade_samples;
const int buf_cap = (int)(sizeof(sdl_audio_buf) / (2 * sizeof(int16_t)));
@@ -1094,7 +1109,24 @@ static void sdl_audio_update(int turbo_active) {
extern int g_audio_unmute_resync;
g_audio_unmute_resync = 1;
}
sdl_audio_pump();
if (turbo_sink_active) {
if (!sink_was_active) {
sink_was_active = 1;
audio_trace_event(AUDIO_EV_MUTE, 0, 2); /* b=2: discard-only sink */
}
g_turbo_audio_sink_active = 1;
sdl_audio_pump(true);
return;
}
g_turbo_audio_sink_active = 0;
if (sink_was_active) {
sink_was_active = 0;
sdl_audio_fadein_left = sdl_audio_fade_samples;
audio_trace_event(AUDIO_EV_UNMUTE,
(uint32_t)sdl_audio_fadein_left, 2);
g_audio_unmute_resync = 1;
}
sdl_audio_pump(false);
}
/* PS1 digital pad button bits (active-low: 0=pressed, 1=released).
@@ -2009,6 +2041,44 @@ static PresRingEntry* present_ring_commit(uint8_t path, uint16_t disp_w,
return e;
}
/* ---- Load-transition ring ---------------------------------------------
* State edges only, rather than one record per frame. This makes the exact
* CD-read -> bridged-load -> turbo -> paced transitions cheap and queryable
* without changing any runtime behavior. */
#define LOAD_TRANSITION_CAP 512u
static LoadTransitionEntry s_load_transitions[LOAD_TRANSITION_CAP];
static uint64_t s_load_transition_total = 0;
extern "C" uint64_t load_transition_total(void) {
return s_load_transition_total;
}
extern "C" int load_transition_get(uint64_t seq, LoadTransitionEntry *out) {
if (seq >= s_load_transition_total) return 0;
if (s_load_transition_total - seq > LOAD_TRANSITION_CAP) return 0;
*out = s_load_transitions[seq & (LOAD_TRANSITION_CAP - 1u)];
return 1;
}
static void load_transition_note(int read_active, int load_active,
int turbo_active, int load_run) {
static int prev_read = -1, prev_load = -1, prev_turbo = -1;
if (read_active == prev_read && load_active == prev_load &&
turbo_active == prev_turbo) return;
extern uint64_t s_frame_count;
LoadTransitionEntry *e =
&s_load_transitions[s_load_transition_total & (LOAD_TRANSITION_CAP - 1u)];
s_load_transition_total++;
e->frame = (uint32_t)s_frame_count;
e->host_ms = (uint32_t)SDL_GetTicks();
e->load_run = (uint16_t)(load_run > 0xFFFF ? 0xFFFF : load_run);
e->read_active = (uint8_t)(read_active != 0);
e->load_active = (uint8_t)(load_active != 0);
e->turbo_active = (uint8_t)(turbo_active != 0);
prev_read = read_active;
prev_load = load_active;
prev_turbo = turbo_active;
}
/* Called from gpu_vblank_tick() at each simulated vblank. */
static void sdl_vblank_present(void) {
#ifndef PSX_NO_DEBUG_TOOLS
@@ -2097,26 +2167,33 @@ static void sdl_vblank_present(void) {
* sampled into SIO. Always-on; queried via the debug server "latency". */
latency_ring_frame_begin();
/* Turbo-active test, shared by the audio gate here and the pacing/
* present gate below. sdl_audio_update owns the mute + fade-in/out +
* debounce across turbo transitions (see its comment). */
/* Turbo-active test shared by the pacing/present gate below. */
int turbo_loads_active = 0;
extern int fntrace_is_game_started(void);
int logical_load_active = fntrace_is_game_started() && cdrom_load_in_progress();
int load_run_value = 0;
static int load_run = 0;
static int release_run = 0;
if (g_turbo_loads_enabled) {
extern int fntrace_is_game_started(void);
/* Hysteresis: count consecutive frames the load has held, and only
* engage turbo once it is SUSTAINED (TURBO_LOADS_ENGAGE_FRAMES). This
* stops brief incidental reads on music screens (boss/stage select) from
* flickering turbo on and chopping the audio. cdrom_load_in_progress()
* already bridges short intra-load gaps (CD_BURST_GAP_FRAMES), so a real
* load's counter does not reset mid-load. */
static int load_run = 0;
if (fntrace_is_game_started() && cdrom_load_in_progress()) {
/* Require a short sustained predicate on entry, then retain turbo over
* a short false gap after it has engaged. */
if (logical_load_active) {
if (load_run < (1 << 20)) load_run++;
if (load_run >= TURBO_LOADS_ENGAGE_FRAMES || release_run > 0) {
turbo_loads_active = 1;
release_run = TURBO_LOADS_RELEASE_FRAMES;
}
} else {
load_run = 0;
if (release_run > 0) {
turbo_loads_active = 1;
release_run--;
}
}
if (load_run >= TURBO_LOADS_ENGAGE_FRAMES)
turbo_loads_active = 1;
load_run_value = load_run;
} else {
load_run = 0;
release_run = 0;
}
/* HLE boot-skip window: from reset until the game entry PC first
* dispatches, run unpaced so the (shell-skipped) BIOS kernel init +
@@ -2125,6 +2202,9 @@ static void sdl_vblank_present(void) {
if (psx_bios_hle_boot_turbo_active())
turbo_loads_active = 1;
load_transition_note(cdrom_data_read_active(), logical_load_active,
turbo_loads_active, load_run_value);
/* FMV auto-skip ([video] auto_skip_fmv). A streaming FMV is XA audio + MDEC
* video together. Detect "MDEC produced a frame since the last vblank AND XA
* is streaming"; a short hold rides out brief inter-frame gaps. The present/
@@ -2170,13 +2250,12 @@ static void sdl_vblank_present(void) {
}
#ifndef PSX_SDL_NO_AUDIO
/* Turbo-loads no longer mutes audio. Loads at 1x+turbo are sub-second, and
* the mute/fade/hangover model produced more audible disruption (cuts and
* "restarts") than just letting the stream play through; the audio queue is
* capped (sdl_audio_pump max_queue_bytes) so a brief turbo burst can't build
* unbounded latency. FMV-skip still mutes: it fast-forwards an entire movie,
* where rendering the time-compressed audio would be pure noise. */
sdl_audio_update(fmv_skip_active);
/* Optional turbo host sink advances the canonical SPU on the exact guest
* sample budget but drops accelerated output before SDL. This is distinct
* from the old mute/freeze model: voice and CD state never pause. FMV skip
* retains the hard mute because it deliberately fast-forwards a movie. */
sdl_audio_update(fmv_skip_active,
turbo_loads_active && g_turbo_audio_sink_enabled);
#endif
if (g_headless) return;
@@ -2688,6 +2767,7 @@ int main(int argc, char** argv) {
uint32_t game_disc_crc = 0;
std::string disc_speed; /* "1x" | "2x" | "4x" | "instant" */
int instant_rate = 0; /* 0 = cdrom.c built-in default */
std::vector<PSXRecompV4::RuntimeConfig::WarmCdRoute> warm_cd_routes;
uint32_t game_entry_pc = 0;
bool fast_boot = false; /* DEPRECATED alias: HLE boot-skip only */
bool bios_hle = false; /* HLE kernel-service tier (bios_hle.c) */
@@ -2719,10 +2799,30 @@ int main(int argc, char** argv) {
if (gc.runtime.has_disc_speed) disc_speed = gc.runtime.disc_speed;
if (gc.runtime.has_instant_max_per_frame)
instant_rate = gc.runtime.instant_max_per_frame;
warm_cd_routes = gc.runtime.warm_cd_routes;
if (gc.runtime.turbo_loads) {
g_turbo_loads_enabled = 1;
std::fprintf(stdout, "psxrecomp: turbo_loads enabled (opt-in)\n");
}
if (gc.runtime.turbo_audio_sink) {
g_turbo_audio_sink_enabled = 1;
std::fprintf(stdout,
"psxrecomp: turbo_audio_sink enabled (opt-in)\n");
}
{
extern int g_idle_skip_enabled;
const char *idle_env = std::getenv("PSX_IDLE_SKIP");
g_idle_skip_enabled = idle_env
? (idle_env[0] == '1' ? 1 : 0)
: (gc.runtime.idle_skip ? 1 : 0);
std::fprintf(stdout, "psxrecomp: idle_skip %s%s\n",
g_idle_skip_enabled ? "enabled" : "disabled",
idle_env ? " (environment override)" : "");
}
for (uint32_t site : gc.vsync_event_horizon_sites)
psx_vsync_query_hle_add_event_horizon_site(site);
for (uint32_t site : gc.vsync_event_horizon_extra_sites)
psx_vsync_query_hle_add_extra_event_horizon_site(site);
g_video_scale = gc.runtime.video_supersampling;
g_video_aa = gc.runtime.video_antialiasing;
g_video_texfilter = gc.runtime.video_texture_filter;
@@ -3386,6 +3486,16 @@ int main(int argc, char** argv) {
std::fprintf(stdout, "psxrecomp: SPU float-shadow enabled (verified-enhancement)\n");
spu_init();
cdrom_init(disc_path_str.empty() ? NULL : disc_path_str.c_str());
for (const auto& route : warm_cd_routes) {
cdrom_register_warm_route(route.arm_lba, route.lbas.data(),
(int)route.lbas.size(),
route.instant_max_per_frame);
std::fprintf(stdout,
"psxrecomp: warm CD route armed at LBA %d (%zu entries, "
"%d sectors/frame)\n",
route.arm_lba, route.lbas.size(),
route.instant_max_per_frame);
}
if (!disc_path_str.empty()) {
/* GetID must report the inserted disc's license region (the BIOS CD
* driver revalidates it mid-game). Derive it from the disc's boot
+4
View File
@@ -1678,6 +1678,10 @@ uint32_t psx_cyc_load_word(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t r
psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask);
return psx_read_word(addr);
}
void psx_cyc_load_word_timing_only(CPUState* cpu, uint32_t addr,
uint32_t rt, uint32_t reg_mask) {
psx_cyc_load_timing(cpu, addr, 4u, rt, reg_mask);
}
uint16_t psx_cyc_load_half(CPUState* cpu, uint32_t addr, uint32_t rt, uint32_t reg_mask) {
psx_cyc_load_timing(cpu, addr, 2u, rt, reg_mask);
return psx_read_half(addr);
+5 -11
View File
@@ -259,7 +259,7 @@ uint64_t psx_get_cycle_count(void) {
* lockstep record/replay, and PSX_COSIM builds entirely (the cosim oracle
* compares per-instruction streams; elided iterations would false-diverge).
*
* Off switch: PSX_IDLE_SKIP=0 (env) or {"cmd":"idle_skip","enable":0} (TCP).
* Opt in: game config, PSX_IDLE_SKIP=1 (env), or the `idle_skip` TCP command.
* The always-on counters below are the observability surface. */
enum {
@@ -298,17 +298,11 @@ static void idle_snapshot_regs(const CPUState *cpu) {
static int idle_skip_on(void) {
if (g_idle_skip_enabled < 0) {
/* DISABLED BY DEFAULT (2026-07-06). The skip fast-forwards guest time
* to the next INTERNAL device event, but an SIO/memory-card transfer
* in flight is not currently modeled as a bounding deadline, so the
* skip jumps past the write-completion the card driver polls for at
* boot — Tomba2's "checking memory card" livelocks (frame rate
* collapses to a few fps; the game never advances). Opt in with
* PSX_IDLE_SKIP=1. Re-enable by default only once the skip is bounded
* by the SIO transfer/IRQ deadline (devices_cycles_to_next_internal_
* event must include the in-flight card-byte completion). */
/* No game config reached this process (for example a BIOS-only
* runtime). Keep the enhancement inert unless the environment opts in;
* normal game launches set g_idle_skip_enabled from RuntimeConfig. */
const char *e = getenv("PSX_IDLE_SKIP");
g_idle_skip_enabled = (e && e[0] == '1');
g_idle_skip_enabled = e ? (e[0] == '1') : 0;
}
return g_idle_skip_enabled;
}
+27 -12
View File
@@ -75,15 +75,23 @@ def delta_top(before, after, k=10):
return items[:k]
def snap(c):
fz = c.cmd("freeze_check", window=1)
return {
"t": time.monotonic(),
"wall": time.time(),
"cycles": int(fz["psx_cycle_count"]),
"frame": int(fz["frame_count"]),
"dirty_insns": int(fz["dirty_ram_insns"]),
}
def snap(c, retries=3):
# freeze_check occasionally returns a short/err response; retry rather
# than kill a long monitoring run over one bad sample.
for attempt in range(retries):
try:
fz = c.cmd("freeze_check", window=1)
return {
"t": time.monotonic(),
"wall": time.time(),
"cycles": int(fz["psx_cycle_count"]),
"frame": int(fz["frame_count"]),
"dirty_insns": int(fz["dirty_ram_insns"]),
}
except (KeyError, ValueError, json.JSONDecodeError):
if attempt == retries - 1:
raise
time.sleep(0.1)
def run(c, secs, tick, out):
@@ -92,12 +100,14 @@ def run(c, secs, tick, out):
t0 = time.monotonic()
bursts0 = c.cmd("cdrom_bursts", count=1).get("total", 0)
phot0 = phot_map(c.cmd("phase_hot", top=64))
stat0 = phot_map(c.cmd("phase_hot", set="static", top=64))
perpc0 = perpc_map(c.cmd("dirty_ram_stats"))
series = [snap(c)]
while time.monotonic() - t0 < secs:
time.sleep(tick)
series.append(snap(c))
phot1 = phot_map(c.cmd("phase_hot", top=64))
stat1 = phot_map(c.cmd("phase_hot", set="static", top=64))
perpc1 = perpc_map(c.cmd("dirty_ram_stats"))
bursts = c.cmd("cdrom_bursts", count=64)
span_s = series[-1]["t"] - series[0]["t"]
@@ -124,6 +134,7 @@ def run(c, secs, tick, out):
report = {"span_s": span_s, "span_mcyc_per_s": rate,
"bursts": [], "phot_delta": delta_top(phot0, phot1),
"static_delta": delta_top(stat0, stat1, 15),
"perpc_delta": delta_top(perpc0, perpc1)}
now_t = time.monotonic()
@@ -158,11 +169,15 @@ def run(c, secs, tick, out):
print(f" guest {gcyc/1e6:.0f} Mcyc = {rec['guest_s_at_1x']}s at 1x"
f" -> {rec['multiplier_vs_1x']}x effective")
print(f" interp insns in window: {gint}")
keys = ("interp", "native", "static", "gpu", "other", "exception")
w = ph.get("window", ph)
shares = {k: w.get(k) for k in keys if k in w}
shares = {k.replace("_share", ""): v for k, v in ph.items()
if k.endswith("_share")}
print(f" phase shares (covering secs, approximate): {shares}")
if report["static_delta"]:
tot = sum(v for _, v in report["static_delta"]) or 1
print("\ntop STATIC funcs by wall samples (span delta):")
for a, v in report["static_delta"]:
print(f" {a} {v} ({100.0*v/tot:.1f}% of top)")
if report["phot_delta"]:
print("\ntop native-overlay funcs by wall samples (span delta):")
for a, v in report["phot_delta"]: