The Scene Is the Forcing Function

The bug report was one character long.

Midway through my latest RISE project, one of my agents documented a small quality-of-life feature: put a spectral absorption curve in a plain text file, reference it from the scene, and the renderer carries it per wavelength through the entire light transport. The data file began, as human-authored data files do, with a # comment describing its contents. I loaded the scene, and RISE hung forever.

The loader was the textbook mistake, a while (!feof(f)) wrapped around an unchecked fscanf. On a comment line the fscanf matches nothing and doesn’t advance the stream, so the loop spins on the same line indefinitely. Every render the agent had produced along the way used a curve inlined directly in the scene text; the file path, the one its own documentation advertised, had never been exercised even once. The documentation was writing a check the code couldn’t cash, and it took a human reader one pass to find out.

A thirty-minute bug, on its face. What it turned into is the subject of this essay.

The rule

Some context first. The project was to reproduce a vitreous enamel watch dial from physical first principles: a deep red anOrdain Model 1, its enamel a few hundred microns of colored glass fused over a stamped silver dome, its color a genuinely spectral absorption curve, and its signature darkening toward the rim (the fumé gradient) nothing more exotic than Beer-Lambert absorption through glass that pools thicker where the dome falls away. If you read the Ming Lightning post, this is the same game with different physics, run through the same loop of coding agents, adversarial reviewers, and me as the controller.

What was different this time was a rule, written at the top of the design journal on day one: improve RISE along the way, and never cut corners or accept a limitation for the sake of rendering the scene. The scene is the forcing function, not the deliverable. My last essay mentioned this discipline in a single clause; this is the long version, from inside one project.

The rule sounds like it’s about ambition. In practice it’s about what you are not allowed to do when something breaks.

Nine loaders, then sixteen getters

Back to the hanging curve file. The expedient fix is obvious: patch that one loader, reload the scene, get on with the render. The rule says no; a bug with this shape rarely lives alone. So the agent working the parser grepped for the shape itself (a feof loop around an unchecked read) and turned up nine loaders carrying it, spread across every file-driven form the scene language has. One shared comment-tolerant line reader replaced all nine, and as a small bonus, inline trailing comments now work everywhere.

That closed the text loaders, and the agent filed itself a follow-up about two binary loaders that ignored their read counts; one had no error branch at all for a missing file and simply handed back uninitialized memory. This is where the story stops being about file parsing. A second agent, reviewing the first, reframed the pattern from “unchecked fread” to “unchecked bulk binary read into a caller’s buffer,” and one hop out sits getBytes, the read primitive underneath every image and mesh loader in the engine. Its bounds check existed only inside #ifdef _DEBUG. In a release build, a truncated asset whose header promises more bytes than the file holds reads clean off the end of the heap allocation: an out-of-bounds heap read, guarded in every developer build and compiled out of every build anyone would actually run. The one configuration where the check mattered was the one where it didn’t exist.

The first agent fixed it and wrote in the commit message that the binary-read family was now genuinely complete. Another reviewer called that claim, ran the grep it should have run itself, and proved it wrong: the real pattern was “bounds check hidden behind #ifdef _DEBUG,” and all sixteen scalar getters in both buffer classes carried it, across 242 call sites. Fixing those, the agent caught a defect in its own patch, a guard that would have traded the out-of-bounds read for an infinite loop on the same malformed input, and found it only by narrating the behavior to itself, line by line, in its journal.

Three humbling things came out of that one thread, and for once none of the humility was mine, since my entire contribution was the #. The “family complete” claim was a guess dressed as a fact, and it took another agent’s grep to expose it. The fix for a correctness bug shipped with a bug of its own. And a security-relevant defect had lived for years in exactly the place nobody looks: the build you ship, rather than the build you test. The agents made precisely the mistakes I would have made, at considerably higher speed, and the loop caught every one.

None of this is the enamel. All of it exists because a watch dial wanted to read a curve from a file, and a rule said the agents weren’t allowed to stop at the symptom.

The bugs that don’t crash

The curve-file thread is the flashiest thing the rule produced, but not the most important. That distinction belongs to two quieter discoveries from earlier in the project, both of a type I’ve come to fear more than any crash.

The first surfaced during initial research, before a line of new code was written. RISE’s homogeneous medium, the thing that would become the enamel body, collapsed its coefficients to a single luminance value in spectral mode. A gold-ruby red absorber rendered as gray fog under the very spectral pipeline the project existed to use. A sibling audit found the same collapse in three separate places in the heterogeneous medium, a much newer addition. The second discovery was worse: the volume estimator’s no-scatter branch multiplied the full transmittance on top of the stochastic survival probability, counting Beer-Lambert twice. Every absorbing medium RISE had ever rendered was roughly twice too thick, exp(-2σd) where the physics says exp(-σd), in RGB and spectral alike, for as long as the volume system had existed, with no regression test standing guard.

What makes these dangerous is that nothing fails. No hang, no crash, no black frame; the image is simply a little too dark, and a renderer is an instrument with a thousand knobs. You can calibrate a factor of two away without ever knowing it was there, match your reference, publish the picture, and bake the lie in permanently. The only defense I know of is an external oracle. Here that meant an analytic Beer-Lambert slab computed from scratch: the formula was ground truth, and the renderer was the thing on trial. That’s a strange inversion to hold in your head about a codebase you first wrote twenty-five years ago, and exactly the right one.

The payoff, when it came, was almost comically legible. Once the loop had rebuilt the medium to carry genuine per-wavelength coefficients, we authored a curve that absorbs green and blue, bound it to a clear slab, and rendered a white environment through it. Red came out the other side, twenty-two times more red than blue. Swap in a flat curve: gray. Remove the curve entirely: gray, the old collapse. That three-way contrast, authored purely through scene text, was the proof that the whole stack finally carried wavelength. A year of “the medium renders gray in spectral mode” ended with a red slab, and later the same day with a slab of colloidal-gold ruby over silver: a clean, deep cranberry produced by absorption physics rather than a tint.

The sparkle

Which brings me to the strangest arc of the project. The anOrdain’s most distinctive trait in the reference footage isn’t the color; it’s a crystalline, orange-peel sparkle across the whole face, hundreds of tiny white glints shifting as the watch tilts. I had a committed design for it: a new material, a conductor with a discrete glint lobe, in the lineage of the sparkle models used for snow and car paint. Before writing any code I ran the spec through a three-reviewer adversarial design gate with a single question attached: is this even the most physically principled approach? The spec died twice in one afternoon.

The first death was mechanical. The enamel scene bans transparent-shadow approximations (a straight-through shadow is precisely the kind of corner the rule forbids on any render you keep), which means direct-light sampling is occluded at surfaces buried under the glass. Glints down there can only be lit through BSDF sampling, and the design as written didn’t importance-sample its glint lobe. The feature, as specced, would have rendered no buried flecks at all; a null result, approved by committee. The remedy wasn’t better sampling but a better vehicle: not a new material, a small normal-perturbation modifier that composes with the existing, already-correct materials and inherits their sampling machinery for free.

The second death was empirical, and came from measurement rather than argument. Zoomed crops of the reference footage show the flecks neutral white and sharply angle-gated. Glints originating at the silver substrate would exit maroon, having passed twice through the gold-ruby glass on the way out. Neutral means the sparkle happens at the top surface, before light ever reaches the colorant. The design had been aimed at the wrong layer of the physics entirely.

So the feature pivoted twice before implementation, then went through the full grind: four slices, seven review rounds, eighteen-odd revert-proven mutations, three defects found by fresh reviewers, each one a class of error I could not have caught alone. A test that couldn’t fail against the very mutation it was written to catch. A claim I transcribed from one reviewer without re-verifying, which turned out to be inverted. A NaN-handling fix that the compiler silently deleted under -ffast-math, a flag that deserves and will eventually get its own essay. At the end of it the modifier worked, provably: on a plain dark metal plane, discrete facets flash and turn over as the light moves, each pinned to a fixed spot on the surface, unmistakable.

Then I put it on the watch, rendered the with/without comparison, and could not tell the difference.

For an hour that read as failure. An absurd-parameter render (full coverage, wide spread) proved the modifier was firing fine; the real explanation was arithmetic. At the density calibrated from the reference, the facet layer adds a few dozen flecks to a dial whose orange-peel relief already carries about nine hundred bright pixels of shimmer. The system was performing exactly to spec. My expectations were the miscalibrated component.

Which finally forced the question I should have asked on day one: what is the most physically accurate description of what this object actually does? Grand feu enamel is fired glass, a continuous, fire-polished surface; its twinkle is that surface’s own Fresnel glints catching a light as the dial tilts. And the dial already had that surface: a real micro-displaced heightfield whose cells measure out to roughly 380 microns, the same scale as the flecks measured from the footage. The fleck-scale structure had been modeled all along, correctly, as continuous geometry. Discrete, independently oriented facets are the physics of genuinely discrete scatterers, metal-flake paint, glitter, aventurine glass; the wrong material class for fired glass, and redundant on this dial besides. A final check confirmed the dimples alone twinkle just fine: 88 percent of the bright pixels turn over across a ten-degree tilt. I took the modifier off the watch.

So the feature I spent the session building is not used by the scene that motivated it, and that is the correct outcome rather than a failure. RISE keeps a general, proven glint capability with its own showcase scene (five spheres: a smooth control, fine flakes, coarse flakes, car paint, glitter) for the materials that genuinely are discrete. The dial keeps the model that matches its physics. One investigation produced both answers, and keeping a physically wrong model on the hero render because I had paid for it would have been its own corner, cut from the other side.

That was the part of the rule that took me longest to understand. “Never cut corners” reads as a commitment to doing more, and the loader saga fits that reading: don’t stop early, follow the pattern past the symptom until the family is closed. The glint saga is the same rule facing the other way: don’t stop late, and don’t let sunk cost keep something the physics has ruled against. The rule was never about effort in either direction. It’s about letting the physics decide where the work ends.

The ledger

“We’ve Run This Experiment Before” ended on the claim that every shortcut carries a price tag and every invoice a name. This project is the degenerate case that makes the mechanics visible: one person is both the operator of the amplifier and the payer of every invoice it prints. When the loop is that tight, the discipline stops being a virtue and becomes arithmetic. Skipping the loader audit would have saved an afternoon and left an out-of-bounds heap read for a future me to meet under worse circumstances. Keeping the glint modifier on the dial would have preserved a week’s pride at the cost of shipping a model I knew was wrong. Neither invoice disappears when declined; you only get to pick the delivery date, and the interest rate is not in your favor.

The tally for one watch dial: a spectral participating medium rebuilt end to end, a double-count as old as the volume estimator itself retired, nine hanging loaders replaced with one honest reader, an out-of-bounds read family closed across two buffer classes and 242 call sites, and a general glint feature the dial itself declined to use. The scene never cared about getBytes. Forcing functions are like that; they don’t know what they’re forcing until you refuse to let anything slide.

The render, for the record, is a nice picture of a watch. The renderer is the thing that changed.

Leave a comment