Skip to content

The kernel's layouts in its source: accessors, WESL blocks and constants, the loop guard on the syntax tree - #71

Open
yujiaxu-ops wants to merge 1 commit into
mainfrom
kernel-tables
Open

yujiaxu-ops wants to merge 1 commit into
mainfrom
kernel-tables

Conversation

@yujiaxu-ops

@yujiaxu-ops yujiaxu-ops commented Nov 13, 2026 •

Copy link
Copy Markdown
Collaborator

The browser's merged binding layout, Android's uniform tables and the loop guard were made by rewriting the kernel's text: merged.rs scanned the WGSL for every use of thirteen buffer names, followed the locals that shadowed them through their scopes, rewrote each use (MT.materials[, min(u32(i), last)), and put a counter after the { of every loop it found by its keyword. An edit of the kernel could break the rewrite at run time, and nothing in the source said a layout existed. This puts the layouts in the source.

What changes

  • Accessors. The kernel reads its seven tables and uses its six group-1 buffers only through functions: material_at, light_at, emitter_at, tex_info_at, object_at, instance_at, medium_at; cache_key_load / _claim / _clear, cache_acc_add / _load / _clear, cache_val_load / _store, refl_store, motion_store, direct_load / _store, *_len. That is 87 reads of the tables and 40 uses of group 1.
  • Each layout is a block of declarations in pathtrace.wgsl, declaring the buffers and those functions: @if(merged) { .. } @else { @if(uniform_tables) { .. } @else { .. } }. These are WESL's compound declarations (the spec's @if / @else blocks). The merged block declares none of the buffers by name, so a read past the accessors does not compile there, and the tests read the merged variants with naga.
  • Constants. The numbers a variant needs are WESL constants that the source imports (import constants::..): the merged arrays' lengths, the uniform arrays' lengths, and the loop guard's limit. gpu::KernelVariant gives a variant's 15 flags and its constants. kernel_wgsl compiles through a small resolver of our own: the kernel module plus the constants package.
  • The loop guard is a pass over the variant's syntax tree (gpu::guard_loops): if (loop_guard()) { break; } goes at the start of every loop / for / while body, including the scene's functions. The guard's declarations are in the source under @if(guard). (I had planned to write the guard into the kernel's loops by hand. It has to bound the scene's functions' loops too, so a pass over the tree is the one place that covers both.)
  • merged.rs drops its scanner, its rewriter, the uniform tables' rewrite, the guard's text pass and their tests (637 → 302 lines). The numbers the Rust side needs for its buffers stay. What is still text: a scene's functions in their slots.

Checks

  • cargo test --release with the loop guard on (GPU tests included): 125 passed. That is main's 126, less the rewriter's three tests, plus two for the constants.

  • render-identity against main, 190 outputs each. The four guarded runs come first, because a changed kernel and the software traversal run guarded before anything else:

    run byte-identical within run-to-run spread changed
    native + guard 175 15 0
    merged + guard 172 18 0
    uniform tables + guard 174 16 0
    every switch on + guard 175 15 0
    native, no guard 172 18 0

    The guarded runs show that the tree pass bounds the same loops as the text scan did (52 in the merged kernel either way).

  • tools/web-hello.ts in Chrome: pass. The page runs with eight storage buffers a stage, the guard at 8192, and the guard stopped nothing in 826 frames.

  • Cost: per-pass profile at 1440p, render scale 0.375. Runs alternate between main and this branch, in both orders, three prints a run.

    • Native hardware traversal: no cost. Bistro trace 8.28–8.97 ms against main's 8.30–8.96 ms; Kabukichō 15.46–16.17 ms against 15.53–16.25 ms.
    • Software traversal in the native layout: 0.2 ms faster (46.8 against 47.0 ms).
    • Software traversal in the merged layout: 0.2 ms slower (47.3 against 47.1 ms).

    I compared the Metal source naga writes for main's merged kernel and for this one, function by function. With naga's names normalized, the two mean the same thing. Two experiments did not move the merged +0.2: accessors that read one field, and declarations placed where main had them (main's parameter order). The sign flips with the layout, so I read it as the Metal compiler arranging the code around the accessors' boundaries differently (±0.5 %), not a cost of the structure.

docs/RUNTIME.md ("The engine in a browser", "The kernel's source", the Android uniform tables) and a ROADMAP record are updated.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how every browser/Android kernel variant is compiled and instrumented (merged binding layout, uniform table clamps, loop guard), though tests claim render-identical output and unchanged traversal cost within noise.

Overview
Moves browser merged layouts, Android uniform tables, and the loop guard out of merged.rs text rewriting and into the kernel WGSL (pathtrace.wgsl), with WESL @if(merged|uniform_tables|guard) blocks, accessor functions for scene tables and group-1 live buffers, and import constants:: for array lengths and guard limits.

Rust compilation path: replaces kernel_flags / string patches with KernelVariant (flags + constants), wesl_wgsl + a small constants resolver, and guard_loops (syntax-tree instrumentation) instead of merged::guard. Shader call sites switch from direct buffer indexing to material_at, instance_at, cache_acc_add, refl_store, etc.

merged.rs shrinks to layout metadata, offsets, and naga validation tests; the WGSL scanner/rewriter (~637 lines) is removed. Docs (RUNTIME.md, ROADMAP.md) describe the new model.

Reviewed by Cursor Bugbot for commit 94e7ffc. Bugbot is set up for automated code reviews on this repo. Configure here.

…ors, WESL blocks and constants, the loop guard on the syntax tree

The browser's merged layout, Android's uniform tables and the loop guard were made by rewriting the kernel's text:
merged.rs scanned the WGSL for every use of thirteen buffer names, followed shadowing locals through their scopes,
rewrote each use, and put a counter after the brace of every loop it found by keyword.

- The kernel reads its seven tables and uses its six group-1 buffers only through functions (material_at, ...,
  cache_key_claim, cache_acc_add, refl_store, direct_len, ...): 87 table reads, 40 group-1 uses.
- Each layout is a block of declarations in pathtrace.wgsl declaring the buffers and those functions:
  @if(merged) { .. } @else { @if(uniform_tables) { .. } @else { .. } } (WESL compound declarations).
- The numbers a variant needs are WESL constants the source imports (import constants::..): the merged arrays'
  lengths, the uniform arrays', the loop guard's limit. gpu::KernelVariant gives a variant's flags and constants;
  kernel_wgsl resolves the kernel module and the constants package.
- The loop guard's count goes into every loop, the scene's functions' too, by a pass over the variant's syntax tree
  (gpu::guard_loops); its declarations are in the source under @if(guard).
- merged.rs loses its scanner, its rewriter, the uniform tables' rewrite, the guard's text pass and their tests
  (637 -> 302 lines); Member keeps only what the Rust side's buffers need.

Checks: render-identity against main with the guard (native, merged, uniform tables, every switch on) and native
without it: 0 of 190 changed; web-hello in Chrome passes; native hardware traversal costs the same (Bistro, Kabukichō),
software traversal -0.2 ms as written and +0.2 ms merged (the compiler's arrangement; the Metal source is the same in
meaning).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant