DDS Vibe Academy · Class #81 · Mastery

You Cannot Debug What You Cannot See

Six thousand glyphs of live source code on a Shopify page, and five instruments that proved the build wrong before shipping. Every bug was invisible to the checks that came before it.

Subject WebGL observability GPU RTX 3060 Draw calls 30 / frame Read time ~55 min Level Advanced
  • Five instruments built during the project, each catching what the last one missed
  • Live artifact you can operate right now on the Academy hub
  • Six silent failures that returned success while producing nothing
  • Graded evidence every claim marked MEASURED, CALCULATED, or ASSERTED
Quick Answer

You cannot engineer what you cannot observe. This class documents five instruments built during a real-time 3D section on Shopify, each one immediately catching a defect the previous instruments had been silently passing. The section works. The half worth teaching is that every single thing wrong with it was invisible to the checks already running.

Key Takeaways
  • 6,000 instanced glyphs drawing live source code. 30 draw calls per frame, 12,025 triangles, 15.4 ms (~65 fps) on an RTX 3060. The instancing claim holds; the frame is not one draw call.
  • A filmstrip recorder that works in hidden browser tabs caught six visual defects in one image that no numerical check had detected.
  • Signed distance field atlas reduced brightness spread from 28 to 8 across six canvas sizes. Exact Euclidean distance via Felzenszwalb, max error zero.
  • Three.js resets renderer.info per render call. With seven passes per frame, the reported draw-call count was measuring the last post pass. The flattering number came from a broken instrument.
  • Six silent failures that returned success. Every one is documented with the symptom and the reason nothing errored.
  • WebGPU was evaluated and deliberately not built. navigator.gpu being present is not the same as having shipped a compute tier.
  • Every performance figure in this class was measured on an RTX 3060. No generalising.

Section 01

What Was Built

Answer capsule A real-time 3D graduation cap built from 6,000 instanced quads of live source code, running on a Shopify page with no build step, no bundler, and one dependency. The cap is built from the source code of the file that draws it, read out of the DOM at runtime. Click a component and it cites the exact lines it was made from.

The section is live in two places on the DDS Vibe Academy hub: the Manifesto tab and the Press Kit tab. You can operate it right now. Open the X-ray chip, the Slow motion chip, and the Force fallback chip while reading this class.

Build summary (all values MEASURED on RTX 3060)
FactValueGrade
Glyphs6,000 instanced quadsMEASURED
Source characters read at runtime~60,000MEASURED
Source lines cited1,958MEASURED
Simulation texture78 x 78 float RGBA, ping-pongedMEASURED
AtlasSDF, 93 unique glyphs, 1024 x 1024, ~330 ms to buildMEASURED
MSAA4x on the scene passMEASURED
Draw calls / frame30MEASURED
Triangles / frame12,025MEASURED
Frame time15.4 ms (~65 fps) on an RTX 3060MEASURED
Pixel ratio2.0, adaptive, device cap 2.5MEASURED
Dependenciesthree.module.js onlyMEASURED
FileOne Liquid snippet, ~101,888 bytesMEASURED

No build step. three.module.js by full URL via dynamic import(). No importmap, because a second importmap anywhere on the page throws, so the bloom post-processing is hand-written rather than using three/addons/. Grade: MEASURED.

Section 02

The Filmstrip

Answer capsule requestAnimationFrame does not run in a hidden tab. Neither does IntersectionObserver. The section was unobservable and unstartable from automation. A filmstrip recorder that drives the simulation on a fixed timestep caught six visual defects in one image, first run. 12 frames in 162 ms. Grade: MEASURED.

The browser was not broken. The tab was asleep. document.visibilityState returned "hidden"; a control observer on the same element never fired in 1.2 seconds. Grade: MEASURED.

The filmstrip drives the simulation on a fixed timestep with no rAF, composites each frame into an offscreen render target, reads the pixels back deterministically, and tiles them into one image that returns as a data URL. You can run it yourself right now in the console on the live Academy hub page:

__dvaCap['dva-cap-manifesto'].filmstrip({ frames: 12 })

Six defects caught in one image, first run

#DefectDetail
1Camera too low0.42 rad (24 degrees) showed the mortarboard edge-on; composition collapsed on orbit
2Board-to-crown proportion wrongDiagonal 8.63 vs crown 2.60 = 3.32:1, against ~2:1 in the mark
3Crown hollowLateral surface only. From below it was a torus, not a cap
4Dimension rail disconnectedFixed line at x = -3.6, related to nothing
5Glyphs unreadableBlobs, not characters
6Staged assembly correctThe one thing already provable numerically was the one thing already right

The lesson in miniature

Every number I could measure was fine. Everything wrong was visual, and nothing I was doing could see it.

Fixing those exposed a seventh: at full explode the assembly spans ~5 units against 2.6 assembled, so a fixed camera distance cropped the board and pushed the tassel out of frame. The camera now dollies back with explode. Framing is part of the model.

Section 03

Pixel Statistics, the SDF, and the Brightness Table

Answer capsule Mean brightness of lit pixels ran 86 at 200 px wide against 112 at 900 px, climbing monotonically. A coverage atlas with a fixed alpha threshold is resolution-dependent. Replacing it with a signed distance field and scaling glyph world size to keep screen-pixel size constant brought the spread from 28 down to 8. Grade: MEASURED.

The atlas stored coverage, so the shader thresholded alpha, and a fixed threshold is resolution-dependent. At small sizes each glyph covers fewer pixels, more fragments fall under the cutoff, strokes thin out. This hits mobile hardest.

The fix: signed distance field

Store distance to the edge, not coverage. Distance interpolates and downsamples gracefully where coverage does not. Exact Euclidean via Felzenszwalb's two-pass 1D transform, 128 px per glyph, box-downsampled to 64. In the shader fwidth() keeps the smoothstep band exactly one screen pixel wide at any projected size.

The EDT was checked against a brute-force reference over 576 texels. Max error: 0. Exact, not chamfer-approximate. Grade: MEASURED.

Then the measurement found what the SDF could not fix. An SDF makes the stroke resolution-independent; it cannot make a sub-pixel glyph legible. The answer is not more bloom, it is bigger glyphs. World size now scales so apparent size in screen pixels stays constant.

Brightness after SDF + scale (MEASURED on RTX 3060)
Stage height320460520620720900
Mean brightness120117115113112113

Spread: 28 down to 8. Grade: MEASURED.

Section 04

X-ray, and the Broken Instrument

Answer capsule Three.js resets renderer.info at the start of every render() call. This engine makes seven per frame: scene plus six post passes, plus simulation steps. The "6,000 glyphs in one draw call" figure was measuring the last post pass. The frame costs 30 draw calls. The flattering number came from a broken instrument. Grade: MEASURED.

True numbers with autoReset off and one reset per frame: 30 draw calls, 12,025 triangles, 15.4 ms (~65 fps) on an RTX 3060. Grade: MEASURED.

X-ray mode is a chip next to Reset View on the live artifact. Twelve live rows, every one read from the running renderer: tier, glyph count, true draw calls and triangles per frame, smoothed frame time, pixel ratio against the device cap, canvas size, simulation texture size, atlas type and build time, source characters, explode. Nothing hardcoded. You can operate it yourself.

__dvaCap['dva-cap-manifesto'].info()

Section 05

Source Citation, and the Cap That Was Made of Its Own Opening

Answer capsule Every retained character carries its line number. Reading order groups by part, so each component maps to a contiguous span of the file. First reading: all four components spanned lines 2 to 146 of a 1,935-line file. Walking the source straight through meant 6,000 glyphs only ever reached the first 6,000 characters. The cap was made of its own opening, not its own source. Grade: MEASURED.

Striding one character at a time would cover the file and destroy readability. Fix: contiguous 24-character blocks spread evenly. Verified numerically across four configurations: 99.0 to 99.7 percent coverage, zero index collisions, every within-block step +1 so runs stay readable. Spans now run 2 to 1,937.

Click a component on the live artifact and the spec panel now ends with "Drawn from lines 2 to 808 of this section's own source, out of 1,958." The self-reference stops being a claim and becomes a citation.

Section 06

Operating the UI Twice, and the Listener Leak

Answer capsule The Force fallback chip dropped the tier correctly on the first click and would not restore on the second. The control chips live on elements that outlive the engine. Every re-mount bound another copy. Two handlers meant one click set the flag and the next cleared it. Fixed with one AbortController per engine instance. Grade: MEASURED.

Not a toggle bug: this section re-mounts every time a tab reopens, so handlers were accumulating for the life of the page. All 15 addEventListener calls carry the AbortController's signal, release() aborts it.

Verified across four full tier cycles: gpgpu-webgl2 → vertex-lerp → gpgpu-webgl2 → vertex-lerp → gpgpu-webgl2.

The lesson

A feature you never operate twice is a feature you have not tested.

Try it yourself: open the Manifesto tab on the live Academy hub, click Force fallback, wait for the tier to drop to vertex-lerp, click again, and confirm it restores to gpgpu-webgl2.

Section 07

The Silent-Failure Table

Answer capsule Six failures that threw nothing, returned success, and cost days. Every entry was hit during this build. These are the ones worth collecting because nothing points you toward the cause. Grade: MEASURED.
FailureSymptomWhy nothing errored
Fullscreen triangle frustum-culledEvery post pass renders nothingThree culls it correctly; the pass "succeeds"
Writing to an unpublished themeHTTP 200, self-verifies green, visitors see nothingThe write genuinely succeeded, on the wrong theme
Hidden tabrAF and IntersectionObserver never fireThe tab is asleep, not broken
renderer.info autoResetDraw calls under-reported ~30xEach render() legitimately resets its own counters
Sibling section claiming a tab keyElement never togglesThe controller binds only to the panel it owns
Listener accumulation on re-mountA toggle that will not toggleEvery individual handler works perfectly

Section 08

Colour and Blending

Answer capsule sRGB has a linear toe below 0.0031308. pow(1/2.2) does not. Using the power curve as an sRGB encode lifted the #04090c background to rgb(12, 17, 20), a visibly lighter rectangle against the page. The fix uses the real transfer function: linear segment below 0.0031308, 1.055 * x^(1/2.4) - 0.055 above. Grade: MEASURED, sampled at three corners.

Additive blending is right for a particle field and wrong for text. I bracketed it from both ends:

SettingResult
Additive + 0.28 bloom thresholdGlows beautifully, every letterform dissolves
Normal blending + 0.62 thresholdPerfectly legible, flat and dim, brand gone
Normal + 0.62 + raised glyph gainLegible stroke with a halo. Shipped.

Additive gave halo without stroke. Flat gave stroke without halo. The trick is making only the highlights cross the knee. Grade: MEASURED.

Section 09

The Tier Ladder

Answer capsule Three rungs chosen at runtime, each proving itself before adoption. The top tier (gpgpu-webgl2) integrates positions as damped springs in float textures. The floor (vertex-lerp) runs a fixed easing curve in the vertex shader. Below that: static SVG, no WebGL at all. A ladder down beats a top rung nobody can reach.
TierTechniqueReach
gpgpu-webgl2 (shipped)Positions integrated as damped springs in ping-ponged float textures; never touch the CPUWebGL2 + EXT_color_buffer_float: Safari, mobile, hardware back to ~2017
vertex-lerp (floor)Fixed easing curve in the vertex shaderAnywhere WebGL runs
Static SVG blueprintNo WebGL at allEverywhere

Why not WebGPU

Three's WebGPU path is a separate renderer with its own shader language, and GPU buffers cannot be shared with a WebGL context. A compute tier is a second parallel engine, not an add-on. WebGPU today reaches neither Safari nor most Android. Grade: ASSERTED (Three.js architecture), with navigator.gpu availability MEASURED as present on Chrome/Edge on this machine.

The tier proves itself

buildSim() gates on isWebGL2 and EXT_color_buffer_float, then verify() seeds, runs six steps, reads pixels back and requires finite values that actually moved. A driver that advertises float targets and then produces NaN is a real thing. Any failure disposes and falls through.

And the physics has to agree with the diagram. First spring tuning (stiffness 58, damping 9) settled over ~1 second while the wireframe and dimension rail moved on the eased value instantly. Physics that disagrees with the diagram is worse than no physics. Retuned to 160/20, near-critical. Grade: MEASURED (filmstrip).

Section 10

Shipping It on Shopify

Answer capsule No build step. No importmap (a second importmap throws). No bundler. three.module.js by full URL via dynamic import(). The bloom is hand-written because three/addons/ requires an importmap. To live inside a tab, it must be a snippet. Two instances, one page, each releasing on tab close. Grade: MEASURED.

Undocumented range schema limits that caused a 422 with no explanation until you read the response body, because PowerShell's Invoke-RestMethod throws a generic WebException and hides the real error:

  • max must be less than 10,000
  • step must have one decimal digit or fewer (0.05 rejected, 0.1 fine)
  • default must have one decimal digit or fewer

Grade: MEASURED (Shopify's own error body, verbatim).

Tab architecture

To live inside a tab, it must be a snippet. The tab controller toggles hidden on the panel element within each tab section. A probe element claiming the same key from outside was never toggled while the real panel toggled correctly. Sections cannot nest, so anything living in a tab has to be a snippet rendered from inside that tab's section file. Grade: MEASURED (live DOM experiment).

Bonus: a hidden tab panel is display:none, so an IntersectionObserver-gated section inside it never boots until the student opens that tab. Free lazy-loading.

Two instances, one page

Manifesto and Press Kit each hold one. Neither boots until its tab opens. When a panel closes, a 12-second grace timer fires and the engine releases everything: listeners, composer, geometries, materials, the atlas texture, the renderer. Then it explicitly loses the context via WEBGL_lose_context and swaps the canvas for a clean clone, because a force-lost canvas will not reliably grant a replacement. Grade: MEASURED (release, re-boot verified: is-live false, API entry deleted, canvas element replaced, then all restored).

Section 11

Student Takeaways

  1. Build the instrument before you trust the build. Every instrument in this class immediately found a defect the previous instruments had been silently passing.
  2. If it only fails silently, it will fail for a long time. The six failures in the table cost days collectively.
  3. Measure across the whole range, not at your desk resolution. The brightness spread of 28 to 8 was invisible at 900 px and catastrophic at 320 px.
  4. Operate every control twice. Re-mount handlers accumulate. A feature you never toggle back is a feature you have not tested.
  5. A ladder down beats a top rung nobody can reach. gpgpu-webgl2 reaches Safari and 2017 hardware. WebGPU reaches Chrome.
  6. Physics must agree with the diagram. A spring that settles in one second while the wireframe moves instantly is worse than no physics.
  7. Cite your sources, even when the source is yourself. The cap is built from its own source code. Before the fix, it was built from its own opening.
Bottom Line

You cannot engineer what you cannot observe. You can only hope. Every instrument in this class was built because the build looked fine and was not. The section shipped works. That is the boring half. The half worth teaching is that every single thing wrong with it was invisible to the checks already running, and each new instrument immediately found a defect the previous instruments had been silently passing.

FAQ

Frequently Asked Questions

What does the cap blueprint section actually render?

Six thousand instanced quads displaying live source code from the file that draws them. The source is read out of the DOM at runtime and mapped onto a 3D graduation cap. The entire section is one Liquid snippet of approximately 101,888 bytes with no build step, no bundler, and only three.module.js as a dependency. It runs in the Manifesto and Press Kit tabs of the DDS Vibe Academy hub page.

How many draw calls does the cap blueprint cost per frame?

Thirty draw calls per frame, measured on an RTX 3060 with renderer.info.autoReset disabled. The 6,000 instanced glyphs are one draw call. The remaining 29 come from the scene pass, six post-processing passes, and simulation steps. The misleading figure of one draw call came from Three.js resetting renderer.info at the start of every render() call, so the X-ray was reporting only the last post pass.

What is a filmstrip recorder and why was it needed?

A filmstrip recorder drives the simulation on a fixed timestep with no requestAnimationFrame, composites each frame into an offscreen render target, reads the pixels back deterministically, and tiles them into one image. It was needed because requestAnimationFrame and IntersectionObserver do not fire in hidden or backgrounded browser tabs. The section was unobservable from automation. The first filmstrip of 12 frames took 162 milliseconds and immediately caught six visual defects that no numerical check had detected.

What is a signed distance field atlas and why does it matter?

A signed distance field atlas stores the distance to the glyph edge instead of coverage. Distance interpolates and downsamples gracefully where coverage does not, so one 64-pixel cell stays crisp from a 6-pixel glyph on a phone to a 60-pixel glyph at close orbit. The exact Euclidean distance is computed via Felzenszwalb two-pass 1D transform at 128 pixels per glyph, box-downsampled to 64. Verified against a brute-force reference over 576 texels with max error zero.

Does the cap blueprint use WebGPU?

No. WebGPU was evaluated and deliberately not built. Three.js WebGPU path is a separate renderer with its own shader language, and GPU buffers cannot be shared with a WebGL context. A compute tier would be a second parallel engine, not an add-on. WebGPU today reaches neither Safari nor most Android. The shipped tier uses WebGL2 with EXT_color_buffer_float for GPGPU simulation.

Why is pow(1/2.2) not the same as sRGB encoding?

sRGB has a linear toe segment below 0.0031308 before transitioning to the power curve 1.055 times x to the power of 1/2.4 minus 0.055. Using the simple power function pow(1/2.2) lifted the background color from the intended hex 04090c to rgb(12, 17, 20), creating a visibly lighter rectangle against the page background. The fix uses the real sRGB transfer function with both segments.

What silent failures were discovered during the build?

Six silent failures that threw nothing. A fullscreen triangle was frustum-culled so every post pass rendered nothing. Writing to an unpublished theme returned HTTP 200 while visitors saw nothing. A hidden tab prevented rAF and IntersectionObserver from firing. renderer.info autoReset under-reported draw calls by approximately 30 times. A sibling section claiming a tab key was never toggled. Listener accumulation on re-mount caused a toggle that would not toggle. Every one returned success.

How does the tier ladder work?

Three rungs chosen at runtime. The top tier is gpgpu-webgl2, integrating positions as damped springs in ping-ponged float textures that never touch the CPU. It reaches WebGL2 plus EXT_color_buffer_float including Safari and mobile hardware back to approximately 2017. The floor tier is vertex-lerp, running a fixed easing curve in the vertex shader anywhere WebGL runs. Below that is a static SVG blueprint with no WebGL at all.

What is the listener accumulation bug?

The Force fallback chip dropped the tier correctly on the first click and would not restore on the second. The control chips live on elements that outlive the engine. The release function removed window and document listeners but left chip handlers attached, so every re-mount bound another copy. Two handlers meant one click set the flag and the next cleared it. Fixed with one AbortController per engine instance. All 15 addEventListener calls carry its signal and release aborts it.

What does this masterclass cost?

Nothing. This masterclass is free. No signup, no email, no paywall. It is part of the DDS Vibe Academy, a free AI coding curriculum built by Robert McCullock at Design Delight Studio. The live 3D artifact discussed in this class is running right now on the Academy hub page where you can operate it yourself.

DDS Vibe Academy

Build the instrument before you trust the build

This class is one of a free curriculum that maps the full territory from first prompt to sovereign stack. No signup. No paywall. Built in the spirit of the first internet.