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
+59 -42
View File
@@ -369,6 +369,14 @@ void ImGui_ImplSpice_NewFrame() {
const auto overlay_visible = overlay::OVERLAY && overlay::OVERLAY->get_active();
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
std::array<BYTE, VKEY_MAX> KeysDownOld;
for (size_t i = 0; i < VKEY_MAX; i++) {
@@ -378,11 +386,13 @@ void ImGui_ImplSpice_NewFrame() {
const auto MouseDownOld = g_MouseDown;
// reset keys state
g_MouseDown.fill(false);
g_KeysDown.fill(false);
if (drive_input_from_rawinput) {
g_MouseDown.fill(false);
g_KeysDown.fill(false);
}
// 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_Right] |= get_async_secondary_mouse();
g_MouseDown[ImGuiMouseButton_Middle] |= (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0;
@@ -391,7 +401,7 @@ void ImGui_ImplSpice_NewFrame() {
// read new keys state
static long mouse_wheel_last = 0;
long mouse_wheel = 0;
if (RI_MGR != nullptr) {
if (drive_input_from_rawinput && RI_MGR != nullptr) {
auto devices = RI_MGR->devices_get();
for (auto &device : devices) {
switch (device.type) {
@@ -447,41 +457,45 @@ void ImGui_ImplSpice_NewFrame() {
}
}
// process keyboard input from all keyboard collapsed into one state (g_KeysDown)
for (size_t vKey = 0; vKey < VKEY_MAX; vKey++) {
const bool state = g_KeysDown[vKey];
const auto imgui_key = get_imgui_key(vKey);
const auto changed =
(state && !KeysDownOld[vKey]) ||
(!state && KeysDownOld[vKey]);
// 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++) {
const bool state = g_KeysDown[vKey];
const auto imgui_key = get_imgui_key(vKey);
const auto changed =
(state && !KeysDownOld[vKey]) ||
(!state && KeysDownOld[vKey]);
if (imgui_key != ImGuiKey_None && changed) {
io.AddKeyEvent(imgui_key, state);
log_debug("imgui_impl_spice", "vkey {:#x} added as navigation event, state: {}", static_cast<uint64_t>(vKey), state);
if (imgui_key != ImGuiKey_None && changed) {
io.AddKeyEvent(imgui_key, state);
log_debug("imgui_impl_spice", "vkey {:#x} added as navigation event, state: {}", static_cast<uint64_t>(vKey), state);
// mod key must also be processed separately
const auto imgui_mod_key = get_imgui_mod_key(vKey);
if (imgui_mod_key != ImGuiMod_None) {
io.AddKeyEvent(imgui_mod_key, state);
// mod key must also be processed separately
const auto imgui_mod_key = get_imgui_mod_key(vKey);
if (imgui_mod_key != ImGuiMod_None) {
io.AddKeyEvent(imgui_mod_key, state);
}
}
}
// generate character input, but only if WM_CHAR didn't take over the functionality
// only detecting rising edges here - this means holding a key won't work
// (it's better than repeating a character input every frame - cost we pay for providing input
// on top of rawinput instead of WM_CHAR)
if (!overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT && !KeysDownOld[vKey] && state) {
UCHAR buf[2];
auto ret = ToAscii(
static_cast<UINT>(vKey),
0,
static_cast<const BYTE *>(KeysDownOld.data()),
reinterpret_cast<LPWORD>(buf),
0);
if (ret > 0) {
for (int i = 0; i < ret; i++) {
overlay::OVERLAY->input_char(buf[i]);
log_debug("imgui_impl_spice", "vkey {:#x} inputted as character", vKey);
// generate character input, but only if WM_CHAR didn't take over the functionality
// only detecting rising edges here - this means holding a key won't work
// (it's better than repeating a character input every frame - cost we pay for providing input
// on top of rawinput instead of WM_CHAR)
if (!overlay::USE_WM_CHAR_FOR_IMGUI_CHAR_INPUT && !KeysDownOld[vKey] && state) {
UCHAR buf[2];
auto ret = ToAscii(
static_cast<UINT>(vKey),
0,
static_cast<const BYTE *>(KeysDownOld.data()),
reinterpret_cast<LPWORD>(buf),
0);
if (ret > 0) {
for (int i = 0; i < ret; i++) {
overlay::OVERLAY->input_char(buf[i]);
log_debug("imgui_impl_spice", "vkey {:#x} inputted as character", vKey);
}
}
}
}
@@ -490,23 +504,26 @@ void ImGui_ImplSpice_NewFrame() {
// set mouse wheel
long wheel_diff = mouse_wheel - mouse_wheel_last;
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);
}
// 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;
if (accept_new_input) {
if (accept_new_input && drive_input_from_rawinput) {
ImGui_ImplSpice_UpdateMousePos();
}
const auto new_mouse_pos = io.MousePos;
// update mouse buttons
// doing this after ImGui_ImplSpice_UpdateMousePos since it can set g_MouseDown for touch input
for (size_t i = 0; i < g_MouseDown.size(); i++) {
if (MouseDownOld[i] != g_MouseDown[i]) {
io.AddMouseButtonEvent(i, g_MouseDown[i]);
log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]);
if (drive_input_from_rawinput) {
for (size_t i = 0; i < g_MouseDown.size(); i++) {
if (MouseDownOld[i] != g_MouseDown[i]) {
io.AddMouseButtonEvent(i, g_MouseDown[i]);
log_debug("imgui_impl_spice", "mouse button {} event", g_MouseDown[i]);
}
}
}
+108 -3
View File
@@ -327,8 +327,9 @@ void overlay::SpiceOverlay::init() {
// disable config
io.IniFilename = nullptr;
// allow CTRL+WHEEL scaling
io.FontAllowUserScaling = true;
// allow CTRL+WHEEL scaling in the in-game overlay; disable for the standalone
// configurator so the mouse wheel always scrolls the configuration window.
io.FontAllowUserScaling = !cfg::CONFIGURATOR_STANDALONE;
// add default font
io.Fonts->AddFontDefaultBitmap();
@@ -590,6 +591,57 @@ void overlay::SpiceOverlay::new_frame() {
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() {
// skip if new_frame() didn't begin a frame this tick
@@ -603,7 +655,28 @@ void overlay::SpiceOverlay::render() {
// implementation render
switch (this->renderer) {
case OverlayRenderer::D3D9:
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
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());
}
break;
#ifdef SPICE_D3D11
case OverlayRenderer::D3D11:
@@ -619,6 +692,23 @@ void overlay::SpiceOverlay::render() {
auto height = static_cast<size_t>(std::ceil(io.DisplaySize.y));
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
if (this->pixel_data.size() < pixels) {
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);
pixel_data_width = width;
pixel_data_height = height;
this->sw_pixels_dirty = true;
break;
}
@@ -647,6 +738,16 @@ void overlay::SpiceOverlay::render() {
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() {
// check overlay toggle
@@ -795,6 +896,10 @@ void overlay::SpiceOverlay::reset_invalidate() {
}
switch (overlay::OVERLAY->renderer) {
case OverlayRenderer::D3D9:
if (cfg::CONFIGURATOR_STANDALONE) {
overlay::OVERLAY->d3d9_has_last_draw_hash = false;
overlay::OVERLAY->d3d9_frame_dirty = true;
}
ImGui_ImplDX9_InvalidateDeviceObjects();
break;
#ifdef SPICE_D3D11
+20
View File
@@ -83,6 +83,9 @@ namespace overlay {
void window_add(Window *wnd);
void new_frame();
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 toggle_active(bool overlay_key = false);
void show_main_menu();
@@ -139,6 +142,16 @@ namespace overlay {
OverlayRenderer renderer;
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:
HWND hWnd = nullptr;
@@ -159,6 +172,13 @@ namespace overlay {
std::vector<uint32_t> pixel_data;
size_t pixel_data_width = 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;
+4
View File
@@ -78,6 +78,10 @@ namespace overlay::windows {
this->flags |= ImGuiWindowFlags_NoTitleBar;
this->flags |= ImGuiWindowFlags_NoCollapse;
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;