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:
128
host/src/capture/shared_texture.cpp
Normal file
128
host/src/capture/shared_texture.cpp
Normal 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
|
||||
69
host/src/capture/shared_texture.hpp
Normal file
69
host/src/capture/shared_texture.hpp
Normal 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
|
||||
Reference in New Issue
Block a user