Files
CoopAllTheThings/hook/src/ipc_client.hpp
BlackMark 36b861d167 Phase 2: Present-hook video path (shared-texture mirror)
Add an injected IDXGISwapChain::Present / Present1 hook as a lower-latency,
border-free alternative to WGC. The hook copies the swapchain backbuffer into a
shared keyed-mutex texture (coop_video_<pid>); the host opens it by name and
samples it. New opt-in HookSubsys_Video (protocol v6 -> v7); the Video mirror
panel gains a WGC vs Hooked source toggle that installs/removes the subsystem.

Verified by present_hook_test (drives a real D3D11 swapchain end-to-end and reads
the rendered pixels back through the shared texture) and against Phantom Brave
(D3D9: hook installs cleanly and stays idle, WGC fallback). All 5 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 11:34:12 +02:00

223 lines
5.8 KiB
C++

// Hook-side view of the shared-memory IPC channel. The host creates the section
// (named by the target game's pid); we open it from inside the game and read the
// forwarded pad state the host publishes each frame.
#pragma once
#include <atomic>
#include <cstdint>
#include <windows.h>
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
namespace coop::hook
{
class IpcClient
{
public:
// Tries to open the section a few times: the host may inject us slightly
// before (or after) it creates the mapping. Returns true once connected.
bool connect(int attempts, int delay_ms)
{
const std::wstring name = shared_memory_name(GetCurrentProcessId());
for (int i = 0; i < attempts; ++i)
{
if (shm_.open(name, sizeof(SharedBlock)))
{
auto* block = shm_.as<SharedBlock>();
if (block->magic == kProtocolMagic && block->version == kProtocolVersion)
{
block_ = block;
return true;
}
shm_.reset(); // present but not a contract we understand; retry
}
Sleep(static_cast<DWORD>(delay_ms));
}
return false;
}
[[nodiscard]] bool connected() const
{
return block_ != nullptr;
}
// Host-requested install state for a subsystem (default = install, since the
// mapping is zero-filled and 0 means "disabled flag clear" = install).
[[nodiscard]] bool subsystem_install_requested(std::uint32_t subsystem) const
{
if (block_ == nullptr || subsystem >= HookSubsys_Count)
{
return true;
}
return block_->control.subsystem_disabled[subsystem].load(std::memory_order_acquire) == 0;
}
// Copies a torn-free snapshot of all slots. Returns false only if the host
// was mid-write for the whole spin window (caller should reuse its cache).
bool snapshot(CoopPadState (&out)[kMaxPads], std::uint32_t& count) const
{
if (block_ == nullptr)
{
return false;
}
return read_pads(*block_, out, count);
}
// --- Status back-channel (hook -> host diagnostics) --------------------
// Record that the game queried a controller slot via XInputGetState/Ex.
void note_state_query(std::uint32_t user_index)
{
if (block_ != nullptr && user_index < kMaxPads)
{
block_->status.get_state_calls[user_index].fetch_add(1, std::memory_order_relaxed);
}
}
void note_caps_query(std::uint32_t user_index)
{
if (block_ != nullptr && user_index < kMaxPads)
{
block_->status.get_caps_calls[user_index].fetch_add(1, std::memory_order_relaxed);
}
}
void note_focus_query(FocusApi which)
{
if (block_ != nullptr && which < FocusApi_Count)
{
block_->status.focus_query_calls[which].fetch_add(1, std::memory_order_relaxed);
}
}
void mark_attached()
{
if (block_ != nullptr)
{
block_->status.game_pid = GetCurrentProcessId();
block_->status.attached = 1;
}
}
// XInput hooks were removed (host unhooked input): clear the attached flag so
// the Controllers panel stops showing stale poll rates.
void mark_detached()
{
if (block_ != nullptr)
{
block_->status.attached = 0;
}
}
void mark_focus_spoof(bool active, std::uint64_t game_hwnd)
{
if (block_ != nullptr)
{
block_->status.focus_spoof = active ? 1u : 0u;
block_->status.game_hwnd = game_hwnd;
}
}
void set_input_diagnostics(bool raw_registered, bool raw_gamepad, bool raw_gamepad_sink, bool dinput)
{
if (block_ != nullptr)
{
block_->status.raw_input_registered = raw_registered ? 1u : 0u;
block_->status.raw_input_gamepad = raw_gamepad ? 1u : 0u;
block_->status.raw_input_gamepad_sink = raw_gamepad_sink ? 1u : 0u;
block_->status.dinput_loaded = dinput ? 1u : 0u;
}
}
void heartbeat()
{
if (block_ != nullptr)
{
block_->status.heartbeat.fetch_add(1, std::memory_order_relaxed);
}
}
// --- Audio render-hook diagnostics -------------------------------------
// Total distinct render streams the audio hook has observed.
void set_audio_streams_seen(std::uint32_t count)
{
if (block_ != nullptr)
{
block_->status.audio_streams_seen = count;
}
}
// Publish a tracked stream's format/role into its debug slot.
void publish_audio_stream(std::uint32_t slot, const AudioStreamInfo& info)
{
if (block_ != nullptr && slot < kMaxAudioStreams)
{
block_->status.audio_streams[slot] = info;
}
}
// Update a tracked stream's cumulative frame count (host derives live/idle).
void note_audio_frames(std::uint32_t slot, std::uint64_t frames)
{
if (block_ != nullptr && slot < kMaxAudioStreams)
{
block_->status.audio_streams[slot].frames_rendered = frames;
}
}
// --- Present-hook video channel ----------------------------------------
// Record that the game's Present() ran (diagnostic counter, hook is sole writer).
void note_present()
{
if (block_ != nullptr)
{
block_->video.present_calls += 1;
}
}
// Publish that a fresh backbuffer copy is in the shared texture (named
// coop_video_<pid>) with these dimensions/format; bumps the generation the host
// polls. The texture itself is shared out-of-band by name, not through here.
void publish_video_frame(std::uint32_t width, std::uint32_t height, std::uint32_t format)
{
if (block_ != nullptr)
{
block_->video.width = width;
block_->video.height = height;
block_->video.format = format;
block_->video.generation.fetch_add(1, std::memory_order_release);
}
}
// --- Hook registry -----------------------------------------------------
// Publish the installed-hooks table (name / subsystem / installed / calls).
void publish_hook_entries(const HookEntry* entries, std::uint32_t count)
{
if (block_ == nullptr)
{
return;
}
if (count > kMaxHookEntries)
{
count = kMaxHookEntries;
}
for (std::uint32_t i = 0; i < count; ++i)
{
block_->status.hook_entries[i] = entries[i];
}
block_->status.hook_entry_count = count;
}
private:
SharedMemory shm_;
SharedBlock* block_ = nullptr;
};
} // namespace coop::hook