// CoopAllTheThings Vulkan capture layer. // // A real Vulkan *implicit layer* the loader inserts at vkCreateInstance -- guaranteed to be in // the chain before the game resolves vkQueuePresentKHR. This is the reliable early-presence path // for games that initialize Vulkan immediately (which inject-after-launch + the inline-hook // vk_hook can't catch). It does the same capture as vk_hook -- read the presented image back with // vkCmdCopyImageToBuffer, swizzle BGRA->RGBA, upload into the shared keyed-mutex texture on a // hook-owned D3D11 device, and re-chain the present's wait semaphores -- but via proper // layer-chain dispatch instead of an inline hook. // // Scoping: an implicit layer loads into *every* Vulkan app, so the layer only *captures* when it // recognises the process as the host's target -- env COOP_VK_LAYER_FORCE=1 (tests), or this // process's image basename matches %TEMP%\coop_vk_target.txt (written by the host's "Set up Vulkan // layer" checkbox). Otherwise it's a pure pass-through. // // The loader/layer interface structs (VkLayer*CreateInfo, VkNegotiateLayerInterface) live in // vk_layer.h, which Vulkan-Headers doesn't ship, so they're declared here to the stable // loader-interface-version-2 ABI. #include #include #include #include #include #include #include #include #include #define VK_NO_PROTOTYPES #define VK_USE_PLATFORM_WIN32_KHR #include #include "coop/shared_memory.hpp" #include "ipc_client.hpp" // --- Loader/layer interface (interface version 2) --------------------------- extern "C" { typedef enum VkLayerFunction_ { COOP_VK_LAYER_LINK_INFO = 0, COOP_VK_LOADER_DATA_CALLBACK = 1, COOP_VK_LOADER_LAYER_CREATE_DEVICE_CALLBACK = 2, COOP_VK_LOADER_FEATURES = 3, } CoopVkLayerFunction; typedef PFN_vkVoidFunction(VKAPI_PTR* PFN_GetPhysicalDeviceProcAddr)(VkInstance, const char*); typedef struct VkLayerInstanceLink_ { struct VkLayerInstanceLink_* pNext; PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr; PFN_GetPhysicalDeviceProcAddr pfnNextGetPhysicalDeviceProcAddr; } VkLayerInstanceLink; typedef struct VkLayerInstanceCreateInfo { VkStructureType sType; // 1000000000 = VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO const void* pNext; CoopVkLayerFunction function; union { VkLayerInstanceLink* pLayerInfo; void* pfnCallback; // other callbacks (unused here); keeps the union pointer-sized } u; } VkLayerInstanceCreateInfo; typedef struct VkLayerDeviceLink_ { struct VkLayerDeviceLink_* pNext; PFN_vkGetInstanceProcAddr pfnNextGetInstanceProcAddr; PFN_vkGetDeviceProcAddr pfnNextGetDeviceProcAddr; } VkLayerDeviceLink; typedef struct VkLayerDeviceCreateInfo { VkStructureType sType; // 1000000001 = VK_STRUCTURE_TYPE_LOADER_DEVICE_CREATE_INFO const void* pNext; CoopVkLayerFunction function; union { VkLayerDeviceLink* pLayerInfo; void* pfnCallback; } u; } VkLayerDeviceCreateInfo; typedef struct VkNegotiateLayerInterface { uint32_t sType; // 1 = LAYER_NEGOTIATE_INTERFACE_STRUCT void* pNext; uint32_t loaderLayerInterfaceVersion; PFN_vkGetInstanceProcAddr pfnGetInstanceProcAddr; PFN_vkGetDeviceProcAddr pfnGetDeviceProcAddr; PFN_GetPhysicalDeviceProcAddr pfnGetPhysicalDeviceProcAddr; } VkNegotiateLayerInterface; } namespace { // The loader tags its chain-link structs with small, loader-internal sType values (not the // 1000000000-range): VK_STRUCTURE_TYPE_LOADER_INSTANCE_CREATE_INFO = 47, _DEVICE = 48. These are // from the (unvendored) vk_layer.h and are stable across loader versions. constexpr VkStructureType kLoaderInstanceCreateInfo = static_cast(47); constexpr VkStructureType kLoaderDeviceCreateInfo = static_cast(48); using coop::hook::IpcClient; IpcClient g_ipc; bool g_ipc_tried = false; bool g_active = false; // do we capture in this process? (scoping) // Chain dispatch. PFN_vkGetInstanceProcAddr g_next_gipa = nullptr; PFN_vkGetDeviceProcAddr g_next_gdpa = nullptr; PFN_vkQueuePresentKHR g_real_present = nullptr; PFN_vkCreateSwapchainKHR g_real_create_swapchain = nullptr; VkInstance g_instance = VK_NULL_HANDLE; VkPhysicalDevice g_phys = VK_NULL_HANDLE; VkDevice g_device = VK_NULL_HANDLE; std::uint32_t g_qfam = 0; VkQueue g_queue = VK_NULL_HANDLE; // Device functions for the read-back (same set as vk_hook). struct VkFns { PFN_vkGetDeviceQueue GetDeviceQueue; PFN_vkCreateCommandPool CreateCommandPool; PFN_vkDestroyCommandPool DestroyCommandPool; PFN_vkAllocateCommandBuffers AllocateCommandBuffers; PFN_vkBeginCommandBuffer BeginCommandBuffer; PFN_vkEndCommandBuffer EndCommandBuffer; PFN_vkResetCommandBuffer ResetCommandBuffer; PFN_vkCmdPipelineBarrier CmdPipelineBarrier; PFN_vkCmdCopyImageToBuffer CmdCopyImageToBuffer; PFN_vkQueueSubmit QueueSubmit; PFN_vkCreateFence CreateFence; PFN_vkDestroyFence DestroyFence; PFN_vkWaitForFences WaitForFences; PFN_vkResetFences ResetFences; PFN_vkCreateSemaphore CreateSemaphore; PFN_vkDestroySemaphore DestroySemaphore; PFN_vkCreateBuffer CreateBuffer; PFN_vkDestroyBuffer DestroyBuffer; PFN_vkGetBufferMemoryRequirements GetBufferMemoryRequirements; PFN_vkAllocateMemory AllocateMemory; PFN_vkFreeMemory FreeMemory; PFN_vkBindBufferMemory BindBufferMemory; PFN_vkMapMemory MapMemory; PFN_vkUnmapMemory UnmapMemory; PFN_vkGetSwapchainImagesKHR GetSwapchainImagesKHR; PFN_vkDeviceWaitIdle DeviceWaitIdle; PFN_vkGetPhysicalDeviceMemoryProperties GetPhysicalDeviceMemoryProperties; }; VkFns g_fns{}; VkCommandPool g_pool = VK_NULL_HANDLE; VkCommandBuffer g_cmd = VK_NULL_HANDLE; VkFence g_fence = VK_NULL_HANDLE; VkSemaphore g_present_sem = VK_NULL_HANDLE; VkBuffer g_staging = VK_NULL_HANDLE; VkDeviceMemory g_staging_mem = VK_NULL_HANDLE; VkDeviceSize g_staging_size = 0; void* g_staging_mapped = nullptr; struct SwapInfo { VkSwapchainKHR sc; VkFormat fmt; std::uint32_t w, h; std::vector images; }; std::vector g_swaps; ID3D11Device* g_d3d = nullptr; ID3D11DeviceContext* g_d3d_ctx = nullptr; ID3D11Texture2D* g_shared_tex = nullptr; IDXGIKeyedMutex* g_shared_mutex = nullptr; HANDLE g_shared_handle = nullptr; UINT g_share_w = 0, g_share_h = 0; std::vector g_rgba; bool eq(const char* a, const char* b) { return std::strcmp(a, b) == 0; } // Optional file trace for debugging the chain dispatch (enable with COOP_VK_LAYER_LOG). void logvk(const char* fmt, ...) { static int enabled = -1; if (enabled < 0) { enabled = GetEnvironmentVariableW(L"COOP_VK_LAYER_LOG", nullptr, 0) != 0 ? 1 : 0; } if (enabled == 0) { return; } wchar_t dir[MAX_PATH] = {}; if (GetTempPathW(MAX_PATH, dir) == 0) { return; } FILE* f = _wfopen((std::wstring(dir) + L"coop_vk_layer.log").c_str(), L"a"); if (f == nullptr) { return; } va_list ap; va_start(ap, fmt); std::vfprintf(f, fmt, ap); va_end(ap); std::fputc('\n', f); std::fclose(f); } // Decide whether this process is the host's capture target (see file header). bool decide_active() { if (GetEnvironmentVariableW(L"COOP_VK_LAYER_FORCE", nullptr, 0) != 0) { return true; } wchar_t dir[MAX_PATH] = {}; const DWORD n = GetTempPathW(MAX_PATH, dir); if (n == 0 || n >= MAX_PATH) { return false; } HANDLE f = CreateFileW((std::wstring(dir) + L"coop_vk_target.txt").c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (f == INVALID_HANDLE_VALUE) { return false; } char want[MAX_PATH] = {}; DWORD got = 0; ReadFile(f, want, sizeof(want) - 1, &got, nullptr); CloseHandle(f); // Trim trailing whitespace/newline. while (got > 0 && (want[got - 1] == '\n' || want[got - 1] == '\r' || want[got - 1] == ' ')) { want[--got] = '\0'; } if (got == 0) { return false; } wchar_t self[MAX_PATH] = {}; GetModuleFileNameW(nullptr, self, MAX_PATH); const wchar_t* base = wcsrchr(self, L'\\'); base = base != nullptr ? base + 1 : self; char self8[MAX_PATH] = {}; WideCharToMultiByte(CP_UTF8, 0, base, -1, self8, sizeof(self8), nullptr, nullptr); return _stricmp(self8, want) == 0; } bool ensure_d3d() { if (g_d3d != nullptr) { return true; } return SUCCEEDED(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, &g_d3d, nullptr, &g_d3d_ctx)) && g_d3d != nullptr; } void release_shared() { if (g_shared_mutex) { g_shared_mutex->Release(); g_shared_mutex = nullptr; } if (g_shared_tex) { g_shared_tex->Release(); g_shared_tex = nullptr; } if (g_shared_handle) { CloseHandle(g_shared_handle); g_shared_handle = nullptr; } g_share_w = g_share_h = 0; } bool ensure_shared_texture(UINT w, UINT h) { if (g_shared_tex && g_share_w == w && g_share_h == h) { return true; } release_shared(); D3D11_TEXTURE2D_DESC d{}; d.Width = w; d.Height = h; d.MipLevels = 1; d.ArraySize = 1; d.Format = DXGI_FORMAT_R8G8B8A8_UNORM; d.SampleDesc.Count = 1; d.Usage = D3D11_USAGE_DEFAULT; d.BindFlags = D3D11_BIND_SHADER_RESOURCE; d.MiscFlags = D3D11_RESOURCE_MISC_SHARED_NTHANDLE | D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX; if (FAILED(g_d3d->CreateTexture2D(&d, nullptr, &g_shared_tex)) || !g_shared_tex) { return false; } IDXGIResource1* res = nullptr; if (FAILED(g_shared_tex->QueryInterface(__uuidof(IDXGIResource1), reinterpret_cast(&res))) || !res) { release_shared(); return false; } const std::wstring name = coop::video_share_name(GetCurrentProcessId()); const HRESULT hr = res->CreateSharedHandle( nullptr, DXGI_SHARED_RESOURCE_READ | DXGI_SHARED_RESOURCE_WRITE, name.c_str(), &g_shared_handle); res->Release(); if (FAILED(hr) || !g_shared_handle || FAILED(g_shared_tex->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast(&g_shared_mutex)))) { release_shared(); return false; } g_share_w = w; g_share_h = h; return true; } bool find_host_visible_memory(std::uint32_t bits, std::uint32_t& out) { VkPhysicalDeviceMemoryProperties mp{}; g_fns.GetPhysicalDeviceMemoryProperties(g_phys, &mp); const VkMemoryPropertyFlags want = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; for (std::uint32_t i = 0; i < mp.memoryTypeCount; ++i) { if ((bits & (1u << i)) && (mp.memoryTypes[i].propertyFlags & want) == want) { out = i; return true; } } return false; } void release_staging() { if (g_staging_mapped && g_staging_mem) { g_fns.UnmapMemory(g_device, g_staging_mem); g_staging_mapped = nullptr; } if (g_staging) { g_fns.DestroyBuffer(g_device, g_staging, nullptr); g_staging = VK_NULL_HANDLE; } if (g_staging_mem) { g_fns.FreeMemory(g_device, g_staging_mem, nullptr); g_staging_mem = VK_NULL_HANDLE; } g_staging_size = 0; } bool ensure_vk_resources(std::uint32_t w, std::uint32_t h) { if (g_queue == VK_NULL_HANDLE) { g_fns.GetDeviceQueue(g_device, g_qfam, 0, &g_queue); } if (g_pool == VK_NULL_HANDLE) { VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; pci.queueFamilyIndex = g_qfam; if (g_fns.CreateCommandPool(g_device, &pci, nullptr, &g_pool) != VK_SUCCESS) { return false; } VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; ai.commandPool = g_pool; ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; ai.commandBufferCount = 1; VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; if (g_fns.AllocateCommandBuffers(g_device, &ai, &g_cmd) != VK_SUCCESS || g_fns.CreateFence(g_device, &fi, nullptr, &g_fence) != VK_SUCCESS || g_fns.CreateSemaphore(g_device, &si, nullptr, &g_present_sem) != VK_SUCCESS) { return false; } } const VkDeviceSize need = static_cast(w) * h * 4; if (g_staging != VK_NULL_HANDLE && g_staging_size == need) { return true; } release_staging(); VkBufferCreateInfo bci{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO}; bci.size = need; bci.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; bci.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (g_fns.CreateBuffer(g_device, &bci, nullptr, &g_staging) != VK_SUCCESS) { return false; } VkMemoryRequirements mr{}; g_fns.GetBufferMemoryRequirements(g_device, g_staging, &mr); std::uint32_t mt = 0; if (!find_host_visible_memory(mr.memoryTypeBits, mt)) { release_staging(); return false; } VkMemoryAllocateInfo mai{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO}; mai.allocationSize = mr.size; mai.memoryTypeIndex = mt; if (g_fns.AllocateMemory(g_device, &mai, nullptr, &g_staging_mem) != VK_SUCCESS || g_fns.BindBufferMemory(g_device, g_staging, g_staging_mem, 0) != VK_SUCCESS || g_fns.MapMemory(g_device, g_staging_mem, 0, VK_WHOLE_SIZE, 0, &g_staging_mapped) != VK_SUCCESS) { release_staging(); return false; } g_staging_size = need; return true; } void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, VkAccessFlags s, VkAccessFlags d) { VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; b.srcAccessMask = s; b.dstAccessMask = d; b.oldLayout = from; b.newLayout = to; b.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; b.image = img; b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; g_fns.CmdPipelineBarrier(cb, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &b); } bool capture(VkImage image, VkFormat fmt, std::uint32_t w, std::uint32_t h, const VkSemaphore* wait, std::uint32_t wait_count, VkSemaphore& out_sem) { const bool bgra = fmt == VK_FORMAT_B8G8R8A8_UNORM || fmt == VK_FORMAT_B8G8R8A8_SRGB; const bool rgba = fmt == VK_FORMAT_R8G8B8A8_UNORM || fmt == VK_FORMAT_R8G8B8A8_SRGB; if ((!bgra && !rgba) || !ensure_d3d() || !ensure_shared_texture(w, h) || !ensure_vk_resources(w, h)) { return false; } g_fns.ResetCommandBuffer(g_cmd, 0); VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; g_fns.BeginCommandBuffer(g_cmd, &bi); barrier(g_cmd, image, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_ACCESS_MEMORY_READ_BIT, VK_ACCESS_TRANSFER_READ_BIT); VkBufferImageCopy region{}; region.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; region.imageExtent = {w, h, 1}; g_fns.CmdCopyImageToBuffer(g_cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g_staging, 1, ®ion); barrier(g_cmd, image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_MEMORY_READ_BIT); g_fns.EndCommandBuffer(g_cmd); std::vector stages(wait_count, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT); VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO}; si.waitSemaphoreCount = wait_count; si.pWaitSemaphores = wait; si.pWaitDstStageMask = wait_count ? stages.data() : nullptr; si.commandBufferCount = 1; si.pCommandBuffers = &g_cmd; si.signalSemaphoreCount = 1; si.pSignalSemaphores = &g_present_sem; g_fns.ResetFences(g_device, 1, &g_fence); if (g_fns.QueueSubmit(g_queue, 1, &si, g_fence) != VK_SUCCESS) { return false; } g_fns.WaitForFences(g_device, 1, &g_fence, VK_TRUE, UINT64_MAX); const size_t row = static_cast(w) * 4; if (g_rgba.size() != row * h) { g_rgba.resize(row * h); } const auto* src = static_cast(g_staging_mapped); for (std::uint32_t y = 0; y < h; ++y) { const unsigned char* s = src + static_cast(y) * row; unsigned char* o = g_rgba.data() + static_cast(y) * row; if (bgra) { for (std::uint32_t x = 0; x < w; ++x) { o[x * 4 + 0] = s[x * 4 + 2]; o[x * 4 + 1] = s[x * 4 + 1]; o[x * 4 + 2] = s[x * 4 + 0]; o[x * 4 + 3] = 255; } } else { std::memcpy(o, s, row); } } out_sem = g_present_sem; if (g_shared_mutex->AcquireSync(coop::kVideoMutexKey, 8) == S_OK) { g_d3d_ctx->UpdateSubresource(g_shared_tex, 0, nullptr, g_rgba.data(), static_cast(row), 0); g_d3d_ctx->Flush(); g_shared_mutex->ReleaseSync(coop::kVideoMutexKey); if (!g_ipc_tried) { g_ipc_tried = true; g_ipc.connect(/*attempts=*/40, /*delay_ms=*/25); } if (g_ipc.connected()) { g_ipc.publish_video_frame(w, h, static_cast(DXGI_FORMAT_R8G8B8A8_UNORM)); } } return true; } const SwapInfo* find_swap(VkSwapchainKHR sc) { for (const SwapInfo& s : g_swaps) { if (s.sc == sc) { return &s; } } return nullptr; } VKAPI_ATTR VkResult VKAPI_CALL layer_QueuePresentKHR(VkQueue queue, const VkPresentInfoKHR* pi) { if (g_active && g_ipc.connected()) { g_ipc.note_present(); } if (g_active && pi != nullptr && pi->swapchainCount == 1) { const SwapInfo* s = find_swap(pi->pSwapchains[0]); if (s != nullptr && pi->pImageIndices[0] < s->images.size()) { VkSemaphore chained = VK_NULL_HANDLE; if (capture(s->images[pi->pImageIndices[0]], s->fmt, s->w, s->h, pi->pWaitSemaphores, pi->waitSemaphoreCount, chained)) { VkPresentInfoKHR p = *pi; p.waitSemaphoreCount = 1; p.pWaitSemaphores = &chained; return g_real_present(queue, &p); } } } return g_real_present(queue, pi); } VKAPI_ATTR VkResult VKAPI_CALL layer_CreateSwapchainKHR(VkDevice device, const VkSwapchainCreateInfoKHR* ci, const VkAllocationCallbacks* a, VkSwapchainKHR* out) { const VkResult r = g_real_create_swapchain(device, ci, a, out); if (g_active && r == VK_SUCCESS && out && g_fns.GetSwapchainImagesKHR) { SwapInfo info{}; info.sc = *out; info.fmt = ci->imageFormat; info.w = ci->imageExtent.width; info.h = ci->imageExtent.height; std::uint32_t n = 0; g_fns.GetSwapchainImagesKHR(device, *out, &n, nullptr); info.images.resize(n); g_fns.GetSwapchainImagesKHR(device, *out, &n, info.images.data()); g_swaps.push_back(std::move(info)); } return r; } void load_device_fns(VkDevice dev) { #define LOAD(field, vkname) g_fns.field = reinterpret_cast(g_next_gdpa(dev, #vkname)) LOAD(GetDeviceQueue, vkGetDeviceQueue); LOAD(CreateCommandPool, vkCreateCommandPool); LOAD(DestroyCommandPool, vkDestroyCommandPool); LOAD(AllocateCommandBuffers, vkAllocateCommandBuffers); LOAD(BeginCommandBuffer, vkBeginCommandBuffer); LOAD(EndCommandBuffer, vkEndCommandBuffer); LOAD(ResetCommandBuffer, vkResetCommandBuffer); LOAD(CmdPipelineBarrier, vkCmdPipelineBarrier); LOAD(CmdCopyImageToBuffer, vkCmdCopyImageToBuffer); LOAD(QueueSubmit, vkQueueSubmit); LOAD(CreateFence, vkCreateFence); LOAD(DestroyFence, vkDestroyFence); LOAD(WaitForFences, vkWaitForFences); LOAD(ResetFences, vkResetFences); LOAD(CreateSemaphore, vkCreateSemaphore); LOAD(DestroySemaphore, vkDestroySemaphore); LOAD(CreateBuffer, vkCreateBuffer); LOAD(DestroyBuffer, vkDestroyBuffer); LOAD(GetBufferMemoryRequirements, vkGetBufferMemoryRequirements); LOAD(AllocateMemory, vkAllocateMemory); LOAD(FreeMemory, vkFreeMemory); LOAD(BindBufferMemory, vkBindBufferMemory); LOAD(MapMemory, vkMapMemory); LOAD(UnmapMemory, vkUnmapMemory); LOAD(GetSwapchainImagesKHR, vkGetSwapchainImagesKHR); LOAD(DeviceWaitIdle, vkDeviceWaitIdle); #undef LOAD g_fns.GetPhysicalDeviceMemoryProperties = reinterpret_cast( g_next_gipa(g_instance, "vkGetPhysicalDeviceMemoryProperties")); } VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_gdpa(VkDevice device, const char* name); VKAPI_ATTR VkResult VKAPI_CALL layer_CreateDevice(VkPhysicalDevice phys, const VkDeviceCreateInfo* ci, const VkAllocationCallbacks* a, VkDevice* out) { auto* link = reinterpret_cast(const_cast(ci->pNext)); while (link != nullptr && !(link->sType == kLoaderDeviceCreateInfo && link->function == COOP_VK_LAYER_LINK_INFO)) { link = reinterpret_cast(const_cast(link->pNext)); } if (link == nullptr) { logvk("CreateDevice: LINK_INFO not found"); return VK_ERROR_INITIALIZATION_FAILED; } PFN_vkGetInstanceProcAddr next_gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr; PFN_vkGetDeviceProcAddr next_gdpa = link->u.pLayerInfo->pfnNextGetDeviceProcAddr; link->u.pLayerInfo = link->u.pLayerInfo->pNext; // advance the chain for the next layer auto create = reinterpret_cast(next_gipa(g_instance, "vkCreateDevice")); const VkResult r = create(phys, ci, a, out); logvk("CreateDevice: result=%d active=%d", (int)r, g_active ? 1 : 0); if (r == VK_SUCCESS && out != nullptr && g_device == VK_NULL_HANDLE) { g_phys = phys; g_device = *out; g_next_gdpa = next_gdpa; g_qfam = ci->queueCreateInfoCount > 0 ? ci->pQueueCreateInfos[0].queueFamilyIndex : 0; g_real_present = reinterpret_cast(next_gdpa(*out, "vkQueuePresentKHR")); g_real_create_swapchain = reinterpret_cast(next_gdpa(*out, "vkCreateSwapchainKHR")); if (g_active) { load_device_fns(*out); } } return r; } VKAPI_ATTR VkResult VKAPI_CALL layer_CreateInstance(const VkInstanceCreateInfo* ci, const VkAllocationCallbacks* a, VkInstance* out) { auto* link = reinterpret_cast(const_cast(ci->pNext)); while (link != nullptr && !(link->sType == kLoaderInstanceCreateInfo && link->function == COOP_VK_LAYER_LINK_INFO)) { link = reinterpret_cast(const_cast(link->pNext)); } if (link == nullptr) { logvk("CreateInstance: LINK_INFO not found"); return VK_ERROR_INITIALIZATION_FAILED; } PFN_vkGetInstanceProcAddr next_gipa = link->u.pLayerInfo->pfnNextGetInstanceProcAddr; link->u.pLayerInfo = link->u.pLayerInfo->pNext; // advance the chain auto create = reinterpret_cast(next_gipa(nullptr, "vkCreateInstance")); const VkResult r = create(ci, a, out); if (r == VK_SUCCESS && out != nullptr) { g_instance = *out; g_next_gipa = next_gipa; g_active = decide_active(); } logvk("CreateInstance: result=%d active=%d", (int)r, g_active ? 1 : 0); return r; } VKAPI_ATTR void VKAPI_CALL layer_DestroyDevice(VkDevice device, const VkAllocationCallbacks* a) { auto destroy = reinterpret_cast(g_next_gdpa(device, "vkDestroyDevice")); if (g_active && device == g_device && g_fns.DeviceWaitIdle != nullptr) { g_fns.DeviceWaitIdle(device); release_staging(); if (g_present_sem) { g_fns.DestroySemaphore(device, g_present_sem, nullptr); g_present_sem = VK_NULL_HANDLE; } if (g_fence) { g_fns.DestroyFence(device, g_fence, nullptr); g_fence = VK_NULL_HANDLE; } if (g_pool) { g_fns.DestroyCommandPool(device, g_pool, nullptr); g_pool = VK_NULL_HANDLE; } release_shared(); if (g_d3d_ctx) { g_d3d_ctx->Release(); g_d3d_ctx = nullptr; } if (g_d3d) { g_d3d->Release(); g_d3d = nullptr; } g_swaps.clear(); g_device = VK_NULL_HANDLE; g_queue = VK_NULL_HANDLE; g_cmd = VK_NULL_HANDLE; } destroy(device, a); } VKAPI_ATTR void VKAPI_CALL layer_DestroyInstance(VkInstance instance, const VkAllocationCallbacks* a) { auto destroy = reinterpret_cast(g_next_gipa(instance, "vkDestroyInstance")); g_instance = VK_NULL_HANDLE; destroy(instance, a); } VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_gdpa(VkDevice device, const char* name) { if (name == nullptr) { return nullptr; } if (eq(name, "vkGetDeviceProcAddr")) return reinterpret_cast(&layer_gdpa); if (eq(name, "vkQueuePresentKHR")) return reinterpret_cast(&layer_QueuePresentKHR); if (eq(name, "vkCreateSwapchainKHR")) return reinterpret_cast(&layer_CreateSwapchainKHR); if (eq(name, "vkDestroyDevice")) return reinterpret_cast(&layer_DestroyDevice); return g_next_gdpa != nullptr ? g_next_gdpa(device, name) : nullptr; } VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL layer_gipa(VkInstance instance, const char* name) { if (name == nullptr) { return nullptr; } if (eq(name, "vkGetInstanceProcAddr")) return reinterpret_cast(&layer_gipa); if (eq(name, "vkCreateInstance")) return reinterpret_cast(&layer_CreateInstance); if (eq(name, "vkCreateDevice")) return reinterpret_cast(&layer_CreateDevice); if (eq(name, "vkDestroyInstance")) return reinterpret_cast(&layer_DestroyInstance); if (eq(name, "vkGetDeviceProcAddr")) return reinterpret_cast(&layer_gdpa); return g_next_gipa != nullptr ? g_next_gipa(instance, name) : nullptr; } } // namespace extern "C" __declspec(dllexport) VkResult VKAPI_CALL vkNegotiateLoaderLayerInterfaceVersion(VkNegotiateLayerInterface* pVersionStruct) { logvk("negotiate: requestedVersion=%u", pVersionStruct->loaderLayerInterfaceVersion); if (pVersionStruct->loaderLayerInterfaceVersion > 2) { pVersionStruct->loaderLayerInterfaceVersion = 2; } pVersionStruct->pfnGetInstanceProcAddr = layer_gipa; pVersionStruct->pfnGetDeviceProcAddr = layer_gdpa; pVersionStruct->pfnGetPhysicalDeviceProcAddr = nullptr; return VK_SUCCESS; } // Also export the entry points directly, for loaders that probe them by name. extern "C" __declspec(dllexport) PFN_vkVoidFunction VKAPI_CALL coop_vkGetInstanceProcAddr(VkInstance i, const char* n) { return layer_gipa(i, n); } extern "C" __declspec(dllexport) PFN_vkVoidFunction VKAPI_CALL coop_vkGetDeviceProcAddr(VkDevice d, const char* n) { return layer_gdpa(d, n); }