Many-area-light resampling estimator review¶
Status: reviewed October 27, 2026. Revised the same day to reconcile it with the
CPU and GPU reference requirement and with the polygonal-light work's licensing,
traversal and primitive decisions. The primitives listed under
Ownership are implemented. The ris-projected-solid-angle template
(stage 1) is implemented as of October 28, 2026, with the departures listed under
Implementation notes. The RT1 profile (stage 2) is
implemented as of October 29, 2026, in the CPU oracle and the GPU-compute
reference, with the choices listed under RT1 implementation notes.
Acceptance item 5 was measured the same day; see
RT1 equal-time evaluation. Later that day both
stages gained Hill and Heitz's approximate target, which
lowers the per-candidate cost behind those equal-time results.
This review is the estimator review that
hybrid and real-time transport
requires before resampling enters a Ludus estimator. It covers next-event
estimation (NEE) for emissive triangles, delivered in two stages:
- A built-in render graph template,
ris-projected-solid-angle: single-bounce direct lighting beside the existing MIS and ReSTIR DI templates. - An explicit second light-sampling profile for the RT1 progressive reference, delivered in the CPU oracle and the GPU-compute reference in the same change.
Spatiotemporal reservoir reuse remains a separate RT4 upgrade. The Peters 2021
polygonal-light work runs in parallel and shares three pieces with this design: the
projected-solid-angle primitives, the template shadow-ray traversal and, since
October 29, 2026, the §6.1 shading-normal clip. Its
comparison template, polygonal-light-sampling, is implemented as of October 28,
2026: it shades every emissive triangle, without light selection, with clamped
optimal MIS, the balance heuristic and each technique alone, and has a CPU oracle
with GPU parity tests. On October 29, 2026 it gained Peters' §6.1 clip of the
shading normal to the view's hemisphere, in the template and the oracle, and an
occluded-penumbra fixture after his Fig. 10, where the GPU shows clamped optimal
MIS beating the weighted balance heuristic. The ris-projected-solid-angle
template gained the same clip on October 29, 2026, through a FoundationMath
function that both templates share. The polygonal-light template's limit of eight
emissive triangles
stays: lifting it needs a change to the scene-interface contract. Its usage,
departures, evidence and those options are in the
authoring guide.
Sources reviewed¶
| Source actually reviewed | Consequence for Ludus |
|---|---|
| Ishaan Shah, Aakash KT and P. J. Narayanan, “Combining Resampled Importance and Projected Solid Angle Samplings for Many Area Light Rendering”, SIGGRAPH Asia 2023 Technical Communications; author PDF §§2–6 and Algorithm 1 read in full | Resample lights, not points. The candidate density is p(A_j|x), the target is the unshadowed LTC shading of the whole light, M = 32, candidates stream through a weighted reservoir, and the selected light then receives one projected-solid-angle/LTC sample. The naive variant (their §3.1), which runs the expensive point sampler once per candidate, is rejected for the reason they give. Their renderer computes direct lighting only, with no BSDF-hit MIS and no reuse. Their §6 single-lobe limitation is addressed below rather than inherited. |
| Christoph Peters, “BRDF Importance Sampling for Polygonal Lights”, ACM TOG 40(4), article 140, 2021; author PDF §§3.1, 3.7, 4.1–4.3, 6.1 and 6.5 | Projected-solid-angle sampling of a convex polygon clipped to z ≥ 0 in the shading frame, with clockwise winding (flip y when the polygon is seen from behind), FP32 stability notes and a fixed two-iteration inversion. LTC sampling transforms the polygon by M⁻¹, clips only in LTC space, and disables the specular technique when that clip is empty. Mixed BRDFs take one sample per technique and combine them with clamped optimal MIS, v = 1/2; the paper recommends against the one-sample estimator (§4.2). Diffuse albedo in the MIS coefficients is clamped to at least 0.01 per channel so the diffuse technique always covers the polygon. |
| Eric Heitz, Jonathan Dupuy, Stephen Hill and David Neubelt, “Real-Time Polygonal-Light Shading with Linearly Transformed Cosines”, ACM TOG 35(4), article 41, 2016 (SIGGRAPH 2016); author preprint §§3.1–3.2, 4 and 5.1, reviewed for the LTC transform | The LTC density (Eq. 1) is the clamped cosine (Eq. 4) of M⁻¹ω/‖M⁻¹ω‖ times the Jacobian |M⁻¹|/‖M⁻¹ω‖³. It is invariant to scaling M, its polygon integral is the clamped-cosine integral over M⁻¹P (Eq. 3), and it is sampled by mapping a cosine sample through M (Algorithm 1). Isotropic BRDF fits use the four-parameter form of Eq. 6 with the L3 error and store M⁻¹ plus the BRDF norm over (θ, √α). The fitter must reimplement this from the text, because the published utilities are not used. |
| Chris Wyman, “Weighted Reservoir Sampling,” Ray Tracing Gems II, chapter 22, via the existing streaming selection review | Sequential, with-replacement reservoir over an explicit candidate order, with zero totals handled explicitly. |
| Stephen Hill and Eric Heitz, “Real-Time Area Lighting: a Journey from Research to Production”, SIGGRAPH 2016 Advances in Real-Time Rendering course; slides and speaker notes 63–77 (edge integrals, θ/sin θ fits) and 87–102 (horizon clipping, vector form factor, proxy sphere), reviewed October 29, 2026 | The approximate target. Without its projection onto the normal, the sum of edge integrals is the polygon's vector form factor F, whose length is the form factor in the direction of F (slides 90–93). A sphere with the same F has angular extent asin(√|F|) (slides 94–96). Its horizon-clipped form factor is tabulated from Snyder's closed form (slide 97), or approximated by max((|F|² + F.z) / (|F| + 1), 0) without a table (slide 102). θ/sin θ uses a quadratic fit for diffuse (slide 77) and a cubic rational fit where accuracy matters (slides 72–74). For the LTC term, the course transforms the polygon by M⁻¹ before integrating (slide 63). Ludus adopts the table-free sphere approximation and the quadratic fit for both terms, because a target needs only a positive weight that tracks the integral, not shading accuracy. |
Cited through those sources but not separately reviewed in this pass. Review the cited sections before implementing the corresponding piece, and add their attribution beside the code:
- John Snyder, “Area Light Sources for Real-Time Graphics,” Microsoft Research technical report MSR-TR-96-11, 1996: the exact horizon-clipped sphere form factor that Hill and Heitz tabulate. Ludus uses only their table-free approximation.
- Benedikt Bitterli et al., “Spatiotemporal Reservoir Resampling for Real-Time Ray Tracing with Dynamic Direct Lighting,” ACM TOG 39(4), 2020, and Justin Talbot, David Cline and Parris Egbert, “Importance Resampling for Global Illumination,” EGSR 2005: the RIS contribution weight and its unbiasedness conditions.
Licensing, as decided for the polygonal-light work.
- Of Peters' published renderer code, only polygon_sampling.glsl is dual
licensed BSD-3/GPLv3. It may be ported, keeping its notice.
- The clipping, LTC table and utility code, and shading passes are GPLv3-only.
They are reimplemented from the paper's text (and, for LTCs, from Heitz et al.
above), never copied.
- No third-party LTC tables are imported. Ludus fits its own.
Current estimators¶
| Estimator | Emissive-triangle NEE today |
|---|---|
| RT1 reference: reference_path.cpp, renderer_reference.slang, renderer_gpu_preview.slang | Slot 0 picks a category uniformly from N = Na + Ne + e. If it lands on an emitter, slot 7 (LUDUS_PATH_RANDOM_EMITTER_SLOT) picks a triangle from the power-weighted integer CDF (luminance × area, quantized against 2²⁴, at least one unit per positive emitter). Slots 1–2 then sample uniformly by area. BSDF hits weight with the power heuristic using the same selection probability (frozen profile). |
veach-guibas-mis template |
SelectLight is uniform over GetLightCount. Within the chosen emitter surface, the triangle is chosen in proportion to area and the point uniformly, then converted to solid angle. The template exports MIS, light-only and BSDF-only outputs with selectable heuristics. |
restir-di template |
RIS over points in area measure. M = 8 by default, a distance-weighted emitter PMF and an unshadowed point target, with bounded temporal and spatial reuse. It is light-only and documented as not a full unbiased reference. |
tiled-vbuffer-nee template |
A per-tile emitter CDF with floors, combined with the BSDF by the power heuristic. |
None of these accounts for the cosine and BSDF when sampling a point on a
triangle, and only ReSTIR adapts light choice to the receiver. When this review
was written, develop had no LTC, projected-solid-angle or polygon-clipping code.
FoundationMath has since gained the projected-solid-angle primitives, the
sequential reservoir and the LTC polygon transform (see Ownership).
The ReSTIR template still uses its private reservoir helpers.
Proposed estimator¶
Notation: receiver x with outgoing direction ωo; emissive triangles t;
candidate density p_src(t); Y(c) is BT.709 luminance of an RGB value. The
template and RT1 differ only where noted.
- Category selection is unchanged. RT1 slot 0 and the template's
SelectLightdraw keep their uniform choice. Analytic lights and the environment keep their current estimators and MIS densities. A draw that lands on an emitter selects the emissive set with probabilityP_E(Ne/N), and its specific emitter is discarded. Only the emissive branch changes. - Candidate density.
p_srcis the existing power-weighted triangle CDF that RT1 already packs. Graph shaders read it through two additive scene-interface v1 functions:SelectEmissiveTriangleselects a triangle from an explicit dimension, andEvaluateEmissiveTriangleSelectionPmfreturns a triangle's probability. Every positive emitter keeps a nonzero probability. - Per-receiver LTC setup. Look up the specular LTC once per receiver from
roughness,
dot(n, ωo)andF0. This givesM₁⁻¹and specular albedoa₁. The diffuse technique is the identity LTCM₀ = I, with albedoa₀approximating the Fresnel-weighted Lambert lobe and clamped to 0.01 per channel (Peters §4.2). These guide sampling and weights only; the BSDF (EvaluateStandardBsdf/ L4 reference BSDF) is always evaluated exactly. - Target. For triangle
t:
E₀(t) = projected solid angle / π of t clipped to z ≥ 0 in the shading frame
E₁(t) = the same integral after transforming the vertices by M₁⁻¹
p̂(t) = Y(L̄_t) · (Y(a₀)·E₀(t) + Y(a₁)·E₁(t))
- A one-sided emitter seen from behind has
p̂ = 0. L̄_tis the surface's constant emission factor. Emission maps are RGBA8 values in[0, 1]multiplied by that factor, so it bounds textured emission from above and needs no new cooked data.- Textured emission, alpha cutoff, finite range and visibility only make the true integrand smaller, which is allowed.
- Support invariant:
p̂(t) > 0wheneverthas positive area above the shading horizon andY(L̄_t) > 0. The clampeda₀and an exact clippedE₀guarantee it. Approximate evaluations need a new version that proves the same invariant; the approximate target is one. - Resampled light selection.
- Draw
Mcandidatest_i ~ p_srcwith replacement. The template'sRIS_CANDIDATE_COUNTsetting has a maximum of 32; RT1's sampler version 2 fixesM = 32. With the approximate target, the template defaults toM = 8and RT1's version 3 fixes its ownM. - Weight each candidate
w_i = p̂(t_i) / p_src(t_i). - Stream candidates in index order:
w_sum += w_i, and acceptt_iwhenu_i · w_sum < w_i. Zero-weight candidates are never selected. Ifw_sum = 0, the branch contributes zero; this is a valid outcome, not an invalid sample. - The selected triangle
yhas contribution weightW = w_sum / (M · p̂(y)). For anygthat is zero whereverp̂is zero,E[W · g(Y)] = Σ_t g(t). - Float tickets quantize acceptance to
2⁻²⁴, the same declared approximation as other uses ofSampleFloat01. - Projected-solid-angle and LTC sampling on
y(Peters §§3, 4.1, 4.3). - Take one diffuse sample
ω₀withp₀ = cos θ / (π · E₀). - Take one LTC sample
ω₁withp₁ = p_M₁(ω) / E₁, unless the LTC-space clip is empty, in which casep₁ ≡ 0and onlyω₀is traced. - Combine them with clamped optimal MIS,
vdefaulting to 1/2, using coefficientsc_t = Y(L̄_y) · Y(a_t) · E_t. - Map each direction to barycentrics by intersecting the triangle's plane.
Emission, alpha, sidedness and visibility come from that point
(
GetSurfaceHit,EvaluateEmission,TestVisibilityin the template). A plane hit outside the triangle in FP32 contributes zero. - Emissive NEE therefore costs up to two shadow rays. The fixed
two-iteration inversion has a small backward error, which Peters §3.6
reports near
1e-5. Tests bound it and the estimator declares it. - The contribution of light technique
sisW_L(ω_s, y) · w^c_s(ω_s) · F(ω_s) / p_s(ω_s) · W / P_E. HereFis the shadowed BSDF·cosine·emission andW_Lis the BSDF-hit split defined next.
Decision: BSDF-hit MIS¶
A BSDF ray that hits an emitter needs the light strategy's density, and RIS has
no tractable density: whether t is selected depends on all M candidates. MIS
weights do not need true densities, however. Weights stay unbiased if they are
fixed functions of the sample, sum to one across strategies, and are zero where a
strategy cannot sample.
| Option | Verdict |
|---|---|
Use the candidate density P_E · p_src · p_area as the light density |
Rejected: it ignores resampling, so the BSDF wins weights it should lose. |
Exact p̂(t) / Σ p̂ |
Rejected: O(emitters) target evaluations at every hit. |
Reuse this reservoir's w_sum |
Rejected: the weight would correlate with the candidates that chose y, which biases the estimate. |
Light-only (W_L = 1 wherever p̂ · (p₀ + p₁) > 0), the paper's estimator |
Not the combined estimator. The template exports it as lightSampling. It misses sharp highlights from emitters that are rarely candidates. |
| Surrogate with an independent normalization estimate | Adopted. |
The adopted surrogate draws K further candidates t'_k ~ p_src from their own
dimensions. They are independent of the reservoir and of the BSDF sample. The
template's RIS_NORMALIZATION_CANDIDATE_COUNT has a maximum of 16 and defaults to
4 with the approximate target; RT1's sampler version 2
fixes K = 8, and version 3 fixes its own K.
Ẑ = (1/K) · Σ_k p̂(t'_k) / p_src(t'_k)
cap = min(1, M · p_src(t)) // ≈ chance that t is a candidate at all
P̃(t) = P_E · min(p̂(t) / Ẑ, cap) // p̂/Ẑ := +∞ when Ẑ = 0 and p̂ > 0; 0 when p̂ = 0
q = P̃(t) · (p₀(ω) + p₁(ω)) // one sample per light technique
W_L = q² / (q² + p_b²) // power heuristic; BSDF weight 1 − W_L
- Why it is unbiased. Conditioned on the normalization candidates,
W_Lis a fixed function ofx,ωo,tandω. - The light estimator integrates
W_L · Fwithout bias, and the BSDF estimator integrates(1 − W_L) · F. - Peters'
w^c_spartition the light group. - Zero-density cases are consistent:
p_s = 0givesw^c_s = 0,q = 0givesW_L = 0, andp_b = 0givesW_L = 1. - So every normalization draw sums to
∫F. - Why the cap matters. It hands rarely-included sharp highlights back to BSDF sampling.
- Template cost. Both strategies run in the same shade invocation, so
Ẑand the receiver frame stay in registers. A BSDF hit on an emissive triangle adds one target evaluation plus the projected-solid-angle and LTC density setup. - RT1 cost. Addressed randomness makes
Ẑa pure function of(pixel, sample, bounce), so it is computed lazily by one shared function: - at NEE, when the emissive branch is chosen;
- at the next vertex's emissive hit, using the previous bounce's slots.
Both sites must read bit-identical stored inputs: the previous receiver's
position, shading frame, M₁⁻¹, a₀/a₁ luminance and P_E. These join the
path state wherever the emitter-hit MIS density is computed today.
Template: ris-projected-solid-angle¶
The first deliverable is a built-in editable template, so the estimator ships
inside a legal, inspectable graph instead of a hidden renderer mode. Its shape
copies veach-guibas-mis, so the editor's compute-template path and the
existing evidence pattern apply unchanged.
main: Scene, Camera, RenderSize, color ──► [transport: Subgraph RisProjectedSolidAngle]
color ─────────► Accumulate(4096) ─► ToneMap(1) ─► output
lightSampling ─► Accumulate(4096) ─► ToneMap(1) ─► lightSamplingOutput
bsdfSampling ──► Accumulate(4096) ─► ToneMap(1) ─► bsdfSamplingOutput
library: hits Buffer (32 B/px, RenderSize)
primary ──► shade ──► shadeLight ──► shadeBsdf (ComputePipeline ×4; hits Load/Store)
│ │color │lightSampling │bsdfSampling
- Passes. Four
ComputePipelinepasses, the currentMAX_RAY_PASSES: primaryrecords hits;shaderuns the combined estimator above;shadeLightruns RIS plus projected-solid-angle sampling alone (the paper's estimator);shadeBsdfruns BSDF sampling alone.
The technique is RendererTriangles.RayTracing with
scene_interface {version 1, surface_queries 1, visibility_queries 1}, plus
the polygon-light requirement below.
- Traversal. Shadow and BSDF rays use the renderer's GPU-compute BVH, the same
decision as the polygonal-light templates, so the template does not require
native ray tracing.
- Graph passes select it with "traversal": "ComputeBvh", the strategy added
once at the shared
traversal boundary
rather than inside either template's callbacks. TriangleLoop remains a
linear scan meant for small scenes.
- A NativeQuery configuration can follow later as a separate configuration.
- Browser execution stays outside the graph ray profile until that profile
admits it.
- Settings. Finite, author-declared values:
- RIS_CANDIDATE_COUNT: 8, range 1–32 (32 before the approximate target);
- RIS_NORMALIZATION_CANDIDATE_COUNT: 4, range 1–16 (8 before it);
- RIS_TARGET: 1, the approximate target; 0 evaluates
the exact clipped integrals;
- PROJECTED_SOLID_ANGLE_MIS_VISIBILITY: Peters' v, 0.5, range 0–1.
Settings never change the random layout. Its ranges are sized for the maximums.
- Baseline. The UniformArea counterpart is the existing veach-guibas-mis
template, not a duplicate. Shade dimensions 2–8 keep Veach's meanings, so
analytic, environment and BSDF draws are paired across the two templates.
| Template dimension | Meaning |
|---|---|
| 0–1 | Primary jitter (primary pass) |
| 2 | SelectLight category |
| 3–5 | Analytic/environment SampleIncidentLight shape |
| 6–8 | BSDF sample |
| 9–10 | Diffuse projected-solid-angle sample ω₀ |
| 11 | Reserved |
| 12–13 | LTC sample ω₁ |
| 14–15 | Reserved |
16 + 2i, 17 + 2i, i < 32 |
Candidate i: power-CDF ticket, reservoir ticket |
80 + k, k < 16 |
Normalization candidate k: power-CDF ticket |
Shader helpers.
- The projected-solid-angle shader source from the polygonal-light work is
reused, not duplicated. FoundationMath adds the sequential reservoir beside it.
- The renderer owns the LTC table, shared with the polygonal-light LTC
templates. Its 64 × 64 × 51 transforms (3.4 MB) are too large for shader
constants, so shaders read the renderer's GPU copy from a read-only storage
buffer through ltc_table.slang.
- Stage them through the engine ray helpers (POLYGONAL_LIGHT_HELPERS in
render_graph_ray.py and the installed shader directory).
- The technique declares scene_interface.polygonal_light_sampling, in the same
way visibility_buffer adds its include and textures. The wrapper then includes
the helpers, and the renderer binds the table buffer at slot 13, which every
backend leaves free, when the linked kernel's reflection keeps it. Version 25
runtime packages record that binding. Older SDKs reject the key explicitly. The
passes must be compute-invoked: native ray stages cannot bind the table
(use the table).
Implementation notes¶
The template ships as
render_graphs/ris_projected_solid_angle.
Its usage is in the
authoring guide.
It follows the estimator, BSDF-hit decision, settings and random table above, with
these departures and choices:
- Visibility budget. The technique declares
visibility_queries: 2, not 1, because the selected triangle's diffuse and LTC samples each trace a shadow ray. - Helper requirement. The requirement is
scene_interface.polygonal_light_sampling: true. It stages and includes the projected-solid-angle, LTC and reservoir primitives and the LTC table lookup, and binds the renderer's table buffer (see the shader helpers above). Packages without it keep their snapshots. The builder rejects a non-Boolean value, and older SDKs reject the unknown field. F0slice.MakeReceiverLightingFramelooks up the specular LTC with the receiver'sF0,0.04 · (1 − metallic) + baseColor · metallic, so the table's Peters slice matches the albedo it scales.- Emitter alpha. Light samples do not apply the emitter's own alpha cutoff,
because the scene interface has no alpha-acceptance query.
veach-guibas-mislight sampling behaves the same way. Emission maps, sidedness and occlusion are evaluated at the sampled point. - Diffuse albedo.
a₀ = baseColor · (1 − metallic) · (1 − F₀)per channel, Schlick's reflectance at normal incidence, clamped to 0.01. This settles the open question for the template only; RT1 may measure another. - Shading normal clip. Since October 29, 2026, a front face's shading normal is clipped to the hemisphere of the view direction, as in Peters' renderer (§6.1), with FoundationMath's shared clip and its 0.01 margin. Every strategy uses the clipped normal. RT1 does not clip, so the template and RT1 estimate different integrals at normal-mapped pixels and within 0.01 of the shading horizon; RT1's shading normal is shared by all of its sampler profiles, and clipping it there would be a separate, versioned change to the reference.
- Other MIS pairs. Analytic and environment light samples and the BSDF sample
use the same power heuristic as
W_L, and delta lights keep weight one. - Consistent densities. Both MIS sites evaluate
P̃withEvaluateEmissiveTriangleSelectionPmf, the same target function and the densities at the normalized world direction, soW_Lis one function of the direction.Ẑis computed at most once per invocation, on first use. - Comparison baseline. GPU tests compare against
veach-guibas-miscooked with every pass on the triangle loop, so neither template needs native ray tracing. - The two-sided, minimum-roughness fixture is compared with that template's MIS output instead of its light-only output. Area-uniform light sampling of a light 2 cm below the ceiling has unbounded variance, and its 256-sample mean reads about 4% low. Its MIS and BSDF outputs agree with all three of this template's.
- The MSE check measures error on the tone-mapped image, against that template's 1024-sample MIS output with an independent seed. On decoded linear radiance, the pixels that see emitters directly dominate both errors: 0.079 against 0.080, compared with 0.0011 against 0.0025 tone-mapped on Apple M5.
Evidence and remaining acceptance work, against Acceptance item 3:
- Tested. The GPU tests on Metal cover the glossy Cornell room, the
many-emitter room, a textured emitter and two-sided emitters over
minimum-roughness metal. The weight unit test covers the MIS partition of unity,
Ẑ = 0, the empty LTC clip, and receivers viewed from below their shading horizon, whose clipped frames it compares with RT1's C++ functions. - The textured fixture puts a 4 × 4 linear emission map on the Cornell light,
over rough dielectric surfaces. The map is white, warm, cool and black in
diagonal stripes, so it is zero on a quarter of the light. The power CDF and the
RIS target see only the material's constant
Emission, so a light sample that ignored the map would bias the image. - At 256 samples per pixel on Apple M5, the three outputs' image means are within 0.2% of the MIS template's light-sampling output. Without the map, the image mean rises from 0.116 to 0.204, so the map does reach every estimate.
- Equal-time benchmark. Measured; see below.
- Not run. Vulkan first runs in batch CI.
Equal-time benchmark¶
The opt-in Catch2 tag [.ris-projected-solid-angle-benchmark] in
ludus_render_graph_ray_tests measures Shah et al.'s claim: lower error at equal
time with many lights. Its
usage
is in the authoring guide.
- Scene. The Cornell room has its light replaced by a 40 × 40 field of one-sided emitters 3 cm wide, 3200 emissive triangles in all. Four power levels spanning a factor of 64 and three tints vary between neighbors.
- The diffuse variant has white rough dielectric surfaces (roughness 1).
- The specular variant has white metal at roughness 0.2, which mirrors the emitter field. The two variants follow the paper's diffuse and glossy scenes.
- Packages. Both templates are cooked with every pass on
ComputeBvh, so both use one traversal. - The BSDF-only program of each is replaced by a test program
(
tests/render_graph_query_count.slang). It runs the template's ownShadeMultipleImportanceunchanged and outputs the number of visibility and surface queries it issued, which gives rays per pixel without instrumenting the template. - The MIS output is each template's own.
- Timing.
- The image is 288 × 192: one RGBA8 readback slice holds at most 256 KiB, and the next size up of this aspect ratio does not fit.
- Each package renders 8 warmup and 64 timed frames with per-pass GPU timing, in the order RIS, Veach–Guibas, Veach–Guibas, RIS. Its cost per sample is the mean of its two runs' median kernel times, primary plus MIS shade.
- Accumulate and ToneMap are identical in both templates and are left out.
- Error.
- The reference is the Veach–Guibas MIS output at 4096 samples per pixel, with an independent seed. RIS is read at 4, 16 and 64 samples. Veach–Guibas is read at the same samples, and at the same kernel time.
- MAPE is
|x − r| / (r + 0.01). MAPE is the metric Shah et al. report; the 0.01 guard against black reference channels is this benchmark's own. It is averaged with linear RMSE over the RGB channels of pixels whose decoded reference stays below 4. - Brighter pixels see emitters directly, and the 8-bit readback cannot resolve them; they are about 1.5% of the image. Tone-mapped MSE covers every pixel.
- Failure pixels. In the recorded run, both reference images showed three
failure pixels, which every metric leaves out. They came from a
ComputeBvhtraversal defect, not from either estimator, and are now fixed; see the paragraph after the results.
Results on Apple M5 (Metal) with Slang and Clang 23 from config/tool_versions.json,
October 29, 2026. The machine was shared with other builds, at a load average of 70–95.
| Variant | Template | Primary + MIS kernel per sample | Shadow rays per pixel | BSDF rays per pixel |
|---|---|---|---|---|
| Diffuse | ris-projected-solid-angle |
0.155 + 12.16 ms | 0.913 | 0.473 |
| Diffuse | veach-guibas-mis |
0.411 + 2.28 ms | 0.463 | 0.473 |
| Specular | ris-projected-solid-angle |
0.145 + 11.51 ms | 0.911 | 0.534 |
| Specular | veach-guibas-mis |
0.411 + 2.08 ms | 0.463 | 0.534 |
Rays are averaged over all pixels. About half the image is shaded surface: the BSDF ray count equals the shaded fraction, so each shaded pixel traces about 1.9 shadow rays with RIS and 1.0 with Veach–Guibas. One RIS sample costs 4.6 times as much kernel time as a Veach–Guibas sample (diffuse), or 4.7 times (specular). In an earlier run under heavier GPU contention, the diffuse ratio was about 9, so the equal-time sample counts below are specific to this run.
Each cell gives MAPE / linear RMSE / tone-mapped MSE:
| Variant | RIS samples | RIS | Veach–Guibas, equal samples | Veach–Guibas, equal time (samples) |
|---|---|---|---|---|
| Diffuse | 4 | 0.136 / 0.288 / 0.0088 | 0.357 / 0.339 / 0.0191 | 0.181 / 0.163 / 0.0044 (18) |
| Diffuse | 16 | 0.072 / 0.145 / 0.0021 | 0.192 / 0.172 / 0.0051 | 0.091 / 0.081 / 0.0010 (73) |
| Diffuse | 64 | 0.039 / 0.073 / 0.00046 | 0.098 / 0.087 / 0.0012 | 0.047 / 0.041 / 0.00025 (293) |
| Specular | 4 | 0.119 / 0.300 / 0.0086 | 0.274 / 0.349 / 0.0153 | 0.139 / 0.165 / 0.0033 (19) |
| Specular | 16 | 0.064 / 0.151 / 0.0020 | 0.151 / 0.178 / 0.0040 | 0.071 / 0.083 / 0.00076 (75) |
| Specular | 64 | 0.034 / 0.077 / 0.00044 | 0.077 / 0.090 / 0.00092 | 0.037 / 0.042 / 0.00019 (300) |
What the results show:
- Equal samples. RIS has lower error on every metric, in both variants. MAPE falls by a factor of 2.2–2.7 and tone-mapped MSE by 1.8–2.6. This repeats, at 3200 emitters, the CTest MSE result for sixteen emitters.
- Equal time.
- RIS keeps a lower MAPE, by 8–25%.
- Veach–Guibas has the lower linear RMSE, by a factor of about 1.8, and the lower tone-mapped MSE, by 1.8–2.6.
- MAPE weights relative error, which is dominated by dim regions that uniform emitter selection serves poorly. RMSE is dominated by bright pixels, where the extra Veach–Guibas samples pay off.
- So, at equal time, this implementation reproduces the paper's claim only for MAPE.
- Why RIS is expensive. Its cost is the per-candidate target evaluation. Each of
the 32 candidates clips and integrates two polygons, diffuse and LTC. Shah et al.
evaluate an approximate form factor instead (Hill and Heitz). The
approximate target now does the same, and its
measurements include an
MandKsweep.
The three failure pixels also appeared with the stock ris-projected-solid-angle
package, which showed one by sample 256 at another pixel with this seed.
- Location. They were on the ceiling, where the emitter quads are. Every output
failed at the same pixel and sample, including the BSDF-only output, so the shared
surface load failed, not a sampling strategy.
- Not reproduced on the triangle loop. With the same seed, the triangle-loop
Veach–Guibas package showed no failure through sample 1920, while the ComputeBvh
one failed by then.
- Not a stack overflow. REFERENCE_BVH_MAX_DEPTH bounds the traversal stack.
- Cause. IntersectBvhTriangle divided the edge functions separately. Near the
edge opposite a triangle's first corner, the two float32 quotients could round to
a sum above one. For these tiles that is an outer tile edge: their index order
puts the shared diagonal through the first corner. GetSurfaceHit rejects that
sum, so the pass latched a failure.
- Fix. The traversal now clamps both barycentrics to [0, 1] and bounds the
third by one minus the second, so the float32 sum never exceeds one. The
ComputeBvh regression in render_graph_ray_tests.cpp traces 6144 rays down a
shared diagonal opposite both triangles' first corners. Before the fix, 118 of
those hits failed the reload on Apple M5 (Metal). The
GPU-compute reference records this
float32 departure.
Registration follows the existing template checklist:
- TEMPLATES and the CLI --template choices;
- the root CMake install list and the editor resource list and catalog (graph
and snippet entries);
- the editor compute-template dispatch;
- the identity lists in the Python and editor template tests;
- the renderer GPU staging loop;
- a usage section in the authoring guide beside
Compare light and BSDF sampling with MIS,
added with the template.
RT1 profile split and identity¶
After the template proves the estimator, RT1 gains an explicit integrator setting with two named modes. Under the CPU and GPU reference requirement, the resampled mode lands in the CPU oracle (reference_path.cpp) and the GPU-compute reference (renderer_reference.slang) in the same change, with parity evidence. A CPU-only first step is not a deliverable.
| Mode | Contract |
|---|---|
UniformArea |
The current estimator, including the power-weighted emitter CDF. It stays the default and bit-identical to existing captures (SamplerVersion = 1, 8 dimensions per bounce), and remains the validation oracle. |
ResampledProjectedSolidAngle |
This review's estimator. SamplerVersion = 2, 128 dimensions per bounce. Requires the renderer's LTC table. |
ResampledFormFactor |
The same estimator with the approximate target. SamplerVersion = 3, version 2's random layout and its own frozen M and K. Requires the LTC table. |
- API. The
ReferenceLightSamplingenum onReferenceCaptureDescription, recorded inReferenceCaptureInfo. CpuOracleandNativeQueriesshare the CPU transport;GpuComputeruns the same estimator in its kernels.- The setting joins the integral/accumulation key, so changing it builds a replacement job and resets accumulation.
- The separate GPU preview may adopt the mode later. That does not count toward the reference requirement, which the preview does not yet satisfy.
- LUHDR. The sampler field carries 2 (3 for
ResampledFormFactor), and new flag bit128marks a resampled mode (bit 64 already meansgpu_compute). Todayscripts/reference-hdrrejects any sampler other than 1 and any flag above 127, so old converters refuse these captures rather than mislabel them. Extend the converter and its manifest in the same change. - Versions.
ProfileVersion(LIGHTING_PROFILE_VERSION) is unchanged, because both modes estimate the same truncated RGB integral. Changes to packed words bump the reference kernel layout, and the preview's separateLUDUS_GPU_PREVIEW_KERNEL_VERSIONif the preview adopts the mode. - Acceptance. Both modes are unbiased for the same integral at matched depth, roulette and range, so acceptance is statistical agreement across seeds, not pixel equality.
- CPU and GPU-compute resampled outputs must also agree with each other, on raw emission, direct, indirect and total radiance, with documented tolerances. Their discrete selections can diverge under FP32 differences.
- Record the backend, device and toolchain with the evidence.
- No fallback. A device or package without the LTC table rejects the resampled mode explicitly.
Random layout, sampler version 2¶
Slots 0–7 keep their version-1 meanings. New samples use unused dimensions, aligned to Philox's four-lane blocks:
| Bounce slot | Meaning |
|---|---|
| 0 | Category selection, bounded over N (unchanged) |
| 1–2 | Environment sample, or the diffuse projected-solid-angle sample ω₀ |
| 3–5 | BSDF continuation (unchanged) |
| 6 | Russian roulette (unchanged) |
| 7 | Power-CDF emitter ticket for the UniformArea branch; unused by resampling |
| 8–9 | LTC sample ω₁ |
| 10–11 | Reserved |
12 + 2i, 13 + 2i, i < 32 (12–75) |
Candidate i: power-CDF ticket, reservoir ticket |
76 + k, k < 8 (76–83) |
Normalization candidate k: power-CDF ticket |
| 84–127 | Reserved; future meanings take unused slots and never renumber |
- Add a version-2 stride constant of 128 beside
LUDUS_PATH_RANDOM_DIMENSIONS_PER_BOUNCE, which keeps 8 for version 1. - The largest dimension at
LUDUS_REFERENCE_MAX_DEPTH = 64is2 + 63 · 128 + 127 = 8193 ≤ 65535. Extend thestatic_assertandReferenceBounceDimensionper sampler version. M,Kand the stride are frozen constants of version 2.- Sampler version 3 uses this table unchanged. Its own frozen
MandKmay only be smaller, so it reads a prefix of the candidate and normalization slots.
RT1 implementation notes¶
Usage and evidence are in Light-sampling modes and the GPU-compute reference. The profile follows the estimator, BSDF-hit decision and random table above, with these choices:
- Owners. The CPU estimator is
reference_light_sampling.cpp, called fromreference_path.cpp.renderer_reference.slangports it, with the scene-independent pieces inreference_light_sampling.slang. The slot table and the frozenM,Karerenderer_reference_layout.hconstants. The template keeps its own copy of the scene-independent pieces, so either can change without renumbering the other; sharing one source is possible follow-up work. - Diffuse albedo and
v. As in the template:baseColor · (1 − metallic) · (1 − F₀)clamped to 0.01, andv = 1/2. Both remain open questions. - Emitter alpha. Unlike the template, light samples apply the emitter's alpha
cutoff at the sampled point, because RT1's
UniformAreaalready does. The emitter's sidedness and the secondary range are applied there too. - One kernel. The GPU-compute trace kernel serves every mode; a parameter flag
selects sampler version 2, and a second flag (
128) version 3's approximate target. The LTC table is the kernel's sixth binding, uploaded once perEnableReferenceRenderingand charged to its GPU budget. Kernels without the binding are rejected there, so no capture falls back.LUDUS_REFERENCE_KERNEL_VERSIONis 4 since version 3. - Shadow rays. The CPU path queues up to two shadow rays per vertex, so
NativeQueriestiles need three queries per path. The GPU kernel traces each technique's shadow ray as soon as it is sampled; the order of the sums matches the CPU's. - Probes. Static-lighting diffuse probes reject the resampled mode, because their first receiver is virtual and has no stored frame.
Evidence against Acceptance item 4, on Apple M5 Metal:
- Tested. Each mode agrees with UniformArea across six seeds at depths 1, 3,
4 and 6. CPU and GPU raw AOVs agree within the UniformArea parity tolerances.
Both implementations cover Ẑ = 0, an empty LTC clip, emitters behind every
receiver, finite contributions and weights that partition unity, in the CPU unit
tests, the shader parity kernel and the capture tests.
- UniformArea. Captures stay bit-identical in both implementations.
- Textured emitters. Only the item 5 benchmark below has them. There the image
means of both modes agree within 0.1%, but no test asserts that.
- Not run. Vulkan, WebGPU and NativeQueries captures of the resampled mode,
and sanitizer builds, first run in batch CI.
RT1 equal-time evaluation¶
Evidence against Acceptance item 5. The opt-in Catch2 tag
[.reference-light-sampling-benchmark] in ludus_renderer_render_tests
(source)
renders RT1 in both modes. Its usage is under
Light-sampling modes.
- Scene. The emitter field of the template benchmark, rebuilt as renderer meshes: the Cornell room lit by 3200 one-sided emissive triangles with power spanning a factor of 64.
- One tint's emitters, a third of the field, multiply their emission by a 4 × 4 checker of 1 and 0.25. The power CDF and the target use the constant emission factor, which bounds the textured emission from above.
- The surface finishes match the template's diffuse and specular variants. Each
finish runs with direct lighting only (
MaxDepth1) and at depth 4. - The mixed-light diffuse case runs at depth 4 with a 5 × 5 grid (50 emissive triangles), one directional light through the open front, and a constant sky. The grid preserves the total emissive area of the 40 × 40 field.
- Timing.
- Each mode renders 32 samples per pixel of the 288 × 192 image twice, in the
order
UniformArea, resampled, resampled,UniformArea. - Its cost per sample is the mean of the two runs' median device times of the GPU-compute transport pass. Each pass traces one sample of every pixel.
- Shadow and closest rays per pixel come from
ReferenceCaptureInfo's visibility and closest query counts. - Error.
- The reference is a GPU-compute
UniformAreaimage at 8192 samples per pixel, with an independent seed. - The resampled mode is read at 4, 16 and 64 samples.
UniformAreais read at the same samples, and at the same transport kernel time. - Metrics cover the linear
Direct + IndirectAOVs of every pixel and RGB channel: MAPE|x − r| / (r + 0.01), with the template's guard, and RMSE. Float AOVs need no tone-mapped readback or saturation cut. - Primary emission is left out: both modes evaluate it bit-identically from unchanged random dimensions.
- CPU oracle. The diffuse room at 96 × 64 renders 4 and 16 samples per pixel in
both modes, at depths 1 and 4, against a GPU-compute
UniformAreareference at 8192 samples. Larger extents and the equal-time comparison are GPU-only.
Results on Apple M5 (Metal, macOS 26.6), with the macos-clang-development build
(Clang 23.1.3, Slang 2026.1.2), October 29, 2026. The machine was shared, at a load
average of 5–23. The tag ran three times, taking 4–7 minutes each. Runs 1 and 2
built develop at da35d03e; run 3 built a34fccf1, the base of this change. All
three runs produced bit-identical references and equal-sample images.
| Case | UniformArea kernel per sample |
Resampled kernel per sample | Cost ratio | Shadow rays per pixel, UniformArea / resampled |
|---|---|---|---|---|
| Diffuse, direct | 1.13–1.15 ms | 20.5–21.0 ms | 17.9–18.3 | 0.458 / 0.916 |
| Specular, direct | 0.88–1.29 ms | 20.6–22.1 ms | 16.7–25.2 | 0.458 / 0.913 |
| Diffuse, depth 4 | 2.75–4.14 ms | 85.0–90.1 ms | 20.5–32.7 | 0.992 / 1.981 |
| Specular, depth 4 | 2.29–4.00 ms | 81.6–86.0 ms | 20.4–37.6 | 1.055 / 2.099 |
Closest rays per pixel are equal in both modes: 1.40–1.44 with direct lighting only and 1.97–2.12 at depth 4. As in the template benchmark, rays are averaged over all pixels.
Each error cell gives MAPE / RMSE. The two equal-time columns are runs 1 and 2, whose cost ratios differ. Run 3 has the same ordering in every cell.
| Case | Samples | Resampled | UniformArea, equal samples |
UniformArea, equal time, run 1 (samples) |
Run 2 (samples) |
|---|---|---|---|---|---|
| Diffuse, direct | 4 | 0.083 / 0.064 | 0.164 / 0.140 | 0.043 / 0.033 (73) | 0.043 / 0.033 (73) |
| Diffuse, direct | 16 | 0.042 / 0.031 | 0.088 / 0.071 | 0.022 / 0.017 (292) | 0.022 / 0.017 (293) |
| Diffuse, direct | 64 | 0.022 / 0.017 | 0.045 / 0.036 | 0.011 / 0.0091 (1166) | 0.011 / 0.0091 (1171) |
| Specular, direct | 4 | 0.062 / 0.116 | 0.137 / 0.191 | 0.030 / 0.040 (101) | 0.037 / 0.048 (67) |
| Specular, direct | 16 | 0.032 / 0.057 | 0.073 / 0.096 | 0.016 / 0.020 (403) | 0.019 / 0.024 (268) |
| Specular, direct | 64 | 0.017 / 0.029 | 0.038 / 0.049 | 0.0084 / 0.011 (1610) | 0.010 / 0.013 (1070) |
| Diffuse, depth 4 | 4 | 0.186 / 0.093 | 0.262 / 0.180 | 0.067 / 0.037 (92) | 0.057 / 0.031 (131) |
| Diffuse, depth 4 | 16 | 0.098 / 0.047 | 0.147 / 0.090 | 0.036 / 0.019 (367) | 0.030 / 0.016 (524) |
| Diffuse, depth 4 | 64 | 0.051 / 0.025 | 0.079 / 0.045 | 0.019 / 0.010 (1467) | 0.017 / 0.0087 (2095) |
| Specular, depth 4 | 4 | 0.286 / 0.141 | 0.398 / 0.235 | 0.125 / 0.038 (150) | 0.159 / 0.052 (82) |
| Specular, depth 4 | 16 | 0.183 / 0.071 | 0.274 / 0.116 | 0.069 / 0.019 (602) | 0.090 / 0.026 (327) |
| Specular, depth 4 | 64 | 0.108 / 0.036 | 0.174 / 0.058 | 0.040 / 0.011 (2406) | 0.050 / 0.014 (1307) |
On the CPU oracle at 96 × 64, with the same field and the diffuse finish:
| Depth | Samples | UniformArea |
Resampled |
|---|---|---|---|
| 1 | 4 | 0.184 / 0.141 | 0.104 / 0.064 |
| 1 | 16 | 0.098 / 0.061 | 0.054 / 0.036 |
| 4 | 4 | 0.279 / 0.168 | 0.207 / 0.095 |
| 4 | 16 | 0.159 / 0.081 | 0.110 / 0.048 |
In run 1, the oracle needed 137 ms of wall time for 16 UniformArea samples per
pixel at depth 1 and 1516 ms for 16 resampled samples. At depth 4 it needed 103 ms
and 1110 ms. These times include setup and are not equal-time measurements.
What the results show:
- Equal samples. The resampled mode has lower error in every case and in both implementations.
- With direct lighting only, MAPE falls by 2.0–2.3× and RMSE by 1.6–2.3×.
- At depth 4, MAPE falls by 1.4–1.6× and RMSE by 1.6–1.9×. Indirect bounces gain less than direct light.
- Equal time.
UniformAreahas lower error in every case and in all three runs. - Its MAPE is 1.7–2.1× lower with direct lighting only, and 1.8–3.3× lower at depth 4. Its RMSE is 1.8–2.9× and 2.3–3.7× lower.
- Equal error per unit time needs a cost ratio near the square of the
equal-sample error ratio: about 3–5 with direct lighting only, and 2–4 at depth
- The measured ratio is 17–38.
- Unlike the template. There, RIS kept a lower MAPE at equal time against Veach–Guibas. Two differences probably explain the change; neither was measured on its own:
- RT1's
UniformAreaalready selects triangles by power, where the template's Veach–Guibas baseline selects emitters uniformly. RT1's baseline therefore has less to gain from resampling. - With direct lighting only, as in the template, RT1's
UniformAreatransport costs 0.9–1.3 ms per sample, against 2.7 ms for the template's Veach–Guibas primary and MIS kernels. The resampled transport costs 20–22 ms, against 12.3 ms for the template's RIS. - Cost. It comes from the per-vertex target evaluations; see the template's
explanation in Equal-time benchmark. The
ResampledFormFactormode lowers it; see Approximate target measurements. - Bias. At 64 samples, the resampled image's mean transport luminance is within 0.1% of the reference in each case: 0.07359 against 0.07358, 0.04461 against 0.04464, 0.12527 against 0.12519, and 0.07978 against 0.07972. The textured emitters give no measurable bias, but this is not a test.
Approximate target¶
Implemented October 29, 2026, as the template's RIS_TARGET setting and RT1's
ResampledFormFactor mode (sampler version 3). It replaces the exact E₀ and E₁
of the target by Hill and Heitz's approximation (see
Sources reviewed), which Shah et al. use for speed.
- Approximation. For the triangle's unit vertex directions
dᵢ, the vector form factor isF = (1/2π) Σ r(dᵢ·dᵢ₊₁) dᵢ × dᵢ₊₁, oriented towards the triangle, whereris the course's quadratic fit of θ/sin θ (within 0.73%). The integral isF.zwhen no vertex lies below the horizon, zero when none lies above it, and otherwise the clipped proxy spheremax((|F|² + F.z) / (|F| + 1), 0).E₁applies the same steps after transforming the vertices byM₁⁻¹. FoundationMath owns both asTryApproximateClampedCosinePolygonIntegralandTryApproximateLinearlyTransformedCosinePolygonIntegral, in C++ and Slang. - Departure. Using
F.zfor triangles entirely above the horizon reproduces the course's tabulated variant there, where the table-free fit would not. Results are clamped to[0, 1], which the fitted ratio can exceed slightly. - Support. The proxy sphere can lie below the horizon while part of the
triangle is above it, so the approximate
E₀alone can be zero where the exact one is positive. The target therefore usesẼ₀ = max(E₀ approximation, 2⁻⁶ |d₀ · (d₁ × d₂)|)whenever a vertex lies above the horizon. The triple product is positive for every triangle whose plane does not contain the receiver, which is exactly when the exact projected solid angle can be positive. Sop̂ > 0wherever the triangle has area above the horizon and positive emission, and the estimator's expected value is unchanged. The FoundationMath, RT1 unit and template weight tests check this on random triangles, including ones the sphere alone would drop. - What stays exact. The candidate weights,
ẐandP̃all use the approximatep̂, at both MIS sites. The selected triangle and a BSDF hit's triangle still clip and prepare both polygons, because their samples, densitiesp₀,p₁and Peters' coefficientsc_tneed the exact integrals. Changing the target changes only variance and cost. - Cost per vertex. The exact target prepares two polygons for each of
M + Kcandidates and for the selected triangle. The approximate one evaluates three fitted edge terms twice per candidate and prepares polygons only for the selected triangle and BSDF hits. - Identity. RT1's version 2 keeps the exact target and its captures. Version 3
shares version 2's random slots and reads a prefix of its candidate and
normalization slots, with frozen
MandKchosen from the measurements. The LUHDR sampler field records 3, with the resampled flag 128. The template'sRIS_TARGETdefaults to the approximation;0restores the exact target.
Approximate target measurements¶
Both opt-in benchmarks measured the approximate target with the method of
Equal-time benchmark and
RT1 equal-time evaluation, on Apple M5 (Metal,
macOS 26.6), macos-clang-development with Clang 23.1.3 and Slang 2026.1.2, on
October 29, 2026. The machine was shared with other builds, at a load average of
22–78. Kernel times of one package varied by up to 1.7× between runs under that
load, so costs are compared within one run.
Template. One run of [.ris-projected-solid-angle-benchmark] cooked the exact
target and seven RIS_TARGET = 1 variants
(LUDUS_RIS_PROJECTED_SOLID_ANGLE_BENCHMARK_VARIANTS), all measured against the
same Veach–Guibas reference and its own timing rounds. In this run Veach–Guibas's
MIS shade kernel took 0.56–0.69 ms per sample, against 2.08–2.28 ms in the earlier
recorded run, so its equal-time sample counts are three times as high as there.
Primary plus MIS shade kernel per sample, and its ratio to Veach–Guibas:
| Target | M |
K |
Diffuse | Specular |
|---|---|---|---|---|
| Exact | 32 | 8 | 13.25 ms (16.4×) | 12.13 ms (18.1×) |
| Form factor | 32 | 8 | 3.33 ms (4.1×) | 3.07 ms (4.6×) |
| Form factor | 16 | 8 | 2.60 ms (3.2×) | 2.37 ms (3.5×) |
| Form factor | 16 | 4 | 2.51 ms (3.1×) | 2.14 ms (3.2×) |
| Form factor | 8 | 8 | 2.21 ms (2.7×) | 2.13 ms (3.2×) |
| Form factor | 8 | 4 | 1.95 ms (2.4×) | 1.80 ms (2.7×) |
| Form factor | 8 | 2 | 1.97 ms (2.4×) | 1.82 ms (2.7×) |
| Form factor | 4 | 8 | 1.88 ms (2.3×) | 1.70 ms (2.5×) |
| Veach–Guibas | — | — | 0.81 ms | 0.67 ms |
Shadow and BSDF rays per pixel do not depend on the target: 0.91 and 0.47 (diffuse), 0.91 and 0.53 (specular).
At 64 samples per pixel, each cell gives MAPE / linear RMSE / tone-mapped MSE, then Veach–Guibas at the same kernel time, with its sample count:
Target, M, K |
Diffuse | Veach–Guibas, equal time | Specular | Veach–Guibas, equal time |
|---|---|---|---|---|
| Exact, 32, 8 | 0.0387 / 0.0734 / 0.00046 | 0.0271 / 0.0248 / 0.000084 (1049) | 0.0341 / 0.0771 / 0.00044 | 0.0207 / 0.0249 / 0.000058 (1157) |
| Form factor, 32, 8 | 0.0389 / 0.0734 / 0.00046 | 0.0498 / 0.0431 / 0.00028 (263) | 0.0342 / 0.0771 / 0.00044 | 0.0373 / 0.0428 / 0.00019 (293) |
| Form factor, 16, 8 | 0.0409 / 0.0743 / 0.00048 | 0.0559 / 0.0488 / 0.00037 (206) | 0.0362 / 0.0788 / 0.00046 | 0.0422 / 0.0484 / 0.00025 (226) |
| Form factor, 16, 4 | 0.0409 / 0.0743 / 0.00048 | 0.0568 / 0.0494 / 0.00038 (198) | 0.0362 / 0.0789 / 0.00046 | 0.0444 / 0.0504 / 0.00028 (204) |
| Form factor, 8, 8 | 0.0438 / 0.0755 / 0.00051 | 0.0603 / 0.0527 / 0.00043 (175) | 0.0390 / 0.0804 / 0.00049 | 0.0444 / 0.0504 / 0.00028 (204) |
| Form factor, 8, 4 | 0.0439 / 0.0755 / 0.00051 | 0.0640 / 0.0559 / 0.00049 (155) | 0.0390 / 0.0804 / 0.00049 | 0.0479 / 0.0541 / 0.00033 (172) |
| Form factor, 8, 2 | 0.0439 / 0.0755 / 0.00051 | 0.0638 / 0.0558 / 0.00049 (156) | 0.0390 / 0.0804 / 0.00049 | 0.0478 / 0.0541 / 0.00033 (173) |
| Form factor, 4, 8 | 0.0478 / 0.0770 / 0.00056 | 0.0652 / 0.0572 / 0.00051 (149) | 0.0429 / 0.0829 / 0.00053 | 0.0493 / 0.0559 / 0.00036 (162) |
Veach–Guibas at 64 samples measured 0.0976 / 0.0867 / 0.0012 (diffuse) and
0.0767 / 0.0902 / 0.00092 (specular). The new defaults, M = 8 and K = 4, at
fewer samples:
| Variant | Samples | Default RIS | Veach–Guibas, equal time (samples) |
|---|---|---|---|
| Diffuse | 4 | 0.157 / 0.295 / 0.0095 | 0.237 / 0.214 / 0.0082 (10) |
| Diffuse | 16 | 0.083 / 0.149 / 0.0023 | 0.124 / 0.112 / 0.0019 (39) |
| Specular | 4 | 0.141 / 0.316 / 0.0095 | 0.179 / 0.213 / 0.0061 (11) |
| Specular | 16 | 0.074 / 0.159 / 0.0022 | 0.093 / 0.110 / 0.0013 (43) |
What the template results show:
- Same error, a quarter of the cost. At
M = 32,K = 8the approximate target has the exact target's error to within 1% at every budget, for 25% of its kernel time. Its equal-sample gain over Veach–Guibas (MAPE 2.2–2.6×) is unchanged. MandK. Fewer candidates raise equal-sample error slowly (MAPE +13% atM = 8, +23% atM = 4) and lower cost.Kchanges no error at three significant digits and costs about 0.1–0.35 ms per sample from 8 to 4. MAPE × √time is lowest atM = 8,K = 4in both variants, within about 7% ofM = 16andM = 4;K = 2measured no further gain. These are the new defaults.- Equal time. With the defaults, RIS has a lower MAPE than Veach–Guibas at every budget: 31–34% lower (diffuse) and 19–22% lower (specular). With the exact target in the same run it was 42–65% higher, because this run's Veach–Guibas kernel was fast. Veach–Guibas keeps the lower linear RMSE, by 1.3–1.5×, and the lower tone-mapped MSE, by 1.04–1.19× (diffuse) and 1.5–1.7× (specular).
- What limits RMSE. RIS's linear RMSE barely moves with
M,Kor the target (0.073–0.077 diffuse at 64 samples), while MAPE does. The squared error therefore comes from pixels where emitter selection is not the bottleneck; which term of the estimator dominates there was not measured.
RT1. [.reference-light-sampling-benchmark] renders all three modes in each
run, timed in the order UniformArea, version 2, version 3, version 3, version 2,
UniformArea. Version 3's M and K are compile-time constants, so each setting
needs its own build and run. Equal-sample errors are deterministic: every run that
renders a given mode and setting reproduces them bit for bit.
Transport kernel time per sample in run 1 (version 3 with M = 32, K = 8), and
its ratio to UniformArea:
| Case | UniformArea |
Version 2, exact target | Version 3, form factor |
|---|---|---|---|
| Diffuse, direct | 1.10 ms | 21.8 ms (19.7×) | 4.15 ms (3.8×) |
| Specular, direct | 1.63 ms | 20.5 ms (12.6×) | 4.42 ms (2.7×) |
| Diffuse, depth 4 | 3.72 ms | 85.6 ms (23.0×) | 16.8 ms (4.5×) |
| Specular, depth 4 | 3.40 ms | 87.8 ms (25.8×) | 18.1 ms (5.3×) |
Shadow rays per pixel are those of version 2 within 0.2%. The specular
UniformArea time above is high for this machine. In other runs it measured
0.74–2.66 ms, so its ratios are less certain than the others.
Version 3 against UniformArea at the same kernel time in run 1, as MAPE / RMSE
(UniformArea samples in parentheses):
| Case | Samples | Version 3 | UniformArea, equal time |
|---|---|---|---|
| Diffuse, direct | 4 | 0.084 / 0.064 | 0.090 / 0.074 (15) |
| Diffuse, direct | 16 | 0.042 / 0.031 | 0.047 / 0.037 (60) |
| Diffuse, direct | 64 | 0.022 / 0.017 | 0.024 / 0.019 (240) |
| Specular, direct | 4 | 0.063 / 0.116 | 0.088 / 0.115 (11) |
| Specular, direct | 16 | 0.032 / 0.057 | 0.045 / 0.060 (43) |
| Specular, direct | 64 | 0.017 / 0.029 | 0.024 / 0.031 (173) |
| Diffuse, depth 4 | 4 | 0.186 / 0.093 | 0.139 / 0.084 (18) |
| Diffuse, depth 4 | 16 | 0.098 / 0.047 | 0.075 / 0.042 (72) |
| Diffuse, depth 4 | 64 | 0.051 / 0.025 | 0.040 / 0.021 (289) |
| Specular, depth 4 | 4 | 0.286 / 0.141 | 0.253 / 0.102 (21) |
| Specular, depth 4 | 16 | 0.183 / 0.071 | 0.156 / 0.051 (85) |
| Specular, depth 4 | 64 | 0.108 / 0.036 | 0.088 / 0.026 (340) |
Version 3's equal-sample errors match version 2's within 1% in every case, as in the template. For comparison, version 2 at equal time in the same run lost by 1.5–2.8× in MAPE.
Equal-sample MAPE / RMSE at 64 samples for each version 3 setting measured, with
UniformArea at 64 samples:
| Case | M = 32, K = 8 |
M = 16, K = 8 |
M = 8, K = 4 |
UniformArea |
|---|---|---|---|---|
| Diffuse, direct | 0.0221 / 0.0171 | 0.0239 / 0.0200 | 0.0266 / 0.0226 | 0.0453 / 0.0358 |
| Specular, direct | 0.0168 / 0.0289 | 0.0188 / 0.0313 | 0.0217 / 0.0355 | 0.0375 / 0.0494 |
| Diffuse, depth 4 | 0.0514 / 0.0247 | 0.0543 / 0.0275 | 0.0582 / 0.0304 | 0.0793 / 0.0446 |
| Specular, depth 4 | 0.108 / 0.0359 | 0.119 / 0.0395 | 0.132 / 0.0447 | 0.174 / 0.0582 |
On the CPU oracle at 96 × 64 (diffuse room, run 1), version 3 also matched version 2's error. Its wall time to 16 samples was 1.6 s against 2.4 s with direct lighting, and 3.6 s against 5.0 s at depth 4. These times include setup and are not equal-time measurements; the oracle's exact polygon preparation for the selected triangle and BSDF hits is a larger share of its time.
Run 2 repeated M = 32, K = 8 on this change's final code, merged with
develop. It reproduced every equal-sample error of run 1 bit for bit. Its
UniformArea kernel times differed by up to 1.7×: 0.65, 1.60, 5.00 and 4.86 ms
per sample in the four cases. Version 3 took 4.26, 4.17, 18.7 and 16.9 ms. At equal
time (MAPE / RMSE, UniformArea samples in parentheses):
| Case | Samples | Version 3 | UniformArea, equal time |
|---|---|---|---|
| Diffuse, direct | 64 | 0.022 / 0.017 | 0.018 / 0.015 (419) |
| Specular, direct | 64 | 0.017 / 0.029 | 0.024 / 0.031 (167) |
| Diffuse, depth 4 | 64 | 0.051 / 0.025 | 0.043 / 0.023 (240) |
| Specular, depth 4 | 64 | 0.108 / 0.036 | 0.106 / 0.032 (223) |
Because the equal-sample errors are deterministic, the cost ratio at which version
3 breaks even with UniformArea follows from them: the squared ratio of their
64-sample errors. A measured cost ratio below it wins at equal time:
| Case | Break-even cost ratio, RMSE / MAPE | Measured, run 1 / run 2 |
|---|---|---|
| Diffuse, direct | 4.4 / 4.2 | 3.8 / 6.5 |
| Specular, direct | 2.9 / 5.0 | 2.7 / 2.6 |
| Diffuse, depth 4 | 3.3 / 2.4 | 4.5 / 3.7 |
| Specular, depth 4 | 2.6 / 2.6 | 5.3 / 3.5 |
Version 2's measured ratios were 12.6–30.6 in the same runs.
What the RT1 results show:
- Cost. The approximate target cuts version 2's transport kernel time by
4.6–5.3× at the same
MandK, with the same equal-sample error. - Equal time. Version 3 moves RT1's resampled mode from a clear loss to about break-even, so the outcome depends on this machine's timing noise.
- Specular, direct. Version 3 wins in both runs, by 28–30% in MAPE and 6–8% in RMSE at 64 samples.
- Diffuse, direct. The cost ratio straddles break-even. Version 3 won in run 1
(by 7% in MAPE, 9% in RMSE at 64 samples) and lost in run 2 (by 20% and 17%),
where
UniformArea's kernel ran 1.7× faster. - Depth 4.
UniformAreastill wins, by up to 1.3× in MAPE and 1.4× in RMSE. In run 2's specular room the MAPE was nearly level (2%). Each indirect vertex pays for resampling, and indirect bounces gain less from it. - Version 2. It lost every case by 1.5–2.8× in MAPE. So Shah et al.'s equal-time claim holds in RT1 for specular direct lighting. The other cases now measure 0.9–2.0× their break-even cost, against 2.5–10× for version 2.
MandK. Fewer candidates raise RT1's error faster than the template's. FromM = 32toM = 8, RMSE² grows by 1.5–1.75×, soM = 8would have to cost under 0.57–0.66× as much; it measured about 0.7× at best, because the candidates are only part of the kernel. Timing noise between runs is too large to separateM = 16fromM = 32on time: the sameM = 8kernel measured 3.0 and 4.7 ms with direct lighting.M = 16's RMSE² is 17–37% higher, for about half of the timeM = 8saves. Version 3 therefore freezesM = 32andK = 8, version 2's counts.K = 4atM = 32was not measured in RT1.
Mixed-light and many-emitter measurements¶
Both diffuse rooms ran at depth 4 on Apple M5 (Metal, macOS 26.6) with
macos-clang-development (Clang 23.1.3, Slang 2026.1.2), November 4, 2026.
Run the GPU benchmark with Catch2's -c 'diffuse room, depth 4' or
-c 'mixed-light diffuse room, depth 4' to select the scene. Both use the timing
and independent 8192-sample reference described above. Each error cell gives
guarded MAPE / linear RMSE for direct plus indirect lighting. The matched-time
sample counts and kernel timings are from one run per scene on a shared machine.
| Scene | Resampled mode | Kernel ms/sample, resampled / UniformArea |
Resampled at 64 spp | UniformArea at 64 spp |
UniformArea at equal time |
|---|---|---|---|---|---|
| 3200 emissive triangles | Projected solid angle | 159.39 / 2.00 | 0.0504 / 0.0248 | 0.0782 / 0.0448 | 0.0122 / 0.0064 (5096 spp) |
| 3200 emissive triangles | Form factor | 19.65 / 2.00 | 0.0505 / 0.0248 | 0.0782 / 0.0448 | 0.0274 / 0.0147 (628 spp) |
| 50 emissive triangles, Sun and sky | Projected solid angle | 185.87 / 1.99 | 0.1061 / 0.0848 | 0.1129 / 0.0904 | 0.0160 / 0.0122 (5977 spp) |
| 50 emissive triangles, Sun and sky | Form factor | 20.82 / 1.99 | 0.1061 / 0.0848 | 0.1129 / 0.0904 | 0.0383 / 0.0291 (669 spp) |
Both resampled modes lower 64-sample RMSE by about 45% in the many-emitter
room and 6% in the mixed-light room. UniformArea has lower error at all three
matched-time budgets (4, 16 and 64 resampled spp) in both rooms. These are
comparisons of complete RT1 sampling modes. To measure the effect of source
selection on its own, keep the incident proposal fixed and vary the source
selection PMF.
Veach–Guibas source selection at equal GPU time¶
The opt-in [.mesh-power-benchmark] case in ludus_render_graph_ray_tests
compares the uniform and mesh-power selectors in the same Veach–Guibas graph.
The diffuse Cornell room has one independent mesh source per ceiling tile, with
emitted luminance varying across the tiles. One scene has 576 mesh sources. The
other has 25 mesh sources plus a directional light and a constant sky. Both
selectors use the same incident proposal, Philox seed, and sample dimensions.
The Metal run on Apple M5 used 288 × 192 pixels and the compute BVH. It averaged the sum of the median primary and MIS shade kernel times from two timing rounds per selector, in uniform, mesh power, mesh power, uniform order. Each round used 8 warmup and 64 measured frames. The benchmark rounds the matched uniform sample count to the nearest whole sample. Error uses decoded linear RGB from the tone-mapped readback, over pixels with all reference channels at most 4. The reference uses uniform selection at 4096 spp and an independent seed. Linear error covered 54,609 pixels for the many-mesh scene and 54,891 for the mixed scene. Both scenes had zero failure pixels.
| Scene | Uniform kernel ms/sample | Mesh power kernel ms/sample |
|---|---|---|
| 576 mesh lights | 0.944 | 1.052 |
| 25 mesh lights, Sun and sky | 0.966 | 1.010 |
Each error cell gives linear RMSE and guarded MAPE.
| Scene | Mesh power spp | Mesh power | Uniform at equal spp | Uniform at equal GPU time |
|---|---|---|---|---|
| 576 mesh lights | 4 | 0.24634 / 0.20198 | 0.27867 / 0.34110 | 0.27867 / 0.34110 (4 spp) |
| 576 mesh lights | 16 | 0.12744 / 0.10795 | 0.14312 / 0.18187 | 0.13606 / 0.17177 (18 spp) |
| 576 mesh lights | 64 | 0.06260 / 0.05593 | 0.07066 / 0.09331 | 0.06682 / 0.08872 (71 spp) |
| 25 mesh lights, Sun and sky | 4 | 0.25703 / 0.27073 | 0.30298 / 0.35412 | 0.30298 / 0.35412 (4 spp) |
| 25 mesh lights, Sun and sky | 16 | 0.12543 / 0.15147 | 0.14559 / 0.19025 | 0.14121 / 0.18406 (17 spp) |
| 25 mesh lights, Sun and sky | 64 | 0.06340 / 0.07553 | 0.07332 / 0.09483 | 0.07171 / 0.09263 (67 spp) |
Mesh power reduced RMSE at matched GPU time in both rooms. At the 64-spp budget the reduction was 6.3% with 576 mesh lights and 11.6% in the mixed scene. These measurements cover direct illumination on one device.
Ownership¶
| Piece | Owner | Notes |
|---|---|---|
| Convex polygon horizon clipping (up to seven input vertices), clipped projected-solid-angle integral, Peters sampler and density | FoundationMath projected_solid_angle.hpp and shaders/projected_solid_angle.slang, implemented by the polygonal-light work |
The sampler is ported from the BSD-licensed polygon_sampling.glsl with its notice. Clipping is an independent reimplementation. This design consumes these primitives and adds none of its own. |
| Generic LTC polygon transform | FoundationMath, beside those primitives | Implemented as linearly_transformed_cosine.hpp and shaders/linearly_transformed_cosine.slang (contract): transform the vertices by M⁻¹, then reuse the same clipping, integral and sampler. Credits Heitz et al. 2016 (§3.1, Eq. 3, Algorithm 1) and Peters 2021 §4.1. |
| Shading normal clipped to the view's hemisphere (Peters §6.1) | FoundationMath | Implemented as shading_normal.hpp and shaders/shading_normal.slang (contract), with Ludus's 0.01 margin. The polygonal-light and RIS templates clip front faces with it; RT1 does not clip. |
| Sequential weighted reservoir | FoundationMath | Implemented as weighted_reservoir.hpp and shaders/weighted_reservoir.slang (contract): explicit order, zero totals, and selection-frequency tests on exact small integers. The ReSTIR template may adopt it separately; this design does not change that template. |
| LTC table for Ludus GGX/correlated Smith/Schlick, its fitter and provenance | Renderer (embedded table and a GPU buffer) plus an offline Ludus tool. Owned by this RIS/many-area-light workstream (decided October 27, 2026). The polygonal-light LTC templates reuse the same table and fitter rather than building their own. | Implemented as tools/ltc-fit, data/ltc_table.bin with its provenance and shaders/ltc_table_layout.h, and the ltc_table.slang and CPU lookups (fit, regeneration and accuracy). Reimplemented from Heitz et al. §4 and Peters §§4.1–4.2, not from GPLv3 utilities. Like Peters' renderer it has 51 F0 slices, chosen by the luminance of F0; the albedo stays exact per channel. It covers roughness down to 0.045. A poor fit costs variance, not correctness. Render graphs bind it with scene_interface.polygonal_light_sampling. |
| Compute-BVH graph traversal strategy | Renderer, at the shared traversal boundary | Implemented as ComputeBvh, sharing the RT1 GPU reference's BVH traversal. Shared with the polygonal-light templates; a prerequisite for both |
| Power-CDF scene-interface accessors | Renderer | Implemented as SelectEmissiveTriangle and EvaluateEmissiveTriangleSelectionPmf in scene_shader.slang (usage). They read the CDF that PackReferenceEmitterSelection packs for RT1, and the private C++ counterparts SelectReferenceEmissiveTriangle and EvaluateReferenceEmissiveTriangleSelectionPmf match them in the [power-cdf] GPU parity test |
| Diffuse albedo approximation, RT1 path state | Renderer | One packed CDF serves RT1 and graph shaders |
Acceptance¶
- Primitives. The projected-solid-angle primitives carry their own C++ and Slang parity tests from the polygonal-light work. This design adds parity tests for the reservoir and the LTC transform.
- LTC table. Integral checks against Monte Carlo estimates of the exact BRDF.
- Template. Python template tests modeled on
VeachGuibasMisTemplateTests, including capability rejection for the selected traversal. GPU tests on Cornell, many-emitter, textured, two-sided, metallic and minimum-roughness fixtures: - the three outputs agree with each other and with
veach-guibas-mislight-only within 2% of the mean at 256 spp; - MIS mean squared error against a 1024-spp image is below
veach-guibas-misMIS at equal spp on the many-emitter fixture; - an opt-in equal-time benchmark that reports shadow rays per pixel and kernel time.
- RT1 resampled profile. The CPU oracle and the GPU-compute reference in one change.
- Each agrees statistically with
UniformAreaacross seeds and depths, and CPU and GPU agree with each other on raw outputs. - Edge cases are covered in both paths:
Ẑ = 0, an empty LTC clip, all emitters behind the receiver, invalid contributions, and a unit test that the MIS weights sum to one. - Evaluation. Equal-sample and equal-time error against high-sample
UniformAreareferences, reporting MAPE as in Shah et al. §5 plus RMSE, on scenes with thousands of emissive triangles.
Out of scope:
- reservoir reuse (RT4) and light-level reservoirs in restir-di, which would
need their own review;
- a light tree;
- raster LTC area lighting;
- anisotropic roughness;
- polygons beyond triangles;
- Shah et al.'s naive per-candidate variant.
Open questions, to settle by measurement:
- Peters' v; K is settled for the template (K = 4 changed no error at three
significant digits; see Approximate target measurements)
but not measured below 8 in RT1;
- the diffuse albedo approximation;
- LTC table resolution (the table follows Peters' 64 × 64 × 51 cells; see
its accuracy);
- whether two shadow rays beat Peters' one-sample variant for this workload.