acdream/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
Erik b3427554c3 perf(rendering): retain synchronous frame scratch
Reuse the PView frame input, publish mutation-invalidated landblock views, and avoid constructing optional shadow iterators while preserving title and lifecycle visibility facts.
2026-07-25 05:28:30 +02:00

728 lines
27 KiB
C#

using System.Numerics;
using System.Reflection;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering;
public sealed class RetailPViewPassExecutorTests
{
[Fact]
public void DrawInside_outdoor_executes_the_real_typed_sequence_without_interior_clear()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
LoadedCell root = OutdoorCellNode.Build(0xA9B40000u);
renderer.DrawInside(Frame(root), executor);
Assert.Equal(
[
"begin",
"assemble",
"prepare-clip:3",
"indoor-routing",
"prepare-cells",
"diagnostics",
"terrain-clip",
"clear-routing",
"landscape-early",
"terrain-clip",
"clear-routing",
"outstage-routing",
"landscape-late",
"landscape-alpha",
"indoor-routing",
"indoor-routing",
"exit-mask",
"indoor-routing",
"opaque-shells",
],
executor.Operations);
}
[Fact]
public void DrawInside_interior_exit_flushes_before_clear_and_resets_borrowed_result()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
LoadedCell outdoor = OutdoorCellNode.Build(0xA9B40000u);
RetailPViewFrameResult borrowed = renderer.DrawInside(Frame(outdoor), executor);
Assert.Contains(outdoor.CellId, borrowed.DrawableCells);
executor.Operations.Clear();
LoadedCell interior = InteriorWithExit(0xA9B40100u);
RetailPViewFrameResult reused = renderer.DrawInside(Frame(interior), executor);
Assert.Same(borrowed, reused);
Assert.DoesNotContain(outdoor.CellId, reused.DrawableCells);
Assert.Contains(interior.CellId, reused.DrawableCells);
AssertAppearsInOrder(
string.Join('|', executor.Operations),
"landscape-early",
"landscape-late",
"unattached-particles",
"landscape-alpha",
"interior-depth-clear",
"indoor-routing",
"exit-mask");
}
[Fact]
public void DrawInside_interior_without_an_outside_slice_skips_landscape_and_depth_clear()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
var root = new LoadedCell
{
CellId = 0xA9B40100u,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
};
renderer.DrawInside(Frame(root), executor);
Assert.DoesNotContain("terrain-clip", executor.Operations);
Assert.DoesNotContain("landscape-early", executor.Operations);
Assert.DoesNotContain("landscape-late", executor.Operations);
Assert.DoesNotContain("landscape-alpha", executor.Operations);
Assert.DoesNotContain("interior-depth-clear", executor.Operations);
}
[Fact]
public void Particle_classifications_reset_before_an_empty_following_frame()
{
var classifications = new RetailPViewParticleClassifications();
classifications.ReplaceOutdoor([Entity(7u)]);
classifications.Visible.Add(8u);
classifications.Dynamics.Add(9u);
classifications.BeginFrame();
Assert.Empty(classifications.Outdoor);
Assert.Empty(classifications.Visible);
Assert.Empty(classifications.Dynamics);
}
[Fact]
public void DrawInside_real_fixtures_reach_entity_particle_and_transparent_shell_operations()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor { HasTransparentShells = true };
LoadedCell interior = InteriorWithExit(0xA9B40100u);
WorldEntity cellStatic = Entity(10u, parentCellId: interior.CellId);
WorldEntity dynamic = Entity(
11u,
serverGuid: 0x80000011u,
parentCellId: interior.CellId);
renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor);
Assert.Contains("opaque-shells", executor.Operations);
Assert.Contains("transparent-shells-ordered", executor.Operations);
Assert.Contains("entity-bucket", executor.Operations);
Assert.Contains("cell-particles", executor.Operations);
Assert.Contains("dynamics-particles", executor.Operations);
Assert.Contains("phantom-objects", executor.Operations);
}
[Fact]
public void DrawInside_referee_records_the_exact_routed_entity_buckets()
{
var oracle = new CurrentRenderSceneOracle();
var renderer = new RetailPViewRenderer(oracle);
using var executor = new RecordingExecutor();
LoadedCell interior = InteriorWithExit(0xA9B40100u);
WorldEntity cellStatic = Entity(20u, parentCellId: interior.CellId);
WorldEntity dynamic = Entity(
21u,
serverGuid: 0x80000021u,
parentCellId: interior.CellId);
renderer.DrawInside(Frame(interior, [cellStatic, dynamic]), executor);
Assert.Equal(1uL, oracle.Snapshot.CompletedPViewFrameSequence);
Assert.Equal(2, oracle.Snapshot.PViewCandidateCount);
Assert.Collection(
oracle.PViewCandidates,
candidate =>
{
Assert.Equal(CurrentRenderPViewRoute.CellStatic, candidate.Route);
Assert.Equal(cellStatic.Id, candidate.Projection.EntityId);
},
candidate =>
{
Assert.Equal(CurrentRenderPViewRoute.DynamicLast, candidate.Route);
Assert.Equal(dynamic.Id, candidate.Projection.EntityId);
});
}
[Fact]
public void DrawInside_production_product_supplies_the_ordered_entity_routes()
{
LoadedCell interior = InteriorWithExit(0xA9B40100u);
WorldEntity cellStatic = Entity(
30u,
parentCellId: interior.CellId);
WorldEntity dynamic = Entity(
31u,
serverGuid: 0x80000031u,
parentCellId: interior.CellId);
uint landblockId = interior.CellId & 0xFFFF0000u;
var oracle = new CurrentRenderSceneOracle();
using var shadow = new RenderSceneShadowRuntime(
RenderSceneGeneration.FromRaw(1));
RenderProjectionRecord staticRecord =
RenderProjectionRecordFactory.ProjectEntity(
StaticRenderProjectionJournal.StaticEntityId(
landblockId,
cellStatic.Id),
RenderProjectionClass.IndoorCellStatic,
RenderOwnerIncarnation.FromRaw(cellStatic.Id),
landblockId,
interior.CellId,
cellStatic,
spatiallyVisible: true);
RenderProjectionRecord dynamicRecord =
RenderProjectionRecordFactory.ProjectEntity(
LiveRenderProjectionJournal.ProjectionId(dynamic.Id),
RenderProjectionClass.LiveDynamicRoot,
RenderOwnerIncarnation.FromRaw(dynamic.Id),
landblockId,
interior.CellId,
dynamic,
spatiallyVisible: true);
shadow.Journal.Register(in staticRecord);
shadow.Journal.Register(in dynamicRecord);
shadow.DrainUpdateBoundary();
var product = new RenderScenePViewFrameProductController(
shadow,
oracle);
var renderer = new RetailPViewRenderer(oracle, product);
using var executor = new RecordingExecutor();
renderer.DrawInside(
Frame(interior, [cellStatic, dynamic]),
executor);
AssertAppearsInOrder(
string.Join('|', executor.Operations),
"entity-frame-begin",
"entity-route:CellStatic:0:00000000",
"entity-route:DynamicLast:0:00000000",
"entity-frame-complete");
Assert.Equal(0, product.Snapshot.MismatchCount);
Assert.Equal(
product.Snapshot.ExpectedDigest,
product.Snapshot.ActualDigest);
}
[Fact]
public void DrawInside_production_product_does_not_require_compare_only_referee()
{
LoadedCell interior = InteriorWithExit(0xA9B40100u);
WorldEntity dynamic = Entity(
32u,
serverGuid: 0x80000032u,
parentCellId: interior.CellId);
uint landblockId = interior.CellId & 0xFFFF0000u;
using var shadow = new RenderSceneShadowRuntime(
RenderSceneGeneration.FromRaw(1));
RenderProjectionRecord dynamicRecord =
RenderProjectionRecordFactory.ProjectEntity(
LiveRenderProjectionJournal.ProjectionId(dynamic.Id),
RenderProjectionClass.LiveDynamicRoot,
RenderOwnerIncarnation.FromRaw(dynamic.Id),
landblockId,
interior.CellId,
dynamic,
spatiallyVisible: true);
shadow.Journal.Register(in dynamicRecord);
shadow.DrainUpdateBoundary();
var product = new RenderScenePViewFrameProductController(shadow);
var renderer = new RetailPViewRenderer(null, product);
using var executor = new RecordingExecutor();
renderer.DrawInside(Frame(interior, [dynamic]), executor);
AssertAppearsInOrder(
string.Join('|', executor.Operations),
"entity-frame-begin",
"entity-route:DynamicLast:0:00000000",
"entity-frame-complete");
Assert.Equal(0uL, product.Snapshot.ComparisonCount);
Assert.Equal(1, product.Snapshot.ActualCandidateCount);
}
[Fact]
public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch()
{
var renderer = new RetailPViewRenderer();
using var executor = new RecordingExecutor();
LoadedCell root = InteriorWithExit(0xA9B40100u);
root.BuildingId = 1u;
LoadedCell[] building = NearbyTwoCellBuilding();
renderer.DrawInside(
Frame(
root,
nearbyBuildingCells: building,
additionalCells: building),
executor);
Assert.Contains("look-in-punch", executor.Operations);
}
[Fact]
public void Renderer_source_preserves_retail_stage_order_through_typed_operations()
{
string source = RendererSource();
string drawInside = MethodBody(source, "public RetailPViewFrameResult DrawInside(");
AssertAppearsInOrder(
drawInside,
"passes.BeginFrame();",
"passes.EmitDiagnostics(ctx, result);",
"DrawLandscapeThroughOutsideView(",
"DrawExitPortalMasks(ctx, passes",
"DrawEnvCellShells(passes, pvFrame);",
"DrawCellObjectLists(",
"DrawDynamicsLast(");
string landscape = MethodBody(
source,
"private void DrawLandscapeThroughOutsideView(");
AssertAppearsInOrder(
landscape,
"passes.SetTerrainClip(slice.Planes);",
"passes.DrawLandscapeSlice(",
"DrawBuildingLookIns(",
"passes.DrawLandscapeSliceLate(",
"passes.DrawUnattachedSceneParticles(ctx);",
"passes.FlushLandscapeAlpha();",
"passes.ClearInteriorDepth();");
string lookIns = MethodBody(source, "private void DrawBuildingLookIns(");
AssertAppearsInOrder(
lookIns,
"passes.DrawLookInPortalPunch(ctx",
"passes.DrawOpaqueCellShells(_shellBatch);",
"passes.DrawCellParticles(ctx");
}
[Fact]
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
{
Assert.DoesNotContain(
typeof(RetailPViewFrameInput).GetProperties(),
property => typeof(Delegate).IsAssignableFrom(property.PropertyType));
FieldInfo[] fields = typeof(RetailPViewPassExecutor).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(CellVisibility));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameInput));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameResult));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(ClipFrameAssembly));
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
[Fact]
public void Concrete_executor_forwards_frame_reset_and_brackets_terrain_diagnostics()
{
string source = ExecutorClassSource();
string begin = MethodBody(source, "public void BeginFrame()");
Assert.Contains("_particleClassifications.BeginFrame();", begin);
string landscape = MethodBody(source, "public void DrawLandscapeSlice(");
AssertAppearsInOrder(
landscape,
"_terrainDiagnostics.Begin();",
"_terrain?.Draw(",
"_terrainDiagnostics.Complete();");
Assert.DoesNotContain("Console.WriteLine", source);
Assert.DoesNotContain("Console.WriteLine", RendererSource());
}
[Fact]
public void GameWindow_composes_one_executor_instead_of_a_draw_callback_bag()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
Assert.Contains("new RetailPViewPassExecutor(", source);
Assert.Contains("new WorldScenePViewRenderer(", source);
Assert.DoesNotContain("new AcDream.App.Rendering.RetailPViewFrameInput", source);
Assert.DoesNotContain("RetailPViewDrawContext", source);
Assert.DoesNotContain("DrawLandscapeSlice =", source);
Assert.DoesNotContain("DrawExitPortalMasks =", source);
}
private static string RendererSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"RetailPViewRenderer.cs"));
private static string ExecutorSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"RetailPViewPassExecutor.cs"));
private static string ExecutorClassSource()
{
string source = ExecutorSource();
int start = source.IndexOf(
"internal sealed class RetailPViewPassExecutor",
StringComparison.Ordinal);
Assert.True(start >= 0, "Missing concrete RetailPViewPassExecutor.");
return source[start..];
}
private static RetailPViewFrameInput Frame(
LoadedCell root,
IReadOnlyList<WorldEntity>? entities = null,
IReadOnlyList<LoadedCell>? nearbyBuildingCells = null,
IReadOnlyList<LoadedCell>? additionalCells = null)
{
var cells = new Dictionary<uint, LoadedCell> { [root.CellId] = root };
if (additionalCells is not null)
{
foreach (LoadedCell cell in additionalCells)
cells[cell.CellId] = cell;
}
var entries = new List<(
uint LandblockId,
Vector3 AabbMin,
Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)>();
if (entities is not null)
{
entries.Add((
root.CellId & 0xFFFF0000u,
Vector3.Zero,
Vector3.Zero,
entities,
null));
}
return new RetailPViewFrameInput().Reset(
root,
nearbyBuildingCells,
Vector3.Zero,
TestCamera.ViewProjection,
new DictionaryCellSource(cells),
new TestCamera(),
Vector3.Zero,
frustum: null,
playerLandblockId: root.CellId & 0xFFFF0000u,
animatedEntityIds: null,
renderCenterLbX: 0,
renderCenterLbY: 0,
renderRadius: 1,
landblockEntries: entries,
renderSky: true,
renderWeather: true,
dayFraction: 0f,
activeDayGroup: null,
skyKeyframe: default,
environOverrideActive: false,
viewerCellId: root.CellId,
playerCellId: root.CellId,
playerViewPosition: Vector3.Zero,
cameraView: TestCamera.ViewMatrix,
cameraCellResolution: CameraCellResolution.None);
}
private static LoadedCell InteriorWithExit(uint cellId)
{
var cell = new LoadedCell
{
CellId = cellId,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals = [new CellPortalInfo(0xFFFF, 0, 0, 0)],
};
cell.PortalPolygons.Add(
[
new Vector3(-1f, -1f, -2f),
new Vector3(1f, -1f, -2f),
new Vector3(1f, 1f, -2f),
new Vector3(-1f, 1f, -2f),
]);
return cell;
}
private static LoadedCell[] NearbyTwoCellBuilding()
{
const uint vestibuleId = 0xA9B40170u;
const uint roomId = 0xA9B40171u;
var vestibule = new LoadedCell
{
CellId = vestibuleId,
BuildingId = 2u,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals =
[
new CellPortalInfo(0xFFFF, 0, 0, 0),
new CellPortalInfo(0x0171, 1, 0, 0),
],
ClipPlanes =
[
new PortalClipPlane
{
Normal = new Vector3(0, 0, 1),
D = 3f,
InsideSide = 1,
},
],
};
vestibule.PortalPolygons.Add(
[
new Vector3(-0.5f, -0.5f, -2f),
new Vector3(0.5f, -0.5f, -2f),
new Vector3(0.5f, 0.5f, -2f),
new Vector3(-0.5f, 0.5f, -2f),
]);
vestibule.PortalPolygons.Add(
[
new Vector3(-0.6f, -0.6f, -4f),
new Vector3(0.6f, -0.6f, -4f),
new Vector3(0.6f, 0.6f, -4f),
new Vector3(-0.6f, 0.6f, -4f),
]);
var room = new LoadedCell
{
CellId = roomId,
BuildingId = 2u,
WorldTransform = Matrix4x4.Identity,
InverseWorldTransform = Matrix4x4.Identity,
Portals = [new CellPortalInfo(0x0170, 0, 0, 1)],
};
room.PortalPolygons.Add(
[
new Vector3(-0.6f, -0.6f, -4f),
new Vector3(0.6f, -0.6f, -4f),
new Vector3(0.6f, 0.6f, -4f),
new Vector3(-0.6f, 0.6f, -4f),
]);
return [vestibule, room];
}
private static WorldEntity Entity(
uint id,
uint serverGuid = 0,
uint? parentCellId = null) => new()
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = [new MeshRef(1u, Matrix4x4.Identity)],
ParentCellId = parentCellId,
};
private static string MethodBody(string source, string signature)
{
int start = source.IndexOf(signature, StringComparison.Ordinal);
Assert.True(start >= 0, $"Missing method: {signature}");
int open = source.IndexOf('{', start);
int depth = 0;
for (int index = open; index < source.Length; index++)
{
if (source[index] == '{')
depth++;
else if (source[index] == '}' && --depth == 0)
return source[start..(index + 1)];
}
throw new InvalidOperationException($"Unterminated method: {signature}");
}
private static void AssertAppearsInOrder(string source, params string[] needles)
{
int cursor = -1;
foreach (string needle in needles)
{
int next = source.IndexOf(needle, cursor + 1, StringComparison.Ordinal);
Assert.True(next > cursor, $"Missing or out-of-order fragment: {needle}");
cursor = next;
}
}
private static string FindRepoRoot()
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
return directory.FullName;
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
}
private sealed class DictionaryCellSource(
IReadOnlyDictionary<uint, LoadedCell> cells) : IRetailPViewCellSource
{
public LoadedCell? Find(uint cellId) =>
cells.TryGetValue(cellId, out LoadedCell? cell) ? cell : null;
}
private sealed class TestCamera : ICamera
{
public static Matrix4x4 ViewMatrix { get; } = Matrix4x4.CreateLookAt(
Vector3.Zero,
new Vector3(0, 0, -1),
Vector3.UnitY);
public static Matrix4x4 ViewProjection { get; } = ViewMatrix
* Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
public Matrix4x4 View => ViewMatrix;
public Matrix4x4 Projection { get; } =
Matrix4x4.CreatePerspectiveFieldOfView(1.2f, 1f, 0.1f, 1000f);
public float Aspect { get; set; } = 1f;
}
private sealed class RecordingExecutor :
IRetailPViewPassExecutor,
IRenderFrameEntityPassExecutor,
IDisposable
{
public void AbortFrame() => Operations.Add("abort");
private readonly ClipFrame _clipFrame = ClipFrame.NoClip();
public List<string> Operations { get; } = [];
public bool HasTransparentShells { get; init; }
public void BeginFrame() => Operations.Add("begin");
public ClipFrameAssembly AssembleClipFrame(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly reuseAssembly)
{
Operations.Add("assemble");
return ClipFrameAssembler.Assemble(_clipFrame, portalFrame, reuseAssembly);
}
public void PrepareClipFrame(int terrainUploadCount) =>
Operations.Add($"prepare-clip:{terrainUploadCount}");
public void SetTerrainClip(ReadOnlySpan<Vector4> planes) =>
Operations.Add("terrain-clip");
public void ClearClipRouting() => Operations.Add("clear-routing");
public void UseIndoorMembershipOnlyRouting() => Operations.Add("indoor-routing");
public void PrepareCellBatches(RetailPViewFrameInput frame, HashSet<uint> visibleCellIds) =>
Operations.Add("prepare-cells");
public void DrawOpaqueCellShells(HashSet<uint> cellIds) => Operations.Add("opaque-shells");
public bool CellHasTransparentShell(uint cellId) => HasTransparentShells;
public void DrawTransparentCellShells(HashSet<uint> cellIds) => Operations.Add("transparent-shells");
public void DrawTransparentCellShellsOrdered(IReadOnlyList<uint> cellIds) =>
Operations.Add("transparent-shells-ordered");
public void DrawEntityBucket(
RetailPViewFrameInput frame,
IReadOnlyList<WorldEntity> entities,
HashSet<uint>? visibleCellIds) => Operations.Add("entity-bucket");
public void EmitClipRouteProbe(
ClipFrameAssembly clipAssembly,
ClipViewSlice slice,
int sliceIndex) => Operations.Add("clip-probe");
public void EmitOutStageOwner(
WorldEntity entity,
Vector3 sphereCenter,
float sphereRadius,
int sliceIndex,
bool passed) => Operations.Add("outstage-owner");
public void EmitOutStageRouting(
int sliceIndex,
IReadOnlyList<WorldEntity> entities,
ViewconeCuller viewcone) => Operations.Add("outstage-routing");
public void EmitPhantomObjects(uint cellId, int survivorCount) =>
Operations.Add("phantom-objects");
public void DrawLandscapeSlice(
RetailPViewFrameInput frame,
RetailPViewLandscapeSliceContext context)
{
Operations.Add("landscape-early");
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView view = request.View;
DrawEntityRoute(
frame.Camera,
in view,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
}
public void DrawLandscapeSliceLate(
RetailPViewFrameInput frame,
RetailPViewLandscapeLateSliceContext context)
{
Operations.Add("landscape-late");
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
{
RenderFrameView view = request.View;
DrawEntityRoute(
frame.Camera,
in view,
request.Route,
request.RouteIndex,
request.CellId,
request.TupleLandblockId);
}
}
public void ClearInteriorDepth() => Operations.Add("interior-depth-clear");
public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask");
public void DrawLookInPortalPunch(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("look-in-punch");
public void DrawUnattachedSceneParticles(RetailPViewFrameInput frame) => Operations.Add("unattached-particles");
public void FlushLandscapeAlpha() => Operations.Add("landscape-alpha");
public void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("cell-particles");
public void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> survivors) => Operations.Add("dynamics-particles");
public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics");
public void BeginEntityFrame(in RenderFrameView view) =>
Operations.Add("entity-frame-begin");
public bool DrawEntityRoute(
ICamera camera,
in RenderFrameView view,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId,
uint tupleLandblockId)
{
Operations.Add(
$"entity-route:{route}:{routeIndex}:{cellId:X8}");
return true;
}
public void CompleteEntityFrame(in RenderFrameView view) =>
Operations.Add("entity-frame-complete");
public void AbortEntityFrame() =>
Operations.Add("entity-frame-abort");
public void Dispose() => _clipFrame.Dispose();
}
}