# CLAUDE.md Tips for future agents working in this repo. ## Architecture ``` wasm/matmul.c naive + blocked/SIMD (f64x2) kernels, plus a pthreads matmul_blocked_mt (rows split across threads, -DMATMUL_MT). wasm/build-wasm.sh (emsdk) builds it twice: - st ES module -> wasm/dist/matmul.{js,wasm} - threaded -> public/matmul/matmul_mt.{js,wasm} blis/matmul_blis.c thin wrapper over libFLAME's dgemm_ (BLIS-backed); built st + mt (pthreads) by blis/build-blis-wasm.sh into public/matmul/matmul_blis_{st,mt}.{js,wasm}. It links the WASM .a libraries from a sibling concept-collection/ libflame2wasm checkout (LIBFLAME=..., default ../../../libflame2wasm), which are built by that repo's build-all-wasm.sh. NOT committed — CI builds them. native/bench_native.c same style dgemm benchmark, linked against OpenBLAS, run standalone outside the browser src/methods/ one file per method, all implementing the MatmulMethod interface in types.ts (id, precision, available(), run?()) src/methods/benchWorker.ts + workerClient.ts JS and both non-threaded custom WASM kernels run inside a Vite module Web Worker — naive JS/WASM at n=2048 take ~10s and would freeze the tab on the main thread. WebGPU runs on the main thread (async). The worker regenerates inputs from (n, seed) rather than receiving them over postMessage. src/methods/threadedClient.ts + public/matmul/worker.js ALL pthread-capable modules (threaded custom C, BLIS st + mt) run in ONE shared classic worker (not a Vite module worker) — threaded builds spawn pthread workers from it (nested workers), and importScripts of the emscripten glue is the proven path (mirrors libflame2wasm's bench_worker.js). worker.js has a kind->module REGISTRY; every module exports a (a,b,c,n,nthreads) function (st builds ignore nthreads). Served verbatim from public/, out of Vite's module graph. src/components/ BenchmarkRunner (orchestrates runs, thread selector) + ResultsTable src/data/nativeReference.ts hardcoded numbers from native/bench_native, hand-copied in — no runtime fetch public/coi-serviceworker.js vendored (gzuidhof, MIT) — adds COOP/COEP so SharedArrayBuffer works on GitHub Pages (no header control) ``` ## Key gotchas - **WASM .wasm URL resolution.** `wasm/dist/matmul.js` (emscripten glue) is imported directly; its sibling `matmul.wasm` is resolved via Vite's `?url` import and passed through `locateFile` — same pattern qhull-wasm-demo uses for `qhull-wasm/dist/qhull.wasm`. Don't rely on emscripten's default `import.meta.url`-relative lookup working under Vite's bundler. - **Two threaded builds, one classic worker.** The threaded custom kernel (`matmul_mt`, from `wasm/matmul.c` with `-pthread -DMATMUL_MT`) and the threaded BLIS build (`matmul_blis_mt`) both use pthreads/SharedArrayBuffer and both dispatch through `public/matmul/worker.js`. The non-threaded custom kernels (`matmul.c` st build) have no `-pthread` and run in the Vite module worker instead. - **Cross-origin isolation for threaded methods.** `matmul_mt` and `matmul_blis_mt` need `crossOriginIsolated === true` (COOP: same-origin, COEP: require-corp). Vite dev/preview set these headers (see vite.config.ts). GitHub Pages can't set headers, so `public/coi-serviceworker.js` installs them via a service worker (page reloads once on first visit to gain control). Their `available()` gates on `crossOriginIsolated`; without it the cell = n/a. - **Everything under public/matmul/ except worker.js is CI-built** (gitignored). The threaded custom module (`matmul_mt.*`) needs only `matmul.c` + emsdk. The BLIS modules (`matmul_blis_*.*`) need the WASM libflame/BLIS `.a` files, which CI produces by cloning the sibling **concept-collection/libflame2wasm** repo and running its `build-all-wasm.sh` (result cached, keyed on that repo's SHA — see `.github/workflows/deploy.yml`). `worker.js` is the only committed file there. Locally: check out libflame2wasm beside this repo, run its `build-all-wasm.sh` once, then `blis/build-blis-wasm.sh`. - **Row-major via operand swap.** dgemm_ is column-major; `matmul_blis` calls it with operands swapped — `dgemm_(B, A)` computes row-major C = A*B in the same flat buffer — so BLIS `c[0]` matches the other (row-major) methods. - **Deterministic seeded inputs, not transferred arrays.** Every method for a given `n` multiplies bit-identical A/B (from `generateMatrix(n, seed)`); the worker regenerates them from the same seed rather than receiving the arrays over `postMessage`, since structured-cloning multi-megabyte `Float64Array`s per run would be slower than regenerating. - Rebuilding `wasm/dist/*` requires emsdk on PATH or at `~/emsdk` (`wasm/build-wasm.sh` sources `~/emsdk/emsdk_env.sh` if `emcc` isn't found). CI installs it via `mymindstorm/setup-emsdk`. ## Testing - `npm run dev`, click through each method's run button at a small size (n=128) first and check the cross-check panel — the f64 methods (JS, the three WASM kernels, both BLIS builds) should agree to ~1e-9, WebGPU (f32) should be close but not identical. The "cross-origin isolated ✓" chip must be green for the threaded methods to be available. - `wasm/build-wasm.sh && npm run build` to verify the CI path locally (build-wasm.sh emits both the st module and the threaded matmul_mt). - `blis/build-blis-wasm.sh` rebuilds the BLIS modules (needs emsdk + a sibling libflame2wasm checkout already built via its `build-all-wasm.sh`; override the location with `LIBFLAME=/path/to/libflame2wasm`). - `native/build.sh && native/bench_native` regenerates the values pasted into `src/data/nativeReference.ts` (no automated round-trip — hand-copy after running).