Phase 0: donor-launch spike foundation
Scaffold CoopAllTheThings: Remote Play Together for any XInput game via a mirror app under a donor appid (real game keeps its own appid, so DRM, achievements, and playtime stay intact). - Build: CMake skeleton, ImGui + SafetyHook submodules (no vcpkg) - common/: host<->hook IPC contract (seqlock pad state, shared-memory RAII) - host/: borderless D3D11 window + ImGui overlay listing visible XInput pads, behind an InputSource interface (Steam Input slots in later) - README documents the Phase 0 donor-launch validation procedure, anti-cheat limitation, and XInput/bitness constraints Phase 0 validates the riskiest assumption (Steam RPT streams an arbitrary window under a donor appid and routes guest input to it) before capture and injection are built. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
222
host/src/d3d11_window.cpp
Normal file
222
host/src/d3d11_window.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "d3d11_window.hpp"
|
||||
|
||||
#include <imgui_impl_win32.h>
|
||||
|
||||
// Forward declared in the ImGui Win32 backend; lets ImGui consume input first.
|
||||
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
|
||||
|
||||
using Microsoft::WRL::ComPtr;
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr wchar_t kWindowClass[] = L"CoopAllTheThingsWindow";
|
||||
}
|
||||
|
||||
D3D11Window::~D3D11Window()
|
||||
{
|
||||
release_render_target();
|
||||
if (hwnd_ != nullptr)
|
||||
{
|
||||
DestroyWindow(hwnd_);
|
||||
hwnd_ = nullptr;
|
||||
}
|
||||
UnregisterClassW(kWindowClass, GetModuleHandleW(nullptr));
|
||||
}
|
||||
|
||||
bool D3D11Window::create(const wchar_t* title)
|
||||
{
|
||||
const HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
|
||||
WNDCLASSEXW wc = {};
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||
wc.lpfnWndProc = &D3D11Window::wnd_proc;
|
||||
wc.hInstance = instance;
|
||||
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
wc.lpszClassName = kWindowClass;
|
||||
if (RegisterClassExW(&wc) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Borderless popup covering the primary monitor. Steam RPT only streams the
|
||||
// focused window, and exclusive-fullscreen swap chains can't be captured, so
|
||||
// a plain WS_POPUP is exactly what we want.
|
||||
const int width = GetSystemMetrics(SM_CXSCREEN);
|
||||
const int height = GetSystemMetrics(SM_CYSCREEN);
|
||||
|
||||
hwnd_ = CreateWindowExW(0, kWindowClass, title, WS_POPUP, 0, 0, width, height, nullptr, nullptr, instance, this);
|
||||
if (hwnd_ == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!create_device())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ShowWindow(hwnd_, SW_SHOW);
|
||||
UpdateWindow(hwnd_);
|
||||
SetForegroundWindow(hwnd_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool D3D11Window::create_device()
|
||||
{
|
||||
DXGI_SWAP_CHAIN_DESC1 desc = {};
|
||||
desc.Width = 0; // derive from the window client area
|
||||
desc.Height = 0;
|
||||
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||
desc.SampleDesc.Count = 1;
|
||||
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
|
||||
desc.BufferCount = 2;
|
||||
desc.Scaling = DXGI_SCALING_STRETCH;
|
||||
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
|
||||
desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
|
||||
|
||||
UINT flags = 0;
|
||||
#ifdef _DEBUG
|
||||
flags |= D3D11_CREATE_DEVICE_DEBUG;
|
||||
#endif
|
||||
const D3D_FEATURE_LEVEL levels[] = {D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0};
|
||||
|
||||
if (FAILED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, levels, _countof(levels),
|
||||
D3D11_SDK_VERSION, device_.GetAddressOf(), nullptr, context_.GetAddressOf())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ComPtr<IDXGIDevice> dxgi_device;
|
||||
if (FAILED(device_.As(&dxgi_device)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ComPtr<IDXGIAdapter> adapter;
|
||||
if (FAILED(dxgi_device->GetAdapter(adapter.GetAddressOf())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ComPtr<IDXGIFactory2> factory;
|
||||
if (FAILED(adapter->GetParent(IID_PPV_ARGS(factory.GetAddressOf()))))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FAILED(factory->CreateSwapChainForHwnd(device_.Get(), hwnd_, &desc, nullptr, nullptr,
|
||||
swap_chain_.GetAddressOf())))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// Don't let DXGI swallow Alt+Enter into an exclusive-fullscreen transition.
|
||||
factory->MakeWindowAssociation(hwnd_, DXGI_MWA_NO_ALT_ENTER);
|
||||
|
||||
create_render_target();
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3D11Window::create_render_target()
|
||||
{
|
||||
ComPtr<ID3D11Texture2D> back_buffer;
|
||||
if (SUCCEEDED(swap_chain_->GetBuffer(0, IID_PPV_ARGS(back_buffer.GetAddressOf()))))
|
||||
{
|
||||
device_->CreateRenderTargetView(back_buffer.Get(), nullptr, rtv_.ReleaseAndGetAddressOf());
|
||||
}
|
||||
}
|
||||
|
||||
void D3D11Window::release_render_target()
|
||||
{
|
||||
rtv_.Reset();
|
||||
}
|
||||
|
||||
void D3D11Window::handle_resize(UINT width, UINT height)
|
||||
{
|
||||
if (swap_chain_ == nullptr || width == 0 || height == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
release_render_target();
|
||||
swap_chain_->ResizeBuffers(0, width, height, DXGI_FORMAT_UNKNOWN, 0);
|
||||
create_render_target();
|
||||
}
|
||||
|
||||
bool D3D11Window::pump_messages()
|
||||
{
|
||||
MSG msg;
|
||||
while (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
|
||||
{
|
||||
if (msg.message == WM_QUIT)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
if (resize_pending_)
|
||||
{
|
||||
handle_resize(resize_width_, resize_height_);
|
||||
resize_pending_ = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3D11Window::render_frame(const RenderCallback& render)
|
||||
{
|
||||
const float clear[4] = {0.06f, 0.06f, 0.08f, 1.0f};
|
||||
context_->OMSetRenderTargets(1, rtv_.GetAddressOf(), nullptr);
|
||||
context_->ClearRenderTargetView(rtv_.Get(), clear);
|
||||
|
||||
if (render)
|
||||
{
|
||||
render();
|
||||
}
|
||||
|
||||
// vsync on: matches the captured stream cadence and avoids a busy spin.
|
||||
swap_chain_->Present(1, 0);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK D3D11Window::wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
|
||||
{
|
||||
if (msg == WM_NCCREATE)
|
||||
{
|
||||
auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(create->lpCreateParams));
|
||||
}
|
||||
|
||||
if (ImGui_ImplWin32_WndProcHandler(hwnd, msg, wparam, lparam))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
auto* self = reinterpret_cast<D3D11Window*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case WM_SIZE:
|
||||
if (self != nullptr && wparam != SIZE_MINIMIZED)
|
||||
{
|
||||
self->resize_pending_ = true;
|
||||
self->resize_width_ = LOWORD(lparam);
|
||||
self->resize_height_ = HIWORD(lparam);
|
||||
}
|
||||
return 0;
|
||||
case WM_KEYDOWN:
|
||||
// Spike convenience: Esc quits so we aren't stuck in a borderless window.
|
||||
if (wparam == VK_ESCAPE)
|
||||
{
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wparam, lparam);
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
Reference in New Issue
Block a user