docs(render): S3 review closeout — AD-23 retired, AD-17 says deleted not dormant, probe doc truthful, per-view EC loop guarded, conformance harness's per-cell EC limit recorded

The two lenses passed 6575cfcee and left these: AD-23 described the
ClipSlotCull the fix round deleted; AD-17's 'dormant, live for the flat
path' clause was false (the routing had no caller anywhere); the
ProbeClipRouteEnabled doc named a deleted producer; the per-view EC
print ran its loop with the flag off; the replay-side derivation still
prints one EC per flood cell while production prints one per live view,
which no fixture can tell apart today — recorded at the harness for S5.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 20:17:16 +02:00
parent 89c4494d05
commit ea76a38035
4 changed files with 29 additions and 12 deletions

View file

@ -144,13 +144,13 @@ readiness/requeue adaptation. See
| AD-13 | 1-second dedup window for identical system chat messages (retail has none) | `src/AcDream.Core/Chat/ChatLog.cs:29` | ACE dual-sends the same system text (0xF7E0 + 0x02EB) for back-compat; without dedup every line doubled (Phase J compromise) | Two genuinely distinct but textually identical system messages within 1 s collapse to one line where retail shows both | ACE dual-send 0xF7E0 + 0x02EB |
| AD-15 | `IsEnv` masks low-16 of the cell id (`(Id & 0xFFFF) >= 0x100`) where retail tests the full id | `src/AcDream.Core/World/Cells/ObjCell.cs:25` | Every real prefixed EnvCell id has low-16 ≥ 0x100 and every outdoor cell ≤ 0x40 — identical answers for all real dat ids, works for both bare and prefixed forms | None for real dat data; a hypothetical convention-violating id would route to the wrong (BSP vs terrain) point-in-cell logic | `CObjCell::GetVisible` pc:308215 |
| AD-16 | Building-flood gate is a CPU frustum test on each building's `PortalBounds` AABB; retail floods exactly when the shell draws and an aperture survives (no bounds constant anywhere) | `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameBuildingSource.Gather`) | Documented as the tight equivalent of the shell viewconeCheck for flood purposes (the FPS fix the Chebyshev≤1 hack approximated); per-portal admission still goes through BuildFromExterior's screen clip; missing-bounds buildings always flood (safe over-include) | A too-small/stale PortalBounds AABB means the interior never floods — doorway shows a hole/black aperture from outside (inverse of the vanishing-staircase class) | `DrawBuilding` 0x0059f2a0; `BSPPORTAL::portal_draw_portals_only` 0x53d870 |
| AD-17 | **REWRITTEN 2026-09-03 at Campaign OVERHAUL v2 S3 chunk 4 (rounds `ff607a1e0` + `7df0b94c9`).** Only two GPU clips remain, both retail `PView::DrawCells` portal-polygon draws: the exit seals and the punch fans. Each carries ≤8 `gl_ClipDistance` half-planes per view region (`ClipPlaneSet.MaxPlanes`; `Render::copy_view` caps a view at 31 vertices). Overflow handling differs by consumer: a >8-edge exit-seal view uses its convex NDC AABB as a conservative four-plane gate (`WalkFrameDriver.AppendClipSlot`); a >8-edge or multi-polygon OUTSIDE view yields a zero-plane slice (`ClipFrameAssembler.AppendOutsideSlice`'s fallback: slot 0, empty `Planes`, `ScissorFallbacks` counted) and that view's punch fan draws UNCLIPPED. Retail CPU-clips its portal polygons exactly. The former union-AABB scissor fallback, the sky's doorway scissor bracket, the per-slice terrain/sky/weather clips, the TerrainClip UBO and `NdcScissorRect` are DELETED: sky, terrain and weather are never GPU-clipped, matching retail (`LScape::draw` installs no view before any of them). The mesh and particle shaders KEEP a per-cell `clipRegions[aClipSlot]` input, but the walk feeds every ordinary part (`WbDrawDispatcher.WalkClassify.ResolvePartVisible` — a Boolean sphere test, whole-mesh slot 0) and every particle (`ParticleRenderer`'s `clipSlot` default 0) the no-clip slot, matching retail's `DrawMeshInternal` sphere test; that machinery is dormant in the walk path and live only for the non-walk flat path. | `src/AcDream.App/Rendering/ClipPlaneSet.cs:23`; `src/AcDream.App/Rendering/ClipFrameAssembler.cs` (`AppendOutsideSlice`); `src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs` (`AppendClipSlot`); `src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs` (`DrawWalkPunchFan` reads `OutsideViewSlices[i].Planes`) | Vulkan's `VkPhysicalDeviceLimits::maxClipDistances` floor is 8; over-inclusion is the safe direction for a depth-only seal/punch (it can cover at most the whole aperture fan), under-inclusion is the bug class | A >8-edge exit-seal view seals its AABB instead of the exact polygon and a >8-edge outside view punches its whole fan — depth-only over-include with no color bleed (the former **#130** strip family died with the scissor); an interior root's aperture exactness now rests on the depth clear + exit seals + interior repaint, never on a GPU clip of the landscape | `ACRender::polyClipFinish` decomp:702749; `PView::DrawCells` 0x005a4840 portal_view slices; `Render::copy_view` 0x0054dfc0; `LScape::draw` 0x00506330 |
| AD-17 | **REWRITTEN 2026-09-03 at Campaign OVERHAUL v2 S3 chunk 4 (rounds `ff607a1e0` + `7df0b94c9`).** Only two GPU clips remain, both retail `PView::DrawCells` portal-polygon draws: the exit seals and the punch fans. Each carries ≤8 `gl_ClipDistance` half-planes per view region (`ClipPlaneSet.MaxPlanes`; `Render::copy_view` caps a view at 31 vertices). Overflow handling differs by consumer: a >8-edge exit-seal view uses its convex NDC AABB as a conservative four-plane gate (`WalkFrameDriver.AppendClipSlot`); a >8-edge or multi-polygon OUTSIDE view yields a zero-plane slice (`ClipFrameAssembler.AppendOutsideSlice`'s fallback: slot 0, empty `Planes`, `ScissorFallbacks` counted) and that view's punch fan draws UNCLIPPED. Retail CPU-clips its portal polygons exactly. The former union-AABB scissor fallback, the sky's doorway scissor bracket, the per-slice terrain/sky/weather clips, the TerrainClip UBO and `NdcScissorRect` are DELETED: sky, terrain and weather are never GPU-clipped, matching retail (`LScape::draw` installs no view before any of them). The per-cell clip machinery for ordinary parts and particles is DELETED (S3 review fix round 1, `6575cfcee`): `mesh_modern`, `mesh_atmospheric`, `mesh_detail`, `particle` and `particle_mesh` no longer declare the CellClip SSBO, the `gl_ClipDistance` loop or the `gl_PerVertex` redeclaration, and `WbDrawDispatcher`'s routing state (`SetClipRouting`, `_clipRoutingActive`, `ResolveSlotForFrame`'s routing arm — which had ZERO callers) is gone; every ordinary part is admitted by the walk's Boolean sphere test (`WbDrawDispatcher.WalkClassify.ResolvePartVisible`), matching retail's `DrawMeshInternal` @0x0059f360, and every particle draws unclipped. Residue named for the S5 cleanup inventory: the vertex layouts' `aClipSlot`/`instanceClipSlot` inputs (fed 0) and the CPU-side `ClipRegions` byte-pack + SSBO publish in `RhiWorldPassSurface.PrepareClipFrame` (no GPU reader; `ClipFrame.AppendSlot`/`GetSlotPlanes` stay as the CPU plane store the exit seals read). | `src/AcDream.App/Rendering/ClipPlaneSet.cs:23`; `src/AcDream.App/Rendering/ClipFrameAssembler.cs` (`AppendOutsideSlice`); `src/AcDream.App/Rendering/Walk/WalkFrameDriver.cs` (`AppendClipSlot`); `src/AcDream.App/Rendering/RetailPViewPassExecutor.WalkLeaf.cs` (`DrawWalkPunchFan` reads `OutsideViewSlices[i].Planes`) | Vulkan's `VkPhysicalDeviceLimits::maxClipDistances` floor is 8; over-inclusion is the safe direction for a depth-only seal/punch (it can cover at most the whole aperture fan), under-inclusion is the bug class | A >8-edge exit-seal view seals its AABB instead of the exact polygon and a >8-edge outside view punches its whole fan — depth-only over-include with no color bleed (the former **#130** strip family died with the scissor); an interior root's aperture exactness now rests on the depth clear + exit seals + interior repaint, never on a GPU clip of the landscape | `ACRender::polyClipFinish` decomp:702749; `PView::DrawCells` 0x005a4840 portal_view slices; `Render::copy_view` 0x0054dfc0; `LScape::draw` 0x00506330 |
| AD-18 | Aperture far-Z punch is two-pass stencil-gated with an invented mark bias: 0.0005 NDC capped to a 0.5 m EYE-SPACE span (`MarkBiasNdc`); retail's single DEPTHTEST_ALWAYS punch is safe only under painter's far→near order we don't have | `src/AcDream.App/Rendering/PortalDepthMaskRenderer.cs:149` | **#117** (2026-06-11): the unconditional punch erased nearer occluders, painting interiors through them; the two-pass form is the z-buffered equivalent of retail's ordering safety. **#129** (2026-06-12): the constant-NDC bias spanned ~190 m of eye depth at a landblock (non-linear depth) → distant occluders punched; the eye-space cap bounds the reach (`Issue129PunchBiasTests`). DO-NOT-RETRY: punch must stay depth-gated (ISSUES #108) | Door-plane-hugging geometry beyond the 0.5 m cap re-occludes the aperture (a **#108**-class regression at >10 m viewing range); an occluder within the cap in front of a distant aperture still punches through | `D3DPolyRender::DrawPortalPolyInternal` 0x0059bc90 (maxZ1=7 / maxZ2=6) |
| AD-19 | Under outdoor roots, ALL dynamics draw in one z-buffered final pass; retail draws objects painter-ordered per landcell inside the landscape pass (interior roots route per **#118**) | `src/AcDream.App/Rendering/RetailPViewRenderer.cs:126` | The dynamics-drawn-LAST invariant is what makes the aperture depth punch safe (first BR-2 attempt punched after dynamics and erased the player, reverted `88be519`); z-buffer substitutes for painter's order on opaque geometry | Punch/seal correctness hinges on an ordering invariant — any pass added after DrawDynamicsLast, or alpha content needing painter order, gets erased inside apertures or composites wrong | `LScape::draw``DrawBlock` 0x005a17c0 → DrawSortCell pc:430124; `PView::DrawCells` 0x005a4840 |
| AD-20 | Camera sweep fallback seeds the eye's `AdjustPosition` from the PLAYER's cell; retail re-seats at the sought eye's own tracked cell (rest of function is a verbatim `update_viewer` port) | `src/AcDream.App/Rendering/PhysicsCameraCollisionProbe.cs:97` | acdream's camera doesn't track the sought-eye's cell separately; the eye is near the player so the player-cell stab list is assumed to cover it | An eye outside the player cell's stab-list coverage (boundary corners, cross-landblock pull-back) seats in the wrong cell — and the viewer cell roots the whole render: one-frame wrong root (flap-class flash) | `SmartBox::update_viewer` 0x00453ce0, pc:92878-92883 |
| AD-21 | Null-clipRoot legacy outdoor safety path (no portal visibility, no punches/seals, no-clip terrain) for pre-spawn / login / legacy cameras; in-world retail always has a viewer_cell root | `src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs` (`RuntimeWorldFrameRootSource`, `WorldRenderFrame.ClipRoot`); `src/AcDream.App/Rendering/WorldSceneRenderer.cs` (null-root safety draw) | Result is null ONLY when neither an interior root nor the synthetic outdoor node exists; kept so the login screen shows the live sky | If viewer-root resolution ever returns null in-world (membership bug, fly-camera edge), the frame silently degrades — interiors stop drawing through doorways; the old two-branch FLAP reappears for those frames | `SmartBox::RenderNormalMode` decomp:92635 |
| AD-22 | Async streamed mesh loading with bounded CPU replay residency, per-frame upload budgets, and point-of-use self-heal (`EnsureLoaded` re-request in the dispatcher's mesh-missing path, **#128**); retail loads synchronously — geometry is never absent | `src/AcDream.App/Rendering/Wb/WbMeshAdapter.cs`; `src/AcDream.App/Rendering/Wb/MeshUploadCaches.cs`; `src/AcDream.App/Rendering/Wb/MeshUploadFrameBudget.cs` | Immutable preparation descriptors and the bounded CPU cache can re-stage an evicted mesh; dispatcher self-heal makes absence transient while upload budgets prevent a portal arrival from monopolizing a frame | A future consumer that neither retains an owner nor reaches the self-heal/replay path can remain invisible; under heavy admission pressure a valid mesh can pop in later than retail's synchronous path | retail synchronous content load; `docs/architecture/worldbuilder-inventory.md` portal-readiness and bounded-residency seams |
| AD-23 | Live entities with `ServerGuid != 0` and null `ParentCellId` are culled (ClipSlotCull) while indoor clip routing is active; retail objects are always cell-resident (synchronous add-to-cell at creation) | `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs:484` | Phase U.4 policy: parentless = unresolved indoors, equivalent to retail's not-in-any-visible-cell ⇒ not drawn, *given membership resolves promptly* | An entity whose membership lags (late CreateObject hydration, resolver hiccup) blinks invisible while the player is indoors, even in plain sight | retail per-cell object lists in PView traversal |
| ~~AD-23~~ | **RETIRED 2026-09-03 at Campaign OVERHAUL v2 S3 review fix round 1 (`6575cfcee`).** The mechanism this row described — `ClipSlotCull` of live entities with `ServerGuid != 0` and null `ParentCellId` "while indoor clip routing is active" — is deleted with the per-cell clip routing itself (`SetClipRouting`, `_clipRoutingActive`, `ResolveSlotForFrame`'s routing arm): the routing had ZERO callers since the walk cutover, so the culling never ran, and the walk admits every part by retail's sphere test (`DrawMeshInternal` @0x0059f360) with cell membership owned by the S2 graph. Historical text: live entities with `ServerGuid != 0` and null `ParentCellId` were culled while indoor clip routing was active; retail objects are always cell-resident (synchronous add-to-cell at creation) | (deleted) formerly `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs` `ResolveSlotForFrame`/`ClipSlotCull` | Phase U.4 policy: parentless = unresolved indoors, equivalent to retail's not-in-any-visible-cell ⇒ not drawn, *given membership resolves promptly* | An entity whose membership lags (late CreateObject hydration, resolver hiccup) blinks invisible while the player is indoors, even in plain sight | retail per-cell object lists in PView traversal |
| AD-24 | EnvCell shell geometry content-deduplicated and instanced; retail draws each CEnvCell's own structure directly | `src/AcDream.Core/Rendering/Wb/EnvCellGeometryIdentity.cs` | Phase A8 retained WB's 31× hash; the 2026-07-24 full-DAT gate proved a real collision (`0x00030175`/`0x01BC0105`), so App+Bake now share a namespaced FNV-1a tuple identity and the bake rejects any full-tuple collision | A future collision outside the installed full-DAT gate could still merge different shells at runtime; the stronger 59-bit payload makes this extremely unlikely, and every bake fails loudly rather than publishing it | retail `PView::DrawCells` → per-cell drawing_bsp (cited at the former renderer `:319`) |
| AD-27 | PickUp fires on natural moveto completion via the `MoveToComplete` client-addition seam (retail's `CleanUpAndCallWeenie` contains no weenie call in this build and notifies nothing on arrival). The companion `MoveToCancelled` seam only withdraws the waiting pickup presentation/action. **Use retired 2026-07-25:** `ItemHolder::UseObject` sends `Event_UseEvent` before `CPlayerSystem::UsingItem`; acdream now does the same and leaves approach to ACE's authoritative MoveToChain. | `src/AcDream.App/Interaction/SelectionInteractionController.cs` (`OnNaturalMoveToComplete`/`OnMoveToCancelled`); `src/AcDream.App/Input/PlayerModeController.cs` (player MoveTo seam binding); `src/AcDream.Core/Physics/Motion/MoveToManager.cs` (`MoveToComplete`/`MoveToCancelled`) | ACE's server-side pickup chain may have timed out by the time our body arrives; the close-range deferred send hits ACE's WithinUseRadius fast-path. | If the server's chain has not timed out, pickup may execute twice or produce protocol noise on non-ACE servers | ACE CreateMoveToChain / WithinUseRadius; `MoveToManager::CleanUpAndCallWeenie` 00529650 §7e (no weenie call); `ItemHolder::UseObject` 0x00588A80 |
| AD-28 | Chat transcript (`UiText`) and input (`UiChatInput`) are two separate widget classes placed inside their dat-authored container panels; retail's `ChatInterface` uses a single mode-flagged `UIElement_Text` (Type-12) that switches between read and edit mode | `src/AcDream.App/UI/Layout/ChatWindowController.cs:135` (transcript) + `:150` (input) | `UIElement_Text` is inside keystone.dll with no PDB/decomp; a two-widget split is functionally equivalent (read-only scroll, editable input) and is the structural adaptation required by our UiElement architecture | A future consumer expecting a single widget for both read/write (e.g. a plugin calling the chat API and getting one widget back) must be written to the two-widget contract | `UIElement_Text` (Type-12) @ keystone.dll; `gmMainChatUI::PostInit` @0x4ce130 |