// IPC contract shared between the host (coop_host.exe) and the injected hook // DLL (coop_hook.dll). Both sides compile this identical header, so the memory // layout must stay POD and version-locked. #pragma once #include #include 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 = 2; // 'COOP' little-endian, used to sanity-check the mapping before trusting it. inline constexpr std::uint32_t kProtocolMagic = 0x504F4F43u; // XInput exposes four controller slots; we mirror that fixed count. inline constexpr std::uint32_t kMaxPads = 4; // The shared-memory section is named per host process id so multiple sessions // can coexist. Format with the target game's pid: coop_ipc_. inline constexpr wchar_t kSharedMemoryPrefix[] = L"Local\\coop_ipc_"; // One controller's state, laid out to map 1:1 onto XINPUT_GAMEPAD plus the // metadata the hook needs. Field names/types match XINPUT_GAMEPAD so the hook // can memcpy the trailing region straight into an XINPUT_STATE. struct CoopPadState { std::uint8_t connected; // 1 if a guest/host pad is mapped to this slot std::uint8_t reserved[3]; std::uint32_t packet; // bumps on change -> XINPUT_STATE::dwPacketNumber std::uint16_t buttons; // XINPUT_GAMEPAD_* bitmask std::uint8_t left_trigger; std::uint8_t right_trigger; std::int16_t thumb_lx; std::int16_t thumb_ly; std::int16_t thumb_rx; std::int16_t thumb_ry; }; static_assert(sizeof(CoopPadState) == 20, "CoopPadState layout must stay stable across both modules"); // Hook -> host back-channel. The injected DLL is the sole writer; the host reads // it for the diagnostics overlay (is the hook attached? is the game actually // polling it? did focus spoofing take?). Diagnostics only, so the few non-atomic // fields tolerate benign cross-process races. struct HookStatus { std::atomic heartbeat; // DLL bumps this ~4x/sec while alive std::atomic xinput_queries; // cumulative XInputGetState/Ex calls served std::uint32_t attached; // 1 once XInput hooks are installed std::uint32_t focus_spoof; // 1 once focus spoofing is active std::uint32_t game_pid; // the DLL's own pid (sanity check) std::uint32_t last_user_index; // last slot the game queried std::uint64_t game_hwnd; // window the DLL subclassed (0 if none yet) }; // Top-level shared block. The host is the sole writer of pad state; the hook is // the sole reader. A seqlock (even = stable, odd = write in progress) lets the // reader grab a torn-free snapshot without a kernel lock on the hot path. struct SharedBlock { std::uint32_t magic; std::uint32_t version; std::uint32_t pad_count; // number of populated slots, <= kMaxPads std::atomic sequence; CoopPadState pads[kMaxPads]; // Hook -> host diagnostics back-channel. HookStatus status; // Phase 2 appends the shared-texture handle/dimensions control fields here; // keep new members at the end so existing offsets never shift. }; static_assert(std::atomic::is_always_lock_free, "seqlock requires a lock-free 32-bit atomic for cross-process use"); static_assert(std::atomic::is_always_lock_free, "status counters need a lock-free 64-bit atomic for cross-process use"); // --- Seqlock helpers ------------------------------------------------------- // Writer side: publish a fresh set of pad states. Called from the host. inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint32_t count) { if (count > kMaxPads) { count = kMaxPads; } const std::uint32_t seq = block.sequence.load(std::memory_order_relaxed); block.sequence.store(seq + 1, std::memory_order_release); // -> odd: write begins std::atomic_thread_fence(std::memory_order_release); block.pad_count = count; for (std::uint32_t i = 0; i < count; ++i) { block.pads[i] = pads[i]; } for (std::uint32_t i = count; i < kMaxPads; ++i) { block.pads[i] = CoopPadState{}; } block.sequence.store(seq + 2, std::memory_order_release); // -> even: write done } // Reader side: copy a consistent snapshot. Called from the hook. Spins briefly // if a write is in flight; bounded so a crashed writer can't hang the game. inline bool read_pads(const SharedBlock& block, CoopPadState (&out)[kMaxPads], std::uint32_t& out_count) { for (int attempt = 0; attempt < 64; ++attempt) { const std::uint32_t before = block.sequence.load(std::memory_order_acquire); if (before & 1u) { continue; // writer mid-update, retry } std::uint32_t count = block.pad_count; if (count > kMaxPads) { count = kMaxPads; } for (std::uint32_t i = 0; i < kMaxPads; ++i) { out[i] = block.pads[i]; } std::atomic_thread_fence(std::memory_order_acquire); const std::uint32_t after = block.sequence.load(std::memory_order_acquire); if (before == after) { out_count = count; return true; } } return false; } } // namespace coop