fix #130 (the real strip): drawn-shell lift vs draw-space portal consumers

The user's re-gate refuted the scissor fix as THE strip (6c4b6d6 was a
real but sub-pixel under-coverage): the strip survived, screenshot at a
doorway, full width of the opening, top edge only, "very subtle".

Root cause (pinned by Issue130DoorwayStripTests.UnliftedGate_*): the
+0.02 m shell render lift. Cell shells DRAW 2 cm above the dat origin
(z-fight vs coplanar terrain); f35cb8b (the #119-residual fix,
2026-06-11) deliberately reverted the VISIBILITY graph to the physics
(unlifted) transform - but the OutsideView color gate (terrain/sky/
scissor through the doorway) and the seal/punch depth fans are
DRAW-space consumers and kept projecting the unlifted polygons. The
drawn lintel therefore sits one lift-projection above the gate's top
edge - measured 6.7 px at a 2.4 m doorway - and that band never
receives terrain/sky color while the seal also stamps 2 cm low.
A regression from f35cb8b, NOT from the W=0 clip port (987313a stays
exonerated). Vertical aperture edges are immune (the lift slides them
along themselves) - top edge only, exactly as reported; explains the
"also NOW" timing precisely.

Fix - draw space draws lifted, visibility stays physics (the f35cb8b
invariant, now symmetric):
- PortalVisibilityBuilder.Build gains drawLiftZ: the exit-portal branch
  projects the OutsideView region with the lifted transform; flood
  admission, side tests, and CellViews are untouched (default 0 keeps
  every existing visibility test bit-identical).
- The seal/punch fans (DrawRetailPViewPortalDepthWrite) lift their
  world verts to the drawn shell's space.
- One shared constant PortalVisibilityBuilder.ShellDrawLiftZ feeds the
  shell registration (GameWindow:5604), the gate, and the fans.

Register: AP-32 ADDED - the +0.02 lift had NO row (a pre-register
deviation the 2026-06-12 sweep missed). The row records the split
invariant both ways: a draw-space consumer that forgets the lift
re-opens the #130 strip; a visibility consumer that picks the lifted
transform re-opens the #119-residual side-cull.

Pins: the lifted gate covers the drawn (lifted) aperture to 0.00 px
across the 147-combo sweep; the unlifted gate shows the 6.7 px strip
(sensitivity proof - if the lift is ever removed, this test says the
drawLiftZ plumbing can go too).

Suites: App 257+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user re-gate at a doorway with the lintel on screen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-12 14:28:16 +02:00
parent 4ba714835d
commit 5135066733
6 changed files with 191 additions and 38 deletions

View file

@ -4514,45 +4514,50 @@ cap constant (0.5 m) is the tuning knob — see AD-18.
## #130 — Background-color strip along the TOP outer edge of a doorway when looking out from inside
**Status:** FIX SHIPPED — awaiting user visual gate
**Status:** FIX 2 SHIPPED — awaiting user visual re-gate
**Severity:** LOW-MEDIUM (small strip, but on the most-stared-at pixels in the game)
**Filed:** 2026-06-12 (user report, post-#119-close session)
**Component:** render — doorway-slice scissor box math (AD-17 family)
**Component:** render — drawn-shell lift vs draw-space portal consumers (AP-32)
**Symptom (user):** standing inside looking out through a doorway, a
thin strip of background (clear/world) color runs along the OUTER edge
of the TOP of the doorway opening.
of the TOP of the doorway opening. Survived the scissor fix (`6c4b6d6`)
— user screenshot 2026-06-12 evening, "very subtle".
**Root cause (pinned headlessly 2026-06-12, `Issue130DoorwayStripTests`
— 147 eye/gaze combos at the real A9B4 0x0170 exit door):** the
`BeginDoorwayScissor` NDC→pixel conversion (`Floor(origin) +
Ceiling(size)`) put the box's far edge at `floor(min)+ceil(maxmin)` —
up to ONE PIXEL SHORT of the true top/right edge at unlucky fractional
alignments. The scissor brackets the ENTIRE landscape slice (sky,
terrain, statics, weather), the seal stamps the full aperture at true
depth, and the shell ends at the aperture edge — so the cut pixel row
never receives color: a background strip along the top edge that comes
and goes as the eye moves (alignment shifts). Captured live by the
harness: top edge y=0.7938 at 1080p → row 968 cut; right edge column
1296 cut at 1920. This violated AD-17's own doctrine (over-inclusion
safe, under-inclusion is the bug class).
**Root cause (the REAL strip, pinned by
`Issue130DoorwayStripTests.UnliftedGate_LeavesTheStripAtTheDrawnTopEdge`):
the +0.02 m shell render lift.** Cell shells DRAW 2 cm above the dat
origin (z-fight vs terrain, AP-32); since `f35cb8b` (the #119-residual
fix) the visibility graph deliberately uses the PHYSICS (unlifted)
transform — but the OutsideView color gate and the seal fans, which are
DRAW-space consumers, kept the unlifted polygons. The drawn lintel
therefore sits one lift-projection ABOVE the gate's top edge —
**6.7 px at a 2.4 m doorway** (measured) — and that band gets no
terrain/sky color while the seal also stamps 2 cm low. Regression from
`f35cb8b` (2026-06-11), NOT from the W=0 clip port. Vertical edges are
immune (the lift slides them along themselves) — top edge only, exactly
as reported.
**Lead 1 REFUTED:** the W=0 clip port `987313a` is exonerated by the
same harness — the CPU polygon pipeline (ProjectToClip → ClipToRegion
merges → ClipPlaneSet planes) is sub-pixel exact against the raw
aperture projection (worst 0.54 px; 0.00 px in the aligned case). For
an all-in-front doorway polygon the port is bit-identical to the old
path by construction (the W clip pass only runs when a vertex has
w < 0).
**Fix 2:** draw-space consumers re-apply the lift —
`PortalVisibilityBuilder.Build(drawLiftZ:)` projects the exit-portal
OutsideView region with the lifted transform (flood admission, side
tests, CellViews stay physics-space per f35cb8b), and the seal/punch
fans lift their world verts. One shared constant
`PortalVisibilityBuilder.ShellDrawLiftZ` now feeds the shell
registration, the gate, and the fans. AP-32 register row added (the
lift had no row). Pins: the lifted gate covers the drawn aperture to
0.00 px across the 147-combo sweep; the unlifted gate shows the 6.7 px
strip (sensitivity).
**Fix:** conservative outer bound `floor(min)/ceil(max)` extracted to
`NdcScissorRect.ToPixels` (GL-free, unit-tested); `BeginDoorwayScissor`
delegates. Pins: `NdcScissorRectTests` (containment property + both
captured alignments) + `Issue130DoorwayStripTests` (scissor never cuts
plane-admitted fragments; CPU-pipeline exactness canary ≤1.2 px).
**Fix 1 (also real, sub-pixel): `6c4b6d6`** — the doorway-slice scissor
`Floor(origin)+Ceiling(size)` cut up to 1 px off the top/right edges;
now a conservative outer bound (`NdcScissorRect`, AD-17 doctrine).
The W=0 clip port `987313a` is exonerated (CPU pipeline sub-pixel exact
in like-for-like space).
**Gate:** stand inside any cottage, look out the door, sweep the gaze —
no background strip at the top edge at any alignment.
**Gate:** stand inside, look out the door with the lintel on screen,
sweep the gaze — no background strip at the top edge at any alignment
or distance.
---

View file

@ -92,7 +92,7 @@ accepted-divergence entries (#96, #49, #50).
---
## 3. Documented approximation (AP) — 31 rows
## 3. Documented approximation (AP) — 32 rows
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|---|---|---|---|---|---|
@ -127,6 +127,7 @@ accepted-divergence entries (#96, #49, #50).
| AP-29 | Target-indicator fallback for entities with no baked selection sphere: invented 1.5 m × scale box + 16/12 px screen floors (primary path is a faithful `GetObjectBoundingBox` port) | `src/AcDream.App/UI/TargetIndicatorPanel.cs:86` | Fallback only fires when the Setup didn't bake a selection sphere — rare in practice | Sphere-less entities get a non-retail indicator size/placement; the pixel floors prevent retail's far-distance collapse | `SmartBox::GetObjectBoundingBox` 0x00452e20; `GetSelectionSphere` |
| AP-30 | AutonomousPosition diff cadence compares with epsilons (1 mm pos, 1e-4 normal, 1 mm dist); retail's `Frame::is_equal` is an exact float compare | `src/AcDream.App/Input/PlayerMovementController.cs:1541` | Sub-millimeter epsilon is well below any movement worth suppressing; comparisons are against last-SENT state so drift accumulates past the epsilon | Sub-epsilon drift suppresses an AP send retail would have made — negligible today; a consumer expecting retail's exact send-on-any-change cadence sees fewer packets | `Frame::is_equal` pc:700263 |
| AP-31 | Scenery placement drift + the 0xA9B1 road-edge tree — WB-upstream divergences from retail, ACCEPTED (**#49/#50**, 2026-05-11) | `src/AcDream.Core/World/SceneryGenerator.cs` (via `WbSceneryAdapter`) | Piecemeal patching against WB upstream is net-negative (the `e279c46` road-check attempt over-suppressed scenery elsewhere, reverted `677a726`); visible impact = a handful of trees a few meters off | The same WB-upstream class could hide a *larger* placement divergence elsewhere; revisit only via a coherent ACME-style per-vertex filter port | `CLandBlock::get_land_scenes`; ACME GameScene.cs:1074 per-vertex road filter |
| AP-32 | Cell shells DRAW +0.02 m above the dat EnvCell origin (`ShellDrawLiftZ`, z-fight vs coplanar terrain); retail draws at the origin verbatim. Split invariant: PHYSICS + visibility graph UNLIFTED (f35cb8b, **#119**-residual), every DRAW-space consumer of portal/cell geometry LIFTED (OutsideView color gate via `Build(drawLiftZ)`, seal/punch fans — **#130**) | `src/AcDream.App/Rendering/GameWindow.cs:5604` (const at `PortalVisibilityBuilder.ShellDrawLiftZ`) | Shell floors coplanar with terrain z-fight in our z-buffered frame; the 2 cm lift is the documented stand-in | A new draw-space consumer of portal/cell polygons that forgets the lift re-opens a 2 cm seam at horizontal aperture edges (the #130 top-edge strip, ~7 px at 2.4 m); a visibility consumer that picks up the LIFTED transform re-opens the #119-residual horizontal-portal side-cull | retail draws cell geometry at the dat EnvCell origin (no lift) |
---