mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
load: add opt-in event horizon, warm routes, and audio sink
This commit is contained in:
+35
-15
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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"]:
|
||||
|
||||
Reference in New Issue
Block a user