using System.Numerics; using System.Reflection; using AcDream.App.Composition; using AcDream.App.Input; using AcDream.App.Rendering; using AcDream.App.Rendering.Wb; using AcDream.App.Streaming; using AcDream.App.Tests.Architecture; 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 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 { 41u, 42u }; var buildings = new List(); 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 { 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(() => new WorldRenderFrameBuilder(null!, visibility, settings, roots, environment, animated, buildings)); Assert.Throws(() => new WorldRenderFrameBuilder(camera, null!, settings, roots, environment, animated, buildings)); Assert.Throws(() => new WorldRenderFrameBuilder(camera, visibility, null!, roots, environment, animated, buildings)); Assert.Throws(() => new WorldRenderFrameBuilder(camera, visibility, settings, null!, environment, animated, buildings)); Assert.Throws(() => new WorldRenderFrameBuilder(camera, visibility, settings, roots, null!, animated, buildings)); Assert.Throws(() => new WorldRenderFrameBuilder(camera, visibility, settings, roots, environment, null!, buildings)); Assert.Throws(() => 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(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(slot); var source = new RuntimeWorldFrameAnimatedEntitySource( live, statics: null, equipped: null); HashSet first = source.Capture(); first.Add(0xDEADBEEFu); HashSet 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>(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 { 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 Environment_preparation_keeps_lighting_snapshot_before_ubo_upload() { MethodInfo prepare = typeof(RuntimeWorldFrameEnvironmentPreparation).GetMethod( nameof(RuntimeWorldFrameEnvironmentPreparation.Prepare))!; IReadOnlyList calls = CompiledCallGraph.Read(prepare); Type[] ownerOrder = [ typeof(RuntimeWorldFrameEnvironmentPreparation), typeof(LightManager), typeof(LightManager), typeof(LightManager), typeof(WbDrawDispatcher), typeof(EnvCellRenderer), typeof(SceneLightingUbo), typeof(SceneLightingUboBinding), typeof(RenderingDiagnostics), ]; string[] methodOrder = [ "UpdateSunFromSky", nameof(LightManager.UpdateViewerLight), nameof(LightManager.Tick), nameof(LightManager.BuildPointLightSnapshot), nameof(WbDrawDispatcher.SetSceneLights), nameof(EnvCellRenderer.SetPointSnapshot), nameof(SceneLightingUbo.Build), nameof(SceneLightingUboBinding.Upload), nameof(RenderingDiagnostics.EmitLight), ]; AssertCompiledCallOrder(calls, ownerOrder, methodOrder); } [Fact] public void World_scene_uses_the_typed_builder_and_orchestrator_owns_its_local_composition() { MethodInfo[] windowMethods = typeof(GameWindow).GetMethods( BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); Assert.DoesNotContain( windowMethods, method => method.Name is "UpdateSunFromSky" or "UpdateSkyPes" or "ParseEnvFloat"); FieldInfo[] windowFields = typeof(GameWindow).GetFields( BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); Assert.DoesNotContain( windowFields, field => field.FieldType == typeof(WorldRenderFrameBuilder) || field.FieldType == typeof(SkyPesFrameController)); // Gate-fix round (2026-08-17): the renderer's own IsWaitingForLogin // short-circuit is deleted — the frame gate // (LocalPlayerTeleportRenderStateSource) folds the waiting state // into PortalViewportVisible, so the portal-visible return above // covers every waiting frame (one gate computes, this phase // enforces). MethodInfo render = typeof(WorldSceneRenderer).GetMethod( nameof(WorldSceneRenderer.Render))!; IReadOnlyList renderCalls = CompiledCallGraph.Read(render); AssertCompiledCallOrder( renderCalls, [ typeof(IWorldSceneAlphaFrame), typeof(IWorldRenderFrameBuilder), typeof(IWorldScenePassExecutor), typeof(WorldRenderFrameOutcome), ], [ nameof(IWorldSceneAlphaFrame.BeginFrame), nameof(IWorldRenderFrameBuilder.Build), nameof(IWorldScenePassExecutor.DrawFlatTerrain), ".ctor", ]); Assert.Single( renderCalls, call => call.Target.DeclaringType == typeof(IWorldRenderFrameBuilder) && call.Target.Name == nameof(IWorldRenderFrameBuilder.Build)); MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod( "ComposeCore", BindingFlags.Instance | BindingFlags.NonPublic)!; IReadOnlyList composeCalls = CompiledCallGraph.Read(compose); AssertCompiledCallOrder( composeCalls, [ typeof(SkyPesFrameController), typeof(WorldRenderFrameBuilder), typeof(RenderFrameOrchestrator), typeof(GameFrameGraphSlot), ], [".ctor", ".ctor", ".ctor", nameof(GameFrameGraphSlot.PublishOwned)]); // The former source assertion also mixed in teardown ordering. That // distinct lifetime contract remains covered by the dedicated lifetime // suite and is reconciled with the later host/lifetime R3 batch. } 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(), Array.Empty(), seenOutside, containmentBsp: null); private static void AssertCompiledCallOrder( IReadOnlyList calls, IReadOnlyList declaringTypes, IReadOnlyList methodNames) { Assert.Equal(declaringTypes.Count, methodNames.Count); int cursor = -1; for (int index = 0; index < declaringTypes.Count; index++) { int next = CompiledCallGraph.IndexOf( calls, declaringTypes[index], methodNames[index], cursor + 1); Assert.True( next > cursor, $"Missing or out-of-order compiled call: {declaringTypes[index].Name}.{methodNames[index]}"); cursor = next; } } private sealed class RecordingCamera( List calls, WorldCameraFrame result) : IWorldFrameCameraSource { public WorldCameraFrame Resolve() { calls.Add("camera"); return result; } } private sealed class RecordingVisibility(List 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 calls) : IWorldFrameSettingsPreview { public void Apply(in WorldCameraFrame camera) => calls.Add("settings"); } private sealed class RecordingRoots( List calls, WorldRootFrame result) : IWorldFrameRootSource { public WorldRootFrame Resolve(in WorldCameraFrame camera) { calls.Add("roots"); return result; } } private sealed class RecordingEnvironment(List calls) : IWorldFrameEnvironmentPreparation { public RenderFrameFoundation Foundation { get; private set; } public IReadOnlySet? 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 drawableCells) => DrawableCells = drawableCells; public void ClearDrawableCells() => ClearCount++; } private sealed class RecordingAnimated( List calls, HashSet result) : IWorldFrameAnimatedEntitySource { public HashSet Capture() { calls.Add("animated"); return result; } } private sealed class RecordingBuildings( List calls, WorldBuildingFrame result) : IWorldFrameBuildingSource { public WorldBuildingFrame Gather( LoadedCell? viewerRoot, uint viewerCellId, in FrustumPlanes frustum) { calls.Add("buildings"); return result; } } }