From 6bdee40219c5c43e126e46a606471228ce6b2371 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Wed, 24 Jun 2026 01:38:52 +0200 Subject: [PATCH] Release held keys/buttons when MKB forwarding stops (no sticky inputs) forward_mkb_frame early-returned the whole mouse block when mirroring was off or ImGui wanted the mouse, and the top-level gates returned when the subsystem was off / focus was lost / the game died. A key or mouse button held at that moment never got its KeyUp/MouseUp, so it stuck DOWN in the guest -- a held mouse button fires continuously, a held key walks forever -- contradicting the "send the up so nothing sticks" intent. Track what we've forwarded as held (g_mouse_down / g_key_down) and release it whenever we stop forwarding for any reason: the can't-forward gate, ImGui grabbing the keyboard/mouse, or the mouse-not-mirroring path all now release held inputs before returning. Normal down/up still flips the held state. Fix by inspection: forward_mkb_frame needs a live ImGui context + injection panel, so it isn't unit-tested; the logic is a straightforward held-state release. Co-Authored-By: Claude Opus 4.8 --- README.md | 2 - host/src/inject/mkb_forward.cpp | 66 +++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9771aea..242f773 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,6 @@ From an in-depth review pass. Each item is fixed test-first (a failing test, the as its own commit; "verify" items are confirmed real before any change, and dropped if not. Robustness (verify, then fix if real): -- **`mkb_forward` sticky mouse-up** — a held button isn't released when mirroring toggles off / ImGui - grabs the mouse. Release held buttons. - **Swallowed audio loopback `start()` failures** — surface a process-loopback activation failure distinctly from "no correlation" in the verifier + loopback. - **Permissive injector bitness gate** — `IsWow64Process2` failure is treated as 64-bit; fall back to diff --git a/host/src/inject/mkb_forward.cpp b/host/src/inject/mkb_forward.cpp index eba1775..03516ae 100644 --- a/host/src/inject/mkb_forward.cpp +++ b/host/src/inject/mkb_forward.cpp @@ -88,30 +88,59 @@ int imgui_key_to_vk(ImGuiKey k) int g_last_gx = 0; int g_last_gy = 0; +// What we've forwarded as currently-held, so we can always release it -- even when forwarding stops +// (mirror off, focus lost, ImGui grabbed the input, the game died) -- so nothing sticks in the guest. +bool g_mouse_down[3] = {}; +bool g_key_down[256] = {}; // indexed by VK + +void release_held_keys(InjectionPanel& injection) +{ + for (int vk = 0; vk < 256; ++vk) + { + if (g_key_down[vk]) + { + injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast(vk), 0, 0}); + g_key_down[vk] = false; + } + } +} + +void release_held_mouse(InjectionPanel& injection) +{ + for (int b = 0; b < 3; ++b) + { + if (g_mouse_down[b]) + { + injection.push_mkb(MkbEvent{Mkb_MouseUp, static_cast(b), g_last_gx, g_last_gy}); + g_mouse_down[b] = false; + } + } +} + } // namespace void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring, bool source_hooked) { - if (!injection.mkb_enabled()) - { - return; - } - // Under RPT the guest's MKB lands on our (focused) window; only forward then, so - // the operator's own desktop use isn't injected into the game. - if (GetForegroundWindow() != host_hwnd) - { - return; - } const HWND game = injection.game_hwnd(); - if (game == nullptr || !IsWindow(game)) + // Under RPT the guest's MKB lands on our (focused) window; only forward then, so the operator's + // own desktop use isn't injected. If we can't forward for ANY reason -- subsystem off, we lost + // focus, or the game is gone -- release everything we're still holding first, so a key/button + // held at that moment doesn't stick down in the guest. + if (!injection.mkb_enabled() || GetForegroundWindow() != host_hwnd || game == nullptr || !IsWindow(game)) { + release_held_keys(injection); + release_held_mouse(injection); return; } ImGuiIO& io = ImGui::GetIO(); - // --- Keyboard (always, unless ImGui is using it for e.g. a text field) --- - if (!io.WantCaptureKeyboard) + // --- Keyboard (unless ImGui is using it for e.g. a text field -- then release what we hold) --- + if (io.WantCaptureKeyboard) + { + release_held_keys(injection); + } + else { for (ImGuiKey k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k = static_cast(k + 1)) { @@ -123,10 +152,12 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring if (ImGui::IsKeyPressed(k, false)) { injection.push_mkb(MkbEvent{Mkb_KeyDown, static_cast(vk), 0, 0}); + g_key_down[vk & 0xFF] = true; } if (ImGui::IsKeyReleased(k)) { injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast(vk), 0, 0}); + g_key_down[vk & 0xFF] = false; } } for (int i = 0; i < io.InputQueueCharacters.Size; ++i) @@ -139,9 +170,12 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring } } - // --- Mouse (clicks + wheel only, and only while mirroring) --- - if (!mirroring || io.WantCaptureMouse) + // --- Mouse (clicks + wheel only, and only while mirroring and ImGui isn't using the mouse) --- + // When we're not forwarding the mouse, still release any button we hold (below), so it can't stick. + const bool forwarding_mouse = mirroring && !io.WantCaptureMouse; + if (!forwarding_mouse) { + release_held_mouse(injection); return; } @@ -191,10 +225,12 @@ void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring if (on_game && ImGui::IsMouseClicked(button)) { injection.push_mkb(MkbEvent{Mkb_MouseDown, static_cast(button), mx, my}); + g_mouse_down[button] = true; } if (ImGui::IsMouseReleased(button)) // send the up even off-game, so nothing sticks { injection.push_mkb(MkbEvent{Mkb_MouseUp, static_cast(button), mx, my}); + g_mouse_down[button] = false; } } if (on_game && io.MouseWheel != 0.0f)