Vulkan is the sole, user-signed-off backend (V10 landed) and step 1 already removed ImGui/Studio/DevTools. This step deletes the GL rendering backend itself: every Gpu/Gl/** implementation, the Wb ManagedGL*/GLHelpers/GLSLShader/GLStateScope/RenderStateCache/ BindlessSupport family, Shader/ShaderProgramConstruction/SamplerCache, RenderBootstrap, and RenderFrameGlStateController. GameWindow.cs's Run()/CreateGraphics()/CreateBackbufferReader()/ OnLoad() collapse to their Vulkan-only arm; GameWindowGraphics loses its OpenGlGameWindowGraphics subclass. RuntimeOptions.RenderBackend and RenderBackendKind (incl. the Gl member of GpuBackendKind) are gone — there is nothing left to select between. The five world-draw dual-arm renderers (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer, SkyRenderer) and the composition roots (WorldRenderComposition, HostInputCameraComposition, LivePresentationComposition, FrameRootComposition) collapse to their RHI-only arm. GL-only diagnostic properties with a live external reader (DynamicBufferCount and friends) simplify to a documented `=> 0`/no-op rather than disappearing, since the reader is out of this commit's scope. A few GL-flavored mechanisms turned out to be backend-neutral once isolated: GlConstructionCleanupLedger is renamed ResourceConstructionCleanupLedger (exception-chain walking has nothing to do with GL), and GlfwNativePlatformProbe moved out of the otherwise GL-only GraphicalCapabilityRecord.cs into GraphicalWindowBackendSelection.cs before the rest of that file was deleted. Test files with no surviving subject are deleted outright (GraphicalCapabilityRequirementsTests, ShaderProgramConstructionTests, PortalDepthShaderParityTests, TextureCacheBindlessTests, TextRendererFailureSafetyTests, ClipFrameUploadTests, every Gpu/Gl/*Tests, GlTextureOwnershipTests, RenderFrameGlStateControllerTests); others get their dead GL-only members trimmed while their live assertions stay (ClipFrameLayoutTests' MeshClipSsboBinding check now reads GpuBindingModel.StorageClipRegions, the same binding index under its new backend-neutral name; GpuResourceRetirementTransactionTests drops its OpenGLGraphicsDevice-subclassing test double and the two GL queue tests it existed for). EnvCellRendererTests' construction helper now builds a real ObjectMeshManager via VulkanMeshPipelineDevice instead of passing null through a null-forgiving operator, since the RHI constructor never tolerated a null mesh manager and the old GL constructor (which did) is gone. Deferred to the next two steps, deliberately not touched here: the Silk.NET.OpenGL/.Extensions.ARB package references, IMeshPipelineDevice.Gl (WbMeshAdapter's GL? threading stays in place), Chorizite.Core's stale csproj comment (the package itself is still load-bearing — TextureFormat and friends are used well beyond the deleted ManagedGLUniformBuffer), and the CI/gate scripts. Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors. Tests: full-solution `dotnet test` green across every project (App.Tests 3937/3940 + 3 skips, Core.Tests 3296/3298 + 2 skips, all others 100%); the 2 App.Tests names that flake under full-suite parallel execution (#250-family, documented pre-existing) pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
277 lines
10 KiB
C#
277 lines
10 KiB
C#
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using Silk.NET.Input;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// Packages the <see cref="UiRoot"/>, the 2D sprite batcher
|
|
/// (<see cref="Rendering.TextRenderer"/>), and a default font so
|
|
/// <c>GameWindow</c> can wire the retail-style UI in with one
|
|
/// construction and a handful of input callbacks.
|
|
///
|
|
/// Usage (from <c>GameWindow.OnLoad</c>):
|
|
/// <code>
|
|
/// _uiHost = new UiHost(_gl, shadersDir, _debugFont);
|
|
/// _uiHost.Root.WorldMouseFallThrough += (btn, x, y, f) => HandleWorldClick(btn, x, y);
|
|
/// _uiHost.Root.WorldKeyFallThrough += (vk, lp) => HandleHotkey(vk);
|
|
///
|
|
/// foreach (var mouse in _input.Mice)
|
|
/// _uiHost.WireMouse(mouse);
|
|
/// foreach (var kb in _input.Keyboards)
|
|
/// _uiHost.WireKeyboard(kb);
|
|
/// </code>
|
|
///
|
|
/// And per frame (from <c>GameWindow.OnRender</c>):
|
|
/// <code>
|
|
/// _uiHost.Tick(deltaSeconds);
|
|
/// _uiHost.Draw(new Vector2(_window!.Size.X, _window.Size.Y));
|
|
/// </code>
|
|
///
|
|
/// Retail analog: the trio of <c>DAT_00870340</c> (Core, owns fonts/atlases),
|
|
/// <c>DAT_00837ff4</c> (Device, owns input state), <c>DAT_00870c2c</c>
|
|
/// (Keystone root, widget tree). We fuse them into a single host class
|
|
/// because we're not linking to Keystone.
|
|
/// </summary>
|
|
public sealed class UiHost : System.IDisposable
|
|
{
|
|
public UiRoot Root { get; } = new();
|
|
public RetailWindowManager WindowManager => Root.WindowManager;
|
|
public TextRenderer TextRenderer { get; }
|
|
public BitmapFont? DefaultFont { get; set; }
|
|
|
|
/// <summary>The last wired keyboard. Exposed so widgets that need clipboard
|
|
/// access (<see cref="IKeyboard.ClipboardText"/>) or modifier-key state
|
|
/// (<see cref="IKeyboard.IsKeyPressed"/>) — e.g. <see cref="UiText"/>'s
|
|
/// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins.</summary>
|
|
public IKeyboard? Keyboard { get; private set; }
|
|
|
|
private long _startTicks = System.Environment.TickCount64;
|
|
private readonly HostQuiescenceGate _quiescence;
|
|
private readonly List<IRetainedUiInputBinding> _inputBindings = new();
|
|
private ResourceShutdownTransaction? _inputShutdown;
|
|
private ResourceShutdownTransaction? _shutdown;
|
|
private bool _disposeRequested;
|
|
private bool _disposed;
|
|
|
|
internal bool IsDisposalComplete => _disposed;
|
|
|
|
// internal, not public: IGpuDevice/ICurrentGpuFrameSource are internal
|
|
// types (the pinned RHI contract). UiHost stays public — only
|
|
// construction is restricted.
|
|
internal UiHost(
|
|
IGpuDevice device,
|
|
ICurrentGpuFrameSource frameSource,
|
|
string shaderDir,
|
|
BitmapFont? defaultFont = null)
|
|
: this(device, frameSource, shaderDir, defaultFont, new HostQuiescenceGate())
|
|
{
|
|
}
|
|
|
|
internal UiHost(
|
|
IGpuDevice device,
|
|
ICurrentGpuFrameSource frameSource,
|
|
string shaderDir,
|
|
BitmapFont? defaultFont,
|
|
HostQuiescenceGate quiescence)
|
|
{
|
|
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
|
|
TextRenderer = new TextRenderer(device, frameSource, shaderDir);
|
|
DefaultFont = defaultFont;
|
|
}
|
|
|
|
// ── Per-frame ──────────────────────────────────────────────────────
|
|
|
|
public void Tick(double deltaSeconds)
|
|
{
|
|
long now = System.Environment.TickCount64 - _startTicks;
|
|
Root.Tick(deltaSeconds, now);
|
|
}
|
|
|
|
public void Draw(Vector2 screenSize)
|
|
{
|
|
// Set UiRoot bounds to full screen so HitTestTopDown works.
|
|
Root.Width = screenSize.X;
|
|
Root.Height = screenSize.Y;
|
|
var ctx = new UiRenderContext(TextRenderer, screenSize, DefaultFont);
|
|
TextRenderer.Begin(screenSize);
|
|
Root.Draw(ctx);
|
|
TextRenderer.Flush(DefaultFont);
|
|
}
|
|
|
|
// ── Input wiring helpers ───────────────────────────────────────────
|
|
|
|
public void WireMouse(IMouse mouse)
|
|
{
|
|
System.ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
|
|
System.ArgumentNullException.ThrowIfNull(mouse);
|
|
|
|
var binding = new RetainedMouseInputBinding(
|
|
new SilkRetainedMouseSurface(mouse),
|
|
Root,
|
|
_quiescence);
|
|
_inputBindings.Add(binding);
|
|
try
|
|
{
|
|
binding.Attach();
|
|
}
|
|
catch
|
|
{
|
|
if (binding.IsDisposalComplete)
|
|
_inputBindings.Remove(binding);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public void WireKeyboard(IKeyboard kb)
|
|
{
|
|
System.ObjectDisposedException.ThrowIf(_disposeRequested || _disposed, this);
|
|
System.ArgumentNullException.ThrowIfNull(kb);
|
|
Keyboard = kb; // last wired keyboard wins (one-keyboard desktop)
|
|
var binding = new RetainedKeyboardInputBinding(
|
|
new SilkRetainedKeyboardSurface(kb),
|
|
Root,
|
|
_quiescence);
|
|
_inputBindings.Add(binding);
|
|
try
|
|
{
|
|
binding.Attach();
|
|
}
|
|
catch
|
|
{
|
|
if (binding.IsDisposalComplete)
|
|
_inputBindings.Remove(binding);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops retained-device delivery without retiring the window tree or GL
|
|
/// renderer. Safe to call before a potentially long live-session close.
|
|
/// </summary>
|
|
public void QuiesceInput()
|
|
{
|
|
_disposeRequested = true;
|
|
foreach (IRetainedUiInputBinding binding in _inputBindings)
|
|
binding.Deactivate();
|
|
Keyboard = null;
|
|
}
|
|
|
|
/// <summary>Physically removes every retained input edge after quiescence.</summary>
|
|
public void DeactivateInput() => CompleteInputShutdown(reportFailures: true);
|
|
|
|
private void CompleteInputShutdown(bool reportFailures)
|
|
{
|
|
QuiesceInput();
|
|
|
|
_inputShutdown ??= new ResourceShutdownTransaction(
|
|
new ResourceShutdownStage(
|
|
"retained UI input subscriptions",
|
|
_inputBindings
|
|
.AsEnumerable()
|
|
.Reverse()
|
|
.Select((binding, index) => new ResourceShutdownOperation(
|
|
$"input binding {index}",
|
|
binding.Dispose,
|
|
ResourceShutdownOperationPolicy.ReportAndContinue))
|
|
.ToArray()));
|
|
_inputShutdown.CompleteOrThrow();
|
|
if (_inputShutdown.IsComplete && _inputShutdown.CleanupFailures.Count == 0)
|
|
_inputBindings.Clear();
|
|
if (reportFailures && _inputShutdown.CleanupFailures.Count != 0)
|
|
{
|
|
throw new AggregateException(
|
|
"Retained UI input callback cleanup completed with failures.",
|
|
_inputShutdown.CleanupFailures.Select(static failure =>
|
|
new InvalidOperationException(
|
|
$"Retained UI operation '{failure.Operation}' failed in stage '{failure.Stage}'.",
|
|
failure.Error)));
|
|
}
|
|
}
|
|
|
|
// ── Window manager forwarders (delegate to UiRoot) ─────────────────
|
|
|
|
/// <summary>Register a top-level window for Show/Hide/Toggle. See <see cref="UiRoot.RegisterWindow"/>.</summary>
|
|
public RetailWindowHandle RegisterWindow(
|
|
string name,
|
|
UiElement window,
|
|
UiElement? contentRoot = null,
|
|
IRetainedPanelController? controller = null,
|
|
IRetainedWindowStateController? stateController = null,
|
|
int authoredGeometryRevision = 0)
|
|
=> Root.RegisterWindow(
|
|
name,
|
|
window,
|
|
contentRoot,
|
|
controller,
|
|
stateController,
|
|
authoredGeometryRevision);
|
|
|
|
public bool UnregisterWindow(string name) => Root.UnregisterWindow(name);
|
|
|
|
/// <summary>Show a registered window; returns false if the name is unknown.</summary>
|
|
public bool ShowWindow(string name) => Root.ShowWindow(name);
|
|
|
|
/// <summary>Hide a registered window; returns false if the name is unknown.</summary>
|
|
public bool HideWindow(string name) => Root.HideWindow(name);
|
|
|
|
public bool CloseWindow(string name) => Root.CloseWindow(name);
|
|
|
|
/// <summary>Return the current visibility of a registered window.</summary>
|
|
public bool IsWindowVisible(string name) => Root.IsWindowVisible(name);
|
|
|
|
/// <summary>Toggle a registered window's visibility; returns the new IsVisible.</summary>
|
|
public bool ToggleWindow(string name) => Root.ToggleWindow(name);
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed)
|
|
return;
|
|
|
|
_disposeRequested = true;
|
|
_shutdown ??= CreateShutdownTransaction(
|
|
[() => CompleteInputShutdown(reportFailures: false)],
|
|
() => { },
|
|
WindowManager.Dispose,
|
|
TextRenderer.Dispose);
|
|
_shutdown.CompleteOrThrow();
|
|
_disposed = _shutdown.IsComplete;
|
|
}
|
|
|
|
internal static ResourceShutdownTransaction CreateShutdownTransaction(
|
|
IReadOnlyList<Action> inputUnsubscribers,
|
|
Action releaseInputState,
|
|
Action disposeWindowManager,
|
|
Action disposeTextRenderer)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(inputUnsubscribers);
|
|
ArgumentNullException.ThrowIfNull(releaseInputState);
|
|
ArgumentNullException.ThrowIfNull(disposeWindowManager);
|
|
ArgumentNullException.ThrowIfNull(disposeTextRenderer);
|
|
|
|
return new ResourceShutdownTransaction(
|
|
new ResourceShutdownStage(
|
|
"retained UI input subscriptions",
|
|
inputUnsubscribers
|
|
.Select((unsubscribe, index) => new ResourceShutdownOperation(
|
|
$"input subscription {index}",
|
|
unsubscribe ?? throw new ArgumentException(
|
|
"Input unsubscriber entries cannot be null.",
|
|
nameof(inputUnsubscribers))))
|
|
.ToArray()),
|
|
new ResourceShutdownStage("retained UI input state",
|
|
[
|
|
new("input state", releaseInputState),
|
|
]),
|
|
new ResourceShutdownStage("retained UI windows",
|
|
[
|
|
new("window manager", disposeWindowManager),
|
|
]),
|
|
new ResourceShutdownStage("retained UI renderer",
|
|
[
|
|
new("text renderer", disposeTextRenderer),
|
|
]));
|
|
}
|
|
}
|