fix #451: stabilize portal seam rendering
All checks were successful
CI / linux-portable (push) Successful in 3m32s
CI / windows-gate (push) Successful in 6m55s
CI / release (push) Successful in 2m12s

This commit is contained in:
Erik 2026-08-27 14:30:21 +02:00
parent f6fe0f2a4f
commit 1d2f2f738f
29 changed files with 1650 additions and 239 deletions

View file

@ -21,6 +21,10 @@ public sealed class RenderScenePViewFrameProductTests
RenderProjectionRecord outdoor = Record(
0x0100_0000_0000_0001,
RenderProjectionClass.OutdoorStatic);
RenderProjectionRecord buildingShell = Record(
0x0100_0000_0000_0008,
RenderProjectionClass.OutdoorStatic,
isBuildingShell: true);
RenderProjectionRecord hidden = Record(
0x0100_0000_0000_0002,
RenderProjectionClass.OutdoorStatic,
@ -48,6 +52,7 @@ public sealed class RenderScenePViewFrameProductTests
RenderProjectionRecord[] records =
[
outdoor,
buildingShell,
hidden,
withdrawn,
shell,
@ -90,7 +95,7 @@ public sealed class RenderScenePViewFrameProductTests
try
{
Assert.Equal(
[outdoor.Id, hidden.Id],
[outdoor.Id, hidden.Id, buildingShell.Id],
view.OutdoorStaticCandidates.ToArray()
.Select(static item => item.Id));
Assert.Equal(
@ -101,28 +106,42 @@ public sealed class RenderScenePViewFrameProductTests
[outdoorDynamic.Id, cellDynamic.Id],
view.DynamicCandidates.ToArray()
.Select(static item => item.Id));
Assert.Equal(5, view.Transforms.Length);
Assert.Equal(5, view.RouteCandidates.Length);
Assert.Equal(6, view.Transforms.Length);
Assert.Equal(6, view.RouteCandidates.Length);
Assert.Equal(3, view.RouteRanges.Length);
RenderFrameCandidateRange outdoorRange = Assert.Single(
view.RouteRanges.ToArray(),
range => range.Route
== RenderFrameCandidateRoute.LandscapeOutdoorStatic);
Assert.Equal(3, outdoorRange.Count);
Assert.Contains(
view.RouteCandidates.Slice(
outdoorRange.Offset,
outdoorRange.Count).ToArray(),
item => item.Id == buildingShell.Id);
Assert.DoesNotContain(
view.RouteRanges.ToArray(),
range => range.Route
== RenderFrameCandidateRoute.LandscapeBuildingShell);
Assert.DoesNotContain(
view.RouteCandidates.ToArray(),
item => item.Id == withdrawn.Id || item.Id == shell.Id);
Assert.Equal(
new RenderFrameDiagnosticCounts(
OutdoorStaticCandidates: 2,
OutdoorStaticCandidates: 3,
CellStaticCandidates: 1,
DynamicCandidates: 2,
TransformCount: 5,
TransformCount: 6,
OpaqueClassificationCount: 0,
AlphaClassificationCount: 0,
LightSetCount: 0,
SelectionPartCount: 0,
RouteCandidateCount: 5,
EntityCandidateCount: 5,
MeshPartCount: 5),
RouteCandidateCount: 6,
EntityCandidateCount: 6,
MeshPartCount: 6),
view.DiagnosticCounts);
Assert.Equal(5, view.EntityCandidates.Length);
Assert.Equal(5, view.MeshParts.Length);
Assert.Equal(6, view.EntityCandidates.Length);
Assert.Equal(6, view.MeshParts.Length);
Assert.Same(portal, view.PortalFrame);
Assert.Same(clip, view.ClipAssembly);
Assert.Equal(digest, view.SourceDigest);
@ -133,6 +152,178 @@ public sealed class RenderScenePViewFrameProductTests
}
}
[Fact]
public void Builder_OrdersMultiSliceBuildingShellsAfterLookIns()
{
using var scene = new ArchRenderScene(Generation);
RenderProjectionRecord outdoor = Record(
0x0100_0000_0000_0101,
RenderProjectionClass.OutdoorStatic);
RenderProjectionRecord buildingShell = Record(
0x0100_0000_0000_0102,
RenderProjectionClass.OutdoorStatic,
isBuildingShell: true,
buildingShellAnchorCellId: Cell);
RenderProjectionRecord lookInObject = Record(
0x0100_0000_0000_0103,
RenderProjectionClass.IndoorCellStatic,
parentCell: Cell);
scene.Apply(
[
RenderProjectionDelta.Register(Generation, 1, outdoor),
RenderProjectionDelta.Register(Generation, 2, buildingShell),
RenderProjectionDelta.Register(Generation, 3, lookInObject),
]);
PortalVisibilityFrame portal = Portal(Cell);
PortalVisibilityFrame lookIn = Portal(Cell);
lookIn.SourceBuildingKey = 2u;
lookIn.SourceBuildingLandblockId = Cell & 0xFFFF0000u;
var anchorCell = new LoadedCell
{
CellId = Cell,
BuildingId = 2u,
};
ClipFrameAssembly clip = TwoSliceClip(Cell);
clip.LookInCellToViewSlices[new LookInClipCell(0, Cell)] =
[clip.CellIdToViewSlices[Cell][0]];
ViewconeCuller viewcone =
ViewconeCuller.Build(clip, Matrix4x4.Identity);
var exchange = new RenderFrameExchange();
var builder = new RenderScenePViewFrameBuilder();
RenderSceneDigest digest =
scene.BuildDigest(new RenderSceneDigestBuffer());
var input = new RenderScenePViewBuildInput(
scene.OpenQuery(),
digest,
portal,
clip,
viewcone,
[lookIn],
[Cell],
new DictionaryCellSource(anchorCell),
[],
RootIsOutdoor: true);
builder.Build(exchange, frameSequence: 1, in input);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
try
{
Assert.Equal(
[
(RenderFrameCandidateRoute.LandscapeOutdoorStatic, 0, 0u),
(RenderFrameCandidateRoute.LandscapeOutdoorStatic, 1, 0u),
(RenderFrameCandidateRoute.LookInObject, 0, Cell),
(RenderFrameCandidateRoute.LandscapeBuildingShell, 0, 0u),
(RenderFrameCandidateRoute.LandscapeBuildingShell, 1, 0u),
(RenderFrameCandidateRoute.CellStatic, 0, 0u),
],
view.RouteRanges.ToArray().Select(static range =>
(range.Route, range.RouteIndex, range.CellId)));
}
finally
{
exchange.Release(in view);
}
}
[Fact]
public void Builder_InterleavesEachLookInWithItsOwnPackedBuildingShellRoutes()
{
const uint secondCell = 0xA9B40180u;
using var scene = new ArchRenderScene(Generation);
RenderProjectionRecord firstShell = Record(
0x0100_0000_0000_0201,
RenderProjectionClass.OutdoorStatic,
isBuildingShell: true,
buildingShellAnchorCellId: Cell);
RenderProjectionRecord secondShell = Record(
0x0100_0000_0000_0202,
RenderProjectionClass.OutdoorStatic,
isBuildingShell: true,
buildingShellAnchorCellId: secondCell);
RenderProjectionRecord firstObject = Record(
0x0100_0000_0000_0203,
RenderProjectionClass.IndoorCellStatic,
parentCell: Cell);
RenderProjectionRecord secondObject = Record(
0x0100_0000_0000_0204,
RenderProjectionClass.IndoorCellStatic,
parentCell: secondCell);
scene.Apply(
[
RenderProjectionDelta.Register(Generation, 1, firstShell),
RenderProjectionDelta.Register(Generation, 2, secondShell),
RenderProjectionDelta.Register(Generation, 3, firstObject),
RenderProjectionDelta.Register(Generation, 4, secondObject),
]);
PortalVisibilityFrame portal = Portal(Cell);
PortalVisibilityFrame firstLookIn = Portal(Cell);
firstLookIn.SourceBuildingKey = 2u;
firstLookIn.SourceBuildingLandblockId = Cell & 0xFFFF0000u;
PortalVisibilityFrame secondLookIn = Portal(secondCell);
secondLookIn.SourceBuildingKey = 3u;
secondLookIn.SourceBuildingLandblockId = secondCell & 0xFFFF0000u;
var firstAnchor = new LoadedCell
{
CellId = Cell,
BuildingId = 2u,
};
var secondAnchor = new LoadedCell
{
CellId = secondCell,
BuildingId = 3u,
};
ClipFrameAssembly clip = TwoSliceClip(Cell);
clip.LookInCellToViewSlices[new LookInClipCell(0, Cell)] =
[clip.CellIdToViewSlices[Cell][0]];
clip.LookInCellToViewSlices[new LookInClipCell(1, secondCell)] =
[clip.CellIdToViewSlices[Cell][0]];
ViewconeCuller viewcone =
ViewconeCuller.Build(clip, Matrix4x4.Identity);
var exchange = new RenderFrameExchange();
var builder = new RenderScenePViewFrameBuilder();
RenderSceneDigest digest =
scene.BuildDigest(new RenderSceneDigestBuffer());
var input = new RenderScenePViewBuildInput(
scene.OpenQuery(),
digest,
portal,
clip,
viewcone,
[firstLookIn, secondLookIn],
[Cell],
new DictionaryCellSource(firstAnchor, secondAnchor),
[],
RootIsOutdoor: true);
builder.Build(exchange, frameSequence: 1, in input);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
try
{
Assert.Equal(
[
(RenderFrameCandidateRoute.LookInObject, 0, Cell),
(RenderFrameCandidateRoute.LandscapeBuildingShell, 0, 0u),
(RenderFrameCandidateRoute.LandscapeBuildingShell, 1, 0u),
(RenderFrameCandidateRoute.LookInObject, 1, secondCell),
(RenderFrameCandidateRoute.LandscapeBuildingShell, 2, 0u),
(RenderFrameCandidateRoute.LandscapeBuildingShell, 3, 0u),
],
view.RouteRanges.ToArray()
.Where(static range =>
range.Route is RenderFrameCandidateRoute.LookInObject
or RenderFrameCandidateRoute.LandscapeBuildingShell)
.Select(static range =>
(range.Route, range.RouteIndex, range.CellId)));
}
finally
{
exchange.Release(in view);
}
}
[Fact]
public void Builder_ReusesOrderedIndicesWhileRefreshingDirtyRecords()
{
@ -501,7 +692,9 @@ public sealed class RenderScenePViewFrameProductTests
uint? parentCell = null,
RenderProjectionFlags flags =
RenderProjectionFlags.Draw
| RenderProjectionFlags.SpatiallyResident)
| RenderProjectionFlags.SpatiallyResident,
bool isBuildingShell = false,
uint buildingShellAnchorCellId = 0)
{
uint fullCellId = parentCell ?? Landblock;
Matrix4x4 transform = Matrix4x4.Identity;
@ -531,14 +724,14 @@ public sealed class RenderScenePViewFrameProductTests
SourceId: 1,
ParentCellId: parentCell ?? 0,
EffectCellId: 0,
BuildingShellAnchorCellId: 0,
BuildingShellAnchorCellId: buildingShellAnchorCellId,
TransformFingerprint: default,
GeometryFingerprint: default,
AppearanceFingerprint: default),
new RenderEntityPayload(
[new MeshRef((uint)id, Matrix4x4.Identity)],
PaletteOverride: null,
IsBuildingShell: false));
IsBuildingShell: isBuildingShell));
}
private static WorldEntity Entity(
@ -578,6 +771,22 @@ public sealed class RenderScenePViewFrameProductTests
return assembly;
}
private static ClipFrameAssembly TwoSliceClip(uint cellId)
{
var first = new ClipViewSlice(
Slot: 0,
NdcAabb: new Vector4(-1, -1, 0, 1),
Planes: []);
var second = new ClipViewSlice(
Slot: 1,
NdcAabb: new Vector4(0, -1, 1, 1),
Planes: []);
var assembly = new ClipFrameAssembly();
assembly.SetOutsideViewSlices([first, second]);
assembly.CellIdToViewSlices[cellId] = [first];
return assembly;
}
private static (
uint LandblockId,
Vector3 AabbMin,
@ -602,4 +811,15 @@ public sealed class RenderScenePViewFrameProductTests
public LoadedCell? Find(uint cellId) => null;
}
private sealed class DictionaryCellSource : IRetailPViewCellSource
{
private readonly Dictionary<uint, LoadedCell> _cells;
public DictionaryCellSource(params LoadedCell[] cells) =>
_cells = cells.ToDictionary(static cell => cell.CellId);
public LoadedCell? Find(uint cellId) =>
_cells.TryGetValue(cellId, out LoadedCell? cell) ? cell : null;
}
}