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:
19
.clang-format
Normal file
19
.clang-format
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
BasedOnStyle: LLVM
|
||||
ColumnLimit: 120
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
UseTab: ForIndentation
|
||||
|
||||
BreakBeforeBraces: Custom
|
||||
BraceWrapping:
|
||||
AfterFunction: true
|
||||
SplitEmptyRecord: false
|
||||
|
||||
AlignEscapedNewlines: DontAlign
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
BreakBeforeBinaryOperators: NonAssignment
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
PointerAlignment: Left
|
||||
...
|
||||
19
.editorconfig
Normal file
19
.editorconfig
Normal file
@@ -0,0 +1,19 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{c,cpp,h,hpp,inc}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[{CMakeLists.txt,*.cmake}]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[*.{sln,vcxproj,props,filters}]
|
||||
end_of_line = crlf
|
||||
insert_final_newline = false
|
||||
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
.vs
|
||||
*.vcxproj.user
|
||||
*.args.json
|
||||
|
||||
/bin
|
||||
/build
|
||||
CMakeUserPresets.json
|
||||
|
||||
x64
|
||||
Debug
|
||||
Release
|
||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
[submodule "third_party/imgui"]
|
||||
path = third_party/imgui
|
||||
url = https://github.com/ocornut/imgui.git
|
||||
[submodule "third_party/safetyhook"]
|
||||
path = third_party/safetyhook
|
||||
url = https://github.com/cursey/safetyhook.git
|
||||
36
CMakeLists.txt
Normal file
36
CMakeLists.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(CoopAllTheThings
|
||||
VERSION 0.0.1
|
||||
DESCRIPTION "Steam Remote Play Together for any XInput game"
|
||||
LANGUAGES CXX)
|
||||
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "CoopAllTheThings targets Windows only.")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Single output dir keeps the donor-launch / inject workflow simple: host exe and
|
||||
# hook dll land next to each other under /bin (already in .gitignore).
|
||||
set(COOP_OUTPUT_DIR "${CMAKE_SOURCE_DIR}/bin/$<CONFIG>")
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}")
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${COOP_OUTPUT_DIR}")
|
||||
|
||||
if(MSVC)
|
||||
add_compile_options(/W4 /permissive- /Zc:__cplusplus /utf-8 /MP)
|
||||
add_compile_definitions(UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||
endif()
|
||||
|
||||
add_subdirectory(third_party)
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(host)
|
||||
|
||||
# The injected hook DLL pulls in SafetyHook (+ Zydis). It is built in Phase 1;
|
||||
# enable once the dependency is wired so Phase 0 stays minimal.
|
||||
option(COOP_BUILD_HOOK "Build the injected game-side hook DLL" OFF)
|
||||
if(COOP_BUILD_HOOK)
|
||||
add_subdirectory(hook)
|
||||
endif()
|
||||
109
README.md
Normal file
109
README.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# CoopAllTheThings
|
||||
|
||||
Steam **Remote Play Together (RPT)** for any XInput game — without breaking DRM,
|
||||
achievements, or playtime.
|
||||
|
||||
Existing "donor game" tools (e.g. RemotePlayWhatever) copy a target game's files
|
||||
into a donor game's folder and rename the executable so Steam streams the target
|
||||
under the donor's appid. That breaks DRM-protected games, breaks achievements,
|
||||
and credits playtime to the donor.
|
||||
|
||||
CoopAllTheThings takes a different approach: the **real game runs normally under
|
||||
its own appid** (so DRM, achievements, and playtime all work), while a lightweight
|
||||
**mirror app runs under the donor appid**. The mirror presents a borderless
|
||||
window that is a live copy of the game's video + audio, and forwards the guests'
|
||||
input back into the real game. Steam's RPT captures the mirror window — so any
|
||||
XInput game becomes Remote-Play-Together-able.
|
||||
|
||||
See [`docs`](docs) and the in-repo plan for the full design.
|
||||
|
||||
## Architecture (target)
|
||||
|
||||
| Concern | Mechanism | Where |
|
||||
| --- | --- | --- |
|
||||
| Receive guest input | Steam Input / XInput (RPT delivers guests to the focused window) | `coop_host.exe` |
|
||||
| Forward input to game | DLL injection + XInput hook (SafetyHook) — game sees *only* our pad | `coop_hook.dll` |
|
||||
| Mirror video | Windows Graphics Capture first; `IDXGISwapChain::Present` hook as the low-latency upgrade | host (+ hook) |
|
||||
| Mirror audio | WASAPI process-loopback capture of the game, re-rendered | `coop_host.exe` |
|
||||
| Host ↔ hook IPC | Named shared memory (seqlock for input, shared D3D11 texture for video) | `common/` |
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Anti-cheat:** the input path injects `coop_hook.dll` into the target game.
|
||||
Games protected by kernel-level anti-cheat (Easy Anti-Cheat, BattlEye,
|
||||
Vanguard, etc.) will detect the injected module and may **kick the player or
|
||||
issue a ban**. Such games are explicitly **out of scope and unsupported** — do
|
||||
not use CoopAllTheThings with them. The tool targets single-player and
|
||||
co-op/local-multiplayer titles without active anti-cheat.
|
||||
- **XInput only:** the game must read controllers via XInput (the common case).
|
||||
DirectInput-only / RawInput-only games are not handled.
|
||||
- **Architecture match:** the host and hook DLL must match the game's bitness.
|
||||
x64 is supported first; x86 support is a later phase (see the plan).
|
||||
|
||||
## Status
|
||||
|
||||
**Phase 0 — donor spike (current).** `coop_host.exe` is a borderless D3D11 window
|
||||
with an ImGui overlay that lists every controller it can see. It exists to
|
||||
validate the riskiest assumption before anything else is built: *can Steam RPT
|
||||
stream an arbitrary window launched under a donor appid, and route a guest's
|
||||
gamepad into it?*
|
||||
|
||||
Later phases (capture, injection, audio) are scoped in the plan and gated on
|
||||
Phase 0 passing.
|
||||
|
||||
## Building
|
||||
|
||||
Requirements: Windows 10/11, Visual Studio 2022 (MSVC + C++ workload), CMake ≥ 3.21.
|
||||
|
||||
```sh
|
||||
git clone --recurse-submodules <repo-url>
|
||||
# or, if already cloned:
|
||||
git submodule update --init --recursive
|
||||
|
||||
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
|
||||
cmake --build build --config Debug
|
||||
# output: bin/Debug/coop_host.exe
|
||||
```
|
||||
|
||||
Third-party dependencies (Dear ImGui, SafetyHook) are git submodules under
|
||||
`third_party/`; the Steamworks SDK is vendored manually there when wired. No
|
||||
vcpkg / package manager is used.
|
||||
|
||||
## Phase 0: validating the donor-launch assumption
|
||||
|
||||
This is a manual test — it needs Steam, a second person (or second account), and
|
||||
a donor game that supports Remote Play Together.
|
||||
|
||||
1. **Pick a donor game** you own that supports Remote Play Together (check the
|
||||
"Remote Play Together" tag on its store page). The donor only needs RPT
|
||||
support; it is never actually played.
|
||||
|
||||
2. **Launch the host under the donor's appid.** Find the donor's appid (the
|
||||
number in its store URL), then run:
|
||||
|
||||
```text
|
||||
"C:\Program Files (x86)\Steam\steam.exe" -applaunch <donorAppId> "D:\dev\CoopAllTheThings\bin\Debug\coop_host.exe"
|
||||
```
|
||||
|
||||
The borderless overlay window should appear and Steam should consider the
|
||||
donor "running" (green status / "Stop" button in the library).
|
||||
|
||||
> If the donor ignores the trailing path, set the host as the donor's
|
||||
> **Launch Options** instead (`"D:\...\coop_host.exe" %command%` variants),
|
||||
> or use a launcher such as RemotePlayDetached. Recording which method makes
|
||||
> Steam attribute our window to the donor is the main deliverable of Phase 0.
|
||||
|
||||
3. **Start Remote Play Together** from the Steam friends list / overlay and invite
|
||||
a friend (or a second machine/account).
|
||||
|
||||
4. **Verify on the guest side:**
|
||||
- The guest sees the borderless overlay window streamed (not a black screen).
|
||||
- The guest presses buttons on their controller and the corresponding slot in
|
||||
the overlay lights up. This proves RPT routes guest input into our window
|
||||
as XInput — the foundation the whole tool relies on.
|
||||
|
||||
5. Press **Esc** in the host window to quit.
|
||||
|
||||
**If steps 2 and 4 both work, the core premise holds** and we proceed to Phase 1
|
||||
(window capture + input injection). If not, we revisit the donor-attribution
|
||||
approach before building further.
|
||||
6
common/CMakeLists.txt
Normal file
6
common/CMakeLists.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# Header-only IPC contract shared by the host and the injected hook DLL.
|
||||
add_library(coop_common INTERFACE)
|
||||
|
||||
target_include_directories(coop_common INTERFACE include)
|
||||
|
||||
target_compile_features(coop_common INTERFACE cxx_std_20)
|
||||
118
common/include/coop/protocol.hpp
Normal file
118
common/include/coop/protocol.hpp
Normal file
@@ -0,0 +1,118 @@
|
||||
// IPC contract shared between the host (coop_host.exe) and the injected hook
|
||||
// DLL (coop_hook.dll). Both sides compile this identical header, so the memory
|
||||
// layout must stay POD and version-locked.
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
// Bump whenever the layout of SharedBlock or CoopPadState changes. The hook
|
||||
// refuses to attach to a host with a mismatched version.
|
||||
inline constexpr std::uint32_t kProtocolVersion = 1;
|
||||
|
||||
// 'COOP' little-endian, used to sanity-check the mapping before trusting it.
|
||||
inline constexpr std::uint32_t kProtocolMagic = 0x504F4F43u;
|
||||
|
||||
// XInput exposes four controller slots; we mirror that fixed count.
|
||||
inline constexpr std::uint32_t kMaxPads = 4;
|
||||
|
||||
// The shared-memory section is named per host process id so multiple sessions
|
||||
// can coexist. Format with the target game's pid: coop_ipc_<pid>.
|
||||
inline constexpr wchar_t kSharedMemoryPrefix[] = L"Local\\coop_ipc_";
|
||||
|
||||
// One controller's state, laid out to map 1:1 onto XINPUT_GAMEPAD plus the
|
||||
// metadata the hook needs. Field names/types match XINPUT_GAMEPAD so the hook
|
||||
// can memcpy the trailing region straight into an XINPUT_STATE.
|
||||
struct CoopPadState
|
||||
{
|
||||
std::uint8_t connected; // 1 if a guest/host pad is mapped to this slot
|
||||
std::uint8_t reserved[3];
|
||||
std::uint32_t packet; // bumps on change -> XINPUT_STATE::dwPacketNumber
|
||||
std::uint16_t buttons; // XINPUT_GAMEPAD_* bitmask
|
||||
std::uint8_t left_trigger;
|
||||
std::uint8_t right_trigger;
|
||||
std::int16_t thumb_lx;
|
||||
std::int16_t thumb_ly;
|
||||
std::int16_t thumb_rx;
|
||||
std::int16_t thumb_ry;
|
||||
};
|
||||
|
||||
static_assert(sizeof(CoopPadState) == 20, "CoopPadState layout must stay stable across both modules");
|
||||
|
||||
// Top-level shared block. The host is the sole writer of pad state; the hook is
|
||||
// the sole reader. A seqlock (even = stable, odd = write in progress) lets the
|
||||
// reader grab a torn-free snapshot without a kernel lock on the hot path.
|
||||
struct SharedBlock
|
||||
{
|
||||
std::uint32_t magic;
|
||||
std::uint32_t version;
|
||||
std::uint32_t pad_count; // number of populated slots, <= kMaxPads
|
||||
std::atomic<std::uint32_t> sequence;
|
||||
CoopPadState pads[kMaxPads];
|
||||
|
||||
// Phase 2 appends the shared-texture handle/dimensions control fields here;
|
||||
// keep new members at the end so existing offsets never shift.
|
||||
};
|
||||
|
||||
static_assert(std::atomic<std::uint32_t>::is_always_lock_free,
|
||||
"seqlock requires a lock-free 32-bit atomic for cross-process use");
|
||||
|
||||
// --- Seqlock helpers -------------------------------------------------------
|
||||
|
||||
// Writer side: publish a fresh set of pad states. Called from the host.
|
||||
inline void publish_pads(SharedBlock& block, const CoopPadState* pads, std::uint32_t count)
|
||||
{
|
||||
if (count > kMaxPads)
|
||||
{
|
||||
count = kMaxPads;
|
||||
}
|
||||
const std::uint32_t seq = block.sequence.load(std::memory_order_relaxed);
|
||||
block.sequence.store(seq + 1, std::memory_order_release); // -> odd: write begins
|
||||
std::atomic_thread_fence(std::memory_order_release);
|
||||
block.pad_count = count;
|
||||
for (std::uint32_t i = 0; i < count; ++i)
|
||||
{
|
||||
block.pads[i] = pads[i];
|
||||
}
|
||||
for (std::uint32_t i = count; i < kMaxPads; ++i)
|
||||
{
|
||||
block.pads[i] = CoopPadState{};
|
||||
}
|
||||
block.sequence.store(seq + 2, std::memory_order_release); // -> even: write done
|
||||
}
|
||||
|
||||
// Reader side: copy a consistent snapshot. Called from the hook. Spins briefly
|
||||
// if a write is in flight; bounded so a crashed writer can't hang the game.
|
||||
inline bool read_pads(const SharedBlock& block, CoopPadState (&out)[kMaxPads], std::uint32_t& out_count)
|
||||
{
|
||||
for (int attempt = 0; attempt < 64; ++attempt)
|
||||
{
|
||||
const std::uint32_t before = block.sequence.load(std::memory_order_acquire);
|
||||
if (before & 1u)
|
||||
{
|
||||
continue; // writer mid-update, retry
|
||||
}
|
||||
std::uint32_t count = block.pad_count;
|
||||
if (count > kMaxPads)
|
||||
{
|
||||
count = kMaxPads;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < kMaxPads; ++i)
|
||||
{
|
||||
out[i] = block.pads[i];
|
||||
}
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
const std::uint32_t after = block.sequence.load(std::memory_order_acquire);
|
||||
if (before == after)
|
||||
{
|
||||
out_count = count;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
131
common/include/coop/shared_memory.hpp
Normal file
131
common/include/coop/shared_memory.hpp
Normal file
@@ -0,0 +1,131 @@
|
||||
// Thin RAII wrapper over a Win32 file-mapping section used as the host<->hook
|
||||
// IPC transport. Header-only so both modules share one implementation.
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "coop/protocol.hpp"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
class SharedMemory
|
||||
{
|
||||
public:
|
||||
SharedMemory() = default;
|
||||
|
||||
SharedMemory(const SharedMemory&) = delete;
|
||||
SharedMemory& operator=(const SharedMemory&) = delete;
|
||||
|
||||
SharedMemory(SharedMemory&& other) noexcept
|
||||
{
|
||||
*this = std::move(other);
|
||||
}
|
||||
|
||||
SharedMemory& operator=(SharedMemory&& other) noexcept
|
||||
{
|
||||
if (this != &other)
|
||||
{
|
||||
reset();
|
||||
mapping_ = std::exchange(other.mapping_, nullptr);
|
||||
view_ = std::exchange(other.view_, nullptr);
|
||||
size_ = std::exchange(other.size_, 0);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~SharedMemory()
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
// Host side: create (or open if it already exists) the named section.
|
||||
bool create(const std::wstring& name, std::size_t size)
|
||||
{
|
||||
reset();
|
||||
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE, 0,
|
||||
static_cast<DWORD>(size), name.c_str());
|
||||
if (mapping_ == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return map(size);
|
||||
}
|
||||
|
||||
// Hook side: open an existing section created by the host.
|
||||
bool open(const std::wstring& name, std::size_t size)
|
||||
{
|
||||
reset();
|
||||
mapping_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, name.c_str());
|
||||
if (mapping_ == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return map(size);
|
||||
}
|
||||
|
||||
void reset()
|
||||
{
|
||||
if (view_ != nullptr)
|
||||
{
|
||||
UnmapViewOfFile(view_);
|
||||
view_ = nullptr;
|
||||
}
|
||||
if (mapping_ != nullptr)
|
||||
{
|
||||
CloseHandle(mapping_);
|
||||
mapping_ = nullptr;
|
||||
}
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool valid() const
|
||||
{
|
||||
return view_ != nullptr;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
[[nodiscard]] T* as() const
|
||||
{
|
||||
return static_cast<T*>(view_);
|
||||
}
|
||||
|
||||
[[nodiscard]] void* data() const
|
||||
{
|
||||
return view_;
|
||||
}
|
||||
|
||||
[[nodiscard]] std::size_t size() const
|
||||
{
|
||||
return size_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool map(std::size_t size)
|
||||
{
|
||||
view_ = MapViewOfFile(mapping_, FILE_MAP_ALL_ACCESS, 0, 0, size);
|
||||
if (view_ == nullptr)
|
||||
{
|
||||
CloseHandle(mapping_);
|
||||
mapping_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
size_ = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
HANDLE mapping_ = nullptr;
|
||||
void* view_ = nullptr;
|
||||
std::size_t size_ = 0;
|
||||
};
|
||||
|
||||
// Build the per-pid section name both sides agree on.
|
||||
inline std::wstring shared_memory_name(unsigned long target_pid)
|
||||
{
|
||||
return std::wstring(kSharedMemoryPrefix) + std::to_wstring(target_pid);
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
18
host/CMakeLists.txt
Normal file
18
host/CMakeLists.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
add_executable(coop_host WIN32
|
||||
src/main.cpp
|
||||
src/d3d11_window.cpp
|
||||
src/imgui_layer.cpp
|
||||
src/debug_overlay.cpp
|
||||
src/input/xinput_source.cpp)
|
||||
|
||||
target_include_directories(coop_host PRIVATE src)
|
||||
|
||||
target_link_libraries(coop_host PRIVATE
|
||||
coop_common
|
||||
imgui
|
||||
d3d11
|
||||
dxgi
|
||||
dwmapi
|
||||
xinput)
|
||||
|
||||
set_target_properties(coop_host PROPERTIES OUTPUT_NAME "coop_host")
|
||||
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
|
||||
68
host/src/d3d11_window.hpp
Normal file
68
host/src/d3d11_window.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// Borderless full-screen D3D11 window. This is the surface Steam Remote Play
|
||||
// Together captures, so it must be a normal (non-exclusive) top-level window
|
||||
// that owns a flip-model swap chain.
|
||||
#pragma once
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <dxgi1_2.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
class D3D11Window
|
||||
{
|
||||
public:
|
||||
using RenderCallback = std::function<void()>;
|
||||
|
||||
D3D11Window() = default;
|
||||
~D3D11Window();
|
||||
|
||||
D3D11Window(const D3D11Window&) = delete;
|
||||
D3D11Window& operator=(const D3D11Window&) = delete;
|
||||
|
||||
// Creates the borderless window sized to the primary monitor and brings up
|
||||
// the D3D11 device + swap chain. Returns false on any failure.
|
||||
bool create(const wchar_t* title);
|
||||
|
||||
// Pumps the message queue once. Returns false when the window is closing.
|
||||
bool pump_messages();
|
||||
|
||||
// Clears the back buffer, invokes render (where ImGui draws), and presents.
|
||||
void render_frame(const RenderCallback& render);
|
||||
|
||||
[[nodiscard]] HWND hwnd() const
|
||||
{
|
||||
return hwnd_;
|
||||
}
|
||||
[[nodiscard]] ID3D11Device* device() const
|
||||
{
|
||||
return device_.Get();
|
||||
}
|
||||
[[nodiscard]] ID3D11DeviceContext* context() const
|
||||
{
|
||||
return context_.Get();
|
||||
}
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
|
||||
|
||||
bool create_device();
|
||||
void create_render_target();
|
||||
void release_render_target();
|
||||
void handle_resize(UINT width, UINT height);
|
||||
|
||||
HWND hwnd_ = nullptr;
|
||||
bool resize_pending_ = false;
|
||||
UINT resize_width_ = 0;
|
||||
UINT resize_height_ = 0;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D11Device> device_;
|
||||
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context_;
|
||||
Microsoft::WRL::ComPtr<IDXGISwapChain1> swap_chain_;
|
||||
Microsoft::WRL::ComPtr<ID3D11RenderTargetView> rtv_;
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
82
host/src/debug_overlay.cpp
Normal file
82
host/src/debug_overlay.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
#include "debug_overlay.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
struct ButtonBit
|
||||
{
|
||||
std::uint16_t mask;
|
||||
const char* label;
|
||||
};
|
||||
|
||||
// XINPUT_GAMEPAD_* bit values (kept local so this file needn't include Xinput.h).
|
||||
constexpr ButtonBit kButtons[] = {
|
||||
{0x0001, "Up"}, {0x0002, "Down"}, {0x0004, "Left"}, {0x0008, "Right"}, {0x0010, "Start"},
|
||||
{0x0020, "Back"}, {0x0040, "LS"}, {0x0080, "RS"}, {0x0100, "LB"}, {0x0200, "RB"},
|
||||
{0x1000, "A"}, {0x2000, "B"}, {0x4000, "X"}, {0x8000, "Y"},
|
||||
};
|
||||
|
||||
void draw_pad(int index, const PadInfo& pad)
|
||||
{
|
||||
ImGui::PushID(index);
|
||||
if (!pad.connected)
|
||||
{
|
||||
ImGui::TextDisabled("Slot %d: disconnected", index);
|
||||
ImGui::PopID();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Slot %d [%s]", index, pad.source.c_str());
|
||||
ImGui::Text("LT %3u RT %3u", pad.state.left_trigger, pad.state.right_trigger);
|
||||
ImGui::Text("L (%6d, %6d) R (%6d, %6d)", pad.state.thumb_lx, pad.state.thumb_ly, pad.state.thumb_rx,
|
||||
pad.state.thumb_ry);
|
||||
|
||||
bool first = true;
|
||||
ImGui::TextUnformatted("Buttons: ");
|
||||
for (const ButtonBit& b : kButtons)
|
||||
{
|
||||
if ((pad.state.buttons & b.mask) != 0)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, 1.0f), "%s%s", first ? "" : ", ", b.label);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
if (first)
|
||||
{
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(none)");
|
||||
}
|
||||
ImGui::Separator();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void draw_debug_overlay(const InputSource& input)
|
||||
{
|
||||
ImGui::SetNextWindowPos(ImVec2(24, 24), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(420, 0), ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin("CoopAllTheThings - Phase 0 spike");
|
||||
|
||||
ImGui::Text("Input backend: %s", input.name());
|
||||
ImGui::Text("%.1f FPS (%.2f ms)", ImGui::GetIO().Framerate, 1000.0f / ImGui::GetIO().Framerate);
|
||||
ImGui::TextDisabled("This window is what Remote Play Together captures.");
|
||||
ImGui::TextDisabled("Press Esc to quit.");
|
||||
ImGui::Separator();
|
||||
|
||||
const auto& pads = input.pads();
|
||||
for (int i = 0; i < static_cast<int>(pads.size()); ++i)
|
||||
{
|
||||
draw_pad(i, pads[i]);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
13
host/src/debug_overlay.hpp
Normal file
13
host/src/debug_overlay.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "input/input_source.hpp"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
// Phase 0 overlay: confirms the host is alive and, crucially, shows which
|
||||
// controllers are visible -- this is how we verify Remote Play Together is
|
||||
// routing a guest's gamepad into our window.
|
||||
void draw_debug_overlay(const InputSource& input);
|
||||
|
||||
} // namespace coop
|
||||
54
host/src/imgui_layer.cpp
Normal file
54
host/src/imgui_layer.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "imgui_layer.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_impl_dx11.h>
|
||||
#include <imgui_impl_win32.h>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
ImGuiLayer::~ImGuiLayer()
|
||||
{
|
||||
if (initialized_)
|
||||
{
|
||||
ImGui_ImplDX11_Shutdown();
|
||||
ImGui_ImplWin32_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
initialized_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ImGuiLayer::init(HWND hwnd, ID3D11Device* device, ID3D11DeviceContext* context)
|
||||
{
|
||||
IMGUI_CHECKVERSION();
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.IniFilename = nullptr; // don't litter the cwd with imgui.ini during the spike
|
||||
ImGui::StyleColorsDark();
|
||||
|
||||
if (!ImGui_ImplWin32_Init(hwnd))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!ImGui_ImplDX11_Init(device, context))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGuiLayer::begin_frame()
|
||||
{
|
||||
ImGui_ImplDX11_NewFrame();
|
||||
ImGui_ImplWin32_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
}
|
||||
|
||||
void ImGuiLayer::end_frame()
|
||||
{
|
||||
ImGui::Render();
|
||||
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
27
host/src/imgui_layer.hpp
Normal file
27
host/src/imgui_layer.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// Owns ImGui setup/teardown for the Win32 + DX11 backends.
|
||||
#pragma once
|
||||
|
||||
#include <d3d11.h>
|
||||
#include <windows.h>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
class ImGuiLayer
|
||||
{
|
||||
public:
|
||||
ImGuiLayer() = default;
|
||||
~ImGuiLayer();
|
||||
|
||||
ImGuiLayer(const ImGuiLayer&) = delete;
|
||||
ImGuiLayer& operator=(const ImGuiLayer&) = delete;
|
||||
|
||||
bool init(HWND hwnd, ID3D11Device* device, ID3D11DeviceContext* context);
|
||||
void begin_frame();
|
||||
void end_frame();
|
||||
|
||||
private:
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
39
host/src/input/input_source.hpp
Normal file
39
host/src/input/input_source.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
// Abstraction over "where controller input comes from" on the host side.
|
||||
//
|
||||
// Phase 0/1 use XInputSource: Remote Play Together delivers each guest's gamepad
|
||||
// to the focused window (our host) as a virtual XInput controller, so reading
|
||||
// XInput is enough to see guests. A future SteamInputSource can implement this
|
||||
// same interface for cleaner per-guest handles once the Steamworks SDK is wired.
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#include "coop/protocol.hpp"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
struct PadInfo
|
||||
{
|
||||
bool connected = false;
|
||||
CoopPadState state = {};
|
||||
std::string source; // human-readable label for the debug overlay
|
||||
};
|
||||
|
||||
class InputSource
|
||||
{
|
||||
public:
|
||||
virtual ~InputSource() = default;
|
||||
|
||||
// Name of the backend, shown in the overlay.
|
||||
[[nodiscard]] virtual const char* name() const = 0;
|
||||
|
||||
// Refresh all pad slots; call once per frame.
|
||||
virtual void poll() = 0;
|
||||
|
||||
// Latest snapshot of every slot (indexed 0..kMaxPads-1).
|
||||
[[nodiscard]] virtual const std::array<PadInfo, kMaxPads>& pads() const = 0;
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
42
host/src/input/xinput_source.cpp
Normal file
42
host/src/input/xinput_source.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "input/xinput_source.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
#include <xinput.h>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
void XInputSource::poll()
|
||||
{
|
||||
for (DWORD i = 0; i < kMaxPads; ++i)
|
||||
{
|
||||
XINPUT_STATE state = {};
|
||||
const DWORD result = XInputGetState(i, &state);
|
||||
|
||||
PadInfo& info = pads_[i];
|
||||
if (result == ERROR_SUCCESS)
|
||||
{
|
||||
info.connected = true;
|
||||
info.source = "XInput #" + std::to_string(i);
|
||||
|
||||
// XINPUT_GAMEPAD is laid out identically to the tail of CoopPadState,
|
||||
// so copy field by field to keep the mapping explicit and safe.
|
||||
const XINPUT_GAMEPAD& g = state.Gamepad;
|
||||
info.state.connected = 1;
|
||||
info.state.packet = state.dwPacketNumber;
|
||||
info.state.buttons = g.wButtons;
|
||||
info.state.left_trigger = g.bLeftTrigger;
|
||||
info.state.right_trigger = g.bRightTrigger;
|
||||
info.state.thumb_lx = g.sThumbLX;
|
||||
info.state.thumb_ly = g.sThumbLY;
|
||||
info.state.thumb_rx = g.sThumbRX;
|
||||
info.state.thumb_ry = g.sThumbRY;
|
||||
}
|
||||
else
|
||||
{
|
||||
info = PadInfo{};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
29
host/src/input/xinput_source.hpp
Normal file
29
host/src/input/xinput_source.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include "input/input_source.hpp"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
// Reads the four XInput slots. Remote Play Together exposes guest controllers
|
||||
// here, alongside any controllers physically attached to the host.
|
||||
class XInputSource final : public InputSource
|
||||
{
|
||||
public:
|
||||
[[nodiscard]] const char* name() const override
|
||||
{
|
||||
return "XInput";
|
||||
}
|
||||
|
||||
void poll() override;
|
||||
|
||||
[[nodiscard]] const std::array<PadInfo, kMaxPads>& pads() const override
|
||||
{
|
||||
return pads_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<PadInfo, kMaxPads> pads_;
|
||||
};
|
||||
|
||||
} // namespace coop
|
||||
58
host/src/main.cpp
Normal file
58
host/src/main.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
// CoopAllTheThings host -- Phase 0 spike.
|
||||
//
|
||||
// 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.
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "d3d11_window.hpp"
|
||||
#include "debug_overlay.hpp"
|
||||
#include "imgui_layer.hpp"
|
||||
#include "input/xinput_source.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int run()
|
||||
{
|
||||
coop::D3D11Window window;
|
||||
if (!window.create(L"CoopAllTheThings"))
|
||||
{
|
||||
MessageBoxW(nullptr, L"Failed to create the D3D11 window.", L"CoopAllTheThings", MB_ICONERROR);
|
||||
return 1;
|
||||
}
|
||||
|
||||
coop::ImGuiLayer imgui;
|
||||
if (!imgui.init(window.hwnd(), window.device(), window.context()))
|
||||
{
|
||||
MessageBoxW(nullptr, L"Failed to initialize ImGui.", L"CoopAllTheThings", MB_ICONERROR);
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto input = std::make_unique<coop::XInputSource>();
|
||||
|
||||
while (window.pump_messages())
|
||||
{
|
||||
input->poll();
|
||||
|
||||
imgui.begin_frame();
|
||||
coop::draw_debug_overlay(*input);
|
||||
|
||||
window.render_frame([&imgui]() { imgui.end_frame(); });
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
|
||||
{
|
||||
return run();
|
||||
}
|
||||
20
third_party/CMakeLists.txt
vendored
Normal file
20
third_party/CMakeLists.txt
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Dear ImGui built as a static lib with the Win32 + DX11 backends the host uses.
|
||||
add_library(imgui STATIC
|
||||
imgui/imgui.cpp
|
||||
imgui/imgui_draw.cpp
|
||||
imgui/imgui_tables.cpp
|
||||
imgui/imgui_widgets.cpp
|
||||
imgui/imgui_demo.cpp
|
||||
imgui/backends/imgui_impl_win32.cpp
|
||||
imgui/backends/imgui_impl_dx11.cpp)
|
||||
|
||||
target_include_directories(imgui PUBLIC
|
||||
imgui
|
||||
imgui/backends)
|
||||
|
||||
# ImGui's own sources are third-party; don't subject them to /W4.
|
||||
if(MSVC)
|
||||
target_compile_options(imgui PRIVATE /W0)
|
||||
endif()
|
||||
|
||||
target_link_libraries(imgui PUBLIC d3d11 dxgi dwmapi)
|
||||
1
third_party/imgui
vendored
Submodule
1
third_party/imgui
vendored
Submodule
Submodule third_party/imgui added at d15966ff6c
1
third_party/safetyhook
vendored
Submodule
1
third_party/safetyhook
vendored
Submodule
Submodule third_party/safetyhook added at 2f28386618
Reference in New Issue
Block a user