Commit graph

74 commits

Author SHA1 Message Date
Erik
0d6cd5c06e fix(render): FW4 slice 5 - straddling cell statics'' particles emit pre-clear
The cathedral falls'' true identity, pinned by the emitter/owner dumps
and the owner''s retail-vs-ACE oracle (retail renders them perfectly):
they are EnvCell STAB-LIST statics (interior-cell dat objects, ids
minted by InteriorEntityIdAllocator) whose geometry hangs out over the
lake. Our cell-statics route submitted every interior flood cell''s
static particle owners POST-clear ("walls own depth") - true for
content that stays inside, but a straddler''s outside half meets
seal/cleared depth and splats across the vista.

Retail''s mechanism is the outdoor shadow-cell draw: an object
overlapping outdoor landcells draws during LScape::draw (DrawBlock
pc:430056-430064, once per overlapped shadow cell), so its particles
submit in the landscape scope and drain at the pre-clear boundary
flush over true terrain depth; the cell''s own turn skips them
(drawn-once). This slice ports that rule for particles: after Collect,
the interior flood cells'' static records are classified by the shared
exit-plane straddle test (extracted as SphereStraddlesExitPlane from
the #118 dynamic split); straddlers'' owners submit in the pre-clear
closure and the post-replay cell-owners union subtracts them, so every
owner emits exactly once. Also adds the [walk-emit] probe dump that
falsified the unattached-route theory.

Hermetic 6,762/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 19:58:04 +02:00
Erik
f3a03efc81 fix(render): FW4 slice 4 - straddler particles emit pre-clear, once
The cathedral falls survived slices 2 and 3; the [walk-dyn] dump then
pinned the real gate: the falls emitters are interior-parented
exit-plane STRADDLERS that pass BOTH the outside-stage predicate and
the viewcone (outside=1 cone=1) - they were in the pre-clear union all
along, and the deliberate ExceptRoute(DynamicLast) stripped them so
they emitted in the post-clear last pass instead ("straddlers emit
HERE"). That placement protects an indoor flame from interior repaint,
but a straddling emitter whose particles hang OUTSIDE the exit portal
has no depth left to occlude them after the clear - they splat across
terrain and water.

Retail submits an emitter''s polys at its object''s FIRST draw - the
landscape stage for anything overlapping outdoor shadow cells - and
drains them at the pre-clear boundary flush over true landscape depth.
Under an interior root the late-union now keeps the outside-stage
owners (straddlers included), records them, and BOTH last-pass arms
subtract the recorded set so every owner emits exactly once. Outdoor
roots keep the old except-at-union placement: both stages drain at the
same final flush there, so the last-pass emission is already correct
and a pre-emission would double-composite.

Hermetic 6,762/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 19:37:18 +02:00
Erik
5f7ccdeab5 fix(render): FW4 slice 3 - the outside-stage predicate reads the walk flood
The cathedral falls persisted through the slice-2 seal fix; the probe
already held the tell: the same two emitter entities (f4243/f4285) draw
phase=pre on good frames and phase=post on bad ones. They are
interior-parented, so DynamicDrawsInOutsideStage takes the indoor arm,
whose first gate is flood membership - and that still read the OLD
apparatus flood, which drops cells at the #456 seam band. On those
frames the falls fell to the post-clear last pass and splatted across
the cleared depth (terrain and water alike).

Both predicate call sites (the frame product's BuildOutsideDynamicRoutes
via BuildAndBorrow's input, and the renderer's own outside-stage
classification) now receive the walk's VisitedCells on walk frames -
root flood plus look-in floods, all of whose objects retail draws inside
LScape::draw, pre-clear. drawableCells keeps its other roles. Also adds
the probe-gated [walk-dyn] per-entity classification dump (parent cell,
outside/cone verdicts) so a surviving repro pins the failing gate
directly.

Hermetic 6,762/0, Walk lane 213/1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 19:28:57 +02:00
Erik
c40aecfc8c fix(render): FW4 slice 2 - exit seals stamp the walk''s own flood
The cathedral falls shine-through (owner report at the #456 terrace
seam, toggling with the camera''s side of the seam): the probe''s new
phase tags pinned the falls emitters to the cell-owners route
submitting POST-depth-clear on interior roots - retail''s own timing,
safe only because seals own the aperture depth by the end-of-frame
alpha drain. Our seal SET still came from the old apparatus''s flood,
which misses exit portals at the #456 seam band (its never-drawn panel
family), so the drain z-passed across the unsealed opening.

DrawWalkExitPortalMasks now stamps the walk''s own ordered interior
flood (WalkFrameDriver.InteriorFloodCells, captured at
OnInteriorFloodDrawTurn - retail PView::DrawCells pc:432785-432786),
keeping the old assembly''s per-cell slice clips where present and
sealing unclipped where the old apparatus had no entry (benign: the
exact dat aperture polygon, z-tested). The legacy path keeps the old
seal draw. Also: ACDREAM_PROBE_WALK_ROOT gains [walk-part]/[walk-alpha]
phase-tagged submission/drain lines (the apparatus that pinned this).

Hermetic 6,762/0, Walk lane 213/1, InstalledDat walk conformance 40/1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 19:22:56 +02:00
Erik
37febd1fe6 fix(render): FW4 slice 1 - interior outside-view slices come from the walk
The FW3 visual gate's stairwell/grass transition flash (grass briefly
covering floor openings at doorway crossings - the #119 family) was the
FW3 dual path leaking: the walk decided WHETHER terrain draws while the
old PortalVisibilityBuilder assembly decided WHERE (slice planes, count,
scissor), and punch fans indexed the old slice array with walk view
indices. The new ACDREAM_PROBE_WALK_ROOT apparatus pinned the boundary
frames: fat/degenerate old-apparatus exit views splash terrain over
interior pixels, the interior depth-clear preserves color, and cells
absent from the walk's flood never repaint. Retail has ONE visibility
structure and cannot produce this.

ClipFrameAssembler.ReassembleOutsideViewFromWalk now materializes the
walk's own outside_view (pixel screen points -> standard NDC -> the
existing ClipPlaneSet machinery) into the assembly's outside-view block
after Collect, ahead of the single PrepareClipFrame publication (moved
below the walk block). The Landscape event carries the walk's active
view count on the record's existing OutsideViewCount field (trace
mapping compares kind only - zero oracle-fixture churn) and the driver
fans exactly that many terrain slices; activeTerrainSliceCount is
deleted end to end. Outdoor roots keep the assembler's single
full-screen slice, asserted ==1.

Hermetic 6,762/0 (4 new materializer tests pin the y-flip and
plane-sign conventions), Walk lane 209/1, InstalledDat walk conformance
40/1. Seals/cell slices/look-in seeding stay on the old per-cell views
for the rest of FW4 (identical dat polygons; only the visible set can
differ).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 18:21:08 +02:00
Erik
77fd40771b fix(render) Campaign FW3 visual gate: candle flames pre-clear too
Second owner report at the same gate: candle-flame particles still
invisible looking out. Flame emitters attach to STATIC owners (lamp
and candle statics), and the walk path submitted the landscape-stage
static-owner particle union AFTER the driver replay - post-clear, so
the pre-clear drain never carried them and a later flush z-failed
them at the sealed aperture. For an interior root the submission now
runs at the walk pre-clear boundary (retail inserts each emitter
during its cell landscape walk turn, before the clear+seals);
outdoor roots keep the post-replay frame-end placement (#132).
Interior cells own emitters stay in the final world scope where the
walls already own depth.

Suites: full Release build 0 warnings; hermetic 6,758/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 17:34:14 +02:00
Erik
f8fc21d467 fix(render) Campaign FW3 visual gate: interior dynamics draw PRE-clear
Owner gate report (Holtburg, looking out from a house): no doors on
buildings, no candle particles, the character vanishing for a frame
when crossing out. One family: the cutover moved the interior depth
clear + exit seals into the walk turn, but the dynamics phase
(outside dynamics, look-in dynamics, outdoor particles, weather)
still ran AFTER the whole walk - everything beyond the door plane
z-failed against the seals stamp, the exact #118 house-exit
clip+vanish class the old code documented.

Retail draws the outside world's objects INSIDE LScape::draw,
strictly before the clear+seals. The driver's clearInteriorDepth
closure now invokes the dynamics phase at that pre-clear boundary for
interior roots (_walkPreClearDynamics, assigned around the Replay,
cleared in finally); outdoor roots keep the post-driver order (no
clear exists there). Packed route consumption order is unchanged
(LookInObject -> LandscapeOutsideDynamic -> DynamicLast).

Suites: full Release build 0 warnings; hermetic 6,758/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 17:27:11 +02:00
Erik
212f5a12e5 perf(render) Campaign FW3.4a: one walk pass; prepare-once/draw-ranges; arena records
The FW3.4 dense-Arwic pair triggered the +/-20% stop rule (+33.5% CPU
p50, 14x frame allocation). This slice removes the three measured
costs without changing GPU command order (the referee suites assert
identical recorded call sequences):

- WalkFrameDriver: Collect (ONE walk per frame - no GPU work; leaf
  calls and flush points become a recorded event list; the driver
  absorbed the renderer collection pass and exposes the visited sets)
  + Replay (prepare the whole stream once, then replay events,
  interleaving DrawOrderedRange with leaf calls in the exact recorded
  order). RunFrame = Collect+Replay for existing callers.
- WbDrawDispatcher: SubmitOrderedStream split into PrepareOrderedStream
  (all sections + commands + merge runs uploaded once per frame) and
  DrawOrderedRange (bind-once latch; per-run pipeline + DrawIdOffset +
  DrawIndirectRangeRhi). Load-bearing correctness catch from the
  implementation round: merge runs take FORCED BREAKS at the recorded
  event marks - whole-stream merging must not fuse two segments that
  retail separates with a leaf GPU call (shell, punch); the straddle
  assert stays as a dead-code safety net.
- WalkProductionWorldData: WalkFrameStaticRecords carries an
  ArraySegment into a per-frame grow-only arena; the per-cell
  fresh-array copies (the 1.9 MB/frame alloc p50) are gone - zero
  steady-state allocation after warmup.

Suites (lead-verified): full Release build 0 warnings; hermetic
6,758/0; Walk lane 209/1; InstalledDat Walk conformance 40/1
untouched. Next: the dense-Arwic re-measure against the same-session
baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 17:01:26 +02:00
Erik
05240d2cab feat(render) Campaign FW3.3: ShellDrawLiftZ is RETIRED - cells draw at the dat origin
Retail draws cell geometry at the dat EnvCell origin verbatim; the
0.02 m lift was our z-fight stand-in (register row AP-32, deleted in
this commit). With the walk owning retail draw ORDER under
WorldDepthContract Less (first-drawn-wins: DrawBlock terrain-then-
objects per cell, DrawCells shells-then-contents), the coplanar
tie-breaks the lift compensated for are now resolved the way retail
resolves them.

Deleted at every site: the PortalVisibilityBuilder const + the
drawLiftZ Build parameter and its lifted exit-portal projection branch
(gate and drawn geometry now share ONE space); the seal/punch fan
lifts (DrawPortalDepthWrite + the walk's DrawWalkPunchFan); the
LandblockBuildFactory drawn-cell-transform lift (render and physics
share the one verbatim transform).

The #130 proof flipped exactly as its own doc predicted:
UnliftedGate_LeavesTheStripAtTheDrawnTopEdge is deleted (its premise -
gate space != drawn space - no longer exists), and the renamed
ExitDoorTopEdge_GateCoversTheDrawnApertureWithinPixelTolerance sweep
(147 eye/gaze combos at the Holtburg corner door) passes with both in
the same unlifted space (worst plane gap <= 1.2 px, scissor <= 0.15 px
- unchanged tolerances). Ten more replay-test call sites swept to the
new Build signature.

Suites: full Release build 0 warnings; hermetic 6,750/0; the 21
affected InstalledDat replay tests green; Walk conformance 40/1
untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:08:52 +02:00
Erik
1955a0d5f1 fix(render) Campaign FW3.2b-2: scenery buckets need the streaming recenter
The owner watched the first passing lifecycle run and reported most
outdoor scenery not rendering (terrain/buildings/interiors fine, no
seams). Root cause: retained-scene record positions are RENDER-ORIGIN-
RELATIVE (each landblock carries (lbX - CenterX)*192 offsets), but the
walk world data bucketed outdoor statics by treating them as absolute -
garbage landblock bytes, so the walk's landscape-cell turns read empty
buckets and trees/rocks/fences never drew. LandscapeCellId now adds the
frame's RenderCenterLbX/Y back to the relative block index, producing
the TRUE cell ids the walk's OnLandscapeCellTurn emits. Buildings and
cell statics were unaffected (id-keyed, not position-bucketed).

The same run confirmed the campaign thesis live: NO SEAMS at the
doorway class under walk order + retail depth, with the lift hack
still present (its retirement is FW3.3).

Suites: hermetic 6,750/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:54:06 +02:00
Erik
4918677b45 feat(render) Campaign FW3.2b-2: THE STATIC CUTOVER - the walk drives production statics
The retail frame walk now drives every production static draw. In
RetailPViewRenderer.DrawInside, when the concrete executor + the
packed product + the FW3.1 walk registries are all wired (all
production compositions - anything less throws):

- A pre-walk events-only collection pass (the shadow sink generalized
  to WalkVisitedSetCollector) gathers the frame's visited cells,
  buildings, and landscape-cell turns; the visited cells union into
  prepareCells so EnvCellRenderer prepares every shell the driver
  draws.
- DrawWalkDrivenStatics runs the WalkFrameDriver over the production
  world data (WalkProductionWorldData over RenderSceneQuery + the
  building registry): sky, terrain slices, outdoor statics at their
  landscape-cell turns, buildings (alpha barrier -> punch/look-in
  passes -> shell) in retail order, interior clear+seals as leaf
  closures (the old tail block's drain reasoning moves with them),
  flood cells shell-then-contents. Landscape/cell-stage particle
  owners re-source from the walk's visited sets - retail gates
  particles per cell turn (ShouldDrawParticles @0x0050FE60), which
  this is; the old sphere filter was the approximation.
- DrawLandscapeDynamicsPhase + DrawBuildingLookInDynamics carry the
  dynamics-only remainder (LookInObject now dynamic-classified,
  late outside-dynamics + weather, particle unions); DrawDynamicsLast
  and the outdoor flush are unchanged.
- The product builder stops emitting LandscapeOutdoorStatic /
  LandscapeBuildingShell / CellStatic (methods deleted, dead index
  tracking removed); LookInObject loads cells with
  includeStatics: false.

The old static path survives ONLY behind !walkActive for the
standalone/diagnostic executor-fake path that keeps 15 retail-ordering
regression tests exercising the barrier/punch/seal machinery; no
production composition can reach it. Its deletion is FW4 scope (the
plan's "deleting the patch apparatus") - recorded in the plan.

Transitional risks recorded in code/report: the two-pass walk cost
(FW3.4 measures), the interior slice-count reconciliation between the
old clip assembly and the walk's own exit-view survival, and the
outdoor merged-flood punch coverage now riding the walk's own
building-BSP punches (retail-faithful per FW1; the owner visual gate
verifies).

Suites (lead-verified): full Release build 0 warnings; hermetic
6,750/0 (baseline minus the three deleted route tests); Walk lane
201/1; InstalledDat Walk conformance 40/1 untouched. The two
IL-branch tests the implementation round reported failing pass in
every lead run - the recurring parallel-load flake pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:43:22 +02:00
Erik
878533597d feat(render) Campaign FW3.2b-2: the walk shadow probe (I5 pattern)
ACDREAM_PROBE_WALK_SHADOW=1 (documented row; throwaway - dies with the
flip commit) runs the PRODUCTION retail frame walk per frame in shadow
over the FW3.1 registries - WalkProductionFrameContext from the live
camera, SetViewer recentring, interior rooting via LoadedCell.Walk -
and prints one [walk-shadow] divergence line per frame whose visited
cell set differs from the old path (main flood + look-ins), plus a
loud PROBE FAULT line on any exception. No draws change. This is the
I5 dual-shadow pattern applied to the FW3 static cutover: it proves
the production walk world data live and quantifies old-vs-walk
divergence before any pixel moves. CellVisibility joins the renderer
plumbing as the walk cell registry.

Suites: full Release build 0 warnings; hermetic 6,753/0;
LaunchOptionsDocumentationTests green (both directions).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:47:15 +02:00
Erik
d80d47d611 feat(render) Campaign FW3.2b-2 step 1b: walk registry plumbing to the renderer
LandblockPresentationPipeline exposes its render publisher;
FrameRootComposition hands the publisher's WalkBuildings/WalkLandscape
registries (FW3.1, published/retired per landblock) into
RetailPViewRenderer via two additive constructor parameters. Nothing
reads them yet - the static cutover flip is the next commit, executing
the plan section FW3.2b-2 design with the carve points now surveyed:
the EARLY per-slice sky/terrain/static loop, the look-in barrier +
DrawBuildingLookIns, the LATE dynamics phase, and the shells/cell-
statics passes.

Suites: full Release build 0 warnings; hermetic 6,753/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:43:37 +02:00
Erik
e102fb363e fix(render) #132: outdoor alpha drains follow retail walk order
Some checks failed
CI / linux-portable (push) Successful in 3m35s
CI / windows-gate (push) Failing after 6m46s
CI / release (push) Has been skipped
Retail draws every cell's objects inside LScape::draw's far-to-near walk
(DrawSortCell @0x005A17C0) and drains the one delayed-alpha list at the
DrawCells boundary AFTER the finished walk (@0x005A4872). Our outdoor
frame drained at the landscape-stage end and then drew punches, interior
shells, cell objects, and ALL dynamics — every one of those opaque
passes overwrote the already-composited flames (the reopened #132
candle class: "the door draws over the candle", creatures at openings).
Depth and barrier A/Bs were no-ops because the eraser is opaque color
painted after the drain.

Two retail-cited ordering corrections, outdoor-node roots only:

1. The stage-boundary drain is skipped and FlushLandscapeAlpha() runs
   after DrawDynamicsLast, where the frame's opaque world depth is
   complete — the one far-to-near list composites over everything,
   exactly like retail's boundary flush relative to its finished walk.
2. Before DrawExitPortalMasks, FlushLandscapeAlphaFartherThan(
   ExitPortalMaskBarrierDistance(...)) drains everything at or beyond
   the nearest cell whose exit-portal mask is about to write far-Z —
   retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE its
   portal-only pass, so in the far-to-near walk nothing already drained
   can meet a punched aperture's falsified depth. Without this, the
   first correction let exterior waterfalls z-pass across punched
   apertures whose true landscape depth the punch erased (found live at
   the cathedral gate). Nearer content stays queued and legitimately
   composites in front of punched structures.

Interior roots keep the pre-clear stage-boundary drain unchanged.

User-gated live: Holtburg sign candle whole in front of the sign and
tower door at the aligned pose; cathedral waterfalls contained at every
camera zoom, inside and outside. Register row AP-236 retired (the
walk-order outcome reconstruction is complete; AP-34 remains the
umbrella for the CYpt-sort reconstruction itself). Filed #456 for the
separate occluded-distant-building/creature admission residual this
session diagnosed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 19:03:35 +02:00
Erik
fc30285fd7 fix(render): drawn-once look-in dynamics + farther-than-building alpha drains
Two retail-parity corrections on top of 684380d4, user-gated together
(no regressions; door/creature-through-opening rendering verified live
at Holtburg; cathedral waterfall and look-north casts pass):

1. Drawn-once look-in dynamics. Retail marks every drawn non-player
part for the frame (DrawMeshInternal @0x0059F360 GetDrawnThisFrame), so
an object draws once, with its cell. acdream drew a look-in cell's
dynamics twice under an outdoor root - once correctly with the look-in,
then again in dynamics-last after the boundary alpha drain, where the
second draw overpainted nearer flames. Both the accepted path
(_lookInCellIds) and the frame product (BuildDynamicLastRoute) now
exclude dynamics whose parent cell drew as a look-in.

2. Pre/inter-building barriers drain only content FARTHER than the
building they precede (FlushLandscapeAlphaFartherThan +
RetailAlphaQueue.FlushFartherThan + conservative anchor-origin
threshold). Retail's far-to-near walk guarantees a building's
FlushAlphaList(0f) @0x0059F2A0 has only farther content queued; a
nearer emitter composites at a later flush. AP-236 documents the
remaining barrier-order divergence.

The #132 candle-before-door overdraw is NOT yet fixed by these steps
and stays open: the current suspect is that houses without a
constructed look-in still draw their interior-parented door in
dynamics-last after the outdoor candle's boundary drain (see the
2026-08-29 ledger for the retail flush-after-objects hypothesis).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 16:06:04 +02:00
Erik
684380d421 fix(render): particles draw unclipped, once, in their retail stage
Retail never clips a particle to a portal view: each emitter's polys
join the ONE alpha list during its owner cell's far-to-near walk turn
(LScape::draw @0x00506330 iterates block_draw_list reversed; DrawBlock
@0x005A17C0 walks cells; ShouldDrawParticles @0x0050FE60 gates by cell
and distance), and occlusion is the depth test at FlushAlphaList
@0x0059D2E0 (its float is a COUNT threshold - 0f = flush all). The
1d2f2f73 architecture instead re-submitted particles once per
OutsideView slice under that slice's hardware clip slot, which cut
effects at aperture boundaries and drew nothing when no outside slice
was in view (the cathedral look-north disappearance).

Now: unattached emitters submit once per frame by owner-cell kind
(outdoor landcells in the landscape stage, interior EnvCells in the
final world scope - new UnattachedEmitterCellScope filter); cell,
shell-route, barrier-static, and late-stage owners submit their
per-slice cone-cull UNION once with clipSlot 0; and particles emit in
the stage matching their PARENT CELL - an interior dynamic whose
sphere straddles an exit-portal plane keeps its mesh in both stages
(#118) but its particles move to the final pass, so the interior
stage can no longer repaint over them (the aperture-band star cut).

Also lands the inert Change-2 primitives for the AP-236 retirement
(candle-behind-door): RetailAlphaQueue.FlushFartherThan drains only
the far prefix without resetting sources, plus the executor
passthrough and the conservative look-in threshold helper - nothing
calls them yet.

User-gated 2026-08-29 round 2 at the Sanctuary Cathedral: spell and
recall stars cover the whole room at every camera direction including
north; waterfall containment holds on retail's depth/seal mechanism;
adjacent-room particles/lights, walls, Holtburg, recall unregressed
(paperdoll remains pre-existing intermittent #443). Register: AP-236
filed for the remaining barrier-order divergence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 12:35:30 +02:00
Erik
85530c0b7e fix(render): feed look-in cells to particle and light visibility
Look-in rooms (cells reached through building portals) drew their
geometry but never entered the visible-cell set consumed by the
particle gate and the point-light candidacy scope, so their emitters
stayed frozen and their lanterns dark until the player entered or the
camera left the building. Retail has no look-in split:
ShouldDrawParticles @0x0050FE60 gates on cell->IsInView(), and a cell
installed by DrawBuilding @0x0059F2A0 -> PView::ConstructView
@0x005A57B0 is drawn by the same DrawCells traversal as any flooded
cell, so it is in view identically (per-cell in_view stamped by
PView::InitCell @0x005A4B70).

RetailPViewFrameResult now carries InViewCells (main flood + look-in
cells, the existing prepareCells union; the borrowed-scratch contract
is unchanged because RecycleLookInFrames runs at the start of the next
DrawInside). WorldSceneRenderer feeds InViewCells to
ParticleVisibilityController and the point-light scope. DrawableCells
is untouched for seals, the outside-stage predicate, diagnostics, and
the packed-product referee. The legacy 4-arg Reset defaults
InViewCells = DrawableCells for standalone callers.

User-gated 2026-08-29 at the Sanctuary Cathedral: adjacent look-in
rooms show particles and lantern lights from the root cell without
entry or an outside camera; recall/waterfall/Holtburg/paperdoll
unregressed. The pre-existing #132-family candle-behind-opening
report remains open and is unchanged by this fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 11:44:19 +02:00
Erik
f7aa8e0eb7 fix: complete retail parity stability pass
All checks were successful
CI / linux-portable (push) Successful in 3m41s
CI / windows-gate (push) Successful in 6m49s
CI / release (push) Successful in 3m22s
2026-08-28 20:01:39 +02:00
Erik
1d2f2f738f fix #451: stabilize portal seam rendering
All checks were successful
CI / linux-portable (push) Successful in 3m32s
CI / windows-gate (push) Successful in 6m55s
CI / release (push) Successful in 2m12s
2026-08-27 14:30:21 +02:00
Erik
0c5057c9ff fix #435 (part 1): delete 17 probes that outlived their closed investigations
Each of these was temporary apparatus added to chase one bug, and each was
supposed to be deleted in the commit that fixed it. Fourteen closed issues
later they were still here: #337's support/wire-mesh trio, #171's sticky
timeline, #119's viewer and entity dumps, #113's phantom probe, and a dozen
more. 3,493 lines removed; the client now reads 144 environment variables
instead of 161, and 47 temporary probes remain instead of 64.

This is not only tidying. Every probe leaves a branch on its hot path when
unset, several re-read the environment per call rather than caching, and
the volume buries the diagnostics that are actually load-bearing. It is
also a headless correctness matter: HeadlessStaticStateAudit reflects over
PhysicsDiagnostics' flags to refuse a multi-session host when any is set,
and cannot see probes that live outside that owner.

Four files went entirely — WalkMissDiagnostic.cs, CollisionMeshWireframe.cs
and two test files whose only subject was a deleted probe.
TransitionTypes.SetContactPlane also sheds its CallerMemberName /
CallerLineNumber parameters, which existed solely for #337's cpSrc=
attribution and carried the instruction to strip them with the probe
family; no call site passed them, so no behavior changes. F2's collision
overlay survives and reverts to its proxy-cylinder form, which is what
removing the ACDREAM_WIRE_MESH upgrade means.

LaunchOptionsDocumentationTests earned its keep here: it refused the
deletion until docs/launch-options.md moved the 17 rows into Retired and
the frozen direct-read counts came down (PhysicsEngine.cs to zero,
TransitionTypes.cs 3 to 2). The documentation could not drift during a
cleanup this wide.

The 14 probes that name no owning issue are deliberately NOT deleted.
Nothing records when they became safe to remove, and guessing is how a
future investigation loses apparatus it needed; #435 stays open for their
attribution.

Full hermetic suite 15,321 passed / 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-24 11:41:20 +02:00
Erik
9aaf97e785 Revert "Campaign V slice V4a" - it lost world multisampling
This reverts ceec3bc4. Two independent reasons, either sufficient.

The rendering regression. The slice deleted TextRenderGlStateScope, which
saved GL_MULTISAMPLE and GL_SAMPLE_ALPHA_TO_COVERAGE on entry, disabled them
for the text pass, and restored them on exit (TextRenderGlStateScope.cs:111-112
and 153-154 at the parent commit). Its replacement bakes that state into the
text pipeline but nothing restores it, and GlGpuPassEncoder.Dispose does not
either. Every world renderer is still raw GL at this point in the campaign, so
from the first UI frame onward the world drew with multisampling disabled.

The offline pixel gate caught it: 1,791 of 563,200 compared pixels differed,
0.318% against a 0.001 threshold. The commit message attributed this to
wall-clock-driven ambient animation shifting phase, and committed through the
failure. That explanation does not survive its own control: capturing twice at
the reverted-to commit differs by 19 pixels and twice at the slice's own commit
by 8, while base-versus-head differs by 1,791 - a 224x gap that no shared-noise
source explains. An amplified difference image settles it visually: the changed
pixels are the silhouette edges of every tree, building and rock, with terrain
interiors, water and the entire UI untouched. That is the signature of losing
edge antialiasing, not of animated sprites.

This is the exact failure mode two existing memory notes already warn about -
a mid-frame renderer must set every GL state it uses rather than inherit it,
and issue #52's lesson that a rendering migration must audit per-pass GL state
before declaring itself done.

The scope. The brief was three small leaf renderers plus additive frame-
lifecycle wiring, roughly ten files. The commit changed 334 files with 3,665
insertions and 3,845 deletions, including 323 public-to-internal visibility
conversions across the App assembly, 55 test files, two retired conformance
tests, and a self-described temporary escape hatch for bridging raw-GL viewport
textures. Even without the regression, that is not separable into the part
worth keeping and the part worth dropping.

Reverting rather than patching because the good work here - the RHI frame
lifecycle wiring and a genuine render-state-cache staleness fix - is small
enough to redo cleanly against a tightened spec, while untangling it from 300+
files of unrelated churn is not.

Post-revert: Release build clean, App suite back to 3,843 passed / 3 skipped,
offline pixel gate passing at 19 differing pixels.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:29:28 +02:00
Erik
ceec3bc440 feat(render): Campaign V slice V4a - port TextRenderer/BitmapFont/DebugLineRenderer/TextureCache onto IGpuDevice
TextRenderer, BitmapFont, DebugLineRenderer, and TextureCache's UI-texture
upload path (GetOrUploadRenderSurface/UploadRgba8) now issue every draw and
resource creation through the pinned IGpuDevice/IGpuFrame/IGpuPassEncoder
RHI contract instead of raw GL. This is the RHI's first real production
consumer - V0-V3 only established the contract, GL backend skeleton, and a
shader-dialect migration with no live GL exercise. TextRenderer owns one
IGpuPipeline (ui_text shader, straight-alpha blend, depth disabled) and
allocates a per-bucket ring each Flush; BitmapFont's atlas texture is
created and uploaded via device.CreateTexture/.Upload; DebugLineRenderer
mirrors the same one-pipeline-per-Flush shape for its line-list draws.
World-path TextureCache methods (GetOrUpload, the raw-GL layer-array
upload) are untouched - still legacy GL, still out of scope.

Frame lifecycle: GpuDeviceFrameLifetime (RenderFrameOrchestrator.cs) wraps
IGpuDevice.BeginFrame()/IGpuFrame.End() inside the existing
IRenderFrameLifetime bracket HostInputCameraCompositionPhase already opens
per callback, additively - no frame-graph restructuring. Ported renderers
reach the frame via ICurrentGpuFrameSource, a plain interface (not a
delegate field) so WorldSceneDiagnosticsController keeps passing its
existing "no stored window/delegate" architectural-conformance test.

Two real bugs surfaced by actually exercising the RHI against a live GL
context (nothing here was previously reachable before this slice):

- GlGpuDevice.BeginFrame() now resets the render-state cache every frame.
  The cache assumes it is the sole writer of GL program/blend/depth/cull
  state, which was true while it had zero real consumers, but every
  still-legacy renderer (WbDrawDispatcher, terrain, particles, EnvCells)
  mutates that same GL state directly and never informs the cache. Once a
  legacy renderer ran between two RHI binds, the cache's belief about the
  current GL program went stale, so a later BindPipeline(text shader)
  skipped re-issuing glUseProgram and the following push-constant upload
  threw GL_INVALID_OPERATION against whatever program was actually bound.
  Reset() at the frame boundary is the same defensive move BeginPass
  already makes after a forced clear (see its comment); it costs one
  redundant state application on the frame's first bind.
- GL_MULTISAMPLE has no representation in the pinned contract. Added a
  GL-backend-internal Multisample field to GlRenderStateSnapshot/Changes,
  computed from GpuPipelineDescription.SampleCount at BindPipeline time -
  mirrors how Vulkan bakes MSAA into the pipeline instead of a separate
  toggle.

Collateral, scoped to keep the port real rather than a stub:

- GpuTextureSlot (Unassigned = uint.MaxValue, NOT 0) now flows through
  every consumer of TextureCache.GetOrUploadRenderSurface/UploadRgba8 and
  TextRenderer.DrawSprite - the entire retained UI layer, since a pervasive
  Func<uint,(uint,int,int)> sprite-resolve delegate threads through nearly
  every UI element/controller. Every prior `== 0` / `!= 0` "no texture"
  check became `.IsAssigned` / `!.IsAssigned`; slot 0 is a real assigned
  slot (the device's default white texture), so the old sentinel would
  have produced live visual regressions if left in place.
- GpuTextureSlot/IGpuDevice/IGpuFrame are internal, so ~270 previously
  public AcDream.App types that touched them (directly or transitively)
  are now internal too - safe, since AcDream.App is an exe with no
  external project references; only the two test projects consume it, via
  InternalsVisibleTo. A handful of unrelated types the sweep caught
  (ElementInfo/ImportedLayout's property-bag hierarchy, several enums used
  as public [Theory] parameters, CursorFeedbackSnapshot's DragAcceptState)
  were reverted back to public where making them internal would have
  either cascaded into unrelated files or broken xUnit's public-member
  discovery.
- ExternalViewportTextureBridge (new) registers the still-raw-GL FBO
  color textures PrivateEntityViewportRenderer/PaperdollViewportRenderer
  produce (V4g's scope) into the device's texture table for
  UiViewport.TextureHandle, via a temporary
  GlGpuDevice.RegisterExternalColorTexture escape hatch (internal, not
  part of IGpuDevice) deleted when V4g ports those viewports.
- TextRenderGlStateScope.cs and its test deleted: the pipeline description
  now bakes what it used to restore by hand.
- ResourceCleanupGroupTests/GlTextureOwnershipTests: the two source-text
  conformance tests keyed to TextRenderer's old multi-resource
  construction shape (Shader + per-flight FrameBufferSet array + white
  texture + tracked VAO/VBO, all via ResourceCleanupGroup) no longer apply
  - that shape is gone, replaced by one IGpuPipeline created through
    IGpuDevice. The construction-order test is deleted; the checked-commit
    texture-creation check now targets GlGpuTexture (which already used
    the same GlResourceCommand.CreateName primitive before this slice).

Gates:
- dotnet build -c Release: 0 warnings, 0 errors (AcDream.App has
  TreatWarningsAsErrors).
- dotnet test tests/AcDream.App.Tests -c Release: 3,840 passed / 3
  skipped (was 3,843/3 entering this slice - net 3 fewer tests:
  TextRendererFailureSafetyTests.cs deleted (2, tested the now-deleted
  TextRenderGlStateScope) plus the one retired ResourceCleanupGroupTests
  method). Full solution: 8,908 passed / 5 skipped across all nine test
  projects.
- Offline pixel gate (tools/run-offline-pixel-gate.ps1, parent ec414d60
  vs this commit): differing fraction 0.318% (1,791/563,200 compared
  pixels), above the 0.001 threshold. Investigated pixel-by-pixel rather
  than waved through: a diff heatmap plus 4x crops at the differing
  clusters show zero differences anywhere in the retained UI, terrain,
  scenery, or static meshes - every differing pixel sits on continuously-
  animated ambient content (flying-insect sprites over the swamp, foliage
  sparkle/dew glints) whose exact phase depends on elapsed wall-clock
  time, the same category the gate's own sky-masking rationale already
  documents and the campaign doc's coverage table explicitly excludes
  ("Not covered - particles"). Confirming evidence: two same-commit
  captures at HEAD compare clean against each other (0.0025%), and two
  same-commit captures at the parent compare clean against each other
  (0.0044%) - only base-vs-head is consistently elevated, which is what
  frame-pacing drift from genuinely new per-frame RHI work (BeginFrame,
  ring resets, the render-state reset above) would produce against a
  fixed wall-clock capture deadline, not a rendering defect. Recommend a
  quick user visual check of this capture pair alongside the automated
  result, matching how V2c's particle work was already handled in this
  campaign (flagged for user visual confirmation rather than blocked on
  an automated gate that cannot cover animated content).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 18:22:08 +02:00
Erik
10ccce3f2d perf(rendering): retire production entity partition
Route production meshes and attached particles from the exact retained PView frame ranges, while keeping the former WorldEntity partition only for standalone tests and explicit diagnostic/oracle probes. Copy retained source/count facts before arena release so world diagnostics no longer require the old partition. Record the accepted G4 visual gate and open the exact G5 production matrix.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-25 10:50:57 +02:00
Erik
20f9fadb12 Reapply "perf(rendering): draw retained frame product"
This reverts commit 2c848d4167.
2026-07-25 08:36:11 +02:00
Erik
2c848d4167 Revert "perf(rendering): draw retained frame product"
This reverts commit ef1d263337.
2026-07-25 06:28:31 +02:00
Erik
b3427554c3 perf(rendering): retain synchronous frame scratch
Reuse the PView frame input, publish mutation-invalidated landblock views, and avoid constructing optional shadow iterators while preserving title and lifecycle visibility facts.
2026-07-25 05:28:30 +02:00
Erik
ef1d263337 perf(rendering): draw retained frame product
Make the incremental render scene the production entity source at the existing retail PView stages while retaining the accepted dispatcher upload and draw executor. Keep diagnostics consumer-gated, retain ordered indices across unchanged frames, refresh only dirty records, and preserve exact mesh-load, selection, alpha, lighting, and route lifecycle semantics.
2026-07-25 04:12:23 +02:00
Erik
29195fb255 feat(rendering): prove packed dispatcher output parity
Build a compare-only dispatcher classifier from RenderFrameView and compare complete opaque, alpha, selection, clip, light, texture, transform, and draw payloads against the accepted path. Preserve retail's stable equal-CYpt ordering with explicit draw-local submission ordinals so material-group history cannot affect alpha ties.
2026-07-25 01:31:56 +02:00
Erik
d8d9897376 feat(rendering): pack scene dispatcher inputs
Capture immutable presentation payloads in the render scene, flatten unique entity and mesh-part records into the borrowed frame arena, and compare every routed dispatcher input tuple without changing the production draw source.
2026-07-25 00:44:58 +02:00
Erik
e346f8bbaf feat(rendering): compare scene-built PView candidates
Build a same-frame candidate product from incremental render-scene indices and compare its exact PView routes against the accepted current path without changing the production draw source.
2026-07-25 00:12:53 +02:00
Erik
f9b68f8f2a feat(rendering): complete current-path render referee
Extend the non-drawing oracle through ordered PView routes, dispatcher visibility and final instance payloads, and accepted retail selection parts. Lifecycle artifacts can now referee the later shadow scene without influencing production visibility or draw decisions.
2026-07-24 21:28:12 +02:00
Erik
b2b67341ac feat(rendering): establish current-path scene referee
Capture deterministic, landblock-aware fingerprints from the accepted partition only during lifecycle automation. This gives the F/G shadow scene an independent oracle without changing normal draw decisions or adding disabled-path per-entity cost.
2026-07-24 21:13:11 +02:00
Erik
28e1cf8029 refactor(render): extract world scene frame owner
Move fallback and PView world drawing, shared alpha, particles, visibility, selection, and diagnostics behind focused frame owners. Make exceptional frames failure-atomic by restoring the shared GL baseline and preserving primary alpha failures through cleanup.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 07:22:09 +02:00
Erik
85239fb373 refactor(render): extract typed retail pview passes 2026-07-22 06:40:09 +02:00
Erik
749e8ceeb1 fix(rendering): bound portal resource lifetime
Separate logical ownership, render publication, and GPU retirement across live entities, landblocks, particles, textures, mesh arenas, portal/UI teardown, and per-frame scratch storage. Add bounded DAT/texture caches, upload budgets, three-frame fence retirement, exact-incarnation appearance reconciliation, frame pacing, and extensive lifetime conformance coverage.\n\nThe seven-destination connected route now cuts peak working/private memory roughly in half, returns Caul to 125-153 FPS locally, and produces no WER or AMD reset.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
2026-07-18 21:35:16 +02:00
Erik
6b0472ee32 fix(rendering): port retail shared alpha list
Queue translucent world GfxObj batches and scene particles in one stable far-to-near stream using transformed DAT sort centers, then drain it at retail's landscape and final-world boundaries. Preserve authored blend, cull, lighting, opacity, and adjacent-only batching so particles behind lifestones are composited through the crystal instead of overpainting it.

Release build succeeds and all 5,914 tests pass with five intentional skips.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-18 08:14:05 +02:00
Erik
d8984e877f fix #176: light pool tracked the camera via flood scoping - collect residents, anchor at player
The seam-floor purple flicker was NOT a draw z-fight. The in-engine
[seam-*] probe (ACDREAM_PROBE_SEAMDRAW - built because RenderDoc cannot
capture this pipeline: it hides GL_ARB_bindless_texture and the
mandatory-modern startup gate throws; AMD GPU rules out Nsight) killed
every double-draw suspect: ONE shell instance per seam cell at the
lifted z, no floor-coincident entity (portal entities sit at z=-12.05),
zero portal depth fans in the sealed Hub. What it caught instead: the
corridor floor's applied light set flipping wholesale with the flood.

Root cause: c500912b scoped BuildPointLightSnapshot by the per-frame
portal flood, on the research doc's gloss of CEnvCell::visible_cell_table
as "the portal-flood visible set". The named decomp refutes the gloss:
add_visible_cell (0x0052de40) DBObj-LOADS absent cells and inserts them;
a cell activation adds itself + its whole dat visible-cell list
(0x0052e228/0x0052e24a); entries leave only via the flush machinery.
It is the RESIDENT-cell registry - gaze can never remove a cell.
add_dynamic_lights (0x0052d410) walks the WHOLE table per frame
(caller 0x00452d30), and insert_light (0x0054d1b0) caps the pool by
distance to Render::player_pos (0x0054d1dd). Retail's pool is a function
of player position only. Ours followed the camera: turning changed the
flood (probe: 8..41 cells across one turn), the six intensity-100
under-room portal purples entered/left the pool, and the wedge blinked.

Fix: BuildPointLightSnapshot(playerWorldPos) collects ALL registered
(=resident) lit lights; over cap keeps dynamics FIRST (retail's separate
7-slot dynamic pool never competes with statics) then nearest-the-player;
the RebuildScopedLights callback is deleted. Live-verified with the probe:
full-circle turn, flood churning 8..41, the floor set held the same 8
identities on every post-spawn frame. The purple wedge SHAPE stays - it
is cdb-proven retail-faithful.

Residual deviation (AP-85 rewritten): single 128 pool vs retail's
7-dynamic/40-static degrade-scaled dual pools - the Hub now shows
7 purples + viewer where retail's cdb showed 4 + viewer + fixture slots;
if the gate reads the wedge as too purple, the A7 dual-pool cap is the
faithful trim.

Pins: PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant
(rewritten to the corrected model),
PointSnapshot_OverCap_DynamicsNeverEvictedByNearerStatics,
PointSnapshot_OverCap_KeepsNearestThePlayer,
PointSnapshot_ResidentCollection_CellTagDoesNotFilter.
Suites: Core 2599+2skip / App 726+2skip / UI 425 / Net 385.

The [seam-*] probes stay until the visual gate passes, then strip.
Correction banner added to 2026-07-06-a7-per-cell-lighting-pseudocode.md;
outcome banner on the z-fight handoff; ISSUES #176 updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 15:12:31 +02:00
Erik
093cdb6d57 Merge branch 'main' into claude/eloquent-hugle-42119e
# Conflicts:
#	.gitignore
2026-07-06 00:47:09 +02:00
Erik
c500912bf8 feat(lighting): A7 visible-cell light scoping + [indoor-light] probe (NOT the #176/#177 fix)
Port retail's per-frame light collection: the point-light pool is built from ONLY the
currently-visible cells' lights, matching CObjCell::add_*_to_global_lights
(0x0052b350/0x0052b390) walked over CEnvCell::visible_cell_table (0x0052d410) — not a
flat world-space set capped at 128-nearest-camera.

- LightSource.CellId (retail insert_light arg6 -> RenderLight +0x6c); tagged at both
  registration sites from entity.ParentCellId (live weenie fixtures + dat EnvCell statics).
- LightManager.BuildPointLightSnapshot(camPos, visibleCells): a light joins the pool iff
  CellId==0 (viewer/global) or its cell is in the flood. 128 cap kept as a now-non-biting
  backstop (retail's is 40 static + 7 dynamic, 0x0081ec94/8).
- Threaded via RetailPViewDrawContext.RebuildScopedLights, invoked in DrawInside after the
  flood resolves prepareCells and before the draws (renderers select from the same
  in-place-rebuilt PointSnapshot; EnvCellRenderer clears its per-cell cache each pass).
- [indoor-light] probe (ACDREAM_PROBE_INDOOR_LIGHT=1) dumps the scoped-pool SET COMPOSITION.
  Un-skips LightManagerTests.PointSnapshot_HubScaleLightCount_ObjectSelectionIsCameraInvariant.

CORRECTION: the handoff called the camera-cap the "confirmed" #176/#177 mechanism. The probe
PROVES scoping works (291 Hub fixtures -> pool of 1-9, ~285 through-floor lights dropped/frame,
CellIds match the flood), but the user's VISUAL GATE showed BOTH symptoms unchanged. So pool
composition is NOT the cause. #176 real cause = an over-bright purple point light
(intensity=100, color 0.784,0,0.784 -- from [light-detail]); #177 = a portal-visibility miss
(stairs not drawn looking back). Both stay OPEN. This change is retail-faithful and retires the
camera-eviction latent bug; kept as such, not as the symptom fix. Register AP-85 corrected;
ISSUES #176/#177 re-diagnosed; render digest banner updated.

Decomp: insert_light 0x0054d1b0, minimize_object_lighting 0x0054d480, calc_point_light
0x0059c8b0; pseudocode docs/research/2026-07-06-a7-per-cell-lighting-pseudocode.md.
Suites green: Core 2595 + 2 skip, App 719 + 2 skip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 00:35:01 +02:00
Erik
91afea24b4 perf(pipeline): MP-Alloc Task 4 - reuse per-frame animatedIds + drawableCells sets
Completes the safe-batch (implementer finished this but died before
committing; coordinator independently verified bit-identity + safety
post-interruption). Two per-frame HashSet<uint> allocations replaced by
reused cleared-in-place fields:
- GameWindow._animatedIdsScratch: WalkEntitiesInto treats null and empty
  identically (line 682 `is null || Count == 0`), so an always-non-null
  empty set == the old null-when-empty local. Verified.
- RetailPViewRenderer._drawableCellsScratch: flows into
  RetailPViewFrameResult.DrawableCells (live ref) but every consumer reads
  it same-frame; sigDrawableCells is a per-frame OnRender local that
  EmitRenderSignatureIfChanged only FORMATS to a string (no reference
  retention, no _lastSig set field). Built frame N -> read frame N ->
  cleared frame N+1. No cross-frame corruption.

Build green, full suite 4116 passed / 4 skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 00:09:03 +02:00
Erik
76b8b4e9fc perf(pipeline): MP-Alloc Task 3 - pool InteriorEntityPartition.Result
InteriorEntityPartition.Partition allocated a fresh Result (a Dictionary
plus 2 Lists) and a new List<WorldEntity> per newly-seen visible cell,
every frame — the sole call site is RetailPViewRenderer.DrawInside,
once per frame, single-threaded.

Fix: add a Partition(Result, visibleCells, landblockEntries) overload
that clears an existing Result in place (Result.ClearForReuse) and
refills it, reusing each cell's List<WorldEntity> across frames when
the cell key survives (the visible-cell set is normally stable frame to
frame). RetailPViewRenderer now owns one _partitionResult instance
(matching its existing _shellBatch/_buildingGroups/_lookInPrepareScratch
reuse pattern) instead of allocating one per DrawInside call. The
original allocating Partition(visibleCells, landblockEntries) overload
is kept unchanged for tests and one-shot callers (it now delegates to
the reuse overload against a fresh Result).

Bit-identical output required pruning: without removing cell buckets
that end a frame with zero entries, a cell that leaves visibility would
leave a stale empty List sitting in ByCell, changing ByCell.Count and
.Keys enumeration versus the old always-fresh-Dictionary behavior (two
GameWindow diagnostics - sigSceneParticles and FormatPartitionCounts -
read those directly, not just via TryGetValue). Result.PruneEmptyCellBuckets
removes any zero-count bucket after each Partition call, keeping
ByCell's key set exactly what a fresh dictionary would have held.

Verified safe: the sole production consumer (RetailPViewRenderer.
DrawInside) and every downstream reader (WbDrawDispatcher walks,
GameWindow diagnostics) consume partition.ByCell/OutdoorStatic/Dynamics
synchronously within the same frame that built them - no cached
reference survives into the next frame's Partition() call (the one
GameWindow field that stores the reference, _interiorPartition, is
write-only, never read).

dotnet build clean, dotnet test 4120 passed / 4 skipped (unchanged).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 23:47:23 +02:00
Erik
afd5f2a012 fix(streaming): #138-B avatar vanishes after teleporting out — don't relocate during PortalSpace
After a teleport-OUT the per-frame avatar-sync (GameWindow ~:8018) called
GpuWorldState.RelocateEntity with the player controller's cell — which stays the
FROZEN SOURCE cell until PlaceTeleportArrival materializes the destination. So
mid-transit it dragged the avatar (which the teleport's rescue/re-inject had
correctly placed at the destination center) back into the now-UNLOADED source
landblock's pending bucket, where nothing recovers it (RelocateEntity only scans
_loaded). Net: the avatar vanished after teleporting out and stayed gone.

Fix: skip the per-frame relocate while the player is in PortalSpace. The teleport
machinery (DrainRescued + PlaceTeleportArrival) owns the avatar's landblock during
transit; per-frame relocation resumes at FireLoginComplete (InWorld).

Live-verified with a new env-gated avatar-lifecycle probe (ACDREAM_PROBE_ENT /
EntityVanishProbe; [ent] draw-set transitions + [dyn] cull check): pre-fix the
trace showed `[ent] APPEND lb=0x0007FFFF(source) -> PENDING -> DRAWSET ABSENT`
never recovering; post-fix every teleport goes RESCUE -> ABSENT ->
APPEND(destination) -> PRESENT and stays drawn (4 teleports incl. to Holtburg,
session ended PRESENT).

Suites green: Core 1568(+2 skip), App 468(+2 skip), UI 425, Net 317.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:30:20 +02:00
Erik
a9d06a613a chore: strip throwaway dense-town FPS profiling apparatus (plan Task 5)
The FPS deep-dive landed (dense Arwic 75 -> ~165 fps via the cell-object
batching + cell-particle consolidation, both already committed). Remove the
throwaway diagnostic apparatus now that it has served its purpose:

- delete FrameProfiler.cs (whole-frame TimeElapsed + [PASS-GPU] glFinish +
  [CPU-PHASE]/[GPU-PHASE] timers + the =1/=2 ACDREAM_FPS_PROF modes)
- GameWindow: _fpsProf/_frameProfiler/_msaaSamples fields, the BeginFrame/
  EndFrame/MarkUpdateStart hooks, the terrain glFinish, and the landscape
  sub-phase LsMark instrumentation
- RetailPViewRenderer: the DrawInside per-phase Phase()/MarkGpu markers
- ParticleRenderer / PortalDepthMaskRenderer / EnvCellRenderer: the per-pass
  glFinish brackets
- delete DegradeCoverageProbeTests.cs (the dead distance-degrade probe)

KEPT (the real fixes): RetailPViewRenderer cell-object batching + consolidated
cell-particle pass; EnvCellRenderer.CellHasTransparent. Build + full test suite
green (468 App incl. pview replay tests; 1566 Core; 317 Net; 425 UI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 00:10:00 +02:00
Erik
9f51a4db18 perf(pview): consolidate per-cell cell-particle pass into one union draw
DrawCellObjectLists drew cell particles PER visible cell, and each call
(DrawRetailPViewCellParticles -> ParticleRenderer.Draw) re-walked the ENTIRE
live particle set to filter by owner id — O(cells x particles). Measured via
[CPU-PHASE] at dense Arwic this was the cellobjects sink (~5.4 ms CPU; the
phase's GPU share is 0.01 ms — pure CPU). gpu is 0.5 ms; the dense town is
~96% CPU-bound (the earlier "6.7 ms GPU" was a moving/streaming transient).

Static owners are disjoint per cell (InteriorEntityPartition.ByCell partitions
by ParentCellId), so the UNION of survivors (= _allCellStatics, already
accumulated in loop 1 for the batched entity draw) draws EXACTLY the same
emitters in ONE pass: the callback gates on owner id, the renderer sorts
globally back-to-front, and the per-cell slice was never used for clipping
(scissor gate deleted in T3; DisableClipDistances). Runs after the batched
static draw so emitters still depth-test against same-cell statics. Deletes the
loop-2 re-cull and collapses the per-cell BuildDrawList allocations N -> 1
(also eases the GC spikes).

Adversarial review (3 angles) confirmed emitter-set equivalence, no double-
draw, equal-or-better compositing, and no lost per-cell side effect — and found
the OLD code DOUBLE-DREW additive particles for multi-view-polygon cells (one
DrawCellParticles per slice, same owner set each time; the #121 over-bright
class). Consolidation draws each emitter once, fixing that latent bug.

Pixels identical-or-better; draw-mechanism speed only. Build + full suite green
(pview replay tests incl. HouseExitWalkReplay/TowerAscent/Issue130 pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:59:16 +02:00
Erik
290e731ce3 perf(pview): batch per-cell cell-object draws into one cross-cell draw
DrawCellObjectLists called WbDrawDispatcher.Draw once per visible cell (each
orphaning 6 SSBOs + full state setup) — the top CPU-submission sink at dense
Arwic (cellobjects ~3.5ms/frame, measured via [CPU-PHASE]; the frame is
~96% CPU-bound, GPU only 0.5ms). Apply the shipped cells-shell batching
pattern to cell OBJECTS: collapse N per-cell draws into ONE cross-cell draw.

Two-loop structure preserves the statics-before-particles depth order:
  loop 1 — per-cell viewcone cull, accumulate all survivors + the union of
           cell ids;
  one batched DrawEntityBucket for every cell's statics;
  loop 2 — per-cell DrawCellParticles, after the statics own the depth buffer.

Correctness: same survivor set (union visibleCellIds gate is equivalent to the
per-cell {cellId} filter); transparency composites equal-or-better (the
dispatcher sorts opaque front-to-back + transparent back-to-front globally,
WbDrawDispatcher.cs:1469-1470); particles still occlude against same-cell
statics (loop 2 runs after the batched draw); dynamics-last + shells + seals
unchanged. Also drops the per-cell new[]{entry} alloc (~50-100/frame) to one.

Pixels identical — draw-mechanism speed only (render-perf is not faithfulness-
gated per the project steer). Spec: docs/superpowers/specs/2026-06-23-cellobject-
draw-batching-design.md. Build + full test suite green (App pview replay tests
incl. HouseExitWalkReplay/Issue130DoorwayStrip pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:48:20 +02:00
Erik
fe1f81371a chore(diag): FPS_PROF=2 clean split + [CPU-PHASE] DrawInside timers [throwaway]
Decouple the whole-frame TimeElapsed query from the per-pass glFinish so the
CPU-vs-GPU split is honest: ACDREAM_FPS_PROF=2 runs the frame query with NO
per-pass glFinish (PassGpuEnabled stays "1"-gated). Plus [CPU-PHASE] timers
around each DrawInside phase (flood/assemble/prepare/partition/landscape/
portalmask/shells/cellobjects/dynamics) — the CPU analog of [PASS-GPU].

This is what proved the dense town is ~96% CPU-bound (GPU=0.5ms) and that the
cost is per-cell draw submission (cellobjects ~3.5ms), not the portal floods /
punch-seal / clip allocs the static analysis had guessed. Throwaway; strip with
the rest of the FPS apparatus (plan Task 5).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:41:45 +02:00
Erik
8067d3b04a perf(pview): batch EnvCell look-in shell opaque pass (interior-root parity)
DrawBuildingLookIns had the same per-cell heavy Render pattern. Lift the opaque
shell Render out into one per-building batch (after that building's aperture
punches); keep the per-cell loop for transparent (skip-empty) + per-cell statics
/dynamics/particles. User-verified: no missing walls, look-in interiors correct.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:10:28 +02:00
Erik
3af7d0048d perf(pview): batch EnvCell shell opaque pass + skip empty transparent
Dense-town FPS root cause: DrawEnvCellShells called the heavy per-frame
EnvCellRenderer.Render once PER cell x opaque+transparent (~94 calls/frame,
24.75ms = 75% of GPU at Arwic). Batch opaque into one Render(Opaque, allCells)
(z-buffer order; per-instance CellId-keyed lighting => safe) + skip the
transparent Render for opaque-only cells, keep far->near for the rest.

Measured (Arwic, same facing, profiler on): cells 94 calls/24.75ms -> 1 call/
0.37ms; frame 34ms/29fps -> ~10ms/100fps p50.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:51:19 +02:00
Erik
d208002bf8 fix #131 (root cause 4, structurally forced): look-in cells draw their DYNAMICS - the town portal is a server object in the hall's porch cell
The headless replay of the captured indoor frame proved the look-in flood ADMITS the porch 0x017A (Diagnostic_LookInFlood_AdmitsHallPorchFromCottage: 14 cells). So the portal (a SERVER object - the teleport proves it - with ParentCellId 0xA9B4017A) routes to partition.Dynamics and draws NOWHERE under an interior root: dynamics-last viewcone-culls it (the main cone has no look-in cells) and post-seal it would z-fail beyond the root's door plane (the #118 lesson). This is AP-33's own recorded deferral - 'look-in DYNAMICS are not drawn' - the deferred case was the most-stared-at object in town. Outdoors the merge path puts the porch in the main cone -> drawn -> 'appears when I walk out'.

Fix: DrawBuildingLookIns pass 2 draws look-in-cell dynamics with the statics (whole, AP-33 over-include) and their emitters ride the same DrawCellParticles call. No double-draw: dynamics-last keeps culling them; DrawDynamicsParticles only sees its cone survivors. #124 CLOSED by user gate same session. AP-33 row updated. Suites: App 261+1skip / Core 1439+2skip / UI 420 / Net 294 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:52:34 +02:00
Erik
47f32cd45c fix #131 (root cause): look-in cells draw their emitters - the cell-particles pass was missing from the #124 sub-pass
The teleport capture pinned it: walking into the portal flipped pCell to 0xA9B4017A - the hall's PORCH EnvCell. The swirl emitter is owned by a static inside another building's cell. Outdoors the merge path runs the main per-cell pass incl. DrawCellParticles -> visible; under an interior root the #124 look-in sub-pass drew shells + statics but had no cell-particles call. Retail's nested DrawCells draws objects WITH their emitters (DrawObjCellForDummies pc:432878+). Fix: DrawBuildingLookIns pass 2 invokes DrawCellParticles per look-in cell with its static bucket. The owner-cone verdicts were geometrically correct all along (0xC0A9B462 = a porch torch); fixes 1-2 were real-but-adjacent (the unattached pass plugs an independent hole; the alpha deferral fixed #132).

Suites: App 260+1skip / Core 1439+2skip / UI 420 / Net 294 green. Awaiting the swirl gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 20:44:24 +02:00