Low-level renderer systems architecture¶
Status: L0 scene submission, L1 views/depth, L2 mutable versions/overlays and
L3 material/content versions and L4 lighting baseline implemented, October 11, 2026. L4 starts from merged
L3 at f9cc7c8. L5 portable scaling is implemented with workload-specific measurement gates; L6 implements the RT0/RT1 static-query and progressive-reference scope below; other advanced effects remain plans.
Correctness fixtures do not establish a measured speedup.
The initial decisions below were saved before opening the Gems article map.
The reference review records the subsequent
article readings and the concrete revisions now incorporated below. Its
conference and journal plan
maps GPC, REAC and other sources to Ludus's owners, phases and validation gates;
recommended full readings remain distinct from completed source inspection.
Existing RHI, camera, resource and text documents retain their respective detailed
contracts.
Build a small data-oriented GraphicsRenderer over the existing
RHI and GraphicsDevice design. Use immutable frame snapshots,
explicit output views, versioned mesh/material resources, bounded submissions,
and a fixed readable pass sequence. Start with CPU visibility and conventional
forward shading. Add clustered lighting and GPU visibility as independently
measured capability paths; keep a direct-draw reference path for debugging.
“Best” means predictable correctness and useful performance for a specified workload. It does not mean that one technique wins on desktop, integrated GPUs, tile GPUs and browsers. An AAA architecture needs scalable contracts and strong inspection tools; it need not begin with every expensive rendering technique.
Initial decisions before article review¶
| Decision | Rationale |
|---|---|
| Renderer owns scene policy; GDI owns work planning; RHI owns native state and lifetime | Preserve existing owners rather than introduce a second device wrapper |
| Compact immutable snapshots and packets, one serialized submission owner | Separate scene mutation from resource use and make captures explainable |
| Stable view identity distinct from camera, target, viewport and logical screen | Support split screen, editor previews and offscreen work without global current state |
| CPU frustum culling, material sorting and adjacent instancing first | Small portable correctness oracle and useful initial optimization |
| Forward raster baseline; clustered forward extension | Share opaque and transparent lighting and retain a simple WebGL path |
| Offline shader packages, explicit material templates and immutable instances | Bound permutations and remove shader compilation/name lookup from draws |
| Ordinary ordered passes with declared uses | Precise hazards and pooling without an opaque scheduler |
| Completion-scoped upload/frame storage and transactional version publication | Avoid in-flight overwrites and half-published reloads |
| Linear-light shading/composition with explicit final output encoding | Prevent gamma errors and inconsistent UI blending |
| Triangle-expanded debug lines and painter-ordered glyph/sprite quads | Portable width and ordering with reusable batching |
| Baked lightmaps/probes plus dynamic direct lighting | Useful quality without requiring real-time global illumination |
| Reference modes, bounded traces and workload gates before advanced features | Optimize with evidence while retaining inspectable behavior |
Repository fit and current limits¶
Read AGENTS.md, ADR 0003
and the foundational include model before implementation.
Use Ludus aliases and owned containers/algorithms, explicit results, noexcept
on infrastructure paths, and small public headers. New borrowed-view APIs belong
to Ludus; the R1 std::span exception does not automatically extend to Renderer.
No public native API types, runtime STL caches, exception-based dependency or
global registration constructors are implied by this proposal. Document new
public contracts with Doxygen and source attribution beside affected code.
| Inspected owner | Shipped boundary | Renderer implication |
|---|---|---|
| R1 portable raster | Vulkan, Metal, WebGPU and WebGL 2; indexed instanced triangles, immutable resources, one acquired color target and private depth | A small flat/unlit consumer can use it now; general pass attachments, streaming and modern depth policy need explicit extensions |
| RHI/GDI architecture | R2 lifetime services, R3 ordered graph and R4 buffer compute/indirect profiles are implemented; R5 remains proposed | Reuse retained batches/completion and authored graph passes; storage textures, raster-stage storage and general mutable uploads remain deferred |
| Camera systems | C0 evaluation is implemented; advanced rigs/history remain proposed | Consume CameraSample; renderer owns projection, jitter, render-origin conversion and history |
| Text and FontSystem | Native CPU shaping/rasterization plus L2 GraphicsText R8 atlas/Renderer adapter; browser consumes copied coverage fixtures, CPU font bootstrap remains pending in the F0–F6 ledger | Keep shaping independent; standalone F0–F6 gates and browser CPU font dependencies remain incomplete |
| Resources | Content/audio portions exist; general coordinator and graphics streaming are proposed | Do not block the first scene on a universal asset manager |
| Frame update, world, UI | Separate simulation, presentation and UI ownership | Extraction adapters sit above Renderer; it must not depend on an ECS or UI toolkit |
The R1 compatibility frame still clears depth to one and defaults to less-than
depth. L1 adds explicit pass clear depth and pipeline comparison state. FoundationMath's
intended 3D convention is reverse-Z [0,1]. Pair reverse-Z projections with clear zero and Greater explicitly. The L1
fixtures prove ordering on native and browser backends; existing consumers keep
their conventional default.
Existing R1 consumers keep their documented behavior. L3 expanded the initial 16
records per kind to 32 and supplied immutable R8, RG8 and linear/sRGB RGBA8 mip
chains. L4 expands records to 64 and adds the bounded lighting profile below. One binding group remains the portable profile;
this bounded content slice does not establish a general streaming/lighting contract.
Portable scene submission (L0)¶
Ludus::GraphicsRenderer now supplies the first portable static scene consumer.
Its public contract is renderer.hpp.
It depends on FoundationMath and GraphicsRhi, with FoundationContainers privately;
application adapters provide ordinary values without an ECS, UI or gameplay-camera
link. Native and browser builds export the same component.
Compile the installed flat shader during the application build:
find_package(Ludus CONFIG REQUIRED COMPONENTS GraphicsRenderer)
target_link_libraries(my_app PRIVATE Ludus::GraphicsRenderer)
ludus_compile_shader(TARGET my_app NAME renderer_flat RASTER
SOURCE "${Ludus_DIR}/shaders/renderer_flat.slang"
VERTEX vertexMain FRAGMENT fragmentMain)
Create a ready portable-raster device, then initialize Renderer with the generated
renderer_flat::Vertex() and Fragment() descriptions. Poll pending setup on later
owner-thread turns. CreateMesh copies finite position-only geometry and uint32
mesh-local indices; poll a pending mesh before snapshot admission. CreateSnapshot
copies SceneItem transforms, flat materials and nonzero unique application source
IDs, and retains each mesh incarnation. Editing the original items or releasing a
mesh cannot change an accepted snapshot. A new snapshot cannot use a detached mesh.
PrepareView copies a finite conventional world-to-clip transform, derives visibility
and creates one immutable instance buffer during setup. Sorting/instance scratch is
reserved in renderer-owned storage at initialization, keeping preparation within the
browser default stack rather than requiring application stack overrides. Repeated Submit only records
bounded retained R2 packets; it performs no renderer heap allocation or resource
creation. The prepared view remains reusable. Releasing its source snapshot/meshes
is legal. The application sets the drawable extent/clear through RHI SetFrameTarget
between frames before submission; zero extent retains work for retry. Releasing the
view drops CPU ownership while accepted GPU uses retire in
RHI. Poll the returned completion independently from presentation. A skipped target
retains a finished batch for retry; terminal failure is still explicit. Reset the
renderer between frames before destroying its borrowed device.
L0 compatibility defaults use six mesh slots, four snapshot slots, eight prepared views and 256 objects per snapshot/view. L5.1 makes these capacities configurable. Retained versions consume slots until their last consumer releases. The six meshes require twelve RHI buffers and four nonempty views require four more; other RHI consumers and GPU retirement can exhaust the configured RHI budget earlier. Setup failures preserve public outputs; Ready/Pending creation alone grants ownership. Large geometry is also constrained by enabled RHI buffer limits. These are bounds of this initial profile, not performance targets or a streaming budget.
Opaque objects use the view transform, depth clear one and less-than depth testing. Use near-zero/far-one projection; do not pass FoundationMath's reverse-Z factories unchanged. Use the L1 view builder and explicit frame/pass path below for reverse-Z. Overlays use their own local-to-clip affine transform, ignore the world view and render after opaque draws without depth testing. Overlay inputs use straight linear RGBA; preparation converts RGB to premultiplied values. Unauthored flat materials default to opaque magenta. General textures, arbitrary shaders/material templates, submeshes and live instance updates remain later phases. L1 adds viewport/scissor and offscreen composition.
Visibility reuses FoundationMath's outward-rounded affine AABB transform and checked
frustum classification, including reflection/shear and boundary contact. Overlays are
classified against the canonical clip volume. Opaque mesh sorting uses the new
caller-scratch FoundationContainers::StableSort only within mutually disjoint
projected-bound runs. Near-plane crossings block reordering. Overlapping geometry
keeps input order, including coplanar depth ties; painter overlays always keep input
order. Adjacent compatible packets instance through a fixed 80-byte record containing
four clip-transform rows and a color. This conservative ordering may batch less than
a general opaque state sort; measure before extending it. ViewReport records actual
source order, visibility and draw counts. ViewDescription::Reference disables
visibility, sorting and instancing while preserving the same opaque/overlay semantics.
The public-only SDK consumer and shared scene exercise the installed shader, detached source ownership and 120 submissions. Native pixel tests and the browser harness compare optimized and unsorted direct images, with independent color samples to reject equally blank images. Unit tests cover empty and 256-object scenes, tangency/shear, painter ordering, coplanar overlap, invalid/stale identities, pending dependencies, capacity recovery, skipped targets and GPU retention. Metal and pinned Chromium WebGPU/WebGL 2 are local qualification paths; Linux Vulkan validation and supported-host sanitizers are CI gates. Software GPU checks make no physical GPU performance claim.
Views and depth (L1)¶
The view API
provides allocation-free ResolveViewMapping, SplitViewRectangle, physical/window
pointer conversion and BuildViewDescription. A resolved mapping carries the actual
drawable dimensions, rounded viewport, owning region, origin, per-axis scale, crop
transform and IntegerFit fallback. Input uses top-left pixel-edge coordinates and
half-open regions. Window input receives explicit physical pixels per window unit;
DPI is applied once. Picking rejects bars/cropped exterior unless clamping is requested.
Zero extent returns NotReady without replacing outputs. Checked shared split edges
cover odd dimensions without cracks. Mapping extents are bounded to 2^24 so binary32
pixel edges remain representable.
BuildViewDescription reuses FoundationMath's right-handed reverse-Z factories for
centered orthographic, finite perspective and infinite perspective. It uses the logical
screen aspect. Its default ProjectionSpace::LogicalScreen produces an uncropped
offscreen image; presentation applies Fill crop once. For direct output-region drawing,
request ProjectionSpace::OutputRegion and use the mapping viewport/scissor; this
applies crop to world geometry and overlays during preparation. Do not present that
already cropped image. Infinite
perspective uses five-plane culling. Near maps to one, far to zero. Prepared views
retain their depth convention; reverse-Z preparation requires the L1 profile ready.
Submit remains the conventional L0 compatibility path; L1 uses DrawView inside
an acquired frame and finishes with R2 EndFrame(device, completion).
Cook the installed composition shader alongside renderer_flat, then call
InitializeViews(renderer_composite::Vertex(), renderer_composite::Fragment())
after the flat renderer becomes Ready. Poll PollViews on subsequent owner turns
until Ready. L1 setup adds five pipelines, an immutable nearest/clamp sampler and
one index buffer. Pipeline creation stays outside frame drawing. It consumes the
existing RHI budgets rather than increasing them; a full L0 allocation can explicitly
reject L1 setup. Failed L1 setup leaves the L0 profile usable but requires Reset to
release its partial candidate before restarting.
Create linear RGBA8 RHI attachment textures with TextureDescription.Attachment=true.
DrawView selects a compatible surface/offscreen flat pipeline and pairs the prepared
view's depth convention with Less/clear one or Greater/clear zero. Its pass has explicit
load/store, viewport and scissor. Clears always cover the complete attachment; they
are never interpreted as per-view rectangular clears. To preserve neighboring split
views with independent clear colors/depth, draw each into its own offscreen target,
then composite into the shared surface with DrawPresentation. Ordinary R3 direct
pass switching ends the producer and establishes the sampled use; RHI's single
ledger retains attachments, bindings and geometry until actual completion.
PreparePresentation copies immutable quad geometry and a resolved mapping and
retains the sampled texture incarnation through its view/binding snapshot. The
source pixel aspect must match the logical screen aspect; its shading resolution
can differ. Four presentations are bounded separately, each using one vertex buffer,
view and binding set. Ready/Pending alone grants ownership. Poll presentation status
between frames; it may create the binding snapshot after asynchronous dependencies
are ready. Repeated draws create no renderer resources or heap allocations.
Destroying the original texture owner cannot invalidate an accepted presentation;
undefined sampled contents still reject. Top-left UVs are shared across backends,
including WebGL's existing stored-attachment flip.
Presentation geometry is prepared between frames. DrawPresentation compares its
mapping dimensions with the actual acquired extent before changing the pass;
a resize mismatch returns NotReady. End that frame, resolve a replacement mapping
using the acquired dimensions, prepare the replacement, then release the old
presentation between frames. Rendering resolution changes similarly create a full
replacement target/presentation pair before publication; old GPU uses retire through
completion. No automatic target mutation, history allocation or hidden DPI state is
introduced. PreparedView/Presentation identities carry owner, slot and non-wrapping
generation; there is no temporal history in this flat L1 profile.
The public-only installed SDK fixture renders orthographic and infinite perspective views, verifies zero-extent skips, replaces 48-square targets with 96-square targets, and resizes presentation from 96x64 to 120x80 over 120 accepted frames. Native pixels compare optimized finite perspective against direct infinite perspective; browser oracles compare optimized/direct and cross-backend images at both extents for Fit and Fill. A native Fill fixture compares logical-source composition with direct output samples and independently detects double cropping. Independent samples prove near red survives later far green, different orthographic/perspective corners, top-right overlay UV orientation, bars and neighboring view preservation. Unit fixtures cover mapping inverses, explicit DPI, crop/fit/integer fallback, shared split edges, output preservation, five-plane culling, stale handles, capacity recovery and texture retention after release. These checks establish correctness, not a speedup.
Implemented L2: mutable versions and overlays¶
L2 adds completion-scoped replacement versions, rather than exposing unsafe
in-place writes. ReplaceSnapshot publishes a copied dynamic instance scene
atomically; earlier prepared views retain the old snapshot. Set
ViewDescription::ScheduledUpload to copy instance bytes into R2's bounded staging
ring. Preparation returns Pending, with an owned prepared view. Poll RHI lifetime
and view readiness between frames before drawing. Release the previous version
after accepted submission; its physical resources remain in the authoritative RHI
registry until GPU completion. Source arrays and CPU lists can immediately be reused.
Admission failure keeps the prior snapshot/ready image. Exhausted upload/resource
quotas cause explicit backpressure, never a wait or an implicit eviction.
The overlay API uses one reserved CPU triangle arena for solids, thick lines and coverage quads. Append operations allocate nothing and reject complete commands on overflow. Homogeneous lines clip the positive-W and canonical near/far planes before perspective division; physical widths have a one-pixel minimum. Butt caps and a square for zero projected length are deliberate baseline choices. Each view builds its own expanded list using that view's clip coordinates and actual viewport size. World-unit widths, joins, persistent timed commands, worker merging and x-ray convenience helpers remain later debug features.
Cook installed renderer_overlay.slang with the existing shader helper. Initialize
OverlayRenderer, then poll its fixed pipelines. Prepare copies the complete list
and captures an exact R8 atlas view/binding; four logical versions share existing
RHI quotas. GetStatus completes upload ownership and binding setup between frames.
Draw consumes the ready version during an acquired frame. Every command loads
and stores initialized color and depth, with read-only depth; the scene pass must
store depth even for mixed always-visible/depth-tested lists. Surface and linear
RGBA8 offscreen pipelines support conventional/reverse-Z tests without modifying
scene depth. Command scissors intersect the pass scissor. Only adjacent compatible
commands merge; font quads and debug triangles retain their authored painter order.
All colors are straight display-linear RGBA, premultiplied once in the shader.
Compose after scene/history production; this baseline has no temporal history.
GraphicsText
owns a 256-square CPU R8 shadow and at most 512 sized glyph-cache entries. Shelves
reserve a transparent one-texel gutter before packing, with linear filtering and
no mipmaps. Cells never move or evict until an explicit reset. Publish copies a
complete page into a new immutable sampled R8 texture. Vulkan retains staging and
polls the upload fence; WebGPU validates asynchronously. Destroying the public
page handle or resetting the CPU shadow cannot change an already prepared label.
Publication, retained old/new pages and pending GPU retirement all count against
RHI resource quotas. Dirty rectangles and range updates are deferred until a
measured need warrants their additional hazard contract.
The native Text bridge
consumes existing shaped glyph IDs, 26.6 positions, bearings and original byte
clusters. It copies temporary raster masks immediately, then produces generic
baseline-relative quads. Caller-assigned font content versions prevent cross-font
cache aliases. Warm runs use the cache without reshaping or rasterization. Missing
glyphs use the face's visible .notdef; atlas pressure is a status that lets the
caller retain its earlier label. Paragraph layout/fallback remain CPU/UI policy.
CPU Text's FreeType/HarfBuzz bootstrap is still native-only; the browser GraphicsText
target accepts copied R8 masks and shaped placements without pretending to shape
Unicode. The standalone F0–F6 ledger
remains authoritative for that larger text milestone.
The simple baseline expands quads into the same triangle stream as debug geometry, sharing one index buffer and one upload per list, rather than adding a separate instanced glyph path. There is no per-line/per-glyph GPU allocation. Fixed-quad instancing remains a measured optimization; it must preserve these exact ordering, scissor and coverage contracts. Whole-version replacement similarly favors a small, auditable lifetime contract over premature in-place range mutation.
The public-only L2 SDK fixture renders 120 accepted frames with new scheduled instance and overlay versions, without waiting for preceding frame completion. Native and browser pixel oracles check R8 orientation, painter blending, scissor intersections and near-clipped thick lines. Reference-backend stress holds submissions outstanding, cancels uploads, checks old bytes and texture retention, exercises capacity recovery, and invalidates stale handles on release/restart. CPU fixtures cover Latin, Hangul and Arabic shaping, cluster provenance, warm cache reuse, transparent gutters and atlas pressure. These are correctness checks; no throughput or physical GPU speedup is claimed.
Selection outline on the GPU¶
SelectionOutlineRenderer
(header)
draws the silhouette outline of one selected triangle mesh over an image. It finds the
outline in screen space instead of from the mesh's edges, so a frame costs one rasterization
of the mesh's triangles and a bounded per-pixel search near it, selecting a mesh costs one
upload, and nothing is built from the mesh on the CPU. The editor uses it for the World
viewport's selection.
- Mask pass. The mesh is rasterized into an RGBA8 mask the size of the target, white where it covers a pixel. There is no depth test, so another object in front does not hide the outline, and depth is pinned inside the clip volume so only the camera plane and the frame sides clip.
- Edge pass. One quad over the mesh's projected bounds, padded by the casing width, samples the mask.
Each pixel searches its neighbourhood, a square of radius
ceil(casing half width)pixels capped at eight, for the nearest pixel whose coverage differs from its own, and turns that distance into coverage of the main stroke and of the wider casing, centred on the silhouette. Stroke and casing combine in premultiplied alpha, and the pass loads and blends over the target. - Why a bounded search, not jump flooding. The strokes are a few pixels wide, so a search that
stops at the stroke width is cheaper and simpler than log2(extent) flood passes over a full-size
texture, and needs only one extra texture. A larger width than
MAX_STROKE_WIDTH_PIXELSis rejected. - Portability. It uses the portable raster profile only: vertex and index buffers, one per-instance
stream, one sampled texture with a sampler, premultiplied blending, and surface or RGBA8 linear
attachments as targets. The Slang sources are
renderer_selection_mask.slangandrenderer_selection_outline.slang, cooked with the shader helper like the other renderer recipes, and the same pipelines serve the native and browser backends.
Initialize with the four stages, poll until Ready, then SetMesh once per selected mesh and Prepare
a version (one 320-byte buffer, at most four at once) for each distinct matrix and target extent.
Draw records its two passes into an open frame; it takes the target and optional viewport from the
pass description it is given. Only the outer contour and the contours of holes in the projected
area are stroked, which is the same silhouette the CPU outline marks.
Implemented L3: material and content versions¶
L3 extends the existing Renderer with MaterialProgram, MaterialVersion and MaterialDescription. A program owns one immutable shader/layout incarnation and six R2 pipeline-cache requests: surface/offscreen × conventional depth/reverse-Z/painter. Create and poll programs outside an acquired frame. A ready program admits immutable material parameters, a sampler and an exact retained sampled texture view. Draws perform no shader compilation, binding construction or parameter upload. Mesh creation copies positions and optional finite top-left UVs into a 20-byte vertex in L3 (40 bytes with L4 normals/bake UVs); L0 flat shaders ignore UVs. Existing flat/error materials keep their behavior.
ludus_compile_shader(TARGET my_app NAME renderer_material RASTER
SOURCE "${Ludus_DIR}/shaders/renderer_material.slang"
VERTEX vertexMain FRAGMENT fragmentMain)
ludus_cook_texture(TARGET my_app NAME albedo SOURCE assets/albedo.texture.json)
The installed shader ABI has seven vertex inputs (position, four transform rows,
instance color, UV) and fragment bindings 0/1/2 (48-byte uniform/texture/sampler).
Per-target reflection exposes and verifies float4 Tint, UvTransform, Options
at 0/16/32 before admission. Equal-sized incompatible numeric shapes reject. The offline raster layout verifier additionally accepts
bounded float4 and float4x4 arrays and matrices, with independently checked WGSL
and GLSL ES packing, including pinned Slang's narrow matrix storage wrappers;
unsupported nested/runtime layouts reject. Source compilation
and generated manifests remain build-time work, never renderer reload work.
Use PrewarmMaterialProgram and GetStatus until ready, then CreateMaterial and
poll that candidate. PublishMaterial(candidate, current) transfers a ready owned
candidate into the current identity and detaches the previous owner. Failed or
pending admission preserves the old material; callers keep drawing its prepared
views. Snapshots and prepared views retain their exact material/program/texture
incarnations, and RHI retains accepted GPU dependencies after public release.
Publication changes future snapshot inputs, not previously captured views. Release
unpublished candidates explicitly. A program's null texture uses its ready white
fallback; a null scene surface uses L0's magenta flat/error material. Opaque and
masked materials use the scene layer and write depth; Painter uses the Overlay
layer and premultiplies straight color/texture alpha exactly once. This remains an
unlit schema; the lit baseline is described below. Packed tangent-space normal detail is available in the opt-in surface fidelity profile.
The raw texture JSON contains integer width, height (1..4096), format
(R8Unorm, Rg8Unorm, Rgba8Unorm, Rgba8Srgb), semantic (color, data,
normal, mask) and complete top-down tightly packed integer-byte pixels.
Color/normal require RGBA8; sRGB is color-only. Complete upload footprint must
fit 64 MiB (or the device's lower enabled limit at admission). The cooker defaults to a complete
immutable chain down to 1×1; optional integer mip_levels selects a complete prefix. It writes four-byte padded rows, source/upload hashes and
inspectable level offsets/pitches. Fractional box footprints include odd edges;
sRGB RGB is decoded before filtering and re-encoded afterward, alpha stays linear.
Color filtering associates alpha then restores straight alpha, avoiding transparent
RGB bleed. Normal RGB is decoded, averaged and renormalized (degenerate averages
use +Z). Data averages channels independently. Optional mask cutoff in (0,1]
selects the nearest attainable discrete coverage using a monotone alpha scale;
the manifest reports coverage error, which need not be zero. Thanks to Khronos's
KTX 2.0 §3.10.2, Use of Transfer Functions
for the decode/filter/encode rule. This raw profile includes no image codec,
compressed formats or runtime mip generation.
RHI requires every declared sampled level to be initialized; offsets/pitches and
aggregate upload extent are validated before native work. Sampled views expose the
whole exact format/chain. Attachment textures remain single-mip; L4 adds capability-gated RGBA16F. Mip filters
and finite LOD clamps are explicit; the compatibility sampler clamps level zero.
MaterialDescription::Lod selects derivatives (-1) or an explicit initialized
level for deterministic previews, still subject to the sampler's mip/LOD clamps. Streaming residency/partial mip ranges remain
planned. AsyncUpload requests nonblocking Vulkan setup with fence-retained staging;
mip/data uploads also use that path. The cooker sets it. Legacy one-level RGBA8
setup retains its compatibility behavior; native/browser readiness is always explicit.
Default budgets are two program and eight material records, including detached CPU-retained incarnations. L3's historical RHI budget of 32 records per kind covered L0/L1's seven pipelines, L2's six and two overlapping six-variant programs (25); this changes table capacity, not binding or hardware limits. R2 pipeline, upload and retirement budgets still apply independently: backpressure returns an explicit status, never overwrites old content. No unbounded cache, persisted driver blobs or draw-time permutations were added. Thanks to the Khronos Vulkan Guide, Pipeline Cache for motivating explicit prewarm; implementation uses Ludus's existing field-comparing R2 cache.
The public-only L3 SDK fixture cooks and renders color mips, masked coverage, painter blending and transactional red→green replacement over 120 frames. Native pixel fixtures compare surface and offscreen conventional/reverse-Z output, including a prepared view retained across publication and rejected reload. Browser checks compare optimized/reference and WebGPU/WebGL2/Auto/fallback images before and after replacement. Unit fixtures exercise bounded overlap, stale identities, retained old versions, exact ABI rejection and no additional pipeline creation during draws. Cooker fixtures independently check transfer functions, odd areas, normal normalization and mask quantization. The installed SDK separately cooks a matrix/vector-array/matrix-array fixture and checks its target's 240-byte range and 0/64/96/224 member offsets. These establish correctness; physical GPU performance, streaming throughput and compressed-content quality require separate qualification.
Implemented L4: bounded forward lighting¶
L4 adds lighting.hpp
and three installed Slang sources: renderer_lit.slang, renderer_shadow.slang,
and renderer_output.slang. It preserves L0–L3 callers and immutable version
publication. Calls are serialized on the renderer/device owner thread; prepare,
prewarm and poll between acquired frames, then draw only ready resources.
Failure preserves caller outputs unless a documented accepted candidate is pending
or failed. Release failed/pending candidates explicitly. No draw compiles a shader,
creates bindings, allocates scene storage or changes an accepted material version.
Initialize flat rendering and views, then call InitializeLighting(outputVertex,
outputFragment, flatShadowFragment) and poll PollLighting. Cook the lit source
with FragmentMain and again with ShadowMain, and pass their vertex, lighting
fragment and masked-shadow fragment descriptions to PrewarmLitProgram.
GetStatus(program) completes the legal depth/painter variants for linear RGBA8,
optional RGBA16F, and conventional-depth masked shadows. Opaque/masked lit materials
belong to the scene layer; painter lit materials belong to Overlay. Lit views draw
into a linear attachment with DrawView; legacy surface Submit rejects them.
Meshes copy position, base UV, normalized normal and bake UV into a 40-byte vertex.
Lit snapshots require normals and an invertible finite normal transform (inverse
transpose components bounded to 1e12), and world bounds within ±1e6 meters.
Instances occupy 192 bytes, retaining the legacy clip/color prefix. Perspective
specular evaluation uses ViewDescription::CameraPosition; orthographic views
use the opposite normalized CameraForward. Both camera fields are validated.
The installed ABI has sixteen vertex attributes, a 1040-byte fragment uniform at
binding 0, base texture/sampler at 1/2, bake texture at 3, and shadow texture/nearest
sampler at 4/5. Reflection verifies all member names, offsets and numeric shapes,
including the masked-shadow subset, before admission. Fewer than sixteen enabled
attributes returns Unsupported. The RHI exposes the enabled attribute bound (up
to sixteen); original shaders still use their original eight-attribute profile.
Material and direct-light model¶
CreateLitMaterial copies metallic [0,1], perceptual roughness [0.045,1], emission,
constant environment irradiance/radiance and the L3 base texture/tint/alpha schema.
Linear emission/environment components are bounded to 10000. The baseline uses
single-scatter isotropic GGX, height-correlated Smith visibility, Schlick Fresnel,
a fixed dielectric F0 of 0.04 and Fresnel-weighted Lambert diffuse. Metallic removes
diffuse. The baseline has no multiscatter compensation, normal-map/tangent profile, BRDF
LUT or environment convolution. Constant specular radiance uses a declared Fresnel
approximation; it is a deterministic fallback, not a full image-based-lighting bake.
EvaluateLitBrdf is an allocation-free independent CPU reference returning BRDF ×
max(N·L,0); degenerate/invalid inputs preserve its output.
Thanks to Google and the Filament authors, Physically Based Rendering in Filament, §4.4 Standard model and §5.2.2 Direct lighting, for the GGX/Smith/Schlick model and photometric conventions. Code credits the source and records the simpler single-scatter/constant-environment departures.
Directional intensity is illuminance in lux; point and spot intensity is candela.
Positions/ranges use meters. Point/spot evaluation uses inverse-square attenuation,
a smooth finite range, and for spots a squared smooth cone between distinct inner
and outer half-angle cosines. Zero-distance attenuation is bounded at 1e-4 square
meters. Intensity is finite [0,1e6], hue channels [0,1], range [0.01,1e6]. Every
submitted light is validated, including dropped entries, and identities must be
unique/nonzero. SelectDirectLights admits at most 128, retaining the eight smallest
SourceIds in ascending order. LightSelection::Dropped reports successful
saturation; excess input capacity rejects and preserves output. Stable selection
avoids frame-order flicker; it does not promise brightness-optimal selection.
Shader radiance clamps finite output to 65504 before attachment conversion. Emissive
materials do not create lights. Clustered lists belong to L5.
Baked chart admission and replacement¶
The offline raw-texel cooker accepts optional bake metadata only for linear
Rgba8Unorm, semantic data, and mip_levels: 1. It validates nonzero uint64
source_id, scene_revision, dependency_revision; optional direct_light_id
(zero means indirect-only); twelve column-major affine transform components;
three scale components; and integer gutter (1..16, default one). Nonzero numeric
components below binary32's minimum magnitude (2^-149) are rejected before header
generation; zero and representable subnormal values are accepted. Every gutter texel
must replicate its nearest interior texel, and the interior is at least 2×2.
The generated Bake(texture) returns the immutable metadata and texel-center UV
transform; the manifest retains that metadata with source/upload hashes.
See the SDK bake source for a complete
example. The input is authored irradiance, not a runtime lightmapper or chart packer.
Materials retain a ready single-mip linear RGBA8 chart. Bake UVs are clamped to [0,1]
before the supplied interior transform, so bilinear level-zero sampling cannot cross
a gutter. A valid bake requires exact SourceId, both revisions and affine transform
on each SceneItem. It replaces environment diffuse irradiance. If DirectLightId
identifies a selected light, it also replaces that light's direct diffuse term;
dynamic direct specular remains. Indirect-only charts preserve all direct lighting.
A moved object or mismatched revision uses environment diffuse and all dynamic direct
terms, counted by ViewReport::BakeFallbacks. No stale chart is silently applied.
Immutable shadow atlases¶
PrepareShadowAtlas(snapshot, description, atlas) retains the exact opaque/masked
caster snapshot and up to 80 light views in a [4,2048] RGBA8 atlas. Call GetStatus,
then draw once with DrawShadowAtlas inside an acquired frame and successfully end
that frame before publishing materials using it. Partial drawing failure poisons the
candidate; a failed/aborted frame requires discarding it and preparing another.
Ready rendered atlases may be retained by lit materials after public ownership is
released. Two atlas incarnations support bounded overlap. No in-place rewrite can
change the image of an older prepared view.
Cells leave a clear texel on all sides and separate expanded gutter rectangles.
All cells use conventional Z [0,1], clear depth one, independent of camera reverse-Z.
Depth is packed into RG16 within RGBA8; nearest 2×2 manual PCF clamps each sample to
that cell's interior texel centers. Receiver bias is explicit [0,0.01] in normalized
light depth. Region order chooses the first containing projection, permitting two
explicit directional cascades and spot projections. Point and weighted area
samples require six canonical cube projections: +X, -X, +Y, -Y, +Z, -Z. Use
BuildPointShadowMatrix with a positive explicit near distance and far equal to
Light.Range; all six faces and matching source terms are validated before
preparation. Local uncovered receiver regions conservatively occlude, while
uncovered directional cascade regions retain the declared unshadowed fallback. Opaque flat/unlit
geometry casts; masked casters require the lit alpha shader; painters never cast.
BuildDirectionalShadowMatrix fits finite unjittered caster/receiver bounds, snaps
XY center to a texel grid and pads one texel. The caller owns cascade splits and
spot projection authoring; no temporal jitter or automatic fit policy is hidden.
Atlas light identity, position/direction/range/cone must match selected material
lights; color/intensity may change without changing visibility. New geometry,
source identity, transform, opacity domain, or masked material incarnation invalidates
cached visibility. The complete caster set is compared conservatively; even a moved
caster outside the main view selects unshadowed direct lighting and increments
ShadowFallbacks. Lighting-only opaque material replacement keeps visibility valid.
A new rendered atlas is required to restore shadows; no stale shadow is sampled.
Linear composition and capabilities¶
RasterCapabilities::HdrColor requires sampleable, linearly filterable, blendable
RGBA16F color attachments. Vulkan queries format features, Metal/WebGPU expose the
supported profile, and WebGL 2 requires EXT_color_buffer_float. Rgba16Float is
attachment-only, one mip; unsupported devices return Unsupported and callers choose
the linear RGBA8 LDR path. LDR clips radiance above one before tone mapping; HDR
preserves it. This is explicit quality selection, not a failed-frame fallback.
PrepareLightingPresentation retains an immutable source, mapping and manual
exposure. It precreates both output encoding choices and selects one using the
acquired FrameInfo.Encoding. Apply exposure and the selected ToneCurve once: guarded per-channel
Reinhard by default, or Khronos PBR Neutral (LightingOutput::Curve, RasterViewport::Initialize's
toneCurve), which keeps hue and saturation below 0.76 and compresses highlights toward white.
The Editor viewport uses PBR Neutral; the engine and the SDK stay on Reinhard. The graph ToneMap node defaults
to Reinhard and also offers PBR Neutral, which the engine default graph selects; graph conversion shares
display_tone_mapping.slang with this presentation.
For UI composition, tone-map scene into a linear RGBA8 display intermediate with
Encode=false, blend linear UI there, then present with ToneMap=false, exposure
one and Encode=true. Hardware sRGB attachments encode automatically; unorm surfaces
use the shader's sRGB curve, so conversion occurs once. Draw-time encoding selection
creates no resource. Old unlit textured materials are not admitted to HDR targets;
flat overlays remain available. Exposure is manual; auto exposure and tone-map curves
beyond Reinhard and PBR Neutral remain planned.
The Editor's Solid and Albedo viewport shows a very subtle vertical gradient behind the scene
(surface.viewportTop to surface.viewport, config/design-tokens.json); Lit keeps the scene's own
background. It costs no pass, draw or attachment: RasterViewport::Initialize's hasBackdrop clears the scene
target to alpha zero, and the existing output pass adds the gradient, in display space, under each pixel by
1 - alpha (LightingOutput::HasBackdrop, Output.w). Every scene blend is premultiplied, so a
partly covered pixel such as a grid line keeps its share of the backdrop. The default is off, which leaves
the clear colour, the image and the uniform contents of games, exports and reference captures unchanged.
The shader's endpoint constants must equal the tokens; test_design_tokens.py enforces it. Measured on
lavapipe at 1080p over 15 runs of 40 frames, a viewport frame (render to acquire) has a median of 7.8 ms
with the backdrop and 8.6 ms without it: no measurable difference.
Spatial antialiasing is FXAA 3.11 in that same output pass (LightingOutput::Antialiasing, SpatialAntialiasing
Off, Fast or Balanced; Output.w carries backdrop + 2 * mode, so the 16-byte uniform did not grow). It runs on
the display colour, after exposure, tone mapping, encoding and the backdrop, so its edge test sees what the viewer
sees; object overlays are drawn into the scene target first and are filtered with the scene. Every pixel loads
itself and its four neighbours and rejects flat ones there, with a square-root brightness in place of the sRGB
encode so the rejection test costs no powers. An edge pixel loads its corners, walks both ways along the edge
until the brightness pair average departs by a quarter of the gradient (four pixels in Fast, sixteen in
Balanced, with the 3.11 thresholds and sub-pixel weights of each) and blends toward the neighbour across the
edge. The taps are exact texel loads, not the half-pixel bilinear taps of the reference algorithm, so the shared
nearest sampler is unchanged and the pair average costs two loads. The editor starts with Fast and offers Off,
Fast and Balanced in the View menu; the choice persists with the navigation state and rebuilds the viewport.
The filter assumes the image maps one to one onto the target, as RasterViewport's does; Off leaves the
single-sample path and every existing pixel unchanged. Measured on lavapipe at 1080p over 15 runs of 40 frames
(a software rasterizer, so pessimistic for the five loads per pixel), the median viewport frame (render to
acquire) is 6.7 ms with Off, 12.5 ms with Fast and 12.2 ms with Balanced (5.7, 10.7 and 10.4 ms in an earlier run; the two modes cost the same because the rejection test of flat pixels dominates); a hardware
GPU is not measured.
Default renderer budgets are eight prepared views (four shadow views plus scene/UI and overlap), two atlases, two programs, eight materials, four snapshots and four presentations. RHI tables grow to 64 per kind: two seven-variant lit programs, L0/L1's seven, L2's six, output/shadow and HDR flat variants exceed L3's 32. These are bounded record capacities, not wider bindings, allocations or hardware guarantees.
The shared public-only SDK fixture uses cooked bake metadata, valid/invalid bake revisions, all four atlas cells, masked casters, a retained atlas across lighting-only replacement and moved-caster fallback. It renders LDR/HDR, emission, environment and painter samples through a linear display intermediate over 120 frames. Native pixel tests compare reference and optimized images and independent interior values. Browser checks cover forced WebGPU/WebGL 2, Auto and unavailable-WebGPU fallback with both profiles, before and after caster movement. Unit tests cover stable overflow, malformed ABI/input, reference BRDF guards, retained atlases and conservative invalidation; cooker tests cover gutters, revisions and mip admission. These validate correctness, not physical GPU performance or production bake quality.
Authored forward graph integration¶
The forward-lit graph template uses the existing authored material program, light selection and environment preparation. Its private scene resource owner retains shared immutable mesh/texture incarnations across instances, passes and replacements. Material and light edits stage new scene generations; camera-only revision promises reuse prepared content. This graph profile is opaque and unshadowed. Its admission, budgets, shader ABI and publication boundaries belong to the linked graph workflow and scene contract.
L5: opt-in portable scaling¶
The engine default now uses these conservative light lists in the clustered deferred graph, with shared material/light evaluation and an optional z-prepass. The L5 forward recipes and their measurements below remain independently available.
L5 adds CPU-built clustered forward lighting, conservative grouped visibility, and caller-authored screen-size LOD. The ordinary L4 shader and eight-light path remain available. These additions are opt-in; performance qualification is workload-specific and does not follow from passing software-GPU fixtures. Native parallel recording and compute assignment remain optional future work.
Clustered lighting and inspection¶
Cook the centralized renderer_clustered and renderer_clustered_shadow
recipes, then use PrewarmLitProgram:
ludus_compile_renderer_shaders(TARGET game SHADERS
renderer_clustered renderer_clustered_shadow)
The renderer-owned recipe manifest
and separate installed LudusRendererShaders.cmake helper own source paths,
entry points, and FoundationMath/RHI include roots. Engine targets, editor,
installed SDK consumers and reference-scene use the same recipes. Unknown or
repeated recipes fail configure; changes to the manifest trigger reconfigure,
and the offline compiler's depfile tracks included Slang sources. Custom shaders
still use ludus_compile_shader; neither helper acquires tools or compiles at
runtime. Recipes preserve artifact names and shader layouts, including
VertexMain/ShadowMain for clustered shadows. The installed SDK
scaling consumer demonstrates
this sequence without private renderer access.
Set LitMaterialDescription::Clusters to a borrowed LightClusterDescription;
Lights/LightCount provide up to 128 unique lights. Creation copies everything
before returning. Program/profile mismatch is rejected without publication.
BuildLightClusters supplies an allocation-free independent inspection API;
invalid input and count overflow preserve its output. Material reports include
all admitted normalized lights, written assignments and overflow cell counts.
The regular 8×8×8 grid has finite bounds in a rigidly transformed space. An unjittered world-to-view transform gives view-space cells; identity gives world-space cells. Columns must be orthonormal within 1e-5, bounds lie within ±1e6 meters, and extents are [0.01,2e6] meters. Unlike a projection-shaped or logarithmic grid, these cells use linear XYZ intervals. Point/spot range spheres intersect conservative cell AABBs; spots do not prune by cone, and directional lights intersect every cell. A rounding margin protects tangencies. Assignment never depends on opaque depth or normals, so it also covers painter recipients.
Each cell stores up to 32 ascending light indices in a fixed stride. Zero
terminates a partial list; byte 255 marks overflow. Overflow and samples outside
the box evaluate all admitted lights in stable order, preserving lighting
instead of reducing quality. The reference view also evaluates all lights. DirectLighting evaluates that same
full list while retaining optimized geometry packets, isolating shading cost.
Counts reports written entries, never attempted append counts. Empty cells
skip direct evaluation. Overflow costs are observable and must be measured.
The portable GPU representation is a 64×64 nearest-sampled RGBA8 index texture (16 KiB), plus an 8800-byte fragment uniform containing the lights, existing material/shadow terms and grid transform. It needs eight bindings, sixteen vertex attributes and that uniform range. Unsupported profiles return Unsupported; applications explicitly choose L4 instead. This CPU/texture implementation runs on the existing raster backends, including WebGL 2, without requiring storage or compute support. Materials own and retire the immutable grid texture and views alongside their other bindings. Assignment is setup work, not a draw-time rebuild. Its reusable CPU workspace is allocated only on first clustered material creation; ordinary L4 has no grid workspace or GPU grid resources. Frequently changing light sets pay assignment and immutable upload costs again, so this profile targets cached light sets.
Thanks to Ola Olsson, Markus Billeter and Ulf Assarsson, Clustered Deferred and Forward Shading, HPG 2012, pp. 87–96, for the spatial-grouping principle described in their abstract. Ludus uses a bounded CPU-built regular grid, without their occupied-sample/normal clustering or historical performance claims. The full-list fallback is a deliberate correctness choice rather than the earlier proposed saturated eight-light list.
Grouped visibility and authored LOD¶
Snapshots cache conservative unions for contiguous groups of sixteen opaque
objects. Rejected groups skip individual tests; fully inside groups admit their
members without further AABB tests. Intersecting groups and overlays retain
individual conservative classification. Input order and painter semantics remain
unchanged. Opaque sequences already ordered by material, uniform set and mesh
preserve source order and skip pairwise overlap searches. This includes
heterogeneous batches: every stable subrange sort would leave that sequence
unchanged. Unordered sequences retain conservative overlap constraints. Reference
views bypass visibility and packet optimization.
ViewReport::CulledGroups and BoundsTests expose the work saved. This fixed
hierarchy benefits spatially coherent source order; it is not a spatial index.
An opaque SceneItem may supply LodGeometry, LodPixels and PreviousLod.
The snapshot retains both exact mesh incarnations and unions their transformed
bounds. Lit alternatives need normals; compatible material and bake UV authoring
is the caller's responsibility. ViewDescription::EnableLod, physical
LodWidth/LodHeight and fractional LodHysteresis select by projected bounding-box
diameter. Near-plane intersections remain conservatively large. Feed
SelectedLods back by SourceIds when authoring the next snapshot, not by array
position. Shadow and reference views use full geometry. Automatic simplification,
streaming, spatial indexing and occlusion are outside this implementation.
Correctness and measurement¶
The shared SDK fixture contains 256 objects, 128 spatial lights, overflow and outside-grid variants. Native and browser pixels compare clustered/optimized against full-list/direct images, with a nonblank check and a brightness check requiring all 128 directional lights. Pixel-aligned geometry avoids treating backend edge ownership differences as lighting error. Browser checks exercise WebGPU, WebGL 2, Auto and fallback, LDR/HDR and 120 frames. Unit tests check conservative boundary assignment, malformed input, output preservation, grouped rejection and retained LOD selection.
Run physical-adapter measurements separately:
python scripts/benchmark-renderer \
--executable out/build/<preset>/modules/graphics/renderer/ludus_renderer_render_tests \
--backend metal --device "actual adapter and OS/driver" \
--runs 3 --output out/renderer-benchmark
The harness checks paired images and reports three variants (clustered, direct reference, direct optimized), cold scene setup, warm view preparation, recording, serialized completion/readback latency, median/p95/p99, resolution, counters, known scene resource bytes and tool pins. GPU pass timestamps are explicitly unavailable; completion/readback CPU latency is not GPU execution time. The 640×360 fixtures are not an AAA scene benchmark or a frames-in-flight throughput test. Known memory excludes driver/pipeline allocations, retired versions and renderer CPU storage. These historical measurements used six meshes/eight materials/256 objects. L5.1 removes those fixed ceilings; neither stage establishes full Sponza or Staircase2 import or fidelity. Record adapter-specific results before choosing the clustered profile.
Apple M5 measurement, October 11, 2026¶
Raw measurements and source hashes record three runs on an Apple M5 (8 GPU cores), Metal OS driver on macOS 26.6 (25G72), 640×360 LDR, eight warmup frames and 64 samples per variant per run. Values below are medians of the three per-run medians/p95s. Host frequency/background work is uncontrolled; these are serialized CPU-clock microfixture measurements, not GPU pass timings or an FPS/production-scene claim.
| Fixture | Variant | Prepare median (µs) | Serialized median (ms) | Serialized p95 (ms) |
|---|---|---|---|---|
| localized | clustered | 64.2 | 5.064 | 5.339 |
| localized | direct-reference | 87.8 | 6.083 | 6.467 |
| localized | direct-optimized | 62.5 | 5.710 | 6.013 |
| overflow | clustered | 64.0 | 4.572 | 4.812 |
| overflow | direct-reference | 89.7 | 4.954 | 5.242 |
| overflow | direct-optimized | 61.1 | 4.553 | 4.774 |
| outside-grid | clustered | 65.3 | 5.710 | 5.944 |
| outside-grid | direct-reference | 87.7 | 6.083 | 6.382 |
| outside-grid | direct-optimized | 63.2 | 5.727 | 5.943 |
Localized clustering improves this workload versus direct lighting with the same optimized geometry. Grouped visibility and homogeneous packet admission reduce preparation cost versus the direct reference. Overflow offers no clustering benefit and can add cost; outside-grid shading is effectively the full-list path. The profile remains opt-in. Benchmark GPU bytes cover geometry, instances, light uniform/index texture and scene target only; presentation/driver/retirement and CPU workspace memory are excluded. Re-measure after changing lights every frame, resolution, layout, backend or adapter before choosing a shipping configuration.
L5.1: budgeted scene capacity¶
Renderer initialization accepts copied Limits, preserving the six-mesh,
eight-material and 256-object compatibility defaults. All registry kinds are
configurable: meshes, snapshots, prepared views, presentations, material programs,
material versions and shadow atlases. GetConfiguredLimits() returns the selected
counts; static GetLimits() returns defaults. Counts are positive and below
UINT32_MAX, with checked counter/size bounds. Objects must fit the enabled RHI
DrawsPerFrame; an incompatible request returns Unsupported.
Registry arrays, each snapshot's items/groups, each view's packets and reusable
visibility/sort/instance workspaces are reserved exactly once at initialization.
Their addresses remain stable, and preparation reuses CPU workspace without
pool growth. Limits::HostBytes covers State, reserved metadata/workspaces and
optional lazily allocated cluster scratch. GetHostBytes() reports requested
reserved bytes, excluding allocator overhead, transient mesh upload conversion,
RT1's separately budgeted retained data, RHI resources and driver allocations.
Invalid counts return InvalidDescription; checked size overflow or budget
exhaustion returns CapacityExceeded; allocation failure returns OutOfMemory.
Failed initialization remains uninitialized. Reset releases the reservations.
These are maximum renderer incarnations, not GPU resource reservations. Detached versions still consume slots while snapshots/views/captures retain them. Renderer slot recycling advances generations; RHI separately retains submitted resource uses through completion. Other RHI consumers, shader variants, simultaneous views, materials, shadows and replacement versions consume the independently configured RHI counts/bytes. Increasing renderer capacities alone cannot promise that every resource combination fits.
For example, configure device storage before startup and pass renderer limits after the device is Ready (shader descriptions come from the installed cooker):
rhi::DeviceDescription deviceDescription;
deviceDescription.Storage.ResourcesPerKind = 512;
deviceDescription.Storage.DrawsPerFrame = 2048;
deviceDescription.Storage.DrawsPerPlan = 2048;
deviceDescription.Storage.RayResourcesPerKind = 256;
deviceDescription.Storage.RayInstances = 2048;
// CreateDevice(..., deviceDescription, device, surface), then poll startup.
rr::Limits limits;
limits.Meshes = 128;
limits.Materials = 128;
limits.Objects = 2048;
limits.HostBytes = usize{64} * 1024 * 1024;
// renderer.Initialize(device, flatVertex, flatFragment, limits).
ViewReport counts cover the entire view. Inline SourceIds/SelectedLods hold
only the first Reported = min(Visible, 256) entries. For full diagnostics, supply
optional borrowed SourceIdOutput/SelectedLodOutput arrays and DetailCapacity
through the input report. Either requested array must accommodate every visible
object; insufficient capacity fails before GPU resource publication. Preparation
failure preserves the report and output arrays. Successful output follows actual
packet/instance order, including source-order reference views and LOD selections.
RT1 retains an explicitly budgeted dynamic instance table instead of 256 embedded
entries. Native RT0 uses R6's stable host-budgeted pages for all ray resource kinds,
with StorageLimits::RayResourcesPerKind independent of general raster counts.
StorageLimits::RayInstances requests the TLAS ceiling; query
RayCapabilities::MaxInstances after startup for the enabled backend limit.
Vulkan also bounds the returned instance identity to its 24-bit custom-index field.
Structure/input/opacity/scratch bytes still require explicit per-resource ray
budgets. Default counts, 4096-ray batches and native per-buffer limits are retained.
No dynamic AS update/refit, scene ingestion, material expansion or virtual residency
is added by this capacity milestone.
Validation covers 20 meshes, 16 material versions, a 513-object snapshot with a partial final culling group, full-order diagnostics, exhaustion/output preservation, detached retention, stale handles and recovery. An RT1 CPU capture shades instance 512 after owners detach. Native RT0 tests retain 72 distinct BLAS geometries in a 513-instance TLAS, verify instance identities 0/64/512 and release dependencies only after the batch retires. Counts establish capacity and lifetime correctness, not full-scene fidelity or performance.
Owners and module shape¶
Game/World/Editor adapters UI layout GameplayCamera Text CPU
\ | | /
+------ immutable scene, view and overlay inputs ------+
|
GraphicsRenderer
extraction-free packets and effects
|
GraphicsDevice
pass plans, frame storage, uploads
|
GraphicsRhi
native objects, accepted uses, completion
|
Vulkan / Metal / WebGPU / WebGL 2
This diagram is data flow, not permission for RHI to depend on gameplay.
Renderer receives ordinary values and resource leases through application
adapters. GameplayCamera and Text remain independent CPU owners. Start with one
renderer instance per device, multiple views and one owner thread. Proposed
modules/graphics/renderer/include/ludus/graphics/renderer/ contains small scene,
view, material, debug and frame contracts; private tables, pass implementations,
shader bindings and caches stay in src/internal/. Native completion/retirement
remains in RHI and is serviced through GDI; never duplicate its handle registry.
Use ordinary functions and a few private records. Do not add a polymorphic renderable per object, a public pass class per effect, or a universal render command bytecode. Pass implementations accept their concrete data and emit RHI encoder work. The first implementation can record serially; native parallel recording is an optional private optimization with completion-scoped pools.
Following Donald Revie's separation of representation and rendering context (GPU Pro 3, V.4, sections 4.4–4.8), store complete pass inputs rather than a mutable global context stack. Material packages explicitly name legal depth, shadow, color and debug variants. Resolve parameters at cook/load, and bound variant combinations before admission. Thanks to Revie for this distinction and the account of configuration/optimization costs; the review explains why Ludus does not adopt his general object/XML pipeline.
Required RHI/GDI extensions¶
| Needed contract | Owner and migration boundary |
|---|---|
| Clear depth, compare/cull state, viewport and scissor | RHI explicit raster state alongside the unchanged R1 compatibility behavior |
| Color/depth attachment views, pass begin/end and resolve intent | RHI encoders; GDI declares uses/imports and preserves shared-target rectangles |
| Scheduled range uploads, mutable instance/constants and R8/mip sampling | RHI validates operations; GDI owns tickets, bounded staging and readiness |
| Reflection/layout growth and ready pipeline requests | Shader tools validate targets; GDI caches/prewarms; Renderer chooses material variants |
| Compute/storage and indirect operations | Independently negotiated RHI capabilities; Renderer has an authored direct/CPU alternative |
| Acceleration-structure descriptions/builds, shader query support and exact retained versions | Optional RHI extension; GDI declares build/read/scratch hazards; Renderer owns ray-scene selection and transport |
Each extension requires native and browser evidence under the pinned toolchains, including explicit unsupported results for optional features. Do not increase current R1 limits globally as an incidental renderer refactor. Keep first consumers within existing limits or explicitly fail their requested profile. Detailed retention, state and submission rules remain owned by RHI/GDI.
Frame contract and concurrency¶
- Service completions, pending resource/pipeline validation and bounded uploads.
- Admit complete resource candidates at the frame boundary. Acquire exact resource versions for this frame; incomplete candidates retain the old version.
- Accept immutable scene, view and overlay snapshots. Validate identity, ranges, finite values and configured capacities before publication.
- Build render views, CPU visibility/LOD and draw lists from those values.
- Resolve ready pipelines/bindings and select declared effect variants.
- Build ordered passes; GDI validates declared subresource uses and plans imports.
- Record/submit through one RHI owner. On acceptance, transfer retained uses to its authoritative ledger; on failure discard unsubmitted reservations safely.
- Commit temporal state only for accepted render work under the view's policy. Presentation outcome is tracked separately; skipped acquisition does not advance rendered history. Recycle frame storage only after all CPU and GPU users.
Inputs are copied into owned bounded storage before a submission API returns, unless it explicitly accepts a transferable lease. Worker tasks receive disjoint packet ranges and exact leases; their joins are CPU readiness, not GPU retirement. No callback borrows an application's stack or addresses into a mutable world. The render snapshot can contain interpolated transforms while simulation remains fixed-tick. Avoid a dedicated render thread initially: measure extraction, recording, joins, memory and input-to-display latency before adding buffering.
Creation requires a null output. Invalid description/handle, unsupported feature, not-ready input, out-of-memory, budget exhaustion and device loss are distinct outcomes. Failed admission preserves outputs and the previously usable version. Bounded diagnostics carry source/view/object/pass IDs. Optional debug geometry may be dropped with a counter; essential scene work must not silently disappear because a packet buffer overflowed. A frame admission failure returns a status and preserves the last valid presentation where the surface permits it.
Primitive submission, visibility and batching¶
Use a persistent mesh resource plus frame-local instance data. A mesh owns
immutable vertex/index chunks, vertex layout, topology, bounds, submesh ranges
and LOD metadata. A scene item supplies mesh/version, submesh, material/version,
current/previous transform, visibility flags, stable object ID and sort layer.
Visibility flags describe policy; they must not expose native render state.
Large world positions use float64; convert to checked origin-relative
float32 per view. Singular normal transforms, NaNs and unrepresentable bounds
are explicit invalid inputs, with a diagnostic and documented fallback.
Build dense arrays, reuse reserved capacity and keep authoring names out of hot packets. Initially perform scalar conservative frustum/bounds tests and authored screen-size LOD with hysteresis. Add spatial indexing, SIMD and occlusion only after measured need. Animated/deformed bounds must conservatively contain the posed geometry; delayed visibility must never hide a newly visible object. Shadow visibility is computed for the light's view independently of main-view visibility. Streaming hints use visibility but never synchronously load in culling.
Sort opaque packets by pass, coarse depth bucket, pipeline, binding/material, mesh and stable source ordinal. Compare depth-first versus state-first sorting on actual scenes; no hardcoded bit allocation establishes an optimal key. All key collisions resolve with explicit record comparisons. Masked geometry preserves coverage rules across depth/shadow/color variants. Transparent 3D draws use stable back-to-front bounds depth with explicit layer overrides; intersecting surfaces have documented approximation limits. UI and 2D overlays preserve painter order and merge only adjacent compatible packets.
Instance adjacent draws only when mesh/submesh, layout, pipeline, bindings, pass state, scissors and blend semantics agree. Instance range limits and transform handedness/cull mode must be honored. Mesh-local indices and deliberate buffer slices preserve R1's zero base-vertex/first-instance portable contract; later native paths may use capabilities without changing scene identity.
Transient triangles/quads use a reserved upload allocator with checked sizes and per-frame quotas. Persistent terrain, sprites and dynamic meshes use the same packet path, with geometry preparation in their owners. No per-line/per-glyph resource creation or virtual call. GPU culling/indirect draws later consume the same logical scene records and expose the same source IDs; direct CPU rendering remains available as a comparison mode.
Views, viewports and virtual screens¶
Keep these concepts distinct:
| Concept | Meaning and lifetime |
|---|---|
ViewId |
Renderer-owned stable identity plus generation; one logical output's settings/history |
| Camera sample | Pose/lens input for this frame, not a render target or GPU owner |
| Physical target | Surface image borrowed for the acquired frame, or an owned offscreen texture version |
| Viewport | Top-left physical pixel rectangle and explicit canonical depth range |
| Scissor | Intersection of drawable bounds and clipping rectangles; empty is a no-op |
| Virtual screen | Authored logical extent and fit/fill/stretch/integer-fit policy mapped into a physical viewport |
| Render extent | Shading resolution, optionally scaled independently of presentation/overlay resolution |
Resolve against the actual acquired extent, not a requested window size. Use one mapping for drawing and pointer coordinates: translate to viewport, subtract fit offsets, divide by scale; reject letterbox bars unless the caller requests clamping. Keep logical UI units, physical pixels, texture texels and world units explicit. Round viewport edges once with checked integer arithmetic; derive neighboring split-screen edges from shared boundaries to avoid cracks. Scissors use consistent half-open bounds. DPI is input to the logical/physical mapping and glyph raster size, not a second hidden multiplier.
For logical extent (Lw,Lh) and physical viewport (x,y,W,H), fit uses
s = min(W/Lw,H/Lh) and centered offset ((W-s*Lw)/2,(H-s*Lh)/2).
Map logical point p to (x,y) + offset + s*p; invert that same mapping for
pointer input. Validate positive finite dimensions before division. Fill uses
the maximum scale; stretch has separate axis scales. The mapping record retains
the final rounded content rectangle so drawing, clipping and hit tests agree.
Zero-sized or hidden views skip work without allocating histories. Fit preserves aspect with bars; fill crops; stretch is explicit. Integer-fit uses the largest whole-number scale that fits; when none fits, select fractional fit and expose that fallback in the mapping record. In a shared target, each view owns a rectangle; load/store/clear semantics must preserve neighbors. Whole-attachment clears may not masquerade as per-view clears. Compose offscreen views when scissored clears or distinct lighting/extents cannot be supported correctly in one pass.
The renderer owns unjittered matrices for culling/picking and jittered matrices
for temporal shading. Use right-handed +Y-up/-Z-forward math, reverse-Z [0,1]
after the explicit RHI extension, and per-backend viewport/depth conversion.
History keys include ViewId generation, render extent, projection convention,
origin epoch and effect/layout revision. Reset on camera cuts, restart, incompatible
resize/projection changes and failed history production. Origin rebasing requires
consistent current/previous transforms or reset; it is not a camera teleport.
Dynamic resolution never changes simulation aim or logical UI coordinates.
Textures, surfaces and GPU memory¶
Use three separate owners: content texture versions, GDI-managed transient attachments, and RHI-borrowed acquired surface images. A surface is not an asset. Texture descriptors declare dimension, format/encoding, extent, mips, layers, samples and legal usage. Texture views name checked subresource ranges and compatible interpretation; sampler state is independent. Reject unsupported combinations before frame recording using per-format capability matrices.
Offline cooking generates semantic mips: color in linear light, normal-map renormalization, mask coverage preservation where requested, and separate data texture treatment. Choose compression per target profile, with a verified uncompressed variant; a backend name never guarantees a codec. Streaming publishes only initialized mip ranges with legal sampling clamps and exact immutable binding snapshots. Begin with whole-texture replacement; partial uploads need RHI range hazards and a residency protocol before they become a feature.
GDI upload tickets distinguish copied source ownership, scheduled destination work and sampling readiness. Caller bytes may be freed only under that declared source contract; staging ranges live through their backend consumption obligation. Budget upload bytes/work per frame and apply backpressure. Never block rendering on disk, synchronous readback or device-idle to make routine updates convenient.
Separate logical release, CPU lease release, submitted completion and physical memory retirement. Descriptors/atlas regions cannot be reused while earlier packets reference them. Report live, pending-upload, transient, cached and pending-retirement bytes separately. Budgets include temporary old/new overlap during hot reload, aligned storage and multiple frames in flight.
Start with transient pooling by exact compatible descriptor and completion proof. Attachment aliasing is a later native path whose lifetime overlap and memory requirements are validated; distinct WebGPU textures are not promised physical aliasing. Histories and cached shadows are persistent imports. Every pass states load/clear/discard and store/resolve intent, resource ranges and readers/writers. GDI compiles those uses in the author's order; RHI commits state only on accepted submissions. Undefined transient contents cannot be sampled.
Arseny Kapoulkine's GPU Zen 2 article 4, sections 4.4–4.6, reinforces this ordered plan and explicit attachment intent. Thanks for the pipeline/attachment tradeoffs: precreate the legal material states, and use pass-integrated resolve when supported and sufficient. A streamed destination enters the pass plan with a readiness dependency; streaming scheduling itself remains a GDI service. Ludus does not infer safety from a discard hint or assume all GPUs benefit equally.
Resize creates complete replacement target sets, publishes at a boundary and retires old sets after use. On device loss invalidate all GPU incarnations and history, keep recreatable CPU descriptions, and explicitly restart/rebuild. Use fallback textures/materials only if ready and compatible. An invalid shader or surface cannot trigger an undocumented backend switch.
Materials, shaders and pipeline control¶
The proposed render graph authoring architecture owns reusable graph/function/resource nodes, camera output routes, scene coverage, author-declared compilation configurations, shared schemas, incremental builds and editor/reload integration. It is an authoring layer over these Renderer and RHI contracts, not a claim that the current material graph editor is implemented. Users can consume selected graph outputs; this never synthesizes shader variants. Depth-only rendering requires an authored technique preserving geometry/coverage.
The material system design defines World assignments, material interfaces and execution requirements.
Retain Ludus's pinned Slang/tool chain and SPIR-V, MSL, WGSL and GLSL ES artifacts. Cook explicit reflection with numeric bindings, stage interfaces, parameter offsets, matrix packing, sizes and alignment. Validate each emitted target; equal total buffer sizes do not prove matching members. CPU constants use generated serialization/packing at the boundary rather than accidental C++ struct layout. Shader source/artifact hashes and compiler/schema versions belong in the cooked package; unsupported profiles reject cooking or select authored variants, never silently omit fields.
View/Material/Draw are update-frequency conventions, not a promise of three physical descriptor groups on the present R1 interface. Map logical data to each profile's verified layout. Cache immutable binding snapshots, allocate dynamic constants from aligned completion-scoped storage, and intern compatible layout identities through GDI rather than relying on structurally similar handles.
Pipeline keys include shader versions, layout identity, vertex input, topology, attachment formats, samples, raster/depth/blend state and declared specialization. Viewport size and dynamic constants normally do not belong in the key. Hash collisions compare descriptions. Enumerate the legal static variant space; record used variants and prewarm before activation. Pipeline readiness is asynchronous where the backend permits, with a compatible ready fallback or explicit not-ready status. Cold first-use stalls remain visible telemetry. Persist native caches only with validated device/driver compatibility; cache failure is recoverable. Never claim offline shader compilation eliminates all driver pipeline work. Reload swaps package, layouts, bindings and pipelines as one validated version set; failed reload leaves the prior set usable.
Material system design¶
Status Proposed. The current scene path resolves material IDs into an indexed table of built-in models. This design extends that boundary to project-defined materials and pass requirements. The shared scene shader contract describes the current shader inputs.
Definitions, instances and World bindings¶
A material definition is versioned project content. It describes typed parameter expressions, scattering and layer composition, output interfaces and dependencies. Its parameter schema records stable field identities, defaults, units, valid ranges and texture semantics. Engine-provided definitions use the same asset model as project definitions.
A material instance references a definition and supplies parameter and texture overrides. Changing those values produces an immutable instance revision. Editing composition produces a definition revision. A prepared instance pins the exact revisions of its definition and dependencies.
A material binding assigns a logical instance ID to a durable mesh slot. World owns assignments and overrides. A mesh supplies slot identities and default assignments. An object can override a slot without changing other users of the mesh. The renderer resolves each draw and surface hit to a compact material handle valid for its prepared scene generation. Persist logical IDs and slot identities according to the content contract.
Resources owns content revisions and leases. Renderer owns preparation, GPU bindings, pipeline compatibility and retirement. World and material assets carry neither backend bindings nor graph pass identities. The editor owns drafts and undo through the existing document history.
Composition and programmable operations¶
Parameter expressions compute values such as color, roughness and texture coordinates. A Mix combines scattering responses with a declared weighting rule. A Layer models ordered interfaces and any intervening medium, including attenuation and internal reflection. Each composition operation owns matching evaluation and sampling semantics. Begin with the existing coated-diffuse model as the first layer operation.
The baseline to prototype is a bounded typed program for parameter expressions and closure records for scattering. Validate types, graph cycles, operation versions and resource requirements before execution. The format owns admission limits for instruction count, closure storage, nesting and scratch. Measure those limits and execution cost on representative targets before freezing the representation. CPU and GPU reference evaluators consume the same declared semantics. Optimized lowering can use a different physical representation with parity evidence.
A new instance or a supported composition can enter a live World as data. The shader package supplies the set of executable operations. A new custom operation requires a compatible implementation package before use. Material programmers can supply namespaced, versioned operations with typed inputs, resources, supported interfaces and sampling contracts. Technical artists can compose those operations and expose instance controls. Operations used for reference transport follow the CPU and GPU acceptance contract.
Use the existing shader cooker to validate target artifacts and include operation implementations in a finite package. Runtime dispatch belongs to Renderer. An explicit specialized technique can bind an implementation during cooking. Parameter values, instance counts and ordinary compositions expressed in the data representation do not create shader variants. Any authored static specializations count toward the existing shader and pipeline budgets.
Interfaces and pass requirements¶
Material authors own surface behavior. Render graph authors own passes, traversal, lighting, sampling strategy, MIS, reuse and outputs. A general pass declares the material interfaces and guarantees it needs. A specialized pass can declare a named extension interface or an exact implementation when the technique or experiment requires it.
| Interface | Contract |
|---|---|
| Coverage | Evaluate surface presence consistently for color, depth, closest-hit and shadow queries under the profile's declared sampling policy. |
| Emission | Evaluate emitted radiance at a point and direction. Expose conservative emissive participation for emitter discovery. A light-sampling strategy declares any additional sampling or power-estimation requirements. |
| Local surface scattering | Evaluate and sample reflection and transmission at the hit, with declared density and random-address semantics. |
| Subsurface scattering | Sample transport to another surface point, with explicit geometry queries, distance units and density measures. |
| Medium boundary | Describe any supported interior and exterior transitions. |
Absence has defined semantics where appropriate. For example, a missing emission output emits zero radiance. A missing required scattering interface is a coverage failure.
A local scattering sample reports its outcome, direction, event kind, measure and complete continuation weight. The contract distinguishes continuous directional density, delta probability mass and any estimated density for MIS. A null event retains its probability mass. An integrator uses the returned weight directly. Address material randomness from the caller's path event so internal work cannot shift later transport dimensions.
Declare guarantees at the level an estimator actually consumes. A reuse pass can require replayable sampling or a particular directional density contract. Renderer checks those requirements against the assigned material before activation. For example, a graph that cannot consume an internal-walk weight can report its sampling requirement for any incompatible closure. It needs no test for the coated-diffuse model by name.
Raster techniques declare their own representation. Deferred routes require a compatible G-buffer output. A forward route can evaluate a richer surface interface or bind a declared technique. A ray-compatible closure may need a separate raster approximation. Select it through authored policy and report it as an approximation. The raster baseline continues to admit unlit, metallic/roughness, masked and premultiplied transparent families where the profile supports them. Limit two-sided and specialized effects to justified variants. Depth and shadow techniques preserve the same geometry and coverage requirements. The scene coverage contract owns reporting per slot, role and view.
Diffuse and rough conductor Mix¶
The scene source format exposes a single weighted composition of Lambertian and isotropic rough conductor. Import it into an open World through the scene importer. The cooker retains the Lambertian color, the conductor's optical constants and GGX alpha, and the conductor weight as material data. LUSCN v8 admits the Mix tag in the v7 record layout. Its metallic field stores the conductor weight and supplies the metallic fraction for the raster approximation. Earlier cooked versions keep their existing model set.
If the conductor weight is $w$, the surface value is $(1-w) f_d + w f_c$. One uniform selects a lobe with those weights and two more sample its direction. The evaluator returns $(1-w) p_d + w p_c$ in solid angle for direct lighting and MIS. Continuation uses that full density with the weighted value and cosine. Null GGX proposals retain their probability. Thanks to Pharr, Jakob and Humphreys, Physically Based Rendering, fourth edition, section 2.2.3, for the one-sample mixture-density construction.
The Wave Tracing graph requests the full surface transport interface. Rao-Blackwellized Metropolis requests continuous reflection with a fixed three-value sampler. The scene packer records whether the assigned materials satisfy that restriction. The graph checks it before tracing and reports an incompatible material requirement for internal walks and delta transmission. Both graphs admit the Mix through their common BSDF calls.
Project BRDF programs¶
A project can import a versioned reflection program under its own material name. Version 1 supports three cosine-power terms with project-authored weights, powers and base color. This is a bounded data program with a CPU oracle and a shared Slang implementation.
For normalized lobe weight $w_i$ and exponent $e_i = (1-r) p_i$, the BRDF multiplies the base color by the weighted sum of $(e_i+2)(n·v)^{e_i}(n·l)^{e_i}/(2\pi)$. One uniform selects a lobe. Two uniforms sample its cosine-power solid-angle density $(e_i+2)(n·l)^{e_i+1}/(2\pi)$. Evaluation and continuation return the full mixture density. Thanks to Pharr, Jakob and Humphreys for the mixture construction and hemisphere CDF inversion in Physically Based Rendering, fourth edition. Section 2.2.3 and Appendix A.5.2.
Wave Tracing uses the standard BSDF calls for the program. Metropolis uses those calls when the program declares fixed-dimension reflection. An omitted or false declaration produces the existing incompatible material requirement diagnostic. Rebuild existing ray graph shader packages for scene kernel ABI 14 to pick up the evaluator. The graph sources use the same interface.
Preparation and publication¶
Validate the candidate definition and overrides, resolve its exact dependencies, prepare renderer bindings and techniques, and evaluate coverage for the affected views. Publish the complete generation once it is ready. Keep the last drawable generation visible while preparation is pending or fails. Diagnostics identify the candidate separately from the presented revision.
A request carries project and revision identity so stale completions cannot publish. A generation updates material bindings and dependent emitter or coverage data together. Invalidate affected reference accumulation when the integral or sampling state changes. Retain the old resources until their accepted uses complete. Unchanged geometry can retain its prepared resources.
Import and repair¶
Keep supported geometry usable when a material cannot be resolved. Preserve source identity, slot assignments, recoverable parameters and texture references in bounded import data. Distinguish missing definitions or operations, invalid inputs, failed preparation and unavailable techniques.
Use a conspicuous purple diagnostic technique where the role can draw the geometry. Otherwise show a selectable marker or bounds proxy. The diagnostic leads to the affected slot and a repair. Required unresolved coverage makes reference output incomplete. Shipping follows the graph coverage contract for rejection or a named authored fallback.
- Map to existing material replaces the selected assignments with a compatible project material.
- Create material from import opens a typed draft carrying supported values and texture references. Show unsupported inputs and their source identity so the author can supply a mapping or implementation.
- Locate dependency or Choose compatible profile addresses missing implementations or unavailable techniques at their respective boundaries.
Show the affected slots before a shared replacement and apply it as one undoable edit. Keep source identity for reimport. Clear the diagnostic after a compatible revision publishes.
The Material document opens from the World slot and shows instance parameters, shared uses, layer composition and active-profile compatibility. Parameters and canvas actions share validation and history. A later Scattering Lab can inspect lobes, sampled directions, energy, and raw CPU/GPU reference results.
Implementation gates¶
- Preserve existing material IDs and rendering behavior through adapters for legacy scene records. Open a coated-diffuse instance from World, edit it, apply, undo, save and reopen it. Exercise pending, failed and successful publication.
- Prototype data-driven parameter and closure evaluation with measured CPU and GPU costs. Migrate one general transport graph and one restricted estimator to declared material requirements. Load a supported new composition on newly loaded geometry without editing either graph.
- Import an unsupported material shared across several slots. Select its diagnostic surface, create or map a replacement, then verify undo, save/reopen and reimport.
- Register a project-defined BRDF with paired reference implementations. Verify that the general graph accepts it with unchanged source and the restricted graph either admits it or identifies a missing guarantee. Expand physical layering and subsurface transport with raw linear CPU/GPU parity and explicit profile coverage.
Lighting and the default pass sequence¶
Use a conventional forward baseline with bounded CPU-selected per-object lights. Selection is stable and deterministic, with a declared maximum and an overflow counter/quality policy. L5 adds an opt-in CPU/texture clustered profile across the portable backends. Future compute assignment requires a compute/storage-capable profile and its own measurement gate. Cluster bounds and light lists must be conservative, capacity-checked and inspectable; overflow uses a declared bounded fallback rather than corrupting memory or silently dropping arbitrary lights. WebGL 2 retains CPU assignment and the L4 light-list option. Deferred shading, visibility buffers, bindless, mesh shaders and ray tracing are separate options, not startup requirements; their bandwidth/material/platform costs need workload evidence.
The review of Billeter, Olsson and Assarsson's “Tiled Forward Shading” (GPU Pro 4, II.4, sections 4.4–4.8) adds a correctness rule: transparent lighting cannot reuse light rejection derived only from opaque occupied depths. Start with a regular view-space 3D grid covering the declared near-to-lighting-far range and conservative light-volume intersections, independent of opaque occupancy. Infinite-far camera projections still require a finite lighting range and a declared farther-surface fallback. Later opaque-only depth/normal pruning uses separate lists. Empty sky samples and every MSAA sample need explicit handling. Thanks to the authors; their historical timings are not Ludus performance evidence.
Harada, McKee and Yang's “Forward+” (GPU Pro 4, II.5, sections 5.3 and 5.6) motivates compact culling bounds separate from shading properties, occupancy visualization and independent shadow budgets. Use a fixed-stride capped list per cluster initially. Bounds-check both the local append and total buffer extent; store the written count, never an unbounded attempted count. Saturation sets an overflow flag and makes the shading variant select a stable bounded fallback list prepared on the CPU and uploaded before assignment; this needs no synchronous GPU readback. Async telemetry reports affected clusters and quality reduction. Unshadowed lights and shadow assignments have independent limits. Compact prefix-sum lists, light BVHs and 2.5D masks are later alternatives. Thanks to the authors; do not copy their demonstration's fixed workgroup/list capacities or make opaque depth masks authoritative for transparent shading.
Use linear RGB shading, a documented GGX metallic/roughness BRDF with energy conservation, roughness floors and numerical guards. Define directional light illuminance in lux and point/spot luminous intensity in candela; author lumens only through an explicit solid-angle conversion. Radius/cutoff and temperature conversion are bounded; world distance is in meters. An emissive material does not automatically illuminate surrounding objects. Exposure belongs to each view; the initial mode is manual and stable. BRDF details, artistic approximations and reference fixtures must be specified in the lighting implementation change.
Static direct lighting may be baked into lightmaps, while irradiance probes approximate indirect diffuse for moving objects; filtered reflection environments serve indirect specular. Cook versions carry bake-space transforms, UV/chart data, normalization, scene/dependency hashes and encoding. Keep baked direct and dynamic direct contributions distinct to avoid double lighting. A moved baked object/light invalidates its bake or uses a documented dynamic fallback. Lightmap packing, probe placement and content quality belong to cook tools; rendering only consumes ready versions. Missing bakes select a declared neutral/ambient fallback.
Dynamic shadows start with one directional cascaded map and a bounded local-light atlas. Stabilize cascade texel placement, derive splits from unjittered views, reserve filtering gutters, bound bias in light-space/receiver units and expose split/bias/occupancy debug modes. Shadow maps may deliberately use conventional depth with an explicit sampler/compare contract even when the main view uses reverse-Z. Static shadow reuse keys include caster/material/light revisions; dynamic casters invalidate affected regions. Point/spot map costs count against an explicit per-view/frame budget. No cache claims correctness from “light did not move” alone.
An initial lit view follows this author-visible sequence:
| Stage | Inputs and outputs |
|---|---|
| Upload readiness | Exact prepared versions become available before their first use |
| Shadow passes | Light views and conservative caster lists produce depth maps |
| Optional depth prepass | Opaque/masked geometry produces depth; enabled only by measured benefit |
| Optional cluster assignment | View/light bounds produce capped cluster lists |
| Opaque/masked forward | Geometry, direct lights, bakes and environment produce linear scene color/depth |
| Sky and transparent | Declared depth behavior and ordered blending into scene color |
| Scene effects | Authored AO/temporal/other variants with explicit inputs/history, initially disabled |
| Exposure/tone mapping | Scene-linear HDR, when supported, becomes display-linear color |
| Debug/UI/text | Display overlays compose in linear display space at presentation resolution |
| Output | Encode once according to the actual attachment/output contract, then present |
World-space debug may instead compose into scene color after temporal effects and history production, before exposure/tone mapping; use retained scene depth for testing. Display overlays are display-referred. Tag the domain so neither is tone-mapped twice and debug primitives do not enter temporal history. HDR attachments/blending and multisampling require format/capability checks; the portable baseline has an authored LDR path. Hardware sRGB output and shader encoding are mutually exclusive. On a nonlinear unorm target lacking linear blending, composite into a supported linear intermediate then encode once. Antialiasing starts with a simple spatial variant or verified MSAA path (the Editor viewport's FXAA is the spatial variant; see the output pass above); TAA is a later feature needing motion vectors, jitter and history rejection tests.
Ray tracing and path tracing¶
Ray tracing supplies intersection and visibility queries. Path tracing uses those queries to estimate light transport through sampled paths. A ray-traced shadow does not require a path tracer; a path tracer does not require a native ray pipeline or recursive hit shaders. Treat traversal, transport, sample reuse and reconstruction as separate private components with shared scene contracts.
| Rendering mode | Role and initial scope | Cost and fallback policy |
|---|---|---|
| Raster | Portable gameplay and direct-draw correctness path | Existing portable profile remains independently usable |
| Hybrid rays | Raster primary visibility plus selected ray visibility, reflections or indirect diffuse | Effects have independent ray/build/history budgets and authored raster/bake/probe alternatives |
| Progressive reference path tracing | Frozen-scene editor/debug captures; direct and indirect transport for supported materials | Accumulate independent samples without denoising or temporal reuse; explicit unsupported status on devices/content outside the profile |
| Real-time path tracing | Optional high-end gameplay mode with moving scenes | Requires measured sampling/reconstruction quality and tail latency; no universal frame-rate promise |
One GraphicsRenderer accepts the same scene/view/material versions for these
modes. Each mode composes a concrete pass plan; do not introduce another asset
manager, camera stack or GPU lifetime registry. Hybrid transport replaces a
specified lighting term rather than adding another copy of it. For example,
ray-traced indirect diffuse replaces the selected probe/lightmap indirect term.
Full path tracing evaluates supported emitters and scattering directly; it does
not also add baked lighting, ambient occlusion or prefiltered IBL approximations.
Record any intentionally mixed approximation in the capture's rendering profile.
Unified light discovery and incident sampling¶
Scene shaders enumerate analytic lights, mesh emitters and the environment through
GetLightCount and GetLightInfo. For direct lighting, SampleLight selects one
catalog source and samples its incident light. The chosen kind owns its draws.
The result carries the source PMF and the conditional probability separately.
For a BSDF ray, EvaluateLightAlongRay returns emitted radiance and the two
probability terms under the same selection policy, whether the ray hits a mesh
or escapes to the environment.
Use the uniform policy for ordinary direct lighting. A shader that already estimates the mesh contribution with a specialized proposal can use the non-mesh policy for its remaining contribution. Its PMF is normalized over the analytic and environment sources, and the two estimates are added. The CPU oracle and GPU reference keep their matched named proposals and random addresses. Existing authored shaders can still use the explicit-coordinate API.
Scene ownership and source identity¶
Publish one logical catalog of light sources with each ready ray scene snapshot. Each entry has a kind, a stable source ID and a compact index local to that snapshot. Start with the existing packed scene data and typed light lookup. Analytic lights use their authored identity, an emissive mesh light is an instance and emitting material slot, and the environment is the aggregate background source. Triangles are primitives of a mesh light. Tessellation alone does not change the number of sources. A mesh hit can resolve its catalog entry and primitive directly so a BSDF sample can evaluate the light proposal.
Keep catalog data, geometry, emission and proposal data in the same snapshot lifetime. An index may be reused in a later generation. Passes that reuse samples validate the generation and resolve stable IDs when reuse across generations is supported. A rejected scene replacement keeps the previous ready snapshot drawable. A source kind whose emission or sampling contract is unavailable is handled by the profile's explicit capability and admission rules. This includes specialized panel luminaires, whose beam proposal needs its own integration before they can participate in the default selector.
Selection, sampling and weighting¶
| Operation | Contract |
|---|---|
| Enumerate sources | Return the source count and checked typed records from the scene snapshot. Graphs can enumerate sources for deterministic summation, diagnostics or custom proposals. |
| Select a source | Given a receiver and a selection random address, return a catalog index and the discrete probability mass (PMF) of choosing it. The selector also evaluates that PMF for a specified source at the original receiver. An empty supported set returns no sample. |
| Sample incident light | Given a source, a receiver and an incident random address, dispatch by light kind. Return the world-space incident direction, incident coefficient before BSDF and visibility, an endpoint or an infinite connection, and the conditional probability and its measure. The sampler never selects a source or tests visibility. |
| Evaluate the incident PDF | Given the original receiver and an emitter hit or environment direction, evaluate the same conditional proposal as incident sampling. The evaluator uses the same proposal parameters and support. |
| Integrate | Evaluate the BSDF and receiver cosine, trace visibility, and weight the result. Integrators can substitute a proposal for a source kind if they supply the corresponding probability evaluation wherever MIS requires it. |
For a continuous incident sample, the probability used by the direct estimator is the source PMF times the conditional solid-angle density at the receiver. Convert an area proposal to solid angle using the sampled endpoint geometry. For a delta source, use a conditional mass of one for its sampled event and a continuous directional density of zero when comparing with a BSDF proposal. The measure tag controls which probabilities can be compared in MIS. On a BSDF hit or environment miss, evaluate the same selector and incident proposal from the previous receiver. An integrator that explicitly sums all sources uses only the per-source conditional weights. Zero emission, a masked endpoint under a profile that samples it, and occlusion give zero contribution with no retry. Invalid inputs and exhausted budgets use the existing explicit failure channel.
The ray renderer owns both selection and incident sampling. Keep the uniform catalog selector as the general baseline. Reference capture keeps its existing distributions as named proposals, including the category draw and conditional emitter selection. A hierarchical proposal must include the probability of each decision in its evaluated density. A power table or receiver-dependent light tree can replace the uniform selector once a representative workload shows a benefit and matching PMF evaluation is available. The catalog API does not require a light list scan for every shaded point.
Random addresses and implementation order¶
Use independently addressed slots for source selection, incident sampling and BSDF continuation. Address ownership belongs to the integrator's sampler layout, including its pass, bounce and candidate indexing where applicable. The incident sampler owns the draws within its assigned address range.
| Chosen kind | Draws within the incident proposal |
|---|---|
| Directional, point or spot | Zero. Direction and falloff follow from the source and receiver. |
| Environment | Two for the baseline sphere-direction proposal. |
| Emissive mesh | Three for the baseline proposal: one for triangle selection and two for the surface point. |
Use only the coordinates required by the selected proposal. Skipping a draw leaves addresses for subsequent BSDF or roulette decisions intact. Existing explicit-coordinate and sequential-stream shader calls retain their contracts during migration. Changes to reference sample addressing or proposal semantics require the owning sampler version and accumulation identity to reflect the change.
- Add kind-owned draws and separate source PMF from conditional incident sampling and PDF evaluation over the existing scene layout. Migrate a built-in direct-light graph through its ordinary entry point. Use the polygonal graph to exercise a custom mesh proposal alongside the default proposals for analytic lights and the environment.
- Migrate the CPU oracle and GPU reference together under the reference acceptance contract. Keep the renderer profiles' documented integrals and coverage explicit.
- Before adopting a different storage layout or selector, measure variance and device time on mixed-light and many-emitter scenes. Keep the uniform selector available as a baseline.
Acceptance covers sampled versus evaluated probabilities for mesh endpoints and environment directions, delta mass versus solid-angle density, zero incident draws for analytic sources, stable later random addresses, and mixed scenes where each source can contribute. Exercise empty catalogs, disabled sources, scene replacement and the applicable ray budgets. Compare raw linear CPU and GPU results under the same reference proposal and documented tolerances. Include an authored graph using the existing sampling API in the compatibility check.
Thanks to Matt Pharr, Wenzel Jakob and Greg Humphreys, Physically Based Rendering: From Theory to Implementation, fourth edition, sections 12.1 and 12.6, for the separation between source selection and per-light sampling and the treatment of delta distributions. Ludus uses indexed scene snapshots and explicitly addressed randomness for its CPU and shader paths.
CPU and GPU reference implementation requirement¶
Building or extending a reference path tracer requires both a CPU oracle and a GPU reference implementation. Deliver changes to reference transport, materials, sampling and accumulation in both implementations in the same change. The CPU oracle remains independently executable for correctness checks and debugging; the GPU implementation keeps path transport, shading and accumulation on the device for practical captures. CPU shading with batched GPU intersection queries is a traversal variant of the CPU integrator, not the required GPU integrator. Offline output/progress readback is allowed; a per-bounce CPU transport loop does not establish GPU implementation coverage.
Both implementations must estimate the same declared integral: frozen scene and resource versions, camera, material/emitter profile, texture filtering, depth and range limits, sampler version, seed and sample target. Preserve reproducible sample addressing and replay within each implementation. Scheduling and floating-point differences may require documented numerical or statistical tolerances rather than bit-identical images. Reference acceptance uses raw, scene-linear, independently sampled outputs without denoising or temporal reuse.
Before claiming a reference feature complete, exercise both production paths on matched fixtures, compare emission/direct/indirect and total radiance, and check multiple seeds and depth sensitivity where relevant. Cover invalid contributions, unsupported content, capacity failures, reset/cancellation and resource lifetimes in each affected path. Record backend/device/toolchain, profile, sample counts and comparison tolerances with the evidence. GPU capability rejection remains explicit; selecting the CPU oracle is a caller choice, not a silent fallback.
This is the acceptance requirement for new work. RT1 pairs the CpuOracle
traversal with the GPU-compute reference, which runs
the same transport, shading and accumulation in compute kernels. NativeQueries
remains CPU transport with native traversal. The separate
GPU preview still needs matched profile,
raw-output and production-path evidence before it can satisfy this requirement.
Existing coverage gaps remain open work, not evidence that both implementations
are qualified: GPU-compute pixel parity is so far qualified only on Apple M5 Metal.
L6 implemented scope: RT0 and RT1¶
RT0/RT1 are an opt-in static triangle query profile and progressive RGB reference capture. RT2–RT4, temporal effects, GPU-driven submission and virtual residency remain plans. The scene import workflow is unchanged; configurable raster/ray capacities are supplied by L5.1; this does not yet establish faithful rendering of Staircase2.
RHI owns immutable native BLAS/TLAS incarnations and asynchronous query batches.
Vulkan negotiates API 1.2, buffer device address, acceleration structures, ray
queries and deferred host operations independently of ordinary Vulkan 1.1 raster
startup. Metal checks supportsRaytracing and compiles MSL 2.4. Browser/native
adapters without the optional profile reject native queries; preferred requests
preserve raster, required requests fail startup explicitly. Metal additionally exposes
native ray stage/function-table execution on hardware-ray devices supporting function
pointers. The logical RT node selects this separately from inline-query compute or
custom/software compute traversal; see ray invocation authoring.
Updates/refits, procedural primitives, motion and asynchronous compute remain unavailable.
The installed ludus_compile_ray_queries(TARGET ... NAME ...) helper cooks the
fixed query kernel with verified Slang/SPIRV-Cross source and binary provenance.
It emits Vulkan SPIR-V 1.5 and MSL 2.4, preserves all five binding decorations,
and checks the 48-byte input / 32-byte result ABI and 64-thread workgroup. Linux
also runs pinned spirv-val --target-env vulkan1.2; macOS instead checks structure
and relies on real Metal compilation/reflection and execution, with that narrower
validation recorded in its manifest. Tools are prepared explicitly with
./scripts/shader-probe bootstrap and ./scripts/bootstrap-spirv-cross.
Native scenes retain shared geometry and an ordered instance/opacity table, including offscreen instances. Arbitrary finite nonsingular affine transforms, eight-bit visibility masks, closest and any-hit visibility queries are supported. Candidate acceptance uses explicit opacity/cutoff and optional retained alpha, UV transform, nearest/bilinear filtering and clamp/repeat addressing. Geometry, scene and program owners can detach while accepted batches retain their versions. Polling never waits; shutdown drains accepted work before destroying native state. Build/read failures and invalid/stale handles are explicit statuses.
Reference workflow and frozen profile¶
After renderer initialization, call EnableReferenceRendering before creating
meshes/material versions, with positive CPU budget and tile size (1–2048).
Select NativeQueries explicitly with a cooked query program and ray-byte budget,
CpuOracle explicitly, or GpuCompute with its two compute kernels and a GPU
byte budget (see GPU-compute reference). The CPU oracle
builds a balanced world-space BVH and uses double-precision permuted/sheared
triangle edge tests. No mode performs implicit fallback. Ordinary raster use retains no new CPU geometry or texture bytes.
Create lit L4/L5 material versions and meshes, then an immutable snapshot. Color
textures require TextureDescription::RetainCpuContent at upload; RGBA8 unorm/sRGB
initialized mips are retained, attachments/HDR retention are rejected. Capture
admission rejects unavailable mips, unlit surfaces, degenerate triangles,
non-finite or zero-length corner normals and other unsupported content, instead of
quietly replacing them. LOD selection,
bakes, AO, shadow atlases and prefiltered IBL do not enter reference transport.
Shading normals decide which side of a triangle reflects and emits, as in pbrt-v4
(Pharr, Jakob and Humphreys, Physically Based Rendering, 4th edition, section 6.5.3,
and Triangle::InteractionFromIntersection/Triangle::Sample): at every hit and
every sampled emitter point the geometric normal is flipped into the hemisphere of
the interpolated corner normal. Winding that opposes authored normals, and corner
normals on both sides of a face (smoothed normals across a crease), are therefore
admitted; where the interpolated normal cancels, the geometric normal shades.
Reflection is two-sided, as pbrt-v4's diffuse, conductor and layered models are
(sections 9.2 and 14.3): a path that reaches the back of the oriented face scatters
there with both normals mirrored towards the viewer instead of ending. Emission stays
one-sided unless authored two-sided. The CPU oracle, native-query transport,
GPU-compute reference and GPU preview share both rules. Capture profile 7 records
them; comparisons require matching profile versions, so profile 6 captures stay
distinguishable.
BeginReferenceCapture copies camera/light/integrator settings and retains the
snapshot. AdvanceReferenceCapture advances one bounded stage/tile or polls a
native batch. Even a Ready setup has zero samples until advanced. There are
four simultaneous capture slots. With NativeQueries, CPU transport/shading submits batched native
queries and explicitly reads their completed results; this implementation is an
offline diagnostic reference, with no GPU-resident transport loop or real-time
path-tracing performance claim. It never hides a wait/readback in raster submission.
The profile uses L4's isotropic single-scattering GGX, correlated Smith, Schlick
Fresnel and Fresnel-weighted Lambert, visible-GGX/cosine mixture sampling with
explicit null events, one light proposal per vertex, power-heuristic MIS and
compensated roulette. The proposal first picks a light uniformly among analytic
lights, emissive triangles and the environment. When it picks an emissive triangle,
a second bounded draw (per-bounce random slot LUDUS_PATH_RANDOM_EMITTER_SLOT)
replaces that uniform choice with one from an integer CDF over emitted luminance
times area, after PBR 4e section 12.6.2's power light sampler. Weights are quantized
against 2^24 and every positive one keeps at least one unit, so each emitter's
selection probability is exact and nonzero. Emitter hits look up the same
probability for MIS by binary search over the ascending emitter list. Many small,
bright emitters therefore no longer crowd out large, dim panels (for example the
candle flames and ceiling light of the pbrt-v4 staircase). Analytic lights and the
environment keep their uniform share; a power-weighted choice across categories is
future work. This is the default ReferenceLightSampling::UniformArea estimator;
Light-sampling modes describes the resampled alternative. Profile version 2 matches L4 point/spot range windows, the squared smooth cone
and the 1e-4 square-meter near-light attenuation floor. Version 1 used a different
spot interpolation and unbounded nonzero-distance attenuation; old captures retain
that profile identity and cannot be compared as the same integral. Emission is two-sided; reflection is restricted to the geometric and
shading upper hemispheres. Textured RGB decodes sRGB before fixed-mip filtering;
alpha stays linear. The capture mip overrides per-material LOD/mip filtering
and LOD clamps. Transmission is limited to the
smooth dielectric reference; there is no rough
transmission, volume, spectral transport, coat,
multiple-scattering energy compensation or shading-normal energy correction.
Depth and finite secondary range are declared truncations. Local/world geometry, generated ray origins, UVs and transform components are
bounded to ±1,000,000 for the reference profile. Admission rejects oversized
finite UV scales before address arithmetic; invalid generated origins fail the
uncommitted tile equally in CPU/native mode. Reconstruction error uses a conservative absolute
FP32 affine-product allowance, geometric-normal projection and outward rounding;
near-zero offsets use a minimum normal float to tolerate GPU flush-to-zero.
Traversal uncertainty also includes twice the largest world-space triangle edge
component, multiplied by this profile's 1e-6 allowance on every coordinate axis.
Affine-only component bounds vanish along the normal of a coordinate-zero plane;
the resulting minimum-float offset allowed CPU/native near-zero self-hits in the
full-scene acceptance workload. The extent term preserves positive normal
uncertainty even on those planes and is shared by both traversal modes. Thanks to
Dirk Gerrit van Antwerpen, Solving Self-Intersection Artifacts in DirectX Raytracing,
Error bounds
(NVIDIA Technical Blog, 2023), for the cross-axis triangle-extent idea. This
world-space allowance differs from that article's RTX-specific object-space
constants; it is not a proof of robustness for arbitrary unresolved thin geometry
or every native intersector. The shifted-plane GPU regression and prepared-scene
CPU/native acceptance retain the original radiance tolerances.
Philox addresses are (pixel, committed sample, dimension) with versioned profile,
sampler and seed. Successful tiles publish compensated raw emission/direct/indirect
sums atomically; a bad contribution fails the tile, preserves committed means and
records its pixel/depth. Increasing only the target continues the same samples.
Changing snapshot incarnation, camera, dimensions, light values, mip, seed or
integrator settings builds a replacement transactionally and increments revision;
failed replacement preserves the previous image. Scene fingerprints are diagnostic
XXH3-128 digests, never identity/reset authority. Retrying the identical failed
request retries its uncommitted sample addresses. Release/disable cancels future
work and retires accepted native uses through completion.
Light-sampling modes¶
ReferenceCaptureDescription::LightSampling selects the next-event estimator for
emissive triangles. All modes estimate the same truncated integral under the same
ProfileVersion, so their means agree statistically, never per pixel. The mode joins
the integral key: changing it replaces the job and restarts accumulation.
ReferenceCaptureInfo records it with its sampler version.
| Mode | Sampler | Emissive next-event estimation |
|---|---|---|
UniformArea (default) |
1, 8 dimensions per bounce | One power-CDF triangle, one uniform area sample, one shadow ray. Existing captures stay bit-identical. |
ResampledProjectedSolidAngle |
2, 128 dimensions per bounce | Resampled importance sampling of one triangle from 32 power-CDF candidates with an unshadowed LTC target, then one projected-solid-angle and one LTC sample of it, so up to two shadow rays per vertex. |
ResampledFormFactor |
3, 128 dimensions per bounce | The same estimator with a cheaper target: Hill and Heitz's approximate horizon-clipped form factors instead of clipped polygon integrals, kept positive wherever the triangle is above the horizon, and its own frozen candidate counts. The selected triangle's samples and MIS weights are unchanged. |
The resampled estimator, its random slots and its BSDF-hit MIS are the many-area-light resampling review. The category draw at slot 0, analytic lights and the environment are unchanged.
The CPU oracle and GPU reference select a typed source and retain its selection
mass before evaluating the incident sample. The category draw still uses slot
0, and the power-CDF triangle draw still uses slot 7 in the uniform-area
proposal. A triangle point or environment direction uses slots 1 and 2.
Analytic lights evaluate deterministically with unit conditional mass. The
resampled category keeps its existing candidate and technique addresses. The
integrator combines the source mass and the matching incident probability in
the existing evaluation order, so this separation does not change the proposal
or require a new sampler version.
- Receiver. Each vertex builds its shading frame and the renderer's
LTC table lookup at its roughness, view and
F0 = 0.04 (1 − metallic) + baseColor · metallic. The diffuse technique's
albedo is baseColor · (1 − metallic) · (1 − F0), clamped to 0.01 per channel,
as in the ris-projected-solid-angle template.
- Light samples. Each technique's direction is intersected with the selected
triangle's plane. Emission, sidedness, alpha and the secondary range are
evaluated at that point, as UniformArea evaluates its area sample, and the
shadow segment ends one representable step before it.
- BSDF hits. A continuation that reaches an emitter is weighted against the
surrogate density P̃ (p₀ + p₁) of the previous vertex. The path keeps that
vertex's frame, LTC and albedo luminances, and its Ẑ once computed, so both MIS
sites evaluate the same function of the direction from identical inputs.
- Cost. A ResampledProjectedSolidAngle vertex evaluates up to 41 triangle
targets (32 candidates, the selected triangle and 8 normalization candidates),
each with two polygon clips. A ResampledFormFactor vertex evaluates its
candidates' approximate targets without clipping and prepares polygons only for
the selected triangle and a BSDF hit's
(approximate target).
NativeQueries tiles need three queries per path, so a resampled capture
requires 3 × TilePixels within RayCapabilities::MaxRays and returns
Unsupported otherwise.
- Probes. Static-lighting diffuse probes stay on UniformArea; their virtual
first receiver has no stored frame for the BSDF-hit density. Both resampled modes
are rejected there.
The opt-in Catch2 tag [.reference-light-sampling-benchmark] in
ludus_renderer_render_tests compares the two modes on 3200 emissive triangles, a
third of them textured. It reports the transport kernel's device time per sample,
shadow rays per pixel, and MAPE and RMSE against a high-sample UniformArea
reference, at equal samples and at equal kernel time. It takes 4–7 minutes on Apple
M5 and is not part of the CTest run:
out/build/macos-clang-development/modules/graphics/renderer/ludus_renderer_render_tests \
"[.reference-light-sampling-benchmark]"
On Apple M5 Metal (macOS 26.6, macos-clang-development with Clang 23.1.3 and
Slang 2026.1.2), on October 29, 2026, at 288 × 192:
- Cost. One ResampledProjectedSolidAngle sample took 17–38 times as much
kernel time as a UniformArea sample: 20–22 ms against 0.9–1.3 ms with direct
lighting only, and 82–90 ms against 2.3–4.1 ms at depth 4. It traced twice as
many shadow rays: 0.92 against 0.46 per pixel with direct lighting only, and
1.98–2.10 against 0.99–1.06 at depth 4.
- Equal samples. The resampled mode had lower error in every case, on the GPU
and on the CPU oracle. With direct lighting only, MAPE fell by 2.0–2.3× and RMSE
by 1.6–2.3×; at depth 4, by 1.4–1.6× and 1.6–1.9×.
- Equal time. UniformArea had lower error in every case: 1.7–3.3× lower MAPE
and 1.8–3.7× lower RMSE, in three runs.
So ResampledProjectedSolidAngle is the lower-variance estimator per sample, but not
per unit of time on this workload. The
review's evaluation
has the method, all measurements and the comparison with the template benchmark.
ResampledFormFactor, measured the same way on the same day in two runs, has
ResampledProjectedSolidAngle's equal-sample error within 1% for 4.6–5.3 times less
transport kernel time: 2.6–6.5 times a UniformArea sample. At equal time it beat
UniformArea with specular direct lighting in both runs. It was within timing noise
of UniformArea with diffuse direct lighting, and still lost by up to 1.4× at
depth 4. The approximate target measurements
have the tables and the M and K sweep.
CPU budgets cover renderer-owned mesh/material/job arrays and setup scratch,
including simultaneous old/new jobs during reset. They exclude existing renderer
state, RHI CPU upload shadows/registry storage, caller arrays and tool/driver memory.
Native budgets cover device-wide requested ray buffer/AS bytes and pending
build scratch, checked again before query submission. GetRayResourceStats reports
resident, pending scratch and detached bytes; pipeline/driver allocation and VMA
backing-page overhead are unavailable, not measured zero. These are admission
budgets, not total process/device-memory measurements. Capacity errors never drop
instances, lights, pixels or samples.
GPU-compute reference¶
ReferenceTraversal::GpuCompute is RT1's GPU implementation. Cook the
renderer_reference and renderer_reference_reduce recipes with
ludus_compile_renderer_shaders and pass both kernels to the three-argument
EnableReferenceRendering, with positive CPU and GPU byte budgets, before any
mesh or material version exists. It needs negotiated compute storage but no
native ray queries. Devices without compute reject with Unsupported.
Setup is the CPU oracle's: the same admitted geometry, materials, retained
texture mip and binned-SAH BVH, packed once in the GPU preview's scene layout. Each
AdvanceReferenceCapture(capture, surface) step polls setup or a readback, or
submits one ordered graph with two compute passes in a frame of surface. The
transport pass traces one sample for every pixel at the image's minimum committed
count. It walks the BVH with float32 ports of the oracle's slab and watertight
triangle tests (ray_bvh_traversal.slang, shared with render-graph ComputeBvh
ray passes). Each edge function is evaluated in a canonical vertex order, so fast-math
multiply-add contraction cannot open cracks between triangles that share an edge. The
pass applies the same masked opacity, light proposals, power MIS,
visible-GGX/Lambert sampling and compensated roulette. It adds emission, direct
and indirect transport to Kahan-compensated device sums. Every random value uses
the oracle's Philox address, so both implementations trace matching paths. A
single-group reduction totals per-pixel query and event counts into a 48-byte
status record, the only per-sample readback. The committed sums, 48 of each
pixel's 96 accumulation bytes, are copied back in transfer-sized slices after
power-of-two sample counts, every 16th sample, a failure and the target.
Ready follows the final copy; until then CopyReferencePixels returns the
previous copy, whose counts can trail MinimumSamples.
Commits are per pixel. A nonfinite or negative contribution, invalid ray or BVH
stack overflow leaves that pixel's sums and count unchanged and records its
lowest failing pixel, addressed sample, scattering depth and validation stage.
The stage is retained through device reduction and host readback, including camera,
scene-layout and traversal-capacity errors; a failed GPU path does not report None.
The capture fails once the other pixels' sums are read back,
with CapacityExceeded for a stack overflow. The identical Update retries only
the uncommitted sample addresses; changing only the target continues the
accumulation, and other changes rebuild the job transactionally. Release
cancels future dispatches; the RHI retains accepted dispatches and readbacks
through completion. The GPU budget charges the packed scene and texels, the
224-byte parameters, 96 accumulation bytes per pixel and the status record. The
RHI resource budget, storage range and dispatch limits can reject a capture
sooner. Transfer rings, pipelines and driver padding are excluded. The trace kernel
also binds the renderer's 3,407,872-byte LTC table at slot 5, uploaded once when
rendering is enabled and charged to the GPU budget then. It reads the table only
for ResampledProjectedSolidAngle, which a parameter flag selects at run time.
Kernels cooked before the table binding fail EnableReferenceRendering with
InvalidDescription.
Normalized vertex colors and decoded sRGB texels stay in [0,1] in both paths. The FoundationMath CPU/shader sRGB conversion preserves exact black and white endpoints: GPU power/division rounding must not turn a white texel into an out-of-range BSDF base color. Nonfinite inputs still fail validation; radiance and throughput are not clamped. The white-texture raw-AOV regression covers this case independently of the supplied Staircase2 World acceptance test.
Float32 is the only departure from the oracle's arithmetic. Slab exits widen by
1 + 2γ(3) (Pharr, Jakob and Humphreys, PBRT 3rd edition, section 3.9.2). The
watertight edge test omits the paper's float64 fallback for exactly-zero edges.
Near the edge opposite a triangle's first corner, the two float32 barycentric
quotients can round to a sum above one, which surface reloads such as
GetSurfaceHit reject. The traversal therefore clamps both to [0,1] and bounds
the third corner's weight by one minus the second's. The oracle's float64
quotients, cast to float32, already satisfy that bound.
Normalization and the environment CDF also use float32. These can change an
individual path at grazing or near-edge hits; they do not change the declared
integral. One dispatch covers the whole image, so very large dense scenes can
approach a device's command timeout; there is no tiling yet.
The CPU oracle and the GPU kernel share one BVH. It is built with a 16-bin surface area heuristic over triangle centroids (Wald, "On fast Construction of SAH-based Bounding Volume Hierarchies", RT 2007). Leaves hold at most four triangles. Any range whose remaining SAH splitting could exceed depth 48 falls back to a balanced median split, so traversal always fits the 64-entry stacks. Each interior node records its split axis. Both traversals visit the child on the ray's near side first, so a closest hit there culls the far child sooner. Closest hits do not depend on node order, except where a box test's rounding decides a grazing hit or an exact tie on a shared edge. In the staircase2 capture below, one of 7,168 CPU-oracle pixels changed by 3e-7 luminance. In the GPU capture, 89 of 115,200 pixels changed and the mean luminance moved from 0.427322 to 0.427327.
ludus_reference_benchmark (built with the renderer's GPU tests) times a capture
of a cooked LUSCN scene, such as ludus-cook cook output for staircase2. It
reports setup-to-first-sample time, total wall time and the device time of the
transport passes. The device time is not inflated by CPU load from other processes.
It also prints a checksum of the converged radiance. Setting
LUDUS_REFERENCE_BENCHMARK_OUTPUT to a directory saves the last image as
float32 RGB:
out/build/macos-clang-debug/modules/content/scene_cook/ludus-cook cook \
references/resources/scenes/staircase2 out/bench/staircase2
out/build/macos-clang-debug/modules/graphics/renderer/ludus_reference_benchmark \
out/bench/staircase2 512 225 64 8 gpu 3
On Apple M5 Metal (macOS 26.6, debug build, about 29,000 triangles), the median BVH needed 7.8–7.9 s of transport device time for 512×225 pixels at 64 samples per pixel and depth 8. Wall time was 9.5–9.6 s. The SAH BVH with near-first traversal needs 1.03–1.13 s of device time and 2.6–3.0 s of wall time. Of that gain, SAH alone gives about 7× and near-first ordering about 10%. Setup to the first sample grew by about 0.2–0.3 s for the larger CPU build. The CPU oracle at 128×56 and 8 samples went from 9.4–10.0 s to 2.9–3.5 s. Wall time is now mostly the per-sample dispatch, wait and status readback, not tracing.
On Apple M5 Metal (macOS 26.6, debug build), the renderer test renders the L6 fixture at 16×12 and 64 samples per pixel with both implementations. The cases are orthographic depth 4 with the emitter and environment; perspective depth 8 with directional, point and spot lights and roulette from depth 2; and a single bounce. The largest RGB distance between CPU and GPU means is zero for every AOV. Closest, null-BSDF and roulette counts are equal; one of 11,661 visibility queries differed, where a shadow contribution rounded to zero in one precision. A target-only update equals a fresh capture bit for bit. The editor's lit viewport fixture, with textures and point lamps, agrees within 0.002 per pixel. Vulkan and WebGPU compile the kernels, but their pixel parity is unqualified.
On the same Apple M5 Metal debug build, ResampledProjectedSolidAngle is checked
in both implementations by the
light-sampling unit tests,
the shader parity kernel
and the capture tests in the renderer test above:
- CPU and GPU. Raw AOVs agree within the UniformArea tolerances: at most
3.2e-5 RGB distance per pixel at 16×12 and 64 samples per pixel on the L6
fixture at depth 4 with a sun, the dense glossy sphere at depth 6 and a room
where most vertices estimate Ẑ = 0. Closest, null-BSDF and roulette counts are
equal; visibility counts differ by at most 3 of 16,160. Matching candidates and
tickets select the same emitters in both precisions on these fixtures; the
tests still allow a selection to flip.
- Against UniformArea. Six seeds of 64 samples per pixel on the CPU oracle
agree in mean emission, direct, indirect and total radiance at depths 1, 3, 4
and 6. The largest difference is 1.4 combined seed standard errors (direct light
in the room); the test allows 5 plus a 2e-4 floor. Primary emission is
bit-identical. The seed standard error of direct light falls by 1.3× on L6, 1.4×
on the glossy sphere and 2.6× in the room, and of glossy-sphere indirect light
by 2×.
- Edge cases. An emitter behind every receiver gives zero radiance and no
shadow rays in both modes and implementations. Ẑ = 0 applies the inclusion
bound bit for bit, an empty LTC-space clip disables the specular technique, and
the light density is bit-identical at both MIS sites. The weights partition
unity. On the device, 3 of 4,096 drawn receivers differ from the host by more
than 1e-3 relative in an integral or density above 1e-5; smaller slivers are
limited by single precision in both.
- Identity. UniformArea captures on both implementations, including query
and event counts, are bit-identical to those before the mode was added.
Raw capture and installed SDK¶
CopyReferencePixels copies raw linear means and committed counts without
exposure/tonemapping. EncodeReferenceHdr writes 256 + Width*Height*16 bytes:
magic LUHDR01\0, little-endian version/profile/sampler/header sizes at bytes
8/12/16/20, width/height/target/depth/roulette/flags at 24–44, seed/revision and two
64-bit scene digest words at 48–72, secondary range/mip/light count at 80–88,
column-major world-to-clip floats at 96–159, eye at 160–171 and environment at
172–183. Optical wavelength in metres occupies byte 92 (zero for geometric transport);
remaining header bytes are reserved zero. Flags encode CPU (1), center
rays (2), orthographic (4), reverse-Z (8), infinite far (16), partial (32),
GPU compute (64), resampled light sampling (128) and scalar optical connections (256); neither
traversal bit marks native queries. The sampler field is 1 for UniformArea and 2
with flag 128; scripts/reference-hdr rejects any other pairing and records
light_sampling in its manifest, with center_rays for flag 2.
The scalar optical connection profile and its CPU/GPU contract are described in
render-graph authoring.
Each top-left ordered pixel contains three FP32 mean RGB channels and a uint32
committed count. The light count/digest identify the retained scene; applications
must save original light/asset inputs for independent scene reconstruction.
The independent installed SDK fixture
uses only public APIs and FoundationFilesystem atomic publication. Configure
tests/sdk_consumer/renderer with the installed SDK, shader compiler, validator
(Linux) and translator paths. Run ludus_sdk_reference_capture out/reference.luhdr
for the explicit CPU oracle, or append native to require native queries. Convert
without changing radiance with:
python scripts/reference-hdr out/reference.luhdr --pfm out/reference.pfm --manifest out/reference.json
PFM rows are flipped to its bottom-up convention; JSON retains counts, settings, digest and capture SHA-256. Partial files remain visibly marked. Invalid headers, counts and negative/nonfinite pixels are rejected.
Evidence and departures¶
Unit tests cover deterministic replay, progressive target continuation, transactional resets, snapshot retention, export/output preservation, BSDF mixture PDF integration versus accepted mass, furnace energy and null proposals. Physical Apple M5 Metal execution compares raw direct/indirect/total radiance against the CPU BVH at 128 samples/pixel with an offscreen emitter and diffuse wall; RGB-vector distance tolerance is 0.002, with nonzero indirect radiance and zero invalid samples. Native query tests cover alpha rejection, offscreen instances and detached-owner lifetimes. A public-only installed-SDK 32×32 capture at 256 samples/pixel also agreed between CPU/native traversal: maximum RGB-vector error 7.3e-8 and RMS 9.8e-9 on Apple M5; equal scene digests and complete pixel counts were verified. These fixtures establish a baseline, not convergence for the full reference scene suite or GPU speedups. Linux CI compiles Vulkan, runs validation layers and preserves raster on software devices without the optional ray profile; a ray-capable Vulkan hardware qualification remains necessary.
Implementation attribution beside code credits PBRT 4e §6.1/§13.4 for geometric origin offsets and MIS/roulette, Heitz JCGT 7(4) §3–4/Appendix B for visible-GGX, Woop/Benthin/Wald JCGT 2(1) §3 for the CPU triangle oracle, Filament §4.4/§5.2.2 for the shared reflection model, W3C CSS Color 4 §10.2 for sRGB transfer, and Khronos/Apple for native query/build synchronization. The more ambitious plan below retains future goals; it is not a statement that dynamic scenes, GPU transport, all parity AOVs, arbitrary content import or full Staircase2 are already qualified.
Traversal capability and backend boundary¶
Start with opaque triangle acceleration structures and shader ray queries on
verified native devices. Expose distinct capabilities for structure build/update,
inline queries, dedicated ray pipelines, procedural intersections and motion;
also query limits, alignment, scratch requirements and shader target support.
Unsupported, NotReady, capacity failure and device loss remain explicit.
Ray hardware, compute availability and bindless support are separate facts.
Khronos's ray tracing guide distinguishes shared acceleration structures from query and pipeline execution. Queries fit ordinary shader passes; dedicated stages offer different scheduling choices. Begin with query-based compute effects and a loop-based path integrator. Keep nearest accepted hit and early-exit visibility as separate shader operations. Native Metal stage execution uses visible function tables for ray-generation, closest-hit and miss callbacks, plus intersection function tables for alpha acceptance. Vulkan native stage pipelines remain unavailable. Backend stage support does not promise a speedup over inline queries. Shader binding tables, native addresses, hit-group indexing and recursion limits remain backend details; do not force Metal's model into a Vulkan-shaped public table abstraction.
The existing Vulkan subset does not establish support: negotiate the necessary
extensions/features/dependencies in a separate RHI profile and validate shader
compiler output. Metal acceleration structures
also require device/OS/compiler evidence; the pinned MSL 2.3 raster target is not
proof that a proposed ray shader works. A newer ray target needs an explicit
toolchain/profile change. Ludus's current WebGPU/WebGL paths have no hardware
ray contract. Render-graph ray passes can select the ComputeBvh strategy, which
walks the renderer's packed BVH in ordinary compute with the RT1 reference's
traversal; it is another implementation with its own cost, not a hardware
capability or an automatic fallback. Its kernel already compiles to WGSL, but the
graph ray profile does not yet admit browser targets. Do not hide CPU readbacks
behind it.
Scene and acceleration-structure ownership¶
Renderer's private ray scene references exact mesh, deformation, transform, material and texture versions. BLAS (bottom-level acceleration structures) describe geometry; TLAS (top-level structures) instance those geometries. RHI owns native structures and authoritative retention; GDI schedules their work.
- Reuse one static BLAS per compatible mesh/geometry version across instances. Key it by layout, topology, build flags and opacity classification. TLAS records contain stable instance IDs, masks, transforms and bounded shading-table indices. A material reload that changes opacity may require a different structure version.
- The ray scene includes offscreen shadow casters and reflection/indirect geometry. Camera frustum culling of raster packets must not remove ray-visible instances. Streaming regions, ray masks, trace distance and proxy/LOD selection are explicit quality policies. Reference captures require a frozen complete declared scene; missing geometry/residency is a status, not an invisible approximation.
- Raster and rays use the same selected pose and object transforms. Skin/morph evaluation completes before building/refitting its BLAS and before raster use. Refit only when supported and topology/build flags permit it. Rebuild after measured traversal degradation; animated geometry can make repeated refits slow. Bound rebuild work and expose deferrals or quality reductions.
- Keep BLAS geometry local and use a declared render origin for TLAS, rays and shading. Multiple views can share a compatible TLAS or use different origin versions. Validate nonuniform/mirrored transforms and geometric-normal winding; singular transforms fail admission. Rebasing invalidates incompatible histories.
- Declare deformation-write to build-read, BLAS-build to TLAS-build and final build-write to trace-read dependencies. Scratch reuse waits for completion of all overlapping builds, not the end of CPU recording. Cross-queue execution requires explicit RHI ownership and completion evidence.
- Compaction or relocation publishes a new ready version after its copy/build dependency. Retain every old structure and shading resource until its accepted uses complete; TLAS versions retain the BLAS versions they reference. Never patch an in-flight TLAS address or replace its shading table in place. Build inputs remain alive through builds and longer when later shading reads them.
Start serialized on one queue with static geometry and a full TLAS build when instances change. Add supported updates, amortized static builds, compaction and queue overlap individually after measuring build time, traversal time and peak overlap memory. AS caching is session-local initially; native serialization needs an explicit device/driver compatibility and content validation contract.
Material and intersection parity¶
Cook one surface description into raster evaluation and ray evaluation/sampling variants. Private shader functions define BSDF evaluation, sample and probability density with matching lobe weights and conventions; a shared name alone does not prove equivalence. Validate albedo, roughness, normals, UVs, emission, handedness and texture encoding with raster/ray AOVs (auxiliary output images).
The first reference profile is RGB, triangle surfaces, diffuse/GGX reflection, opaque and explicitly alpha-masked materials, finite area emitters, analytic point/spot/directional lights and an environment. Profile 8 adds smooth transmission with Fresnel, inside/outside index tracking and total internal reflection (smooth dielectric reference); rough transmission, volumes, subsurface scattering, hair and spectral transport are further features. Raster alpha blending is not a physically specified transmission model. Unsupported materials make a capture incomplete or fail its requested reference profile; an error-material preview must never be labeled a reference image.
Retain geometric and shading normals separately, including their front/back-side meaning. Opacity acceptance uses the declared texture/threshold/LOD policy in both closest-hit and shadow queries; an opaque early-exit query cannot be reused for alpha-masked casters without candidate filtering. Two-sided masks and transmission visibility are distinct policies. Ray texture filtering has no implicit raster derivatives: initially use an explicit fixed-mip reference policy, then add verified footprints/ray cones as a named filtering profile. Residency and mip-policy changes invalidate accumulation; compare like filtering models.
Carsten Wächter and Nikolaus Binder's “A Fast and Robust Method for Avoiding
Self-Intersection” (Ray Tracing Gems, chapter 6, sections 6.2–6.3) motivates
barycentric hit reconstruction and scale-aware offsets along the oriented
geometric normal. Thanks to the authors. A global 0.001 offset, shading-normal
offset or primitive-ID rejection alone is insufficient. Verify the chosen method
under Ludus's transforms/compiler precision; the chapter itself identifies thin
crevices and extreme transforms as limitations. Define normalized directions,
ray interval units and endpoint handling, including offsetting both endpoints
for area-light visibility segments. Expose offset diagnostics instead of scene
authors tuning a hidden epsilon.
Progressive reference integrator¶
Jakub Boksansky and Adam Marrs's “The Reference Path Tracer” (Ray Tracing Gems II, chapter 14, especially 14.3.1–14.3.7) supports integrating an inspectable reference into the existing renderer. Thanks to them: retain their staged primary-ray/AOV checks, shared scene data and simple iterative transport, while replacing their DXR-specific setup with Ludus's optional traversal contract. This is a planned reference implementation, not a claim that their sample or companion code shipped.
- Freeze a scene snapshot and all exact resource versions. Generate pinhole rays from the same camera/projection and viewport conventions as raster, sampling within pixel footprints. Compare unjittered center rays first; camera near/far clipping is an explicit primary-visibility policy, distinct from secondary-ray ranges. Thin lens and shutter/motion integration are extensions.
- Carry radiance, throughput, sample state and previous sampling information in a bounded iterative loop. At a hit, evaluate emission, sample a light and its visibility, then sample the BSDF to continue. An escaped path evaluates the environment. Start with one light sample and one continuation per vertex.
- Combine light and BSDF sampling with multiple importance sampling (MIS). Light selection probability multiplies the light's conditional density; convert area and directional densities to the same measure before weighting. Evaluate the competing density for BSDF-hit emitters/environment and handle delta lights/lobes separately. This avoids double-counting emission and excessive noise from choosing only one proposal. Zero-light and zero-density cases terminate or contribute zero safely, without invalid arithmetic.
- Begin compensated Russian roulette after a defined minimum depth; divide surviving throughput by survival probability. A finite maximum depth truncates transport and introduces bias: report it in metadata and perform depth sensitivity checks. Accumulation converges to the declared truncated model, not an unqualified physical ground truth. Energy clamps, path regularization and roughness modifications belong to separately labeled preview profiles.
- Accumulate scene-linear, unexposed radiance in FP32 with a defined numerical accumulation scheme and checked per-pixel sample counts. Exposure/tone mapping operate on the displayed mean, never on accumulated samples. For long exports, combine bounded sample batches with a higher-precision offline accumulator; document rounding and count limits. Export linear HDR plus the profile metadata.
Anders Lindqvist's “Multiple Importance Sampling 101” (Ray Tracing Gems II, chapter 20, sections 20.1.3–20.2) adds the explicit same-measure and matching-PDF checks. Thanks to Lindqvist; choose and test a documented heuristic rather than copying the sample's scene/light assumptions. The Gems review records this revision. PBRT 4th edition, section 13.4, provides a further check on emission weights and compensated path termination; Ludus's RGB/bounded profile differs from its broader transport implementation.
Use deterministic random sample addressing by pixel, sample index and dimension,
with a capture seed and versioned sampler. Retries reproduce uncommitted samples;
the accepted-work boundary controls accumulation just as it controls other
histories. Store an accumulation key containing camera, viewport/resolution,
scene/pose, lights, materials/textures, origin, sampler and integrator versions.
Reset on any change that alters the sampled integral; changing only display
exposure need not reset unexposed data. Overlays never enter accumulated radiance.
Raw, independently sampled accumulation is the validation output; a denoised
preview is an additional image. The optional graph
Denoise operation
uses OIDN on linear HDR color before display conversion and leaves the accumulation
unfiltered. Its initial native sharing profile is Metal on Apple silicon. Measure convergence across multiple seeds.
Nonfinite contributions or broken density invariants invalidate a reference
batch and report the offending sample; replacing them with black silently would
bias the result. ReferenceCaptureInfo::Failure identifies the rejecting validation
stage, while FailurePixel, FailureSample and FailureDepth locate the path.
These fields are meaningful when InvalidContributions is nonzero; a backend
failure can occur without a path-validation record. Previously committed tiles
remain available for partial diagnostic comparisons. CPU and GPU reference paths
bound interpolated vertex RGBA multipliers to their admitted [0,1] domain before
shading and opacity tests. A three-term FP32 barycentric sum can otherwise turn
constant white into 1.00000012 and falsely reject a valid BSDF. Nonfinite
interpolation still fails validation; radiance and throughput retain their
existing checks. Capture jobs reserve a bounded residency budget for their pinned
versions and support cancellation, releasing submitted uses through completion.
Hybrid and real-time transport¶
Introduce effects in order: selected shadow visibility, then roughness-aware reflections, then indirect diffuse, each with its own quality and fallback policy. Raster provides primary visibility and the attributes needed to launch secondary rays. Hit shading uses the same ray-scene tables; screen-only geometry/material data is inadequate for an offscreen hit. Assign contributions explicitly so reflection/probe blending and partial shadow replacement do not double light.
Trace into effect-specific linear signal buffers with validity, normal/depth, roughness, motion and any required hit-distance guides. Define whether signals contain albedo, their exposure domain, motion direction/units and guide spaces. Keep reconstruction behind concrete pass functions with those contracts. Reproject using actual previous camera/object/deformation data; reject disocclusions and incompatible material/light/geometry versions. Reflection motion may require hit surface motion in addition to the primary surface. A stable camera alone does not make a lighting history valid. Bound history memory separately for each view.
Start with ordinary sampling plus a modest, inspectable temporal/spatial filter.
ReSTIR DI (direct light sampling), GI (indirect paths) and PT (path resampling)
are different upgrades. Reservoir reuse requires specified target densities,
weights, sample identity, visibility revalidation, age limits and bias policy;
do not substitute “reuse the last ray” for an estimator. The May 2026 paper
ReSTIR PT Enhanced
by Daqi Lin, Markus Kettunen and Chris Wyman identifies contemporary work on
reconnection, correlation and reuse cost. Its abstract is discovery evidence,
not a completed algorithm review or a Ludus speedup. Review the full estimator
and validate it against the independent reference before adopting it. The Guided ReSTIR FG+ graph template
implements a bounded reflective/static-history photon pipeline with separate path
and caustic reservoirs. Its owning guide states the material, sampling, memory and
qualification limits; it is not a completed dynamic/transmissive reference pair.
The
many-area-light resampling review
proposes RIS over emissive triangles with projected solid angle/LTC sampling,
first as a ris-projected-solid-angle render graph template and then as an
explicit second RT1 light-sampling profile. Both are implemented; the RT1 profile
is ResampledProjectedSolidAngle in the CPU oracle and the
GPU-compute reference, and UniformArea remains the default reference oracle.
Keep a single loop kernel initially. A wavefront implementation can later split intersection, hit shading and shadow work into compact queues when divergence and occupancy measurements justify added dispatches/storage; PBRT's chapter 15 is a follow-up source. Queue bounds are correctness contracts: process bounded tiles or return an explicit incomplete result instead of dropping paths. Sorting, ray reordering, adaptive sampling and reduced resolution need independent quality/cost evidence. Fix sampling variance and AS build/traversal costs before adding a general GPU work scheduler. No vendor denoiser or resampling SDK is a mandatory dependency.
Ray milestones and evidence¶
These optional milestones depend on explicit compute/material/attachment and RHI extensions; they do not claim any existing R1 backend already traces rays.
| Phase | Deliverable | Acceptance |
|---|---|---|
| RT0: scene/query foundation | Optional native capability profile, static triangle BLAS/TLAS, retained tables, primary and visibility queries | Raster/ray instance, position, normal, UV and depth agreement; offscreen hits; build/read/lifetime stress; unsupported devices preserve raster |
| RT1: progressive reference | CPU oracle and GPU reference implementations of the same frozen RGB surface profile, light/environment/BSDF sampling, MIS, raw accumulation and HDR export | Matched raw CPU/GPU emission/direct/indirect/total comparisons; furnace/Cornell/emissive fixtures, sampling/PDF normalization, multiple seeds/depths, no double lighting; unsupported content and overflow are visible |
| RT2: hybrid effects | Bounded shadows, reflections and then indirect diffuse with reconstruction/fallbacks | Raw and reconstructed images compared to RT1; moving/disoccluded/reloaded scenes, per-view history, build/ray/filter median and tail times |
| RT3: dynamic scene scaling | Skinned/morphed BLAS, measured refit/rebuild, optional compaction and scheduling | Pose parity, changing topology/opacity, extreme/mirrored transforms, thin geometry, streaming/rebase/loss and peak overlap memory |
| RT4: high-end real-time PT | Reviewed resampling, reconstruction and optional wavefront/ray-pipeline variants | Scene suite at fixed resolution/quality budget; temporal artifacts and error versus RT1; measured target-GPU benefit and disabled-mode support |
Track AS resident/scratch/pending bytes, build/refit/compaction costs, traced ray classes, path-depth distribution, opacity candidates, NaN/invalid-PDF counts, sample counts, rejected histories and incomplete tiles. Actual hardware traversal statistics may be unavailable; distinguish estimates from measured counters. Inspect instance/primitive IDs, barycentrics, geometric/shading normals, ray offsets, BSDF/light PDFs, MIS weights, direct/indirect/emission AOVs and raw/filtered error. Selected-pixel path logging is bounded and opt-in. Reference scene fixtures and statistical image tolerances matter more than reproducing one vendor's exact floating-point image. No synchronous shipping-frame readback is required.
Debug drawing, sprites, text and fonts¶
Debug submissions contain explicit view mask, space, depth mode, color domain, width units and lifetime. Start with lines, boxes, axes, spheres and labels. Reserve a frame arena; persistent commands use generation-checked handles and bounded lifetime, while each worker has a private append buffer with deterministic merge. Optional overflow drops complete commands and increments counters.
Portable thick lines expand into triangles. Clip world segments against the near plane before perspective divide; handle zero-length segments, finite joins, caps and minimum physical width. World-unit and physical-pixel width are distinct. Depth-tested, always-on-top and two-pass x-ray modes are explicit variants. Hardware wide lines are optional because API limits and raster rules differ. Translucent overlays preserve order; opaque depth-tested primitives may batch under the ordinary compatibility rules. Geometry is shared across views where possible, but clip/width expansion can be per view. Debug work must not modify scene visibility, lighting or temporal history.
Text shaping, script/direction runs, cluster identity, font bytes and rasterization remain with the existing Text owner. UI/paragraph code owns bidi itemization, fallback, line breaking and carets; renderer never maps one Unicode code point to one quad. Submit shaped glyph IDs/positions with a font version and physical raster size. Preserve original cluster provenance for inspection.
The first text adapter uses size-specific grayscale coverage, an append-only bounded R8 atlas, transparent gutters and no mipmaps, plus adjacent instanced quads in painter order. R8 sampling and streaming atlas writes are explicit RHI prerequisites; RGBA replication is a declared transitional variant if necessary. Atlas coordinates stay stable until a version reset; submitted pages/regions remain leased. Missing/not-ready glyphs have a visible deterministic fallback, and atlas pressure is reported. Reuse layouts and glyph cache entries; do not reshape/rasterize static labels every frame. MSDF or analytic outline rendering is a separate measured option for large/transformed text, preserving shaping and the application contract. Never assume subpixel LCD rendering survives browser composition or arbitrary display orientation.
Preserve the selected GraphicsText ownership: GraphicsText owns glyph preparation/cache, CPU atlas shadows and ordered text lists. Its Renderer adapter contributes generic leased quad packets to the frame plan; Renderer owns composition, and RHI owns GPU resources. Do not create a second glyph cache inside Renderer or make CPU Text depend on graphics. The standalone text milestone and its narrow coverage interface remain a valid independent consumer; moving its uploads into shared GDI services is an explicit integration change with the same readiness and lifetime guarantees. Its pinned shader build path need not be replaced merely to share packet submission.
Sprites and screen quads reuse the submission allocator but keep independent material/texture semantics. Nine-slice and layout remain UI policy. Batching across a different scissor, blend mode, atlas page or layer is forbidden. Filtering gutter/mipmap policy belongs to the atlas's content type; font rules do not automatically apply to sprites or lightmaps.
The Aurelio Reis chapter (Game Programming Gems 8, 1.1) supports separating fixed quad geometry from glyph-instance data, but its constant-array/console constraints are historical. Thanks to Reis: measure one-glyph, short-label and large-overlay cases before choosing a packing/instance path. An optimization must preserve painter order and the Text shaping contract.
Manny Ko's atlas chapter (Game Engine Gems 3, 9.4–9.5) sharpens the cook/runtime boundary. Thanks to Ko: reserve filtering footprints before packing, report occupancy including gutters, and validate chart seams. Use shelves for mutable rectangular glyph pages and an offline chart packer for lightmaps; only measured offline packing loss justifies irregular bitmask packing. Lightmap mip generation must respect chart adjacency and mip-safe padding (or a clamped LOD policy). Glyph coverage uses transparent borders; unrelated sprites use their own edge extrusion. Diffusing unrelated glyphs or colors across atlas cells is invalid.
Inspection, budgets and optimization gates¶
Expose counters for extraction/culling/sorting/recording time, visible/culled/LOD counts, batches/instances, triangles, binding/pipeline switches, upload bytes, pipeline misses, light-list/shadow overflow, atlas occupancy, peak frame arena, GPU memory categories and pending retirement. GPU pass timings are optional and report unavailable distinctly from zero; asynchronous readback never stalls a shipping frame. Report median and tail frame times with resolution, scene, adapter/driver/backend/tool pins, frames in flight and cold/warm state.
Give every pass/resource/view a bounded label and each packet a source ID. Inspect why an object was culled, which pipeline/version bound, exact texture ranges, light selection, shadow ownership, viewport mapping and history resets. Provide modes for direct CPU draws, unsorted packets, disabled batching, frozen LOD, no shadows, constant materials, overdraw, normals, depth, cluster occupancy, lightmaps, atlas pages and linear/output color checks. Capture the immutable frame plan and reproducible resource revisions; native GPU captures supplement that record. Do not build a second command serializer to obtain basic tracing.
Optimization acceptance compares image correctness and memory/latency as well as throughput. Turn on a new path only after representative scenes show an improvement at acceptable tail latency and maintenance cost. Keep opt-outs for investigation. GPU-driven geometry, async compute, virtual textures and virtual shadow maps need separate proposals with residency, synchronization, capability fallback and debugging evidence. Ray/path tracing follows the optional RT0–RT4 plan above. These are modern candidates, not unconditional definitions of a good low-level renderer.
Modern technique selection¶
The SIGGRAPH 2025 Advances course includes production work on stochastic many-light rendering, ray-traced world lighting and order-independent transparency. The program establishes relevant research directions; it does not establish their suitability or cost for Ludus. The following choices are engineering judgments, with future algorithm reviews required before implementation.
| Option | Adopt when | Contract required first |
|---|---|---|
| Clustered forward | Many local lights and varied materials make per-object lists expensive | Conservative 3D assignment, checked lists and transparent coverage |
| Deferred/visibility buffer | Expensive opaque shading/overdraw dominates and bandwidth/material indirection wins in comparison | Separate transparent path, material reconstruction, derivative/MSAA rules |
| GPU visibility/indirect | CPU submission/culling dominates after ordinary batching | Stable object IDs, output capacity, conservative occlusion, direct reference mode |
| Bindless/mesh shaders | Verified target limits and workload justify reduced binding or geometry work | Descriptor/version retirement and authored portable shader variants |
| Temporal upscaling/TAA | Shading cost or image quality warrants history complexity | Motion vectors for camera/object/deformation, disocclusion, exposure and jitter conventions |
| Hybrid ray tracing / real-time path tracing | Dynamic scenes need quality beyond bake/probe/direct-light approximations | RT0–RT4: shared ray scene, retained structures, independent reference, reviewed estimators and reconstruction budgets |
| Virtual textures/shadows | Measured working set exceeds conventional residency | Page-table versions, feedback latency, initialization, eviction proof and coarse fallback |
| Async compute | Actual queue overlap improves total time without excessive bandwidth contention | Cross-queue dependencies, ownership and completion proof in RHI |
Optional advanced features must preserve scene/view/material identities and expose their own diagnostic data. A tiny scene, simple UI or limited GPU should not pay mandatory constant costs for a technique chosen to scale a different workload. The contextualized research plan orders architecture/lifetime and shader-debugging reviews before advanced scaling. Record the consulted source sections and a concrete contract change or experiment before promoting a program description into an implementation decision.
Interactive World viewport¶
The primary editor viewport renders the selected project's current World/level
extraction and current camera through the ordinary renderer. World ownership,
entity-to-mesh/material extraction, selection and camera controls belong above
GraphicsRenderer; the renderer consumes Snapshot and ViewDescription, just
as a game runtime does. It must not substitute a fixed acceptance scene or an
offline capture for the selected World.
RasterViewport packages that existing raster path into GPU output textures.
Its ViewportColorMode::Linear8 mode supports unlit textured snapshots using a
linear RGBA8 scene attachment; the default prefers HDR for lit snapshots when
enabled. Unlit textured material programs cannot draw into HDR attachments.
Initialize views/lighting on the renderer, then initialize the viewport with
physical dimensions and exposure. On a World edit, publish a complete replacement
snapshot; on a camera change, supply its current WorldToClip and camera values.
Update atomically prepares the latest snapshot/view and preserves the prior
view on rejection. Render draws that view and tone-maps it on GPU; it requires
no reference input retention, BVH, compute or ray-query capability and has no
sample target or convergence state. Poll Pending resource/view setup before
rendering. No driver shader compilation occurs in the per-frame draw path.
Browser consumers can initialize with shouldPresentToSurface = true to composite
the encoded display texture into the acquired surface in the same submission.
Surface and viewport extents must match. This uses the renderer's ordinary
composition shader and applies no second tone map or encoding. The default
texture-only mode retains the native Qt interop contract below.
Acquire a completed ViewportImage with a ViewportFrame lease. Two leased
images apply backpressure and neither may be overwritten. The image is top-left,
physical-pixel RGBA8Unorm carrying sRGB-encoded tone-mapped RGB, matching the optional PT image convention.
The consumer must not apply a second tone map or encoding. A Qt adapter must negotiate the exact logical
GPU device and retain an RHI interop lease through foreign sampling completion,
then return the renderer frame lease. There is no CPU image presentation fallback.
For resize prepare a candidate viewport, swap when Ready, and retire the old
viewport after its consumers finish. Reset the viewport before its borrowed
Renderer/device; RHI interop leases independently retain external backing.
Raster mode uses authored materials, direct lights, environment and admitted bakes/shadows. The default clustered deferred graph evaluates direct mesh area lighting, shadowed by cached cube shadow maps, from emissive triangles, merged into convex polygons where coplanar, using polygon integrals and fitted LTC specular, for the records a conservative world-space culling grid lists at each pixel. Other raster profiles retain their existing emission behavior. Emissive indirect transport is still a separate lighting capability; see the default raster contract. No editor lights or fake environment are added to hide that limitation. Choose optional GPU path tracing explicitly when emissive/indirect transport is wanted; do not make it the prerequisite for ordinary scene navigation.
Native acceptance changes a snapshot's material color and its camera while retaining a prior GPU image, verifies visible content changes and culling, checks two-image backpressure and stale identities, and uses no tracing setup. This establishes the renderer output contract. Selected-project loading, interactive UI controls and Qt GPU presentation require the editor integration and RHI native-sharing bridge; renderer fixture acceptance alone does not claim those end-to-end checks are complete.
GPU-resident progressive preview¶
Renderer::EnableGpuPreview explicitly enables optional GPU preview before mesh
and material creation. It requires negotiated compute, storage buffers and
StorageVertex support. Choose the installed renderer_gpu_preview recipe for
native ray queries, or renderer_gpu_preview_software for explicit triangle
traversal in the compute shader. The software recipe performs linear work per ray.
Use renderer_gpu_preview_image for presentation and pass the independently
reflected descriptors. Native Metal cooking requires the pinned SPIRV-Cross
translator. WebGPU and WebGL2 reject this path with Unsupported.
Native-query artifacts additionally require native BLAS/TLAS traversal. Their first acceptance covers native Metal; other backends require their own pixel qualification. The installed reference-scene sample checks exported authored-emission pixels with the software recipe on Vulkan; dense-scene accuracy and performance require separate qualification. Triangle graph kernels may also explicitly select the software compute ABI described in render-graph authoring; that path creates no native structures and does not use opacity ranges. No automatic traversal or CPU transport/readback fallback is selected.
CPU setup freezes the existing snapshot, retained normals/UVs/materials and one
retained RGBA8 texture mip, builds the static binned-SAH BVH, and packs GPU buffers.
Shared texture content is packed once, in a separate read-only buffer so the
geometry/BVH and texel payload each respect the R4 storage-range limit. BeginGpuPreview then owns that immutable
scene independently of caller snapshot/mesh/material owners. Native world-triangle
BLAS construction groups triangles by authored opaque/masked alpha mode, with an
opaque prefix and masked suffix represented by native geometry ranges inside one
BLAS and one identity TLAS instance. Opaque-only and masked-only scenes retain one
native geometry range; empty Worlds use an empty TLAS. RayGeometryDescription's
OpaqueTriangleCount declares that prefix; zero preserves ordinary alpha-filterable
geometry. Native query results still expose the full geometry-local triangle index,
independent of the driver's range-local primitive IDs. The matching SDK query shader
reads the prefix count from private metadata preceding the existing triangle-UV table.
The renderer's packed indirection preserves original shading, material, emitter and visibility identities independently of the CPU oracle's unchanged BVH order. Software GPU reference packing also retains that original BVH order. The native indirection's first word stores the masked range's primitive base. Each indirection word's Z component keeps the unpartitioned BVH leaf order, so compute traversal can share the packed scene with native opacity ranges. This is private preview kernel ABI version 8; recook preview and graph shaders with the matching runtime/SDK. Native geometry opacity bypasses opaque candidates for first-accepted-hit visibility queries. Mixed-scene closest-hit queries retain forced nonopaque traversal and commit opaque-range candidates directly without material/alpha lookups; masked candidates evaluate the same fixed-mip texture, tint, vertex alpha and inclusive cutoff in both queries. Automatic opaque closest-hit acceptance failed the stronger CPU-oracle image tolerance on native Metal, so it is not enabled for mixed scenes. One native BLAS build admits or rejects the combined ranges under the existing budget; its input, structure and scratch charges remain under the existing lifetime ledger. Bindings publish after native builds reach Ready and retain exact versions through GPU completion. No per-pixel CPU transport or accumulation arrays enter GPU setup. The retained-input setup machinery is shared with RT1; selecting its CPU-oracle input retention internally does not execute its CPU tracing loop.
A compute dispatch performs closest/visibility native acceleration-structure queries, shared material opacity filtering, RGB GGX/Lambert sampling, analytic-light/emissive-triangle/constant-environment sampling, MIS, roulette and compensated accumulation. It uses RT1's addressed Philox dimensions and world-space origin error allowance. A second ordered raster pass draws one fullscreen quad; its fragment shader reads compute-written display colors by pixel coordinate into an RHI attachment texture. Current preview admission retains the 2^24-pixel bound. The graph declares storage history, compute-to-fragment visibility and the sampled texture export. No ray, radiance, counter or pixel readback occurs in these renderer APIs. GPU numerical failure makes an error pixel magenta; CPU progress reports completed dispatches, not observed valid sample counts or convergence.
The native opacity regression ([opacity] in ludus_renderer_render_tests)
compares empty, opaque-only, masked-only and mixed native-preview scenes against
raw linear CPU-oracle and software GPU-reference RGB and displayed pixels at
64×64 and 16 samples, with matched seed and camera.
Linear channel tolerance is 0.001 radiance units plus 0.5% of the reference channel;
display tolerance is two RGBA8 levels. A private test-build accessor reads completed
accumulation while retaining the frame; builds with tests disabled omit the accessor. Its mixed fixture contains both rejecting and accepting masked surfaces,
distinct emissive materials and directional shadows. [opacity-budget] exercises
combined-range budget rejection and resource cleanup. The
[scene-shader][opacity] unit test verifies stable original identities and that GPU
partitioning leaves the CPU BVH order unchanged.
To benchmark the production preview path, run the paired harness:
python3 scripts/python/benchmark_ray_opacity.py \
--executable out/build/<native-build>/modules/graphics/renderer/ludus_renderer_render_tests \
--output out/opacity/paired --runs 3
For each scene it alternates the original single nonopaque BLAS (CPU-BVH primitive
order and original uniform traversal flags) and partitioned layouts within the
same test executable and compiled shader.
The original layout control exists only in test builds; production always partitions.
LUDUS_OPACITY_BASELINE=1 selects that control for a standalone [opacity] run.
LUDUS_OPACITY_SCENE optionally selects empty, opaque, masked or mixed.
The runner writes a JSON comparison of per-run/pooled timing medians and displayed
and raw linear differences. It reports differences without treating timing or
image similarity as independent CPU-oracle acceptance.
The benchmark uses 512×512 pixels, depth two, seed one and 48 accumulated samples
per scene; it discards the first eight timing samples. Each scene writes its final
.rgba image, .linear.bin raw readback (two binary32 float4 records per pixel:
mean RGB/sample count, then compensation/history tag), .gpu.bin device-clock frame intervals (binary64 nanoseconds),
.wall.bin submit-to-completion intervals (uint64 nanoseconds), and .setup.bin
CPU preparation interval (one uint64 nanosecond value). GPU intervals include the
preview compute and output raster pass; they are not isolated traversal timings.
Setup measures BeginGpuPreview through native readiness and is reported separately
from warm frames; device/renderer/material-program enable-time prewarming precedes it. Retain the unchanged executable for an additional original-build
comparison with --baseline-executable <saved-unmodified-tests>; that mode alternates
whole-scene batches for legacy harness compatibility. The in-process layout control
isolates partitioning from compiler changes. Each run records executable hashes.
Benchmark mode omits the
small CPU-oracle render; run the ordinary regression separately. GPU timings must
be available; the harness never substitutes CPU time for missing GPU measurements.
On 2026-10-26, an 8-core Apple M5 GPU on macOS 26.6, Clang 23.1.3 and the
Development profile produced the following original-executable comparison. The
unchanged executable was retained from base 8e35d219, and the opacity implementation
was benchmarked at 9b94fc10 before integration with newer develop changes. Each row pools five
alternating runs of 40 warm frames. The editor viewport was closed. GPU intervals
include compute and the output raster pass. The mixed fixture has 6,144 triangles:
one opaque layer, one rejecting masked layer and one accepting masked patch,
with emissive materials, a directional light and two-bounce transport.
| Scene | Original executable GPU median (ms) | Opacity ranges GPU median (ms) | Original/ranges |
|---|---|---|---|
| Empty | 0.956 | 0.986 | 0.970× |
| Opaque only | 3.529 | 4.120 | 0.857× |
| Masked only | 5.440 | 5.426 | 1.003× |
| Mixed | 6.152 | 5.461 | 1.127× |
These measurements do not establish a reliable speedup. A separate five-pair same-kernel layout comparison gave mixed medians of 2.264 versus 4.919 ms, the opposite direction. Its unchanged opaque control varied from 1.006 to 4.234 ms between runs. Background GPU activity and frequency variation confound these process-level measurements; more isolated device measurements are required before claiming acceleration or a regression. Individual timings and setup/wall medians remain in the runner's JSON report rather than being discarded.
After integration at 6a149003, three further alternating layout pairs measured
mixed medians of 6.652 versus 6.305 ms (5.2% less GPU time). The unchanged masked
control measured 5.889 versus 5.058 ms, while opaque measured 3.441 versus 3.860 ms.
Those larger control changes leave the acceleration claim unresolved. The final
repetition retained bit-identical RGBA8 and the same raw RGB error bounds below.
All four scenes produced bit-identical RGBA8 output in both final comparisons. The same-kernel comparison also retained raw linear readbacks: empty, opaque-only and masked-only RGB were bit-identical; mixed maximum absolute RGB difference was 1.78814e-7, RMSE 6.72124e-10 and mean signed difference -9.09495e-13. Independent CPU-oracle regressions passed the stated per-channel tolerance at 64×64 and 16 samples. An earlier automatic-opaque closest-hit variant failed that stronger regression and was rejected; the final visibility optimization preserves the existing closest-hit traversal mode. Thin/near-edge native FP32 precision and arbitrary-scene convergence remain independent qualification limits below. The harness retains raw readbacks and individual timing samples under its chosen output directory; generated/machine-local evidence is not source-controlled.
Visibility queries use the same native traversal and candidate alpha acceptance as
closest-hit queries, but consume only committed hit/miss status. They skip committed
primitive/range remapping, distance and barycentric reads. The closest-hit result
and preview ABI remain unchanged. Ray-path capture variants retain full shadow-hit
extraction so diagnostic records still contain committed identity and distance;
normal rendering keeps the visibility-only fast path. Explicit software graph traversal retains its
existing hit/miss/failure contract. The diagnostic recipe
renderer_gpu_preview_visibility_full shares the production integrator but restores
full committed-hit extraction for shadows; it exists for reproducible A/B evidence.
Generated Metal has one set of committed primitive/geometry/distance/barycentric
reads in the production kernel (closest-hit), versus two sets in that diagnostic.
Committed native barycentrics are clamped to the closed triangle, as the GPU-compute
reference's traversal does: both to [0,1], and the third corner's weight to at most
one minus the second's. The hardware rounds the two coordinates separately, so on
Metal a hit near the edge opposite a triangle's first corner occasionally summed just
above one. A graph pass that records the hit and rebuilds it later with
GetSurfaceHit then rejected it as SceneData. The Veach and Guibas template
latched one such pixel on a wall's top edge in a few revisions; the
[native-barycentrics] ray test renders two of them. The clamp moves a coordinate
by at most its rounding error.
Run the frame-interleaved visibility benchmark with:
python3 scripts/python/benchmark_ray_visibility.py \
--executable out/build/<native-build>/modules/graphics/renderer/ludus_renderer_render_tests \
--output out/visibility/interleaved --runs 5
Both immutable pipelines and scenes are prepared on one device/queue/surface before timing. Every sample pairs full-hit and visibility-only frames, alternates AB/BA, and reverses the initial order between independent processes. No image readback or file I/O occurs between timed pairs. Each scene uses 512×512 pixels, depth two, seed one and 128 samples, discarding the first 16 pairs. A tessellated receiver and opaque or accepting masked slats create blocked and unblocked shadow rays; masked and mixed scenes add a fully rejecting masked layer. Materials are nonemissive, with one directional light and constant environment. Opaque has 3,072 triangles; masked and mixed each have 5,120. Empty is the no-geometry control.
The runner freezes and hashes its executable, preserves individual GPU/wall timings,
records AB/BA order and the CPU gap from the first completion observation to the
second submission, and checks complete per-pixel sample coverage. Its JSON reports
paired ratios, separate AB/BA ratios and run medians. The 95% percentile bootstrap
resamples independent process run medians, not correlated frames. GPU intervals
include compute and output raster and do not isolate traversal. Final RGBA8 and raw
linear readbacks happen after timing. The private little-endian artifact protocol
uses each scene's .configuration.bin (version, extent, target samples, warmup
samples as four uint32 values), .order.bin (uint8 baseline-first flags) and
.gap.bin (uint64 nanoseconds), with each variant's .gpu.bin, .wall.bin,
.rgba and .linear.bin using the formats described above. Existing output
directories are rejected to preserve evidence.
On 2026-10-26, two five-process repetitions on the same 8-core Apple M5 GPU,
macOS 26.6, Clang 23.1.3 Development build based on 0fbb4f29 gave the following
results. Each process contributes 112 balanced AB/BA pairs per scene (1,120 total
pairs per row). Times are medians of process GPU medians; reductions and intervals
use process median paired ratios, so they need not equal the ratio of those times.
| Scene | Full hit (ms) | Visibility only (ms) | Paired GPU time reduction | 95% run-bootstrap full/visibility ratio |
|---|---|---|---|---|
| Empty | 0.2492 | 0.2515 | -1.13% | 0.9871–0.9923× |
| Opaque | 1.3071 | 1.2916 | 1.19% | 1.0108–1.0129× |
| Masked | 2.3816 | 2.3204 | 2.54% | 1.0256–1.0265× |
| Mixed | 2.5444 | 2.4810 | 2.47% | 1.0249–1.0256× |
Both submission orders retained gains on geometry scenes: median AB/BA ratios were 1.0110/1.0130× opaque, 1.0241/1.0285× masked and 1.0231/1.0278× mixed. The empty control was slower overall, with order sensitivity (0.9823/1.0003×). The largest recorded CPU pair gap was 1 microsecond. This is a modest measured improvement on these Metal fixtures, with an approximately 1% empty-scene cost; it is not qualification of other scenes, GPUs or native backends.
Across all ten processes, every scene retained bit-identical normalized raw RGB
and RGBA8 output. The ordinary [visibility] regression separately compares both
kernels at 64×64 and 16 samples, verifies complete sample coverage, then compares
the fast path with independent CPU-oracle and software GPU-reference raw RGB at
the existing 0.001 absolute plus 0.5% relative channel tolerance. It also requires
zero invalid reference contributions and visibility-query coverage on geometry
scenes. Run this ordinary mode independently of benchmark mode, which omits oracle
renders during timing. Neither denoising nor temporal reuse establishes parity.
The opt-in renderer_gpu_preview_hit_cached recipe experiments with per-hit material
word-address and interpolated-UV reuse. The default preview, reference integral,
packed-scene ABI and candidate acceptance remain unchanged. HitMaterial is local
to one intersection; it introduces no persistent history, host allocation or GPU
buffer. Map transforms, fixed-mip filtering, decoding and normal mapping retain
the existing texture helper, including its out-of-line SPIR-V filter. The material
word constants are shared by the host packer and shader readers.
Generated-code inspection at base 44d7510a found three separate primary-hit UV
interpolations for emission, base color and surface maps. The default artifact has
8,745 SPIR-V instructions, 907 static OpLoad and 354 OpAccessChain instructions;
the opt-in artifact has 8,571, 874 and 321 respectively. The default retains the
original counts. These are module-wide static instruction counts, including local
loads and branches, not measured device-memory traffic. Translated Metal source
is 160,150 versus 158,462 bytes. SPIRV-Cross still marks the texture helper
always_inline; downstream driver optimization can eliminate source redundancy.
Fewer intermediate instructions do not establish faster GPU execution.
Use the same interleaved harness with the cache experiment selected:
python3 scripts/python/benchmark_ray_visibility.py --experiment hit-cache \
--executable out/build/<native-build>/modules/graphics/renderer/ludus_renderer_render_tests \
--output out/hit-cache/interleaved --runs 5
The experiment prepares default/uncached and opt-in/cached pipelines together and
uses the same matched-sample AB/BA protocol, warmup, timing scope, executable hash,
complete sample-coverage checks and independent-process bootstrap described above.
Artifacts use uncached/ and cached/ variant directories. Four untextured controls
are joined by textured-nearest and textured-linear scenes with retained sRGB base
and emission maps, a linear normal/roughness/metallic map, explicit mesh UVs, negative
V scale, repeated U and clamped V. Each textured scene has 5,120 triangles. Its
emissive surfaces exercise mesh-light sampling too. The ordinary [hit-cache]
regression compares final RGBA8 A/B output, raw RGB, and the independent CPU oracle
and software GPU reference at 64×64/16 samples, with the same documented channel
tolerances. Benchmark mode omits oracle rendering during timing.
On 2026-10-27, five independent processes on an 8-core Apple M5 GPU, macOS 26.6 and Clang 23.1.3 Development measured 112 balanced AB/BA pairs after 16 warmup pairs per process: 560 measured pairs per scene. Positive savings mean the cached recipe used less GPU time. Each row uses the median of process median matched-frame ratios; the bootstrap resamples those five process medians.
| Scene | Paired GPU time saved | 95% run-bootstrap uncached/cached ratio |
|---|---|---|
| empty | 0.76% | 1.0009–1.0106× |
| opaque | -2.33% | 0.9760–0.9803× |
| masked | -0.39% | 0.9918–1.0015× |
| mixed | -0.74% | 0.9846–1.0003× |
| textured-nearest | -0.04% | 0.9948–1.0003× |
| textured-linear | 0.31% | 0.9985–1.0067× |
All six scenes retained bit-identical normalized raw RGB and RGBA8 across all five processes. The ordinary regression also passed independent CPU/software-GPU parity. The maximum recorded CPU gap within a pair was 2.754 ms (empty control); the largest geometry-scene gap was 0.186 ms. Individual gaps and both order-specific ratios remain in the JSON report; no additional timings were discarded. The textured-linear median gain is small and its interval includes no gain; opaque rendering has a repeatable regression. This does not establish a general speedup.
Two earlier variants were also measured and rejected. Resolving material addresses through the texture filter reduced static loads from 907 to 805 but regressed the opaque fixture by 2.40% over five runs. Adding lazy UV evaluation and per-filter metadata caching regressed opaque by 4.21% and textured-linear by 1.77% over three runs. The final implementation keeps the texture-filter boundary unchanged. A separate three-run repetition of that narrow implementation also measured an opaque regression (2.44%) and only 0.23% textured-linear savings. These results motivate the opt-in status rather than a default performance claim.
The recipe remains an opt-in experiment. Native backend qualification and a representative measured improvement are required before default activation.
The pass-timings benchmark experiment uses the existing RHI per-pass probes to
measure the named GPU preview transport compute operation and GPU preview image
raster operation separately. Both variants use the same production shader with
material caching disabled. unprobed keeps the frame probe alone; profiled also
arms ArmGpuPassTimings before submission and polls GetGpuPassTimings after
completion. Their matched AB/BA frame intervals expose the observed instrumentation
overhead, independently of the per-pass durations. Run:
python3 scripts/python/benchmark_ray_visibility.py --experiment pass-timings \
--executable out/build/<native-build>/modules/graphics/renderer/ludus_renderer_render_tests \
--output out/ray-pass/interleaved --runs 5
It reuses all six opaque/masked/textured fixtures, sample addresses, warmup and
independent-process repetitions above. The ordinary [ray-pass-timings] regression
checks the same shader with probes off/on, sample coverage and independent CPU/
software-GPU reference parity. Benchmark mode validates exactly one compute and
one raster interval per profiled frame, their distinct nonzero scope identities,
stable names/precision, zero dropped records, and explicit Available states with
finite nonnegative durations. A quantized zero is retained and counted. Unsupported
per-pass sampling skips the ordinary hardware test, but fails a requested benchmark;
unavailable, disjoint, missing or unexpected data never becomes a CPU-time fallback.
These intervals include GPU stalls. Metal raster timing is the elapsed envelope of
vertex/fragment stages, which can overlap other operation envelopes. The report
therefore retains each raw duration and, when the RHI supplies validated clock
alignment, their overlap and the output's end-time tail after compute. The latter
is max(0, outputEnd - transportEnd) on the calibrated clock, not raster shader
busy time. Overlap/tail statistics use only aligned samples and always report their
count and the largest clock-mapping uncertainty. Missing alignment leaves these
statistics absent while raw GPU durations remain available. Pass durations are
not an additive partition of frame time. The profiling contract
owns backend stage, clock-calibration and uncertainty details.
In each profiled/ directory, .transport.gpu.bin and .output.gpu.bin contain one
little-endian binary64 nanosecond duration per measured sample. .pass-info.bin
contains seven explicit 64-bit words: version 1 (uint64), transport/output tick
nanoseconds (binary64), their valid-bit counts (uint64), and their scope identities
(uint64). Zero precision fields mean unknown; identities remain process/device
local. .pass-names.bin contains transport then output UTF-8 names, each prefixed
by its uint32 little-endian byte length, without a terminator. .pass-intervals.bin
has six uint64 little-endian words per sample: transport start/end, output start/end,
clock uncertainty in nanoseconds, and a 0/1 alignment-availability word. Unavailable
intervals contain zero timestamp/bound words and are not interpreted as zero overlap.
The JSON preserves per-run metadata, names, precision, zero-duration/alignment counts,
pass medians and process-bootstrap intervals, plus the original frame and image
checks. No image readback or file I/O occurs between paired submissions; counter
resolution happens after completion and its CPU cost is visible in the pair gap.
Measurement on 2026-10-27 used Apple M5 (8 GPU cores), macOS 26.6,
Clang 23.1.3 RelWithDebInfo (-O2), the then-default 64×1 visibility-only / uncached
shader, 512×512 pixels and 128 samples (16 warmup). Five independent processes
contributed 112 matched AB/BA pairs per scene each. The source base was
02df0b37e3e2283f88a86ea1a012e2794a8905d6; the frozen executable SHA-256 was
7da4672c54616f898f58905a7f9145e64914aa5594b579c00eb3c0875cc30e91. Values below are medians of
process medians; bracketed ranges are 95% process-bootstrap intervals for raw
pass durations. Durations and overlap/tail are in milliseconds.
| Scene | Transport [95% interval] | Output envelope [95% interval] | Overlap | Output tail | Aligned samples | Max clock uncertainty (µs) |
|---|---|---|---|---|---|---|
| empty | 0.5345 [0.3561, 0.6879] | 0.5906 [0.3935, 0.7491] | 0.5075 | 0.0499 | 558/560 | 0.667 |
| opaque | 1.8432 [1.3666, 2.6971] | 1.8757 [1.3445, 2.7698] | 1.8361 | 0.0629 | 560/560 | 5.125 |
| masked | 3.4468 [2.6952, 6.2121] | 3.4984 [2.7276, 6.2898] | 3.4346 | 0.0664 | 560/560 | 9.000 |
| mixed | 3.6646 [3.2291, 6.2483] | 3.6408 [3.2203, 6.3440] | 3.5853 | 0.0670 | 559/560 | 17.730 |
| textured-nearest | 4.2184 [3.6288, 5.0066] | 4.2706 [3.6437, 5.2127] | 4.2063 | 0.0612 | 559/560 | 8.938 |
| textured-linear | 4.8813 [4.5703, 8.0433] | 4.8721 [4.5881, 8.0585] | 4.7998 | 0.0742 | 556/560 | 13.667 |
All 3360 profiled frames retained valid GPU pass durations; eight lacked validated clock alignment and were excluded only from overlap/tail statistics. Tick precision was 1 ns with 64 valid bits and no zero-duration samples. The large raster envelope mostly overlaps transport; neither it nor the output tail identifies raster shader busy time. Absolute times vary substantially between processes, so these are observed workload intervals rather than uncontended hardware costs.
Matched frame overhead point estimates ranged from −0.71% to +0.60%; every 95% process-bootstrap unprobed/profiled ratio interval included 1. This does not establish zero probe overhead. Median inter-frame pair gaps were 0.012–0.020 ms across scene/process combinations; the JSON retains AB/BA splits and gaps for order-bias inspection. Every displayed RGBA8 channel and raw linear RGB value was identical between unprobed and profiled variants, with complete sample coverage. The ordinary regression separately checks the independent CPU oracle and software GPU-reference outputs at 64×64 / 16 samples using the tolerances above.
The standalone native-preview transport uses an 8×8 workgroup. Dispatch and image
admission use the supplied compute artifact's reflected X/Y dimensions, including
partial edge groups; Z must be one. Zero dimensions reject as InvalidDescription,
and workgroup volume/dimensions or dispatch counts exceeding device limits reject
as Unsupported. Authored render-graph callback entry points default to 64×1 and can opt into
8×8 through the scene compute state; see the authored-graph experiment below. This experiment qualifies the standalone preview workload, with
performance evidence from the M5 backend below.
Reproduce workgroup comparisons against the former 64×1 shape with:
python3 scripts/python/benchmark_ray_visibility.py --experiment workgroups \
--workgroup 8x8 \
--executable out/build/<native-build>/modules/graphics/renderer/ludus_renderer_render_tests \
--output out/workgroups/confirmation --runs 5
--workgroup also accepts 32x1, 128x1 and 64x1; the last is an A/A control.
Both baseline/ and candidate/ use frame and pass probes. The same 512×512,
128-sample, 16-warmup fixtures alternate AB/BA at matched pixel/sample addresses;
processes reverse their initial order. The report retains both sets of raw pass
records and independently computes paired transport and full-frame ratios, order
splits and 95% process-bootstrap intervals. Quantized-zero transport durations
remain valid raw measurements; pairs containing one cannot form a ratio and are
counted explicitly. A run with no positive pairs has no transport ratio. Image
readback and file writing follow the timed sequence.
Generated-code inspection on the pinned Slang/SPIRV-Cross toolchain found identical
175,969-byte MSL for 32×1, 64×1, 128×1 and 8×8, including the proposed production
recipe (SHA-256 da66fc86c67d5eda31ecd44ac3a409fb92fdd47cb00a897bf7b8a0623b754893).
All SPIR-V artifacts had 9,377 instructions and 144,872 bytes; only the local-size
execution mode differed. Reflection matched each intended shape. There was no
threadgroup memory or barrier in the emitted MSL. The native pipeline receives the
reflected group size at dispatch; this change does not add a compilation occupancy
hint or change transport arithmetic. Native register/spill statistics were
unavailable because the local offline Metal compiler was not installed. Thanks to
Apple, Scale compute workloads across Apple GPUs, WWDC22,
for the workload-specific threadgroup-size guidance used to select this experiment.
Measurement on 2026-10-27 used Apple M5 (8 GPU cores), macOS 26.6 and Clang
23.1.3 RelWithDebInfo (-O2). A three-process-block screen rotated 32×1, 128×1,
8×8 and a 64×1/64×1 control; 32×1 and 128×1 showed small/inconsistent changes,
while 8×8 improved transport and full-frame intervals across every nonempty fixture.
An independent five-block confirmation alternated 8×8 and the A/A control, reversing
initial AB/BA order per block: 560 matched measured pairs per scene and experiment.
The final source base was 94fa8ef9b26a5d3f081721baebebaa3402213b7a; the frozen
executable SHA-256 was 75368d6d70c41d82097e1ecdf5b74c987edd703c329f4cfcc5e22124864f5f7a.
No other builds or GPU tests from this experiment ran during timed submissions.
Raw duration columns are medians of process medians; percentage reductions are
medians of matched-frame ratios, with 95% process-bootstrap intervals in brackets.
Positive percentages mean less elapsed GPU time.
| Scene | 64×1 transport (ms) | 8×8 transport (ms) | Transport reduction [95% interval] | Full-frame reduction [95% interval] | A/A transport reduction |
|---|---|---|---|---|---|
| empty | 0.5271 | 0.5204 | 1.51% [1.22%, 1.53%] | 1.30% [1.08%, 1.37%] | 0.14% |
| opaque | 1.6577 | 1.5675 | 5.23% [4.92%, 5.50%] | 5.13% [4.69%, 5.41%] | -0.12% |
| masked | 2.9145 | 2.7124 | 7.06% [6.82%, 7.20%] | 6.78% [6.63%, 7.49%] | -0.28% |
| mixed | 2.9202 | 2.6693 | 8.99% [8.49%, 9.32%] | 8.77% [7.72%, 9.07%] | -0.26% |
| textured-nearest | 3.5355 | 3.0704 | 12.07% [12.03%, 13.32%] | 12.15% [11.81%, 13.37%] | 0.31% |
| textured-linear | 4.0648 | 3.5850 | 10.92% [10.59%, 11.08%] | 10.80% [10.64%, 10.94%] | -0.56% |
Every nonempty scene improved in all five independent runs and both frame-order splits; transport and full-frame confidence intervals exclude no change. All A/A control transport intervals include no change, with point estimates from −0.56% to +0.31% for nonempty scenes. The confirmation retained 3360 pairs for each experiment and complete sample coverage. Every raw accumulation file and displayed RGBA8 image was byte-identical between its paired variants. The earlier screen showed occasional one-ULP raw RGB drift (maximum 2.98×10⁻⁸), including in the A/A control; no displayed channel changed. This observation is recorded rather than increasing any reference comparison tolerance. GPU elapsed intervals include stalls and overlapping raster envelopes as described above; these gains do not establish native shader busy time or reduced register pressure. Performance on other adapters and authored graph estimators remains unqualified.
At 64×64 / 16 samples, every tested shape passed the existing independent raw
CPU-oracle and software GPU-reference tolerances for all six fixtures. A separate
67×67 / 16-sample diagnostic exercises partial edge groups: native shape comparisons
and the software GPU reference agree, while the CPU oracle differs in nine channel
comparisons at pixel 1888 (x=12, y=28) across the nonempty fixtures, with maximum
absolute RGB difference about 0.0341. The same failure signature occurs in the
64×1/64×1 control and all candidate shapes; it is retained as a baseline reference
precision limitation, and tolerances are not increased. Set
LUDUS_WORKGROUP_ODD_EXTENT=1 when running [workgroups] to reproduce that diagnostic.
The explicit quality profile admits at most four live jobs, 4096 requested
samples, depth eight, the reference secondary-distance/camera bounds and a fixed
texture mip. It has no denoising, motion history, transmission, spectral transport,
coating model. Native acceleration structures are mandatory. Up to 128 authored directional,
point and spot lights are admitted without dropping entries. Baked/shadow atlas
inputs reject with Unsupported; lighting comes from the admitted
emissive surfaces and caller-supplied constant environment. This is a finite-depth
preview of the prepared RGB profile, not general PBRT fidelity. FP32 BVH triangle
intersection remains subject to thin/near-edge precision qualification. The
existing native-query Staircase2 parity gap remains independent of this path.
The GPU budget also charges native structure inputs, BLAS/TLAS storage and build scratch conservatively until the preview retires. Native ray capability rejection preserves ordinary raster rendering. Mixed-hardware projects select an authored raster graph on unsupported devices; see renderer selection.
Enable-time CPU budget covers retained renderer geometry, preview state, BVH and packing/zero-initialization scratch. RHI-retained source texels/registries and driver staging are separate. GPU budget covers shared quad/index bytes, immutable scene/uniform buffers, 32 accumulation bytes and 16 display-color bytes per pixel, and two RGBA8/private-depth output allocations (16 bytes per pixel together). It excludes RHI registries, staging, shaders/pipeline objects and driver padding. Negotiated storage-range, dispatch and attachment limits can reject a scene before resource creation. Jobs share the budget; cancelled jobs with acquired frames or pending producers still count. PollGpuPreview/Begin reclaim cancelled completed jobs; cancellation cannot bypass the in-flight GPU byte budget. Setup allocates and uploads; frame execution rebuilds a small GDI plan and uploads no scene or pixel data.
Use PollGpuPreview for asynchronous enablement. After BeginGpuPreview, poll
GetGpuPreviewInfo, dispatch with AdvanceGpuPreview, and acquire an image with
AcquireGpuPreviewFrame when producer completion is known. Pending progress
may still expose the preceding complete image. The output is top-left physical
pixels, opaque RGBA8Unorm, exposure multiplied into linear radiance, Reinhard
compression then sRGB encoding. The consumer must not apply that conversion a
second time. Exposure/camera/content changes create a new job and restart
accumulation; resize uses the same replacement workflow.
Each job has two images. GpuPreviewFrame is an opaque ownership lease on one
complete texture incarnation. The renderer never overwrites an acquired image;
if both images are leased, dispatch returns NotReady. Acquire cannot issue a
second lease for the same image. Cancel the job with Release(GpuPreview&);
acquired frames keep its GPU resources alive until they are returned. Stale
copies reject, and generations do not wrap. Reset/destruction invalidates all
renderer identities; RHI's accepted GPU uses retire through producer completion.
For Qt sharing, import this exact RHI texture on the same native logical device/backend. Matching a physical adapter alone does not permit direct texture sharing. Order Qt sampling after the returned producer token, and independently retain its native backing with the RHI interoperability service. Return the renderer frame only after every external GPU consumer has completed and released its RHI lease. Renderer frame ownership is not a cross-device import, foreign-queue semaphore, Qt completion token or permission to destroy/mutate the borrowed texture. The RHI/Qt bridge remains a separate prerequisite; this renderer change alone cannot claim an editor viewport is connected or teardown is cross-queue safe.
The native acceptance compares the small emissive/indirect fixture against the
CPU oracle at matched seed/depth/128 samples, checks opaque output and error
pixels, preserves a leased image across later accumulation, exercises two-image
backpressure and cancelled/stale ownership. The installed-SDK sample's gpu mode
exports a texture through public RHI readback only for offline acceptance;
interactive rendering/presentation must retain the GPU image instead. On native
Metal the full prepared Staircase2 scene (28,831 instanced triangles), 64×64,
32 samples, depth four, seed 871 produced 4,072 nonblack pixels and no magenta
error pixels. Its display comparison to the matched CPU capture had maximum
channel error 14/255, RMS of per-pixel maximum channel error 0.2923/255, with
3/4096 pixels above two display levels. This qualifies GPU execution and lighting
of the full scene; it does not qualify dense-scene pixel parity. The scene
fingerprint matches the CPU capture. The RGB output SHA-256 (including alpha) was
e702b19b7f3f7235cbc6860e66c98c2279c1d81d743016d592679e2e974935dc.
See the scene workflow.
Render-graph ray scene capacity¶
Each render-graph ray pass is one preview job. Passes of one graph update that
read the same scene data share the first pass's uploaded scene
(GpuPreviewGraphInvocation::SceneSourceOrNull): its packed triangles, BVH,
surfaces, lights, environment, texels and native geometry are stored once and
charged to the preview and RHI budgets once. The last job released frees the
copy. Each pass still keeps its own parameters, accumulation, images and TLAS.
Before this sharing, a template with three ray passes uploaded three copies, so
every byte ceiling admitted a third of the scene it could hold.
Measured on Intel Sponza's glTF triangles with Metal on October 29, 2026:
| Cost per triangle | Bytes | What it holds |
|---|---|---|
| Packed GPU scene | about 322 | 16 float4 words per triangle, 3 per BVH node (about 1.03 nodes per triangle) and one order word |
| CPU scene estimate | 364 | ReferenceTriangle, two BVH nodes and three index words, reserved before the build |
| CPU fingerprint stream | 228 | Serialized triangle identity, checked against the scratch remaining after the scene |
The first 999,981 triangles pack to 321,543,168 bytes; 1,999,967 pack to 644,522,080 bytes. Independent byte ceilings bound scene preparation:
| Ceiling | Owner | Scene limit |
|---|---|---|
CACHE_IDENTITY_BYTES, 1 GiB on 64-bit hosts |
Raster renderer (render_graph_scene_resources.cpp) |
Current/pending/idle mesh and texture identity copies; 32-bit hosts retain 128 MiB |
RENDER_GRAPH_RAY_CPU_BYTES, 4 GiB on 64-bit hosts |
Renderer (render_graph.hpp) |
Scene estimate plus fingerprint, packing and native-input scratch |
RENDER_GRAPH_RAY_GPU_BYTES, 6 GiB on 64-bit hosts |
Renderer (render_graph.hpp) |
Current and replacement scenes, images, native structures and pending GPU holds |
rhi::StorageLimits::ResourceBytes |
The device creator | All logical RHI buffers and textures on the device. The 512 MiB default holds about 1.6 million triangles; the Editor viewport adds a separate 1 GiB raster/target allowance to the graph-ray GPU budget (7 GiB on 64-bit hosts) |
The former 2 GiB CPU ceiling rejected the full 3,747,018-triangle Sponza on
its first pass: CpuScratch needed 854,313,156 bytes with 783,538,976 left after
the 1,363,925,720-byte scene estimate. The 64-bit policy now admits that setup
and reserves GPU headroom for two complete scenes during replacement. On M5
Metal, one complete diagnostic scene charges 2,147,831,068 GPU bytes; two
exceed even 4 GiB before adding larger per-pass images or material texels. These
are ceilings, not eager allocations or measurements of free RAM/VRAM. 32-bit
hosts retain the 2 GiB CPU/GPU ceilings; device storage-buffer ranges and
physical allocation failures still apply independently. The viewport's shared
and offscreen devices use the same policy in MakeViewportDeviceDescription.
Native ray scenes also admit aligned opacity/UV tables up to 256 MiB, subject
to the device buffer range and byte budget. Float32 offsets remain exact because
UV offsets count float4 words and alpha scalar offsets are four-aligned. The
former 64 MiB scalar ceiling rejected full Sponza's 114 MiB UV table. The
viewport allows 512 MiB of RHI host metadata, including overlapping native UV
records and temporary opacity arrays during replacement. The raster identity
cache likewise needs more than its former 128 MiB for the complete mesh/texture
set; its independent 1 GiB native ceiling admits retained asset versions. The
raster emitter-record ceiling is 32,768: Sponza authors 8,098 emissive light-bulb
triangles, exceeding the former 4,096-record bound, and the Sample project's Bistro
needs 17,571 records, exceeding the later 8,192-record bound once the mesh-light
culling grid made that many affordable to shade. Admission retains the
authored emitters and their existing diffuse/LTC evaluation.
Renderer ray scene/scratch/preview byte rejections report the exceeded ceiling,
required bytes and available bytes in RenderGraphReport::SceneDiagnostics.
Native/RHI and raster cache failures can report only their status.
The [scene-capacity] regression in render_graph_ray_tests.cpp prepares a
generated 102,400-triangle scene (importable as a test scene)
through the three-pass ComputeBvh
line-sampling template. It checks that the graph's logical RHI bytes grow by
less than two packed copies. The hidden [.line-traversal-performance]
measurement selects an explicit device budget; full-scene viewport acceptance
uses the Editor's device policy.
The optional editor tests prepare the supplied glTF through the production import/catalog path and require all 3,747,018 triangles. Use a new, isolated output project, leaving supplied resources and existing projects unchanged:
QT_QPA_PLATFORM=offscreen \
LUDUS_FULL_SPONZA_PROJECT="$PWD/out/full-sponza" \
LUDUS_FULL_SPONZA_ENTRY=/absolute/path/to/main_sponza/NewSponza_Main_glTF_003.gltf \
out/build/macos-clang-development/apps/editor/ludus_editor_tests '[.prepare-full-sponza]'
QT_QPA_PLATFORM=offscreen \
LUDUS_SPONZA_TEST_PROJECT="$PWD/out/full-sponza" \
LUDUS_SPONZA_TEST_RUNTIME=/absolute/path/to/authored-ray-graph.lurgrt \
out/build/macos-clang-development/apps/editor/ludus_editor_tests '[.sponza-ray-project]'
LUDUS_SPONZA_TEST_PROJECT="$PWD/out/full-sponza" \
LUDUS_SPONZA_NATIVE_IMAGE="$PWD/out/sponza-native.png" \
out/build/macos-clang-development/apps/editor/ludus_editor_tests '[.full-sponza-native]'
The authored-graph test checks camera navigation, a content replacement above 2 GiB of logical RHI resources, and preservation of the last frame while pending. The native test requires a shared-device Qt presentation host. It presents the ordinary raster profile first, then the declared flat-color diagnostic ray profile, navigates and resizes, checks image variation/error pixels and verifies that graph presentation performs no CPU image readback. The diagnostic ray profile displays authored base-color averages; it adds no lights to compensate for the supplied glTF's zero-intensity punctual sources. This is viewport and capacity acceptance, not lighting fidelity or steady-state frame-rate evidence.
Material and lighting fidelity¶
This implemented RGB surface milestone extends L4/L5 materials and both reference paths. It preserves the baseline untextured-parameter programs and does not infer new scene lights or convert raster transparency into glass transmission.
Prewarm renderer_surface.slang (bounded forward) or
renderer_clustered_surface.slang (all-light clustered forward), using
VertexMain, FragmentMain and ShadowMain. The lit/clustered baseline sources
also use those PascalCase entry names. Source builds supply the FoundationMath
and RHI shader include directories; installed SDK shader sources share one
directory. Both profiles retain the existing sixteen-attribute mesh ABI.
Set LitMaterialDescription::SurfaceMap to a ready sampled linear
Rgba8Unorm texture. The fixed packed layout uses RG for tangent normal XY
(2*channel-1, positive Z reconstructed), B for perceptual roughness multiplier,
and A for metallic multiplier. NormalScale in [0,8] scales reconstructed XY,
then renormalizes; zero disables normal detail while preserving parameter sampling.
Roughness retains the 0.045 floor. For a roughness/metallic-only map, use strength
zero. This is a separate data texture: base color still decodes its own sRGB
encoding and alpha remains opacity. Do not feed an ordinary RGB normal map or
ORM map without packing its channels to this contract.
The map shares the base UV transform, sampler and raster LOD. Ray paths instead
use the capture's fixed TextureMip; that initialized mip must exist and have
retained CPU content in both input textures. CPU retention supplies immutable
setup bytes, not a GPU transport/readback fallback. Mip filtering is the declared
filtered-channel model, without footprint-aware ray filtering or specular AA.
Creation retains an exact texture view and rejects sRGB/data-format mismatch,
attachments, unavailable resources and incompatible shader programs explicitly.
Null preserves constant parameters and the mesh normal. Failed creation preserves
material/report outputs; accepted asynchronous candidates retain the normal
poll/publish/release contract.
GPU normal-frame math belongs to FoundationMath; the renderer owns channel decoding and BRDF/material evaluation. Raster derives the UV frame from fragment position/UV derivatives; rays derive it from transformed triangle edges. Both normalize edge magnitudes, reject relatively singular UV frames, orthogonalize tangent against the shading normal and preserve mirrored UV handedness. Degenerate frames preserve the original normal. This UV-derived basis is not MikkTSpace: tangent-baked assets need a matching conversion/profile before claiming exact source fidelity. Ray offsets and visibility remain geometric-normal operations, and continuations below the viewer's side of the oriented geometric normal are null events. Shading-normal energy correction is still absent; this is the declared RGB reflection profile, not unrestricted physical fidelity.
The forward profile extends the 1040-byte uniform to 1056 bytes and binds the map
at slot 6. The clustered profile extends 8800 to 8816 bytes, binds the map at 7,
and shares the existing nearest shadow sampler for cluster indexing. It stays
within RHI's eight-binding limit while retaining base, bake, shadow and cluster
inputs. Uniform names, offsets, shapes and shadow subsets are independently checked.
Older baseline layouts stay admitted and reject non-null maps with Unsupported.
GPU reference rendering now copies all normalized authored analytic lights into its budgeted scene buffer, uniformly selects among them, finite triangle emitters and a positive constant environment, and uses unit MIS weight for delta lights. Within the emitter share, both GPU kernels pick triangles from the packed power-weighted CDF (emitter word Y), matching the CPU proposal described above. Point/spot shadow segments terminate before the light endpoint. Raster, CPU reference and GPU reference use the same units, inverse-square floor, range window and squared cone. No lighting selection truncates the reference integral. Existing baked/shadow-atlas reference rejection remains explicit.
Pending raster viewport uploads keep the prior drawable view until preparation succeeds; a failed candidate is discarded and reported once. Update reports describe the accepted candidate, including Pending, rather than the last presented image. A regression observes old packets during pending/failure and new packets only after success. The closest-hit shader explicitly restricts primitive-index tie breaking to equal distances, supplementing its existing closest-distance intersection bound.
The private GPU kernel uniform grows from 208 to 224 bytes, adding an integer
SurfaceLayout record for surface stride, analytic-light offset and kernel ABI 2.
Old cooked kernels are rejected by reflected uniform-size admission before scene
allocation; they must be recooked with the installed sources. A regression verifies
this rejection, so an old six-word surface decoder cannot silently consume the new
eight-word records. Unsupported runtime layout produces an error pixel.
Reference ProfileVersion is 2. LUHDR file schema remains 1, and so does the
addressed sampler version of the default UniformArea light sampling
(ResampledProjectedSolidAngle captures record sampler 2); the decoder retains
profile versions 1/2 as distinct metadata. Maps extend the
canonical scene fingerprint with normal strength, image layout and immutable bytes;
unmapped scene fingerprints remain stable. Comparisons require matching profile
versions. Changing a material/map creates a new snapshot/integral and restarts
accumulation; old accepted captures and leased viewport images retain their inputs.
Acceptance covers independent packed-channel decoding, rim/zero-strength and invalid-output preservation; mirrored/tiny/degenerate UV frames; incompatible program and sRGB-map rejection; complete native raster/clustered/CPU/GPU comparisons for directional, point and off-axis spot illumination; and texture-owner destruction before rendering/capture. Numerical pixel expectations are derived independently, not merely compared between two shader variants. Emitted WGSL/GLES layout checks cover all profile entries. These small fixtures establish the declared contract; full Staircase2 native-query parity and source-PBRT fidelity remain unqualified.
The larger compute artifact is stored byte-for-byte in one generated .cpp when
its textual UTF-8 source reaches 60000 bytes, with only an external array declaration
in the generated header. This avoids C++'s giant-literal limit and repeated header
parsing while retaining exact source, line diagnostics and manifest hashes. The
installed CMake helper owns compilation of the generated companion; no runtime
concatenation, file loading or shader compiler is introduced.
Thanks to Google/Filament's authors, Physically Based Rendering in Filament, §4.4, §5.2.2 and §5.7, for the reflection/photometric conventions and tangent-normal surface-detail model. The packed layout, UV-derived frame and bounded immutable admission are Ludus choices.
Typed diffuse and conductor reference materials¶
Reference profile 5 extends ReferenceBsdf, LitMaterialDescription and
SceneShaderMaterial with format-owned MaterialScattering parameters. The
legacy default retains GGX/Schlick plus Fresnel-weighted diffuse. Diffuse
evaluates Lambertian reflectance without an added dielectric specular lobe and
samples the cosine hemisphere. Conductor uses retained RGB complex-IOR Fresnel,
GGX with directly supplied alpha, correlated Smith masking and visible-normal
sampling with its matching PDF. Below-surface proposals remain null events;
there is no retry, energy clamp or hidden change to conductor alpha. Storage
preserves AlphaU and AlphaV; reference admission currently requires them to
match, and requires positive alpha rather than implementing delta transport.
CPU/native-query reference transport evaluates the CPU implementation; GPU-compute reference transport evaluates the corresponding Slang implementation. The authored graph's standard BSDF helpers use the same model selection. Existing custom BRDFs remain author-owned. The GPU material records extend the existing indexed read-only scene storage table, selected by each triangle's resolved material binding. Eta/model, K/alpha and coating metadata have shared named word addresses; the private scene-kernel ABI advances to 10. The table requires one storage binding and does not require a descriptor array of constant buffers or bindless textures. Existing resource retention and producer/consumer completion rules continue to protect the complete accepted snapshot.
The source-preserving profile is an RGB proxy. Profile 5 retained coating parameters with approximate shading; profile 6 adds the bounded non-scattering layered model below. Raster lit shaders retain their approximation. Spectral PBRT equivalence is not claimed. References: thanks to Matt Pharr, Wenzel Jakob and Greg Humphreys, Physically Based Rendering, fourth edition, §9.3, for complex-IOR Fresnel, and the existing Heitz visible-GGX sampling reference.
Acceptance uses independent complex-amplitude Fresnel calculations, Lambertian throughput identities, invalid-input output preservation, source-material asset round trips and a mixed diffuse/conductor scene comparing raw CPU/GPU reference AOVs. Full staircase2 source-PBRT fidelity and the previously recorded dense native-query qualification gap remain separate acceptance items.
Non-scattering coated-diffuse reference¶
Reference profile 6 evaluates staircase2's Wood and FloorTiles as an isotropic
rough dielectric interface above a textured Lambertian base. Their direct GGX alpha
values are 0.1 and 0.01 (remaproughness=false), with default coating IOR 1.5 and
optical thickness 0.01. The intervening medium has zero scattering albedo and unit
extinction in optical-depth coordinates: zero albedo still absorbs light. Every
internal crossing applies exp(-thickness / abs(directionTangent.z)).
CPU and GPU implement Fresnel reflection/refraction, total internal reflection, correlated Smith GGX, visible-normal sampling, internal interface/base reflections and absorption. The interfaces remain colocated at the shading point, as in PBRT's layered BSDF approximation; this does not add geometric refraction or transmission through the opaque base. The internal random walk has PBRT's default ten-event limit and compensated roulette after event three. It does not consume scene path depth or trace scene intersections. This finite limit is part of the declared profile, not a claim of an infinite-depth layer solution.
The sampler returns a complete continuation Weight and a separate directional
MisPdf. A layered sample's proportional Evaluation retains a valid f*cos/pdf
ratio but its PDF must not drive external MIS. Direct-light evaluation estimates
the layered BSDF, with an independently addressed approximate directional PDF for
MIS. Both light-sampled and BSDF-hit contributions use those same semantics.
Legacy/diffuse/conductor sampling and their exact proposal PDFs remain available.
ReferenceBsdfRandomContext uses the caller's Philox key and logical receiver/path
address. Scope identifies the path, Event its sample and Dimension the scene
bounce/material event (0 through 255). Each event reserves 256 internal dimensions;
evaluation, PDF estimation and continuation use distinct key domains from
coated_diffuse_layout.h. The reference tracer supplies its pixel, committed sample
and scene bounce. Internal branching cannot shift the outer sampler's dimensions.
Null proposals, absorption and depth/roulette termination retain their probability
mass; there is no retry, energy clamp or roughness remapping.
LUSCN v7 and its existing indexed GPU table already carry alpha, IOR and thickness;
no format extension is needed for this restricted medium. The cooker resolves
explicit scalar eta/thickness and rejects scattering albedo, scattering textures,
anisotropy, smooth interfaces, IOR below one, and nondefault maxdepth/nsamples.
An explicit RGB zero albedo and default controls are admitted. The best-effort pbrt
importer types the same admitted coatings, accepting any nsamples because it only
changes the estimator's variance, and leaves the others on legacy terms with
MATERIAL_APPROXIMATED (best-effort import).
Storage may retain
parameters outside the renderer's supported profile; reference material admission
rejects those inputs. Shader packages must be rebuilt for scene-kernel ABI 11 and
reference kernel ABI 5. Capture profile 6 separates these results from older
coating approximations and restarts accumulation under the new integral.
The authored shader interface exposes CoatingEta and CoatingThickness.
Use EvaluateStandardBsdf with CoatingRandomContext and
SampleStandardBsdfTransport together for layered reference transport. The latter
returns direction, complete throughput weight and the external MIS density.
The original direction-only helpers retain their compatibility approximation for
coatings; re-evaluating a sampled layered direction cannot reconstruct its internal
path weight. Ordinary raster and GPU preview shading remain approximate.
Thanks to Matt Pharr, Wenzel Jakob and Greg Humphreys,
Physically Based Rendering, fourth edition, §9.7 and §14.3,
and pbrt-v4 revision b0bdf461ba5dd6504b70ebf0932acc0fe7aa744f, for the adapted
dielectric and layered estimators. The implementation specializes their two-sided
layer model to a rough isotropic top interface, opaque diffuse base and zero
medium scattering, using RGB channels and Ludus Philox streams. The Apache-2.0
license is retained in third_party/pbrt/LICENSE.txt and installed with the SDK.
Validation includes independent black-base Fresnel and no-IOR-contrast absorption
limits, internal-path replay, output preservation, source-parameter round trips,
PBRT BSDF/continuation measurements, and matched raw CPU/GPU captures containing
textured coatings at both staircase2 alpha values. Full scene geometry/traversal
parity, spectral textures and texture footprint filtering remain separate limits.
The frozen PBRT fixture uses 131072 samples per alpha/reflectance pair; the Ludus
regression uses 32768 independent samples and accepts each BSDF, continuation and
MIS-density mean within six combined standard errors plus an absolute 1e-5
rounding allowance. It also checks accepted-sample probability, mean reciprocity
and energy no greater than one within sampling error. The mixed textured scene
retains the existing raw linear CPU/GPU RMS tolerance of 1e-3.
The independent measurement harness uses PBRT's own build/toolchain outside the
engine dependency graph. To reproduce the fixture in
modules/graphics/renderer/tests/reference_material_tests.cpp, construct
CoatedDiffuseBxDF with eta 1.5, alpha 0.1 or 0.01 on both axes, scalar base
reflectance 0.2, 0.6 or 0.9, thickness 0.01, albedo/g zero, maxDepth 10 and nSamples
one. Evaluate in radiance mode at wo=(0.6,0,0.8) and
wi=(-0.4,0,sqrt(0.84)). For each pair, vary GetOptions().seed from zero through
131071 and measure f, PDF, and Sample_f's f*abs(wi.z)/pdf ratio. One external
PBRT RNG(871) stream supplies three uniforms per continuation sample and advances
across all six pairs in the fixture's order. Null proposals contribute zero to the
mean; divide by the total sample count. Record each mean and its standard error
sqrt((mean(square)-square(mean))/count), plus the accepted-sample fraction.
CPU/GPU branch replay is checked separately from agreement of independent sample
means; numerical/statistical acceptance never uses tone mapping or denoising.
Smooth dielectric reference¶
Reference profile 8 renders pbrt-v4's smooth dielectric and thindielectric
materials, such as the glass balustrade beside the staircase. Dielectric is the
interface of a nonabsorbing medium with one real index in every Eta channel and
zero K. A closed mesh bounds the medium, and its oriented front side faces the
outside. ThinDielectric is an infinitesimally thin slab of that index. Reference
admission accepts equal RGB indices, zero extinction and GGX alpha below
LUDUS_SMOOTH_INTERFACE_ALPHA (pbrt-v4's EffectivelySmooth threshold, 0.001).
Dispersion, absorption inside the medium and rough transmission are rejected.
Both lobes are directional deltas. Sampling reflects with probability equal to the
unpolarized Fresnel reflectance and otherwise refracts by Snell's law; total
internal reflection reflects every sample. Selecting a lobe with its own Fresnel
probability cancels that factor, so a reflected sample carries weight one and a
refracted sample 1 / eta^2, the radiance scaling of pbrt-v4's
TransportMode::Radiance. A thin slab reflects with
R + T^2 R / (1 - R^2), the sum of its internal reflections, and otherwise passes
the ray undeviated with weight one. EvaluateReferenceBsdf returns zero value and
density for every direction pair.
The reference tracers keep the pbrt-v4 two-sided convention. Each hit mirrors its
normals towards the viewer, and ReferenceBsdf::IsViewerInside records that the
viewer was behind the oriented front side, so refraction uses the inverse relative
index. A refracted continuation must leave on the far side of the geometric
surface; any other lobe stays on the viewer's side. Specular vertices skip
next-event estimation, because no light proposal can reach a delta direction.
Emission or environment found after a specular bounce takes full weight instead of
a MIS weight (ReferenceBsdfSample::IsSpecular), also under OpticalWavelengthWorld,
whose wave connections carry all other light but cannot reach a delta direction. Shadow rays stay opaque to glass,
as in pbrt-v4, so a surface behind glass receives light through it only by BSDF
sampling. Delta bounces count towards MaxDepth and roulette like any other event.
Metropolis captures report Unsupported for dielectrics.
Scene assets need no format extension: LUSCN v7 already stores the model tag,
Eta, K and alpha, and readers accept tags 4 and 5. The record's legacy base
colour, metallic and roughness keep the previous opaque approximation, a smooth
dark reflector with base colour DIELECTRIC_FALLBACK_REFLECTANCE (0.04). Raster
shading and render-graph shaders without transmission still shade these legacy
fields. They do not render glass as transparent. The typed pbrt-reference-materials-v2
cooker admits scalar eta and smooth roughness, and rejects rough dielectrics. The
best-effort importer stores the typed model and imports a rough dielectric as a
smooth one with ROUGH_TRANSMISSION_APPROXIMATED. LIGHTING_PROFILE_VERSION 8
separates these captures from the opaque approximation.
shaders/dielectric.slang is the GPU twin of src/reference_dielectric.cpp.
The CPU sampler is checked against an independent float64 Fresnel/Snell oracle,
total internal reflection, the thin-slab series, index-matched pass-through and
rejected inputs. Both tracers measure an emitter seen through a slab and a thin
pane against the analytic transmitted fractions T^2 (1 + R^2 + R^4) (depth six)
and (1 - R) / (1 + R), and the raw CPU/GPU parity scene adds a glass sphere and
a thin pane.
Thanks to Matt Pharr, Wenzel Jakob and Greg Humphreys,
Physically Based Rendering, fourth edition, §9.3 and §9.5,
and pbrt-v4 revision b0bdf461ba5dd6504b70ebf0932acc0fe7aa744f
(DielectricBxDF, ThinDielectricBxDF, FrDielectric, Refract; Apache-2.0,
third_party/pbrt/LICENSE.txt), for the adapted smooth dielectric estimators and
the specular-bounce MIS convention of §13.4.
Implementation phases and acceptance¶
Complete prepared-scene acceptance uses the existing installed-SDK reference consumer and an independent serialized-bundle verifier. See the full prepared-scene renderer acceptance workflow for exact resource/triangle accounting, source-order versus optimized raster pixels, detached-owner captures, optional native/CPU parity and provenance. Passing that admitted RGB profile does not qualify arbitrary source materials, scene-scale performance or the remaining L6 effects.
These phases complement RHI R0–R5; they do not rename or claim completion of them.
| Phase | Deliverable and prerequisites | Acceptance |
|---|---|---|
| L0: portable scene packets (implemented) | R1 static flat/unlit meshes with fixed transforms, immutable snapshots, CPU visibility, ordered 2D overlays, error materials; remain within current limits and R1-compatible depth | Public-only SDK fixture on Vulkan/Metal/WebGPU/WebGL; invalid/stale inputs, capacity failure and pixels match an unsorted direct path |
| L1: views and depth (implemented) | GDI/RHI explicit attachments, clear/compare state and frame retention; orthographic/perspective, virtual screens, offscreen/split views | Reverse-Z ordering, clip/UV orientation, letterbox picking, DPI, zero extent, resize and neighboring view preservation |
| L2: mutable data and overlays (implemented) | Completion-scoped replacement uploads; dynamic instances, debug triangles, R8 text adapter | In-flight overwrite stress, line near-plane/degenerate cases, painter/scissor order, atlas pressure, CPU text fixtures and loss/restart |
| L3: material/content versions (implemented) | Cook/reflection/layout expansion, texture formats/mips and pipeline prewarm | Per-target layout fixtures; invalid reload preserves old image; mip/color semantics; bounded cache/upload overlap and deterministic fallback |
| L4: lighting baseline and RGB surface fidelity (implemented) | Lit material, opt-in packed normal/roughness/metallic map, bounded direct lists, environment, shadows, baked assets, linear intermediate/output | Reference BRDF/color tests; no double lighting, shadow invalidation/atlas borders, saturation/overflow and LDR/HDR capability variants |
| L5: measured scaling (portable implementation) | Opt-in CPU clustered forward, grouped culling and authored LOD; optional native parallel recording deferred | Dense-light and many-object fixtures; direct-path comparisons, overflow correctness, physical GPU median/tail/memory/latency evidence |
| L5.1: budgeted scene capacity (implemented) | Initialization-time registry and snapshot/view workspace reservations; full-order borrowed diagnostics; budgeted RT0 records and configurable static TLAS instance limits | Above-default capacity, partial groups, exact-budget rejection, detached/in-flight retention, stale handles and recovery |
| L6: optional advanced effects | Temporal/GPU-driven/virtual-residency plans and the RT0–RT4 ray plan | Capability probes and disabled-path support; image/temporal tests and workload-specific gains before default enablement |
Use empty, single triangle, Cornell box, transparent intersections, split-screen, many small meshes, many lights, heavy overdraw, animated bounds, streamed textures, multilingual overlays and repeated resize/reload/loss scenes. Separate software-GPU CI from physical GPU performance evidence. Required implementation checks are warning-clean pinned builds, meaningful unit/pixel/SDK tests, ASan/UBSan and pinned format/tidy; documentation changes run documentation gates. Do not invent FPS targets or buffer capacities without the supported workload and measured budget.
Contemporary primary references¶
- Khronos, Vulkan Pipeline Cache and Pipeline Management sample: pipeline creation can remain costly; validate persistent cache compatibility and prewarm legal variants. Cache miss behavior must remain visible.
- Khronos, Render Pass and Synchronization: explicit attachment preservation and declared hazards inform pass contracts; actual operations remain constrained by Ludus's pinned API/toolchain.
- W3C, WebGPU specification: validate enabled features, usage scopes, limits and formats. Timestamp queries and relevant indirect features are capability paths, not assumptions about every browser.
- Google Filament authors, Physically Based Rendering in Filament: consult its energy-conserving BRDF and light/exposure conventions when specifying L4. The choice of a bounded forward baseline and staged features is Ludus's design judgment, not a performance result from Filament.
Thanks to these authors and organizations. Future implementation must cite the specific consulted algorithm/section beside the code, preserve notices and explain departures; this architecture's bibliography does not replace code attribution.
Authored material inputs and HDR environments¶
The authored material recipes (renderer_authored and
renderer_clustered_authored, with matching shadow recipes) add mesh tangent
frames, linear vertex RGBA, a second material UV channel, textured emission and
HDR image-based lighting. Select LitMaterialDescription::AreAuthoredInputsEnabled
explicitly; older recipes reject the requested extension. MaterialUvSet selects
UV0 or UV1 for the base, packed surface and emission maps. These maps share their
UV scale/offset, sampler and LOD; independently transformed inputs require an
explicit cook/rebake. Bake UVs remain separate. Vertex color multiplies base RGBA,
including alpha-test coverage, but does not multiply emission.
MeshDescription::Tangents contains XYZ and handedness W. Creation orthogonalizes
and normalizes finite authored XYZ against the supplied normal. W must be +1 or
-1; W=0 explicitly declares an unavailable vertex tangent. Mirrored object
transforms adjust handedness. Missing/unavailable tangents use the existing
UV-derived fallback; Ludus does not regenerate or claim MikkTSpace tangents.
The authored recipes derive the inverse-transpose normal direction from the
world matrix, using a positive common rescaling before cofactors. This frees
three attribute locations for tangent, color and UV1 while preserving the
16-attribute interface. The common copied vertex record is now 80 bytes; legacy
recipes keep their original attribute offsets and semantics. Account for this
larger record when sizing mesh buffers.
The portable RHI binding address space now has 16 slots, shared by its public metadata/dynamic-offset arrays, runtime and offline shader tooling. Physical stage/descriptor limits remain separate admission constraints. Authored forward uses ten bindings; clustered uses eleven, including independent environment texture/sampler and emission texture. Baked lighting, shadow atlases and clustered light lists coexist with HDR lighting. Rebuild SDK consumers after this public metadata layout change.
EnvironmentMap is a move-only owner initialized from borrowed, tightly packed
linear HDR Vector4 pixels. +Y is the north pole, longitude zero is +X and
increasing longitude turns toward +Z. RGB is finite radiance in [0,1e12]; alpha
is ignored. Width/height are bounded at 4096 each. Initialization copies the
source, builds a luminance/solid-angle CDF, integrates nine diffuse SH coefficients
with binary64 accumulation, and creates a deterministic GGX-filtered RGBA16F
latitude/longitude mip chain. Default filtering uses width 128 and 128 samples;
quality and a positive CPU byte budget are caller inputs. Preparation is cold
setup work, never part of a draw. Unsupported HDR devices reject explicitly.
The raster texture stores radiance divided by a recorded scale when the source exceeds binary16's range, and restores that scale during sampling. This is storage normalization, not exposure or clipping of the original source. Diffuse uses cosine-convolved SH; specular uses roughness-selected prefiltered mips and the analytic DFG approximation documented by Filament. These are finite-resolution, single-scattering approximations, without local-probe visibility or energy compensation. Environment lighting replaces the constant environment terms; a valid bake still supplies diffuse irradiance. Background/sky composition is a separate caller task.
EnvironmentMap environment;
EnvironmentDescription source;
source.Pixels = hdrPixels;
source.Width = hdrWidth;
source.Height = hdrHeight;
source.CpuByteBudget = environmentPreparationBudget;
// Initialize returns Ready/Pending with ownership; other statuses preserve empty.
const rhi::RasterStatus status = environment.Initialize(device, source);
// Poll GetStatus until Ready before creating a material/capture.
LitMaterialDescription material;
material.AreAuthoredInputsEnabled = true;
material.Environment = &environment;
material.EmissionMap = emissionTexture;
All map operations use the device owner thread. Initialize, Reset, moves and destruction require a closed frame. Accepted materials retain the immutable map and sampled resources after its public owner resets; keep the device alive until all retained uses finish. The map's source/CDF CPU storage has its own explicit budget, separate from renderer/reference job budgets.
Reference captures select ReferenceCaptureDescription::EnvironmentTexture
explicitly. They evaluate the original source radiance and its importance PDF,
including miss and next-event MIS terms; raster SH/prefiltered mips are never added
to transport. CPU sampling uses exact row solid angles and a binary64 CDF;
GPU packing stores the normalized CDF in binary32 and evaluates the corresponding
quantized density. Both use piecewise-constant source radiance. Black maps have a
valid zero-radiance distribution. Tangents, vertex color, routed UVs and emission
maps are copied/fingerprinted with the integral. The reference lighting profile remains
version 4 (geometric emission sidedness); the packed GPU kernel ABI is version
5 for opacity-range primitive identity. Recook matching preview, reference and
graph artifacts. Existing LUHDR container/sampler versions stay one.
Historical evidence retains its original profile identity.
CPU oracle and compute preview implement authored masked coverage. Native query
capture returns Unsupported for authored masked materials because the current
native opacity interface cannot evaluate routed UVs and vertex alpha. Other
native reflection inputs use the retained authored geometry. Dense-scene native
query numerical qualification remains a separate open item.
The implementation adapts Romain Guy and Mathias Agopian's Physically Based Rendering in Filament, sections 5.3 and 9.6, including its documented Lazarov DFG approximation, and Matt Pharr, Wenzel Jakob and Greg Humphreys's Physically Based Rendering, fourth edition, Infinite Area Lights. The adopted departures are latitude/longitude prefilter storage, exact row solid angles, a flat CDF and the explicit finite RGB profile above. See full static resource acceptance for assets, commands and limitations.
Authored input view and naming contract¶
MeshDescription::Tangents, Colors and MaterialTexcoords1 use Foundation's
BorrowedView<const T>.
Each channel is empty (the documented fallback) or has exactly VertexCount
elements. CreateMesh rejects invalid or mismatched views before reading and
copies accepted data; the views do not extend caller storage lifetime. For an
array use geometry.Colors = colors; for dynamic storage use
geometry.Colors = {colors.GetData(), colors.GetSize()}.
Optional environment owner pointers are named EnvironmentOrNull for lit
materials and EnvironmentTextureOrNull for reference captures. Environment
source pixels are PixelsOrNull; successful initialization still requires a
nonnull pointer with the declared width/height extent. Move parameters are
named other, because they transfer ownership rather than output through an
lvalue reference. Shared/weak smart pointers are not currently a Foundation
facility; synchronous mesh borrowing does not require shared ownership.
Authored static area lighting and worker diffuse transport¶
The scene interchange contract retains analytic photometric lights, geometric emitter sidedness, per-placement power and linear HDR source radiance. Version 1/2 decoder identities derive from object/material order and retain two-sided emission. The renderer creates no invented fill lights. Importers declare their fixed Rec.709 photometric proxy; this does not establish PBRT spectral equivalence.
BuildTriangleAreaLights generates one centroid or three symmetric weighted
Lambertian samples. BuildPlanarAreaLight instead produces one area-weighted
centroid for a consistently oriented connected planar source, so triangulating a
disk or quad does not multiply its proxy lights. It validates nondegeneracy,
coplanarity and a centroid inside the supplied mesh. Connectivity is a producer
contract, not a topology proof. Curved sources or concave patches whose centroid
is outside fail explicitly. These are declared finite-source quadrature
approximations: near-field intensity, light shape and soft shadows are not exact.
Distances are metres; a weighted area sample stores RGB nits times square metres
and evaluates its emitting cosine over squared distance, with the existing
bounded range window. AreaOneSided clamps the cosine; AreaTwoSided uses its
absolute value. Material emission sidedness is also retained by raster and CPU/
compute reference transport, including mirrored placements.
The 80-region atlas admits thirteen six-face point/area proxies without reducing
the 128-entry clustered list. The authored clustered fragment uniform is 16272
bytes, within the 16 KiB portable profile. Use clustered recipes when more than
eight lights are present; forward selection still reports its dropped count.
Reflected parameter packing, light-selection reports and shadow-region staging
use reusable renderer-owned CPU scratch, allocated lazily on the device owner
thread and charged to Limits::HostBytes. They do not place the expanded layouts
on the browser's default 64 KiB call stack. Insufficient scratch budget returns
CapacityExceeded without publishing a material/atlas or changing its output
report. Switching between forward and clustered admissions resets report counters;
retained scratch is reused until renderer shutdown.
Each region owns a prepared view with conservative light-frustum culling and
compatible packet grouping; casters remain full-detail and independent of the
main camera. Configure at least 156 views plus ordinary
views for overlapping replacements of a 78-region atlas, and sufficient RHI
resource, binding and byte budgets (the reference acceptance uses 160 views and
256 resources per kind). DrawShadowAtlas runs before indexed draws in its frame
and preflights its aggregate packet count plus caller-reserved later draws against
the enabled per-frame draw budget. The direct pass loop does not consume GDI's
per-graph pass budget. Other backend failures can still partially encode a
candidate; discard it and keep the previous published image.
StaticLightingBake prepares CPU-only world-space geometry/BVH and immutable
surface terms. Serially, Initialize then Step(maxPathSamples) advance a bounded
number of complete paths on one thread. Scheduled, Submit runs preparation and
coarse disjoint triangle ranges on a caller-owned JobSystem; the owner's Poll
or Finish merges ranges in stable order and encodes the charts after the Wait
fence. Both modes produce bit-identical charts. Cancel stops scheduled ranges at
the next path sample; destroying an in-flight bake cancels and drains it. The jobs
carry the labels StaticLightingPrepare and StaticLightingRange, so a system
initialized with the profiler hooks shows them in captured traces. See the
threading consumer contract.
No renderer registry, native ray-query API, GPU upload, readback or Qt state is
touched there. Keep the input scene's texture storage immutable and alive until
the bake is destroyed. Composition must discard results with obsolete exact
revisions before publication.
Input flaws that cannot change the result are tolerated and reported, not fatal.
A zero-area (collapsed) triangle covers no surface: it is left out of the BVH,
emitter selection and probes, keeps its chart tile (zero irradiance) so layout
and bake coordinates are unchanged, and an emitter whose triangles all collapsed
emits nothing. GetDiagnostics and GetSkippedTriangleCount report the skipped
triangles as soon as preparation completes, while ranges still run, so a caller
can warn its user and offer Cancel; each Ready chart also carries its object's
count. Real triangles keep their indices and random streams, so their charts are
bit-identical to a scene without the collapsed triangles. Non-finite geometry,
including an area that overflows binary32, still fails preparation.
The virtual diffuse receiver uses cosine-hemisphere sampling plus the reference integrator's same-measure next-event MIS, visibility, metallic/roughness transport and compensated roulette, including its power-weighted emitter proposal. It includes all admitted emissive triangles and raw authored environment direct diffuse, plus subsequent indirect transport. Analytic direct diffuse stays in raster. The declared path-depth/range bounds introduce truncation bias, sample count controls variance, and per-triangle centroid reconstruction is spatially coarse. There is no denoiser or general lightmap unwrap. The private probe shares the CPU reference's BVH, opacity, surface/emission sampling, random primitives, MIS and continuation code.
Ready charts are per placement and contain linear RGBA8 irradiance plus a
per-channel scale, fully replicated four-texel triangle tiles, stable SourceId,
exact scene/dependency revisions and mesh-to-world transform. GPU upload occurs
on the RHI owner after worker completion. Assign the SceneFormat helper UVs to
deindexed vertices (LUSCN v4 to v6 meshes are indexed and store no chart UVs, so
expand their corners first), keep source UVs separate, select no mipmaps, and enable
BakedLighting::AreTriangleChartsEnabled with the returned identity UV transform.
HasStaticAreaDirect suppresses the area proxy's diffuse term only when that
chart is valid. DirectLightId=0 replaces no analytic direct term; area diffuse
is separately declared. A matching chart replaces the environment diffuse term
and retains the shadowed proxy specular term. A stale chart falls back to raster
area diffuse and increments the existing bake diagnostics.
Publish a ready shadowed direct-light candidate while worker indirect preparation runs, with an explicit preparation status. Retain the previous drawable until that first direct candidate is Ready. Publish the completed charts as a new immutable material/snapshot version only when their provenance still matches the selected World. Failed/pending replacements retain the current drawable. Camera navigation never requires a progressive path-tracing preview or a finished bake. Area specular uses the shadowed planar centroid proxy; it is approximate but must include every admitted source rather than the brightest source alone.
Transport draws on Matt Pharr, Wenzel Jakob and Greg Humphreys, Physically Based Rendering, 4th edition, sections 12.5 (area lights) and 13.4 (path tracing). The implementation acknowledges these authors beside the shared transport code; the deterministic raster quadrature and coarse triangle chart layout are explicit Ludus departures.
For live World material edits, supply the optional per-object
StaticLightingBakeDescription::Surfaces overrides. They copy the current tint,
UV transform, metallic/roughness, resolved emission/sidedness and opaque/masked
coverage. Texture indices reference the immutable retained CPU scene array;
normal/surface maps and routed secondary material UVs are outside this interchange
bake profile and must be diagnosed by its adapter. Resolve authored power against
the current placement before supplying overridden emission. Overrides use the
raster material's white untextured base contract; an empty list retains the
legacy decoded scene's black untextured semantics. The override array is consumed
at Initialize, while the source texture bytes remain borrowed through destruction.
Use a bake revision key for physical geometry/material/light/environment changes;
camera movement and exposure do not change these view-independent irradiance charts.
Material debug views¶
LitMaterialDescription::DebugView (MaterialDebugView) replaces a lit
material's shaded output with one inspection quantity per pixel: world-space
normal as RGB (n * 0.5 + 0.5), linear depth, perceptual roughness, metallic,
UV0 as red/green, the number of lights whose shadowed contribution is nonzero
as a viridis ramp saturating at MATERIAL_DEBUG_LIGHT_COUNT_SATURATION, or a
wireframe colour (MATERIAL_DEBUG_WIREFRAME_RED/GREEN/BLUE) dimmed toward
MATERIAL_DEBUG_WIREFRAME_EDGE_ON_PERCENT as the surface turns edge-on.
Wireframe draws edges only when the program was prewarmed with
PrewarmLitProgram(..., rhi::RasterFillMode::Lines), which applies line fill to
its colour and depth-prepass pipelines while its shadow pipeline keeps filling
triangles; a solid program fills triangles in the wireframe colour.
Only the debug recipes honour it: renderer_lit_debug and
renderer_clustered_authored_debug, with matching shadow recipes. They compile
the production sources with LUDUS_DEBUG_VIEW, keep the production binding and
parameter layout, and read the mode from the reserved Options.w; the
production recipes are unchanged and ignore it (it stays zero). Creation rejects
values above MaterialDebugView::Wireframe.
ViewDescription::DebugDepthRange supplies the linear-depth {near, far} in
world units: radial distance from the camera for perspective views and distance
along CameraForward for orthographic ones. It must be finite with
0 <= near < far <= 1e7, or {0, 0} when no view uses depth; anything else
fails PrepareView with InvalidDescription. The range rides in the unused
per-instance Color.zw of lit items.
Debug colours are display values. Each debug shader inverts the viewport output
pass (PBR Neutral, then sRGB encoding at exposure 1; InvertPbrNeutral is the CPU twin) so the
encoded pixel equals the intended colour. Inversion is capped just below 1, so pure white encodes
at about 99.5%. Compression desaturates toward white, so a saturated colour with a peak above about
0.95 (for example a pure blue) cannot keep its dark channels and shifts slightly toward white.
Render debug views into an HDR (PreferHdr) target: an RGBA8 target clips the inverted
values at one, which limits white to 0.87 display-linear. Overdraw is not a debug view.
Translucent painter layers draw in a separate overlay pass with their own
transform, and the world pass has no additive, depth-off state to accumulate
fragment counts.
Authored graph workgroup experiment¶
The measurements below were collected before native ray-stage invocation was added,
using commit 44ade68e and the generated-code hashes recorded below. They describe
the compute invocation on that revision; native ray-stage dispatch does not use the
scene-compute workgroup override. Runtime packages now use version 21 so versions
19 (custom traversal) and 20 (native ray stages) retain their existing layouts.
Scene-compute bundles can opt into 8×8 groups using
"state": {"group_size": [8, 8, 1]} on a scene compute node. All explicit
sizes across the bundle's configurations must agree; automatic members inherit
that choice. No override means 64×1. The editor exposes the same shapes through
Pipeline State → Advanced overrides → Group size, including the canvas form.
Runtime package version 21 carries reflected dimensions for every ray compute
stage; older packages retain 64×1. Device limits and edge bounds checks apply.
The ludus_graph_workgroup_tests target cooks the actual ComputeRayTracing and
VeachGuibasMis templates twice, using 64×1 and 8×8, without editing the source
templates. It compares generated-header/runtime dimensions and shader text,
then executes the runtime packages. The two-pass compute graph has three physical
operations: primary compute, shade compute and output raster. The four-pass MIS
graph has seven: primary compute, then MIS, light-only and BSDF-only compute/raster
pairs. The unused primary color is not rasterized. Physical operation position
identifies these intervals; the backend's retained scope name/index is also saved.
Scope names may cover the shared ordered sequence rather than identify each callback.
Reproduce with a native ray-query/timestamp-capable build and an idle viewport:
cmake --build out/build/<preset> --target ludus_graph_workgroup_tests
out/build/<preset>/modules/graphics/renderer/ludus_graph_workgroup_tests '[graph-workgroups]'
LUDUS_GRAPH_WORKGROUP_ODD_EXTENT=1 \
out/build/<preset>/modules/graphics/renderer/ludus_graph_workgroup_tests '[graph-workgroups]'
python3 scripts/python/benchmark_graph_workgroups.py \
--executable out/build/<preset>/modules/graphics/renderer/ludus_graph_workgroup_tests \
--output out/graph-workgroups/confirmation --runs 5
Each independent process runs both graphs over opaque, geometric-cutout and
bilinearly textured planar fixtures under a constant environment. Cutouts omit
alternating geometry columns; these are not alpha-masked materials. The graph
material interface does not expose alpha coverage. The CPU oracle and all shaded
GPU outputs estimate the same first-hit direct-lighting integral, with no
indirect bounce, temporal reuse or denoising. Normal regressions compare 64×64
raw linear means within 0.002 + 0.02 * CPU mean per channel and normalized RGB
RMSE ≤0.12, using 128 samples and matched camera/seed. Every pixel/pass must record
exactly 128 samples. Variant RGB differences must be ≤1e-5 after dividing by the
sample count, and every exported RGBA8 image must match exactly. The odd-extent
mode repeats those checks at 67×67, including partial groups.
Timing uses 512×512, 128 total samples, 16 warmups and 112 measured AB/BA frame pairs, with both frame and pass probes armed on both variants. Readback and image comparison happen after the paired sequence. Each block also runs a 64×1/64×1 A/A process. Blocks alternate experimental/control process order and reverse the first frame pair. Confidence intervals bootstrap independent process medians; AB/BA splits, all physical pass durations, timestamp precision and scope metadata remain in the JSON/raw output. The tool rejects unavailable timings, incomplete sample coverage, nonfinite RGB, missing exports and variant changes.
Generated code was checked before timing on Apple M5 / Metal, macOS 26.6,
Clang 23.1.3 RelWithDebInfo (-O2). ComputeRayTracing emitted identical
209,456-byte MSL for both shapes (SHA-256
0905586217d0b369125770e0b2324c4def5c01b00d1c866055dcb40ffa3c7bb8);
MIS emitted identical 510,545-byte MSL (SHA-256
fdf3f5fda75e93d539df7692f4542ab37f3babd8da132664c9051bad876e12a1).
Reflection changed only the dispatch shape. Offline Metal register/spill reports
were unavailable, so the evidence does not establish an occupancy explanation.
The three-block screen found a large benefit for geometric cutouts and little
change for opaque/textured fixtures; this supports an opt-in, not a new general
default. Performance qualification is specific to this adapter and these fixtures.
Five fresh confirmation blocks used executable SHA-256
b51f3f555fa3ffe470250b7e551c3c21718deddec1a1fe01056a7f024a9c5c1a,
from base 5d718cf6 plus this change (the base includes the merged editor test-link
fix and preserves the screened ray kernels). The paired full-frame results were:
| Graph / fixture | GPU time reduction [95% process interval] | A/A reduction |
|---|---|---|
| compute-opaque | 0.68% [-0.19, 0.87] | 0.09% |
| compute-cutout | 27.98% [27.43, 28.35] | -0.17% |
| compute-textured | 0.73% [-0.69, 0.77] | 0.40% |
| mis-opaque | 0.24% [-0.85, 0.43] | 0.09% |
| mis-cutout | 31.18% [31.10, 31.48] | -0.05% |
| mis-textured | 0.23% [0.12, 0.28] | 0.02% |
Every confirmation raw buffer was bit-identical between variants, and all exported RGBA8 images matched exactly, in both A/B and A/A. Only the cutout workload establishes a material improvement. The opaque/textured changes are small relative to controls/order effects; no general default changes.
For cutout geometry, the separately recorded physical GPU intervals were:
| Graph / physical operation | 64×1 (ms) | 8×8 (ms) | Paired reduction |
|---|---|---|---|
| compute / frame | 1.4541 | 1.0494 | 27.98% |
| compute / primary | 0.4558 | 0.4100 | 10.49% |
| compute / shade | 0.9379 | 0.5777 | 37.76% |
| compute / shade_raster | 1.4350 | 1.0311 | 28.22% |
| mis / frame | 3.6816 | 2.5358 | 31.18% |
| mis / primary | 0.4549 | 0.4089 | 10.16% |
| mis / mis | 1.3609 | 0.8779 | 35.58% |
| mis / mis_raster | 2.0125 | 1.4676 | 27.08% |
| mis / light | 0.9430 | 0.6106 | 35.33% |
| mis / light_raster | 2.7861 | 1.9829 | 30.41% |
| mis / bsdf | 0.8780 | 0.5905 | 32.77% |
| mis / bsdf_raster | 3.6063 | 2.4598 | 31.89% |
Time columns are medians of independent process medians; percentage reductions use matched per-frame ratios before taking process medians. They need not equal the ratio of the displayed marginal medians. Metal raster probes span the earliest vertex/fragment start through the latest end. They can overlap compute and include dependency waits: the raster envelopes above are not isolated fragment costs, and neither their durations nor their reductions are additive. Full-frame timing independently establishes the improvement.
Native viewport acceptance uses the selected temporary starter World, an explicitly
authored point light, the cooked runtime and the free editor camera. Run the
[viewport-native-input] case in ludus_editor_tests on an awake, exposed Cocoa
desktop with LUDUS_NATIVE_GRAPH_RUNTIME set to the absolute cooked variant path;
LUDUS_NATIVE_GRAPH_CAPTURE optionally saves its final framebuffer. The fixture
checks native orbit/pan/fly/frame gestures, resize, a reloaded World transform edit,
GPU-frame retention and publication revision. Framebuffer capture follows interaction
so it does not perturb the graph texture leases during navigation.
The original MIS native acceptance attempt published the GPU generation but stalled
while waiting for Qt to paint it. The fixture polled QTest::qWaitFor, which manually
pumps events rather than entering Cocoa's normal NSApp run path. The native fixture
now uses a bounded QEventLoop::exec() wait, allowing display-link/main-queue delivery
while retaining the original deadlines and normal GPU texture leases. See The Qt
Company's Qt 6.12 Cocoa event dispatcher
(QCocoaEventDispatcher::processEvents) for that distinction. Production presentation
and transport scheduling are unchanged. The ready-state overlay now recognizes GPU-only
snapshots, so an empty CPU readback image no longer leaves “Preparing renderer…” over
a completed native view. The fixture supplies cooked candidates directly,
so it disables the built-in document's debounced live build; a late default preview
must not overwrite the MIS runtime being tested.
The editor build cooks the canonical MIS template at 64×1 and 8×8 for the dedicated native case:
cmake --build out/build/<preset> --target ludus_editor_tests
QT_QPA_PLATFORM=cocoa out/build/<preset>/apps/editor/ludus_editor_tests '[viewport-native-mis]'
This case checks native orbit, trackpad/middle-button pan, look/fly and framing;
each navigation wait requires a newer rendered camera revision as well as another
presented image. Resize changes both the embedded surface and graph extent. The
World edit moves starter.cube through the project's normal extraction boundary.
A valid runtime with deliberately broken Metal shader source exercises asynchronous
replacement failure while the prior GPU view remains drawable and navigable. A
subsequent valid replacement must retain that view until its new generation presents.
A runtime load can queue staging behind an export copy, so the test observes the new
candidate serial after the safe boundary rather than assuming the immediate activation
report already describes it. Every graph presentation wait checks the serial actually
sampled by Qt against the first snapshot published for the required revision/generation;
painting another sample of the old view cannot satisfy it.
Framebuffer capture remains optional and follows interaction, so it cannot drive the
paint waits itself. These checks qualify presentation separately from the historical
transport timings and CPU/GPU parity above.