Files
ObsidianDragon/src/main.cpp
DanS 975743f754 feat(wallet): persist history and surface pending sends
Add an encrypted SQLite transaction history cache with cached tip metadata and
per-address shielded scan progress so startup and full refreshes avoid
re-scanning every z-address while still invalidating on wallet/address/rescan
changes.

Improve wallet history loading by paging transparent transactions, preserving
cached shielded and sent rows, keeping recent/unconfirmed activity visible, and
classifying mining-address receives. Show z_sendmany opid sends immediately in
History and Overview, pin pending rows through refreshes, and apply optimistic
address/balance debits until opids resolve.

Add timestamped RPC console tracing by source/method without logging params or
results, reduce redundant refresh/RPC calls, and cache Explorer recent block
summaries in SQLite.

Expand focused tests for transaction cache encryption, scan-progress
persistence/invalidation, history preservation, operation-status parsing,
pending send visibility, and Explorer/RPC refresh behavior.
2026-05-05 03:22:14 -05:00

2019 lines
88 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/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 "resources/embedded_resources.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 <cstdio>
#include <cstdlib>
#include <cmath>
#include <filesystem>
#include <string>
#include <chrono>
#include "util/logger.h"
#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 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);
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
static dragonx::util::SingleInstance g_single_instance("obsidiandragon");
// 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 "";
}
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::error_code ec;
std::filesystem::create_directories(odDir, ec);
}
#ifdef _WIN32
// Redirect stdout/stderr to a log file so diagnostic output is visible
// even when built as a GUI app (WIN32_EXECUTABLE hides the console).
{
std::string logPath = (std::filesystem::path(dragonx::util::Platform::getObsidianDragonDir()) / "dragonx-debug.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);
// Check for existing instance
if (!g_single_instance.tryLock()) {
fprintf(stderr, "Another instance of ObsidianDragon is already running.\n");
DEBUG_LOGF("Please close the existing instance first.\n");
#ifdef _WIN32
MessageBoxW(nullptr, L"Another instance of ObsidianDragon is already running.\nPlease close it first.",
L"ObsidianDragon", 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;
{
dragonx::config::Settings earlySettings;
if (earlySettings.load()) {
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);
}
}
}
// ---------------------------------------------------------------
// 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__
if (currentDpiScale > 1.01f) {
int curW = 0, curH = 0;
SDL_GetWindowSize(window, &curW, &curH);
int newW = (int)(curW * currentDpiScale);
int newH = (int)(curH * currentDpiScale);
// Clamp to the display's work area so the window fits on screen.
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);
}
}
SDL_SetWindowSize(window, newW, newH);
SDL_SetWindowMinimumSize(window,
(int)(1024 * currentDpiScale),
(int)(720 * currentDpiScale));
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
DEBUG_LOGF("HiDPI startup: window resized %dx%d -> %dx%d (scale %.2f)\n",
curW, curH, newW, newH, currentDpiScale);
}
#endif
// Create application instance
dragonx::App app;
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();
}
// Initialize performance profiler — writes to <ObsidianDragonDir>/perf.log
{
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);
ImTextureID curGradTex2 = ctx->app->getGradientTexture();
float winOp2 = ctx->app->settings() ? ctx->app->settings()->getWindowOpacity() : 1.0f;
// Scale only background layer alpha by window opacity so the
// desktop shows through while UI elements stay fully opaque.
auto scaleA2 = [&](ImU32 col) -> ImU32 {
uint8_t a = (col >> IM_COL32_A_SHIFT) & 0xFF;
a = (uint8_t)(a * winOp2);
return (col & ~(0xFFu << IM_COL32_A_SHIFT)) | ((ImU32)a << IM_COL32_A_SHIFT);
};
if (curGradTex2 != 0) {
const auto& S1 = dragonx::ui::schema::UI();
ImU32 baseTop1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-top").color, IM_COL32(18,28,65,200));
ImU32 baseBottom1 = S1.resolveColor(S1.drawElement("backdrop", "base-color-bottom").color, IM_COL32(8,12,35,200));
int tintAlpha1 = (int)S1.drawElement("backdrop", "texture-tint-alpha").size;
if (tintAlpha1 <= 0) tintAlpha1 = 140;
bgDL->AddRectFilledMultiColor(p0, p1, scaleA2(baseTop1), scaleA2(baseTop1),
scaleA2(baseBottom1), scaleA2(baseBottom1));
int scaledTA1 = (int)(tintAlpha1 * winOp2);
ImU32 tintCol1 = IM_COL32(255, 255, 255, scaledTA1);
bgDL->AddImage(curGradTex2, p0, p1, ImVec2(0, 0), ImVec2(1, 1), tintCol1);
} else if (ctx->backdrop_active) {
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) * winOp2));
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) * winOp2));
bgDL->AddRectFilledMultiColor(p0, p1, colTop, colTop, colBottom, colBottom);
}
// Acrylic capture callback — must fire BEFORE the noise
// overlay so the noise grain is not captured and blurred
// into the glass cards.
auto captureCb2 = dragonx::ui::effects::ImGuiAcrylic::GetBackgroundCaptureCallback();
if (captureCb2) {
bgDL->AddCallback(captureCb2, 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 are scaled so the desktop shows through while UI
// elements (cards, text, buttons) remain fully readable.
if (ctx->backdrop_active || curGradTex2 != 0) {
const auto& S4 = dragonx::ui::schema::UI();
float baseBgAlpha2 = S4.drawElement("backdrop", "background-alpha").sizeOr(0.40f);
ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = baseBgAlpha2;
}
}
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();
// 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;
{
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;
SDL_SetWindowSize(window, dpiTargetW, dpiTargetH);
DEBUG_LOGF(" Deferred resize retry %d: %dx%d -> %dx%d\n",
dpiResizeRetries, curW, curH, dpiTargetW, dpiTargetH);
} else {
// Size matches — done
dpiResizeRetries = 0;
}
--dpiResizeRetries;
needsRedraw = true;
}
// P6: Idle rendering — sleep if nothing needs redrawing
if (!needsRedraw) {
// Wait up to 200ms for user input or wake event
SDL_Event waitEvent;
if (SDL_WaitEventTimeout(&waitEvent, 200)) {
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 = dpiResizePending ||
std::abs(actualScale - storedScale) > 0.01f;
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 from minimized — trigger immediate data refresh
if (waitEvent.type == SDL_EVENT_WINDOW_RESTORED &&
waitEvent.window.windowID == SDL_GetWindowID(window)) {
app.refreshNow();
}
// 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
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 (idle): %.2f -> %.2f (ratio %.2f)\n",
oldScale, newScale, ratio);
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);
}
// 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;
} 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);
}
// 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);
}
}
dpiResizePending = true;
SDL_SetWindowSize(window, newW, newH);
lastKnownW = newW;
lastKnownH = newH;
// Arm deferred retry — the SetWindowSize above may
// not stick if the user is mid-drag (modal loop).
dpiTargetW = newW;
dpiTargetH = newH;
dpiResizeRetries = 30; // retry for ~30 frames
SDL_SetWindowMinimumSize(window,
(int)(1024 * newScale), (int)(720 * newScale));
typo.reload(io, newScale);
ImGui::GetStyle() = ImGuiStyle();
dragonx::ui::schema::UISchema::instance().reapplyColorsToImGui();
if (newScale > 1.01f) {
ImGui::GetStyle().ScaleAllSizes(newScale);
}
}
}
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: async RPC results or visible notifications
bool hasImmediateWork = app.hasPendingRPCResults()
|| app.hasTransactionSendProgress()
|| app.isTransactionRefreshInProgress()
|| dragonx::ui::Notifications::instance().hasActive();
// Periodic maintenance: fire refresh timers in app.update()
// Low-spec: every 5s; normal: every 2s
int periodMs = dragonx::ui::effects::isLowSpecMode() ? 5000 : 2000;
if (hasImmediateWork || sinceLast >= periodMs) {
needsRedraw = true;
} else {
continue; // Nothing to do — go back to sleep
}
}
}
// 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)) {
// 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();
}
#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 = dpiResizePending ||
std::abs(actualScale - storedScale) > 0.01f;
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 from minimized — trigger immediate data refresh
if (event.type == SDL_EVENT_WINDOW_RESTORED &&
event.window.windowID == SDL_GetWindowID(window)) {
app.refreshNow();
}
// 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
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);
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);
}
// 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;
} 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);
}
// 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);
}
}
dpiResizePending = true;
SDL_SetWindowSize(window, newW, newH);
lastKnownW = newW;
lastKnownH = newH;
// Arm deferred retry — the SetWindowSize above may
// not stick if the user is mid-drag (modal loop).
dpiTargetW = newW;
dpiTargetH = newH;
dpiResizeRetries = 30; // retry for ~30 frames
SDL_SetWindowMinimumSize(window,
(int)(1024 * newScale),
(int)(720 * newScale));
DEBUG_LOGF(" Window resized: %dx%d (scale %.2f, pct %d)\n",
newW, newH, newScale, newPct);
// 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");
}
}
}
// Check if window is minimized
if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) {
// Still check shouldQuit while minimized to avoid hang
if (app.shouldQuit()) {
running = false;
}
SDL_Delay(10);
continue;
}
// --- 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);
// 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;
bool lowSpec = dragonx::ui::effects::isLowSpecMode();
// 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;
}
}
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
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());
// 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());
// 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);
}
SDL_GL_SwapWindow(window);
#endif
PERF_END("Present", _perfPresent);
// --- PerfLog: end frame ---
dragonx::util::PerfLog::instance().endFrame();
// 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: Determine if next frame needs redraw or can idle
{
ImGuiContext& g = *GImGui;
// Detect actual user interaction — NOT WantCaptureMouse/Keyboard
// which are just "ImGui has windows that accept input" (always true).
bool mouseMoving = g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f;
bool mouseClicked = false;
for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++)
mouseClicked |= g.IO.MouseDown[i];
bool scrolling = g.IO.MouseWheel != 0.0f || g.IO.MouseWheelH != 0.0f;
bool typing = g.IO.InputQueueCharacters.Size > 0;
bool widgetActive = g.ActiveId != 0 || g.MovingWindow != nullptr;
bool uiActive = mouseMoving || mouseClicked || scrolling
|| typing || widgetActive;
// When the background is fully opaque, backdrop doesn't
// need continuous frames — let the app idle and save power.
float wo2 = app.settings() ? app.settings()->getWindowOpacity() : 1.0f;
bool opaqueBackground = dragonx::ui::effects::isLowSpecMode() || wo2 >= 0.99f;
bool backdropNeedsFrames = (backdrop_active || app.getGradientTexture() != 0)
&& !opaqueBackground;
bool animating = app.isShuttingDown()
|| backdropNeedsFrames
|| app.hasTransactionSendProgress()
|| app.isTransactionRefreshInProgress()
|| dragonx::ui::effects::ThemeEffects::instance().hasActiveAnimation()
|| dragonx::ui::Notifications::instance().hasActive()
|| dragonx::ui::material::SmoothScrollAnimating();
// If nothing is happening, allow the next iteration to idle
needsRedraw = uiActive || animating;
}
}
// 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);
if (logW >= 1024 && logH >= 720) {
app.settings()->setWindowSize(logW, logH);
app.settings()->save();
}
}
// 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 — all critical state (settings, daemon
// stop) was handled in beginShutdown().
// Allow enough time for the daemon to shut down gracefully (up to
// 10s in stop()) plus RPC disconnect overhead.
std::thread([]() {
std::this_thread::sleep_for(std::chrono::seconds(15));
fflush(stdout);
fflush(stderr);
_Exit(0);
}).detach();
// Final cleanup (daemon already stopped by beginShutdown)
app.shutdown();
#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.
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