The three world renderers' submission arms, both pass executors, and the
composition that reaches them. This is the unit three predecessors stopped at.
What it produces. ACDREAM_RENDER_BACKEND=vulkan on the offline scene renders
terrain with blended textures and road overlays, the water edge, static world
meshes, procedural scenery, and the complete retained UI - the same frame the GL
pixel gate captures, from the same camera, minus the sky. artifacts/v6j-vk2.
The shape, and why it is not V4c's. Section 5.5.6 chose option (B) after NVIDIA
rendered the V4c binary 10/10 where AMD's GL stack did not: GL keeps its raw
world path through to V10 as a documented fork confined to the submission seam,
and the RHI world path ships on Vulkan. So V4c's and V4d-2's content returns as a
SECOND arm rather than a replacement. The GL arm issues the same GL statements in
the same order against the same objects; the encoder arm lives in three .Rhi.cs
partials and is entered by one branch per submission site.
Three differences from V4c, each because the tree moved under it. There is no
binding-9 texture table - V4t put the slot on the device and Vulkan binds set 2,
so the arm that used to intern bindless handles simply has nothing to do. The
pipelines carry the device's sample count rather than 1, because Vulkan requires
rasterizationSamples to match the pass and alpha-to-coverage is a no-op at one
sample. And no renderer opens a pass.
That last one is structural, not tidiness. Under MSAA the frame's one backbuffer
pass resolves into the swapchain image and stores DONT_CARE into the multisampled
scratch, so a second pass declaring Load would load undefined contents; the
backend also permits one open pass per frame. VulkanWorldScenePhase therefore
opens the pass, publishes the encoder on VulkanWorldPassScope for exactly the
span of the inner WorldSceneRenderer, and every renderer borrows it.
Three sections are frame-global on GL and cannot be on Vulkan: the SceneLighting
UBO, the per-cell clip regions, and the terrain clip block. GL binds each to a
global binding point and every consumer inherits it. Vulkan binds a descriptor
set per draw, and a renderer's own binds are what select the scope those sections
must land in - so their writers PUBLISH into WorldFrameSections and each renderer
binds them inside the pass, after its own binds. SceneLightingUboBinding's
per-flight-slot buffer pool disappears with it: a ring allocation is already
distinct memory that lives until the frame retires, which is the property the
pool existed to provide.
Both pass executors became backend-neutral rather than gaining twins. Everything
they do is delegation to a renderer except four concerns - the clip-frame
publication, the doorway scissor, gl_ClipDistance enablement, and retail's
interior depth clear - so those four move behind IWorldPassSurface and retail's
ordering, which is what these classes are actually for, is written once. The GL
implementation issues the statements the executors used to issue inline.
Clip distances are no-ops on the Vulkan arm, and that is safe rather than a
divergence: Vulkan activates every element the shader declares, and all three
world vertex shaders already write 1.0 into every slot past the active count.
The interior depth clear becomes vkCmdClearAttachments, reached through the scope
so the pinned contract stays frozen and the backend-only verb stays in the
backend. The hook for it was already committed at V6i-3 with a cref to a type
that did not exist yet; it exists now.
The collision-wireframe DebugLineRenderer is composed as null on the Vulkan arm.
DrawAndPublish flushes it INSIDE the world phase and it opens its own pass, which
the one-pass rule forbids. The toggle is DevTools-only and DevTools is not
composed there, so nothing is lost - composing it would throw on the first
wireframe frame rather than silently misdraw.
Two seams widened rather than invented. GameWindowGraphics answers whether the
backend has a world-pass seam, because the three composition phases that need it
already borrow that handle and "does this backend work that way" is what the type
exists to answer. And MeshSourceReady replaces the anyVao != 0 gate with the same
question in backend-neutral form - V6i-3 published HasStores for exactly this -
so the predicate evaluates identically on GL.
What is NOT here, and is expected. Sky and weather are still raw GL (V4f), so the
Vulkan frame's sky is the atmosphere fog clear. Particles (V4e), the paperdoll and
appraisal viewports and the portal depth mask (V4g) likewise. The executors
already accepted all of them as absent.
Gates. Release build green. App tests 4,112 passed / 3 skipped, the unchanged
baseline; complete Release suite 9,175 / 5. Strict GL offline pixel gate against
847f14ae: 5.50e-05, 31 differing pixels of 563,200, inside the documented 9-31
band and 18x under the threshold. Characterised rather than accepted, because 31
is the band's top: cross-commit pairs measured 21, 29 and 31 while same-commit
controls measured 12 and 20, and maximumChannelDelta is 46-52 in every comparison
INCLUDING the pure controls - so the few large-delta pixels are a property of the
capture, and a cross-commit pair at 21 against a same-commit pair at 20 is not
what a systematic shift looks like. GL connected repeat gate at 3 runs: 3/3
RENDERED on the desktop witness and 3/3 on the client capture. One offline Vulkan
run with VK_LAYER_KHRONOS_validation proven inserted by the loader: zero
validation errors, zero warnings, a captured world frame, and a graceful close.
Coverage gap, stated rather than assumed. The offline scene is a fixed outdoor
view, so EnvCellRenderer's Vulkan arm draws nothing in it - dungeon interiors are
half of this slice and are unproven by anything automated, exactly as they were
for V4c. The deferred-alpha path and the doorway scissor are likewise untouched
by this scene. They join the accumulated user-gate debt in plan section 5.1.
No divergence-register row: no retail-facing behaviour changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
713 lines
31 KiB
C#
713 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(",
|
|
// Campaign V slice V6j: terrain is composed on both arms, so its
|
|
// acquisition is unconditional. The boundary this test pins — that
|
|
// the atlas is acquired, then the factory names the renderer, then
|
|
// the renderer is acquired AND published in one step — is unchanged.
|
|
"TerrainModernRenderer? terrain = AcquireAndPublish(");
|
|
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.");
|
|
}
|
|
}
|