checkpoint: preserve cathedral look-in investigation state

This commit is contained in:
Erik 2026-08-30 23:29:28 +02:00
parent c287db8651
commit 572de1ec30
11 changed files with 757 additions and 197 deletions

View file

@ -3,12 +3,23 @@ using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Walk;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering;
public sealed class RenderScenePViewFrameProductTests
{
private sealed class RejectAllLookInViews(uint cellId) : IWalkLookInViewSource
{
public IReadOnlyList<uint> LookInCellTurns { get; } = [cellId];
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius) => false;
}
private const uint Landblock = 0xA9B4FFFF;
private const uint Cell = 0xA9B40170;
private static readonly RenderSceneGeneration Generation =
@ -159,6 +170,7 @@ public sealed class RenderScenePViewFrameProductTests
clip,
viewcone,
[],
null,
[Cell],
EmptyCellSource.Instance,
[],
@ -168,6 +180,134 @@ public sealed class RenderScenePViewFrameProductTests
}
}
[Fact]
public void Builder_KeysLookInDynamicsToWalkTurnsAndExcludesThemFromDynamicLast()
{
const uint lookInCell = 0xA9B4_0171;
using var scene = new ArchRenderScene(Generation);
RenderProjectionRecord rootDynamic = Record(
0x0100_0000_0000_0021,
RenderProjectionClass.LiveDynamicRoot,
parentCell: Cell);
RenderProjectionRecord lookInDynamic = Record(
0x0100_0000_0000_0022,
RenderProjectionClass.LiveDynamicRoot,
parentCell: lookInCell);
scene.Apply(
[
RenderProjectionDelta.Register(Generation, 1, rootDynamic),
RenderProjectionDelta.Register(Generation, 2, lookInDynamic),
]);
PortalVisibilityFrame portal = Portal(Cell);
ClipFrameAssembly clip = FullScreenClip(Cell);
ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity);
RenderSceneQuery query = scene.OpenQuery();
var input = new RenderScenePViewBuildInput(
query,
new RenderSceneDigest(query.Generation, query.Counts, default),
portal,
clip,
viewcone,
LookInCellTurns: [lookInCell],
WalkLookInViews: null,
DrawableCells: [Cell, lookInCell],
Cells: EmptyCellSource.Instance,
AnimatedEntityIds: [],
RootIsOutdoor: false,
RootFloodCells: new HashSet<uint> { Cell });
var exchange = new RenderFrameExchange();
var builder = new RenderScenePViewFrameBuilder();
builder.Build(exchange, frameSequence: 1, in input);
RenderFrameView frame = exchange.BorrowLatest(Generation, 1);
try
{
RenderFrameCandidateRange lookInRange = Assert.Single(
frame.RouteRanges.ToArray(),
range => range.Route == RenderFrameCandidateRoute.LookInObject);
Assert.Equal(0, lookInRange.RouteIndex);
Assert.Equal(lookInCell, lookInRange.CellId);
Assert.Equal(
[lookInDynamic.Id],
frame.RouteCandidates
.Slice(lookInRange.Offset, lookInRange.Count)
.ToArray()
.Select(static record => record.Id));
RenderFrameCandidateRange dynamicLast = Assert.Single(
frame.RouteRanges.ToArray(),
range => range.Route == RenderFrameCandidateRoute.DynamicLast);
RenderProjectionId[] lastIds = frame.RouteCandidates
.Slice(dynamicLast.Offset, dynamicLast.Count)
.ToArray()
.Select(static record => record.Id)
.ToArray();
Assert.Contains(rootDynamic.Id, lastIds);
Assert.DoesNotContain(lookInDynamic.Id, lastIds);
}
finally
{
exchange.Release(in frame);
}
}
[Fact]
public void Builder_CarriesExactWalkConeForPerPartDispatcherAdmission()
{
const uint lookInCell = 0xA9B4_0171;
using var scene = new ArchRenderScene(Generation);
RenderProjectionRecord lookInDynamic = Record(
0x0100_0000_0000_0022,
RenderProjectionClass.LiveDynamicRoot,
parentCell: lookInCell);
scene.Apply([RenderProjectionDelta.Register(Generation, 1, lookInDynamic)]);
PortalVisibilityFrame portal = Portal(Cell);
ClipFrameAssembly clip = FullScreenClip(Cell);
ViewconeCuller viewcone = ViewconeCuller.Build(clip, Matrix4x4.Identity);
RenderSceneQuery query = scene.OpenQuery();
var input = new RenderScenePViewBuildInput(
query,
new RenderSceneDigest(query.Generation, query.Counts, default),
portal,
clip,
viewcone,
LookInCellTurns: [lookInCell],
WalkLookInViews: new RejectAllLookInViews(lookInCell),
DrawableCells: [Cell, lookInCell],
Cells: EmptyCellSource.Instance,
AnimatedEntityIds: [],
RootIsOutdoor: false,
RootFloodCells: new HashSet<uint> { Cell });
var exchange = new RenderFrameExchange();
var builder = new RenderScenePViewFrameBuilder();
builder.Build(exchange, frameSequence: 1, in input);
RenderFrameView frame = exchange.BorrowLatest(Generation, 1);
try
{
RenderFrameCandidateRange range = Assert.Single(
frame.RouteRanges.ToArray(),
candidate => candidate.Route == RenderFrameCandidateRoute.LookInObject);
Assert.Equal(
[lookInDynamic.Id],
frame.RouteCandidates
.Slice(range.Offset, range.Count)
.ToArray()
.Select(static record => record.Id));
Assert.NotNull(frame.WalkLookInViews);
Assert.False(frame.WalkLookInViews!.SphereVisibleInLookInTurn(
routeIndex: 0,
center: Vector3.Zero,
radius: 1f));
}
finally
{
exchange.Release(in frame);
}
}
[Fact]
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
{

View file

@ -67,6 +67,18 @@ public sealed class WalkFrameDriverTests
sorted.Sort();
log.Add($"PARTICLES:{string.Join(",", sorted.ConvertAll(o => o.ToString("x")))}");
}
public void DrawLookInDynamics(
uint cellId,
int routeIndex,
IReadOnlySet<uint> staticParticleOwnerIds)
{
var sorted = new List<uint>(staticParticleOwnerIds);
sorted.Sort();
log.Add(
$"LOOKIN:{cellId:x8}:{routeIndex}:"
+ string.Join(",", sorted.ConvertAll(o => o.ToString("x"))));
}
}
private sealed class RecordingTrace(List<string> log) : IWalkFrameDriverTrace
@ -417,10 +429,28 @@ public sealed class WalkFrameDriverTests
new[]
{
"ALPHA:12.50", "PUNCH:4@v0", "SHELL:00000104",
"FLUSH:1:LookInStatic", "FLUSH:1:BuildingShell", "PARTICLES:c9",
"FLUSH:1:LookInStatic", "LOOKIN:00000104:0:ca",
"FLUSH:1:BuildingShell", "PARTICLES:c9",
},
log);
Assert.Equal([0x104u], driver.LookInCellTurns);
Assert.Equal([0x104u], driver.LookInCells);
WalkPortalView capturedView = ctx.Cells[0x104].PortalViews[0];
WalkViewPoly capturedPoly = Assert.Single(capturedView.View.Polys);
Vector2 capturedCenter = Vector2.Zero;
for (int edge = 0; edge < capturedPoly.VertexCount; edge++)
{
capturedCenter += capturedView.View.Vertices[
capturedPoly.VertexIndex + edge].Point;
}
capturedCenter /= capturedPoly.VertexCount;
Vector3 insideCone = ctx.Rays.RayThrough(capturedCenter.X, capturedCenter.Y);
Assert.True(driver.SphereVisibleInLookInTurn(
0, in insideCone, 0.1f));
Assert.False(driver.SphereVisibleInLookInTurn(
0, new Vector3(10_000f, 0f, 10f), 0.1f));
// The punch polygon reached the leaf renderer in WORLD space: the
// building-local Quad(-2f) vertex (-0.5,-0.5,-2) translates by
// (10,0,0) under the caller-supplied building world transform.

View file

@ -0,0 +1,60 @@
using System.Numerics;
using AcDream.App.Rendering.Walk;
using AcDream.App.Rendering.Wb;
using DatReaderWriter.Types;
namespace AcDream.App.Tests.Rendering.Wb;
public sealed class WbDrawDispatcherLookInConeTests
{
[Fact]
public void DrawingSphereAdmissionUsesTheIndividualPartTransformAndLargestScale()
{
var cone = new RecordingLookInViews();
var sphere = new Sphere
{
Origin = new Vector3(1f, 2f, 3f),
Radius = 0.5f,
};
Matrix4x4 transform =
Matrix4x4.CreateScale(2f, 3f, 4f)
* Matrix4x4.CreateTranslation(10f, 20f, 30f);
bool visible = WbDrawDispatcher.LookInDrawingSphereVisible(
cone,
routeIndex: 7,
sphere,
transform,
out Vector3 center,
out float radius);
Assert.True(visible);
Assert.Equal(7, cone.RouteIndex);
Assert.Equal(new Vector3(12f, 26f, 42f), center);
Assert.Equal(center, cone.Center);
Assert.Equal(2f, radius);
Assert.Equal(radius, cone.Radius);
}
private sealed class RecordingLookInViews : IWalkLookInViewSource
{
public IReadOnlyList<uint> LookInCellTurns { get; } = [0xF4180112u];
public int RouteIndex { get; private set; }
public Vector3 Center { get; private set; }
public float Radius { get; private set; }
public bool SphereVisibleInLookInTurn(
int routeIndex,
in Vector3 center,
float radius)
{
RouteIndex = routeIndex;
Center = center;
Radius = radius;
return true;
}
}
}