OHAO · Codebase tree

Monograph · Tree · ← Sitemap · Status discipline

§

systems

Status discipline

Design unit

Evidence-based feature matrix protocol.

The promise a status file has to survive

Every engine has a document that says what works, and almost all of them are written by the person who wrote the feature, on the day they wrote it, from the diff. OHAO's `STATUS.md` names that failure mode and its countermeasure in its own header: the file is updated by rendering the examples and looking at the pixels, not by reading commit messages.

> Single source of truth for "what actually works." Updated by running theSTATUS.md:3

The way to test a promise like that is to look at what vocabulary the document has for failure. A status page that can only emit ✅ is not measuring anything. This one declares five states and spends four: ✅ on most rows, ⚠️ on two, 🗑️ on two, 🧪 on one.

Legend: ✅ works · ⚠️ works with caveats · ❌ broken · 🧪 experimental · 🗑️ removedSTATUS.md:20

Declared and used vocabulary diverge in both directions. ❌ never appears below the legend line — the strongest admission the document reserves for itself has never been cashed. And the turntable row emits a ✅ / ❓ pair, ❓ being an undeclared sixth state glossed only by the row's own note that the interactive half needs a display.

| **Turntable / interactive** | ✅ / ❓ | Turntable frames OK; interactive needs display |STATUS.md:35

The 🗑️ row against the OptiX denoiser reports a completed removal, not a plan.

| **Denoiser — OptiX** | 🗑️ | Removed; `optix` → OIDN fallback |STATUS.md:32

Every OptiX source file is gone from the tree; what survives is one CLI branch that prints a demotion notice and falls through to OIDN. That row's claim is literally the assertion in a unit test: `optix` and `OptiX` both parse to `DenoiseMode::OIDN`.

std::cerr << "[Denoise] --denoise=optix is no longer supported — falling back to OIDN\n";ohao/render/rt/denoise/denoise_types.cpp:24
TEST(DenoiseTypes, OptixRemovedFallsBackToOidn) {tests/renderer/denoise_parse_test.cpp:29

The subsurface-scattering row is sharper, because nothing external forced it: it marks OHAO's own SSS 🧪 and calls it biased look-dev rather than a BSSRDF.

| **SSS** | 🧪 | Biased look-dev hacks; not a true BSSRDF |STATUS.md:37

The limitations list below the matrix restates that as an instruction — keep SSS out of ground-truth claims — voluntarily disqualifying a shipped feature from the offline-reference standing the rest of the engine rests on.

2. **SSS** — biased; keep out of “offline ground truth” claims.STATUS.md:59

The gate that makes a status line cost something

Prose is free. The part of this system with teeth is a golden-image regression gate: a two-scene manifest, a Python harness, and a `pre-push` hook that renders both scenes on the local GPU and refuses the push if either drifts. The hook lives at that layer for a reason stated in its own header — hosted CI has no GPU, so the only machine that can produce a verification frame is the developer's.

# drifts beyond tolerance. Cloud CI cannot do this (no GPU), so the GPU safety.githooks/pre-push:7

The manifest pins both scenes at 16 spp with the denoiser explicitly disabled, and carries the contract that the corpus grows with every fixed bug:

"command": "./build/cornell_box {out} 16 --denoise=none",tests/golden/manifest.json:6
"_comment": "OHAO golden-image regression corpus (renovation Phase 0). Run from repo root. Tolerance compare absorbs the ~6px@1LSB GPU floating-point floor; any real regression shifts far more. Denoiser OFF (we pin raw deterministic beauty). Grow this list as features land — every fixed bug should add a scene so it can't silently regress.",tests/golden/manifest.json:2

Turning OIDN off is not a convenience. A denoiser is a large, version-dependent nonlinearity between the integrator and the PNG; leaving it in the loop would mean a golden failure could be an OIDN upgrade rather than a renderer regression, and the gate would train the team to ignore it.

Why the comparison is a tolerance, not a hash

The obvious golden test is `sha256(render) == sha256(golden)`. A second place in the repository reached for that invariant and dropped it: an NRD verification log wanted a bit-exactness check, ran the same binary three times, got three different hashes, and tracked the invariant by file size plus visual equivalence instead.

- Beauty PNG (binding 2, `out.png`) structurally identical to pre-4.D baseline; file-size delta < 0.02 %. Bit-for-bit PNG hash is not stable across runs in this codebase (pre-existing non-determinism at spp=1 — confirmed by three back-to-back runs of the same binary producing three different hashes), so the invariant is tracked via file-size + visual equivalence rather than sha256.tests/reference_scenes/custom/envlit_turntable/verification_log.md:482

That is corroboration, not the goldens' own evidence, and the regimes differ: those three runs were `env_demo` at **1 spp** in the NRD remodulation work, not 16-spp offline path traces of the golden scenes.

./build/env_demo assets/realistic_female.glb assets/test_models/env_studio.hdr /tmp/beauty_4d.png 1 \tests/reference_scenes/custom/envlit_turntable/verification_log.md:469

The harness header attributes the residual to non-associative floating-point reduction order on the GPU, sizes it at about six pixels at one LSB on a 1080p frame, and calls that floor irreducible.

from non-associative reduction order). Bit-exact hashing would flag that ghost;tests/golden/render_golden.py:10
Why

Bit-exactness was rejected against a stated floor rather than for convenience: naming the floor's size is what makes the tolerance a number someone can argue with. What replaced it is a two-sided tolerance compare preceded by a bilinear downscale to 640 px, which averages the per-pixel FP ghost away and keeps the committed goldens at 640×360 instead of shipping two 1080p PNGs. {{cite tests/golden/render_golden.py "DOWNSCALE_WIDTH = 640"}}

Two conditions, because either one alone is blind

A scene passes only when both bounds hold. Let $A$ and $G$ be candidate and golden after the downscale, indexed by pixel $p \in \{1..N\}$ and channel $c \in \{R,G,B\}$, with $N$ the downscaled pixel count, and let $\tau$, $\phi$ be that scene's two tolerances:

$$ \max_{p,c}\bigl|A_{pc}-G_{pc}\bigr| \le \tau \quad\wedge\quad \frac{1}{N}\Bigl|\{\,p \;:\; \exists c,\; A_{pc} \neq G_{pc}\,\}\Bigr| \le \phi $$
def verdict(s, max_abs_diff, max_diff_frac):tests/golden/render_golden.py:72

$\tau$ and $\phi$ are per-scene manifest fields, not constants of the gate: the harness reads `max_abs_diff` and `max_diff_frac` off each scene entry, falling back to `DEFAULT_MAX_ABS_DIFF = 4` / `DEFAULT_MAX_DIFF_FRAC = 0.01` only where a scene omits them. Both committed scenes set them explicitly, to exactly those defaults — so a scene added later with looser numbers would be held to a looser contract than the one written here.

mad = sc.get("max_abs_diff", DEFAULT_MAX_ABS_DIFF)tests/golden/render_golden.py:89

The first is an $L^\infty$ bound: it catches a small region that moved a lot — a broken material lookup, a flipped normal, a dropped light. The second bounds the *support* of the difference: it catches a whole-image shift of a single LSB — a tonemap constant nudged, a different RNG stream — which the max test can never see, because 1 ≤ 4. Each closes the other's blind spot. Both committed goldens are 640×360, so $N = 230{,}400$ and today's $\phi = 10^{-2}$ tolerates roughly 2,300 differing pixels and no more.

The harness also ships a `--selftest` mode that renders each scene twice and compares the two renders to each other instead of to the golden. That is not redundant: it measures the gate's own noise floor, which is the only way to tell a tolerance that is too tight from a renderer that genuinely regressed.

# render a second time, compare the two renders to each othertests/golden/render_golden.py:111

Where the net is soft

  • The hook is opt-in per clone — it runs only after someone types

`git config core.hooksPath .githooks`. A fresh clone has no gate at all.

# Install once (per clone): git config core.hooksPath .githooks.githooks/pre-push:4
  • It fails open. If `build/cornell_box` is missing or not executable (a docs-only

checkout, or a build that just broke) the hook prints a notice and exits 0, so an unverified push is indistinguishable from a verified one.

echo "[pre-push] build/cornell_box not found — skipping golden checks.".githooks/pre-push:14
  • The corpus is still the two scenes it started with, against a manifest asking

for one per fixed bug. The NRD YCoCg-packing bug and the deferred black-metal bug are both written up in `STATUS.md`; neither has a golden scene, so both can return silently.

- **Root cause:** REBLUR expects **YCoCg + normalized hit-distance** (`NRD.hlsli` Pack/Unpack). We wrote linear RGB + raw world hit-dist.STATUS.md:48
### Deferred metals pure black (partial)STATUS.md:51
  • The gtest suites under `tests/` are not part of the gate. Most of them build by

default, but nothing invokes them unprompted: the hook's only command is the golden harness, and the repository's single GitHub workflow publishes this site. They defend code — `parseDenoiseMode`, the environment CDF, the Sobol sequence — only when a human runs the binaries.

option(BUILD_RENDERER_TESTS "Build renderer pipeline tests" ON)CMakeLists.txt:111
python3 tests/golden/render_golden.py tests/golden/manifest.json.githooks/pre-push:20
  • `STATUS.md` lists the missing GPU-less cloud workflow as an open limitation

rather than passing the pre-push hook off as CI.

3. **Cloud CI** — still no GPU-less build/unit workflow (Phase 0 leftover).STATUS.md:60

Two bug writeups, and what a refactor did to their pointers

`docs/bugs_solved/` is the older half of the discipline: numbered writeups with symptom, root cause, fix, files touched, verification. Two entries exist, and both describe bugs whose *fixes are still load-bearing* and whose *file paths are all wrong*. Bug 001 places its fix in `src/renderer/frame/frame_resources.hpp`:

- `src/renderer/frame/frame_resources.hpp` - Added `resizeStagingBuffers()` declarationdocs/bugs_solved/001_staging_buffer_resize.md:45

There is no `src/` in this tree. The renovation moved everything under `ohao/`, and `resizeStagingBuffers` — the method that stops per-frame staging buffers from staying 64×64 after a resize to 1080p, which is what produced the original viewport flicker — is alive at a different address entirely.

bool FrameResourceManager::resizeStagingBuffers(size_t newSize) {ohao/render/frame/frame_resources.cpp:269

Bug 002 fared the same way. Its conclusion was that every pass touching shared uniform data must bind the *same* per-frame descriptor set, because mixing a per-frame set with the legacy one renders the shadow map with light matrix A and samples it with matrix B — the reported symptom was shadows rotated 90°. The parameterised signature that enforces it survives, still called from both the per-frame and the legacy path:

renderShadowPass(cmd, frame.descriptorSet);ohao/gpu/vulkan/render_dispatch.cpp:281

So the archive's invariants outlived the refactor and its pointers did not. That is the general shape of rot here, and it is not confined to the bug log: `CLAUDE.md` still documents a Skeletal Animation module with a file map, while `STATUS.md` marks skeletal animation 🗑️ and `ohao/animation/` does not exist.

## Skeletal Animation (Module D)CLAUDE.md:143
Key idea

`STATUS.md` is trustworthy in proportion to the gate behind it, not in proportion to its own confidence. Two of its fourteen matrix rows are covered by the golden gate — the repository's only check wired to run unbidden, and then only in clones that installed the hook. One more, the removed OptiX backend, is asserted by a unit test nothing runs automatically. Of the eleven rows with no gate at all, six still read as a plain ✅. And the file does not say when a human last looked: its freshness marker is a list of milestones, not a date. {{cite STATUS.md "**Last verified:** after C++20"}} OHAO has a determinism contract on two frames and a disclosure habit everywhere else — and the disclosure habit is worth more than it sounds, precisely because it names what it cannot verify.

Contracts

  • Regenerating with `--update` overwrites the golden without comparing first.

Nothing distinguishes an intentional refresh from laundering a regression; that judgement is entirely on the human running it.

  • The compare is aspect-normalised, not resolution-normalised: the downscale runs

on both sides, fixes width at 640 and derives height, so 4K, 1080p and 720p all land on the same 640×360 grid. A regression confined to sub-640 detail — fine specular aliasing, a one-texel UV shift — is averaged away, and a change to `model_viewer`'s default resolution trips the size check only if it changes the aspect ratio or falls below 640 px wide. That default is pinned at 1080p by a VRAM constraint recorded at the decision point.

// 1080p default: dual PathTracer (realtime+offline) at 4K OOMs on 8 GiB cardsexamples/model_viewer.cpp:66
  • Manifest commands are not shell strings. They are `shlex`-tokenised into an

argv vector and spawned with no shell, so a pipe, glob, redirect or `$VAR` written into a manifest command reaches the binary as a literal argument.

r = subprocess.run(shlex.split(cmd), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)tests/golden/render_golden.py:47
  • Nor does the harness ever `chdir`: the manifest's relative `./build/cornell_box` and

`tests/golden/*.png` resolve against the caller's cwd, and repo-root comes from the hook `cd`-ing to the worktree top first. Invoke the harness by hand from anywhere else and the missing executable escapes `render()` as an uncaught `FileNotFoundError`, not a scene FAIL.

cd "$(git rev-parse --show-toplevel)".githooks/pre-push:10
  • No `STATUS.md` claim outside the two golden scenes is guarded by anything that

runs unprompted. Read ✅ on those rows as exactly what the header promises — someone ran the example and looked at the pixels — and note that it promises nothing about when.

Source files

STATUS.md
ohao/render/rt/denoise/denoise_types.cpp
tests/renderer/denoise_parse_test.cpp
.githooks/pre-push
tests/golden/manifest.json
tests/reference_scenes/custom/envlit_turntable/verification_log.md
tests/golden/render_golden.py
CMakeLists.txt
docs/bugs_solved/001_staging_buffer_resize.md
ohao/render/frame/frame_resources.cpp
ohao/gpu/vulkan/render_dispatch.cpp
CLAUDE.md
examples/model_viewer.cpp
docs/bugs_solved
Navigate

Parent hub for the full pipeline narrative; this page is the file-level design unit. Sitemap · hover glossary terms anywhere.