From 58822fed964945d50bb97a53373ce478e6d15c39 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 2 Jun 2026 20:10:26 +0200 Subject: [PATCH] =?UTF-8?q?fix(render):=20R1=20=E2=80=94=20repurpose=20the?= =?UTF-8?q?=20ParentCellId=3D=3Dnull=20cell-gate=20bypass=20(#78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EntityPassesVisibleCellGate no longer returns true unconditionally for outdoor scenery under a cell filter (was the headline #78 bleed). Outdoor scenery now draws only via the unfiltered bucket (visibleCellIds: null) + ResolveEntitySlot's OutsideView routing. The outdoor-root global Draw passes visibleCellIds: null (no portal-cell scoping outdoors; retires VisibleCellIds as a render gate — peering into buildings is R5). Updated the EntityClipTests case that pinned the old bypass (Included -> Excluded). 174/174 App tests green. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 8 ++++++-- src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs | 15 ++++++++------- .../Rendering/EntityClipTests.cs | 12 +++++++----- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index db8206c..274d4cf 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7584,10 +7584,14 @@ public sealed class GameWindow : IDisposable } else { - // Outdoor root: the global entity pass (unchanged). + // Outdoor root: draw the full outdoor world. No cell filter — outdoors there is no + // portal-cell scoping (ClearClipRouting made every instance slot 0). R1 retires + // visibility.VisibleCellIds as a render gate (peering into buildings is R5, a + // separate pass). On the outdoor root visibility is null anyway, so this is the + // same set the old code passed; null makes that explicit + gate-change-safe. _wbDrawDispatcher!.Draw(camera, _worldState.LandblockEntries, frustum, neverCullLandblockId: playerLb, - visibleCellIds: visibility?.VisibleCellIds, + visibleCellIds: null, animatedEntityIds: animatedIds); } diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 741162d..5efa800 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -1741,19 +1741,20 @@ public sealed unsafe class WbDrawDispatcher : IDisposable HashSet? visibleCellIds, EntitySet set) { + // No cell filter (outdoor root, or a bucket drawn unfiltered like live-dynamics / outdoor + // scenery) ⇒ every entity passes; clip-slot routing (ResolveEntitySlot) does the gating. if (visibleCellIds is null) return true; + // A cell-membership filter is active. An interior static passes iff its cell is visible. if (entity.ParentCellId.HasValue) return visibleCellIds.Contains(entity.ParentCellId.Value); - if (IsShellScopedSet(set) && entity.IsBuildingShell) - { - return entity.BuildingShellAnchorCellId is uint anchorCellId - && visibleCellIds.Contains(anchorCellId); - } - - return true; + // ParentCellId == null (outdoor scenery / building shell): NOT a member of any interior cell, + // so it does NOT pass a cell-membership filter (R1: the bleed fix — was an unconditional + // `return true`). When such entities must draw (through the doorway), the caller passes + // visibleCellIds: null and relies on ResolveEntitySlot's OutsideView routing instead. + return false; } // Phase U.1 (2026-05-30): the shell-scoped sets (IndoorPass / BuildingShells) diff --git a/tests/AcDream.App.Tests/Rendering/EntityClipTests.cs b/tests/AcDream.App.Tests/Rendering/EntityClipTests.cs index 70ae1ec..8ad712a 100644 --- a/tests/AcDream.App.Tests/Rendering/EntityClipTests.cs +++ b/tests/AcDream.App.Tests/Rendering/EntityClipTests.cs @@ -85,17 +85,19 @@ public sealed class EntityClipTests } [Fact] - public void EntityClip_NullParentCell_NonNullVisibleSet_Included() + public void EntityClip_NullParentCell_NonNullVisibleSet_Excluded() { - // An outdoor entity (ParentCellId == null) with a non-null visibleCellIds - // falls through to the final return-true (not a shell, not shell-scoped); - // outdoor scenery is not gated by the indoor cell filter. + // R1 (bleed fix #78): an outdoor entity (ParentCellId == null) with a non-null cell filter + // does NOT pass — it is not a member of any interior cell. (Was an unconditional return-true + // bypass, the headline outdoor-scenery bleed.) When such entities must draw through the + // doorway, the caller passes visibleCellIds: null and the OutsideView clip-slot routing + // (ResolveEntitySlot) gates them instead. var visibleCellIds = new HashSet { 0xA9B40170u }; var entity = Entity(parentCellId: null); bool result = WbDrawDispatcher.EntityPassesVisibleCellGate( entity, visibleCellIds, WbDrawDispatcher.EntitySet.All); - Assert.True(result); + Assert.False(result); } }