M2(Vulkan layer): registry register/unregister + opt-in Injection-panel checkbox
Host helper register_vk_layer/unregister_vk_layer registers coop_vk_layer's manifest as a per-user (HKCU, no admin) implicit Vulkan layer and writes the scoping file (%TEMP%\coop_vk_target.txt) naming the target game's exe, so the layer captures only that game and stays inert for every other Vulkan app. The Injection panel gains an opt-in "Set up Vulkan layer" checkbox (next to Auto re-attach, which the too-late banner points at); it registers on tick, unregisters on untick, and the panel destructor unregisters on host exit so no stale registration is left behind. Verified live: a registry-registered layer is loaded by the loader for a fresh vk launch (no env vars) and the scoping matches (active=1), then cleans up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,7 @@ add_executable(coop_host WIN32
|
||||
src/audio/audio_loopback.cpp
|
||||
src/audio/audio_overrides.cpp
|
||||
src/audio/process_loopback_capture.cpp
|
||||
src/vk_layer_setup.cpp
|
||||
src/test_harness.cpp)
|
||||
|
||||
target_include_directories(coop_host PRIVATE src)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "injection_panel.hpp"
|
||||
|
||||
#include "vk_layer_setup.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <windows.h>
|
||||
@@ -96,6 +98,10 @@ void InjectionPanel::refresh_targets()
|
||||
|
||||
InjectionPanel::~InjectionPanel()
|
||||
{
|
||||
if (vk_layer_enabled_)
|
||||
{
|
||||
unregister_vk_layer(); // don't leave the implicit layer registered after the tool closes
|
||||
}
|
||||
close_target_handle();
|
||||
}
|
||||
|
||||
@@ -580,6 +586,27 @@ void InjectionPanel::draw(bool debug_details)
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(kGrey, "(watching for %s...)", narrow(selected_name_).c_str());
|
||||
}
|
||||
// Opt-in Vulkan capture layer: for Vulkan games that initialize Vulkan immediately
|
||||
// (where even auto-attach injects too late -- see the red banner), register a per-user
|
||||
// implicit layer scoped to this game so the next launch is captured from the first frame.
|
||||
// Removed when unticked or the host exits.
|
||||
if (ImGui::Checkbox("Set up Vulkan layer (for immediate-init Vulkan games)", &vk_layer_enabled_))
|
||||
{
|
||||
if (vk_layer_enabled_)
|
||||
{
|
||||
vk_layer_enabled_ = register_vk_layer(selected_name_);
|
||||
}
|
||||
else
|
||||
{
|
||||
unregister_vk_layer();
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
{
|
||||
ImGui::SetTooltip("Registers a per-user (HKCU, no admin) implicit Vulkan layer scoped to\n"
|
||||
"this game, so a relaunch is captured before Vulkan init. Pair with\n"
|
||||
"Auto re-attach. Removed when you untick it or close the tool.");
|
||||
}
|
||||
}
|
||||
ImGui::Separator();
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@ private:
|
||||
// the same image name the moment it relaunches, so a quick kill+relaunch re-attaches
|
||||
// early (catching IAudioClient::Initialize) without the operator picking a target.
|
||||
bool auto_reattach_ = false;
|
||||
bool vk_layer_enabled_ = false; // opt-in: registered the implicit Vulkan capture layer for this game
|
||||
double last_auto_poll_ = 0.0; // throttle the process-list poll
|
||||
|
||||
// Heartbeat liveness tracking (is the injected DLL responding?).
|
||||
|
||||
79
host/src/vk_layer_setup.cpp
Normal file
79
host/src/vk_layer_setup.cpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#include "vk_layer_setup.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "coop/tool_paths.hpp"
|
||||
|
||||
namespace coop
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// The Vulkan loader's per-user implicit-layer registry list. Each value is a manifest path; its
|
||||
// DWORD data 0 = enabled.
|
||||
constexpr const wchar_t* kImplicitLayersKey = L"SOFTWARE\\Khronos\\Vulkan\\ImplicitLayers";
|
||||
|
||||
std::wstring manifest_path()
|
||||
{
|
||||
return exe_directory() + L"coop_vk_layer.json"; // staged next to coop_host.exe
|
||||
}
|
||||
|
||||
std::wstring scoping_file()
|
||||
{
|
||||
wchar_t dir[MAX_PATH] = {};
|
||||
const DWORD n = GetTempPathW(MAX_PATH, dir);
|
||||
return (n != 0 && n < MAX_PATH) ? std::wstring(dir) + L"coop_vk_target.txt" : std::wstring();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool register_vk_layer(const std::wstring& target_image)
|
||||
{
|
||||
// Write the scoping file (target image basename, UTF-8) the layer checks against its own image.
|
||||
const std::wstring sf = scoping_file();
|
||||
if (!sf.empty())
|
||||
{
|
||||
const std::size_t slash = target_image.find_last_of(L"\\/");
|
||||
const std::wstring base = slash == std::wstring::npos ? target_image : target_image.substr(slash + 1);
|
||||
const int n = WideCharToMultiByte(CP_UTF8, 0, base.c_str(), static_cast<int>(base.size()), nullptr, 0,
|
||||
nullptr, nullptr);
|
||||
std::string utf8(static_cast<std::size_t>(n), '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, base.c_str(), static_cast<int>(base.size()), utf8.data(), n, nullptr,
|
||||
nullptr);
|
||||
HANDLE f = CreateFileW(sf.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (f != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
DWORD written = 0;
|
||||
WriteFile(f, utf8.data(), static_cast<DWORD>(utf8.size()), &written, nullptr);
|
||||
CloseHandle(f);
|
||||
}
|
||||
}
|
||||
|
||||
HKEY key = nullptr;
|
||||
if (RegCreateKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, nullptr, 0, KEY_SET_VALUE, nullptr, &key,
|
||||
nullptr) != ERROR_SUCCESS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const std::wstring mp = manifest_path();
|
||||
DWORD enabled = 0; // 0 = enabled, per the loader's convention
|
||||
const LONG r = RegSetValueExW(key, mp.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&enabled),
|
||||
sizeof(enabled));
|
||||
RegCloseKey(key);
|
||||
return r == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
void unregister_vk_layer()
|
||||
{
|
||||
HKEY key = nullptr;
|
||||
if (RegOpenKeyExW(HKEY_CURRENT_USER, kImplicitLayersKey, 0, KEY_SET_VALUE, &key) == ERROR_SUCCESS)
|
||||
{
|
||||
RegDeleteValueW(key, manifest_path().c_str());
|
||||
RegCloseKey(key);
|
||||
}
|
||||
const std::wstring sf = scoping_file();
|
||||
if (!sf.empty())
|
||||
{
|
||||
DeleteFileW(sf.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
24
host/src/vk_layer_setup.hpp
Normal file
24
host/src/vk_layer_setup.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
// Host-side setup for the opt-in Vulkan capture layer (coop_vk_layer). Registering it as a
|
||||
// per-user implicit Vulkan layer makes the Vulkan loader insert it at vkCreateInstance -- so it's
|
||||
// in the chain before a game (that caches its present pointer at init) can resolve
|
||||
// vkQueuePresentKHR, the reliable early-presence path the inject-after-launch hook can't get.
|
||||
//
|
||||
// An implicit layer loads into *every* Vulkan app, so the layer itself only captures the process
|
||||
// whose image basename matches the scoping file this writes -- keeping it inert for everything but
|
||||
// the host's target. Registration is HKCU-only (no admin) and fully reversible.
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
// Register the implicit layer (HKCU) and scope it to `target_image` (the game's exe basename,
|
||||
// e.g. "game.exe"). Returns true on success. Idempotent.
|
||||
bool register_vk_layer(const std::wstring& target_image);
|
||||
|
||||
// Remove the registry registration and the scoping file. Safe to call when not registered (e.g.
|
||||
// on host shutdown as a belt-and-suspenders cleanup).
|
||||
void unregister_vk_layer();
|
||||
|
||||
} // namespace coop
|
||||
Reference in New Issue
Block a user