makepad/libs/render/CLUSTERED.md

5.3 KiB
Raw Permalink Blame History

Clustered forward lighting

Renderer defaults to clustered local lights. Geometry submission, static slabs, instancing, and GPU skinning are unchanged: one forward material pass, with separate sun-shadow passes. No G-buffer, geometry prepass, compute shader, depth readback, or per-object light selection is required by clustering.

This follows the CPU-assigned clustered-forward approach described in Filament's rendering documentation. It is a first lighting increment, not Filament feature or visual parity.

Lighting and shadow policy

Local lighting Sun mode World lighting work
Clustered (default) Realtime CSM; no world atlas or CPU probe bake
Clustered OnChange Sun-only atlas and legacy CPU probes
Legacy (MAKEPAD_CLUSTERED=off) Either Original eight-light selection and lamp atlas

Baked per-model AO is retained in every mode. Clustered lights reach primitives, terrain, diffuse/PBR/custom models, and skinned characters per fragment. PBR materials evaluate local GGX specular as well as diffuse. Explicitly prelit materials retain their existing unlit/baked-color semantics.

Local lights can request budgeted realtime shadows; see Entity lights for Splash headlights, point/spot fixtures and shadow budgets. Generic lights default to unshadowed and can shine through walls. The sun retains its own tier. Engine-authored lights use (1 - distance / radius)^2; imported glTF punctual lights use their separate photometric mode. Full linear HDR, exposure/tone mapping, IBL and unified PBR materials remain follow-ups. Clustering does not itself add MSAA.

Budgets and stereo

Renderer::set_cluster_config(ClusterConfig { ... }) controls XY tiles, depth slices, lights per cluster, and total uploaded lights. Defaults are 16 × 9 × 24, 32 lights/cluster, and 1024 total. Hard limits are 32 × 24 × 32, 64 lights/cluster, and 4096 total. set_clustered_lighting(bool) switches the complete legacy/new lighting policy and invalidates old baked state. Policy survives realm changes.

Flat views use frustum tiles with logarithmic depth. The backend currently late-latches XR eye matrices without exposing them to this host. XR therefore uses a conservative world-space 3D grid over light influence, shared by both eyes—not clusters derived from an unrelated flat camera. Large, dispersed worlds make that fallback less selective. Exact stereo/per-eye frustum clustering needs an eye-view snapshot API before it can replace this fallback.

Smaller lights_per_cluster bounds fragment cost but may visibly omit lights; finer grids cost CPU/memory but can reduce false-positive light references. Start device tuning with the counters, not a claimed universal Quest preset. This path has not yet been benchmarked on a headset.

RenderStats::clustered / Renderer::cluster_stats() report build/upload-queue CPU microseconds, uploaded bytes, occupancy, references, rejected lights, and overflow. They do not measure GPU time. Per-cluster overflow retains lights with the strongest conservative contribution estimate; equal scores retain the earlier input. The global cap keeps the first valid input lights. Counts are explicit because neither cap promises all lights survive. Overflow logs are throttled; MAKEPAD_CLUSTER_STATS=1 also logs frame counters periodically.

Texture ABI

One nearest-sampled RGBA32F texture contains cluster headers, packed light indices (four scalar indices per texel), then three texels per light: position/radius, RGB/angular falloff, and normalized emission direction/mode. Headers store scalar index offset/count. CPU list storage and texture upload vectors are reused. Integer-valued floats are exactly representable within the hard caps. The shader mixin and CPU packing live together in src/clustered.rs.

Local shadows use a separate nearest-sampled metadata texture (one light header, four camera rows per face) and a depth atlas, preserving the light-data stride. Inherited texture bindings are resolved by name for derived PBR materials.

Reproduce

Build the sandbox in its own workspace, then launch from the checkout root:

# In apps/commercial/sandbox:
cargo build --release -p makepad-sandbox
# From the checkout root:
MAKEPAD_CLUSTER_STATS=1 ./apps/commercial/sandbox/target/release/makepad-sandbox --remote   # with Sandbox's world setting set to `clustered`

The fixture supplies 256 moving lights and a single large floor, independent of downloaded models. MAKEPAD_CLUSTER_DEMO_LIGHTS=N changes the count; MAKEPAD_CLUSTER_DEMO_FREEZE=1 fixes light motion for A/B images. Add MAKEPAD_CLUSTERED=off for the old path. That comparison deliberately shows missing lights in the old eight-slot path; it is not equal-quality GPU work. F8 switches sun modes. Finish owned test instances via the printed remote port's /gq endpoint.

cargo test --release -p makepad-render
cargo test --release -p makepad-render clustered::tests::benchmark_build_and_pack -- --ignored --nocapture

Coverage includes more than eight overlapping lights, depth/tile boundaries, rotated and orthographic cameras, XR/world-grid lookup, empty/invalid inputs, bounded overflow, texture packing, material shader compilation/binding order, and mode/realm lifecycle. The ignored benchmark measures CPU build+pack only.