diff --git a/.gitignore b/.gitignore
index 1731f2e..904fdf9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,7 @@ references/
# Claude Code session state
.claude/
launch.log
+launch-*.log
+
+# ImGui auto-saved window/docking state (per-user, not source)
+imgui.ini
diff --git a/CLAUDE.md b/CLAUDE.md
index 84c1d80..96449ed 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -25,13 +25,14 @@ The codebase is organized by layer (see architecture doc). Current phase
state lives in memory (`memory/project_*.md`), plans in `docs/plans/`,
research in `docs/research/`.
-**UI strategy:** three-layer split — swappable backend (Hexa.NET.ImGui for
-Phase D.2a short-term, custom retail-look toolkit for D.2b later) /
-stable `AcDream.UI.Abstractions` layer (ViewModels + Commands + `IPanel`
-/ `IPanelRenderer`) / unchanged game state. **All plugin-facing UI
-targets `AcDream.UI.Abstractions` — never import a backend namespace
-from a panel.** Full design: `docs/plans/2026-04-24-ui-framework.md`.
-Memory crib: `memory/project_ui_architecture.md`.
+**UI strategy:** three-layer split — swappable backend (ImGui.NET +
+`Silk.NET.OpenGL.Extensions.ImGui` for Phase D.2a short-term, custom
+retail-look toolkit for D.2b later) / stable `AcDream.UI.Abstractions`
+layer (ViewModels + Commands + `IPanel` / `IPanelRenderer`) / unchanged
+game state. **All plugin-facing UI targets `AcDream.UI.Abstractions` —
+never import a backend namespace from a panel.** Full design:
+`docs/plans/2026-04-24-ui-framework.md`. Memory crib:
+`memory/project_ui_architecture.md`.
## How to operate
diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md
index e5f7f85..80536a1 100644
--- a/docs/architecture/acdream-architecture.md
+++ b/docs/architecture/acdream-architecture.md
@@ -74,7 +74,8 @@ designed 2026-04-24. Full design: `docs/plans/2026-04-24-ui-framework.md`.
```
┌─────────────────────────────────────────────────────────────┐
│ UI BACKEND (swappable) │
-│ Hexa.NET.ImGui (Phase D.2a, short-term) │
+│ ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui │
+│ (Phase D.2a, short-term) │
│ or custom retail-look toolkit (Phase D.2b, later) │
├─────────────────────────────────────────────────────────────┤
│ AcDream.UI.Abstractions (stable contract) │
diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md
index 1559590..aabe568 100644
--- a/docs/plans/2026-04-11-roadmap.md
+++ b/docs/plans/2026-04-11-roadmap.md
@@ -46,6 +46,7 @@
| G.1+ | Full sky visuals + weather + dynamic-light shader — SkyDescLoader parses Region 0x13000000 dat keyframes with retail fog fields (start/end/mode); WeatherSystem picks Clear/Overcast/Rain/Snow/Storm deterministically per in-game day with 10s fade; SkyRenderer draws far-plane-1e6 celestial meshes with UV scroll; SceneLightingUbo binds at std140 location=1 with 8 Light slots + fog + lightning flash; terrain.vert + mesh.frag + mesh_instanced.frag + sky.frag all consume the shared UBO; LightingHookSink auto-registers Setup.Lights per entity + flips IsLit on SetLightHook; ParticleRenderer renders rain/snow billboards; F7 cycles day time override, F10 cycles weather; WorldSession surfaces server time via ServerTimeUpdated (ConnectRequest + TimeSync flag) | Tests ✓ |
| H.1 | Chat wire layer — Talk (0x0015) / Tell (0x005D) / ChatChannel (0x0147) outbound, HearSpeech (0x02BB local + 0x02BC ranged) inbound, ChatLog ring buffer with adapters for every chat source | Tests ✓ |
| Glue | GameEventWiring.WireAll — single-call registration mapping parsed GameEvents → Core state classes (ChatLog, CombatState, Spellbook, ItemRepository); GameWindow exposes state classes + wires them to live session | Tests ✓ |
+| D.2a | UI scaffold — `AcDream.UI.Abstractions` stable contract (`IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + `VitalsVM` / `VitalsPanel`); `AcDream.UI.ImGui` backend on ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` (pivoted from Hexa.NET.ImGui on 2026-04-25 — Hexa's native OpenGL3 backend resolves GL via GLFW/SDL and crashed 0xC0000005 without them); VitalsPanel wired into GameWindow behind `ACDREAM_DEVTOOLS=1` with `ImGui.WantCaptureKeyboard` WASD suppression. 11 new tests. | Live ✓ |
Plus polish that doesn't get its own phase number:
- FlyCamera default speed lowered + Shift-to-boost
@@ -126,9 +127,11 @@ Plus polish that doesn't get its own phase number:
> [`docs/plans/2026-04-24-ui-framework.md`](2026-04-24-ui-framework.md)
> for the full design. Short version:
>
-> 1. **D.2a — Hexa.NET.ImGui as the short-term backend.** Wire up in days,
+> 1. **D.2a — ImGui as the short-term backend.** Wire up in days,
> iterate game logic (chat-send, inventory actions, vitals HUD reading
-> real state) in weeks. Looks like a debugger; that's fine.
+> real state) in weeks. Looks like a debugger; that's fine. *(Shipped
+> 2026-04-25 on ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` after a
+> day-one pivot away from Hexa.NET.ImGui; see shipped table.)*
> 2. **Stable `AcDream.UI.Abstractions` layer** — ViewModels + Commands +
> `IPanel` / `IPanelRenderer` interfaces. Backend-agnostic. Plugin API
> publishes against this layer and never sees ImGui.
@@ -142,7 +145,7 @@ Plus polish that doesn't get its own phase number:
**Sub-pieces:**
- **D.1 — 2D ortho overlay + font rendering.** ✅ SHIPPED 2026-04-17 as the dev-facing debug overlay (StbTrueTypeSharp system-font atlas + `TextRenderer` + `DebugOverlay`).
-- **D.2a — Hexa.NET.ImGui scaffold + `AcDream.UI.Abstractions` layer.** NEW pre-piece introduced 2026-04-24. Wires Hexa.NET.ImGui as the short-term backend behind `ACDREAM_DEVTOOLS=1`. Defines `IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + the first ViewModels (`VitalsVM` etc.) in the new `AcDream.UI.Abstractions` project. First real panel: `VitalsPanel` reading HP/stam/mana from `IGameState`. This is what gets game-logic iteration moving; looks like a debugger, acceptable.
+- **✓ SHIPPED — D.2a — ImGui scaffold + `AcDream.UI.Abstractions` layer.** Shipped 2026-04-25. Wires ImGui as the short-term backend behind `ACDREAM_DEVTOOLS=1`. Defines `IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + the first ViewModel (`VitalsVM`) in the new `AcDream.UI.Abstractions` project. First real panel: `VitalsPanel` reading HP from `CombatState.GetHealthPercent`. **Backend pivoted Hexa.NET.ImGui → ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` during integration** — Hexa's native OpenGL3 backend does its own GL function resolution via GLFW/SDL and crashed with `0xC0000005` in `ImGuiImplOpenGL3.InitNative` against Silk.NET (no GLFW/SDL present). The Silk.NET extension is purpose-built for this scenario and is the `ImGui.NET` mitigation path that `docs/plans/2026-04-24-ui-framework.md` already called out as a "one-morning operation". Stam/Mana return `float?` null in D.2a because absolute values need `LocalPlayerState` + `PlayerDescription (0x0013)` parsing (filed post-D.2a). 11 new `AcDream.UI.Abstractions.Tests` green.
- **D.2b — Custom retail-look backend.** Implements the same `IPanel` / `IPanelRenderer` contracts using a custom retained-mode toolkit sourced from retail dat assets. Requires D.2a shipped. Panels get reskinned one at a time; ImGui stays as the `ACDREAM_DEVTOOLS=1` overlay forever. The original 2026-04-17 scaffold research (`UiRoot` / `UiElement` / `UiPanel` / `UiHost` + retail event codes + focus / drag-drop state machine + `WorldMouseFallThrough`) is the implementation foundation here — see `docs/research/retail-ui/`.
- **D.3 — AcFont from portal.dat.** Replace stb_truetype system font with retail `Font` DBObjs (`0x40000000..0x40000FFF`) baked from `RenderSurface` source sheets — see research slice 03 §4. Preserves retail visual identity. **(D.2b dependency — needs the custom renderer.)**
- **D.4 — Dat sprites + 9-slice panel backgrounds.** Load `RenderSurface` (`0x06xxxxxx`) as GL textures; add `DrawSprite` to `UiRenderContext`. Enables retail panel art. **(D.2b dependency.)**
diff --git a/docs/plans/2026-04-24-ui-framework.md b/docs/plans/2026-04-24-ui-framework.md
index 85d8a41..e6fce26 100644
--- a/docs/plans/2026-04-24-ui-framework.md
+++ b/docs/plans/2026-04-24-ui-framework.md
@@ -1,13 +1,51 @@
# UI framework plan
-**Date:** 2026-04-24
-**Status:** design — not yet implemented
+**Date:** 2026-04-24 (design), shipped 2026-04-25
+**Status:** **Phase D.2a shipped** — `AcDream.UI.Abstractions` + ImGui backend
++ `VitalsPanel` gated on `ACDREAM_DEVTOOLS=1`. Backend pivoted from
+`Hexa.NET.ImGui` to `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui` during
+first-light integration — see the pivot note below. Phase D.2b (custom
+retail-look backend) remains design-only.
**Owner:** lead engineer (erik) + Claude
Captures the UI strategy agreed via discussion on 2026-04-24. Documents
the choices AND the alternatives considered so future sessions can
re-evaluate with the same context.
+## 2026-04-25 pivot: Hexa.NET.ImGui → ImGui.NET
+
+The original choice (documented below) was `Hexa.NET.ImGui` +
+`Hexa.NET.ImGui.Backends.OpenGL3`. It did not survive first-light
+integration:
+
+- First launch with Hexa's backend crashed with `0xC0000005` inside
+ `Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative`.
+- Root cause: Hexa's native OpenGL3 backend does its own GL function
+ resolution, looking up symbols via GLFW or SDL. Silk.NET uses neither,
+ so the resolved function pointers were null and the native code
+ dereferenced them on init.
+- Hexa's Silk.NET examples rely on GLFW being co-loaded (its default
+ on Hexa's own scenes) — not applicable here.
+
+**Mitigation path** was already written into this doc (§"What we give
+up": *"switching to ImGui.NET later is a one-morning operation if Hexa
+misbehaves"*) and taken:
+
+- Packages swapped → `ImGui.NET 1.91.6.1` + `Silk.NET.OpenGL.Extensions.ImGui 2.23.0`.
+- `Silk.NET.OpenGL.Extensions.ImGui.ImGuiController` handles the whole
+ integration (GL backend init against the Silk.NET GL binding,
+ keyboard + mouse IO event subscription). No hand-written input
+ bridge needed.
+- `ImGuiBootstrapper` is a ~10-line `IDisposable` wrapping the
+ `ImGuiController` instance. `ImGuiPanelRenderer` wraps `ImGuiNET.ImGui.*`.
+- Boundary discipline preserved — panels never import `ImGuiNET`
+ directly; they only use `IPanelRenderer`. The backend swap is
+ invisible above the abstraction layer, as designed.
+
+Sections below from §"Choice: Hexa.NET.ImGui" onward are kept as the
+historical design reasoning. They remain useful if we ever re-evaluate
+native AOT / upstream-tracking tradeoffs.
+
## Goal
acdream needs a playable game UI: chat, vitals HUD, inventory, character
diff --git a/docs/research/retail-ui/00-master-synthesis.md b/docs/research/retail-ui/00-master-synthesis.md
index 7882a9c..0dbbf77 100644
--- a/docs/research/retail-ui/00-master-synthesis.md
+++ b/docs/research/retail-ui/00-master-synthesis.md
@@ -1,10 +1,12 @@
# Retail AC Client GUI — Master Synthesis
-> **Scope note (2026-04-24):** This document describes retail's Keystone
-> UI toolkit — it is the research foundation for **Phase D.2b (custom
-> retail-look backend)**, not **Phase D.2a (Hexa.NET.ImGui scaffold)**.
-> When reading this for implementation guidance, assume D.2a has shipped
-> a working `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`,
+> **Scope note (2026-04-24, updated 2026-04-25):** This document
+> describes retail's Keystone UI toolkit — it is the research foundation
+> for **Phase D.2b (custom retail-look backend)**, not Phase D.2a
+> (shipped ImGui scaffold, `AcDream.UI.Abstractions` + ImGui.NET +
+> `Silk.NET.OpenGL.Extensions.ImGui` + `VitalsPanel`). When reading this
+> for implementation guidance, assume D.2a has shipped a working
+> `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`,
> ViewModels, Commands) and you are building the custom retained-mode
> toolkit that implements the same contracts using dat-sourced fonts /
> sprites / cursors. See `docs/plans/2026-04-24-ui-framework.md` for the
diff --git a/memory/project_ui_architecture.md b/memory/project_ui_architecture.md
index 2d6240d..bf5e4ad 100644
--- a/memory/project_ui_architecture.md
+++ b/memory/project_ui_architecture.md
@@ -15,10 +15,14 @@
└─────────────────────────────────────────┘
```
-- **UI backend** (bottom swap axis): `Hexa.NET.ImGui` for Phase D.2a
+- **UI backend** (bottom swap axis): **`ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui`** for Phase D.2a
(short-term, debugger-look, validates game logic fast). Custom
retail-look toolkit for Phase D.2b (long-term, uses dat assets).
ImGui stays **forever** as the `ACDREAM_DEVTOOLS=1` overlay.
+ (Pivoted from `Hexa.NET.ImGui` on 2026-04-25 — Hexa's native OpenGL3
+ backend resolves GL via GLFW/SDL internally and crashed `0xC0000005`
+ against Silk.NET; the Silk.NET extension is purpose-built for this
+ stack.)
- **ViewModels + Commands** (the stable contract): per-panel data
records (`VitalsVM`, `InventoryVM`, `ChatVM`, …) and action records
(`UseItemCmd`, `SendChatCmd`, `CastSpellCmd`, …). Lives in
@@ -32,16 +36,19 @@
- `src/AcDream.UI.Abstractions/` — `IPanel`, `IPanelHost`,
`IPanelRenderer`, `ICommandBus`, all ViewModels + Commands. Backend-
agnostic.
-- `src/AcDream.UI.ImGui/` — Hexa.NET.ImGui-based implementation of
- `IPanelRenderer` + ImGui bootstrap. Phase D.2a.
+- `src/AcDream.UI.ImGui/` — `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui`
+ implementation of `IPanelRenderer` + ImGui bootstrap. Phase D.2a.
+ `ImGuiController` (from the Silk.NET extension) handles GL backend +
+ input event subscription; `ImGuiBootstrapper` is a thin IDisposable
+ wrapper; `ImGuiPanelRenderer` wraps the widgets `IPanelRenderer` needs.
- `src/AcDream.UI.Retail/` (later) — custom retained-mode toolkit using
dat assets, same `IPanelRenderer` contract. Phase D.2b.
## Hard rules
1. **No panel references a backend namespace.** If a panel imports
- `Hexa.NET.ImGui` or a custom-toolkit widget class directly, it's a
- bug.
+ `ImGuiNET` / `Silk.NET.OpenGL.Extensions.ImGui` or a custom-toolkit
+ widget class directly, it's a bug — extend `IPanelRenderer` instead.
2. **Plugin API targets the abstraction layer only.** Plugins define
`IPanel` instances; they never see which backend draws them.
3. **Features that only ImGui can express → not in `IPanelRenderer`.**
diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj
index 277ae99..6fb3af0 100644
--- a/src/AcDream.App/AcDream.App.csproj
+++ b/src/AcDream.App/AcDream.App.csproj
@@ -24,6 +24,8 @@
+
+
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index cbd4b27..ab4479f 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -281,6 +281,14 @@ public sealed class GameWindow : IDisposable
public readonly AcDream.Core.Spells.Spellbook SpellBook = new();
public readonly AcDream.Core.Items.ItemRepository Items = new();
+ // Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1.
+ // See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
+ private AcDream.UI.ImGui.ImGuiBootstrapper? _imguiBootstrap;
+ private AcDream.UI.ImGui.ImGuiPanelHost? _panelHost;
+ private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
+ private static readonly bool DevToolsEnabled =
+ Environment.GetEnvironmentVariable("ACDREAM_DEVTOOLS") == "1";
+
// Phase G.1-G.2 world lighting/time state.
public readonly AcDream.Core.World.WorldTimeService WorldTime =
new AcDream.Core.World.WorldTimeService(
@@ -870,6 +878,35 @@ public sealed class GameWindow : IDisposable
}
}
+ // Phase D.2a — ImGui devtools overlay. Zero cost when the env var
+ // isn't set: no context creation, no per-frame branches hit.
+ // See docs/plans/2026-04-24-ui-framework.md + memory/project_ui_architecture.md.
+ if (DevToolsEnabled)
+ {
+ try
+ {
+ _imguiBootstrap = new AcDream.UI.ImGui.ImGuiBootstrapper(_gl!, _window!, _input!);
+ _panelHost = new AcDream.UI.ImGui.ImGuiPanelHost();
+
+ // VitalsVM: GUID=0 at construction; set later at EnterWorld
+ // (see the _playerServerGuid assignment path). Pre-login the
+ // HP bar just reads 1.0 (safe default) — harmless.
+ _vitalsVm = new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat);
+ _panelHost.Register(
+ new AcDream.UI.Abstractions.Panels.Vitals.VitalsPanel(_vitalsVm));
+
+ Console.WriteLine("devtools: ImGui panel host ready (VitalsPanel registered)");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"devtools: ImGui init failed: {ex.Message} — devtools disabled");
+ _imguiBootstrap?.Dispose();
+ _imguiBootstrap = null;
+ _panelHost = null;
+ _vitalsVm = null;
+ }
+ }
+
uint centerLandblockId = 0xA9B4FFFFu;
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
@@ -1149,6 +1186,7 @@ public sealed class GameWindow : IDisposable
var chosen = _liveSession.Characters.Characters[0];
_playerServerGuid = chosen.Id; // Phase B.2: store for Tab-key player-mode entry
+ _vitalsVm?.SetLocalPlayerGuid(chosen.Id); // Phase D.2a — devtools HP bar tracks this guid
_worldState.MarkPersistent(chosen.Id); // player entity survives landblock unloads
Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}");
_liveSession.EnterWorld(user, characterIndex: 0);
@@ -3502,6 +3540,13 @@ public sealed class GameWindow : IDisposable
var kb = _input.Keyboards[0];
+ // Phase D.2a — suppress game-side WASD / interaction polling when
+ // ImGui has keyboard focus (e.g. a text field is active). Without
+ // this, typing "walk" into a chat field would actually walk.
+ bool suppressGameInput =
+ DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard;
+ if (suppressGameInput) return;
+
if (_cameraController.IsFlyMode)
{
_cameraController.Fly.Update(
@@ -3709,6 +3754,12 @@ public sealed class GameWindow : IDisposable
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+ // Phase D.2a — begin ImGui frame. Paired with the Render() call
+ // after the scene draws (below). ImGuiController.Update()
+ // consumes buffered Silk.NET input events and calls ImGui.NewFrame.
+ if (DevToolsEnabled && _imguiBootstrap is not null)
+ _imguiBootstrap.BeginFrame((float)deltaSeconds);
+
// Phase 6.4: advance per-entity animation playback before drawing
// so the renderer always sees the up-to-date per-part transforms.
if (_animatedEntities.Count > 0)
@@ -4002,6 +4053,20 @@ public sealed class GameWindow : IDisposable
}
}
+ // Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws
+ // so ImGui composites on top. ImGuiController save/restores the
+ // GL state it touches (blend, scissor, VAO, shader, texture); any
+ // state not in its save-list (e.g. GL_FRAMEBUFFER_SRGB, unused
+ // today) would need manual protection.
+ if (DevToolsEnabled && _imguiBootstrap is not null && _panelHost is not null)
+ {
+ var ctx = new AcDream.UI.Abstractions.PanelContext(
+ (float)deltaSeconds,
+ AcDream.UI.Abstractions.NullCommandBus.Instance);
+ _panelHost.RenderAll(ctx);
+ _imguiBootstrap.Render();
+ }
+
// Update the window title with performance stats every ~0.5s.
_perfAccum += deltaSeconds;
_perfFrameCount++;
diff --git a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
index 65853fa..66f5bba 100644
--- a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
+++ b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
@@ -7,15 +7,20 @@
true
-
-
-
-
+
+
+
+
diff --git a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
index 70ba689..bbcc3a8 100644
--- a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
+++ b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
@@ -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;
///
-/// One-shot ImGui setup / teardown for the devtools overlay. Called from
-/// GameWindow when ACDREAM_DEVTOOLS=1. Hides the cimgui
-/// context + OpenGL3 renderer-impl lifecycles behind two static methods
-/// so the calling code stays clean.
+/// Owns the ImGuiController from Silk.NET.OpenGL.Extensions.ImGui,
+/// which handles the whole Silk.NET ↔ ImGui.NET integration:
+///
+/// - Creates the ImGui context + OpenGL3 backend using Silk.NET's GL binding
+/// (no GLFW / SDL dependency — unlike Hexa.NET.ImGui, which assumed one).
+/// - Subscribes to Silk.NET's window + input events to drive IO.
+/// - Per frame: Update(dt) calls ImGui.NewFrame(); Render()
+/// calls ImGui.Render() + uploads draw data via its OpenGL3 backend.
+///
///
///
-/// Intentionally not an IDisposable singleton — the host
-/// window owns the one call to at application
-/// exit. Re-initialisation mid-session is not supported.
+/// Instance-scoped rather than static so GL-context lifetime is explicit.
+/// GameWindow owns the one instance and disposes on shutdown.
+///
+///
+///
+/// History: tried Hexa.NET.ImGui + Hexa.NET.ImGui.Backends.OpenGL3 first
+/// per the original plan, but its native OpenGL3 backend resolves GL functions
+/// via GLFW / SDL internally and crashed (0xC0000005) in InitNative without
+/// one of those present. Pivoted to the official Silk.NET extension on 2026-04-25.
///
///
-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);
+ }
///
- /// 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 ImGui.* widget calls.
+ /// Internally: consumes buffered input events, calls ImGui.NewFrame().
///
- ///
- /// GLSL version directive for the ImGui-internal shader.
- /// "#version 330" matches acdream's existing shaders and is
- /// the safest default for the OpenGL 4.3 core profile we ship.
- ///
- 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();
+ ///
+ /// Finalise the ImGui frame and draw to the framebuffer. Call AFTER all
+ /// panel draws, within the same frame as . The
+ /// OpenGL3 backend save/restores the GL state it touches (shader, VAO,
+ /// texture, blend, scissor); state not in its save-list (e.g.
+ /// GL_FRAMEBUFFER_SRGB) is caller's responsibility.
+ ///
+ 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;
- }
-
- /// Tear down the OpenGL3 renderer + destroy the ImGui context.
- public static void Shutdown()
- {
- if (!_initialized) return;
-
- ImGuiImplOpenGL3.Shutdown();
- Hexa.NET.ImGui.ImGui.DestroyContext();
-
- _initialized = false;
- }
-
- /// True after has run successfully.
- public static bool IsInitialized => _initialized;
+ public void Dispose() => _controller.Dispose();
}
diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
index 17e463d..d1fde2f 100644
--- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
+++ b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
@@ -1,31 +1,32 @@
using System.Numerics;
using AcDream.UI.Abstractions;
+using ImGuiNET;
namespace AcDream.UI.ImGui;
///
/// implemented as thin wrappers around
-/// Hexa.NET.ImGui calls. This is the ONLY place where Hexa.NET.ImGui
-/// types appear outside of bootstrap / input-bridge plumbing — panels
-/// that need a feature must extend the abstraction here, not by importing
-/// ImGui in panel files.
+/// ImGui.NET calls. This is the ONLY place where ImGuiNET types appear
+/// outside of bootstrap plumbing — panels that need a feature must
+/// extend the abstraction here, not by importing ImGuiNET in panel
+/// files.
///
public sealed class ImGuiPanelRenderer : IPanelRenderer
{
///
- public bool Begin(string title) => Hexa.NET.ImGui.ImGui.Begin(title);
+ public bool Begin(string title) => ImGuiNET.ImGui.Begin(title);
///
- public void End() => Hexa.NET.ImGui.ImGui.End();
+ public void End() => ImGuiNET.ImGui.End();
///
- public void Text(string text) => Hexa.NET.ImGui.ImGui.TextUnformatted(text);
+ public void Text(string text) => ImGuiNET.ImGui.TextUnformatted(text);
///
- public void SameLine() => Hexa.NET.ImGui.ImGui.SameLine();
+ public void SameLine() => ImGuiNET.ImGui.SameLine();
///
- public void Separator() => Hexa.NET.ImGui.ImGui.Separator();
+ public void Separator() => ImGuiNET.ImGui.Separator();
///
public void ProgressBar(float fraction, float width, string? overlay = null)
@@ -36,6 +37,6 @@ public sealed class ImGuiPanelRenderer : IPanelRenderer
else if (fraction > 1f) fraction = 1f;
var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font
- Hexa.NET.ImGui.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty);
+ ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty);
}
}
diff --git a/src/AcDream.UI.ImGui/SilkInputBridge.cs b/src/AcDream.UI.ImGui/SilkInputBridge.cs
deleted file mode 100644
index 6c47f28..0000000
--- a/src/AcDream.UI.ImGui/SilkInputBridge.cs
+++ /dev/null
@@ -1,188 +0,0 @@
-using System.Numerics;
-using Hexa.NET.ImGui;
-using Silk.NET.Input;
-
-namespace AcDream.UI.ImGui;
-
-///
-/// Forwards Silk.NET keyboard / mouse events to ImGui's IO. Replaces what
-/// you'd get from the stock GLFW or SDL backends in a non-Silk.NET host.
-///
-///
-/// Event-driven (we subscribe to Silk.NET events); does not poll. Each
-/// handler writes directly to ImGui.GetIO() via the AddXxx
-/// family of calls. Frame-start book-keeping (display size, delta time,
-/// active modifier latch) happens in .
-///
-///
-///
-/// Call at app shutdown to unsubscribe from Silk.NET
-/// events.
-///
-///
-public sealed class SilkInputBridge : IDisposable
-{
- private readonly IInputContext _input;
- private readonly IKeyboard? _keyboard;
- private readonly IMouse? _mouse;
-
- public SilkInputBridge(IInputContext input)
- {
- _input = input ?? throw new ArgumentNullException(nameof(input));
-
- _keyboard = input.Keyboards.Count > 0 ? input.Keyboards[0] : null;
- _mouse = input.Mice.Count > 0 ? input.Mice[0] : null;
-
- if (_keyboard is not null)
- {
- _keyboard.KeyDown += OnKeyDown;
- _keyboard.KeyUp += OnKeyUp;
- _keyboard.KeyChar += OnKeyChar;
- }
-
- if (_mouse is not null)
- {
- _mouse.MouseMove += OnMouseMove;
- _mouse.MouseDown += OnMouseDown;
- _mouse.MouseUp += OnMouseUp;
- _mouse.Scroll += OnScroll;
- }
- }
-
- ///
- /// Per-frame bookkeeping. Call right before ImGui.NewFrame().
- /// Sets display size (in logical pixels) and delta-time on ImGui's IO.
- ///
- public void BeginFrame(Vector2 displaySize, float deltaSeconds)
- {
- var io = Hexa.NET.ImGui.ImGui.GetIO();
- io.DisplaySize = displaySize;
- io.DeltaTime = deltaSeconds > 0f ? deltaSeconds : 1f / 60f;
- }
-
- // ─── event handlers ──────────────────────────────────────────────
-
- private void OnKeyDown(IKeyboard kb, Key key, int scancode) => AddKey(key, down: true);
- private void OnKeyUp (IKeyboard kb, Key key, int scancode) => AddKey(key, down: false);
-
- private void OnKeyChar(IKeyboard kb, char c)
- {
- // Feeds typed text into any focused ImGui TextField. Safe to call
- // even when no TextField has focus — ImGui buffers the character
- // and discards it if nothing claims it.
- Hexa.NET.ImGui.ImGui.GetIO().AddInputCharacter(c);
- }
-
- private void OnMouseMove(IMouse m, Vector2 pos)
- {
- Hexa.NET.ImGui.ImGui.GetIO().AddMousePosEvent(pos.X, pos.Y);
- }
-
- private void OnMouseDown(IMouse m, MouseButton button) => AddMouseButton(button, down: true);
- private void OnMouseUp (IMouse m, MouseButton button) => AddMouseButton(button, down: false);
-
- private void OnScroll(IMouse m, ScrollWheel wheel)
- {
- Hexa.NET.ImGui.ImGui.GetIO().AddMouseWheelEvent(wheel.X, wheel.Y);
- }
-
- // ─── helpers ─────────────────────────────────────────────────────
-
- private static void AddKey(Key key, bool down)
- {
- // Update modifier latches first (ImGui reads these when any AddKeyEvent fires).
- var io = Hexa.NET.ImGui.ImGui.GetIO();
- if (key is Key.ControlLeft or Key.ControlRight) io.AddKeyEvent(ImGuiKey.ModCtrl, down);
- if (key is Key.ShiftLeft or Key.ShiftRight) io.AddKeyEvent(ImGuiKey.ModShift, down);
- if (key is Key.AltLeft or Key.AltRight) io.AddKeyEvent(ImGuiKey.ModAlt, down);
- if (key is Key.SuperLeft or Key.SuperRight) io.AddKeyEvent(ImGuiKey.ModSuper, down);
-
- if (KeyMap.TryGetValue(key, out var imguiKey))
- io.AddKeyEvent(imguiKey, down);
- // Unmapped keys are silently ignored — fine for D.2a; panels that
- // need exotic keys can extend the map.
- }
-
- private static void AddMouseButton(MouseButton button, bool down)
- {
- int idx = button switch
- {
- MouseButton.Left => 0,
- MouseButton.Right => 1,
- MouseButton.Middle => 2,
- _ => -1,
- };
- if (idx < 0) return;
- Hexa.NET.ImGui.ImGui.GetIO().AddMouseButtonEvent(idx, down);
- }
-
- ///
- /// Silk.NET → ImGui key map. Covers text-input + navigation keys +
- /// WASD + function keys. Unlisted keys fall through to no-op.
- ///
- private static readonly Dictionary KeyMap = new()
- {
- // Navigation + control
- [Key.Tab] = ImGuiKey.Tab,
- [Key.Left] = ImGuiKey.LeftArrow,
- [Key.Right] = ImGuiKey.RightArrow,
- [Key.Up] = ImGuiKey.UpArrow,
- [Key.Down] = ImGuiKey.DownArrow,
- [Key.PageUp] = ImGuiKey.PageUp,
- [Key.PageDown] = ImGuiKey.PageDown,
- [Key.Home] = ImGuiKey.Home,
- [Key.End] = ImGuiKey.End,
- [Key.Insert] = ImGuiKey.Insert,
- [Key.Delete] = ImGuiKey.Delete,
- [Key.Backspace] = ImGuiKey.Backspace,
- [Key.Space] = ImGuiKey.Space,
- [Key.Enter] = ImGuiKey.Enter,
- [Key.Escape] = ImGuiKey.Escape,
-
- // Modifiers (also add via the mod-flag path, but these let ImGui
- // see them as named keys too).
- [Key.ControlLeft] = ImGuiKey.LeftCtrl,
- [Key.ControlRight] = ImGuiKey.RightCtrl,
- [Key.ShiftLeft] = ImGuiKey.LeftShift,
- [Key.ShiftRight] = ImGuiKey.RightShift,
- [Key.AltLeft] = ImGuiKey.LeftAlt,
- [Key.AltRight] = ImGuiKey.RightAlt,
-
- // Letters (Silk.NET.Key.A..Z map 1:1 to ImGuiKey.A..Z).
- [Key.A] = ImGuiKey.A, [Key.B] = ImGuiKey.B, [Key.C] = ImGuiKey.C, [Key.D] = ImGuiKey.D,
- [Key.E] = ImGuiKey.E, [Key.F] = ImGuiKey.F, [Key.G] = ImGuiKey.G, [Key.H] = ImGuiKey.H,
- [Key.I] = ImGuiKey.I, [Key.J] = ImGuiKey.J, [Key.K] = ImGuiKey.K, [Key.L] = ImGuiKey.L,
- [Key.M] = ImGuiKey.M, [Key.N] = ImGuiKey.N, [Key.O] = ImGuiKey.O, [Key.P] = ImGuiKey.P,
- [Key.Q] = ImGuiKey.Q, [Key.R] = ImGuiKey.R, [Key.S] = ImGuiKey.S, [Key.T] = ImGuiKey.T,
- [Key.U] = ImGuiKey.U, [Key.V] = ImGuiKey.V, [Key.W] = ImGuiKey.W, [Key.X] = ImGuiKey.X,
- [Key.Y] = ImGuiKey.Y, [Key.Z] = ImGuiKey.Z,
-
- // Digit row
- [Key.Number0] = ImGuiKey.Key0, [Key.Number1] = ImGuiKey.Key1, [Key.Number2] = ImGuiKey.Key2,
- [Key.Number3] = ImGuiKey.Key3, [Key.Number4] = ImGuiKey.Key4, [Key.Number5] = ImGuiKey.Key5,
- [Key.Number6] = ImGuiKey.Key6, [Key.Number7] = ImGuiKey.Key7, [Key.Number8] = ImGuiKey.Key8,
- [Key.Number9] = ImGuiKey.Key9,
-
- // Function keys
- [Key.F1] = ImGuiKey.F1, [Key.F2] = ImGuiKey.F2, [Key.F3] = ImGuiKey.F3, [Key.F4] = ImGuiKey.F4,
- [Key.F5] = ImGuiKey.F5, [Key.F6] = ImGuiKey.F6, [Key.F7] = ImGuiKey.F7, [Key.F8] = ImGuiKey.F8,
- [Key.F9] = ImGuiKey.F9, [Key.F10] = ImGuiKey.F10, [Key.F11] = ImGuiKey.F11, [Key.F12] = ImGuiKey.F12,
- };
-
- public void Dispose()
- {
- if (_keyboard is not null)
- {
- _keyboard.KeyDown -= OnKeyDown;
- _keyboard.KeyUp -= OnKeyUp;
- _keyboard.KeyChar -= OnKeyChar;
- }
- if (_mouse is not null)
- {
- _mouse.MouseMove -= OnMouseMove;
- _mouse.MouseDown -= OnMouseDown;
- _mouse.MouseUp -= OnMouseUp;
- _mouse.Scroll -= OnScroll;
- }
- }
-}