refactor(render): Phase U.1 — delete two-pipe inside-out machinery

Remove IndoorCellStencilPipeline + portal_stencil shaders, RenderInsideOutAcdream,
RenderOutsideInAcdream, the A8-perf instrumentation, the cameraInsideBuilding /
ACDREAM_A8_INDOOR_BRANCH branch, and the dead EntitySet partition values. Collapse
the render branch to the default Draw(All) path (U.4a replaces it with the gated
unified pass). Keep all audited EnvCellRenderer / BuildingLoader / CellVisibility /
camera-collision fixes.

Also deleted with the partition: the two test-only walk helpers
(WbDrawDispatcher.WalkEntitiesForTest / WalkEntitiesForTestByCellIds) and their
test files (WbDrawDispatcherEntitySetTests, WbDrawDispatcherCellIdsOverloadTests),
which existed solely to exercise the removed IndoorPass/OutdoorScenery/
BuildingShells/LiveDynamic partition. EntityMatchesSet / IsShellScopedSet collapse
to the All-path constants; the set: parameter is retained as a seam for the
unified pass.

Note: the depth-clear-if-inside default-path workaround was removed per the
U.1 task list — any current indoor-wall degradation persists until a later
Phase U task lands the unified pass (expected, not a regression introduced here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-30 16:05:19 +02:00
parent 0f7b395be1
commit 3fc77be5de
8 changed files with 37 additions and 2612 deletions

View file

@ -1,232 +0,0 @@
// Phase A8 — portal mesh triangle-fan generation tests.
//
// Pure-math coverage of PortalMeshBuilder.BuildTriangles — the part of
// IndoorCellStencilPipeline that converts a list of LoadedCell with
// PortalPolygons + WorldTransform into a flat Vector3[] of triangles
// in world space. The GL/upload portion is exercised at runtime only.
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering;
using Xunit;
namespace AcDream.App.Tests.Rendering;
public class IndoorCellStencilPipelineTests
{
[Fact]
public void BuildTriangles_NoCells_ReturnsEmpty()
{
var verts = PortalMeshBuilder.BuildTriangles(new List<LoadedCell>());
Assert.Empty(verts);
}
[Fact]
public void BuildTriangles_SkipsInnerPortals()
{
// Two portals on one cell: one exit (OtherCellId=0xFFFF, should be
// included), one inner (OtherCellId=0x0102, should be skipped).
var cell = new LoadedCell
{
WorldTransform = Matrix4x4.Identity,
Portals = new()
{
new CellPortalInfo(0xFFFF, 100, 0), // exit — included
new CellPortalInfo(0x0102, 101, 0), // inner — skipped
},
ClipPlanes = new() { default, default },
PortalPolygons = new()
{
new[]
{
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(1, 1, 0),
},
new[]
{
new Vector3(10, 0, 0),
new Vector3(11, 0, 0),
new Vector3(11, 1, 0),
},
},
};
var verts = PortalMeshBuilder.BuildTriangles(new List<LoadedCell> { cell });
// Only the exit polygon (3 verts → 1 triangle → 3 vertices).
Assert.Equal(3, verts.Length);
Assert.Equal(new Vector3(0, 0, 0), verts[0]);
Assert.Equal(new Vector3(1, 0, 0), verts[1]);
Assert.Equal(new Vector3(1, 1, 0), verts[2]);
}
[Fact]
public void BuildTriangles_OnlyIncludesProvidedVisibleCells()
{
// The render path now feeds BuildTriangles from the portal traversal's
// visible cells, not every cell in the building. A hidden room's exit
// portal must not punch outdoor terrain into the current view.
var visibleInnerCell = new LoadedCell
{
WorldTransform = Matrix4x4.Identity,
Portals = new() { new CellPortalInfo(0x0102, 100, 0) },
ClipPlanes = new() { default },
PortalPolygons = new()
{
new[]
{
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(1, 1, 0),
},
},
};
var hiddenExitCell = new LoadedCell
{
WorldTransform = Matrix4x4.Identity,
Portals = new() { new CellPortalInfo(0xFFFF, 101, 0) },
ClipPlanes = new() { default },
PortalPolygons = new()
{
new[]
{
new Vector3(10, 0, 0),
new Vector3(11, 0, 0),
new Vector3(11, 1, 0),
},
},
};
var visibleOnly = PortalMeshBuilder.BuildTriangles(new List<LoadedCell> { visibleInnerCell });
var allBuildingCells = PortalMeshBuilder.BuildTriangles(new List<LoadedCell> { visibleInnerCell, hiddenExitCell });
Assert.Empty(visibleOnly);
Assert.Equal(3, allBuildingCells.Length);
Assert.Equal(new Vector3(10, 0, 0), allBuildingCells[0]);
}
[Fact]
public void BuildTriangles_CameraSideFilterSkipsExitPortalsBehindCamera()
{
var cell = new LoadedCell
{
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals = new() { new CellPortalInfo(0xFFFF, 100, 0) },
ClipPlanes = new()
{
new PortalClipPlane
{
Normal = Vector3.UnitX,
D = 0f,
InsideSide = 0,
},
},
PortalPolygons = new()
{
new[]
{
new Vector3(0, 0, 0),
new Vector3(0, 1, 0),
new Vector3(0, 0, 1),
},
},
};
var visible = PortalMeshBuilder.BuildTriangles(new List<LoadedCell> { cell }, new Vector3(1, 0, 0));
var rejected = PortalMeshBuilder.BuildTriangles(new List<LoadedCell> { cell }, new Vector3(-1, 0, 0));
Assert.Equal(3, visible.Length);
Assert.Empty(rejected);
}
[Fact]
public void BuildTriangles_TriangulatesAsFan()
{
// 4-vertex quad → fan = 2 triangles → 6 vertices.
// Quad: (0,0,0), (1,0,0), (1,1,0), (0,1,0).
// Fan from vertex 0: (0,1,2), (0,2,3).
var cell = new LoadedCell
{
WorldTransform = Matrix4x4.Identity,
Portals = new() { new CellPortalInfo(0xFFFF, 100, 0) },
ClipPlanes = new() { default },
PortalPolygons = new()
{
new[]
{
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(1, 1, 0),
new Vector3(0, 1, 0),
},
},
};
var verts = PortalMeshBuilder.BuildTriangles(new List<LoadedCell> { cell });
Assert.Equal(6, verts.Length);
// Triangle 1: (0,1,2)
Assert.Equal(new Vector3(0, 0, 0), verts[0]);
Assert.Equal(new Vector3(1, 0, 0), verts[1]);
Assert.Equal(new Vector3(1, 1, 0), verts[2]);
// Triangle 2: (0,2,3)
Assert.Equal(new Vector3(0, 0, 0), verts[3]);
Assert.Equal(new Vector3(1, 1, 0), verts[4]);
Assert.Equal(new Vector3(0, 1, 0), verts[5]);
}
[Fact]
public void BuildTriangles_AppliesWorldTransform()
{
// Identity cell-local triangle, translated by WorldTransform.
var translate = Matrix4x4.CreateTranslation(new Vector3(100, 200, 300));
var cell = new LoadedCell
{
WorldTransform = translate,
Portals = new() { new CellPortalInfo(0xFFFF, 100, 0) },
ClipPlanes = new() { default },
PortalPolygons = new()
{
new[]
{
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(0, 1, 0),
},
},
};
var verts = PortalMeshBuilder.BuildTriangles(new List<LoadedCell> { cell });
Assert.Equal(3, verts.Length);
Assert.Equal(new Vector3(100, 200, 300), verts[0]);
Assert.Equal(new Vector3(101, 200, 300), verts[1]);
Assert.Equal(new Vector3(100, 201, 300), verts[2]);
}
[Fact]
public void BuildTriangles_SkipsEmptyOrDegeneratePolygons()
{
var cell = new LoadedCell
{
WorldTransform = Matrix4x4.Identity,
Portals = new()
{
new CellPortalInfo(0xFFFF, 100, 0),
new CellPortalInfo(0xFFFF, 101, 0),
},
ClipPlanes = new() { default, default },
PortalPolygons = new()
{
System.Array.Empty<Vector3>(), // empty
new[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0) }, // degenerate (2 verts)
},
};
var verts = PortalMeshBuilder.BuildTriangles(new List<LoadedCell> { cell });
Assert.Empty(verts);
}
}

View file

@ -1,92 +0,0 @@
// Phase A8 RR5 — verify WbDrawDispatcher.WalkEntitiesForTestByCellIds,
// the pure-data companion to the new Draw(cellIds:) production overload.
//
// Semantics: indoor entities (ParentCellId.HasValue) are gated by explicit
// membership in cellIds. Building shells (IsBuildingShell) pass only when their
// BuildingShellAnchorCellId belongs to the same cell set.
// Outdoor scenery (no ParentCellId, not a shell) is excluded by EntitySet.IndoorPass.
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
using Xunit;
namespace AcDream.Core.Tests.Rendering.Wb;
public class WbDrawDispatcherCellIdsOverloadTests
{
private static WorldEntity CellEnt(uint id, uint cellId) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x01000001u,
ParentCellId = cellId,
MeshRefs = new List<MeshRef> { new(0x01000001u, Matrix4x4.Identity) },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
private static WorldEntity OutdoorScenery(uint id) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x01000001u,
ParentCellId = null,
IsBuildingShell = false,
MeshRefs = new List<MeshRef> { new(0x01000001u, Matrix4x4.Identity) },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
private static WorldEntity BuildingShell(uint id, uint? anchorCellId) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x02000001u,
ParentCellId = null,
IsBuildingShell = true,
BuildingShellAnchorCellId = anchorCellId,
MeshRefs = new List<MeshRef> { new(0x01000001u, Matrix4x4.Identity) },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
[Fact]
public void WalkEntitiesByCellIds_IncludesOnlyEntitiesInListedCells()
{
var entities = new List<WorldEntity>
{
CellEnt(0x40000001u, 0xA9B40150u), // in listed cells
CellEnt(0x40000002u, 0xA9B40151u), // in listed cells
CellEnt(0x40000003u, 0xA9B40999u), // OUT — not in list
BuildingShell(0xC0000001u, 0xA9B40150u), // in listed building cells
BuildingShell(0xC0000003u, 0xA9B40999u), // OUT — another building shell
OutdoorScenery(0xC0000002u), // OUT — not a shell, not in cell list
};
var cellIds = new HashSet<uint> { 0xA9B40150u, 0xA9B40151u };
var result = WbDrawDispatcher.WalkEntitiesForTestByCellIds(
entities, cellIds, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Equal(3, result.Count);
Assert.Contains(0x40000001u, result);
Assert.Contains(0x40000002u, result);
Assert.Contains(0xC0000001u, result);
Assert.DoesNotContain(0x40000003u, result);
Assert.DoesNotContain(0xC0000003u, result);
Assert.DoesNotContain(0xC0000002u, result);
}
[Fact]
public void WalkEntitiesByCellIds_EmptyCellList_ExcludesBuildingShells()
{
var entities = new List<WorldEntity>
{
CellEnt(0x40000001u, 0xA9B40150u),
BuildingShell(0xC0000001u, 0xA9B40150u),
};
var result = WbDrawDispatcher.WalkEntitiesForTestByCellIds(
entities, new HashSet<uint>(), set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Empty(result);
}
}

View file

@ -1,282 +0,0 @@
// Phase A8 — verify the WbDrawDispatcher EntitySet partition (taxonomy-aware).
//
// The pure-data WalkEntitiesForTest helper iterates a flat entity list and
// returns the IDs that survive the EntitySet filter + visibleCellIds gate.
//
// EntitySet.IndoorPass — ParentCellId.HasValue OR IsBuildingShell,
// and NOT live-dynamic (ServerGuid == 0).
// Building shells are gated by their dat anchor;
// live-dynamic flows through LiveDynamic instead.
// EntitySet.OutdoorScenery — ParentCellId == null AND not live-dynamic.
// Includes building shells for exterior/depth repair passes.
// EntitySet.BuildingShells — IsBuildingShell only, gated by dat anchor when
// visibleCellIds are supplied.
// EntitySet.LiveDynamic — ServerGuid != 0 (player, NPCs, dropped items,
// idle doors after animation). Drawn last with
// stencil disabled.
// EntitySet.All — pre-A8 behavior (visibleCellIds gates indoor;
// outdoor entities pass through).
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
using Xunit;
namespace AcDream.Core.Tests.Rendering.Wb;
public class WbDrawDispatcherEntitySetTests
{
private static WorldEntity CellEnt(uint id, uint cellId) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x01000001u,
ParentCellId = cellId,
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
private static WorldEntity OutdoorScenery(uint id) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x01000001u,
ParentCellId = null,
IsBuildingShell = false,
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
private static WorldEntity BuildingShell(uint id, uint? anchorCellId = null) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x02000001u,
ParentCellId = null,
IsBuildingShell = true,
BuildingShellAnchorCellId = anchorCellId,
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
private static WorldEntity LiveDynamic(uint id, uint serverGuid) => new()
{
Id = id,
SourceGfxObjOrSetupId = 0x02000001u,
ServerGuid = serverGuid,
ParentCellId = null,
IsBuildingShell = false,
MeshRefs = new List<MeshRef> { new() { GfxObjId = 0x01000001u } },
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
};
[Fact]
public void IndoorPass_IncludesCellEntities()
{
var entities = new List<WorldEntity>
{
CellEnt(0x10000001, 0xA9B40143),
OutdoorScenery(0x10000002),
CellEnt(0x10000003, 0xA9B40144),
};
var visible = new HashSet<uint> { 0xA9B40143u, 0xA9B40144u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Equal(2, result.Count);
Assert.Contains(0x10000001u, result);
Assert.Contains(0x10000003u, result);
Assert.DoesNotContain(0x10000002u, result);
}
[Fact]
public void IndoorPass_IncludesBuildingShells_WhenAnchorCellIsVisible()
{
var entities = new List<WorldEntity>
{
BuildingShell(0xC0000001, 0xA9B40143u), // cottage wall
OutdoorScenery(0xC0000002), // tree
CellEnt(0x40000001, 0xA9B40143),
};
var visible = new HashSet<uint> { 0xA9B40143u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Equal(2, result.Count);
Assert.Contains(0xC0000001u, result); // building shell included
Assert.Contains(0x40000001u, result); // cell entity included
Assert.DoesNotContain(0xC0000002u, result); // tree excluded
}
[Fact]
public void IndoorPass_WithNullCellFilter_UsesEntitySetOnly()
{
var entities = new List<WorldEntity>
{
BuildingShell(0xC0000001, 0xA9B40143u),
CellEnt(0x40000001, 0xA9B40143),
CellEnt(0x40000002, 0xA9B40199),
OutdoorScenery(0xC0000002),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
};
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Equal(3, result.Count);
Assert.Contains(0xC0000001u, result);
Assert.Contains(0x40000001u, result);
Assert.Contains(0x40000002u, result);
Assert.DoesNotContain(0xC0000002u, result);
Assert.DoesNotContain(0x10000001u, result);
}
[Fact]
public void IndoorPass_ExcludesBuildingShells_WhenAnchorCellIsNotVisible()
{
var entities = new List<WorldEntity>
{
BuildingShell(0xC0000001, 0xA9B40150u),
CellEnt(0x40000001, 0xA9B40143),
};
var visible = new HashSet<uint> { 0xA9B40143u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Single(result);
Assert.Contains(0x40000001u, result);
Assert.DoesNotContain(0xC0000001u, result);
}
[Fact]
public void IndoorPass_ExcludesLiveDynamic()
{
var entities = new List<WorldEntity>
{
CellEnt(0x40000001, 0xA9B40143),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
};
var visible = new HashSet<uint> { 0xA9B40143u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.IndoorPass);
Assert.Single(result);
Assert.Contains(0x40000001u, result);
Assert.DoesNotContain(0x10000001u, result); // live-dynamic excluded
}
[Fact]
public void OutdoorScenery_IncludesBuildingShells()
{
var entities = new List<WorldEntity>
{
BuildingShell(0xC0000001), // cottage wall — included
OutdoorScenery(0xC0000002), // tree — included
CellEnt(0x40000001, 0xA9B40143), // cell — excluded
};
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.OutdoorScenery);
Assert.Equal(2, result.Count);
Assert.Contains(0xC0000002u, result);
Assert.Contains(0xC0000001u, result);
Assert.DoesNotContain(0x40000001u, result);
}
[Fact]
public void OutdoorScenery_ExcludesLiveDynamic()
{
var entities = new List<WorldEntity>
{
OutdoorScenery(0xC0000001),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
};
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.OutdoorScenery);
Assert.Single(result);
Assert.Contains(0xC0000001u, result);
Assert.DoesNotContain(0x10000001u, result);
}
[Fact]
public void BuildingShells_IncludesOnlyAnchoredShells()
{
var entities = new List<WorldEntity>
{
BuildingShell(0xC0000001, 0xA9B40143u),
BuildingShell(0xC0000002, 0xA9B40999u),
OutdoorScenery(0xC0000003),
CellEnt(0x40000001, 0xA9B40143),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
};
var visible = new HashSet<uint> { 0xA9B40143u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.BuildingShells);
Assert.Single(result);
Assert.Contains(0xC0000001u, result);
Assert.DoesNotContain(0xC0000002u, result);
Assert.DoesNotContain(0xC0000003u, result);
Assert.DoesNotContain(0x40000001u, result);
Assert.DoesNotContain(0x10000001u, result);
}
[Fact]
public void LiveDynamic_IncludesOnlyServerSpawned()
{
var entities = new List<WorldEntity>
{
OutdoorScenery(0xC0000001),
BuildingShell(0xC0000002),
CellEnt(0x40000001, 0xA9B40143),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
LiveDynamic(0x10000002, serverGuid: 0x50000456u),
};
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: null, set: WbDrawDispatcher.EntitySet.LiveDynamic);
Assert.Equal(2, result.Count);
Assert.Contains(0x10000001u, result);
Assert.Contains(0x10000002u, result);
Assert.DoesNotContain(0xC0000001u, result);
Assert.DoesNotContain(0xC0000002u, result);
Assert.DoesNotContain(0x40000001u, result);
}
[Fact]
public void All_MatchesPreA8Behavior()
{
var entities = new List<WorldEntity>
{
CellEnt(0x40000001, 0xA9B40143),
OutdoorScenery(0xC0000001),
BuildingShell(0xC0000002),
LiveDynamic(0x10000001, serverGuid: 0x50000123u),
CellEnt(0x40000002, 0xA9B40999), // not in visibleCellIds
};
var visible = new HashSet<uint> { 0xA9B40143u };
var result = WbDrawDispatcher.WalkEntitiesForTest(
entities, visibleCellIds: visible, set: WbDrawDispatcher.EntitySet.All);
// Pre-A8: visibleCellIds gates indoor entities only; outdoor entities
// (regardless of building/scenery/live-dynamic) pass through.
Assert.Equal(4, result.Count);
Assert.Contains(0x40000001u, result);
Assert.Contains(0xC0000001u, result);
Assert.Contains(0xC0000002u, result);
Assert.Contains(0x10000001u, result);
Assert.DoesNotContain(0x40000002u, result);
}
}