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