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>
223 lines
5.8 KiB
C++
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
|