Renderer · const
REVERSED_DEPTH
True while the engine renders its scene depth reversed.
Explained in Hello world.
const REVERSED_DEPTH = trueimport { REVERSED_DEPTH } from '@driftengine/core';In depth
It is true, on both backends, and this is what it took. Nine attempts. The eight that
failed were not wrong about the conversion — the compares, clears, clip corrections and formats
were right early — they were wrong about where to look, and the note below on the instrument
is the most reusable thing on this page.
What it is gated at. Each backend against its own conventional output, at zero tolerance, across every published scene on a real GPU: WebGL2's worst frame 7,348 pixels of 921,600 and WebGPU's 11,832, most a few hundred or none, and all of it seams resolving the other way, which is the improvement. The two backends against each other come to 443,035 pixels on their worst scene reversed against 443,039 conventional, so the conversion costs four pixels of the ordinary disagreement between two independent implementations of an animated scene, in the direction of agreeing slightly better.
npm run wgsl is part of changing anything here. WebGPU draws from generated WGSL, so a
GLSL change left unregenerated keeps the old expression compiled in and the gate measures the
previous shader.
The instrument
Compare the two backends against each other, not each against its own conventional baseline. Five attempts read an ambiguous number because a diff against a baseline can say that something changed and never which side is wrong. Two independent implementations that agree conventionally and stop agreeing reversed localise the fault to one of them. Always take the conventional pair as the control in the same run: it is not zero, and reading a reversed number without it is what made several earlier measurements unreadable.
Then bisect the shader with a constant-output probe, and neutralise terms one at a time. The
last three defects were each found by forcing one factor to a constant and watching the two
backends fall into agreement. flat/main.ts has exactly one outColor assignment and it is the
last line of the shader, so a probe inserted earlier is overwritten and reads as "this feature
is not active" — verify any probe by forcing magenta first and confirming the gate moves.
The four defects, all of them WebGL2's, none of them in the depth conversion
- The lit shader was handed a range-corrected light matrix.
shadowFactorprojects withuLightViewProjand then doesp = p * 0.5 + 0.5on all three axes, andGL_SHADOW_REMAPperforms that same range change in the matrix. Both applied put every receiver depth in[0.5, 1]against a map holding[0, 1].webgpu/renderer.tshad this right and says so. - Point-shadow face matrices were never range-corrected at all.
EXT_clip_controlis context state, so a face matrix still emitting OpenGL's[-1, 1]loses everything below zero and half of every face is clipped away. bakeOnehad no depth bracket.bakeAreaOneput the conventional compare and clear back for the length of an area light's bake; the ordinary lamp path, which is nearly every point shadow a scene has, ran under the frame's ownGEQUALagainst a scratch cleared to 0 while writing depths from a matrix that is deliberately not reversed. Both paths go throughbakePointShadownow, unconditionally, so there is no branch left to forget.- Three shadow matrices shared one scratch buffer, so whichever ran last rewrote the
others, and
beginShadowPassuploaded the raw matrix while holding the corrected one.
Every one of them was invisible conventionally, and none of them looks like a depth bug: a wrong shadow map does not draw broken, it draws dark. That is why the scene read as a lighting regression for eight attempts and why bisecting the lit expression is what finally named them.
The trap, recorded because it cost a whole attempt. Reversed depth measured more view-stable than conventional on both scenes across two nearby frames, which reads as evidence that the difference is the improvement. It is not: both scenes animate, so most of that is motion. Do not stop at the stability number.
Candidates eliminated by measurement along the way, so nobody pays for them twice: ambient
occlusion and its far-plane guards, drawScatter and drawSdfText, POLYGON_OFFSET_FILL and
the offset sign, environment irradiance, depth of field, and the directional cascade's bracket
and range, which were correct before any of this started.
Everything else in this module is live and correct in both senses, and every consumer reads it instead of a constant of its own.