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>
This commit is contained in:
2026-06-20 11:34:12 +02:00
parent 9557b9ca69
commit 36b861d167
21 changed files with 1160 additions and 36 deletions

View File

@@ -0,0 +1,128 @@
#include "capture/shared_texture.hpp"
#include "coop/protocol.hpp"
#include "coop/shared_memory.hpp"
namespace coop
{
bool SharedTextureSource::init(ID3D11Device* device)
{
if (device == nullptr || FAILED(device->QueryInterface(IID_PPV_ARGS(&device_))))
{
return false;
}
device_->GetImmediateContext(&ctx_);
return ctx_ != nullptr;
}
void SharedTextureSource::reset()
{
srv_.Reset();
private_.Reset();
mutex_.Reset();
shared_.Reset();
pid_ = 0;
width_ = height_ = format_ = 0;
last_generation_ = 0;
frames_copied_ = 0;
}
bool SharedTextureSource::reopen(unsigned long pid, const VideoShareView& share)
{
srv_.Reset();
private_.Reset();
mutex_.Reset();
shared_.Reset();
width_ = height_ = format_ = 0;
pid_ = pid;
if (pid == 0 || share.width == 0 || share.height == 0)
{
return false; // the hook hasn't shared a backbuffer yet
}
const std::wstring name = video_share_name(pid);
if (FAILED(device_->OpenSharedResourceByName(name.c_str(),
DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE,
IID_PPV_ARGS(&shared_))) ||
shared_ == nullptr)
{
return false;
}
if (FAILED(shared_.As(&mutex_)) || mutex_ == nullptr)
{
shared_.Reset();
return false;
}
// Private copy we sample from, so we only hold the keyed mutex during the copy.
D3D11_TEXTURE2D_DESC desc{};
desc.Width = share.width;
desc.Height = share.height;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = static_cast<DXGI_FORMAT>(share.format);
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
if (FAILED(device_->CreateTexture2D(&desc, nullptr, &private_)) || private_ == nullptr)
{
mutex_.Reset();
shared_.Reset();
return false;
}
if (FAILED(device_->CreateShaderResourceView(private_.Get(), nullptr, &srv_)))
{
srv_.Reset();
private_.Reset();
mutex_.Reset();
shared_.Reset();
return false;
}
width_ = share.width;
height_ = share.height;
format_ = share.format;
return true;
}
bool SharedTextureSource::update(const VideoShareView& share, unsigned long pid)
{
if (device_ == nullptr || pid == 0)
{
reset();
return false;
}
// (Re)open whenever the target or the published backbuffer geometry changes.
if (pid != pid_ || share.width != width_ || share.height != height_ || share.format != format_)
{
if (!reopen(pid, share))
{
return srv_ != nullptr; // couldn't open yet; keep any prior frame
}
last_generation_ = 0; // force a copy of the current frame
}
if (shared_ == nullptr || mutex_ == nullptr)
{
return srv_ != nullptr;
}
if (share.generation == last_generation_)
{
return srv_ != nullptr; // no new frame; keep showing the last copy
}
// Bounded wait so a stalled producer can't hang the host's render thread.
if (mutex_->AcquireSync(kVideoMutexKey, 8) == S_OK)
{
ctx_->CopyResource(private_.Get(), shared_.Get());
mutex_->ReleaseSync(kVideoMutexKey);
last_generation_ = share.generation;
++frames_copied_;
}
return srv_ != nullptr;
}
} // namespace coop

View File

@@ -0,0 +1,69 @@
// Host-side consumer of the injected Present-hook's shared backbuffer texture.
// Opens the keyed-mutex texture the hook publishes (coop_video_<pid>) by name,
// copies the latest frame into a private texture under the keyed mutex, and
// exposes an SRV the FrameRenderer can draw -- the lower-latency alternative to
// Windows Graphics Capture. See hook/src/present_hook.cpp and coop/protocol.hpp.
#pragma once
#include <cstdint>
#include <string>
#include <d3d11_1.h>
#include <wrl/client.h>
#include "ipc/ipc_server.hpp"
namespace coop
{
class SharedTextureSource
{
public:
// Binds to the host's device (must support ID3D11Device1). Returns false if not.
bool init(ID3D11Device* device);
// Polls the hook's video channel for `pid`. (Re)opens the shared texture when
// the pid/dimensions/format change, and copies the newest frame into the
// private texture under the keyed mutex. Returns true if a frame is available
// to draw (sticky: keeps the last copy until a new one or a reset).
bool update(const VideoShareView& share, unsigned long pid);
// Drop the opened resources (target changed / mirror turned off).
void reset();
[[nodiscard]] ID3D11ShaderResourceView* srv() const
{
return srv_.Get();
}
[[nodiscard]] std::uint32_t width() const
{
return width_;
}
[[nodiscard]] std::uint32_t height() const
{
return height_;
}
[[nodiscard]] std::uint64_t frames_copied() const
{
return frames_copied_;
}
private:
bool reopen(unsigned long pid, const VideoShareView& share);
Microsoft::WRL::ComPtr<ID3D11Device1> device_;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> ctx_;
Microsoft::WRL::ComPtr<ID3D11Texture2D> shared_; // opened from the hook, by name
Microsoft::WRL::ComPtr<IDXGIKeyedMutex> mutex_;
Microsoft::WRL::ComPtr<ID3D11Texture2D> private_; // our sampled copy
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> srv_;
unsigned long pid_ = 0;
std::uint32_t width_ = 0;
std::uint32_t height_ = 0;
std::uint32_t format_ = 0;
std::uint32_t last_generation_ = 0;
std::uint64_t frames_copied_ = 0;
};
} // namespace coop