Skip to content

Embree comparison, adopt its ideas (occlusion/watertight/instancing/SBVH/BVH4/masks/motion/curves), and extract the crust-rt kernel crate - #113

Merged
doubleailes merged 12 commits into
mainfrom
claude/embree-crust-comparison-2en5le
Jul 26, 2026
Merged

Embree comparison, adopt its ideas (occlusion/watertight/instancing/SBVH/BVH4/masks/motion/curves), and extract the crust-rt kernel crate#113
doubleailes merged 12 commits into
mainfrom
claude/embree-crust-comparison-2en5le

Conversation

@doubleailes

@doubleailes doubleailes commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Starts with a deep analysis of Embree 4.4.x vs crust's intersection layer (docs/embree_comparison.md), implements all seven of the doc's "adopt natively" recommendations in safe Rust, then completes the "openqmc move": the whole intersection layer extracted into a dedicated crust-rt crate behind an Embree-shaped, ID-based API.

The comparison

docs/embree_comparison.md analyzes Embree's geometry types, BVH builders, query API, and ISA/SYCL support against crust's intersection layer, documents what crust covers that Embree doesn't (the whole renderer above the query line), and ranks which Embree ideas were worth adopting. Its head-to-head table and addenda reflect the final state of this branch.

Adopted, one commit each

  1. Dedicated occlusion query — early-exit, unordered traversal for NEE shadow rays (the rtcIntersect/rtcOccluded split).
  2. Watertight triangles — Woop et al. 2013 replaces epsilon-based Möller–Trumbore: dominant-axis shear, 2D signed edge functions with an f64 fallback on exact-zero ties, division-free t range test. Pinned by 10k-sample shared-edge/shared-vertex pinhole tests.
  3. Instancing — local-space mesh BVHs shared across prims with identical points/topology/material (content hash + memoized material Arcs); rays transform into local space, normals map back by inverse-transpose.
  4. Parallel deterministic BVH build — subtrees above 4096 references build via rayon::join; split decisions are input-only, so the tree is byte-identical run-to-run (pinned by a build-twice test).
  5. SBVH spatial splits — reference-based build (Stich et al. 2009): binned object SAH plus an α-gated binned spatial split with entry/exit counting; straddling references are clipped (exact Sutherland–Hodgman for triangles) and duplicated across children.
  6. BVH4 — the binary tree collapses into 4-wide SoA nodes; traversal tests four child slabs per visit with Vec4 min/max (clamped reciprocals keep zero-direction lanes NaN-free), near-to-far lane ordering for closest-hit, early exit for occlusion.
  7. Ray masks, motion blur, curvescrust:rayMask visibility categories (camera/shadow/indirect); crust:motion:translate transform motion blur (QMC shutter time on primary rays, inherited along the path; instances lerp their placement per ray with a conservative union-of-endpoints bbox); RoundCurveSegment sphere-swept cones with UsdGeomBasisCurves import (linear direct, cubic bezier/bspline/catmullRom flattened).

The crust-rt kernel crate

The intersection layer is now its own crate (the way openqmc-rs factored out sampling), deliberately Embree-shaped so Embree bindings could slot in behind the same seam:

  • Geometry variants (triangle meshes with optional per-vertex shading normals, spheres, round curves, single-level instances with motion) attach to a SceneBuilder with per-geometry masks; commit() builds; Scene::intersect/Scene::occluded mirror rtcIntersect1/rtcOccluded1.
  • Hits are plain Copy RayHits carrying geom_id/prim_id — the kernel never sees materials. Depends only on glam + rayon; 27 unit tests + doctest.
  • crust-core consumes it through rt_world.rs: WorldBuilder::attach(geometry, material) -> geom_id and a committed World that resolves hits back to (HitRecord, &dyn Material, geom_id, prim_id). Light attribution moved from Arc-address identity to geom_id (LightList::find_by_geom) — cleaner and faster than pointer comparison. The old Hittable/HittableList/Bvh/primitive types are gone from crust-core.

Measured performance

Stress scene from scripts/gen_stress_scene.py (383k world-baked triangles: 36 identical 9.8k-tri spheres, a 30k thin-shard field, an occluder slab for shadow rays), 640×360, median of 3 runs:

build spp=1 (parse+build+1 pass) spp=16 render cost / spp
pre-adoption (021d8dd) 2.40 s 26.91 s 1.63 s
+ occlusion query, watertight (b4a48ab) 1.91 s 20.39 s 1.23 s
+ instancing, parallel SBVH (3736eb3) 1.02 s 8.33 s 0.49 s
+ BVH4 (3671527) 0.89 s 6.60 s 0.38 s
+ crust-rt extraction (HEAD) 6.46 s

4.1× end-to-end at 16 spp (4.3× on pure per-sample render cost, 2.7× on startup), with the final image matching the pre-adoption render to a mean abs pixel difference of 1.5e-5, and the kernel extraction adding zero overhead (pixel-identical renders, unchanged wall time). Peak RSS rose from ~78 MB to ~96 MB on this scene (build-transient allocations). On the small bundled samples (shading-bound) wall time is unchanged.

Verification

  • 125 tests green across the workspace (27 kernel + 76 core + 10 USD integration + others), including new integration tests over two new sample scenes (samples/curves.usda, samples/motionblur.usda) asserting exact strand-hit distances, shutter-time hits/misses, and mask filtering.
  • Cornellbox and the stress scene render pixel-identical before/after the kernel extraction; curves/motionblur renders visually confirmed (streaks, camera-invisible shadow card, hair strands).
  • Zero compiler warnings; benches compile.

Known caveats (documented in CLAUDE.md)

Motion blur is transform-only with linear matrix lerp (no deformation/quaternion motion); cubic curves are flattened rather than intersected exactly; mesh sharing requires identical geometry and material — UsdGeomPointInstancer / instanceable arcs are not consumed; emissive curves/instances are not light-list entries. world.count() now counts geometries (a rect light's two triangles are one mesh geometry).

🤖 Generated with Claude Code

https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F

claude added 8 commits July 26, 2026 01:04
Analyzes Embree 4.4.x (geometry types, BVH builders, query API, ISA/SYCL
support) against crust's intersection layer, documents what crust covers
that Embree doesn't, and ranks Embree ideas worth adopting natively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
Hittable::hit_any defaults to hit().is_some(); Bvh overrides it with an
early-exit traversal (no front-to-back ordering, first confirmed hit
wins), HittableList and Mesh short-circuit likewise. NEE shadow tests in
shadow_transmittance now use it instead of searching for the closest hit
- Embree's rtcIntersect/rtcOccluded split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
Replaces the epsilon-based Möller-Trumbore test with the watertight
scheme: winding-preserving dominant-axis permutation, shear onto +Z, 2D
signed edge functions with an f64 fallback on exact-zero ties, and a
division-free t range test. Triangle and SmoothTriangle now share one
intersector that also yields the barycentrics for normal interpolation.
Pinned by shared-edge/shared-vertex pinhole tests (10k edge samples).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
Ray gains time (shutter position in [0,1)) and mask (Embree-style
visibility category bit) with builder setters and a transformed() helper
for instancing. The camera stamps primary rays (new QMC domain K_TIME
feeds the shutter), and the integrator stamps every secondary ray with
the path's time and the indirect category, shadow rays with the shadow
category. Rays built outside the integrator default to MASK_ALL, so
nothing changes until geometry opts into a narrower mask.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
New Instance primitive holds an Arc'd Hittable plus a local-to-world
placement: rays transform into local space (direction unnormalized so t
carries over), normals map back through the inverse-transpose, and an
optional end-of-shutter transform linearly interpolates the placement at
the ray's time (transform motion blur; the bbox is the conservative
union of both endpoints).

The USD importer now builds every mesh's triangle BVH in local space and
shares it across prims with identical points/topology/material (content
hash + memoized material Arcs, so binding paths compare by pointer):
N placements of a mesh cost one copy of its triangles. Non-invertible
transforms fall back to world-space baking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
Rebuilds the BVH constructor in the SBVH mold (Stich et al. 2009):
subtrees own PrimRef lists (bbox + primitive index), each node evaluates
the binned object SAH split and - when the object children overlap by
more than alpha=1e-5 of the root surface area - a binned spatial split
with entry/exit counting, chopping straddling references at the plane
and duplicating them into both children. Clipped bounds come from a new
Hittable::clipped_aabb (default bbox-cap-slab; exact Sutherland-Hodgman
polygon clipping for triangles). Leaves now index a shared indices array
so a primitive can sit in several leaves while being stored once.

Subtrees above 4096 references build on parallel rayon::join tasks;
every split decision is input-only, so the tree stays deterministic
(pinned by a build-twice test). Diagonal-shard tests pin that spatial
splits actually fire and agree with a linear scan.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
The SBVH build now feeds a post-build collapse: each wide node adopts
its binary node's children and repeatedly expands the largest-area
internal child until four lanes fill. Nodes store child slab bounds in
SoA Vec4 lanes, so traversal tests all four boxes per visit with vector
min/max; direction reciprocals are clamped finite (safe_inv) so 0-dir
components cannot produce NaN lanes, and empty lanes carry +INF/+INF
bounds the swapped slab test can never pass. Closest-hit traversal
processes hit lanes near-to-far (leaf lanes immediately, internal lanes
pushed far-to-near); occlusion traversal stays unordered and early-exits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
Ray masks: a Masked wrapper gates intersection on the ray's category bit
(camera / shadow / indirect) against the geometry's crust:rayMask attr -
e.g. rayMask = 6 makes a shadow card that darkens the floor without
appearing in the image.

Motion blur: crust:motion:translate on a Mesh or Sphere prim rides the
prim on an Instance whose end-of-shutter transform is the authored
world-space translation; primary rays already carry a QMC-drawn shutter
time, so moving geometry streaks with zero cost to static scenes.

Curves: new RoundCurveSegment primitive (sphere-swept cone: tangent
frustum + spherical caps, Quilez's rounded-cone intersector evaluated on
a normalized direction and rescaled). UsdGeomBasisCurves imports linear
curves directly and flattens cubic bezier/bspline/catmullRom spans to
8-segment polylines, with vertex/uniform/constant width resolution;
segments live in local space under an Instance like meshes.

Sample scenes samples/curves.usda and samples/motionblur.usda, pinned by
integration tests (strand hit t, shutter-time hits, mask filtering).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
@qodo-code-review

Copy link
Copy Markdown
Contributor

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

README gains a Geometry & acceleration section; CLAUDE.md's core-traits
and USD-import sections now describe hit_any, watertight triangles,
Instance/Masked/RoundCurveSegment, the parallel SBVH+BVH4 build, and the
crust:rayMask / crust:motion:translate attrs, with new caveats under
known incomplete work. The Embree comparison doc's head-to-head table
and Route B section are updated to reflect the implemented items. The
binary build node's axis field was only consumed by the old ordered
binary traversal - BVH4 lane ordering replaced it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
@doubleailes doubleailes changed the title Add deep Embree vs crust ray tracing feature comparison Embree comparison + adopt its ideas: occlusion queries, watertight triangles, instancing, parallel SBVH/BVH4, masks, motion blur, curves Jul 26, 2026
claude added 3 commits July 26, 2026 01:57
Generates a ~383k-triangle scene (36 identical 9.8k-tri spheres for the
instancing dedup, a 30k thin-shard field for spatial splits, and an
occluder slab for shadow-ray early-exit) used to benchmark the
Embree-adoption pass. The generated file is ~15 MB and not checked in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
Factors the ray tracing kernel out of crust-core the way openqmc-rs
factored out sampling, behind a deliberately Embree-shaped API: Geometry
objects (triangle meshes with optional per-vertex shading normals,
analytic spheres, round curve segments, single-level instances with
transform motion blur) attach to a SceneBuilder with per-geometry
visibility masks, commit() builds the acceleration structure, and
Scene::intersect / Scene::occluded mirror rtcIntersect1 / rtcOccluded1.

Hits are plain Copy data carrying geom_id / prim_id - the application
owns the mapping from IDs to materials; the kernel never sees shading
state. Internals are the ported SBVH build (parallel, deterministic,
spatial splits with exact triangle clipping) collapsed to BVH4 with Vec4
slab tests, the watertight Woop-2013 triangle test, and the rounded-cone
curve intersector. 27 unit tests + doctest; nothing depends on the crate
yet - the crust-core port lands separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
crust-core no longer owns any intersection code: the Hittable trait,
HittableList, Bvh, and the primitive types are gone, replaced by
rt_world.rs - a WorldBuilder that pairs every attached rt::Geometry with
its material (attach() returns the geom_id) and a committed World whose
intersect() resolves kernel hits back to (HitRecord, &dyn Material,
geom_id, prim_id) and whose occluded() is the shadow-ray query.

Light attribution moves from Arc-address identity to geometry ids:
AreaLight records the geom_id of its emissive geometry and the
integrator matches bounce-hit emitters via LightList::find_by_geom.
crust_core::Ray wraps the kernel ray plus the renderer-side carried
medium. The USD importer attaches kernel geometries directly (meshes as
local-space inner scenes shared via instances, curves as RoundCurves
batches, rect lights as one two-triangle mesh geometry - world.count()
now counts geometries), and world.rs builds the procedural scene through
the same WorldBuilder.

Renders are pixel-identical to the pre-port build on cornellbox (32 spp)
and the 383k-triangle stress scene (16 spp), with unchanged wall time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XTp9J7yinpSvfjX5tmh4F
@doubleailes doubleailes changed the title Embree comparison + adopt its ideas: occlusion queries, watertight triangles, instancing, parallel SBVH/BVH4, masks, motion blur, curves Embree comparison, adopt its ideas (occlusion/watertight/instancing/SBVH/BVH4/masks/motion/curves), and extract the crust-rt kernel crate Jul 26, 2026
@doubleailes
doubleailes merged commit 2e0b430 into main Jul 26, 2026
2 checks passed
@doubleailes
doubleailes deleted the claude/embree-crust-comparison-2en5le branch July 26, 2026 07:26
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.

2 participants