acdream/tests/AcDream.App.Tests/Rendering/GameWindowSlice8BoundaryTests.cs
Erik b16f820643 feat(render): Campaign V slice V6h — the Vulkan composition host
ACDREAM_RENDER_BACKEND=vulkan now runs the real GameWindow composition rather
than a second main(). All nine phases execute: DAT load, streaming, camera,
entity table, session, and the real retained UiHost drawing through the RHI.
No world renderers — they are raw GL until V4t and the world arm behind it.

The offline log is the client's own (acdream.pak opened, 6266 spells, Region
0x13000000, "loading world view centered on 0xA9B4FFFF", fourteen retail
LayoutDesc lines, streaming radii), and the captured frame is the retail
retained UI: vitals, combat/spell bar with DAT scarab icons, the nine-slot
toolbar, chat with tabs and Send, radar/compass with dat-font glyphs. Sampled
against the GL capture the widgets agree — chat interior RGBA (25,24,27,158)
vs (22,21,23,158), vitals bar (117,1,0) and toolbar slot (0,11,17) identical.

Three seams, as §5.5.9 specified:

1. Platform acquisition — already generic — publishes GameWindowGraphics
   instead of a bare GL. Phases that still speak raw GL read Graphics.Gl and
   take their Vulkan arm when it is null; each branch names the slice that
   removes it.
2. VulkanHostInputCameraCompositionFactory is a new file and the whole of the
   Phase-1 fork: four graphics members differ, input/camera/pointer delegate.
   The default factory is chosen inside the phase from the platform result.
   HostInputCameraResult gained backend-neutral Retirement and FrameSlots.
3. The frame root forks on one condition. The GL world-scene assembly is
   unchanged, wrapped in `if (gl is not null)`; the Vulkan arm's graph is one
   backbuffer clear pass computing the same RenderFrameFoundation from the same
   clock and weather owners, then private presentation over it.

§5.5.9's three TextureCache couplings are unpicked: the constructor takes GL?
and rejects bindless without one, world entry points route through a Gl
property that throws naming V4t, and the (GlGpuTexture) VRAM-accounting cast
became a backend test. That cast's stated reason — DrawSprite's texture-unit
binding — was already stale, deleted at V6d.

VulkanBringUpHost is reduced to the capability-probe harness it is named for:
the instance/surface/device/swapchain sequence moved into VulkanGraphicsContext,
which the composition host and the harness now share. It is reached only with
ACDREAM_VULKAN_PROBE=1.

One latent Vulkan defect surfaced and is fixed here. The first composition-host
frame died with ErrorDeviceLost; validation named VUID-vkCmdDraw-None-08600 —
descriptor set 2 never bound. VulkanGpuPassEncoder bound sets 0/1/2 only as a
side effect of BindStorageBuffer/BindUniformBuffer, so a pass sampling the
texture table while binding no buffer — every retained-UI and debug-line pass —
drew with the table unbound. It survived V6c-V6g because the bring-up host
always drew VulkanRhiScene first and the UI pass inherited its binds; the
composition host has no 3-D scene. The fix is one line in the encoder's
constructor beside the viewport and scissor defaults, which exist for exactly
the same reason: a pass opens with complete binding state rather than depending
on what preceded it.

Gates: strict GL offline pixel gate against 46d893f7 measures 1.24e-05 (7 of
563,200 pixels), inside the documented 15-23 px / 4.1e-05 band, so GL behaviour
did not move. App tests 4,075/3 skips; complete Release suite 9,138/5 skips.
One full Vulkan run with VK_LAYER_KHRONOS_validation: zero errors, zero
warnings. Both Vulkan runs converged the ownership ledger — no [shutdown]
diagnostic on either stream. The reduced probe harness presented 34,811
validation-clean frames.

No divergence-register row: GL is the shipping backend and the pixel gate proves
it unmoved; the Vulkan arm is not a retail deviation but a backend under
construction.

Next is V4t, the texture stack, which the world arm cannot be written without.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 11:47:37 +02:00

709 lines
31 KiB
C#

namespace AcDream.App.Tests.Rendering;
/// <summary>
/// Temporary source-shape freeze for the Slice 8 cutover. Each later checkpoint
/// replaces the corresponding assertion with functional tests on the extracted
/// owner; this file prevents a mechanical move from silently reordering host
/// behavior before that owner exists.
/// </summary>
public sealed class GameWindowSlice8BoundaryTests
{
[Fact]
public void Run_PreservesNativeAttributeAndCallbackOrder()
{
string body = MethodBody("public void Run()", "private void OnLoad()");
AssertAppearsInOrder(
body,
"RuntimeSettingsSnapshot startup = _runtimeSettings.Startup",
"_displayFramePacing.InitializeStartup(startup.Display.VSync)",
"VSync = startupPacing.UseVSync",
"Samples = startup.Quality.MsaaSamples",
"Window.Create(options)",
"_displayFramePacing.BindSurface(",
"_windowCallbacks = SilkWindowCallbackBinding.Create(",
"new WindowCallbackTargets(",
"OnLoad,",
"OnUpdate,",
"OnRender,",
"OnClosing,",
"OnFocusChanged,",
"OnFramebufferResize),",
"_displayFramePacing,",
"_hostQuiescence);",
"_windowCallbacks.Attach();",
"_window.Run();");
Assert.Equal(1, CountOccurrences(body, "SilkWindowCallbackBinding.Create("));
Assert.Equal(1, CountOccurrences(body, "_windowCallbacks.Attach();"));
Assert.DoesNotContain("_window.Load +=", body, StringComparison.Ordinal);
Assert.DoesNotContain("_window.Update +=", body, StringComparison.Ordinal);
Assert.DoesNotContain("_window.Render +=", body, StringComparison.Ordinal);
Assert.DoesNotContain("_window.Closing +=", body, StringComparison.Ordinal);
}
[Fact]
public void SessionStart_FollowsFrameGraphPublication()
{
string body = MethodBody(
"private void OnLoad()",
"private void OnUpdate(double dt)");
AssertAppearsInOrder(
body,
"GameWindowPlatformResult<GameWindowGraphics, IInputContext> platform = AcquirePlatform();",
"GameWindowCompositionPipeline.Run<",
"new HostInputCameraCompositionPhase(",
"this).Compose(platformResult),",
"new ContentEffectsAudioCompositionPhase(",
"this).Compose(platformResult, hostInputCamera),",
"new SettingsDevToolsCompositionPhase(",
"this).Compose(platformResult, hostInputCamera, contentEffectsAudio);",
"new InteractionRetainedUiCompositionPhase(",
"this).Compose(",
"new LivePresentationCompositionPhase(",
"this).Compose(",
"new SessionPlayerCompositionPhase(",
"new FrameRootCompositionPhase(",
"new SessionStartCompositionPhase(",
".Start(frameRoots));");
string framePhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
AssertAppearsInOrder(
framePhase,
"new RenderFrameOrchestrator(",
"new UpdateFrameOrchestrator(",
"d.FrameGraphs.PublishOwned(",
"_publication.PublishFrameRoots(result);");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
AssertAppearsInOrder(
sessionPhase,
"LiveSessionHost sessionHost = sessionRuntimeFactory.Create(",
"d.CombatModeCommands.BindOwned(combatCommand)",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)",
"GameplayInputActionRouter.Create(",
"gameplayActions.Attach();",
"_publication.PublishSessionPlayer(result);");
Assert.Equal(1, CountOccurrences(
body,
".Start(frameRoots));"));
string phaseOne = Slice(
File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"HostInputCameraComposition.cs")),
"private HostInputCameraResult ComposeCore(",
"private void Fault(");
Assert.Contains("if (firstKeyboard is not null)", phaseOne, StringComparison.Ordinal);
Assert.Contains("if (firstMouse is not null)", phaseOne, StringComparison.Ordinal);
Assert.Contains(
"if (keyboard is not null && mouse is not null)",
phaseOne,
StringComparison.Ordinal);
Assert.DoesNotContain(
"if (firstKeyboard is not null && firstMouse is not null)",
phaseOne,
StringComparison.Ordinal);
string startPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionStartComposition.cs"));
AssertAppearsInOrder(
startPhase,
"frame.GameRuntime.Session.Start(frame.GameRuntime.Generation)",
"switch (result.Status)",
"case RuntimeSessionStartStatus.MissingCredentials:",
"case RuntimeSessionStartStatus.Failed:");
int start = body.IndexOf(
".Start(frameRoots));",
StringComparison.Ordinal);
string postStart = body[(start + ".Start(frameRoots));".Length)..];
Assert.Empty(System.Text.RegularExpressions.Regex.Matches(
postStart,
@"(?m)^\s+_[A-Za-z]\w*\s*(?:\?\?=|=)"));
Assert.DoesNotContain("new ", postStart, StringComparison.Ordinal);
Assert.DoesNotContain(".Mount(", postStart, StringComparison.Ordinal);
Assert.DoesNotContain(".Bind(", postStart, StringComparison.Ordinal);
Assert.DoesNotContain(".Attach(", postStart, StringComparison.Ordinal);
Assert.DoesNotContain(".Compose(", postStart, StringComparison.Ordinal);
Assert.DoesNotContain("PrepareResources(", postStart, StringComparison.Ordinal);
}
[Fact]
public void WorldEnvironment_IsOwnedAndGameWindowOnlyComposesItsTypedEdges()
{
string source = GameWindowSource();
string load = MethodBody(
"private void OnLoad()",
"private void OnUpdate(double dt)");
string sessionFactorySource = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Net",
"LiveSessionRuntimeFactory.cs"));
string sessionFactory = Slice(
sessionFactorySource,
"private LiveSessionEventRouter CreateEventRouter(",
"private LiveInventorySessionBindings CreateInventoryBindings()");
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"WorldRenderComposition.cs"));
Assert.Contains(
"private readonly AcDream.App.World.WorldEnvironmentController _worldEnvironment;",
source,
StringComparison.Ordinal);
Assert.Contains(
"new WorldRenderDependencies(\n _worldEnvironment,",
load.Replace("\r\n", "\n", StringComparison.Ordinal),
StringComparison.Ordinal);
string worldCompose = Slice(
worldPhase,
"public WorldRenderResult Compose(",
"private (BitmapFont? Font, TextRenderer? Text) ComposeOptionalHudResources(");
AssertAppearsInOrder(
worldCompose,
"WorldRegionData region = _factory.LoadRegion(content.Dats);",
"_factory.InitializeEnvironment(_dependencies.Environment, region.Region);");
Assert.Contains("environment.Initialize(region);", worldPhase, StringComparison.Ordinal);
AssertAppearsInOrder(
sessionFactory,
"new LiveEnvironmentSessionSink(",
"_world.Environment.ApplyAdminEnvirons,",
"_world.Environment.SynchronizeFromServer)");
Assert.DoesNotContain("_loadedSkyDesc", source, StringComparison.Ordinal);
Assert.DoesNotContain("_loadedSkyDayIndex", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void RefreshSkyForCurrentDay()", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void OnEnvironChanged(", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void CycleTimeOfDay()", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void CycleWeather()", source, StringComparison.Ordinal);
string settingsPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SettingsDevToolsComposition.cs"));
AssertAppearsInOrder(
settingsPhase,
"debugVm.CycleTimeOfDay = _dependencies.DiagnosticCommands.CycleTimeOfDay;",
"debugVm.CycleWeather = _dependencies.DiagnosticCommands.CycleWeather;",
"debugVm.ToggleCollisionWires =");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
AssertAppearsInOrder(
sessionPhase,
"new RuntimeDiagnosticCommandController(",
"d.RuntimeDiagnosticCommands.BindOwned(runtimeDiagnostics)");
}
[Fact]
public void InputAction_IsOneTypedOwnerHandoff()
{
string source = GameWindowSource();
string load = MethodBody(
"private void OnLoad()",
"private void OnUpdate(double dt)");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
string shutdown = GameWindowLifetimeSource();
Assert.DoesNotContain("private void OnInputAction(", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void SetInputCombatScope(", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void ToggleLiveCombatMode()", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void OnUiDragReleasedOutside(", source, StringComparison.Ordinal);
Assert.DoesNotContain("DragReleasedOutsideUi +=", source, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(
sessionPhase,
"GameplayInputActionRouter.Create("));
Assert.Equal(1, CountOccurrences(
sessionPhase,
"gameplayActions.Attach();"));
Assert.Equal(1, CountOccurrences(
LivePresentationSource(),
"RetainedUiGameplayBinding.Create("));
Assert.Equal(1, CountOccurrences(
LivePresentationSource(),
"retainedGameplayLease.Resource.Attach();"));
AssertAppearsInOrder(
shutdown,
"Hard(\"combat command slot\", ingress.CombatCommands.Deactivate)",
"Hard(\"diagnostic command slot\", ingress.DiagnosticCommands.Deactivate)",
"Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())",
"Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())",
"Hard(\"game runtime session\", ingress.Runtime.StopSession)",
"new ResourceShutdownStage(\"physical ingress cleanup\"",
"Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))",
"Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))",
"Soft(\"camera pointer\", () => DisposeCameraPointer(ingress.CameraPointer))",
"new ResourceShutdownStage(\"session dependents\"",
"Hard(\"mouse capture\", () => live.CameraPointer?.ReleaseMouseLookAfterSessionRetirement())");
}
[Fact]
public void FramebufferResize_IsOneTypedOwnerHandoff()
{
string load = MethodBody(
"private void OnLoad()",
"private void OnUpdate(double dt)");
string body = MethodBody(
"private void OnFramebufferResize(",
"private void OnClosing()");
Assert.Contains("=> _framebufferResize.Resize(newSize);", body, StringComparison.Ordinal);
Assert.DoesNotContain("Viewport(", body, StringComparison.Ordinal);
Assert.DoesNotContain("SetAspect(", body, StringComparison.Ordinal);
Assert.DoesNotContain("ResetLayout(", body, StringComparison.Ordinal);
string phaseOne = Slice(
File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"HostInputCameraComposition.cs")),
"private HostInputCameraResult ComposeCore(",
"private void Fault(");
AssertAppearsInOrder(
phaseOne,
"_dependencies.FramebufferResize.BindViewport(",
"_publication.PublishCameraController(camera)",
"_dependencies.FramebufferResize.BindCamera(",
"_dependencies.FramebufferResize.Resize(");
Assert.DoesNotContain(
"_viewportAspect.Update(_window",
load,
StringComparison.Ordinal);
}
[Fact]
public void RuntimeSettings_IsOneTwoPhaseOwnerWithoutWindowStateMirrors()
{
string source = GameWindowSource();
string run = MethodBody("public void Run()", "private void OnLoad()");
string load = MethodBody(
"private void OnLoad()",
"private void OnUpdate(double dt)");
string shutdown = GameWindowLifetimeSource();
Assert.Contains(
"private readonly RuntimeSettingsController _runtimeSettings;",
source,
StringComparison.Ordinal);
Assert.Contains(
"_runtimeSettings = new RuntimeSettingsController(",
source,
StringComparison.Ordinal);
Assert.DoesNotContain("new SettingsStore(", source, StringComparison.Ordinal);
Assert.DoesNotContain("new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(", source, StringComparison.Ordinal);
Assert.DoesNotContain(".LoadDisplay()", source, StringComparison.Ordinal);
Assert.DoesNotContain(".LoadAudio()", source, StringComparison.Ordinal);
Assert.DoesNotContain(".LoadGameplay()", source, StringComparison.Ordinal);
Assert.DoesNotContain(".LoadChat()", source, StringComparison.Ordinal);
Assert.DoesNotContain(".LoadCharacter(", source, StringComparison.Ordinal);
Assert.DoesNotContain("_persistedDisplay", source, StringComparison.Ordinal);
Assert.DoesNotContain("_persistedAudio", source, StringComparison.Ordinal);
Assert.DoesNotContain("_persistedGameplay", source, StringComparison.Ordinal);
Assert.DoesNotContain("_persistedChat", source, StringComparison.Ordinal);
Assert.DoesNotContain("_persistedCharacter", source, StringComparison.Ordinal);
Assert.DoesNotContain("_activeToonKey", source, StringComparison.Ordinal);
Assert.DoesNotContain("_settingsStore", source, StringComparison.Ordinal);
Assert.DoesNotContain("_settingsVm", source, StringComparison.Ordinal);
Assert.DoesNotContain("LoadAndApplyPersistedSettings", source, StringComparison.Ordinal);
Assert.DoesNotContain("private void ApplyDisplayWindowState", source, StringComparison.Ordinal);
Assert.DoesNotContain("public void ReapplyQualityPreset", source, StringComparison.Ordinal);
AssertAppearsInOrder(
run,
"RuntimeSettingsSnapshot startup = _runtimeSettings.Startup",
"_displayFramePacing.InitializeStartup(startup.Display.VSync)",
"Samples = startup.Quality.MsaaSamples",
"Window.Create(options)");
AssertAppearsInOrder(
load,
"GameWindowCompositionPipeline.Run<",
"new SettingsDevToolsCompositionPhase(",
"this).Compose(platformResult, hostInputCamera, contentEffectsAudio);",
"new WorldRenderCompositionPhase(",
"new SessionPlayerCompositionPhase(",
"new FrameRootCompositionPhase(",
"new SessionStartCompositionPhase(",
".Start(frameRoots));");
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
Assert.Contains(
"d.Settings.BindRuntimeTargetsOwned(settingsTargets)",
sessionPhase,
StringComparison.Ordinal);
string settingsPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SettingsDevToolsComposition.cs"));
AssertAppearsInOrder(
settingsPhase,
"_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);",
"_dependencies.DevTools is { } optional");
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"WorldRenderComposition.cs"));
AssertAppearsInOrder(
worldPhase,
"TerrainAtlas.Build(gl, dats, bindless)",
"settings.ResolvedQuality.AnisotropicLevel",
"_factory.CreateTerrain(");
AssertAppearsInOrder(
shutdown,
"Soft(\"settings view model\", () => ingress.Settings.UnbindViewModel())",
"new ResourceShutdownStage(\"frame borrowers\"",
"Hard(\"frame-root bindings\", () => frame.FrameBindings?.Dispose())",
"Hard(\"retail UI\", () => DisposeRetailUi(live.RetailUi))",
"Hard(\"streamer\", () => live.Streamer?.Dispose())",
"Hard(\"mesh draw dispatcher\", () => render.DrawDispatcher?.Dispose())",
"Hard(\"terrain\", () => render.Terrain?.Dispose())");
}
[Fact]
public void UpdateRenderFocusAndCloseRemainNarrowHostEdges()
{
string source = GameWindowSource();
string update = Slice(
source,
"private void OnUpdate(double dt)",
"private void OnRender(double deltaSeconds)");
string render = Slice(
source,
"private void OnRender(double deltaSeconds)",
"private void OnFramebufferResize(");
string focus = Slice(
source,
"private void OnFocusChanged(bool focused)",
"public void Dispose()");
string close = Slice(
source,
"private void OnClosing()",
"private void OnFocusChanged(bool focused)");
Assert.Equal(1, CountOccurrences(update, "_frameGraphs.Tick("));
Assert.Equal(1, CountOccurrences(render, "_frameGraphs.Render("));
AssertAppearsInOrder(
render,
"Vector2D<int> size = _window!.Size;",
"_frameGraphs.Render(",
"new AcDream.App.Rendering.RenderFrameInput(");
Assert.DoesNotContain("FramebufferSize", render, StringComparison.Ordinal);
Assert.Contains(
"=> _cameraPointerInput?.HandleFocusChanged(focused);",
focus,
StringComparison.Ordinal);
Assert.Contains(
"private void OnClosing() => CompleteShutdown(releaseNativeWindow: false);",
close,
StringComparison.Ordinal);
AssertAppearsInOrder(
close,
"private void CompleteShutdown(bool releaseNativeWindow)",
"if (!_lifetime.HasShutdownRoots)",
"_lifetime.PublishShutdownRoots(CaptureShutdownRoots());",
"? _lifetime.CompleteAndReleaseNativeWindow()",
": _lifetime.TryComplete();");
Assert.DoesNotContain("new ResourceShutdownStage(", close, StringComparison.Ordinal);
Assert.DoesNotContain("CompleteOrThrow", close, StringComparison.Ordinal);
}
[Fact]
public void Shutdown_PreservesDependencyStagesAndNativeWindowLast()
{
string source = GameWindowSource();
string lifetime = GameWindowLifetimeSource();
string manifest = Slice(
lifetime,
"internal static class GameWindowShutdownManifest",
"private static ResourceShutdownOperation Hard");
string nativeRelease = Slice(
lifetime,
"public GameWindowLifetimeReport CompleteAndReleaseNativeWindow()",
"private void EnsureTransaction()");
int disposeStart = source.IndexOf("public void Dispose()", StringComparison.Ordinal);
Assert.True(disposeStart >= 0);
string dispose = source[disposeStart..];
string[] stages =
[
"new ResourceShutdownStage(\"host and session barriers\"",
"new ResourceShutdownStage(\"physical ingress cleanup\"",
"new ResourceShutdownStage(\"frame borrowers\"",
"new ResourceShutdownStage(\"session dependents\"",
"new ResourceShutdownStage(\"live entities\"",
"new ResourceShutdownStage(\"effect dispatch edges\"",
"new ResourceShutdownStage(\"live entity dependents\"",
"new ResourceShutdownStage(\"submitted GPU work\"",
"new ResourceShutdownStage(\"render frontends\"",
"new ResourceShutdownStage(\"game runtime root\"",
"new ResourceShutdownStage(\"shared texture owners\"",
"new ResourceShutdownStage(\"mesh adapter\"",
"new ResourceShutdownStage(\"remaining render owners\"",
"new ResourceShutdownStage(\"dedicated render resources\"",
"new ResourceShutdownStage(\"failed render construction cleanup\"",
"new ResourceShutdownStage(\"frame flight owner\"",
"new ResourceShutdownStage(\"content mappings\"",
"new ResourceShutdownStage(\"input context\"",
"new ResourceShutdownStage(\"OpenGL context\"",
];
AssertAppearsInOrder(manifest, stages);
Assert.Equal(stages.Length, CountOccurrences(manifest, "new ResourceShutdownStage("));
foreach (string stage in stages)
Assert.Equal(1, CountOccurrences(manifest, stage));
AssertAppearsInOrder(
manifest,
"Hard(\"combat command slot\", ingress.CombatCommands.Deactivate)",
"Hard(\"diagnostic command slot\", ingress.DiagnosticCommands.Deactivate)",
"Hard(\"retained gameplay\", () => ingress.RetainedGameplay?.Deactivate())",
"Hard(\"gameplay actions\", () => ingress.GameplayActions?.Deactivate())",
"Hard(\"camera pointer\", () => ingress.CameraPointer?.Deactivate())",
"Hard(\"game runtime session\", ingress.Runtime.StopSession)",
"new ResourceShutdownStage(\"physical ingress cleanup\"",
"Soft(\"retained gameplay\", () => DisposeRetainedGameplay(ingress.RetainedGameplay))",
"Soft(\"gameplay actions\", () => DisposeGameplayActions(ingress.GameplayActions))",
"Soft(\"camera pointer\", () => DisposeCameraPointer(ingress.CameraPointer))",
"Soft(\"native window callbacks\", () => DisposeWindowCallbacks(ingress.WindowCallbacks))",
"new ResourceShutdownStage(\"session dependents\"",
"Hard(\"mouse capture\", () => live.CameraPointer?.ReleaseMouseLookAfterSessionRetirement())");
Assert.Contains(
"new(name, action, ResourceShutdownOperationPolicy.ReportAndContinue)",
lifetime,
StringComparison.Ordinal);
Assert.Contains(
"UiHost? RetainedUiHost,",
lifetime,
StringComparison.Ordinal);
Assert.Contains("_uiHost,", source, StringComparison.Ordinal);
AssertAppearsInOrder(
nativeRelease,
"TryComplete();",
"ReleaseNativeWindow();");
AssertAppearsInOrder(
dispose,
"CompleteShutdown(releaseNativeWindow: true);",
"_window = null;");
Assert.Equal(1, CountOccurrences(dispose, "CompleteShutdown(releaseNativeWindow: true);"));
Assert.DoesNotContain("_window?.Dispose();", dispose, StringComparison.Ordinal);
Assert.Equal(1, CountOccurrences(dispose, "_window = null;"));
int nativeReleased = dispose.IndexOf("_window = null;", StringComparison.Ordinal)
+ "_window = null;".Length;
string afterNativeRelease = dispose[nativeReleased..];
int fallbackDocumentation = afterNativeRelease.IndexOf(
"/// <summary>",
StringComparison.Ordinal);
if (fallbackDocumentation >= 0)
afterNativeRelease = afterNativeRelease[..fallbackDocumentation];
Assert.All(
afterNativeRelease,
character => Assert.True(char.IsWhiteSpace(character) || character == '}'));
}
[Fact]
public void ResourceRootsAndFramePairHaveExplicitAcquireTransferAndReleaseBoundaries()
{
string source = GameWindowSource();
string load = MethodBody(
"private void OnLoad()",
"private void OnUpdate(double dt)");
string shutdown = GameWindowLifetimeSource();
string terrainAtlas = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"TerrainAtlas.cs"));
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"WorldRenderComposition.cs"));
string livePhase = LivePresentationSource();
string sessionPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SessionPlayerComposition.cs"));
string framePhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"FrameRootComposition.cs"));
AssertAppearsInOrder(
livePhase,
"d.PortalTunnelFallback.AcquirePrepared(",
"static tunnel => tunnel.PrepareResources());",
"d.RenderResourceLifetime.AcquireSkyShader(",
"new SkyRenderer(");
AssertAppearsInOrder(
load,
"new WorldRenderCompositionPhase(",
"new LivePresentationCompositionPhase(",
"new SessionPlayerCompositionPhase(",
"new FrameRootCompositionPhase(");
AssertAppearsInOrder(
framePhase,
"new RenderFrameOrchestrator(",
"new UpdateFrameOrchestrator(",
"d.FrameGraphs.PublishOwned(");
AssertAppearsInOrder(
sessionPhase,
"d.PortalTunnelFallback.Transfer(",
"new LocalPlayerTeleportController(",
"_publication.PublishSessionPlayer(result);");
AssertAppearsInOrder(
worldPhase,
"lifetime.AcquireTerrainAtlas(",
"TerrainAtlas.Build(gl, dats, bindless)",
"TerrainModernRenderer CreateTerrain(",
"TerrainModernRenderer? terrain = AcquireAndPublishIf(");
AssertAppearsInOrder(
shutdown,
"frame.FrameGraphPublication?.Dispose()",
"frame.FrameBindings?.Dispose()",
"DisposeRetailUi(live.RetailUi)",
"render.LocalTeleport?.Dispose();",
"render.PortalTunnelFallback.ReleaseFallback();",
"render.Sky?.Dispose()",
"render.Terrain?.Dispose()",
"render.DedicatedResources.ReleaseSkyShader",
"render.DedicatedResources.ReleaseTerrainAtlas",
"render.ConstructionCleanup.Dispose",
"platform.Graphics?.Dispose()");
Assert.DoesNotContain(
"RetailUiRuntime.Mount(",
load,
StringComparison.Ordinal);
Assert.DoesNotContain("gl.GenTexture()", terrainAtlas, StringComparison.Ordinal);
Assert.DoesNotContain(
"portalTunnel.PrepareResources();",
load,
StringComparison.Ordinal);
Assert.DoesNotContain("_portalTunnel =", source, StringComparison.Ordinal);
Assert.DoesNotContain("_renderFrameOrchestrator", source, StringComparison.Ordinal);
Assert.DoesNotContain("_updateFrameOrchestrator", source, StringComparison.Ordinal);
AssertAppearsInOrder(
source,
"_window.Run();",
"_glConstructionCleanup.RetainFrom(failure);",
"private GameWindowShutdownRoots CaptureShutdownRoots()",
"_glConstructionCleanup)");
Assert.Contains(
"Hard(\"GL construction ledger\", render.ConstructionCleanup.Dispose)",
shutdown,
StringComparison.Ordinal);
}
private static string MethodBody(string start, string end) =>
Slice(GameWindowSource(), start, end);
private static string Slice(string source, string start, string end)
{
int first = source.IndexOf(start, StringComparison.Ordinal);
int last = source.IndexOf(end, first + 1, StringComparison.Ordinal);
Assert.True(first >= 0, $"Missing source boundary: {start}");
Assert.True(last > first, $"Missing source boundary: {end}");
return source[first..last];
}
private static int CountOccurrences(string source, string value)
{
int count = 0;
int cursor = 0;
while ((cursor = source.IndexOf(value, cursor, StringComparison.Ordinal)) >= 0)
{
count++;
cursor += value.Length;
}
return count;
}
private static void AssertAppearsInOrder(string source, params string[] fragments)
{
int cursor = -1;
foreach (string fragment in fragments)
{
int next = source.IndexOf(fragment, cursor + 1, StringComparison.Ordinal);
Assert.True(next >= 0, $"Missing expected source fragment: {fragment}");
Assert.True(next > cursor, $"Out-of-order source fragment: {fragment}");
cursor = next;
}
}
private static string GameWindowSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindow.cs"));
private static string GameWindowLifetimeSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Rendering",
"GameWindowLifetime.cs"));
private static string LivePresentationSource() => File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"LivePresentationComposition.cs"));
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.");
}
}