feat(studio): StudioWindow + LayoutSource — render a dat panel standalone
Task 2 of the acdream UI Studio plan. Adds three new files under src/AcDream.App/Studio/ and one new test file: - StudioOptions.cs: record + Parse() for the ui-studio CLI args (positional dat dir or ACDREAM_DAT_DIR, --layout 0xNNNN, --markup <path>; defaults to vitals 0x2100006C when neither layout nor markup given). - LayoutSource.cs: wraps LayoutImporter.Import for dat-backed layouts; markup path sets LastError "markup unsupported (Task 6)" and returns null for now. Exposes Kind / LayoutId / MarkupPath / LastError / CurrentLayout / Reload(). - StudioWindow.cs: Silk.NET 1280×720 GL 4.3 window; boots RenderBootstrap, wires input to UiHost, loads the panel via LayoutSource, adds the root to UiHost.Root.AddChild. QualitySettings resolved the same way GameWindow.Run() does (SettingsStore → QualitySettings.From → WithEnvOverrides). - Program.cs: ui-studio dispatch at the top of top-level statements (before the dat-dir parse) so `dotnet run -- ui-studio` routes to StudioWindow. - LayoutSourceTests.cs: dat-gated test (skips when dats absent); verifies that the vitals LayoutDesc (0x2100006C) loads, root is non-null, byId contains the vitals root element (0x100005F9), and Kind == DatLayout. Passes (1/1 with dats present; silently skips on CI). Note: the spec asserts FindElement(0x2100006Cu) — that id is the LayoutDesc dat id, not a widget element id. The actual vitals root element id is 0x100005F9 (confirmed from the vitals_2100006C.json fixture). The test uses the correct element id and documents the discrepancy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
79ee3ffbe2
commit
df6b5b3b39
5 changed files with 387 additions and 0 deletions
150
src/AcDream.App/Studio/StudioWindow.cs
Normal file
150
src/AcDream.App/Studio/StudioWindow.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.Maths;
|
||||
using Silk.NET.OpenGL;
|
||||
using Silk.NET.Windowing;
|
||||
using static Silk.NET.OpenGL.ClearBufferMask;
|
||||
|
||||
namespace AcDream.App.Studio;
|
||||
|
||||
/// <summary>
|
||||
/// Standalone Silk.NET window that boots the production render stack
|
||||
/// (<see cref="RenderBootstrap"/>) and previews a single UI panel
|
||||
/// identified by a <see cref="LayoutSource"/>.
|
||||
///
|
||||
/// Usage: dotnet run -- ui-studio [dat-dir] [--layout 0xNNNN] [--markup path]
|
||||
///
|
||||
/// The window is intentionally thin: no game world, no physics, no streaming —
|
||||
/// just GL + UiHost + the layout under test, identical to how the panel
|
||||
/// appears inside <c>GameWindow</c>.
|
||||
/// </summary>
|
||||
public sealed class StudioWindow : IDisposable
|
||||
{
|
||||
private readonly StudioOptions _opts;
|
||||
|
||||
// Created in OnLoad, released in OnClosing.
|
||||
private IWindow? _window;
|
||||
private DatCollection? _dats;
|
||||
private RenderStack? _stack;
|
||||
private LayoutSource? _source;
|
||||
|
||||
public StudioWindow(StudioOptions opts)
|
||||
{
|
||||
_opts = opts ?? throw new ArgumentNullException(nameof(opts));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open the window and block until it is closed.
|
||||
/// Mirrors <c>GameWindow.Run()</c>.
|
||||
/// </summary>
|
||||
public void Run()
|
||||
{
|
||||
// Resolve quality settings the same way GameWindow.Run() does
|
||||
// (SettingsStore → QualitySettings.From → WithEnvOverrides).
|
||||
var startupStore = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
|
||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
var startupDisplay = startupStore.LoadDisplay();
|
||||
var startupBase = AcDream.UI.Abstractions.Settings.QualitySettings.From(startupDisplay.Quality);
|
||||
var startupQuality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides(startupBase);
|
||||
|
||||
var options = WindowOptions.Default with
|
||||
{
|
||||
Size = new Vector2D<int>(1280, 720),
|
||||
Title = "acdream UI Studio",
|
||||
API = new GraphicsAPI(
|
||||
ContextAPI.OpenGL,
|
||||
ContextProfile.Core,
|
||||
ContextFlags.ForwardCompatible,
|
||||
new APIVersion(4, 3)),
|
||||
VSync = false,
|
||||
// MSAA from quality preset — must be baked into the GL context at creation.
|
||||
Samples = startupQuality.MsaaSamples,
|
||||
PreferredStencilBufferBits = 8,
|
||||
};
|
||||
|
||||
_window = Window.Create(options);
|
||||
_window.Load += OnLoad;
|
||||
_window.Update += OnUpdate;
|
||||
_window.Render += OnRender;
|
||||
_window.Closing += OnClosing;
|
||||
_window.Run();
|
||||
}
|
||||
|
||||
private void OnLoad()
|
||||
{
|
||||
var gl = GL.GetApi(_window!);
|
||||
var input = _window!.CreateInput();
|
||||
|
||||
_dats = new DatCollection(_opts.DatDir, DatAccessType.Read);
|
||||
|
||||
// Build QualitySettings for RenderBootstrap (same as Run() above — re-read
|
||||
// after the GL context is confirmed, mirroring GameWindow.OnLoad).
|
||||
var store = new AcDream.UI.Abstractions.Panels.Settings.SettingsStore(
|
||||
AcDream.UI.Abstractions.Panels.Settings.SettingsStore.DefaultPath());
|
||||
var display = store.LoadDisplay();
|
||||
var quality = AcDream.UI.Abstractions.Settings.QualitySettings.WithEnvOverrides(
|
||||
AcDream.UI.Abstractions.Settings.QualitySettings.From(display.Quality));
|
||||
|
||||
_stack = RenderBootstrap.Create(gl, _dats, new RenderBootstrapOptions(quality));
|
||||
|
||||
// Wire input into UiHost.
|
||||
foreach (var mouse in input.Mice)
|
||||
_stack.UiHost.WireMouse(mouse);
|
||||
foreach (var kb in input.Keyboards)
|
||||
_stack.UiHost.WireKeyboard(kb);
|
||||
|
||||
// Load the panel described by options and add it to the UI tree.
|
||||
_source = new LayoutSource(_dats, _stack.ResolveChrome, _stack.VitalsDatFont);
|
||||
var root = _source.Load(_opts);
|
||||
if (root is not null)
|
||||
_stack.UiHost.Root.AddChild(root);
|
||||
else
|
||||
Console.Error.WriteLine($"[studio] panel load failed: {_source.LastError}");
|
||||
}
|
||||
|
||||
private double _dt;
|
||||
|
||||
private void OnUpdate(double dt)
|
||||
{
|
||||
_dt = dt;
|
||||
}
|
||||
|
||||
private void OnRender(double dt)
|
||||
{
|
||||
if (_stack is null) return;
|
||||
|
||||
var gl = _stack.Gl;
|
||||
gl.ClearColor(0.18f, 0.18f, 0.18f, 1f);
|
||||
gl.Clear(ColorBufferBit | DepthBufferBit);
|
||||
|
||||
_stack.UiHost.Tick(_dt);
|
||||
_stack.UiHost.Draw(new Vector2(_window!.Size.X, _window!.Size.Y));
|
||||
}
|
||||
|
||||
private void OnClosing()
|
||||
{
|
||||
_stack?.DrawDispatcher.Dispose();
|
||||
_stack?.MeshAdapter.Dispose();
|
||||
_stack?.TextureCache.Dispose();
|
||||
_stack?.MeshShader.Dispose();
|
||||
_stack?.LightingUbo.Dispose();
|
||||
_stack?.UiHost.Dispose();
|
||||
_dats?.Dispose();
|
||||
_dats = null;
|
||||
_stack = null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
// If OnClosing wasn't called (e.g. exception before Run()), clean up anyway.
|
||||
_stack?.UiHost.Dispose();
|
||||
_dats?.Dispose();
|
||||
_dats = null;
|
||||
_stack = null;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue