From 772d69c7a63634ecfd5aecd4d212c25863c30551 Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 27 May 2026 19:54:45 +0200 Subject: [PATCH] =?UTF-8?q?fix(render)+feat(diag):=20Phase=20A8=20?= =?UTF-8?q?=E2=80=94=20sky-when-inside-building=20+=20per-cell=20audit=20p?= =?UTF-8?q?robe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.App/Rendering/GameWindow.cs | 29 +++++++++- .../Rendering/Wb/EnvCellRenderer.cs | 55 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index cfb6a83..3815737 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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) diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index 48927ef..32e1a96 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -103,6 +103,61 @@ public sealed unsafe class EnvCellRenderer : IDisposable return (poolTotal, hwm); } + /// + /// 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 ACDREAM_A8_AUDIT=1 to enable. + /// 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. + /// + public IReadOnlyList CollectCellAuditLines(HashSet<(uint cellId, ulong gfxObjId)> alreadyLogged) + { + var lines = new List(); + 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(); + 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 // ---------------------------------------------------------------------------