mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 01:00:04 +02:00
241 lines
13 KiB
C++
241 lines
13 KiB
C++
// 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 <algorithm>
|
|
#include <array>
|
|
#include <bit>
|
|
#include <cassert>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
|
|
namespace {
|
|
template<class T> T handle(uintptr_t value) { return std::bit_cast<T>(value); }
|
|
std::set<uintptr_t> live;
|
|
uintptr_t next_handle = 10;
|
|
unsigned calls = 0, fail_at = 0, images = 0, submits = 0, flushes = 0;
|
|
bool coherent = true;
|
|
std::map<VkDeviceMemory, std::vector<unsigned char>> allocations;
|
|
VkDeviceMemory image_memory{}, buffer_memory{};
|
|
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
std::vector<std::function<void()>> recorded, pending;
|
|
std::vector<std::vector<unsigned char>> observed;
|
|
VkResult result() { return ++calls == fail_at ? VK_ERROR_OUT_OF_DEVICE_MEMORY : VK_SUCCESS; }
|
|
template<class T> VkResult create(T* out) {
|
|
auto status = result();
|
|
if (status == VK_SUCCESS) { *out = handle<T>(next_handle++); live.insert(std::bit_cast<uintptr_t>(*out)); }
|
|
return status;
|
|
}
|
|
template<class T> void destroy(T resource) { assert(live.erase(std::bit_cast<uintptr_t>(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<std::string> instance_extensions{"VK_KHR_external_memory_capabilities"};
|
|
auto select_device = [](VkInstance instance) {
|
|
assert(live.contains(std::bit_cast<uintptr_t>(instance)));
|
|
return handle<VkPhysicalDevice>(2);
|
|
};
|
|
auto device_extensions = [](VkPhysicalDevice physical) {
|
|
assert(physical == handle<VkPhysicalDevice>(2));
|
|
return std::vector<std::string>{"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<vr::VRVulkanTextureData_t*>(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<VkPhysicalDevice>(2));
|
|
assert(data.m_pQueue == handle<VkQueue>(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<class F> 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<VkPhysicalDevice>(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<VkQueue>(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<VkCommandBuffer>(4); return status;
|
|
}
|
|
VKAPI_ATTR VkResult VKAPI_CALL vkQueueWaitIdle(VkQueue queue) {
|
|
assert(queue == handle<VkQueue>(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<VkQueue>(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<unsigned char, 16> first{255,0,0,255, 0,255,0,128, 0,0,255,0, 255,255,255,255};
|
|
const std::array<unsigned char, 16> 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<vr::VRVulkanTextureData_t*>(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<vr::VRVulkanTextureData_t*>(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
|
|
}
|