From 49daa1d81f9ec479eded7d4cd64bf73f4e401764 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Mon, 22 Jun 2026 12:12:55 +0200 Subject: [PATCH] M2(Vulkan): Vulkan mock-game backend + volk/Vulkan-Headers submodules Add render_vk.cpp (selectable as `vk`): brings up a real Vulkan instance/device/swap chain via volk (which dlopens vulkan-1.dll -- the loader-bypass case the capture hook must handle) and clears the swap-chain image to the frame-counter colour each frame with vkCmdClearColorImage (no pipeline, no shaders, no SPIR-V) and presents. The whole image encodes the frame number, so it animates and stale frames are detectable. Adds the official Khronos Vulkan-Headers + zeux/volk submodules and a coop_require_submodule() CMake helper that fails with a clear "git submodule update --init --recursive" message rather than auto-cloning. volk is pinned to the project's dynamic CRT (no LNK4098). mock_game_test gets a vk liveness check (its present pointer is cached at init, so late injection can't hook it -- the capture path needs the early-load path, to come). 16/16 ctest, skips cleanly without a Vulkan driver. Co-Authored-By: Claude Opus 4.8 --- .gitmodules | 6 + CMakeLists.txt | 13 ++ tests/mock_game_test.cpp | 35 ++++ third_party/CMakeLists.txt | 23 +++ third_party/Vulkan-Headers | 1 + third_party/volk | 1 + tools/mock_game/CMakeLists.txt | 5 +- tools/mock_game/main.cpp | 2 +- tools/mock_game/render_backend.cpp | 4 + tools/mock_game/render_backend.hpp | 1 + tools/mock_game/render_vk.cpp | 318 +++++++++++++++++++++++++++++ 11 files changed, 406 insertions(+), 3 deletions(-) create mode 160000 third_party/Vulkan-Headers create mode 160000 third_party/volk create mode 100644 tools/mock_game/render_vk.cpp diff --git a/.gitmodules b/.gitmodules index b077a4f..6502599 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,9 @@ [submodule "third_party/safetyhook"] path = third_party/safetyhook url = https://github.com/cursey/safetyhook.git +[submodule "third_party/Vulkan-Headers"] + path = third_party/Vulkan-Headers + url = https://github.com/KhronosGroup/Vulkan-Headers.git +[submodule "third_party/volk"] + path = third_party/volk + url = https://github.com/zeux/volk.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 69a3518..f89f294 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,19 @@ function(coop_output_subdir subdir) endforeach() endfunction() +# Fail with a clear message if a git submodule wasn't checked out, instead of a confusing +# "file not found" deep in a build. `sentinel` is a path (relative to the source root) that +# only exists when the submodule is populated. We deliberately do NOT auto-clone -- explicit +# is better than a surprise network fetch during configure. +# Usage: coop_require_submodule( ). +function(coop_require_submodule name sentinel) + if(NOT EXISTS "${CMAKE_SOURCE_DIR}/${sentinel}") + message(FATAL_ERROR + "Submodule '${name}' is not checked out (missing ${sentinel}).\n" + "Run: git submodule update --init --recursive") + endif() +endfunction() + if(MSVC) add_compile_options(/W4 /permissive- /Zc:__cplusplus /utf-8 /MP) add_compile_definitions(UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX) diff --git a/tests/mock_game_test.cpp b/tests/mock_game_test.cpp index 32bcdc6..3161b79 100644 --- a/tests/mock_game_test.cpp +++ b/tests/mock_game_test.cpp @@ -300,6 +300,37 @@ void test_video_capture(const char* backend, ID3D11Device* device) game.kill(); } +// Liveness smoke check for a backend whose capture can't be exercised by late injection +// (Vulkan caches its present pointer at init): launch it for a couple of seconds and assert it +// comes up and exits cleanly. Exit code 2 = backend unavailable on this machine (e.g. no Vulkan +// driver) -> skip without failing. +void test_liveness(const char* backend) +{ + std::printf("== liveness: %s ==\n", backend); + std::wstring args; + for (const char* p = backend; *p != '\0'; ++p) + { + args.push_back(static_cast(*p)); + } + args += L" 2"; // run 2s then exit on its own + MockGame game = MockGame::launch(args); + if (!game.ok) + { + check(false, "launch coop_mock_game (liveness)"); + return; + } + WaitForSingleObject(game.pi.hProcess, 6000); // let the 2s run finish + if (!game.alive() && game.exit_code() == 2) + { + std::printf(" backend '%s' unavailable on this machine -- skipping\n", backend); + game.kill(); + return; + } + check(!game.alive(), "backend ran and exited within the time limit (no hang)"); + check(game.exit_code() == 0, "backend came up and exited cleanly"); + game.kill(); +} + // Launch the mock game rendering audio at `rate`/`channels`/`bits`/`fmt`, inject the audio // hook (late, so it's the guessed path), and verify the hook MEASURES the right sample rate // for this variant and captures non-silent audio. (Channels/bit-depth aren't recoverable for @@ -522,6 +553,10 @@ int main() return 0; } + // Vulkan: its present pointer is cached at init, so late injection can't hook it -- a + // liveness check (the mock comes up + presents + exits) is the automated coverage here; the + // capture path is exercised via the early-load path documented in the README. + test_liveness("vk"); test_video_capture("gl", device); test_video_capture("dx9ex", device); test_video_capture("dx9", device); diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 0013a1a..05b9d8c 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -18,3 +18,26 @@ if(MSVC) endif() target_link_libraries(imgui PUBLIC d3d11 dxgi dwmapi) + +# --- Vulkan: official Khronos headers + volk meta-loader (submodules) ------- +# volk dynamically loads vulkan-1.dll at runtime (no link-time loader lib, no SDK install), +# so the Vulkan mock backend + capture hook need only these two submodules. +coop_require_submodule("Vulkan-Headers" "third_party/Vulkan-Headers/include/vulkan/vulkan.h") +coop_require_submodule("volk" "third_party/volk/volk.h") + +add_library(vulkan_headers INTERFACE) +target_include_directories(vulkan_headers INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/Vulkan-Headers/include) + +add_library(volk STATIC volk/volk.c) +target_include_directories(volk PUBLIC volk) +target_link_libraries(volk PUBLIC vulkan_headers) +# VK_NO_PROTOTYPES: volk provides the entry points itself (no link to a loader lib). +target_compile_definitions(volk PUBLIC VK_NO_PROTOTYPES) +if(WIN32) + target_compile_definitions(volk PUBLIC VK_USE_PLATFORM_WIN32_KHR) +endif() +if(MSVC) + target_compile_options(volk PRIVATE /W0) # third-party source + # Match the rest of the project's CRT (dynamic) so it doesn't drag in LIBCMT (LNK4098). + set_target_properties(volk PROPERTIES MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") +endif() diff --git a/third_party/Vulkan-Headers b/third_party/Vulkan-Headers new file mode 160000 index 0000000..01393c3 --- /dev/null +++ b/third_party/Vulkan-Headers @@ -0,0 +1 @@ +Subproject commit 01393c3df0e5285b54ee6527466513f9e614be94 diff --git a/third_party/volk b/third_party/volk new file mode 160000 index 0000000..477a354 --- /dev/null +++ b/third_party/volk @@ -0,0 +1 @@ +Subproject commit 477a354c50c233ac95fb52c4250815d08f6a1571 diff --git a/tools/mock_game/CMakeLists.txt b/tools/mock_game/CMakeLists.txt index 185a8d9..b1dd4c5 100644 --- a/tools/mock_game/CMakeLists.txt +++ b/tools/mock_game/CMakeLists.txt @@ -7,12 +7,13 @@ add_executable(coop_mock_game render_dx12.cpp render_dx10.cpp render_dx09.cpp - render_gl.cpp) + render_gl.cpp + render_vk.cpp) # Reuses the shared ToneSource (also used by coop_tone + the audio hook self-test). target_include_directories(coop_mock_game PRIVATE ${CMAKE_SOURCE_DIR}/tools/audio_tone) -target_link_libraries(coop_mock_game PRIVATE d3d11 d3d12 d3d10 d3d9 dxgi ole32 opengl32 gdi32) +target_link_libraries(coop_mock_game PRIVATE d3d11 d3d12 d3d10 d3d9 dxgi ole32 opengl32 gdi32 volk) set_target_properties(coop_mock_game PROPERTIES OUTPUT_NAME "coop_mock_game") # Test fixture -> stage next to the tests (alongside coop_tone), not in the deployable root. diff --git a/tools/mock_game/main.cpp b/tools/mock_game/main.cpp index 1f3ed6c..685ec41 100644 --- a/tools/mock_game/main.cpp +++ b/tools/mock_game/main.cpp @@ -1,6 +1,6 @@ // CoopMockGame -- a tiny test "game" used to exercise the capture + audio + hook paths. // -// coop_mock_game.exe [dx9|dx9ex|dx10|dx11|dx12|gl] [seconds] [rate] [channels] [bits] [pcm|float] +// coop_mock_game.exe [dx9|dx9ex|dx10|dx11|dx12|gl|vk] [seconds] [rate] [channels] [bits] [pcm|float] // // It opens a normal visible window and renders an animated, frame-numbered pattern (see // render_backend.hpp): a moving bar + per-frame background colour make motion obvious, and diff --git a/tools/mock_game/render_backend.cpp b/tools/mock_game/render_backend.cpp index 4f47586..ca0216f 100644 --- a/tools/mock_game/render_backend.cpp +++ b/tools/mock_game/render_backend.cpp @@ -29,6 +29,10 @@ std::unique_ptr RenderBackend::create(const std::string& name) { return create_gl_backend(); } + if (name == "vk" || name == "vulkan") + { + return create_vk_backend(); + } return nullptr; } diff --git a/tools/mock_game/render_backend.hpp b/tools/mock_game/render_backend.hpp index 3fedff5..e8f50c6 100644 --- a/tools/mock_game/render_backend.hpp +++ b/tools/mock_game/render_backend.hpp @@ -63,5 +63,6 @@ std::unique_ptr create_dx12_backend(); std::unique_ptr create_dx10_backend(); std::unique_ptr create_dx9_backend(bool ex); // ex: D3D9Ex vs plain D3D9 std::unique_ptr create_gl_backend(); +std::unique_ptr create_vk_backend(); } // namespace coop::mock diff --git a/tools/mock_game/render_vk.cpp b/tools/mock_game/render_vk.cpp new file mode 100644 index 0000000..1a24c97 --- /dev/null +++ b/tools/mock_game/render_vk.cpp @@ -0,0 +1,318 @@ +// Vulkan backend for the mock game. Brings up a real Vulkan instance/device/swap chain via +// volk (which dlopens vulkan-1.dll -- the loader-bypass case the capture hook must handle) and +// each frame clears the swap-chain image to the frame-counter colour with vkCmdClearColorImage +// (no pipeline, no shaders, no SPIR-V) and presents. The whole image encodes the frame number, +// so it animates and a dropped/stale frame is detectable by the capture test. Clear-only keeps +// this to one image-clear per frame; richer per-rect drawing would need a render pass. +#include "render_backend.hpp" + +#include + +#include + +namespace coop::mock +{ +namespace +{ +class VkBackend : public RenderBackend +{ +public: + bool init(HWND hwnd, std::uint32_t width, std::uint32_t height) override + { + width_ = width; + height_ = height; + if (volkInitialize() != VK_SUCCESS) + { + return false; // no Vulkan loader on this machine + } + + VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO}; + app.pApplicationName = "coop_mock_game"; + app.apiVersion = VK_API_VERSION_1_1; + const char* inst_ext[] = {VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WIN32_SURFACE_EXTENSION_NAME}; + VkInstanceCreateInfo ici{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO}; + ici.pApplicationInfo = &app; + ici.enabledExtensionCount = 2; + ici.ppEnabledExtensionNames = inst_ext; + if (vkCreateInstance(&ici, nullptr, &instance_) != VK_SUCCESS) + { + return false; + } + volkLoadInstance(instance_); + + VkWin32SurfaceCreateInfoKHR sci{VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR}; + sci.hinstance = GetModuleHandleW(nullptr); + sci.hwnd = hwnd; + if (vkCreateWin32SurfaceKHR(instance_, &sci, nullptr, &surface_) != VK_SUCCESS) + { + return false; + } + + if (!pick_device() || !create_device() || !create_swapchain() || !create_commands()) + { + return false; + } + return true; + } + + void render_and_present(std::uint32_t frame) override + { + if (device_ == VK_NULL_HANDLE || swapchain_ == VK_NULL_HANDLE) + { + return; + } + vkWaitForFences(device_, 1, &in_flight_, VK_TRUE, UINT64_MAX); + + std::uint32_t idx = 0; + VkResult acq = vkAcquireNextImageKHR(device_, swapchain_, UINT64_MAX, acquire_sem_, VK_NULL_HANDLE, &idx); + if (acq == VK_ERROR_OUT_OF_DATE_KHR || acq == VK_SUBOPTIMAL_KHR) + { + return; // skip this frame (the mock window isn't resized in practice) + } + if (acq != VK_SUCCESS) + { + return; + } + vkResetFences(device_, 1, &in_flight_); + + VkCommandBuffer cb = cmd_; + vkResetCommandBuffer(cb, 0); + VkCommandBufferBeginInfo bi{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO}; + bi.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + vkBeginCommandBuffer(cb, &bi); + + barrier(cb, images_[idx], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, + VK_ACCESS_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT); + + std::uint8_t r = 0, g = 0, b = 0; + frame_to_rgb(frame, r, g, b); // whole image encodes the frame -> animates + decodable + VkClearColorValue cc{}; + cc.float32[0] = r / 255.0f; + cc.float32[1] = g / 255.0f; + cc.float32[2] = b / 255.0f; + cc.float32[3] = 1.0f; + VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdClearColorImage(cb, images_[idx], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &cc, 1, &range); + + barrier(cb, images_[idx], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, + VK_ACCESS_TRANSFER_WRITE_BIT, 0, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT); + vkEndCommandBuffer(cb); + + VkPipelineStageFlags wait_stage = VK_PIPELINE_STAGE_TRANSFER_BIT; + VkSubmitInfo si{VK_STRUCTURE_TYPE_SUBMIT_INFO}; + si.waitSemaphoreCount = 1; + si.pWaitSemaphores = &acquire_sem_; + si.pWaitDstStageMask = &wait_stage; + si.commandBufferCount = 1; + si.pCommandBuffers = &cb; + si.signalSemaphoreCount = 1; + si.pSignalSemaphores = &submit_sem_; + vkQueueSubmit(queue_, 1, &si, in_flight_); + + VkPresentInfoKHR pi{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR}; + pi.waitSemaphoreCount = 1; + pi.pWaitSemaphores = &submit_sem_; + pi.swapchainCount = 1; + pi.pSwapchains = &swapchain_; + pi.pImageIndices = &idx; + vkQueuePresentKHR(queue_, &pi); + } + + [[nodiscard]] const char* name() const override + { + return "vk"; + } + + ~VkBackend() override + { + if (device_ != VK_NULL_HANDLE) + { + vkDeviceWaitIdle(device_); + if (in_flight_ != VK_NULL_HANDLE) + vkDestroyFence(device_, in_flight_, nullptr); + if (acquire_sem_ != VK_NULL_HANDLE) + vkDestroySemaphore(device_, acquire_sem_, nullptr); + if (submit_sem_ != VK_NULL_HANDLE) + vkDestroySemaphore(device_, submit_sem_, nullptr); + if (pool_ != VK_NULL_HANDLE) + vkDestroyCommandPool(device_, pool_, nullptr); + if (swapchain_ != VK_NULL_HANDLE) + vkDestroySwapchainKHR(device_, swapchain_, nullptr); + vkDestroyDevice(device_, nullptr); + } + if (surface_ != VK_NULL_HANDLE) + vkDestroySurfaceKHR(instance_, surface_, nullptr); + if (instance_ != VK_NULL_HANDLE) + vkDestroyInstance(instance_, nullptr); + } + +private: + bool pick_device() + { + std::uint32_t n = 0; + vkEnumeratePhysicalDevices(instance_, &n, nullptr); + std::vector devs(n); + vkEnumeratePhysicalDevices(instance_, &n, devs.data()); + for (VkPhysicalDevice pd : devs) + { + std::uint32_t qn = 0; + vkGetPhysicalDeviceQueueFamilyProperties(pd, &qn, nullptr); + std::vector qf(qn); + vkGetPhysicalDeviceQueueFamilyProperties(pd, &qn, qf.data()); + for (std::uint32_t i = 0; i < qn; ++i) + { + VkBool32 present = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(pd, i, surface_, &present); + if ((qf[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) && present) + { + phys_ = pd; + qfam_ = i; + return true; + } + } + } + return false; + } + + bool create_device() + { + float prio = 1.0f; + VkDeviceQueueCreateInfo qci{VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO}; + qci.queueFamilyIndex = qfam_; + qci.queueCount = 1; + qci.pQueuePriorities = &prio; + const char* dev_ext[] = {VK_KHR_SWAPCHAIN_EXTENSION_NAME}; + VkDeviceCreateInfo dci{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO}; + dci.queueCreateInfoCount = 1; + dci.pQueueCreateInfos = &qci; + dci.enabledExtensionCount = 1; + dci.ppEnabledExtensionNames = dev_ext; + if (vkCreateDevice(phys_, &dci, nullptr, &device_) != VK_SUCCESS) + { + return false; + } + volkLoadDevice(device_); + vkGetDeviceQueue(device_, qfam_, 0, &queue_); + return true; + } + + bool create_swapchain() + { + VkSurfaceCapabilitiesKHR caps{}; + vkGetPhysicalDeviceSurfaceCapabilitiesKHR(phys_, surface_, &caps); + + std::uint32_t fn = 0; + vkGetPhysicalDeviceSurfaceFormatsKHR(phys_, surface_, &fn, nullptr); + std::vector formats(fn); + vkGetPhysicalDeviceSurfaceFormatsKHR(phys_, surface_, &fn, formats.data()); + VkSurfaceFormatKHR chosen = formats.empty() ? VkSurfaceFormatKHR{VK_FORMAT_B8G8R8A8_UNORM, + VK_COLOR_SPACE_SRGB_NONLINEAR_KHR} + : formats[0]; + for (const VkSurfaceFormatKHR& f : formats) + { + if (f.format == VK_FORMAT_B8G8R8A8_UNORM || f.format == VK_FORMAT_R8G8B8A8_UNORM) + { + chosen = f; + break; + } + } + format_ = chosen.format; + + std::uint32_t want = caps.minImageCount + 1; + if (caps.maxImageCount > 0 && want > caps.maxImageCount) + { + want = caps.maxImageCount; + } + VkSwapchainCreateInfoKHR sc{VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR}; + sc.surface = surface_; + sc.minImageCount = want; + sc.imageFormat = chosen.format; + sc.imageColorSpace = chosen.colorSpace; + sc.imageExtent = caps.currentExtent.width != 0xFFFFFFFFu + ? caps.currentExtent + : VkExtent2D{width_, height_}; + sc.imageArrayLayers = 1; + // TRANSFER_DST so we can clear it; TRANSFER_SRC so the capture hook can copy it out. + sc.imageUsage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; + sc.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + sc.preTransform = caps.currentTransform; + sc.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; + sc.presentMode = VK_PRESENT_MODE_FIFO_KHR; // vsync, universally supported + sc.clipped = VK_TRUE; + if (vkCreateSwapchainKHR(device_, &sc, nullptr, &swapchain_) != VK_SUCCESS) + { + return false; + } + std::uint32_t in = 0; + vkGetSwapchainImagesKHR(device_, swapchain_, &in, nullptr); + images_.resize(in); + vkGetSwapchainImagesKHR(device_, swapchain_, &in, images_.data()); + return true; + } + + bool create_commands() + { + VkCommandPoolCreateInfo pci{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO}; + pci.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; + pci.queueFamilyIndex = qfam_; + if (vkCreateCommandPool(device_, &pci, nullptr, &pool_) != VK_SUCCESS) + { + return false; + } + VkCommandBufferAllocateInfo ai{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO}; + ai.commandPool = pool_; + ai.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + ai.commandBufferCount = 1; + if (vkAllocateCommandBuffers(device_, &ai, &cmd_) != VK_SUCCESS) + { + return false; + } + VkSemaphoreCreateInfo si{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; + VkFenceCreateInfo fi{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO}; + fi.flags = VK_FENCE_CREATE_SIGNALED_BIT; + return vkCreateSemaphore(device_, &si, nullptr, &acquire_sem_) == VK_SUCCESS && + vkCreateSemaphore(device_, &si, nullptr, &submit_sem_) == VK_SUCCESS && + vkCreateFence(device_, &fi, nullptr, &in_flight_) == VK_SUCCESS; + } + + static void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkImageLayout to, + VkAccessFlags src_access, VkAccessFlags dst_access, VkPipelineStageFlags src_stage, + VkPipelineStageFlags dst_stage) + { + VkImageMemoryBarrier b{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + b.srcAccessMask = src_access; + b.dstAccessMask = dst_access; + 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}; + vkCmdPipelineBarrier(cb, src_stage, dst_stage, 0, 0, nullptr, 0, nullptr, 1, &b); + } + + std::uint32_t width_ = 0; + std::uint32_t height_ = 0; + VkInstance instance_ = VK_NULL_HANDLE; + VkSurfaceKHR surface_ = VK_NULL_HANDLE; + VkPhysicalDevice phys_ = VK_NULL_HANDLE; + std::uint32_t qfam_ = 0; + VkDevice device_ = VK_NULL_HANDLE; + VkQueue queue_ = VK_NULL_HANDLE; + VkSwapchainKHR swapchain_ = VK_NULL_HANDLE; + VkFormat format_ = VK_FORMAT_UNDEFINED; + std::vector images_; + VkCommandPool pool_ = VK_NULL_HANDLE; + VkCommandBuffer cmd_ = VK_NULL_HANDLE; + VkSemaphore acquire_sem_ = VK_NULL_HANDLE; + VkSemaphore submit_sem_ = VK_NULL_HANDLE; + VkFence in_flight_ = VK_NULL_HANDLE; +}; +} // namespace + +std::unique_ptr create_vk_backend() +{ + return std::make_unique(); +} + +} // namespace coop::mock