// 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 = 1; // '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"); // 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]; // 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"); // --- 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