From c4fd71149a7b75f287fa213583daa19fb800d2d2 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 20:01:53 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20R1=20=E2=80=94=20binary=20rende?= =?UTF-8?q?r=20decision,=20indoor=20=3D=20per-cell=20DrawInside=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GameWindow.OnRender: when clipRoot != null, run only InteriorRenderer.DrawInside (per-cell shells + per-cell objects + live-dynamics); the global entity pass + global shell pass are no longer issued indoors. Outdoor scenery drawn clipped to the doorway (after terrain, before the Z-clear). Outdoor root path unchanged. pvFrame hoisted so the splice reads OrderedVisibleCells; per-frame 3-bucket partition built on the indoor root. Retail RenderNormalMode @ 0x453aa0. InteriorRenderer amended with a DrawableCells membership filter (an IsNothingVisible cell can be in OrderedVisibleCells but absent from CellIdToSlot — iterate for ORDER, filter for membership; matches the old envCellShellFilter set exactly). Build green, 174/174 App tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 90 +++++++++++++------ src/AcDream.App/Rendering/InteriorRenderer.cs | 8 ++ 2 files changed, 72 insertions(+), 26 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 8ea9f6a..db8206c 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -166,6 +166,12 @@ public sealed class GameWindow : IDisposable private AcDream.App.Rendering.Wb.EnvCellRenderer? _envCellRenderer; private AcDream.App.Rendering.Wb.WbFrustum? _envCellFrustum; + // R1 (render redesign): the per-cell DrawInside flood + its per-frame entity partition. + // _interiorRenderer is constructed once both renderers exist; _interiorPartition is rebuilt + // each frame on an indoor root (null on the outdoor root). + private AcDream.App.Rendering.InteriorRenderer? _interiorRenderer; + private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition; + // Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain // UBO). In U.3 a single ClipFrame.NoClip() instance is created lazily (??=) and // REUSED across frames — its GL buffers persist; only the cheap CPU-side no-clip @@ -1796,6 +1802,9 @@ public sealed class GameWindow : IDisposable _envCellRenderer = new AcDream.App.Rendering.Wb.EnvCellRenderer( _gl, _wbMeshAdapter!.MeshManager!, _envCellFrustum); _envCellRenderer.Initialize(_meshShader!); + + // R1: the per-cell DrawInside flood. Both renderers exist here (just constructed). + _interiorRenderer = new AcDream.App.Rendering.InteriorRenderer(_envCellRenderer!, _wbDrawDispatcher!); } // Phase G.1 sky renderer — its own shader (sky.vert / sky.frag) @@ -7305,6 +7314,7 @@ public sealed class GameWindow : IDisposable _clipFrame ??= ClipFrame.NoClip(); var clipRoot = visibility?.CameraCell; ClipFrameAssembly? clipAssembly = null; + PortalVisibilityFrame? pvFrame = null; // R1: hoisted so the binary decision below reads OrderedVisibleCells var terrainClipMode = TerrainClipMode.Planes; // overwritten below for indoor root System.Numerics.Vector4 terrainScissorNdc = default; HashSet? envCellShellFilter = null; // drawable visible cells (cellIdToSlot keys) @@ -7313,7 +7323,7 @@ public sealed class GameWindow : IDisposable // Phase U.4c: side test + distance ordering use the PLAYER position (visRootPos, // stable inside the cell); projection uses the eye's envCellViewProj (the screen // view). See the visRootPos rationale at the ComputeVisibility call above. - var pvFrame = PortalVisibilityBuilder.Build( + pvFrame = PortalVisibilityBuilder.Build( clipRoot, visRootPos, id => _cellVisibility.TryGetCell(id, out var c) ? c : null, @@ -7332,6 +7342,12 @@ public sealed class GameWindow : IDisposable // map's keys; IsNothingVisible cells were excluded by the assembler). envCellShellFilter = new HashSet(clipAssembly.CellIdToSlot.Keys); + // R1: partition this frame's entities into per-cell / outdoor / live-dynamic buckets + // for the DrawInside flood + the outdoor-scenery-through-door draw. Keyed by the SAME + // visible-cell set the shells use (cellIdToSlot.Keys). + _interiorPartition = AcDream.App.Rendering.InteriorEntityPartition.Partition( + envCellShellFilter, _worldState.LandblockEntries); + // [vis] probe (ACDREAM_PROBE_VIS=1) — the real PortalVisibilityFrame // numbers, replacing the old camera-state-only spike. Cell-change // throttled inside EmitVis so launch.log stays readable under motion. @@ -7368,6 +7384,7 @@ public sealed class GameWindow : IDisposable _clipFrame.Reset(); _wbDrawDispatcher?.ClearClipRouting(); _envCellRenderer?.SetClipRouting(null); + _interiorPartition = null; // R1: no indoor flood on the outdoor root } _clipFrame.UploadShared(_gl); @@ -7512,6 +7529,22 @@ public sealed class GameWindow : IDisposable animatedIds.Add(k); } + // R1: outdoor scenery (ParentCellId == null) is part of the landscape seen through the + // doorway (retail LScape::draw draws the exterior, clipped to OutsideView). Drawn here — + // after terrain, BEFORE the Z-clear — only on an indoor root, scoped to the outdoor bucket. + // ResolveEntitySlot routes these (ParentCellId == null) to OutdoorSlot when OutdoorVisible, + // else CULLs them, via the SetClipRouting installed above. visibleCellIds: null ⇒ they pass + // the membership gate (no cell filter) and are gated purely by the clip slot. + if (clipAssembly is not null && _interiorPartition is not null + && _interiorPartition.Outdoor.Count > 0 && clipAssembly.OutdoorVisible) + { + var sceneryEntry = (playerLb ?? 0u, System.Numerics.Vector3.Zero, System.Numerics.Vector3.Zero, + (IReadOnlyList)_interiorPartition.Outdoor, + (IReadOnlyDictionary?)null); + _wbDrawDispatcher!.Draw(camera, new[] { sceneryEntry }, frustum, + neverCullLandblockId: playerLb, visibleCellIds: null, animatedEntityIds: animatedIds); + } + // ── [Stage 4] conditional doorway Z-clear ─────────────────────────────────── // Retail PView::DrawCells @ pseudo_c:432731: after the landscape (sky + terrain) is drawn // through the exit portal, RenderDevice->Clear(flag 4 = Z-BUFFER ONLY, NOT color) resets @@ -7527,31 +7560,36 @@ public sealed class GameWindow : IDisposable if (_zc) _gl.Disable(EnableCap.ScissorTest); } - // Phase U.4: render the indoor cell SHELLS (walls / floors / ceilings) - // — previously DORMANT (EnvCellRenderer.Render was never called in the - // live loop). Inside the clip bracket so each cell's instances are gated - // to its CellClip slot via the binding=3 map we installed above. Opaque - // pass BEFORE the entity dispatcher (front-to-back, depth writes on); - // Transparent pass AFTER. Filter = the drawable visible cells. Only when - // there's an indoor root (clipAssembly != null) — outdoor frames draw no - // shells. PrepareRenderBatches already ran earlier this frame. - if (clipAssembly is not null && envCellShellFilter is not null) - _envCellRenderer?.Render(AcDream.App.Rendering.Wb.WbRenderPass.Opaque, envCellShellFilter); - - // Scene entity draw. N.5: WbDrawDispatcher is always non-null - // (modern path mandatory). Default EntitySet.All — every entity - // walked, gated only by the ParentCellId ∈ visibleCellIds filter. - // Phase U.4: per-instance clip slots come from SetClipRouting above - // (indoor root) or ClearClipRouting (outdoor root → every instance slot 0). - _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, - neverCullLandblockId: playerLb, - visibleCellIds: visibility?.VisibleCellIds, - animatedEntityIds: animatedIds); - - // Phase U.4: cell shells transparent pass (additive / alpha-blend cell - // surfaces, e.g. stained glass). Still inside the clip bracket. - if (clipAssembly is not null && envCellShellFilter is not null) - _envCellRenderer?.Render(AcDream.App.Rendering.Wb.WbRenderPass.Transparent, envCellShellFilter); + // R1 — the binary render decision (retail RenderNormalMode @ 0x453aa0): + // INDOOR root (clipRoot != null): run ONLY the per-cell DrawInside flood. The global + // entity pass + global shell pass are NOT issued — visibility IS the cull, so the + // outdoor world cannot bleed (it is never iterated; outdoor scenery entered above, + // clipped to the doorway). DrawInside draws per-cell shells (opaque + transparent) + + // per-cell objects + live-dynamics, closest-first over the drawable visible cells. + // OUTDOOR root: the existing global entity pass (no shells, no DrawInside). + if (clipRoot is not null && _interiorRenderer is not null + && _interiorPartition is not null && envCellShellFilter is not null) + { + var interiorCtx = new AcDream.App.Rendering.InteriorRenderContext + { + OrderedVisibleCells = pvFrame!.OrderedVisibleCells, + DrawableCells = envCellShellFilter, + Partition = _interiorPartition, + Camera = camera, + Frustum = frustum, + PlayerLandblockId = playerLb, + AnimatedEntityIds = animatedIds, + }; + _interiorRenderer.DrawInside(interiorCtx); + } + else + { + // Outdoor root: the global entity pass (unchanged). + _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, + neverCullLandblockId: playerLb, + visibleCellIds: visibility?.VisibleCellIds, + animatedEntityIds: animatedIds); + } // Phase U.3: close the world-geometry clip bracket opened above. From here down the // scene particles, debug lines, and UI use shaders that do NOT write gl_ClipDistance, so diff --git a/src/AcDream.App/Rendering/InteriorRenderer.cs b/src/AcDream.App/Rendering/InteriorRenderer.cs index ec0dd26..110f898 100644 --- a/src/AcDream.App/Rendering/InteriorRenderer.cs +++ b/src/AcDream.App/Rendering/InteriorRenderer.cs @@ -11,6 +11,12 @@ public sealed class InteriorRenderContext /// Visible cells, closest-first (retail cell_draw_list). From PortalVisibilityFrame. public required IReadOnlyList OrderedVisibleCells { get; init; } + /// The cells the assembler mapped a clip slot for (ClipFrameAssembly.CellIdToSlot.Keys = + /// the GameWindow envCellShellFilter). A cell may appear in but + /// reduce to IsNothingVisible in the assembler (no slot) — those are skipped. This is the + /// membership filter; supplies the draw ORDER. + public required IReadOnlySet DrawableCells { get; init; } + /// The 3-bucket entity split (). Only ByCell + /// LiveDynamic are used here; Outdoor scenery is drawn by the caller's landscape-through-door /// step (clipped to OutsideView). @@ -54,6 +60,7 @@ public sealed class InteriorRenderer // Loop A — per-cell OPAQUE shell + that cell's static objects (closest-first). foreach (uint cellId in ctx.OrderedVisibleCells) { + if (!ctx.DrawableCells.Contains(cellId)) continue; // no clip slot ⇒ assembler culled it _oneCell.Clear(); _oneCell.Add(cellId); _envCells.Render(WbRenderPass.Opaque, _oneCell); @@ -70,6 +77,7 @@ public sealed class InteriorRenderer // Loop B — per-cell TRANSPARENT shells (stained glass / additive cell surfaces). foreach (uint cellId in ctx.OrderedVisibleCells) { + if (!ctx.DrawableCells.Contains(cellId)) continue; _oneCell.Clear(); _oneCell.Add(cellId); _envCells.Render(WbRenderPass.Transparent, _oneCell);