// 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 = 5; // '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"); // Maximum render streams the diagnostics track. The hook captures only the // first ("primary"); the rest are surfaced so a multi-stream game is visible. inline constexpr std::uint32_t kMaxAudioStreams = 4; // One render stream the hook observed, for the Audio panel's debug view. Plain // POD (no atomics): diagnostics tolerate benign cross-process races like the // other HookStatus counters. frames_rendered is cumulative; the host derives // "live vs idle" from successive deltas. struct AudioStreamInfo { std::uint32_t is_primary; // 1 = the stream the hook captures/silences std::uint32_t sample_rate; std::uint16_t channels; std::uint16_t bits; std::uint32_t format_tag; // WAVE_FORMAT_* of this stream std::uint64_t frames_rendered; }; // Orthogonal hook subsystems the host can install/remove independently. enum HookSubsystem : std::uint32_t { HookSubsys_Input = 0, // XInput hooks (forward the guest pad) HookSubsys_Focus = 1, // focus spoof (keep the game running unfocused) HookSubsys_Audio = 2, // WASAPI render-hook (audio mirror without echo) HookSubsys_Count = 3, }; // Maximum individual hooks reported in the registry (a few per subsystem). inline constexpr std::uint32_t kMaxHookEntries = 24; // One installed hook, for the Injection panel's hook list. POD diagnostics, like // AudioStreamInfo: the hook is the sole writer; benign cross-process races are ok. struct HookEntry { char name[40]; // e.g. "XInputGetState" std::uint32_t subsystem; // HookSubsystem std::uint32_t installed; // 1 if currently hooked std::uint64_t calls; // cumulative times the detour ran }; // Indices into HookStatus::focus_query_calls. enum FocusApi : std::uint32_t { FocusApi_Foreground = 0, // GetForegroundWindow FocusApi_Active = 1, // GetActiveWindow FocusApi_Focus = 2, // GetFocus FocusApi_Count = 3, }; // Hook -> host back-channel. The injected DLL is the sole writer; the host reads // it for the diagnostics overlay: is the hook attached, which slots is the game // polling, does it use the focus APIs, and does it read input through a // focus-gated path (Raw Input / DirectInput)? Diagnostics only, so the non-atomic // fields tolerate benign cross-process races. struct HookStatus { std::atomic heartbeat; // DLL bumps ~4x/sec while alive std::atomic get_state_calls[kMaxPads]; // XInputGetState/Ex per slot std::atomic get_caps_calls[kMaxPads]; // XInputGetCapabilities per slot std::atomic focus_query_calls[FocusApi_Count]; // focus API calls, see FocusApi 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::uint64_t game_hwnd; // window the DLL subclassed (0 if none yet) // Input-path diagnostics: which focus-gated mechanism (if any) the game uses. std::uint32_t raw_input_registered; // process has any Raw Input registration std::uint32_t raw_input_gamepad; // ... for a joystick/gamepad usage page std::uint32_t raw_input_gamepad_sink; // ... and that usage has RIDEV_INPUTSINK (bg delivery) std::uint32_t dinput_loaded; // dinput8.dll is present in the process // Audio render-hook diagnostics. Stream counting runs whenever the DLL is // injected, independent of whether audio mirroring is enabled, so a // multi-stream game is visible before/without turning the mirror on. std::uint32_t audio_streams_seen; // distinct render clients ever created AudioStreamInfo audio_streams[kMaxAudioStreams]; // per-slot detail, [0] is primary // Hook registry: every individual hook the DLL has installed, with a running // call count. Lets the Injection panel list exactly what's hooked and how busy. std::uint32_t hook_entry_count; HookEntry hook_entries[kMaxHookEntries]; }; // 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