cfg: d3d9 standalone configurator (#720)

## Description of change
**Configurator (spicecfg.exe)**

- Attempts to create a D3D9 device on startup and uses the hardware
ImGui DX9 path when successful; falls back to the existing software
rasterizer automatically if D3D9 init fails.
- Rewrites the configurator window loop: refresh-rate-aware render
timer, minimize/resize/`WM_DISPLAYCHANGE` handling, direct Win32 ->
ImGui input (keyboard, mouse, wheel), and optimized software painting
via `SetDIBitsToDevice` instead of per-frame GDI `HBITMAP` allocation.

**Overlay / input (scoped to standalone configurator)**

- Skips expensive per-frame rawinput polling in
`ImGui_ImplSpice_NewFrame` when `CONFIGURATOR_STANDALONE` is set (Win32
messages drive input instead).
- Adds software-renderer idle-frame detection (`sw_pixels_dirty` +
draw-data hash) so spicecfg only repaints when pixels actually change.
- Adds `ImGuiWindowFlags_NoScrollWithMouse` on the configurator root
window to prevent scroll jolt on non-scrollable child widgets.
This commit is contained in:
drmext
2026-05-30 19:52:35 +00:00
committed by GitHub
parent 31aabe9786
commit 4c73200f58
8 changed files with 721 additions and 93 deletions
+2 -2
View File
@@ -765,7 +765,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_cfg target_link_libraries(spicetools_cfg
PUBLIC ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_cfg PUBLIC winscard) target_link_libraries(spicetools_cfg PUBLIC winscard)
set_target_properties(spicetools_cfg PROPERTIES PREFIX "") set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
@@ -783,7 +783,7 @@ set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_cfg_linux target_link_libraries(spicetools_cfg_linux
PUBLIC ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "") set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux") set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
+68 -2
View File
@@ -1,6 +1,9 @@
#include "configurator.h" #include "configurator.h"
#include <d3d9.h>
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/logging.h"
namespace cfg { namespace cfg {
@@ -16,11 +19,74 @@ namespace cfg {
CONFIGURATOR_STANDALONE = false; CONFIGURATOR_STANDALONE = false;
} }
// Attempt to bring up a D3D9 device backing the configurator's window so the
// overlay can use the hardware-accelerated imgui_impl_dx9 path instead of
// the CPU rasterizer. Returns true if both Direct3DCreate9 and CreateDevice
// succeed; the caller falls back to overlay::create_software() otherwise so
// that environments without D3D9 (Wine without dxvk, headless test boxes,
// GPUs whose drivers reject HAL) still get a working configurator.
static bool try_init_d3d9(ConfiguratorWindow &wnd) {
if (wnd.hWnd == nullptr) {
return false;
}
IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION);
if (d3d == nullptr) {
log_warning("configurator", "Direct3DCreate9 returned NULL, falling back to software renderer");
return false;
}
D3DPRESENT_PARAMETERS pp {};
pp.Windowed = TRUE;
pp.SwapEffect = D3DSWAPEFFECT_DISCARD;
// D3DFMT_UNKNOWN -> driver picks the current desktop format
pp.BackBufferFormat = D3DFMT_UNKNOWN;
pp.BackBufferWidth = static_cast<UINT>(wnd.client_width);
pp.BackBufferHeight = static_cast<UINT>(wnd.client_height);
pp.hDeviceWindow = wnd.hWnd;
pp.EnableAutoDepthStencil = FALSE;
pp.PresentationInterval = D3DPRESENT_INTERVAL_ONE;
IDirect3DDevice9 *device = nullptr;
// SOFTWARE_VERTEXPROCESSING is the most compatible behavior flag; the UI
// is tiny so we don't need hardware T&L. FPU_PRESERVE keeps our floating
// point environment intact in case other spice code relies on it.
const DWORD behavior_flags = D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_FPU_PRESERVE;
HRESULT hr = d3d->CreateDevice(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
wnd.hWnd,
behavior_flags,
&pp,
&device);
if (FAILED(hr) || device == nullptr) {
log_warning("configurator",
"D3D9 CreateDevice failed (hr={:#x}), falling back to software renderer",
static_cast<unsigned int>(hr));
d3d->Release();
return false;
}
wnd.d3d = d3d;
wnd.device = device;
wnd.pp = pp;
wnd.use_d3d9 = true;
return true;
}
void Configurator::run() { void Configurator::run() {
// create instance // bring up the overlay against either a real D3D9 device or the software
// rasterizer. The choice is one-shot - the in-game overlay always uses
// the same renderer the configurator picked here.
overlay::ENABLED = true; overlay::ENABLED = true;
if (try_init_d3d9(this->wnd)) {
log_info("configurator", "using D3D9 hardware-accelerated renderer");
overlay::create_d3d9(this->wnd.hWnd, this->wnd.d3d, this->wnd.device);
} else {
log_info("configurator", "using software renderer");
overlay::create_software(this->wnd.hWnd); overlay::create_software(this->wnd.hWnd);
}
overlay::OVERLAY->set_active(true); overlay::OVERLAY->set_active(true);
overlay::OVERLAY->hotkeys_enable = false; overlay::OVERLAY->hotkeys_enable = false;
ImGui::GetIO().MouseDrawCursor = false; ImGui::GetIO().MouseDrawCursor = false;
+426 -40
View File
@@ -1,11 +1,17 @@
#include "configurator_wnd.h" #include "configurator_wnd.h"
#include <algorithm>
#include <cstring>
#include <windows.h> #include <windows.h>
#include <windowsx.h>
#include "build/defs.h" #include "build/defs.h"
#include "external/imgui/imgui.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "icon.h" #include "icon.h"
@@ -16,6 +22,156 @@ static int WINDOW_SIZE_X = 800;
static int WINDOW_SIZE_Y = 600; static int WINDOW_SIZE_Y = 600;
static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON)); static HICON WINDOW_ICON = LoadIcon(GetModuleHandle(nullptr), MAKEINTRESOURCE(MAINICON));
static const UINT_PTR RENDER_TIMER_ID = 1;
// Map a virtual key code to the matching ImGuiKey. Mirrors the table used by
// the in-game ImGui spice backend so navigation keys behave identically when
// the configurator runs standalone and drives ImGui from Win32 messages.
static ImGuiKey vk_to_imgui_key(WPARAM vkey) {
switch (vkey) {
case VK_TAB: return ImGuiKey_Tab;
case VK_LEFT: return ImGuiKey_LeftArrow;
case VK_RIGHT: return ImGuiKey_RightArrow;
case VK_UP: return ImGuiKey_UpArrow;
case VK_DOWN: return ImGuiKey_DownArrow;
case VK_PRIOR: return ImGuiKey_PageUp;
case VK_NEXT: return ImGuiKey_PageDown;
case VK_HOME: return ImGuiKey_Home;
case VK_END: return ImGuiKey_End;
case VK_INSERT: return ImGuiKey_Insert;
case VK_DELETE: return ImGuiKey_Delete;
case VK_BACK: return ImGuiKey_Backspace;
case VK_SPACE: return ImGuiKey_Space;
case VK_RETURN: return ImGuiKey_Enter;
case VK_ESCAPE: return ImGuiKey_Escape;
case VK_LSHIFT: return ImGuiKey_LeftShift;
case VK_RSHIFT: return ImGuiKey_RightShift;
case VK_SHIFT: return ImGuiKey_LeftShift;
case VK_LCONTROL: return ImGuiKey_LeftCtrl;
case VK_RCONTROL: return ImGuiKey_RightCtrl;
case VK_CONTROL: return ImGuiKey_LeftCtrl;
case 'A': return ImGuiKey_A;
case 'C': return ImGuiKey_C;
case 'V': return ImGuiKey_V;
case 'X': return ImGuiKey_X;
case 'Y': return ImGuiKey_Y;
case 'Z': return ImGuiKey_Z;
default: return ImGuiKey_None;
}
}
static ImGuiKey vk_to_imgui_mod_key(WPARAM vkey) {
switch (vkey) {
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
return ImGuiMod_Shift;
case VK_CONTROL:
case VK_LCONTROL:
case VK_RCONTROL:
return ImGuiMod_Ctrl;
case VK_MENU:
case VK_LMENU:
case VK_RMENU:
return ImGuiMod_Alt;
default:
return ImGuiMod_None;
}
}
static cfg::ConfiguratorWindow *get_state(HWND hWnd) {
return reinterpret_cast<cfg::ConfiguratorWindow *>(
GetWindowLongPtrW(hWnd, GWLP_USERDATA));
}
// Returns the refresh rate (Hz) of the monitor the window currently lives on,
// clamped to a sane range. Falls back to 60 if detection fails or the value
// looks invalid (DEVMODE may report 0 or 1 to mean "use hardware default").
static UINT detect_monitor_refresh_hz(HWND hWnd) {
UINT hz = 0;
HMONITOR mon = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
if (mon) {
MONITORINFOEXW mi {};
mi.cbSize = sizeof(mi);
if (GetMonitorInfoW(mon, reinterpret_cast<LPMONITORINFO>(&mi))) {
DEVMODEW dm {};
dm.dmSize = sizeof(dm);
if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm)) {
hz = dm.dmDisplayFrequency;
}
}
}
if (hz <= 1) {
HDC hdc = GetDC(hWnd);
if (hdc) {
int v = GetDeviceCaps(hdc, VREFRESH);
if (v > 1) {
hz = static_cast<UINT>(v);
}
ReleaseDC(hWnd, hdc);
}
}
if (hz < 30 || hz > 240) {
hz = 60;
}
return hz;
}
// Software-path WM_PAINT: blit the overlay's pixel buffer to the window using
// SetDIBitsToDevice. Avoids creating/destroying a GDI HBITMAP every frame.
static void paint_software(HWND hWnd) {
if (!overlay::OVERLAY) {
PAINTSTRUCT ps {};
BeginPaint(hWnd, &ps);
EndPaint(hWnd, &ps);
return;
}
int width = 0;
int height = 0;
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
PAINTSTRUCT paint {};
HDC hdc = BeginPaint(hWnd, &paint);
if (pixel_data && width > 0 && height > 0) {
BITMAPINFO bmi {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = width;
// negative height -> top-down DIB, matching how ImGui packs pixels
bmi.bmiHeader.biHeight = -height;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
SetDIBitsToDevice(
hdc,
0, 0,
width, height,
0, 0,
0, height,
pixel_data,
&bmi,
DIB_RGB_COLORS);
}
EndPaint(hWnd, &paint);
}
void cfg::ConfiguratorWindow::start_timer() {
if (!this->timer_running && this->hWnd) {
SetTimer(this->hWnd, RENDER_TIMER_ID, this->timer_interval_ms, nullptr);
this->timer_running = true;
}
}
void cfg::ConfiguratorWindow::stop_timer() {
if (this->timer_running && this->hWnd) {
KillTimer(this->hWnd, RENDER_TIMER_ID);
this->timer_running = false;
}
}
cfg::ConfiguratorWindow::ConfiguratorWindow() { cfg::ConfiguratorWindow::ConfiguratorWindow() {
// register the window class // register the window class
@@ -27,12 +183,22 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
wc.hIcon = WINDOW_ICON; wc.hIcon = WINDOW_ICON;
RegisterClass(&wc); RegisterClass(&wc);
// raise SetTimer resolution so high refresh rates (120/144Hz) are actually
// achievable. Without this, USER timers quantize to ~15.6ms and cap us
// near ~64 FPS. Uses the shared helper so it respects -notimerhacks
// (Use Legacy Timers) and the Win11 timer-throttling opt-out. The helper
// intentionally never calls timeEndPeriod; the kernel releases the request
// when spicecfg exits.
timeutils::set_timer_resolution();
// determine window title // determine window title
if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) { if (cfg::CONFIGURATOR_TYPE == cfg::ConfigType::Config) {
WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")"; WINDOW_TITLE = "spice2x config (" + to_string(VERSION_STRING_CFG) + ")";
WINDOW_SIZE_X = 800; WINDOW_SIZE_X = 800;
WINDOW_SIZE_Y = 600; WINDOW_SIZE_Y = 600;
} }
this->client_width = WINDOW_SIZE_X;
this->client_height = WINDOW_SIZE_Y;
// open window // open window
this->hWnd = CreateWindowEx( this->hWnd = CreateWindowEx(
@@ -53,11 +219,23 @@ cfg::ConfiguratorWindow::ConfiguratorWindow() {
cfg::ConfiguratorWindow::~ConfiguratorWindow() { cfg::ConfiguratorWindow::~ConfiguratorWindow() {
this->stop_timer();
// close window // close window
DestroyWindow(this->hWnd); DestroyWindow(this->hWnd);
// unregister class // unregister class
UnregisterClass(CLASS_NAME, GetModuleHandle(NULL)); UnregisterClass(CLASS_NAME, GetModuleHandle(NULL));
// release D3D9 resources if any
if (this->device) {
this->device->Release();
this->device = nullptr;
}
if (this->d3d) {
this->d3d->Release();
this->d3d = nullptr;
}
} }
void cfg::ConfiguratorWindow::run() { void cfg::ConfiguratorWindow::run() {
@@ -67,8 +245,23 @@ void cfg::ConfiguratorWindow::run() {
ShowWindow(this->hWnd, SW_SHOWNORMAL); ShowWindow(this->hWnd, SW_SHOWNORMAL);
UpdateWindow(this->hWnd); UpdateWindow(this->hWnd);
// draw overlay in 60 FPS // SW_SHOWNORMAL usually activates a top-level window, but not always (e.g.,
SetTimer(this->hWnd, 1, 1000 / 60, nullptr); // when the launching process doesn't have foreground rights to transfer).
// Force foreground+focus explicitly so WM_MOUSEWHEEL is routed to us from
// the very first frame; the Win32 default routes wheel events to the
// keyboard-focused window.
SetForegroundWindow(this->hWnd);
SetFocus(this->hWnd);
// match the render timer to the monitor refresh rate so scrolling stays smooth
// on 60/120/144 Hz panels. Idle CPU is bounded by overlay::sw_pixels_dirty
// (software path) and overlay::d3d9_frame_dirty (D3D9 path); the timer is also
// paused entirely when the window is minimized (see WM_SIZE handler).
const UINT hz = detect_monitor_refresh_hz(this->hWnd);
this->timer_interval_ms = std::max<UINT>(1, 1000 / hz);
log_info("configurator", "render timer {} ms ({} Hz)",
this->timer_interval_ms, hz);
this->start_timer();
// window loop // window loop
BOOL ret; BOOL ret;
@@ -102,6 +295,72 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
break; break;
} }
case WM_SIZE: {
// pause/resume the render timer based on visibility
auto *state = get_state(hWnd);
if (state) {
if (wParam == SIZE_MINIMIZED) {
state->window_minimized = true;
state->stop_timer();
} else {
if (state->window_minimized) {
state->window_minimized = false;
// force a full repaint on the next frame
state->has_valid_draw_hash = false;
state->start_timer();
}
const int new_w = LOWORD(lParam);
const int new_h = HIWORD(lParam);
if (new_w > 0 && new_h > 0
&& (new_w != state->client_width || new_h != state->client_height)) {
state->client_width = new_w;
state->client_height = new_h;
// reset the D3D9 device with the new back-buffer size so the
// hardware-accelerated path matches the window dimensions.
if (state->use_d3d9 && state->device) {
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_invalidate();
}
state->pp.BackBufferWidth = static_cast<UINT>(new_w);
state->pp.BackBufferHeight = static_cast<UINT>(new_h);
HRESULT hr = state->device->Reset(&state->pp);
if (FAILED(hr)) {
log_warning("configurator", "D3D9 device Reset failed, hr={:#x}",
static_cast<unsigned int>(hr));
}
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_recreate();
}
}
// force a full repaint after resize
state->has_valid_draw_hash = false;
}
}
}
break;
}
case WM_DISPLAYCHANGE: {
// monitor refresh rate may have changed (or the window moved to a
// different monitor); re-detect and re-arm the render timer.
auto *state = get_state(hWnd);
if (state && state->timer_running) {
const UINT hz = detect_monitor_refresh_hz(hWnd);
const UINT new_interval = std::max<UINT>(1, 1000 / hz);
if (new_interval != state->timer_interval_ms) {
state->stop_timer();
state->timer_interval_ms = new_interval;
state->start_timer();
log_info("configurator",
"WM_DISPLAYCHANGE: render timer {} ms ({} Hz)",
new_interval, hz);
}
}
break;
}
case WM_CLOSE: case WM_CLOSE:
case WM_DESTROY: { case WM_DESTROY: {
@@ -110,62 +369,189 @@ LRESULT CALLBACK cfg::ConfiguratorWindow::window_proc(HWND hWnd, UINT uMsg, WPAR
break; break;
} }
case WM_TIMER: { case WM_TIMER: {
if (wParam != RENDER_TIMER_ID) {
break;
}
// update overlay auto *state = get_state(hWnd);
// skip rendering when the window is minimized or hidden - the timer is
// already paused on minimize, but defensively skip here as well.
if (state && (state->window_minimized || !IsWindowVisible(hWnd))) {
break;
}
const bool use_d3d9 = state && state->use_d3d9 && state->device;
// build the imgui frame (input is already buffered via WM_* messages)
if (overlay::OVERLAY) { if (overlay::OVERLAY) {
overlay::OVERLAY->update(); overlay::OVERLAY->update();
overlay::OVERLAY->set_active(true); overlay::OVERLAY->set_active(true);
overlay::OVERLAY->new_frame(); overlay::OVERLAY->new_frame();
}
if (use_d3d9) {
const HRESULT cl = state->device->TestCooperativeLevel();
if (cl == D3DERR_DEVICELOST) {
break;
}
if (cl == D3DERR_DEVICENOTRESET) {
if (overlay::OVERLAY) {
overlay::OVERLAY->reset_invalidate();
}
const HRESULT reset_hr = state->device->Reset(&state->pp);
if (SUCCEEDED(reset_hr) && overlay::OVERLAY) {
overlay::OVERLAY->reset_recreate();
}
break;
}
if (FAILED(cl)) {
break;
}
if (overlay::OVERLAY) {
overlay::OVERLAY->render(); overlay::OVERLAY->render();
} }
// repaint window // skip Clear/Present when ImGui draw data is unchanged; the last
InvalidateRect(hWnd, nullptr, TRUE); // presented frame stays visible (same fast path as sw_pixels_dirty).
const bool dirty = overlay::OVERLAY && overlay::OVERLAY->d3d9_frame_dirty;
const bool force_present = state && !state->has_valid_draw_hash;
if (dirty || force_present) {
if (state) {
state->has_valid_draw_hash = true;
}
state->device->Clear(0, nullptr, D3DCLEAR_TARGET,
D3DCOLOR_RGBA(20, 18, 18, 255), 1.0f, 0);
if (SUCCEEDED(state->device->BeginScene())) {
if (overlay::OVERLAY) {
overlay::OVERLAY->d3d9_render_draw(force_present);
}
state->device->EndScene();
}
const HRESULT hr = state->device->Present(nullptr, nullptr, nullptr, nullptr);
if (hr == D3DERR_DEVICELOST) {
break;
}
}
} else {
if (overlay::OVERLAY) {
overlay::OVERLAY->render();
}
// software path: the overlay's renderer already skipped the costly
// paint_imgui call when the draw data was unchanged. Only invalidate
// the window when those pixels actually changed, or on the first
// frame / after a resize when we lost the previously cached state.
const bool dirty = overlay::OVERLAY && overlay::OVERLAY->sw_pixels_dirty;
const bool force_repaint = state && !state->has_valid_draw_hash;
if (dirty || force_repaint) {
if (state) {
state->has_valid_draw_hash = true;
}
// pass FALSE for bErase - WM_ERASEBKGND already short-circuits, so
// skipping the erase region avoids one extra Win32 round trip.
InvalidateRect(hWnd, nullptr, FALSE);
}
}
break; break;
} }
case WM_ERASEBKGND: { case WM_ERASEBKGND: {
return 1; return 1;
} }
case WM_PAINT: { case WM_PAINT: {
paint_software(hWnd);
// render overlay break;
if (overlay::OVERLAY) {
// get pixel data
int width, height;
uint32_t *pixel_data = overlay::OVERLAY->sw_get_pixel_data(&width, &height);
if (width > 0 && height > 0) {
// create bitmap
HBITMAP bitmap = CreateBitmap(width, height, 1, 8 * sizeof(uint32_t), pixel_data);
// prepare paint
PAINTSTRUCT paint{};
HDC hdc = BeginPaint(hWnd, &paint);
HDC hdcMem = CreateCompatibleDC(hdc);
SetBkMode(hdc, TRANSPARENT);
// draw bitmap
SelectObject(hdcMem, bitmap);
BitBlt(hdc, paint.rcPaint.left, paint.rcPaint.top,
paint.rcPaint.right - paint.rcPaint.left,
paint.rcPaint.bottom - paint.rcPaint.top,
hdcMem, paint.rcPaint.left, paint.rcPaint.top, SRCCOPY);
// delete bitmap
DeleteObject(bitmap);
// clean up
DeleteDC(hdcMem);
EndPaint(hWnd, &paint);
} else {
return DefWindowProc(hWnd, uMsg, wParam, lParam);
} }
case WM_ACTIVATE: {
const WORD activation = LOWORD(wParam);
if (activation == WA_INACTIVE) {
// dropping any held mouse buttons so we don't get stuck in a
// "button still pressed" state when we come back.
auto &io = ImGui::GetIO();
io.AddMouseButtonEvent(ImGuiMouseButton_Left, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Right, false);
io.AddMouseButtonEvent(ImGuiMouseButton_Middle, false);
} else { } else {
return DefWindowProc(hWnd, uMsg, wParam, lParam); // WA_ACTIVE or WA_CLICKACTIVE: make sure the keyboard focus is
// actually on this window so WM_MOUSEWHEEL gets routed to us
// again. Without this, scrolling silently stops working after
// the user alt-tabs back to spicecfg.
SetFocus(hWnd);
} }
break; break;
} }
case WM_KILLFOCUS: {
// mirror the WA_INACTIVE mouse-button drop for the keyboard -
// without this, a key held during alt-tab stays "down" in ImGui
// state until the user presses and releases it again.
ImGui::GetIO().ClearInputKeys();
break;
}
// input messages routed straight into ImGui IO. This replaces the previous
// per-frame 256-VK rawinput scan in ImGui_ImplSpice_NewFrame for the
// standalone configurator (see CONFIGURATOR_STANDALONE branches there).
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP: {
const bool down = (uMsg == WM_KEYDOWN) || (uMsg == WM_SYSKEYDOWN);
auto &io = ImGui::GetIO();
const ImGuiKey key = vk_to_imgui_key(wParam);
if (key != ImGuiKey_None) {
io.AddKeyEvent(key, down);
}
const ImGuiKey mod = vk_to_imgui_mod_key(wParam);
if (mod != ImGuiMod_None) {
io.AddKeyEvent(mod, down);
}
break;
}
case WM_MOUSEMOVE: {
auto &io = ImGui::GetIO();
io.AddMousePosEvent(static_cast<float>(GET_X_LPARAM(lParam)),
static_cast<float>(GET_Y_LPARAM(lParam)));
break;
}
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_RBUTTONDOWN:
case WM_RBUTTONUP:
case WM_MBUTTONDOWN:
case WM_MBUTTONUP: {
int button = 0;
bool down = false;
switch (uMsg) {
case WM_LBUTTONDOWN: button = ImGuiMouseButton_Left; down = true; break;
case WM_LBUTTONUP: button = ImGuiMouseButton_Left; down = false; break;
case WM_RBUTTONDOWN: button = ImGuiMouseButton_Right; down = true; break;
case WM_RBUTTONUP: button = ImGuiMouseButton_Right; down = false; break;
case WM_MBUTTONDOWN: button = ImGuiMouseButton_Middle; down = true; break;
case WM_MBUTTONUP: button = ImGuiMouseButton_Middle; down = false; break;
}
auto &io = ImGui::GetIO();
io.AddMouseButtonEvent(button, down);
if (down) {
SetCapture(hWnd);
} else {
ReleaseCapture();
}
break;
}
case WM_MOUSEWHEEL: {
const float delta = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam))
/ static_cast<float>(WHEEL_DELTA);
ImGui::GetIO().AddMouseWheelEvent(0.0f, delta);
break;
}
#if !SPICE_XP
case WM_MOUSEHWHEEL: {
const float delta = static_cast<float>(GET_WHEEL_DELTA_WPARAM(wParam))
/ static_cast<float>(WHEEL_DELTA);
ImGui::GetIO().AddMouseWheelEvent(delta, 0.0f);
break;
}
#endif
default: default:
return DefWindowProc(hWnd, uMsg, wParam, lParam); return DefWindowProc(hWnd, uMsg, wParam, lParam);
} }
+31 -1
View File
@@ -1,6 +1,9 @@
#pragma once #pragma once
#include <cstdint>
#include <windows.h> #include <windows.h>
#include <d3d9.h>
namespace cfg { namespace cfg {
@@ -9,11 +12,38 @@ namespace cfg {
HWND hWnd; HWND hWnd;
// optional D3D9 device backing the configurator window; nullptr when running
// the software-rendered path. Owned by ConfiguratorWindow when set.
IDirect3D9 *d3d = nullptr;
IDirect3DDevice9 *device = nullptr;
D3DPRESENT_PARAMETERS pp {};
bool use_d3d9 = false;
// throttling / pause state. timer_interval_ms is the default until run()
// refines it to the actual monitor refresh rate (see detect_monitor_refresh_hz).
UINT timer_interval_ms = 1000 / 60;
bool timer_running = false;
bool window_minimized = false;
// tracks whether we've issued at least one InvalidateRect since window
// creation/resize. The overlay's per-frame "pixels changed" flag suppresses
// idle blits; this flag forces the very first blit on startup or after
// a resize so the window doesn't show garbage until the user moves the mouse.
bool has_valid_draw_hash = false;
// dimensions of the configurator client area (kept in sync with WM_SIZE)
int client_width = 0;
int client_height = 0;
ConfiguratorWindow(); ConfiguratorWindow();
~ConfiguratorWindow(); ~ConfiguratorWindow();
void run(); void run();
// start/stop the render timer based on visibility state
void start_timer();
void stop_timer();
static LRESULT CALLBACK window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); static LRESULT CALLBACK window_proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
}; };
} }
+23 -6
View File
@@ -369,6 +369,14 @@ void ImGui_ImplSpice_NewFrame() {
const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_active(); const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_active();
const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE && overlay_visible; const auto accept_new_input = superexit::has_focus() && !rawinput::OS_WINDOW_ACTIVE && overlay_visible;
// when running as standalone spicecfg.exe the configurator window proc feeds
// ImGui directly via WM_KEY*/WM_MOUSE*/WM_MOUSEWHEEL messages. The per-frame
// rawinput device walk and the 256-VK scan below are pure CPU waste in that
// case (and the dominant per-frame cost on low-end PCs), so short-circuit
// them entirely. Rebind dialogs that explicitly need fresh device state call
// RI_MGR->devices_get_updated() themselves; see overlay/windows/config.cpp.
const bool drive_input_from_rawinput = !cfg::CONFIGURATOR_STANDALONE;
// remember old state // remember old state
std::array<BYTE, VKEY_MAX> KeysDownOld; std::array<BYTE, VKEY_MAX> KeysDownOld;
for (size_t i = 0; i < VKEY_MAX; i++) { for (size_t i = 0; i < VKEY_MAX; i++) {
@@ -378,11 +386,13 @@ void ImGui_ImplSpice_NewFrame() {
const auto MouseDownOld = g_MouseDown; const auto MouseDownOld = g_MouseDown;
// reset keys state // reset keys state
if (drive_input_from_rawinput) {
g_MouseDown.fill(false); g_MouseDown.fill(false);
g_KeysDown.fill(false); g_KeysDown.fill(false);
}
// apply windows mouse buttons // apply windows mouse buttons
if (accept_new_input) { if (accept_new_input && drive_input_from_rawinput) {
g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse(); g_MouseDown[ImGuiMouseButton_Left] |= get_async_primary_mouse();
g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse(); g_MouseDown[ImGuiMouseButton_Right] |= get_async_secondary_mouse();
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0; g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
@@ -391,7 +401,7 @@ void ImGui_ImplSpice_NewFrame() {
// read new keys state // read new keys state
static long mouse_wheel_last = 0; static long mouse_wheel_last = 0;
long mouse_wheel = 0; long mouse_wheel = 0;
if (RI_MGR != nullptr) { if (drive_input_from_rawinput && RI_MGR != nullptr) {
auto devices = RI_MGR->devices_get(); auto devices = RI_MGR->devices_get();
for (auto &device : devices) { for (auto &device : devices) {
switch (device.type) { switch (device.type) {
@@ -447,7 +457,10 @@ void ImGui_ImplSpice_NewFrame() {
} }
} }
// process keyboard input from all keyboard collapsed into one state (g_KeysDown) // process keyboard input from all keyboards collapsed into one state (g_KeysDown).
// Skipped entirely in standalone configurator mode where Win32 messages already
// drive io.AddKeyEvent / io.AddInputCharacter directly.
if (drive_input_from_rawinput) {
for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) { for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) {
const bool state = g_KeysDown[vKey]; const bool state = g_KeysDown[vKey];
const auto imgui_key = get_imgui_key(vKey); const auto imgui_key = get_imgui_key(vKey);
@@ -486,29 +499,33 @@ void ImGui_ImplSpice_NewFrame() {
} }
} }
} }
}
// set mouse wheel // set mouse wheel
long wheel_diff = mouse_wheel - mouse_wheel_last; long wheel_diff = mouse_wheel - mouse_wheel_last;
mouse_wheel_last = mouse_wheel; mouse_wheel_last = mouse_wheel;
if (wheel_diff != 0 && accept_new_input) { if (wheel_diff != 0 && accept_new_input && drive_input_from_rawinput) {
io.AddMouseWheelEvent(0, wheel_diff); io.AddMouseWheelEvent(0, wheel_diff);
} }
// update OS mouse position // update OS mouse position. The standalone configurator gets mouse position
// straight from WM_MOUSEMOVE, so skip the per-frame cursor poll there.
const auto old_mouse_pos = io.MousePos; const auto old_mouse_pos = io.MousePos;
if (accept_new_input) { if (accept_new_input && drive_input_from_rawinput) {
ImGui_ImplSpice_UpdateMousePos(); ImGui_ImplSpice_UpdateMousePos();
} }
const auto new_mouse_pos = io.MousePos; const auto new_mouse_pos = io.MousePos;
// update mouse buttons // update mouse buttons
// doing this after ImGui_ImplSpice_UpdateMousePos since it can set g_MouseDown for touch input // doing this after ImGui_ImplSpice_UpdateMousePos since it can set g_MouseDown for touch input
if (drive_input_from_rawinput) {
for (size_t i = 0; i < g_MouseDown.size(); i++) { for (size_t i = 0; i < g_MouseDown.size(); i++) {
if (MouseDownOld[i] != g_MouseDown[i]) { if (MouseDownOld[i] != g_MouseDown[i]) {
io.AddMouseButtonEvent(i, g_MouseDown[i]); io.AddMouseButtonEvent(i, g_MouseDown[i]);
log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]); log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]);
} }
} }
}
// automatically hide cursor // automatically hide cursor
if (g_MouseCursorAutoHide) { if (g_MouseCursorAutoHide) {
+107 -2
View File
@@ -327,8 +327,9 @@ void overlay::SpiceOverlay::init() {
// disable config // disable config
io.IniFilename = nullptr; io.IniFilename = nullptr;
// allow CTRL+WHEEL scaling // allow CTRL+WHEEL scaling in the in-game overlay; disable for the standalone
io.FontAllowUserScaling = true; // configurator so the mouse wheel always scrolls the configuration window.
io.FontAllowUserScaling = !cfg::CONFIGURATOR_STANDALONE;
// add default font // add default font
io.Fonts->AddFontDefaultBitmap(); io.Fonts->AddFontDefaultBitmap();
@@ -590,6 +591,57 @@ void overlay::SpiceOverlay::new_frame() {
ImGui::EndFrame(); ImGui::EndFrame();
} }
// FNV-1a 64-bit hash helper used by the software renderer to detect idle frames.
static inline uint64_t overlay_fnv1a64(uint64_t h, const void *data, size_t len) {
const auto *p = static_cast<const uint8_t *>(data);
for (size_t i = 0; i < len; i++) {
h ^= p[i];
h *= 0x100000001B3ULL;
}
return h;
}
// Compute a hash over the visual content of ImDrawData. Only the geometry the
// software rasterizer actually consumes (vertex/index buffers + display size)
// feeds into the hash, so frames where ImGui produces identical draw data
// (i.e. nothing animated this tick) can skip the full pixel rasterization.
static uint64_t overlay_hash_draw_data(const ImDrawData *draw_data) {
uint64_t h = 0xcbf29ce484222325ULL;
if (draw_data == nullptr) {
return h;
}
const float display[4] = {
draw_data->DisplayPos.x,
draw_data->DisplayPos.y,
draw_data->DisplaySize.x,
draw_data->DisplaySize.y,
};
h = overlay_fnv1a64(h, display, sizeof(display));
const int cmd_lists = draw_data->CmdListsCount;
h = overlay_fnv1a64(h, &cmd_lists, sizeof(cmd_lists));
for (int i = 0; i < draw_data->CmdListsCount; i++) {
const ImDrawList *cmd_list = draw_data->CmdLists[i];
if (cmd_list == nullptr) {
continue;
}
const int vtx_count = cmd_list->VtxBuffer.Size;
const int idx_count = cmd_list->IdxBuffer.Size;
h = overlay_fnv1a64(h, &vtx_count, sizeof(vtx_count));
h = overlay_fnv1a64(h, &idx_count, sizeof(idx_count));
if (vtx_count > 0) {
h = overlay_fnv1a64(h, cmd_list->VtxBuffer.Data,
static_cast<size_t>(vtx_count) * sizeof(ImDrawVert));
}
if (idx_count > 0) {
h = overlay_fnv1a64(h, cmd_list->IdxBuffer.Data,
static_cast<size_t>(idx_count) * sizeof(ImDrawIdx));
}
}
return h;
}
void overlay::SpiceOverlay::render() { void overlay::SpiceOverlay::render() {
// skip if new_frame() didn't begin a frame this tick // skip if new_frame() didn't begin a frame this tick
@@ -603,7 +655,28 @@ void overlay::SpiceOverlay::render() {
// implementation render // implementation render
switch (this->renderer) { switch (this->renderer) {
case OverlayRenderer::D3D9: case OverlayRenderer::D3D9:
if (cfg::CONFIGURATOR_STANDALONE) {
const auto *draw_data = ImGui::GetDrawData();
const uint64_t draw_hash = overlay_hash_draw_data(draw_data);
const auto &io = ImGui::GetIO();
const int display_w = static_cast<int>(std::ceil(io.DisplaySize.x));
const int display_h = static_cast<int>(std::ceil(io.DisplaySize.y));
const bool size_matches = (this->d3d9_last_display_w == display_w
&& this->d3d9_last_display_h == display_h);
if (this->d3d9_has_last_draw_hash
&& draw_hash == this->d3d9_last_draw_hash
&& size_matches) {
this->d3d9_frame_dirty = false;
break;
}
this->d3d9_last_draw_hash = draw_hash;
this->d3d9_has_last_draw_hash = true;
this->d3d9_last_display_w = display_w;
this->d3d9_last_display_h = display_h;
this->d3d9_frame_dirty = true;
} else {
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData()); ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
}
break; break;
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
case OverlayRenderer::D3D11: case OverlayRenderer::D3D11:
@@ -619,6 +692,23 @@ void overlay::SpiceOverlay::render() {
auto height = static_cast<size_t>(std::ceil(io.DisplaySize.y)); auto height = static_cast<size_t>(std::ceil(io.DisplaySize.y));
auto pixels = width * height; auto pixels = width * height;
// skip the (expensive) full software rasterization when the draw data
// is byte-identical to the previous frame and the existing pixel
// buffer still matches the current display size.
const auto *draw_data = ImGui::GetDrawData();
const uint64_t draw_hash = overlay_hash_draw_data(draw_data);
const bool size_matches = (this->pixel_data_width == width
&& this->pixel_data_height == height
&& this->pixel_data.size() >= pixels);
if (this->sw_has_last_draw_hash
&& draw_hash == this->sw_last_draw_hash
&& size_matches) {
this->sw_pixels_dirty = false;
break;
}
this->sw_last_draw_hash = draw_hash;
this->sw_has_last_draw_hash = true;
// make sure buffer is big enough // make sure buffer is big enough
if (this->pixel_data.size() < pixels) { if (this->pixel_data.size() < pixels) {
this->pixel_data.resize(pixels, 0); this->pixel_data.resize(pixels, 0);
@@ -635,6 +725,7 @@ void overlay::SpiceOverlay::render() {
imgui_sw::paint_imgui(&this->pixel_data[0], width, height, options); imgui_sw::paint_imgui(&this->pixel_data[0], width, height, options);
pixel_data_width = width; pixel_data_width = width;
pixel_data_height = height; pixel_data_height = height;
this->sw_pixels_dirty = true;
break; break;
} }
@@ -647,6 +738,16 @@ void overlay::SpiceOverlay::render() {
this->has_pending_frame = false; this->has_pending_frame = false;
} }
void overlay::SpiceOverlay::d3d9_render_draw(const bool force_submit) {
if (this->renderer != OverlayRenderer::D3D9) {
return;
}
if (!force_submit && !this->d3d9_frame_dirty) {
return;
}
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
}
void overlay::SpiceOverlay::update() { void overlay::SpiceOverlay::update() {
// check overlay toggle // check overlay toggle
@@ -795,6 +896,10 @@ void overlay::SpiceOverlay::reset_invalidate() {
} }
switch (overlay::OVERLAY->renderer) { switch (overlay::OVERLAY->renderer) {
case OverlayRenderer::D3D9: case OverlayRenderer::D3D9:
if (cfg::CONFIGURATOR_STANDALONE) {
overlay::OVERLAY->d3d9_has_last_draw_hash = false;
overlay::OVERLAY->d3d9_frame_dirty = true;
}
ImGui_ImplDX9_InvalidateDeviceObjects(); ImGui_ImplDX9_InvalidateDeviceObjects();
break; break;
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
+20
View File
@@ -83,6 +83,9 @@ namespace overlay {
void window_add(Window *wnd); void window_add(Window *wnd);
void new_frame(); void new_frame();
void render(); void render();
// configurator-only (spicecfg D3D9): submit ImGui draw data inside BeginScene
// after render() when the frame is being presented. In-game overlay draws in render().
void d3d9_render_draw(bool force_submit = false);
void update(); void update();
void toggle_active(bool overlay_key = false); void toggle_active(bool overlay_key = false);
void show_main_menu(); void show_main_menu();
@@ -139,6 +142,16 @@ namespace overlay {
OverlayRenderer renderer; OverlayRenderer renderer;
float total_elapsed = 0.f; float total_elapsed = 0.f;
// set to true by the SOFTWARE renderer when the pixel buffer was updated
// in the most recent render() call. Allows the configurator's WM_PAINT
// driver to skip its full-window InvalidateRect on idle frames where the
// ImGui draw data is bitwise-identical to the previous frame.
bool sw_pixels_dirty = false;
// configurator-only (spicecfg D3D9): set by render() when ImGui draw data
// changed since the last submitted frame; configurator skips Clear/Present when false.
bool d3d9_frame_dirty = false;
private: private:
HWND hWnd = nullptr; HWND hWnd = nullptr;
@@ -159,6 +172,13 @@ namespace overlay {
std::vector<uint32_t> pixel_data; std::vector<uint32_t> pixel_data;
size_t pixel_data_width = 0; size_t pixel_data_width = 0;
size_t pixel_data_height = 0; size_t pixel_data_height = 0;
uint64_t sw_last_draw_hash = 0;
bool sw_has_last_draw_hash = false;
uint64_t d3d9_last_draw_hash = 0;
bool d3d9_has_last_draw_hash = false;
int d3d9_last_display_w = 0;
int d3d9_last_display_h = 0;
std::vector<std::unique_ptr<Window>> windows; std::vector<std::unique_ptr<Window>> windows;
+4
View File
@@ -78,6 +78,10 @@ namespace overlay::windows {
this->flags |= ImGuiWindowFlags_NoTitleBar; this->flags |= ImGuiWindowFlags_NoTitleBar;
this->flags |= ImGuiWindowFlags_NoCollapse; this->flags |= ImGuiWindowFlags_NoCollapse;
this->flags |= ImGuiWindowFlags_NoDecoration; this->flags |= ImGuiWindowFlags_NoDecoration;
// prevent the parent window from absorbing wheel events when a hovered
// child has no scrollable content (otherwise the parent's tiny overflow
// causes the whole UI to jolt by a few pixels each tick)
this->flags |= ImGuiWindowFlags_NoScrollWithMouse;
} }
this->flags |= ImGuiWindowFlags_MenuBar; this->flags |= ImGuiWindowFlags_MenuBar;