acdream/tests/AcDream.App.Tests/Rendering/RenderFrameProductTests.cs
Erik e346f8bbaf feat(rendering): compare scene-built PView candidates
Build a same-frame candidate product from incremental render-scene indices and compare its exact PView routes against the accepted current path without changing the production draw source.
2026-07-25 00:12:53 +02:00

289 lines
9.9 KiB
C#

using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.Core.Selection;
namespace AcDream.App.Tests.Rendering;
public sealed class RenderFrameProductTests
{
private static readonly RenderSceneGeneration Generation =
RenderSceneGeneration.FromRaw(7);
[Fact]
public void PublishesAndBorrowsTheFrameBuiltForTheSameSequence()
{
var exchange = new RenderFrameExchange();
RenderFrameWriter writer = exchange.BeginBuild(Generation, 41);
RenderProjectionRecord outdoor = Projection(1);
RenderProjectionRecord cellA = Projection(2);
RenderProjectionRecord cellB = Projection(3);
RenderProjectionRecord dynamic = Projection(4);
writer.AddOutdoor(in outdoor);
writer.AddCellRange(0x12340102, 5, [cellA, cellB]);
writer.AddDynamic(in dynamic);
writer.AddTransform(new RenderFrameTransformRecord(
dynamic.Id,
0,
Matrix4x4.CreateTranslation(1, 2, 3)));
writer.AddClassification(new RenderFrameClassificationRecord(
outdoor.Id,
0,
0,
RenderFrameBlendClass.Opaque,
1f));
writer.AddClassification(new RenderFrameClassificationRecord(
dynamic.Id,
1,
0,
RenderFrameBlendClass.Alpha,
0.5f));
writer.AddLightSet(new RenderFrameObjectLightSet(
dynamic.Id,
0, 1, 2, 3, 4, 5, 6, 7));
writer.AddSelectionPart(new RenderFrameSelectionPart(
dynamic.Id,
0x50000001,
4,
0,
0x01000001,
Matrix4x4.Identity,
SelectionMesh()));
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
0,
0,
[outdoor]);
var digest = new RenderSceneDigest(
Generation,
new RenderProjectionCounts(Total: 4, 1, 2, 1, 0, 0),
new RenderSceneHash128(11, 12));
writer.SetSourceDigest(in digest);
writer.Publish();
RenderFrameView view = exchange.BorrowLatest(Generation, 41);
Assert.Equal((ulong)41, view.FrameSequence);
Assert.Equal(Generation, view.Generation);
Assert.Equal(outdoor.Id, view.OutdoorStaticCandidates[0].Id);
Assert.Equal([cellA.Id, cellB.Id],
view.CellStaticCandidates.ToArray().Select(static item => item.Id));
Assert.Equal(new RenderFrameCellRange(0x12340102, 5, 0, 2),
view.CellRanges[0]);
Assert.Equal(dynamic.Id, view.DynamicCandidates[0].Id);
Assert.Equal(1, view.Transforms.Length);
Assert.Equal(2, view.Classifications.Length);
Assert.Equal(1, view.LightSets.Length);
Assert.Equal(1, view.SelectionParts.Length);
Assert.Equal(outdoor.Id, view.RouteCandidates[0].Id);
Assert.Equal(
new RenderFrameCandidateRange(
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
0,
0,
0,
1),
view.RouteRanges[0]);
Assert.Equal(
new RenderFrameDiagnosticCounts(1, 2, 1, 1, 1, 1, 1, 1, 1),
view.DiagnosticCounts);
Assert.Equal(digest, view.SourceDigest);
exchange.Release(in view);
}
[Fact]
public void BuildsInOtherArenaWhilePriorFrameRemainsBorrowed()
{
var exchange = new RenderFrameExchange();
PublishEmpty(exchange, Generation, 1);
RenderFrameView first = exchange.BorrowLatest(Generation, 1);
PublishEmpty(exchange, Generation, 2);
Assert.Equal((ulong)1, first.FrameSequence);
RenderFrameView second = exchange.BorrowLatest(Generation, 2);
Assert.Equal((ulong)2, second.FrameSequence);
Assert.Throws<InvalidOperationException>(
() => exchange.BeginBuild(Generation, 3));
exchange.Release(in first);
exchange.Release(in second);
}
[Fact]
public void ReuseInvalidatesEveryCopyOfReleasedView()
{
var exchange = new RenderFrameExchange();
PublishEmpty(exchange, Generation, 1);
RenderFrameView first = exchange.BorrowLatest(Generation, 1);
RenderFrameView copy = first;
exchange.Release(in first);
Assert.Throws<InvalidOperationException>(() => _ = copy.FrameSequence);
PublishEmpty(exchange, Generation, 2);
RenderFrameView second = exchange.BorrowLatest(Generation, 2);
exchange.Release(in second);
PublishEmpty(exchange, Generation, 3);
Assert.Throws<InvalidOperationException>(() => _ = copy.Generation);
}
[Fact]
public void AbortDoesNotPublishIncompleteFrame()
{
var exchange = new RenderFrameExchange();
PublishEmpty(exchange, Generation, 1);
RenderFrameWriter writer = exchange.BeginBuild(Generation, 2);
RenderProjectionRecord record = Projection(8);
writer.AddOutdoor(in record);
writer.Abort();
RenderFrameView prior = exchange.BorrowLatest(Generation, 1);
Assert.Equal(0, prior.OutdoorStaticCandidates.Length);
exchange.Release(in prior);
PublishEmpty(exchange, Generation, 2);
RenderFrameView replacement = exchange.BorrowLatest(Generation, 2);
Assert.Equal(0, replacement.OutdoorStaticCandidates.Length);
exchange.Release(in replacement);
}
[Fact]
public void BorrowRequiresExactGenerationAndFrameSequence()
{
var exchange = new RenderFrameExchange();
PublishEmpty(exchange, Generation, 9);
Assert.Throws<InvalidOperationException>(
() => exchange.BorrowLatest(
RenderSceneGeneration.FromRaw(8),
9));
Assert.Throws<InvalidOperationException>(
() => exchange.BorrowLatest(Generation, 10));
RenderFrameView view = exchange.BorrowLatest(Generation, 9);
exchange.Release(in view);
}
[Fact]
public void SourceDigestMustMatchArenaGeneration()
{
var exchange = new RenderFrameExchange();
RenderFrameWriter writer = exchange.BeginBuild(Generation, 1);
var digest = new RenderSceneDigest(
RenderSceneGeneration.FromRaw(8),
default,
default);
Assert.Throws<InvalidOperationException>(
() => writer.SetSourceDigest(in digest));
writer.Abort();
}
[Fact]
public void PublishRequiresSourceDigest()
{
var exchange = new RenderFrameExchange();
RenderFrameWriter writer = exchange.BeginBuild(Generation, 1);
Assert.Throws<InvalidOperationException>(writer.Publish);
writer.Abort();
}
[Fact]
public void WarmProductBuildAndBorrowAllocateNothing()
{
var exchange = new RenderFrameExchange();
RenderProjectionRecord record = Projection(1);
RenderFrameSelectionPart selection = new(
record.Id,
1,
1,
0,
1,
Matrix4x4.Identity,
SelectionMesh());
for (ulong sequence = 1; sequence <= 8; sequence++)
BuildPopulated(exchange, sequence, in record, in selection);
long before = GC.GetAllocatedBytesForCurrentThread();
for (ulong sequence = 9; sequence <= 1_008; sequence++)
BuildPopulated(exchange, sequence, in record, in selection);
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0, allocated);
}
private static void BuildPopulated(
RenderFrameExchange exchange,
ulong sequence,
in RenderProjectionRecord record,
in RenderFrameSelectionPart selection)
{
RenderFrameWriter writer = exchange.BeginBuild(Generation, sequence);
writer.AddOutdoor(in record);
writer.AddCellRange(0x01010001, 0, in record);
writer.AddDynamic(in record);
writer.AddTransform(new RenderFrameTransformRecord(
record.Id,
0,
Matrix4x4.Identity));
writer.AddClassification(new RenderFrameClassificationRecord(
record.Id,
0,
0,
RenderFrameBlendClass.Opaque,
1f));
writer.AddLightSet(new RenderFrameObjectLightSet(
record.Id,
0, 1, 2, 3, 4, 5, 6, 7));
writer.AddSelectionPart(in selection);
writer.SetSourceDigest(new RenderSceneDigest(
Generation,
new RenderProjectionCounts(3, 1, 1, 1, 0, 0),
default));
writer.Publish();
RenderFrameView view = exchange.BorrowLatest(Generation, sequence);
_ = view.DiagnosticCounts;
exchange.Release(in view);
}
private static void PublishEmpty(
RenderFrameExchange exchange,
RenderSceneGeneration generation,
ulong sequence)
{
RenderFrameWriter writer = exchange.BeginBuild(generation, sequence);
writer.SetSourceDigest(new RenderSceneDigest(
generation,
default,
default));
writer.Publish();
}
private static RenderProjectionRecord Projection(ulong value) =>
new RenderProjectionRecord() with
{
Id = RenderProjectionId.FromRaw(value),
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
MeshSet = new RenderMeshSet(
RenderAssetHandle.FromRaw(value),
1,
1),
Material = new RenderMaterialVariant(0, 0, 1),
Transform = new RenderTransform(Matrix4x4.Identity),
PreviousTransform = new PreviousRenderTransform(Matrix4x4.Identity),
Flags = RenderProjectionFlags.Draw,
};
private static RetailSelectionMesh SelectionMesh() =>
new(
Vector3.Zero,
1f,
[new RetailSelectionPolygon(
[Vector3.Zero, Vector3.UnitX, Vector3.UnitY],
SingleSided: false)]);
}