Panels used to Begin at cascade positions, so they overlapped and clipped. A new
apply_panel_layout() in app_chrome positions/sizes each panel from the main
viewport work area (ImGuiCond_FirstUseEver, still movable): Injection left column
full height (room for hook diagnostics), Controllers/Video/Audio stacked in the
center column, Log right edge full height (max room for the log stream). Added a
"View -> Reset layout" menu item (request_layout_reset / apply_layout_end_frame
re-apply the defaults once via ImGuiCond_Always). Each panel now calls
apply_panel_layout(Panel::X) instead of its own ad-hoc SetNextWindowPos/Size.
Verified live: captured the host overlay -- Injection (left, full height),
Controllers/Video/Audio (center stack), Log (right, full height), no overlap among
the panels. x64 build + ctest green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New host/src/inject/window_list.{hpp,cpp} enumerates visible, titled, non-tool
top-level (alt-tab-style) windows via EnumWindows -- root-owner only, our own
process excluded -- and maps each to its owning pid + image name. The Injection
panel now defaults to this window list (each row "title [process.exe pid]", with a
filter over title or process), since there are far fewer windows than processes and
a window maps straight to the HWND the capturer wants. The full process list stays
as the advanced picker under Debug details.
Verified live: launched the host and captured its window -- the picker lists real
windows (Discord/Firefox/Explorer/...) in "title [exe pid]" form, filter present,
and the host's own window correctly excluded. x64 build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the Terminated state the Injection panel now offers a Re-attach button: it
refreshes the process list and finds live processes whose image name matches the
original target's (case-insensitive). Exactly one match -> tear down the stale IPC
channel and inject into the new pid via the normal path; several matches -> don't
guess, filter the picker to the name and prompt the operator to pick one; none ->
report it. Saves hunting for a relaunched game's new pid in the list.
Verified: x64 build + ctest 7/7 green. Re-attach is a Terminated-state button flow,
so its end-to-end behavior is best confirmed live (ImGui clicks can't be scripted);
logic reviewed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Injection panel kept showing "Attached" after the game exited. Now it tracks
target liveness each frame (InjectionPanel::tick from the main loop, independent of
panel visibility):
- a SYNCHRONIZE|QUERY process handle taken at inject time -> WaitForSingleObject
detects the process exiting (Terminated);
- the hook heartbeat stalling for ~2 s while the process still exists flags a
distinct Hung state (games here can freeze without exiting).
The panel shows a clear colored banner per state and disables the subsystem
hook/unhook controls and the synthetic-input toggle when the target isn't alive.
game_hwnd() returns null once Terminated, so the Video and Audio panels drop to
idle instead of chasing a dead window.
Verified: x64 build + ctest 7/7 green; host launches and renders the panels without
regression (screenshot smoke test). The interactive terminated/hung visual against a
real game is best confirmed in a live session (ImGui injection can't be GUI-scripted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an injected IDXGISwapChain::Present / Present1 hook as a lower-latency,
border-free alternative to WGC. The hook copies the swapchain backbuffer into a
shared keyed-mutex texture (coop_video_<pid>); the host opens it by name and
samples it. New opt-in HookSubsys_Video (protocol v6 -> v7); the Video mirror
panel gains a WGC vs Hooked source toggle that installs/removes the subsystem.
Verified by present_hook_test (drives a real D3D11 swapchain end-to-end and reads
the rendered pixels back through the shared texture) and against Phantom Brave
(D3D9: hook installs cleanly and stays idle, WGC fallback). All 5 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a host->hook control channel (protocol v5 -> v6: HookControl in SharedBlock,
per-subsystem "disabled" flags, 0 = install so the zero-filled default is
unchanged). The worker now reconciles each subsystem every tick: install what's
requested-and-missing, remove what's no longer wanted -- so the audio hooks
re-attach the ring and republish format on a reinstall, and XInput/focus clear
their stale status flags on removal.
Injection panel: a checkbox per subsystem (input forwarding / focus spoof /
audio render-hook) toggles it at runtime, showing the requested vs actual
installed state from the registry, plus DLL heartbeat liveness. The hook-status
section now keys off whether a DLL was injected (host-side) rather than the
input-hook "attached" flag, so it stays visible with input unhooked.
Guards for dependent features: the synthetic-input control is disabled when
input forwarding is off, and the Audio panel explains that mirroring uses
loopback (echo) when the render-hook is off.
Verified against Phantom Brave via coop_audio_probe: starting with audio
requested off installs only input+focus (8 hooks, no capture); re-enabling at
runtime installs the audio hooks (13) and capture starts immediately. All four
tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a process-wide hook registry (hook/src/hook_registry) that every hook
module registers its hooks with and bumps a counter from each detour. The
XInput, focus-spoof, and audio render-hooks now register their individual
hooks (XInputGetState/Ex/Caps/SetState; GetForegroundWindow/GetActiveWindow/
GetFocus/WndProc guard; IMMDevice::Activate, IAudioClient::Initialize/
GetService, IAudioRenderClient::GetBuffer/ReleaseBuffer) and count calls.
The worker publishes the table to the host each tick over a new HookStatus
field (protocol v4 -> v5: HookEntry[] + count). The Injection panel shows it
as a collapsible table grouped by subsystem with an installed flag and call
count per hook; coop_audio_probe prints the same table headless.
Verified against Phantom Brave: 13 hooks listed with live counts (focus APIs
polled heavily, GetBuffer/ReleaseBuffer ticking with the audio render loop).
All four tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-slot XInput poll rates and the "game reading controller" summary
describe the controller, not the injection mechanism, so move them to the
Controllers panel (renamed from debug_overlay -> ControllersPanel, now a class
that owns the poll-rate sampling). The panel now shows both directions: the
guest pads the host receives from RPT, and what the injected game reads back.
The Injection panel keeps the hook attach state, focus spoof, focus-API
counts, and input-path diagnostics, and points to the Controllers panel for
poll rates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework the spike-era debug readouts into general-purpose status, with the
verbose diagnostics gated behind the menu bar's "Debug details" switch:
- Controllers (was "Phase 0 spike"): always shows slot/source + live
buttons; the raw stick/trigger numbers are debug-only.
- Injection hook status: general view is attached + focus spoof + a single
"Game reading controller: N polls/s" summary; the per-slot poll table,
focus-API counts, and input-path detection are debug-only.
- Audio: general view adds a "Buffered: N ms" health/latency proxy
(AudioMirror now tracks buffered audio in both render paths) and keeps the
render-stream count; the per-stream table is debug-only.
Default overlay is now clean general status; flip Debug details for the full
diagnostics. Completes the "generalize the UI" roadmap task. All tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaced by a game (Life is Strange: Before the Storm) that ignores
controller input when it lacks true OS focus even though it still polls
XInput. To find the focus-gated detection path, instrument the hook.
Protocol v3 status back-channel now reports:
- per-slot XInputGetState and XInputGetCapabilities counters (replacing the
single aggregate), so the overlay shows exactly which slots the game polls
and how fast;
- focus-API call counts (GetForegroundWindow/GetActiveWindow/GetFocus) to see
whether the game consults the APIs we spoof;
- input-path diagnostics: whether the process registered Raw Input for a
gamepad usage and whether it set RIDEV_INPUTSINK (background delivery), and
whether a DirectInput dll is loaded.
Host overlay gains a per-slot poll table and an "Input path" section. The DLL
refreshes input diagnostics each worker tick via GetRegisteredRawInputDevices.
hook_selftest updated for per-slot counters; passes.
This is diagnostic-only: once a real run shows which path LiS uses, the
targeted focus fix (e.g. forcing RIDEV_INPUTSINK or DI background coop) follows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two problems surfaced in testing: (1) no way to tell whether the injected
hook was actually the input source, and (2) the final design needs the tool
window focused for Steam RPT capture, which would pause/silence games that
react to focus loss. Both are addressed here.
- Focus spoofing (hook/focus_spoof): find the game's main window, subclass it
to rewrite/swallow WM_ACTIVATE/ACTIVATEAPP/NCACTIVATE/KILLFOCUS, and inline-
hook GetForegroundWindow/GetActiveWindow/GetFocus to always report the game
as active. The game keeps running and polling while unfocused.
- Status back-channel (protocol v2): the DLL reports attached/focus-spoof
flags, game pid/hwnd, a heartbeat, and a cumulative XInputGetState counter.
The host overlay turns the counter into a live poll rate, so "is the hook
working" is directly observable.
- Synthetic test-input toggle in the host: forwards a known automated pattern
(stick circle + periodic A) to prove forwarding independent of the physical
pad.
- hook_selftest extended to assert the status channel; passes.
Documented the windowed/borderless requirement and the new observable test
flow in the README.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The host can now inject coop_hook.dll into a running game and forward
controller state to it over shared memory, so the game reads the host's
(eventually the guest's) input and nothing else.
- hook/: coop_hook.dll. DllMain spawns a worker that opens the shared-memory
channel (named by the game's pid) and installs SafetyHook inline hooks on
XInputGetState/GetStateEx/GetCapabilities/SetState. Detours synthesize state
from shared memory; unmanaged slots report disconnected, hiding physical pads.
- host/: process picker (Toolhelp32), CreateRemoteThread(LoadLibraryW) injector
with an IsWow64Process2 bitness guard, IPC server publishing pads each frame,
and an ImGui Injection panel wiring it together.
- tests/: hook_selftest exercises the IPC seqlock + hook detours in-process
(no game/controller needed); passes.
Build: SafetyHook wired in (COOP_BUILD_HOOK=ON), Zydis via FetchContent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>