acdream/tests/AcDream.App.Tests/Rendering/WorldRenderFrameBuilderTests.cs
Erik 3628aeb520 refactor(app): compose atomic frame roots
Move the complete update/render construction graph into a typed Phase-8 owner, explicitly carry the content dependencies it consumes, and publish both roots through one exact lease. Extract lifecycle resource sampling and frame-owned late bindings so partial startup and shutdown withdraw the same generation without window callbacks.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 19:02:08 +02:00

547 lines
19 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;
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 LocalPlayerControllerSlot(),
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 LocalPlayerControllerSlot(),
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 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(
gameWindow,
"publication.Dispose();",
"new(\"equipped children\"",
"new(\"effect network state\"",
"new(\"audio\"",
"new(\"mesh draw dispatcher\"",
"new(\"environment cells\"",
"new(\"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;
}
}
}