Light transport research · RadianceLab

The instrument
panel

Not a renderer with a GUI bolted on. An instrument you drive — and the reason the research moved as fast as it did.

Every other page on this site shows a picture and then argues about it. This one is about the apparatus that decided which picture to take.

Node types
57
in 7 palette categories, with live 2-D and orbiting 3-D previews
Unit assertions
61
one compiled test binary, exit code 1 on any failure
Scene presets
63
menu entries · 62 of them build · 65 scene builds in all
Controls that do nothing
32
catalogued and published as a claim about standards; three of them fake a response, and the catalogue's own addendum has since found five more

01 — The claim

The tools were built alongside the estimator, not after it

A photon-primitive estimator fails in ways a single number cannot describe. It is unbiased and still wrong at one arc of one surface; it converges and still to the wrong constant; it looks perfect and is four and a half times too bright. Debugging that requires looking at the sampling geometry, not at the image.

So the instruments came first, or at least alongside. The primitive explorer that draws a swept manifold in world space, the correlated batch that freezes the sample set so a weight change is the only thing that moves, the split wipe that puts two estimators in one frame — those three are argued in full on Where the fireflies come from, and this page does not repeat them.

What this page covers is the rest of the panel: the parts that are not about photon primitives at all, and which a reader would have no way to guess exist. A node graph with fifty-seven node types whose output the GPU tracer evaluates per entity. A comparison-report generator that renders each configuration and writes a browsable HTML page. A ray tree you can fire backwards out of a light. An alarm that notices the renderer has been left in a diagnostic state and offers a way out.

Most of it has never been deliberately rendered, and that is said with some pride. It is the most interesting category on the site: the tools were built to answer a question, the question got answered, and nobody went back to photograph the tool. Instruments get used, not photographed. Where that is the case, the row says so.

measured  a number exists, and the page says which source produced it92
seen  looked at, unquantified251
never viewed  reachable, apparently never deliberately rendered116
inert / absent  a reachable control that reaches nothing, or a stub31 / 24
superseded · scene-dependent  works but elsewhere · real but silent without particular content18 / 16
Total548

The first four rows are the four marks used across this site; the catalogue keeps two more, and the sibling pages fold them into never viewed and inert / orphan rather than drawing new glyphs. They are printed here so the column adds up: an earlier version of this read-out stopped after four rows and left a reader 34 short of the total it claimed beside them.

The counts are the whole renderer, not this page: 555 catalogued features, of which 500 work — 253 seen, 116 never viewed, 97 measured, 18 superseded, 16 scene-dependent — and 116 have never been deliberately rendered. The remaining 55 are the 31 inert and the 24 absent, which closes. (Those figures are counted by docs/portfolio/coverage.py on every run. Until 2026-09-06 the catalogue's headline said 544 / 115 / 89 / 32 against its own rows, and this page copied it; the inert difference was a withdrawal the catalogue recorded in an addendum and never carried up.) (Earlier counts are superseded in order: 457, which closed against nothing; 515, what docs/FEATURE-CATALOGUE.md said before the features added on 2026-09-04 and 05 were catalogued; 521 and 523 during the 05th; then 526 with the passthrough surfaces, the exterior preset and the headless camera override; 528 with the surface caustic map and its acceptance harness; then, through 2026-09-06, 529 as passthrough support reached the whole integrator roster, 534 with the four gate-family presets and the aperture vocabulary they share, 536 with BSDF-general surface shading and its self-consistency read-out, 537 with the volume-light rig, 542 with the refraction chain, the post-ball mirror and their four presets, and 544 with photon ring-alt and the five-member gloss series; then, on 2026-09-07, 546 with the scene refusal printed for every strategy and 548 with the one-dimensional group and its frozen-batch swarm; 551 before the hourglass-medium mode and its walk-medium flag were catalogued on 2026-09-10; 553 before preset 79, “The Atrium” — the big room built for that mode — was catalogued the same day; and 554 before THE COLLAGE, the two-render mode, was catalogued that afternoon.) A disproportionate share of that 116 is on this page, which is exactly what a working instrument looks like.

InstrumentWhat it isRouteGrade
Node editor57 node types, 7 categories, live previews, detached editing with a Commit; output evaluated per entity by the GPU tracerView ▸ Node Editornever viewed
Comparison reportRenders up to 8 integrators on a time budget, writes PNGs, a browsable HTML report and replayable JSON. The only working image exportAdvanced ▸ Comparenever viewed
Split A/BTwo independent estimator passes over one scene, composited as a draggable wipeCompare menu · pp.splitnever viewed
PP vs GPU windowLive CPU estimator beside a live GPU path trace at an aligned bounce depthPP Estimator ▸ Compareseen
Ray treeA branching tree from a picked point, or in reverse from a random emitterVisual ▸ Ray Treenever viewed
Gizmo + panel mirrorMMB transform gizmo; the panel draws an animated copy that is itself draggableMMB · Visual ▸ Visualizationseen / mirror never viewed
Clean viewHides every viewport indicator so only the renderer's output remainsGseen
Dolly zoomPins a target and solves focal length from distance as you moveQ in FPS captureseen
Non-standard-state alarmDetects five diagnostic configurations and offers one-click escapeautomatic, Technique tabseen
PrismShapeTestThe only unit tests in the repo. 61 assertions, glm-only link, non-zero exit on failurebuild/bin/<cfg>/PrismShapeTestmeasured
Scene preset menu63 entries in one submenu, in labelled bands — and the only way to change what is in the sceneFile ▸ Scene Presetsseen
Film dumps · PGR1Three hooks writing linear-HDR float films; the format every number on this site was measured frompp.dump*Atmeasured
Scene authoringAdding or deleting an entity or a light from the UI—absent
Inert controlsReachable widgets that reach nothing, three of which visibly rebuild the photon map§08inert ×32

What was checked, and how. Every count on this page was read out of the source in this repository rather than taken from a summary — the node-type enum at core/NodeGraph.h:272-346, the preset menu items at ui/UIManager.cpp:275-372, the assertion call sites in tests/, the tab tables at ui/UIManagerRender.cpp:29-120. Three of those readings disagree with the project's own feature catalogue, and §06 and §07 say where and by how much.

02 — The node editor

Fifty-seven node types is a subsystem, not a feature

Every page on this site closes with the same scope note, and buried in it is one clause: the renderer "also carries a node-graph material and projection editor with texture-space baking". That clause has been carrying a two-and-a-half-thousand-line editor, a thousand-line preview engine and fifty-seven node types on its back. This section is that clause spelled out.

It is a parameter graph, not a shader compiler. Nodes are evaluated on the CPU into per-entity material overrides and a GPU-uploaded noise configuration; nothing is codegen'd and no pipeline is rebuilt. That is the design constraint that makes everything else in this section cheap — a live preview is just the same evaluator on a small domain.

CategorynWhat is in it
Sources5Constant Float, Constant Colour, Time, WorldPos, UV
Texture11Texture (a GL handle from a loaded image), two checkers, Noise2D, FBM2D, Noise Lab 2D, reaction–diffusion 2D, legacy noise 2D, SDF 2D, Eulerian fluid 2D, physics 2D
Vol10The 3-D forms — Checker3D, Noise3D, Noise Lab 3D, reaction–diffusion 3D, legacy noise 3D, SDF 3D, Eulerian fluid 3D, volume incremental, physics 3D — plus Medium Container
Math21Add, Subtract, Multiply, Divide, Power, Sqrt, Abs, Sin, Cos, Clamp, Lerp, Remap, Scale, Step, SmoothStep, OneMinus, Fract, Floor, Min, Max, Negate
Vector5SeparateXYZ, CombineXYZ, Normalize, Length, Dot
Output3BSDF, Medium, Emission — the three terminals the tracer reads
Scene2Camera (a captured view-projection pose) and DataMosh Projector
Total57Seven categories, read straight off the cats[] table at NodeEditor.cpp:2485. The 58th enumerator is the COUNT sentinel, which is where the 56 / 57 / 58 disagreement in the catalogue's own footnote F1 came from

What actually reaches the render

Wiring a BSDF, Medium or Emission output node bakes the graph into three RGBA8 layers of a 2-D texture array, one slice per entity, at a resolution the user sets between 64 and 1024 px (default 256). The packing is fixed and worth stating plainly, because it is the contract between the editor and the tracer:

layer 0  =  albedo.rgb + roughness.a
layer 1  =  emission.rgb + metallic.a
layer 2  =  ior.r + opacity.g + anisotropy.b + dispersion spread.a
Twelve channel slots, three uploads, one array — and the twelfth slot is booked twice. A wired dispersive gate reads that same alpha and zeroes the spread below 0.05, so the gate and the value it gates cannot both be authored.

A correction to this page, found by reading the write instead of the comment. That last line said anisotropy angle until now, because the vector's declaration comment says AnisoAngle.A. Four hundred lines below it the byte that is actually stored is clamp01(dispersionSpread / 10), and the megakernel decodes it by multiplying by ten again. Anisotropy angle is baked nowhere: the channel mask has ten bits — albedo, roughness, metallic, emission colour, emission strength, IOR, opacity, anisotropy, dispersion spread and the dispersive gate — while the auto-seeded BSDF node offers eleven inputs. Three of them, anisotropy angle, thin-film thickness and glint density, can be wired to anything you like and will never leave the editor. The angle still reaches the shader, as a per-material uniform; it just cannot be driven by a graph.

Two details in that path are more interesting than the bake itself. A directly-wired 3-D procedural is deliberately excluded from the bake mask and evaluated in the shader instead, so a socket-level Multiply keeps the real material's base value rather than flattening it into a texture. And box faces, which normally deduplicate to a single primitive, are kept unmerged when baking to UV, so a photograph projected onto a box puts a different part of itself on each of the six faces.

Selecting an entity auto-creates and pre-seeds a graph from its current material — eleven BSDF inputs including anisotropy angle, thin-film thickness, glint density and dispersion spread — with an automatic unique-material split so an override on one box does not leak to its siblings.

The medium half is a different machine, and the page that says otherwise is being generous

Everything above is the surface half. Two node types are the medium — a MediumOutput terminal carrying absorption, scattering, phase g and colour, and a bakeable MediumContainer — and eleven upstream types can drive a volume live. It is tempting to describe that as the same machinery pointed at fog. It is not. There are exactly three routes from a medium graph to a rendered voxel, and they behave differently enough that a user would be surprised by two of them:

  • Constant fold. Anything the resolver does not recognise as a live 3-D generator is evaluated once, at the entity's centre, and averaged to a scalar. So an arbitrary math chain does not become a field; it becomes one number for the whole volume, silently and at full fidelity to what you wired.
  • The live handshake, and its one slot. A recognised generator reaches the shader as a procedural-type integer plus a three-component multiplier, per primitive — and the source says the limit out loud: “The live GPU path only has one procedural payload slot per primitive.” Wire two different generators into absorption and scattering and the second wins while the first's target bits are cleared. Noise Lab 3-D is deliberately routed around this into its own block of the noise buffer, which is why it is the only route that carries a per-channel replace-or-multiply blend mask.
  • The explicit bake. bakeMediumContainer walks a resolution-cubed grid on the CPU and uploads a pair of floating-point 3-D textures — absorption, scattering and two channels of tint in one, the third tint channel and emission in the other — up to eight containers at a time. This is the only route that stores a real field, and it is the one route on this site graded never viewed on both halves of the catalogue.

And none of it reaches the CPU. The photon-primitive estimator contains no reference to the node graph at all; it reads a volume's scattering and absorption coefficients straight off the entity. The CPU integrators that do consult the graph — every one of them except the estimator, through the photon mapper's header — share a single helper whose output struct has twelve fields: albedo, roughness, metallic, IOR, opacity, anisotropy, anisotropy angle, thin-film thickness, glint density, dispersion spread, emission colour and emission strength. None for absorption, scattering or phase. There is no medium equivalent on the CPU to leave unimplemented; the structure has no slot for one. A node-authored fog is a GPU-only object, and the baked-node-graph row of the density-source table on Ten integrators, one scene now says so.

Detached editing, and why it is the right default

The editor keeps a shadow copy of the graph. With Detached Editing on, the scene points at the committed copy and keeps rendering it while you rewire; the Commit button is disabled until there is something to commit, and pressing it swaps the graphs. A renderer that resets a 4000-pass accumulation every time you drag a node is not usable for material work, and this is the mechanism that avoids it.

Previews

Per-node inline 64² thumbnails, lazily-created 256² detail views, and — for Medium outputs — a ray-marched 3-D volume preview with its own orbit camera, drawn by a dedicated preview shader. Three preview modes (GPU, CPU Fast, CPU Full) trade fidelity for frame time. Double-clicking a node detaches it into its own manipulator window with its own orbit preview, and a checkbox decides whether you may have several open at once.

The create menu is type-filtered: dragging from a pin and releasing on empty canvas offers only nodes whose primary output type is compatible with that pin, and creates the node already connected.

Two instruments on the toolbar, one of which explains itself

The editor is a floating window over a viewport in which you have just selected the thing you are editing, so it has a placement solver: it measures the selected entity's screen rectangle and tries to fit itself left, right, above or below it. When it cannot, it says which of six reasons applied — the screen is really tiny, the item takes most of the screen, the item is larger than half the screen, there is not enough free space around it, the item is clipped by the viewport edge, or none of those and it covers the selection anyway. Each reason exists in two forms, overlaps and fully covers. Placement debug is where you watch it think: a scaled map of the work area with four rectangles drawn on it — the viewport, the selection's screen bounds, where the window actually is, and where the solver would put it — under the sentence it chose.

A window that tells you why it is in your way is a small thing, and it is the same instinct as the alarm in §06: the program would rather explain a bad state than present it.

Toolbar controlWhat it doesGrade
Debug ▸ Show Layout MapPlacement debug. The 220-pixel map above, plus the solver's twelve-way reason stringnever viewed
Bake / Clear BakeMedium bake. On a Medium Container node: freeze the live procedural into a cached grid, after which the node reads “Baked N³ static field” instead of “Live preview only until baked”, and a Clear Bake button appears. The node's own Bake Res slider stops at 96; the dispatcher clamps at 128 and, at zero, falls back to the graph's master volume reference resolution — so the slider cannot reach the resolutions the baker will acceptnever viewed
Screenshot — View ▸ Node Editor on preset 47 "Vol Caustic Lab" · click the floor plane in the viewport so the selection fills most of the frame · in the editor toolbar press Debug and tick "Show Layout Map" · shoot the top of the editor window with the map in it: the outer work-area rectangle, the viewport rectangle inside it, the selection's bounds, the editor's own window and the solver's suggested placement, with the reason sentence printed under the map — on this selection it should read that the placement covers or overlaps the visible selection because the selected item is larger than half the screen · keep the viewport in shot so the covered selection is visible behind the window

Photographs, in and out

Dropping an image file over the open editor makes it a Texture node at the cursor. Eleven extensions are accepted — png jpg jpeg bmp tga gif psd hdr pic ppm pgm — and the drop is silently ignored unless the node editor panel is open — a wart, named here because it should be: there is no feedback for a drop that goes nowhere.

The other direction is stranger and better. Any Texture node can request a snapshot of the clean frame — after the effects chain, before data moshing — as its own image. So you can photograph the scene from where you are standing and then project that photograph back into the scene through a captured camera pose, where it lands in the tracer's albedo array and receives correct global illumination. It is the strangest and best thing in the editor. Nothing on this site has shown it happening yet; the recording below is the shooting instruction.

Screenshot — View ▸ Node Editor with preset 3 (Material Showcase) loaded · select the rough-glass sphere so a graph auto-seeds · wire NoiseLab3D → Multiply → BSDFOutput.roughness · Previews ON, mode "GPU" · open one Medium-output node's manipulator window and orbit its 3-D volume preview about 30 degrees so the shell reads as solid · Detached Editing ON with the Commit button lit · frame the canvas, the inline thumbnails and the detached window together
video · A photograph becomes a material
Preset 3 (Material Showcase). Frame the sphere row, press G for clean view, and let it accumulate ~10 s. Then: open the node editor, add a Camera node to capture the current pose, add a Texture node and use its in-engine capture to snapshot the clean frame, wire Texture → BSDFOutput.albedo on the large floor plane with Bake to UV on, and Commit. Hold on the result for 10 s while it re-accumulates. Do not move the camera at any point.
Watch for The floor is now wearing a photograph of the room it is in — and the photograph is lit. Watch the sphere row's reflections pick up the baked image, and watch the image darken in the shadowed corner: it entered the tracer as an albedo, not as a decal, so it receives global illumination like any other surface.

The limitation, in the same breath. nodeGraph appears nowhere in src/scene/. No scene preset builds a node graph. Every textured or procedurally-shaded frame this project has ever produced was hand-authored in the editor and cannot be shipped as a preset, replayed from a config, or captured headlessly. That is also why nothing here is photographed yet: the tool has no persistent output the rest of the pipeline can pick up, and the shot list above is how it gets photographed anyway.

03 — The comparison report

A portfolio about comparing transport algorithms that ships a comparison-report generator

Pick two or more of eight integrators, give each a time budget, press one button. The renderer switches modes on a timer, captures each one, and writes a folder you can open in a browser: one PNG per integrator, an index.html that wipes between them under your cursor, and a config.json that replays the run.

It is the most on-message tool in the program, and this section is the first place on the site it gets more than a ledger row. That row grades it measured, which is the wrong axis: it emits PNGs, HTML and JSON, not a validation number, and no generated report exists in this repository. This page grades it never viewed and stands by the stricter mark.

StageWhat happensDetail
SelectEight checkboxes: GPU Path Tracer, Photon Mapper, VCM, MLT, PSSMLT, UPBP, Path Guiding, ManifoldThe Generate button is disabled below two
BudgetSeconds per integrator, 1–120, with the estimated total printed liveEqual time, not equal sample
RunSaves your current mode and every per-integrator enable flag, then walks the list, invalidating and re-arming each integrator as it goesProgress bar, per-integrator status line, and a tooltip asking you not to turn the camera
CaptureReads back the display texture and writes <Integrator>.pngCPU integrators render at half the scaled viewport
ReportWrites index.html with the images, names and layout inlined as JSThree layouts, below
Round-tripWrites config.json — 81 leaf fields across viewport, camera, render, compare, eight integrator blocks and the sceneDroppable back onto the panel to restore the run
RestorePuts your integrator mode and all seven enable flags back the way it found themOutput folder test_YYYYMMDD_HHMMSS/

The report itself is a cursor-driven wipe

The generated page is a single canvas that redraws on every mouse move, clipping each integrator's image into a region defined by the cursor. There are three:

  • 2-way — a vertical divider that lives at the cursor's x. Drag left and right and the boundary between two algorithms sweeps across the frame.
  • 4-way — quadrants meeting at the cursor, so four integrators share one image and you can put the crossing point on the exact caustic you are arguing about.
  • 9-way — a radial pizza: up to nine sectors fanning out from the cursor. Absurd and, for a nine-way comparison, correct.

A Shuffle button rotates which integrator lands in which region, which matters more than it sounds: a wipe comparison has an ordering bias, and rotating the assignment is how you check that what you are seeing is the algorithm and not the position.

Screenshot — Advanced ▸ Compare · preset 47 "Vol Caustic Lab" · tick GPU Path Tracer, Photon Mapper, VCM and UPBP · Seconds per integrator = 60 · Layout = 4-way · press Generate and shoot the panel MID-RUN, with the progress bar filled and the status line reading "Rendering 3/4: VCM" · include the estimated-total line
video · Four integrators, one cursor
Open the generated test_*/index.html from the shot above in a browser. Start with the cursor at the top-left corner, then move it slowly in a diagonal to the bottom-right over 8 s so all four quadrants pass over the caustic in turn. Then press Shuffle twice, pausing 3 s each time, with the cursor held still on the brightest part of the caustic.
Watch for The same fifteen seconds of compute spent four ways. Watch the caustic core specifically: two of the four resolve it and two are still noise there at equal time. The Shuffle presses are the control — the caustic changes character when the algorithm behind the quadrant changes, and does not when only the position does.

Three things it does not do, and one of them is a bug

The export is 8-bit. Capture reads the display texture back as float, clamps each channel to [0,1], quantises to a byte and writes PNG. So the "only working image export in the program" is a post-tonemap, low-dynamic-range screenshot. It is fine for a side-by-side and useless for a ratio — which is exactly why not one number anywhere on this site came from it. Every measured result was taken from a PGR1 float film dump instead (§04).

Equal time is a choice, and it is the harder one to read. Eight integrators with wildly different per-sample costs, each given the same wall clock, produces the comparison a user cares about and not the comparison a paper reports. Nothing in the panel records sample counts, so a report cannot be converted to an equal-sample comparison after the fact.

The JSON round-trip has a hole, and it is the whole reason the Comparison JSON import is graded scene-dependent — one of the two grades this site draws no glyph for — rather than seen: what it restores is restored correctly, and whether the restored run means anything depends on which room happens to be loaded. The exporter writes scene.preset. The importer never reads it. Everything else comes back — camera pose, focal length, f-stop, focus distance, film size, aspect, max bounces, exposure, light strategy, spectral mode, resolution scale, the budget, the layout, the eight selection flags and every parameter of all eight integrators — but the scene does not. Load a saved comparison while a different preset is up and you will replay the settings against the wrong room, silently.

And one latent trap worth naming. The importer restores the photon mapper's v2Mode flag from JSON. That flag gates the entire photon-gather pass. It has exactly three references in the source — its declaration, one read in the render loop, one read in a status string — and no widget anywhere. A config.json carrying "v2Mode": false therefore turns photon gathering off with no control in the program that can turn it back on.

04 — Two images in one frame

Three ways to put two renders in one frame — and the numbers came from a fourth

The comparison report is the offline one. Two more run live, and the distinction between them matters: one compares two configurations of the same estimator, the other compares two different estimators of the same scene.

Split A/B

Two independent estimator passes over the same scene and camera, composited as a wipe with a draggable divider, and a radio deciding which side advances so you can freeze one and let the other climb — the frozen side is labelled as such. Side B overrides strategy, bounce counts, beam radius, guard, MIS point, visualisation, and the comp-mode backup and seam, so "A composited against A alone" is one field apart.

Its mechanics — why both panes bypass the stylisation chain, and why identical settings give bit-identical panes because the two sides share an RNG stream — are argued on Where the fireflies come from, and this page defers to it.

A grade disagreement, resolved in the strict direction. The feature catalogue marks both Split A/B and clean view seen; the visualisation page marks both never viewed. The two are answering different questions — "is this code path exercised" versus "has a person driven it and looked at the result". This page uses the second, stricter reading for Split A/B, because the page that owns that feature says no capture of it exists anywhere in the repository; and it keeps seen for clean view, because clean view is exercised deliberately in nearly every capture instruction on this site even though nobody has photographed the act of pressing G. Where two of this project's own documents disagree, the disagreement is worth printing rather than picking a winner quietly.

PP versus GPU

A separate window: the CPU photon-primitive estimator live on the left, a live GPU path trace on the right, at an aligned bounce depth, with a draggable divider and a bounce-offset slider for the GPU side. Two switches make it usable rather than decorative — a GPU medium-only mode that restricts the right-hand pane to medium transport through the path-type filter with no shader change, and a full-resolution toggle for a true pixel-for-pixel compare rather than a scaled one.

And the one that actually produced the numbers

Neither. Every ratio, median and byte-identity claim on this site came out of the linear-HDR PGR1 film dumps, whose format and validation role are set out on How I know the pictures are right. What belongs here is only the driving surface: three hooks, one analysis script, and enough config keys to run the whole estimator with no window at all.

GPU reference hookpp.dumpGpuRefAt → pp_gpu_ref.bin
PP film hookpp.dumpPpRefAt → pp_pp_ref.bin
CPU integrator hookpp.dumpCpuAt
Analysisdocs/tools/filmstats.py — stats · png · chroma · ratio
Headless drive67 pp.* config keys — 62 in Engine.cpp, 5 in EngineRender.cpp (the three hooks plus the camera override)

One detail of the GPU hook is an instrument in its own right: as it writes, it prints four probe luminances — centre, two points toward the light, and a corner — so a dump alone tells you the GPU's scatter-order semantics before you have compared it with anything. That is the kind of thing that only gets built after it has cost somebody a day.

The division of labour. The visualisation page puts it best and it applies to everything in this section: these tools decide where to look. They never decide whether the answer is right. A wipe is an argument; a float film and a median ratio are evidence.

05 — Driving the viewport

Everything in the frame is pickable, and the picker is one linear scan

Clicking in the viewport does three things at once: it selects, it can focus, and it can set the autofocus distance from the clicked point. All three run through the same 169-line CPU intersector that also serves the gizmo, the ray tree and every CPU integrator — a flat scan over visible entities, no acceleration structure, because there are at most a few dozen analytic primitives in any scene.

The gizmo, and the copy of it in the panel

Middle-mouse on a selection raises a transform gizmo: translate arrows, rotate rings, per-axis scale squares, and a right-button uniform-scale circle. Point-light proxies get the same gizmo restricted to movement, which is the correct restriction and a nice touch — a light has no scale to drag.

The part nobody has photographed is in the Visualization panel. It draws an animated mirror of the gizmo — three breathing axis arrows and their handles, hand-drawn into the panel's draw list — and that diagram is itself draggable and drives the real transform, staying in sync with the viewport gizmo in both directions. The legend adapts when the target is a light. It is a legend that works.

The ray tree

Fire a branching tree of rays and look at where light actually goes. Forward from a point you click, or in reverse from a random emitter, which is the mode that answers "why is nothing reaching this corner".

Directioncamera → scene, or emitter → scene
Bounces1 – 6
Splits per bounce1 – 12
Render modesUnbounded (X-ray) · Occluded + Bounded · Bounded
Occlusion samples20 – 100,000, on a log slider
Continuous modere-shoots to screen centre, or continuously from the light

Segments are coloured by weight, interaction type and whether they are inside a medium, sharing the same glyph vocabulary as every other overlay in the program — cyan for the camera side, warm yellow for the photon side, a red dot at a swept or specular vertex. Six bounces at twelve splits is 3 million leaves if you took it literally; the render modes exist because you cannot look at that, and the occlusion-sample slider exists because deciding which of those segments are actually blocked is the expensive part.

Screenshot — Visual ▸ Ray Tree on preset 44 "Caustic Foggy Room" · Reverse (from light source) ON · Bounces 4 · Splits/Bounce 6 · Render Mode "Occluded + Bounded" · Show Hit Points ON · Occlusion Samples 20000 · press Shoot Ray Tree · clean view OFF so the tree is visible · frame the glass spheres and the two mirror reflectors so the refracted and reflected branches are both in shot
video · Watching light leave the lamp
Preset 44, Ray Tree ON, Reverse ON, Bounces 4, Splits 6, Interactive ON, Occlusion Samples 20000, render mode "Occluded + Bounded". Let it re-shoot continuously for 12 s without touching anything, then switch render mode to "Unbounded (X-ray)" and hold 6 s.
Watch for In occluded mode the tree is sparse and the surviving branches cluster on the glass spheres and the mirrors — that clustering is the caustic, drawn as geometry before any estimator has run. In X-ray mode the same tree fills the room, and the difference between the two frames is precisely the fraction of emitted directions that reach anything.

Clean view, and dolly zoom

G hides viewport indicators so that what is left is the renderer's output — nine guard sites across the render and overlay paths honour it, including a blanket early-return in the debug-viz frame and a suppression of all HUD text. It is why almost every capture instruction on this site says "press G". The visualisation page's inspection also found what it misses: photon dots, path overlays, the KD-tree, physics particles, the hover material badge and the dolly-zoom HUD are drawn outside those nine guards and survive it. Nine guards is the coverage, six overlays is the gap, and both are counted.

A correction to the sentence above. It called the last of those six a “read-out” until 2026-09-06. It is a full cinematic HUD — letterbox bars, pulsing accent lines, a crosshair, four corner brackets, a DOLLY ZOOM [Q] title and a live focal-length pair — and it is drawn inside the performance overlay's own function, after that function's early return. The performance overlay defaults to off, so by default pressing Q puts nothing on screen at all; turn View ▸ Performance on and clean view then hides none of it. Worse, the greyed Debug Overlay checkbox beside the dolly controls carries a tooltip reading “no dolly overlay was ever written” — and some fifty lines of one sit about a hundred and fifty lines below it in the same file. The checkbox really is inert; the sentence next to it is simply false. Argued in full on The camera is not a viewpoint.

Q, in FPS capture, pins a target at the current focus distance and then solves focal length from distance on every frame:

fnew = fref · ( dnew / dref )   clamped to [4, 1200] mm
A Hitchcock zoom, solved rather than keyframed. Disabling it optionally restores the camera and focal length it started from.

The hotkey surface

KeyDoesNote
1–9, 0Select integrator mode 0–9 and jump to that mode's settings tabSuppressed while Ctrl is held
TabCycle all ten techniquesThe switch is deferred three frames so the GPU can drain
Ctrl+1/2/3Pinhole / thin lens / tilt-shift cameraThe third is inert and honestly marked (WIP)
F1–F8Shaded, Wireframe, Normals, UV, Depth, Tangent, Material ID, Checker
GClean viewChosen over Q because Q was taken twice already
B / HomeSelection outline off / all selection and hover visuals offB exists because standing inside a selected fog volume tints the whole screen
Enter / EscCapture and release the mouseRight-button also releases
VWalk ⇄ noclipWASD, Space to jump with grounding, scroll sets speed, Shift/Ctrl/Alt multiply it
E / QProjector on-off / dolly zoom, in FPS capture — and ascend / descend, in noclip and flyDouble-bound. In captured noclip both fire from one press
RCamera-driven wind force on the physics sim
F12Take Snapshotabsent — logs "Snapshot requested" and returns

The double binding is real and the source half-knows it. The comment next to the clean-view key explains that G was chosen "because Q is already the FPS descend key AND the dolly-zoom toggle" — the collision is documented as a reason to avoid a third use, and never fixed. Both handlers are gated on viewport capture; the movement handlers run in noclip and fly. So in captured noclip, E ascends and toggles the projector, and Q descends and toggles the dolly zoom. Found by reading both handlers, not by driving it.

The rest of the control surface, one row each

What follows is the tail: overlays, chrome and shell controls that work, that this site has been photographed through for twelve pages, and that have nothing to argue. Under the rule published on Everything else a feature earns prose only where the page can say something its own name does not — so these get a row, a clause and their grade, and no paragraph.

ControlWhat it is, in one clauseGrade
Pick-ray debugThe last picking ray in yellow with a red dot at the hit, a green image-plane quad one metre out built from the live field of view and aspect, and a text line giving the origin, direction and hit distance. Its own legend reads “Yellow=pick ray, Cyan=aperture, Green=image plane” — and the cyan ring is a fixed 0.05-unit marker of where the aperture is, not a drawing of how big it isseen
Better selection / Selection occlusionProjected glow silhouettes instead of the coarse screen-space box, and a second toggle that filters the silhouette against the depth buffer so hidden parts fade. Both are read in the render path and both honour clean viewseen
Interactive gizmo mirrorThe panel-drawn gizmo described at the head of this section: animated, draggable, two-way with the viewport gizmo, legend adapting for lightsnever viewed
Accel structure overlayOrange dashed BVH internal nodes, or yellow non-empty grid cells with an option to show the empty ones, plus green and cyan per-primitive AABBs. It is the only consumer either structure has: the panel prints its own not-implemented border beside both, so the overlay draws boxes no ray traversesseen
Window title telemetryRadianceLab | FPS | renderer | camera | frame, rebuilt every frame. The fourth field is the camera class's own name, not the integrator mode as the catalogue's copy of the format string has it — which makes the title bar one of exactly two places in the program that read typeName(), the other being the estimator's refusal message on the camera pageseen
Renderer radiosRasterizer or Path Tracer. activeRenderer has 21 references outside src/ui/ and every one of them is a readseen
Menu-bar right clusterA Node Editor button under a three-layer pulsing glow, a Top-Down projector-setup button with the same treatment, an FPG Select / Pick-Off toggle, and the live frame rateseen
Custom font stackOne face, not four: SourceCodePro-Regular.ttf is loaded four times at 16, 21, 13 and 17 px with rasteriser multipliers 1.15, 1.25, 1.10 and 1.30, so the four “weights” are synthesised from one file. Three search paths are tried and the default ImGui font is used silently if none hitsseen
Entity context menuRight-click a Scene Hierarchy row: one item, Hide or Show, which flips visible and marks the scene dirtyseen
PP quick settings on the Overall tabThe estimator's 31-entry strategy combo, the arbitrary-plane angle when strategy 16 is up, and medium bounces — without leaving the tab. Its own comment explains the length is taken with IM_ARRAYSIZE rather than a literal, because a hardcoded count is what hid strategies 17, 18 and 19 from this dropdown until one commit fixed itseen

06 — Things that argue back

The renderer notices when you have left it in a diagnostic state

A path tracer has a failure mode that costs more hours than any bug: you turn on a filter to isolate something, you get distracted, and three days later you are debugging a black image caused by a checkbox you set on purpose. So the Technique tab watches for it.

The restricted-render warning banner pulses orange at the top of the panel whenever any of five conditions holds, and it names each one it found rather than saying something is wrong:

  • Path Type Filter is ON — only a subset of light paths render
  • Bounce Filter is ON — only a range of bounces render
  • Path Length (ToF) Filter is ON
  • Max Bounces = 0 — no light transport, near-black
  • Medium Isolation is ON — surfaces absorb, so walls go black

Underneath is a single button, Reset to full render, that clears all five and puts max bounces back to 8. That last part is the design decision worth naming: the alarm does not restore the value you had, it restores a value that renders. An escape hatch that returns you to an unknown state is not an escape hatch.

Two more warning families, in the same panel

The banner is the loudest of three. The other two fire on the light-strategy combo, and they are worth separating because they warn about different things: one warns that the estimator you picked will be noisy on this scene, the other that it cannot fire at all with the camera you have.

The variance warning is the interesting one, because it does not say “high variance” and stop. Selecting naive sampling prints (!) Naive fog is high-variance, and hovering it gives the mechanism and then the two cases in this program's own preset library where it bites: a scene's tiny ceiling lights, and the pow(cos,500) laser profile, which random scattered rays almost never hit. Beside it each strategy prints its own path-type algebra as a coloured line — + L(D|S|V)*E for what it gains, − LS(D|V)*E  (no caustics) in red for what pure next-event estimation loses. A warning that names the preset it will fail on is a different object from a warning that names a risk.

The pinhole-size warnings have three severities and two thresholds, both readable off the source: red Needs pinhole size > 0! below 0.01 mm on a pinhole camera; red on a thin-lens camera at any size, because its aperture is too small for a naive light tracer whatever you do; and amber Naive LT converges slowly. Try pinhole 500+ mm. up to 200 mm, above which it stops being drawn. What that estimator needs, and why, is argued on The camera is not a viewpoint.

WarningFires whenGrade
Restricted-render bannerAny of five narrowing filters is on, or max bounces is 0. Names each, and offers one button that clears all fiveseen
Variance warning per strategyNaive sampling is selected; names tiny ceiling lights and the pow(cos,500) laser as the cases it fails onseen
Pinhole-size warnings, three severitiesA light-tracer strategy is selected with an aperture that cannot receive it: < 0.01 mm, any thin lens, or < 200 mmseen
Screenshot — Technique ▸ Overall on any preset · first set Max Bounces = 0, turn Medium Isolation ON, and enable the Path Type Filter with one pattern · shoot the top of the panel with the banner showing three bullets at once and the "Reset to full render" button beneath · catch the pulse at its bright phase · include enough of the near-black viewport to show what the banner is explaining

The unit tests, and a correction to two of this project's own documents

PrismShapeTest is a real compiled binary, linked against glm and nothing else — no GL, no window — so it builds in seconds and runs headless. It is produced by every ordinary build, and main returns 1 on any failure, which makes it a CI gate that already exists rather than one somebody should add.

It covers the two pieces of the photon-primitive machinery that are pure functions: the analytical shape classifier (which swept dimensions produce which shape) and the descriptor and evaluation helpers (Jacobians against finite differences, sphere-shell two-root cases, domain rejection, the legacy-versus-canonical warp, an inverse-square check, a multi-light furnace fold, and a Henyey–Greenstein sampler checked against a 200,000-point quadrature).

test_ppshape_classify.cpp — 166 lines31 assertions
test_pp_eval.cpp — 326 lines30 assertions
Total61
What the binary printstwo subtotals, never a sum
Linksglm only
Exit code on any failure1

Sixty-one, not forty-four — and the visualisation page was right all along. This project's feature catalogue says 44 in a footnote; the visualisation page says 61. The catalogue counted only expectTrue and expectApprox, missing the classifier's two other assertion helpers entirely, and it adds that "several are inside loops" — there are three loops in the test files and not one of them contains an assertion.

The count that matters is the number of times the counter increments. Each of the four helpers — expectTrue, expectApprox, expectType, expectPath — increments once per call, expectApprox delegates rather than double-counting, and none is called from a loop. That is 14 + 10 + 7 = 31 in test_ppshape_classify.cpp and 18 + 12 = 30 in test_pp_eval.cpp: 61.

A correction to this section, which was itself a correction. It first published 60 and called 61 wrong, by counting six expectPath calls where there are seven — the missed one is at test_ppshape_classify.cpp:150. And it said the binary prints a total: it does not. Each translation unit declares its own static int g_total (test_pp_eval.cpp:12, test_ppshape_classify.cpp:16), so main at test_pp_eval.cpp:322 runs two independently-counting suites and prints two subtotals, never a sum. A section written to correct two other documents got the arithmetic wrong in the direction that made its own case, which is the only direction that matters. A claim about a test suite that nobody re-derives is not a gate, it is a rumour — including this one.

Byte-identity as a contract

Five options in the estimator ship with an explicit guarantee in their own tooltip — off is byte-identical. Correlated batching off, surface NEE off, specular continuation off in both surface-NEE states, Lambertian emitter off, camera-leg far cap off. The claim is testable in the strongest way available: render, hash, flip the switch off, render, hash again, compare. A feature that promises to be free when disabled and is checked by md5 is a different kind of object from a feature that promises it in a comment.

Beside them sits the comp-mode class counter, which is the same instinct pointed at a harder target: per-class hit and energy counters on both estimator layers, printed at a fixed pass count, that raise <<< BOTH LAYERS -- DOUBLE COUNT when a bucket is non-empty on both. Its own comment explains why it has to exist — a mean averages a double count away, a median ignores it, and a seam shift cancels it out of both films. measured

07 — The preset menu

Sixty-two ways to load a scene, because there is no other way to make one

One submenu, sixty-three entries, labelled bands, one click each. It reads like a convenience. It is the scene editor.

The menu spans the whole history of the project: the Cornell Box that every CPU-versus-GPU ratio was measured in, the noise and reaction–diffusion volumes, four NEE test rigs, the Veach MIS plate, two analytic gate scenes that exist only to be checked against closed-form answers, the caustic labs, six showcase rooms with names like The Reliquary and Verdigris Cellar, and — added through 2026-09-06 — the four gate apertures, the two chain pairs and the five-member gloss series that the newest estimators were validated on.

CountWhatNote
63Menu entriesVerified by counting the writes to scenePresetRequested in UIManager.cpp at the current commit — 62 of them, with 62 distinct values, plus the one greyed entry that writes nothing. It was 44 until 2026-09-05, then 50, then 63 as the gate, chain, mirror and gloss families landed on 2026-09-06. It is one BeginMenu, split into visual bands by TextDisabled headers — “eight submenus” was the wrong noun for a right number
62Menu entries that build somethingNot quite 62 distinct scenes: 70 and 71 are one scene from two poses, so the menu reaches 61 rooms. Preset 9, "Outdoor", is the entry that builds nothing — it has no dispatch branch. Until 2026-09-04 it fell through and built a plain Cornell Box under the wrong name; since then the entry is greyed out, disabled and labelled "(not built)", with a tooltip naming the two open scenes to use instead — still an inert entry, now an honest one
3Builders with no menu entryPresets 38, 39 and 43 — the sphere-, cube- and ball-light foggy boxes — reachable only through pp.scenePreset
65Scene builders in totalCounted as scenes built, not functions: presets 58–62 are one function, createLightSizeRig, called at five emitter sizes; 72–76 are one createGlossRig at five roughnesses; 70 and 71 are one scene from two poses, the second of which is a reference view rather than a picture
2Numbers that do not exist45 and 53 have neither a menu entry nor a branch
13Call sites of one factoryvclBuildShowcase assembles a room, a ball, an occluder and a prop from typed structs, and it is now called thirteen times in ScenePresetVolCaustic.cpp — the four showcase scenes, the three rich rooms, The Gate and three of its four apertures, the light-size rig, and the gloss rig, which calls it with no glass ball at all. This row has been wrong twice: it first said seven and omitted The Apothecary, then said eight and stopped counting when the gate and gloss families arrived
4Shortcut buttons in the PP panelThe gate scenes, one click from the estimator that uses them

Eleven of the sixty-three, and the two that build them

The whole menu is a list of names, and a list of names is not worth a page. These are the entries the rest of this site keeps referring to without ever saying what is in them — plus the two source-only routines that assemble most of the recent ones, and the two numbers that are not there at all.

EntryWhat is in the roomGrade
Preset 4 — Glowing SpheresFive tinted emissive volume spheres on the legacy noise bandsseen
Presets 10-13 — RD BoxThe four reaction–diffusion patterns as one large volume boxseen
Preset 16 — Cornell + Glowing VolumeOne fire-band volume with a positive volume-emission strength: a volume that is the lightseen
Preset 30 — Prism (Rainbow)A dispersive glass prism and a laser emitter. It auto-enables spectral mode, and it holds the repository's only rotated boxseen
Preset 34 — Foggy Cornell, black wallsThe apples-to-apples reference room for the estimator's gate Cmeasured
Preset 35 — Simple Foggy BoxOne ceiling quad emitter, pure-scattering fog, black walls — the simplest scene in which a volume estimator can be wrongmeasured
Preset 37 — Multi-Light Foggy BoxFour lights of four kinds — quad, line, laser, point — each with its own colour and emission profile. It is the scene the variance warning in §06 is describingmeasured
Preset 42 — Cam-Ray Hit RefOne area light dead ahead in vacuum, so the centre pixel must read exactly the emitted radiance. A scene built to have a known answermeasured
Preset 47 — Vol Caustic LabThree glass balls and two auto-aimed reflectors, the second red-tinted. A site hero image, and the room most of the capture recipes on this page nameseen
Preset 49 — Lens ArrayFive identical glass balls, five analytic caustic cones. Wants a minimum bounce of 2, or the direct light drowns what it is showingseen
Preset 50 — IOR LadderEqual radii at index 1.3, 1.5, 1.7 and 2.0 — a staircase of foci. Same minimum-bounce caveatseen
Presets 45 and 53Do not exist. No menu entry, no dispatch branch, no builder — two gaps in the numbering rather than two missing scenesabsent
Parametrised showcase factoryvclBuildShowcase composes a room, a ball, an occluder and a prop from typed structs, so a new showcase is a struct literal rather than a new function. It is the mechanism behind the count in the table aboveseen
vclSlottedPlane — gobo generatorTiles a rectangle with a rectangular hole out of four coplanar quads. Any band thinner than 0.15 in half-extent is dropped rather than emitted — the comment beside the test reads “too thin to survive the pads”, and the pads are the +0.01 the quad builder adds and the +0.02 the plane intersector adds on top of it, which between them would grow a thin band across the hole it bordersseen

Several presets configure the renderer as well as the scene, which is the difference between a demo and a rig: the prism scene auto-enables spectral mode, the Veach test auto-selects power-weighted light selection because its lights span a factor of five hundred, and two of the config-only fog boxes force volume-emitter sampling on. Fourteen of them now have an opinion about lensing, across ten dispatch branches — 24 to 34 mm, each with its framing reason written beside it in source. That is also the origin of a bug this site reports fixed on the camera page: a focal length set by one preset used to leak into every preset loaded after it, and it is fixed by a global reset in the prologue that the dolly-zoom solver overwrites one frame later.

Now the part that makes the menu load-bearing. entities.push_back and lights.push_back occur only inside preset builders — nowhere in src/ui/. There is no way to add or delete an entity or a light from the interface. You can move, scale, retexture, relight and rewire what a preset built, and you cannot change the cast. absent

File ▸ Save Scene, File ▸ Open Scene and File ▸ Export Image are menu stubs that draw a "not implemented" border; F12 logs a line and returns. So the scene graph has no serialisation in either direction, which is the same fact from the other side: the sixty-five builders are the file format.

This is worth stating plainly rather than burying, because it reframes the node editor in §02. That editor is not a scene authoring tool that happens to lack a save button. It is a material authoring tool for a fixed cast, and the reason no preset ships a node graph is that presets are C++ and graphs are runtime state, with nothing in between.

08 — Inert and mislabelled

Thirty-two controls that do nothing, and eighteen that lie

Publishing this list is a claim about standards, so it belongs on the page about the instrument rather than in a footnote somewhere. A knob that reaches nothing is a bug in the interface even when the algorithm underneath is perfect, because the whole premise of this site is that a screenshot means what it appears to mean.

The test applied to every one of them: the variable this widget writes has zero reads outside src/ui/, and there is no alias or derived write carrying the behaviour elsewhere. That second clause is not decoration — two candidates failed it during the audit and are graded live instead. The projector's "Size Matching" block looks UI-local and in fact computes an angle that three render-side files read; the fixed-tracer-seed toggle greps as four UI-only references and reaches the GPU through a reference alias one line long.

ControlWhereStatus
Photon ▸ JacobianPhoton tabTwo references: its declaration and its own toggle. Sets the rebuild flag
Photon ▸ Base PrimitivePhoton tabThree references, all in src/ui/. Sets the rebuild flag
Photon ▸ Final PrimitivePhoton tabSame. Sets the rebuild flag
Russian Roulette toggleTracer ▸ BasicsThe algorithm runs unconditionally regardless — kill below 0.1 throughput after bounce 3
Light tracer ▸ Paths/FrameLight StrategySits directly above the working Paths/Pixel slider
Reconstruction KernelTracer ▸ BasicsBox / Tent / Gaussian, none of them read
MSAA / Antialiasing knobRasterGreyed, labelled [inert] and tooltipped: the window is created at one sample per pixel and no multisample framebuffer is ever built. Its neighbour, backface culling, is live
Tonemapping comboEffects ▸ HDRAll three options render Reinhard. ACES exists in a shader that is never used
Dolly ▸ Debug OverlayView ▸ POVDeclaration and checkbox
Physics ▸ Show Debug VizAdvanced ▸ VolumeThe Fluid Viz combo beside it is the real gate
Manifold ▸ Newton iterations / step sizeManifold tabSliders on a solver whose Newton step count is a hard zero
Active Integrators ▸ Photon Mapper thumbnailalways visiblebgTexPM is declared and never assigned, so mode 2 shows the path tracer's image — under a caption, added later, that says so. Mode 9 falls back the same way and has no caption
prism.json decorative keysconfig fileBackend, integrator, spp, russianRoulette, spectralMode, tileSize, and the whole camera, debug and physics blocks are read by nothing

Thirteen of thirty-two are above; the full list is in docs/FEATURE-CATALOGUE.md §8, and nine of them are honest — they say "(WIP)", or draw a not-implemented border, or in one case are literally labelled [inert] in the widget text. Those nine are a completeness gap, not a communication failure, and they are not the problem. The list also grows in the right direction: the catalogue's own addendum, §8a, written while acting on §8, withdrew one row (the UPBP beam overlay draws after all) and found five more inert controls by chasing the ones it had — a "Shoot Ray" button with an empty handler, two placeholder overlays, a Regen button that regenerates nothing, and a beam radius no estimator integrates over. The true figure is therefore at least thirty-six. The headline stays at thirty-two because that is what the four-sweep count produced; the delta is recorded rather than a new total invented.

The three that fake a response

These are the ones that matter, and they are all in the same panel. The Photon Mapper's Jacobian toggle and its Base Primitive and Final Primitive combos each set the photon-map rebuild flag and the accumulation reset when you touch them. Nothing else reads them. So the map visibly rebuilds, the image visibly restarts from zero, several seconds pass, and a new image resolves that is identical to the old one.

That is worse than doing nothing quietly. A dead control teaches you it is dead the first time you try it. A control that performs the ritual of working teaches you that the parameter has no effect on this scene — a conclusion about the renderer drawn from a defect in the panel. If any single item on this page is a bug rather than a gap, it is these three.

The mislabelled ones

Eighteen controls promise something other than what they do. The worst are not the inert ones — they are the ones that are busily doing something else:

  • UPBP ▸ Photon Beams claims beam segments for volumetric gathering. It is a global homogeneous-fog fallback, a light-path-count multiplier and a connection-transmittance factor. Beams are generated, packed and uploaded; the buffer is never bound.
  • Photon ▸ gather mode "Final Gather" silently renders kNN density instead. The shader has two branches; the selector does not choose between them.
  • Manifold ▸ SDS / Newton solve is primary-sample-space Metropolis with a specular heuristic that shrinks sigma by ten. There is no manifold walk.
  • Tracer ▸ Path-type filter advertises expressions over L D S V E *. Each pattern compresses to four integers — first type, last type, exact length, minimum length — and interior vertices are never inspected, so LDSDE and LDDDE are the same query.
  • A tooltip advertising "denoising" describes a temporal exponential average and a 5-pixel Gaussian. There is no OIDN, no OptiX denoiser, no A-SVGF anywhere in the source.
  • A source comment in the path tracer says BDPT interior connections are "NOT YET IMPLEMENTED"; they are implemented fourteen hundred lines below it, and the strategy tooltip repeats the stale claim to the user.
  • A nineteenth, found after the count was taken. The greyed Dolly ▸ Debug Overlay checkbox in the table above is honestly inert — and its own disabled tooltip explains that "no dolly overlay was ever written". Some fifty lines of one live about a hundred and fifty lines below it in the same file. As with the inert list, the headline stays at eighteen because that is what the four-sweep count produced, and the delta is recorded rather than a new total invented.
  • A twentieth, found the same way. Experimental ▸ Allow Negative Emission carries a tooltip stating that "sampling uses abs(intensity) so both positive and negative emitters contribute proportionally". Nothing implements it: the switch has four references in the program and no reader outside src/ui/, and every emissive gate in the megakernel is a signed > 0.01. The catalogue copied the tooltip into its own record, which is how a wrong sentence gets a second life. Argued on Ten integrators, one scene; the headline stays at eighteen for the same reason as above.

Two entries on that list of eighteen are rows on this site's own landing-page ledger, wrong for the same reason as everything else here: a claim written once and never re-derived. One of them said four material models are unreachable from any preset; a preset added later reaches all four.

Four more, one row each — and one the grades disagree about

The four inert ones below are the honest kind: each is greyed, bordered or captioned in its own widget text, so nobody is misled by them. They are listed because a completeness gap you can see is still a gap, and because three of the four were made honest after the audit that found them, which is the useful half of publishing a list like this.

ControlWhat is really thereGrade
Volume Tracking ▸ Ratio Tracking / Null Scattering / Ray MarchingThree of a four-entry combo whose selection is a function-local static int. Picking one draws a not-implemented border and the words “Not yet implemented”; picking the fourth, delta tracking, prints a green line claiming the active, unbiased method — which is the one the shader really runsinert
Menu bar ▸ Physics (Start / Pause / Reset)Three disabled items under two lines of their own text saying that the physics is implemented and that its start, pause and reset controls live in Render Settings ▸ Volume. A dead menu that points at the live oneinert
Advanced VFX ▸ stAdvIterationsAn iteration count on a transfer that is a fixed two-pass structure-tensor plus statistics transfer. Greyed and labelled [inert], with a tooltip naming the three sliders beside it that are liveinert
Advanced VFX ▸ ddAdvChannelsAn 8-or-16 channel combo read only to set its own index — no uniform, no dispatch size. Greyed and labelled the same wayinert
Expr ▸ Negative EmissionThe switch from the list above, and the one row here whose grade this page disputes. The catalogue grades it never viewed — reachable, never deliberately rendered — and records its tooltip's promise that sampling takes the absolute intensity so both signs contribute. The source disagrees: the flag has no reader outside src/ui/, and every emissive gate in the megakernel is a signed comparison against 0.01. Graded here as the catalogue grades it, and contradicted in the same rownever viewed

Why publish it at all. Because the alternative is a portfolio in which every control looks equally trustworthy, and this one contains three that rebuild a photon map to convince you they did something. The list is the standard: a feature is not shipped when it compiles, and a number is not true because it was written down.