A full Release App run failed once at Issue181WallPressEquilibriumTests
.Diagnostic_WallPressedCamera_EyeWanderAndViewerCellStability. It passed in
isolation and did not recur across five further whole-suite runs, and the diff
under test touched only the world texture-creation stack -- nothing in camera,
visibility or physics. A cross-class parallelism race was the only plausible
mechanism, not a regression.
Ten App test classes share three process-global mutable statics, and xUnit runs
distinct test classes in parallel by default:
- CameraDiagnostics: AlignToSlope, CollideCamera, TranslationStiffness,
RotationStiffness, UseRetailChaseCamera. These are not merely written, they
are written AWAY from their defaults -- RetailChaseCameraTests sets
AlignToSlope and CollideCamera to false, and three classes set
UseRetailChaseCamera to false -- while RetailChaseCamera.Update,
CameraController.Active, CameraFrameController, WorldRenderFrameBuilder and
MouseLookController read them.
- RenderingDiagnostics.ProbeFlapEnabled, written by CornerFloodReplayTests
and Issue181WallPressEquilibriumTests.
- System.Console.Out, redirected by those same two classes to capture probe
output.
Every one of these classes already saved and restored in try/finally. That is
correct within a class and remains necessary, but it was never sufficient. A
finally bounds a mutation in TIME along its own thread; it cannot stop another
class from reading the static inside that window. Worse, two overlapping
save/restore pairs can interleave so the second restore writes back the FIRST
one's temporary value, leaving the global permanently wrong for the rest of the
run. The Console.Out case is the sharpest instance: an interleaved restore can
install a DISPOSED StringWriter as the process-wide Console.Out, which then
throws in unrelated tests. Serializing the sharers is what makes each class's
existing finally sufficient.
The fix is a marker CollectionDefinition applied to the ten sharing classes,
following the WorldEnvironmentControllerCollection precedent. No collection
fixture: several members are [Theory] cases that need different knob values per
case, so a fixture cannot own the save/restore without rewriting every member's
internals, and it would not help the read side at all. Because every member
references the same compile-time const for the collection name, the grouping
cannot silently drift via a typo.
Membership is deliberately narrow. It covers the eight writers plus two classes
that drive production code which READS a knob another member moves off its
default (HouseExitWalkReplayTests and CameraFrameControllerTests both run
RetailChaseCamera.Update and assert on the resulting eye). Classes that merely
construct a CameraController without a retail chase camera are NOT members --
their reads fall through the null branch and are insensitive.
No production code changed; no assertion was weakened, and no retry, sleep or
tolerance was added.
Verification. Base commit f6275f45 measured empirically at 3,763 passed / 3
skipped. Post-fix: 136 whole-suite Release runs. Every failure observed was in
the pre-existing zero-allocation family tracked as #250 (an Expected 0 / Actual
N bytes assertion), and none was in any collection member. A matched 55-run
baseline at f6275f45 reproduced that same family, confirming it predates this
change. Serialization cost is inside run-to-run noise: the suite is ~3 s of a
~4.5 s wall-clock dotnet test, and the ten serialized classes are a small
fraction of it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
554 lines
20 KiB
C#
554 lines
20 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.Core.World;
|
|
using AcDream.Core.World.Cells;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
[Collection(CameraDiagnosticsCollection.Name)]
|
|
public sealed class WorldRenderFrameBuilderTests
|
|
{
|
|
[Fact]
|
|
public void Build_orders_world_preparation_and_combines_borrowed_results()
|
|
{
|
|
List<string> calls = [];
|
|
var camera = new FlyCamera { Position = new Vector3(11f, 22f, 33f) };
|
|
var cameraFrame = new WorldCameraFrame(
|
|
camera,
|
|
camera.Projection,
|
|
camera.View * camera.Projection,
|
|
default,
|
|
Matrix4x4.Identity,
|
|
camera.Position);
|
|
WorldRootFrame roots = default;
|
|
var animated = new HashSet<uint> { 41u, 42u };
|
|
var buildings = new List<LoadedCell>();
|
|
var foundation = new RenderFrameFoundation(
|
|
PortalViewportVisible: false,
|
|
Sky: default,
|
|
Atmosphere: default);
|
|
var visibility = new RecordingVisibility(calls);
|
|
var environment = new RecordingEnvironment(calls);
|
|
var builder = new WorldRenderFrameBuilder(
|
|
new RecordingCamera(calls, cameraFrame),
|
|
visibility,
|
|
new RecordingSettings(calls),
|
|
new RecordingRoots(calls, roots),
|
|
environment,
|
|
new RecordingAnimated(calls, animated),
|
|
new RecordingBuildings(calls, new WorldBuildingFrame(null, buildings)));
|
|
|
|
WorldRenderFrame result = builder.Build(
|
|
in foundation,
|
|
waitingForLogin: true,
|
|
activeDayGroup: null);
|
|
var drawableCells = new HashSet<uint> { 0x01010001u };
|
|
builder.ObserveDrawableCells(drawableCells);
|
|
builder.ClearDrawableCells();
|
|
|
|
Assert.Equal(
|
|
[
|
|
"camera",
|
|
"visibility:begin",
|
|
"settings",
|
|
"roots",
|
|
"environment",
|
|
"visibility:projection",
|
|
"animated",
|
|
"buildings",
|
|
],
|
|
calls);
|
|
Assert.Same(camera, result.Camera.Camera);
|
|
Assert.Same(animated, result.AnimatedEntityIds);
|
|
Assert.Same(buildings, result.Buildings.NearbyBuildingCells);
|
|
Assert.Null(result.ClipRoot);
|
|
Assert.True(visibility.WaitingForLogin);
|
|
Assert.Equal(foundation, environment.Foundation);
|
|
Assert.Same(drawableCells, environment.DrawableCells);
|
|
Assert.Equal(1, environment.ClearCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Required_sources_fail_fast()
|
|
{
|
|
var camera = new RecordingCamera([], default);
|
|
var visibility = new RecordingVisibility([]);
|
|
var settings = new RecordingSettings([]);
|
|
var roots = new RecordingRoots([], default);
|
|
var environment = new RecordingEnvironment([]);
|
|
var animated = new RecordingAnimated([], []);
|
|
var buildings = new RecordingBuildings([], default);
|
|
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new WorldRenderFrameBuilder(null!, visibility, settings, roots, environment, animated, buildings));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new WorldRenderFrameBuilder(camera, null!, settings, roots, environment, animated, buildings));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new WorldRenderFrameBuilder(camera, visibility, null!, roots, environment, animated, buildings));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new WorldRenderFrameBuilder(camera, visibility, settings, null!, environment, animated, buildings));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new WorldRenderFrameBuilder(camera, visibility, settings, roots, null!, animated, buildings));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new WorldRenderFrameBuilder(camera, visibility, settings, roots, environment, null!, buildings));
|
|
Assert.Throws<ArgumentNullException>(() =>
|
|
new WorldRenderFrameBuilder(camera, visibility, settings, roots, environment, animated, null!));
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_range_state_updates_both_tiers_without_replacing_the_source()
|
|
{
|
|
IWorldRenderRangeSource source = new WorldRenderRangeState(4, 12);
|
|
var mutable = Assert.IsType<WorldRenderRangeState>(source);
|
|
|
|
mutable.NearRadius = 7;
|
|
mutable.FarRadius = 19;
|
|
|
|
Assert.Equal(7, source.NearRadius);
|
|
Assert.Equal(19, source.FarRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void Runtime_animated_source_reuses_scratch_and_removes_stale_ids()
|
|
{
|
|
var slot = new LiveEntityRuntimeSlot();
|
|
var live = new LiveEntityAnimationRuntimeView<LiveEntityAnimationState>(slot);
|
|
var source = new RuntimeWorldFrameAnimatedEntitySource(
|
|
live,
|
|
statics: null,
|
|
equipped: null);
|
|
|
|
HashSet<uint> first = source.Capture();
|
|
first.Add(0xDEADBEEFu);
|
|
HashSet<uint> second = source.Capture();
|
|
|
|
Assert.Same(first, second);
|
|
Assert.Empty(second);
|
|
}
|
|
|
|
[Fact]
|
|
public void Runtime_building_source_reuses_scratch_and_rebuilds_outdoor_root()
|
|
{
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => { },
|
|
state: new GpuWorldState());
|
|
var source = new RuntimeWorldFrameBuildingSource(
|
|
pipeline,
|
|
new CellVisibility());
|
|
FrustumPlanes frustum = default;
|
|
|
|
WorldBuildingFrame first = source.Gather(
|
|
viewerRoot: null,
|
|
viewerCellId: 0x01010001u,
|
|
in frustum);
|
|
var scratch = Assert.IsType<List<LoadedCell>>(first.NearbyBuildingCells);
|
|
scratch.Add(new LoadedCell { CellId = 0x01010100u });
|
|
WorldBuildingFrame second = source.Gather(
|
|
viewerRoot: null,
|
|
viewerCellId: 0x02020001u,
|
|
in frustum);
|
|
|
|
Assert.Same(scratch, second.NearbyBuildingCells);
|
|
Assert.Empty(second.NearbyBuildingCells);
|
|
Assert.Equal(0x01010001u, first.OutdoorNode?.CellId);
|
|
Assert.Equal(0x02020001u, second.OutdoorNode?.CellId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Runtime_root_source_keeps_player_lighting_and_viewer_render_cells_distinct()
|
|
{
|
|
const uint playerCellId = 0x01010100u;
|
|
const uint viewerCellId = 0x01010101u;
|
|
var physics = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
|
physics.DataCache.CellGraph.Add(CoreCell(playerCellId, seenOutside: false));
|
|
physics.UpdatePlayerCurrCell(playerCellId);
|
|
var cells = new CellVisibility();
|
|
var playerCell = new LoadedCell { CellId = playerCellId, SeenOutside = false };
|
|
var viewerCell = new LoadedCell { CellId = viewerCellId, SeenOutside = true };
|
|
cells.AddCell(playerCell);
|
|
cells.AddCell(viewerCell);
|
|
var retailChase = new RetailChaseCamera();
|
|
retailChase.Update(
|
|
playerPosition: Vector3.Zero,
|
|
playerYaw: 0f,
|
|
playerVelocity: Vector3.Zero,
|
|
isOnGround: true,
|
|
contactPlaneNormal: Vector3.UnitZ,
|
|
dt: 1f / 60f,
|
|
cellId: viewerCellId);
|
|
var mode = new LocalPlayerModeState { IsPlayerMode = true };
|
|
var chase = new ChaseCameraInputState { Retail = retailChase };
|
|
var origin = new LiveWorldOriginState();
|
|
origin.SetPlaceholder(0x01, 0x01);
|
|
var source = new RuntimeWorldFrameRootSource(
|
|
physics,
|
|
cells,
|
|
mode,
|
|
chase,
|
|
new RuntimeLocalPlayerMovementState(),
|
|
origin);
|
|
var camera = new FlyCamera { Position = new Vector3(5f, 6f, 7f) };
|
|
WorldCameraFrame cameraFrame = CameraFrame(camera);
|
|
bool previousRetailCamera = CameraDiagnostics.UseRetailChaseCamera;
|
|
|
|
try
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = true;
|
|
WorldRootFrame result = source.Resolve(in cameraFrame);
|
|
|
|
Assert.Same(playerCell, result.PlayerRoot);
|
|
Assert.Same(viewerCell, result.ViewerRoot);
|
|
Assert.True(result.PlayerInsideCell);
|
|
Assert.True(result.CameraInsideCell);
|
|
Assert.False(result.PlayerSeenOutside);
|
|
Assert.True(result.RootSeenOutside);
|
|
Assert.True(result.RenderSky);
|
|
Assert.Equal(playerCellId, result.PlayerCellId);
|
|
Assert.Equal(viewerCellId, result.ViewerCellId);
|
|
Assert.Equal(camera.Position, result.ViewerEyePosition);
|
|
}
|
|
finally
|
|
{
|
|
CameraDiagnostics.UseRetailChaseCamera = previousRetailCamera;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Runtime_root_source_preserves_null_root_fallback_before_player_membership()
|
|
{
|
|
var physics = new PhysicsEngine();
|
|
var source = new RuntimeWorldFrameRootSource(
|
|
physics,
|
|
new CellVisibility(),
|
|
new LocalPlayerModeState(),
|
|
new ChaseCameraInputState(),
|
|
new RuntimeLocalPlayerMovementState(),
|
|
new LiveWorldOriginState());
|
|
WorldCameraFrame camera = CameraFrame(new FlyCamera());
|
|
|
|
WorldRootFrame result = source.Resolve(in camera);
|
|
|
|
Assert.Null(result.PlayerRoot);
|
|
Assert.Null(result.ViewerRoot);
|
|
Assert.Equal(0u, result.PlayerCellId);
|
|
Assert.Equal(0u, result.ViewerCellId);
|
|
Assert.False(result.PlayerInsideCell);
|
|
Assert.False(result.CameraInsideCell);
|
|
Assert.True(result.RenderSky);
|
|
}
|
|
|
|
[Fact]
|
|
public void Runtime_environment_copies_visible_cells_and_clear_restores_unscoped_lights()
|
|
{
|
|
const uint visibleCell = 0x01010100u;
|
|
const uint hiddenCell = 0x01010101u;
|
|
var lighting = new LightManager();
|
|
var visibleLight = new LightSource
|
|
{
|
|
Kind = LightKind.Point,
|
|
CellId = visibleCell,
|
|
};
|
|
var hiddenLight = new LightSource
|
|
{
|
|
Kind = LightKind.Point,
|
|
CellId = hiddenCell,
|
|
};
|
|
lighting.Register(visibleLight);
|
|
lighting.Register(hiddenLight);
|
|
var environment = new RuntimeWorldFrameEnvironmentPreparation(
|
|
RuntimeOptions.Parse("test-dat", _ => null),
|
|
new WorldTimeService(SkyStateProvider.Default()),
|
|
lighting,
|
|
dispatcher: null,
|
|
environmentCells: null,
|
|
lightingUbo: null,
|
|
new WorldRenderRangeState(4, 12),
|
|
skyPes: null);
|
|
var borrowed = new HashSet<uint> { visibleCell };
|
|
environment.ObserveDrawableCells(borrowed);
|
|
borrowed.Clear();
|
|
borrowed.Add(hiddenCell);
|
|
WorldCameraFrame camera = CameraFrame(new FlyCamera());
|
|
WorldRootFrame roots = default;
|
|
RenderFrameFoundation foundation = default;
|
|
|
|
environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null);
|
|
|
|
Assert.Contains(visibleLight, lighting.PointSnapshot);
|
|
Assert.DoesNotContain(hiddenLight, lighting.PointSnapshot);
|
|
|
|
environment.ClearDrawableCells();
|
|
environment.Prepare(in camera, in roots, in foundation, activeDayGroup: null);
|
|
|
|
Assert.Contains(visibleLight, lighting.PointSnapshot);
|
|
Assert.Contains(hiddenLight, lighting.PointSnapshot);
|
|
}
|
|
|
|
[Fact]
|
|
public void Production_builder_preserves_the_frame_preparation_order()
|
|
{
|
|
string source = BuilderSource();
|
|
|
|
AssertAppearsInOrder(
|
|
source,
|
|
"WorldCameraFrame camera = _camera.Resolve();",
|
|
"_visibility.Begin(in camera, waitingForLogin);",
|
|
"_settings.Apply(in camera);",
|
|
"WorldRootFrame roots = _roots.Resolve(in camera);",
|
|
"_environment.Prepare(in camera, in roots, in foundation, activeDayGroup);",
|
|
"_visibility.PublishViewProjection(in camera);",
|
|
"HashSet<uint> animated = _animated.Capture();",
|
|
"WorldBuildingFrame buildings = _buildings.Gather(");
|
|
}
|
|
|
|
[Fact]
|
|
public void Environment_preparation_keeps_lighting_snapshot_before_ubo_upload()
|
|
{
|
|
string source = BuilderSource();
|
|
|
|
AssertAppearsInOrder(
|
|
source,
|
|
"UpdateSunFromSky(foundation.Sky, roots.PlayerInsideCell);",
|
|
"_lighting.UpdateViewerLight(roots.PlayerViewPosition);",
|
|
"_lighting.Tick(camera.Position);",
|
|
"_lighting.BuildPointLightSnapshot(",
|
|
"_dispatcher?.SetSceneLights(_lighting.PointSnapshot);",
|
|
"_environmentCells?.SetPointSnapshot(_lighting.PointSnapshot);",
|
|
"SceneLightingUbo ubo = SceneLightingUbo.Build(",
|
|
"_lightingUbo?.Upload(ubo);",
|
|
"RenderingDiagnostics.EmitLight(");
|
|
}
|
|
|
|
[Fact]
|
|
public void World_scene_uses_the_typed_builder_and_orchestrator_owns_its_local_composition()
|
|
{
|
|
string gameWindow = GameWindowSource();
|
|
string lifetime = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindowLifetime.cs"));
|
|
string frameRoot = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Composition",
|
|
"FrameRootComposition.cs"));
|
|
string worldScene = File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"WorldSceneRenderer.cs"));
|
|
|
|
Assert.Equal(
|
|
1,
|
|
CountOccurrences(worldScene, "_frames.Build("));
|
|
Assert.DoesNotContain("private void UpdateSunFromSky(", gameWindow, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("private void UpdateSkyPes(", gameWindow, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("private static float ParseEnvFloat(", gameWindow, StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"private AcDream.App.Rendering.WorldRenderFrameBuilder?",
|
|
gameWindow,
|
|
StringComparison.Ordinal);
|
|
Assert.DoesNotContain(
|
|
"private AcDream.App.Rendering.SkyPesFrameController?",
|
|
gameWindow,
|
|
StringComparison.Ordinal);
|
|
AssertAppearsInOrder(
|
|
worldScene,
|
|
"_alpha.BeginFrame();",
|
|
"_frames.Build(",
|
|
"if (_login.IsWaitingForLogin)",
|
|
"_passes.DrawFlatTerrain(",
|
|
"NormalWorldDrawn: true");
|
|
AssertAppearsInOrder(
|
|
frameRoot,
|
|
"var skyPesFrame = new SkyPesFrameController(",
|
|
"var worldRenderFrameBuilder =",
|
|
"new RenderFrameOrchestrator(",
|
|
"d.FrameGraphs.PublishOwned(");
|
|
AssertAppearsInOrder(
|
|
lifetime,
|
|
"frame.FrameGraphPublication?.Dispose()",
|
|
"Hard(\"equipped children\"",
|
|
"Hard(\"effect network state\"",
|
|
"Hard(\"audio\"",
|
|
"Hard(\"mesh draw dispatcher\"",
|
|
"Hard(\"environment cells\"",
|
|
"Hard(\"scene lighting\"");
|
|
}
|
|
|
|
private static string BuilderSource() => File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"WorldRenderFrameBuilder.cs"));
|
|
|
|
private static WorldCameraFrame CameraFrame(FlyCamera camera) => new(
|
|
camera,
|
|
camera.Projection,
|
|
camera.View * camera.Projection,
|
|
default,
|
|
Matrix4x4.Identity,
|
|
camera.Position);
|
|
|
|
private static AcDream.Core.World.Cells.EnvCell CoreCell(
|
|
uint id,
|
|
bool seenOutside) => new(
|
|
id,
|
|
Matrix4x4.Identity,
|
|
Matrix4x4.Identity,
|
|
Vector3.Zero,
|
|
Vector3.One,
|
|
Array.Empty<CellPortal>(),
|
|
Array.Empty<uint>(),
|
|
seenOutside,
|
|
containmentBsp: null);
|
|
|
|
private static string GameWindowSource() => File.ReadAllText(Path.Combine(
|
|
FindRepoRoot(),
|
|
"src",
|
|
"AcDream.App",
|
|
"Rendering",
|
|
"GameWindow.cs"));
|
|
|
|
private static int CountOccurrences(string source, string needle)
|
|
{
|
|
int count = 0;
|
|
int cursor = 0;
|
|
while ((cursor = source.IndexOf(needle, cursor, StringComparison.Ordinal)) >= 0)
|
|
{
|
|
count++;
|
|
cursor += needle.Length;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
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 >= 0, $"Missing expected source fragment: {needle}");
|
|
Assert.True(next > cursor, $"Out-of-order source 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 RecordingCamera(
|
|
List<string> calls,
|
|
WorldCameraFrame result) : IWorldFrameCameraSource
|
|
{
|
|
public WorldCameraFrame Resolve()
|
|
{
|
|
calls.Add("camera");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingVisibility(List<string> calls)
|
|
: IWorldFrameVisibilityPreparation
|
|
{
|
|
public bool WaitingForLogin { get; private set; }
|
|
|
|
public void Begin(in WorldCameraFrame camera, bool waitingForLogin)
|
|
{
|
|
calls.Add("visibility:begin");
|
|
WaitingForLogin = waitingForLogin;
|
|
}
|
|
|
|
public void PublishViewProjection(in WorldCameraFrame camera) =>
|
|
calls.Add("visibility:projection");
|
|
}
|
|
|
|
private sealed class RecordingSettings(List<string> calls)
|
|
: IWorldFrameSettingsPreview
|
|
{
|
|
public void Apply(in WorldCameraFrame camera) => calls.Add("settings");
|
|
}
|
|
|
|
private sealed class RecordingRoots(
|
|
List<string> calls,
|
|
WorldRootFrame result) : IWorldFrameRootSource
|
|
{
|
|
public WorldRootFrame Resolve(in WorldCameraFrame camera)
|
|
{
|
|
calls.Add("roots");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingEnvironment(List<string> calls)
|
|
: IWorldFrameEnvironmentPreparation
|
|
{
|
|
public RenderFrameFoundation Foundation { get; private set; }
|
|
|
|
public IReadOnlySet<uint>? DrawableCells { get; private set; }
|
|
|
|
public int ClearCount { get; private set; }
|
|
|
|
public void Prepare(
|
|
in WorldCameraFrame camera,
|
|
in WorldRootFrame roots,
|
|
in RenderFrameFoundation foundation,
|
|
DayGroupData? activeDayGroup)
|
|
{
|
|
calls.Add("environment");
|
|
Foundation = foundation;
|
|
}
|
|
|
|
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
|
|
DrawableCells = drawableCells;
|
|
|
|
public void ClearDrawableCells() => ClearCount++;
|
|
}
|
|
|
|
private sealed class RecordingAnimated(
|
|
List<string> calls,
|
|
HashSet<uint> result) : IWorldFrameAnimatedEntitySource
|
|
{
|
|
public HashSet<uint> Capture()
|
|
{
|
|
calls.Add("animated");
|
|
return result;
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingBuildings(
|
|
List<string> calls,
|
|
WorldBuildingFrame result) : IWorldFrameBuildingSource
|
|
{
|
|
public WorldBuildingFrame Gather(
|
|
LoadedCell? viewerRoot,
|
|
uint viewerCellId,
|
|
in FrustumPlanes frustum)
|
|
{
|
|
calls.Add("buildings");
|
|
return result;
|
|
}
|
|
}
|
|
}
|