Files
ObsidianDragon/src/main.cpp
T
DanSandClaude Opus 5.5 b50d5ac31f perf(startup): build the font atlas once at the final ui.toml sizes
main() built the font atlas before App::init() loaded ui.toml, so the
first build used the hard-coded fallback sizes (H1 24 px, Body2 12 px).
App::init() then rebuilt for a saved font scale above 1.0, still before
the schema load and so still at the fallback sizes. Only the color-emoji
rebuild requested for the first frame reached the ui.toml sizes, so
color-emoji users built the atlas twice at font scale 1.0 and three
times at 1.5.

Users with chat_emoji_color=false never got that last rebuild. They
built the atlas once or twice and ran the whole session at the fallback
sizes, e.g. Body2 at 12 px instead of 15 and H1 at 24 px instead of 20.

Now:
- App::loadBaseUiSchema(), the ui.toml load moved out of App::init()
  unchanged, runs in main() before both InitImGui paths (DX11 and GL),
  together with the saved font scale and emoji style from the early
  settings read. It is idempotent, so the call left in App::init() is a
  no-op;
- the two rebuild triggers in App::init() are gone;
- Typography records what the atlas was built from (the 15 schema sizes,
  the schema's font scale, the effective emoji style and the schema
  generation), and preFrame() rebuilds once when they differ. While the
  schema generation and the emoji style are unchanged this costs two
  compares per frame.

Measured on the Lite build (color and mono emoji, font scale 1.0 and
1.5, English and Japanese): every run builds the atlas exactly once, at
the ui.toml sizes times the scale (H1 20 px and Body2 15 px at 1.0,
30 px and 22 px at 1.5), and ui.toml is loaded before the window and GL
context are created. The packaged Lite zip and AppImage log one atlas
build instead of two. Color-emoji screenshot sweeps match the previous
build within noise.

Visible changes:
- users with chat_emoji_color=false had been rendering the whole UI at
  the fallback font sizes; they now get the designed ui.toml sizes, so
  every mono-emoji screen differs from before and matches the
  color-emoji layout;
- a user skin whose [fonts] differ from ui.toml now rebuilds the atlas
  once when it is activated or left; on a runtime switch the atlas used
  to keep the old sizes until a restart. With a test skin that sets
  Body2 to 18 px, startup builds twice and ends at 18 px; switching to a
  bundled skin rebuilds once, switching among the 9 bundled skins
  rebuilds nothing (they carry an identical copy of [fonts]), and
  switching back rebuilds once;
- editing ui.toml while running still gives exactly one rebuild.

Not verifiable here: the Windows DX11 path with the schema loaded before
its first FreeType build, macOS, and the interactive font-scale slider,
Alt+scroll and emoji toggle, which were checked by reading the code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 06:05:07 -05:00

2548 lines
117 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "app.h"
#include "config/settings.h"
#include "config/version.h"
#include "rpc/rpc_worker.h"
#include "ui/schema/ui_schema.h"
#include "ui/effects/low_spec.h"
#include "ui/notifications.h"
#include "ui/windows/contacts_tab.h"
#include "ui/theme.h"
#include "ui/material/color_theme.h"
#include "ui/material/typography.h"
#include "ui/material/draw_helpers.h"
#include "ui/layout.h"
#include "util/single_instance.h"
#include "util/payment_uri.h"
#include "util/perf_log.h"
#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_impl_sdl3.h"
#include "ui/effects/imgui_acrylic.h"
#include "ui/effects/theme_effects.h"
#include "util/texture_loader.h"
#include "util/platform.h"
#include "daemon/seed_wallet_creator.h"
#include "resources/embedded_resources.h"
#include <sodium.h>
// Backend-specific headers
#ifdef DRAGONX_USE_DX11
#include "imgui_impl_dx11.h"
#include "platform/dx11_context.h"
#else
#include "imgui_impl_opengl3.h"
#include <SDL3/SDL_opengl.h>
#ifdef DRAGONX_HAS_GLAD
#include <glad/gl.h>
#endif
#endif
#include <SDL3/SDL.h>
#include <map>
#ifdef _WIN32
#include "platform/windows_backdrop.h"
#include <windows.h>
#include <dwmapi.h>
#include <shlobj.h>
#include <propkey.h>
#include <propsys.h>
// SetCurrentProcessExplicitAppUserModelID lives behind NTDDI_WIN7 in
// MinGW's shobjidl.h. Rather than forcing the version macro globally,
// declare just the one function we need (available on Windows 7+).
extern "C" HRESULT __stdcall SetCurrentProcessExplicitAppUserModelID(PCWSTR AppID);
// SHGetPropertyStoreForWindow is also behind NTDDI_WIN7 in MinGW headers.
extern "C" HRESULT __stdcall SHGetPropertyStoreForWindow(HWND hwnd, REFIID riid, void** ppv);
// Not defined in older MinGW SDK headers
#ifndef WS_EX_NOREDIRECTIONBITMAP
#define WS_EX_NOREDIRECTIONBITMAP 0x00200000L
#endif
// Needed by the borderless WndProc for WM_NCHITTEST
#ifndef GET_X_LPARAM
#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
#endif
#ifndef GET_Y_LPARAM
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))
#endif
#endif
#include <algorithm>
#include <cstdio>
#include <cstdarg>
#include <cstdlib>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>
#include <chrono>
#include <miniz.h>
#include "util/logger.h"
// Read the just-rendered framebuffer and write it to a PNG (used by the debug screenshot sweep).
// GL: read FBO 0 (RGBA, bottom-up -> miniz flip). DX11: copy the backbuffer to a staging texture,
// map it (BGRA, top-down -> channel-swap, no flip). Alpha forced opaque (the window is transparent).
namespace {
inline void writePng(const std::string& path, const std::vector<uint8_t>& rgba, int w, int h, bool flip) {
size_t len = 0;
void* png = tdefl_write_image_to_png_file_in_memory_ex(rgba.data(), w, h, 4, &len, 6, flip ? MZ_TRUE : MZ_FALSE);
if (!png) return;
std::error_code ec; std::filesystem::create_directories(std::filesystem::path(path).parent_path(), ec);
std::ofstream out(path, std::ios::binary | std::ios::trunc);
if (out) out.write(reinterpret_cast<const char*>(png), static_cast<std::streamsize>(len));
mz_free(png);
}
#ifdef DRAGONX_USE_DX11
inline void captureFrameToPng(dragonx::platform::DX11Context& dx, const std::string& path) {
IDXGISwapChain1* sc = dx.swapChain(); ID3D11Device* dev = dx.device(); ID3D11DeviceContext* ctx = dx.deviceContext();
if (!sc || !dev || !ctx) return;
ID3D11Texture2D* back = nullptr;
if (FAILED(sc->GetBuffer(0, IID_PPV_ARGS(&back))) || !back) return;
D3D11_TEXTURE2D_DESC bd; back->GetDesc(&bd);
D3D11_TEXTURE2D_DESC sd = bd; sd.Usage = D3D11_USAGE_STAGING; sd.BindFlags = 0;
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; sd.MiscFlags = 0;
ID3D11Texture2D* staging = nullptr;
if (SUCCEEDED(dev->CreateTexture2D(&sd, nullptr, &staging)) && staging) {
ctx->CopyResource(staging, back);
D3D11_MAPPED_SUBRESOURCE m;
if (SUCCEEDED(ctx->Map(staging, 0, D3D11_MAP_READ, 0, &m))) {
int w = (int)bd.Width, h = (int)bd.Height;
std::vector<uint8_t> px((size_t)w * h * 4);
for (int y = 0; y < h; ++y) {
const uint8_t* s = (const uint8_t*)m.pData + (size_t)y * m.RowPitch;
uint8_t* d = px.data() + (size_t)y * w * 4;
for (int x = 0; x < w; ++x) { d[x*4+0]=s[x*4+2]; d[x*4+1]=s[x*4+1]; d[x*4+2]=s[x*4+0]; d[x*4+3]=255; }
}
ctx->Unmap(staging, 0);
writePng(path, px, w, h, /*flip*/false); // DX11 textures are top-down
}
staging->Release();
}
back->Release();
}
#else
inline void captureFrameToPng(const std::string& path, int w, int h) {
if (w <= 0 || h <= 0) return;
std::vector<uint8_t> px((size_t)w * h * 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glReadBuffer(GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
for (size_t i = 3; i < px.size(); i += 4) px[i] = 255; // opaque
writePng(path, px, w, h, /*flip*/true); // GL default framebuffer is bottom-up
}
#endif
} // namespace
#ifdef _WIN32
// Windows crash handler — writes a minidump and log entry before exit
static LONG WINAPI CrashHandler(EXCEPTION_POINTERS* ep)
{
// Write crash info to the debug log and a dedicated crash log
const char* crashMsg = "\n=== CRASH DETECTED ===";
DEBUG_LOGF("%s", crashMsg);
DEBUG_LOGF("\nException code: 0x%08lX", ep->ExceptionRecord->ExceptionCode);
DEBUG_LOGF("\nException address: %p", ep->ExceptionRecord->ExceptionAddress);
if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION &&
ep->ExceptionRecord->NumberParameters >= 2) {
DEBUG_LOGF("\nAccess type: %s, target address: %p",
ep->ExceptionRecord->ExceptionInformation[0] ? "WRITE" : "READ",
(void*)ep->ExceptionRecord->ExceptionInformation[1]);
}
DEBUG_LOGF("\n=== END CRASH INFO ===\n");
fflush(stdout);
fflush(stderr);
// Also write to a separate crash log file
try {
std::string crashPath = (std::filesystem::path(
dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-crash.log").string();
FILE* f = fopen(crashPath.c_str(), "a");
if (f) {
fprintf(f, "\n=== CRASH at PID %lu ===", (unsigned long)GetCurrentProcessId());
fprintf(f, "\nException code: 0x%08lX", ep->ExceptionRecord->ExceptionCode);
fprintf(f, "\nException address: %p", ep->ExceptionRecord->ExceptionAddress);
if (ep->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION &&
ep->ExceptionRecord->NumberParameters >= 2) {
fprintf(f, "\nAccess type: %s, target address: %p",
ep->ExceptionRecord->ExceptionInformation[0] ? "WRITE" : "READ",
(void*)ep->ExceptionRecord->ExceptionInformation[1]);
}
fprintf(f, "\n=== END CRASH INFO ===\n");
fclose(f);
}
} catch (...) {}
return EXCEPTION_EXECUTE_HANDLER;
}
// Set the window's shell property store so Task Manager, taskbar, and shell
// always show "ObsidianDragon" regardless of any cached VERSIONINFO metadata.
static void SetWindowIdentity(HWND hwnd)
{
IPropertyStore* pps = nullptr;
HRESULT hr = SHGetPropertyStoreForWindow(hwnd, IID_IPropertyStore, (void**)&pps);
if (SUCCEEDED(hr) && pps) {
// Set AppUserModel.ID on the window (overrides process-level ID for this window)
PROPVARIANT pvId;
PropVariantInit(&pvId);
pvId.vt = VT_LPWSTR;
pvId.pwszVal = const_cast<LPWSTR>(L"DragonX.ObsidianDragon.Wallet");
pps->SetValue(PKEY_AppUserModel_ID, pvId);
// Don't PropVariantClear — the string is a static literal
// Set RelaunchDisplayNameResource so the shell shows our name
PROPVARIANT pvName;
PropVariantInit(&pvName);
pvName.vt = VT_LPWSTR;
pvName.pwszVal = const_cast<LPWSTR>(L"ObsidianDragon");
pps->SetValue(PKEY_AppUserModel_RelaunchDisplayNameResource, pvName);
// Set RelaunchCommand (required alongside RelaunchDisplayNameResource)
PROPVARIANT pvCmd;
PropVariantInit(&pvCmd);
pvCmd.vt = VT_LPWSTR;
wchar_t exePath[MAX_PATH] = {};
GetModuleFileNameW(nullptr, exePath, MAX_PATH);
pvCmd.pwszVal = exePath;
pps->SetValue(PKEY_AppUserModel_RelaunchCommand, pvCmd);
pps->Commit();
pps->Release();
DEBUG_LOGF("Window property store: identity set to ObsidianDragon\n");
} else {
DEBUG_LOGF("SHGetPropertyStoreForWindow failed: 0x%08lx\n", (unsigned long)hr);
}
}
#endif
// ---------------------------------------------------------------
// Borderless window support (DX11 / Windows only)
// ---------------------------------------------------------------
#ifdef DRAGONX_USE_DX11
static WNDPROC g_sdlWndProc = nullptr;
// Caption-zone state: updated every frame by renderBorderlessControls()
// so that WM_NCHITTEST can return HTCAPTION for window dragging.
static float g_captionHeight = 20.0f; // height of the title/menu bar (px)
static float g_captionBtnStartX = 0.0f; // left edge of the min/max/close buttons
static bool g_captionImGuiHover = false; // true when ImGui has a hovered/active item
static float g_borderlessDpi = 1.0f; // DPI scale, updated from SDL each frame
// Custom WndProc that removes the native title bar by extending the
// client area to cover the entire window (WM_NCCALCSIZE) and provides
// resize borders + caption drag zone (WM_NCHITTEST).
static void winlog(const char* fmt, ...); // [WINLOG] tracer (defined below)
static LRESULT CALLBACK BorderlessWndProc(HWND hwnd, UINT msg,
WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_NCCALCSIZE:
if (wParam == TRUE) {
// Returning 0 makes the entire window rect into client area,
// which effectively removes the title bar. When maximized,
// constrain to the monitor work area so content isn't clipped
// behind the taskbar.
WINDOWPLACEMENT wp = {};
wp.length = sizeof(wp);
GetWindowPlacement(hwnd, &wp);
if (wp.showCmd == SW_MAXIMIZE) {
auto* p = reinterpret_cast<NCCALCSIZE_PARAMS*>(lParam);
HMONITOR mon = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
MONITORINFO mi = {};
mi.cbSize = sizeof(mi);
GetMonitorInfo(mon, &mi);
p->rgrc[0] = mi.rcWork;
}
return 0;
}
break;
case WM_NCHITTEST: {
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
ScreenToClient(hwnd, &pt);
RECT rc;
GetClientRect(hwnd, &rc);
// Resize borders (not when maximized).
WINDOWPLACEMENT wp = {};
wp.length = sizeof(wp);
GetWindowPlacement(hwnd, &wp);
if (wp.showCmd != SW_MAXIMIZE) {
const int bdr = (int)(5.0f * g_borderlessDpi);
bool t = pt.y <= bdr, b = pt.y >= rc.bottom - bdr;
bool l = pt.x <= bdr, r = pt.x >= rc.right - bdr;
if (t && l) return HTTOPLEFT;
if (t && r) return HTTOPRIGHT;
if (b && l) return HTBOTTOMLEFT;
if (b && r) return HTBOTTOMRIGHT;
if (t) return HTTOP;
if (b) return HTBOTTOM;
if (l) return HTLEFT;
if (r) return HTRIGHT;
}
// Caption zone: the top bar where the menu + window buttons live.
// Return HTCAPTION so Windows handles drag/move natively — but
// only when the cursor is NOT over the window-control buttons and
// NOT over an ImGui interactive item (e.g. a menu-bar entry).
if (pt.y < (int)g_captionHeight) {
// Window-control buttons (min/max/close) on the right
if (pt.x >= (int)g_captionBtnStartX) return HTCLIENT;
// ImGui menu item hovered — let ImGui handle the click
if (g_captionImGuiHover) return HTCLIENT;
return HTCAPTION;
}
return HTCLIENT;
}
case WM_SETCURSOR:
// Prevent DefWindowProc from resetting the cursor to the window-
// class cursor (IDC_ARROW) on every mouse move. Our render loop
// calls SDL_SetCursor() each frame based on ImGui's cursor state;
// without this, WM_SETCURSOR fires between frames and reverts the
// hand/resize cursor back to the arrow.
if (LOWORD(lParam) == HTCLIENT)
return TRUE; // "handled" — keep whatever cursor we last set
break;
case WM_GETMINMAXINFO: {
auto* mmi = reinterpret_cast<MINMAXINFO*>(lParam);
// Scale min window size by DPI so it matches the SDL-side minimum.
mmi->ptMinTrackSize.x = (LONG)(1024 * g_borderlessDpi);
mmi->ptMinTrackSize.y = (LONG)(720 * g_borderlessDpi);
static float s_lastBd = -1.0f; // [WINLOG] log only when g_borderlessDpi changes (this fires a lot)
if (g_borderlessDpi != s_lastBd) {
s_lastBd = g_borderlessDpi;
winlog("WM_GETMINMAXINFO g_borderlessDpi=%.3f -> minTrack=%ldx%ld",
g_borderlessDpi, (long)mmi->ptMinTrackSize.x, (long)mmi->ptMinTrackSize.y);
}
return 0;
}
}
return CallWindowProcW(g_sdlWndProc, hwnd, msg, wParam, lParam);
}
// Draw custom window-control buttons (minimize / maximize-restore / close)
// on the foreground draw list, and handle title-bar dragging.
static void renderBorderlessControls(SDL_Window* window, HWND /*hwnd*/,
dragonx::App& app)
{
ImGuiIO& io = ImGui::GetIO();
// Use the main viewport's foreground draw list so buttons always render
// on the primary window (not on a secondary viewport that has focus).
ImGuiViewport* vp = ImGui::GetMainViewport();
ImDrawList* fg = ImGui::GetForegroundDrawList(vp);
float captionH = ImGui::GetFrameHeight(); // same height as menu bar
float displayW = io.DisplaySize.x;
// DPI scale factor for borderless controls
float dp = dragonx::ui::Layout::dpiScale();
// With multi-viewport, draw-list coordinates and io.MousePos are in
// OS screen-space. Offset all positions by the main viewport origin.
const float ox = vp->Pos.x;
const float oy = vp->Pos.y;
const float btnW = 46.0f * dp;
const float btnH = captionH;
float closeX = ox + displayW - btnW;
float maxX = closeX - btnW;
float minX = maxX - btnW;
ImVec2 mp = io.MousePos;
bool click = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
const auto& themeS = dragonx::ui::schema::UI();
ImU32 wcHover = themeS.resolveColor("var(--window-control-hover)", IM_COL32(255, 255, 255, 30));
ImU32 wcIcon = themeS.resolveColor("var(--window-control)", IM_COL32(255, 255, 255, 200));
ImU32 wcClose = themeS.resolveColor("var(--window-close-hover)", IM_COL32(232, 17, 35, 200));
// Icon glyph half-size and line thickness, scaled by DPI
const float iconHalf = 5.0f * dp;
const float lineThk = 1.0f * dp;
// --- Minimize button ---
{
ImVec2 p0(minX, oy), p1(minX + btnW, oy + btnH);
bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y);
if (hov) fg->AddRectFilled(p0, p1, wcHover);
float cy = oy + btnH * 0.5f, lx = minX + btnW * 0.5f - iconHalf;
fg->AddLine(ImVec2(lx, cy), ImVec2(lx + iconHalf * 2.0f, cy),
wcIcon, lineThk);
if (hov && click) SDL_MinimizeWindow(window);
}
// --- Maximize / Restore button ---
{
ImVec2 p0(maxX, oy), p1(maxX + btnW, oy + btnH);
bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y);
if (hov) fg->AddRectFilled(p0, p1, wcHover);
bool maximized = (SDL_GetWindowFlags(window) & SDL_WINDOW_MAXIMIZED) != 0;
float cx = maxX + btnW * 0.5f, cy = oy + btnH * 0.5f;
if (maximized) {
float s1 = 4.0f * dp, s2 = 3.0f * dp;
float s3 = 2.0f * dp, s4 = 5.0f * dp;
fg->AddRect(ImVec2(cx - s1, cy - s2), ImVec2(cx + s2, cy + s1),
wcIcon, 0, 0, lineThk);
fg->AddRect(ImVec2(cx - s3, cy - s4), ImVec2(cx + s4, cy + s3),
wcIcon, 0, 0, lineThk);
} else {
fg->AddRect(ImVec2(cx - iconHalf, cy - iconHalf + dp),
ImVec2(cx + iconHalf, cy + iconHalf - dp),
wcIcon, 0, 0, lineThk);
}
if (hov && click) {
if (maximized) SDL_RestoreWindow(window);
else SDL_MaximizeWindow(window);
}
}
// --- Close button ---
{
ImVec2 p0(closeX, oy), p1(closeX + btnW, oy + btnH);
bool hov = (mp.x >= p0.x && mp.x < p1.x && mp.y >= p0.y && mp.y < p1.y);
if (hov) fg->AddRectFilled(p0, p1, wcClose);
float cx = closeX + btnW * 0.5f, cy = oy + btnH * 0.5f;
fg->AddLine(ImVec2(cx - iconHalf, cy - iconHalf),
ImVec2(cx + iconHalf, cy + iconHalf),
wcIcon, lineThk);
fg->AddLine(ImVec2(cx + iconHalf, cy - iconHalf),
ImVec2(cx - iconHalf, cy + iconHalf),
wcIcon, lineThk);
if (hov && click) app.beginShutdown();
}
// --- Update caption-zone state for WM_NCHITTEST ---
// WM_NCHITTEST uses client-local coordinates, so subtract viewport origin.
g_captionHeight = captionH;
g_captionBtnStartX = minX - ox;
g_captionImGuiHover = ImGui::IsAnyItemHovered() || ImGui::IsAnyItemActive();
g_borderlessDpi = dp;
}
#endif // DRAGONX_USE_DX11
// Forward declarations
static bool InitSDL();
#ifdef DRAGONX_USE_DX11
static bool InitImGui(SDL_Window* window, dragonx::platform::DX11Context& dx);
static void Shutdown(SDL_Window* window, dragonx::platform::DX11Context& dx);
#else
static bool InitImGui(SDL_Window* window, SDL_GLContext gl_context);
static void Shutdown(SDL_Window* window, SDL_GLContext gl_context);
#endif
// Global single instance lock. Keyed PER VARIANT so the full node and Lite can run side by side —
// their config dirs are already separate (DRAGONX_APP_NAME), so only this lock kept them apart.
// NB: DRAGONX_LITE_BUILD is ALWAYS defined (0 for the full node, 1 for Lite) via $<BOOL:...>, so this
// must be #if (value), not #ifdef (existence) — #ifdef is true for both and made the full node grab
// the Lite lock.
#if DRAGONX_LITE_BUILD
static dragonx::util::SingleInstance g_single_instance("obsidiandragonlite");
#else
static dragonx::util::SingleInstance g_single_instance("obsidiandragon");
#endif
// Check for payment URI in command line args
static std::string findPaymentURI(int argc, char* argv[])
{
for (int i = 1; i < argc; i++) {
if (dragonx::util::isPaymentURI(argv[i])) {
return argv[i];
}
}
return "";
}
// ---------------------------------------------------------------
// Shared render-loop helpers (used by both the main loop and the
// resize event-watcher). Extracted verbatim to de-duplicate the
// per-frame code — same calls, same order, same #ifdef branches.
// ---------------------------------------------------------------
// Draw the window backdrop (base gradient / texture tint / acrylic
// capture callback / WindowBg alpha). The main loop passes lowSpec =
// isLowSpecMode() so low-spec skips texture sampling and the capture
// callback; the resize watcher passes lowSpec = false to reproduce its
// current always-sample-and-capture behavior.
static void drawWindowBackdrop(dragonx::App& app, ImDrawList* bgDL, ImVec2 p0, ImVec2 p1,
bool backdrop_active, bool lowSpec)
{
// Read the current gradient texture from the app each frame
// so hot-reloaded / skin-changed images are picked up immediately.
ImTextureID curGradTex = app.getGradientTexture();
// Window opacity: scale only background layer alpha so the
// desktop shows through while UI stays fully opaque.
float winOpacity = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
// Scale a single color's alpha channel by window opacity
auto scaleAlpha = [&](ImU32 col) -> ImU32 {
uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
a = (uint8_t)(a * winOpacity);
return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
};
if (curGradTex != 0) {
// Base color gradient underneath the texture — read from ui.toml
const auto& S2 = dragonx::ui::schema::UI();
ImU32 baseTop = S2.resolveColor(S2.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200));
ImU32 baseBottom = S2.resolveColor(S2.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
if (lowSpec) {
// Low-spec: skip texture sampling — just draw the base gradient.
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
} else {
int tintAlpha = (int)S2.drawElement("backdrop", "texture-tint-alpha").size;
if (tintAlpha <= 0) tintAlpha = 140;
// Scale background gradient + texture tint alpha by
// window opacity — only affects the backdrop, not UI.
bgDL->AddRectFilledMultiColor(p0, p1, scaleAlpha(baseTop), scaleAlpha(baseTop),
scaleAlpha(baseBottom), scaleAlpha(baseBottom));
int scaledTintAlpha = (int)(tintAlpha * winOpacity);
ImU32 tintCol = IM_COL32(255, 255, 255, scaledTintAlpha);
bgDL->AddImage(curGradTex, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol);
}
} else if (backdrop_active) {
// Programmatic gradient tint (fallback when texture unavailable)
const auto& S = dragonx::ui::schema::UI();
auto bde = [&](const char* key, float fb) {
float v = S.drawElement("backdrop", key).size;
return v >= 0 ? v : fb;
};
ImU32 colTop = IM_COL32((int)bde("gradient-top-r",8), (int)bde("gradient-top-g",12),
(int)bde("gradient-top-b",28), (int)(bde("gradient-top-a",80) * winOpacity));
ImU32 colBottom = IM_COL32((int)bde("gradient-bottom-r",6), (int)bde("gradient-bottom-g",8),
(int)bde("gradient-bottom-b",18), (int)(bde("gradient-bottom-a",60) * winOpacity));
bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
}
// Insert acrylic capture callback BEFORE the noise overlay
// so the noise grain is not captured and blurred into the
// glass cards (the noise should only be a visual overlay).
// Skip in low-spec mode — acrylic is disabled so the FBO
// capture/blur callback is unnecessary GPU work.
if (!lowSpec) {
auto captureCb = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
if (captureCb) {
bgDL->AddCallback(captureCb, nullptr);
bgDL->AddCallback(ImDrawCallback_ResetRenderState, nullptr);
}
}
// WindowBg alpha stays at its base theme value — NOT scaled
// by window opacity. Only the background gradient/texture
// layers fade so UI elements remain fully readable.
if (backdrop_active || curGradTex != 0) {
const auto& S3 = dragonx::ui::schema::UI();
float baseBgAlpha = S3.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha;
}
}
// ── Window-dimension diagnostic tracer ──────────────────────────────────────────────────────
// Always-on (writes to stderr → dragonx-debug.log on Windows) so it's visible in release builds,
// unlike DRAGONX_DEBUG-gated DEBUG_LOGF. Grep the log for "[WINLOG]" to trace how the window size
// changes when dragging between monitors of different DPI. Low volume — only fires on window
// resize / move / DPI-scale events. TODO: remove once the multi-monitor sizing bug is resolved.
static void winlog(const char* fmt, ...)
{
char buf[1024];
va_list a; va_start(a, fmt); vsnprintf(buf, sizeof(buf), fmt, a); va_end(a);
fprintf(stderr, "[WINLOG] %s\n", buf);
fflush(stderr);
}
// Dump the full current window state with a short tag.
static void winlogState(SDL_Window* w, const char* tag)
{
int sw = 0, sh = 0, pw = 0, ph = 0, mw = 0, mh = 0;
SDL_GetWindowSize(w, &sw, &sh);
SDL_GetWindowSizeInPixels(w, &pw, &ph);
SDL_GetWindowMinimumSize(w, &mw, &mh);
float ds = SDL_GetWindowDisplayScale(w);
float st = dragonx::ui::material::Typography::instance().getDpiScale();
SDL_DisplayID did = SDL_GetDisplayForWindow(w);
winlog("%-20s size=%dx%d px=%dx%d min=%dx%d dispScale=%.3f storedScale=%.3f display=%u",
tag, sw, sh, pw, ph, mw, mh, ds, st, (unsigned)did);
#ifdef _WIN32
HWND hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(w),
SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
if (hwnd) {
RECT wr = {}, cr = {};
GetWindowRect(hwnd, &wr);
GetClientRect(hwnd, &cr);
winlog("%-20s native win=%ldx%ld client=%ldx%ld pos=%ld,%ld",
tag, (long)(wr.right - wr.left), (long)(wr.bottom - wr.top),
(long)(cr.right - cr.left), (long)(cr.bottom - cr.top), (long)wr.left, (long)wr.top);
}
#endif
}
// Resize the window to an EXACT client size. On the DX11 borderless native HWND, SDL_SetWindowSize
// inflates the result by a phantom caption+border frame: SDL runs AdjustWindowRectEx to turn the
// requested client size into a window rect (adding the WS_CAPTION/WS_THICKFRAME frame), but our
// WM_NCCALCSIZE handler folds that entire window rect back into the client area — so the client ends
// up ~frame bigger than asked (e.g. a 720 min becomes 759, unreachable). Resize the HWND directly:
// for a borderless window the window rect equals the client, so SetWindowPos gives the exact size.
static void resizeWindowExact(SDL_Window* window, int w, int h)
{
#if defined(_WIN32) && defined(DRAGONX_USE_DX11)
HWND hwnd = (HWND)SDL_GetPointerProperty(SDL_GetWindowProperties(window),
SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
if (hwnd) {
SetWindowPos(hwnd, nullptr, 0, 0, w, h, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
return;
}
#endif
SDL_SetWindowSize(window, w, h);
}
// Handle SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED: save the old-scale size,
// compute + clamp the new physical size, arm the deferred resize retry,
// and rebuild fonts / reset ImGui style at the new DPI scale. newScale
// is pre-normalized by the caller (<=0 -> 1.0f, and macOS -> 1.0f).
static void handleDisplayScaleChange(SDL_Window* window, float newScale,
std::map<int,std::pair<int,int>>& savedSizeForScale,
int& lastKnownW, int& lastKnownH,
int& dpiTargetW, int& dpiTargetH,
int& dpiResizeRetries, bool& dpiResizePending,
int& dpiSettleFrames)
{
auto& typo = dragonx::ui::material::Typography::instance();
float oldScale = typo.getDpiScale();
if (std::abs(newScale - oldScale) > 0.01f) {
float ratio = newScale / oldScale;
DEBUG_LOGF("Display scale changed: %.2f -> %.2f (ratio %.2f)\n",
oldScale, newScale, ratio);
winlog("======== DPI CHANGE storedScale %.3f -> newScale %.3f (ratio %.3f) ========",
oldScale, newScale, ratio);
winlogState(window, " entry");
ImGuiIO& io = ImGui::GetIO();
#ifdef DRAGONX_USE_DX11
g_borderlessDpi = newScale;
#endif
// Save the last known size (before auto-resize) under
// the OLD scale. We use lastKnownW/H instead of
// SDL_GetWindowSize() because by now WM_DPICHANGED
// may have already auto-resized the window.
{
int oldPct = (int)lroundf(oldScale * 100.0f);
savedSizeForScale[oldPct] = {lastKnownW, lastKnownH};
DEBUG_LOGF(" Saved %dx%d for scale %d%%\n",
lastKnownW, lastKnownH, oldPct);
winlog(" saved lastKnown %dx%d under old scale %d%%", lastKnownW, lastKnownH, oldPct);
}
// Compute new physical size: if we've been to this
// scale before, restore that exact size; otherwise
// proportionally scale from the current size.
int newPct = (int)lroundf(newScale * 100.0f);
int newW, newH;
auto it = savedSizeForScale.find(newPct);
if (it != savedSizeForScale.end()) {
newW = it->second.first;
newH = it->second.second;
winlog(" restore saved %dx%d for scale %d%%", newW, newH, newPct);
} else {
// Use lastKnownW/H (pre-auto-resize) instead of
// SDL_GetWindowSize() which already reflects the
// WM_DPICHANGED auto-resize.
newW = (int)lroundf((float)lastKnownW * newScale / oldScale);
newH = (int)lroundf((float)lastKnownH * newScale / oldScale);
winlog(" proportional %dx%d = lastKnown %dx%d * %.3f/%.3f",
newW, newH, lastKnownW, lastKnownH, newScale, oldScale);
}
// Never restore/scale below the new scale's minimum — guarantees the window lands exactly
// on the min in both directions even if the per-scale map was ever seeded oddly.
const int minW = (int)(1024 * newScale), minH = (int)(720 * newScale);
newW = std::max(newW, minW);
newH = std::max(newH, minH);
winlog(" after min-clamp (min %dx%d): %dx%d", minW, minH, newW, newH);
// Clamp to the target display's work area so the
// window doesn't overflow a smaller/higher-density monitor.
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
if (did) {
SDL_Rect usable;
if (SDL_GetDisplayUsableBounds(did, &usable)) {
newW = std::min(newW, usable.w);
newH = std::min(newH, usable.h);
winlog(" after display-clamp (usable %dx%d): %dx%d", usable.w, usable.h, newW, newH);
}
}
// Set the new minimum BEFORE resizing. SDL_SetWindowSize clamps the request UP to the
// current minimum, so when moving to a LOWER scale the shrink (e.g. 1080 -> 720) issued
// while the min is still the old 1080 would be clamped back to 1080 and stick there
// (SDL_SetWindowMinimumSize only ever grows a window, never shrinks it). Min-then-size
// makes the shrink land on the new min immediately, without relying on the deferred retry.
SDL_SetWindowMinimumSize(window, minW, minH);
dpiResizePending = true;
resizeWindowExact(window, newW, newH);
lastKnownW = newW;
lastKnownH = newH;
// Arm deferred retry (SetWindowSize may not stick mid-drag) and suppress resize-recording
// for the whole settle burst: a transient WINDOW_RESIZED (ours, the WM_DPICHANGED settle,
// or a retry frame) that arrives after dpiResizePending was consumed and with scales already
// matching would otherwise be misclassified as a user resize and corrupt savedSizeForScale.
dpiTargetW = newW;
dpiTargetH = newH;
dpiResizeRetries = 30; // retry for ~30 frames
dpiSettleFrames = 40; // ignore resize-recording until the transition settles
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
newW, newH, newScale, newPct);
winlog(" SetWindowMinimumSize(%dx%d) then SetWindowSize(%dx%d); armed retries=30 settle=40",
minW, minH, newW, newH);
winlogState(window, " after set");
// 2) Rebuild font atlas at the new DPI scale
typo.reload(io, newScale);
// 3) Reset style → reapply base theme → scale by new DPI
ImGui::GetStyle() = ImGuiStyle();
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
if (newScale > 1.01f) {
ImGui::GetStyle().ScaleAllSizes(newScale);
}
DEBUG_LOGF(" DPI transition complete\n");
}
}
#if !defined(_WIN32)
#include <csignal>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#if defined(__has_include)
# if __has_include(<execinfo.h>)
# include <execinfo.h>
# define DRAGONX_HAVE_BACKTRACE 1
# endif
#endif
// Absolute path to the crash log, filled at install time so the async-signal handler needs no
// allocation. (POSIX counterpart of the Windows SEH CrashHandler above — W7-3.)
static char g_crashLogPath[1024] = {0};
// Async-signal-safe crash handler: only open()/write()/backtrace_symbols_fd()/raise() are used —
// no stdio, std::filesystem or malloc (all unsafe inside a signal handler).
static void PosixCrashHandler(int sig)
{
int fd = g_crashLogPath[0] ? open(g_crashLogPath, O_WRONLY | O_CREAT | O_APPEND, 0600) : -1;
if (fd >= 0) {
auto put = [fd](const char* s) { ssize_t n = write(fd, s, std::strlen(s)); (void)n; };
put("\n=== CRASH: signal ");
char num[16]; int i = 0, v = sig; // signal number -> decimal, no stdio
if (v == 0) { num[i++] = '0'; }
else { char tmp[16]; int t = 0; while (v > 0) { tmp[t++] = char('0' + v % 10); v /= 10; }
while (t > 0) num[i++] = tmp[--t]; }
num[i] = '\n';
ssize_t nn = write(fd, num, i + 1); (void)nn;
#ifdef DRAGONX_HAVE_BACKTRACE
void* frames[64];
int nframes = backtrace(frames, 64);
backtrace_symbols_fd(frames, nframes, fd); // async-signal-safe
#endif
put("=== END CRASH ===\n");
close(fd);
}
// Restore the default disposition and re-raise so we still get a core dump / normal termination.
signal(sig, SIG_DFL);
raise(sig);
}
static void installPosixCrashHandler(const std::string& crashLogPath)
{
std::snprintf(g_crashLogPath, sizeof(g_crashLogPath), "%s", crashLogPath.c_str());
struct sigaction sa;
std::memset(&sa, 0, sizeof(sa));
sa.sa_handler = PosixCrashHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
for (int sig : {SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL}) {
sigaction(sig, &sa, nullptr);
}
}
#endif // !_WIN32
// ============================================================================
// Frame pacing (P6)
// ============================================================================
// The main loop renders a frame only when something needs one:
// - full rate: the user is interacting (and for kRecentInputMs after the last input event), smooth
// scrolling, the screenshot sweep, and a few settle frames after a window-state change or a window
// appearing;
// - ambient: continuous motion that isn't tied to input (theme-effect animation, the translucent
// backdrop, animated avatars, toast fades) at kAmbientFrameMs while the app has focus and
// kAmbientFrameUnfocusedMs while it doesn't;
// - progress: things that only need to keep visibly moving (spinners and progress bars for work on
// other threads, countdowns, a focused text field's caret, the send/refresh/switch/shutdown
// states) at kProgressFrameMs / kProgressFrameUnfocusedMs;
// - idle: no frames until input, RPC results, or the periodic housekeeping wake.
// Hidden or occluded windows get neither ambient nor progress frames.
// frameBudgetNs() caps every rate at the display refresh where swap/present doesn't block on vsync
// (WSLg, VMs, vsync forced off in the driver), which otherwise spins a core at hundreds of fps.
namespace {
constexpr Uint32 kAmbientFrameMs = 33; // ~30 fps while the app has focus
constexpr Uint32 kAmbientFrameUnfocusedMs = 100; // ~10 fps while another app has focus
constexpr Uint32 kProgressFrameMs = 100; // ~10 fps: spinners, progress, caret blink
constexpr Uint32 kProgressFrameUnfocusedMs = 250; // ~4 fps
constexpr Uint64 kRecentInputMs = 750; // stay at full rate this long after input stops
constexpr int kSettleFrames = 3; // full-rate frames after a window-state change
// Minimum spacing between frame starts: 90% of the display's refresh interval, so a working vsync
// never waits here and a non-blocking swap can't outrun the display.
Uint64 frameBudgetNs(SDL_Window* window)
{
float hz = 60.0f;
if (const SDL_DisplayMode* dm = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(window))) {
if (dm->refresh_rate > 1.0f) hz = dm->refresh_rate;
}
hz = std::clamp(hz, 30.0f, 240.0f);
return static_cast<Uint64>(0.9e9 / hz);
}
bool isInputEvent(Uint32 type)
{
switch (type) {
case SDL_EVENT_KEY_DOWN:
case SDL_EVENT_KEY_UP:
case SDL_EVENT_TEXT_INPUT:
case SDL_EVENT_TEXT_EDITING:
case SDL_EVENT_TEXT_EDITING_CANDIDATES:
case SDL_EVENT_MOUSE_MOTION:
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP:
case SDL_EVENT_MOUSE_WHEEL:
case SDL_EVENT_FINGER_DOWN:
case SDL_EVENT_FINGER_UP:
case SDL_EVENT_FINGER_MOTION:
case SDL_EVENT_PEN_DOWN:
case SDL_EVENT_PEN_UP:
case SDL_EVENT_PEN_MOTION:
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
case SDL_EVENT_GAMEPAD_BUTTON_UP:
return true;
default:
return false;
}
}
// Window-state changes after which the compositor or the acrylic capture may need a few fresh frames.
bool isWindowStateEvent(Uint32 type)
{
switch (type) {
case SDL_EVENT_WINDOW_SHOWN:
case SDL_EVENT_WINDOW_EXPOSED:
case SDL_EVENT_WINDOW_RESTORED:
case SDL_EVENT_WINDOW_MAXIMIZED:
case SDL_EVENT_WINDOW_RESIZED:
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
case SDL_EVENT_WINDOW_FOCUS_GAINED:
case SDL_EVENT_WINDOW_FOCUS_LOST:
case SDL_EVENT_WINDOW_OCCLUDED:
return true;
default:
return false;
}
}
// A held keyboard key keeps ImGui's own key repeat running even where the OS sends no repeat events.
// Keyboard only: gamepad keys come from polled analog state, so a drifting stick or a trigger resting
// past ImGui's 0.1 threshold would read as held forever (gamepad presses still count as input events).
bool anyKeyDown()
{
for (int k = ImGuiKey_Keyboard_BEGIN; k < ImGuiKey_Keyboard_END; ++k) {
if (ImGui::IsKeyDown(static_cast<ImGuiKey>(k))) return true;
}
return false;
}
} // namespace
int main(int argc, char* argv[])
{
// Ensure ObsidianDragon config directory exists early (before any file I/O)
{
std::string odDir = dragonx::util::Platform::getObsidianDragonDir();
std::string odErr;
if (!dragonx::util::Platform::ensureDirectory(odDir, &odErr)) {
// Pre-App-init: nothing (ini, logs, config) can persist if this fails, and the
// Windows log redirect below isn't set up yet — report loudly before any setup.
std::fprintf(stderr, "%s\n", odErr.c_str());
#ifdef _WIN32
MessageBoxA(nullptr, odErr.c_str(), DRAGONX_APP_NAME, MB_OK | MB_ICONERROR);
#endif
return 1;
}
}
// W7-2: initialize the app-level Logger's file sink on ALL platforms so LOG/LOGF/VERBOSE_LOGF are
// actually persisted to dragonx-debug.log. Previously init() was never called, so on Linux/macOS the
// file never existed at all (the Windows-only stdout freopen below is a separate mechanism).
{
const std::string logPath =
(std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.log").string();
dragonx::util::Logger::instance().init(logPath);
}
#if !defined(_WIN32)
// W7-3: install the POSIX crash handler (the Windows SEH filter is installed below). A segfault or
// abort now leaves a backtrace in dragonx-crash.log instead of vanishing silently on Linux/macOS.
{
const std::string crashPath =
(std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-crash.log").string();
installPosixCrashHandler(crashPath);
}
#endif
#ifdef _WIN32
// Redirect raw stdout/stderr (library / daemon-pipe writes) to a log file so it's visible even when
// built as a GUI app (WIN32_EXECUTABLE hides the console). Separate file from the structured Logger
// above so the two writers don't interleave/contend on one file.
{
std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-stdout.log").string();
freopen(logPath.c_str(), "w", stdout);
freopen(logPath.c_str(), "a", stderr);
}
// Install crash handler for diagnostics
SetUnhandledExceptionFilter(CrashHandler);
// Set the Application User Model ID so Windows Task Manager, taskbar,
// and jump lists show "ObsidianDragon" instead of inheriting a
// description from the MinGW runtime ("POSIX WinThreads for Windows").
SetCurrentProcessExplicitAppUserModelID(L"DragonX.ObsidianDragon.Wallet");
#endif
// Check for payment URI in command line
std::string pendingURI = findPaymentURI(argc, argv);
DEBUG_LOGF("%s v%s\n", DRAGONX_APP_NAME, DRAGONX_VERSION);
DEBUG_LOGF("Built with Dear ImGui %s\n", IMGUI_VERSION);
fflush(stdout);
// Env-gated headless smoke for the isolated seed-wallet creator (migrate-to-seed Phase 1).
// Exercises SeedWalletCreator::create() end-to-end without the GUI, then exits. Secret-safe:
// it never prints the seed words — only ok/word-count/address/error. Run with an isolated HOME.
if (const char* smk = std::getenv("DRAGONX_SEED_CREATE_SMOKE"); smk && std::string(smk) == "1") {
sodium_init();
printf("[seed-create-smoke] starting isolated create...\n"); fflush(stdout);
auto r = dragonx::daemon::SeedWalletCreator::create(/*keepDatadir=*/false,
[](const std::string& m) { printf("[seed-create-smoke] %s\n", m.c_str()); fflush(stdout); });
int words = 0; bool inWord = false;
for (char c : r.seedPhrase) { bool sp = (c == ' '); if (!sp && !inWord) { inWord = true; ++words; } else if (sp) inWord = false; }
printf("[seed-create-smoke] ok=%d words=%d addr=%s err=%s\n",
r.ok ? 1 : 0, words, r.destAddress.c_str(), r.error.c_str());
if (!r.seedPhrase.empty()) sodium_memzero(&r.seedPhrase[0], r.seedPhrase.size());
return r.ok ? 0 : 1;
}
// Check for existing instance
if (!g_single_instance.tryLock()) {
fprintf(stderr, "Another instance of %s is already running.\n", DRAGONX_APP_NAME);
DEBUG_LOGF("Please close the existing instance first.\n");
#ifdef _WIN32
const std::string msg = std::string("Another instance of ") + DRAGONX_APP_NAME +
" is already running.\nPlease close it first.";
MessageBoxA(nullptr, msg.c_str(), DRAGONX_APP_NAME, MB_OK | MB_ICONINFORMATION);
#endif
return 1;
}
// Initialize SDL
if (!InitSDL()) {
fprintf(stderr, "Failed to initialize SDL!\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Failed to initialize SDL. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
#endif
return 1;
}
// Load saved window size early (before window creation, before App exists).
// Settings are loaded again later in app.init(), this is just for the size.
int savedWinW = 1200, savedWinH = 720;
float earlyFontScale = 1.0f; // consumed below, before the first (and only) font-atlas build
bool earlyEmojiColor = false;
{
dragonx::config::Settings earlySettings;
const bool earlyLoaded = earlySettings.load();
earlyFontScale = earlySettings.getFontScale(); // defaults when load failed, same as App::init sees
earlyEmojiColor = earlySettings.getChatEmojiColor();
if (earlyLoaded) {
int sw = earlySettings.getWindowWidth();
int sh = earlySettings.getWindowHeight();
if (sw >= 1024 && sh >= 720) {
savedWinW = sw;
savedWinH = sh;
DEBUG_LOGF("Restored window size from settings: %dx%d\n", sw, sh);
} else if (sw > 0 || sh > 0) {
DEBUG_LOGF("Ignored saved window size %dx%d (below 1024x720 minimum) — using default %dx%d\n",
sw, sh, savedWinW, savedWinH);
}
}
}
// Everything the first Typography::load() reads must be final BEFORE InitImGui (DX11 and GL paths below):
// ui.toml sizes, the saved font scale (non-flagging setter so the first preFrame does not reload), emoji style.
dragonx::App::loadBaseUiSchema();
dragonx::ui::Layout::setUserFontScaleVisual(earlyFontScale);
dragonx::ui::material::Typography::instance().setColorEmoji(earlyEmojiColor);
// ---------------------------------------------------------------
// Platform-specific window creation and graphics context
// ---------------------------------------------------------------
#ifdef DRAGONX_USE_DX11
// DirectX 11 path (Windows)
//
// We create the native Win32 HWND ourselves so we can set
// WS_EX_NOREDIRECTIONBITMAP at creation time. This style MUST be
// present when the window is first created -- setting it later with
// SetWindowLongPtrW does NOT reliably release the opaque redirection
// surface that DWM allocates. Without it, the legacy redirection
// surface paints over our transparent DirectComposition visual.
// This is the same approach Windows Terminal uses.
// Register a minimal window class
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(WNDCLASSEXW);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = DefWindowProcW;
wc.hInstance = GetModuleHandleW(nullptr);
wc.hCursor = LoadCursorW(nullptr, MAKEINTRESOURCEW(32512)); // IDC_ARROW
// Load application icon from .exe resources (.rc embeds IDI_ICON1 = ordinal 1)
wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(1));
wc.hIconSm = (HICON)LoadImageW(wc.hInstance, MAKEINTRESOURCEW(1),
IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_DEFAULTCOLOR);
wc.lpszClassName = L"ObsidianDragon";
RegisterClassExW(&wc);
// Compute centered position including frame
// Use saved window size from settings if available, otherwise default
int winW = savedWinW, winH = savedWinH;
DWORD dwStyle = WS_OVERLAPPEDWINDOW;
DWORD dwExStyle = WS_EX_APPWINDOW | WS_EX_NOREDIRECTIONBITMAP;
RECT wr = {0, 0, winW, winH};
AdjustWindowRectEx(&wr, dwStyle, FALSE, dwExStyle);
int adjW = wr.right - wr.left;
int adjH = wr.bottom - wr.top;
int posX = (GetSystemMetrics(SM_CXSCREEN) - adjW) / 2;
int posY = (GetSystemMetrics(SM_CYSCREEN) - adjH) / 2;
HWND nativeHwnd = CreateWindowExW(
dwExStyle, L"ObsidianDragon",
L"ObsidianDragon",
dwStyle,
posX, posY, adjW, adjH,
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
if (!nativeHwnd) {
fprintf(stderr, "Failed to create native Win32 window (error %lu)\n", GetLastError());
MessageBoxW(nullptr, L"Failed to create window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
SDL_Quit();
return 1;
}
DEBUG_LOGF("Native HWND created with WS_EX_NOREDIRECTIONBITMAP\n");
// Don't show yet — wait until borderless subclass is installed
// so the window appears without a visible native title bar.
// Explicitly set the window icon on the HWND for taskbar + Alt-Tab.
// The WNDCLASSEXW already sets hIcon/hIconSm, but WM_SETICON ensures
// the shell picks up the icon even for borderless windows.
{
HINSTANCE hInst = GetModuleHandleW(nullptr);
HICON bigIcon = LoadIconW(hInst, MAKEINTRESOURCEW(1));
HICON smIcon = (HICON)LoadImageW(hInst, MAKEINTRESOURCEW(1),
IMAGE_ICON,
GetSystemMetrics(SM_CXSMICON),
GetSystemMetrics(SM_CYSMICON),
LR_DEFAULTCOLOR);
if (bigIcon) SendMessageW(nativeHwnd, WM_SETICON, ICON_BIG, (LPARAM)bigIcon);
if (smIcon) SendMessageW(nativeHwnd, WM_SETICON, ICON_SMALL, (LPARAM)smIcon);
DEBUG_LOGF("Taskbar icon set from .exe resource (big=%p small=%p)\n",
(void*)bigIcon, (void*)smIcon);
}
// Wrap the native HWND with SDL for event handling + ImGui integration.
// SDL_WINDOW_TRANSPARENT triggers DwmEnableBlurBehindWindow internally,
// which is another signal to DWM to respect per-pixel alpha.
SDL_PropertiesID createProps = SDL_CreateProperties();
SDL_SetPointerProperty(createProps, SDL_PROP_WINDOW_CREATE_WIN32_HWND_POINTER, (void*)nativeHwnd);
SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_TRANSPARENT_BOOLEAN, true);
SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
SDL_SetBooleanProperty(createProps, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
SDL_SetStringProperty(createProps, SDL_PROP_WINDOW_CREATE_TITLE_STRING, DRAGONX_APP_NAME);
SDL_SetNumberProperty(createProps, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, winW);
SDL_SetNumberProperty(createProps, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, winH);
SDL_Window* window = SDL_CreateWindowWithProperties(createProps);
SDL_DestroyProperties(createProps);
if (window == nullptr) {
fprintf(stderr, "Error: SDL_CreateWindowWithProperties(): %s\n", SDL_GetError());
MessageBoxW(nullptr, L"Failed to create SDL window. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
DestroyWindow(nativeHwnd);
SDL_Quit();
return 1;
}
DEBUG_LOGF("SDL window wrapping native HWND (external)\n");
// Subclass for borderless window (removes native title bar).
// SDL has already installed its own WndProc; we save it and chain to it.
g_sdlWndProc = (WNDPROC)GetWindowLongPtrW(nativeHwnd, GWLP_WNDPROC);
SetWindowLongPtrW(nativeHwnd, GWLP_WNDPROC, (LONG_PTR)BorderlessWndProc);
// Resize to intended client dimensions (no frame/title-bar overhead)
// and trigger a frame recalculation so WM_NCCALCSIZE fires.
SetWindowPos(nativeHwnd, nullptr, 0, 0, winW, winH,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOZORDER);
ShowWindow(nativeHwnd, SW_SHOW);
UpdateWindow(nativeHwnd);
DEBUG_LOGF("Borderless window: native title bar removed\n");
// Set shell property store on the HWND so Task Manager and the taskbar
// always show "ObsidianDragon" (overrides any cached metadata).
SetWindowIdentity(nativeHwnd);
// Initialize DirectX 11 context with DXGI alpha swap chain
dragonx::platform::DX11Context dx;
if (!dx.init(window)) {
fprintf(stderr, "Error: Failed to initialize DirectX 11 context\n");
MessageBoxW(nullptr, L"Failed to initialize DirectX 11.\nPlease ensure your graphics drivers are up to date.\n\nCheck the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Graphics Error", MB_OK | MB_ICONERROR);
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
DEBUG_LOGF("DirectX 11 rendering backend initialized\n");
fflush(stdout);
bool backdrop_active = false;
#ifdef _WIN32
// Transparency pipeline:
// 1) WS_EX_NOREDIRECTIONBITMAP (set at HWND creation)
// 2) DirectComposition swap chain with DXGI_ALPHA_MODE_PREMULTIPLIED
// 3) DwmEnableBlurBehindWindow with full-window region for blur
// 4) On Win11 22H2+: Acrylic (DWMSBT_TRANSIENTWINDOW) for modern blur
//
// NOTE: Mica/MicaAlt (DWMSBT_MAINWINDOW / DWMSBT_TABBEDWINDOW) are
// opaque materials and must NOT be used — they fill the entire window
// with a wallpaper-derived color. Acrylic is the correct choice: it
// provides gaussian blur of the desktop behind the window.
if (dx.hasAlphaCompositing()) {
backdrop_active = true;
dragonx::ui::material::SetBackdropActive(true);
HWND hwnd_blur = (HWND)SDL_GetPointerProperty(
SDL_GetWindowProperties(window),
SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
if (hwnd_blur) {
// Override SDL's zero-area blur region with a full-window blur.
// This gives DWM the signal to blur everything behind our window.
HRGN blurRgn = CreateRectRgn(0, 0, -1, -1); // full window
DWM_BLURBEHIND bb = {};
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
bb.fEnable = TRUE;
bb.hRgnBlur = blurRgn;
HRESULT hr_blur = DwmEnableBlurBehindWindow(hwnd_blur, &bb);
DeleteObject(blurRgn);
DEBUG_LOGF("DwmEnableBlurBehindWindow (full region): 0x%08lx\n", hr_blur);
// On Windows 11 22H2+, also enable Acrylic for nicer modern blur
auto ver = dragonx::platform::getWindowsVersion();
if (ver.build >= 22621) {
// Signal to DWM that we handle our own frame rendering.
// Use {0,0,1,0} (1 pixel top) instead of {-1,-1,-1,-1}
// because full-window margins cause DWM to render its own
// caption buttons (min/max/close) on top of ours.
// WM_NCCALCSIZE returning 0 already extends the client area
// to the full window, which satisfies the system backdrop
// requirement. The 1-pixel top margin is enough to keep
// DWM's shadow and rounded corners on Win11.
MARGINS margins = {0, 0, 1, 0};
DwmExtendFrameIntoClientArea(hwnd_blur, &margins);
// Acrylic = blurred, semi-transparent desktop view
// (NOT Mica which is opaque)
DWORD backdrop = 3; // DWMSBT_TRANSIENTWINDOW = Acrylic
HRESULT hr_acr = DwmSetWindowAttribute(hwnd_blur, 38,
&backdrop, sizeof(backdrop));
DEBUG_LOGF("DWM Acrylic (DWMSBT_TRANSIENTWINDOW): 0x%08lx\n", hr_acr);
BOOL useDarkMode = TRUE;
DwmSetWindowAttribute(hwnd_blur, 20,
&useDarkMode, sizeof(useDarkMode));
}
}
DEBUG_LOGF("DX11: Alpha compositing active with blur\n");
fflush(stdout);
} else {
DEBUG_LOGF("DX11: No alpha compositing - window is opaque\n");
}
#endif
// Initialize ImGui with DX11 backend
if (!InitImGui(window, dx)) {
fprintf(stderr, "Failed to initialize ImGui!\n");
MessageBoxW(nullptr, L"Failed to initialize ImGui. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
Shutdown(window, dx);
return 1;
}
// Initialize acrylic effects system (DX11 backend)
if (dragonx::ui::effects::ImGuiAcrylic::Init()) {
DEBUG_LOGF("Acrylic effects initialized (DX11)\n");
} else {
DEBUG_LOGF("Acrylic effects not available on DX11 (will use fallback colors)\n");
}
#else
// OpenGL path (Linux / macOS)
#ifdef __APPLE__
// macOS requires GL 3.2 Core Profile + GLSL 150
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
#else
// GL 3.0 + GLSL 130
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#endif
// Create window with graphics context
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_TRANSPARENT);
SDL_Window* window = SDL_CreateWindow(
DRAGONX_APP_NAME,
savedWinW, savedWinH,
window_flags
);
if (window == nullptr) {
// Retry without transparency if compositor doesn't support it
window_flags = (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
window = SDL_CreateWindow(
DRAGONX_APP_NAME,
savedWinW, savedWinH,
window_flags
);
}
if (window == nullptr) {
fprintf(stderr, "Error: SDL_CreateWindow(): %s\n", SDL_GetError());
return 1;
}
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
SDL_SetWindowMinimumSize(window, 1024, 720);
SDL_GLContext gl_context = SDL_GL_CreateContext(window);
if (gl_context == nullptr) {
fprintf(stderr, "Error: SDL_GL_CreateContext(): %s\n", SDL_GetError());
return 1;
}
SDL_GL_MakeCurrent(window, gl_context);
SDL_GL_SetSwapInterval(1); // Enable vsync
bool backdrop_active = false;
// Check if the compositor granted window transparency
if (SDL_GetWindowFlags(window) & SDL_WINDOW_TRANSPARENT) {
backdrop_active = true;
dragonx::ui::material::SetBackdropActive(true);
DEBUG_LOGF("Window transparency enabled by compositor\n");
}
#ifdef DRAGONX_HAS_GLAD
// Initialize GLAD - load OpenGL function pointers
int glad_version = gladLoadGL((GLADloadfunc)SDL_GL_GetProcAddress);
if (!glad_version) {
fprintf(stderr, "Error: Failed to initialize GLAD (OpenGL loader)\n");
return 1;
}
DEBUG_LOGF("GLAD initialized - OpenGL %d.%d loaded\n", GLAD_VERSION_MAJOR(glad_version), GLAD_VERSION_MINOR(glad_version));
// Initialize acrylic effects system
if (dragonx::ui::effects::ImGuiAcrylic::Init()) {
DEBUG_LOGF("Acrylic effects initialized\n");
// Enable backdrop so glass panels use acrylic blur
if (!backdrop_active) {
backdrop_active = true;
dragonx::ui::material::SetBackdropActive(true);
DEBUG_LOGF("Backdrop activated for acrylic effects\n");
}
} else {
DEBUG_LOGF("Acrylic effects not available (will use fallback colors)\n");
}
#endif
// Initialize ImGui with OpenGL backend
if (!InitImGui(window, gl_context)) {
fprintf(stderr, "Failed to initialize ImGui!\n");
Shutdown(window, gl_context);
return 1;
}
#endif // DRAGONX_USE_DX11
// Set window icon from logo_ObsidianDragon.png
{
std::string iconPath = dragonx::util::getExecutableDirectory() + "/res/img/logos/logo_ObsidianDragon.png";
int iconW = 0, iconH = 0;
unsigned char* iconPixels = dragonx::util::LoadRawPixelsFromFile(iconPath.c_str(), &iconW, &iconH);
if (!iconPixels) {
// Try embedded resource fallback (Windows single-file distribution)
const auto* iconRes = dragonx::resources::getEmbeddedResource("logo_ObsidianDragon.png");
if (iconRes && iconRes->data && iconRes->size > 0) {
iconPixels = dragonx::util::LoadRawPixelsFromMemory(iconRes->data, iconRes->size, &iconW, &iconH);
}
}
if (iconPixels) {
SDL_Surface* iconSurface = SDL_CreateSurfaceFrom(iconW, iconH,
SDL_PIXELFORMAT_RGBA32, iconPixels, iconW * 4);
if (iconSurface) {
SDL_SetWindowIcon(window, iconSurface);
SDL_DestroySurface(iconSurface);
DEBUG_LOGF("Window icon set (%dx%d)\n", iconW, iconH);
}
dragonx::util::FreeRawPixels(iconPixels);
} else {
DEBUG_LOGF("Warning: Could not load window icon from %s or embedded\n", iconPath.c_str());
}
}
// Apply DragonX Material Design theme
dragonx::ui::SetDragonXMaterialTheme();
// Scale ImGui style sizes (padding, rounding, scrollbar, etc.) by the
// display DPI factor. On Windows Per-Monitor DPI v2, SDL3 works in
// physical pixels and DisplayFramebufferScale is 1.0, so we must
// manually scale both fonts (done in Typography::load) and style.
// The window is also resized by dpiScale below so everything fits.
float currentDpiScale = dragonx::ui::material::Typography::instance().getDpiScale();
#ifdef DRAGONX_USE_DX11
g_borderlessDpi = currentDpiScale;
#endif
if (currentDpiScale > 1.01f) {
ImGui::GetStyle().ScaleAllSizes(currentDpiScale);
DEBUG_LOGF("Scaled ImGui style sizes by %.2f for DPI awareness\n", currentDpiScale);
}
// If we're starting on a HiDPI display, scale the window so the UI
// appears the same physical size as on a 100% display.
// On macOS with HIGH_PIXEL_DENSITY, window sizes are already in logical
// (point) coordinates and the framebuffer handles pixel density, so
// we must NOT multiply the window size by the content scale.
#ifndef __APPLE__
{
int curW = 0, curH = 0;
SDL_GetWindowSize(window, &curW, &curH);
int newW = curW, newH = curH;
if (currentDpiScale > 1.01f) {
// On a HiDPI display the saved size is logical px — scale it up to physical so the UI
// appears the same physical size as on a 100% display, and scale the minimum too.
newW = (int)(curW * currentDpiScale);
newH = (int)(curH * currentDpiScale);
SDL_SetWindowMinimumSize(window, (int)(1024 * currentDpiScale), (int)(720 * currentDpiScale));
}
// DEV/TEST hook (dormant unless the env is set): DRAGONX_WIN_GEOM="WxH" forces an exact window
// size, placing it on the largest display that can hold it and bypassing the primary-monitor
// clamp. Lets a headless WSLg sweep render at sizes wider than the 1280 primary (e.g. 2560x1440
// on the mirrored 4K/1440p outputs). Needs the x11 backend so absolute positioning takes effect.
int wantW = 0, wantH = 0;
if (const char* geom = std::getenv("DRAGONX_WIN_GEOM"))
sscanf(geom, "%dx%d", &wantW, &wantH);
if (wantW > 0 && wantH > 0) {
int count = 0;
SDL_DisplayID* disp = SDL_GetDisplays(&count);
SDL_DisplayID best = 0; SDL_Rect bestUsable{0, 0, 0, 0};
for (int i = 0; i < count; ++i) {
SDL_Rect u;
if (!SDL_GetDisplayUsableBounds(disp[i], &u)) continue;
bool fits = (u.w >= wantW && u.h >= wantH);
bool bestFits = (bestUsable.w >= wantW && bestUsable.h >= wantH);
// Prefer a display that fits; among those, the smallest; else the largest available.
if ((fits && !bestFits) ||
(fits && bestFits && (long)u.w * u.h < (long)bestUsable.w * bestUsable.h) ||
(!fits && !bestFits && (long)u.w * u.h > (long)bestUsable.w * bestUsable.h)) {
bestUsable = u; best = disp[i];
}
}
if (disp) SDL_free(disp);
if (best) {
SDL_SetWindowMinimumSize(window, 320, 240);
SDL_SetWindowPosition(window, bestUsable.x + 10, bestUsable.y + 10);
SDL_SetWindowSize(window, wantW, wantH);
}
} else {
// Clamp to the current display's work area — runs on EVERY startup (this clamp used to live
// inside the HiDPI branch, so a size saved on a larger/disconnected monitor could open the
// window off-screen or bigger than the screen on a same-DPI cold start).
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
if (did) {
SDL_Rect usable;
if (SDL_GetDisplayUsableBounds(did, &usable)) {
newW = std::min(newW, usable.w);
newH = std::min(newH, usable.h);
}
}
if (newW != curW || newH != curH) {
SDL_SetWindowSize(window, newW, newH);
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
DEBUG_LOGF("Startup: window fitted %dx%d -> %dx%d (scale %.2f)\n",
curW, curH, newW, newH, currentDpiScale);
}
}
}
#endif
winlog("STARTUP savedSize=%dx%d currentDpiScale=%.3f", savedWinW, savedWinH, currentDpiScale);
winlogState(window, " startup");
// Create application instance
dragonx::App app;
// A quit deferred for the "keep the node running?" prompt brings the window back so it can be seen.
app.setRaiseWindowCallback([window]() {
SDL_RestoreWindow(window);
SDL_RaiseWindow(window);
});
if (!app.init()) {
fprintf(stderr, "Failed to initialize application!\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Failed to initialize application. Please check the debug log at\n%APPDATA%\\ObsidianDragon\\dragonx-debug.log",
L"ObsidianDragon - Startup Error", MB_OK | MB_ICONERROR);
#endif
#ifdef DRAGONX_USE_DX11
Shutdown(window, dx);
#else
Shutdown(window, gl_context);
#endif
return 1;
}
// Activate backdrop transparency if the skin system loaded a background
// image during app.init() → SkinManager::setActiveSkin().
if (app.getGradientTexture() != 0 && !backdrop_active) {
backdrop_active = true;
dragonx::ui::material::SetBackdropActive(true);
// Re-apply TOML-based colors with the new backdrop alpha values.
// Must NOT call SetDragonXMaterialTheme() here — that would clobber
// the active TOML palette with hardcoded C++ DragonX colors, causing
// a visual shift when the user later cycles themes via Ctrl+Arrow.
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
}
// Performance profiler — writes to <ObsidianDragonDir>/perf.log. Opt-in (DRAGONX_PERFLOG=1): always
// on, it appended a block every 300 frames and was never rotated (tens of MB a day for any wallet
// left open).
if (const char* perfEnv = std::getenv("DRAGONX_PERFLOG"); perfEnv && *perfEnv && std::strcmp(perfEnv, "0") != 0) {
std::string perfPath = (std::filesystem::path(
dragonx::util::Platform::getObsidianDragonDir()) / "perf.log").string();
dragonx::util::PerfLog::instance().init(perfPath);
}
// NOTE: No font-scale startup resize here. The saved window size
// already includes any font-scale inflation from the previous session
// (save divides by dpiScale but not fontScale). Multiplying by
// fontScale again would compound the effect on every restart.
// The per-frame font-scale block in the main loop handles resizing
// when the user actually changes the slider.
// Handle pending payment URI from command line
if (!pendingURI.empty()) {
DEBUG_LOGF("Processing payment URI: %s\n", pendingURI.c_str());
app.handlePaymentURI(pendingURI);
}
// ================================================================
// Live-resize support (Linux/X11)
// On X11, the window manager runs a modal loop during drag-resize,
// blocking SDL_PollEvent. We install an event watcher that fires
// immediately from the WM callback and performs a full render cycle
// so the UI redraws in real time while the window is being resized.
// ================================================================
struct ResizeCtx {
SDL_Window* window;
dragonx::App* app;
bool backdrop_active;
#ifdef DRAGONX_USE_DX11
dragonx::platform::DX11Context* dx;
#else
SDL_GLContext gl_context;
#endif
};
ResizeCtx resizeCtx;
resizeCtx.window = window;
resizeCtx.app = &app;
resizeCtx.backdrop_active = backdrop_active;
#ifdef DRAGONX_USE_DX11
resizeCtx.dx = &dx;
#else
resizeCtx.gl_context = gl_context;
#endif
auto resizeWatcher = [](void* userdata, SDL_Event* event) -> bool {
if (event->type != SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED &&
event->type != SDL_EVENT_WINDOW_RESIZED)
return true;
auto* ctx = static_cast<ResizeCtx*>(userdata);
if (event->window.windowID != SDL_GetWindowID(ctx->window))
return true;
// Avoid re-entrancy
static bool in_resize_render = false;
if (in_resize_render) return true;
in_resize_render = true;
// Pre-frame: font atlas rebuilds before NewFrame()
ctx->app->preFrame();
#ifdef DRAGONX_USE_DX11
ctx->dx->resize(event->window.data1, event->window.data2);
ImGui_ImplDX11_NewFrame();
#else
SDL_GL_MakeCurrent(ctx->window, ctx->gl_context);
ImGui_ImplOpenGL3_NewFrame();
#endif
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
// Background gradient
{
ImGuiViewport* vp = ImGui::GetMainViewport();
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
ImVec2 p0 = vp->Pos;
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
// Resize path historically always sampled the texture and always
// added the capture callback — pass lowSpec = false to preserve
// that behavior exactly.
drawWindowBackdrop(*ctx->app, bgDL, p0, p1, ctx->backdrop_active, /*lowSpec=*/false);
}
ImGuiIO& io = ImGui::GetIO();
dragonx::ui::effects::ImGuiAcrylic::BeginFrame(
(int)io.DisplaySize.x, (int)io.DisplaySize.y);
ctx->app->update();
ctx->app->render();
ImGui::Render();
#ifdef DRAGONX_USE_DX11
ctx->dx->ensureSize();
ID3D11RenderTargetView* rtv = ctx->dx->renderTargetView();
ctx->dx->deviceContext()->OMSetRenderTargets(1, &rtv, nullptr);
{
float wo = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;
bool opq = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f;
bool eb = (ctx->backdrop_active || ctx->app->getGradientTexture() != 0) && !opq;
ctx->dx->clear(0.0f, 0.0f, 0.0f, eb ? 0.0f : 1.0f);
}
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
ctx->dx->present(0); // immediate present during resize
#else
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
{
float wo = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;
bool opq = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f;
bool eb = (ctx->backdrop_active || ctx->app->getGradientTexture() != 0) && !opq;
glClearColor(0.0f, 0.0f, 0.0f, eb ? 0.0f : 1.0f);
}
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(ctx->window);
#endif
in_resize_render = false;
return true;
};
SDL_AddEventWatch(resizeWatcher, &resizeCtx);
// Main loop
bool running = true;
bool needsRedraw = true; // P6: Start with a full redraw
auto lastPeriodicFrame = std::chrono::steady_clock::now();
// Frame pacing state (P6; see kAmbientFrameMs and friends above main()).
Uint64 lastInputMs = 0; // SDL_GetTicks() of the last input-class event
Uint64 nextAmbientFrameMs = 0; // when the next ambient frame is due; 0 = none scheduled
Uint64 lastFrameStartNs = 0; // software frame cap bookkeeping (SDL_GetTicksNS)
int settleFrames = kSettleFrames;
auto noteEvent = [&](const SDL_Event& e) {
if (isInputEvent(e.type)) lastInputMs = SDL_GetTicks();
else if (isWindowStateEvent(e.type)) settleFrames = kSettleFrames;
};
// Headless verification hook (DRAGONX_FULL_SWEEP=1, handled after present), hoisted so the pacing
// decision can keep its warm-up frames coming.
const bool autoSweep = std::getenv("DRAGONX_FULL_SWEEP") != nullptr;
int autoSweepFrames = 0;
bool autoSweepStarted = false;
// Per-scale saved window sizes for DPI round-trips.
// Each entry maps scale-percentage (100, 125, 150, 200, …) to the
// physical pixel size the user had at that scale. When transitioning
// to a scale we've visited before the stored size is restored, so
// resizing on a 200% screen doesn't clobber the 100% size.
std::map<int, std::pair<int,int>> savedSizeForScale;
bool dpiResizePending = false;
bool fontScaleResizePending = false;
// Font-scale window reference: size at scale 1.0 (updated on user resize)
float fontScaleRefFS = 0.0f;
int fontScaleRefW = 0, fontScaleRefH = 0;
// Track last known window size at the current DPI scale.
// Updated on user-initiated resizes (not DPI auto-resizes).
// When DISPLAY_SCALE_CHANGED fires, SDL_GetWindowSize() already
// returns the auto-resized value, so we use these instead.
int lastKnownW = 0, lastKnownH = 0;
// Deferred resize: SDL_SetWindowSize may not stick when the user
// is dragging the window between monitors (Windows modal drag loop
// overrides SetWindowPos). We retry for several frames.
int dpiTargetW = 0, dpiTargetH = 0;
int dpiResizeRetries = 0;
// Frames to keep ignoring resize-recording after a DPI transition (settle window) so transient
// WINDOW_RESIZED events during the transition can't corrupt savedSizeForScale / lastKnownW/H.
int dpiSettleFrames = 0;
SDL_DisplayID lastLoggedDisplay = 0; // [WINLOG] throttle: log MOVED only when the display changes
Uint64 minimizedLastTickMs = 0; // real-clock tick for the minimized "keep syncing" update
Uint64 backgroundSinceMs = 0; // when the window started to look backgrounded (0 = it doesn't)
{
float s = dragonx::ui::material::Typography::instance().getDpiScale();
int w = 0, h = 0;
SDL_GetWindowSize(window, &w, &h);
int pct = (int)lroundf(s * 100.0f);
savedSizeForScale[pct] = {w, h};
lastKnownW = w;
lastKnownH = h;
}
while (running) {
// Deferred DPI resize: if a previous DPI transition set a target
// size that SDL_SetWindowSize didn't achieve (e.g. because the
// window was being dragged), retry until it sticks.
if (dpiResizeRetries > 0) {
int curW = 0, curH = 0;
SDL_GetWindowSize(window, &curW, &curH);
if (curW != dpiTargetW || curH != dpiTargetH) {
dpiResizePending = true;
resizeWindowExact(window, dpiTargetW, dpiTargetH);
DEBUG_LOGF(" Deferred resize retry %d: %dx%d -> %dx%d\n",
dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
winlog("RETRY %d: cur %dx%d != target %dx%d -> SetWindowSize(target)",
dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
} else {
// Size matches — done
winlog("RETRY done: reached target %dx%d", dpiTargetW, dpiTargetH);
dpiResizeRetries = 0;
}
if (dpiResizeRetries == 1) winlogState(window, "RETRY-last(stuck?)");
--dpiResizeRetries;
needsRedraw = true;
}
// Burn down the post-DPI-transition settle window (see handleDisplayScaleChange). The retry
// above forces redraws while active, so this drains promptly even if the app is otherwise idle.
if (dpiSettleFrames > 0) --dpiSettleFrames;
// P6: Idle rendering — sleep until input, the next ambient frame, or a 200 ms housekeeping tick
if (!needsRedraw) {
int waitMs = 200;
if (nextAmbientFrameMs != 0) {
const Uint64 nowMs = SDL_GetTicks();
waitMs = nextAmbientFrameMs > nowMs
? static_cast<int>(std::min<Uint64>(nextAmbientFrameMs - nowMs, 200)) : 0;
}
SDL_Event waitEvent;
if (waitMs > 0 && SDL_WaitEventTimeout(&waitEvent, waitMs)) {
noteEvent(waitEvent);
ImGui_ImplSDL3_ProcessEvent(&waitEvent);
if (waitEvent.type == SDL_EVENT_QUIT) {
app.beginShutdown();
}
if (waitEvent.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) {
app.beginShutdown();
}
#ifdef DRAGONX_USE_DX11
if (waitEvent.type == SDL_EVENT_WINDOW_RESIZED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) {
dx.resize(waitEvent.window.data1, waitEvent.window.data2);
}
#endif
// Update saved size for current scale on user-initiated resize.
// DPI-related resizes (from WM_DPICHANGED or our own SDL_SetWindowSize)
// are skipped so they don't overwrite the saved size.
if (waitEvent.type == SDL_EVENT_WINDOW_RESIZED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) {
float actualScale = SDL_GetWindowDisplayScale(window);
float storedScale = dragonx::ui::material::Typography::instance().getDpiScale();
#ifdef __APPLE__
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
#endif
bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
std::abs(actualScale - storedScale) > 0.01f;
winlog("RESIZED(wait) evt=%dx%d actualScale=%.3f storedScale=%.3f dpiPending=%d settle=%d retries=%d -> %s",
waitEvent.window.data1, waitEvent.window.data2, actualScale, storedScale,
(int)dpiResizePending, dpiSettleFrames, dpiResizeRetries,
isDpiResize ? "IGNORED(dpi)" : "RECORDED(user)");
if (dpiResizePending) dpiResizePending = false;
if (!isDpiResize) {
int pct = (int)lroundf(storedScale * 100.0f);
savedSizeForScale[pct] = {waitEvent.window.data1, waitEvent.window.data2};
lastKnownW = waitEvent.window.data1;
lastKnownH = waitEvent.window.data2;
}
}
// Window restored. No forced refresh: update() keeps running while minimized, so the data
// is current, and refreshNow() wiped the shielded-scan progress and re-scanned every
// z-address (SDL also sends RESTORED on un-maximize, so a title-bar double-click did it).
if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) {
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
}
// Handle DPI change that arrived while idle (same logic as poll loop)
if (waitEvent.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) {
float newScale = SDL_GetWindowDisplayScale(window);
if (newScale <= 0.0f) newScale = 1.0f;
#ifdef __APPLE__
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
#endif
winlog("SCALE_CHANGED(wait) reported newScale=%.3f", newScale);
handleDisplayScaleChange(window, newScale, savedSizeForScale,
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
dpiResizeRetries, dpiResizePending, dpiSettleFrames);
}
needsRedraw = true; // Got an event — redraw
}
// Timeout expired: only redraw if there's actual work
if (!needsRedraw) {
auto now = std::chrono::steady_clock::now();
auto sinceLast = std::chrono::duration_cast<std::chrono::milliseconds>(
now - lastPeriodicFrame).count();
// Immediate triggers: background results, visible notifications, work in progress
// (also what keeps a hidden or occluded window responsive: ~5 fps while it lasts)
bool hasImmediateWork = app.hasPendingBackgroundResults()
|| app.hasTransactionSendProgress()
|| app.isTransactionRefreshInProgress()
|| app.isShuttingDown()
|| dragonx::ui::Notifications::instance().hasActive();
// Ambient frame due (scheduled by the pacing decision at the end of the last frame)
const bool ambientDue = nextAmbientFrameMs != 0 && SDL_GetTicks() >= nextAmbientFrameMs;
// Periodic maintenance: fire refresh timers in app.update()
// Low-spec: every 5s; normal: every 2s
int periodMs = dragonx::ui::effects::isLowSpecMode() ? 5000 : 2000;
if (ambientDue || hasImmediateWork || sinceLast >= periodMs) {
needsRedraw = true;
} else {
continue; // Nothing to do — go back to sleep
}
}
}
// Software frame cap: never start frames faster than the display refreshes. A no-op where
// swap/present blocks on vsync; elsewhere it stops the loop spinning a core. The screenshot
// sweep switches vsync off on purpose and is exempt. Before the event poll, so the frame still
// renders with the freshest input.
if (!app.isScreenshotSweeping() && lastFrameStartNs != 0) {
const Uint64 budgetNs = frameBudgetNs(window);
const Uint64 sinceStartNs = SDL_GetTicksNS() - lastFrameStartNs;
if (sinceStartNs < budgetNs) SDL_DelayNS(budgetNs - sinceStartNs);
}
lastFrameStartNs = SDL_GetTicksNS();
// Poll remaining events
SDL_Event event;
// Deferred font-scale commit: smooth visual update during Alt+scroll,
// atlas rebuild after scrolling stops (~200ms idle).
static Uint64 fontScaleCommitTick = 0; // 0 = nothing pending
while (SDL_PollEvent(&event)) {
noteEvent(event);
// Alt + scroll wheel: smooth font scale adjustment
if (event.type == SDL_EVENT_MOUSE_WHEEL) {
SDL_Keymod mods = SDL_GetModState();
if (mods & SDL_KMOD_ALT) {
float step = 0.05f * event.wheel.y;
float cur = dragonx::ui::Layout::userFontScale();
float next = std::max(1.0f, std::min(1.5f, cur + step));
if (next != cur) {
// Smooth preview — no atlas rebuild yet
dragonx::ui::Layout::setUserFontScaleVisual(next);
// Schedule atlas rebuild after scrolling stops
fontScaleCommitTick = SDL_GetTicks() + 200;
}
// Don't pass Alt+scroll to ImGui (would scroll windows)
continue;
}
}
ImGui_ImplSDL3_ProcessEvent(&event);
if (event.type == SDL_EVENT_QUIT) {
app.beginShutdown();
}
if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED &&
event.window.windowID == SDL_GetWindowID(window)) {
app.beginShutdown();
}
// [WINLOG] Log a window MOVE only when it crosses to a different display (avoids drag spam).
if (event.type == SDL_EVENT_WINDOW_MOVED &&
event.window.windowID == SDL_GetWindowID(window)) {
SDL_DisplayID d = SDL_GetDisplayForWindow(window);
if (d != lastLoggedDisplay) {
lastLoggedDisplay = d;
winlog("MOVED to display %u pos=%d,%d dispScale=%.3f",
(unsigned)d, event.window.data1, event.window.data2,
SDL_GetWindowDisplayScale(window));
winlogState(window, " on-cross");
}
}
#ifdef DRAGONX_USE_DX11
// Handle window resize for DX11 swap chain
if (event.type == SDL_EVENT_WINDOW_RESIZED &&
event.window.windowID == SDL_GetWindowID(window)) {
dx.resize(event.window.data1, event.window.data2);
}
#endif
// Update saved size for current scale on user-initiated resize.
// DPI-related resizes (from WM_DPICHANGED or our own SDL_SetWindowSize)
// are skipped so they don't overwrite the saved size.
if (event.type == SDL_EVENT_WINDOW_RESIZED &&
event.window.windowID == SDL_GetWindowID(window)) {
float actualScale = SDL_GetWindowDisplayScale(window);
float storedScale = dragonx::ui::material::Typography::instance().getDpiScale();
#ifdef __APPLE__
actualScale = storedScale; // macOS Retina: scales always match (both 1.0)
#endif
bool isDpiResize = dpiSettleFrames > 0 || dpiResizePending ||
std::abs(actualScale - storedScale) > 0.01f;
winlog("RESIZED(poll) evt=%dx%d actualScale=%.3f storedScale=%.3f dpiPending=%d settle=%d retries=%d fsPending=%d -> %s",
event.window.data1, event.window.data2, actualScale, storedScale,
(int)dpiResizePending, dpiSettleFrames, dpiResizeRetries, (int)fontScaleResizePending,
isDpiResize ? "IGNORED(dpi)" : "RECORDED(user)");
if (dpiResizePending) dpiResizePending = false;
bool isFSResize = fontScaleResizePending;
if (fontScaleResizePending) fontScaleResizePending = false;
if (!isDpiResize) {
int pct = (int)lroundf(storedScale * 100.0f);
savedSizeForScale[pct] = {event.window.data1, event.window.data2};
lastKnownW = event.window.data1;
lastKnownH = event.window.data2;
// User manually resized — update the font-scale reference
// so the next scale change is relative to this size.
if (!isFSResize) {
float fs = dragonx::ui::Layout::userFontScale();
fontScaleRefW = (int)lroundf((float)event.window.data1 / fs);
fontScaleRefH = (int)lroundf((float)event.window.data2 / fs);
}
}
}
// Window restored: no forced refresh (see the wait branch above).
if (event.type == SDL_EVENT_WINDOW_RESTORED &&
event.window.windowID == SDL_GetWindowID(window)) {
app.skipDaemonOutputBacklog(); // drop the minimized-period backlog (avoids a spurious "rescan complete" toast)
}
// Handle DPI/display scale changes (e.g. window dragged to a
// different-DPI monitor, or user changes Windows scaling)
if (event.type == SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED &&
event.window.windowID == SDL_GetWindowID(window)) {
float newScale = SDL_GetWindowDisplayScale(window);
if (newScale <= 0.0f) newScale = 1.0f;
#ifdef __APPLE__
newScale = 1.0f; // macOS handles Retina via DisplayFramebufferScale
#endif
winlog("SCALE_CHANGED(poll) reported newScale=%.3f", newScale);
handleDisplayScaleChange(window, newScale, savedSizeForScale,
lastKnownW, lastKnownH, dpiTargetW, dpiTargetH,
dpiResizeRetries, dpiResizePending, dpiSettleFrames);
}
}
// C134: the window is in the background when it is minimized, hidden, or covered while none of our
// windows has focus, for a few seconds with no input meanwhile. Focus loss alone never counts. The
// app then polls only the chain state; coming back is immediate.
{
const Uint64 nowMs = SDL_GetTicks();
const SDL_WindowFlags wf = SDL_GetWindowFlags(window);
const bool flagged = dragonx::services::windowLooksBackground(
(wf & SDL_WINDOW_MINIMIZED) != 0, (wf & SDL_WINDOW_HIDDEN) != 0,
(wf & SDL_WINDOW_OCCLUDED) != 0, SDL_GetKeyboardFocus() != nullptr);
if (!flagged) backgroundSinceMs = 0;
else if (backgroundSinceMs == 0) backgroundSinceMs = nowMs;
app.setWindowBackground(dragonx::services::isBackgroundWindow(
flagged, backgroundSinceMs != 0 ? (nowMs - backgroundSinceMs) / 1000.0 : 0.0,
lastInputMs != 0 ? (nowMs - lastInputMs) / 1000.0 : 1.0e9));
}
// Check if window is minimized
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
// Keep the wallet syncing while minimized: run the logic update (drains RPC results, ticks the
// refresh scheduler, keeps the daemon connection/reconnect + sync status live) but skip the
// ImGui frame + GPU present since nothing is visible. app.update() only reads
// GetIO()/GetTime()/IsAnyItemActive() — all valid outside a frame — so it's safe without a
// NewFrame; feed it a real-clock DeltaTime (NewFrame, which normally sets it, is skipped) and
// let app.update() clamp it. Throttled to ~5 Hz so CPU stays near-idle (refresh cadences are
// seconds-scale). shouldQuit is still checked so a quit request never hangs behind minimize.
Uint64 nowMs = SDL_GetTicks();
float minDelta = (minimizedLastTickMs == 0) ? 0.001f
: (float)(nowMs - minimizedLastTickMs) / 1000.0f;
minimizedLastTickMs = nowMs;
ImGui::GetIO().DeltaTime = (minDelta > 0.0f) ? minDelta : 0.001f;
try {
app.update();
} catch (const std::exception& e) {
DEBUG_LOGF("[Main] minimized app.update() threw: %s\n", e.what());
} catch (...) {}
if (app.shouldQuit()) running = false;
SDL_Delay(200);
continue;
}
minimizedLastTickMs = 0; // visible again — reset the minimized clock
// --- PerfLog: begin frame ---
dragonx::util::PerfLog::instance().beginFrame();
// Commit deferred font-scale change once scrolling has stopped.
if (fontScaleCommitTick != 0 && SDL_GetTicks() >= fontScaleCommitTick) {
fontScaleCommitTick = 0;
float fs = dragonx::ui::Layout::userFontScale();
dragonx::ui::Layout::setUserFontScale(fs);
app.settings()->setFontScale(fs);
app.settings()->save();
}
// Pre-frame: font atlas rebuilds and schema hot-reload must
// happen BEFORE NewFrame() because NewFrame() caches font ptrs.
app.preFrame();
// Smooth font-scale: compensate visual font size via FontScaleMain
// when the atlas hasn't been rebuilt yet (during slider drag).
// After atlas rebuild, atlasScale == userFontScale so this is 1.0.
{
float userFS = dragonx::ui::Layout::userFontScale();
float atlasFS = dragonx::ui::Layout::fontAtlasScale();
if (atlasFS > 0.001f)
ImGui::GetStyle().FontScaleMain = userFS / atlasFS;
}
// If font scale changed, resize window proportionally.
// Reference size (at scale 1.0) is updated on user-initiated resizes
// so the next scale change is relative to the current window, not a
// stale snapshot. target = ref * curFS — no rounding drift.
{
float curFS = dragonx::ui::Layout::userFontScale();
if (fontScaleRefFS < 0.001f) {
// First frame — record reference, no resize.
fontScaleRefFS = curFS;
int w, h;
SDL_GetWindowSize(window, &w, &h);
fontScaleRefW = (int)lroundf((float)w / curFS);
fontScaleRefH = (int)lroundf((float)h / curFS);
}
if (std::fabs(curFS - fontScaleRefFS) > 0.001f) {
int newW = (int)lroundf((float)fontScaleRefW * curFS);
int newH = (int)lroundf((float)fontScaleRefH * curFS);
// Clamp to display work area
SDL_DisplayID did = SDL_GetDisplayForWindow(window);
if (did) {
SDL_Rect usable;
if (SDL_GetDisplayUsableBounds(did, &usable)) {
newW = std::min(newW, usable.w);
newH = std::min(newH, usable.h);
}
}
float hwDpi = dragonx::ui::Layout::rawDpiScale();
SDL_SetWindowMinimumSize(window,
(int)(1024 * hwDpi),
(int)(720 * hwDpi));
fontScaleResizePending = true;
SDL_SetWindowSize(window, newW, newH);
lastKnownW = newW;
lastKnownH = newH;
fontScaleRefFS = curFS;
}
}
// Start the Dear ImGui frame
PERF_BEGIN(_perfNewFrame);
#ifdef DRAGONX_USE_DX11
ImGui_ImplDX11_NewFrame();
#else
ImGui_ImplOpenGL3_NewFrame();
#endif
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
PERF_END("NewFrame", _perfNewFrame);
PERF_BEGIN(_perfBackdrop);
// Draw background image overlay across the entire viewport.
// Uses theme-specified texture stretched to fill, with the
// programmatic gradient as fallback when texture isn't available.
{
ImGuiViewport* vp = ImGui::GetMainViewport();
ImDrawList* bgDL = ImGui::GetBackgroundDrawList();
ImVec2 p0 = vp->Pos;
ImVec2 p1 = ImVec2(vp->Pos.x + vp->Size.x, vp->Pos.y + vp->Size.y);
drawWindowBackdrop(app, bgDL, p0, p1, backdrop_active,
dragonx::ui::effects::isLowSpecMode());
}
PERF_END("Backdrop", _perfBackdrop);
// Begin acrylic frame (update viewport dimensions)
// Skip in low-spec mode — acrylic is disabled, no need to track FBO sizes
ImGuiIO& io = ImGui::GetIO();
if (!dragonx::ui::effects::isLowSpecMode()) {
dragonx::ui::effects::ImGuiAcrylic::BeginFrame(
(int)io.DisplaySize.x, (int)io.DisplaySize.y);
}
// Update theme visual effects timing (held still under Reduce Motion)
dragonx::ui::effects::ThemeEffects::instance().setMotionFrozen(
app.settings() && app.settings()->getReduceMotion());
dragonx::ui::effects::ThemeEffects::instance().beginFrame();
// Propagate window opacity to theme effects so they fade with the
// backdrop while UI elements (cards, text, buttons) stay opaque.
{
float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
dragonx::ui::effects::ThemeEffects::instance().setBackgroundOpacity(wo);
}
// Reset smooth-scroll animation flag — ApplySmoothScroll() calls
// during this frame will set it back to true if still interpolating.
dragonx::ui::material::SmoothScrollAnimating() = false;
// Always render normal UI
try {
PERF_BEGIN(_perfUpdate);
app.update();
PERF_END("AppUpdate", _perfUpdate);
} catch (const std::exception& e) {
DEBUG_LOGF("[Main] app.update() threw: %s\n", e.what());
} catch (...) {
DEBUG_LOGF("[Main] app.update() threw unknown exception\n");
}
// Background capture is now handled by the draw callback
// inserted into the BackgroundDrawList above. It fires
// during RenderDrawData() at exactly the right moment.
ImGuiErrorRecoveryState erState;
ImGui::ErrorRecoveryStoreState(&erState);
try {
PERF_BEGIN(_perfRender);
app.render();
PERF_END("AppRender", _perfRender);
} catch (const std::exception& e) {
DEBUG_LOGF("[Main] app.render() threw: %s\n", e.what());
ImGui::ErrorRecoveryTryToRecoverState(&erState);
} catch (...) {
DEBUG_LOGF("[Main] app.render() threw unknown exception\n");
ImGui::ErrorRecoveryTryToRecoverState(&erState);
}
// Draw shutdown overlay on top of everything when shutting down
if (app.isShuttingDown()) {
app.renderShutdownScreen();
}
#ifdef DRAGONX_USE_DX11
// Borderless title bar: window controls + drag handling
renderBorderlessControls(window, nativeHwnd, app);
#endif
// Global cursor: show hand cursor when hovering any clickable widget
// (ImGui buttons don't change the cursor by default)
// Persist cursor for 2 extra frames to avoid flicker on Windows
// where g.HoveredId may clear briefly between frames.
{
ImGuiContext& g = *GImGui;
static ImGuiMouseCursor s_prevCursor = ImGuiMouseCursor_Arrow;
static int s_holdFrames = 0;
bool isInputField = (g.InputTextState.ID != 0 &&
g.InputTextState.ID == g.HoveredId);
if (g.MouseCursor == ImGuiMouseCursor_Arrow && g.HoveredId != 0
&& !(g.HoveredIdIsDisabled) && !isInputField) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
s_prevCursor = ImGuiMouseCursor_Hand;
s_holdFrames = 2;
} else if (s_holdFrames > 0 && g.MouseCursor == ImGuiMouseCursor_Arrow) {
// Hold previous cursor to avoid single-frame revert flicker
ImGui::SetMouseCursor(s_prevCursor);
s_holdFrames--;
} else {
s_prevCursor = g.MouseCursor;
s_holdFrames = 0;
}
}
// Rendering
PERF_BEGIN(_perfImRender);
ImGui::Render();
PERF_END("ImGui::Render", _perfImRender);
// Apply cursor immediately via SDL so it takes effect this frame.
// ImGui_ImplSDL3_UpdateMouseCursor() runs at the START of the
// next frame, creating a 1-frame lag. By calling SDL_SetCursor()
// here we eliminate that delay and any flicker.
{
static SDL_Cursor* sdlCursors[ImGuiMouseCursor_COUNT] = {};
static bool sdlCursorsInit = false;
if (!sdlCursorsInit) {
sdlCursors[ImGuiMouseCursor_Arrow] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_DEFAULT);
sdlCursors[ImGuiMouseCursor_TextInput] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_TEXT);
sdlCursors[ImGuiMouseCursor_ResizeAll] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_MOVE);
sdlCursors[ImGuiMouseCursor_ResizeNS] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NS_RESIZE);
sdlCursors[ImGuiMouseCursor_ResizeEW] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_EW_RESIZE);
sdlCursors[ImGuiMouseCursor_ResizeNESW] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NESW_RESIZE);
sdlCursors[ImGuiMouseCursor_ResizeNWSE] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NWSE_RESIZE);
sdlCursors[ImGuiMouseCursor_Hand] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_POINTER);
sdlCursors[ImGuiMouseCursor_NotAllowed] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_NOT_ALLOWED);
sdlCursorsInit = true;
}
ImGuiMouseCursor cur = ImGui::GetMouseCursor();
if (cur >= 0 && cur < ImGuiMouseCursor_COUNT && sdlCursors[cur])
SDL_SetCursor(sdlCursors[cur]);
}
PERF_BEGIN(_perfPresent);
#ifdef DRAGONX_USE_DX11
// Ensure swap chain matches current window size.
// SDL_EVENT_WINDOW_RESIZED may be delayed during live resize
// (Windows runs a modal loop while the user drags the border).
dx.ensureSize();
// DX11: Set render target and clear with transparent black for DWM
ID3D11RenderTargetView* rtv = dx.renderTargetView();
dx.deviceContext()->OMSetRenderTargets(1, &rtv, nullptr);
{
// When the background is fully opaque (low-spec mode or
// window_opacity >= 1.0), clear with alpha 1.0 so the
// compositor/DWM skips expensive blur compositing.
float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f;
bool effectiveBackdrop = (backdrop_active || app.getGradientTexture() != 0)
&& !opaqueBackground;
dx.clear(0.0f, 0.0f, 0.0f, effectiveBackdrop ? 0.0f : 1.0f);
}
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
// Debug screenshot sweep — grab the finished frame before present.
if (app.wantsScreenshotThisFrame()) {
captureFrameToPng(dx, app.screenshotSweepPath());
app.onScreenshotCaptured();
}
// Update and Render additional Platform Windows (multi-viewport)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
}
dx.present(1);
#else
glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
{
// When the background is fully opaque (low-spec mode or
// window_opacity >= 1.0), clear with alpha 1.0 so the
// compositor skips alpha blending against the desktop.
float wo = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo >= 0.99f;
bool effectiveBackdrop = (backdrop_active || app.getGradientTexture() != 0)
&& !opaqueBackground;
glClearColor(0.0f, 0.0f, 0.0f, effectiveBackdrop ? 0.0f : 1.0f);
}
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
// Debug screenshot sweep — grab the finished frame before swap.
if (app.wantsScreenshotThisFrame()) {
captureFrameToPng(app.screenshotSweepPath(), (int)io.DisplaySize.x, (int)io.DisplaySize.y);
app.onScreenshotCaptured();
}
// Update and Render additional Platform Windows (multi-viewport)
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
{
SDL_Window* backup_current_window = SDL_GL_GetCurrentWindow();
SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext();
ImGui::UpdatePlatformWindows();
ImGui::RenderPlatformWindowsDefault();
SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
}
// Uncap FPS during a screenshot sweep so it doesn't crawl at the compositor's (often
// heavily throttled for an unfocused window) vsync rate. Restored when the sweep ends.
{
static bool sweepVsyncOff = false;
const bool sweeping = app.isScreenshotSweeping();
if (sweeping != sweepVsyncOff) {
SDL_GL_SetSwapInterval(sweeping ? 0 : 1);
sweepVsyncOff = sweeping;
}
}
SDL_GL_SwapWindow(window);
#endif
PERF_END("Present", _perfPresent);
// --- PerfLog: end frame ---
dragonx::util::PerfLog::instance().endFrame();
// Headless verification hook: DRAGONX_FULL_SWEEP=1 runs the full UI sweep once the app has
// settled, then quits when it finishes. Used to run the sweep without clicking the button.
if (autoSweep) {
if (!autoSweepStarted) {
if (++autoSweepFrames >= 90) { app.startFullUiSweep(); autoSweepStarted = true; }
} else if (!app.isScreenshotSweeping()) {
running = false; // sweep complete
}
}
// Exit when shutdown is complete
if (app.shouldQuit()) {
running = false;
}
// Periodic log flush so crash diagnostics are visible
{
static auto lastFlush = std::chrono::steady_clock::now();
auto now = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - lastFlush).count() >= 5) {
fflush(stdout);
fflush(stderr);
lastFlush = now;
}
}
// Track last render for idle periodic timer
lastPeriodicFrame = std::chrono::steady_clock::now();
// P6: Decide when the next frame is needed: right away (full rate), at the ambient rate, or not
// until the next event / housekeeping wake (idle). See the frame-pacing notes above main().
{
ImGuiContext& g = *GImGui;
const Uint64 nowMs = SDL_GetTicks();
// Hidden or occluded windows get no ambient frames (SDL reports occlusion on macOS and
// Wayland; elsewhere a covered window usually also lacks focus). Focus is per application:
// any of our windows, multi-viewport platform windows included, not just the main one.
const SDL_WindowFlags wflags = SDL_GetWindowFlags(window);
const bool visible =
(wflags & (SDL_WINDOW_HIDDEN | SDL_WINDOW_OCCLUDED | SDL_WINDOW_MINIMIZED)) == 0;
const bool focused = SDL_GetKeyboardFocus() != nullptr;
// Interaction — not WantCaptureMouse/Keyboard, which only mean "ImGui has windows that
// accept input" (always true). A focused text field by itself (only the caret moves: the
// lock screen's passphrase box, the console input) is ambient below; it used to hold the
// loop at the display rate for as long as the field kept focus.
const bool textEditing = g.ActiveId != 0 && g.ActiveId == g.InputTextState.ID;
bool mouseDown = false;
for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++)
mouseDown |= g.IO.MouseDown[i];
const bool uiActive = g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f
|| mouseDown
|| g.IO.MouseWheel != 0.0f || g.IO.MouseWheelH != 0.0f
|| (g.ActiveId != 0 && !textEditing) || g.MovingWindow != nullptr
|| g.InputEventsQueue.Size > 0 // input still trickling in
|| (focused && anyKeyDown()) // ImGui key repeat
|| (lastInputMs != 0 && nowMs - lastInputMs < kRecentInputMs);
// A window that appeared this frame (a popup hidden for its auto-fit frame, an overlay
// dialog's scrim while its card measures and captures its blur) needs a few more frames
// to show, even when it opened from an RPC result or a timer rather than input.
for (ImGuiWindow* w : g.Windows) {
if (w->Active && w->Appearing) { settleFrames = kSettleFrames; break; }
}
const bool fullRate = uiActive
|| settleFrames > 0
|| app.isScreenshotSweeping()
|| (autoSweep && !autoSweepStarted)
|| dragonx::ui::material::SmoothScrollAnimating();
if (settleFrames > 0) --settleFrames;
// Ambient: continuous motion without input. Theme effects stop requesting frames under
// Reduce Motion (their clock is frozen above). The backdrop itself is static; the
// translucent window stays on this tier as the conservative choice for compositors.
const bool avatarAnimating = dragonx::ui::ConsumeContactsAvatarAnimation(); // consume every frame
const bool progressRequested = dragonx::ui::material::ConsumeProgressFrameRequest(); // ditto
const float wo2 = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
const bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo2 >= 0.99f;
const bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0)
&& !opaqueBackground;
const bool reduceMotion = app.settings() && app.settings()->getReduceMotion();
const bool effectsAnimating = !reduceMotion
&& dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation();
const bool ambient = effectsAnimating
|| backdropNeedsFrames
|| avatarAnimating
|| dragonx::ui::Notifications::instance().hasActive();
// Progress: only needs to keep visibly moving (a spinner, a percentage, dots, a caret).
const bool progress = progressRequested
|| (textEditing && focused)
|| app.isShuttingDown()
|| app.isWalletSwitchInProgress()
|| app.hasTransactionSendProgress()
|| app.isTransactionRefreshInProgress();
needsRedraw = fullRate;
nextAmbientFrameMs = 0;
if (!fullRate && visible && (ambient || progress)) {
const Uint32 interval = ambient ? (focused ? kAmbientFrameMs : kAmbientFrameUnfocusedMs)
: (focused ? kProgressFrameMs : kProgressFrameUnfocusedMs);
nextAmbientFrameMs = lastFrameStartNs / 1000000 + interval;
}
}
}
// Remove resize event watcher
SDL_RemoveEventWatch(resizeWatcher, &resizeCtx);
// Save window size to settings before shutdown
if (app.settings()) {
int curW = 0, curH = 0;
SDL_GetWindowSize(window, &curW, &curH);
float s = dragonx::ui::material::Typography::instance().getDpiScale();
int logW = (int)lroundf((float)curW / s);
int logH = (int)lroundf((float)curH / s);
winlog("SHUTDOWN save: size=%dx%d storedScale=%.3f -> logical=%dx%d", curW, curH, s, logW, logH);
if (logW >= 1024 && logH >= 720) {
app.settings()->setWindowSize(logW, logH);
app.settings()->save();
} else {
DEBUG_LOGF("Not saving window size %dx%d (logical, below 1024x720 minimum)\n", logW, logH);
}
}
// Hide the window immediately so the user perceives the app as closed
// while background cleanup (thread joins, RPC disconnect) continues.
SDL_HideWindow(window);
// Watchdog: if cleanup takes too long the process lingers without a
// window, showing up as a "Background Service" in Task Manager.
// Force-exit after timeout. The daemon stop is already over by the time the render loop exits: the
// node has exited, or the user chose to leave it running (it carries on as its own process), so this
// only bounds the remaining thread joins and RPC disconnects — it never cuts a node's shutdown short.
std::thread([]() {
std::this_thread::sleep_for(std::chrono::seconds(15));
fflush(stdout);
fflush(stderr);
_Exit(0);
}).detach();
// Land the queued settings/wallets.json writes (incl. the window size saved above) before the joins
// in shutdown(); bounded (3 s) so a wedged disk can't eat the watchdog's budget.
app.flushPendingWrites();
// Final cleanup (daemon already stopped by beginShutdown)
app.shutdown();
// shutdown()'s fallback branch saves settings again; land that too, BEFORE the single-instance lock
// is released below so a relaunch can't read a stale settings.json. _Exit() skips ~App().
app.flushPendingWrites();
#ifdef DRAGONX_USE_DX11
Shutdown(window, dx);
#else
Shutdown(window, gl_context);
#endif
// Explicitly release the single-instance lock before exit so a new
// instance can start immediately.
g_single_instance.unlock();
// Force-terminate the process. All important cleanup (daemon stop,
// settings save, RPC disconnect, SDL teardown) has completed above.
// On Windows with mingw-w64 POSIX threads, normal CRT cleanup
// deadlocks waiting for detached pthreads. On Linux, static
// destructors and atexit handlers can also block. _Exit() bypasses
// all of that.
app.wipeSecrets(); // _Exit() below bypasses ~App(), so scrub secret buffers here (L-05)
fflush(stdout);
fflush(stderr);
_Exit(0);
return 0;
}
static bool InitSDL()
{
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) {
fprintf(stderr, "Error: SDL_Init(): %s\n", SDL_GetError());
return false;
}
return true;
}
// Persistent storage for IniFilename — must outlive ImGui context
static std::string s_iniPath;
#ifdef DRAGONX_USE_DX11
static bool InitImGui(SDL_Window* window, dragonx::platform::DX11Context& dx)
{
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
// Save imgui.ini to ObsidianDragon config directory
s_iniPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "imgui.ini").string();
io.IniFilename = s_iniPath.c_str();
// Enable keyboard navigation
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
// Enable gamepad navigation
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
// Enable multi-viewport (windows can float outside main window)
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
// Setup Platform/Renderer backends
ImGui_ImplSDL3_InitForD3D(window);
ImGui_ImplDX11_Init(dx.device(), dx.deviceContext());
// Query display DPI scale (e.g. 1.5 for 150% Windows scaling)
float dpiScale = SDL_GetWindowDisplayScale(window);
if (dpiScale <= 0.0f) dpiScale = 1.0f;
DEBUG_LOGF("Display scale: %.2f\n", dpiScale);
// Load Material Design typography system with DPI awareness
if (!dragonx::ui::material::Typography::instance().load(io, dpiScale)) {
DEBUG_LOGF("Warning: Could not load typography system, using defaults\n");
io.Fonts->AddFontDefault();
}
return true;
}
static void Shutdown(SDL_Window* window, dragonx::platform::DX11Context& dx)
{
ImGui_ImplDX11_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext();
dx.shutdown();
// Save native HWND before SDL releases its reference.
// SDL marks externally-provided HWNDs as SDL_WINDOW_EXTERNAL and
// does NOT call DestroyWindow -- we must do it ourselves.
HWND hwnd = nullptr;
if (window) {
hwnd = (HWND)SDL_GetPointerProperty(
SDL_GetWindowProperties(window),
SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
SDL_DestroyWindow(window);
}
if (hwnd) {
DestroyWindow(hwnd);
}
SDL_Quit();
}
#else // OpenGL path
static bool InitImGui(SDL_Window* window, SDL_GLContext gl_context)
{
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
// Save imgui.ini to ObsidianDragon config directory
s_iniPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "imgui.ini").string();
io.IniFilename = s_iniPath.c_str();
// Enable keyboard navigation
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
// Enable gamepad navigation
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
// Enable multi-viewport (windows can float outside main window)
io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;
// Setup Platform/Renderer backends
ImGui_ImplSDL3_InitForOpenGL(window, gl_context);
#ifdef __APPLE__
ImGui_ImplOpenGL3_Init("#version 150");
#else
ImGui_ImplOpenGL3_Init("#version 130");
#endif
// Query display DPI scale (e.g. 1.5 for 150% scaling)
float dpiScale = SDL_GetWindowDisplayScale(window);
if (dpiScale <= 0.0f) dpiScale = 1.0f;
#ifdef __APPLE__
// On macOS with SDL_WINDOW_HIGH_PIXEL_DENSITY, ImGui handles Retina
// resolution automatically via io.DisplayFramebufferScale. Window
// coordinates are already in logical points, so we must NOT also
// scale fonts and style sizes by the content scale — that would
// double everything.
dpiScale = 1.0f;
#endif
DEBUG_LOGF("Display scale: %.2f\n", dpiScale);
// Load Material Design typography system with DPI awareness
// This loads Ubuntu fonts in Light, Regular, and Medium weights
// at all sizes needed for the type scale (H1-H6, Body1-2, etc.)
if (!dragonx::ui::material::Typography::instance().load(io, dpiScale)) {
DEBUG_LOGF("Warning: Could not load typography system, using defaults\n");
io.Fonts->AddFontDefault();
}
return true;
}
static void Shutdown(SDL_Window* window, SDL_GLContext gl_context)
{
// Shutdown acrylic effects before destroying GL context
dragonx::ui::effects::ImGuiAcrylic::Shutdown();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext();
if (gl_context) {
SDL_GL_DestroyContext(gl_context);
}
if (window) {
SDL_DestroyWindow(window);
}
SDL_Quit();
}
#endif // DRAGONX_USE_DX11