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
|
||||
@@ -3,13 +3,21 @@
|
||||
#include <cstdio>
|
||||
|
||||
#include "imgui.h"
|
||||
#include "injection_panel.hpp"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
const ImVec4 kGreen(0.4f, 1.0f, 0.4f, 1.0f);
|
||||
const ImVec4 kRed(1.0f, 0.45f, 0.4f, 1.0f);
|
||||
} // namespace
|
||||
|
||||
bool CapturePanel::init(ID3D11Device* device)
|
||||
{
|
||||
device_ = device;
|
||||
shared_.init(device); // best effort; the Hooked source is unavailable if it fails
|
||||
return renderer_.init(device);
|
||||
}
|
||||
|
||||
@@ -19,35 +27,89 @@ void CapturePanel::draw_ui(const FrameStats& stats)
|
||||
ImGui::SetNextWindowSize(ImVec2(360, 0), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("Video mirror");
|
||||
|
||||
const bool have_target = target_ != nullptr && IsWindow(target_);
|
||||
ImGui::BeginDisabled(!have_target);
|
||||
if (ImGui::Checkbox("Mirror game window", &enabled_))
|
||||
const unsigned long hook_pid = injection_ != nullptr ? injection_->target_pid() : 0;
|
||||
const bool have_wgc_target = target_ != nullptr && IsWindow(target_);
|
||||
const bool have_hook = hook_pid != 0;
|
||||
const bool have_source = source_ == Source_Hooked ? have_hook : have_wgc_target;
|
||||
|
||||
ImGui::BeginDisabled(!have_source);
|
||||
if (ImGui::Checkbox("Mirror game window", &enabled_) && !enabled_)
|
||||
{
|
||||
if (!enabled_)
|
||||
capture_.stop();
|
||||
shared_.reset();
|
||||
if (injection_ != nullptr)
|
||||
{
|
||||
capture_.stop();
|
||||
injection_->request_video(false); // stop the in-game Present hook
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (!have_target)
|
||||
{
|
||||
ImGui::TextDisabled("Inject into a game first (its window is the source).");
|
||||
}
|
||||
|
||||
// Start/restart capture when enabled and the target window changes.
|
||||
if (enabled_ && have_target && capture_.target() != target_)
|
||||
// Source selector. Switching tears down the other source and (un)installs the
|
||||
// Present hook so the game only pays for the path actually in use.
|
||||
ImGui::TextUnformatted("Source:");
|
||||
ImGui::SameLine();
|
||||
int prev_source = source_;
|
||||
ImGui::RadioButton("WGC", &source_, Source_Wgc);
|
||||
ImGui::SameLine();
|
||||
ImGui::RadioButton("Hooked (Present)", &source_, Source_Hooked);
|
||||
if (source_ != prev_source)
|
||||
{
|
||||
if (!capture_.start(target_, device_))
|
||||
capture_.stop();
|
||||
shared_.reset();
|
||||
if (injection_ != nullptr)
|
||||
{
|
||||
enabled_ = false;
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "Failed to start capture.");
|
||||
injection_->request_video(enabled_ && source_ == Source_Hooked);
|
||||
}
|
||||
}
|
||||
|
||||
if (capture_.running())
|
||||
if (!have_source)
|
||||
{
|
||||
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Capturing %ux%u", capture_.frame_width(),
|
||||
capture_.frame_height());
|
||||
ImGui::TextDisabled(source_ == Source_Hooked
|
||||
? "Inject into a game first (the Present hook is the source)."
|
||||
: "Inject into a game first (its window is the source).");
|
||||
}
|
||||
|
||||
if (source_ == Source_Wgc)
|
||||
{
|
||||
// Start/restart WGC capture when enabled and the target window changes.
|
||||
if (enabled_ && have_wgc_target && capture_.target() != target_)
|
||||
{
|
||||
if (!capture_.start(target_, device_))
|
||||
{
|
||||
enabled_ = false;
|
||||
ImGui::TextColored(kRed, "Failed to start capture.");
|
||||
}
|
||||
}
|
||||
if (capture_.running())
|
||||
{
|
||||
ImGui::TextColored(kGreen, "Capturing %ux%u (WGC)", capture_.frame_width(), capture_.frame_height());
|
||||
}
|
||||
}
|
||||
else // Source_Hooked
|
||||
{
|
||||
if (enabled_ && injection_ != nullptr)
|
||||
{
|
||||
// Keep the subsystem requested (a fresh inject may have reset control).
|
||||
if (!injection_->video_requested())
|
||||
{
|
||||
injection_->request_video(true);
|
||||
}
|
||||
const VideoShareView share = injection_->video_share();
|
||||
if (shared_.frames_copied() > 0 && shared_.width() > 0)
|
||||
{
|
||||
ImGui::TextColored(kGreen, "Mirroring %ux%u (hooked, %llu frames)", shared_.width(),
|
||||
shared_.height(), static_cast<unsigned long long>(shared_.frames_copied()));
|
||||
}
|
||||
else if (share.present_calls > 0)
|
||||
{
|
||||
ImGui::TextColored(kGreen, "Present hooked (%llu calls); opening shared texture...",
|
||||
static_cast<unsigned long long>(share.present_calls));
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui::TextDisabled("Waiting for hooked frames (the game may not render via DXGI).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draw_perf_graphs(stats);
|
||||
@@ -95,7 +157,22 @@ void CapturePanel::draw_perf_graphs(const FrameStats& stats)
|
||||
|
||||
void CapturePanel::render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h)
|
||||
{
|
||||
if (capture_.running())
|
||||
if (!enabled_)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (source_ == Source_Hooked)
|
||||
{
|
||||
if (injection_ == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (shared_.update(injection_->video_share(), injection_->target_pid()) && shared_.srv() != nullptr)
|
||||
{
|
||||
renderer_.draw(ctx, shared_.srv(), shared_.width(), shared_.height(), dst_w, dst_h);
|
||||
}
|
||||
}
|
||||
else if (capture_.running())
|
||||
{
|
||||
capture_.draw_latest(renderer_, ctx, dst_w, dst_h);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// Owns the video-mirror pipeline (WGC capture + frame renderer) and its UI.
|
||||
// The target window comes from the injected hook's reported game HWND.
|
||||
// Owns the video-mirror pipeline and its UI. Two interchangeable sources feed the
|
||||
// same letterboxed renderer: Windows Graphics Capture (no injection, the default)
|
||||
// and the injected Present-hook's shared backbuffer texture (lower latency, needs
|
||||
// the video subsystem hooked). The target window/pid come from the injected hook.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
@@ -8,23 +10,33 @@
|
||||
#include <windows.h>
|
||||
|
||||
#include "capture/frame_renderer.hpp"
|
||||
#include "capture/shared_texture.hpp"
|
||||
#include "capture/window_capture.hpp"
|
||||
#include "ui/app_chrome.hpp"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
class InjectionPanel;
|
||||
|
||||
class CapturePanel
|
||||
{
|
||||
public:
|
||||
bool init(ID3D11Device* device);
|
||||
|
||||
// The window to mirror (0 if none yet); typically the injected game's HWND.
|
||||
// The window to mirror via WGC (0 if none yet); typically the injected game's HWND.
|
||||
void set_target(HWND target)
|
||||
{
|
||||
target_ = target;
|
||||
}
|
||||
|
||||
// The injection panel supplies the target pid + Present-hook video channel and
|
||||
// lets this panel install/remove the video subsystem when the source is Hooked.
|
||||
void set_injection(InjectionPanel* injection)
|
||||
{
|
||||
injection_ = injection;
|
||||
}
|
||||
|
||||
// `stats` are the host's render frame-timing, drawn as the mirror's
|
||||
// frametime / FPS graphs (this window is what the mirror renders into).
|
||||
void draw_ui(const FrameStats& stats);
|
||||
@@ -33,13 +45,22 @@ public:
|
||||
void render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h);
|
||||
|
||||
private:
|
||||
enum Source : int
|
||||
{
|
||||
Source_Wgc = 0, // Windows Graphics Capture
|
||||
Source_Hooked = 1, // injected Present-hook shared texture
|
||||
};
|
||||
|
||||
void draw_perf_graphs(const FrameStats& stats);
|
||||
|
||||
ID3D11Device* device_ = nullptr;
|
||||
FrameRenderer renderer_;
|
||||
WindowCapture capture_;
|
||||
SharedTextureSource shared_;
|
||||
InjectionPanel* injection_ = nullptr;
|
||||
HWND target_ = nullptr;
|
||||
bool enabled_ = false;
|
||||
int source_ = Source_Wgc;
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
|
||||
@@ -96,6 +96,7 @@ void InjectionPanel::inject_selected()
|
||||
server_.set_subsystem_enabled(HookSubsys_Input, want_input_);
|
||||
server_.set_subsystem_enabled(HookSubsys_Focus, want_focus_);
|
||||
server_.set_subsystem_enabled(HookSubsys_Audio, want_audio_);
|
||||
server_.set_subsystem_enabled(HookSubsys_Video, want_video_);
|
||||
|
||||
const InjectResult result = inject_dll(selected_pid_, hook_dll_path());
|
||||
if (result.status == InjectStatus::Ok)
|
||||
@@ -147,7 +148,7 @@ void InjectionPanel::publish(const std::array<PadInfo, kMaxPads>& pads)
|
||||
|
||||
void InjectionPanel::draw_hook_list(const HookStatusView& status)
|
||||
{
|
||||
static const char* kSubsysName[] = {"Input", "Focus", "Audio"};
|
||||
static const char* kSubsysName[] = {"Input", "Focus", "Audio", "Video"};
|
||||
|
||||
const std::uint32_t n = status.hook_entry_count < kMaxHookEntries ? status.hook_entry_count : kMaxHookEntries;
|
||||
if (n == 0)
|
||||
@@ -181,7 +182,7 @@ void InjectionPanel::draw_hook_list(const HookStatusView& status)
|
||||
{
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextDisabled("%s", kSubsysName[sub < 3 ? sub : 0]);
|
||||
ImGui::TextDisabled("%s", kSubsysName[sub < HookSubsys_Count ? sub : 0]);
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TableNextColumn();
|
||||
header_done = true;
|
||||
@@ -235,6 +236,7 @@ void InjectionPanel::draw_subsystem_controls(const HookStatusView& status)
|
||||
{"Input forwarding (XInput)", HookSubsys_Input, &want_input_, "controller input reaches the game"},
|
||||
{"Focus spoof", HookSubsys_Focus, &want_focus_, "the game keeps running unfocused"},
|
||||
{"Audio render-hook", HookSubsys_Audio, &want_audio_, "audio mirror without echo"},
|
||||
{"Video Present-hook", HookSubsys_Video, &want_video_, "Video mirror can use the hooked source"},
|
||||
};
|
||||
|
||||
for (const Row& r : rows)
|
||||
|
||||
@@ -49,6 +49,32 @@ public:
|
||||
server_.drain_logs(std::forward<F>(emit));
|
||||
}
|
||||
|
||||
// --- Present-hook video path (consumed by the Video mirror panel) ----------
|
||||
|
||||
[[nodiscard]] unsigned long target_pid() const
|
||||
{
|
||||
return server_.target_pid();
|
||||
}
|
||||
|
||||
// The hook's Present-hook video channel snapshot (shared-texture descriptor).
|
||||
[[nodiscard]] VideoShareView video_share() const
|
||||
{
|
||||
return server_.video_share();
|
||||
}
|
||||
|
||||
// Request the Present-hook video subsystem be installed/removed. Keeps the
|
||||
// Injection panel's own checkbox in sync, so the Video panel can drive it.
|
||||
void request_video(bool on)
|
||||
{
|
||||
want_video_ = on;
|
||||
server_.set_subsystem_enabled(HookSubsys_Video, on);
|
||||
}
|
||||
|
||||
[[nodiscard]] bool video_requested() const
|
||||
{
|
||||
return want_video_;
|
||||
}
|
||||
|
||||
private:
|
||||
void refresh_processes();
|
||||
void inject_selected();
|
||||
@@ -72,7 +98,8 @@ private:
|
||||
bool want_input_ = true;
|
||||
bool want_focus_ = true;
|
||||
bool want_audio_ = true;
|
||||
bool injected_ = false; // a hook DLL is loaded in the target
|
||||
bool want_video_ = false; // Present-hook video path: opt-in (WGC is the default)
|
||||
bool injected_ = false; // a hook DLL is loaded in the target
|
||||
|
||||
// Heartbeat liveness tracking (is the injected DLL responding?).
|
||||
std::uint32_t last_heartbeat_ = 0;
|
||||
|
||||
@@ -88,6 +88,22 @@ HookStatusView IpcServer::hook_status() const
|
||||
return view;
|
||||
}
|
||||
|
||||
VideoShareView IpcServer::video_share() const
|
||||
{
|
||||
VideoShareView v;
|
||||
if (block_ == nullptr)
|
||||
{
|
||||
return v;
|
||||
}
|
||||
const VideoShare& s = block_->video;
|
||||
v.generation = s.generation.load(std::memory_order_acquire);
|
||||
v.width = s.width;
|
||||
v.height = s.height;
|
||||
v.format = s.format;
|
||||
v.present_calls = s.present_calls;
|
||||
return v;
|
||||
}
|
||||
|
||||
void IpcServer::set_subsystem_enabled(std::uint32_t subsystem, bool enabled)
|
||||
{
|
||||
if (block_ != nullptr && subsystem < HookSubsys_Count)
|
||||
|
||||
@@ -38,6 +38,16 @@ struct HookStatusView
|
||||
HookEntry hook_entries[kMaxHookEntries] = {};
|
||||
};
|
||||
|
||||
// Plain snapshot of the Present-hook video channel for the Video mirror panel.
|
||||
struct VideoShareView
|
||||
{
|
||||
std::uint32_t generation = 0; // bumps per shared frame; 0 = nothing shared yet
|
||||
std::uint32_t width = 0; // shared texture dimensions / DXGI format
|
||||
std::uint32_t height = 0;
|
||||
std::uint32_t format = 0;
|
||||
std::uint64_t present_calls = 0; // cumulative Present() detours (diagnostic)
|
||||
};
|
||||
|
||||
class IpcServer
|
||||
{
|
||||
public:
|
||||
@@ -53,6 +63,9 @@ public:
|
||||
// Reads the hook's diagnostics back-channel (zeroed if not started).
|
||||
[[nodiscard]] HookStatusView hook_status() const;
|
||||
|
||||
// Reads the Present-hook video channel (zeroed if not started).
|
||||
[[nodiscard]] VideoShareView video_share() const;
|
||||
|
||||
// Request a hook subsystem be installed (true) or removed (false). The hook
|
||||
// reconciles on its next tick. No-op if not started.
|
||||
void set_subsystem_enabled(std::uint32_t subsystem, bool enabled);
|
||||
|
||||
@@ -75,6 +75,7 @@ int run()
|
||||
MessageBoxW(nullptr, L"Failed to initialize the video mirror.", L"CoopAllTheThings", MB_ICONERROR);
|
||||
return 1;
|
||||
}
|
||||
capture.set_injection(&injection); // for the Present-hook (Hooked) video source
|
||||
|
||||
// The overlay can be hidden (F1) so the window is a clean mirror for Remote
|
||||
// Play Together; the pipelines keep running underneath either way.
|
||||
|
||||
Reference in New Issue
Block a user