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:
@@ -12,7 +12,7 @@ namespace coop
|
||||
|
||||
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
||||
// 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.
|
||||
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_Audio = 2, // WASAPI render-hook (audio mirror without echo)
|
||||
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).
|
||||
@@ -152,6 +153,43 @@ struct VideoShare
|
||||
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.
|
||||
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).
|
||||
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,
|
||||
@@ -249,4 +290,34 @@ inline bool read_pads(const SharedBlock& block, CoopPadState (&out)[kMaxPads], s
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user