// Link against these Vulkan fakes, never the loader. CTest must not touch a GPU // or initialize OpenVR. Model queued transfers so reuse before completion fails. #include "overlay_texture.hpp" #include #include #include #include #include #include #include #include namespace { template T handle(uintptr_t value) { return std::bit_cast(value); } std::set live; uintptr_t next_handle = 10; unsigned calls = 0, fail_at = 0, images = 0, submits = 0, flushes = 0; bool coherent = true; std::map> allocations; VkDeviceMemory image_memory{}, buffer_memory{}; VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED; std::vector> recorded, pending; std::vector> observed; VkResult result() { return ++calls == fail_at ? VK_ERROR_OUT_OF_DEVICE_MEMORY : VK_SUCCESS; } template VkResult create(T* out) { auto status = result(); if (status == VK_SUCCESS) { *out = handle(next_handle++); live.insert(std::bit_cast(*out)); } return status; } template void destroy(T resource) { assert(live.erase(std::bit_cast(resource)) == 1); } void complete() { for (auto& work : pending) work(); pending.clear(); } void reset() { assert(live.empty() && allocations.empty() && pending.empty()); calls = images = submits = flushes = 0; recorded.clear(); observed.clear(); layout = VK_IMAGE_LAYOUT_UNDEFINED; } const std::vector instance_extensions{"VK_KHR_external_memory_capabilities"}; auto select_device = [](VkInstance instance) { assert(live.contains(std::bit_cast(instance))); return handle(2); }; auto device_extensions = [](VkPhysicalDevice physical) { assert(physical == handle(2)); return std::vector{"VK_KHR_external_memory"}; }; void consume(frameyap::OverlayTexture& texture) { const auto t = texture.texture(); assert(t.eType == vr::TextureType_Vulkan && t.eColorSpace == vr::ColorSpace_Gamma); const auto& data = *static_cast(t.handle); assert(data.m_nWidth == 2 && data.m_nHeight == 2 && data.m_nSampleCount == 1); assert(data.m_nFormat == VK_FORMAT_R8G8B8A8_UNORM && data.m_nQueueFamilyIndex == 1); assert(data.m_pPhysicalDevice == handle(2)); assert(data.m_pQueue == handle(3)); assert(live.contains(data.m_nImage)); // Emulate SetOverlayTexture enqueuing a read AFTER our upload, on our queue. pending.push_back([] { assert(layout == VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL); auto& bytes = allocations.at(image_memory); observed.emplace_back(bytes.begin(), bytes.begin() + 16); }); } template void throws(F f) { bool threw = false; try { f(); } catch (const std::runtime_error&) { threw = true; } assert(threw); } } // namespace extern "C" { VKAPI_ATTR VkResult VKAPI_CALL vkCreateInstance(const VkInstanceCreateInfo* info, const VkAllocationCallbacks*, VkInstance* out) { assert(info->enabledExtensionCount == 1); assert(std::string(info->ppEnabledExtensionNames[0]) == instance_extensions[0]); return create(out); } VKAPI_ATTR void VKAPI_CALL vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks*) { assert(live.size() == 1); destroy(instance); } VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceProperties(VkPhysicalDevice, VkPhysicalDeviceProperties* properties) { properties->limits.maxImageDimension2D = 4096; } VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceQueueFamilyProperties(VkPhysicalDevice, uint32_t* count, VkQueueFamilyProperties* out) { if (!out) { *count = 2; return; } assert(*count == 2); out[0] = {}; out[0].queueFlags = VK_QUEUE_TRANSFER_BIT; out[0].queueCount = 1; out[1] = {}; out[1].queueFlags = VK_QUEUE_GRAPHICS_BIT; out[1].queueCount = 1; } VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(VkPhysicalDevice physical, const VkDeviceCreateInfo* info, const VkAllocationCallbacks*, VkDevice* out) { assert(physical == handle(2)); assert(info->queueCreateInfoCount == 1 && info->pQueueCreateInfos->queueFamilyIndex == 1); assert(info->enabledExtensionCount == 1); assert(std::string(info->ppEnabledExtensionNames[0]) == "VK_KHR_external_memory"); return create(out); } VKAPI_ATTR void VKAPI_CALL vkDestroyDevice(VkDevice device, const VkAllocationCallbacks*) { assert(live.size() == 2 && allocations.empty() && pending.empty()); destroy(device); } VKAPI_ATTR VkResult VKAPI_CALL vkDeviceWaitIdle(VkDevice) { complete(); return VK_SUCCESS; } VKAPI_ATTR void VKAPI_CALL vkGetDeviceQueue(VkDevice, uint32_t family, uint32_t index, VkQueue* queue) { assert(family == 1 && index == 0); *queue = handle(3); } VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties* memory) { memory->memoryTypeCount = 2; memory->memoryTypes[0].propertyFlags = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT; memory->memoryTypes[1].propertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | (coherent ? VK_MEMORY_PROPERTY_HOST_COHERENT_BIT : 0); } VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(VkDevice, const VkImageCreateInfo* info, const VkAllocationCallbacks*, VkImage* out) { assert(info->format == VK_FORMAT_R8G8B8A8_UNORM); assert(info->extent.width == 2 && info->extent.height == 2 && info->extent.depth == 1); assert(info->mipLevels == 1 && info->arrayLayers == 1 && info->samples == VK_SAMPLE_COUNT_1_BIT); assert(info->tiling == VK_IMAGE_TILING_OPTIMAL); assert(info->usage == (VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT)); ++images; return create(out); } VKAPI_ATTR void VKAPI_CALL vkDestroyImage(VkDevice, VkImage image, const VkAllocationCallbacks*) { destroy(image); } VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements(VkDevice, VkImage, VkMemoryRequirements* requirements) { *requirements = {256, 256, 1}; } VKAPI_ATTR VkResult VKAPI_CALL vkAllocateMemory(VkDevice, const VkMemoryAllocateInfo* info, const VkAllocationCallbacks*, VkDeviceMemory* out) { auto status = create(out); if (status == VK_SUCCESS) allocations[*out].resize(info->allocationSize); return status; } VKAPI_ATTR void VKAPI_CALL vkFreeMemory(VkDevice, VkDeviceMemory memory, const VkAllocationCallbacks*) { assert(allocations.erase(memory) == 1); destroy(memory); } VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory(VkDevice, VkImage, VkDeviceMemory memory, VkDeviceSize) { image_memory = memory; return result(); } VKAPI_ATTR VkResult VKAPI_CALL vkCreateBuffer(VkDevice, const VkBufferCreateInfo* info, const VkAllocationCallbacks*, VkBuffer* out) { assert(info->size == 16 && info->usage == VK_BUFFER_USAGE_TRANSFER_SRC_BIT); return create(out); } VKAPI_ATTR void VKAPI_CALL vkDestroyBuffer(VkDevice, VkBuffer buffer, const VkAllocationCallbacks*) { destroy(buffer); } VKAPI_ATTR void VKAPI_CALL vkGetBufferMemoryRequirements(VkDevice, VkBuffer, VkMemoryRequirements* requirements) { *requirements = {256, 256, 2}; } VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(VkDevice, VkBuffer, VkDeviceMemory memory, VkDeviceSize) { buffer_memory = memory; return result(); } VKAPI_ATTR VkResult VKAPI_CALL vkMapMemory(VkDevice, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize size, VkMemoryMapFlags, void** out) { assert(offset == 0 && size == VK_WHOLE_SIZE); auto status = result(); if (status == VK_SUCCESS) *out = allocations.at(memory).data(); return status; } VKAPI_ATTR void VKAPI_CALL vkUnmapMemory(VkDevice, VkDeviceMemory memory) { assert(allocations.contains(memory)); } VKAPI_ATTR VkResult VKAPI_CALL vkFlushMappedMemoryRanges(VkDevice, uint32_t count, const VkMappedMemoryRange* range) { assert(!coherent && count == 1 && range->offset == 0 && range->size == VK_WHOLE_SIZE); assert(range->memory == buffer_memory); ++flushes; return result(); } VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool(VkDevice, const VkCommandPoolCreateInfo* info, const VkAllocationCallbacks*, VkCommandPool* out) { assert(info->queueFamilyIndex == 1); return create(out); } VKAPI_ATTR void VKAPI_CALL vkDestroyCommandPool(VkDevice, VkCommandPool pool, const VkAllocationCallbacks*) { destroy(pool); } VKAPI_ATTR VkResult VKAPI_CALL vkAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* out) { auto status = result(); if (status == VK_SUCCESS) *out = handle(4); return status; } VKAPI_ATTR VkResult VKAPI_CALL vkQueueWaitIdle(VkQueue queue) { assert(queue == handle(3)); auto status = result(); if (status == VK_SUCCESS) complete(); return status; } VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(VkCommandBuffer, VkCommandBufferResetFlags) { assert(pending.empty()); recorded.clear(); return result(); } VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return result(); } VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier(VkCommandBuffer, VkPipelineStageFlags, VkPipelineStageFlags, VkDependencyFlags, uint32_t, const VkMemoryBarrier*, uint32_t, const VkBufferMemoryBarrier*, uint32_t count, const VkImageMemoryBarrier* barriers) { assert(count == 1); const auto b = barriers[0]; assert(b.srcQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED && b.dstQueueFamilyIndex == VK_QUEUE_FAMILY_IGNORED); assert(b.subresourceRange.aspectMask == VK_IMAGE_ASPECT_COLOR_BIT); assert(b.subresourceRange.levelCount == 1 && b.subresourceRange.layerCount == 1); recorded.push_back([b] { assert(layout == b.oldLayout); layout = b.newLayout; }); } VKAPI_ATTR void VKAPI_CALL vkCmdCopyBufferToImage(VkCommandBuffer, VkBuffer, VkImage, VkImageLayout target, uint32_t count, const VkBufferImageCopy* copy) { assert(count == 1 && target == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); assert(copy->bufferRowLength == 0 && copy->bufferImageHeight == 0 && copy->bufferOffset == 0); assert(copy->imageExtent.width == 2 && copy->imageExtent.height == 2 && copy->imageExtent.depth == 1); recorded.push_back([] { assert(layout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); std::copy_n(allocations.at(buffer_memory).begin(), 16, allocations.at(image_memory).begin()); }); } VKAPI_ATTR VkResult VKAPI_CALL vkEndCommandBuffer(VkCommandBuffer) { return result(); } VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(VkQueue queue, uint32_t count, const VkSubmitInfo* submit, VkFence) { assert(queue == handle(3) && count == 1 && submit->commandBufferCount == 1); auto status = result(); if (status == VK_SUCCESS) { pending.insert(pending.end(), recorded.begin(), recorded.end()); ++submits; } return status; } } // extern C int main() { const std::array first{255,0,0,255, 0,255,0,128, 0,0,255,0, 255,255,255,255}; const std::array second{0,0,0,0, 4,5,6,7, 8,9,10,11, 12,13,14,15}; for (bool use_coherent : {true, false}) { reset(); coherent = use_coherent; { frameyap::OverlayTexture texture(2, 2, instance_extensions, select_device, device_extensions); throws([&] { texture.texture(); }); throws([&] { texture.upload(std::span(first).first(15)); }); texture.upload(first); const auto descriptor = texture.texture(); const auto image = static_cast(descriptor.handle)->m_nImage; consume(texture); texture.upload(second); // must drain first read before replacing staging bytes assert(observed.size() == 1 && std::ranges::equal(observed[0], first)); assert(texture.texture().handle == descriptor.handle); assert(static_cast(descriptor.handle)->m_nImage == image); consume(texture); assert(images == 1 && submits == 2 && flushes == (coherent ? 0u : 2u)); } assert(observed.size() == 2 && std::ranges::equal(observed[1], second)); assert(live.empty() && allocations.empty()); const auto operation_count = calls; // Every fallible allocation/upload operation must unwind all owned // resources, including a failure while a previous frame is pending. for (unsigned failure = 1; failure <= operation_count; ++failure) { reset(); fail_at = failure; throws([&] { frameyap::OverlayTexture texture(2, 2, instance_extensions, select_device, device_extensions); texture.upload(first); consume(texture); texture.upload(second); consume(texture); }); assert(live.empty() && allocations.empty() && pending.empty()); } fail_at = 0; } reset(); throws([&] { frameyap::OverlayTexture texture(0, 2, instance_extensions, select_device, device_extensions); }); assert(calls == 0); // invalid sizes must not initialize Vulkan throws([&] { frameyap::OverlayTexture texture(2, 2, instance_extensions, [](VkInstance) { return VkPhysicalDevice{}; }, device_extensions); }); assert(live.empty()); // never pick an unrelated GPU when SteamVR returns none }