The Shading Path That Never Shaded

When you set a material in Lux, you are trusting one thing without thinking about it: that the gold you tuned in one scene is the same gold in the next. That trust has a technical prerequisite. There can be only one place in the whole engine where light meets a surface. The moment there are two, they drift, and your gold turns to brass somewhere you are not looking.
Every Shade Path Imports PBR made exactly that promise to the codebase: one home for Cook-Torrance, lux::pbr, and every shader that lights a surface goes through it. The live forward path does. The bindless raster path does. They both call evaluate_shade_standard_params for direct light and evaluate_shade_ibl_params for the environment, and that is the whole story of how Lux turns a normal and a material into a colour.
Except it was not the whole story, because there was a second shading path. It just never shaded anything.
A deferred renderer that deferred forever
Tucked into the scene crate was a complete deferred-shading chain. If you have not built one: deferred shading splits lighting into two halves. First you rasterise every surface into a fat G-buffer (positions, normals, albedo, roughness, metalness, all written to a stack of render targets). Then a second pass reads that buffer back and lights each pixel once, decoupled from geometry. Add a tile classifier on top, which bins each screen tile by the kind of material it holds so a tile of plain plastic does not pay for clearcoat maths, and you have a tidy, fashionable, genuinely-good-on-paper architecture.
Lux had all of it:
| File | Lines | What it claimed to be |
|---|---|---|
pipelines/shade.rs | 614 | A shader-permutation cache for the shade pass |
scene_buffer_pipeline.rs | 630 | The consumer that recorded classify plus shade |
scene_shade.wgsl | 369 | The shade pass itself |
scene_shade_6mrt.wgsl | 173 | A six-render-target fat-G fallback for older tiers |
shade_classify.wgsl | 148 | The GPU tile classifier |
Plus two test files, 647 lines between them, dutifully exercising all of the above. Roughly 2,600 lines of renderer: tested, documented, architecturally sound.
It had never drawn a frame anyone saw. No live engine path reached scene_shade. The forward and bindless arms had walked past it months ago and shaded through mesh.wgsl and mesh_bindless.wgsl instead. And scene_shade.wgsl knew it. At its core was a stub that, lacking the GPU-side PBR modules the bindless path eventually grew, output a flat vec3(1.0, 0.0, 1.0).
Magenta. The universal colour of I am not real yet. An entire deferred pipeline, faithfully sorting tiles and managing a six-target G-buffer, so that at the very end it could paint every surface the colour of a missing texture. The classifier worked perfectly. It was carefully sorting surfaces into buckets for a shader that filled every bucket with the same shade of “to do.”
Why two paths is your problem, not just mine
This is the part that reaches your screen. A second shading path is not free even when it is dormant, because it is a standing invitation to divergence. The day anyone revived it, there would be two renderers to keep in lockstep: every BRDF fix, every tonemap, every multi-scatter energy correction landed twice or your materials looked different depending on which path happened to draw them. A clearcoat that gleams in one path and goes flat in the other. Consistency is not a nice-to-have in a tool you light scenes with. It is the whole job.
So there were two roads.
Option A: finish it. The chain was not broken, only unwired. I could port the lux::pbr modules across, swap the magenta stub for real lighting, and reanimate the 614 lines of permutation-cache plumbing. A weekend, maybe. And at the end of it, two production shading paths to keep identical forever, which is a steady tax on every lighting improvement Lux will ever make, paid by you in the form of materials that occasionally do not match.
Option B: delete it. The bindless forward raster is the production target. It is the path that is alive, the path the bindless arm lit up, the path the legacy renderer was retired in favour of. Keeping a second architecture warm for a future that is not on the roadmap is the exact dormant debt the pre-launch tenets exist to refuse.
I deleted it. Seven files, one commit, no shim, no feature flag left holding the door open. One shading path means every colour improvement from here lands everywhere you look, at the same time.
The cascade
Dead code is never as isolated as it looks. Pulling the chain out tugged a string of smaller dependents loose, and chasing them to zero is most of the actual work:
mesh_pipeline_cachehad ashade_6mrt_defs_for_keyhelper that built shader-define permutations for the six-target fallback. Dead the moment the shader was gone, deleted with its two unit tests.lib.rshad been re-exportingscene_buffer_pipeline. Gone.pipelines/mod.rsdeclaredpub mod shade. Gone.- A layout-strategy test asserted things about a
ScenePipelinestype that no longer exists. Deleted. - A WGSL-compile test bundled
SCENE_SHADE_6MRT_SRCinto its checks. The source it referenced vanished out from under it; the test went too. - The hot-path audit’s file list named
scene_buffer_pipeline.rs. A file that does not exist cannot allocate on the hot path. Removed.
When the dust settled the workspace compiled clean, lux-render tests passed, and the rendered image was byte-for-byte what it was before, because the thing I deleted had never contributed a pixel to it.
What you get is quieter than a feature and more durable than one: a single, trustworthy answer to the question “what colour is this surface,” everywhere, in every scene, for every improvement still to come.
There was one loose thread, though. The deferred chain had a GPU-side partner holding device buffers, and deleting the chain orphaned it. That is the next post.