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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user