reikhelm/.dev/2026-06-15-diffusion-pipeline-reliability.md
Parley Hatch a86b52b4c5 docs(pipeline): iteration-4 QA sweep + strength-0.70 finding
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:17:55 -06:00

8.1 KiB
Raw Blame History

Diffusion pipeline reliability — making the depth ControlNet actually followed

Started: 2026-06-15 · ongoing (/loop) Problem (user): "controlnet maps generated are barely followed; images of the dungeons are very hit and miss."

Root cause found (iteration 1)

The FPV depth maps fed to the Z-Image Fun depth ControlNet used only a sliver of the tonal range. Histogram over all 68 baked seed-7 frames:

  • median dynamic range 193 / 255, worst frame only 54
  • 0/68 frames reached true black (<32); the deepest recesses topped out ~mid-gray
  • only 35/68 reached near-white at the near surfaces

Cause in reikhelm-web/fpv.js: a fixed t = dpix / maxView with maxView = 12. The nearest floor sits ~1 cell away (never hit white) and a wall 3 cells ahead landed at mid-gray (never black), so small rooms collapsed into the 160214 band. Depth ControlNets are trained on full-range MiDaS / depth-anything maps, so a washed-out map gives almost no structural signal → the model freelances.

Fix shipped — commit 018909b

Two-pass raycast in computeFPVDepth:

  • Pass 1 records each column's wall distance + the frame's true near/far.
  • Pass 2 maps depth through a per-frame window: near → white, deepest visible recess → black, every frame.
  • Far plane tracks the deepest real surface but is clamped to minSpan (5 cells) so a shallow dead-end nook is NOT stretched into a fake tunnel — its faced wall stays an honest mid-gray slab (no black recess → no phantom-door bait).
  • gamma 2.0 → 1.5 (normalization now owns the range). nearPlane/minSpan are opts.

Verified offline (headless bakeAtlasDepths(7), 68 frames): median range 193 → 255; near-white frames 35/68 → 68/68; worst range 54 → 106. Visual before/after (/tmp/ba.png during the session): open frames now frame a black passage; closed frames a clean mid-gray wall.

Next — needs the render workstation (192.168.1.26:8188, was OFFLINE this iteration)

The depth signal is now strong, so the old strength is almost certainly wrong. With a weak signal you crank strength to force adherence; a strong signal wants less. bake_atlas.py defaults --strength 0.85.

  1. Re-probe the workstation: python3 tools/comfy-spike/comfy.py probe http://192.168.1.26:8188 (2026-06-15 iter 2: host pings but port 8188 closed — ComfyUI not running; needs a manual start on the workstation before any of the below.)
  2. A/B + strength sweep in one run (tool written & validated; old rev-2 maps persisted to tools/out/compare/rev2_depths/ so the fix is provable, not just "looks better"):
    python3 tools/comfy-spike/sweep_strength.py http://192.168.1.26:8188 \
        --seed-dir tools/out/atlas/7 --alt-work tools/out/compare/rev2_depths \
        --open r8_N --closed r0_N --strengths 0.55,0.7,0.85,1.0
    
    tools/out/compare/sweep_<run>.png: each frame gets a NEW row (full-range maps) and an OLD row (washed-out rev-2) across every strength. Read adherence: does the rendered passage land on the black recess? does the closed wall stay a wall (no phantom door)? Expect NEW ≫ OLD; pick the lowest strength that still honors geometry, update bake_atlas.py --strength 0.85 default. (Drop --alt-work once the win is confirmed.)
  3. Then re-bake a full atlas at the chosen strength and spot-check a montage vs the old acc8137/a12bcbb bakes.

Iteration 3 — first GPU pass (2026-06-15/16, workstation back up)

Ran the A/B + strength sweep (16 renders) and follow-ups. Findings:

  1. Open frames: the rev-3 normalization works. r8_N renders a clean pillared hall with the passage landing exactly on the depth's black recess. NEW ≈ OLD here only because r8_N already had structure; the win shows on small-room recesses.
  2. Blown-highlight regression (introduced by rev-3, now FIXED). Mapping the nearest pixel to pure white (nearPlane = nearRaw) made the ControlNet render overexposed near floors/ceilings at strength 0.85. Fix: nearPlane = 0 (anchor white at the camera, nearest real surface ≈ gray 214) + minSpan = 8. Re-baked + re-rendered: blown ceiling gone, geometry adherence intact, recesses still black.
  3. Phantom doors on closed walls = a PROMPT problem, not depth. Present in OLD maps too. Root cause: opening-NOUNS in the positive prompt. threshold's body ("dungeon gatehouse, iron portcullis, entrance") painted a gate onto a solid wall; at cfg 1.0 the negative can't cancel it. A generic opening-free body ("rough-hewn stone chamber, bare granite walls") renders a clean dead-end. "Bricked-up archway" BACKFIRES — "archway" alone re-summons the arch. → Fix in bake_atlas.py: THEME_BODY_CLOSED (theme flavor, zero opening nouns) for closed facings + opening-nouns added to the negative on closed facings only.

Tooling fixes: sweep_strength.py re-exec-under-venv guard (the venv's python is a symlink to the base interpreter, so the realpath check wrongly skipped the hop — now an env-sentinel guard); montage now succeeds in one invocation.

Iteration 3 end-to-end result — VALIDATED

Full seed-7 re-bake (67/68; one transient ComfyUI miss, re-rendered) through rev-3 maps + closed-body prompts. Old-vs-new montage (tools/out/compare/):

  • r0_N (threshold, closed): OLD painted a grand gateway + portcullis on a solid wall; NEW renders a clean ashlar wall with torches. Phantom door gone.
  • r16_E (throne, closed): clean wall with banners/columns — theme flavor survives with zero opening nouns. Generalizes past threshold.
  • r16_N (throne, open) / r8_N (hall, open): rich themed scenes, passage exactly on the depth recess. Forge lava / crypt sarcophagi / library shelves / den all good.
  • Strength 0.85 kept — honors geometry without stiffness on the high-contrast maps.

Campaign upgrade (in progress)

DESCENT loads these atlases directly (game.js setFrame /out/atlas/<seed>/r<room>_<dir>.png). Campaign = seeds 7, 69, 59 (game-web/game.js CAMPAIGN), all previously baked at the broken rev-2 (a12bcbb). Seed 7 done. Re-baking 69 + 59 through rev-3 (depths re-exported headless, old frames backed up to compare/old_bake_<seed>/).

Iteration 4 — QA sweep + strength tune (2026-06-16)

Systematic QA of all 208 baked campaign frames (not just spot-checks):

  • Closed walls (83): zero phantom doors. Ranked every closed frame by center-vs-flanking-wall darkness; even the worst (r6_W forge, r11_E crypt, r0_W den) are genuine solid walls — the dark center is lighting falloff / a real stone mass, not a recess. The closed-body prompt fix holds campaign-wide.
  • Open frames (125): median 0.18%% blown. Low-contrast flags were false positives (atmospheric dark throne halls). One real pattern: deep-corridor frames — mostly crypt — blew out a white ceiling band at the top edge (r9_N 27.7%%, r13_N 21%%, r11_N 18%%).
  • Root cause + fix: the depth's near-ceiling top edge is gray ~209 (bright); at strength 0.85 the ControlNet forced it to overexposed white, amplified by crypt's "pale light". GPU A/B on r9_N: strength 0.70 → 0.0%% blown (vs 27.7%% at 0.85) with geometry intact; de-paling the prompt did nothing (cfg-1 negatives inert). Verified r0_N closed wall stays solid at 0.70 → one global strength is safe. Set bake_atlas.py --strength 0.85 → 0.70 (commit b661989). Campaign re-baked at 0.70; 0.85 frames archived under tools/out/compare/str85_<seed>/.

Optional later

  • The single-frame retry-on-empty in bake_atlas would avoid the occasional ComfyUI miss.

Open levers if still hit-and-miss after strength tuning

  • nearPlane/minSpan/gamma are now tunable per-export — A/B if near surfaces read too blown-out (the AFTER maps are ~47% near-white; may want a slightly deeper nearPlane to retain near-field texture).
  • Subtle masonry-block relief on closed-wall slices so flat walls render as detailed stone (offline-buildable, but verify on GPU it doesn't read as structure).
  • Strength start_percent/end_percent scheduling (currently 0→1 full) — easing control off in late steps frees texture while locking composition.