Phase 1b: video mirror via Windows Graphics Capture
Mirror the injected game's window into the host's borderless window so Steam RPT streams a live copy of the game. No injection needed for video. - capture/window_capture: WGC capture of a target HWND. Wraps our D3D11 device as an IDirect3DDevice, creates a free-threaded frame pool + capture session, hides the cursor and (best-effort) the capture border. Frames arrive on a WGC thread and are handed to the render thread, which copies the newest into a shader-resource texture and draws it -- keeping all D3D11 context use on one thread. Handles window resize via frame-pool Recreate. - capture/frame_renderer: fullscreen-triangle shader that blits the captured texture letterboxed (aspect-preserved) into the window. - capture_panel: "Mirror game window" toggle + lifecycle; target HWND comes from the hook's reported game window. - main: winrt apartment init; mirrored frame drawn as background, ImGui on top. - CMake: link windowsapp + d3dcompiler. Verified: builds clean; host starts (apartment init + shader compile succeed). Visual capture quality/latency to be judged on a real game. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,13 @@ add_executable(coop_host WIN32
|
||||
src/imgui_layer.cpp
|
||||
src/debug_overlay.cpp
|
||||
src/injection_panel.cpp
|
||||
src/capture_panel.cpp
|
||||
src/input/xinput_source.cpp
|
||||
src/inject/process_list.cpp
|
||||
src/inject/injector.cpp
|
||||
src/ipc/ipc_server.cpp)
|
||||
src/ipc/ipc_server.cpp
|
||||
src/capture/frame_renderer.cpp
|
||||
src/capture/window_capture.cpp)
|
||||
|
||||
target_include_directories(coop_host PRIVATE src)
|
||||
|
||||
@@ -17,6 +20,8 @@ target_link_libraries(coop_host PRIVATE
|
||||
d3d11
|
||||
dxgi
|
||||
dwmapi
|
||||
d3dcompiler
|
||||
windowsapp
|
||||
xinput)
|
||||
|
||||
set_target_properties(coop_host PROPERTIES OUTPUT_NAME "coop_host")
|
||||
|
||||
119
host/src/capture/frame_renderer.cpp
Normal file
119
host/src/capture/frame_renderer.cpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#include "capture/frame_renderer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <d3dcompiler.h>
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
// Fullscreen triangle generated from SV_VertexID -- no vertex/index buffers
|
||||
// needed. Samples the source texture across the [0,1] UV range.
|
||||
constexpr char kShaderSource[] = R"(
|
||||
Texture2D g_tex : register(t0);
|
||||
SamplerState g_smp : register(s0);
|
||||
|
||||
struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; };
|
||||
|
||||
VSOut vs_main(uint id : SV_VertexID)
|
||||
{
|
||||
VSOut o;
|
||||
o.uv = float2((id << 1) & 2, id & 2);
|
||||
o.pos = float4(o.uv * float2(2, -2) + float2(-1, 1), 0, 1);
|
||||
return o;
|
||||
}
|
||||
|
||||
float4 ps_main(VSOut i) : SV_Target
|
||||
{
|
||||
return g_tex.Sample(g_smp, i.uv);
|
||||
}
|
||||
)";
|
||||
|
||||
ComPtr<ID3DBlob> compile(const char* entry, const char* target)
|
||||
{
|
||||
ComPtr<ID3DBlob> blob;
|
||||
ComPtr<ID3DBlob> errors;
|
||||
const HRESULT hr = D3DCompile(kShaderSource, sizeof(kShaderSource) - 1, "frame_renderer", nullptr, nullptr, entry,
|
||||
target, D3DCOMPILE_OPTIMIZATION_LEVEL3, 0, blob.GetAddressOf(), errors.GetAddressOf());
|
||||
if (FAILED(hr))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool FrameRenderer::init(ID3D11Device* device)
|
||||
{
|
||||
ComPtr<ID3DBlob> vs_blob = compile("vs_main", "vs_5_0");
|
||||
ComPtr<ID3DBlob> ps_blob = compile("ps_main", "ps_5_0");
|
||||
if (vs_blob == nullptr || ps_blob == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (FAILED(device->CreateVertexShader(vs_blob->GetBufferPointer(), vs_blob->GetBufferSize(), nullptr,
|
||||
vs_.GetAddressOf())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (FAILED(device->CreatePixelShader(ps_blob->GetBufferPointer(), ps_blob->GetBufferSize(), nullptr,
|
||||
ps_.GetAddressOf())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
D3D11_SAMPLER_DESC sd = {};
|
||||
sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
||||
sd.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sd.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
|
||||
sd.ComparisonFunc = D3D11_COMPARISON_NEVER;
|
||||
if (FAILED(device->CreateSamplerState(&sd, sampler_.GetAddressOf())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FrameRenderer::draw(ID3D11DeviceContext* ctx, ID3D11ShaderResourceView* srv, std::uint32_t src_w,
|
||||
std::uint32_t src_h, std::uint32_t dst_w, std::uint32_t dst_h)
|
||||
{
|
||||
if (srv == nullptr || src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Letterbox: fit the source rect inside the destination, preserving aspect.
|
||||
const float scale = std::min(static_cast<float>(dst_w) / src_w, static_cast<float>(dst_h) / src_h);
|
||||
const float vp_w = src_w * scale;
|
||||
const float vp_h = src_h * scale;
|
||||
|
||||
D3D11_VIEWPORT vp = {};
|
||||
vp.TopLeftX = (static_cast<float>(dst_w) - vp_w) * 0.5f;
|
||||
vp.TopLeftY = (static_cast<float>(dst_h) - vp_h) * 0.5f;
|
||||
vp.Width = vp_w;
|
||||
vp.Height = vp_h;
|
||||
vp.MinDepth = 0.0f;
|
||||
vp.MaxDepth = 1.0f;
|
||||
ctx->RSSetViewports(1, &vp);
|
||||
|
||||
ctx->IASetInputLayout(nullptr);
|
||||
ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
||||
ctx->VSSetShader(vs_.Get(), nullptr, 0);
|
||||
ctx->PSSetShader(ps_.Get(), nullptr, 0);
|
||||
ctx->PSSetShaderResources(0, 1, &srv);
|
||||
ctx->PSSetSamplers(0, 1, sampler_.GetAddressOf());
|
||||
ctx->Draw(3, 0);
|
||||
|
||||
// Unbind the SRV so the same texture can be a copy destination next frame.
|
||||
ID3D11ShaderResourceView* null_srv = nullptr;
|
||||
ctx->PSSetShaderResources(0, 1, &null_srv);
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
30
host/src/capture/frame_renderer.hpp
Normal file
30
host/src/capture/frame_renderer.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
// Draws a captured frame texture into the current render target as a letterboxed
|
||||
// full-screen image (aspect-preserved; the cleared background fills the bars).
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
class FrameRenderer
|
||||
{
|
||||
public:
|
||||
bool init(ID3D11Device* device);
|
||||
|
||||
// Draws `srv` (a srcW x srcH image) centered and scaled to fit within a
|
||||
// dstW x dstH target. Sets its own viewport; callers that draw afterwards
|
||||
// (e.g. ImGui) should restore theirs.
|
||||
void draw(ID3D11DeviceContext* ctx, ID3D11ShaderResourceView* srv, std::uint32_t src_w, std::uint32_t src_h,
|
||||
std::uint32_t dst_w, std::uint32_t dst_h);
|
||||
|
||||
private:
|
||||
Microsoft::WRL::ComPtr<ID3D11VertexShader> vs_;
|
||||
Microsoft::WRL::ComPtr<ID3D11PixelShader> ps_;
|
||||
Microsoft::WRL::ComPtr<ID3D11SamplerState> sampler_;
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
218
host/src/capture/window_capture.cpp
Normal file
218
host/src/capture/window_capture.cpp
Normal file
@@ -0,0 +1,218 @@
|
||||
#include "capture/window_capture.hpp"
|
||||
|
||||
#include "capture/frame_renderer.hpp"
|
||||
|
||||
// Interop headers bridge classic D3D11/HWND types and the WinRT capture API.
|
||||
#include <windows.graphics.capture.interop.h>
|
||||
#include <windows.graphics.directx.direct3d11.interop.h>
|
||||
|
||||
// Foundation must be included for IClosable::Close() definitions used below.
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Graphics.DirectX.h>
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
namespace winrt
|
||||
{
|
||||
using namespace Windows::Graphics;
|
||||
using namespace Windows::Graphics::Capture;
|
||||
using namespace Windows::Graphics::DirectX;
|
||||
using namespace Windows::Graphics::DirectX::Direct3D11;
|
||||
} // namespace winrt
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr auto kPixelFormat = winrt::DirectXPixelFormat::B8G8R8A8UIntNormalized;
|
||||
|
||||
// Pulls the underlying ID3D11Texture2D out of a captured frame's surface.
|
||||
ComPtr<ID3D11Texture2D> texture_from_surface(winrt::IDirect3DSurface const& surface)
|
||||
{
|
||||
auto access = surface.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>();
|
||||
ComPtr<ID3D11Texture2D> texture;
|
||||
if (access)
|
||||
{
|
||||
access->GetInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void**>(texture.GetAddressOf()));
|
||||
}
|
||||
return texture;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
WindowCapture::~WindowCapture()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool WindowCapture::start(HWND target, ID3D11Device* device)
|
||||
{
|
||||
stop();
|
||||
if (target == nullptr || device == nullptr || !IsWindow(target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
device_ = device;
|
||||
|
||||
// Wrap our D3D11 device as the WinRT device the frame pool renders on.
|
||||
ComPtr<IDXGIDevice> dxgi_device;
|
||||
if (FAILED(device->QueryInterface(IID_PPV_ARGS(dxgi_device.GetAddressOf()))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
winrt::com_ptr<::IInspectable> inspectable;
|
||||
if (FAILED(CreateDirect3D11DeviceFromDXGIDevice(dxgi_device.Get(), inspectable.put())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
winrt_device_ = inspectable.as<winrt::IDirect3DDevice>();
|
||||
|
||||
// Create a capture item for the target window via the interop factory.
|
||||
auto interop = winrt::get_activation_factory<winrt::GraphicsCaptureItem, ::IGraphicsCaptureItemInterop>();
|
||||
if (FAILED(interop->CreateForWindow(target, winrt::guid_of<winrt::GraphicsCaptureItem>(),
|
||||
winrt::put_abi(item_))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pool_size_ = item_.Size();
|
||||
frame_pool_ =
|
||||
winrt::Direct3D11CaptureFramePool::CreateFreeThreaded(winrt_device_, kPixelFormat, 2, pool_size_);
|
||||
session_ = frame_pool_.CreateCaptureSession(item_);
|
||||
frame_token_ = frame_pool_.FrameArrived({this, &WindowCapture::on_frame_arrived});
|
||||
|
||||
// Best-effort: hide the cursor and the yellow capture border (the border
|
||||
// API requires a recent Windows build, hence the guard).
|
||||
try
|
||||
{
|
||||
session_.IsCursorCaptureEnabled(false);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
try
|
||||
{
|
||||
session_.IsBorderRequired(false);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
|
||||
session_.StartCapture();
|
||||
target_ = target;
|
||||
return true;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void WindowCapture::stop()
|
||||
{
|
||||
if (frame_pool_ != nullptr && frame_token_)
|
||||
{
|
||||
frame_pool_.FrameArrived(frame_token_);
|
||||
frame_token_ = {};
|
||||
}
|
||||
if (session_ != nullptr)
|
||||
{
|
||||
session_.Close();
|
||||
session_ = nullptr;
|
||||
}
|
||||
if (frame_pool_ != nullptr)
|
||||
{
|
||||
frame_pool_.Close();
|
||||
frame_pool_ = nullptr;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
pending_ = nullptr;
|
||||
}
|
||||
item_ = nullptr;
|
||||
winrt_device_ = nullptr;
|
||||
latest_srv_.Reset();
|
||||
latest_.Reset();
|
||||
device_.Reset();
|
||||
target_ = nullptr;
|
||||
width_ = 0;
|
||||
height_ = 0;
|
||||
}
|
||||
|
||||
void WindowCapture::on_frame_arrived(winrt::Direct3D11CaptureFramePool const& pool,
|
||||
winrt::Windows::Foundation::IInspectable const&)
|
||||
{
|
||||
// Just grab the newest frame and stash it; the render thread does the copy.
|
||||
auto frame = pool.TryGetNextFrame();
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (pending_ != nullptr)
|
||||
{
|
||||
pending_.Close(); // drop the un-consumed previous frame back to the pool
|
||||
}
|
||||
pending_ = frame;
|
||||
}
|
||||
|
||||
void WindowCapture::draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ctx, std::uint32_t dst_w,
|
||||
std::uint32_t dst_h)
|
||||
{
|
||||
winrt::Direct3D11CaptureFrame frame{nullptr};
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
frame = pending_;
|
||||
pending_ = nullptr;
|
||||
}
|
||||
|
||||
if (frame != nullptr)
|
||||
{
|
||||
if (ComPtr<ID3D11Texture2D> src = texture_from_surface(frame.Surface()))
|
||||
{
|
||||
D3D11_TEXTURE2D_DESC desc = {};
|
||||
src->GetDesc(&desc);
|
||||
|
||||
if (latest_ == nullptr || desc.Width != width_ || desc.Height != height_)
|
||||
{
|
||||
latest_srv_.Reset();
|
||||
latest_.Reset();
|
||||
|
||||
D3D11_TEXTURE2D_DESC dst = desc;
|
||||
dst.Usage = D3D11_USAGE_DEFAULT;
|
||||
dst.BindFlags = D3D11_BIND_SHADER_RESOURCE;
|
||||
dst.CPUAccessFlags = 0;
|
||||
dst.MiscFlags = 0;
|
||||
if (SUCCEEDED(device_->CreateTexture2D(&dst, nullptr, latest_.GetAddressOf())))
|
||||
{
|
||||
device_->CreateShaderResourceView(latest_.Get(), nullptr, latest_srv_.GetAddressOf());
|
||||
width_ = desc.Width;
|
||||
height_ = desc.Height;
|
||||
}
|
||||
}
|
||||
|
||||
if (latest_ != nullptr)
|
||||
{
|
||||
ctx->CopyResource(latest_.Get(), src.Get());
|
||||
}
|
||||
}
|
||||
frame.Close();
|
||||
|
||||
// If the window resized, the capture item changes size; re-fit the pool.
|
||||
const winrt::SizeInt32 size = item_.Size();
|
||||
if (size.Width != pool_size_.Width || size.Height != pool_size_.Height)
|
||||
{
|
||||
pool_size_ = size;
|
||||
frame_pool_.Recreate(winrt_device_, kPixelFormat, 2, size);
|
||||
}
|
||||
}
|
||||
|
||||
if (latest_srv_ != nullptr)
|
||||
{
|
||||
renderer.draw(ctx, latest_srv_.Get(), width_, height_, dst_w, dst_h);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
75
host/src/capture/window_capture.hpp
Normal file
75
host/src/capture/window_capture.hpp
Normal file
@@ -0,0 +1,75 @@
|
||||
// Mirrors a target window into a D3D11 texture using Windows.Graphics.Capture
|
||||
// (no injection). Frames arrive on a WGC thread; the newest is handed to the
|
||||
// render thread, which copies and draws it -- so all D3D11 context use stays on
|
||||
// one thread.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <windows.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <winrt/Windows.Graphics.Capture.h>
|
||||
#include <winrt/Windows.Graphics.DirectX.Direct3D11.h>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
class FrameRenderer;
|
||||
|
||||
class WindowCapture
|
||||
{
|
||||
public:
|
||||
~WindowCapture();
|
||||
|
||||
// Begins capturing `target`. Returns false if WGC is unavailable or the
|
||||
// window can't be captured.
|
||||
bool start(HWND target, ID3D11Device* device);
|
||||
void stop();
|
||||
|
||||
[[nodiscard]] bool running() const
|
||||
{
|
||||
return session_ != nullptr;
|
||||
}
|
||||
[[nodiscard]] HWND target() const
|
||||
{
|
||||
return target_;
|
||||
}
|
||||
[[nodiscard]] std::uint32_t frame_width() const
|
||||
{
|
||||
return width_;
|
||||
}
|
||||
[[nodiscard]] std::uint32_t frame_height() const
|
||||
{
|
||||
return height_;
|
||||
}
|
||||
|
||||
// Render thread: consume the newest frame (if any) and draw it letterboxed
|
||||
// into a dst_w x dst_h target via `renderer`.
|
||||
void draw_latest(FrameRenderer& renderer, ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h);
|
||||
|
||||
private:
|
||||
void on_frame_arrived(winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& pool,
|
||||
winrt::Windows::Foundation::IInspectable const&);
|
||||
|
||||
HWND target_ = nullptr;
|
||||
Microsoft::WRL::ComPtr<ID3D11Device> device_;
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> latest_;
|
||||
Microsoft::WRL::ComPtr<ID3D11ShaderResourceView> latest_srv_;
|
||||
std::uint32_t width_ = 0;
|
||||
std::uint32_t height_ = 0;
|
||||
|
||||
std::mutex mutex_; // guards pending_ handoff only (no context work under it)
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame pending_{nullptr};
|
||||
|
||||
winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice winrt_device_{nullptr};
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item_{nullptr};
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool frame_pool_{nullptr};
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureSession session_{nullptr};
|
||||
winrt::event_token frame_token_{};
|
||||
winrt::Windows::Graphics::SizeInt32 pool_size_{0, 0};
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
63
host/src/capture_panel.cpp
Normal file
63
host/src/capture_panel.cpp
Normal file
@@ -0,0 +1,63 @@
|
||||
#include "capture_panel.hpp"
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
bool CapturePanel::init(ID3D11Device* device)
|
||||
{
|
||||
device_ = device;
|
||||
return renderer_.init(device);
|
||||
}
|
||||
|
||||
void CapturePanel::draw_ui()
|
||||
{
|
||||
ImGui::SetNextWindowPos(ImVec2(460, 24), ImGuiCond_FirstUseEver);
|
||||
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_))
|
||||
{
|
||||
if (!enabled_)
|
||||
{
|
||||
capture_.stop();
|
||||
}
|
||||
}
|
||||
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_)
|
||||
{
|
||||
if (!capture_.start(target_, device_))
|
||||
{
|
||||
enabled_ = false;
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "Failed to start capture.");
|
||||
}
|
||||
}
|
||||
|
||||
if (capture_.running())
|
||||
{
|
||||
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "Capturing %ux%u", capture_.frame_width(),
|
||||
capture_.frame_height());
|
||||
ImGui::Text("Render: %.1f FPS", ImGui::GetIO().Framerate);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void CapturePanel::render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h)
|
||||
{
|
||||
if (capture_.running())
|
||||
{
|
||||
capture_.draw_latest(renderer_, ctx, dst_w, dst_h);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
40
host/src/capture_panel.hpp
Normal file
40
host/src/capture_panel.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
// Owns the video-mirror pipeline (WGC capture + frame renderer) and its UI.
|
||||
// The target window comes from the injected hook's reported game HWND.
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "capture/frame_renderer.hpp"
|
||||
#include "capture/window_capture.hpp"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
class CapturePanel
|
||||
{
|
||||
public:
|
||||
bool init(ID3D11Device* device);
|
||||
|
||||
// The window to mirror (0 if none yet); typically the injected game's HWND.
|
||||
void set_target(HWND target)
|
||||
{
|
||||
target_ = target;
|
||||
}
|
||||
|
||||
void draw_ui();
|
||||
|
||||
// Render thread: draw the mirrored frame as the window background.
|
||||
void render(ID3D11DeviceContext* ctx, std::uint32_t dst_w, std::uint32_t dst_h);
|
||||
|
||||
private:
|
||||
ID3D11Device* device_ = nullptr;
|
||||
FrameRenderer renderer_;
|
||||
WindowCapture capture_;
|
||||
HWND target_ = nullptr;
|
||||
bool enabled_ = false;
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "coop/protocol.hpp"
|
||||
#include "imgui.h"
|
||||
#include "inject/process_list.hpp"
|
||||
@@ -25,6 +27,12 @@ public:
|
||||
// test-input mode is on, a synthetic pattern is sent instead of `pads`.
|
||||
void publish(const std::array<PadInfo, kMaxPads>& pads);
|
||||
|
||||
// The injected game's main window, as reported by the hook (null if none).
|
||||
[[nodiscard]] HWND game_hwnd() const
|
||||
{
|
||||
return reinterpret_cast<HWND>(server_.hook_status().game_hwnd);
|
||||
}
|
||||
|
||||
private:
|
||||
void refresh_processes();
|
||||
void inject_selected();
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
// CoopAllTheThings host -- Phase 0 spike.
|
||||
// CoopAllTheThings host.
|
||||
//
|
||||
// Brings up the borderless window Steam Remote Play Together will capture and an
|
||||
// ImGui overlay that lists every controller it can see. The goal of this build
|
||||
// is to validate the riskiest assumption end-to-end: launch this exe under a
|
||||
// donor appid (steam -applaunch <donorAppId> <path-to-host.exe>), invite a
|
||||
// friend, and confirm (a) the window streams and (b) the guest's gamepad shows
|
||||
// up in the overlay. Capture/injection are built on top of this once it holds.
|
||||
// Borderless window that Steam Remote Play Together captures, plus the overlay
|
||||
// that drives the tool: it forwards controller input into an injected game
|
||||
// (Injection panel), spoofs the game's focus, and mirrors the game's window into
|
||||
// this window via Windows Graphics Capture (Video mirror panel).
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
|
||||
#include "capture_panel.hpp"
|
||||
#include "d3d11_window.hpp"
|
||||
#include "debug_overlay.hpp"
|
||||
#include "imgui_layer.hpp"
|
||||
@@ -38,17 +39,34 @@ int run()
|
||||
|
||||
auto input = std::make_unique<coop::XInputSource>();
|
||||
coop::InjectionPanel injection;
|
||||
coop::CapturePanel capture;
|
||||
if (!capture.init(window.device()))
|
||||
{
|
||||
MessageBoxW(nullptr, L"Failed to initialize the video mirror.", L"CoopAllTheThings", MB_ICONERROR);
|
||||
return 1;
|
||||
}
|
||||
|
||||
while (window.pump_messages())
|
||||
{
|
||||
input->poll();
|
||||
injection.publish(input->pads());
|
||||
capture.set_target(injection.game_hwnd());
|
||||
|
||||
imgui.begin_frame();
|
||||
coop::draw_debug_overlay(*input);
|
||||
injection.draw();
|
||||
capture.draw_ui();
|
||||
|
||||
window.render_frame([&imgui]() { imgui.end_frame(); });
|
||||
RECT client = {};
|
||||
GetClientRect(window.hwnd(), &client);
|
||||
const auto dst_w = static_cast<std::uint32_t>(client.right - client.left);
|
||||
const auto dst_h = static_cast<std::uint32_t>(client.bottom - client.top);
|
||||
|
||||
// Mirrored frame first (the window background), ImGui overlay on top.
|
||||
window.render_frame([&]() {
|
||||
capture.render(window.context(), dst_w, dst_h);
|
||||
imgui.end_frame();
|
||||
});
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -58,5 +76,10 @@ int run()
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
|
||||
{
|
||||
return run();
|
||||
// WGC requires an initialized apartment; multi-threaded suits the
|
||||
// free-threaded frame pool.
|
||||
winrt::init_apartment(winrt::apartment_type::multi_threaded);
|
||||
const int result = run();
|
||||
winrt::uninit_apartment();
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user