ComputeVisibilityFromRoot(null, …) now returns null (outdoor root) instead of
calling FindCameraCell(fallbackPos). Retail CellManager::ChangePosition
(0x004559B0) reads the transition-owned curr_cell — it does NOT re-derive from
a static position. W2a guarantees CurrCell is set from the first tick, so the
AABB fallback is dead. Deleted: FindCameraCell (389–446), _lastCameraCell,
_cellSwitchGraceFrames, CellSwitchGraceFrameCount. GetVisibleCells retains a
brute-force AABB scan for test-compat; ComputeVisibility stays for the same
reason. Updated 3 null-root tests in CellVisibilityFromRootTests to assert the
new null-returns-null behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the BFS visibility root to DataCache.CellGraph.CurrCell (the physics
membership answer written in W2 Task 1) rather than resolving independently
from a position via FindCameraCell. Closes the render/physics disagreement
that causes the "world from below" spawn-in flicker.
Changes:
- CellVisibility.GetVisibleCells: extracted BFS body into new private
GetVisibleCellsFromRoot(LoadedCell root, Vector3 cameraPos); existing
GetVisibleCells delegates to it after FindCameraCell (behavior unchanged).
- CellVisibility.ComputeVisibilityFromRoot(LoadedCell? root, Vector3 fallbackPos):
new public entry point; when root is null falls through to ComputeVisibility
(exact today's behavior), otherwise sets _lastCameraCell = root and delegates
to GetVisibleCellsFromRoot — cannot regress below baseline.
- GameWindow (line 7156): replaced ComputeVisibility(visRootPos) with
ComputeVisibilityFromRoot(physicsRoot, visRootPos) where physicsRoot is
resolved from _physicsEngine.DataCache.CellGraph.CurrCell via TryGetCell.
physicsRoot is null whenever CurrCell is null or its id is not yet in the
render registry, so the fallback fires until the cell loads.
- 6 new tests in CellVisibilityFromRootTests: null-root fallback equivalence
(3 cases), registered root → CameraCell == root (3 cases). All 160 App.Tests
pass, 0 regressions.
Visual verification PENDING — behavior change; do not claim it works visually.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per-frame (not cell-change-throttled, so it catches the flicker at a stable root):
[flap] line from the builder — root cell's per-portal side-test D + traverse/cull +
NDC projection, plus OutsideView poly count + visible-cell count; localEye exposes
when the eye has crossed an interior portal plane. Paired [flap-cam] line from the
draw site — FindCameraCell resolution branch (CameraCellResolution enum, new),
eyeInRoot AABB flag (stale-root signal), eye + player worldpos, and the frame's
TerrainMode/OutdoorVisible outcome. Disambiguates side-cull vs empty-projection vs
stale-root. Inert when off (gated). Throwaway apparatus to converge the flap fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
VisibleCells (full ids) + SeenOutside, populated at the EnvCell-build site from
envCell.VisibleCells + envCell.Flags. Mirrors retail CEnvCell.stab_list /
seen_outside (acclient.h ~30925). Data already in-process; render path no longer
drops it. Consumed by the builder in U.4c-3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code review caught a CRITICAL under-inclusion: ApplyReciprocalClip scanned for the
first OtherCellId match, so a cell with two portals to the same neighbour clipped both
near-side openings against the FIRST reciprocal polygon — hiding geometry through the
second opening (real on Holtburg cellar cells 0x148<->0x149). Plumb the dat's
OtherPortalId back-link through CellPortalInfo + BuildLoadedCell and index the reciprocal
directly (retail arg2->other_portal_id, 433557). Skip (degrade to over-include) when the
index is unresolvable — never clip against a guessed polygon. Adds a disjoint two-back-
portal regression test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lands the working A8 indoor-rendering and streaming fixes accumulated this
session. User has verified these visually to some degree (e.g. lifestone /
translucent meshes confirmed fine under the FrontFace flip; bridge / wall /
collision regressions confirmed fixed after travel); not every path has been
exhaustively gated. The cellar-flap defect remains OPEN and will be solved
the retail-faithful way via a dedicated brainstorm (see handoff docs).
Rendering core (reviewed, high confidence):
- EnvCellRenderer SSBO stride fix: upload packed Matrix4x4[] (64B) instead of
the 80B CPU InstanceData struct the shader never expected — fixes the
transform/texture "explosion" for any draw with >1 instance (cells that
dedupe to a shared cellGeomId). Real root cause.
- WB-style global FrontFace(CW) + per-batch CullMode carried through the MDI
layout (GroupKey + BuildIndirectArrays + DrawIndirectRange split into
same-cull runs with absolute uDrawIDOffset per run).
- EntitySet partitioning (IndoorPass / OutdoorScenery / LiveDynamic) +
WorldEntity.BuildingShellAnchorCellId so building shells scope to their
dat-derived building cell instead of rendering everywhere.
- RenderOutsideInAcdream (look into buildings from outside) +
CollectVisiblePortalBuildings frustum cull of portal bounds.
- Sky-when-inside-building + per-cell audit probe + GL-state probe.
Streaming / perf (test-covered; not independently code-reviewed this session):
- Near/far priority queues so near work wins over far; PromoteToNear carries
full landblock + mesh data; LandblockEntriesWithoutAnimatedIndex avoids
rebuilding the animated-lookup dict in the hot draw path. Fixes the
bridge-not-appearing / missing-walls / broken-collision-after-travel
regressions and improves post-transition FPS.
Tooling + docs:
- tools/A8CellAudit: offline dat cell/portal/building dumper (portals +
buildings modes) — reproduces the cellar-flap investigation with no launch.
- docs/research cellar-flap root-cause + option-2 handoff (the didInsideStencil
double-duty finding + the WB-recursive design decision + brainstorm prompt),
entity-taxonomy, replan, issue-78 visibility investigation.
Diagnostics retained on purpose: ACDREAM_A8_DIAG_* gates, portal_stencil.vert
provisional pos.w clamp, and the probe families are kept (env-var gated, zero
cost when off) because the pending option-2 cellar-flap brainstorm needs them.
Strip in the option-2 ship commit.
Indoor branch stays behind ACDREAM_A8_INDOOR_BRANCH=1 (default off = pre-A8
visual). Build green; App tests + Core (streaming/dispatcher/loader) tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First visual-gate launch showed 8,737 [vis] lines (player at Holtburg
cottage cell 0xA9B40143, inside=True really=True) but ZERO [buildings] /
[envcells] / [stencil] / [draworder] probe emissions. Root cause: same as
the original RR7.1 saga — BuildingLoader.Build was passed only the
per-frame drainedCells dict, missing cells loaded on PRIOR frames. Those
cells stayed with BuildingId=null, the strict cameraInsideBuilding gate
returned false, the indoor branch never fired.
Fix: in ApplyLoadedTerrainLocked, merge drainedCells with the cells
already registered in _cellVisibility for the same landblock prefix
before passing to BuildingLoader. The richer dict ensures the stamping
loop in BuildingLoader.Build covers EVERY cell in this landblock.
Added IReadOnlyList<LoadedCell> GetCellsForLandblock(uint lbId) on
CellVisibility — minimal API expose; existing _cellsByLandblock dict
was already the right shape (lbId = upper 16 bits).
Build green. Tests unchanged.
Next: relaunch the client. With the fix, [buildings] probe should fire
with camBldgs=[0x1,...] when the player is inside a Holtburg cottage,
[envcells] should report cells>=1 tris>=1 per indoor frame, and the
indoor branch should be exercising the WB-faithful Steps 1-5 pipeline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RR7 visual gate (2026-05-27) revealed the indoor branch NEVER fired even
when the strict gate's PointInCell + non-null CameraCell hit: 17,748
inside=True frames, 0 branch=indoor decisions. Root cause: RR4 wired
BuildingLoader.Build with the per-frame drainedCells dict — cells that
streamed in on earlier frames (the common case, since cells arrive
asynchronously over many frames after the landblock-info completion)
were not in drainedCells, so the BFS short-circuited and the registry's
EnvCellIds set was systematically incomplete. Cells loaded ahead of
lbInfo arrival never got their BuildingId stamped.
Fix has two parts:
1. CellVisibility.AllLoadedCells — new public IReadOnlyDictionary
exposing the existing private _cellLookup. BuildingLoader.Build at
landblock-info-arrival now walks the full cell set, not just this
frame's drain.
2. _pendingCells drain loop — late-stamps BuildingId on each arriving
cell if its landblock's BuildingRegistry already exists. Covers cells
that arrive AFTER the registry-build pass.
Together these handle all four timing cases:
- Cells loaded before lbInfo arrives → stamped in BuildingLoader.Build
- Cells loaded with lbInfo (same frame) → stamped in BuildingLoader.Build
- Cells loaded after lbInfo arrives → stamped in drain loop
- lbInfo never arrives (LB has no info) → registry never built, cells
stay at BuildingId == null
(intended — flow through outdoor
render path)
Probe data from the failed gate launch confirmed cell 0xA9B40150
(cottage idx=6 cellar from the #98 saga) was reached as the camera cell
with visN=16 visible neighbours, but BuildingId stayed null. This fix
gets the indoor branch fired in that scenario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LoadedCell.BuildingId (init + internal setter) — set exactly once at
landblock load time by BuildingLoader; null when the cell isn't
part of any building (outdoor surface cells; dungeon cells not
enumerated in LandBlockInfo.Buildings).
GameWindow landblock-load path: builds BuildingRegistry from
LandBlockInfo.Buildings; stamps each cell's BuildingId; stores the
registry on _buildingRegistries[landblockId] (GameWindow-level dict)
for render-frame lookups. Note: LoadedLandblock is AcDream.Core.World
(a sealed record) — adding an App-type field there would violate
Code Structure Rule #2, so the registry is stored in a new
GameWindow-level dictionary instead. Cleanup wired in both
removeTerrain lambdas (OnLoad + OnResize paths).
drainedCells dict: the existing _pendingCells drain loop is extended
to also build a local CellId→LoadedCell dict; BuildingLoader.Build
uses this dict for the stamping pass so no second iteration is needed.
New BuildingLoaderTest verifies the stamping path. 5 BuildingLoader
tests total (4 from RR3 + 1 new).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First slice of the indoor-cell visibility culling pipeline (#78). Adds
PortalPolygons: List<Vector3[]> to LoadedCell, parallel-indexed to the
existing Portals + ClipPlanes lists. Empty arrays for portals whose
polygon could not be resolved. Field is populated in Task 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User report: third-person chase camera enters interiors before the
player body does, so the camera-based cameraInsideCell flag was
flipping the scene to indoor lighting prematurely (ambient drops to
0.2 white before the player has actually crossed the doorway).
Retail keys lighting off the PLAYER's cell. CellManager::ChangePosition
@ 0x004559B0 reads CObjCell::seen_outside on the player's current
cell — never on the camera. Match that semantics.
- CellVisibility.IsInsideAnyCell(Vector3): new non-caching brute-force
scan that's safe to call alongside ComputeVisibility(cameraPos)
without thrashing the camera cell cache.
- GameWindow render loop: derive playerInsideCell from the player's
Position when in player mode, otherwise fall back to cameraInsideCell
(orbit/fly debug camera).
- UpdateSunFromSky now takes playerInsideCell. The sky-render and
depth-buffer-clear decisions still use cameraInsideCell — those are
legitimately camera-POV concerns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port ACME's EnvCellManager portal visibility system:
- New CellVisibility class: BFS portal traversal from camera cell,
portal-side clip-plane test, FindCameraCell with grace period
- LoadedCell data populated during streaming (portals, clip planes,
world/inverse transforms, local AABB from CellStruct vertices)
- WorldEntity.ParentCellId tags interior entities for filtering
- InstancedMeshRenderer.Draw accepts optional visibleCellIds set —
interior entities whose parent cell isn't visible are skipped
- Conditional depth clear between terrain and static mesh when
camera is inside a cell (ACME GameScene.cs pattern)
When camera is outdoors, all interiors render (visibleCellIds=null).
When camera enters a building, only BFS-reachable cells render.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>