From 877e935ac41f05c84628b6fa1a43a1af38c3e659 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 31 Aug 2026 04:15:55 +0200 Subject: [PATCH] refactor(render): publish the walk visible-cell set --- .../Rendering/RetailPViewRenderer.cs | 44 +++++++------------ .../Rendering/WorldSceneRenderer.cs | 17 +++---- src/AcDream.Core/Lighting/LightManager.cs | 5 +-- .../Rendering/WorldSceneRendererTests.cs | 9 ++-- 4 files changed, 29 insertions(+), 46 deletions(-) diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index a464a7bf..301a41ed 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -442,14 +442,18 @@ public sealed class RetailPViewRenderer int terrainUploadCount = checked(1 + clipAssembly.OutsideViewSlices.Length * 2); passes.PrepareClipFrame(terrainUploadCount); - // #124: look-in cells need prepared shell batches + their statics routed - // into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main - // cell-object pass iterates pvFrame.OrderedVisibleCells, which never - // contains them). drawableCells itself stays the MAIN flood: it feeds the - // seals, the outside-stage predicate, and the frame result. + // Production prepares exactly the walk's visited-cell set. The + // legacy builder's main-flood/look-in union survives only for + // standalone fake executors until the shell-deletion slice. var prepareCells = drawableCells; _lookInCellIds.Clear(); - if (_lookInFrames.Count > 0 || walkActive) + if (walkActive) + { + _lookInPrepareScratch.Clear(); + _lookInPrepareScratch.UnionWith(walkDriver!.VisitedCells); + prepareCells = _lookInPrepareScratch; + } + else if (_lookInFrames.Count > 0) { _lookInPrepareScratch.Clear(); _lookInPrepareScratch.UnionWith(drawableCells); @@ -461,13 +465,6 @@ public sealed class RetailPViewRenderer _lookInCellIds.Add(c); } } - if (walkActive) - { - // The walk's own flood/look-in cell set — unioned in (never - // aliased with drawableCells, which the outside-stage and seal - // predicates below still need scoped to the OLD flood only). - _lookInPrepareScratch.UnionWith(walkDriver!.VisitedCells); - } prepareCells = _lookInPrepareScratch; } @@ -2876,20 +2873,11 @@ public sealed class RetailPViewFrameResult public HashSet DrawableCells { get; private set; } = null!; /// - /// Every cell this completed view actually reached: the main flood - /// () plus the building look-in cells. This is - /// retail's per-cell in_view answer for effect consumers — - /// CPhysicsObj::ShouldDrawParticles @0x0050FE60 gates on - /// cell->IsInView(), and a cell entered through a building portal - /// (PView::ConstructView @0x005A57B0, installed by - /// RenderDeviceD3D::DrawBuilding @0x0059F2A0) is drawn by the same - /// PView::DrawCells traversal as a flooded cell, so retail marks it - /// in view identically. acdream's look-in adaptation keeps those cells out - /// of (seals / outside-stage predicate stay - /// main-flood scoped, #124); particle and light visibility must consume - /// THIS set or look-in rooms render with frozen emitters and dark lights. + /// The production retail walk's exact visited-cell set. This is the one + /// visibility answer consumed by EnvCell preparation, particles, lights, + /// and directional-shadow filtering. /// - public HashSet InViewCells { get; private set; } = null!; + public HashSet VisibleCells { get; private set; } = null!; internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; } internal RenderProjectionCounts SourceCounts { get; private set; } @@ -2900,7 +2888,7 @@ public sealed class RetailPViewFrameResult PortalVisibilityFrame portalFrame, ClipFrameAssembly clipAssembly, HashSet drawableCells, - HashSet inViewCells, + HashSet visibleCells, RenderFrameDiagnosticCounts diagnosticCounts, RenderProjectionCounts sourceCounts, InteriorEntityPartition.Result? diagnosticPartition) @@ -2908,7 +2896,7 @@ public sealed class RetailPViewFrameResult PortalFrame = portalFrame; ClipAssembly = clipAssembly; DrawableCells = drawableCells; - InViewCells = inViewCells; + VisibleCells = visibleCells; DiagnosticCounts = diagnosticCounts; SourceCounts = sourceCounts; DiagnosticPartition = diagnosticPartition; diff --git a/src/AcDream.App/Rendering/WorldSceneRenderer.cs b/src/AcDream.App/Rendering/WorldSceneRenderer.cs index 39ea601d..cfad2750 100644 --- a/src/AcDream.App/Rendering/WorldSceneRenderer.cs +++ b/src/AcDream.App/Rendering/WorldSceneRenderer.cs @@ -217,6 +217,8 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase // this phase only enforces it. _passes.DrawFlatTerrain(in camera, roots.PlayerLandblockId); terrainDrawn = true; + if (_passes.TerrainVisibleCellIds is { } flatTerrainCells) + _particleVisibility.MarkVisibleCells(flatTerrainCells); } else { @@ -254,14 +256,11 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase camera.Camera.View, _diagnostics.CameraCellResolution)); - // Effect visibility consumes InViewCells (main flood ∪ look-in - // cells), not the flood-only DrawableCells: retail's - // ShouldDrawParticles @0x0050FE60 asks cell->IsInView(), and a - // look-in cell drawn via DrawBuilding @0x0059F2A0 is in view - // exactly like a flooded cell. Flood-only scoping froze - // emitters and darkened lights in visible adjacent rooms. - _particleVisibility.MarkVisibleCells(pviewResult.InViewCells); - _frames.ObserveDrawableCells(pviewResult.InViewCells); + // One visibility answer: the walk's exact visited-cell set + // feeds effects and the frame environment (point lights and + // directional-shadow filtering). + _particleVisibility.MarkVisibleCells(pviewResult.VisibleCells); + _frames.ObserveDrawableCells(pviewResult.VisibleCells); _diagnostics.EmitPViewInput( pviewResult.PortalFrame, camera.ViewProjection, @@ -441,8 +440,6 @@ internal sealed class WorldSceneRenderer : IPreparedWorldSceneFramePhase private void CompleteWorldFrame() { _alpha.EndFrame(); - if (_passes.TerrainVisibleCellIds is { } visibleTerrainCells) - _particleVisibility.MarkVisibleCells(visibleTerrainCells); _particleVisibility.CompleteFrame(); } diff --git a/src/AcDream.Core/Lighting/LightManager.cs b/src/AcDream.Core/Lighting/LightManager.cs index 7a63e96c..0fb11c00 100644 --- a/src/AcDream.Core/Lighting/LightManager.cs +++ b/src/AcDream.Core/Lighting/LightManager.cs @@ -271,9 +271,8 @@ public sealed class LightManager /// geometrically closer than the player's own room's torches and win the cap, /// leaving the visible room dark. Scoping candidacy to the frame's actual /// visible cells (the render already computes this — callers pass last frame's - /// RetailPViewFrameResult.InViewCells, the main flood PLUS building - /// look-in cells, one frame of latency, to avoid re-threading a mid-render - /// callback; flood-only scoping darkened look-in rooms' lanterns) removes + /// retail-walk visited-cell set, one frame of latency, to avoid re-threading + /// a mid-render callback) removes /// non-visible cells from contention before the /// cap ever applies. The distance-sort anchor stays the PLAYER either way — this /// parameter only narrows candidacy, it does not change the sort (the #176 diff --git a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs index c67028eb..ed7400f3 100644 --- a/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs +++ b/tests/AcDream.App.Tests/Rendering/WorldSceneRendererTests.cs @@ -180,6 +180,7 @@ public sealed class WorldSceneRendererTests "flat:clip", "flat:sky", "flat:terrain", + "visibility:mark", "flat:entities", "frame:clear-cells", "passes:disable-clip", @@ -188,7 +189,6 @@ public sealed class WorldSceneRendererTests "diagnostics:signature:OutdoorRoot", "diagnostics:draw", "alpha:end", - "visibility:mark", "visibility:complete", "selection:complete", ], @@ -206,7 +206,7 @@ public sealed class WorldSceneRendererTests } [Fact] - public void PViewWorld_UsesOnePViewProductAndPublishesItsDrawableCells() + public void PViewWorld_PublishesTheWalkVisibleCellsOnce() { var root = new LoadedCell { @@ -233,7 +233,6 @@ public sealed class WorldSceneRendererTests "diagnostics:signature:RetailPViewInside", "diagnostics:draw", "alpha:end", - "visibility:mark", "visibility:complete", "selection:complete", ], @@ -267,8 +266,8 @@ public sealed class WorldSceneRendererTests // @0x0050FE60), and a cell entered through a building portal // (DrawBuilding @0x0059F2A0 -> PView::ConstructView @0x005A57B0) is // drawn by the same traversal as a flooded cell. The particle gate and - // the light-candidate scope must therefore receive InViewCells (flood - // plus look-ins), not the flood-only DrawableCells. + // light-candidate scope must therefore receive the walk's one visited + // set, including look-ins. Assert.Contains(0x01010003u, rig.Visibility.MarkedCells); Assert.NotNull(rig.Frames.ObservedCells); Assert.Contains(0x01010003u, rig.Frames.ObservedCells!);