diff --git a/README.md b/README.md index 33ae6d3..0d678c3 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,9 @@ intercept the resolution chain (`vkCreateInstance` / `vkCreateDevice` / `vkCreateSwapchainKHR` / `vkQueuePresentKHR`) and reads the presented image back with `vkCmdCopyImageToBuffer` — but only when the hook is present **before** the game initializes Vulkan (it caches its present pointer at init), so the Vulkan path needs early presence via -Auto-attach (and an opt-in implicit layer — see Roadmap); a too-late attach shows a red -relaunch banner and falls back to WGC. The host samples the copy as plain UNORM +Auto-attach or the opt-in implicit **capture layer** (`coop_vk_layer`, registered per-user by the +Injection panel's "Set up Vulkan layer" checkbox and scoped to the target game); a too-late attach +shows a red relaunch banner and falls back to WGC. The host samples the copy as plain UNORM (`srgb_to_unorm`) so `*_SRGB`-backbuffer games mirror at correct brightness. **WGC remains the default** and covers anything the hooked path doesn't. @@ -107,77 +108,6 @@ default** and covers anything the hooked path doesn't. ## Roadmap -### Current work — per-API capture, end to end - -Each rendering API is built as **one milestone**: first its `coop_mock_game` backend (an -animated, frame-numbered A/V source), then the injected **capture** for that same API -directly after — so every API reaches *verified end-to-end* (the mirror decodes the frame -counter and asserts a *monotonic, advancing* sequence) before the next one starts, rather -than building all the mock backends first and all the capture later. Order is -easiest-to-hardest, dependencies last. - -Conventions for every milestone below: - -- Each mock backend implements the existing - [`RenderBackend`](tools/mock_game/render_backend.hpp) interface (animated background + - moving bar + frame-counter block), is selectable on the command line, and renders - **clear/fill-only** — no geometry, no shaders, so **no shader-compiler dependency**. -- Sub-steps are **separate commits**; the README, CMake, and (where needed) `.gitmodules` - move with each. A mock-backend commit lands with a *liveness* smoke check (launches, - presents N frames, exits clean); the frame-accurate decode-through-the-hook assertion - lands with that API's capture commit right after. -- The host samples the **standard shared keyed-mutex texture** unchanged regardless of - source API: every backend publishes into it — native D3D11 on the game's device, D3D12 via - D3D11On12, and D3D9 / D3D10 / OpenGL / Vulkan via a **hook-owned D3D11 device**. -- New submodules are **not** auto-cloned — CMake checks each is populated and stops with a - `FATAL_ERROR` naming `git submodule update --init --recursive` (a reusable - `coop_require_submodule()` helper). - -- **M1 — End-user UI fit pass.** Verify the shipping ImGui overlay (the *end-user* view, not - just the dev layout) with the debug-driving harness + F10 screenshot, and guarantee **every - panel fits its assigned window size even with Debug details enabled** — the - maximum-information case. Drive each panel to its fullest state via the harness (inject, - enable audio + video, expand Debug details, show the override controls, multiple audio - streams, the longest status / fallback strings), screenshot, and check nothing is clipped - or scrolled out of view. Where the densest case overflows, **resize and/or rearrange** the - panel so it fits (content can be moved between columns / rows — the most detailed case - should still fit). Land an automated harness check (drive-to-max → screenshot → assert no - overflow) so later milestones that add UI keep it green. *Independent of the backend work; - every milestone below must preserve this test* (M2's red banner and Vulkan-layer checkbox - in particular). - - **Mandatory final visual inspection.** The headless `ui_fit_test` proves content *fits* - its window, but not that it *looks* right. Before this milestone is considered done, run - the **actual `coop_host.exe`** (build with `-DCOOP_TEST_HARNESS=ON`), drive it to its - maximum-information state (inject a target, audio on, Debug details on), take an **F10 - screenshot of the running tool**, and **eyeball the real overlay** — every panel readable, - nothing clipped/overlapping, colours/labels correct. A green unit test is not a substitute - for looking at the product; this real-screenshot check is required, not optional. - -- **M2 — Vulkan: opt-in reliability layer (the one piece left).** The Vulkan **mock backend and - hooked capture are done and tested**: the hook inline-hooks the `vulkan-1.dll` - `vkGetInstanceProcAddr` export and intercepts the resolution chain (`vkCreateInstance` / - `vkCreateDevice` / `vkCreateSwapchainKHR` / `vkQueuePresentKHR`), reads the presented image back - with `vkCmdCopyImageToBuffer`, and re-chains the present's wait semaphores so the copy orders - after rendering. `mock_game_test` decodes Vulkan frames through it (via a suspended-launch + - early-load path) and verifies the **too-late detection**; the host shows the **red relaunch - banner** (verified live). **Best-effort auto-attach** already works (the existing poll-and-inject - re-injects on relaunch, the vk hook retries until `vulkan-1.dll` loads), enough for games that - don't initialize Vulkan instantly. Remaining: - - **Opt-in implicit Vulkan layer** for games that init Vulkan *immediately* (where even a - relaunch+auto-attach injects too late). A real chain-aware Vulkan layer (`coop_vk_layer.dll` - + JSON manifest) the loader loads at `vkCreateInstance` — guaranteed before init — registered - per-user (HKCU, no admin) and scoped to the target image, with an **opt-in** "Set up Vulkan - layer" Injection-panel checkbox that registers/unregisters it (self-deactivating for non-target - apps so a stale entry after a host crash is harmless). This is a separate component from the - inline-hook capture (layer-chain dispatch + manifest/registry + the checkbox + IPC handshake), - and it needs the loader/layer interface header `vk_layer.h` — which is **not** in - `Vulkan-Headers` (it lives in `Vulkan-Loader` / the SDK), so it requires either a new submodule - or hand-declared `VkLayer*CreateInfo` link structs. Until it lands, the banner directs - immediate-init Vulkan games to WGC (which mirrors them fine, just at higher latency). - - Each of 1–3 is its own commit; test the early/layer path against the mock (decode frames) and - the too-late path (assert the status + banner fire). - ### Future work - **Mouse + keyboard forwarding for Raw Input / DirectInput games.** The MKB @@ -199,7 +129,7 @@ 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 (+ coop_hook.dll, test exes) +# output: bin/Debug/coop_host.exe (+ coop_hook.dll, coop_vk_layer.dll + manifest, test exes) ``` The x64 build also drives a nested Win32 sub-build (CMake `ExternalProject`, @@ -207,8 +137,10 @@ configured into `build/x86/`) that produces `coop_hook_x86.dll` and `coop_inject_x86.exe` for 32-bit games, staged next to the x64 binaries. Disable it with `-DCOOP_BUILD_X86_HELPER=OFF` if you don't need 32-bit support. -Third-party dependencies (Dear ImGui, SafetyHook) are git submodules under -`third_party/`. No vcpkg / package manager is used. +Third-party dependencies (Dear ImGui, SafetyHook, the official Khronos Vulkan-Headers, and the +`zeux/volk` Vulkan meta-loader) are git submodules under `third_party/`. No vcpkg / package +manager is used; CMake fails with a clear `git submodule update --init --recursive` hint if one +is missing (`coop_require_submodule`). **Steam Input is optional.** It's enabled automatically when the Steamworks SDK is vendored at `third_party/steamworks_sdk/` (extract the `steamworks_sdk_*.zip` @@ -294,8 +226,10 @@ ctest --test-dir build -C Debug --output-on-failure backend (the bar for no dropped / stale / out-of-order frames — what the DX12 rotating-backbuffer bug broke). **Vulkan** is special: since its present pointer is cached at init, the test launches the mock **suspended**, injects, then resumes (the mock loads Vulkan and waits so the hook arms - first) to decode frames; it also late-injects a Vulkan game and asserts the **too-late** flag - trips (which drives the host's relaunch banner). It launches the game at several **audio formats** + first) to decode frames; it also exercises the **implicit capture layer** (registered via + `VK_LAYER_PATH` / `VK_INSTANCE_LAYERS`, decoding frames through the loader-inserted layer) and + late-injects a Vulkan game to assert the **too-late** flag trips (which drives the host's + relaunch banner). It launches the game at several **audio formats** (44100/48000/96000, PCM + float) and asserts the hook measures each one's rate through the full inject path, then injects with audio + video, checks both stream, cycles the audio subsystem off/on (hook/unhook stress), and confirms the game never crashes and capture resumes. This suite @@ -472,6 +406,19 @@ Non-obvious things that cost time and constrain the design: everything up before us) → red relaunch banner, WGC meanwhile. Testing needs an **early-load** path (suspended launch + inject + resume; the mock loads Vulkan and waits), since the normal late-inject flow can't catch a Vulkan present. +- **The reliable Vulkan path is a real implicit layer — and the loader tags its link structs with + small `sType`s.** Games that init Vulkan instantly resolve their present pointer before any + inject can land, so the only robust early presence is an implicit layer (`coop_vk_layer`) the + loader inserts at `vkCreateInstance`. It must do **chain dispatch** (pull + `pfnNextGetInstanceProcAddr` / `pfnNextGetDeviceProcAddr` out of the `VkLayer*CreateInfo` link in + `pNext`, advance the link, call down) rather than inline-hook. The trap that cost time: those + link structs use **loader-internal `sType` values 47 (instance) / 48 (device)** — *not* the + `1000000000`-range I assumed (and `vk_layer.h` isn't in Vulkan-Headers, so the structs are + hand-declared) — matching the wrong value silently failed the device-chain walk + (`vkCreateDevice: Failed to create device chain`). An implicit layer loads into *every* Vulkan + app, so it self-scopes: capture only when the process image matches the host-written target file, + else pure pass-through. Register it per-user (HKCU `…\Vulkan\ImplicitLayers`, no admin) and + unregister on untick / host exit. - **A render client that predates our injection has no knowable format — measure it.** We inject into already-running games, so we usually never see the game's `IAudioClient::Initialize`; the render-hook then assumes the device mix format for that