feat(app): UCG W2 Task 2 — render root from physics CurrCell (FindCameraCell fallback)

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>
This commit is contained in:
Erik 2026-06-02 10:24:23 +02:00
parent 0e27a6cc3f
commit 02acac5572
3 changed files with 209 additions and 2 deletions

View file

@ -7153,7 +7153,17 @@ public sealed class GameWindow : IDisposable
var visRootPos = (_playerMode && _playerController is not null)
? _playerController.Position
: camPos;
var visibility = _cellVisibility.ComputeVisibility(visRootPos);
// UCG W2: use the physics membership answer (DataCache.CellGraph.CurrCell) as the
// BFS root instead of resolving from position via FindCameraCell. Falls back to the
// original ComputeVisibility path when the physics answer isn't usable yet (null
// CurrCell, or its cell id not yet registered with the render CellVisibility system).
// This closes the render/physics disagreement — both now key off the same BSP-based
// resolution — which is the root cause of the "world from below" spawn flicker.
LoadedCell? physicsRoot = null;
if (_physicsEngine.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell physCell
&& _cellVisibility.TryGetCell(physCell.Id, out var registeredCell))
physicsRoot = registeredCell;
var visibility = _cellVisibility.ComputeVisibilityFromRoot(physicsRoot, visRootPos);
bool cameraInsideCell = visibility?.CameraCell is not null;
// Phase U.4 (2026-05-30): the [vis] probe moved DOWN to the unified