ADR-0001: fx-gui cross-platform backend — SDL3 + OpenGL 3.3 + miniaudio¶
Status: Accepted (2026-07-02) Serves: epic #46 (#85 is this record); informs fighters-legacy#156
Context¶
fx-gui is the interactive validation surface for the format research, but it is
Windows-only: a Win32 message pump and DX11 swapchain host
(gui/src/main.cpp),
waveOut audio preview, Win32 common dialogs, and registry-based dark/light theming.
Primary development moved to Fedora (see development.md), so the
GUI cannot be exercised against the local FA install where the rest of the toolkit
lives. Phase 3 of the roadmap requires parity on Fedora and Windows,
shipping in CI and releases.
Constraints that shaped the decision:
- Dependency ethos. The toolkit builds without a package manager; runtime dependencies are vendored (Dear ImGui, stb) and the only network fetch is Catch2 for tests. A windowing library is the first dependency too large to vendor as source.
- One small 3D preview. The only bespoke GPU work is the SH mesh viewer — an offscreen render of a few hundred triangles. Whatever the backend, Dear ImGui does the rest.
- Lesson transfer. The fighters-legacy engine's platform layer is SDL3-based with
pure-virtual HAL interfaces; its upcoming
IGuiHAL (fighters-legacy#156) will host Dear ImGui behindimgui_impl_sdl3. The closer this port's seams are to that design, the more the porting experience feeds it.
Options considered¶
Windowing + rendering¶
| Option | Assessment |
|---|---|
| SDL3 + OpenGL 3.3 core (chosen) | Both ImGui backends (imgui_impl_sdl3, imgui_impl_opengl3) are mature, maintained, and version-locked to the vendored ImGui. GL 3.3 core is everywhere fx-gui needs to run, including Mesa llvmpipe for headless CI. SDL3 additionally covers file dialogs, system-theme queries and change events, display scale, and clipboard — eliminating three would-be dependencies. Matches fighters-legacy's platform layer. |
| SDL3 + SDL_GPU | Wrong altitude for this tool: adds a shader-toolchain step (SDL_shadercross/offline compile) and a younger ImGui backend for zero visual benefit on a 200-triangle preview. Revisit if fx-gui ever needs real rendering throughput. |
| SDL3 + Vulkan | Maximum boilerplate for the same preview; complicates headless CI (llvmpipe GL is simpler than lavapipe swapchains); imgui_impl_vulkan integration effort belongs in fighters-legacy where Vulkan is the actual renderer. |
| GLFW + OpenGL 3.3 | Lighter than SDL3 but covers only windowing/input — dialogs, theme detection, and DPI-change events would each need another dependency or per-OS code, and nothing transfers to fighters-legacy. |
Audio preview¶
| Option | Assessment |
|---|---|
| miniaudio (chosen) | Single vendored header (public domain / MIT-0), fits the stb-style convention. Loads platform audio backends (WASAPI, ALSA/PulseAudio/PipeWire) at runtime via dlopen, so it adds zero link-time dependencies and keeps artifacts self-contained. Keeps audio decoupled from windowing — mirroring fighters-legacy's separate IAudio HAL. Its null backend enables hardware-free unit tests of the player state machine. |
| SDL3 audio | No extra dependency, but couples audio preview to the windowing library and diverges from fighters-legacy's audio split. |
| OpenAL Soft | A 3D-audio API for one-shot mono PCM preview is overkill, and it is an external library to acquire on both OSes. |
SDL3 acquisition¶
| Option | Assessment |
|---|---|
| System-first + pinned fallback (chosen) | find_package(SDL3 CONFIG) first — Fedora ships SDL3-devel, so local builds use the system package with zero configure-time network. Where absent, FetchContent builds a pinned release tarball (URL + SHA-256), static. FX_SDL3_VENDORED=ON forces the pinned path; CI and release jobs set it so shipped artifacts never silently pick up a system SDL3. Both acquisition paths get exercised for free (Fedora developers vs. CI runners). |
| FetchContent always | Reproducible everywhere but recompiles SDL3 on every local first configure and ignores the system package the primary platform already provides. |
| Vendor SDL3 source / require system everywhere | SDL3 is far too large to vendor in-tree; system-only is impossible on GitHub's Ubuntu runners (no libsdl3-dev ≤ 24.04) and would make Linux artifacts depend on libSDL3.so. |
Decision¶
fx-gui runs on SDL3 (window, GL context, input, file dialogs, theme, DPI) with
OpenGL 3.3 core rendering through the stock ImGui backends, a vendored glad
GL loader for first-party GL code, and miniaudio for audio preview. The SH
preview is reimplemented as a GL FBO pipeline with GLSL ports of the HLSL shaders.
Initial pin: SDL 3.4.12 (hash recorded alongside the FetchContent declaration in
gui/CMakeLists.txt).
The DX11/Win32 path is removed, not maintained in parallel — Windows uses the same
SDL3/GL3 backend.
Consequences¶
- Dependency policy amendment. The "only network fetch is Catch2" rule becomes "Catch2, plus SDL3 when no system package exists" — system-first, pinned, and checksummed. miniaudio and glad are vendored under the existing policy. development.md documents the amended rule; this ADR is the authority for why.
- CI. Ubuntu runners install X11/Wayland dev headers so FetchContent can compile SDL3; the GUI enters CodeQL scope on Linux; a headless smoke test runs under Xvfb/llvmpipe. GitHub's Windows runners expose no GL 3.3, so Windows CI compile-checks the GUI and runs its display-free unit tests only.
- Artifacts stay self-contained. Static SDL3 in release builds (SDL resolves
platform libraries at runtime via
dlopen), so the Linux tarball and Windows zip remain single executables. - Wayland caveat. Window position save/restore is a no-op under Wayland (size and maximized state still restore). Accepted; documented in gui.md.
- Behavior changes. Settings move from a CWD-relative
fx-gui.inito the per-user SDL preferences path; theming followsSDL_GetSystemThemeinstead of the Windows registry; DPI scaling becomes dynamic.
Lessons for fighters-legacy#156¶
Seeded here; finalized with findings from the port itself.
- Frame lifecycle.
IGui::beginFramemust run after the platform event pump;endFrameshould emit into a render target it is handed (the frame's command buffer under Vulkan) — presentation belongs toIRenderer, neverIGui. - Texture handles. Editors that only ever touched an opaque
ImTextureIDported for free; the one leak of a backend type (AppholdingID3D11Device*) is exactly what had to be rewritten.IGui::drawImageshould take an opaque 64-bitTextureHandleminted byIRenderer. - Input arbitration.
wantsKeyboardInput()/wantsMouseInput()map toio.WantCaptureKeyboard/Mouseand are valid only afterbeginFrame— forward events to the GUI unconditionally and have the sim consult the flags per frame. - DPI. Style rebuild must be idempotent (ImGui's
ScaleAllSizesis cumulative);IGuiwants adpiChanged(scale)notification fromIWindow. - Dialogs are async. SDL3's file dialogs are callback-based and may complete on any thread — a blocking dialog API in a HAL would be unimplementable on the SDL3 backend. The workable shape: continuation queue pumped on the main thread, capture-by-value, revalidate state on arrival.
- Audio. A control-plane (main thread) / data-plane (realtime callback) split
with atomics-only crossings ports cleanly between audio APIs; track playback
position from the submission cursor, not device queries. Found empirically
during the port: device start-up prefill can consume a sub-period clip
synchronously, so
isPlaying()may legitimately be false immediately after a successful play — consumers must treat play-then-finished as a normal sequence, not an error. - Theme is a platform concern (query + change event);
IGuireceives it rather than detecting it.