From 50fc80a1c30d36a56b6b96e43265b7e9888273d3 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:51:31 -0700 Subject: [PATCH] graphics: respect Windows dark mode (#765) Only affects the caption bar. Should work for: - [x] standalone configurator - [x] dx9 games, including multi-window games like gitadora - [x] Unity games --- src/spice2x/cfg/configurator_wnd.cpp | 3 ++ .../backends/d3d11/d3d11_swapchain.cpp | 5 +++ src/spice2x/hooks/graphics/graphics.cpp | 6 +++ src/spice2x/util/utils.cpp | 43 +++++++++++++++++++ src/spice2x/util/utils.h | 4 ++ 5 files changed, 61 insertions(+) diff --git a/src/spice2x/cfg/configurator_wnd.cpp b/src/spice2x/cfg/configurator_wnd.cpp index fb3e22c..ca7580d 100644 --- a/src/spice2x/cfg/configurator_wnd.cpp +++ b/src/spice2x/cfg/configurator_wnd.cpp @@ -12,6 +12,7 @@ #include "overlay/overlay.h" #include "util/logging.h" #include "util/precise_timer.h" +#include "util/utils.h" #include "cfg/configurator.h" #include "icon.h" @@ -214,6 +215,8 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() { if (this->hWnd) { overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT = true; + // force dark title bar + set_window_dark_titlebar(this->hWnd, true); } } diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp index bffe2ec..c7a8b00 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp @@ -27,6 +27,7 @@ #include "hooks/graphics/graphics.h" #include "launcher/launcher.h" #include "misc/eamuse.h" +#include "util/utils.h" // -------------------------------------------------------------------------- // overlay render bridge @@ -123,6 +124,10 @@ void try_create_overlay(IDXGISwapChain *swapchain) { return; } + // theme the native title bar; first present is the only reliable point for + // windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll) + set_window_dark_titlebar(desc.OutputWindow); + ID3D11Device *device = nullptr; if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) { return; diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index a478bd4..cadbd23 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -585,6 +585,9 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC hWndParent, hMenu, hInstance, lpParam); GRAPHICS_WINDOWS.push_back(result); + // theme the native title bar (dark/light) + set_window_dark_titlebar(result); + if (is_tdj_sub_window) { // TDJ windowed mode: remember the subscreen window handle for later TDJ_SUBSCREEN_WINDOW = result; @@ -706,6 +709,9 @@ static HWND WINAPI CreateWindowExW_hook(DWORD dwExStyle, LPCWSTR lpClassName, LP hWndParent, hMenu, hInstance, lpParam); GRAPHICS_WINDOWS.push_back(result); + // theme the native title bar (dark/light) + set_window_dark_titlebar(result); + log_misc( "graphics", "CreateWindowExW returned {}, {}", diff --git a/src/spice2x/util/utils.cpp b/src/spice2x/util/utils.cpp index c90e591..f91be99 100644 --- a/src/spice2x/util/utils.cpp +++ b/src/spice2x/util/utils.cpp @@ -4,6 +4,15 @@ #include "utils.h" +#if !SPICE_XP +#include +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif +// older numbering used by Win10 1809..1909 +#define DWMWA_USE_IMMERSIVE_DARK_MODE_OLD 19 +#endif + const char *inet_ntop(short af, const void *src, char *dst, DWORD size) { // prepare storage @@ -124,3 +133,37 @@ void generate_ea_card(char card[17]) { // terminate and flush card[16] = 0; } + +void set_window_dark_titlebar(HWND hWnd, bool force_dark) { +#if !SPICE_XP + if (hWnd == nullptr) { + return; + } + + // AppsUseLightTheme == 0 means the dark app theme is selected + DWORD light = 1; + if (!force_dark) { + DWORD size = sizeof(light); + HKEY key; + if (RegOpenKeyExW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) { + RegQueryValueExW(key, L"AppsUseLightTheme", nullptr, nullptr, + reinterpret_cast(&light), &size); + RegCloseKey(key); + } + } + + BOOL dark = (force_dark || light == 0) ? TRUE : FALSE; + + // fall back to the older attribute numbering on Win10 1809..1909 + if (FAILED(DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE, + &dark, sizeof(dark)))) { + DwmSetWindowAttribute(hWnd, DWMWA_USE_IMMERSIVE_DARK_MODE_OLD, + &dark, sizeof(dark)); + } +#else + (void) hWnd; + (void) force_dark; +#endif +} diff --git a/src/spice2x/util/utils.h b/src/spice2x/util/utils.h index 6d09dbf..6cb0ef8 100644 --- a/src/spice2x/util/utils.h +++ b/src/spice2x/util/utils.h @@ -28,6 +28,10 @@ static const char HEX_LOOKUP_UPPERCASE[16] = { const char *inet_ntop(short af, const void *src, char *dst, DWORD size); +// make a window's native title bar follow the Windows dark/light app theme. +// pass force_dark=true to always use the dark caption regardless of the system theme. +void set_window_dark_titlebar(HWND hWnd, bool force_dark = false); + static inline bool string_begins_with(const std::string &s, const std::string &prefix) { return s.compare(0, prefix.size(), prefix) == 0; }