acdream/tests/AcDream.App.Tests/Rendering/RenderFrameRouteOwnerSelectorTests.cs
Erik c730632075 refactor(rendering): prepare retained particle routing
Derive attached particle owners from the exact retained PView route stream with a zero-allocation warm path. Correct G4 gate provenance, record the current production-only RDP sample, and keep G5 old-route deletion explicitly blocked until the reapplied retained cutover passes its connected visual gate.
2026-07-25 08:45:18 +02:00

178 lines
5.7 KiB
C#

using AcDream.App.Rendering.Scene;
namespace AcDream.App.Tests.Rendering;
public sealed class RenderFrameRouteOwnerSelectorTests
{
private static readonly RenderSceneGeneration Generation =
RenderSceneGeneration.FromRaw(1);
[Fact]
public void ReplaceAndUnionUseExactRouteIdentityAndDeduplicateOwners()
{
var exchange = new RenderFrameExchange();
PublishRoutes(exchange, sequence: 1);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
var owners = new HashSet<uint> { 999 };
RenderFrameRouteOwnerSelector.Replace(
owners,
in view,
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
routeIndex: 0,
cellId: 0);
RenderFrameRouteOwnerSelector.Union(
owners,
in view,
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
routeIndex: 0,
cellId: 0);
Assert.Equal([10u, 20u, 30u], owners.Order());
exchange.Release(in view);
}
[Fact]
public void ExceptRouteRemovesOwnersAcrossEveryRangeOfThatRoute()
{
var exchange = new RenderFrameExchange();
PublishRoutes(exchange, sequence: 1);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
var owners = new HashSet<uint>();
RenderFrameRouteOwnerSelector.Replace(
owners,
in view,
RenderFrameCandidateRoute.DynamicLast,
routeIndex: 0,
cellId: 0);
RenderFrameRouteOwnerSelector.ExceptRoute(
owners,
in view,
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
Assert.Equal([50u], owners);
exchange.Release(in view);
}
[Fact]
public void MissingRouteClearsOnReplaceAndDoesNotMutateOnUnion()
{
var exchange = new RenderFrameExchange();
PublishRoutes(exchange, sequence: 1);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
var owners = new HashSet<uint> { 10, 20 };
RenderFrameRouteOwnerSelector.Union(
owners,
in view,
RenderFrameCandidateRoute.LookInObject,
routeIndex: 9,
cellId: 0x01010009);
Assert.Equal([10u, 20u], owners.Order());
RenderFrameRouteOwnerSelector.Replace(
owners,
in view,
RenderFrameCandidateRoute.LookInObject,
routeIndex: 9,
cellId: 0x01010009);
Assert.Empty(owners);
exchange.Release(in view);
}
[Fact]
public void WarmSelectionAllocatesNothing()
{
var exchange = new RenderFrameExchange();
PublishRoutes(exchange, sequence: 1);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
var owners = new HashSet<uint>();
owners.EnsureCapacity(16);
SelectOwners(owners, in view);
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 1_000; iteration++)
SelectOwners(owners, in view);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
Assert.Equal([50u], owners);
exchange.Release(in view);
}
private static void SelectOwners(
HashSet<uint> owners,
in RenderFrameView view)
{
RenderFrameRouteOwnerSelector.Replace(
owners,
in view,
RenderFrameCandidateRoute.DynamicLast,
routeIndex: 0,
cellId: 0);
RenderFrameRouteOwnerSelector.ExceptRoute(
owners,
in view,
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
}
private static void PublishRoutes(
RenderFrameExchange exchange,
ulong sequence)
{
RenderProjectionRecord owner10 = Projection(1, 10);
RenderProjectionRecord owner20 = Projection(2, 20);
RenderProjectionRecord owner30 = Projection(3, 30);
RenderProjectionRecord owner40 = Projection(4, 40);
RenderProjectionRecord owner50 = Projection(5, 50);
RenderProjectionRecord anonymous = Projection(6, 0);
RenderFrameWriter writer =
exchange.BeginBuild(Generation, sequence);
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
0,
0,
[owner10, owner20, anonymous]);
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
0,
0,
[owner20, owner30]);
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
1,
0,
[owner40]);
writer.AddRouteRange(
RenderFrameCandidateRoute.DynamicLast,
0,
0,
[owner20, owner40, owner50]);
writer.SetSourceDigest(
new RenderSceneDigest(
Generation,
default,
default));
writer.Publish();
}
private static RenderProjectionRecord Projection(
ulong projectionId,
uint localEntityId) =>
new()
{
Id = RenderProjectionId.FromRaw(projectionId),
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
Source = new RenderSourceMetadata(
LocalEntityId: localEntityId,
ServerGuid: 0,
SourceId: 0,
ParentCellId: 0,
EffectCellId: 0,
BuildingShellAnchorCellId: 0,
TransformFingerprint: default,
GeometryFingerprint: default,
AppearanceFingerprint: default),
};
}