refactor(input): own pointer and callback lifetime

Move camera pointer, framebuffer resize, and retained/devtools input edges behind focused reversible owners. Preserve input priority while making shutdown deactivate callbacks before live-session retirement and retry physical detach without stranding transport teardown.
This commit is contained in:
Erik 2026-07-22 11:59:33 +02:00
parent d09e246d3a
commit 8b8afeefa3
42 changed files with 4029 additions and 461 deletions

View file

@ -54,14 +54,18 @@ public sealed class GameWindowSlice8BoundaryTests
body,
"_gl = GL.GetApi(_window!);",
"_input = _window!.CreateInput();",
"_kbSource = new AcDream.App.Input.SilkKeyboardSource(firstKb);",
"_mouseSource = new AcDream.App.Input.SilkMouseSource(",
"_inputDispatcher = new AcDream.UI.Abstractions.Input.InputDispatcher(",
"_kbSource = AcDream.App.Input.SilkKeyboardSource.CreateDetached(",
"_kbSource.Attach();",
"_mouseSource = AcDream.App.Input.SilkMouseSource.CreateDetached(",
"_mouseSource.Attach();",
"_inputDispatcher = AcDream.UI.Abstractions.Input.InputDispatcher.CreateDetached(",
"_inputDispatcher.Attach();",
"_movementInput.Bind(_inputDispatcher);",
"_cameraInput.Bind(_inputDispatcher);",
"_inputDispatcher.Fired += OnInputAction;",
"mouse.MouseMove +=",
"_cameraController = new CameraController(orbit, fly);",
"_cameraPointerInput = AcDream.App.Input.CameraPointerInputController.Create(",
"_cameraPointerInput.AttachRaw();",
"_dats = RuntimeDatCollectionFactory.OpenReadOnly(_datDir);",
"LoadAndApplyPersistedSettings();",
"_uiHost = new AcDream.App.UI.UiHost(",
@ -74,6 +78,16 @@ public sealed class GameWindowSlice8BoundaryTests
"_liveSessionHost = CreateLiveSessionHost();",
"_liveSessionHost.Start(_options)");
Assert.Equal(1, CountOccurrences(body, "_liveSessionHost.Start(_options)"));
Assert.Contains("if (firstKb is not null)", body, StringComparison.Ordinal);
Assert.Contains("if (firstMouse is not null)", body, StringComparison.Ordinal);
Assert.Contains(
"if (_kbSource is not null && _mouseSource is not null)",
body,
StringComparison.Ordinal);
Assert.DoesNotContain(
"if (firstKb is not null && firstMouse is not null)",
body,
StringComparison.Ordinal);
int start = body.IndexOf("_liveSessionHost.Start(_options)", StringComparison.Ordinal);
string postStart = body[start..];
Assert.Contains("switch (liveStart.Status)", postStart, StringComparison.Ordinal);
@ -144,20 +158,28 @@ public sealed class GameWindowSlice8BoundaryTests
}
[Fact]
public void FramebufferResize_PreservesViewportCameraAndDevtoolsOrder()
public void FramebufferResize_IsOneTypedOwnerHandoff()
{
string load = MethodBody(
"private void OnLoad()",
"private AcDream.App.Net.LiveSessionHost");
string body = MethodBody(
"private void OnFramebufferResize(",
"private void ApplyDisplayWindowState(");
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);
AssertAppearsInOrder(
body,
"if (newSize.X <= 0 || newSize.Y <= 0) return;",
"_gl?.Viewport(",
"_viewportAspect.Update(",
"_cameraController?.SetAspect(",
"_devToolsFramePresenter?.ResetLayout(");
Assert.DoesNotContain("_uiHost", body, StringComparison.Ordinal);
load,
"_framebufferResize.BindViewport(",
"_framebufferResize.BindCamera(",
"_framebufferResize.Resize(_window!.FramebufferSize);");
Assert.DoesNotContain(
"_viewportAspect.Update(_window",
load,
StringComparison.Ordinal);
}
[Fact]
@ -167,7 +189,7 @@ public sealed class GameWindowSlice8BoundaryTests
string update = Slice(
source,
"private void OnUpdate(double dt)",
"private void OnCameraModeChanged(");
"private void OnRender(double deltaSeconds)");
string render = Slice(
source,
"private void OnRender(double deltaSeconds)",
@ -189,7 +211,10 @@ public sealed class GameWindowSlice8BoundaryTests
"_renderFrameOrchestrator!.Render(",
"new AcDream.App.Rendering.RenderFrameInput(");
Assert.DoesNotContain("FramebufferSize", render, StringComparison.Ordinal);
AssertAppearsInOrder(focus, "if (!focused)", "_gameplayInputFrame?.EndMouseLook();");
Assert.Contains(
"=> _cameraPointerInput?.HandleFocusChanged(focused);",
focus,
StringComparison.Ordinal);
AssertAppearsInOrder(
close,
"_hostQuiescence.StopAccepting();",
@ -212,7 +237,9 @@ public sealed class GameWindowSlice8BoundaryTests
string[] stages =
[
"new ResourceShutdownStage(\"input callback deactivation\"",
"new ResourceShutdownStage(\"session lifetime\"",
"new ResourceShutdownStage(\"input callback detach\"",
"new ResourceShutdownStage(\"frame borrowers\"",
"new ResourceShutdownStage(\"session dependents\"",
"new ResourceShutdownStage(\"live entities\"",
@ -236,6 +263,17 @@ public sealed class GameWindowSlice8BoundaryTests
"binding.Dispose();",
"if (!binding.IsDisposalComplete)",
"_windowCallbacks = null;");
AssertAppearsInOrder(
manifest,
"new(\"camera pointer\", () => _cameraPointerInput?.Deactivate())",
"new ResourceShutdownStage(\"session lifetime\"",
"controller.Dispose();",
"new ResourceShutdownStage(\"input callback detach\"",
"pointer.Dispose();",
"new ResourceShutdownStage(\"session dependents\"",
"pointer.ReleaseMouseLookAfterSessionRetirement();",
"pointer.UnbindGameplayFrame(gameplayFrame);",
"_cameraPointerInput = null;");
AssertAppearsInOrder(
dispose,
"CompleteShutdown();",