Audio: interim cue tables retired, native water splash, published tools

- The interim cue tables and their opt-outs are gone; the native runtime is the only audio path. Old
  settings/audio.json files still load ("interim" / "native" ignored). Missing native data logs an error
  instead of falling back.
- Water splash from retail's mechanism (SFXObj_OffBoard start / update): Skate_Collisions 1187 / 1197 once
  per water stay, 1198 when the board is in water.
- The random interim riding bed is removed; riding collision and cloth sounds compared with the recomp
  (doc 11). Native output otherwise byte-identical (13 scenarios + 5 sessions).
- Published tools under tools/ (one folder per tool with a README; the recomp ones labelled for use with the
  research hooks), doc 14 and PR plan bucket L.
- Docs: doc 11 (retirement, bail density measured against the recomp, collision posts), doc 13 (usage guide).
This commit is contained in:
Hails committed 2026-10-03 10:20:08 -06:00
1 parent 6499b7dea0
commit 418dcecd41
85 files changed
+5372 -1348

No files matched your search

+310 -3
View File
@@ -3126,8 +3126,8 @@ CALL IT ALMOST OR EXACTLY RETAIL"; the footsteps while running / walking "sounde
- Bodyslide: on 33 / 38, 1.8 voices, Σg² 0.006.
- Collision-manager posts: mean 137, p50 133.
- Of these, ids 968 / 969 (7.9 + 3.5 per bail) are world / ped collisions, not the rider's body.
- *Ours, rendered: NOT DONE.* The headless e2e build (own target `.local/check-target`) was stopped mid-compile for a PC
restart. To finish it:
- *Ours, rendered:* done after the restart; results in "Bail density: ours rendered" below. The original note:
the headless e2e build (own target `.local/check-target`) was stopped mid-compile for a PC restart. To finish it:
- The scenario is already cut: `.local/audio-re/check0847/e2e/real_084712.tsv` (`scenarios.py --from-log … --cut
r0-999999`).
- Render: `CARGO_TARGET_DIR=.local/check-target E2E_DIR=.local/audio-re/check0847/e2e E2E_FPS=60 cargo test -q -p
@@ -3138,6 +3138,158 @@ CALL IT ALMOST OR EXACTLY RETAIL"; the footsteps while running / walking "sounde
(ids 1030–1032). If ours lacks them, compare `skate_events::region_impacts` (Δv · normal · mass × 10) against the
recomp before changing the poster.
#### Bail density: ours rendered, the impact scale checked (2026-10-03, analysis only; no code change)
Session 084712 rendered headless through the e2e path (target `.local/fma-target`, `E2E_FPS=60`; `E2E_DIR` must be an
**absolute** path, a relative one fails with "path not found" because the test runs from the crate folder). The
state log has 0 malformed lines. `bails_cmp.py` took the time label from a missing `ms` column; it now uses row / 60.
| per bail, window −0.5…+2.5 s | the recomp (38 bails) | ours bail 1 (18.2 s) | ours bail 2 (36.2 s) |
|---|---|---|---|
| Skate_Collisions voices | 207.7 | 46 | 45 |
| Skate_Collisions Σg² | 4.68 | 1.89 | 1.53 |
| Skate_Collisions peak | p50 0.61 | 0.72 | 0.72 |
| three banks (Collisions + Metal + Bodyslide) Σg² | ≈ 5.1 | 1.95 | 1.84 |
| collision-manager posts | 137 (p50 133) | ≈ 30 (estimate: voices / 1.5, the recomp's voices per post) | ≈ 30 |
| tier-2 body ids 1030 / 1031 / 1032 (+ torso 951) | 1.2 / 2.5 / 1.3 (+ 1.8) | 0 | 0 |
**The gap is confirmed, and it is wider than tier 2.** Ours plays no tier-1 or tier-2 body id at all except the
head's 953 (3 in bail 2). The recomp's posts by material and tier (`fixes/bail_splc.py`, nine sessions):
| region material (bands: tier 1 > b8, tier 2 > b0; floor b12) | tier 0 | tier 1 | tier 2 | ours (both bails, voices) |
|---|---|---|---|---|
| head 97 (0.05 / 0.375; 0.002) | 952: 2.9 | 953: 3.1 | 1032: 1.3 | 952: 3, 953: 3 |
| torso 98 (0.1 / 0.25; 0.002) | 949: 3.9 | 950: 3.2 | 951: 1.8 | 949: 7 |
| legs 99 (0.1 / 0.35; 0.002) | 947: 13.0 | 948: 6.0 | 1031: 2.5 | 947: 9 |
| arms 100 (0.2 / 0.65; 0.005) | 956: 19.2 | 957: < 0.6 | 1030: 1.2 | 956: 6 |
| skin 107 / denim 108–109 (cloth, tier 0) | 954: 20.7 / 955: 25.4 | | | 954: 7 / 955: 25 |
| concrete 3 (1.0 / 1.85; 0.12) | 958: 8.7 | 1047: none | 991: none | 958: 0 |
In the recomp 40–60 % of the head / torso / leg posts are tier 1 or 2, so its region impacts are often above 0.1 and
several times per bail above 0.25–0.65. Ours never exceeds 0.21 in the whole log (bail 1 max 0.086).
**What scale retail expects: [0, 1], the scale we already have.** The thresholds are not in an AEMS program. The poster
picks the id by the collision material's impact bands (the AudioSurface records, `impact_band`), and the AEMS program
only plays the id. The body bands sit inside [0, 1] (floors 0.002–0.005, tier 2 at 0.25–0.65). The concrete record's
tier 1 / 2 (> 1.0 / > 1.85) cannot be reached by a clamped region impact, and the recomp never posts 1047 / 991 in a
bail, while 958 (> 0.12) plays 8.7 times. So retail's impacts are clamped to [0, 1] too. Re-read of `sub_82BD60C8`'s
region loop (0x82BD68F0…0x82BD69D4) matches `skate_events::region_impacts` term by term:
- part = SkeletonCollision `+1200 + 4i` (−1 = none);
- Δv = SkeletonState `+4048 + 16·part`;
- the region normal is `+1008 + 16i`;
- mass = SkeletonState `+4560 + 4·part`;
- × config `+164` (10);
- floor 0x82063A48 (0.001), then clamp to [0, 1];
- written to Collision `+80 + 4i`, slide speed to `+112 + 4i`.
`+4560` is the raw bone-box product that `82BEBAA8` normalises (`SkeletonAnimationMasses::part_weights`). Same
indices, same constants. **So the poster and the impact formula are not the gap. The input is:** ours has small
per-step velocity changes of the ragdoll parts along the contact normal. Rough sizes (full-extent bone boxes ≈ 0.003
arm … 0.015 torso m³): tier 2 needs Δv·n per 1/60 s step of about 1.7 m/s (torso) to 4–5 m/s (head, legs). Ours peaks
near 2 m/s, though the regions touch the ground for 1–2 s per bail (58–114 contact frames, sliding at 3–7 m/s). In
our ragdoll the parts land and slide smoothly. Retail's give sharp per-step stops and bounces.
**Not changed:** the mechanism on the audio side is already retail. Scaling the impacts or the bands would be a guessed
weighting. The cause is in the physics (the ragdoll's contact response / part velocities during a bail), not in
`contacts::body`.
Caveats:
- Two bails only (from off-board air at 6.5 m/s, and from 500 → 300 at 7.6 m/s after an air). The recomp's 38 are of
mixed heights.
- The recomp's post count is inflated by its frame rate. It runs the poster at ~345 fps, so the 15-frame body cooldown
is ≈ 43 ms there, against 0.5 s on the 30 fps console.
**Open (needs a decision or data):**
1. Measure retail's impacts directly: a recomp hook logging Collision `+80..+100` (or the audio state `+496..+516`)
and the touched parts' SkeletonState `+4048` Δv per physics step during bails. This would run alongside the
planned `PLAY_TRACE_AUDIOX.bat bail` session (the user's session). It would say whether retail's per-step Δv is
really 2–3× ours, and whether that comes from the contact solve or from the bail drives.
*Hook built 2026-10-03 (recomp `research-hooks`, category `audiox`; session pending).* Kinds `BAILSTEP` / `BAILREG`
log per pass of `sub_82BD60C8`, only during the local player's bails. For each of the **8** regions with a part they
log the impact the game wrote (Collision `+80 + 4i`; the loop runs 8 times, so `+80..+108`), |Δv·n|, the signed
normal velocity before and after, |Δv|, the normal, the mass, the slide speed and the tag. They also log the update
interval. The local block comes from the skater-entry update `sub_827A1B78`: entry `+152` bit 31, X =
`[[character + 1808] + 1800]`. Found on the way: the SkeletonState velocities are finite differences × 60
(`sub_82BEBD28`, constant `0x822F860C` = 60; `+3216` position, `+3632` velocity, `+4048` Δv), so Δv is per update and
assumes 1/60 s. Summary: `.claude/skills/recomp-research/tools/bail_impacts.py <trace>`.
*First session (the user's `audiox_bail_20261003_094336`, 3 min, 0 malformed lines): no BAIL\* lines at all.* Cause:
the hook accepted only entries inside the global table `*(0x83083C38) + 0x2F070`, but neither caller of
`sub_827A1B78` passes a table entry. The per-skater loop `sub_827A11B0` (return address `0x827A13FC`) passes a
stack array (its `r1 + 1064 − 152`, stride 544), and `sub_827A1030` (return `0x827A10A0`) a single zeroed stack entry
with r6 = 1. So the "local entry" test never held, BAILLOCAL never logged and no pass matched the local X. Entry
`+152` bit 31 is the r6 argument (in the loop: the skater's controller test). Fixed in the recomp (uncommitted): the
hook accepts only the loop's call (`lr == 0x827A13FC`) with bit 31 set, and a bail is active when the entry's bits
or `+676` / `+677` of a GREC-local audio state are set. A rate-limited `BAILCAND` line (7 fields) shows the lookup's
raw inputs (first 32 calls; each new X of the output pass), so a failed lookup is visible in the next trace.
*What the session shows without the BAIL\* lines* (`.local/research/bail/bail_session_summary.py`):
- **Two GREC owners pass the local test** in this session (board objects `40C33020` from the start, `40C34020` from
22.9 s), each once per frame. FIRSTHIT kept one set of last values, so the +676 / +677 bytes "alternated" 1 / 0
every call: one owner was bailing and the other was not. `40C34020` bailed once (154.7 s) with B+16 still 0, so it
is not the local skater. `40C33020` is. FIRSTHIT now keeps its last values per state. Earlier sessions had one
owner. GREC / GRECX / TREAT / SEAMPAT readers should filter by owner (GREC's published local state alternates
too, so the TREAT / SEAMPAT local-mask bit 2 is unreliable when a second owner exists).
- **10 local bails**, 3.6–7.9 s from +676 rising to clearing. +677 (end) rises 1.4–4.0 s in and clears ≈ 1.0 s before
+676 clears.
- **B+16 rises in the same frame as +676** in all 10 bails, and clears 33–34 ms before +677 clears. So the first-hit
byte is "bail in progress" for the whole bail, not a single first contact. Its rising edge is the bail start.
- **B+24 is not 0.0000.** It resets to 0 at the bail start and then rises monotonically in steps about 17 ms apart
(8–41 steps per bail) to a final value of 0.42–1.0 (clamped at 1.0 in 3 bails). It reaches that value 0.5–4.0 s in
and holds it until the next bail. It is an accumulated bail strength, not a per-hit value.
- **Body posts per bail** (SPLC, window bail start −0.5 s … +676 clear; ids by tier as in the table above):
| bail (s) | posts | tier 0 / 1 / 2 | 1030 / 1031 / 1032 | 951 | 958 / 1047 / 991 | B+24 final |
|---|---|---|---|---|---|---|
| 39.8 | 149 | 106 / 27 / 16 | 2 / 7 / 4 | 3 | 4 / 2 / 0 | 0.68 |
| 81.2 | 67 | 55 / 8 / 4 | 1 / 0 / 2 | 1 | 8 / 1 / 0 | 0.42 |
| 91.9 | 49 | 35 / 9 / 5 | 0 / 2 / 2 | 0 | 2 / 3 / 1 | 0.56 |
| 101.8 | 108 | 74 / 17 / 17 | 5 / 6 / 4 | 1 | 11 / 3 / 1 | 1.00 |
| 121.1 | 45 | 29 / 10 / 6 | 1 / 3 / 0 | 2 | 0 / 0 / 0 | 1.00 |
| 136.0 | 61 | 38 / 13 / 10 | 1 / 3 / 2 | 0 | 3 / 4 / 4 | 0.96 |
| 144.4 | 67 | 46 / 13 / 8 | 0 / 6 / 1 | 1 | 2 / 0 / 0 | 0.64 |
| 157.4 | 46 | 32 / 9 / 5 | 0 / 2 / 2 | 1 | 0 / 0 / 0 | 0.55 |
| 168.7 | 43 | 27 / 7 / 9 | 6 / 0 / 2 | 0 | 1 / 4 / 1 | 0.82 |
| 184.2 | 79 | 52 / 16 / 11 | 2 / 4 / 2 | 1 | 3 / 6 / 2 | 1.00 |
Mean per bail: 71 posts; 1030 / 1031 / 1032 = 1.8 / 3.3 / 2.1 (all 74 posts of those ids fall in the 10 bails);
head 952 / 953 = 2.4 / 3.4, torso 949 / 950 / 951 = 2.1 / 2.1 / 1.0, legs 947 / 948 = 7.1 / 4.3, arms 956 / 957 =
8.8 / 0.8, skin 954 10.8, denim 955 14.8, concrete 958 3.4. **Concrete tier 1 / 2 (1047 / 991) do occur here: 2.3 /
0.9 per bail**, unlike the nine earlier sessions, so some impacts against that surface went above 1.0 / 1.85. Since
the region impacts are clamped to [0, 1], those posts come from a different impact value than the clamped region
impact. Open: which (e.g. the deck or another surface's bands).
*Measured with the fixed hook (2026-10-03, scripted background runs, 0 malformed lines).* Script
`.local/recomp/scripts/bail_ledge.txt`: the Super-Ultra Mega-Park spawn → off the board → a session marker → per attempt
go to the marker, run off the drop and use the bail input. Runs `bailrun_t2_100040` (2 bails), `bailrun_ok_100226` (4 full
bails), `bailrun_ok2_100514` (6 bails, cut ≈ 2.2 s in by the get-up press; the script now waits 6 s) and
`bailrun_ok3_101354` (5 local bails of 6 attempts with the COLLPOST build; one window was an NPC skater's bail and is
flagged by `bail_impacts.py`). No bail had an update > 20 ms, so no recomp hitch fell inside a bail. At spawn
BAILLOCAL logs once per load with `how` 1 and entry index 0 (one skater in the loop). Summary
`bail_impacts.py <trace>`:
- **The impact formula is confirmed line by line.** In 4763 / 4763 BAILREG lines, the impact the game wrote equals
clamp01(max(0.001, |Δv·n| × mass × 10)) within 0.002, with Δv = SS `+4048`, n = SC `+1008`, mass = SS `+4560` and
cfg `+164` = 10.
- **Update interval.** The ragdoll step runs at 60 Hz in the recomp too: step p50 16.6 ms (p10–p90 15.9–17.4) and
the SS `+5220` dt is always 0.01667. So retail's per-step Δv and ours (1/60 s) compare directly, despite the
recomp's ~345 fps render.
- **|Δv·n| per step, the recomp vs ours.** The recomp, all bails of the ok runs, per region: p90 0.01–1.7 m/s, **p99 1.8–13
m/s, max 7.6–20 m/s** (e.g. ok run: regions 3 / 5 / 6 / 7 p99 5.6 / 6.4 / 4.2 / 3.9, max 17 / 9.9 / 14.8 / 20).
Ours peaks near 2 m/s. The tier-2 needs (≈ 1.7 m/s torso, 4–5 m/s head / legs at our masses) are reached
several times per bail in the recomp. The largest steps are contact stops: in the top 10 % per region, the
into-surface v_old·n (−2 … −20 m/s) mostly goes to near 0 or reverses in one step ("stop" share 25–100 %). So the
gap is the physics: retail's parts hit the ground fast and stop in one step, while ours land and slide.
- **Masses (SS `+4560`, the recomp)** by part: 1 0.0052, 3 / 7 0.00034, 4 / 8 0.0017, 5 0.0031, 9 0.0031, 6 / 10 0.0015,
13 0.0058, 15 / 19 0.00031, 16 / 20 0.0010, 17 / 21 0.0051, 18 / 22 0.0067, 23 0.0098. They are smaller than
the ≈ 0.003–0.015 bone-box estimate above, so they are worth checking against ours (`part_weights`) too.
- The bail / end bits of the stack entry match +676 / +677 (BAILSTEP fields). With the GREC-state gate, a bail of the
second owner `40C34020` also opens the window for the local X (one 0.6 s stretch with bits 0 / 0 in run ok2). Filter
those passes by the bits.
- Posts per bail in these runs (higher drop; window bail start −0.5 s … +676 clear): ok run 84 / 399 / 324 / 144,
tier 2 = 7 / 42 / 30 / 10 (legs 1031 4 / 17 / 20 / 8 lead). Every full bail has tier-2 posts.
2. Then a physics-side investigation of the ragdoll's contact Δv (outside the audio port).
3. Frame-rate dependence (separate from the gap): `Contacts::body` decrements its 15-frame cooldown and the 4-frame
region max per rendered frame. At 60 fps that is 0.25 s / 67 ms; on the console it is 0.5 s / 133 ms. Under the
console-cadence rule both should run on the 30 fps cadence. That change would lower our post count further (up
to 2× at 60 fps). Not done here: it is not the asked gap, and it changes the output.
### Session-review leftovers: the lift level, the grind-off lag, world-bank prefetch, the first-hit gate (2026-10-03, overnight)
Four open items from "Session-review fixes". "The recomp" = skate3recomp run locally: its program logic is evidence,
@@ -3241,8 +3393,18 @@ stall windows excluded. Tools: `.local/audio-re/left/`.
- *Hook added (2026-10-03):* `FIRSTHIT` in the recomp's research hooks (category `audiox`, sampled from the board
update hook, local player): B, the B+16 byte, the B+24 float and the audio state's bail / end bytes (`+676` / `+677`),
logged when any of them changes plus once a second. It gives the timing and strength but not the writer (the hook
framework has no write-watch). A short bail session (4–5 bails of different heights on one spot) is queued. Until then the first-hit message and the Contacts.in7 pulse stay
framework has no write-watch). A short bail session (4–5 different bails in the same area) is queued. Until then the first-hit message and the Contacts.in7 pulse stay
unported (our Contacts.in7 / in8 stay 0, as with B+16 clear).
- *Measured 2026-10-03* (the user's session 094336 and the scripted bail runs; details under "Bail density … Open 1"):
- B+16 rises in the same frame as the audio state's +676 at every bail start. It clears 33–34 ms before +677 clears,
so it means "bail in progress".
- B+24 resets to 0 at the bail start and accumulates in ~17 ms steps to 0.42–1.0 (clamped).
- The first-hit torso post (COLLPOST from `sub_824BCEB0`, return `0x824BCFC8`) fires **once per bail**: torso 98
against 143, tiers 1 / 1, 0.16–0.28 s after the bail start (1.77 s once). It happens for an NPC skater's bail too
(local72 0).
- Port: on the rising edge of our bail flag, latch; at the first region contact with region ≠ 1 and tier ≥ 1, post
98 / 143 once and pulse Contacts.in7. In8 = B+24 × 32767 needs the accumulated strength, whose writer is still
unknown.
**Files.**
- `crates/skate-game/src/game_audio/world_sources.rs`, `game_audio/native/prefetch.rs`.
@@ -3392,6 +3554,9 @@ holder fields, the owner bus builder and the Class_Seams function map. No code f
### Native runtime is the default (2026-10-02)
(Superseded 2026-10-03: the tables and these opt-outs are removed; see "Interim cue tables retired; the water
splash is native".)
- The native AEMS runtime is now the default. `settings/audio.json` `"interim": true` or `SKATE_AEMS=0` selects
the interim measured tables; any other `SKATE_AEMS` value, or none, selects native.
- The old `native` setting key is ignored. Installs saved `"native": false` while the tables were the default, and
@@ -3407,3 +3572,145 @@ The remaining 2–5 dB gap between our seam hits and the recomp's capture (after
mechanism. Pitch, per-voice level, bus, voices per hit and the bed spectrum all match. The user closed it as a
likely recomp artefact: the recomp's frame rate is uncapped, its audio thread stalls, and its capture includes
ambience and the other seam voices. The user hears the seams as "WAY BETTER". No levels were changed.
### Interim cue tables retired; the water splash is native (2026-10-03, `player::footsteps::Splash`)
The user, after the listening check of build 04:20: "go ahead and remove the interim cues", then, on hearing some were
still in use: "oh.. if they are still being used then we aren't done." Decisions: splash "port it natively first";
the riding bed: remove it, then check it against the recomp; installs without the data: require it (no fallback).
**1. The water splash, from retail's mechanism (not the carried-over sound).** `SFXObj_OffBoard`'s process ends with
`sub_824EBB58`; its update with `sub_824EBE78`. Read from the TU3 recompilation (reference only):
- Inputs. The bridge `sub_824B0DA8` copies record `+172` bits 30 / 29 / 28 into audio state `+811` / `+812` / `+813`.
The conditioner `sub_827A1B78` writes them:
- `+811` in water = the current state's byte `+81`. In our engine that is Wipeout300's `special_surface_81`
(its water contact), already published into `state_flags`.
- `+812` under the surface = `+811` and the state's surface height `+32` (`surface_height_32`) above the Y of any
of the PhysOut Skeleton points `+128` / `+112` / `+32`. Skeleton::FillPhysOut (`sub_82BE1AE8`) writes them: part
15's and part 19's pose matrices applied to a per-part local point (Skeleton `+2560` / `+2816`, translation at
`+48`), and part 1's pose translation (Skeleton `+8128`).
- `+813` the board in water = Collision `+16` = 12. That is the board's surface vote `82C08818`, which forces 12
when a board contact is water: `physics::board_surface`, the value respawn already reads.
- Poster (every frame, record active; no local or on-foot gate). On `+811`'s rise the time in water `+480` starts at
0, then counts dt; on its fall the latches `+477` / `+478` clear. On `+812` once per stay (`+478`), unless the
game global `+224` is set (0 in free skate), the entry sound `+484` is stopped and a new one starts. The ids are
vault fields of the AudioSurface-class record `water` (`923CCB46EF5BF5BA` / `B2BD1F28DDE601B0`):
- Skate_Collisions **1187** (`35D3B06292CDA10B`) on the water contact's first frame;
- **1197** (`C17485220849574D`) once the time in water has reached 0.001 s (`9CD13431903E3719`), i.e. from the
next frame on;
- `+813` once per rise (`+492`): **1198** (`BA81E93AE985D1C7`) into `+488`.
- Sound path: the collision Splice object `sub_82497F48` into SFX Master, start block [0, 1, 0, 0, 1, 1]. The update
`sub_82498140` sends [OffBoard level(13) (entry) / level(16) (board) / 32767, pitch(14) / 4096, raw(0) × 360/65535,
dt, 0, 1] and the env send level(15) / 32767 (as the grind on / off sounds: the last update's env level is latched
at each start). A sound that ended is released.
- Cross-check: 1197 is the container of **record 869**, which is exactly the sound the user found by ear (476–478,
with 169 at 75 % and one of 171 / 172). 1187 → record 868 (170–172 + 169); 1198 → record 870 (473–475, the set the
user had rejected for small body falls: retail plays it for the board). The 2026-10-01 measured retail splash
(475 + 478 + 172 + 169) is 1197 + 1198.
- Port: `skate_audio::player::footsteps::Splash` (state `in_water` / `under_water` / `board_in_water`, set in
`skate_events::audio_state`; the ids and threshold in `FootstepTuning`). Logged in game as
`AUDIO_EVENT splash native Skate_Collisions:<id>`.
- **UNCERTAIN:** the engine does not publish the per-part local points at Skeleton `+2560` / `+2816`, so the toes'
pose translations stand in for `+128` / `+112`. That can only shift the entry frame by the local offset (a few
cm of depth).
- Through the real MixMap (test `the_water_splash_plays_through_the_real_mixmap`): OffBoard level 13 = 14585
(≈ 0.45), 16 = 14401, env 15 = 862, pitch 14 = 4086.
**2. The riding bed removed, checked against the recomp.** Under native the random bed still played Skate_Collisions
947–968 and sk8_foley 62 / 73 / 74 / 84 / 85 / 88 / 89 as interim voices. All of that is gone (below). Riding = GREC
/ state-log context roll + carve, audible voices (> 0.002), first-voice levels
(`.local/audio-re/retire/bed_vs_recomp.py`, `riding_posters.py`):
| per riding second | the recomp (6 sessions, 682 s) | ours native (5 user sessions, 229 s) | the removed bed (expected, same frames) | ours before (native + bed) |
|---|---|---|---|---|
| Skate_Collisions voices | 8.95 | 2.80 | 5.78 | 8.58 |
| Skate_Collisions Σg² | 0.424 | 0.218 | 0.120 | 0.338 |
| sk8_foley voices | 5.93 | 2.54 | 1.90 | 4.44 |
| sk8_foley Σg² | 0.229 | 0.103 | 0.079 | 0.182 |
(Recomp: all_20261002_180430, 223306, 223613, 214346, 214002, 222155. Ours: the e2e renders of 213757, 214224,
215843, 224747 (row) and 084712 (`E2E_FPS=60`). The native renders are byte-identical before and after this change.)
So without the bed ours is −2.9 dB (Skate_Collisions) and −3.5 dB (sk8_foley) below the recomp while riding. By
poster (the recomp, riding):
- **Skate_Collisions:** the collision manager (`sub_824D1F68` ← `sub_824D2318`) plays 3.3 posts/s while riding.
Most are the body materials **955 / 954 / 956 / 947** (denim / skin / arm / leg, 1.7/s, p50 ≈ 0.1); the rest are
969 / 958 / 968 / 1038 (≈ 0.6/s, mostly quiet). Ours: the body poster (`sub_824BC188`, ported) runs in every state,
but our skeleton publishes almost no body-region contacts while riding (4 of 1,770 riding rows in 084712, impacts ≤
0.011). The manager is shared, though: NPC skaters' Contacts and physics props (`sub_824E3BE0`) post into it too,
and the session review already put 969 / 958 / 968 down to world objects. The SPLC caller chain stops at the
manager's update, and no hook records the messages (`sub_82486EF0`: materials, tiers, levels, poster). So whether
the local rider's regions touch something while riding (and what) is **not established**. Not changed (no
guessed weighting).
*Measured 2026-10-03 (recomp hook `COLLPOST` on `sub_82486EF0`, category `audiox`; run `riderun_20261003_101135`,
scripted: three ~10 s pushing segments from a session marker at the Super-Ultra Mega-Park spawn, straight / carving;
0 malformed lines; summary `.claude/skills/recomp-research/tools/collision_posts.py`).* Each line has the posting
function (from the return address), its object, the object's audio state `[object+32]`, the GREC owner of that state,
`[[object+28]+72]` (local72), the materials, the tiers and the caller chain. Findings:
- **Clean pushing and carving on open ground: no body posts from the local rider.** Segment 1 (straight, 25.6–35.6
s) had none. Every local body-poster post (31, local72 = 1, owner `40C33020`) came in bursts at 52.7 s and
70.5–74.8 s, when the rider brushed and hit a rock wall at the edge of the park (screenshots). They paired legs 99
(tiers 0–2) against materials 2 / 65 / 143 with the denim cloth 108 / 143.
- **The other owner is an NPC skater.** The second GREC owner `40C34020` (doc above) posts with local72 = 0 through its
own Contacts object. Its bail at ≈ 38 s produced the run's body burst (39 posts, 34 body SPLC 956 / 954 / 955 /
947 at 38.2–38.4 s) while the local rider rode normally. So `[[object+28]+72]` tells the local rider from other
skaters, where GREC's `[[owner+16]+72]` does not.
- Deck impacts (`sub_824BD000`, deck 95 against the ground): local 1.28/s, the NPC 0.34/s.
- The 48-byte message's +32 / +36 words are not SPLC ids: mostly 0, otherwise 4-hex-digit values, e.g. 0x6590. The
sounds come later from the manager.
- Conclusion: while riding, retail's body-material sounds come from other skaters' bails and from the rider touching
walls, not from riding contacts. Ours having ~none on open ground matches. The 1.7/s in the user's riding
sessions likely include NPC skaters (no COLLPOST in those sessions; the session review's PCU / park sessions had
AI skaters). To check in a user session: `PLAY_TRACE_AUDIOX.bat` now logs COLLPOST.
- **sk8_foley:** every poster is ported. The rates differ with riding style. The recomp's riding has 0.42 plant /
lift posts per second; ours 0.19 / 0.16 (the user: "the 19 plants are correct, i didn't ride fast"). Push-driven
sounds scale with that: the Clothing push foley 74 / 73 (ours 0.16/s, the recomp 0.79/s) and the on-board
footsteps. Shoe scuffs 94 / 95: ours 1.61/s, the recomp 2.71/s (both p50 0.061). The pedestrians' sk8_foley 62 / 63
(0.53/s in the recomp, `SFXObj_PedestrianSFX`) are a world layer we can't have yet. No mechanism gap found; not
changed.
- **Needed to settle the Skate_Collisions part:** a recomp hook on the collision message post `sub_82486EF0`
(caller chain, the owner's local flag, materials A / B, tiers, levels), plus a short riding session by the user
(flat ground, some pushing and carving, no bails). Then compare the local rider's body-region messages with our
region contacts (`rimp*` / `rtag*` in the state log).
**3. Native data required.** No fallback any more:
- no AEMS banks → the runtime does not start and an error says that the skater's sounds, the world emitters and
rolling are silent;
- no MixMap → error, no player sounds, no rolling;
- no grain recordings / tuning → error, rolling silent;
- missing player banks or Splice trees → error, those sounds silent.
The zone beds, location sets and crossfades still play their measured layers through Bevy voices.
**4. The interim path removed.**
- `cues.rs` deleted. `grain_for` moved to `grain_bed.rs` (with its test); `RETAIL_SCALE` moved to `voices.rs`
(ambience and the Bevy-voice scale still use it).
- `skate_events.rs`: the `Event` enum, the event detectors (pop, landing, flip, bail, push, step, splash, foot
strikes), the `play` system and its loops (rolling bands, bed, grinds, powerslide, foot drag, wheel spin) are
gone. `observe` keeps the audio state, `Riding` (minus the fields only `play` read), the state log and the
`AUDIO_EVENT` brake / push / grind lines.
- `physics::foot_clearance` removed (only the interim steps used it).
- Opt-outs removed: `SKATE_AEMS=0`, `SKATE_AEMS_PLAYER=0`, `SKATE_AEMS_FOOTSTEPS=0`. `"interim"` / `"native"` keys in
`settings/audio.json` still load and are ignored (test `old_settings_files_with_interim_or_native_keys_still_load`).
The other `SKATE_AEMS_*` switches (they pick between native variants) stay.
- `library.rs`: the grain speed bands, the wheel-spin clips, cue preloading and the patch trees (`Patches` /
`Group`) are gone. Old manifests still load: unknown keys are ignored.
- `voices.rs`: the fold / scale stay, because the measured world layers still play through Bevy voices.
`Play::effect` is test-only now.
- Gone with the tables: the `AUDIO_CUE` / `AUDIO_LOOP` lines and `AUDIO_EVENT land` / `rolling` (interim-only).
**Verification.**
- `cargo test -p skate-audio --locked`: all pass, including `the_splash_plays_once_per_water_stay_by_its_time_in_water`.
- `game_audio::` (release): 54 pass, 4 ignored (60 before; the 8 interim tests went, 2 added).
- `cargo build --locked` (dev): OK. The 3 warnings are items that were already test-only at 45c6e65.
- e2e byte-identical to the 45c6e65 renders (`opt/runs/fma_plain3`): 13 scenarios + 4 real sessions, row and
`E2E_FPS=300` (`opt/runs/retire1`), and 084712 at `E2E_FPS=60` against `check0847`. The e2e path never ran the
interim layer, and the splash inputs are false there.
- Not run here: the muted 5-map smoke test (it opens a window) and bin\ staging.
**For the user's check in game:**
- a fall into deep water: one 1197 splash per entry, plus 1198 when the board lands in the water;
- riding on flat ground without the random bed (Skate_Collisions / sk8_foley pieces), against the recomp.
Rerun: `py -3.13 .local/audio-re/retire/bed_vs_recomp.py <recomp sessions> <DIR:NAME,…>`,
`py -3.13 .local/audio-re/retire/riding_posters.py <recomp sessions>` (PYTHONIOENCODING=utf-8).
@@ -41,12 +41,16 @@ The recompilation is **not** a perfect copy of the console game, and its traces
- the input script and record variables;
- `SKATE3_BACKGROUND`.
The branch's `src/research/README.md` lists every variable and line kind.
The branch's `src/research/README.md` lists every variable and line kind, and
[`src/research/USAGE.md`](https://github.com/Hailey-Ross/skate3recomp/blob/research-hooks/src/research/USAGE.md)
is a step-by-step guide: a first trace, choosing categories, reading and checking a trace, watch lists, input
scripts and the audio capture.
4. Check every trace for malformed lines before using it; there should be none.
The analysis scripts we used (trace readers, per-bank level and voice tools, the e2e comparisons) are tied to
this fork's layout and paths. Anyone who wants to use them needs to build the recomp and set up the paths
themselves.
The analysis scripts we used are published in `tools/recomp-trace/` (trace readers, per-bank level and voice
tools, rolling-bed and send analysis) and `tools/recomp-code-search/` (searching the recompiled sources and the
memory image); see [14](14-published-tools.md). They are reference only: anyone who wants to use them needs to
build the recomp and set up the paths themselves.
## Credits
@@ -0,0 +1,82 @@
# 14. Published development and research tools
## Problem
The fork's work (map spawns, collision volumes, setup speed-ups, the native audio) was checked with
many small scripts: regression baselines, equivalence proofs, format readers, audio render analysis,
trace analysis. They lived in a local, untracked workspace with paths hard-wired to one machine, so
nobody else could rerun the checks behind the documents in this folder or build on the readers.
## Change
The generally useful scripts are now in `tools/`, one folder per tool, each with a `README.md` (what it
does, inputs, usage, example output, requirements). An index is in [`tools/README.md`](../../tools/README.md).
| Folder | Contents |
|---|---|
| `tools/regression-checks/` | `check_maps.py` (`--validate-maps` + collision triangle counts), `check_spawns.py` (baked spawns), `check_customiser.py` (customiser output snapshot), `smoke_maps.sh` (muted crash smoke test per map). |
| `tools/setup-equivalence/` | `compare_streams.py` (stream loader byte equivalence), `compare_refpack.py` (native RefPack DLL against the Python decoder). |
| `tools/collision-inspect/` | `map_collision.py`, `analyse_spawn.py`, `collision_attrs.py`, `scan_surfaceless.py`, `unsigned_collision.py`. |
| `tools/world-stream-inspect/` | `big_list.py`, `sim_types.py`, `volumes_dump.py`, `arenas.py` (shared arena reader). |
| `tools/audio-file-inspect/` | `aems_survey.py`, `bank_layout_check.py`, `decode_bank_samples.py`, `multichannel_census.py`, `splc_fields.py`, `ems_dump.py`, `grain_survey.py`. |
| `tools/vault-inspect/` | `find_field.py`, `vault_fields.py`, `vault_layout.py`. |
| `tools/audio-e2e/` | `scenarios.py`, `render_diff.py`, `compare.py`, `frame_levels.py`, `voices_summary.py`, `bank_rate.py`, `wet_level.py`. |
| `tools/audio-bench/` | `e2e_bench.sh`, `compare_runs.sh`, `timing_summary.py`, `audio_timing_summary.py`, `emitter_bank_memory.py`, `prefetch_memory_sim.py`. |
| `tools/recomp-trace/` | For the recomp's research hooks: `trace.py`, `first_pass.py`, `resolve_trace.py`, `retail_voices.py`, `retail_relay.py`, `grec_level.py`, `grec_clean.py`, `grec_material.py`, `send_vectors.py`, `grain_trace_stats.py`, `retail_windows.py`, `veh_trace.py`. |
| `tools/recomp-code-search/` | For the recomp's research hooks: `fn.sh`, `grepfn.sh`, `callctx.sh`, `ppcxref.py`. |
What changed against the working copies:
- Hard-coded paths became arguments with defaults relative to the repository: `--disc` (extracted disc
root, or `$SKATE3_DISC`, default `.local/skate3-disc`), `--assets`, `--vault`, `--work`, `--baseline`,
`--exe`, and environment variables for the shell scripts (`SKATE_EXE`, `BENCH_DIR`,
`RECOMP_GENERATED`). Work output goes under `.local/` (gitignored).
- The regression baselines are not shipped. Each check records its own on a known-good install
(`--update` / `--save`) and stores it in `.local/regression/` by default.
- Game-derived constants became inputs: `retail_windows.py --contact-callers`,
`grec_material.py --no-contact`, `emitter_bank_memory.py --group`, `vault_layout.py --name`,
`grain_trace_stats.py --exclude`; `ppcxref.py` takes the image, function list, base and code range as
arguments.
- `sim_types.py` / `volumes_dump.py` now share a small arena reader (`arenas.py`) instead of depending
on a local research script.
## Recomp tools
The two `recomp-*` folders only work with traces and sources from the research hooks of the Skate 3
recompilation:
[Hailey-Ross/skate3recomp, branch `research-hooks`](https://github.com/Hailey-Ross/skate3recomp/tree/research-hooks)
(see [13](13-recomp-research-hooks.md)). Their READMEs, and the index, label them as such and say they
are reference only: users must build the recomp from their own copy of the game and set up the paths
themselves. The recompilation is not a perfect copy of the console game (uncapped frame rate, different
timing and threading), so its traces are evidence for values and logic, not for timings.
## Left out
- Anything that embeds game code or data: disassembly listings, golden scripts generated from the game,
the MixMap reference evaluator (it carries a name table taken from the game and reads tables at fixed
image addresses), and the image readers built around fixed address lists (registries, vtables, type
tables, image constants).
- Tools tied to one local setup: the comparisons against the local audio proof-of-concept worktree,
the dev-install staging shortcuts (setup now does the same steps), and the upstream-sync helpers for
this fork's branch workflow.
- One-off investigation scripts (NPC speech, pedestrian chases, census layers, single-question surveys).
## Verification
- Every published Python script compiles (`python -m py_compile`) and prints its usage with `--help`.
- Every shell script passes `bash -n`.
- No private paths, names or tool-specific notes remain in the published files (searched).
- The tools were not rerun against game data while publishing; their logic is unchanged from the
working copies apart from the argument handling listed above.
## Open questions
- `crates/skate-game/src/game_audio/e2e.rs` still names the old local script paths in its header
comment; it could point to `tools/audio-e2e/` instead.
- None of this is offered upstream yet; it would make a tooling PR of its own (see the PR plan).
## Credits
[skate3recomp](https://github.com/mchughalex/skate3recomp) by @mchughalex, the
[rexglue SDK](https://github.com/rexglue/rexglue-sdk), [Xenia](https://github.com/xenia-project/xenia)'s
Xbox 360 research, [vgmstream](https://github.com/vgmstream/vgmstream) for audio decoding, and the
vendored map extraction tools in `tools/vendor/`.
+1
View File
@@ -19,6 +19,7 @@ verification, open questions).
| J | Feature (audio) | Game audio: setup group `audio` extracts/decodes the disc's audio (vgmstream r2117), per-map ambience, water emitters, rolling by surface and speed band, trick/landing/grind/bail/splash cues, footsteps from foot strikes, volume settings and `--mute` | [11](11-audio.md) | `gameplay/audio` (`98c52b6`, `d9a1818`); upstream PR branch `audio/retail-audio`, which mirrors `gameplay/audio` exactly (source of truth; batch commit `f438017` "New audio who dis", merged as `ca7b4f3`, 2026-10-02) | H (branched from `gameplay/water`; uses its water detection — `collision_feedback.flags.material_12`, `is_water_tag`). Overlaps upstream #4/#1 (retail-exact player audio via transcribed game code + memory image; not adopted — see doc 11): both edit `tools/asset_pipeline/asset_exports.py` and `install.py`, different asset layouts | Draft upstream [#32](https://github.com/SK8-ENGINE/skate-3-rust-engine/pull/32), **Work in Progress**. Branch has the first version; the world-audio work (.ems emitters, location-set one-shots/sirens, zone ambience) is done locally and goes up with the next batch commit; native AEMS runtime (`crates/skate-audio`, new workspace crate) implemented 2026-10-02 behind `SKATE_AEMS=1` (world emitters only), then the MixMap mixer (emitter words) and the granular rolling bed (`grain/`, `game_audio/grain_bed.rs`; setup keeps the whole grains, copies `MixMapSK8.mxb`, exports the grain tuning), then the player inputs and the first native player components (`skate_audio::player`, `game_audio/player_audio.rs`: Class_grind, SenseOfSpeed, Class_foot_drag; setup exports `player_tuning`; `skate-core` `WheelLineState.seam_patterns`), then the Splice board contacts / skid / squeaks and the collision manager, wheel spin and foot taps / scuffs (`player/{contacts,collision,wheels}.rs`; `skate-core` `BoardGroundState.part_audio_surfaces`; setup `player_tuning.collision`), then the buses (reverb network, eEQChain, flanger returns, collision submix) and Class_Seams, the board layers (`player/rolling.rs`), the grain bus chain (`grain/chain.rs`, `dsp/{fss,shelf}.rs`), tricks / treatment, and footsteps / clothing / hands on the deck (`player/{tricks,treatment,footsteps,clothing,step_on}.rs`; setup adds four Class_rolling banks, `bus_tuning.flange`, `player_tuning.audio_tricks_2`, per-material footstep fields and 16 grain-chain vault keys). All uncommitted, all on #32's checklist (updated 2026-10-02 21:30). The seam sample shuffle is in (Listening test 8: retail's boot utility `Start_up_Play_ctl`; setup now copies `Common.abk`). Listening: landings, pops, footsteps and the hand pick-up / put-down confirmed; rolling "is much better, it just has issues with certain surfaces"; after the shuffle, concrete and brick "both sound better but still need work". Plan, in order: (1) surface parity from the paired sessions (ours / recomp at the Aletown brick and the University PCU Library); (2) footstep submix, remaining engine inputs, reverb-zone emitters (1,040 records), Reverb inputs / global env-send scale, utility boot order (done headless 2026-10-02, doc 11 "FootStep SubMix, the skeleton inputs, …"; needs the listening check; setup exports the zones' `reverb` preset); (3) native as the default, interim cue tables removed; (4) the optimisation pass; (5) world / NPC audio stages as follow-ups. #32's description carries the same status and plan (updated 2026-10-02 evening). Latest listening (build 21:57): "rolling sounds better, griding sounds better, walking sounds better"; a University fountain's "doubling/restarting" is retail's relay of short pieces, with the repeat rate being analysed from the user's WATERRELAY retail session. Seams: retail runs them per rendered frame; per the user's decision ours targets the 360's ~30 fps cadence, frame-rate independent (done, default; user: "seams sound much better. overall much better <3"). Plan step (1) done; (2) Treatments 16/17 from the TREAT trace settled 2026-10-02: the recomp plays them too (their samples equal sense_of_speed 3/4, so the trace tool misread them); `+236` now counts through the landing tick, and the e2e scenarios pass the air words (doc 11 "Treatments slots 16 / 17"). Since the push: native is the default (2026-10-02 late; `interim`/`SKATE_AEMS=0` opt-out, tables kept), uncommitted. Overnight 2026-10-03: the MixMap, the Jitter walk and the eEQChain clear on the console's 30 Hz cadence (`mixmap::cadence`, flag inputs held; `SKATE_AEMS_MIX_CONSOLE=0` = old), frame-rate independent 60–365 fps, uncommitted (doc 11 "The MixMap on the console cadence"); the seam-prominence gap re-measured (the recomp capture lags ~110 ms), no change. Overnight plan: remaining small items, then the optimisation pass, then ped speech / traffic / NPC skaters' boards. World / NPC foundation built overnight 2026-10-03, uncommitted (doc 11 "World sound sources"): `skate_audio::world` (traffic engine / horn / alarm / skid, ped footsteps, speech index and reaction cues), `gaLine truncated
| G | Performance (setup) | Skip decoding duplicate stream copies; run the customiser beside the maps; parallel clothing library and pro roster, plus a native RefPack DLL in `Build.ps1` | [08](08-setup-performance.md) | `8327e94` (+ overlap hunks of `install.py` in `18f41a3`) | — | Draft upstream [#29](https://github.com/SK8-ENGINE/skate-3-rust-engine/pull/29) |
| K | Gameplay fix (physics) | Board/skater solver: 50 constraint iterations (retail live value) instead of `RWMaxIterations` = 25 | [12](12-solver-iterations.md) | uncommitted (on `gameplay/audio` working tree) | — (one-file change in `physics/settings.rs`) | Ready apart from the asset-gated `customiser_equipment_reaches_ground_force_and_torque` question (doc 12) |
| L | Tooling | Development and research tools in `tools/<name>/` (regression checks, setup equivalence, collision / world-stream / audio-file / vault readers, audio e2e and bench; recomp trace and code search, reference only) | [14](14-published-tools.md) | uncommitted (on `gameplay/audio` working tree) | `audio-e2e` / `audio-bench` / `audio-file-inspect` need J's native audio and setup modules; `setup-equivalence/compare_refpack.py` needs G's RefPack DLL; `regression-checks/check_maps.py` uses F's `--validate-maps` (falls back to `--check-assets` without it) | Not opened; the generic folders could go first, the audio ones with or after J |
Opened 2026-10-01 as drafts from per-PR branches cut from upstream `60efdef`
(code only; `docs/hails-additions` stays in the fork). #29 (G) is stacked on
+1
View File
@@ -19,6 +19,7 @@ Each document stands alone. File paths are relative to the repository root.
| 11 | [Game audio](11-audio.md) | Setup (Python, vgmstream), game audio (Rust) | Work in progress (draft #32). Player cues tuned against retail traces; retail world audio done: .ems emitters, location-set one-shots (sirens; 1,859/1,859 retail match over 15 locations), zone ambience beds. Native AEMS runtime (`crates/skate-audio`: evaluator + voice graph) implemented behind `SKATE_AEMS=1` for the world emitters; 234/235 PoC-oracle scripts identical, DSP kernels bit-exact; MixMap + granular rolling bed; player MixMap inputs and the first native player components (grind, wind/rattle, foot drag; wind level within 0.5 dB of retail); needs a listening check. |
| 12 | [Board solver: 50 constraint iterations](12-solver-iterations.md) | Game physics (Rust) | Done; maps identical, water traces as expected; one asset-gated customiser test fails (open question) |
| 13 | [Research hooks for the recompilation](13-recomp-research-hooks.md) | Research tooling (external: skate3recomp `research-hooks` branch) | Published; reference and information gathering only (the recompilation is not a perfect copy of the console game) |
| 14 | [Published development and research tools](14-published-tools.md) | Tooling (`tools/<name>/`, Python and shell) | Done (uncommitted); scripts compile and print their usage; the two `recomp-*` folders are for the recomp's research hooks, reference only |
## Environment used for verification