fixed severe audio slowdown on the news report at begin of the game

fixed severe audio slowdown on the news report at begin of the game
This commit is contained in:
Jessica_Natalia
2026-08-18 00:52:04 -03:00
parent 62f5774b9a
commit 568c95072d
24 changed files with 1540 additions and 76 deletions
+5
View File
@@ -24,6 +24,8 @@ option(PSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH
"Remove per-store write-watch branch from production AOT fast memory" OFF)
option(PSPRECOMP_AOT_PRODUCTION_FASTPATHS
"Compile out AOT chain diagnostic/counter branches for production builds" OFF)
option(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY
"Compile per-chain unit/hotspot telemetry into native cross-unit hot paths" ON)
set(PSPRECOMP_MSVC_CGTHREADS "0" CACHE STRING
"MSVC LTCG code-generation threads (0 = compiler default)")
if(NOT PSPRECOMP_MSVC_CGTHREADS MATCHES "^[0-9]+$")
@@ -56,6 +58,9 @@ endif()
if(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
add_compile_definitions(PSPRECOMP_AOT_PRODUCTION_FASTPATHS=1)
endif()
if(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
add_compile_definitions(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY=1)
endif()
if(MSVC AND PSPRECOMP_NATIVE_AVX2)
add_compile_options($<$<CONFIG:Release>:/arch:AVX2>
$<$<CONFIG:RelWithDebInfo>:/arch:AVX2>)
@@ -0,0 +1,168 @@
# VCS PERF V8.2 CPU RUNTIME LEAN — handoff
Date: 2026-08-17
## Baseline and reason for this stage
V8.1 CPU LEAN is the protected baseline. The user reported that it fixed the
slow-motion feeling at low FPS. Its runtime architecture therefore remains
unchanged where timing/scheduling is concerned.
The latest V8.1 log confirms the CPU direction is better than V7/V8 under heavy
workload. Using all windows with at least 50k game draws:
- V7: 12.781 ms guest CPU, 57.29 FPS (120 windows, mean 74,461 draws)
- V8: 12.952 ms guest CPU, 56.70 FPS (124 windows, mean 74,482 draws)
- V8.1: 11.969 ms guest CPU, 60.69 FPS (122 windows, mean 76,124 draws)
These are workload-threshold aggregates, not deterministic replay pairs. V8.2
therefore does not alter the V8.1 frame/tick behavior; it targets repeated native
chain overhead and code footprint.
## 1. Generic PSPRecomp runtime-chain telemetry compile switch
New CMake option:
`PSPRECOMP_RUNTIME_CHAIN_TELEMETRY`
The generic framework defaults it ON so profiling capability remains available.
The VCS production build script defaults it OFF.
When OFF, the compiler removes per-chain branches/counters for:
- `g_guest_hotspot_profile_enabled`
- `g_guest_hotspot_unit_calls`
- guest-hotspot sampling clocks/records
- `g_unit_profile_enabled`
- `g_unit_profile_counts`
from compile-time direct chains, dynamic generated-unit chains, and Tier-2 fused
transfer entry accounting.
This does **not** disable the low-overhead VCS `PERF` telemetry used for FPS,
guest CPU, GE, Present and renderer workload measurements.
To produce a deep diagnostic build:
```bat
set PSPRECOMP_RUNTIME_CHAIN_TELEMETRY=ON
profiles\vcs\BUILD_VCS_NINJA.bat
```
## 2. PSP scheduler/tick semantics are protected
The following mechanisms are unchanged from V8.1:
- `PSPRECOMP_TIME_TICK_DISPATCHES` policy
- `g_runtime_starvation_interval_fast`
- `dispatches_since_import_`
- `run_starvation_boundary()`
- context-switch invalidation / native-chain unwind behavior
- `account_inlined_generated_leaf()` scheduler cadence
- Tier-2 fused-transfer scheduler accounting
This is intentional because V8.1 resolved the user's low-FPS slow-motion feeling.
## 3. Five tiny Boundary leaf wrappers removed
The Boundary cluster has four unit-0206 targets that pass the existing static
leaf validator:
- `0x08B3E084`
- `0x08B3E08C` (two call sites)
- `0x08B3E254`
- `0x08B3E260`
Total transformed sites: **5**.
The validator rejects nested generated/HLE/syscall calls, non-`$ra` returns and
foreign explicit PC exits. Each transformed site invokes the exact generated
entry and then calls `account_inlined_generated_leaf()` so the removed native
wrapper still contributes exactly one scheduler work edge.
No World direct-leaf expansion was enabled because previous experimentation
showed a compiler/codegen cliff when broad direct-leaf work was mixed with
World's GPR shadow.
## 4. Geometry/World protected shape
V8.2 deliberately keeps:
- Geometry: **379 blocks**, no GPR shadow, no inline/direct-leaf experiment
- World: **402 blocks**, no new direct-leaf experiment
- Tier-2 total: **1,060 blocks**
- static fused calls: **36**
- static fused tails: **13**
- Tier-2 direct-fastmem: **1,969 sites**
This avoids repeating V8's I-cache/code-size regression.
## Static code-size evidence
Same compiler/flags, Linux x86-64, V8.1 vs V8.2:
- generated unit 0085 `.text`: 374,333 -> 354,075 bytes (**-5.41%**)
- generated unit 0158 `.text`: 360,754 -> 345,782 bytes (**-4.15%**)
- Geometry Tier-2 `.text`: 170,247 -> 161,163 bytes (**-5.34%**), still 379 blocks
- Boundary Tier-2 `.text`: 24,937 -> 18,257 bytes (**-26.79%**)
`nm` audit on V8.2 unit 0085 shows no references to the guest-hotspot/unit-profile
symbols that were present in the V8.1 object.
## Isolated wrapper microbenchmark
20 million calls, x86-64 optimized build, scheduler interval disabled to isolate
native wrapper overhead:
- ordinary `invoke_chained_direct`: ~2.8 ns/call
- statically proven leaf direct call + `account_inlined_generated_leaf`: ~1.1–1.2 ns/call
This microbenchmark only validates the local mechanism. It is **not** a game-FPS
claim; Windows VCS gameplay remains authoritative.
## Validation
Passed:
- `psprecomp_tests`
- `vcs_profile_tests` (scheduler/callback/framebuffer)
- `vcs_config_tests`
- `vfpu_tier2_tests`
- V8.2 static audit
- 10/10 hooked generated units syntax-checked with production macros
- 7/7 Tier-2 cluster objects compiled
- generated unit 0085 optimized object compile
- generated unit 0158 optimized object compile
- Geometry/Boundary optimized object A/B code-size builds
A full Linux VCSNative link was started. The 234-unit O3 generated corpus exceeds
the interactive build window; no source diagnostic occurred before interruption.
The Windows VS2022/Ninja build is authoritative for the full executable.
## Runtime identity
Expected log:
```text
stage=perf-v8.2-cpu-runtime-lean-2026-08-17
...
guest_hotspot=0 ... chain_telemetry_compiled=0
...
perf_layer=8 cpu_lean_revision=2 runtime_chain_telemetry_default=0
...
direct_generated_leaf_sites=5
geometry_fusion_rollback=1
```
## What to compare
Primary comparison is V8.2 against V8.1, not against the rejected V8 build.
Compare `guest_cpu_us_avg` and FPS for similar draw bins, especially 50k+, 70k+
and 90k+ game draws. The game-speed/slow-motion feeling must remain identical to
V8.1; any regression there invalidates this stage regardless of average FPS.
## Progress estimate
- overall VCS recomp project: **~92%**
- CPU/Tier-2 architecture: **~97%**
- >150 FPS headroom goal: **~61%**, pending V8.2 Windows gameplay benchmark
@@ -0,0 +1,58 @@
{
"stage": "perf-v8.2-cpu-runtime-lean-2026-08-17",
"base": "perf-v8.1-cpu-lean-2026-08-17",
"protected_baseline": {
"slow_motion_feeling_fixed": true,
"geometry_blocks": 379,
"world_blocks": 402,
"tier2_hot_blocks": 1060,
"tier2_direct_mem_sites": 1969,
"scheduler_tick_semantics_changed": false
},
"runtime_chain_optimization": {
"generic_psprecomp_option": "PSPRECOMP_RUNTIME_CHAIN_TELEMETRY",
"generic_default": true,
"vcs_performance_default": false,
"perf_telemetry_preserved": true,
"scheduler_accounting_preserved": true
},
"boundary_direct_generated_leaves": {
"targets": ["0x08B3E084", "0x08B3E08C", "0x08B3E254", "0x08B3E260"],
"call_sites": 5,
"world_experiment_enabled": false,
"geometry_experiment_enabled": false
},
"static_text_bytes": {
"generated_unit_0085": {"v81": 374333, "v82": 354075, "reduction_percent": 5.41},
"generated_unit_0158": {"v81": 360754, "v82": 345782, "reduction_percent": 4.15},
"tier2_geometry": {"v81": 170247, "v82": 161163, "reduction_percent": 5.34},
"tier2_boundary": {"v81": 24937, "v82": 18257, "reduction_percent": 26.79}
},
"isolated_microbench_ns_per_call": {
"ordinary_invoke_chained_direct_approx": 2.8,
"direct_leaf_plus_scheduler_accounting_approx_range": [1.1, 1.2],
"game_fps_claim": false
},
"v81_runtime_baseline_ge_50000_draws": {
"windows": 122,
"guest_cpu_ms_mean": 11.969,
"fps_mean": 60.69,
"game_draws_mean": 76124
},
"validation": {
"psprecomp_tests": "PASS",
"vcs_profile_tests": "PASS",
"vcs_config_tests": "PASS",
"vfpu_tier2_tests": "PASS",
"v82_static_audit": "PASS",
"hook_units_syntax": "10/10 PASS",
"tier2_cluster_objects": "7/7 PASS",
"full_linux_vcsnative_link": "NOT COMPLETED: generated O3 corpus exceeded interactive build window without source diagnostic",
"windows_vs2022": "PENDING USER BUILD"
},
"progress_estimate": {
"overall_project_percent": 92,
"cpu_tier2_architecture_percent": 97,
"performance_150fps_goal_percent": 61
}
}
@@ -0,0 +1,165 @@
# VCSNative V8.2.1 — Correctness: Save + SAS loops + NEWS radio
**Date:** 2026-08-17
**Base:** PSPRecomp-VCS-PERF-V8.2-CPU-RUNTIME-LEAN-2026-08-17
**Stage:** `correctness-v8.2.1-save-audio-news-2026-08-17`
## Why this release exists
Performance work is intentionally paused. V8.1/V8.2 restored the correct gameplay-speed feeling, but three correctness risks have priority before further CPU work:
1. after a real successful Save Game, some later mission states can leave a black screen;
2. a vehicle-related SAS effect (reported most clearly on motorcycles, possibly brake/engine) can remain looped until entering the menu;
3. NEWS radio transitions may still hitch when a second ATRAC stream starts.
The V8.2 Tier-2 shape, Geometry rollback, scheduler cadence and runtime-chain lean path are unchanged.
## 1. Save Game black-screen hardening
### Firmware lifecycle fix
The successful LISTSAVE result path previously did all of these at once when the user acknowledged `SAVE COMPLETED`:
- changed the utility state to `Quit`;
- destroyed the host savedata UI;
- immediately cleared `display_window`'s system-utility ownership.
The guest had **not yet** observed `Quit` and called `sceUtilitySavedataShutdownStart`. The new path keeps system-utility ownership through that final PSP utility transition and releases it only from the existing ShutdownStart/Finished path.
This does not reuse or alter the older V9.6 LISTSAVE-cancel workaround. Load cancellation behavior remains unchanged.
### Transactional slot writes
The previous save writer truncated `DATA.BIN` before validating/writing every optional payload. Mission-dependent ICON/PIC/SND descriptors could therefore fail after the main file was already replaced.
V8.2.1 now:
1. validates and snapshots the main game save buffer;
2. validates and snapshots every supplied optional savedata buffer;
3. stages every host file beside its final target;
4. moves any old file to a temporary backup;
5. commits the staged files;
6. rolls all already-committed files back if a later commit fails;
7. removes backups only after the complete commit succeeds.
`VCSNative.meta` remains host-only decoration and cannot turn a valid game save into a failed PSP save operation.
A regression test creates an existing `DATA.BIN`, deliberately makes a later auxiliary target impossible, requires the transaction to fail, and verifies the old `DATA.BIN` remains byte-for-byte intact.
## 2. Stuck motorcycle / vehicle sound
The SAS error value `0x80420016` is now named `kSasErrorInvalidState` rather than the misleading `kSasErrorVoicePaused`.
More importantly, `__sceSasSetKeyOff` no longer rejects an active voice merely because that voice is currently mixer-paused. Pause is treated as a mixer gate, while KeyOff still latches:
- `on = false`;
- envelope phase = `Release`.
This closes the state hole where a looped VAG vehicle voice could be paused during a transition, receive a rejected KeyOff, and later resume with its key still logically held.
The existing zero-release-rate safeguard remains in place. A new regression test exercises:
`looped VAG -> paused -> KeyOff -> unpause -> Release -> retired`
and requires the voice to reach zero envelope and stop playing.
## 3. NEWS radio hitch investigation/fix
### Removed synchronous source scan from the common path
ATRAC setup previously identified a host source by iterating all registered AT3/AA3/OMA files with the same size, opening each candidate synchronously and reading up to 256 header bytes.
V8.2.1 records the native path associated with a normal file descriptor. When `sceIoRead` fills a guest buffer from an ATRAC source, it records the exact producer for that guest buffer. `sceAtracSetHalfwayBufferAndGetID` then resolves the source directly from that buffer address:
- no directory-sized candidate scan;
- no host file open;
- no header reread in the common case.
A bounded 256-byte prefix cache remains only as a fallback for raw virtual-disc layouts where the direct fd-to-buffer association is unavailable. Each fallback file is read at most once.
### Runtime proof in normal VCSNative.log
No environment variable is required. Low-volume lines are now written to the normal runtime log:
```text
ATRAC_SOURCE resolve_us=... candidates=... direct_buffer=1 fallback_reads=0 ... source=NEWS_....AT3
ATRAC_DECODE source=NEWS_....AT3 open=1 decode_us=... bytes=... sample=... slow_events=...
```
`ATRAC_DECODE` is emitted for every decoder open and for later decode calls taking at least 2 ms. Therefore the next physical NEWS test can distinguish:
- source resolution stall;
- FFmpeg decoder-open stall;
- slow steady-state decode.
This release removes the known synchronous resolver scan. It does **not** claim that FFmpeg opening a brand-new NEWS decoder is already proven hitch-free; the new log makes that measurable before a more invasive decoder-prewarm change is considered.
## Preserved V8.2 performance baseline
Unchanged:
- hot blocks: 1,060;
- static fused calls: 36;
- Geometry inline generated leaves: 0;
- Boundary direct generated leaf sites: 5;
- `cpu_lean_revision=2`;
- Geometry fusion rollback active;
- direct fastmem active;
- GE async quarantined/off;
- parallel vertex decode quarantined/off;
- scheduler/starvation cadence unchanged.
## Runtime identity
Expected header:
```text
stage=correctness-v8.2.1-save-audio-news-2026-08-17
...
correctness_revision=821
save_transaction=1
save_shutdown_lifecycle=1
sas_paused_keyoff=1
atrac_direct_source=1
atrac_stall_diag=1
```
## Validation completed
- `vcs_profile.cpp` VCSNative Release object: PASS.
- `vcs_runtime_log.cpp` VCSNative Release object: PASS.
- `psprecomp_tests`: PASS.
- `vcs_profile_tests`: PASS.
- `vcs_config_tests`: PASS.
- `vfpu_tier2_tests`: PASS.
- V8.2 CPU runtime-lean audit: PASS.
- V8.2.1 correctness static audit: **13/13 PASS**.
- transactional save rollback fixture: PASS as part of `vcs_profile_tests`.
- paused looped-SAS KeyOff fixture: PASS as part of `vcs_profile_tests`.
- changed-source audit for the prohibited external emulator name: PASS.
A full fresh Linux VCSNative link was not repeated because this host-only correctness overlay does not modify generated AOT/Tier-2/DX12 code; the changed VCSNative target objects were compiled directly. Windows VS2022/Ninja plus real VCS gameplay remains authoritative.
## Windows build / install
Preferred path from current V8.2 source: extract the V8.2.1 overlay over it, overwrite files, **do not delete `out`**, then run:
```bat
profiles\vcs\BUILD_VCS_NINJA.bat
```
The new one-shot stamp invalidates only `vcs_profile` and `vcs_runtime_log` objects. Generated AOT, Tier-2 and DX12 objects are preserved.
## Physical test order
1. Load the same save and complete several missions.
2. Save into an existing slot and a new slot; acknowledge `SAVE COMPLETED`; verify normal return every time.
3. Reproduce the motorcycle/vehicle sound that previously stuck; enter/leave pause and vehicles repeatedly; verify no effect survives incorrectly.
4. Listen until a NEWS bulletin starts. If any hitch remains, provide the new `VCSNative(...).log`; inspect `ATRAC_SOURCE` and `ATRAC_DECODE` around the NEWS filename.
## Progress estimate
- Overall VCS recomp project: **~92%**.
- Correctness/frontend/audio stabilization: **~94% implementation**, pending physical Windows verification of these three reports.
- CPU/Tier-2 optimization: remains **~97% architectural work**, intentionally paused here.
- 150+ FPS headroom objective: unchanged from V8.2 and not advanced by this correctness release.
@@ -0,0 +1,54 @@
{
"release": "PSPRecomp-VCS-V8.2.1-CORRECTNESS-SAVE-AUDIO-NEWS-2026-08-17",
"base": "PSPRecomp-VCS-PERF-V8.2-CPU-RUNTIME-LEAN-2026-08-17",
"stage": "correctness-v8.2.1-save-audio-news-2026-08-17",
"performance_baseline_preserved": {
"hot_blocks": 1060,
"static_fused_calls": 36,
"static_fused_tail": 13,
"hooks": 25,
"boundary_direct_generated_leaf_sites": 5,
"geometry_direct_generated_leaf_sites": 0,
"cpu_lean_revision": 2,
"scheduler_changed": false,
"geometry_changed": false,
"dx12_changed": false
},
"savedata": {
"transactional_buffer_snapshot": true,
"stage_before_replace": true,
"rollback_on_commit_failure": true,
"listsave_system_utility_released_at_shutdown": true,
"v9_6_cancel_policy_changed": false
},
"sas": {
"error_80420016_name": "InvalidState",
"keyoff_while_paused_latches_release": true,
"zero_release_fallback_preserved": true,
"paused_loop_regression_test": true
},
"news_atrac": {
"direct_sceIoRead_buffer_source_resolution": true,
"common_path_host_file_scan": false,
"lazy_prefix_fallback_bytes": 256,
"runtime_source_timing_log": true,
"runtime_decode_open_timing_log": true,
"slow_decode_threshold_us": 2000,
"ffmpeg_open_hitch_physically_verified": false
},
"validation": {
"psprecomp_tests": "PASS",
"vcs_profile_tests": "PASS",
"vcs_config_tests": "PASS",
"vfpu_tier2_tests": "PASS",
"v8_2_baseline_audit": "PASS",
"v8_2_1_correctness_audit": "13/13 PASS",
"vcsnative_vcs_profile_object": "PASS",
"vcsnative_runtime_log_object": "PASS"
},
"progress_estimate_percent": {
"overall": 92,
"correctness_frontend_audio_implementation": 94,
"cpu_tier2_architecture": 97
}
}
@@ -0,0 +1,88 @@
# VCSNative V8.2.2 Correctness Recovery — Save / SAS / ATRAC
Date: 2026-08-17
Base: V8.2 CPU Runtime Lean stable source (not V8.2.1)
Estimated total VCS recomp project progress: ~92%
## Why this recovery exists
The physical V8.2.1 test did not fix the stuck vehicle sound and introduced/revealed an infinite mission-interior Loading state. The supplied runtime log shows the Loading screen is active rather than blocked: from vblank 20461 onward the same 4500 game draws / 4080 GPU draws / 420 merges repeat with host I/O at zero for thousands of vblanks. There are no savedata operations in that run before the interior stall.
V8.2.2 therefore rebases correctness work onto the previously stable V8.2 source and does not carry forward the speculative V8.2.1 paused-KeyOff or LISTSAVE lifecycle changes.
## 1. Mission-interior Loading regression recovery
- Source base restored to V8.2 for runtime behavior.
- V8.2.1 paused-KeyOff behavior removed.
- V8.2.1 LISTSAVE host-ownership/lifecycle experiment removed.
- Tier-2, scheduler, Geometry, World, DX12 and performance architecture are unchanged from V8.2.
This removes V8.2.1-specific runtime changes from the mission/interior path. Physical gameplay remains authoritative to prove the interior transition is restored.
## 2. SAS audio correctness
### Latched EndFlag
The old HLE returned `!voice.playing` directly from `__sceSasGetEndFlag`. PSP SAS exposes end flags as a snapshot refreshed by a completed Core mixer cycle. V8.2.2 stores `SasState::end_flags` and refreshes it only after `sas_mix_into` / `sas_mix_raw` completes.
This prevents guest audio control code from observing a voice transition before the mixer cycle that publishes it, which can otherwise cause premature voice reuse/rearming.
### VAG loop predictor restoration
The decoder already captured `loop_start_history1/2`, but `rewind_loop()` never restored them. V8.2.2 restores both predictor histories on every loop jump. This makes every pass through a loop start from the same ADPCM decoder state instead of accumulating waveform drift.
### What was deliberately reverted
The V8.2.1 helper that accepted KeyOff while a voice was paused is gone. V8.2 paused-KeyOff behavior is restored until a physical trace proves a different state transition is required.
## 3. Savedata
The useful transactional write protection is retained, but the UI/status lifecycle is exactly the V8.2/V9.6 behavior again.
Before replacing an existing slot, V8.2.2 snapshots all guest buffers, stages DATA.BIN and optional auxiliary files to temporary host files, and commits only after all staging succeeds. A failure rolls the old slot back instead of leaving DATA.BIN partially replaced.
Normal VCSNative.log now records low-volume LISTSAVE diagnostics:
- `SAVEDATA_SAVE result=... data=... icon0=... icon1=... pic1=... snd0=...`
- `SAVEDATA_SAVE acknowledged status=QUIT`
- `SAVEDATA_SAVE shutdown status=FINISHED`
These lines will distinguish an I/O/descriptor failure from a guest frontend transition if the original post-save black screen still reproduces.
## 4. ATRAC / radio NEWS stall
V8.2.1 attempted direct source tracking only for ordinary host-file reads. The physical log showed every observed source resolution as `direct_buffer=0`, proving the radio was taking another path. VCS commonly reads these streams through the virtual UMD handle.
V8.2.2 maps virtual-disc read offsets back to the indexed `VirtualDiscFile` before the read, associates an ATRAC guest destination buffer with the exact AT3/AA3/OMA source, and consumes that mapping in `sceAtracSetHalfwayBufferAndGetID`.
The old content-header search remains only as fallback. `ATRAC_SOURCE` and `ATRAC_DECODE` remain low-volume runtime diagnostics.
Expected healthy line:
`ATRAC_SOURCE resolve_us=<small> direct_buffer=1 fallback_reads=0 source=...AT3`
The V8.2.1 physical log contained a 7152 us source lookup while the matching decoder open was only 431 us, so source resolution itself was still capable of producing a visible radio hitch.
## Validation
- CMake Release/Ninja configure: PASS
- changed `vcs_profile.cpp` object: PASS
- changed `vcs_runtime_log.cpp` object: PASS
- psprecomp_tests: PASS
- vcs_profile_tests: PASS
- vcs_config_tests: PASS
- vfpu_tier2_tests: PASS
- V8.2 CPU runtime lean audit: PASS
- V8.2.2 correctness recovery audit: PASS (15 checks)
- forbidden external-emulator reference audit: PASS
The full Windows/DX12 executable was not linked in the Linux container. VS2022/Ninja on the user's machine remains the authoritative full build and physical runtime test.
## Physical test order
1. Enter the same mission interior that looped on Loading. This is the first gate.
2. Reproduce the motorcycle/vehicle sound that used to stay looped; verify it retires without opening the menu.
3. Save after several missions and complete the Save UI. If black returns, send the new log; the SAVEDATA_SAVE lines now expose the exact result/descriptor state.
4. Let radio/news transitions occur. Check whether `ATRAC_SOURCE` is now `direct_buffer=1 fallback_reads=0`, and report any visible hitch.
Do not resume CPU optimization until these correctness gates pass.
@@ -0,0 +1,48 @@
{
"release": "V8.2.2 Correctness Recovery",
"date": "2026-08-17",
"base": "V8.2 CPU Runtime Lean",
"estimated_total_project_progress_percent": 92,
"runtime_stage": "correctness-v8.2.2-recovery-save-audio-news-2026-08-17",
"recovery": {
"v821_paused_keyoff_removed": true,
"v821_save_lifecycle_experiment_removed": true,
"tier2_scheduler_geometry_unchanged_from_v82": true
},
"sas": {
"end_flags_latched_after_core": true,
"vag_loop_predictor_history_restored": true
},
"savedata": {
"transactional_commit": true,
"rollback_on_staging_or_commit_failure": true,
"v82_v96_lifecycle_preserved": true,
"runtime_save_diagnostics": true
},
"atrac": {
"regular_file_buffer_source_tracking": true,
"virtual_umd_buffer_source_tracking": true,
"header_scan_fallback_retained": true,
"source_and_decode_timing_log": true,
"v821_observed_max_source_resolution_us": 7152,
"matching_decode_open_us": 431
},
"loading_loop_observation": {
"first_stable_vblank": 20461,
"last_observed_vblank": 24481,
"repeated_game_draws": 4500,
"repeated_gpu_draws": 4080,
"repeated_batch_merges": 420,
"io_us_avg": 0
},
"validation": {
"psprecomp_tests": "PASS",
"vcs_profile_tests": "PASS",
"vcs_config_tests": "PASS",
"vfpu_tier2_tests": "PASS",
"v82_static_audit": "PASS",
"v822_static_audit": "PASS",
"changed_host_objects": "PASS",
"full_windows_runtime": "PENDING_PHYSICAL_TEST"
}
}
+6
View File
@@ -247,6 +247,7 @@ public:
// invalidation flag. All active direct ancestors see that same hot
// byte and unwind. This replaces two process-global 64-bit loads on
// every fixed cross-unit transfer with one normally-false local load.
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
bool guest_hotspot_sample = false;
std::uint64_t guest_hotspot_start_ns = 0u;
if (g_guest_hotspot_profile_enabled) {
@@ -257,6 +258,7 @@ public:
(ticket & static_cast<std::uint64_t>(g_guest_hotspot_sample_mask)) == 0u;
if (guest_hotspot_sample) guest_hotspot_start_ns = guest_hotspot_clock_ns();
}
#endif
struct DepthGuard {
std::uint32_t &depth;
@@ -278,6 +280,7 @@ public:
} else {
Function(*this, ctx);
}
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
if (guest_hotspot_sample) {
const std::uint64_t end_ns = guest_hotspot_clock_ns();
guest_hotspot_record_sample(UnitIndex, DirectTargetPc,
@@ -285,6 +288,7 @@ public:
? end_ns - guest_hotspot_start_ns : 0u);
}
if (g_unit_profile_enabled) ++g_unit_profile_counts[UnitIndex];
#endif
#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
if (track_dispatch_counters_) {
++chained_dispatches_;
@@ -338,10 +342,12 @@ public:
return false;
}
++chain_depth_;
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
if (g_unit_profile_enabled && UnitIndex < kUnitProfileCapacity)
++g_unit_profile_counts[UnitIndex];
if (g_guest_hotspot_profile_enabled && UnitIndex < kUnitProfileCapacity)
++g_guest_hotspot_unit_calls[UnitIndex];
#endif
return true;
}
+25 -1
View File
@@ -47,6 +47,7 @@ constexpr std::uint64_t kMixSafetyFrames = 1024u;
// channel lapping the ring during normal realtime play.
constexpr std::size_t kRingFrames = kSampleRate * 2u;
constexpr std::size_t kGuestChannels = 9u;
constexpr std::size_t kOutput2Channel = 8u;
constexpr std::uint64_t kChannelDiscontinuityFrames = 64u;
struct Block {
@@ -80,6 +81,7 @@ struct AudioState {
std::uint64_t queued_blocks{};
std::uint64_t underrun_rebuffers{};
std::uint64_t timeline_resyncs{};
std::uint64_t output2_seal_clamps{};
std::uint64_t submit_calls{};
std::uint64_t submit_cpu_ns{};
std::uint64_t submit_cpu_max_ns{};
@@ -330,8 +332,28 @@ void advance_locked(AudioState &state, std::uint64_t guest_time_us) {
// audible underrun without changing the normal multi-channel mix path.
const std::uint64_t safety_frames = state.playback_started && outstanding <= 2u
? 0u : kMixSafetyFrames;
const std::uint64_t sealed_frame = guest_frame > safety_frames
std::uint64_t sealed_frame = guest_frame > safety_frames
? guest_frame - safety_frames : 0u;
// Channel 8 is sceAudioOutput2: VCS' final 44.1-kHz stereo music/radio
// mixer. Do not seal host timeline frames that this producer has not
// actually supplied yet. In heavy scenes virtual_time_us can jump ahead
// when the renderer misses wall-clock pace; the old code then committed
// zeros to waveOut before Output2 delivered the next 512-frame block.
// Once committed those frames cannot be repaired, so spoken NEWS arrived
// as a train of missing chunks and sounded dragged/stuttered.
//
// Keep this bounded: if Output2 really stops for longer than the current
// device prebuffer, it stops being the watermark and other PSP channels
// are allowed to advance normally.
const ChannelStream &output2 = state.channels[kOutput2Channel];
const std::uint64_t producer_grace =
static_cast<std::uint64_t>(state.prebuffer_blocks) * kBlockFrames;
const std::uint64_t unclamped_sealed_frame = sealed_frame;
sealed_frame = audio_output_master_seal_frame(
guest_frame, sealed_frame, output2.active, output2.cursor, producer_grace);
if (sealed_frame != unclamped_sealed_frame) ++state.output2_seal_clamps;
while (sealed_frame >= state.output_frame + kBlockFrames) {
if (!queue_one_block(state)) break;
}
@@ -473,6 +495,7 @@ void audio_output_advance(std::uint64_t guest_time_us) {
<< " recovering=" << state.recovering_from_underrun
<< " underrun_rebuffers=" << state.underrun_rebuffers
<< " resyncs=" << state.timeline_resyncs
<< " output2_clamps=" << state.output2_seal_clamps
<< " late_frames=" << state.late_frames_dropped
<< " overrun_frames=" << state.overrun_frames_dropped
<< " submit_calls=" << state.submit_calls
@@ -529,6 +552,7 @@ void audio_output_shutdown() {
}
state.late_frames_dropped = 0u;
state.overrun_frames_dropped = 0u;
state.output2_seal_clamps = 0u;
}
} // namespace vcs
+13
View File
@@ -5,6 +5,19 @@
namespace vcs {
// sceAudioOutput2 is the game's continuous final music/radio producer. When
// virtual time runs ahead of that producer, sealing future host frames would
// make the gap irreversible. Clamp only while the producer is within the
// bounded device-prebuffer grace window; if it truly stalls beyond that, the
// rest of the PSP channels may advance normally.
[[nodiscard]] constexpr std::uint64_t audio_output_master_seal_frame(
std::uint64_t guest_frame, std::uint64_t sealed_frame, bool producer_active,
std::uint64_t producer_cursor, std::uint64_t grace_frames) noexcept {
if (!producer_active) return sealed_frame;
if (guest_frame > producer_cursor + grace_frames) return sealed_frame;
return sealed_frame > producer_cursor ? producer_cursor : sealed_frame;
}
// Host audio sink for the sceAudio HLE. The PSP exposes eight regular PCM
// channels plus one SRC/Output2 channel; submissions are mixed on the guest's
// virtual-time line before they are handed to the native audio device.
+487 -62
View File
@@ -191,6 +191,11 @@ struct FileTable {
std::int32_t next_fd{3};
std::uint32_t next_virtual_sector{0x00010000u};
std::unordered_map<std::int32_t, std::fstream> files;
std::unordered_map<std::int32_t, std::filesystem::path> file_paths;
// Producer tracking for ATRAC setup. A guest buffer filled directly from an
// AT3/AA3/OMA can be associated with its host source without reopening and
// rescanning candidate files on the audio thread.
std::unordered_map<std::uint32_t, std::filesystem::path> recent_atrac_reads;
std::unordered_set<std::int32_t> synthetic_empty_files;
std::unordered_map<std::int32_t, DirectoryHandle> directories;
std::unordered_map<std::int32_t, VirtualDiscHandle> virtual_disc_handles;
@@ -535,6 +540,26 @@ std::string normalized_native_path(const std::filesystem::path &path) {
return normalized.generic_string();
}
bool is_atrac_source_path(const std::filesystem::path &path) {
std::string extension = path.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
return extension == ".AT3" || extension == ".AA3" || extension == ".OMA";
}
const VirtualDiscFile *virtual_disc_file_at_offset(std::uint64_t absolute) {
const std::uint64_t sector64 = absolute / 2048u;
if (sector64 > 0xFFFFFFFFull) return nullptr;
const auto next = file_table.virtual_path_by_sector.upper_bound(static_cast<std::uint32_t>(sector64));
if (next == file_table.virtual_path_by_sector.begin()) return nullptr;
const auto previous = std::prev(next);
const auto found = file_table.virtual_files_by_path.find(previous->second);
if (found == file_table.virtual_files_by_path.end()) return nullptr;
const std::uint64_t start = static_cast<std::uint64_t>(found->second.start_sector) * 2048u;
if (absolute < start || absolute >= start + found->second.size) return nullptr;
return &found->second;
}
const VirtualDiscFile *register_virtual_disc_file(const std::filesystem::path &path) {
std::error_code error;
if (!std::filesystem::is_regular_file(path, error) || error) return nullptr;
@@ -882,22 +907,49 @@ bool parse_atrac_header(std::span<const std::uint8_t> bytes, ParsedAtracHeader &
return true;
}
std::filesystem::path identify_atrac_source(std::span<const std::uint8_t> header, const ParsedAtracHeader &parsed) {
const std::size_t compare_size = std::min<std::size_t>(header.size(), 256u);
for (const auto &[key, file] : file_table.virtual_files_by_path) {
if (file.size != parsed.file_size) continue;
std::string extension = file.native_path.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
if (extension != ".AT3" && extension != ".AA3" && extension != ".OMA") continue;
std::vector<std::uint8_t> candidate(compare_size);
std::ifstream input(file.native_path, std::ios::binary);
if (!input) continue;
input.read(reinterpret_cast<char *>(candidate.data()), static_cast<std::streamsize>(candidate.size()));
if (input.gcount() == static_cast<std::streamsize>(candidate.size()) &&
std::equal(candidate.begin(), candidate.end(), header.begin())) return file.native_path;
std::filesystem::path identify_atrac_source(std::uint32_t guest_buffer,
std::span<const std::uint8_t> header,
const ParsedAtracHeader &parsed) {
const auto started = std::chrono::steady_clock::now();
bool direct_buffer = false;
std::uint32_t fallback_reads = 0u;
std::filesystem::path matched;
if (const auto direct = file_table.recent_atrac_reads.find(guest_buffer);
direct != file_table.recent_atrac_reads.end()) {
matched = direct->second;
direct_buffer = true;
file_table.recent_atrac_reads.erase(direct);
}
return {};
const std::size_t compare_size = std::min<std::size_t>(header.size(), 256u);
if (matched.empty()) {
for (const auto &[key, file] : file_table.virtual_files_by_path) {
(void)key;
if (file.size != parsed.file_size || !is_atrac_source_path(file.native_path)) continue;
++fallback_reads;
std::vector<std::uint8_t> candidate(compare_size);
std::ifstream input(file.native_path, std::ios::binary);
if (!input) continue;
input.read(reinterpret_cast<char *>(candidate.data()), static_cast<std::streamsize>(candidate.size()));
if (input.gcount() == static_cast<std::streamsize>(candidate.size()) &&
std::equal(candidate.begin(), candidate.end(), header.begin())) {
matched = file.native_path;
break;
}
}
}
const std::uint64_t elapsed_us = static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - started).count());
std::ostringstream line;
line << "ATRAC_SOURCE resolve_us=" << elapsed_us
<< " direct_buffer=" << (direct_buffer ? 1 : 0)
<< " fallback_reads=" << fallback_reads
<< " source=" << (matched.empty() ? "<none>" : matched.filename().string());
runtime_log_line(line.str());
return matched;
}
// sceAtracDecodeData always hands the caller two interleaved channels: the PSP
@@ -951,6 +1003,35 @@ std::uint32_t atrac_samples_per_frame(const AtracContextState &state) {
return state.header.atrac3plus ? 2048u : 1024u;
}
// sceAtrac streaming APIs use negative sentinel values once the encoded stream
// is entirely resident in the caller's halfway buffer. VCS checks these
// values directly after every sceAtracDecodeData call: -1/-2 take the
// all-data-resident path, while a non-negative frame count takes the refill
// path. Returning 0 when a short non-loop stream such as NEWS_*.AT3 was fully
// loaded made the game keep treating it as a streaming/refill source even
// though no more encoded bytes existed.
constexpr std::uint32_t kAtracRemainAllDataOnMemory = 0xFFFFFFFFu; // -1
constexpr std::uint32_t kAtracRemainNonLoopOnMemory = 0xFFFFFFFEu; // -2
constexpr std::uint32_t kAtracRemainLoopOnMemory = 0xFFFFFFFDu; // -3
bool atrac_has_active_loop(const AtracContextState &state) noexcept {
return state.loop_num != 0 && state.header.loop_start >= 0 &&
state.header.loop_end >= state.header.loop_start;
}
std::uint32_t atrac_remain_frame_status(const AtracContextState &state) noexcept {
if (state.next_file_offset >= state.header.file_size) {
// VCS creates its radio decoders through SetHalfwayBufferAndGetID, so
// the streaming-specific -2/-3 statuses are the faithful result once
// the whole file has been fed. (-1 belongs to the all-data API case;
// the guest accepts both -1 and -2 for its non-loop fast path.)
return atrac_has_active_loop(state) ?
kAtracRemainLoopOnMemory : kAtracRemainNonLoopOnMemory;
}
if (state.header.block_align == 0u) return 0u;
return state.buffered_encoded_bytes / state.header.block_align;
}
std::uint32_t atrac_bitrate_kbps(const AtracContextState &state) {
if (state.header.atrac3plus) {
const std::uint32_t raw = (static_cast<std::uint32_t>(state.header.block_align) * 352800u) / 1000u;
@@ -1572,6 +1653,10 @@ struct SasState {
std::uint32_t output_mode{};
std::uint32_t sample_rate{44100u};
std::array<SasVoiceState, 32> voices{};
// sceSasGetEndFlag exposes a hardware-latched snapshot. The flags are
// refreshed by a completed __sceSasCore/__sceSasCoreWithMix cycle, not by
// arbitrary setters in the middle of a grain.
std::uint32_t end_flags{0xFFFFFFFFu};
SasReverbState reverb{};
};
@@ -1591,6 +1676,14 @@ std::size_t sas_playing_voice_count() {
[](const SasVoiceState &voice) { return voice.playing && !voice.paused; }));
}
void sas_refresh_end_flags() noexcept {
std::uint32_t flags = 0u;
for (std::size_t i = 0; i < sas_state.voices.size(); ++i) {
if (!sas_state.voices[i].playing) flags |= 1u << i;
}
sas_state.end_flags = flags;
}
void sas_log_mix_checkpoint(const char *kind, std::uint64_t count) {
if (!sas_audio_diagnostics_enabled()) return;
if (count <= 8u || (count % 256u) == 0u) {
@@ -1866,6 +1959,13 @@ bool sas_decode_next_block(const psprecomp::GuestMemory &memory, SasVoiceState &
// Resetting it at the marker makes otherwise seamless ambient loops
// click every time they wrap.
voice.decode_offset = voice.loop_start_valid ? voice.loop_start_offset : 0u;
if (voice.loop_start_valid) {
voice.history1 = voice.loop_start_history1;
voice.history2 = voice.loop_start_history2;
} else {
voice.history1 = 0;
voice.history2 = 0;
}
voice.remaining_samples = voice.total_samples;
};
@@ -2154,6 +2254,7 @@ void sas_mix_into(psprecomp::Runtime &rt, std::uint32_t output, std::uint32_t fr
rt.memory().store16(output + frame * 4u + 2u, static_cast<std::uint16_t>(
static_cast<std::int16_t>(std::clamp<std::int64_t>(r, -32768, 32767))));
}
sas_refresh_end_flags();
}
// Raw output mode exposes four non-interleaved planes: dry L, dry R, effect L,
@@ -2178,6 +2279,7 @@ void sas_mix_raw(psprecomp::Runtime &rt, std::uint32_t output, std::uint32_t fra
store(send_left_base, effect_send[frame * 2u]);
store(send_right_base, effect_send[frame * 2u + 1u]);
}
sas_refresh_end_flags();
}
enum class UtilityStatus : std::uint32_t {
@@ -2589,15 +2691,120 @@ bool write_guest_file(psprecomp::Runtime &runtime, const std::filesystem::path &
return output.good();
}
bool write_savedata_auxiliary(psprecomp::Runtime &runtime, std::uint32_t parameter_address,
std::uint32_t descriptor_offset, const char *filename) {
struct SavedataPendingWrite {
std::filesystem::path target;
std::vector<std::uint8_t> bytes;
};
bool snapshot_guest_bytes(psprecomp::Runtime &runtime, std::uint32_t buffer,
std::uint32_t size, std::vector<std::uint8_t> &bytes) {
bytes.clear();
if (size == 0u) return true;
if (buffer == 0u || !runtime.memory().contains(buffer, size)) return false;
bytes.resize(size);
runtime.memory().copy_out(buffer, bytes);
return true;
}
bool snapshot_savedata_auxiliary(psprecomp::Runtime &runtime,
std::uint32_t parameter_address,
std::uint32_t descriptor_offset,
const char *filename,
std::vector<SavedataPendingWrite> &writes) {
const std::uint32_t descriptor = parameter_address + descriptor_offset;
const std::uint32_t buffer = runtime.memory().load32(descriptor);
const std::uint32_t buffer_size = runtime.memory().load32(descriptor + 4u);
const std::uint32_t actual_size = runtime.memory().load32(descriptor + 8u);
if (buffer == 0u || actual_size == 0u) return true;
if (actual_size > buffer_size) return false;
return write_guest_file(runtime, savedata_directory(runtime, parameter_address) / filename, buffer, actual_size);
SavedataPendingWrite write{};
write.target = savedata_directory(runtime, parameter_address) / filename;
if (!snapshot_guest_bytes(runtime, buffer, actual_size, write.bytes)) return false;
writes.push_back(std::move(write));
return true;
}
bool write_savedata_host_bytes(const std::filesystem::path &path,
std::span<const std::uint8_t> bytes) {
std::error_code error;
std::filesystem::create_directories(path.parent_path(), error);
if (error) return false;
std::ofstream output(path, std::ios::binary | std::ios::trunc);
if (!output) return false;
if (!bytes.empty())
output.write(reinterpret_cast<const char *>(bytes.data()),
static_cast<std::streamsize>(bytes.size()));
output.flush();
return output.good();
}
bool commit_savedata_writes(std::vector<SavedataPendingWrite> &writes) {
struct CommitPath {
std::filesystem::path target;
std::filesystem::path temporary;
std::filesystem::path backup;
bool had_target{};
bool backup_moved{};
bool committed{};
};
const std::string tag = std::to_string(static_cast<unsigned long long>(
std::chrono::steady_clock::now().time_since_epoch().count()));
std::vector<CommitPath> paths;
paths.reserve(writes.size());
// Stage every file first. No existing save data is touched until all guest
// buffers have validated and every temporary host write has completed.
for (std::size_t i = 0u; i < writes.size(); ++i) {
CommitPath entry{};
entry.target = writes[i].target;
entry.temporary = entry.target;
entry.temporary += ".vcsnative.tmp." + tag + "." + std::to_string(i);
entry.backup = entry.target;
entry.backup += ".vcsnative.bak." + tag + "." + std::to_string(i);
if (!write_savedata_host_bytes(entry.temporary, writes[i].bytes)) {
std::error_code ignore;
for (const auto &old : paths) std::filesystem::remove(old.temporary, ignore);
std::filesystem::remove(entry.temporary, ignore);
return false;
}
paths.push_back(std::move(entry));
}
auto rollback = [&]() noexcept {
std::error_code error;
for (auto it = paths.rbegin(); it != paths.rend(); ++it) {
if (it->committed) std::filesystem::remove(it->target, error);
error.clear();
if (it->backup_moved && std::filesystem::exists(it->backup, error)) {
error.clear();
std::filesystem::rename(it->backup, it->target, error);
}
error.clear();
std::filesystem::remove(it->temporary, error);
}
};
for (auto &entry : paths) {
std::error_code error;
entry.had_target = std::filesystem::exists(entry.target, error) && !error;
if (entry.had_target) {
std::filesystem::remove(entry.backup, error);
error.clear();
std::filesystem::rename(entry.target, entry.backup, error);
if (error) { rollback(); return false; }
entry.backup_moved = true;
}
error.clear();
std::filesystem::rename(entry.temporary, entry.target, error);
if (error) { rollback(); return false; }
entry.committed = true;
}
std::error_code ignore;
for (auto &entry : paths) {
if (entry.backup_moved) std::filesystem::remove(entry.backup, ignore);
std::filesystem::remove(entry.temporary, ignore);
}
return true;
}
std::uint32_t load_savedata_file(psprecomp::Runtime &runtime, std::uint32_t parameter_address,
@@ -2633,20 +2840,32 @@ std::uint32_t save_savedata_file(psprecomp::Runtime &runtime, std::uint32_t para
if (size > capacity || (size != 0u && (source == 0u || !runtime.memory().contains(source, size)))) {
return raw_mode ? 0x80110328u : 0x80110388u;
}
if (!write_guest_file(runtime, path, source, size)) return raw_mode ? 0x80110329u : 0x80110385u;
// Snapshot every guest buffer before touching the existing slot. Some
// missions change the optional savedata payloads; the old path truncated
// DATA.BIN first and only then discovered an invalid auxiliary descriptor.
// A failed save could therefore leave a half-updated slot and a frontend
// completion state that looked successful until VCS tried to leave it.
std::vector<SavedataPendingWrite> writes;
SavedataPendingWrite main_write{};
main_write.target = path;
if (!snapshot_guest_bytes(runtime, source, size, main_write.bytes))
return raw_mode ? 0x80110328u : 0x80110388u;
writes.push_back(std::move(main_write));
if (!raw_mode) {
if (!write_savedata_auxiliary(runtime, parameter_address, kSavedataIcon0Offset, "ICON0.PNG") ||
!write_savedata_auxiliary(runtime, parameter_address, kSavedataIcon1Offset, "ICON1.PMF") ||
!write_savedata_auxiliary(runtime, parameter_address, kSavedataPic1Offset, "PIC1.PNG") ||
!write_savedata_auxiliary(runtime, parameter_address, kSavedataSnd0Offset, "SND0.AT3")) {
return 0x80110385u;
if (!snapshot_savedata_auxiliary(runtime, parameter_address, kSavedataIcon0Offset, "ICON0.PNG", writes) ||
!snapshot_savedata_auxiliary(runtime, parameter_address, kSavedataIcon1Offset, "ICON1.PMF", writes) ||
!snapshot_savedata_auxiliary(runtime, parameter_address, kSavedataPic1Offset, "PIC1.PNG", writes) ||
!snapshot_savedata_auxiliary(runtime, parameter_address, kSavedataSnd0Offset, "SND0.AT3", writes)) {
return 0x80110388u;
}
// The PSP firmware normally persists sfoParam to PARAM.SFO and uses
// savedataTitle/detail on its load screen. VCSNative's host savedata
// path previously dropped that metadata entirely, leaving the slot UI
// with only opaque names like S92F0. Preserve the exact guest-provided
// display strings in a tiny host sidecar so the in-game overlay can show
// the mission/save title on later loads.
}
if (!commit_savedata_writes(writes)) return raw_mode ? 0x80110329u : 0x80110385u;
if (!raw_mode) {
// Metadata is host-only UI decoration; it must never turn a valid game
// save into a failed firmware operation.
(void)write_savedata_metadata_file(path.parent_path(),
savedata_metadata_from_guest(runtime, parameter_address));
}
@@ -2867,6 +3086,22 @@ void execute_selected_savedata_slot(psprecomp::Runtime &runtime) {
runtime.memory().store32(savedata_utility.parameter_address + kUtilityCommonResultOffset, result);
savedata_utility.operation_complete = true;
savedata_utility.last_result = result;
if (savedata_utility.mode == 5u) {
const std::uint32_t data_size = runtime.memory().load32(
savedata_utility.parameter_address + kSavedataDataSizeOffset);
const auto aux_size = [&](std::uint32_t offset) {
return runtime.memory().load32(savedata_utility.parameter_address + offset + 8u);
};
std::ostringstream line;
line << "SAVEDATA_SAVE result=0x" << std::hex << std::uppercase << result
<< std::nouppercase << std::dec
<< " data=" << data_size
<< " icon0=" << aux_size(kSavedataIcon0Offset)
<< " icon1=" << aux_size(kSavedataIcon1Offset)
<< " pic1=" << aux_size(kSavedataPic1Offset)
<< " snd0=" << aux_size(kSavedataSnd0Offset);
runtime_log_line(line.str());
}
if ((savedata_utility.startup_picker || savedata_utility.direct_load_picker) && result == 0u) {
// A successful first-boot choice should hand control back to the retail
// LOAD completion path immediately. There is no GAME frontend in V9.
@@ -2922,6 +3157,8 @@ void update_savedata_list_utility(psprecomp::Runtime &runtime) {
savedata_utility.status = UtilityStatus::Quit;
savedata_utility_ui_end();
display_window_set_system_utility_mode(false);
if (savedata_utility.mode == 5u)
runtime_log_line("SAVEDATA_SAVE acknowledged status=QUIT");
}
}
return;
@@ -2997,6 +3234,11 @@ std::uint32_t audio_buffer_duration_us(std::uint32_t samples) {
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(samples) * 1000000u + 44099u) / 44100u);
}
constexpr std::uint32_t audio_resample_success_value(bool return_queued_samples,
std::uint32_t sample_count) noexcept {
return return_queued_samples ? sample_count : 0u;
}
// Queues one buffer on a channel and returns the virtual time at which it
// starts playing.
//
@@ -3016,16 +3258,19 @@ std::uint64_t audio_queue_buffer(AudioChannelState &channel, std::uint32_t frame
const auto elapsed_us = [&](std::uint64_t sample_frames) {
return (sample_frames * 1000000ull) / rate;
};
std::uint64_t start = channel.queue_anchor_us + elapsed_us(channel.queued_frames);
if (!channel.queue_active || start < virtual_time_us) {
// Either the first buffer of a stream, or the guest fell far enough
// behind that the queue really did drain. Both are genuine
// discontinuities: restart the anchor here.
if (!channel.queue_active) {
channel.queue_active = true;
channel.queue_anchor_us = virtual_time_us;
channel.queued_frames = 0u;
start = virtual_time_us;
} else if (start < virtual_time_us) {
channel.queue_anchor_us = virtual_time_us;
channel.queued_frames = 0u;
start = virtual_time_us;
}
channel.queued_frames += frames;
channel.busy_until_us = channel.queue_anchor_us + elapsed_us(channel.queued_frames);
return start;
@@ -8004,6 +8249,8 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
ctx.set_gpr(2, 0x80110001u);
return;
}
if (savedata_utility.mode == 5u)
runtime_log_line("SAVEDATA_SAVE shutdown status=FINISHED");
savedata_utility.status = UtilityStatus::Finished;
savedata_utility_ui_end();
display_window_set_system_utility_mode(false);
@@ -8203,11 +8450,14 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
set_success(ctx);
});
// sceAudioOutput2OutputBlocking and sceAudioSRCOutputBlocking share this
// body: both drain the single resampling channel, and the rate recorded at
// reserve time is what tells them apart.
const auto audio_src_output =
[](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) {
// Output2 and SRC share the single resampling hardware path, but they do
// NOT share the same ABI result. sceAudioOutput2OutputBlocking returns 0
// on success while sceAudioSRCOutputBlocking returns the queued sample
// count. VCS imports Output2, so returning 512 here was leaking a false
// non-zero result out of its radio mixer every buffer.
const auto audio_resample_output =
[](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx,
bool return_queued_samples) {
auto &state = audio_channels[8];
const std::uint32_t volume = ctx.gpr[4];
const std::uint32_t buffer = ctx.gpr[5];
@@ -8237,7 +8487,8 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
state.left_volume = volume; state.right_volume = volume;
// audio_queue_buffer already paces at the channel's own frequency,
// so a stream that is not 44100 neither starves nor floods the mix.
const std::uint64_t start_us = audio_queue_buffer(state, state.sample_count);
const std::uint64_t start_us =
audio_queue_buffer(state, state.sample_count);
if (buffer != 0u && vcs::audio_output_enabled()) {
std::vector<std::int16_t> pcm(bytes / sizeof(std::int16_t));
for (std::size_t index = 0u; index < pcm.size(); ++index) {
@@ -8255,11 +8506,19 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
<< " channels=" << state.channel_count
<< " freq=" << state.frequency
<< " start_us=" << start_us << " wait_us=" << wait_us << "\n";
const std::uint32_t success_value =
audio_resample_success_value(return_queued_samples, state.sample_count);
(void)delay_current_thread(rt, ctx, static_cast<std::uint32_t>(wait_us),
state.sample_count);
success_value);
};
runtime.register_hle("sceAudio", 0x2D53F36Eu, audio_src_output);
runtime.register_hle("sceAudio", 0xE0727056u, audio_src_output);
runtime.register_hle("sceAudio", 0x2D53F36Eu,
[audio_resample_output](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) {
audio_resample_output(rt, ctx, false);
});
runtime.register_hle("sceAudio", 0xE0727056u,
[audio_resample_output](psprecomp::Runtime &rt, psprecomp::AllegrexContext &ctx) {
audio_resample_output(rt, ctx, true);
});
constexpr std::uint32_t kAtracErrorApiFail = 0x80630002u;
@@ -8308,7 +8567,23 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
state.buffered_encoded_bytes = std::min(state.buffered_encoded_bytes, parsed.data_size);
state.next_file_offset = std::min(read_size, parsed.file_size);
state.write_offset = buffer_size == 0u ? 0u : read_size % buffer_size;
state.source_path = identify_atrac_source(header_bytes, parsed);
state.source_path = identify_atrac_source(buffer, header_bytes, parsed);
if (!state.source_path.empty()) {
const std::string source_name = state.source_path.filename().string();
if (source_name.rfind("NEWS_", 0u) == 0u) {
std::ostringstream line;
line << "ATRAC_NEWS_META source=" << source_name
<< " codec=" << (parsed.atrac3plus ? "at3plus" : "at3")
<< " channels=" << parsed.channels
<< " rate=" << parsed.sample_rate
<< " block=" << parsed.block_align
<< " total_samples=" << parsed.total_samples
<< " initial_read=" << read_size
<< " buffer=" << buffer_size
<< " file=" << parsed.file_size;
runtime_log_line(line.str());
}
}
if (std::getenv("PSPRECOMP_ATRAC_DIAG") != nullptr) {
std::cerr << "[atrac] set-halfway id=" << id
<< " buffer=" << psprecomp::hex32(buffer)
@@ -8420,7 +8695,8 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
if (state->sample_position >= state->header.total_samples && !restart_for_loop()) {
if (samples_addr != 0u) rt.memory().store32(samples_addr, 0u);
if (finish_addr != 0u) rt.memory().store32(finish_addr, 1u);
if (remain_addr != 0u) rt.memory().store32(remain_addr, 0u);
if (remain_addr != 0u)
rt.memory().store32(remain_addr, atrac_remain_frame_status(*state));
set_success(ctx);
return;
}
@@ -8441,12 +8717,24 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
const char *text = std::getenv("PSPRECOMP_AUDIO_SUMMARY");
return text != nullptr && *text != '\0' && std::strcmp(text, "0") != 0;
}();
const auto decode_started = audio_summary_enabled
? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const bool decoder_was_open = state->decoder.is_open();
const auto decode_started = std::chrono::steady_clock::now();
std::size_t got = read_atrac_pcm(*state, pcm_span);
if (got == 0u && restart_for_loop()) {
got = read_atrac_pcm(*state, pcm_span);
}
const std::uint64_t decode_elapsed_us = static_cast<std::uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - decode_started).count());
if (!decoder_was_open || decode_elapsed_us >= 2000u) {
std::ostringstream line;
line << "ATRAC_DECODE source=" << state->source_path.filename().string()
<< " open=" << (!decoder_was_open ? 1 : 0)
<< " decode_us=" << decode_elapsed_us
<< " bytes=" << got
<< " sample=" << state->sample_position;
runtime_log_line(line.str());
}
if (audio_summary_enabled) {
static std::uint64_t decode_calls = 0u;
static std::uint64_t decode_total_ns = 0u;
@@ -8468,14 +8756,15 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
got = static_cast<std::size_t>(samples) * bytes_per_sample;
if (output != 0u && got != 0u) rt.memory().copy_in(output, std::span<const std::uint8_t>(pcm.data(), got));
state->sample_position += samples;
if (state->buffered_encoded_bytes >= state->header.block_align)
state->buffered_encoded_bytes -= state->header.block_align;
else
state->buffered_encoded_bytes = 0u;
if (samples != 0u && state->header.block_align != 0u) {
if (state->buffered_encoded_bytes >= state->header.block_align)
state->buffered_encoded_bytes -= state->header.block_align;
else
state->buffered_encoded_bytes = 0u;
}
const bool finished = samples == 0u ||
(state->sample_position >= state->header.total_samples && state->loop_num == 0);
const std::uint32_t remaining_frames = state->header.block_align == 0u ? 0u :
state->buffered_encoded_bytes / state->header.block_align;
const std::uint32_t remaining_frames = atrac_remain_frame_status(*state);
if (samples_addr != 0u) rt.memory().store32(samples_addr, samples);
if (finish_addr != 0u) rt.memory().store32(finish_addr, finished ? 1u : 0u);
if (remain_addr != 0u) rt.memory().store32(remain_addr, remaining_frames);
@@ -8500,9 +8789,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
auto *state = get_atrac(ctx.gpr[4]);
if (!state) { ctx.set_gpr(2, kAtracErrorBadId); return; }
if (!rt.memory().contains(ctx.gpr[5], 4u)) { ctx.set_gpr(2, kAtracErrorBadAddress); return; }
const std::uint32_t remaining = state->next_file_offset >= state->header.file_size ? 0xFFFFFFFFu :
state->buffered_encoded_bytes / state->header.block_align;
rt.memory().store32(ctx.gpr[5], remaining);
rt.memory().store32(ctx.gpr[5], atrac_remain_frame_status(*state));
set_success(ctx);
});
@@ -8854,10 +9141,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
runtime.register_hle("sceSasCore", 0x68A46B95u,
[](psprecomp::Runtime &, psprecomp::AllegrexContext &ctx) {
if (!sas_valid_core(ctx.gpr[4])) { ctx.set_gpr(2, kSasErrorNotInitialized); return; }
std::uint32_t flags = 0u;
for (std::size_t i = 0; i < sas_state.voices.size(); ++i)
if (!sas_state.voices[i].playing) flags |= 1u << i;
ctx.set_gpr(2, flags);
ctx.set_gpr(2, sas_state.end_flags);
});
runtime.register_hle("sceSasCore", 0x74AE582Au,
@@ -9905,6 +10189,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
if (stream) {
const auto fd = file_table.next_fd++;
file_table.files.emplace(fd, std::move(stream));
file_table.file_paths.emplace(fd, disc_file->native_path);
if (std::getenv("PSPRECOMP_IO_DIAG") != nullptr) {
std::cerr << "[io] raw UMD open lbn=" << raw_lbn
<< " size=" << raw_size
@@ -9956,6 +10241,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
}
const auto fd = file_table.next_fd++;
file_table.files.emplace(fd, std::move(stream));
file_table.file_paths.emplace(fd, native);
if (file_object_diag) {
std::cerr << "[fileobj-hle] open-ok fd=" << fd << " path=\"" << path
<< "\" native=\"" << native.string()
@@ -10078,6 +10364,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
const bool closed = file_table.files.erase(fd) == 1u ||
file_table.synthetic_empty_files.erase(fd) == 1u ||
file_table.virtual_disc_handles.erase(fd) == 1u;
file_table.file_paths.erase(fd);
if (std::getenv("PSPRECOMP_FILE_OBJECT_DIAG") != nullptr)
std::cerr << "[fileobj-hle] close fd=" << fd << " closed=" << closed << "\n";
ctx.set_gpr(2, closed ? 0u : 0x80010009u);
@@ -10106,6 +10393,10 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
ctx.set_gpr(2, 0x80010009u);
return;
}
const std::uint64_t read_absolute =
virtual_handle->second.base_offset + virtual_handle->second.position;
const VirtualDiscFile *read_file = virtual_disc_file_at_offset(read_absolute);
file_table.recent_atrac_reads.erase(dst);
const bool time_io = perf_timing_enabled();
const auto io_entry = time_io ? std::chrono::steady_clock::now()
: std::chrono::steady_clock::time_point{};
@@ -10113,6 +10404,16 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
virtual_handle->second,
std::span<std::uint8_t>(guest_destination, static_cast<std::size_t>(size)));
if (time_io) io_host_time_this_vblank += std::chrono::steady_clock::now() - io_entry;
if (read != 0u && read_file != nullptr && is_atrac_source_path(read_file->native_path)) {
const std::uint64_t file_start =
static_cast<std::uint64_t>(read_file->start_sector) * 2048u;
if (read_absolute >= file_start &&
read_absolute + read <= file_start + read_file->size) {
if (file_table.recent_atrac_reads.size() >= 32u)
file_table.recent_atrac_reads.erase(file_table.recent_atrac_reads.begin());
file_table.recent_atrac_reads[dst] = read_file->native_path;
}
}
static const bool io_diag = std::getenv("PSPRECOMP_IO_DIAG") != nullptr;
static const bool umd_stream_diag = std::getenv("PSPRECOMP_UMD_STREAM_DIAG") != nullptr;
if (io_diag)
@@ -10170,6 +10471,7 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
ctx.set_gpr(2, 0x80010009u);
return;
}
file_table.recent_atrac_reads.erase(dst);
const bool time_io = perf_timing_enabled();
const auto io_entry = time_io ? std::chrono::steady_clock::now()
: std::chrono::steady_clock::time_point{};
@@ -10177,6 +10479,14 @@ void install_profile(psprecomp::Runtime &runtime, std::uint32_t user_arena_start
static_cast<std::streamsize>(size));
const auto read = static_cast<std::size_t>(it->second.gcount());
if (time_io) io_host_time_this_vblank += std::chrono::steady_clock::now() - io_entry;
if (read != 0u) {
if (const auto path_it = file_table.file_paths.find(fd);
path_it != file_table.file_paths.end() && is_atrac_source_path(path_it->second)) {
if (file_table.recent_atrac_reads.size() >= 32u)
file_table.recent_atrac_reads.erase(file_table.recent_atrac_reads.begin());
file_table.recent_atrac_reads[dst] = path_it->second;
}
}
ctx.set_gpr(2, static_cast<std::uint32_t>(read));
});
}
@@ -10443,6 +10753,46 @@ bool run_profile_self_tests(std::string &error) {
virtual_time_us = previous_time;
}
// Output2 and SRC share hardware but not their success ABI. VCS uses
// Output2; returning 512 instead of 0 escapes from its mixer loop and
// changes guest control flow even though the PCM buffer was accepted.
{
require(audio_resample_success_value(false, 512u) == 0u,
"sceAudioOutput2OutputBlocking success must be zero");
require(audio_resample_success_value(true, 512u) == 512u,
"sceAudioSRCOutputBlocking must report queued samples");
}
// VCS branches directly on the negative remain-frame sentinels after
// sceAtracDecodeData. A fully-fed non-loop halfway stream (NEWS) must
// report -2; a fully-fed looping stream reports -3; only an incomplete
// stream may report a non-negative buffered frame count.
{
AtracContextState stream{};
stream.header.file_size = 16'384u;
stream.header.block_align = 384u;
stream.header.loop_start = -1;
stream.header.loop_end = -1;
stream.buffered_encoded_bytes = 1'152u;
stream.next_file_offset = 8'192u;
require(atrac_remain_frame_status(stream) == 3u,
"partial ATRAC stream did not report buffered frame count");
stream.next_file_offset = stream.header.file_size;
require(atrac_remain_frame_status(stream) == kAtracRemainNonLoopOnMemory,
"fully-fed non-loop halfway stream must report -2");
stream.header.loop_start = 1024;
stream.header.loop_end = 8191;
stream.loop_num = -1;
require(atrac_remain_frame_status(stream) == kAtracRemainLoopOnMemory,
"fully-fed looping halfway stream must report -3");
stream.loop_num = 0;
require(atrac_remain_frame_status(stream) == kAtracRemainNonLoopOnMemory,
"disabled ATRAC loop must report the non-loop resident status");
}
// A voice configured through __sceSasSetADSR alone -- rates only, no
// call to __sceSasSetADSRmode -- must still retire when the game keys
// it off. VCS does exactly this for the vehicle engine, and the old
@@ -10493,6 +10843,81 @@ bool run_profile_self_tests(std::string &error) {
"zero-rate KeyOff left a looping SAS voice alive forever");
}
// End flags are a post-Core snapshot. Setters may change a voice in the
// middle of a grain, but GetEndFlag must not expose that transition until
// the next completed mixer cycle refreshes the hardware-visible flags.
{
const SasState previous = sas_state;
sas_state = SasState{};
auto &voice = sas_state.voices[0];
voice.type = SasVoiceType::Vag;
voice.playing = true;
require((sas_state.end_flags & 1u) != 0u,
"SAS end flag changed before a Core refresh");
sas_refresh_end_flags();
require((sas_state.end_flags & 1u) == 0u,
"SAS Core refresh did not clear the playing voice end flag");
voice.playing = false;
require((sas_state.end_flags & 1u) == 0u,
"SAS end flag was not latched between Core cycles");
sas_refresh_end_flags();
require((sas_state.end_flags & 1u) != 0u,
"SAS Core refresh did not publish the ended voice");
sas_state = previous;
}
// A VAG loop jump must restore the predictor state captured immediately
// before the loop-start block. Keeping the history from the loop-end block
// changes the waveform on every pass and can make a vehicle loop drift.
{
psprecomp::Runtime loop_runtime;
constexpr std::uint32_t vag = 0x08850000u;
std::array<std::uint8_t, 32> blocks{};
blocks[0] = 0u; blocks[1] = 6u; // loop start
blocks[16] = 0u; blocks[17] = 3u; // loop end
loop_runtime.memory().copy_in(vag, blocks);
SasVoiceState voice{};
voice.type = SasVoiceType::Vag;
voice.data_address = vag;
voice.data_size = 32;
voice.loop = true;
voice.history1 = 123;
voice.history2 = -45;
require(sas_decode_next_block(loop_runtime.memory(), voice),
"VAG loop-start block failed to decode");
require(voice.loop_start_valid && voice.loop_start_history1 == 123 &&
voice.loop_start_history2 == -45,
"VAG loop-start predictor state was not captured");
require(sas_decode_next_block(loop_runtime.memory(), voice),
"VAG loop-end block failed to decode");
require(voice.decode_offset == 0u && voice.history1 == 123 && voice.history2 == -45,
"VAG loop jump did not restore predictor history");
}
// Transactional savedata writes must never destroy the existing main file
// if a later auxiliary file cannot be staged.
{
const std::filesystem::path root = std::filesystem::temp_directory_path() /
("vcsnative_savedata_tx_" + std::to_string(static_cast<unsigned long long>(
std::chrono::steady_clock::now().time_since_epoch().count())));
const std::filesystem::path existing = root / "DATA.BIN";
const std::filesystem::path blocker = root / "blocker";
std::error_code error;
std::filesystem::create_directories(root, error);
require(!error, "could not create savedata transaction fixture");
{ std::ofstream out(existing, std::ios::binary); out.write("OLD", 3); }
{ std::ofstream out(blocker, std::ios::binary); out.write("X", 1); }
std::vector<SavedataPendingWrite> writes;
writes.push_back(SavedataPendingWrite{existing, {'N','E','W'}});
writes.push_back(SavedataPendingWrite{blocker / "AUX.DAT", {'B','A','D'}});
require(!commit_savedata_writes(writes),
"savedata transaction accepted an impossible auxiliary target");
std::ifstream in(existing, std::ios::binary);
std::string old((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
require(old == "OLD", "failed savedata staging destroyed the previous slot");
std::filesystem::remove_all(root, error);
}
// A GE context supplied to sceGeListEnQueue is a real serialized PSP
// context, not merely a command-memory snapshot. It must include matrix
// DATA words and the global renderer state must be restored after END.
@@ -11146,8 +11571,8 @@ bool run_profile_self_tests(std::string &error) {
sas_context = {};
sas_context.set_gpr(4u, sas_core);
wlan_runtime.invoke_import("sceSasCore", 0x68A46B95u, sas_context);
require((sas_context.gpr[2] & 1u) == 0u,
"active SAS voice was reported ended before mixing");
require((sas_context.gpr[2] & 1u) != 0u,
"SAS end flag changed before the first Core refresh");
wlan_runtime.memory().zero(sas_output, 0x400u);
sas_context = {};
+11 -4
View File
@@ -65,22 +65,29 @@ void runtime_log_initialize(const VcsConfiguration &configuration) {
return;
}
s.file << "VCSNative runtime log\n";
s.file << "stage=perf-v8.1-cpu-lean-2026-08-17\n";
s.file << "stage=correctness-v8.2.5-news-atrac-stream-fix-2026-08-18\n";
s.file << "config=" << configuration.source_path.string() << '\n';
s.file << "started=" << timestamp_now() << '\n';
s.file << "perf_telemetry=" << (configuration.diagnostics.perf_telemetry ? 1 : 0)
<< " interval_vblanks=" << configuration.diagnostics.perf_telemetry_interval_vblanks
<< "\n";
s.file << "guest_hotspot=" << (configuration.diagnostics.guest_hotspot_profile ? 1 : 0)
<< " sample_stride=256 interval_vblanks=300\n";
<< " sample_stride=256 interval_vblanks=300"
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
<< " chain_telemetry_compiled=1\n";
#else
<< " chain_telemetry_compiled=0\n";
#endif
s.file << "tier2_superblocks=" << (tier2_superblocks_enabled() ? 1 : 0)
<< " version=4 clusters=7 mask=0x" << std::hex << tier2_cluster_mask() << std::dec
<< " hot_blocks=1060 static_fused_calls=36 static_fused_tail=13 hooks=25"
<< " entity_leaf_inline_sites=13 geometry_inline_leaf_sites=0 direct_generated_leaf_sites=0"
<< " entity_leaf_inline_sites=13 geometry_inline_leaf_sites=0 direct_generated_leaf_sites=5"
<< " unwind_fix=1 reentry_guard=1 dataflow=1 vfpu_block32=133 mem_runs=35 mem_words=287"
<< " append32=51 advance32=89 simd_mat4=4 simd_matvec=19"
<< " gpr_shadow_clusters=4 gpr_shadow_regs=24 gpr_shadow_occurrences=3461 geometry_shadow=0"
<< " perf_layer=8 cpu_lean_revision=1 arch_fastmem=1 aot_direct_fastmem_default=1"
<< " perf_layer=8 cpu_lean_revision=2 correctness_revision=825 recovery_from_821=1"
<< " save_transaction=1 save_lifecycle_v82=1 sas_endflag_latched=1 sas_loop_history_restore=1"
<< " atrac_virtual_source=1 atrac_stall_diag=1 atrac_stream_resident_status=1 atrac_nonloop_resident=-2 atrac_loop_resident=-3 output2_success_zero=1 output2_master_watermark=1 output2_late_catchup=0 runtime_chain_telemetry_default=0 arch_fastmem=1 aot_direct_fastmem_default=1"
<< " tier2_direct_fastmem=1 tier2_direct_mem_sites=1969 tier2_deep_telemetry_default=0 geometry_fusion_rollback=1"
<< " entity_leaf_inline=1 entity_leaf_scheduler_accounting=1 entity_leaf_resume_pc_fix=1"
<< " ge_async_default=0 parallel_vertex_decode_default=0"
@@ -395,14 +395,30 @@ SB_L_0895C04C:
ctx.gpr[21] = (ctx.gpr[16] | 0u);
ctx.gpr[31] = (0x0895C058u);
tier2_gpr_4 = (ctx.gpr[21] | 0u);
if (([&]() { TIER2_GPR_SYNC_OUT(); const bool tier2_same_ = (rt.invoke_chained_direct<&recomp_unit_0206_entry, 206u, 498u, 0x08B3E084u>(ctx, &aot_mem)); if (tier2_same_) TIER2_GPR_SYNC_IN(); else tier2_gpr_shadow_valid = false; return tier2_same_; }()) && ctx.pc == 0x0895C058u) goto SB_L_0895C058;
TIER2_GPR_SYNC_OUT();
psprecomp::recomp_unit_0206_entry(rt, ctx, 498u, aot_mem);
TIER2_GPR_SYNC_IN();
if (!rt.account_inlined_generated_leaf(ctx)) {
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
}
if (ctx.pc == 0x0895C058u) goto SB_L_0895C058;
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
SB_L_0895C058:
ctx.gpr[16] = (tier2_mem.aot_load32(ctx.gpr[2] + static_cast<std::uint32_t>(0)));
ctx.gpr[31] = (0x0895C064u);
tier2_gpr_4 = (ctx.gpr[21] | 0u);
if (([&]() { TIER2_GPR_SYNC_OUT(); const bool tier2_same_ = (rt.invoke_chained_direct<&recomp_unit_0206_entry, 206u, 499u, 0x08B3E08Cu>(ctx, &aot_mem)); if (tier2_same_) TIER2_GPR_SYNC_IN(); else tier2_gpr_shadow_valid = false; return tier2_same_; }()) && ctx.pc == 0x0895C064u) goto SB_L_0895C064;
TIER2_GPR_SYNC_OUT();
psprecomp::recomp_unit_0206_entry(rt, ctx, 499u, aot_mem);
TIER2_GPR_SYNC_IN();
if (!rt.account_inlined_generated_leaf(ctx)) {
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
}
if (ctx.pc == 0x0895C064u) goto SB_L_0895C064;
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
SB_L_0895C064:
@@ -895,7 +911,15 @@ SB_L_0895C694:
ctx.gpr[16] = (tier2_mem.aot_load32(tier2_gpr_4 + static_cast<std::uint32_t>(0)));
ctx.gpr[31] = (0x0895C6B8u);
tier2_gpr_4 = (ctx.gpr[16] | 0u);
if (([&]() { TIER2_GPR_SYNC_OUT(); const bool tier2_same_ = (rt.invoke_chained_direct<&recomp_unit_0206_entry, 206u, 499u, 0x08B3E08Cu>(ctx, &aot_mem)); if (tier2_same_) TIER2_GPR_SYNC_IN(); else tier2_gpr_shadow_valid = false; return tier2_same_; }()) && ctx.pc == 0x0895C6B8u) goto SB_L_0895C6B8;
TIER2_GPR_SYNC_OUT();
psprecomp::recomp_unit_0206_entry(rt, ctx, 499u, aot_mem);
TIER2_GPR_SYNC_IN();
if (!rt.account_inlined_generated_leaf(ctx)) {
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
}
if (ctx.pc == 0x0895C6B8u) goto SB_L_0895C6B8;
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
SB_L_0895C6B8:
@@ -927,7 +951,15 @@ SB_L_0895C6D0:
SB_L_0895C6DC:
ctx.gpr[31] = (0x0895C6E4u);
tier2_gpr_4 = (ctx.gpr[16] | 0u);
if (([&]() { TIER2_GPR_SYNC_OUT(); const bool tier2_same_ = (rt.invoke_chained_direct<&recomp_unit_0206_entry, 206u, 524u, 0x08B3E260u>(ctx, &aot_mem)); if (tier2_same_) TIER2_GPR_SYNC_IN(); else tier2_gpr_shadow_valid = false; return tier2_same_; }()) && ctx.pc == 0x0895C6E4u) goto SB_L_0895C6E4;
TIER2_GPR_SYNC_OUT();
psprecomp::recomp_unit_0206_entry(rt, ctx, 524u, aot_mem);
TIER2_GPR_SYNC_IN();
if (!rt.account_inlined_generated_leaf(ctx)) {
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
}
if (ctx.pc == 0x0895C6E4u) goto SB_L_0895C6E4;
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
SB_L_0895C6E4:
@@ -960,7 +992,15 @@ SB_L_0895C6FC:
SB_L_0895C708:
ctx.gpr[31] = (0x0895C710u);
tier2_gpr_4 = (ctx.gpr[16] | 0u);
if (([&]() { TIER2_GPR_SYNC_OUT(); const bool tier2_same_ = (rt.invoke_chained_direct<&recomp_unit_0206_entry, 206u, 523u, 0x08B3E254u>(ctx, &aot_mem)); if (tier2_same_) TIER2_GPR_SYNC_IN(); else tier2_gpr_shadow_valid = false; return tier2_same_; }()) && ctx.pc == 0x0895C710u) goto SB_L_0895C710;
TIER2_GPR_SYNC_OUT();
psprecomp::recomp_unit_0206_entry(rt, ctx, 523u, aot_mem);
TIER2_GPR_SYNC_IN();
if (!rt.account_inlined_generated_leaf(ctx)) {
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
}
if (ctx.pc == 0x0895C710u) goto SB_L_0895C710;
tier2_gpr_shadow_valid = false;
TIER2_SB_RETURN();
SB_L_0895C710:
+2
View File
@@ -33,6 +33,7 @@ echo Build pipeline restored to the last known-good pre-reorganization behavior.
echo CMake: !CMAKE_EXE!
echo Compile workers: %JOBS% ^| AOT /MP%JOBS% ^| MSBuild /m:1
echo AOT inlining: /Ob3 hot measured units ^| /Ob0 cold units
echo Runtime chain telemetry: OFF ^(V8.2 performance path^)
echo Link: host/core LTCG only ^| generated AOT /GL- ^| LTCG status visible
echo Build dir preserved: %BUILD%
echo ================================================================
@@ -49,6 +50,7 @@ echo [1/7] Configuring without deleting existing objects...
-DPSPRECOMP_NATIVE_AVX2=ON ^
-DPSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH=ON ^
-DPSPRECOMP_AOT_PRODUCTION_FASTPATHS=ON ^
-DPSPRECOMP_RUNTIME_CHAIN_TELEMETRY=OFF ^
-DPSPRECOMP_MSVC_CGTHREADS=0 ^
-DPSPRECOMP_MSVC_MP_JOBS=%JOBS% ^
-DPSPRECOMP_PROFILE_GUIDED_AOT=ON ^
+70 -3
View File
@@ -104,9 +104,17 @@ set "PERF_V6_ENTITY_LEAF_FIX1_STAMP=%BUILD%\.vcs_perf_v6_entity_leaf_inline_cras
set "PERF_V7_ARCH_FASTMEM_STAMP=%BUILD%\.vcs_perf_v7_arch_fastmem_20260817"
set "PERF_V8_CPU_FUSION_STAMP=%BUILD%\.vcs_perf_v8_cpu_fusion_20260817"
set "PERF_V81_CPU_LEAN_STAMP=%BUILD%\.vcs_perf_v81_cpu_lean_20260817"
set "PERF_V82_CPU_RUNTIME_LEAN_STAMP=%BUILD%\.vcs_perf_v82_cpu_runtime_lean_20260817"
set "CORRECTNESS_V822_STAMP=%BUILD%\.vcs_correctness_v822_recovery_20260817"
set "CORRECTNESS_V823_NEWS_STAMP=%BUILD%\.vcs_correctness_v823_news_audio_20260817"
set "CORRECTNESS_V824_NEWS_PACING_STAMP=%BUILD%\.vcs_correctness_v824_news_pacing_20260818"
set "CORRECTNESS_V825_NEWS_ATRAC_STAMP=%BUILD%\.vcs_correctness_v825_news_atrac_stream_20260818"
if not defined PSPRECOMP_TIER2_DEEP_TELEMETRY set "PSPRECOMP_TIER2_DEEP_TELEMETRY=OFF"
if not defined PSPRECOMP_RUNTIME_CHAIN_TELEMETRY set "PSPRECOMP_RUNTIME_CHAIN_TELEMETRY=OFF"
if /I "%PSPRECOMP_TIER2_DEEP_TELEMETRY%"=="1" set "PSPRECOMP_TIER2_DEEP_TELEMETRY=ON"
if /I "%PSPRECOMP_TIER2_DEEP_TELEMETRY%"=="0" set "PSPRECOMP_TIER2_DEEP_TELEMETRY=OFF"
if /I "%PSPRECOMP_RUNTIME_CHAIN_TELEMETRY%"=="1" set "PSPRECOMP_RUNTIME_CHAIN_TELEMETRY=ON"
if /I "%PSPRECOMP_RUNTIME_CHAIN_TELEMETRY%"=="0" set "PSPRECOMP_RUNTIME_CHAIN_TELEMETRY=OFF"
echo ================================================================
echo VCS - NINJA PERFORMANCE INCREMENTAL BUILD
@@ -118,17 +126,19 @@ echo CMake: %CMAKE_EXE%
echo Ninja: %NINJA_EXE%
echo Ninja workers: %JOBS%
echo cl.exe /MP: OFF ^(Ninja owns compile parallelism^)
echo Generated AOT: O3, cold /Ob0, measured hot /Ob3; V8.1 CPU LEAN: V7-size Geometry + Tier2 direct-fastmem; deep Tier2 telemetry build-switch
echo Generated AOT: O3, cold /Ob0, measured hot /Ob3; V8.2 runtime-chain lean + V8.1 Geometry/direct-fastmem baseline
echo Correctness: V8.2.5 NEWS ATRAC stream state - resident sentinels + V8.2.3 Output2 base
echo Host/core LTCG: ON
echo AVX2/fast paths: ON
echo Tier2 deep diag: %PSPRECOMP_TIER2_DEEP_TELEMETRY%
echo Chain telemetry: %PSPRECOMP_RUNTIME_CHAIN_TELEMETRY%
echo ================================================================
echo.
echo [0b/7] Reapplying BOOTFIX-safe Tier-2 transforms (OPT1 semantic transforms disabled)...
call "%PROFILE%\APPLY_TIER2_EXTREME.bat"
if errorlevel 1 goto :FAIL
echo [0b2/7] Building V8.1 CPU LEAN over V8/V7 stable architecture...
echo [0b2/7] Building V8.2 CPU RUNTIME LEAN over V8.1 stable architecture...
set "PYTHON3_CMD="
py -3 -c "import sys; raise SystemExit(0 if sys.version_info.major == 3 else 1)" >nul 2>&1
if not errorlevel 1 set "PYTHON3_CMD=py -3"
@@ -140,7 +150,10 @@ if not defined PYTHON3_CMD goto :NO_PYTHON3
echo Python 3: %PYTHON3_CMD%
%PYTHON3_CMD% "%PROFILE%\tools\build_tier2_superblocks.py" "%PROFILE%"
if errorlevel 1 goto :FAIL
%PYTHON3_CMD% "%PROFILE%\tests\check_v81_cpu_lean.py"
rem V8.2.5 checker includes the protected V8.2 CPU/Geometry invariants and the
rem V8.2.3 Output2 ABI/watermark invariants. Older revision checkers pin their
rem exact stage strings and therefore must not gate a newer correctness stage.
%PYTHON3_CMD% "%PROFILE%\tests\check_v825_news_atrac_stream.py"
if errorlevel 1 goto :FAIL
if exist "%BUILD%" if not exist "%SUPERBLOCK_STAMP%" (
@@ -276,6 +289,54 @@ if exist "%BUILD%" if not exist "%PERF_V81_CPU_LEAN_STAMP%" (
del /s /q "%BUILD%\*vcs_runtime_log*.obj" >nul 2>&1
)
if exist "%BUILD%" if not exist "%PERF_V82_CPU_RUNTIME_LEAN_STAMP%" (
echo.
echo [0c-v82runtime/7] V8.2 CPU RUNTIME LEAN - one-time native-chain rebuild...
rem runtime.hpp is inline in every generated AOT unit. Rebuild the corpus once so
rem compile-time-known chains lose per-call diagnostic branches in production.
rem Keep the build tree itself: Ninja reuses every unaffected dependency and cache.
del /s /q "%BUILD%\*generated_unit_*.obj" >nul 2>&1
del /s /q "%BUILD%\*vcs_tier2_cluster*.obj" >nul 2>&1
del /s /q "%BUILD%\*runtime*.obj" >nul 2>&1
del /s /q "%BUILD%\*vcs_runtime_log*.obj" >nul 2>&1
)
if exist "%BUILD%" if not exist "%CORRECTNESS_V822_STAMP%" (
echo.
echo [0c-v822fix/7] V8.2.2 CORRECTNESS RECOVERY - rebuilding VCS host correctness objects...
rem V8.2.2 is based on the stable V8.2 runtime. Replace any V8.2.1 host
rem objects but keep generated AOT, Tier2 and DX12 objects intact.
del /s /q "%BUILD%\*vcs_profile*.obj" >nul 2>&1
del /s /q "%BUILD%\*vcs_runtime_log*.obj" >nul 2>&1
)
if exist "%BUILD%" if not exist "%CORRECTNESS_V823_NEWS_STAMP%" (
echo.
echo [0c-v823news/7] V8.2.3 NEWS AUDIO - rebuilding audio/profile correctness objects...
rem Output2 ABI and host watermark only: keep generated AOT, Tier2 and DX12.
del /s /q "%BUILD%\*audio_output*.obj" >nul 2>&1
del /s /q "%BUILD%\*vcs_profile*.obj" >nul 2>&1
del /s /q "%BUILD%\*vcs_runtime_log*.obj" >nul 2>&1
)
if exist "%BUILD%" if not exist "%CORRECTNESS_V824_NEWS_PACING_STAMP%" (
echo.
echo [0c-v824news/7] V8.2.4 NEWS PACING - rebuilding profile/runtime correctness objects...
rem Output2 guest-time pacing only. Keep generated AOT, Tier2, DX12 and the
rem V8.2.3 host watermark object intact.
del /s /q "%BUILD%\*vcs_profile*.obj" >nul 2>&1
del /s /q "%BUILD%\*vcs_runtime_log*.obj" >nul 2>&1
)
if exist "%BUILD%" if not exist "%CORRECTNESS_V825_NEWS_ATRAC_STAMP%" (
echo.
echo [0c-v825news/7] V8.2.5 NEWS ATRAC STREAM - rebuilding profile/runtime correctness objects...
rem ATRAC remain-frame semantics plus rollback of the rejected V8.2.4
rem Output2 catch-up experiment. Keep AOT, Tier2, DX12 and host watermark.
del /s /q "%BUILD%\*vcs_profile*.obj" >nul 2>&1
del /s /q "%BUILD%\*vcs_runtime_log*.obj" >nul 2>&1
)
if exist "%BUILD%" if not exist "%BOOTFIX_STAMP%" (
echo.
echo [0c/7] BOOTFIX revision changed - invalidating stale .obj/.pch once...
@@ -294,6 +355,7 @@ echo [1/7] Configuring persistent Ninja Release tree...
-DPSPRECOMP_NATIVE_AVX2=ON ^
-DPSPRECOMP_AOT_ASSUME_NO_WRITE_WATCH=ON ^
-DPSPRECOMP_AOT_PRODUCTION_FASTPATHS=ON ^
-DPSPRECOMP_RUNTIME_CHAIN_TELEMETRY=%PSPRECOMP_RUNTIME_CHAIN_TELEMETRY% ^
-DPSPRECOMP_MSVC_CGTHREADS=0 ^
-DPSPRECOMP_MSVC_MP_JOBS=1 ^
-DPSPRECOMP_PROFILE_GUIDED_AOT=ON ^
@@ -323,6 +385,11 @@ if errorlevel 1 goto :FAIL
>"%PERF_V7_ARCH_FASTMEM_STAMP%" echo VCS PERF V7 ARCH FASTMEM 2026-08-17
>"%PERF_V8_CPU_FUSION_STAMP%" echo VCS PERF V8 CPU FUSION 2026-08-17
>"%PERF_V81_CPU_LEAN_STAMP%" echo VCS PERF V8.1 CPU LEAN 2026-08-17
>"%PERF_V82_CPU_RUNTIME_LEAN_STAMP%" echo VCS PERF V8.2 CPU RUNTIME LEAN 2026-08-17
>"%CORRECTNESS_V822_STAMP%" echo VCS V8.2.2 CORRECTNESS RECOVERY 2026-08-17
>"%CORRECTNESS_V823_NEWS_STAMP%" echo VCS V8.2.3 NEWS AUDIO FIX 2026-08-17
>"%CORRECTNESS_V824_NEWS_PACING_STAMP%" echo VCS V8.2.4 NEWS PACING FIX 2026-08-18
>"%CORRECTNESS_V825_NEWS_ATRAC_STAMP%" echo VCS V8.2.5 NEWS ATRAC STREAM FIX 2026-08-18
echo.
echo [2b/7] Building tests and DX12 probes...
@@ -1,4 +1,5 @@
#include "audio_resampler.hpp"
#include "audio_output.hpp"
#include <algorithm>
#include <cstdint>
@@ -80,6 +81,25 @@ void test_unity_rate_exact() {
}
}
void test_output2_master_watermark() {
// While Output2 is a live producer inside the device prebuffer window, the
// host must not commit frames beyond what the game has actually mixed.
require(vcs::audio_output_master_seal_frame(4096u, 3072u, true, 2560u, 3072u) == 2560u,
"Output2 watermark did not clamp speculative sealing");
// Never move a seal point backwards when the producer is already ahead.
require(vcs::audio_output_master_seal_frame(4096u, 2048u, true, 2560u, 3072u) == 2048u,
"Output2 watermark moved an already-safe seal point");
// A genuinely stalled/stopped Output2 producer must not freeze the other
// PSP channels forever once it is beyond the bounded prebuffer grace.
require(vcs::audio_output_master_seal_frame(8192u, 7168u, true, 2560u, 3072u) == 7168u,
"stale Output2 producer incorrectly blocked the host timeline");
require(vcs::audio_output_master_seal_frame(4096u, 3072u, false, 0u, 3072u) == 3072u,
"inactive Output2 producer incorrectly changed the host timeline");
}
void test_mono_duplication() {
const auto input = make_signal(1024u, false);
const auto output = run_chunked(input, 1024u, false, 32000u, {127u, 129u});
@@ -96,6 +116,7 @@ int main() {
test_chunk_invariance(48000u, true);
test_chunk_invariance(22050u, true);
test_chunk_invariance(24000u, false);
test_output2_master_watermark();
test_mono_duplication();
std::cout << "audio_resampler_tests: PASS\n";
return 0;
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
profile = Path(__file__).resolve().parents[1]
host = profile / 'host'
source = (host / 'vcs_profile.cpp').read_text(encoding='utf-8')
log = (host / 'vcs_runtime_log.cpp').read_text(encoding='utf-8')
build = (profile / 'scripts' / 'build_release_ninja.bat').read_text(encoding='utf-8')
checks = [
('stage', 'stage=correctness-v8.2.1-save-audio-news-2026-08-17' in log),
('cpu baseline preserved', 'hot_blocks=1060 static_fused_calls=36' in log and 'cpu_lean_revision=2' in log),
('save transaction', 'commit_savedata_writes(writes)' in source and '.vcsnative.tmp.' in source and '.vcsnative.bak.' in source),
('save validates all buffers first', 'snapshot_savedata_auxiliary' in source and 'Snapshot every guest buffer before touching the existing slot' in source),
('save shutdown lifecycle', 'SAVEDATA save-result acknowledged; utility retained until shutdown' in source),
('shutdown still releases utility', 'savedata_utility.status = UtilityStatus::Finished;' in source and 'display_window_set_system_utility_mode(false);' in source),
('SAS error name corrected', 'kSasErrorInvalidState = 0x80420016u' in source and 'kSasErrorVoicePaused' not in source),
('paused KeyOff latch', 'bool sas_key_off_voice' in source and 'if (!sas_key_off_voice(*voice))' in source),
('paused-loop regression test', 'paused vehicle loop resurrected after KeyOff' in source),
('ATRAC direct buffer source', 'recent_atrac_reads' in source and 'direct_buffer_hit' in source),
('ATRAC no common-path rescan', 'identify_atrac_source(buffer, header_bytes, parsed)' in source),
('ATRAC runtime diagnostics', 'ATRAC_SOURCE resolve_us=' in source and 'ATRAC_DECODE source=' in source),
('correctness build stamp', 'CORRECTNESS_V821_STAMP' in build),
]
failed = [name for name, ok in checks if not ok]
for name, ok in checks:
print(f"[{'PASS' if ok else 'FAIL'}] {name}")
if failed:
print(f"V8.2.1 correctness audit failed: {', '.join(failed)}", file=sys.stderr)
raise SystemExit(1)
print(f'V8.2.1 correctness audit PASS ({len(checks)}/{len(checks)})')
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
root = Path(__file__).resolve().parents[3]
profile = root / 'profiles' / 'vcs'
host = profile / 'host'
source = (host / 'vcs_profile.cpp').read_text(encoding='utf-8')
log = (host / 'vcs_runtime_log.cpp').read_text(encoding='utf-8')
def need(cond, msg):
if not cond:
print('FAIL:', msg)
raise SystemExit(1)
print('PASS:', msg)
need('stage=correctness-v8.2.2-recovery-save-audio-news-2026-08-17' in log, 'V8.2.2 runtime stage')
need('recovery_from_821=1' in log and 'save_lifecycle_v82=1' in log, 'V8.2 lifecycle recovery metadata')
need('sas_endflag_latched=1' in log and 'sas_loop_history_restore=1' in log, 'SAS correctness metadata')
need('atrac_virtual_source=1' in log and 'atrac_stall_diag=1' in log, 'ATRAC source metadata')
need('std::uint32_t end_flags{0xFFFFFFFFu};' in source, 'SAS latched end flags stored')
need('ctx.set_gpr(2, sas_state.end_flags);' in source, 'GetEndFlag returns latched flags')
need(source.count('sas_refresh_end_flags();') >= 2, 'Core paths refresh end flags')
need('voice.history1 = voice.loop_start_history1;' in source and 'voice.history2 = voice.loop_start_history2;' in source, 'VAG loop predictor restored')
need('struct SavedataPendingWrite' in source and 'commit_savedata_writes(writes)' in source, 'transactional savedata commit present')
need('SAVEDATA_SAVE acknowledged status=QUIT' in source, 'save lifecycle keeps V8.2 QUIT behavior with diagnostics')
need('utility retained until shutdown' not in source, 'V8.2.1 speculative save lifecycle removed')
need('recent_atrac_reads[dst]' in source and 'virtual_disc_file_at_offset' in source, 'virtual UMD ATRAC producer mapping present')
need('identify_atrac_source(buffer, header_bytes, parsed)' in source, 'ATRAC setup consumes producer mapping')
need('if (voice->paused || !voice->on)' in source, 'V8.2 paused-KeyOff behavior restored')
need('sas_key_off_voice' not in source, 'V8.2.1 paused-KeyOff helper removed')
print('V8.2.2 correctness recovery audit PASS')
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
from pathlib import Path
root = Path(__file__).resolve().parents[3]
profile = root / 'profiles' / 'vcs'
host = profile / 'host'
source = (host / 'vcs_profile.cpp').read_text(encoding='utf-8')
output = (host / 'audio_output.cpp').read_text(encoding='utf-8')
output_h = (host / 'audio_output.hpp').read_text(encoding='utf-8')
log = (host / 'vcs_runtime_log.cpp').read_text(encoding='utf-8')
def need(cond, msg):
if not cond:
print('FAIL:', msg)
raise SystemExit(1)
print('PASS:', msg)
# Preserve the V8.2.2 recovery invariants.
need(('stage=correctness-v8.2.3-news-audio-fix-2026-08-17' in log) or ('stage=correctness-v8.2.4-news-pacing-fix-2026-08-18' in log), 'V8.2.3+ runtime stage')
need('recovery_from_821=1' in log and 'save_lifecycle_v82=1' in log, 'V8.2 lifecycle recovery metadata')
need('sas_endflag_latched=1' in log and 'sas_loop_history_restore=1' in log, 'SAS correctness metadata')
need('atrac_virtual_source=1' in log and 'atrac_stall_diag=1' in log, 'ATRAC source metadata')
need('output2_success_zero=1' in log and 'output2_master_watermark=1' in log, 'Output2 NEWS fix metadata')
need('std::uint32_t end_flags{0xFFFFFFFFu};' in source, 'SAS latched end flags stored')
need('voice.history1 = voice.loop_start_history1;' in source and 'voice.history2 = voice.loop_start_history2;' in source, 'VAG loop predictor restored')
need('struct SavedataPendingWrite' in source and 'commit_savedata_writes(writes)' in source, 'transactional savedata commit preserved')
need('utility retained until shutdown' not in source, 'speculative V8.2.1 savedata lifecycle remains removed')
need('recent_atrac_reads[dst]' in source and 'virtual_disc_file_at_offset' in source, 'virtual UMD ATRAC mapping preserved')
# V8.2.3 NEWS path.
need('audio_resample_success_value(return_queued_samples, state.sample_count)' in source, 'Output2/SRC success ABI split active')
need('audio_resample_output(rt, ctx, false);' in source, 'Output2 explicitly selects zero-success ABI')
need('audio_resample_output(rt, ctx, true);' in source, 'SRC retains queued-sample ABI')
need('sceAudioOutput2OutputBlocking success must be zero' in source, 'Output2 ABI regression self-test present')
need('audio_output_master_seal_frame' in output_h, 'Output2 watermark helper present')
need('kOutput2Channel = 8u' in output, 'Output2 master channel is explicit')
need('sealed_frame = audio_output_master_seal_frame(' in output, 'host timeline uses Output2 producer watermark')
need('output2_seal_clamps' in output and 'output2_clamps=' in output, 'watermark diagnostics present')
need('producer_grace' in output and 'prebuffer_blocks' in output, 'watermark is bounded by device prebuffer')
print('V8.2.3 NEWS audio audit PASS')
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
from pathlib import Path
root = Path(__file__).resolve().parents[3]
profile = root / 'profiles' / 'vcs'
host = profile / 'host'
source = (host / 'vcs_profile.cpp').read_text(encoding='utf-8')
output = (host / 'audio_output.cpp').read_text(encoding='utf-8')
output_h = (host / 'audio_output.hpp').read_text(encoding='utf-8')
log = (host / 'vcs_runtime_log.cpp').read_text(encoding='utf-8')
def need(cond, msg):
if not cond:
print('FAIL:', msg)
raise SystemExit(1)
print('PASS:', msg)
# Stable V8.2.2/V8.2.3 correctness base must remain intact.
need('stage=correctness-v8.2.4-news-pacing-fix-2026-08-18' in log, 'V8.2.4 runtime stage')
need('correctness_revision=824' in log, 'V8.2.4 correctness revision')
need('recovery_from_821=1' in log and 'save_lifecycle_v82=1' in log, 'V8.2.2 lifecycle recovery preserved')
need('sas_endflag_latched=1' in log and 'sas_loop_history_restore=1' in log, 'SAS correctness preserved')
need('atrac_virtual_source=1' in log and 'atrac_stall_diag=1' in log, 'ATRAC source path preserved')
need('output2_success_zero=1' in log and 'output2_master_watermark=1' in log, 'V8.2.3 Output2 fixes preserved')
need('output2_late_catchup=1' in log and 'output2_late_grace_buffers=8' in log, 'V8.2.4 pacing metadata')
# New pacing fix: only Output2 opts into short-lateness absorption.
need('bool absorb_short_lateness = false' in source, 'short-lateness policy is opt-in')
need('audio_queue_buffer(state, state.sample_count, true);' in source, 'Output2 opts into late catch-up')
need('audio_queue_buffer(state, state.sample_count);' in source, 'normal audio channels keep legacy queue policy')
need('late_grace_frames' in source and 'frames) * 8u' in source and '4096u' in source, 'bounded eight-buffer late grace')
need('OUTPUT2_PACING absorbed=' in source and 'OUTPUT2_PACING reanchor=1' in source, 'pacing diagnostics present')
need('short Output2 lateness was converted into a silence gap' in source, 'single-late-block regression self-test')
need('repeated short Output2 lateness stretched the audio timeline' in source, 'repeated-lateness regression self-test')
need('long Output2 stall incorrectly remained on the old timeline' in source, 'long-stall reanchor self-test')
# Do not regress prior audio correctness or host watermark behavior.
need('audio_resample_output(rt, ctx, false);' in source, 'Output2 zero-success ABI still selected')
need('sceAudioOutput2OutputBlocking success must be zero' in source, 'Output2 success ABI test preserved')
need('audio_output_master_seal_frame' in output_h, 'Output2 watermark helper preserved')
need('sealed_frame = audio_output_master_seal_frame(' in output, 'host watermark remains active')
need('producer_grace' in output and 'prebuffer_blocks' in output, 'host watermark remains bounded')
# Critical non-audio architecture must not be touched by this fix.
need('geometry_fusion_rollback=1' in log and 'hot_blocks=1060' in log, 'V8.2 CPU/Geometry baseline preserved')
need('ge_async_quarantined=1' in log and 'parallel_vertex_decode_quarantined=1' in log, 'unstable GE paths remain quarantined')
print('V8.2.4 NEWS pacing audit PASS')
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
from pathlib import Path
root = Path(__file__).resolve().parents[3]
profile = root / 'profiles' / 'vcs'
host = profile / 'host'
source = (host / 'vcs_profile.cpp').read_text(encoding='utf-8')
output = (host / 'audio_output.cpp').read_text(encoding='utf-8')
log = (host / 'vcs_runtime_log.cpp').read_text(encoding='utf-8')
guest = (profile / 'generated' / 'generated_unit_0170.cpp').read_text(encoding='utf-8')
def need(cond, msg):
if not cond:
print('FAIL:', msg)
raise SystemExit(1)
print('PASS:', msg)
need('stage=correctness-v8.2.5-news-atrac-stream-fix-2026-08-18' in log, 'V8.2.5 runtime stage')
need('correctness_revision=825' in log, 'V8.2.5 correctness revision')
need('recovery_from_821=1' in log and 'save_lifecycle_v82=1' in log, 'interior/save lifecycle recovery preserved')
need('output2_success_zero=1' in log and 'output2_master_watermark=1' in log, 'V8.2.3 Output2 correctness preserved')
need('output2_late_catchup=0' in log, 'rejected V8.2.4 pacing experiment rolled back')
need('atrac_stream_resident_status=1' in log and 'atrac_nonloop_resident=-2' in log and 'atrac_loop_resident=-3' in log, 'ATRAC resident-state metadata')
need('kAtracRemainNonLoopOnMemory = 0xFFFFFFFEu' in source, 'non-loop resident sentinel is -2')
need('kAtracRemainLoopOnMemory = 0xFFFFFFFDu' in source, 'loop resident sentinel is -3')
need('atrac_remain_frame_status(*state)' in source, 'Decode/GetRemain share resident-state helper')
need(source.count('atrac_remain_frame_status(*state)') >= 3, 'resident status used by EOF decode, normal decode, and GetRemainFrame')
need('if (samples != 0u && state->header.block_align != 0u)' in source, 'encoded frame is consumed only when PCM was produced')
need('ATRAC_NEWS_META source=' in source, 'NEWS stream metadata diagnostic present')
need('fully-fed non-loop halfway stream must report -2' in source, 'non-loop resident regression test')
need('fully-fed looping halfway stream must report -3' in source, 'loop resident regression test')
# The game itself branches on -1 and -2 immediately after DecodeData's
# outRemainFrame. This audit makes the contract explicit so future HLE cleanup
# cannot silently replace the sentinel with zero again.
need('static_cast<std::uint32_t>(-1)' in guest and 'static_cast<std::uint32_t>(-2)' in guest,
'VCS guest explicitly recognizes -1/-2 ATRAC remain values')
need('bool absorb_short_lateness = false' not in source, 'V8.2.4 catch-up implementation removed')
need('OUTPUT2_PACING absorbed=' not in source, 'V8.2.4 pacing diagnostic removed')
need('audio_queue_buffer(state, state.sample_count, true)' not in source, 'Output2 no longer opts into rejected catch-up')
need('audio_queue_buffer(state, state.sample_count);' in source, 'Output2 uses stable queue policy')
need('audio_output_master_seal_frame' in output, 'host Output2 watermark preserved')
need('geometry_fusion_rollback=1' in log and 'hot_blocks=1060' in log, 'V8.2 CPU/Geometry baseline preserved')
need('ge_async_quarantined=1' in log and 'parallel_vertex_decode_quarantined=1' in log, 'unstable GE paths remain quarantined')
print('V8.2.5 NEWS ATRAC stream audit PASS')
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
PROFILE = ROOT / 'profiles' / 'vcs'
def require(cond, msg):
if not cond:
raise SystemExit('V8.2 CPU RUNTIME LEAN CHECK FAILED: ' + msg)
runtime_h = (ROOT / 'include' / 'psprecomp' / 'runtime.hpp').read_text()
runtime_cpp = (ROOT / 'src' / 'runtime.cpp').read_text()
cmake = (ROOT / 'CMakeLists.txt').read_text()
log = (PROFILE / 'host' / 'vcs_runtime_log.cpp').read_text()
boundary = (PROFILE / 'host' / 'vcs_tier2_cluster_boundary.cpp').read_text()
geometry = (PROFILE / 'host' / 'vcs_tier2_cluster_geometry.cpp').read_text()
world = (PROFILE / 'host' / 'vcs_tier2_cluster_world.cpp').read_text()
require('PSPRECOMP_RUNTIME_CHAIN_TELEMETRY' in cmake, 'missing generic chain telemetry build option')
require(runtime_h.count('#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)') >= 3,
'direct/Tier2 chain telemetry is not compiled behind the build switch')
require(runtime_cpp.count('#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)') >= 4,
'dynamic chain telemetry is not compiled behind the build switch')
require('const std::uint64_t starvation_interval = g_runtime_starvation_interval_fast;' in runtime_h,
'scheduler interval load was removed from inlined-leaf accounting')
require('if (++dispatches_since_import_ < starvation_interval) return true;' in runtime_h,
'scheduler dispatch cadence changed')
require('return run_starvation_boundary(ctx);' in runtime_h,
'scheduler safe-point call missing')
require(any(stage in log for stage in ('stage=perf-v8.2-cpu-runtime-lean-2026-08-17', 'stage=correctness-v8.2.2-recovery-save-audio-news-2026-08-17', 'stage=correctness-v8.2.3-news-audio-fix-2026-08-17', 'stage=correctness-v8.2.4-news-pacing-fix-2026-08-18')), 'runtime stage mismatch')
require('runtime_chain_telemetry_default=0' in log, 'runtime telemetry metadata missing')
require('direct_generated_leaf_sites=5' in log, 'Boundary direct-leaf metadata mismatch')
require(boundary.count('account_inlined_generated_leaf(ctx)') == 5,
'expected exactly five Boundary direct generated leaf call sites')
require(geometry.count('account_inlined_generated_leaf(ctx)') == 0,
'Geometry must remain V8.1 CPU LEAN shape')
require(world.count('account_inlined_generated_leaf(ctx)') == 0,
'World direct-leaf experiment must remain disabled')
require(geometry.count('SB_L_') > 300 and world.count('SB_L_') > 300,
'Tier2 hot cluster bodies unexpectedly missing')
print('V8.2 CPU RUNTIME LEAN CHECK PASS: chain telemetry compiled out by default; Boundary leaves=5; Geometry/World protected')
+12 -1
View File
@@ -93,7 +93,18 @@ GEOMETRY_INLINE_LEAF_BODIES = {}
# through $ra. Tier-2 may call these entry points directly and account the removed
# generated-call scheduler edge without paying invoke_chained_direct's profiling,
# chain-depth and fallback plumbing. Large/indirect helpers are deliberately absent.
DIRECT_GENERATED_LEAF_TARGETS = set()
# V8.2 keeps Geometry/World at the V8.1 shape and only bypasses the generic
# chain wrapper for four tiny Boundary helpers in unit 0206. All four are
# statically validated leaves (no nested generated/HLE/syscall edge, $ra-only
# return). This is intentionally a five-call-site change, not another CFG
# expansion: it preserves the V8.1 instruction-cache footprint and scheduler
# cadence while removing wrapper work around trivial generated helpers.
DIRECT_GENERATED_LEAF_TARGETS = {
(206, 0x08B3E084),
(206, 0x08B3E08C),
(206, 0x08B3E254),
(206, 0x08B3E260),
}
+22
View File
@@ -244,7 +244,11 @@ Runtime::Runtime(std::uint32_t ram_size) : memory_(ram_size) {
// in the middle of guest execution; larger profiles can still grow it.
import_bindings_.resize(256u, nullptr);
hle_histogram_enabled_ = std::getenv("PSPRECOMP_HLE_HISTOGRAM") != nullptr;
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
g_unit_profile_enabled = std::getenv("PSPRECOMP_UNIT_PROFILE") != nullptr;
#else
g_unit_profile_enabled = false;
#endif
#if defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
track_dispatch_counters_ = false;
#else
@@ -359,7 +363,10 @@ bool Runtime::invoke_chained_call(AllegrexContext &ctx, GuestMemory::AotFastView
if (function == nullptr) return false;
}
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY) || !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
const std::uint32_t target_pc = ctx.pc;
#endif
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
std::uint32_t guest_hotspot_unit = static_cast<std::uint32_t>(kUnitProfileCapacity);
bool guest_hotspot_sample = false;
std::uint64_t guest_hotspot_start_ns = 0u;
@@ -379,7 +386,10 @@ bool Runtime::invoke_chained_call(AllegrexContext &ctx, GuestMemory::AotFastView
}
}
}
#endif
#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
const std::uint32_t native_depth = chain_depth_;
#endif
// Always guard execution-context ownership. Even a clean generated unit can
// reach a nested direct chain whose scheduler boundary switches PSP thread.
// guarded every native chain frame; accidentally weakened
@@ -402,12 +412,14 @@ bool Runtime::invoke_chained_call(AllegrexContext &ctx, GuestMemory::AotFastView
} else {
function(*this, ctx);
}
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
if (guest_hotspot_sample) {
const std::uint64_t end_ns = guest_hotspot_clock_ns();
guest_hotspot_record_sample(guest_hotspot_unit, target_pc,
end_ns >= guest_hotspot_start_ns
? end_ns - guest_hotspot_start_ns : 0u);
}
#endif
#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
if (g_post_chained_call_hook != nullptr)
g_post_chained_call_hook(*this, ctx, target_pc, native_depth);
@@ -434,9 +446,14 @@ bool Runtime::invoke_chained_unit(AllegrexContext &ctx, std::uint32_t unit_index
if (unit_index >= kGeneratedUnitFastCapacity || !generated_unit_layout_valid_) return false;
RecompiledFunction function = generated_units_[unit_index];
if (function == nullptr) return false;
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
if (g_unit_profile_enabled) ++g_unit_profile_counts[unit_index];
#endif
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY) || !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
const std::uint32_t target_pc = ctx.pc;
#endif
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
bool guest_hotspot_sample = false;
std::uint64_t guest_hotspot_start_ns = 0u;
if (g_guest_hotspot_profile_enabled) {
@@ -446,7 +463,10 @@ bool Runtime::invoke_chained_unit(AllegrexContext &ctx, std::uint32_t unit_index
(ticket & static_cast<std::uint64_t>(g_guest_hotspot_sample_mask)) == 0u;
if (guest_hotspot_sample) guest_hotspot_start_ns = guest_hotspot_clock_ns();
}
#endif
#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
const std::uint32_t native_depth = chain_depth_;
#endif
const std::uint64_t caller_generation = g_runtime_thread_switch_generation_fast;
#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
if (g_pre_chained_call_hook != nullptr)
@@ -462,12 +482,14 @@ bool Runtime::invoke_chained_unit(AllegrexContext &ctx, std::uint32_t unit_index
} else {
function(*this, ctx);
}
#if defined(PSPRECOMP_RUNTIME_CHAIN_TELEMETRY)
if (guest_hotspot_sample) {
const std::uint64_t end_ns = guest_hotspot_clock_ns();
guest_hotspot_record_sample(unit_index, target_pc,
end_ns >= guest_hotspot_start_ns
? end_ns - guest_hotspot_start_ns : 0u);
}
#endif
#if !defined(PSPRECOMP_AOT_PRODUCTION_FASTPATHS)
if (g_post_chained_call_hook != nullptr)
g_post_chained_call_hook(*this, ctx, target_pc, native_depth);