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
23 changes: 23 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2288,6 +2288,29 @@ orders): native hardware traversal equal (Bistro 8.28-8.97 ms against 8.30-8.96,
one-field accessors nor main's parameter order moved the merged +0.2, which comes and goes with the layout: the
compiler's arrangement around the accessors, +-0.5 %.

**What the kernel trusts, held by the data (2026-11-14, after it).** The kernel follows indices from buffers and
checks none; the loop guard was the only thing between bad data and a dispatch that never ends. Researched against
pbrt-v4, Embree, Cycles, three-mesh-bvh, Falcor, the DXR and Vulkan specs and wgpu: they hold the invariants in the
builders (children after the parent, a depth cap tied to the stack, forced or median leaves), clean the input where
it enters (Embree and Cycles drop non-finite primitives; DXR makes them inactive), and keep the structural checks to
debug builds and tests; none checks a tree before every upload, none caps a walk's iterations. The same here:
- bvh.rs: no deeper than the kernel's stack (48; where the levels left could not finish a split, the median);
items whose box is not finite in no leaf, after every leaf's range; `check_tree` (the units' check, moved here)
on every tree in tests and debug builds; each build reports its depth (`bvh_depth`). Bistro 18, Kabukichō 26, the
district 33: no tree of today's scenes changes.
- ltree.rs: `LightTree::check` (children after the parent, leaves inside the entries, every entry's code to its
leaf) after every build, growth and refit in tests and debug builds.
- Where data enters: geometry positions finite (`World::set_geometry`), a volume's majorant a finite number > 0 (Rust
and packages/scene: `Infinity` passed both and its delta tracking never stepped), a world matrix that is not finite
places its object at a point (as a hidden instance), the hardware acceleration structures' vertices finite
(`scene::blas_tri`).
- Garbage-input tests for each builder (NaN, infinite and huge values, coincident and collinear items).
docs/RUNTIME.md "What the kernel trusts" has the table. Checks: `cargo test --release` with the loop guard, 138 passed;
render-identity against main, guarded natively, merged and in software traversal, then native without it: 0 of 190
changed in each (171-175 byte-identical, the rest within their run-to-run spread); `tools/web-hello.ts` in Chrome:
pass. The district's tree builds in 333 ms against main's 321 (one load each; an earlier load of the same build
338). No shader changed.

## 2. Temporal fixes

Targets (2026-11-08): bench-temporal, bench-ghosting and their UE MRQ references move to Bistro night (§ 0 step 7); the
Expand Down
40 changes: 39 additions & 1 deletion docs/RUNTIME.md
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,8 @@ tree (ROADMAP § 6).
not bit-identical to the kernel's without the guard: the compiler arranges the arithmetic of the functions it is in
differently (hello world, one sample: 354 of 43,200 numbers differ, by at most 2 × 10⁻⁷ of their value). What it
stops it counts: the count is read back without waiting (`LiveCore::guard_trips`, the page's `guardTrips()`, the frame log), an offline
render in which it moved is an error, and anything but 0 means wrong pixels. Natively `THREERT_LOOP_GUARD=<limit>`
render in which it moved is an error, and anything but 0 means wrong pixels, and a bug in what the kernel was given
(the guarantee is the data's: "What the kernel trusts"). Natively `THREERT_LOOP_GUARD=<limit>`
sets it; with nothing set a kernel with units has it (65,536; `THREERT_LOOP_GUARD=0` takes it out) and every other
kernel has none, as before ("Software traversal with units"). The live passes need none: their loops are counted
loops over constants and clamped parameters.
Expand Down Expand Up @@ -1265,6 +1266,43 @@ kernel is not given is an error.
- The repository's Rust is pinned (`rust-toolchain.toml`, 1.100.0): wesl-rs 0.6 asks for 1.97.1 or newer, and the
determinism checks' recorded hashes are of one compiler.

## What the kernel trusts

The kernel follows indices it reads from buffers and loops over counts it is given; it checks none of them (a
check on every read would cost the trace, and naga's and Tint's own guards do not help: a clamped index can land on
a real node and close a cycle, and their loop bound is 2^64 iterations). That a walk ends and reads only what is
there is the data's to guarantee, as in the references (pbrt-v4, Embree, Cycles, three-mesh-bvh, Falcor; DXR and
Vulkan for what a driver is given): the builders hold the invariants by how they build, what enters the engine is
made finite where it enters, and the structural checks run in tests and debug builds. The loop guard ("The engine
in a browser") is the last line, not the guarantee: a count above zero is a bug in what follows here.

| what the kernel walks | built by | what holds it |
|---|---|---|
| the still scene's tree (software traversal), a procedural object's boxes' tree | bvh.rs `build_tris`, `build_boxes` | children after their parent and inside the tree (so a walk that goes down and pops what it pushed reaches each node at most once and ends, whatever the boxes say); every node reached once; leaves of 1 to 16 items inside the items; boxes finite (an item whose box is not is in no leaf, after every leaf's range, its index kept); no deeper than the kernel's stack (48: where the levels left could not finish the split the surface-area heuristic wants, the median); `check_tree` in tests and debug builds |
| the units' trees, the top tree | units.rs | the same, checked at every build and edit (an incremental path, cheap per unit) |
| the light tree | ltree.rs | children after their parent; median splits, so the depth is log2 of the entries, within the path codes' 26 bits; leaves of 1 to 4 entries (the child word's two bits); every entry's code leads to its leaf; `LightTree::check` after every build, growth and refit in tests and debug builds. Its walks end by their bounds whatever the data (`LT_PATH_BITS`, the code's depth, four entries) |
| the emitter CDF's searches | lib.rs | binary searches end on any data (the interval shrinks); the guide table's entries are the CDF's |
| counts (lights, emitters, directional lights, media, procedural objects, bounces) | lib.rs `prepare`, live/edits.rs | the lengths of the arrays uploaded with them; media at most `MAX_MEDIA` |
| a medium's delta tracking | world.rs `set_object`, packages/scene `VolumeMesh` | the majorant a finite number > 0 (an infinite one never steps: the walk through the volume would not end) |

Where what is built from enters, it is finite:
- **geometry**: `World::set_geometry` refuses positions that are not finite (as it refuses indices out of range);
procedural boxes must be finite with min <= max (procasset.rs `check_boxes`); density grids and their sources
finite (as before);
- **transforms** are kept as set (a script may set NaN), and a world matrix that is not finite places its object at
a point (`World::world_matrix`), as a hidden instance is placed (`scene::instance_world`): what it draws is not
seen, and nothing built from it gets a number that is not finite;
- **the hardware acceleration structures' vertices**: a triangle with a vertex that is not finite (what overflows on
the way) is given to them as three vertices at the origin (`scene::blas_tri`): DXR and Vulkan leave such a vertex
undefined behaviour and define a degenerate triangle as never hit; its index stays.

Measured (2026-11-14, software traversal's still tree, `bvh_depth` in a session's load line): Bistro 18 levels,
Kabukichō 26, the Kabukichō district 33: the stack's 48 changes none of them. A tree of boxes spaced in powers of two
is as deep as SAH makes it unheld, and no deeper than asked held (bvh.rs `a_tree_is_no_deeper_than_its_stack`).
Garbage in (NaN and infinite vertices, values near f32's limit, a thousand copies of one triangle, a needle;
emitters likewise) makes trees the checks pass and the kernel's walk ends on (`garbage_in_makes_a_tree_the_kernel_walks`,
`garbage_in_makes_a_tree_the_kernel_reads`).

## Android (Adreno): what a phone's Vulkan driver needed

The engine's library builds for `aarch64-linux-android` and runs under an Android client on Vulkan with ray queries.
Expand Down
2 changes: 1 addition & 1 deletion packages/scene/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1560,7 +1560,7 @@ export class VolumeMesh extends Object3D {
}
constructor(o: { shape?: "box" | "sphere"; volume: VolumeBinding; majorant: number; anisotropy?: number; homogeneous?: boolean; grid?: DensityGrid | null }) {
super();
if (!(o.majorant > 0)) throw new Error("VolumeMesh: majorant must be > 0 (1/m)");
if (!(o.majorant > 0 && Number.isFinite(o.majorant))) throw new Error("VolumeMesh: majorant must be a finite number > 0 (1/m)");
this.grid = o.grid ?? null;
if (this.grid) {
if ((o.shape ?? "box") !== "box") throw new Error("VolumeMesh: a density grid needs a box volume");
Expand Down
2 changes: 2 additions & 0 deletions packages/scene/test/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ test("volume functions: one that reads the time is animated, unless it says othe
assert.deepEqual([still, drift, helper, custom, held].map((f) => f.animated), [false, true, true, true, false]);
const scene = new Scene();
scene.add(new VolumeMesh({ volume: still.with(), majorant: 1 }), new VolumeMesh({ volume: drift.with(), majorant: 1 }));
// (Delta tracking steps by -ln(1 - u) / majorant: an infinite one would never step.)
for (const bad of [Infinity, NaN, 0, -1]) assert.throws(() => new VolumeMesh({ volume: still.with(), majorant: bad }), /finite number > 0/);
const cam = new PerspectiveCamera();
scene.add(cam);
const { calls } = bind(scene, cam);
Expand Down
Loading