Conversation
Explores a minimal wasm module for batched mat4 work. Not wired into src. The library's Mat4 is a plain JS array of doubles, so any wasm path has to marshal into linear memory first. Measured at N=4096, resident buffers give 3.84x over the current JS multiply while marshalling plain arrays in and out gives 0.44x, so batch only APIs over caller resident memory are the only shape that pays. Also measures that flat layout alone accounts for 1.4x and SIMD a further 3.0x, which rules out shipping a scalar wasm fallback. Includes a build script, the committed 1226 byte module so the bench runs without clang, and a README covering language choice and three.js integration points.
…llocator The skill already puts wasm crossing state in a typed array from the start, so residency is the house pattern rather than an obstacle. Measuring that properly found three tiers, not two: element-wise marshalling is 0.44x, a caller's own Float32Array copied in bulk is 1.92x, and a view onto wasm memory is 3.24x. The bulk memcpy costs about 70% on top of the kernel, so accepting a foreign Float32Array roughly halves the win. Since a view onto wasm memory measures the same as an ordinary Float32Array for JS to read and write, the module should be the allocator and callers should source long lived buffers from it. That also forces a no grow rule, since memory.grow detaches every held view, which matches the skill's preallocate to capacity guidance.
Assumes a flat array of matrices throughout, one contiguous Float32Array of 16 floats per matrix, which is what instanceMatrix and boneMatrices already are. Measured AoS against an SoA kernel that needs no lane broadcasts, to check whether the layout GPU interop forces is costing anything. It is not. SoA is 5.9x slower, because one AoS matrix is exactly one cache line and a multiply touches three contiguously, where SoA runs 48 concurrent streams over 768KB and thrashes L2. Tree reduction and a 2x unroll measure neutral and slower respectively, so the loop stays simple. Switched the variant harness to min of trials after a single timed run put the tree reduction anywhere from 0.97x to 1.15x. Adds mat4.mjs, a 5.4KB single file with the wasm inlined as base64 and no fetch or async, taking Float32Array views and deriving pointers from byteOffset so a non resident buffer is rejected rather than misread.
Refocuses the spike on world[i] = world[parent[i]] * local[i] and drops the pairwise batch, compose and broadcast kernels along with the three.js notes. Measures 4.67x over the library's current plain array representation on a 4-ary tree, flat in cost per node from 1K to 262K nodes, so the parent gather does not become a scaling problem. The kernel issues about 56 uops per node and runs at roughly 4 uops per cycle, so it is issue bound. That model predicts the results: relaxed simd fma removes 12 of those uops and gains the predicted 1.2x, while sibling grouping, restrict, unrolling and -O3 all measure neutral or worse. Sibling grouping was the most promising idea and it fails, at 1.01x on the shape it was designed for and 21% slower on short runs. Fma gain tracks the same model, 1.16x to 1.28x on bushy trees but only 1.04x on chains, which are latency bound on the parent dependency instead. Also corrects the earlier precision note. Drift stays near f32 epsilon down a 255 deep chain of realistic transforms, since rotations are well conditioned. The earlier 1e-3 figure came from random matrices, which are not transforms.
…ed loop Moves the head to head onto @pmndrs/labs, which isolates each bench in its own worker, interleaves their blocks, detects dead code elimination and reports machine stability. All five comparators live in one file so the interleaving cancels drift between them, which matters on this container. Uses the real mat4.multiply from src for the plain array baseline rather than a transcription, and checks every kernel against an f64 reference. Checksums go through the snapshot hook so they stay out of the timed region. Timing them was making the wasm benches look several times slower than the JS ones, since 65536 element reads swamp the kernel. Adds a scalar twin of the kernel as a diagnostic, loaded from disk rather than inlined, so the flat layout can be priced separately from SIMD.
The kernel took raw parent indices and trusted them. An index past capacity but still inside wasm memory read a neighbouring slot and returned wrong values with no error, and update(count) with count above capacity wrote over the parent array before eventually trapping. Both are now impossible. One unsigned compare, p >= (unsigned)i, rejects a root, a forward reference and an out of range index together, and treats each as a root, so 0 <= p < i < n always holds and no read leaves the buffer. The module is the same 394 bytes. createTree rejects a capacity that is not a non negative integer, or larger than a 4GiB wasm memory holds, and reports a failed grow. update and validate reject a count outside [0, capacity], and a rejected update leaves the tree untouched. Adds embed.mjs, which regenerates the base64 inlined in tree.mjs from the .wasm files and verifies them with --check, so the two cannot drift. build.sh runs it. Adds 14 tests covering propagation against the f64 mat4.multiply reference, the malformed tree guards, and the validation.
The committed readme carried several claims that better measurement did not support. Corrected against seven labs runs: - the speedup is about 4.0x over the plain array path, not 4.67x - plain arrays and a flat Float32Array measure the same in JS, so the earlier claim that plain is 1.4x faster was a harness artifact - the split is roughly 1.2x from the flat layout and 3.2x from SIMD, not the 0.95x and 4.57x an ad hoc harness reported - the relaxed simd gain measures 1.05x to 1.14x against a +-7.5% comparison resolution, so it is unproven here rather than a 1.2x win Drops the issue bound argument entirely. It assumed a 3GHz clock that was never checked, and labs reports the machine at 2.05GHz, where the uop count it rested on would need more issue width than the core has. The empirical results it was offered to explain stand on their own. Says plainly that the machine is unstable, what the noise floor is, and which results sit outside it. Marks the readme's remaining small effects as measured with the earlier harness. Removes mat4.mjs and bench.mjs. The first is the superseded batch API from before the scope narrowed to trees, unsafe and untested, and the second is the hand rolled harness that benches/wasm/tree.bench.ts replaced.
Comments. The relaxed madd comment still asserted the kernel was issue bound and that fma was the only lever, which the labs measurements did not support. It now describes what the macro does and why the build is opt in, with no rationale it cannot back. The bench comment about the snapshot described a change rather than the mechanism, so it describes the mechanism. Consts. MAX_CAPACITY and F32_TOLERANCE were module scope and each used in one place, so both are inlined where they are read, the capacity bound beside the guard it belongs to and the tolerance beside the comparison it governs. N in the bench stays, matching transform-hierarchy.bench.ts, and DEPTH stays in the precision script where it reads through six uses. Tests. Trimmed from 14 to 11 by folding the thin assertions into the cases that already made them and dropping the view shape test, which duplicated what the propagation tests exercise and came closest to testing internals. What is left documents the feature and keeps every guard against the out of bounds paths. Also adds tree.d.mts and scalar.d.mts. Both tsconfigs are strict and cover the test and bench that import these, so without declarations they resolved to any.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A spike exploring a minimal wasm module for
world[i] = world[parent[i]] * local[i]over a parent/child graph. Everything lives inspikes/wasm-mat4/, plus a labs bench and tests. Nothing is wired intosrc/.Not for merge as-is. Opened to review the design and the numbers.
Result
~4.0x over the current plain-array path, from
benches/wasm/tree.bench.tsat N=4096 over a 4-ary tree:The flat layout is worth ~1.2x and SIMD ~3.2x on top. Non-SIMD wasm barely beats JS, so the fallback for engines without SIMD should be the existing JS rather than a second binary.
Design
restrict, 2x unrolling and-O3all measured neutral or worse.-Ozmatches-O3at half the size.WebAssembly.Modulecompiles synchronously on the main thread and nothing in the API is async.tree.wasmis 394 bytes;tree.mjsinlines both kernels as base64, so there is no fetch or bundler plugin.Float32Arrayview onto wasm memory measures the same as one on its ownArrayBuffer, and memory is sized once becausememory.growdetaches every view handed out.Safety
The kernel treats caller indices as untrusted. One unsigned compare
p >= (unsigned)irejects a root, a forward reference and an out-of-range index together, all treated as roots, so0 <= p < i < nalways holds and no read leaves the buffer. Costs no bytes and no measurable time. 11 tests cover propagation against the f64mat4.multiplyreference, the malformed-tree guards and the bounds checks.Caveats
.wasmfiles are committed.embed.mjs --checkguards the inlined base64 against drift, but nothing rebuilds them in CI yet.Open questions
math/wasmentrypoint, or a separate package so core stays artifact-free?🤖 Generated with Claude Code
https://claude.ai/code/session_01BWQBg51kiyNdHJT3WLfwZj
Generated by Claude Code