reikhelm/.dev/2026-06-15-diffusion-pipeline-reliability.md
Parley Hatch 3773ac2ad7 docs(pipeline): iteration-3 end-to-end validation + campaign re-bake notes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:24:33 -06:00

115 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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>/`).
### Optional later
- Lower strength A/B (0.7) for more natural texture if any frame reads stiff.
- 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.