Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
- run: node tools/npm-pack.ts

renderer:
name: "renderer: release build"
name: "renderer: release build, unit tests"
needs: changes
if: needs.changes.outputs.renderer == 'true'
runs-on: ubuntu-26.04
Expand All @@ -96,6 +96,10 @@ jobs:
with:
workspaces: renderer
- run: cd renderer && cargo build --release --locked
# The renderer's unit tests (the merge gate's `cargo test --release`). The runner has no GPU: the few tests that
# open a device say "skipped" and return, the rest (scene, world, script, voxel, physics, the merged layout's
# naga checks, the kernel's source switches) run as on a Mac.
- run: cd renderer && cargo test --release --locked
# The voxel bodies and the physics on the CPU, through the engine's own path (ROADMAP § 6 step 8), on the
# physics' own threads as the renderer steps (src/physics.rs `in_pool`; tools/physics-determinism.ts steps from
# outside any pool, which is not that path): the recorded hashes at 2 and 4 threads (native parallel = native
Expand Down
3 changes: 3 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4835,6 +4835,9 @@ benchmark level. The order follows the dependencies; a step that meets a further
Basis: the scene is one plane with the library's facade windows and no instance, so nothing of step 7b is in
its kernel (the source is main's byte for byte), and main's build reads the same 0.3934 in both layouts. The
cause was not looked for.
- 2026-11-13 (main `8746130`, 190 tests): 24 fail, the 23 above and `wind/pivot-sway-live`, which reads 9.241 for
10 (its offline twin `wind/pivot-sway` passes; hardware traversal passes both). The same 24 in the merged layout.
Not in any gate: CI runs no render test, and the merge gate runs them on hardware traversal only.
- Offline renders of `bistro`, `bistro-slice`, `bistro-district`, `zero-day` and `zero-day-animated` are not the same
bytes twice under hardware traversal (measured 2026-11-11 in § 6 step 7b's merge gate; the last entry here has
Bistro's ties between coincident surfaces at 2–4 %). Main `a707e1c` against itself, 320 × 180, 4 spp: 1,782 /
Expand Down
16 changes: 8 additions & 8 deletions docs/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,19 +688,19 @@ tree (ROADMAP § 6).
the node host as `GameSession` does, and sends the canvas' clicks down the same path. `node tools/build-web.ts
[game.ts] --serve` builds and serves it (needs the wasm-bindgen CLI of the crate's version).
- **The merged binding layout** (`renderer/src/merged.rs`). WebGPU gives a stage 8 storage buffers by default; the
kernel binds 20 and the live passes 28. There is one source: the browser's shaders are written from
kernel binds 19 and the live passes 24. There is one source: the browser's shaders are written from
`pathtrace.wgsl` and `live.wgsl` as text when a module is compiled, and the native ones are untouched.
- The kernel's seven tables become the arrays of one struct in one buffer; their lengths are in the source, each a
power of two above a floor, so a kernel is compiled again only when a table doubles. The renderer keeps writing
its own buffers and a frame copies them into the struct's.
- The kernel's seven group-1 buffers (radiance cache, reservoirs, what the live passes read of a trace) are ranges
of one buffer, the arrays of a second struct.
- The live passes' buffers of one element type are ranges of a slab each, indexed from offsets in a uniform: the
history's two bind groups are two sets of offsets.
- The kernel's six group-1 buffers (the radiance cache, what the live passes read of a trace) are ranges of one
buffer, the arrays of a second struct.
- The live passes' buffers of one element type are ranges of a slab each, indexed from offsets in a uniform. A
lasting buffer's two halves (this frame's and the last, `LiveParams.state`) lie in its range, as natively.
- A use of a merged name the generator does not know, a declaration not found exactly once, a name it would add
that is taken: each stops the renderer, with the line. Locals that shadow a name are followed through their
scopes. A test reads the result with naga: valid WGSL, 8 storage buffers for the kernel and for the live passes
(20 and 28 as written), the structs' arrays where the Rust side puts them.
(19 and 24 as written), the structs' arrays where the Rust side puts them.
- `THREERT_MERGED=1` runs it natively (software traversal, the device held to 8).
- **The loop guard** (`merged::guard`). The kernel's traversals follow indices they read from buffers and end only on
sound data. On 2026-11-11 two dispatches that did not return took this machine's display down (WindowServer's
Expand Down Expand Up @@ -1205,10 +1205,10 @@ on the device", and differs where it says so.
| `emitters` (6) | the emitter CDF and the light tree | storage: megabytes in a lit scene |
| `env_cdf` (10) | the environment's CDF, then the procedural geometry's data | storage (megabytes); its light sampling is off on Android, below |
| `tri_verts` (1), `attrs` (2), `nodes` (3), `prev_verts` (16) | per triangle | storage (megabytes) |
| `accum` (11), group 1 (radiance cache, reservoirs, the trace's outputs, the loop guard's count) | per pixel, per cache entry | written by the kernel: cannot be uniform |
| `accum` (11), group 1 (radiance cache, the trace's outputs, the loop guard's count) | per pixel, per cache entry | written by the kernel: cannot be uniform |
| group 2, the translucency pass (`timg`, `vpts`, `vcount`, `tlow`) | per pixel | written by the pass |
| the particle passes' `systems`, `spawns`, `grid`; the grid programs' `srcs` | small, read-only | storage: other shaders, of eleven bindings at most and no acceleration structure; not known to misread (the fix on the device left them alone) |
| the live passes (`live.wgsl`: 28 storage buffers, `ae_bins` and `ae_state` among them) | per pixel; two tiny | storage: written by the passes; the device showed their output right |
| the live passes (`live.wgsl`: 24 storage buffers, `ae_bins` and `ae_state` among them) | per pixel; two tiny | storage: written by the passes; the device showed their output right |

Two of these are what a device run should look at first: `prev_verts` is the one storage buffer left after the
acceleration structure's binding (13) and feeds the motion vectors of moving objects and the vertex colours;
Expand Down
5 changes: 3 additions & 2 deletions docs/WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ main. Branches are kept.

`.github/workflows/ci.yml` runs on every push to main and every PR, as parallel jobs:
- `ts`: `npm run typecheck`, `npm test`;
- `renderer`: the renderer's release build;
- `renderer`: the renderer's release build and its unit tests (`cargo test --release`; the runner has no GPU, so the
tests that open a device skip themselves);
- `voxel`: the voxel core's wasm32 build and tests, and native / wasm32 edit determinism;
- `physics`: the physics core's wasm32 build and tests, and native / wasm32 determinism (a script and 1,200 random worlds);
- `wasm`: the engine library's wasm32 build;
Expand All @@ -150,7 +151,7 @@ main. Branches are kept.
A PR runs a job only when it changes that job's inputs (the paths in the workflow's `changes` job; a change to the workflows runs them all); a push to main runs them all.
`threert` sums them up and is the check to read before merging: it fails when a job failed or was cancelled, and passes when each one passed or was skipped by its paths.

The GPU checks (`test:render`, the perf smoke, the benches, the browser test `tools/web-hello.ts`) run on a Mac, per the merge gate above.
The GPU checks (`test:render`, the perf smoke, the benches, the browser test `tools/web-hello.ts`) run on a Mac, per the merge gate above, on hardware traversal only: what software traversal draws (the browser's, a device without ray queries) is in no gate yet (ROADMAP "Known issues").

## The npm packages

Expand Down
3 changes: 2 additions & 1 deletion renderer/src/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1447,7 +1447,8 @@ impl Gpu {
// layout with more does not validate).
let merged = cfg!(target_arch = "wasm32") || std::env::var("THREERT_MERGED").is_ok_and(|v| v == "1");
limits.max_storage_buffers_per_shader_stage = if merged { crate::merged::STORAGE_BUFFERS } else { al.max_storage_buffers_per_shader_stage.min(31) };
// Storage + uniform buffers together (the live passes bind 29; Metal allows 31).
// Storage + uniform buffers together (the kernel binds 19 storage buffers, the live passes 24, each beside its
// uniforms; Metal allows 31).
limits.max_buffers_and_acceleration_structures_per_shader_stage = al.max_buffers_and_acceleration_structures_per_shader_stage.min(31);
limits.max_acceleration_structures_per_shader_stage = al.max_acceleration_structures_per_shader_stage;
limits.max_blas_geometry_count = al.max_blas_geometry_count;
Expand Down
2 changes: 1 addition & 1 deletion renderer/src/merged.rs
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ mod tests {
assert!(out.ends_with("{ return objects[min(u32(a[1] * 3u + 1u), 4095u)].w + tex_info[min(u32(u32(objects[min(u32(k), 4095u)].x)), 4095u)].y + media[min(u32(0), 31u)].b.x; }\n"), "{out}");
}

/// The same for the live passes (28 as written).
/// The same for the live passes (24 as written).
#[test]
fn the_merged_live_passes_fit_eight_storage_buffers() {
let (m, info) = parse(LIVE);
Expand Down