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 // ---------------------------------------------------------------------------