fix(render)+feat(diag): Phase A8 — sky-when-inside-building + per-cell audit probe

Two changes for visual-gate-#1 follow-up. After the pool aliasing fix
(prior commit), walls + objects render cleanly but three residual symptoms
remain: missing floor, purple wall tint, no sky through windows. This
commit addresses one and adds the probe for the second.

Sky fix:
The blanket `!cameraInsideCell` skip of the sky pass was inherited from
when the indoor-cell concept was sealed dungeons. With Phase A8's
RenderInsideOutAcdream pipeline, cottages render through their portals
to outside — and the user expects sky visible through windows + doorways.
WB's VisibilityManager.RenderInsideOut assumes sky has already been
rendered as the far-depth backdrop before stencil setup. New gate:
`!cameraInsideCell || cameraInsideBuilding`. Sky renders inside cottages
(building → portals), skipped inside true dungeons (no portals). The
Step 4 stencil-gated outdoor pass composites terrain + scenery through
portal silhouettes on top of the sky.

Per-cell audit probe (ACDREAM_A8_AUDIT=1):
One-shot dump per (cellId, gfxObjId) pair in the active snapshot:
- renderData null/non-null status
- batches count + total IndexCount
- per-batch CullMode + IsTransparent + IsAdditive + bindless-handle-zero
The first visual gate showed tris=135 for 18 cells — way too low if cell
meshes were complete (expected ~20+ tris/cell). The audit dump will
identify whether (a) some cells aren't uploading, (b) some batches have
zero indices, or (c) batches' CullModes are getting them culled at
typical viewing angles. Without this probe, we'd be back to speculation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-27 19:54:45 +02:00
parent 375f9a7b9b
commit 772d69c7a6
2 changed files with 81 additions and 3 deletions

View file

@ -7257,8 +7257,18 @@ public sealed class GameWindow : IDisposable
// Phase G.1: sky renderer — draws the far-plane-infinity
// celestial meshes FIRST so the rest of the scene z-tests
// on top of them (depth mask off, no depth writes). Skipped
// when indoors; dungeons fully block sky visibility.
// on top of them (depth mask off, no depth writes).
//
// Phase A8 fix (2026-05-28 visual-gate-#1 follow-up): also
// render sky when cameraInsideBuilding=true — cottages have
// portals to outside, and the user expects sky visible through
// windows + doorways. The blanket `!cameraInsideCell` skip
// was incorrect for cottages (only correct for sealed dungeon
// cells). When inside a building, Step 4's stencil-gated
// outdoor pass will composite terrain + scenery through the
// portal silhouettes; sky needs to be there as the far-depth
// backdrop. WB's pipeline (VisibilityManager.RenderInsideOut)
// assumes the sky pass already ran before stencil setup.
//
// Mirrors retail's LScape::draw at 0x00506330 which calls
// GameSky::Draw(0) (sky pass) BEFORE the landblock DrawBlock
@ -7267,7 +7277,8 @@ public sealed class GameWindow : IDisposable
// cylinder 0x01004C42/0x01004C44) need to overlay terrain
// and entities to look volumetric — see the post-scene
// RenderWeather call further below.
if (!cameraInsideCell)
bool renderSky = !cameraInsideCell || cameraInsideBuilding;
if (renderSky)
{
_skyRenderer?.RenderSky(camera, camPos, (float)WorldTime.DayFraction,
_activeDayGroup, kf, environOverrideActive);
@ -10951,6 +10962,8 @@ public sealed class GameWindow : IDisposable
$"vao={vao} prog={prog}");
}
private readonly HashSet<(uint cellId, ulong gfxObjId)> _phaseA8AuditLogged = new();
private void EmitEnvCellProbe(int ourBldgs, int otherBldgs, int filterCnt)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeEnvCellEnabled) return;
@ -10965,6 +10978,16 @@ public sealed class GameWindow : IDisposable
$"[envcells] cells={stats.CellsRendered} tris={stats.TrianglesDrawn} " +
$"ourBldgs={ourBldgs} otherBldgs={otherBldgs} filterCnt={filterCnt} " +
$"poolTotal={pool.PoolTotal} poolHwm={pool.SnapshotPoolHwm}");
// Phase A8 audit (visual-gate-#1 follow-up): one-shot per
// (cellId, gfxObjId) pair dump of cell mesh batch state — to find
// why polys (e.g., floors) might not render. Set ACDREAM_A8_AUDIT=1.
if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_A8_AUDIT"), "1", StringComparison.Ordinal)
&& _envCellRenderer is not null)
{
foreach (var line in _envCellRenderer.CollectCellAuditLines(_phaseA8AuditLogged))
Console.WriteLine(line);
}
}
private void EmitStencilProbe(string op)

View file

@ -103,6 +103,61 @@ public sealed unsafe class EnvCellRenderer : IDisposable
return (poolTotal, hwm);
}
/// <summary>
/// Phase A8 audit probe (2026-05-28 visual-gate-#1 follow-up).
/// One-shot per (cellId, gfxObjId) pair: dumps batch counts + CullModes +
/// transparency flags + bindless-handle-non-zero status, so the operator
/// can read offline and identify why specific polys (e.g., floors) aren't
/// rendering. Set <c>ACDREAM_A8_AUDIT=1</c> to enable.
/// <para>Returns a deduplicated audit-line list per Render snapshot
/// (one entry per (cellId, gfxObjId) seen in BatchedByCell). The caller
/// (GameWindow EmitEnvCellProbe) prints these and tracks which pairs
/// have already been logged.</para>
/// </summary>
public IReadOnlyList<string> CollectCellAuditLines(HashSet<(uint cellId, ulong gfxObjId)> alreadyLogged)
{
var lines = new List<string>();
lock (_renderLock)
{
var snap = _activeSnapshot;
foreach (var (cellId, gfxDict) in snap.BatchedByCell)
{
foreach (var (gfxObjId, transforms) in gfxDict)
{
var key = (cellId, gfxObjId);
if (alreadyLogged.Contains(key)) continue;
alreadyLogged.Add(key);
var rd = _meshManager.TryGetRenderData(gfxObjId);
if (rd is null)
{
lines.Add($"[a8-audit] cell=0x{cellId:X8} gfx=0x{gfxObjId:X10} instances={transforms.Count} renderData=null");
continue;
}
int totalIdx = 0;
var cullModes = new HashSet<DatReaderWriter.Enums.CullMode>();
int translucent = 0;
int additive = 0;
int zeroHandle = 0;
foreach (var b in rd.Batches)
{
totalIdx += b.IndexCount;
cullModes.Add(b.CullMode);
if (b.IsTransparent) translucent++;
if (b.IsAdditive) additive++;
if (b.BindlessTextureHandle == 0) zeroHandle++;
}
var cullList = string.Join(",", cullModes);
lines.Add(
$"[a8-audit] cell=0x{cellId:X8} gfx=0x{gfxObjId:X10} instances={transforms.Count} " +
$"isSetup={rd.IsSetup} batches={rd.Batches.Count} totalIdx={totalIdx} " +
$"cull=[{cullList}] translucent={translucent} additive={additive} zeroHandle={zeroHandle}");
}
}
}
return lines;
}
// ---------------------------------------------------------------------------
// Constructor + Initialize
// ---------------------------------------------------------------------------