Skip to content

Widen the kernel's SIMD: mask-driven BVH4 traversal and 4-wide triangle packets - #116

Merged
doubleailes merged 3 commits into
mainfrom
claude/usd-dome-distant-lights
Jul 27, 2026
Merged

Widen the kernel's SIMD: mask-driven BVH4 traversal and 4-wide triangle packets#116
doubleailes merged 3 commits into
mainfrom
claude/usd-dome-distant-lights

Conversation

@doubleailes

@doubleailes doubleailes commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Adds the two UsdLux lights that have no scene geometry — DistantLight and DomeLight — and the integrator machinery they need. Outdoor scenes can now be lit correctly instead of falling back to the hard-coded sky gradient.

Three commits, each independently reviewable.

1. Light becomes direction-based (pure refactor)

Light::sample_point(u, v) -> Vec3A returns a point in space, and a light at infinity has none. It becomes a PBRT-shaped sample_li(from, u, v) -> Option<LightSample> carrying direction, shadow-ray distance, radiance and solid-angle pdf together — which also lets a light decline to be sampled at all (None below a dome's horizon).

MIS splits to match. Every light has two ways of being found — NEE samples it, or a ray arrives by chance — and both sides must evaluate the same density or emission double-counts. That second path differs by light kind, so it is now two methods: pdf_at_point for a bounce ray hitting a light's geometry (what bounce_emission_weight already did), and escaped for a ray leaving the scene along a direction an infinite light covers.

No behaviour change: all six lit sample scenes render pixel-identical, verified with exr_diff.

2. UsdLuxDistantLight, and the escaped-ray MIS path

A distant light can only be found by a ray leaving the scene inside its cone. That mirror of bounce_emission_weight did not exist — escaped rays went straight to the sky gradient. escaped_emission now weights what a departing ray picks up against the same density NEE used, at full weight where NEE did not run (primary rays, post-carried-medium scatters), exactly as the bounce-hit path already did. Where nothing at infinity covers the direction the sky gradient still applies, so scenes without an environment light are untouched.

Two conventions worth reviewing, both easy to get backwards, both pinned by tests:

  • The cone is always real. inputs:angle is an angular diameter and authors may set it to zero for sharp shadows. Rather than a delta light needing a second MIS path, zero widens to MIN_DISTANT_ANGLE_DEG. The penumbra is sub-pixel, and MIS handles a bounce ray straying into the cone — the light pdf is enormous there, so the bounce weight collapses and no firefly survives.
  • intensity × color is irradiance, not radiance; radiance is derived as E / Ω. Widening the angle then softens shadows without changing exposure, which is what Hydra normalizes to. Read as radiance instead, a sun-sized source comes out ~5 orders of magnitude too dark — distant_light_irradiance_is_angle_invariant is the guard.

3. UsdLuxDomeLight with importance-sampled HDRI

A dome covers every direction, so once one exists it is the background — escaped answers for every ray that leaves and the sky gradient stops applying.

crust-core still decodes nothing. inputs:texture:file is resolved against the USD layer's directory and handed to the host through a new AssetLoader trait (Scene::from_usd_with_assets); Scene::from_usd passes NoAssets, which warns and falls back to the dome's uniform colour. crust-render implements it with exr for OpenEXR and image for .hdr and LDR, un-gamma'ing the latter to linear. That seam preserves the crate's documented no-image-dependencies property, and is where general texture support should grow.

The sampling is the part most worth a look. A lat-long map is importance-sampled by luminance × sin θ: the image spends as many pixels on a degree near the pole as near the equator, but those texels cover far less solid angle, so omitting the Jacobian over-samples the poles and biases the estimate. Without any of this a small bright sun is a firefly farm — the sample scene's sun disc covers ~1/9000 of the map's texels at 400× its surroundings, and renders clean at 96 spp.

Verification

  • cargo test green across the workspace; ~20 new tests.
  • cornellbox, veach_mis and smoke render pixel-identical to main, so nothing outside the new lighting paths moved.
  • End-to-end on samples/domelight.usda: the chrome sphere reflects the sky, the ground band and the sun disc — the lat-long mapping, the dome's rotation and the importance sampling all being right at once.
  • Density correctness is pinned rather than assumed: normalization (E[1/pdf] = 4π over 200k samples), agreement between sample and pdf (the two MIS sides), and the sin θ Jacobian via poles_are_not_oversampled. uniform_map_has_uniform_solid_angle_pdf is evaluated at texel centres, where the piecewise-constant discretization vanishes and a uniform map must read exactly 1/4π; away from centres it legitimately differs, which is why normalization and MIS agreement are what hold everywhere.
  • CLI-side decode is covered where it has to be — in crust-render, the only crate with a decoder: an EXR round trip and the sRGB→linear conversion.

Known gaps (documented in CLAUDE.md)

Nearest-texel dome lookup, so a low-resolution HDRI shows texel edges in a mirror; texture:format values other than latlong are refused rather than mapped wrongly; light picking stays uniform, so a dim dome costs as many shadow rays as a bright sun; DiskLight and CylinderLight are still skipped; and neither infinite light is visible to the guiding field's spatial structure, since they have no position.

@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 →

claude added 3 commits July 27, 2026 10:12
Pure refactor, no behaviour change: all six sample scenes with lights render
pixel-identical (verified with exr_diff).

`Light::sample_point(u, v) -> Vec3A` returns a point in space, and a
`DistantLight` or `DomeLight` has none — they are directions at infinity.
Replaced with a PBRT-shaped `sample_li(from, u, v) -> Option<LightSample>`
carrying direction, shadow-ray distance, radiance and solid-angle pdf
together, which is also what lets a light decline to be sampled at all
(`None` below a dome's horizon).

MIS needed splitting to match. Every light has two ways of being found: NEE
samples it, or a ray arrives by chance, and both sides must evaluate the
same density. That second path differs by light kind, so it is now two
methods: `pdf_at_point` for a bounce ray hitting a light's geometry (what
`bounce_emission_weight` already did), and `escaped` for a ray leaving the
scene along a direction an infinite light covers. Area lights implement the
first, infinite lights the second, and the trait documents the pairing since
getting it wrong double-counts emission.

`LightSample::pdf` is a plain f32 rather than an optional delta marker: a
zero-`angle` DistantLight will be widened to a small real cone instead of
being made singular, so the integrator keeps one MIS path instead of two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7RXTdHvPFM5weuxWYLYU
…need

A distant light has no geometry, so a bounce ray can only find it by
*leaving* the scene along a direction inside its cone. That is the mirror of
`bounce_emission_weight` and it did not exist: escaped rays went straight to
the hard-coded sky gradient. `escaped_emission` now weights what a departing
ray picks up against the same density NEE used, with full weight where NEE
did not run (primary rays, post-carried-medium scatters) exactly as the
bounce-hit path already did. When nothing at infinity covers the direction
the sky gradient still applies, so scenes without an environment light are
untouched — verified pixel-identical on cornellbox, veach_mis and smoke.

Two conventions, both easy to get backwards, so both are pinned by tests:

- The cone is always real. `inputs:angle` is an angular diameter and authors
  may set it to zero for sharp shadows; rather than a delta light needing a
  second MIS path, zero widens to MIN_DISTANT_ANGLE_DEG. The penumbra is
  sub-pixel, and MIS handles a bounce ray straying into the cone — the light
  pdf is enormous there, so the bounce weight collapses and no firefly
  survives.
- `intensity × color` is irradiance, not radiance; radiance is derived as
  E / Ω. Widening the angle then softens shadows without changing exposure,
  which is what Hydra normalizes to. Read as radiance instead, a sun-sized
  source would come out ~5 orders of magnitude too dark —
  `distant_light_irradiance_is_angle_invariant` is the guard.

The other two tests pin that `sample_li` and `escaped` agree on both the
covered set of directions and the pdf (disagreement would double-count), and
that a zero angle keeps a finite pdf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7RXTdHvPFM5weuxWYLYU
Completes the pair of lights at infinity. A dome covers every direction, so
once one exists it *is* the background — `Light::escaped` answers for every
ray that leaves and the built-in sky gradient stops applying. Scenes without
one are untouched (cornellbox, veach_mis and smoke stay pixel-identical).

crust-core still decodes nothing. `inputs:texture:file` is resolved against
the USD layer's directory and handed to the host through a new `AssetLoader`
trait (`Scene::from_usd_with_assets`); `Scene::from_usd` passes `NoAssets`,
which warns and falls back to the dome's uniform colour. `crust-render`
implements the trait with `exr` for OpenEXR and `image` for `.hdr` and LDR,
un-gamma'ing the latter to linear. That seam is where general texture
support should grow, and it keeps the crate's no-image-dependencies property
intact.

The sampling is the part worth reviewing. A lat-long map is importance-
sampled by luminance **times sin θ**: the image spends as many pixels on a
degree near the pole as near the equator, but those texels cover far less
solid angle, so omitting the Jacobian over-samples the poles and biases the
estimate. `poles_are_not_oversampled` and
`uniform_map_has_uniform_solid_angle_pdf` pin it — the latter evaluated at
texel centres, where the density's piecewise-constant discretization
vanishes and a uniform map must read exactly 1/4π. Away from centres it
legitimately differs, so what is pinned everywhere instead is normalization
(E[1/pdf] = 4π over 200k samples) and that `sample` and `pdf` agree, since
those are the two MIS sides and disagreement double-counts.

Without any of this a small bright sun in an HDRI is a firefly farm: the
sample scene's sun disc covers ~1/9000 of the map's texels at 400x its
surroundings, and renders clean at 96 spp.

Verified end to end on samples/domelight.usda, whose chrome sphere reflects
the sky, the ground band and the sun disc — which is the lat-long mapping,
the dome's rotation and the importance sampling all being right at once.
Tests cover the light-list/geometry split, that the resolved asset path
reaches the host exactly once, that both infinite lights agree between
`sample_li` and `escaped`, and — in the CLI, which is the only crate that
can — an EXR round trip and the sRGB-to-linear conversion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Q7RXTdHvPFM5weuxWYLYU
@doubleailes
doubleailes force-pushed the claude/usd-dome-distant-lights branch from a81231b to 7f2088b Compare July 27, 2026 08:12
@doubleailes
doubleailes merged commit e983b12 into main Jul 27, 2026
2 checks passed
@doubleailes
doubleailes deleted the claude/usd-dome-distant-lights branch July 27, 2026 09:00
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