Add mouse + keyboard forwarding (MKB subsystem, opt-in)
Forward the host window's clicks and keystrokes into the injected game so guests can drive menus / "Press Start" / text entry that a pad can't. Protocol (v7->v8): new HookSubsys_Mkb and an SPSC MkbRing of MkbEvents in SharedBlock (host produces, hook consumes); push/pop helpers. Hook (hook/src/mkb_hook.cpp, new subsystem): a worker-loop pump drains the ring at ~5 ms and PostMessageW's the matching window messages (WM_KEY*/WM_CHAR, mouse buttons, WM_MOUSEWHEEL) to the game's main window; it also inline-hooks user32 GetAsyncKeyState / GetKeyboardState / GetCursorPos (stdcall trampolines per the x86 rule) to report a synthesized state so polling games react too. Removing the subsystem clears all synthesized keys (no stuck input). Host: the Injection panel gets a "Mouse + keyboard forwarding" subsystem toggle (opt-in, default off -- the toggle is the hook). host/src/inject/mkb_forward.cpp reads ImGui IO each frame and forwards only when the host window is focused and ImGui isn't capturing the event; keyboard always, mouse only while mirroring (clicks + wheel, not movement). Mouse coords are mapped through the letterbox to game-client space (host/src/inject/mkb_map.hpp), accounting for WGC-of-decorated-window vs hooked/borderless. RawInput/DirectInput games are out of scope for this version. Verified: new mkb_ring_test + mkb_map_test pass; full build x64 + x86 clean; ctest x64 9/9 and x86 3/3 green (no regression from the protocol bump). The subsystem is opt-in, so it can't affect existing behavior unless enabled; the end-to-end click-into-game path needs live Remote Play + a real game to confirm. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
18
README.md
18
README.md
@@ -25,6 +25,7 @@ and forwards guest controllers back into it.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Receive guest input | XInput (RPT delivers guest pads to the focused window); optional, opt-in Steam Input when built with the Steamworks SDK | `coop_host.exe` |
|
| Receive guest input | XInput (RPT delivers guest pads to the focused window); optional, opt-in Steam Input when built with the Steamworks SDK | `coop_host.exe` |
|
||||||
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees *only* our pad | `coop_hook.dll` |
|
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees *only* our pad | `coop_hook.dll` |
|
||||||
|
| Forward mouse + keyboard | Opt-in MKB subsystem: host streams its window's clicks/keys, the hook posts the matching window messages and synthesizes `GetAsyncKeyState`/`GetKeyboardState`/`GetCursorPos` for polling games | `coop_hook.dll` + `coop_host.exe` |
|
||||||
| Keep game running unfocused | Hook spoofs focus so the game polls while the host holds OS focus | `coop_hook.dll` |
|
| Keep game running unfocused | Hook spoofs focus so the game polls while the host holds OS focus | `coop_hook.dll` |
|
||||||
| Mirror video (default) | Windows Graphics Capture of the game window, letterboxed into the host window | `coop_host.exe` |
|
| Mirror video (default) | Windows Graphics Capture of the game window, letterboxed into the host window | `coop_host.exe` |
|
||||||
| Mirror video (hooked) | Injected Present / OpenGL hook copies the backbuffer into a shared keyed-mutex texture the host samples (lower latency, no capture border) | `coop_hook.dll` + `coop_host.exe` |
|
| Mirror video (hooked) | Injected Present / OpenGL hook copies the backbuffer into a shared keyed-mutex texture the host samples (lower latency, no capture border) | `coop_hook.dll` + `coop_host.exe` |
|
||||||
@@ -75,23 +76,6 @@ is removed from this list once done — so the top item is always next. The
|
|||||||
self-verifiable tooling / UI / input items come first; the game-pipeline items that
|
self-verifiable tooling / UI / input items come first; the game-pipeline items that
|
||||||
need a real game (and Remote Play) to fully validate come last.
|
need a real game (and Remote Play) to fully validate come last.
|
||||||
|
|
||||||
- **Mouse & keyboard forwarding (messages + polling-state hooks).** Forward guest
|
|
||||||
clicks and keystrokes into the unfocused game via a new **MKB hook subsystem** in
|
|
||||||
`coop_hook.dll` — the toggle *is* the hook (not installed → no forwarding).
|
|
||||||
*Delivery:* `PostMessage` window-message input (`WM_KEYDOWN`/`WM_KEYUP`/`WM_CHAR`,
|
|
||||||
`WM_*BUTTONDOWN`/`UP`, `WM_MOUSEWHEEL`) to the game HWND, **plus** hook
|
|
||||||
`GetAsyncKeyState` / `GetKeyboardState` / `GetCursorPos` in the DLL so polling games
|
|
||||||
see the synthesized keyboard/cursor state (RawInput and DirectInput games are out of
|
|
||||||
scope for this version). *Keyboard* is always forwarded; *mouse* only while video is
|
|
||||||
mirrored (otherwise the operator can't see where they click), and only **clicks +
|
|
||||||
wheel, not movement** (one cursor can't be in two places). *Coordinate mapping* (the
|
|
||||||
part that must be exact): WGC + decorated windowed → translate by the window
|
|
||||||
decoration / client-area offset; hooked capture → relative to the mirrored viewport
|
|
||||||
only (decorations aren't mirrored); borderless → the same under both backends.
|
|
||||||
*Critical gating:* forward only when the host's main window is focused **and** ImGui
|
|
||||||
doesn't want the event (`ImGuiIO::WantCaptureMouse` / `WantCaptureKeyboard`), so
|
|
||||||
interacting with the overlay's own windows never leaks input into the game. The host
|
|
||||||
sends MKB events to the hook over a new (or extended) IPC region.
|
|
||||||
- **Rumble / haptics forwarding (both backends).** Currently unsupported — the XInput
|
- **Rumble / haptics forwarding (both backends).** Currently unsupported — the XInput
|
||||||
hook swallows `XInputSetState`. Add a reverse path: the hook captures the game's
|
hook swallows `XInputSetState`. Add a reverse path: the hook captures the game's
|
||||||
`XInputSetState` (left/right motor) and publishes it over a hook→host channel (the
|
`XInputSetState` (left/right motor) and publishes it over a hook→host channel (the
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace coop
|
|||||||
|
|
||||||
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
||||||
// refuses to attach to a host with a mismatched version.
|
// refuses to attach to a host with a mismatched version.
|
||||||
inline constexpr std::uint32_t kProtocolVersion = 7;
|
inline constexpr std::uint32_t kProtocolVersion = 8;
|
||||||
|
|
||||||
// 'COOP' little-endian, used to sanity-check the mapping before trusting it.
|
// 'COOP' little-endian, used to sanity-check the mapping before trusting it.
|
||||||
inline constexpr std::uint32_t kProtocolMagic = 0x504F4F43u;
|
inline constexpr std::uint32_t kProtocolMagic = 0x504F4F43u;
|
||||||
@@ -68,7 +68,8 @@ enum HookSubsystem : std::uint32_t
|
|||||||
HookSubsys_Focus = 1, // focus spoof (keep the game running unfocused)
|
HookSubsys_Focus = 1, // focus spoof (keep the game running unfocused)
|
||||||
HookSubsys_Audio = 2, // WASAPI render-hook (audio mirror without echo)
|
HookSubsys_Audio = 2, // WASAPI render-hook (audio mirror without echo)
|
||||||
HookSubsys_Video = 3, // IDXGISwapChain::Present hook (shared-texture video mirror)
|
HookSubsys_Video = 3, // IDXGISwapChain::Present hook (shared-texture video mirror)
|
||||||
HookSubsys_Count = 4,
|
HookSubsys_Mkb = 4, // mouse+keyboard forwarding (PostMessage + polling-state hooks)
|
||||||
|
HookSubsys_Count = 5,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Maximum individual hooks reported in the registry (a few per subsystem).
|
// Maximum individual hooks reported in the registry (a few per subsystem).
|
||||||
@@ -152,6 +153,43 @@ struct VideoShare
|
|||||||
std::uint64_t present_calls; // cumulative Present() detours (diagnostic)
|
std::uint64_t present_calls; // cumulative Present() detours (diagnostic)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Mouse + keyboard forwarding -------------------------------------------
|
||||||
|
// The host captures its own window's MKB input (when focused and ImGui doesn't
|
||||||
|
// want it) and pushes events here; the injected MKB subsystem drains them, posts
|
||||||
|
// the matching window messages to the game, and maintains a synthesized state the
|
||||||
|
// GetAsyncKeyState/GetKeyboardState/GetCursorPos hooks report to polling games.
|
||||||
|
|
||||||
|
enum MkbEventType : std::uint32_t
|
||||||
|
{
|
||||||
|
Mkb_KeyDown = 0, // code = Win32 virtual-key
|
||||||
|
Mkb_KeyUp = 1, // code = Win32 virtual-key
|
||||||
|
Mkb_Char = 2, // code = UTF-16 code unit (WM_CHAR)
|
||||||
|
Mkb_MouseDown = 3, // code = button (0=left,1=right,2=middle); x,y = game client px
|
||||||
|
Mkb_MouseUp = 4, // code = button; x,y = game client px
|
||||||
|
Mkb_Wheel = 5, // code = signed wheel delta (WHEEL_DELTA units); x,y = game client px
|
||||||
|
};
|
||||||
|
|
||||||
|
struct MkbEvent
|
||||||
|
{
|
||||||
|
std::uint32_t type; // MkbEventType
|
||||||
|
std::uint32_t code; // see per-type meaning above
|
||||||
|
std::int32_t x; // game-client x (mouse events)
|
||||||
|
std::int32_t y; // game-client y (mouse events)
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(sizeof(MkbEvent) == 16, "MkbEvent must stay byte-identical across bitness");
|
||||||
|
|
||||||
|
// Power-of-two so the free-running indices mask cleanly.
|
||||||
|
inline constexpr std::uint32_t kMkbQueueSize = 128;
|
||||||
|
|
||||||
|
// Lock-free SPSC ring: host produces, hook consumes. Free-running 32-bit indices.
|
||||||
|
struct MkbRing
|
||||||
|
{
|
||||||
|
std::atomic<std::uint32_t> head; // producer (host) write position
|
||||||
|
std::atomic<std::uint32_t> tail; // consumer (hook) read position
|
||||||
|
MkbEvent events[kMkbQueueSize];
|
||||||
|
};
|
||||||
|
|
||||||
// The shared backbuffer texture is named per target pid, like the audio ring.
|
// The shared backbuffer texture is named per target pid, like the audio ring.
|
||||||
inline constexpr wchar_t kVideoSharePrefix[] = L"Local\\coop_video_";
|
inline constexpr wchar_t kVideoSharePrefix[] = L"Local\\coop_video_";
|
||||||
|
|
||||||
@@ -178,6 +216,9 @@ struct SharedBlock
|
|||||||
|
|
||||||
// Hook -> host Present-hook video channel (shared-texture dimensions/format).
|
// Hook -> host Present-hook video channel (shared-texture dimensions/format).
|
||||||
VideoShare video;
|
VideoShare video;
|
||||||
|
|
||||||
|
// Host -> hook mouse + keyboard event queue (when the MKB subsystem is on).
|
||||||
|
MkbRing mkb;
|
||||||
};
|
};
|
||||||
|
|
||||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
|
static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
|
||||||
@@ -249,4 +290,34 @@ inline bool read_pads(const SharedBlock& block, CoopPadState (&out)[kMaxPads], s
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- MKB ring helpers (SPSC: host pushes, hook pops) -----------------------
|
||||||
|
|
||||||
|
// Host side: enqueue an MKB event. Returns false (dropped) if the ring is full.
|
||||||
|
inline bool push_mkb_event(MkbRing& ring, const MkbEvent& ev)
|
||||||
|
{
|
||||||
|
const std::uint32_t head = ring.head.load(std::memory_order_relaxed);
|
||||||
|
const std::uint32_t tail = ring.tail.load(std::memory_order_acquire);
|
||||||
|
if (head - tail >= kMkbQueueSize)
|
||||||
|
{
|
||||||
|
return false; // full -> drop (host should always drain faster than it fills)
|
||||||
|
}
|
||||||
|
ring.events[head & (kMkbQueueSize - 1)] = ev;
|
||||||
|
ring.head.store(head + 1, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook side: dequeue the next MKB event. Returns false if the ring is empty.
|
||||||
|
inline bool pop_mkb_event(MkbRing& ring, MkbEvent& out)
|
||||||
|
{
|
||||||
|
const std::uint32_t tail = ring.tail.load(std::memory_order_relaxed);
|
||||||
|
const std::uint32_t head = ring.head.load(std::memory_order_acquire);
|
||||||
|
if (tail == head)
|
||||||
|
{
|
||||||
|
return false; // empty
|
||||||
|
}
|
||||||
|
out = ring.events[tail & (kMkbQueueSize - 1)];
|
||||||
|
ring.tail.store(tail + 1, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace coop
|
} // namespace coop
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ add_library(coop_hook SHARED
|
|||||||
src/audio_hook.cpp
|
src/audio_hook.cpp
|
||||||
src/present_hook.cpp
|
src/present_hook.cpp
|
||||||
src/opengl_hook.cpp
|
src/opengl_hook.cpp
|
||||||
|
src/mkb_hook.cpp
|
||||||
src/debug_log.cpp
|
src/debug_log.cpp
|
||||||
src/hook_registry.cpp)
|
src/hook_registry.cpp)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "focus_spoof.hpp"
|
#include "focus_spoof.hpp"
|
||||||
#include "hook_registry.hpp"
|
#include "hook_registry.hpp"
|
||||||
#include "ipc_client.hpp"
|
#include "ipc_client.hpp"
|
||||||
|
#include "mkb_hook.hpp"
|
||||||
#include "opengl_hook.hpp"
|
#include "opengl_hook.hpp"
|
||||||
#include "present_hook.hpp"
|
#include "present_hook.hpp"
|
||||||
#include "xinput_hook.hpp"
|
#include "xinput_hook.hpp"
|
||||||
@@ -71,6 +72,7 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
bool audio_installed = false;
|
bool audio_installed = false;
|
||||||
bool audio_ring_open = false;
|
bool audio_ring_open = false;
|
||||||
bool video_installed = false;
|
bool video_installed = false;
|
||||||
|
bool mkb_installed = false;
|
||||||
|
|
||||||
// Each tick, reconcile each subsystem with the host's requested state: install
|
// Each tick, reconcile each subsystem with the host's requested state: install
|
||||||
// what's wanted but missing (modules / the game window may appear lazily) and
|
// what's wanted but missing (modules / the game window may appear lazily) and
|
||||||
@@ -147,6 +149,25 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
coop::hook::logf("worker_thread: video hooks removed (host request)");
|
coop::hook::logf("worker_thread: video hooks removed (host request)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Mouse + keyboard forwarding ---
|
||||||
|
// Opt-in. When on, the host streams MKB events into the shared ring; we post
|
||||||
|
// them to the game and synthesize polling state. Drained at high rate below.
|
||||||
|
const bool want_mkb = g_ipc.subsystem_install_requested(coop::HookSubsys_Mkb);
|
||||||
|
if (want_mkb && !mkb_installed)
|
||||||
|
{
|
||||||
|
mkb_installed = coop::hook::install_mkb_hooks(g_ipc);
|
||||||
|
if (mkb_installed)
|
||||||
|
{
|
||||||
|
coop::hook::logf("worker_thread: MKB hooks installed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!want_mkb && mkb_installed)
|
||||||
|
{
|
||||||
|
coop::hook::remove_mkb_hooks();
|
||||||
|
mkb_installed = false;
|
||||||
|
coop::hook::logf("worker_thread: MKB hooks removed (host request)");
|
||||||
|
}
|
||||||
|
|
||||||
if (audio_installed && !audio_ring_open)
|
if (audio_installed && !audio_ring_open)
|
||||||
{
|
{
|
||||||
const std::wstring name = coop::audio_ring_name(GetCurrentProcessId());
|
const std::wstring name = coop::audio_ring_name(GetCurrentProcessId());
|
||||||
@@ -179,7 +200,17 @@ DWORD WINAPI worker_thread(LPVOID)
|
|||||||
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
|
coop::hook::update_input_diagnostics(g_ipc); // refreshes each tick; registrations can change
|
||||||
coop::hook::hook_publish(g_ipc); // installed-hooks list + call counts
|
coop::hook::hook_publish(g_ipc); // installed-hooks list + call counts
|
||||||
g_ipc.heartbeat();
|
g_ipc.heartbeat();
|
||||||
Sleep(250);
|
|
||||||
|
// Reconcile ~4x/s, but drain MKB events far more often (input must be
|
||||||
|
// responsive). 50 slices x 5 ms ~= the old 250 ms reconcile period.
|
||||||
|
for (int slice = 0; slice < 50 && g_running.load(std::memory_order_relaxed); ++slice)
|
||||||
|
{
|
||||||
|
if (mkb_installed)
|
||||||
|
{
|
||||||
|
coop::hook::mkb_pump(g_ipc);
|
||||||
|
}
|
||||||
|
Sleep(5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (com_ok)
|
if (com_ok)
|
||||||
@@ -214,6 +245,7 @@ BOOL APIENTRY DllMain(HMODULE module, DWORD reason, LPVOID reserved)
|
|||||||
coop::hook::remove_audio_hooks();
|
coop::hook::remove_audio_hooks();
|
||||||
coop::hook::remove_present_hooks();
|
coop::hook::remove_present_hooks();
|
||||||
coop::hook::remove_opengl_hooks();
|
coop::hook::remove_opengl_hooks();
|
||||||
|
coop::hook::remove_mkb_hooks();
|
||||||
coop::hook::hook_registry_reset();
|
coop::hook::hook_registry_reset();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -194,6 +194,15 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Mouse + keyboard forwarding ---------------------------------------
|
||||||
|
|
||||||
|
// The host's MKB event queue (nullptr if not connected). The MKB subsystem
|
||||||
|
// drains it; the host is the sole producer.
|
||||||
|
[[nodiscard]] MkbRing* mkb_ring()
|
||||||
|
{
|
||||||
|
return block_ != nullptr ? &block_->mkb : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Hook registry -----------------------------------------------------
|
// --- Hook registry -----------------------------------------------------
|
||||||
|
|
||||||
// Publish the installed-hooks table (name / subsystem / installed / calls).
|
// Publish the installed-hooks table (name / subsystem / installed / calls).
|
||||||
|
|||||||
331
hook/src/mkb_hook.cpp
Normal file
331
hook/src/mkb_hook.cpp
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
#include "mkb_hook.hpp"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <safetyhook.hpp>
|
||||||
|
|
||||||
|
#include "hook_registry.hpp"
|
||||||
|
|
||||||
|
namespace coop::hook
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Synthesized input state the polling hooks report. Written by the worker thread
|
||||||
|
// (mkb_pump), read by the game's thread inside the detours -> all atomic.
|
||||||
|
std::atomic<bool> g_active{false};
|
||||||
|
std::atomic<bool> g_key_down[256]; // by Win32 virtual-key (incl. VK_LBUTTON etc.)
|
||||||
|
std::atomic<long> g_cursor_x{0}; // last forwarded mouse position (game client px)
|
||||||
|
std::atomic<long> g_cursor_y{0};
|
||||||
|
std::atomic<bool> g_have_cursor{false}; // a mouse event has been forwarded at least once
|
||||||
|
std::atomic<void*> g_target{nullptr}; // game main window (HWND), resolved lazily
|
||||||
|
|
||||||
|
safetyhook::InlineHook g_hk_async;
|
||||||
|
safetyhook::InlineHook g_hk_kbstate;
|
||||||
|
safetyhook::InlineHook g_hk_cursor;
|
||||||
|
|
||||||
|
int g_id_pump = -1;
|
||||||
|
int g_id_async = -1;
|
||||||
|
int g_id_kbstate = -1;
|
||||||
|
int g_id_cursor = -1;
|
||||||
|
bool g_installed = false;
|
||||||
|
|
||||||
|
// --- Synthesized-state detours (let polling games see forwarded input) ------
|
||||||
|
|
||||||
|
SHORT WINAPI hk_GetAsyncKeyState(int vkey)
|
||||||
|
{
|
||||||
|
const SHORT orig = g_hk_async.stdcall<SHORT>(vkey);
|
||||||
|
if (g_active.load(std::memory_order_relaxed) && vkey >= 0 && vkey < 256 &&
|
||||||
|
g_key_down[vkey].load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
return static_cast<SHORT>(0x8000) | (orig & 0x1);
|
||||||
|
}
|
||||||
|
return orig;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI hk_GetKeyboardState(PBYTE state)
|
||||||
|
{
|
||||||
|
const BOOL r = g_hk_kbstate.stdcall<BOOL>(state);
|
||||||
|
if (r && state != nullptr && g_active.load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
for (int vk = 0; vk < 256; ++vk)
|
||||||
|
{
|
||||||
|
if (g_key_down[vk].load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
state[vk] |= 0x80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL WINAPI hk_GetCursorPos(LPPOINT pt)
|
||||||
|
{
|
||||||
|
const BOOL r = g_hk_cursor.stdcall<BOOL>(pt);
|
||||||
|
if (g_active.load(std::memory_order_relaxed) && g_have_cursor.load(std::memory_order_relaxed) && pt != nullptr)
|
||||||
|
{
|
||||||
|
auto* hwnd = static_cast<HWND>(g_target.load(std::memory_order_relaxed));
|
||||||
|
if (hwnd != nullptr)
|
||||||
|
{
|
||||||
|
POINT c{g_cursor_x.load(std::memory_order_relaxed), g_cursor_y.load(std::memory_order_relaxed)};
|
||||||
|
ClientToScreen(hwnd, &c); // synth state is game-client; GetCursorPos is screen-space
|
||||||
|
*pt = c;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Target window + message synthesis --------------------------------------
|
||||||
|
|
||||||
|
struct FindCtx
|
||||||
|
{
|
||||||
|
DWORD pid;
|
||||||
|
HWND best;
|
||||||
|
long area;
|
||||||
|
};
|
||||||
|
|
||||||
|
BOOL CALLBACK find_main_proc(HWND hwnd, LPARAM lparam)
|
||||||
|
{
|
||||||
|
auto* c = reinterpret_cast<FindCtx*>(lparam);
|
||||||
|
DWORD pid = 0;
|
||||||
|
GetWindowThreadProcessId(hwnd, &pid);
|
||||||
|
if (pid != c->pid || !IsWindowVisible(hwnd) || GetWindow(hwnd, GW_OWNER) != nullptr)
|
||||||
|
{
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
RECT r{};
|
||||||
|
GetWindowRect(hwnd, &r);
|
||||||
|
const long area = (r.right - r.left) * (r.bottom - r.top);
|
||||||
|
if (area > c->area)
|
||||||
|
{
|
||||||
|
c->area = area;
|
||||||
|
c->best = hwnd;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND find_main_window()
|
||||||
|
{
|
||||||
|
FindCtx c{GetCurrentProcessId(), nullptr, 0};
|
||||||
|
EnumWindows(&find_main_proc, reinterpret_cast<LPARAM>(&c));
|
||||||
|
return c.best;
|
||||||
|
}
|
||||||
|
|
||||||
|
LPARAM key_lparam(UINT vk, bool key_up)
|
||||||
|
{
|
||||||
|
const UINT scan = MapVirtualKeyW(vk, MAPVK_VK_TO_VSC);
|
||||||
|
LPARAM lp = 1 | (static_cast<LPARAM>(scan) << 16); // repeat count 1 + scan code
|
||||||
|
if (key_up)
|
||||||
|
{
|
||||||
|
lp |= (LPARAM{1} << 30) | (LPARAM{1} << 31); // previous-down + transition (key released)
|
||||||
|
}
|
||||||
|
return lp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_key(UINT vk, bool down)
|
||||||
|
{
|
||||||
|
if (vk < 256)
|
||||||
|
{
|
||||||
|
g_key_down[vk].store(down, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WPARAM mouse_button_wparam()
|
||||||
|
{
|
||||||
|
WPARAM w = 0;
|
||||||
|
if (g_key_down[VK_LBUTTON].load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
w |= MK_LBUTTON;
|
||||||
|
}
|
||||||
|
if (g_key_down[VK_RBUTTON].load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
w |= MK_RBUTTON;
|
||||||
|
}
|
||||||
|
if (g_key_down[VK_MBUTTON].load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
w |= MK_MBUTTON;
|
||||||
|
}
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_mouse(const MkbEvent& ev, bool down, HWND hwnd)
|
||||||
|
{
|
||||||
|
g_cursor_x.store(ev.x, std::memory_order_relaxed);
|
||||||
|
g_cursor_y.store(ev.y, std::memory_order_relaxed);
|
||||||
|
g_have_cursor.store(true, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
const UINT vk = ev.code == 0 ? VK_LBUTTON : ev.code == 1 ? VK_RBUTTON : VK_MBUTTON;
|
||||||
|
set_key(vk, down);
|
||||||
|
if (hwnd == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UINT msg;
|
||||||
|
if (ev.code == 0)
|
||||||
|
{
|
||||||
|
msg = down ? WM_LBUTTONDOWN : WM_LBUTTONUP;
|
||||||
|
}
|
||||||
|
else if (ev.code == 1)
|
||||||
|
{
|
||||||
|
msg = down ? WM_RBUTTONDOWN : WM_RBUTTONUP;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
msg = down ? WM_MBUTTONDOWN : WM_MBUTTONUP;
|
||||||
|
}
|
||||||
|
PostMessageW(hwnd, msg, mouse_button_wparam(), MAKELPARAM(ev.x, ev.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_wheel(const MkbEvent& ev, HWND hwnd)
|
||||||
|
{
|
||||||
|
g_cursor_x.store(ev.x, std::memory_order_relaxed);
|
||||||
|
g_cursor_y.store(ev.y, std::memory_order_relaxed);
|
||||||
|
g_have_cursor.store(true, std::memory_order_relaxed);
|
||||||
|
if (hwnd == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
POINT pt{ev.x, ev.y};
|
||||||
|
ClientToScreen(hwnd, &pt); // WM_MOUSEWHEEL lParam is screen coordinates
|
||||||
|
const short delta = static_cast<short>(static_cast<std::int32_t>(ev.code));
|
||||||
|
PostMessageW(hwnd, WM_MOUSEWHEEL, MAKEWPARAM(0, delta), MAKELPARAM(pt.x, pt.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
void install_user32_hook(HMODULE user32, const char* name, void* detour, safetyhook::InlineHook& slot, int id)
|
||||||
|
{
|
||||||
|
if (user32 == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (void* target = reinterpret_cast<void*>(GetProcAddress(user32, name)))
|
||||||
|
{
|
||||||
|
slot = safetyhook::create_inline(target, detour);
|
||||||
|
if (slot)
|
||||||
|
{
|
||||||
|
hook_set_installed(id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool install_mkb_hooks(IpcClient& ipc)
|
||||||
|
{
|
||||||
|
if (g_installed)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_id_pump < 0)
|
||||||
|
{
|
||||||
|
g_id_pump = hook_register("MKB pump (PostMessage)", HookSubsys_Mkb);
|
||||||
|
g_id_async = hook_register("GetAsyncKeyState", HookSubsys_Mkb);
|
||||||
|
g_id_kbstate = hook_register("GetKeyboardState", HookSubsys_Mkb);
|
||||||
|
g_id_cursor = hook_register("GetCursorPos", HookSubsys_Mkb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fresh synthesized state so a previous session leaves no stuck keys.
|
||||||
|
for (int vk = 0; vk < 256; ++vk)
|
||||||
|
{
|
||||||
|
g_key_down[vk].store(false, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
g_have_cursor.store(false, std::memory_order_relaxed);
|
||||||
|
g_target.store(nullptr, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
HMODULE user32 = GetModuleHandleW(L"user32.dll");
|
||||||
|
install_user32_hook(user32, "GetAsyncKeyState", reinterpret_cast<void*>(&hk_GetAsyncKeyState), g_hk_async,
|
||||||
|
g_id_async);
|
||||||
|
install_user32_hook(user32, "GetKeyboardState", reinterpret_cast<void*>(&hk_GetKeyboardState), g_hk_kbstate,
|
||||||
|
g_id_kbstate);
|
||||||
|
install_user32_hook(user32, "GetCursorPos", reinterpret_cast<void*>(&hk_GetCursorPos), g_hk_cursor, g_id_cursor);
|
||||||
|
|
||||||
|
g_active.store(true, std::memory_order_release);
|
||||||
|
hook_set_installed(g_id_pump, true);
|
||||||
|
g_installed = true;
|
||||||
|
(void)ipc;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove_mkb_hooks()
|
||||||
|
{
|
||||||
|
if (!g_installed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_active.store(false, std::memory_order_release);
|
||||||
|
g_hk_async = {}; // InlineHook destructor restores the original bytes
|
||||||
|
g_hk_kbstate = {};
|
||||||
|
g_hk_cursor = {};
|
||||||
|
for (int vk = 0; vk < 256; ++vk)
|
||||||
|
{
|
||||||
|
g_key_down[vk].store(false, std::memory_order_relaxed); // no stuck keys
|
||||||
|
}
|
||||||
|
g_have_cursor.store(false, std::memory_order_relaxed);
|
||||||
|
hook_set_installed(g_id_pump, false);
|
||||||
|
hook_set_installed(g_id_async, false);
|
||||||
|
hook_set_installed(g_id_kbstate, false);
|
||||||
|
hook_set_installed(g_id_cursor, false);
|
||||||
|
g_installed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mkb_pump(IpcClient& ipc)
|
||||||
|
{
|
||||||
|
MkbRing* ring = ipc.mkb_ring();
|
||||||
|
if (ring == nullptr || !g_active.load(std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HWND hwnd = static_cast<HWND>(g_target.load(std::memory_order_relaxed));
|
||||||
|
if (hwnd == nullptr || !IsWindow(hwnd))
|
||||||
|
{
|
||||||
|
hwnd = find_main_window();
|
||||||
|
g_target.store(hwnd, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
MkbEvent ev{};
|
||||||
|
while (pop_mkb_event(*ring, ev))
|
||||||
|
{
|
||||||
|
hook_note_call(g_id_pump);
|
||||||
|
switch (ev.type)
|
||||||
|
{
|
||||||
|
case Mkb_KeyDown:
|
||||||
|
set_key(ev.code, true);
|
||||||
|
if (hwnd != nullptr)
|
||||||
|
{
|
||||||
|
PostMessageW(hwnd, WM_KEYDOWN, ev.code, key_lparam(ev.code, false));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Mkb_KeyUp:
|
||||||
|
set_key(ev.code, false);
|
||||||
|
if (hwnd != nullptr)
|
||||||
|
{
|
||||||
|
PostMessageW(hwnd, WM_KEYUP, ev.code, key_lparam(ev.code, true));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Mkb_Char:
|
||||||
|
if (hwnd != nullptr)
|
||||||
|
{
|
||||||
|
PostMessageW(hwnd, WM_CHAR, ev.code, 1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Mkb_MouseDown:
|
||||||
|
handle_mouse(ev, true, hwnd);
|
||||||
|
break;
|
||||||
|
case Mkb_MouseUp:
|
||||||
|
handle_mouse(ev, false, hwnd);
|
||||||
|
break;
|
||||||
|
case Mkb_Wheel:
|
||||||
|
handle_wheel(ev, hwnd);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace coop::hook
|
||||||
23
hook/src/mkb_hook.hpp
Normal file
23
hook/src/mkb_hook.hpp
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// Mouse + keyboard forwarding subsystem (HookSubsys_Mkb).
|
||||||
|
//
|
||||||
|
// The host captures its own window's MKB input and pushes events into the shared
|
||||||
|
// MkbRing. This subsystem drains them on the worker thread, posts the matching
|
||||||
|
// window messages to the game's main window (so message-driven games react), and
|
||||||
|
// maintains a synthesized input state that hooked GetAsyncKeyState /
|
||||||
|
// GetKeyboardState / GetCursorPos report (so polling games react too). The toggle
|
||||||
|
// is the subsystem itself: not installed -> nothing is forwarded.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ipc_client.hpp"
|
||||||
|
|
||||||
|
namespace coop::hook
|
||||||
|
{
|
||||||
|
|
||||||
|
bool install_mkb_hooks(IpcClient& ipc);
|
||||||
|
void remove_mkb_hooks();
|
||||||
|
|
||||||
|
// Drain the host's MKB event queue: post window messages + update synthesized
|
||||||
|
// state. Called frequently from the worker loop while the subsystem is installed.
|
||||||
|
void mkb_pump(IpcClient& ipc);
|
||||||
|
|
||||||
|
} // namespace coop::hook
|
||||||
@@ -12,6 +12,7 @@ add_executable(coop_host WIN32
|
|||||||
src/inject/process_list.cpp
|
src/inject/process_list.cpp
|
||||||
src/inject/window_list.cpp
|
src/inject/window_list.cpp
|
||||||
src/inject/injector.cpp
|
src/inject/injector.cpp
|
||||||
|
src/inject/mkb_forward.cpp
|
||||||
src/ipc/ipc_server.cpp
|
src/ipc/ipc_server.cpp
|
||||||
src/capture/frame_renderer.cpp
|
src/capture/frame_renderer.cpp
|
||||||
src/capture/window_capture.cpp
|
src/capture/window_capture.cpp
|
||||||
|
|||||||
@@ -44,6 +44,20 @@ public:
|
|||||||
// Render thread: draw the mirrored frame as the window background.
|
// Render thread: draw the mirrored frame as the window background.
|
||||||
void render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h);
|
void render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h);
|
||||||
|
|
||||||
|
// Whether the video mirror is on (mouse forwarding is gated on this, since the
|
||||||
|
// operator can't aim clicks without seeing the game).
|
||||||
|
[[nodiscard]] bool mirroring() const
|
||||||
|
{
|
||||||
|
return enabled_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when the active source is the injected Present-hook (client/backbuffer);
|
||||||
|
// false for WGC (whole-window). Drives the mouse coordinate mapping.
|
||||||
|
[[nodiscard]] bool source_hooked() const
|
||||||
|
{
|
||||||
|
return source_ == Source_Hooked;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum Source : int
|
enum Source : int
|
||||||
{
|
{
|
||||||
|
|||||||
207
host/src/inject/mkb_forward.cpp
Normal file
207
host/src/inject/mkb_forward.cpp
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
#include "inject/mkb_forward.hpp"
|
||||||
|
|
||||||
|
#include <imgui.h>
|
||||||
|
|
||||||
|
#include "coop/protocol.hpp"
|
||||||
|
#include "injection_panel.hpp"
|
||||||
|
#include "inject/mkb_map.hpp"
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Map an ImGui key to a Win32 virtual-key. Returns 0 for keys we don't forward.
|
||||||
|
int imgui_key_to_vk(ImGuiKey k)
|
||||||
|
{
|
||||||
|
if (k >= ImGuiKey_A && k <= ImGuiKey_Z)
|
||||||
|
{
|
||||||
|
return 'A' + (k - ImGuiKey_A);
|
||||||
|
}
|
||||||
|
if (k >= ImGuiKey_0 && k <= ImGuiKey_9)
|
||||||
|
{
|
||||||
|
return '0' + (k - ImGuiKey_0);
|
||||||
|
}
|
||||||
|
if (k >= ImGuiKey_Keypad0 && k <= ImGuiKey_Keypad9)
|
||||||
|
{
|
||||||
|
return VK_NUMPAD0 + (k - ImGuiKey_Keypad0);
|
||||||
|
}
|
||||||
|
if (k >= ImGuiKey_F1 && k <= ImGuiKey_F12)
|
||||||
|
{
|
||||||
|
return VK_F1 + (k - ImGuiKey_F1);
|
||||||
|
}
|
||||||
|
switch (k)
|
||||||
|
{
|
||||||
|
case ImGuiKey_Tab: return VK_TAB;
|
||||||
|
case ImGuiKey_LeftArrow: return VK_LEFT;
|
||||||
|
case ImGuiKey_RightArrow: return VK_RIGHT;
|
||||||
|
case ImGuiKey_UpArrow: return VK_UP;
|
||||||
|
case ImGuiKey_DownArrow: return VK_DOWN;
|
||||||
|
case ImGuiKey_PageUp: return VK_PRIOR;
|
||||||
|
case ImGuiKey_PageDown: return VK_NEXT;
|
||||||
|
case ImGuiKey_Home: return VK_HOME;
|
||||||
|
case ImGuiKey_End: return VK_END;
|
||||||
|
case ImGuiKey_Insert: return VK_INSERT;
|
||||||
|
case ImGuiKey_Delete: return VK_DELETE;
|
||||||
|
case ImGuiKey_Backspace: return VK_BACK;
|
||||||
|
case ImGuiKey_Space: return VK_SPACE;
|
||||||
|
case ImGuiKey_Enter: return VK_RETURN;
|
||||||
|
case ImGuiKey_Escape: return VK_ESCAPE;
|
||||||
|
case ImGuiKey_LeftCtrl: return VK_LCONTROL;
|
||||||
|
case ImGuiKey_LeftShift: return VK_LSHIFT;
|
||||||
|
case ImGuiKey_LeftAlt: return VK_LMENU;
|
||||||
|
case ImGuiKey_LeftSuper: return VK_LWIN;
|
||||||
|
case ImGuiKey_RightCtrl: return VK_RCONTROL;
|
||||||
|
case ImGuiKey_RightShift: return VK_RSHIFT;
|
||||||
|
case ImGuiKey_RightAlt: return VK_RMENU;
|
||||||
|
case ImGuiKey_RightSuper: return VK_RWIN;
|
||||||
|
case ImGuiKey_Menu: return VK_APPS;
|
||||||
|
case ImGuiKey_Apostrophe: return VK_OEM_7;
|
||||||
|
case ImGuiKey_Comma: return VK_OEM_COMMA;
|
||||||
|
case ImGuiKey_Minus: return VK_OEM_MINUS;
|
||||||
|
case ImGuiKey_Period: return VK_OEM_PERIOD;
|
||||||
|
case ImGuiKey_Slash: return VK_OEM_2;
|
||||||
|
case ImGuiKey_Semicolon: return VK_OEM_1;
|
||||||
|
case ImGuiKey_Equal: return VK_OEM_PLUS;
|
||||||
|
case ImGuiKey_LeftBracket: return VK_OEM_4;
|
||||||
|
case ImGuiKey_Backslash: return VK_OEM_5;
|
||||||
|
case ImGuiKey_RightBracket: return VK_OEM_6;
|
||||||
|
case ImGuiKey_GraveAccent: return VK_OEM_3;
|
||||||
|
case ImGuiKey_CapsLock: return VK_CAPITAL;
|
||||||
|
case ImGuiKey_ScrollLock: return VK_SCROLL;
|
||||||
|
case ImGuiKey_NumLock: return VK_NUMLOCK;
|
||||||
|
case ImGuiKey_PrintScreen: return VK_SNAPSHOT;
|
||||||
|
case ImGuiKey_Pause: return VK_PAUSE;
|
||||||
|
case ImGuiKey_KeypadDecimal: return VK_DECIMAL;
|
||||||
|
case ImGuiKey_KeypadDivide: return VK_DIVIDE;
|
||||||
|
case ImGuiKey_KeypadMultiply: return VK_MULTIPLY;
|
||||||
|
case ImGuiKey_KeypadSubtract: return VK_SUBTRACT;
|
||||||
|
case ImGuiKey_KeypadAdd: return VK_ADD;
|
||||||
|
case ImGuiKey_KeypadEnter: return VK_RETURN;
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last valid game-client position, so a button release that lands on a letterbox
|
||||||
|
// bar still goes to the game (no stuck buttons).
|
||||||
|
int g_last_gx = 0;
|
||||||
|
int g_last_gy = 0;
|
||||||
|
|
||||||
|
} // 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))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGuiIO& io = ImGui::GetIO();
|
||||||
|
|
||||||
|
// --- Keyboard (always, unless ImGui is using it for e.g. a text field) ---
|
||||||
|
if (!io.WantCaptureKeyboard)
|
||||||
|
{
|
||||||
|
for (ImGuiKey k = ImGuiKey_NamedKey_BEGIN; k < ImGuiKey_NamedKey_END; k = static_cast<ImGuiKey>(k + 1))
|
||||||
|
{
|
||||||
|
const int vk = imgui_key_to_vk(k);
|
||||||
|
if (vk == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(k, false))
|
||||||
|
{
|
||||||
|
injection.push_mkb(MkbEvent{Mkb_KeyDown, static_cast<std::uint32_t>(vk), 0, 0});
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyReleased(k))
|
||||||
|
{
|
||||||
|
injection.push_mkb(MkbEvent{Mkb_KeyUp, static_cast<std::uint32_t>(vk), 0, 0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < io.InputQueueCharacters.Size; ++i)
|
||||||
|
{
|
||||||
|
const ImWchar c = io.InputQueueCharacters[i];
|
||||||
|
if (c != 0)
|
||||||
|
{
|
||||||
|
injection.push_mkb(MkbEvent{Mkb_Char, static_cast<std::uint32_t>(c), 0, 0});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Mouse (clicks + wheel only, and only while mirroring) ---
|
||||||
|
if (!mirroring || io.WantCaptureMouse)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT host_client{};
|
||||||
|
GetClientRect(host_hwnd, &host_client);
|
||||||
|
|
||||||
|
MkbMapInput m;
|
||||||
|
m.host_x = static_cast<int>(io.MousePos.x);
|
||||||
|
m.host_y = static_cast<int>(io.MousePos.y);
|
||||||
|
m.dst_w = host_client.right;
|
||||||
|
m.dst_h = host_client.bottom;
|
||||||
|
if (source_hooked)
|
||||||
|
{
|
||||||
|
// Hooked capture mirrors the backbuffer (client area), no decorations.
|
||||||
|
const VideoShareView v = injection.video_share();
|
||||||
|
m.src_w = m.client_w = static_cast<int>(v.width);
|
||||||
|
m.src_h = m.client_h = static_cast<int>(v.height);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// WGC captures the whole window; the client area sits at a decoration offset.
|
||||||
|
RECT wr{}, cr{};
|
||||||
|
POINT client_origin{0, 0};
|
||||||
|
GetWindowRect(game, &wr);
|
||||||
|
GetClientRect(game, &cr);
|
||||||
|
ClientToScreen(game, &client_origin);
|
||||||
|
m.src_w = wr.right - wr.left;
|
||||||
|
m.src_h = wr.bottom - wr.top;
|
||||||
|
m.client_w = cr.right;
|
||||||
|
m.client_h = cr.bottom;
|
||||||
|
m.client_off_x = client_origin.x - wr.left;
|
||||||
|
m.client_off_y = client_origin.y - wr.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
int gx = 0, gy = 0;
|
||||||
|
const bool on_game = map_host_to_game_client(m, gx, gy);
|
||||||
|
if (on_game)
|
||||||
|
{
|
||||||
|
g_last_gx = gx;
|
||||||
|
g_last_gy = gy;
|
||||||
|
}
|
||||||
|
const int mx = on_game ? gx : g_last_gx;
|
||||||
|
const int my = on_game ? gy : g_last_gy;
|
||||||
|
|
||||||
|
for (int button = 0; button < 3; ++button) // 0=left, 1=right, 2=middle
|
||||||
|
{
|
||||||
|
if (on_game && ImGui::IsMouseClicked(button))
|
||||||
|
{
|
||||||
|
injection.push_mkb(MkbEvent{Mkb_MouseDown, static_cast<std::uint32_t>(button), mx, my});
|
||||||
|
}
|
||||||
|
if (ImGui::IsMouseReleased(button)) // send the up even off-game, so nothing sticks
|
||||||
|
{
|
||||||
|
injection.push_mkb(MkbEvent{Mkb_MouseUp, static_cast<std::uint32_t>(button), mx, my});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (on_game && io.MouseWheel != 0.0f)
|
||||||
|
{
|
||||||
|
const int delta = static_cast<int>(io.MouseWheel * WHEEL_DELTA);
|
||||||
|
injection.push_mkb(MkbEvent{Mkb_Wheel, static_cast<std::uint32_t>(delta), mx, my});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
19
host/src/inject/mkb_forward.hpp
Normal file
19
host/src/inject/mkb_forward.hpp
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Forwards the host window's mouse + keyboard input into the injected game.
|
||||||
|
//
|
||||||
|
// Called once per frame after ImGui's NewFrame. It runs only when the MKB subsystem
|
||||||
|
// is on, the host window is foreground (so this is the guest's input under RPT), and
|
||||||
|
// ImGui isn't capturing the event (so interacting with the overlay never leaks into
|
||||||
|
// the game). Mouse is forwarded only while the video mirror is on, and clicks are
|
||||||
|
// mapped through the letterbox to the game's client coordinates.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
class InjectionPanel;
|
||||||
|
|
||||||
|
void forward_mkb_frame(InjectionPanel& injection, HWND host_hwnd, bool mirroring, bool source_hooked);
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
61
host/src/inject/mkb_map.hpp
Normal file
61
host/src/inject/mkb_map.hpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Maps a mouse position in the host window's client area to the game's client
|
||||||
|
// coordinates, inverting the letterbox the mirror is drawn with. Pure + header-only
|
||||||
|
// so it can be unit-tested without a device.
|
||||||
|
//
|
||||||
|
// The captured frame differs by capture mode:
|
||||||
|
// - Hooked / borderless: the frame IS the game's client area, so the client
|
||||||
|
// offset is (0,0) and client size == frame size.
|
||||||
|
// - WGC of a decorated window: the frame is the whole window (title bar +
|
||||||
|
// borders), so the client area sits at a non-zero offset inside the frame and
|
||||||
|
// clicks on the decorations are rejected.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace coop
|
||||||
|
{
|
||||||
|
|
||||||
|
struct MkbMapInput
|
||||||
|
{
|
||||||
|
int host_x = 0, host_y = 0; // mouse in host-window client pixels
|
||||||
|
int dst_w = 0, dst_h = 0; // host window client size
|
||||||
|
int src_w = 0, src_h = 0; // captured frame size (WGC=window, Hooked=backbuffer)
|
||||||
|
int client_off_x = 0, client_off_y = 0; // client-area top-left within the frame
|
||||||
|
int client_w = 0, client_h = 0; // game client size within the frame
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns true and writes gx,gy (game client px) if the point lands on the game's
|
||||||
|
// client area; false if it falls on a letterbox bar or the window decorations.
|
||||||
|
inline bool map_host_to_game_client(const MkbMapInput& in, int& gx, int& gy)
|
||||||
|
{
|
||||||
|
if (in.src_w <= 0 || in.src_h <= 0 || in.dst_w <= 0 || in.dst_h <= 0 || in.client_w <= 0 || in.client_h <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert the letterbox: the frame is fit (aspect-preserved) and centered in dst.
|
||||||
|
const double scale =
|
||||||
|
std::min(static_cast<double>(in.dst_w) / in.src_w, static_cast<double>(in.dst_h) / in.src_h);
|
||||||
|
const double ox = (in.dst_w - in.src_w * scale) * 0.5;
|
||||||
|
const double oy = (in.dst_h - in.src_h * scale) * 0.5;
|
||||||
|
|
||||||
|
const double fx = (in.host_x - ox) / scale; // position in captured-frame pixels
|
||||||
|
const double fy = (in.host_y - oy) / scale;
|
||||||
|
if (fx < 0.0 || fy < 0.0 || fx >= in.src_w || fy >= in.src_h)
|
||||||
|
{
|
||||||
|
return false; // on a letterbox bar
|
||||||
|
}
|
||||||
|
|
||||||
|
const double cx = fx - in.client_off_x; // into client space
|
||||||
|
const double cy = fy - in.client_off_y;
|
||||||
|
if (cx < 0.0 || cy < 0.0 || cx >= in.client_w || cy >= in.client_h)
|
||||||
|
{
|
||||||
|
return false; // on the window decorations
|
||||||
|
}
|
||||||
|
|
||||||
|
gx = static_cast<int>(cx);
|
||||||
|
gy = static_cast<int>(cy);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace coop
|
||||||
@@ -180,6 +180,7 @@ void InjectionPanel::inject_selected()
|
|||||||
server_.set_subsystem_enabled(HookSubsys_Focus, want_focus_);
|
server_.set_subsystem_enabled(HookSubsys_Focus, want_focus_);
|
||||||
server_.set_subsystem_enabled(HookSubsys_Audio, want_audio_);
|
server_.set_subsystem_enabled(HookSubsys_Audio, want_audio_);
|
||||||
server_.set_subsystem_enabled(HookSubsys_Video, want_video_);
|
server_.set_subsystem_enabled(HookSubsys_Video, want_video_);
|
||||||
|
server_.set_subsystem_enabled(HookSubsys_Mkb, want_mkb_);
|
||||||
|
|
||||||
const InjectResult result = inject_dll(selected_pid_, hook_dll_path());
|
const InjectResult result = inject_dll(selected_pid_, hook_dll_path());
|
||||||
if (result.status == InjectStatus::Ok)
|
if (result.status == InjectStatus::Ok)
|
||||||
@@ -283,7 +284,7 @@ void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
|
|||||||
|
|
||||||
void InjectionPanel::draw_hook_list(const HookStatusView& status)
|
void InjectionPanel::draw_hook_list(const HookStatusView& status)
|
||||||
{
|
{
|
||||||
static const char* kSubsysName[] = {"Input", "Focus", "Audio", "Video"};
|
static const char* kSubsysName[] = {"Input", "Focus", "Audio", "Video", "MKB"};
|
||||||
|
|
||||||
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
||||||
if (n == 0)
|
if (n == 0)
|
||||||
@@ -372,6 +373,7 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status)
|
|||||||
{"Focus spoof", HookSubsys_Focus, &want_focus_, "the game keeps running unfocused"},
|
{"Focus spoof", HookSubsys_Focus, &want_focus_, "the game keeps running unfocused"},
|
||||||
{"Audio render-hook", HookSubsys_Audio, &want_audio_, "audio mirror without echo"},
|
{"Audio render-hook", HookSubsys_Audio, &want_audio_, "audio mirror without echo"},
|
||||||
{"Video Present-hook", HookSubsys_Video, &want_video_, "Video mirror can use the hooked source"},
|
{"Video Present-hook", HookSubsys_Video, &want_video_, "Video mirror can use the hooked source"},
|
||||||
|
{"Mouse + keyboard forwarding", HookSubsys_Mkb, &want_mkb_, "clicks/keys reach the game"},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const Row& r : rows)
|
for (const Row& r : rows)
|
||||||
|
|||||||
@@ -113,6 +113,21 @@ public:
|
|||||||
return want_video_;
|
return want_video_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Mouse + keyboard forwarding -------------------------------------------
|
||||||
|
|
||||||
|
// Whether the operator enabled the MKB-forwarding subsystem (the toggle is the
|
||||||
|
// hook). The host's MKB forwarder only runs while this is on and a hook is alive.
|
||||||
|
[[nodiscard]] bool mkb_enabled() const
|
||||||
|
{
|
||||||
|
return want_mkb_ && injected_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue an MKB event for the hook to forward into the game.
|
||||||
|
void push_mkb(const MkbEvent& ev)
|
||||||
|
{
|
||||||
|
server_.push_mkb(ev);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void refresh_targets(); // refresh both the window list and the process list
|
void refresh_targets(); // refresh both the window list and the process list
|
||||||
void refresh_processes();
|
void refresh_processes();
|
||||||
@@ -143,6 +158,7 @@ private:
|
|||||||
bool want_focus_ = true;
|
bool want_focus_ = true;
|
||||||
bool want_audio_ = true;
|
bool want_audio_ = true;
|
||||||
bool want_video_ = false; // Present-hook video path: opt-in (WGC is the default)
|
bool want_video_ = false; // Present-hook video path: opt-in (WGC is the default)
|
||||||
|
bool want_mkb_ = false; // mouse+keyboard forwarding: opt-in
|
||||||
bool injected_ = false; // a hook DLL is loaded in the target
|
bool injected_ = false; // a hook DLL is loaded in the target
|
||||||
|
|
||||||
// Heartbeat liveness tracking (is the injected DLL responding?).
|
// Heartbeat liveness tracking (is the injected DLL responding?).
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ public:
|
|||||||
// reconciles on its next tick. No-op if not started.
|
// reconciles on its next tick. No-op if not started.
|
||||||
void set_subsystem_enabled(std::uint32_t subsystem, bool enabled);
|
void set_subsystem_enabled(std::uint32_t subsystem, bool enabled);
|
||||||
|
|
||||||
|
// Enqueue a mouse/keyboard event for the hook's MKB subsystem to forward. Drops
|
||||||
|
// silently if not started or the ring is full.
|
||||||
|
void push_mkb(const MkbEvent& ev)
|
||||||
|
{
|
||||||
|
if (block_ != nullptr)
|
||||||
|
{
|
||||||
|
push_mkb_event(block_->mkb, ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Drain new log lines streamed by the hook, calling `emit(const LogRecord&)`
|
// Drain new log lines streamed by the hook, calling `emit(const LogRecord&)`
|
||||||
// for each. No-op if not started. Header-only so the callback can stay generic.
|
// for each. No-op if not started. Header-only so the callback can stay generic.
|
||||||
template <typename F>
|
template <typename F>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "controllers_panel.hpp"
|
#include "controllers_panel.hpp"
|
||||||
#include "d3d11_window.hpp"
|
#include "d3d11_window.hpp"
|
||||||
#include "imgui_layer.hpp"
|
#include "imgui_layer.hpp"
|
||||||
|
#include "inject/mkb_forward.hpp"
|
||||||
#include "injection_panel.hpp"
|
#include "injection_panel.hpp"
|
||||||
#include "input/xinput_source.hpp"
|
#include "input/xinput_source.hpp"
|
||||||
#include "log_panel.hpp"
|
#include "log_panel.hpp"
|
||||||
@@ -194,6 +195,10 @@ int run()
|
|||||||
}
|
}
|
||||||
coop::apply_layout_end_frame(); // clear the one-shot "Reset layout" force
|
coop::apply_layout_end_frame(); // clear the one-shot "Reset layout" force
|
||||||
|
|
||||||
|
// Forward the host window's mouse/keyboard into the game (when the MKB
|
||||||
|
// subsystem is on, we're focused, and ImGui isn't using the event).
|
||||||
|
coop::forward_mkb_frame(injection, window.hwnd(), capture.mirroring(), capture.source_hooked());
|
||||||
|
|
||||||
RECT client = {};
|
RECT client = {};
|
||||||
GetClientRect(window.hwnd(), &client);
|
GetClientRect(window.hwnd(), &client);
|
||||||
const auto dst_w = static_cast<std::uint32_t>(client.right - client.left);
|
const auto dst_w = static_cast<std::uint32_t>(client.right - client.left);
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ add_executable(audio_ring_test audio_ring_test.cpp)
|
|||||||
target_link_libraries(audio_ring_test PRIVATE coop_common)
|
target_link_libraries(audio_ring_test PRIVATE coop_common)
|
||||||
add_test(NAME audio_ring_test COMMAND audio_ring_test)
|
add_test(NAME audio_ring_test COMMAND audio_ring_test)
|
||||||
|
|
||||||
|
# Unit test for the MKB event ring (SPSC push/pop, wrap-around, full/empty).
|
||||||
|
add_executable(mkb_ring_test mkb_ring_test.cpp)
|
||||||
|
target_link_libraries(mkb_ring_test PRIVATE coop_common)
|
||||||
|
add_test(NAME mkb_ring_test COMMAND mkb_ring_test)
|
||||||
|
|
||||||
|
# Unit test for the host->game mouse coordinate mapping (letterbox inverse +
|
||||||
|
# decorated-window client offset). Header-only, no device.
|
||||||
|
add_executable(mkb_map_test mkb_map_test.cpp)
|
||||||
|
target_include_directories(mkb_map_test PRIVATE ${CMAKE_SOURCE_DIR}/host/src)
|
||||||
|
add_test(NAME mkb_map_test COMMAND mkb_map_test)
|
||||||
|
|
||||||
# Integration test for WASAPI process-loopback capture. Reuses the shipping
|
# Integration test for WASAPI process-loopback capture. Reuses the shipping
|
||||||
# capture code and captures from coop_tone (a known sine-wave render process).
|
# capture code and captures from coop_tone (a known sine-wave render process).
|
||||||
add_executable(audio_loopback_test
|
add_executable(audio_loopback_test
|
||||||
@@ -114,6 +125,8 @@ add_test(NAME opengl_hook_test COMMAND opengl_hook_test)
|
|||||||
coop_output_subdir(tests
|
coop_output_subdir(tests
|
||||||
hook_selftest
|
hook_selftest
|
||||||
audio_ring_test
|
audio_ring_test
|
||||||
|
mkb_ring_test
|
||||||
|
mkb_map_test
|
||||||
audio_loopback_test
|
audio_loopback_test
|
||||||
audio_hook_test
|
audio_hook_test
|
||||||
srgb_format_test
|
srgb_format_test
|
||||||
|
|||||||
93
tests/mkb_map_test.cpp
Normal file
93
tests/mkb_map_test.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Unit test for the host->game mouse coordinate mapping (letterbox inverse +
|
||||||
|
// client-offset handling for WGC-of-decorated-window vs hooked/borderless).
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "inject/mkb_map.hpp"
|
||||||
|
|
||||||
|
using namespace coop;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int g_failures = 0;
|
||||||
|
void check(bool cond, const char* what)
|
||||||
|
{
|
||||||
|
if (!cond)
|
||||||
|
{
|
||||||
|
std::printf("FAIL: %s\n", what);
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
// --- Hooked / borderless: frame == client, no offset. dst exactly matches src
|
||||||
|
// (no letterbox), so the mapping is the identity.
|
||||||
|
{
|
||||||
|
MkbMapInput in;
|
||||||
|
in.dst_w = in.src_w = in.client_w = 1920;
|
||||||
|
in.dst_h = in.src_h = in.client_h = 1080;
|
||||||
|
int gx = 0, gy = 0;
|
||||||
|
check(map_host_to_game_client(in, gx, gy) && gx == 0 && gy == 0, "identity: top-left");
|
||||||
|
in.host_x = 960;
|
||||||
|
in.host_y = 540;
|
||||||
|
check(map_host_to_game_client(in, gx, gy) && gx == 960 && gy == 540, "identity: center");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Letterbox: a 1600x900 (16:9) frame fit into a 1000x1000 host window. Scale
|
||||||
|
// = 1000/1600 = 0.625, vertical bars of (1000 - 562.5)/2 = 218.75 px.
|
||||||
|
{
|
||||||
|
MkbMapInput in;
|
||||||
|
in.dst_w = 1000;
|
||||||
|
in.dst_h = 1000;
|
||||||
|
in.src_w = in.client_w = 1600;
|
||||||
|
in.src_h = in.client_h = 900;
|
||||||
|
int gx = 0, gy = 0;
|
||||||
|
// A click in the top letterbox bar is rejected.
|
||||||
|
in.host_x = 500;
|
||||||
|
in.host_y = 10;
|
||||||
|
check(!map_host_to_game_client(in, gx, gy), "letterbox: top bar rejected");
|
||||||
|
// Center of the host window maps to center of the game frame.
|
||||||
|
in.host_x = 500;
|
||||||
|
in.host_y = 500;
|
||||||
|
check(map_host_to_game_client(in, gx, gy) && gx == 800 && gy == 450, "letterbox: center maps to game center");
|
||||||
|
// Just inside the image at the top edge (y = 218.75 -> 219) maps near client top.
|
||||||
|
in.host_x = 500;
|
||||||
|
in.host_y = 219;
|
||||||
|
check(map_host_to_game_client(in, gx, gy) && gy >= 0 && gy <= 2, "letterbox: top edge maps near 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- WGC of a decorated window: frame is the whole window (1608x939) with the
|
||||||
|
// client area (1600x900) offset by an 8 px left border / 31 px title bar. No
|
||||||
|
// host letterbox here (dst matches the window frame).
|
||||||
|
{
|
||||||
|
MkbMapInput in;
|
||||||
|
in.dst_w = in.src_w = 1608;
|
||||||
|
in.dst_h = in.src_h = 939;
|
||||||
|
in.client_off_x = 8;
|
||||||
|
in.client_off_y = 31;
|
||||||
|
in.client_w = 1600;
|
||||||
|
in.client_h = 900;
|
||||||
|
int gx = 0, gy = 0;
|
||||||
|
// A click on the title bar (above the client area) is rejected.
|
||||||
|
in.host_x = 800;
|
||||||
|
in.host_y = 10;
|
||||||
|
check(!map_host_to_game_client(in, gx, gy), "decorated: title bar rejected");
|
||||||
|
// Client top-left corner.
|
||||||
|
in.host_x = 8;
|
||||||
|
in.host_y = 31;
|
||||||
|
check(map_host_to_game_client(in, gx, gy) && gx == 0 && gy == 0, "decorated: client origin");
|
||||||
|
// A point inside the client area subtracts the decoration offset.
|
||||||
|
in.host_x = 108;
|
||||||
|
in.host_y = 131;
|
||||||
|
check(map_host_to_game_client(in, gx, gy) && gx == 100 && gy == 100, "decorated: client interior");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (g_failures == 0)
|
||||||
|
{
|
||||||
|
std::printf("PASS: mkb_map_test\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::printf("FAIL: %d checks failed\n", g_failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
82
tests/mkb_ring_test.cpp
Normal file
82
tests/mkb_ring_test.cpp
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
// Unit test for the MKB event ring (SPSC push/pop, wrap-around, full/empty).
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
#include "coop/protocol.hpp"
|
||||||
|
|
||||||
|
using namespace coop;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int g_failures = 0;
|
||||||
|
void check(bool cond, const char* what)
|
||||||
|
{
|
||||||
|
if (!cond)
|
||||||
|
{
|
||||||
|
std::printf("FAIL: %s\n", what);
|
||||||
|
++g_failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
MkbRing ring{};
|
||||||
|
|
||||||
|
// Empty pop fails.
|
||||||
|
MkbEvent out{};
|
||||||
|
check(!pop_mkb_event(ring, out), "pop on empty ring returns false");
|
||||||
|
|
||||||
|
// Push then pop returns the same event, FIFO.
|
||||||
|
for (std::uint32_t i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
MkbEvent ev{Mkb_KeyDown, i, static_cast<int>(i) * 2, static_cast<int>(i) * 3};
|
||||||
|
check(push_mkb_event(ring, ev), "push succeeds with room");
|
||||||
|
}
|
||||||
|
for (std::uint32_t i = 0; i < 10; ++i)
|
||||||
|
{
|
||||||
|
check(pop_mkb_event(ring, out), "pop succeeds with data");
|
||||||
|
check(out.code == i && out.x == static_cast<int>(i) * 2 && out.y == static_cast<int>(i) * 3,
|
||||||
|
"popped event matches pushed (FIFO)");
|
||||||
|
}
|
||||||
|
check(!pop_mkb_event(ring, out), "ring empty again after draining");
|
||||||
|
|
||||||
|
// Fill to capacity, then one more push is dropped.
|
||||||
|
for (std::uint32_t i = 0; i < kMkbQueueSize; ++i)
|
||||||
|
{
|
||||||
|
check(push_mkb_event(ring, MkbEvent{Mkb_Char, i, 0, 0}), "push fills to capacity");
|
||||||
|
}
|
||||||
|
check(!push_mkb_event(ring, MkbEvent{Mkb_Char, 999, 0, 0}), "push on full ring is dropped");
|
||||||
|
|
||||||
|
// Drain and verify order survived a full buffer.
|
||||||
|
for (std::uint32_t i = 0; i < kMkbQueueSize; ++i)
|
||||||
|
{
|
||||||
|
check(pop_mkb_event(ring, out) && out.code == i, "full-buffer drain is in order");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap-around: indices are free-running, so many cycles must keep working.
|
||||||
|
std::uint32_t produced = 0, consumed = 0;
|
||||||
|
for (int cycle = 0; cycle < 1000; ++cycle)
|
||||||
|
{
|
||||||
|
for (int k = 0; k < 50; ++k)
|
||||||
|
{
|
||||||
|
if (push_mkb_event(ring, MkbEvent{Mkb_MouseDown, produced, 0, 0}))
|
||||||
|
{
|
||||||
|
++produced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (pop_mkb_event(ring, out))
|
||||||
|
{
|
||||||
|
check(out.code == consumed, "wrap-around preserves FIFO order");
|
||||||
|
++consumed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check(produced == consumed, "all wrap-around events consumed");
|
||||||
|
|
||||||
|
if (g_failures == 0)
|
||||||
|
{
|
||||||
|
std::printf("PASS: mkb_ring_test\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
std::printf("FAIL: %d checks failed\n", g_failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
@@ -308,12 +308,12 @@ int wmain(int argc, wchar_t** argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Dump the hook registry so the installed-hooks list can be verified headless.
|
// Dump the hook registry so the installed-hooks list can be verified headless.
|
||||||
static const char* kSubsys[] = {"Input", "Focus", "Audio", "Video"};
|
static const char* kSubsys[] = {"Input", "Focus", "Audio", "Video", "MKB"};
|
||||||
std::printf("\nInstalled hooks (%u):\n", status.hook_entry_count);
|
std::printf("\nInstalled hooks (%u):\n", status.hook_entry_count);
|
||||||
for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i)
|
for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i)
|
||||||
{
|
{
|
||||||
const coop::HookEntry& e = status.hook_entries[i];
|
const coop::HookEntry& e = status.hook_entries[i];
|
||||||
std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 4 ? kSubsys[e.subsystem] : "?", e.name,
|
std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 5 ? kSubsys[e.subsystem] : "?", e.name,
|
||||||
e.installed ? "ON " : "off", static_cast<unsigned long long>(e.calls));
|
e.installed ? "ON " : "off", static_cast<unsigned long long>(e.calls));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -161,10 +161,12 @@ int wmain(int argc, wchar_t** argv)
|
|||||||
// Subsystem isolation: skip installing the ones whose bit is set in disable_mask
|
// Subsystem isolation: skip installing the ones whose bit is set in disable_mask
|
||||||
// (0x1=input 0x2=focus 0x4=audio 0x8=video). Lets us bisect which injected
|
// (0x1=input 0x2=focus 0x4=audio 0x8=video). Lets us bisect which injected
|
||||||
// subsystem freezes a given game.
|
// subsystem freezes a given game.
|
||||||
static const char* kSubsysNames[] = {"input", "focus", "audio", "video"};
|
static const char* kSubsysNames[] = {"input", "focus", "audio", "video", "mkb"};
|
||||||
for (std::uint32_t i = 0; i < coop::HookSubsys_Count; ++i)
|
for (std::uint32_t i = 0; i < coop::HookSubsys_Count; ++i)
|
||||||
{
|
{
|
||||||
const bool disabled = (disable_mask & (1u << i)) != 0;
|
// MKB forwarding needs the host to stream events, which this probe doesn't, so
|
||||||
|
// keep it off here regardless of the mask (avoids confounding crash bisection).
|
||||||
|
const bool disabled = (i == coop::HookSubsys_Mkb) || (disable_mask & (1u << i)) != 0;
|
||||||
block->control.subsystem_disabled[i].store(disabled ? 1u : 0u, std::memory_order_release);
|
block->control.subsystem_disabled[i].store(disabled ? 1u : 0u, std::memory_order_release);
|
||||||
std::printf("subsystem %-6s %s\n", kSubsysNames[i], disabled ? "DISABLED" : "on");
|
std::printf("subsystem %-6s %s\n", kSubsysNames[i], disabled ? "DISABLED" : "on");
|
||||||
}
|
}
|
||||||
@@ -238,11 +240,11 @@ int wmain(int argc, wchar_t** argv)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::printf("\nInstalled hooks (%u):\n", status.hook_entry_count);
|
std::printf("\nInstalled hooks (%u):\n", status.hook_entry_count);
|
||||||
static const char* kSubsys[] = {"Input", "Focus", "Audio", "Video"};
|
static const char* kSubsys[] = {"Input", "Focus", "Audio", "Video", "MKB"};
|
||||||
for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i)
|
for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i)
|
||||||
{
|
{
|
||||||
const coop::HookEntry& e = status.hook_entries[i];
|
const coop::HookEntry& e = status.hook_entries[i];
|
||||||
std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 4 ? kSubsys[e.subsystem] : "?", e.name,
|
std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 5 ? kSubsys[e.subsystem] : "?", e.name,
|
||||||
e.installed ? "ON " : "off", static_cast<unsigned long long>(e.calls));
|
e.installed ? "ON " : "off", static_cast<unsigned long long>(e.calls));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user