feat(ui): Phase D.2a — VitalsPanel wired into GameWindow + backend pivot

Closes Phase D.2a. Launch with ACDREAM_DEVTOOLS=1 now shows a live
ImGui "Vitals" window whose HP bar reads CombatState.GetHealthPercent
for the local player. Without the env var the branches are dead code,
no ImGui context is created, and behaviour is identical to before.

GameWindow hunks:
  - fields: _imguiBootstrap / _panelHost / _vitalsVm + DevToolsEnabled
  - init (OnLoad): construct bootstrap + host, register VitalsPanel
  - GUID push: _vitalsVm?.SetLocalPlayerGuid(chosen.Id) at live-connect
  - frame begin: _imguiBootstrap.BeginFrame(dt) after GL clear
  - frame end: _panelHost.RenderAll(ctx) + _imguiBootstrap.Render() after debug overlay
  - input gating: skip WASD when ImGui.GetIO().WantCaptureKeyboard

Backend pivot: Hexa.NET.ImGui → ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui.

First-light integration with the Hexa backend crashed 0xC0000005 inside
Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative. Root cause:
Hexa's native OpenGL3 backend resolves GL function pointers via GLFW or
SDL internally; with Silk.NET (which uses neither) the pointers are null
and the native code crashes on first use. The mitigation path was
already planned — the design doc's Risk section called a pivot to
ImGui.NET a "one-morning operation" — and that's exactly what happened.

  - Packages: Hexa.NET.ImGui 2.2.9 + Hexa.NET.ImGui.Backends 1.0.18
    → ImGui.NET 1.91.6.1 + Silk.NET.OpenGL.Extensions.ImGui 2.23.0
  - ImGuiBootstrapper: was static Initialize(gl)+Shutdown() wrapping
    Hexa's OpenGL3 init; now an IDisposable wrapping Silk.NET's
    ImGuiController instance which handles GL backend init + input
    subscription in one go.
  - SilkInputBridge.cs deleted (~190 LOC): ImGuiController subscribes
    IKeyboard / IMouse events itself, we don't need a bespoke bridge.
  - ImGuiPanelRenderer: ImGuiNET.ImGui.* calls instead of
    Hexa.NET.ImGui.ImGui.*. Widget surface unchanged.

Boundary discipline is preserved — no panel imports ImGuiNET; only
ImGuiPanelRenderer does. The D.2b custom toolkit will implement the
same IPanelRenderer contract without touching panel code.

Out of scope (tracked for follow-up):
  - Stam/Mana currently return float? null (VitalsVM). Absolute values
    need LocalPlayerState + PlayerDescription (0x0013) parsing to be
    stored rather than discarded — filed as a post-D.2a issue.
  - Mouse-capture gating (WorldMouseFallThrough-style click-through
    tests) — not needed until we add clickable inventory items.

Roadmap + memory + architecture doc + UI framework plan updated in the
same commit per CLAUDE.md roadmap-discipline rules. 753 tests pass
(550 Core + 192 Core.Net + 11 new UI.Abstractions), 0 build warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-25 00:43:46 +02:00
parent a7dbce3474
commit 55aaca7a14
13 changed files with 218 additions and 275 deletions

View file

@ -1,62 +1,64 @@
using Hexa.NET.ImGui;
using Hexa.NET.ImGui.Backends.OpenGL3;
using Silk.NET.Input;
using Silk.NET.OpenGL;
using Silk.NET.OpenGL.Extensions.ImGui;
using Silk.NET.Windowing;
namespace AcDream.UI.ImGui;
/// <summary>
/// One-shot ImGui setup / teardown for the devtools overlay. Called from
/// <c>GameWindow</c> when <c>ACDREAM_DEVTOOLS=1</c>. Hides the cimgui
/// context + OpenGL3 renderer-impl lifecycles behind two static methods
/// so the calling code stays clean.
/// Owns the <c>ImGuiController</c> from <c>Silk.NET.OpenGL.Extensions.ImGui</c>,
/// which handles the whole Silk.NET ↔ ImGui.NET integration:
/// <list type="bullet">
/// <item>Creates the ImGui context + OpenGL3 backend using Silk.NET's GL binding
/// (no GLFW / SDL dependency — unlike Hexa.NET.ImGui, which assumed one).</item>
/// <item>Subscribes to Silk.NET's window + input events to drive IO.</item>
/// <item>Per frame: <c>Update(dt)</c> calls <c>ImGui.NewFrame()</c>; <c>Render()</c>
/// calls <c>ImGui.Render()</c> + uploads draw data via its OpenGL3 backend.</item>
/// </list>
///
/// <para>
/// Intentionally <b>not</b> an <c>IDisposable</c> singleton — the host
/// window owns the one call to <see cref="Shutdown"/> at application
/// exit. Re-initialisation mid-session is not supported.
/// Instance-scoped rather than static so GL-context lifetime is explicit.
/// <c>GameWindow</c> owns the one instance and disposes on shutdown.
/// </para>
///
/// <para>
/// History: tried <c>Hexa.NET.ImGui</c> + <c>Hexa.NET.ImGui.Backends.OpenGL3</c> first
/// per the original plan, but its native OpenGL3 backend resolves GL functions
/// via GLFW / SDL internally and crashed (0xC0000005) in <c>InitNative</c> without
/// one of those present. Pivoted to the official Silk.NET extension on 2026-04-25.
/// </para>
/// </summary>
public static class ImGuiBootstrapper
public sealed class ImGuiBootstrapper : IDisposable
{
private static bool _initialized;
private readonly ImGuiController _controller;
public ImGuiBootstrapper(GL gl, IView window, IInputContext input)
{
ArgumentNullException.ThrowIfNull(gl);
ArgumentNullException.ThrowIfNull(window);
ArgumentNullException.ThrowIfNull(input);
// ImGuiController constructor handles:
// - ImGui.CreateContext()
// - ImGuiOpenGL3 shader + vertex-buffer init (via Silk.NET GL)
// - Keyboard + mouse event subscription (bound to Silk.NET IInputContext)
// - Default style = dark
_controller = new ImGuiController(gl, window, input);
}
/// <summary>
/// Create an ImGui context, apply the dark style + enable keyboard
/// navigation, and bootstrap the OpenGL3 renderer backend. The GL
/// context owned by Silk.NET must be current on the calling thread.
/// Begin an ImGui frame. Call BEFORE any <c>ImGui.*</c> widget calls.
/// Internally: consumes buffered input events, calls <c>ImGui.NewFrame()</c>.
/// </summary>
/// <param name="glslVersion">
/// GLSL version directive for the ImGui-internal shader.
/// <c>"#version 330"</c> matches acdream's existing shaders and is
/// the safest default for the OpenGL 4.3 core profile we ship.
/// </param>
public static void Initialize(string glslVersion = "#version 330")
{
if (_initialized) return;
public void BeginFrame(float deltaSeconds) => _controller.Update(deltaSeconds);
Hexa.NET.ImGui.ImGui.CreateContext();
Hexa.NET.ImGui.ImGui.StyleColorsDark();
/// <summary>
/// Finalise the ImGui frame and draw to the framebuffer. Call AFTER all
/// panel draws, within the same frame as <see cref="BeginFrame"/>. The
/// OpenGL3 backend save/restores the GL state it touches (shader, VAO,
/// texture, blend, scissor); state not in its save-list (e.g.
/// <c>GL_FRAMEBUFFER_SRGB</c>) is caller's responsibility.
/// </summary>
public void Render() => _controller.Render();
var io = Hexa.NET.ImGui.ImGui.GetIO();
io.ConfigFlags |= ImGuiConfigFlags.NavEnableKeyboard;
// DO NOT enable NavEnableGamepad — we don't wire a gamepad backend.
// DO NOT enable DockingEnable / ViewportsEnable — out of scope for D.2a.
ImGuiImplOpenGL3.Init(glslVersion);
_initialized = true;
}
/// <summary>Tear down the OpenGL3 renderer + destroy the ImGui context.</summary>
public static void Shutdown()
{
if (!_initialized) return;
ImGuiImplOpenGL3.Shutdown();
Hexa.NET.ImGui.ImGui.DestroyContext();
_initialized = false;
}
/// <summary>True after <see cref="Initialize"/> has run successfully.</summary>
public static bool IsInitialized => _initialized;
public void Dispose() => _controller.Dispose();
}