feat(render): Campaign V slice V11 commit 1 - delete ImGui, Studio, and the DevTools frontend

The ImGui developer-tools stack (AcDream.UI.ImGui), UI Studio
(src/AcDream.App/Studio), and the DevToolsFramePresenter/
SettingsDevToolsCompositionPhase ImGui composition machinery are removed.
Vulkan never composed a DevTools frontend (DevToolsEnabled already forced
false whenever the backend was Vulkan); this commit makes that permanent by
deleting the only implementation rather than leaving a dead branch behind.

What moved: Studio/SampleData.cs is a live production dependency
(InteractionRetainedUiComposition's character-sheet fallback, plus three
UI.Layout test files) - git mv'd to src/AcDream.App/UI/Layout/SampleData.cs,
namespace AcDream.App.UI.Layout, and trimmed to the SampleCharacter API that
is actually still called (BuildObjectTable/AddItem/AddEquipped/the item-guid
and icon constants had zero callers left once the Studio fixture provider
that used them was deleted).

What survives as backend-neutral seams, per the tests that still exercise
them: IDevToolsFrameLifecycle (moved into RenderFramePreparationController.cs,
now always bound to null), IFramebufferDevToolsTarget/FramebufferDevToolsBinding
in FramebufferResizeController.cs (its concrete DevToolsFramebufferTarget
adapter is deleted), and IDevToolsGameplayCommands in
GameplayInputCommandController.cs (DevToolsGameplayCommands becomes a
documented no-op instead of forwarding to the deleted presenter). A follow-up
re-homes Settings/Debug onto the retained UI through IPanelRenderer; until
then keybind remapping falls back to editing keybinds.json.

DevToolsEnabled is now `private const bool DevToolsEnabled = false`.
RuntimeOptions.DevTools is unchanged and still reaches VulkanGraphicsContext
for the optional debug-utils extensions; Program.cs now logs one line when
ACDREAM_DEVTOOLS=1 explaining that the ImGui UI is gone and the flag is
Vulkan-only now.

Removed: AcDream.UI.ImGui (project + ImGui.NET/Silk.NET.OpenGL.Extensions.ImGui
package refs), src/AcDream.App/Studio (minus SampleData.cs),
DevToolsFramePresenter.cs and everything only it constructed
(ISettingsDevToolsCompositionFactory, RetailSettingsDevToolsCompositionFactory,
DevToolsCompositionOwner, IGameWindowSettingsDevToolsPublication,
SettingsDevToolsOptionalDependencies, the "developer tools" shutdown-ledger
stage and its DevTools-typed fields on IngressShutdownRoots/
RenderShutdownRoots), the ui-studio Program.cs verb, and the cimgui native
manifest entries in GraphicalHostPlatformServices. GameWindow.cs's DevTools
composition branch, its _vitalsVm/_debugVm/_devToolsComposition/
_devToolsFramePresenter/_devToolsCommandBus fields, and every settingsDevTools
.DevTools?.* access across FrameRootComposition.cs/SessionPlayerComposition.cs
are gone with it.

Build green; complete Release solution suite 8,830 / 5 skips (App Tests
4,097/3 skips run standalone - one #250-family zero-allocation test flakes
under the full parallel `dotnet test AcDream.slnx` run, a pre-existing,
documented class unrelated to this change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-28 23:56:04 +02:00
parent db4426d5ef
commit 844cf092a1
48 changed files with 227 additions and 6201 deletions

View file

@ -1,455 +0,0 @@
using System.Numerics;
using System.Reflection;
using AcDream.App.Diagnostics;
using AcDream.App.Net;
using AcDream.App.Rendering;
using AcDream.UI.Abstractions;
namespace AcDream.App.Tests.Rendering;
public sealed class DevToolsFramePresenterTests
{
[Fact]
public void CommandBusOwnedBindingReleasesExactlyAndAllowsRebind()
{
var source = new DevToolsCommandBusSource();
var first = new TestUiSessionTarget();
var second = new TestUiSessionTarget();
IDisposable firstBinding = source.BindOwned(first);
Assert.Same(first.Commands, source.Current);
Assert.Throws<InvalidOperationException>(() => source.BindOwned(second));
firstBinding.Dispose();
using IDisposable secondBinding = source.BindOwned(second);
firstBinding.Dispose();
Assert.Same(second.Commands, source.Current);
}
private sealed class TestUiSessionTarget : ILiveUiSessionTarget
{
public bool IsInWorld => false;
public AcDream.Core.Net.WorldSession? CurrentSession => null;
public ICommandBus Commands { get; } = new RecordingCommandBus();
}
[Fact]
public void Frame_PreservesBeginThenMenuPanelsAndDrawDataOrder()
{
var calls = new List<string>();
var backend = new RecordingBackend(calls);
var commands = new RecordingCommandSource();
var presenter = Create(backend, commands: commands);
presenter.BeginFrame(0.25f);
presenter.Render(0.5, 1280, 720);
Assert.Equal(
[
"begin:0.25",
"begin-bar",
"begin-menu:View",
"begin-menu:Camera",
"end-bar",
"panels:0.5",
"draw-data",
],
calls);
Assert.Same(commands.Current, backend.Contexts.Single().Commands);
}
[Fact]
public void Render_ResolvesTheCurrentCommandBusEveryFrame()
{
var backend = new RecordingBackend([]);
var first = new RecordingCommandBus();
var second = new RecordingCommandBus();
var commands = new RecordingCommandSource { Current = first };
var presenter = Create(backend, commands: commands);
presenter.BeginFrame(0.1f);
presenter.Render(0.1, 800, 600);
commands.Current = second;
presenter.BeginFrame(0.2f);
presenter.Render(0.2, 800, 600);
Assert.Same(first, backend.Contexts[0].Commands);
Assert.Same(second, backend.Contexts[1].Commands);
}
[Fact]
public void ViewMenu_TogglesExactPanelsAndUsesCurrentViewportForReset()
{
var backend = new RecordingBackend([]);
backend.OpenMenus.Add("View");
backend.ClickedItems.UnionWith(
["Settings", "Vitals", "Chat", "Debug", "Reset window layout"]);
var panels = new RecordingPanels();
var presenter = Create(backend, panels: panels);
presenter.BeginFrame(0.1f);
presenter.Render(0.1, 1920, 1080);
Assert.Equal(
[
DevToolsPanelKind.Settings,
DevToolsPanelKind.Vitals,
DevToolsPanelKind.Chat,
DevToolsPanelKind.Debug,
],
panels.Toggles);
Assert.Equal(
[
new LayoutCall("Vitals", new Vector2(10f, 30f), new Vector2(220f, 110f)),
new LayoutCall("Chat", new Vector2(10f, 760f), new Vector2(450f, 300f)),
new LayoutCall("Debug", new Vector2(1540f, 30f), new Vector2(370f, 520f)),
new LayoutCall("Settings", new Vector2(610f, 290f), new Vector2(700f, 500f)),
],
backend.Layouts.Select(call => call.WithoutCondition()));
Assert.All(
backend.Layouts,
call => Assert.Equal(DevToolsPanelLayoutCondition.Always, call.Condition));
}
[Fact]
public void CameraMenu_UsesFlyLabelAndRoundTripsCollisionState()
{
var backend = new RecordingBackend([]);
backend.OpenMenus.Add("Camera");
backend.ClickedItems.UnionWith(
["Exit Free-Fly Mode", "Collide Camera (spring arm)"]);
var camera = new RecordingCamera { IsFlyMode = true, CollideCamera = true };
var presenter = Create(backend, camera: camera);
presenter.BeginFrame(0.1f);
presenter.Render(0.1, 1280, 720);
Assert.Equal(1, camera.ToggleCount);
Assert.False(camera.CollideCamera);
Assert.Contains(
backend.MenuItems,
item => item.Label == "Collide Camera (spring arm)" && item.Selected);
}
[Fact]
public void MissingSettingsPanel_IsInertAndSkippedFromLayout()
{
var backend = new RecordingBackend([]);
backend.OpenMenus.Add("View");
backend.ClickedItems.Add("Settings");
var panels = new RecordingPanels { HasSettings = false };
var presenter = Create(backend, panels: panels);
presenter.ToggleSettingsPanel();
presenter.ResetLayout(1280, 720, DevToolsPanelLayoutCondition.FirstUseEver);
presenter.BeginFrame(0.1f);
presenter.Render(0.1, 1280, 720);
Assert.DoesNotContain(DevToolsPanelKind.Settings, panels.Toggles);
Assert.DoesNotContain(backend.MenuItems, item => item.Label == "Settings");
Assert.DoesNotContain(backend.Layouts, call => call.Title == "Settings");
}
[Theory]
[InlineData(1280, 720, 900, 400, 290, 110)]
[InlineData(1920, 1080, 1540, 760, 610, 290)]
[InlineData(100, 100, 100, 0, -110, -90)]
public void ResetLayout_PreservesExactFormulasAndMinimumViewport(
int width,
int height,
int debugX,
int chatY,
int settingsX,
int settingsY)
{
var backend = new RecordingBackend([]);
var presenter = Create(backend);
presenter.ResetLayout(width, height, DevToolsPanelLayoutCondition.FirstUseEver);
Assert.Collection(
backend.Layouts,
call => Assert.Equal(
new LayoutCall("Vitals", new Vector2(10f, 30f), new Vector2(220f, 110f)),
call.WithoutCondition()),
call => Assert.Equal(
new LayoutCall("Chat", new Vector2(10f, chatY), new Vector2(450f, 300f)),
call.WithoutCondition()),
call => Assert.Equal(
new LayoutCall("Debug", new Vector2(debugX, 30f), new Vector2(370f, 520f)),
call.WithoutCondition()),
call => Assert.Equal(
new LayoutCall(
"Settings",
new Vector2(settingsX, settingsY),
new Vector2(700f, 500f)),
call.WithoutCondition()));
}
[Fact]
public void AbortFrame_ClosesAnOpenBackendFrameAndAllowsTheNextFrame()
{
var calls = new List<string>();
var backend = new RecordingBackend(calls);
var presenter = Create(backend);
presenter.BeginFrame(0.1f);
presenter.AbortFrame();
presenter.AbortFrame();
presenter.BeginFrame(0.2f);
presenter.Render(0.2, 800, 600);
Assert.Equal(1, calls.Count(call => call == "abort"));
Assert.Equal(2, calls.Count(call => call.StartsWith("begin:")));
Assert.Equal(1, calls.Count(call => call == "draw-data"));
}
[Fact]
public void RenderFailure_RemainsAbortableAndDoesNotPoisonTheNextFrame()
{
var calls = new List<string>();
var backend = new RecordingBackend(calls)
{
DrawFailure = new InvalidOperationException("draw"),
};
var presenter = Create(backend);
presenter.BeginFrame(0.1f);
Assert.Throws<InvalidOperationException>(() =>
presenter.Render(0.1, 800, 600));
presenter.AbortFrame();
backend.DrawFailure = null;
presenter.BeginFrame(0.2f);
presenter.Render(0.2, 800, 600);
Assert.Equal(0, calls.Count(call => call == "abort"));
Assert.Equal(2, calls.Count(call => call == "draw-data"));
}
[Fact]
public void BeginFailure_RemainsAbortableAndDoesNotPoisonTheNextFrame()
{
var calls = new List<string>();
var backend = new RecordingBackend(calls)
{
BeginFailure = new InvalidOperationException("begin"),
};
var presenter = Create(backend);
Assert.Throws<InvalidOperationException>(() => presenter.BeginFrame(0.1f));
presenter.AbortFrame();
backend.BeginFailure = null;
presenter.BeginFrame(0.2f);
presenter.Render(0.2, 800, 600);
Assert.Equal(1, calls.Count(call => call == "abort"));
Assert.Equal(2, calls.Count(call => call.StartsWith("begin:")));
}
[Fact]
public void InputFacingActionsDelegateToTheTypedPanelOwner()
{
var panels = new RecordingPanels();
var presenter = Create(new RecordingBackend([]), panels: panels);
presenter.ToggleDebugPanel();
presenter.FocusChatInput();
presenter.ToggleSettingsPanel();
Assert.Equal(
[DevToolsPanelKind.Debug, DevToolsPanelKind.Settings],
panels.Toggles);
Assert.Equal(1, panels.FocusChatCount);
Assert.Equal(["Debug panel ON"], panels.Toasts);
}
[Fact]
public void PresenterAndAdaptersHaveNoWindowOrDelegateBackReferences()
{
Type[] owners =
[
typeof(DevToolsFramePresenter),
typeof(ImGuiDevToolsFrameBackend),
typeof(DevToolsCameraMenuOperations),
typeof(DevToolsCommandBusSource),
typeof(DevToolsPanelSet),
];
foreach (Type owner in owners)
{
FieldInfo[] fields = owner.GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
Assert.DoesNotContain(
fields,
field => field.FieldType == typeof(Silk.NET.Windowing.IWindow));
}
Assert.True(typeof(IDisposable).IsAssignableFrom(
typeof(ImGuiDevToolsFrameBackend)));
}
private static DevToolsFramePresenter Create(
RecordingBackend backend,
RecordingCamera? camera = null,
RecordingCommandSource? commands = null,
RecordingPanels? panels = null) =>
new(
backend,
camera ?? new RecordingCamera(),
commands ?? new RecordingCommandSource(),
new FrameProfiler(),
panels ?? new RecordingPanels());
private sealed class RecordingBackend(List<string> calls) : IDevToolsFrameBackend
{
public void Dispose() { }
public HashSet<string> OpenMenus { get; } = [];
public HashSet<string> ClickedItems { get; } = [];
public List<(string Label, string? Shortcut, bool Selected)> MenuItems { get; } = [];
public List<PanelContext> Contexts { get; } = [];
public List<LayoutCallWithCondition> Layouts { get; } = [];
public Exception? BeginFailure { get; set; }
public Exception? DrawFailure { get; set; }
public bool ControllerFrameOpen { get; private set; }
public void BeginFrame(float deltaSeconds)
{
calls.Add($"begin:{deltaSeconds}");
ControllerFrameOpen = true;
if (BeginFailure is not null)
throw BeginFailure;
}
public void AbortFrame()
{
// Silk ImGuiController.Render is idempotent. It closes an active
// controller frame, but is a no-op if Render already cleared its
// private _frameBegun flag before a GL upload failure.
if (!ControllerFrameOpen)
return;
ControllerFrameOpen = false;
calls.Add("abort");
}
public bool BeginMainMenuBar()
{
calls.Add("begin-bar");
return true;
}
public void EndMainMenuBar() => calls.Add("end-bar");
public bool BeginMenu(string label)
{
calls.Add($"begin-menu:{label}");
return OpenMenus.Contains(label);
}
public void EndMenu() => calls.Add("end-menu");
public bool MenuItem(string label, string? shortcut = null, bool selected = false)
{
MenuItems.Add((label, shortcut, selected));
return ClickedItems.Contains(label);
}
public void Separator() => calls.Add("separator");
public void RenderPanels(PanelContext context)
{
Contexts.Add(context);
calls.Add($"panels:{context.DeltaSeconds}");
}
public void RenderDrawData()
{
Assert.True(ControllerFrameOpen);
ControllerFrameOpen = false;
calls.Add("draw-data");
if (DrawFailure is not null)
throw DrawFailure;
}
public void SetWindowLayout(
string title,
Vector2 position,
Vector2 size,
DevToolsPanelLayoutCondition condition) =>
Layouts.Add(new LayoutCallWithCondition(title, position, size, condition));
}
private sealed class RecordingCamera : IDevToolsCameraMenuOperations
{
public bool IsFlyMode { get; init; }
public bool CollideCamera { get; set; }
public int ToggleCount { get; private set; }
public void ToggleFlyOrChase() => ToggleCount++;
}
private sealed class RecordingCommandSource : IDevToolsCommandBusSource
{
public ICommandBus Current { get; set; } = new RecordingCommandBus();
}
private sealed class RecordingCommandBus : ICommandBus
{
public void Publish<T>(T command) where T : notnull
{
}
}
private sealed class RecordingPanels : IDevToolsPanelSet
{
private readonly HashSet<DevToolsPanelKind> _visible = [];
public bool HasSettings { get; init; } = true;
public List<DevToolsPanelKind> Toggles { get; } = [];
public List<string> Toasts { get; } = [];
public int FocusChatCount { get; private set; }
public bool Contains(DevToolsPanelKind kind) =>
kind != DevToolsPanelKind.Settings || HasSettings;
public string? GetTitle(DevToolsPanelKind kind) => Contains(kind)
? kind.ToString()
: null;
public bool IsVisible(DevToolsPanelKind kind) => _visible.Contains(kind);
public void Toggle(DevToolsPanelKind kind)
{
if (!Contains(kind))
return;
Toggles.Add(kind);
if (!_visible.Add(kind))
_visible.Remove(kind);
}
public void FocusChatInput() => FocusChatCount++;
public void AddDebugToast(string message) => Toasts.Add(message);
}
private readonly record struct LayoutCall(
string Title,
Vector2 Position,
Vector2 Size);
private readonly record struct LayoutCallWithCondition(
string Title,
Vector2 Position,
Vector2 Size,
DevToolsPanelLayoutCondition Condition)
{
public LayoutCall WithoutCondition() => new(Title, Position, Size);
}
}

View file

@ -108,13 +108,6 @@ public sealed class GameWindowRenderLeafCompositionTests
Assert.DoesNotContain(identifier, source, StringComparison.Ordinal);
Assert.Contains("new PaperdollFramePresenter(", LivePresentationSource());
string settingsComposition = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",
"AcDream.App",
"Composition",
"SettingsDevToolsComposition.cs"));
Assert.Contains("new DevToolsFramePresenter(", settingsComposition);
string framePhase = FrameRootSource();
Assert.Contains("new RenderFrameResourceController(", framePhase);
Assert.Contains("new RenderWeatherFrameController(", framePhase);
@ -131,23 +124,21 @@ public sealed class GameWindowRenderLeafCompositionTests
}
[Fact]
public void Shutdown_PreservesBorrowedDevtoolsLifetimeAndDrainsGpuBeforeFrontends()
public void Shutdown_DrainsGpuBeforeFrontendsAndPreservesFrameBorrowerOrder()
{
// Campaign V slice V11 removed the ImGui developer-tools frontend and
// its "developer tools" shutdown stage entry along with it; this test
// used to pin that entry's position and is now renamed to pin what
// survives it.
string source = GameWindowLifetimeSource();
AssertAppearsInOrder(
source,
"new ResourceShutdownStage(\"submitted GPU work\"",
"new ResourceShutdownStage(\"render frontends\"",
"Hard(\"developer tools\", () => DisposeDevToolsFrontend(render.DevTools))",
"Hard(\"portal tunnel\"",
"Hard(\"paperdoll viewport\"",
"new ResourceShutdownStage(\"OpenGL context\"");
AssertAppearsInOrder(
source,
"private static void DisposeDevToolsFrontend(DevToolsCompositionOwner? owner)",
"owner.DisposeFrontend()",
"if (!owner.IsFrontendDisposalComplete)");
AssertAppearsInOrder(
source,
"new ResourceShutdownStage(\"frame borrowers\"",
@ -161,7 +152,7 @@ public sealed class GameWindowRenderLeafCompositionTests
AssertAppearsInOrder(
source,
"frame.FrameGraphPublication?.Dispose()",
"Hard(\"developer tools\", () => DisposeDevToolsFrontend(render.DevTools))",
"new ResourceShutdownStage(\"render frontends\"",
"new ResourceShutdownStage(\"input context\"",
"platform.Input?.Dispose()",
"new ResourceShutdownStage(\"OpenGL context\"");

View file

@ -58,7 +58,7 @@ public sealed class GameWindowSlice8BoundaryTests
"new ContentEffectsAudioCompositionPhase(",
"this).Compose(platformResult, hostInputCamera),",
"new SettingsDevToolsCompositionPhase(",
"this).Compose(platformResult, hostInputCamera, contentEffectsAudio);",
".Compose(platformResult, hostInputCamera, contentEffectsAudio),",
"new InteractionRetainedUiCompositionPhase(",
"this).Compose(",
"new LivePresentationCompositionPhase(",
@ -197,17 +197,6 @@ public sealed class GameWindowSlice8BoundaryTests
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",
@ -349,7 +338,7 @@ public sealed class GameWindowSlice8BoundaryTests
load,
"GameWindowCompositionPipeline.Run<",
"new SettingsDevToolsCompositionPhase(",
"this).Compose(platformResult, hostInputCamera, contentEffectsAudio);",
".Compose(platformResult, hostInputCamera, contentEffectsAudio),",
"new WorldRenderCompositionPhase(",
"new SessionPlayerCompositionPhase(",
"new FrameRootCompositionPhase(",
@ -371,10 +360,10 @@ public sealed class GameWindowSlice8BoundaryTests
"AcDream.App",
"Composition",
"SettingsDevToolsComposition.cs"));
AssertAppearsInOrder(
settingsPhase,
Assert.Contains(
"_dependencies.Settings.ApplyStartup(_dependencies.StartupTarget);",
"_dependencies.DevTools is { } optional");
settingsPhase,
StringComparison.Ordinal);
string worldPhase = File.ReadAllText(Path.Combine(
FindRepoRoot(),
"src",

View file

@ -162,9 +162,6 @@ public sealed class PrivatePresentationRendererTests
Assert.Contains(
typeof(AcDream.App.UI.UiElement),
FieldTypes(typeof(PaperdollInventoryVisibility)));
Assert.Contains(
typeof(AcDream.UI.Abstractions.Panels.Debug.DebugVM),
FieldTypes(typeof(DevToolsPanelSet)));
Assert.Contains(
typeof(AcDream.App.Settings.IRuntimeSettingsPreviewSource),
FieldTypes(typeof(RuntimeWorldFrameSettingsPreview)));