Docs/memory: Vulkan opt-in layer done; remove the completed Current-work roadmap
All of M1 (UI-fit + live inspection) and M2 (every backend mock + capture, incl. the Vulkan implicit layer + checkbox) are done, so the "Current work" roadmap section is removed -- only Future work (Raw Input / DirectInput MKB) remains. Architecture lists the Vulkan layer as the implemented early-presence path; lessons-learned add the chain-dispatch + loader sType 47/48 gotcha; build/test docs note the layer artifact + coverage; submodule list updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
103
README.md
103
README.md
@@ -49,8 +49,9 @@ intercept the resolution chain (`vkCreateInstance` / `vkCreateDevice` /
|
|||||||
`vkCreateSwapchainKHR` / `vkQueuePresentKHR`) and reads the presented image back with
|
`vkCreateSwapchainKHR` / `vkQueuePresentKHR`) and reads the presented image back with
|
||||||
`vkCmdCopyImageToBuffer` — but only when the hook is present **before** the game initializes
|
`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
|
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
|
Auto-attach or the opt-in implicit **capture layer** (`coop_vk_layer`, registered per-user by the
|
||||||
relaunch banner and falls back to WGC. The host samples the copy as plain UNORM
|
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
|
(`srgb_to_unorm`) so `*_SRGB`-backbuffer games mirror at correct brightness. **WGC remains the
|
||||||
default** and covers anything the hooked path doesn't.
|
default** and covers anything the hooked path doesn't.
|
||||||
|
|
||||||
@@ -107,77 +108,6 @@ default** and covers anything the hooked path doesn't.
|
|||||||
|
|
||||||
## Roadmap
|
## 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
|
### Future work
|
||||||
|
|
||||||
- **Mouse + keyboard forwarding for Raw Input / DirectInput games.** The MKB
|
- **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 -S . -B build -G "Visual Studio 17 2022" -A x64
|
||||||
cmake --build build --config Debug
|
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`,
|
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
|
`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.
|
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 dependencies (Dear ImGui, SafetyHook, the official Khronos Vulkan-Headers, and the
|
||||||
`third_party/`. No vcpkg / package manager is used.
|
`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
|
**Steam Input is optional.** It's enabled automatically when the Steamworks SDK is
|
||||||
vendored at `third_party/steamworks_sdk/` (extract the `steamworks_sdk_*.zip`
|
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
|
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
|
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
|
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
|
first) to decode frames; it also exercises the **implicit capture layer** (registered via
|
||||||
trips (which drives the host's relaunch banner). It launches the game at several **audio formats**
|
`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
|
(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
|
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
|
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**
|
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
|
path (suspended launch + inject + resume; the mock loads Vulkan and waits), since the normal
|
||||||
late-inject flow can't catch a Vulkan present.
|
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.**
|
- **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
|
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
|
`IAudioClient::Initialize`; the render-hook then assumes the device mix format for that
|
||||||
|
|||||||
Reference in New Issue
Block a user