From 9cc283d1f5b6ae57adf569a3347ec3ae5c9264c5 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:54:07 +1000 Subject: [PATCH 01/16] Add deterministic WASM SIMD quantization and configured parity gates --- Cargo.lock | 2 + games/tf2/browser/src/gameplay-worker.ts | 4 + packages/formats/mp3/rust/Cargo.toml | 2 + packages/formats/mp3/rust/src/lib.rs | 138 +++++++++++++++++- packages/formats/mp3/rust/tests/configured.rs | 56 ++++++- packages/presentation/audio/src/playback.ts | 4 +- packages/presentation/audio/src/wasm.ts | 8 + .../presentation/audio/tests/wasm.test.ts | 12 ++ tools/playsrc/src/tf2-wasm-build.ts | 18 ++- tools/playsrc/tests/simd-configured.test.ts | 54 +++++++ tools/playsrc/tests/tf2-wasm-build.test.ts | 19 ++- 11 files changed, 299 insertions(+), 18 deletions(-) create mode 100644 packages/presentation/audio/src/wasm.ts create mode 100644 packages/presentation/audio/tests/wasm.test.ts create mode 100644 tools/playsrc/tests/simd-configured.test.ts diff --git a/Cargo.lock b/Cargo.lock index c841f62cd..a8f1b8bb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -647,6 +647,8 @@ name = "playsrc-mp3" version = "0.1.0" dependencies = [ "nanomp3", + "playsrc-vpk", + "serde_json", "sha2 0.10.9", ] diff --git a/games/tf2/browser/src/gameplay-worker.ts b/games/tf2/browser/src/gameplay-worker.ts index f677d78d0..14b51a62f 100644 --- a/games/tf2/browser/src/gameplay-worker.ts +++ b/games/tf2/browser/src/gameplay-worker.ts @@ -243,6 +243,10 @@ async function initialize(request: Extract Result Result= 4 { + // Four readable f32 inputs and four writable i16 outputs. Neither + // load nor store requires vector alignment or touches a tail lane. + unsafe { + let sample = f32x4_mul( + v128_load(input.as_ptr().add(at).cast()), + f32x4_splat(32768.0), + ); + let rounded = i32x4_trunc_sat_f32x4(f32x4_nearest(sample)); + let packed = i16x8_narrow_i32x4(rounded, rounded); + v128_store64_lane::<0>(packed, output.as_mut_ptr().add(at).cast()); + } + at += 4; + } + at + }; + #[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))] + let at = 0; + for (sample, value) in input[at..].iter().zip(&mut output[at..]) { + *value = pcm_sample(*sample, false); + } +} + // minimp3's PC synthesis uses scalar quantization for the pair at positions // 0/16 and nearest-even SIMD quantization for the other synthesis samples. fn pcm_sample(normalized: f32, scalar_pair: bool) -> i16 { @@ -102,4 +149,83 @@ mod tests { assert_eq!(decode(b"ID3", 2, 100).unwrap_err(), Error::InputLimit); assert_eq!(decode(b"ID3", 3, 100).unwrap_err(), Error::InvalidStream); } + + #[test] + fn synthesis_groups_preserve_every_lane_and_short_tail() { + check_synthesis_groups(); + } + + #[cfg(target_arch = "wasm32")] + #[unsafe(no_mangle)] + pub extern "C" fn check_wasm_synthesis_groups() { + check_synthesis_groups(); + } + + #[cfg(target_arch = "wasm32")] + mod wasm_decode { + use std::sync::Mutex; + static PCM: Mutex> = Mutex::new(Vec::new()); + + #[unsafe(no_mangle)] + pub extern "C" fn test_input_alloc(length: usize) -> *mut u8 { + assert!(length <= 32 * 1024 * 1024); + Box::into_raw(vec![0_u8; length].into_boxed_slice()) as *mut u8 + } + + #[unsafe(no_mangle)] + /// # Safety + /// Pass one allocation returned by test_input_alloc with its exact length. + pub unsafe extern "C" fn test_decode(pointer: *mut u8, length: usize) -> usize { + let input = + unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(pointer, length)) }; + let decoded = super::decode(&input, 32 * 1024 * 1024, 32 * 1024 * 1024).unwrap(); + let mut pcm = PCM.lock().unwrap(); + *pcm = decoded.samples; + pcm.len() + } + + #[unsafe(no_mangle)] + pub extern "C" fn test_pcm_pointer() -> *const i16 { + PCM.lock().unwrap().as_ptr() + } + } + + fn check_synthesis_groups() { + let values = [ + 0.0, + -0.0, + f32::from_bits(1), + -f32::from_bits(1), + f32::INFINITY, + f32::NEG_INFINITY, + f32::NAN, + f32::from_bits(0xffc12345), + -32767.5 / 32768.0, + 32766.5 / 32768.0, + -1.0 / 32768.0, + -2.5 / 32768.0, + 2.5 / 32768.0, + 1.0, + -1.0, + ]; + for channels in [1, 2] { + for offset in 0..values.len() { + let input: Vec<_> = (0..97) + .map(|index| values[(index + offset) % values.len()]) + .collect(); + for length in 0..input.len() { + let mut output = vec![12345; length + 2]; + quantize(&input[..length], &mut output[1..length + 1], channels); + assert_eq!(output[0], 12345); + assert_eq!(output[length + 1], 12345); + for index in 0..length { + assert_eq!( + output[index + 1], + pcm_sample(input[index], (index / channels) % 16 == 0) + ); + } + } + } + } + } } diff --git a/packages/formats/mp3/rust/tests/configured.rs b/packages/formats/mp3/rust/tests/configured.rs index 49b103c7b..32ad242e1 100644 --- a/packages/formats/mp3/rust/tests/configured.rs +++ b/packages/formats/mp3/rust/tests/configured.rs @@ -1,10 +1,56 @@ use sha2::{Digest, Sha256}; +use std::{ + fs, + io::{Read, Seek, SeekFrom}, + ops::Range, + path::PathBuf, +}; + +struct Segments(PathBuf); +impl playsrc_vpk::SegmentReader for Segments { + fn len(&self, index: u32) -> Result { + Ok( + fs::metadata(self.0.join(format!("tf2_sound_misc_{index:03}.vpk"))) + .unwrap() + .len(), + ) + } + fn read(&self, index: u32, range: Range) -> Result, playsrc_vpk::SourceError> { + let mut file = + fs::File::open(self.0.join(format!("tf2_sound_misc_{index:03}.vpk"))).unwrap(); + file.seek(SeekFrom::Start(range.start)).unwrap(); + let mut bytes = vec![0; usize::try_from(range.end - range.start).unwrap()]; + file.read_exact(&mut bytes).unwrap(); + Ok(bytes) + } +} #[test] -#[ignore = "requires configured cow1.mp3; no game assets are distributed"] +#[ignore = "requires the exact configured TF2 sound archive; no game assets are distributed"] fn configured_mono_matches_public_minimp3_sse_pcm() { - let root = std::env::var_os("PLAYSRC_AUDIO_EVIDENCE").expect("explicit evidence directory"); - let input = std::fs::read(std::path::PathBuf::from(root).join("cow1.mp3")).unwrap(); + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../.."); + let config: serde_json::Value = + serde_json::from_slice(&fs::read(root.join("playsrc.local.json")).unwrap()).unwrap(); + let contract: serde_json::Value = + serde_json::from_slice(&fs::read(root.join("games/tf2/content-build.json")).unwrap()) + .unwrap(); + let tf2 = PathBuf::from(config["tf2Dir"].as_str().unwrap()); + let directory = fs::read(tf2.join("tf2_sound_misc_dir.vpk")).unwrap(); + assert_eq!( + format!("{:x}", Sha256::digest(&directory)), + contract["archiveIndexes"]["tf2SoundMisc"].as_str().unwrap() + ); + let archive = playsrc_vpk::parse( + &directory, + "tf2_sound_misc_dir.vpk", + playsrc_vpk::Layout::Split, + Default::default(), + ) + .unwrap(); + let input = archive + .read_entry("sound/ambient_mp3/cow1.mp3", &Segments(tf2)) + .unwrap() + .bytes; assert_eq!( format!("{:x}", Sha256::digest(&input)), "6d5029641d1a058b5316d4fd49b7ee923ec6490bb5ce93e40fa25ccaa169aad5" @@ -28,4 +74,8 @@ fn configured_mono_matches_public_minimp3_sse_pcm() { playsrc_mp3::decode(&input, input.len(), decoded.samples.len() - 1).unwrap_err(), playsrc_mp3::Error::OutputLimit ); + let retained = PathBuf::from(config["sourceCacheDir"].as_str().unwrap()) + .join("evidence/tf2-wasm-simd-performance/configured"); + fs::create_dir_all(&retained).unwrap(); + fs::write(retained.join("cow1.mp3"), &input).unwrap(); } diff --git a/packages/presentation/audio/src/playback.ts b/packages/presentation/audio/src/playback.ts index 853030749..ec9a23466 100644 --- a/packages/presentation/audio/src/playback.ts +++ b/packages/presentation/audio/src/playback.ts @@ -1,4 +1,5 @@ import { AudioError } from "./error" +import { compileAudioModule } from "./wasm" import type { Listener, NeutralVoice, SoundSource } from "./source" export type PcmResource = Readonly<{ identity: string; sampleRate: number; numberOfChannels: number; bits: number; length: number; duration: number; loopStartSeconds: number | null }> @@ -60,8 +61,7 @@ export async function createSourceAudioSystem(context: AudioContext, moduleUrl: module = fetch(moduleUrl).then(async response => { if (!response.ok) throw new AudioError("BrowserFailure", "Audio module is unavailable") const bytes = await response.arrayBuffer() - if (bytes.byteLength > 8 * 1024 * 1024) throw new AudioError("Capacity", "Audio module exceeds its bound") - return WebAssembly.compile(bytes) + return compileAudioModule(bytes) }) moduleCache.set(moduleUrl.href, module) void module.catch(() => moduleCache.delete(moduleUrl.href)) diff --git a/packages/presentation/audio/src/wasm.ts b/packages/presentation/audio/src/wasm.ts new file mode 100644 index 000000000..35ebe6013 --- /dev/null +++ b/packages/presentation/audio/src/wasm.ts @@ -0,0 +1,8 @@ +import { AudioError } from "./error" + +/** Validate the actual module, not a browser name or its WebGPU capability. */ +export function compileAudioModule(bytes: ArrayBuffer): Promise { + if (bytes.byteLength > 8 * 1024 * 1024) throw new AudioError("Capacity", "Audio module exceeds its bound") + if (!WebAssembly.validate(bytes)) throw new AudioError("BrowserFailure", "Audio module is invalid or this browser lacks required standard WebAssembly SIMD128 support") + return WebAssembly.compile(bytes) +} diff --git a/packages/presentation/audio/tests/wasm.test.ts b/packages/presentation/audio/tests/wasm.test.ts new file mode 100644 index 000000000..e21e86a2c --- /dev/null +++ b/packages/presentation/audio/tests/wasm.test.ts @@ -0,0 +1,12 @@ +import { expect, test } from "bun:test" +import { compileAudioModule } from "../src/wasm" + +// () -> (), v128.const zero; drop. Standard SIMD, no relaxed instructions. +const simd = new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,23,1,21,0,253,12,...Array(16).fill(0),26,11]).buffer + +test("audio validates actual SIMD bytecode before compiling, independently of GPU or browser identity", async () => { + expect(WebAssembly.validate(simd)).toBe(true) + expect(await compileAudioModule(simd)).toBeInstanceOf(WebAssembly.Module) + expect(() => compileAudioModule(new ArrayBuffer(0))).toThrow("invalid or this browser lacks") + expect(() => compileAudioModule(new ArrayBuffer(8 * 1024 * 1024 + 1))).toThrow("exceeds its bound") +}) diff --git a/tools/playsrc/src/tf2-wasm-build.ts b/tools/playsrc/src/tf2-wasm-build.ts index b6165ee94..1771aa3d8 100644 --- a/tools/playsrc/src/tf2-wasm-build.ts +++ b/tools/playsrc/src/tf2-wasm-build.ts @@ -18,7 +18,7 @@ export class Tf2WasmBuildError extends Error { export function threadedWasmRustFlags(root: string, cargoHome: string, sysroot: string): string[] { return [ - "-Ctarget-feature=+atomics,+bulk-memory", + "-Ctarget-feature=+atomics,+bulk-memory,+simd128", "-Clink-arg=--shared-memory", "-Clink-arg=--max-memory=4294967296", "-Clink-arg=--import-memory", @@ -26,12 +26,20 @@ export function threadedWasmRustFlags(root: string, cargoHome: string, sysroot: "-Clink-arg=--export=__tls_size", "-Clink-arg=--export=__tls_align", "-Clink-arg=--export=__tls_base", - `--remap-path-prefix=${root}=/playsrc`, - `--remap-path-prefix=${cargoHome}=/cargo`, - `--remap-path-prefix=${sysroot}=/rust`, + ...wasmSourcePathFlags(root, cargoHome, sysroot), ] } +function wasmSourcePathFlags(root: string, cargoHome: string, sysroot: string): string[] { + return [`--remap-path-prefix=${root}=/playsrc`, `--remap-path-prefix=${cargoHome}=/cargo`, `--remap-path-prefix=${sysroot}=/rust`] +} + +export function audioWasmRustFlags(root: string, cargoHome: string, sysroot: string): string[] { + // Audio has its own unshared memory; only the deterministic SIMD and source + // remapping contract is common with the threaded gameplay build. + return ["-Ctarget-feature=+simd128", ...wasmSourcePathFlags(root, cargoHome, sysroot)] +} + type WasmBuildManifest = Readonly<{ schema: "playsrc-threaded-wasm-build-v2" identity: string @@ -198,7 +206,7 @@ export async function buildThreadedTf2Wasm( "--target", "wasm32-unknown-unknown", "--target-dir", audioTarget, "--release", "-Z", "build-std=panic_abort,std", ], { cwd: repositoryRoot, - env: { ...buildEnvironment, RUSTFLAGS: undefined, CARGO_ENCODED_RUSTFLAGS: flags.filter(flag => flag.startsWith("--remap-path-prefix=")).join("\x1f"), CARGO_BUILD_JOBS: process.env.PLAYSRC_PROFILE_OWNER_TOKEN ? "2" : process.env.CARGO_BUILD_JOBS }, + env: { ...buildEnvironment, RUSTFLAGS: undefined, CARGO_ENCODED_RUSTFLAGS: audioWasmRustFlags(repositoryRoot, cargoHome, sysroot).join("\x1f"), CARGO_BUILD_JOBS: process.env.PLAYSRC_PROFILE_OWNER_TOKEN ? "2" : process.env.CARGO_BUILD_JOBS }, stdout: "inherit", stderr: "inherit", }) const audioExit = await audio.exited diff --git a/tools/playsrc/tests/simd-configured.test.ts b/tools/playsrc/tests/simd-configured.test.ts new file mode 100644 index 000000000..998461e5a --- /dev/null +++ b/tools/playsrc/tests/simd-configured.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "bun:test" +import { createHash } from "node:crypto" +import { mkdir, readFile, writeFile } from "node:fs/promises" +import path from "node:path" +import { loadLocalConfig, repositoryRoot } from "../src/config" +import { rustEnvironment } from "../src/setup" +import { acquireHeadedProfileLock, releaseHeadedProfileLock } from "../src/profile-lock" +import { borrowedWindowsJobLock } from "../src/windows-job-native" +import toolchains from "../toolchains.json" + +// Configured acceptance is opt-in on the local host, or explicitly scheduled +// under the native local-job supervisor. It is not a browser/profile admission. +test.skipIf(!process.env.PLAYSRC_LOCAL_JOB_OWNER && process.env.RUN_CONFIGURED_SIMD_TESTS !== "1")("configured native and actual scalar/SIMD WASM PCM are byte-exact", async () => { + const config = await loadLocalConfig() + const lockPath = path.join(config.sourceCacheDir, "evidence/tf2-browser-performance/chromium-profile.lock") + const borrowed = await borrowedWindowsJobLock(lockPath, { testFile: import.meta.filename }) + if (process.platform === "win32" && !borrowed) throw new Error("Windows configured tests require the native local-job supervisor") + const lock = borrowed ?? await acquireHeadedProfileLock(lockPath, "simd-configured-parity", 175_000) + const directory = path.join(config.sourceCacheDir, "evidence/tf2-wasm-simd-performance", `configured-${process.platform}-${crypto.randomUUID()}`) + await mkdir(directory, { recursive: true }) + const environment = { ...process.env, ...rustEnvironment(config.sourceCacheDir) } + const cargo = path.join(config.sourceCacheDir, "toolchains/rust/cargo/bin", process.platform === "win32" ? "cargo.exe" : "cargo") + const run = async (name: string, args: string[], flags?: string) => { + const child = Bun.spawn([cargo, `+${toolchains.rust.threadedToolchain}`, ...args], { cwd: repositoryRoot, + env: { ...environment, RUSTFLAGS: flags, CARGO_ENCODED_RUSTFLAGS: undefined, CARGO_BUILD_JOBS: "2" }, stdout: "pipe", stderr: "pipe", timeout: 145_000 }) + const [stdout, stderr, code] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]) + await writeFile(path.join(directory, `${name}.log`), stdout + stderr) + expect(code, stderr).toBe(0) + } + const hash = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex") + try { + await run("native", ["test", "--locked", "-p", "playsrc-mp3", "--", "--include-ignored"]) + const input = await readFile(path.join(config.sourceCacheDir, "evidence/tf2-wasm-simd-performance/configured/cow1.mp3")) + expect(hash(input)).toBe("6d5029641d1a058b5316d4fd49b7ee923ec6490bb5ce93e40fa25ccaa169aad5") + const records = [] + for (const simd of [false, true]) { + const variant = simd ? "simd" : "scalar", target = path.join(directory, `target-${variant}`) + await run(variant, ["rustc", "--locked", "-p", "playsrc-mp3", "--lib", "--crate-type=cdylib", "--target", "wasm32-unknown-unknown", "--target-dir", target, "--release", "-Z", "build-std=panic_abort,std", "--", "--cfg", "test"], `-Ctarget-feature=${simd ? "+" : "-"}simd128`) + const file = path.join(target, "wasm32-unknown-unknown/release/playsrc_mp3.wasm"), bytes = await readFile(file) + const { instance } = await WebAssembly.instantiate(bytes), e = instance.exports as any + e.check_wasm_synthesis_groups() + const pointer = e.test_input_alloc(input.length) + new Uint8Array(e.memory.buffer, pointer, input.length).set(input) + const count = e.test_decode(pointer, input.length) + expect(count).toBe(73728) + const pcmSha256 = hash(new Uint8Array(e.memory.buffer, e.test_pcm_pointer(), count * 2)) + expect(pcmSha256).toBe("b1e43ccf681c3529aad850231599216cfd55778a27bb559b8859917be486ee42") + records.push({ variant, file, bytes: bytes.length, sha256: hash(bytes), samples: count, pcmSha256 }) + } + const commit = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: repositoryRoot }).stdout.toString().trim() + await writeFile(path.join(directory, "result.json"), JSON.stringify({ commit, platform: process.platform, arch: process.arch, engine: process.versions, browserEvidence: false, input: { bytes: input.length, sha256: hash(input) }, records }, null, 2)) + console.log(`SIMD parity evidence: ${directory}`) + } finally { if (!borrowed) await releaseHeadedProfileLock(lockPath, lock.token) } +}, 175_000) diff --git a/tools/playsrc/tests/tf2-wasm-build.test.ts b/tools/playsrc/tests/tf2-wasm-build.test.ts index 5b5bb5cb0..28650bdfa 100644 --- a/tools/playsrc/tests/tf2-wasm-build.test.ts +++ b/tools/playsrc/tests/tf2-wasm-build.test.ts @@ -2,11 +2,11 @@ import { expect, test } from "bun:test" import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises" import os from "node:os" import path from "node:path" -import { threadedWasmRustFlags } from "../src/tf2-wasm-build" +import { audioWasmRustFlags, threadedWasmRustFlags } from "../src/tf2-wasm-build" test("threaded WASM flags retain memory contracts and encode paths with spaces as single arguments", () => { const flags = threadedWasmRustFlags("/build one/app", "/build one/cargo", "/build one/rust") - expect(flags).toContain("-Ctarget-feature=+atomics,+bulk-memory") + expect(flags).toContain("-Ctarget-feature=+atomics,+bulk-memory,+simd128") expect(flags).toContain("-Clink-arg=--shared-memory") expect(flags).toContain("-Clink-arg=--max-memory=4294967296") expect(flags).toContain("--remap-path-prefix=/build one/app=/playsrc") @@ -15,6 +15,21 @@ test("threaded WASM flags retain memory contracts and encode paths with spaces a expect(flags.join("\x1f").split("\x1f")).toEqual(flags) }) +test("audio enables standard SIMD independently without importing gameplay memory or TLS", () => { + const flags = audioWasmRustFlags("C:/build one/app", "C:/build one/cargo", "C:/build one/rust") + expect(flags).toEqual([ + "-Ctarget-feature=+simd128", + "--remap-path-prefix=C:/build one/app=/playsrc", + "--remap-path-prefix=C:/build one/cargo=/cargo", + "--remap-path-prefix=C:/build one/rust=/rust", + ]) + expect(flags.join("\x1f").split("\x1f")).toEqual(flags) + for (const build of [flags, threadedWasmRustFlags("app", "cargo", "rust")]) { + expect(build.filter(flag => flag.startsWith("-Ctarget-feature="))).toHaveLength(1) + expect(build.some(flag => flag.includes("relaxed-simd") || flag.includes("fast-math"))).toBe(false) + } +}) + test("source-location constants compile identically from distinct absolute roots", async () => { const directory = await mkdtemp(path.join(os.tmpdir(), "playsrc-remap-")) try { From 592d866c5ef613c327e0d7793d76dd0b214607b3 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:56:39 +1000 Subject: [PATCH 02/16] Bound decoded PCM capacity and prepare configured native state checks --- packages/formats/mp3/rust/src/lib.rs | 7 ++++ packages/formats/mp3/rust/tests/configured.rs | 4 +++ .../tests/simd-gameplay-preparation.test.ts | 33 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 tools/playsrc/tests/simd-gameplay-preparation.test.ts diff --git a/packages/formats/mp3/rust/src/lib.rs b/packages/formats/mp3/rust/src/lib.rs index 6f938462b..7433c186a 100644 --- a/packages/formats/mp3/rust/src/lib.rs +++ b/packages/formats/mp3/rust/src/lib.rs @@ -53,6 +53,13 @@ pub fn decode(bytes: &[u8], max_input: usize, max_samples: usize) -> Result output.samples.capacity() { + // Reserve a frame without increasing the former per-sample push + // capacity bound (power-of-two growth for this i16 buffer). + output + .samples + .reserve_exact((start + count).next_power_of_two() - start); + } output.samples.resize(start + count, 0); quantize( &pcm[..count], diff --git a/packages/formats/mp3/rust/tests/configured.rs b/packages/formats/mp3/rust/tests/configured.rs index 32ad242e1..bc5f89174 100644 --- a/packages/formats/mp3/rust/tests/configured.rs +++ b/packages/formats/mp3/rust/tests/configured.rs @@ -60,6 +60,10 @@ fn configured_mono_matches_public_minimp3_sse_pcm() { (decoded.sample_rate, decoded.channels, decoded.samples.len()), (44100, 1, 73728) ); + assert_eq!( + decoded.samples.capacity(), + decoded.samples.len().next_power_of_two() + ); let mut digest = Sha256::new(); for sample in &decoded.samples { digest.update(sample.to_le_bytes()); diff --git a/tools/playsrc/tests/simd-gameplay-preparation.test.ts b/tools/playsrc/tests/simd-gameplay-preparation.test.ts new file mode 100644 index 000000000..731292f60 --- /dev/null +++ b/tools/playsrc/tests/simd-gameplay-preparation.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "bun:test" +import { mkdir, writeFile } from "node:fs/promises" +import path from "node:path" +import { loadLocalConfig, repositoryRoot } from "../src/config" +import { rustEnvironment } from "../src/setup" +import { acquireHeadedProfileLock, releaseHeadedProfileLock } from "../src/profile-lock" +import { borrowedWindowsJobLock } from "../src/windows-job-native" +import toolchains from "../toolchains.json" + +test.skipIf(!process.env.PLAYSRC_LOCAL_JOB_OWNER && process.env.RUN_CONFIGURED_SIMD_TESTS !== "1")("current configured KOTH and native audio state tests prepare the SIMD comparison", async () => { + const config = await loadLocalConfig() + const lockPath = path.join(config.sourceCacheDir, "evidence/tf2-browser-performance/chromium-profile.lock") + const borrowed = await borrowedWindowsJobLock(lockPath, { testFile: import.meta.filename }) + if (process.platform === "win32" && !borrowed) throw new Error("Windows configured tests require the native local-job supervisor") + const lock = borrowed ?? await acquireHeadedProfileLock(lockPath, "simd-gameplay-preparation", 175_000) + const directory = path.join(config.sourceCacheDir, "evidence/tf2-wasm-simd-performance", `native-state-${process.platform}-${crypto.randomUUID()}`) + await mkdir(directory, { recursive: true }) + try { + const cargo = path.join(config.sourceCacheDir, "toolchains/rust/cargo/bin", process.platform === "win32" ? "cargo.exe" : "cargo") + const cases = [ + ["audio", "-p", "playsrc-audio", "--lib"], + ["koth", "-p", "playsrc-tf2", "--test", "configured_koth", "viaduct_authored_logic_capture_io_and_generated_clocks", "--", "--ignored"], + ] + for (const [name, ...args] of cases) { + const child = Bun.spawn([cargo, `+${toolchains.rust.threadedToolchain}`, "test", "--locked", ...args], { cwd: repositoryRoot, + env: { ...process.env, ...rustEnvironment(config.sourceCacheDir), CARGO_BUILD_JOBS: "2" }, stdout: "pipe", stderr: "pipe", timeout: 145_000 }) + const [stdout, stderr, code] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]) + await writeFile(path.join(directory, `${name}.log`), stdout + stderr) + expect(code, stderr).toBe(0) + } + console.log(`Native state preparation (not browser or sustained gameplay evidence): ${directory}`) + } finally { if (!borrowed) await releaseHeadedProfileLock(lockPath, lock.token) } +}, 175_000) From b6cf8ab369082a9121cec37f9950b393c20592dc Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:58:55 +1000 Subject: [PATCH 03/16] Probe standard SIMD capability without revalidating full modules --- bun.lock | 9 +++++++++ games/tf2/browser/package.json | 3 ++- games/tf2/browser/src/gameplay-worker.ts | 5 +++-- packages/presentation/audio/package.json | 5 ++++- packages/presentation/audio/src/wasm.ts | 5 +++-- packages/presentation/audio/tests/wasm.test.ts | 13 +++++++++++-- packages/runtime/wasm/package.json | 6 ++++++ packages/runtime/wasm/src/index.ts | 8 ++++++++ packages/runtime/wasm/tests/capability.test.ts | 9 +++++++++ 9 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 packages/runtime/wasm/package.json create mode 100644 packages/runtime/wasm/src/index.ts create mode 100644 packages/runtime/wasm/tests/capability.test.ts diff --git a/bun.lock b/bun.lock index 7a1f0248e..b5cab952f 100644 --- a/bun.lock +++ b/bun.lock @@ -42,6 +42,7 @@ "@playsrc/rendering": "workspace:*", "@playsrc/settings": "workspace:*", "@playsrc/vgui": "workspace:*", + "@playsrc/wasm": "workspace:*", }, }, "packages/asset-store": { @@ -55,6 +56,9 @@ }, "packages/presentation/audio": { "name": "@playsrc/audio", + "dependencies": { + "@playsrc/wasm": "workspace:*", + }, }, "packages/presentation/particle": { "name": "@playsrc/particle", @@ -72,6 +76,9 @@ "packages/runtime/settings": { "name": "@playsrc/settings", }, + "packages/runtime/wasm": { + "name": "@playsrc/wasm", + }, "tools/playsrc": { "name": "@playsrc/tool", "dependencies": { @@ -326,6 +333,8 @@ "@playsrc/vgui": ["@playsrc/vgui@workspace:packages/presentation/vgui"], + "@playsrc/wasm": ["@playsrc/wasm@workspace:packages/runtime/wasm"], + "@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="], "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], diff --git a/games/tf2/browser/package.json b/games/tf2/browser/package.json index 044287776..41491ee42 100644 --- a/games/tf2/browser/package.json +++ b/games/tf2/browser/package.json @@ -36,6 +36,7 @@ "@playsrc/particle": "workspace:*", "@playsrc/rendering": "workspace:*", "@playsrc/settings": "workspace:*", - "@playsrc/vgui": "workspace:*" + "@playsrc/vgui": "workspace:*", + "@playsrc/wasm": "workspace:*" } } diff --git a/games/tf2/browser/src/gameplay-worker.ts b/games/tf2/browser/src/gameplay-worker.ts index 14b51a62f..b0d9f3509 100644 --- a/games/tf2/browser/src/gameplay-worker.ts +++ b/games/tf2/browser/src/gameplay-worker.ts @@ -3,6 +3,7 @@ import { TF2_PRESENTATION_SCHEMA, type InitialView, type WorkerFailureCode, type WorkerRequest, type WorkerResponse } from "./protocol" import { ResourceGenerations } from "./resource-generations" import { MAX_GRAPH_CHUNKS } from "@playsrc/asset-store/graph" +import { supportsSimd128 } from "@playsrc/wasm" import { reclaimModelReads } from "./model-read-ownership" import { ReplyWriter, REPLY_BYTES, type SharedReply, type ReplyRange } from "./reply-transport" import { ADMISSION_EVENT_BYTES, MAX_ADMISSION_EVENTS, decodeAdmissionMetrics } from "./admission-metrics" @@ -243,8 +244,8 @@ async function initialize(request: Extract { if (bytes.byteLength > 8 * 1024 * 1024) throw new AudioError("Capacity", "Audio module exceeds its bound") - if (!WebAssembly.validate(bytes)) throw new AudioError("BrowserFailure", "Audio module is invalid or this browser lacks required standard WebAssembly SIMD128 support") + if (!supportsSimd128()) throw new AudioError("BrowserFailure", "This browser lacks required standard WebAssembly SIMD128 support") return WebAssembly.compile(bytes) } diff --git a/packages/presentation/audio/tests/wasm.test.ts b/packages/presentation/audio/tests/wasm.test.ts index e21e86a2c..8e6977435 100644 --- a/packages/presentation/audio/tests/wasm.test.ts +++ b/packages/presentation/audio/tests/wasm.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "bun:test" +import { expect, spyOn, test } from "bun:test" import { compileAudioModule } from "../src/wasm" // () -> (), v128.const zero; drop. Standard SIMD, no relaxed instructions. @@ -7,6 +7,15 @@ const simd = new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,23,1,2 test("audio validates actual SIMD bytecode before compiling, independently of GPU or browser identity", async () => { expect(WebAssembly.validate(simd)).toBe(true) expect(await compileAudioModule(simd)).toBeInstanceOf(WebAssembly.Module) - expect(() => compileAudioModule(new ArrayBuffer(0))).toThrow("invalid or this browser lacks") + expect(compileAudioModule(new ArrayBuffer(0))).rejects.toBeInstanceOf(WebAssembly.CompileError) expect(() => compileAudioModule(new ArrayBuffer(8 * 1024 * 1024 + 1))).toThrow("exceeds its bound") }) + +test("an unsupported SIMD target is rejected without compiling the audio module", () => { + const validate = spyOn(WebAssembly, "validate").mockReturnValue(false) + const compile = spyOn(WebAssembly, "compile") + try { + expect(() => compileAudioModule(simd)).toThrow("lacks required standard WebAssembly SIMD128") + expect(compile).not.toHaveBeenCalled() + } finally { validate.mockRestore(); compile.mockRestore() } +}) diff --git a/packages/runtime/wasm/package.json b/packages/runtime/wasm/package.json new file mode 100644 index 000000000..c6355e350 --- /dev/null +++ b/packages/runtime/wasm/package.json @@ -0,0 +1,6 @@ +{ + "name": "@playsrc/wasm", + "private": true, + "type": "module", + "exports": "./src/index.ts" +} diff --git a/packages/runtime/wasm/src/index.ts b/packages/runtime/wasm/src/index.ts new file mode 100644 index 000000000..b153db4cf --- /dev/null +++ b/packages/runtime/wasm/src/index.ts @@ -0,0 +1,8 @@ +// (module (func (drop (v128.const i32x4 0 0 0 0)))) +// Probe standard SIMD before instantiation without validating the entire large +// gameplay module twice. WebAssembly compilation still validates its full body. +const SIMD128 = new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,23,1,21,0,253,12,...Array(16).fill(0),26,11]) + +export function supportsSimd128(): boolean { + return typeof WebAssembly !== "undefined" && WebAssembly.validate(SIMD128) +} diff --git a/packages/runtime/wasm/tests/capability.test.ts b/packages/runtime/wasm/tests/capability.test.ts new file mode 100644 index 000000000..7e640df58 --- /dev/null +++ b/packages/runtime/wasm/tests/capability.test.ts @@ -0,0 +1,9 @@ +import { expect, spyOn, test } from "bun:test" +import { supportsSimd128 } from "../src" + +test("standard SIMD admission exercises bytecode, not browser or GPU identity", () => { + expect(supportsSimd128()).toBe(true) + const validate = spyOn(WebAssembly, "validate").mockReturnValue(false) + try { expect(supportsSimd128()).toBe(false); expect(validate).toHaveBeenCalledTimes(1) } + finally { validate.mockRestore() } +}) From eea3694cc25e8ba2bcc7add2ae74afafa686007c Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:06:44 +1000 Subject: [PATCH 04/16] Retain SIMD parity modules outside bounded Windows linker scratch paths --- tools/playsrc/tests/simd-configured.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tools/playsrc/tests/simd-configured.test.ts b/tools/playsrc/tests/simd-configured.test.ts index 998461e5a..e5d3fd9c0 100644 --- a/tools/playsrc/tests/simd-configured.test.ts +++ b/tools/playsrc/tests/simd-configured.test.ts @@ -1,6 +1,6 @@ import { expect, test } from "bun:test" import { createHash } from "node:crypto" -import { mkdir, readFile, writeFile } from "node:fs/promises" +import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises" import path from "node:path" import { loadLocalConfig, repositoryRoot } from "../src/config" import { rustEnvironment } from "../src/setup" @@ -34,9 +34,14 @@ test.skipIf(!process.env.PLAYSRC_LOCAL_JOB_OWNER && process.env.RUN_CONFIGURED_S expect(hash(input)).toBe("6d5029641d1a058b5316d4fd49b7ee923ec6490bb5ce93e40fa25ccaa169aad5") const records = [] for (const simd of [false, true]) { - const variant = simd ? "simd" : "scalar", target = path.join(directory, `target-${variant}`) + // MSVC's host build-script linker still has a bounded output path. Keep + // Cargo scratch short and owned by this checkout; retain module bytes in + // the unique evidence run rather than relying on mutable Cargo output. + const variant = simd ? "simd" : "scalar", target = path.join(config.sourceCacheDir, "simd-tests", hash(Buffer.from(repositoryRoot)).slice(0, 8), variant) await run(variant, ["rustc", "--locked", "-p", "playsrc-mp3", "--lib", "--crate-type=cdylib", "--target", "wasm32-unknown-unknown", "--target-dir", target, "--release", "-Z", "build-std=panic_abort,std", "--", "--cfg", "test"], `-Ctarget-feature=${simd ? "+" : "-"}simd128`) - const file = path.join(target, "wasm32-unknown-unknown/release/playsrc_mp3.wasm"), bytes = await readFile(file) + const file = path.join(directory, `${variant}.wasm`) + await copyFile(path.join(target, "wasm32-unknown-unknown/release/playsrc_mp3.wasm"), file) + const bytes = await readFile(file) const { instance } = await WebAssembly.instantiate(bytes), e = instance.exports as any e.check_wasm_synthesis_groups() const pointer = e.test_input_alloc(input.length) From 7b0d152678d3ab86244f851c90d84cc905788cf2 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:11:03 +1000 Subject: [PATCH 05/16] Check remapped source constants with the target host separator --- tools/playsrc/tests/tf2-wasm-build.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/playsrc/tests/tf2-wasm-build.test.ts b/tools/playsrc/tests/tf2-wasm-build.test.ts index 28650bdfa..ff059b792 100644 --- a/tools/playsrc/tests/tf2-wasm-build.test.ts +++ b/tools/playsrc/tests/tf2-wasm-build.test.ts @@ -46,7 +46,9 @@ test("source-location constants compile identically from distinct absolute roots const errors = await new Response(child.stderr).text() expect(await child.exited, errors).toBe(0) const bytes = await readFile(output) - expect(bytes.toString()).toContain("/playsrc/fixture.rs") + // LLVM IR escapes the host separator retained by file!() after the + // directory prefix is remapped. Compare roots on the same target host. + expect(bytes.toString()).toContain(`/playsrc${process.platform === "win32" ? "\\\\" : "/"}fixture.rs`) expect(bytes.toString()).not.toContain(root) outputs.push(bytes) } From 4629b25c44b54c6a3654799f74c3778c571d9697 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 16:27:58 +1000 Subject: [PATCH 06/16] Include native DSP and playback integration cases in SIMD preparation --- tools/playsrc/tests/simd-gameplay-preparation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/playsrc/tests/simd-gameplay-preparation.test.ts b/tools/playsrc/tests/simd-gameplay-preparation.test.ts index 731292f60..066bb49fa 100644 --- a/tools/playsrc/tests/simd-gameplay-preparation.test.ts +++ b/tools/playsrc/tests/simd-gameplay-preparation.test.ts @@ -18,7 +18,7 @@ test.skipIf(!process.env.PLAYSRC_LOCAL_JOB_OWNER && process.env.RUN_CONFIGURED_S try { const cargo = path.join(config.sourceCacheDir, "toolchains/rust/cargo/bin", process.platform === "win32" ? "cargo.exe" : "cargo") const cases = [ - ["audio", "-p", "playsrc-audio", "--lib"], + ["audio", "-p", "playsrc-audio"], ["koth", "-p", "playsrc-tf2", "--test", "configured_koth", "viaduct_authored_logic_capture_io_and_generated_clocks", "--", "--ignored"], ] for (const [name, ...args] of cases) { From 361c046b49cffb1eafc375d0df95f660d7db0073 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:05:49 +1000 Subject: [PATCH 07/16] Admit SIMD through the authenticated initialization owner --- games/tf2/browser/src/gameplay-worker.ts | 5 ----- games/tf2/browser/src/wasm-initialization.ts | 6 +++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/games/tf2/browser/src/gameplay-worker.ts b/games/tf2/browser/src/gameplay-worker.ts index b0d9f3509..f677d78d0 100644 --- a/games/tf2/browser/src/gameplay-worker.ts +++ b/games/tf2/browser/src/gameplay-worker.ts @@ -3,7 +3,6 @@ import { TF2_PRESENTATION_SCHEMA, type InitialView, type WorkerFailureCode, type WorkerRequest, type WorkerResponse } from "./protocol" import { ResourceGenerations } from "./resource-generations" import { MAX_GRAPH_CHUNKS } from "@playsrc/asset-store/graph" -import { supportsSimd128 } from "@playsrc/wasm" import { reclaimModelReads } from "./model-read-ownership" import { ReplyWriter, REPLY_BYTES, type SharedReply, type ReplyRange } from "./reply-transport" import { ADMISSION_EVENT_BYTES, MAX_ADMISSION_EVENTS, decodeAdmissionMetrics } from "./admission-metrics" @@ -244,10 +243,6 @@ async function initialize(request: Extract value.toString(16).padStart(2, "0")).join("") if (actual !== request.expectedSha256) throw new WasmInitializationError(stage, "SHA256 mismatch") + stage = "simd" + if (!supportsSimd128()) throw new WasmInitializationError(stage, "Standard WebAssembly SIMD128 unavailable") stage = "shared-memory" const SharedMemory = request.sharedArrayBuffer if (!request.isolated || !SharedMemory) throw new WasmInitializationError(stage, `Cross-origin isolated shared memory unavailable (${facts})`) From 58bb668f65feba5af9347883e033a4cc9ca297dc Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:16:18 +1000 Subject: [PATCH 08/16] Measure exact decoded PCM in the guarded native browser lifecycle --- playwright.simd-profile.config.ts | 2 + tools/playsrc/profile/simd-decoder.profile.ts | 98 +++++++++++++++++++ tools/playsrc/src/profile-runner.ts | 1 + tools/playsrc/tests/simd-configured.test.ts | 6 +- 4 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 playwright.simd-profile.config.ts create mode 100644 tools/playsrc/profile/simd-decoder.profile.ts diff --git a/playwright.simd-profile.config.ts b/playwright.simd-profile.config.ts new file mode 100644 index 000000000..5a8acaa59 --- /dev/null +++ b/playwright.simd-profile.config.ts @@ -0,0 +1,2 @@ +import { headedProfileConfiguration } from "./tools/playsrc/profile/profile-config" +export default headedProfileConfiguration({ match: "simd-decoder.profile.ts" }) diff --git a/tools/playsrc/profile/simd-decoder.profile.ts b/tools/playsrc/profile/simd-decoder.profile.ts new file mode 100644 index 000000000..4f726decf --- /dev/null +++ b/tools/playsrc/profile/simd-decoder.profile.ts @@ -0,0 +1,98 @@ +import path from "node:path" +import { readFile, writeFile } from "node:fs/promises" +import { createHash } from "node:crypto" +import { execFileSync } from "node:child_process" +import { test, expect, guardStartupInput } from "./application-test" +import { profileArtifact } from "./profile-artifacts" +import { startupNativeReader } from "./native-startup" +import { requireStartupNative } from "./static-startup-gate" +import { loadLocalConfig } from "../src/config" + +// Module initialization is silent, before the application fixture requests its +// native browser stage. Build these fixtures with simd-configured.test.ts first. +const config = await loadLocalConfig(process.cwd()) +const hash = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex") +const index = JSON.parse(await readFile(path.join(config.sourceCacheDir, "simd-tests", hash(Buffer.from(process.cwd())).slice(0, 8), "comparison.json"), "utf8")) +const recordBytes = await readFile(index.path) +if (hash(recordBytes) !== index.sha256) throw Error("SIMD comparison record changed") +const inputRecord = JSON.parse(recordBytes.toString()) +if (inputRecord.records.length !== 2 || inputRecord.records[0].variant !== "scalar" || inputRecord.records[1].variant !== "simd") throw Error("Expected exactly the scalar and SIMD comparison") +const commit = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim() +if (inputRecord.commit !== commit) throw Error("Prepare SIMD fixtures at this exact source commit") +const input = await readFile(inputRecord.input.path) +if (inputRecord.input.sha256 !== "6d5029641d1a058b5316d4fd49b7ee923ec6490bb5ce93e40fa25ccaa169aad5" || hash(input) !== inputRecord.input.sha256 || input.length !== inputRecord.input.bytes) throw Error("Configured MP3 changed") +const modules = await Promise.all(inputRecord.records.map(async (record: any) => { + const bytes = await readFile(record.file) + if (hash(bytes) !== record.sha256 || bytes.length !== record.bytes) throw Error("SIMD module changed") + return { variant: record.variant, bytes: bytes.toString("base64"), sha256: record.sha256 } +})) + +test("headed x86 browser executes exact scalar and SIMD decoder kernels", async ({ page }, testInfo) => { + const directory = process.env.PLAYSRC_PROFILE_RUN_DIRECTORY! + const native = await startupNativeReader(page, config.sourceCacheDir) + guardStartupInput(page, async () => requireStartupNative(await native.read())) + let result: any, pixels: Buffer + const admissions: unknown[] = [] + try { + const admission = await native.read(); requireStartupNative(admission); admissions.push(admission) + await page.goto("/") + await expect(page.locator("main")).toHaveAttribute("data-phase", "MainMenu", { timeout: 30_000 }) + await page.setContent('Source MP3 SIMD comparison

Source MP3 decoder: exact SIMD comparison

Configured input; real browser WebAssembly execution. This is a decoder diagnostic, not gameplay FPS or a freeze fix.

Preparing scalar and SIMD modules…
') + result = await page.evaluate(async ({ modules, encoded }) => { + const bytes = (value: string) => Uint8Array.from(atob(value), c => c.charCodeAt(0)) + const input = bytes(encoded), records = [] + const digest = async (value: Uint8Array) => [...new Uint8Array(await crypto.subtle.digest("SHA-256", value))].map(value => value.toString(16).padStart(2, "0")).join("") + const states = [] + for (const entry of modules) { + const binary = bytes(entry.bytes), valid = WebAssembly.validate(binary), began = performance.now() + if (!valid) throw Error(`${entry.variant} module unsupported`) + const module = await WebAssembly.compile(binary), compiled = performance.now() + const e = (await WebAssembly.instantiate(module)).exports as any, instantiated = performance.now() + e.check_wasm_synthesis_groups() + const invoke = () => { + const pointer = e.test_input_alloc(input.length) + new Uint8Array(e.memory.buffer, pointer, input.length).set(input) + const started = performance.now(), count = e.test_decode(pointer, input.length) + return { milliseconds: performance.now() - started, count } + } + const first = invoke() + const pcm = new Uint8Array(e.memory.buffer, e.test_pcm_pointer(), first.count * 2).slice() + const pcmSha256 = await digest(pcm) + const record = { variant: entry.variant, moduleSha256: await digest(binary), valid, compileMilliseconds: compiled-began, instantiateMilliseconds: instantiated-compiled, + firstDecodeMilliseconds: first.milliseconds, samples: first.count, pcmSha256, linearBytes: e.memory.buffer.byteLength, batches: [] as number[] } + records.push(record); states.push({ invoke, record }) + if (entry.variant === "simd") { + const samples = new Int16Array(pcm.buffer), canvas = document.querySelector("canvas")!, context = canvas.getContext("2d")! + context.strokeStyle="#66dfb7"; context.beginPath() + for(let x=0;x<1000;x++){const value=samples[Math.floor(x*samples.length/1000)]!/32768; const y=140-value*125; if(x===0)context.moveTo(x,y);else context.lineTo(x,y)}context.stroke() + } + for (let i=0;i<8;i++) invoke() + } + // Alternate ABBA order. Each measurement spans 32 real decoder calls, + // reducing timer granularity without changing the per-call sample/input. + const sampleStart=performance.now(); let round=0 + while(performance.now()-sampleStart<5000) { + for(const i of round++%2===0?[0,1,1,0]:[1,0,0,1]) { + let elapsed=0;for(let n=0;n<32;n++)elapsed+=states[i]!.invoke().milliseconds + states[i]!.record.batches.push(elapsed/32) + } + await new Promise(resolve=>requestAnimationFrame(()=>resolve())) + } + for(const record of records){const values=[...record.batches].sort((a,b)=>a-b);Object.assign(record,{median:values[Math.floor(values.length/2)],p95:values[Math.floor(values.length*.95)],maximum:values.at(-1)})} + document.querySelector("#result")!.textContent=records.map((r:any)=>`${r.variant}: median ${r.median.toFixed(4)} ms, p95 ${r.p95.toFixed(4)} ms; ${r.samples} exact PCM samples`).join("\n") + return { records, sampleMilliseconds:performance.now()-sampleStart, userAgent:navigator.userAgent, platform:navigator.platform, hardwareConcurrency:navigator.hardwareConcurrency, + browserEvidence:true, sustainedGameplayEvidence:false, scope:"Configured decoder kernel; host input allocation/copy excluded identically, decoder allocations and output replacement/free included. No gameplay or freeze claim." } + }, { modules, encoded: input.toString("base64") }) + const after = await native.read(); requireStartupNative(after); admissions.push(after) + for (const record of result.records) { + expect(record.samples).toBe(73728) + expect(record.pcmSha256).toBe("b1e43ccf681c3529aad850231599216cfd55778a27bb559b8859917be486ee42") + } + pixels = await page.screenshot() + } finally { await native.close() } + await profileArtifact(async () => { + await writeFile(path.join(directory, "simd-decoder.json"), JSON.stringify({ commit, inputRecord, result, admissions }, null, 2)) + await writeFile(path.join(directory, "simd-decoder.png"), pixels) + await testInfo.attach("simd-decoder", { body: pixels, contentType: "image/png" }) + }) +}) diff --git a/tools/playsrc/src/profile-runner.ts b/tools/playsrc/src/profile-runner.ts index 467e436fa..e7d5a7161 100644 --- a/tools/playsrc/src/profile-runner.ts +++ b/tools/playsrc/src/profile-runner.ts @@ -35,6 +35,7 @@ const PROFILES = Object.freeze({ "browser-input": { config: "playwright.browser-input.config.ts", target: "pl_upward", minimumRemainingMilliseconds: 90_000 }, "browser-input-lifecycle": { config: "playwright.browser-input.config.ts", target: "pl_upward", environment: { PROFILE_INPUT_LIFECYCLE_ONLY: "1" }, minimumRemainingMilliseconds: 75_000 }, "runner-handoff": { config: "playwright.runner-handoff.config.ts", target: "jump_beef" }, + "simd-decoder": { config: "playwright.simd-profile.config.ts", target: "koth_viaduct", environment: { PLAYSRC_PROFILE_BROWSER_CHANNEL: "msedge" }, minimumRemainingMilliseconds: 40_000 }, "damage-indicator": { config: "playwright.profile.config.ts", target: "pl_upward", environment: { PROFILE_SCENARIOS: "damage-indicator" }, minimumRemainingMilliseconds: environment => environment.PROFILE_DAMAGE_LIFECYCLE === "1" ? 70_000 : environment.PROFILE_DAMAGE_DYNAMIC === "1" ? 40_000 : DEFAULT_BROWSER_MINIMUM_MILLISECONDS }, "setup-round": { config: "playwright.profile.config.ts", target: "pl_upward", environment: { PROFILE_SCENARIOS: "setup-round" }, minimumRemainingMilliseconds: 140_000 }, "soundscape-selection": { config: "playwright.profile.config.ts", target: "cp_granary", environment: { PROFILE_SCENARIOS: "soundscape-selection" }, minimumRemainingMilliseconds: 60_000 }, diff --git a/tools/playsrc/tests/simd-configured.test.ts b/tools/playsrc/tests/simd-configured.test.ts index e5d3fd9c0..65fabaa78 100644 --- a/tools/playsrc/tests/simd-configured.test.ts +++ b/tools/playsrc/tests/simd-configured.test.ts @@ -53,7 +53,11 @@ test.skipIf(!process.env.PLAYSRC_LOCAL_JOB_OWNER && process.env.RUN_CONFIGURED_S records.push({ variant, file, bytes: bytes.length, sha256: hash(bytes), samples: count, pcmSha256 }) } const commit = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: repositoryRoot }).stdout.toString().trim() - await writeFile(path.join(directory, "result.json"), JSON.stringify({ commit, platform: process.platform, arch: process.arch, engine: process.versions, browserEvidence: false, input: { bytes: input.length, sha256: hash(input) }, records }, null, 2)) + const result = JSON.stringify({ commit, platform: process.platform, arch: process.arch, engine: process.versions, browserEvidence: false, input: { path: path.join(config.sourceCacheDir, "evidence/tf2-wasm-simd-performance/configured/cow1.mp3"), bytes: input.length, sha256: hash(input) }, records }, null, 2) + const resultPath = path.join(directory, "result.json") + await writeFile(resultPath, result) + const index = path.join(config.sourceCacheDir, "simd-tests", hash(Buffer.from(repositoryRoot)).slice(0, 8), "comparison.json") + await writeFile(index, JSON.stringify({ path: resultPath, sha256: hash(Buffer.from(result)) })) console.log(`SIMD parity evidence: ${directory}`) } finally { if (!borrowed) await releaseHeadedProfileLock(lockPath, lock.token) } }, 175_000) From f56a9c51b367d824933372a0de59b7c502569960 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:18:24 +1000 Subject: [PATCH 09/16] Retain decoder memory growth and use each native host browser configuration --- tools/playsrc/profile/simd-decoder.profile.ts | 6 +++--- tools/playsrc/src/profile-runner.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/playsrc/profile/simd-decoder.profile.ts b/tools/playsrc/profile/simd-decoder.profile.ts index 4f726decf..25c80954f 100644 --- a/tools/playsrc/profile/simd-decoder.profile.ts +++ b/tools/playsrc/profile/simd-decoder.profile.ts @@ -27,7 +27,7 @@ const modules = await Promise.all(inputRecord.records.map(async (record: any) => return { variant: record.variant, bytes: bytes.toString("base64"), sha256: record.sha256 } })) -test("headed x86 browser executes exact scalar and SIMD decoder kernels", async ({ page }, testInfo) => { +test("headed browser executes exact scalar and SIMD decoder kernels", async ({ page }, testInfo) => { const directory = process.env.PLAYSRC_PROFILE_RUN_DIRECTORY! const native = await startupNativeReader(page, config.sourceCacheDir) guardStartupInput(page, async () => requireStartupNative(await native.read())) @@ -60,7 +60,7 @@ test("headed x86 browser executes exact scalar and SIMD decoder kernels", async const pcmSha256 = await digest(pcm) const record = { variant: entry.variant, moduleSha256: await digest(binary), valid, compileMilliseconds: compiled-began, instantiateMilliseconds: instantiated-compiled, firstDecodeMilliseconds: first.milliseconds, samples: first.count, pcmSha256, linearBytes: e.memory.buffer.byteLength, batches: [] as number[] } - records.push(record); states.push({ invoke, record }) + records.push(record); states.push({ invoke, record, memory: e.memory as WebAssembly.Memory }) if (entry.variant === "simd") { const samples = new Int16Array(pcm.buffer), canvas = document.querySelector("canvas")!, context = canvas.getContext("2d")! context.strokeStyle="#66dfb7"; context.beginPath() @@ -78,7 +78,7 @@ test("headed x86 browser executes exact scalar and SIMD decoder kernels", async } await new Promise(resolve=>requestAnimationFrame(()=>resolve())) } - for(const record of records){const values=[...record.batches].sort((a,b)=>a-b);Object.assign(record,{median:values[Math.floor(values.length/2)],p95:values[Math.floor(values.length*.95)],maximum:values.at(-1)})} + for(const state of states){const record=state.record,values=[...record.batches].sort((a,b)=>a-b);Object.assign(record,{median:values[Math.floor(values.length/2)],p95:values[Math.floor(values.length*.95)],maximum:values.at(-1),finalLinearBytes:state.memory.buffer.byteLength})} document.querySelector("#result")!.textContent=records.map((r:any)=>`${r.variant}: median ${r.median.toFixed(4)} ms, p95 ${r.p95.toFixed(4)} ms; ${r.samples} exact PCM samples`).join("\n") return { records, sampleMilliseconds:performance.now()-sampleStart, userAgent:navigator.userAgent, platform:navigator.platform, hardwareConcurrency:navigator.hardwareConcurrency, browserEvidence:true, sustainedGameplayEvidence:false, scope:"Configured decoder kernel; host input allocation/copy excluded identically, decoder allocations and output replacement/free included. No gameplay or freeze claim." } diff --git a/tools/playsrc/src/profile-runner.ts b/tools/playsrc/src/profile-runner.ts index e7d5a7161..5a26d1177 100644 --- a/tools/playsrc/src/profile-runner.ts +++ b/tools/playsrc/src/profile-runner.ts @@ -35,7 +35,7 @@ const PROFILES = Object.freeze({ "browser-input": { config: "playwright.browser-input.config.ts", target: "pl_upward", minimumRemainingMilliseconds: 90_000 }, "browser-input-lifecycle": { config: "playwright.browser-input.config.ts", target: "pl_upward", environment: { PROFILE_INPUT_LIFECYCLE_ONLY: "1" }, minimumRemainingMilliseconds: 75_000 }, "runner-handoff": { config: "playwright.runner-handoff.config.ts", target: "jump_beef" }, - "simd-decoder": { config: "playwright.simd-profile.config.ts", target: "koth_viaduct", environment: { PLAYSRC_PROFILE_BROWSER_CHANNEL: "msedge" }, minimumRemainingMilliseconds: 40_000 }, + "simd-decoder": { config: "playwright.simd-profile.config.ts", target: "koth_viaduct", environment: process.platform === "win32" ? { PLAYSRC_PROFILE_BROWSER_CHANNEL: "msedge" } : {}, minimumRemainingMilliseconds: 40_000 }, "damage-indicator": { config: "playwright.profile.config.ts", target: "pl_upward", environment: { PROFILE_SCENARIOS: "damage-indicator" }, minimumRemainingMilliseconds: environment => environment.PROFILE_DAMAGE_LIFECYCLE === "1" ? 70_000 : environment.PROFILE_DAMAGE_DYNAMIC === "1" ? 40_000 : DEFAULT_BROWSER_MINIMUM_MILLISECONDS }, "setup-round": { config: "playwright.profile.config.ts", target: "pl_upward", environment: { PROFILE_SCENARIOS: "setup-round" }, minimumRemainingMilliseconds: 140_000 }, "soundscape-selection": { config: "playwright.profile.config.ts", target: "cp_granary", environment: { PROFILE_SCENARIOS: "soundscape-selection" }, minimumRemainingMilliseconds: 60_000 }, From b2c54971362002fa8fce05afbb332396c3cbc139 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:20:45 +1000 Subject: [PATCH 10/16] Keep decoder profile source authentication silent on Windows --- tools/playsrc/profile/simd-decoder.profile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/playsrc/profile/simd-decoder.profile.ts b/tools/playsrc/profile/simd-decoder.profile.ts index 25c80954f..28afbb5b0 100644 --- a/tools/playsrc/profile/simd-decoder.profile.ts +++ b/tools/playsrc/profile/simd-decoder.profile.ts @@ -17,7 +17,7 @@ const recordBytes = await readFile(index.path) if (hash(recordBytes) !== index.sha256) throw Error("SIMD comparison record changed") const inputRecord = JSON.parse(recordBytes.toString()) if (inputRecord.records.length !== 2 || inputRecord.records[0].variant !== "scalar" || inputRecord.records[1].variant !== "simd") throw Error("Expected exactly the scalar and SIMD comparison") -const commit = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim() +const commit = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8", windowsHide: true }).trim() if (inputRecord.commit !== commit) throw Error("Prepare SIMD fixtures at this exact source commit") const input = await readFile(inputRecord.input.path) if (inputRecord.input.sha256 !== "6d5029641d1a058b5316d4fd49b7ee923ec6490bb5ce93e40fa25ccaa169aad5" || hash(input) !== inputRecord.input.sha256 || input.length !== inputRecord.input.bytes) throw Error("Configured MP3 changed") From f851f146138ad5fbfb967b3df984b831692f097e Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:36:31 +1000 Subject: [PATCH 11/16] Retain rejected browser comparisons and check optimized-tier PCM bytes --- tools/playsrc/profile/simd-decoder.profile.ts | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/tools/playsrc/profile/simd-decoder.profile.ts b/tools/playsrc/profile/simd-decoder.profile.ts index 28afbb5b0..214f4dd35 100644 --- a/tools/playsrc/profile/simd-decoder.profile.ts +++ b/tools/playsrc/profile/simd-decoder.profile.ts @@ -31,7 +31,7 @@ test("headed browser executes exact scalar and SIMD decoder kernels", async ({ p const directory = process.env.PLAYSRC_PROFILE_RUN_DIRECTORY! const native = await startupNativeReader(page, config.sourceCacheDir) guardStartupInput(page, async () => requireStartupNative(await native.read())) - let result: any, pixels: Buffer + let result: any, pixels: Buffer | undefined, failure: string | null = null const admissions: unknown[] = [] try { const admission = await native.read(); requireStartupNative(admission); admissions.push(admission) @@ -53,14 +53,17 @@ test("headed browser executes exact scalar and SIMD decoder kernels", async ({ p const pointer = e.test_input_alloc(input.length) new Uint8Array(e.memory.buffer, pointer, input.length).set(input) const started = performance.now(), count = e.test_decode(pointer, input.length) - return { milliseconds: performance.now() - started, count } + const milliseconds = performance.now() - started + if (count !== 73728) throw Error("Decoded sample count changed") + return { milliseconds, count } } const first = invoke() const pcm = new Uint8Array(e.memory.buffer, e.test_pcm_pointer(), first.count * 2).slice() const pcmSha256 = await digest(pcm) const record = { variant: entry.variant, moduleSha256: await digest(binary), valid, compileMilliseconds: compiled-began, instantiateMilliseconds: instantiated-compiled, firstDecodeMilliseconds: first.milliseconds, samples: first.count, pcmSha256, linearBytes: e.memory.buffer.byteLength, batches: [] as number[] } - records.push(record); states.push({ invoke, record, memory: e.memory as WebAssembly.Memory }) + records.push(record); states.push({ invoke, record, memory: e.memory as WebAssembly.Memory, + pcm: () => new Uint8Array(e.memory.buffer, e.test_pcm_pointer(), first.count * 2).slice() }) if (entry.variant === "simd") { const samples = new Int16Array(pcm.buffer), canvas = document.querySelector("canvas")!, context = canvas.getContext("2d")! context.strokeStyle="#66dfb7"; context.beginPath() @@ -78,7 +81,7 @@ test("headed browser executes exact scalar and SIMD decoder kernels", async ({ p } await new Promise(resolve=>requestAnimationFrame(()=>resolve())) } - for(const state of states){const record=state.record,values=[...record.batches].sort((a,b)=>a-b);Object.assign(record,{median:values[Math.floor(values.length/2)],p95:values[Math.floor(values.length*.95)],maximum:values.at(-1),finalLinearBytes:state.memory.buffer.byteLength})} + for(const state of states){const record=state.record,values=[...record.batches].sort((a,b)=>a-b);Object.assign(record,{median:values[Math.floor(values.length/2)],p95:values[Math.floor(values.length*.95)],maximum:values.at(-1),finalLinearBytes:state.memory.buffer.byteLength,finalPcmSha256:await digest(state.pcm())})} document.querySelector("#result")!.textContent=records.map((r:any)=>`${r.variant}: median ${r.median.toFixed(4)} ms, p95 ${r.p95.toFixed(4)} ms; ${r.samples} exact PCM samples`).join("\n") return { records, sampleMilliseconds:performance.now()-sampleStart, userAgent:navigator.userAgent, platform:navigator.platform, hardwareConcurrency:navigator.hardwareConcurrency, browserEvidence:true, sustainedGameplayEvidence:false, scope:"Configured decoder kernel; host input allocation/copy excluded identically, decoder allocations and output replacement/free included. No gameplay or freeze claim." } @@ -87,12 +90,18 @@ test("headed browser executes exact scalar and SIMD decoder kernels", async ({ p for (const record of result.records) { expect(record.samples).toBe(73728) expect(record.pcmSha256).toBe("b1e43ccf681c3529aad850231599216cfd55778a27bb559b8859917be486ee42") + expect(record.finalPcmSha256).toBe(record.pcmSha256) } pixels = await page.screenshot() - } finally { await native.close() } - await profileArtifact(async () => { - await writeFile(path.join(directory, "simd-decoder.json"), JSON.stringify({ commit, inputRecord, result, admissions }, null, 2)) - await writeFile(path.join(directory, "simd-decoder.png"), pixels) - await testInfo.attach("simd-decoder", { body: pixels, contentType: "image/png" }) - }) + } catch (error) { failure = String(error); throw error } + finally { + await native.close() + await profileArtifact(async () => { + await writeFile(path.join(directory, "simd-decoder.json"), JSON.stringify({ commit, inputRecord, result: result ?? null, admissions, failure }, null, 2)) + if (pixels) { + await writeFile(path.join(directory, "simd-decoder.png"), pixels) + await testInfo.attach("simd-decoder", { body: pixels, contentType: "image/png" }) + } + }) + } }) From 6929fbdc2fa07f1bb785443f4a607ba1d038a89a Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:46:03 +1000 Subject: [PATCH 12/16] Canonicalize the configured decoder fixture index across host runners --- tools/playsrc/profile/simd-decoder.profile.ts | 2 +- tools/playsrc/tests/simd-configured.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/playsrc/profile/simd-decoder.profile.ts b/tools/playsrc/profile/simd-decoder.profile.ts index 214f4dd35..d364c71c4 100644 --- a/tools/playsrc/profile/simd-decoder.profile.ts +++ b/tools/playsrc/profile/simd-decoder.profile.ts @@ -12,7 +12,7 @@ import { loadLocalConfig } from "../src/config" // native browser stage. Build these fixtures with simd-configured.test.ts first. const config = await loadLocalConfig(process.cwd()) const hash = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex") -const index = JSON.parse(await readFile(path.join(config.sourceCacheDir, "simd-tests", hash(Buffer.from(process.cwd())).slice(0, 8), "comparison.json"), "utf8")) +const index = JSON.parse(await readFile(path.join(config.sourceCacheDir, "simd-tests", hash(Buffer.from(path.resolve(process.cwd()))).slice(0, 8), "comparison.json"), "utf8")) const recordBytes = await readFile(index.path) if (hash(recordBytes) !== index.sha256) throw Error("SIMD comparison record changed") const inputRecord = JSON.parse(recordBytes.toString()) diff --git a/tools/playsrc/tests/simd-configured.test.ts b/tools/playsrc/tests/simd-configured.test.ts index 65fabaa78..181240bf9 100644 --- a/tools/playsrc/tests/simd-configured.test.ts +++ b/tools/playsrc/tests/simd-configured.test.ts @@ -56,7 +56,8 @@ test.skipIf(!process.env.PLAYSRC_LOCAL_JOB_OWNER && process.env.RUN_CONFIGURED_S const result = JSON.stringify({ commit, platform: process.platform, arch: process.arch, engine: process.versions, browserEvidence: false, input: { path: path.join(config.sourceCacheDir, "evidence/tf2-wasm-simd-performance/configured/cow1.mp3"), bytes: input.length, sha256: hash(input) }, records }, null, 2) const resultPath = path.join(directory, "result.json") await writeFile(resultPath, result) - const index = path.join(config.sourceCacheDir, "simd-tests", hash(Buffer.from(repositoryRoot)).slice(0, 8), "comparison.json") + const index = path.join(config.sourceCacheDir, "simd-tests", hash(Buffer.from(path.resolve(repositoryRoot))).slice(0, 8), "comparison.json") + await mkdir(path.dirname(index), { recursive: true }) await writeFile(index, JSON.stringify({ path: resultPath, sha256: hash(Buffer.from(result)) })) console.log(`SIMD parity evidence: ${directory}`) } finally { if (!borrowed) await releaseHeadedProfileLock(lockPath, lock.token) } From 8eeb89479fd67373006a59e9e97067b5768607b8 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:09:45 +1000 Subject: [PATCH 13/16] Limit SIMD to the measured audio decoder and check live Float32 delivery --- bun.lock | 9 --- games/tf2/browser/package.json | 3 +- games/tf2/browser/src/wasm-initialization.ts | 6 +- packages/presentation/audio/package.json | 5 +- packages/presentation/audio/src/wasm.ts | 7 +- packages/runtime/wasm/package.json | 6 -- packages/runtime/wasm/src/index.ts | 8 --- .../runtime/wasm/tests/capability.test.ts | 9 --- playwright.simd-profile.config.ts | 2 +- tools/playsrc/local-jobs.md | 5 ++ .../profile/simd-audio-playback.profile.ts | 66 +++++++++++++++++++ tools/playsrc/src/profile-runner.ts | 1 + tools/playsrc/src/tf2-wasm-build.ts | 6 +- tools/playsrc/tests/simd-configured.test.ts | 1 + .../tests/simd-gameplay-preparation.test.ts | 33 ---------- tools/playsrc/tests/tf2-wasm-build.test.ts | 3 +- 16 files changed, 87 insertions(+), 83 deletions(-) delete mode 100644 packages/runtime/wasm/package.json delete mode 100644 packages/runtime/wasm/src/index.ts delete mode 100644 packages/runtime/wasm/tests/capability.test.ts create mode 100644 tools/playsrc/profile/simd-audio-playback.profile.ts delete mode 100644 tools/playsrc/tests/simd-gameplay-preparation.test.ts diff --git a/bun.lock b/bun.lock index b5cab952f..7a1f0248e 100644 --- a/bun.lock +++ b/bun.lock @@ -42,7 +42,6 @@ "@playsrc/rendering": "workspace:*", "@playsrc/settings": "workspace:*", "@playsrc/vgui": "workspace:*", - "@playsrc/wasm": "workspace:*", }, }, "packages/asset-store": { @@ -56,9 +55,6 @@ }, "packages/presentation/audio": { "name": "@playsrc/audio", - "dependencies": { - "@playsrc/wasm": "workspace:*", - }, }, "packages/presentation/particle": { "name": "@playsrc/particle", @@ -76,9 +72,6 @@ "packages/runtime/settings": { "name": "@playsrc/settings", }, - "packages/runtime/wasm": { - "name": "@playsrc/wasm", - }, "tools/playsrc": { "name": "@playsrc/tool", "dependencies": { @@ -333,8 +326,6 @@ "@playsrc/vgui": ["@playsrc/vgui@workspace:packages/presentation/vgui"], - "@playsrc/wasm": ["@playsrc/wasm@workspace:packages/runtime/wasm"], - "@playwright/test": ["@playwright/test@1.61.1", "", { "dependencies": { "playwright": "1.61.1" }, "bin": { "playwright": "cli.js" } }, "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig=="], "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], diff --git a/games/tf2/browser/package.json b/games/tf2/browser/package.json index 41491ee42..044287776 100644 --- a/games/tf2/browser/package.json +++ b/games/tf2/browser/package.json @@ -36,7 +36,6 @@ "@playsrc/particle": "workspace:*", "@playsrc/rendering": "workspace:*", "@playsrc/settings": "workspace:*", - "@playsrc/vgui": "workspace:*", - "@playsrc/wasm": "workspace:*" + "@playsrc/vgui": "workspace:*" } } diff --git a/games/tf2/browser/src/wasm-initialization.ts b/games/tf2/browser/src/wasm-initialization.ts index 25706387c..b404b8ffc 100644 --- a/games/tf2/browser/src/wasm-initialization.ts +++ b/games/tf2/browser/src/wasm-initialization.ts @@ -1,6 +1,4 @@ -import { supportsSimd128 } from "@playsrc/wasm" - -export type WasmInitializationStage = "integrity" | "simd" | "shared-memory" | "instantiate" | "exports" | "thread-pool" | "reply-memory" +export type WasmInitializationStage = "integrity" | "shared-memory" | "instantiate" | "exports" | "thread-pool" | "reply-memory" const ERROR_NAMES = new Set(["Error", "TypeError", "RangeError", "CompileError", "LinkError", "RuntimeError", "SecurityError", "NotSupportedError", "DataCloneError", "AbortError"]) @@ -30,8 +28,6 @@ export async function initializeAuthenticatedWasm value.toString(16).padStart(2, "0")).join("") if (actual !== request.expectedSha256) throw new WasmInitializationError(stage, "SHA256 mismatch") - stage = "simd" - if (!supportsSimd128()) throw new WasmInitializationError(stage, "Standard WebAssembly SIMD128 unavailable") stage = "shared-memory" const SharedMemory = request.sharedArrayBuffer if (!request.isolated || !SharedMemory) throw new WasmInitializationError(stage, `Cross-origin isolated shared memory unavailable (${facts})`) diff --git a/packages/presentation/audio/package.json b/packages/presentation/audio/package.json index 98fe748fa..217a7c9a7 100644 --- a/packages/presentation/audio/package.json +++ b/packages/presentation/audio/package.json @@ -2,8 +2,5 @@ "name": "@playsrc/audio", "private": true, "type": "module", - "exports": "./src/index.ts", - "dependencies": { - "@playsrc/wasm": "workspace:*" - } + "exports": "./src/index.ts" } diff --git a/packages/presentation/audio/src/wasm.ts b/packages/presentation/audio/src/wasm.ts index f07ae3f00..983dcb374 100644 --- a/packages/presentation/audio/src/wasm.ts +++ b/packages/presentation/audio/src/wasm.ts @@ -1,9 +1,12 @@ import { AudioError } from "./error" -import { supportsSimd128 } from "@playsrc/wasm" + +// (module (func (drop (v128.const i32x4 0 0 0 0)))) +// Check standard SIMD without validating the complete audio module twice. +const SIMD128 = new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,23,1,21,0,253,12,...Array(16).fill(0),26,11]) /** Reject an unsupported target before compiling or instantiating audio. */ export function compileAudioModule(bytes: ArrayBuffer): Promise { if (bytes.byteLength > 8 * 1024 * 1024) throw new AudioError("Capacity", "Audio module exceeds its bound") - if (!supportsSimd128()) throw new AudioError("BrowserFailure", "This browser lacks required standard WebAssembly SIMD128 support") + if (typeof WebAssembly === "undefined" || !WebAssembly.validate(SIMD128)) throw new AudioError("BrowserFailure", "This browser lacks required standard WebAssembly SIMD128 support") return WebAssembly.compile(bytes) } diff --git a/packages/runtime/wasm/package.json b/packages/runtime/wasm/package.json deleted file mode 100644 index c6355e350..000000000 --- a/packages/runtime/wasm/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@playsrc/wasm", - "private": true, - "type": "module", - "exports": "./src/index.ts" -} diff --git a/packages/runtime/wasm/src/index.ts b/packages/runtime/wasm/src/index.ts deleted file mode 100644 index b153db4cf..000000000 --- a/packages/runtime/wasm/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// (module (func (drop (v128.const i32x4 0 0 0 0)))) -// Probe standard SIMD before instantiation without validating the entire large -// gameplay module twice. WebAssembly compilation still validates its full body. -const SIMD128 = new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,23,1,21,0,253,12,...Array(16).fill(0),26,11]) - -export function supportsSimd128(): boolean { - return typeof WebAssembly !== "undefined" && WebAssembly.validate(SIMD128) -} diff --git a/packages/runtime/wasm/tests/capability.test.ts b/packages/runtime/wasm/tests/capability.test.ts deleted file mode 100644 index 7e640df58..000000000 --- a/packages/runtime/wasm/tests/capability.test.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { expect, spyOn, test } from "bun:test" -import { supportsSimd128 } from "../src" - -test("standard SIMD admission exercises bytecode, not browser or GPU identity", () => { - expect(supportsSimd128()).toBe(true) - const validate = spyOn(WebAssembly, "validate").mockReturnValue(false) - try { expect(supportsSimd128()).toBe(false); expect(validate).toHaveBeenCalledTimes(1) } - finally { validate.mockRestore() } -}) diff --git a/playwright.simd-profile.config.ts b/playwright.simd-profile.config.ts index 5a8acaa59..16e39fdd1 100644 --- a/playwright.simd-profile.config.ts +++ b/playwright.simd-profile.config.ts @@ -1,2 +1,2 @@ import { headedProfileConfiguration } from "./tools/playsrc/profile/profile-config" -export default headedProfileConfiguration({ match: "simd-decoder.profile.ts" }) +export default headedProfileConfiguration({ match: process.env.PROFILE_SIMD_PLAYBACK === "1" ? "simd-audio-playback.profile.ts" : "simd-decoder.profile.ts" }) diff --git a/tools/playsrc/local-jobs.md b/tools/playsrc/local-jobs.md index 6245a0780..384236960 100644 --- a/tools/playsrc/local-jobs.md +++ b/tools/playsrc/local-jobs.md @@ -27,6 +27,11 @@ Builds/tests open no browser. The browser stage installs only the Chromium and supporting binaries selected by the pinned Playwright package and records the executable hash; it never launches a browser or selects a fallback channel. +`simd-decoder` compares the configured MP3 decoder's scalar reference and SIMD +module in the real headed browser, not gameplay FPS. Prepare its authenticated +fixtures with `test tools/playsrc/tests/simd-configured.test.ts` at the exact +checkout commit, then use `PrepareProfile simd-decoder` and ordinary `Run`. + ## Windows consent and ownership Every Windows `run` above returns a scheduled task identity immediately. The diff --git a/tools/playsrc/profile/simd-audio-playback.profile.ts b/tools/playsrc/profile/simd-audio-playback.profile.ts new file mode 100644 index 000000000..d76b8abc3 --- /dev/null +++ b/tools/playsrc/profile/simd-audio-playback.profile.ts @@ -0,0 +1,66 @@ +import path from "node:path" +import { writeFile } from "node:fs/promises" +import { createHash } from "node:crypto" +import { test, expect, guardStartupInput } from "./application-test" +import { profileArtifact } from "./profile-artifacts" +import { startupNativeReader } from "./native-startup" +import { requireStartupNative } from "./static-startup-gate" +import { chooseTf2Team } from "./team-selection-evidence" +import { loadLocalConfig } from "../src/config" + +test("actual full-roster KOTH preserves Float32 paint-to-device delivery", async ({ page }) => { + const directory = process.env.PLAYSRC_PROFILE_RUN_DIRECTORY!, config = await loadLocalConfig(process.cwd()) + await page.addInitScript(() => { (globalThis as any).__playsrcProfile = {} }) + const native = await startupNativeReader(page, config.sourceCacheDir) + const check = async () => requireStartupNative(await native.read()) + guardStartupInput(page, check) + let result: any, pixels: Buffer | undefined, failure: string | null = null + try { + await check(); await page.goto("/") + const main = page.locator("main") + await expect(main).toHaveAttribute("data-phase", "MainMenu", { timeout: 30_000 }) + await check(); await page.locator(".gameui-layer [data-vgui-name='FindAGameButton']").click() + await check(); await page.locator(".gameui-layer [data-vgui-name='CreateServerEntry'] [data-vgui-name='ModeButton']").click() + const dialog = page.getByRole("dialog", { name: "CREATE SERVER" }) + await check(); await dialog.locator("[data-vgui-name='MapList']").click() + await check(); await page.getByRole("option", { name: "koth_viaduct", exact: true }).click() + await check(); await dialog.getByRole("tab", { name: "GAME" }).click() + await check(); await dialog.locator("[data-vgui-name='GameplayPage'] [data-vgui-name='NumPlayersTextEntry']").fill("23") + await check(); await dialog.getByRole("button", { name: "Start", exact: true }).click() + await expect(main).toHaveAttribute("data-team-selection-visible", "true", { timeout: 45_000 }) + await chooseTf2Team(page, "red", check) + await expect(main).toHaveAttribute("data-phase", "Ready", { timeout: 20_000 }) + await expect(main).toHaveAttribute("data-bot-count", "23", { timeout: 15_000 }) + const readyAt = await page.evaluate(() => performance.now()) + await check(); await page.locator("canvas.world-canvas").click() + await page.waitForFunction(() => document.pointerLockElement?.matches("canvas.world-canvas")) + await page.waitForFunction(() => (globalThis as any).__playsrcProfile.audio?.stats().contextState === "running") + await check() + result = await page.evaluate(async readyAt => { + const p=(globalThis as any).__playsrcProfile, audio=p.audio, root=document.querySelector("main")! + const start=performance.now(), before=audio.stats(), tick=Number(root.dataset.snapshotTick) + const capture=await audio.capture(220500), ended=performance.now(), pcm=new Float32Array(capture.pcm) + let nonzero=0,stereoDifferences=0,peak=0 + for(let i=0;iv.toString(16).padStart(2,"0")).join("") + return { scope:"Short actual gameplay audio correctness, not sustained freeze or FPS acceptance",timeOrigin:performance.timeOrigin,readyAt,start,ended,sinceReadyMilliseconds:start-readyAt, + before,after:audio.stats(),tickBefore:tick,tickAfter:Number(root.dataset.snapshotTick),bots:p.bots.length,phase:root.dataset.phase, + capture:{frames:capture.frames,sampleFormat:capture.sampleFormat,sampleRate:capture.sampleRate,differingSamples:capture.differingSamples,uncoveredSamples:capture.uncoveredSamples,underruns:capture.underruns,bytes:capture.pcm.byteLength,sha256,nonzero,stereoDifferences,peak} } + }, readyAt) + await check() + expect(result.phase).toBe("Ready"); expect(result.bots).toBe(23) + expect(result.tickAfter).toBeGreaterThan(result.tickBefore) + expect(result.capture.sampleFormat).toBe("f32le"); expect(result.capture.sampleRate).toBe(44100) + expect(result.capture.differingSamples).toBe(0); expect(result.capture.uncoveredSamples).toBe(0) + expect(result.capture.underruns).toBe(0); expect(result.capture.nonzero).toBeGreaterThan(1000) + pixels=await page.screenshot() + } catch(error) { failure=String(error); throw error } + finally { + await native.close() + await profileArtifact(async () => { + const image=pixels?{file:"simd-audio-playback.png",bytes:pixels.length,sha256:createHash("sha256").update(pixels).digest("hex")}:null + await writeFile(path.join(directory,"simd-audio-playback.json"),JSON.stringify({result:result??null,failure,image,nativeAdmission:native.records},null,2)) + if(pixels)await writeFile(path.join(directory,"simd-audio-playback.png"),pixels) + }) + } +}) diff --git a/tools/playsrc/src/profile-runner.ts b/tools/playsrc/src/profile-runner.ts index 5a26d1177..b63f77b23 100644 --- a/tools/playsrc/src/profile-runner.ts +++ b/tools/playsrc/src/profile-runner.ts @@ -36,6 +36,7 @@ const PROFILES = Object.freeze({ "browser-input-lifecycle": { config: "playwright.browser-input.config.ts", target: "pl_upward", environment: { PROFILE_INPUT_LIFECYCLE_ONLY: "1" }, minimumRemainingMilliseconds: 75_000 }, "runner-handoff": { config: "playwright.runner-handoff.config.ts", target: "jump_beef" }, "simd-decoder": { config: "playwright.simd-profile.config.ts", target: "koth_viaduct", environment: process.platform === "win32" ? { PLAYSRC_PROFILE_BROWSER_CHANNEL: "msedge" } : {}, minimumRemainingMilliseconds: 40_000 }, + "simd-audio-playback": { config: "playwright.simd-profile.config.ts", target: "koth_viaduct", environment: { PROFILE_SIMD_PLAYBACK: "1", ...(process.platform === "win32" ? { PLAYSRC_PROFILE_BROWSER_CHANNEL: "msedge" } : {}) }, minimumRemainingMilliseconds: 70_000 }, "damage-indicator": { config: "playwright.profile.config.ts", target: "pl_upward", environment: { PROFILE_SCENARIOS: "damage-indicator" }, minimumRemainingMilliseconds: environment => environment.PROFILE_DAMAGE_LIFECYCLE === "1" ? 70_000 : environment.PROFILE_DAMAGE_DYNAMIC === "1" ? 40_000 : DEFAULT_BROWSER_MINIMUM_MILLISECONDS }, "setup-round": { config: "playwright.profile.config.ts", target: "pl_upward", environment: { PROFILE_SCENARIOS: "setup-round" }, minimumRemainingMilliseconds: 140_000 }, "soundscape-selection": { config: "playwright.profile.config.ts", target: "cp_granary", environment: { PROFILE_SCENARIOS: "soundscape-selection" }, minimumRemainingMilliseconds: 60_000 }, diff --git a/tools/playsrc/src/tf2-wasm-build.ts b/tools/playsrc/src/tf2-wasm-build.ts index 1771aa3d8..06c69426d 100644 --- a/tools/playsrc/src/tf2-wasm-build.ts +++ b/tools/playsrc/src/tf2-wasm-build.ts @@ -18,7 +18,7 @@ export class Tf2WasmBuildError extends Error { export function threadedWasmRustFlags(root: string, cargoHome: string, sysroot: string): string[] { return [ - "-Ctarget-feature=+atomics,+bulk-memory,+simd128", + "-Ctarget-feature=+atomics,+bulk-memory", "-Clink-arg=--shared-memory", "-Clink-arg=--max-memory=4294967296", "-Clink-arg=--import-memory", @@ -35,8 +35,8 @@ function wasmSourcePathFlags(root: string, cargoHome: string, sysroot: string): } export function audioWasmRustFlags(root: string, cargoHome: string, sysroot: string): string[] { - // Audio has its own unshared memory; only the deterministic SIMD and source - // remapping contract is common with the threaded gameplay build. + // Audio has its own unshared memory and measured decoder SIMD requirement. + // Gameplay keeps its independently qualified target features. return ["-Ctarget-feature=+simd128", ...wasmSourcePathFlags(root, cargoHome, sysroot)] } diff --git a/tools/playsrc/tests/simd-configured.test.ts b/tools/playsrc/tests/simd-configured.test.ts index 181240bf9..a22ddcab1 100644 --- a/tools/playsrc/tests/simd-configured.test.ts +++ b/tools/playsrc/tests/simd-configured.test.ts @@ -30,6 +30,7 @@ test.skipIf(!process.env.PLAYSRC_LOCAL_JOB_OWNER && process.env.RUN_CONFIGURED_S const hash = (bytes: Uint8Array) => createHash("sha256").update(bytes).digest("hex") try { await run("native", ["test", "--locked", "-p", "playsrc-mp3", "--", "--include-ignored"]) + await run("native-audio", ["test", "--locked", "-p", "playsrc-audio"]) const input = await readFile(path.join(config.sourceCacheDir, "evidence/tf2-wasm-simd-performance/configured/cow1.mp3")) expect(hash(input)).toBe("6d5029641d1a058b5316d4fd49b7ee923ec6490bb5ce93e40fa25ccaa169aad5") const records = [] diff --git a/tools/playsrc/tests/simd-gameplay-preparation.test.ts b/tools/playsrc/tests/simd-gameplay-preparation.test.ts deleted file mode 100644 index 066bb49fa..000000000 --- a/tools/playsrc/tests/simd-gameplay-preparation.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect, test } from "bun:test" -import { mkdir, writeFile } from "node:fs/promises" -import path from "node:path" -import { loadLocalConfig, repositoryRoot } from "../src/config" -import { rustEnvironment } from "../src/setup" -import { acquireHeadedProfileLock, releaseHeadedProfileLock } from "../src/profile-lock" -import { borrowedWindowsJobLock } from "../src/windows-job-native" -import toolchains from "../toolchains.json" - -test.skipIf(!process.env.PLAYSRC_LOCAL_JOB_OWNER && process.env.RUN_CONFIGURED_SIMD_TESTS !== "1")("current configured KOTH and native audio state tests prepare the SIMD comparison", async () => { - const config = await loadLocalConfig() - const lockPath = path.join(config.sourceCacheDir, "evidence/tf2-browser-performance/chromium-profile.lock") - const borrowed = await borrowedWindowsJobLock(lockPath, { testFile: import.meta.filename }) - if (process.platform === "win32" && !borrowed) throw new Error("Windows configured tests require the native local-job supervisor") - const lock = borrowed ?? await acquireHeadedProfileLock(lockPath, "simd-gameplay-preparation", 175_000) - const directory = path.join(config.sourceCacheDir, "evidence/tf2-wasm-simd-performance", `native-state-${process.platform}-${crypto.randomUUID()}`) - await mkdir(directory, { recursive: true }) - try { - const cargo = path.join(config.sourceCacheDir, "toolchains/rust/cargo/bin", process.platform === "win32" ? "cargo.exe" : "cargo") - const cases = [ - ["audio", "-p", "playsrc-audio"], - ["koth", "-p", "playsrc-tf2", "--test", "configured_koth", "viaduct_authored_logic_capture_io_and_generated_clocks", "--", "--ignored"], - ] - for (const [name, ...args] of cases) { - const child = Bun.spawn([cargo, `+${toolchains.rust.threadedToolchain}`, "test", "--locked", ...args], { cwd: repositoryRoot, - env: { ...process.env, ...rustEnvironment(config.sourceCacheDir), CARGO_BUILD_JOBS: "2" }, stdout: "pipe", stderr: "pipe", timeout: 145_000 }) - const [stdout, stderr, code] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]) - await writeFile(path.join(directory, `${name}.log`), stdout + stderr) - expect(code, stderr).toBe(0) - } - console.log(`Native state preparation (not browser or sustained gameplay evidence): ${directory}`) - } finally { if (!borrowed) await releaseHeadedProfileLock(lockPath, lock.token) } -}, 175_000) diff --git a/tools/playsrc/tests/tf2-wasm-build.test.ts b/tools/playsrc/tests/tf2-wasm-build.test.ts index ff059b792..927e7da4c 100644 --- a/tools/playsrc/tests/tf2-wasm-build.test.ts +++ b/tools/playsrc/tests/tf2-wasm-build.test.ts @@ -6,7 +6,8 @@ import { audioWasmRustFlags, threadedWasmRustFlags } from "../src/tf2-wasm-build test("threaded WASM flags retain memory contracts and encode paths with spaces as single arguments", () => { const flags = threadedWasmRustFlags("/build one/app", "/build one/cargo", "/build one/rust") - expect(flags).toContain("-Ctarget-feature=+atomics,+bulk-memory,+simd128") + expect(flags).toContain("-Ctarget-feature=+atomics,+bulk-memory") + expect(flags.some(flag => flag.includes("simd128"))).toBe(false) expect(flags).toContain("-Clink-arg=--shared-memory") expect(flags).toContain("-Clink-arg=--max-memory=4294967296") expect(flags).toContain("--remap-path-prefix=/build one/app=/playsrc") From 03177480f20b75eef5b75f2bdf8b6927e521ac84 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:13:12 +1000 Subject: [PATCH 14/16] Retain every decoder duration and explicit browser sample age --- tools/playsrc/profile/simd-decoder.profile.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tools/playsrc/profile/simd-decoder.profile.ts b/tools/playsrc/profile/simd-decoder.profile.ts index d364c71c4..372a0db60 100644 --- a/tools/playsrc/profile/simd-decoder.profile.ts +++ b/tools/playsrc/profile/simd-decoder.profile.ts @@ -61,7 +61,7 @@ test("headed browser executes exact scalar and SIMD decoder kernels", async ({ p const pcm = new Uint8Array(e.memory.buffer, e.test_pcm_pointer(), first.count * 2).slice() const pcmSha256 = await digest(pcm) const record = { variant: entry.variant, moduleSha256: await digest(binary), valid, compileMilliseconds: compiled-began, instantiateMilliseconds: instantiated-compiled, - firstDecodeMilliseconds: first.milliseconds, samples: first.count, pcmSha256, linearBytes: e.memory.buffer.byteLength, batches: [] as number[] } + firstDecodeMilliseconds: first.milliseconds, samples: first.count, pcmSha256, linearBytes: e.memory.buffer.byteLength, batches: [] as number[], calls: [] as number[] } records.push(record); states.push({ invoke, record, memory: e.memory as WebAssembly.Memory, pcm: () => new Uint8Array(e.memory.buffer, e.test_pcm_pointer(), first.count * 2).slice() }) if (entry.variant === "simd") { @@ -76,15 +76,20 @@ test("headed browser executes exact scalar and SIMD decoder kernels", async ({ p const sampleStart=performance.now(); let round=0 while(performance.now()-sampleStart<5000) { for(const i of round++%2===0?[0,1,1,0]:[1,0,0,1]) { - let elapsed=0;for(let n=0;n<32;n++)elapsed+=states[i]!.invoke().milliseconds + let elapsed=0;for(let n=0;n<32;n++){ + const value=states[i]!.invoke().milliseconds + if(states[i]!.record.calls.length>=65536)throw Error("Decoder sample count exceeded its bound") + states[i]!.record.calls.push(value);elapsed+=value + } states[i]!.record.batches.push(elapsed/32) } await new Promise(resolve=>requestAnimationFrame(()=>resolve())) } - for(const state of states){const record=state.record,values=[...record.batches].sort((a,b)=>a-b);Object.assign(record,{median:values[Math.floor(values.length/2)],p95:values[Math.floor(values.length*.95)],maximum:values.at(-1),finalLinearBytes:state.memory.buffer.byteLength,finalPcmSha256:await digest(state.pcm())})} - document.querySelector("#result")!.textContent=records.map((r:any)=>`${r.variant}: median ${r.median.toFixed(4)} ms, p95 ${r.p95.toFixed(4)} ms; ${r.samples} exact PCM samples`).join("\n") - return { records, sampleMilliseconds:performance.now()-sampleStart, userAgent:navigator.userAgent, platform:navigator.platform, hardwareConcurrency:navigator.hardwareConcurrency, - browserEvidence:true, sustainedGameplayEvidence:false, scope:"Configured decoder kernel; host input allocation/copy excluded identically, decoder allocations and output replacement/free included. No gameplay or freeze claim." } + const sampleEnd=performance.now() + for(const state of states){const record=state.record,values=[...record.batches].sort((a,b)=>a-b),calls=[...record.calls].sort((a,b)=>a-b);Object.assign(record,{median:values[Math.floor(values.length/2)],p95:values[Math.floor(values.length*.95)],maximum:values.at(-1),callMedian:calls[Math.floor(calls.length/2)],callP95:calls[Math.floor(calls.length*.95)],callP99:calls[Math.floor(calls.length*.99)],callMaximum:calls.at(-1),finalLinearBytes:state.memory.buffer.byteLength,finalPcmSha256:await digest(state.pcm())})} + document.querySelector("#result")!.textContent=records.map((r:any)=>`${r.variant}: batch median ${r.median.toFixed(4)} ms/call\n individual calls: p95 ${r.callP95.toFixed(4)} ms, MAX ${r.callMaximum.toFixed(4)} ms`).join("\n") + return { records, timeOrigin:performance.timeOrigin,sampleStartedMilliseconds:sampleStart,sampleEndedMilliseconds:sampleEnd,sampleMilliseconds:sampleEnd-sampleStart, userAgent:navigator.userAgent, platform:navigator.platform, hardwareConcurrency:navigator.hardwareConcurrency, + browserEvidence:true, sustainedGameplayEvidence:false, scope:"Configured decoder kernel; edge checks, first decode and eight warmup decodes precede sampling. Every measured call retained; ABBA batches contain 32 calls. Host input allocation/copy excluded identically, decoder allocations and output replacement/free included. No gameplay or freeze claim." } }, { modules, encoded: input.toString("base64") }) const after = await native.read(); requireStartupNative(after); admissions.push(after) for (const record of result.records) { @@ -97,7 +102,8 @@ test("headed browser executes exact scalar and SIMD decoder kernels", async ({ p finally { await native.close() await profileArtifact(async () => { - await writeFile(path.join(directory, "simd-decoder.json"), JSON.stringify({ commit, inputRecord, result: result ?? null, admissions, failure }, null, 2)) + const image=pixels?{file:"simd-decoder.png",bytes:pixels.length,sha256:hash(pixels)}:null + await writeFile(path.join(directory, "simd-decoder.json"), JSON.stringify({ commit, inputRecord, result: result ?? null, admissions, failure, image }, null, 2)) if (pixels) { await writeFile(path.join(directory, "simd-decoder.png"), pixels) await testInfo.attach("simd-decoder", { body: pixels, contentType: "image/png" }) From ef0fd95a743193f8e3ce38517d151d6b05b1112f Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:26:44 +1000 Subject: [PATCH 15/16] Finish the measured audio SIMD scope without an additional gameplay campaign --- playwright.simd-profile.config.ts | 2 +- .../profile/simd-audio-playback.profile.ts | 66 ------------------- tools/playsrc/src/profile-runner.ts | 1 - 3 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 tools/playsrc/profile/simd-audio-playback.profile.ts diff --git a/playwright.simd-profile.config.ts b/playwright.simd-profile.config.ts index 16e39fdd1..5a8acaa59 100644 --- a/playwright.simd-profile.config.ts +++ b/playwright.simd-profile.config.ts @@ -1,2 +1,2 @@ import { headedProfileConfiguration } from "./tools/playsrc/profile/profile-config" -export default headedProfileConfiguration({ match: process.env.PROFILE_SIMD_PLAYBACK === "1" ? "simd-audio-playback.profile.ts" : "simd-decoder.profile.ts" }) +export default headedProfileConfiguration({ match: "simd-decoder.profile.ts" }) diff --git a/tools/playsrc/profile/simd-audio-playback.profile.ts b/tools/playsrc/profile/simd-audio-playback.profile.ts deleted file mode 100644 index d76b8abc3..000000000 --- a/tools/playsrc/profile/simd-audio-playback.profile.ts +++ /dev/null @@ -1,66 +0,0 @@ -import path from "node:path" -import { writeFile } from "node:fs/promises" -import { createHash } from "node:crypto" -import { test, expect, guardStartupInput } from "./application-test" -import { profileArtifact } from "./profile-artifacts" -import { startupNativeReader } from "./native-startup" -import { requireStartupNative } from "./static-startup-gate" -import { chooseTf2Team } from "./team-selection-evidence" -import { loadLocalConfig } from "../src/config" - -test("actual full-roster KOTH preserves Float32 paint-to-device delivery", async ({ page }) => { - const directory = process.env.PLAYSRC_PROFILE_RUN_DIRECTORY!, config = await loadLocalConfig(process.cwd()) - await page.addInitScript(() => { (globalThis as any).__playsrcProfile = {} }) - const native = await startupNativeReader(page, config.sourceCacheDir) - const check = async () => requireStartupNative(await native.read()) - guardStartupInput(page, check) - let result: any, pixels: Buffer | undefined, failure: string | null = null - try { - await check(); await page.goto("/") - const main = page.locator("main") - await expect(main).toHaveAttribute("data-phase", "MainMenu", { timeout: 30_000 }) - await check(); await page.locator(".gameui-layer [data-vgui-name='FindAGameButton']").click() - await check(); await page.locator(".gameui-layer [data-vgui-name='CreateServerEntry'] [data-vgui-name='ModeButton']").click() - const dialog = page.getByRole("dialog", { name: "CREATE SERVER" }) - await check(); await dialog.locator("[data-vgui-name='MapList']").click() - await check(); await page.getByRole("option", { name: "koth_viaduct", exact: true }).click() - await check(); await dialog.getByRole("tab", { name: "GAME" }).click() - await check(); await dialog.locator("[data-vgui-name='GameplayPage'] [data-vgui-name='NumPlayersTextEntry']").fill("23") - await check(); await dialog.getByRole("button", { name: "Start", exact: true }).click() - await expect(main).toHaveAttribute("data-team-selection-visible", "true", { timeout: 45_000 }) - await chooseTf2Team(page, "red", check) - await expect(main).toHaveAttribute("data-phase", "Ready", { timeout: 20_000 }) - await expect(main).toHaveAttribute("data-bot-count", "23", { timeout: 15_000 }) - const readyAt = await page.evaluate(() => performance.now()) - await check(); await page.locator("canvas.world-canvas").click() - await page.waitForFunction(() => document.pointerLockElement?.matches("canvas.world-canvas")) - await page.waitForFunction(() => (globalThis as any).__playsrcProfile.audio?.stats().contextState === "running") - await check() - result = await page.evaluate(async readyAt => { - const p=(globalThis as any).__playsrcProfile, audio=p.audio, root=document.querySelector("main")! - const start=performance.now(), before=audio.stats(), tick=Number(root.dataset.snapshotTick) - const capture=await audio.capture(220500), ended=performance.now(), pcm=new Float32Array(capture.pcm) - let nonzero=0,stereoDifferences=0,peak=0 - for(let i=0;iv.toString(16).padStart(2,"0")).join("") - return { scope:"Short actual gameplay audio correctness, not sustained freeze or FPS acceptance",timeOrigin:performance.timeOrigin,readyAt,start,ended,sinceReadyMilliseconds:start-readyAt, - before,after:audio.stats(),tickBefore:tick,tickAfter:Number(root.dataset.snapshotTick),bots:p.bots.length,phase:root.dataset.phase, - capture:{frames:capture.frames,sampleFormat:capture.sampleFormat,sampleRate:capture.sampleRate,differingSamples:capture.differingSamples,uncoveredSamples:capture.uncoveredSamples,underruns:capture.underruns,bytes:capture.pcm.byteLength,sha256,nonzero,stereoDifferences,peak} } - }, readyAt) - await check() - expect(result.phase).toBe("Ready"); expect(result.bots).toBe(23) - expect(result.tickAfter).toBeGreaterThan(result.tickBefore) - expect(result.capture.sampleFormat).toBe("f32le"); expect(result.capture.sampleRate).toBe(44100) - expect(result.capture.differingSamples).toBe(0); expect(result.capture.uncoveredSamples).toBe(0) - expect(result.capture.underruns).toBe(0); expect(result.capture.nonzero).toBeGreaterThan(1000) - pixels=await page.screenshot() - } catch(error) { failure=String(error); throw error } - finally { - await native.close() - await profileArtifact(async () => { - const image=pixels?{file:"simd-audio-playback.png",bytes:pixels.length,sha256:createHash("sha256").update(pixels).digest("hex")}:null - await writeFile(path.join(directory,"simd-audio-playback.json"),JSON.stringify({result:result??null,failure,image,nativeAdmission:native.records},null,2)) - if(pixels)await writeFile(path.join(directory,"simd-audio-playback.png"),pixels) - }) - } -}) diff --git a/tools/playsrc/src/profile-runner.ts b/tools/playsrc/src/profile-runner.ts index b63f77b23..5a26d1177 100644 --- a/tools/playsrc/src/profile-runner.ts +++ b/tools/playsrc/src/profile-runner.ts @@ -36,7 +36,6 @@ const PROFILES = Object.freeze({ "browser-input-lifecycle": { config: "playwright.browser-input.config.ts", target: "pl_upward", environment: { PROFILE_INPUT_LIFECYCLE_ONLY: "1" }, minimumRemainingMilliseconds: 75_000 }, "runner-handoff": { config: "playwright.runner-handoff.config.ts", target: "jump_beef" }, "simd-decoder": { config: "playwright.simd-profile.config.ts", target: "koth_viaduct", environment: process.platform === "win32" ? { PLAYSRC_PROFILE_BROWSER_CHANNEL: "msedge" } : {}, minimumRemainingMilliseconds: 40_000 }, - "simd-audio-playback": { config: "playwright.simd-profile.config.ts", target: "koth_viaduct", environment: { PROFILE_SIMD_PLAYBACK: "1", ...(process.platform === "win32" ? { PLAYSRC_PROFILE_BROWSER_CHANNEL: "msedge" } : {}) }, minimumRemainingMilliseconds: 70_000 }, "damage-indicator": { config: "playwright.profile.config.ts", target: "pl_upward", environment: { PROFILE_SCENARIOS: "damage-indicator" }, minimumRemainingMilliseconds: environment => environment.PROFILE_DAMAGE_LIFECYCLE === "1" ? 70_000 : environment.PROFILE_DAMAGE_DYNAMIC === "1" ? 40_000 : DEFAULT_BROWSER_MINIMUM_MILLISECONDS }, "setup-round": { config: "playwright.profile.config.ts", target: "pl_upward", environment: { PROFILE_SCENARIOS: "setup-round" }, minimumRemainingMilliseconds: 140_000 }, "soundscape-selection": { config: "playwright.profile.config.ts", target: "cp_granary", environment: { PROFILE_SCENARIOS: "soundscape-selection" }, minimumRemainingMilliseconds: 60_000 }, From 98896f2e88baa1cd2193d85464a2c44cc187414a Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:28:52 +1000 Subject: [PATCH 16/16] Await audio module rejection before restoring capability spies --- packages/presentation/audio/tests/wasm.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/presentation/audio/tests/wasm.test.ts b/packages/presentation/audio/tests/wasm.test.ts index 8e6977435..3d16e3786 100644 --- a/packages/presentation/audio/tests/wasm.test.ts +++ b/packages/presentation/audio/tests/wasm.test.ts @@ -7,7 +7,7 @@ const simd = new Uint8Array([0,97,115,109,1,0,0,0,1,4,1,96,0,0,3,2,1,0,10,23,1,2 test("audio validates actual SIMD bytecode before compiling, independently of GPU or browser identity", async () => { expect(WebAssembly.validate(simd)).toBe(true) expect(await compileAudioModule(simd)).toBeInstanceOf(WebAssembly.Module) - expect(compileAudioModule(new ArrayBuffer(0))).rejects.toBeInstanceOf(WebAssembly.CompileError) + await expect(compileAudioModule(new ArrayBuffer(0))).rejects.toBeInstanceOf(WebAssembly.CompileError) expect(() => compileAudioModule(new ArrayBuffer(8 * 1024 * 1024 + 1))).toThrow("exceeds its bound") })