#include "vulkan_bridge.h" #include #include #include #include #include #include #include #include #include #include #include namespace raven { #ifndef RAVEN_VULKAN_GENERATED // Without the registry at build time no command is marshaled beyond the hand-written ones. std::span vulkan_generated_names() { return {}; } bool vulkan_generated_is_device(std::string_view) { return false; } bool VulkanBridge::dispatch_generated(std::string_view) { return false; } void VulkanBridge::destroy_generic_objects(uint64_t) {} std::span vulkan_hand_names() { return {}; } bool VulkanBridge::dispatch_hand(std::string_view) { return false; } void VulkanBridge::remember_template(uint64_t, const VkDescriptorUpdateTemplateCreateInfo *) {} bool VulkanBridge::swapchain_stale(uint64_t) const { return false; } void VulkanBridge::release_foreign_swapchains(uint64_t) {} #endif // The export table of the guest provider is binary searched, so the names stay in byte order. static constexpr std::array names{ "vkCreateDevice", "vkCreateInstance", "vkDestroyDevice", "vkDestroyInstance", "vkDeviceWaitIdle", "vkEnumerateInstanceVersion", "vkEnumeratePhysicalDevices", "vkGetDeviceProcAddr", "vkGetDeviceQueue", "vkGetInstanceProcAddr", "vkGetPhysicalDeviceFeatures", "vkGetPhysicalDeviceMemoryProperties", "vkGetPhysicalDeviceProperties", "vkGetPhysicalDeviceQueueFamilyProperties" }; // Entry points reached through a device rather than an instance. static constexpr std::array device_names{"vkDestroyDevice", "vkGetDeviceQueue", "vkDeviceWaitIdle", "vkGetDeviceProcAddr"}; static bool is_device_function(std::string_view symbol) { const auto resources = vulkan_resource_names(), enumerate = vulkan_enumerate_names(); return std::find(device_names.begin(), device_names.end(), symbol) != device_names.end() || std::find(resources.begin(), resources.end(), symbol) != resources.end() || (std::find(enumerate.begin(), enumerate.end(), symbol) != enumerate.end() && symbol.find("Device") != std::string_view::npos) || vulkan_generated_is_device(symbol); } static_assert(sizeof(VkInstanceCreateInfo) == 64 && sizeof(VkApplicationInfo) == 48); static_assert(sizeof(VkDeviceCreateInfo) == 72 && sizeof(VkDeviceQueueCreateInfo) == 40 && sizeof(VkQueueFamilyProperties) == 24); static_assert(sizeof(VkPhysicalDeviceMemoryProperties) == 520 && sizeof(VkPhysicalDeviceFeatures) == 220); static_assert(sizeof(VkPhysicalDeviceProperties) == 824 && offsetof(VkPhysicalDeviceProperties, limits) == 296); std::span VulkanBridge::exports() { static const auto merged = [] { std::vector all(names.begin(), names.end()); for (const auto name : vulkan_resource_names()) all.push_back(name); for (const auto name : vulkan_enumerate_names()) all.push_back(name); for (const auto name : vulkan_query_names()) all.push_back(name); for (const auto name : vulkan_generated_names()) all.push_back(name); for (const auto name : vulkan_hand_names()) all.push_back(name); std::sort(all.begin(), all.end()); return all; }(); return merged; } template static H as_handle(uint64_t value) { return reinterpret_cast(value); } void VulkanBridge::destroy_object(const VkDevice &device, const Object &object) { switch (object.kind) { case Kind::shader: vkDestroyShaderModule(device, as_handle(object.handle), nullptr); break; case Kind::set_layout: vkDestroyDescriptorSetLayout(device, as_handle(object.handle), nullptr); break; case Kind::pipeline_layout: vkDestroyPipelineLayout(device, as_handle(object.handle), nullptr); break; case Kind::pipeline: vkDestroyPipeline(device, as_handle(object.handle), nullptr); break; case Kind::pool: vkDestroyDescriptorPool(device, as_handle(object.handle), nullptr); break; case Kind::set: break; case Kind::image: if (!object.borrowed) vkDestroyImage(device, as_handle(object.handle), nullptr); break; case Kind::image_view: vkDestroyImageView(device, as_handle(object.handle), nullptr); break; case Kind::semaphore: vkDestroySemaphore(device, as_handle(object.handle), nullptr); break; case Kind::render_pass: vkDestroyRenderPass(device, as_handle(object.handle), nullptr); break; case Kind::framebuffer: vkDestroyFramebuffer(device, as_handle(object.handle), nullptr); break; case Kind::sampler: vkDestroySampler(device, as_handle(object.handle), nullptr); break; } } // Host threads for driver calls that block: as many as there are calls in flight, kept for the ones that follow. struct VulkanBridge::Workers { std::mutex lock; std::condition_variable signal; std::deque> jobs; std::vector threads; size_t idle = 0; bool stopping = false; void run(std::function job) { std::lock_guard hold(lock); jobs.push_back(std::move(job)); if (jobs.size() > idle) threads.emplace_back([this] { loop(); }); signal.notify_one(); } void loop() { std::unique_lock hold(lock); for (;;) { ++idle; signal.wait(hold, [this] { return stopping || !jobs.empty(); }); --idle; if (stopping) return; auto job = std::move(jobs.front()); jobs.pop_front(); hold.unlock(); job(); hold.lock(); } } ~Workers() { { std::lock_guard hold(lock); stopping = true; } signal.notify_all(); for (auto &thread : threads) thread.join(); } }; VkResult VulkanBridge::detached(const std::function &work, std::atomic *cancel) { if (!park) return work(); if (!workers) workers = std::make_shared(); struct Call { VkResult status = VK_SUCCESS; std::atomic done{false}; }; const auto call = std::make_shared(); workers->run([call, &work, notify = wake] { call->status = work(); call->done.store(true, std::memory_order_release); if (notify) notify(); }); try { park([&call] { return call->done.load(std::memory_order_acquire); }); } catch (...) { if (cancel) cancel->store(true); // The driver reads what this frame decoded, so the frame stays until the call is back. while (!call->done.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::microseconds(100)); throw; } return call->status; } VkResult VulkanBridge::detached_wait(const std::function &wait, uint64_t timeout) { if (!park || timeout == 0) return wait(timeout); constexpr uint64_t slice = 50'000'000; std::atomic cancelled{false}; const auto start = std::chrono::steady_clock::now(); return detached([&] { for (;;) { const auto spent = static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); const uint64_t left = spent >= timeout ? 0 : timeout - spent; const VkResult status = wait(std::min(left, slice)); if (status != VK_TIMEOUT || left <= slice || cancelled.load()) return status; } }, &cancelled); } VulkanBridge::~VulkanBridge() { workers.reset(); while (!devices.empty()) destroy_device(devices.begin()->first); // Surfaces a program left behind belong to the instance that created them, if that instance still exists here. for (const auto &[token, window] : surface_windows) { if (surface_source.release) surface_source.release(window); const auto found = generic.find(token); const auto owner = surface_instances.find(token); if (found == generic.end() || owner == surface_instances.end() || !instances.contains(owner->second)) continue; const auto instance = instances.at(owner->second); if (const auto destroy = reinterpret_cast(vkGetInstanceProcAddr(instance, "vkDestroySurfaceKHR"))) destroy(instance, reinterpret_cast(found->second.host), nullptr); } for (const auto &[token, instance] : instances) vkDestroyInstance(instance, nullptr); } void VulkanBridge::destroy_device(uint64_t token) { const auto found = devices.find(token); if (found == devices.end()) return; std::erase_if(queues, [&](const auto &item) { return item.second.device == token; }); // Resources a guest left behind go before their device, in dependency order. vkDeviceWaitIdle(found->second.handle); release_foreign_swapchains(token); std::erase_if(command_buffers, [&](const auto &item) { return item.second.device == token; }); destroy_generic_objects(token); // Pipelines first, then what they were built from; descriptor sets go with their pool. for (const auto kind : {Kind::pipeline, Kind::framebuffer, Kind::render_pass, Kind::pool, Kind::pipeline_layout, Kind::set_layout, Kind::shader, Kind::image_view, Kind::sampler, Kind::image, Kind::semaphore}) for (auto item = objects.begin(); item != objects.end();) if (item->second.device == token && item->second.kind == kind) { destroy_object(found->second.handle, item->second); item = objects.erase(item); } else ++item; std::erase_if(objects, [&](const auto &item) { return item.second.device == token; }); for (auto item = pools.begin(); item != pools.end();) if (item->second.device == token) { vkDestroyCommandPool(found->second.handle, item->second.handle, nullptr); item = pools.erase(item); } else ++item; for (auto item = fences.begin(); item != fences.end();) if (item->second.device == token) { vkDestroyFence(found->second.handle, item->second.handle, nullptr); item = fences.erase(item); } else ++item; for (auto item = buffers.begin(); item != buffers.end();) if (item->second.device == token) { vkDestroyBuffer(found->second.handle, item->second.handle, nullptr); item = buffers.erase(item); } else ++item; for (auto item = memories.begin(); item != memories.end();) { if (item->second.device == token) { if (item->second.host) { vkUnmapMemory(found->second.handle, item->second.handle); drop_shadow(item->second); } vkFreeMemory(found->second.handle, item->second.handle, nullptr); item = memories.erase(item); } else ++item; } vkDestroyDevice(found->second.handle, nullptr); devices.erase(found); } std::string VulkanBridge::string(uint64_t address) { std::string result; for (size_t index = 0; index < 4096; ++index) { if (address > UINT64_MAX - index) break; const auto byte = process.read_memory(address + index, 1)[0]; if (!byte) return result; result.push_back(static_cast(byte)); } throw std::runtime_error("invalid Vulkan guest string"); } bool VulkanBridge::dispatch(std::string_view name) { if (dispatch_resource(name) || dispatch_enumerate(name) || dispatch_queries(name) || dispatch_hand(name) || dispatch_generated(name)) return true; if (std::find(names.begin(), names.end(), name) == names.end()) return false; const auto words = [&]() -> size_t { if (name == "vkEnumerateInstanceVersion" || name == "vkDeviceWaitIdle") return 1; if (name == "vkGetDeviceQueue" || name == "vkCreateDevice") return 4; if (name == "vkCreateInstance" || name == "vkEnumeratePhysicalDevices" || name == "vkGetPhysicalDeviceQueueFamilyProperties") return 3; return 2; }(); const auto arguments = process.api_arguments(words); const auto read = [&](uint64_t address) { T result{}; const auto bytes = process.read_memory(address, sizeof(T)); std::memcpy(&result, bytes.data(), sizeof(T)); return result; }; const auto write = [&](uint64_t address, const T &value) { process.write_memory(address, std::span(reinterpret_cast(&value), sizeof(T))); }; const auto writable = [&](uint64_t address, size_t size) { process.write_memory(address, process.read_memory(address, size)); }; uint64_t result = 0; if (name == "vkEnumerateInstanceVersion") { writable(arguments[0], 4); uint32_t version = 0; result = static_cast(vkEnumerateInstanceVersion(&version)); write(arguments[0], version); } else if (name == "vkGetInstanceProcAddr") { if (arguments[0] && !instances.contains(arguments[0])) throw std::runtime_error("invalid Vulkan instance handle"); const auto symbol = string(arguments[1]); const bool global = symbol == "vkCreateInstance" || symbol == "vkEnumerateInstanceVersion" || symbol == "vkGetInstanceProcAddr" || symbol == "vkEnumerateInstanceExtensionProperties" || symbol == "vkEnumerateInstanceLayerProperties"; const auto exported = VulkanBridge::exports(); const auto instance = arguments[0] ? instances.at(arguments[0]) : VK_NULL_HANDLE; const bool xcb = surface_source.platform == HostPlatform::xcb; const char *host_name = symbol == "vkCreateWin32SurfaceKHR" ? (xcb ? "vkCreateXcbSurfaceKHR" : "vkCreateWaylandSurfaceKHR") : symbol == "vkGetPhysicalDeviceWin32PresentationSupportKHR" ? (xcb ? "vkGetPhysicalDeviceXcbPresentationSupportKHR" : "vkGetPhysicalDeviceWaylandPresentationSupportKHR") : symbol.c_str(); if ((arguments[0] || global) && std::find(exported.begin(), exported.end(), symbol) != exported.end() && vkGetInstanceProcAddr(instance, host_name)) result = modules.resolve({"vulkan-1.dll", symbol, 0, false, 0}); if (std::getenv("RAVEN_TRACE_API") && !result) std::fprintf(stderr, "vulkan: unavailable instance function %s\n", symbol.c_str()); } else if (name == "vkGetDeviceProcAddr") { if (!devices.contains(arguments[0])) throw std::runtime_error("invalid Vulkan device handle"); const auto symbol = string(arguments[1]); const auto &module = *modules.all().at("vulkan-1.dll"); const bool exported = std::any_of(module.exports.begin(), module.exports.end(), [&](const auto &entry) { return std::find(entry.names.begin(), entry.names.end(), symbol) != entry.names.end(); }); // The Windows handle commands have no host counterpart; they are offered wherever the library exports them. const bool win32_handles = symbol == "vkGetMemoryWin32HandleKHR" || symbol == "vkGetSemaphoreWin32HandleKHR" || symbol == "vkImportSemaphoreWin32HandleKHR"; if (exported && ((is_device_function(symbol) && vkGetDeviceProcAddr(devices.at(arguments[0]).handle, symbol.c_str())) || win32_handles)) result = modules.resolve({"vulkan-1.dll", symbol, 0, false, 0}); if (std::getenv("RAVEN_TRACE_API") && !result) std::fprintf(stderr, "vulkan: unavailable device function %s\n", symbol.c_str()); } else if (name == "vkCreateInstance") { auto info = read.template operator()(arguments[0]); if (arguments[1] || info.pNext || info.sType != VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO || info.flags || info.enabledLayerCount || info.enabledExtensionCount > 32) throw std::runtime_error("Vulkan allocator, pNext, flags or layers are not implemented"); std::vector guest_extensions, host_extensions; for (uint32_t index = 0; index < info.enabledExtensionCount; ++index) guest_extensions.push_back(string(read.template operator()(reinterpret_cast(info.ppEnabledExtensionNames) + uint64_t(index) * 8))); if (!host_extension_names(guest_extensions, false, host_extensions)) throw std::runtime_error("Vulkan instance extension is not offered"); std::vector host_pointers; for (const auto &extension : host_extensions) host_pointers.push_back(extension.c_str()); VkApplicationInfo application{}; std::string application_name, engine_name; if (info.pApplicationInfo) { application = read.template operator()(reinterpret_cast(info.pApplicationInfo)); if (application.pNext || application.sType != VK_STRUCTURE_TYPE_APPLICATION_INFO) throw std::runtime_error("unsupported Vulkan application info"); if (application.pApplicationName) application_name = string(reinterpret_cast(application.pApplicationName)); if (application.pEngineName) engine_name = string(reinterpret_cast(application.pEngineName)); application.pApplicationName = application.pApplicationName ? application_name.c_str() : nullptr; application.pEngineName = application.pEngineName ? engine_name.c_str() : nullptr; info.pApplicationInfo = &application; } info.ppEnabledLayerNames = nullptr; info.enabledExtensionCount = uint32_t(host_pointers.size()); info.ppEnabledExtensionNames = host_pointers.empty() ? nullptr : host_pointers.data(); writable(arguments[2], 8); VkInstance instance = VK_NULL_HANDLE; const auto status = vkCreateInstance(&info, nullptr, &instance); result = static_cast(status); if (status == VK_SUCCESS) { const auto token = next_handle++; try { instances.emplace(token, instance); write(arguments[2], token); } catch (...) { instances.erase(token); vkDestroyInstance(instance, nullptr); throw; } } } else if (name == "vkDestroyInstance") { if (arguments[1]) throw std::runtime_error("Vulkan guest allocation callbacks are not implemented"); if (arguments[0]) { const auto found = instances.find(arguments[0]); if (found == instances.end()) throw std::runtime_error("invalid Vulkan instance handle"); // A device that outlives its instance would be undefined behaviour in the host driver. std::vector owned; for (const auto &[token, device] : devices) if (physical.contains(device.physical) && physical.at(device.physical).instance == arguments[0]) owned.push_back(token); for (const auto token : owned) destroy_device(token); vkDestroyInstance(found->second, nullptr); instances.erase(found); std::erase_if(physical, [&](const auto &item) { return item.second.instance == arguments[0]; }); } } else if (name == "vkEnumeratePhysicalDevices") { const auto found = instances.find(arguments[0]); if (found == instances.end()) throw std::runtime_error("invalid Vulkan instance handle"); writable(arguments[1], 4); uint32_t count = 0; if (arguments[2]) { count = read.template operator()(arguments[1]); if (count > 4096) throw std::runtime_error("Vulkan physical device capacity exceeds limit"); writable(arguments[2], size_t(count) * 8); } std::vector devices(count); VkPhysicalDevice zero_capacity = VK_NULL_HANDLE; const auto status = vkEnumeratePhysicalDevices(found->second, &count, arguments[2] ? (devices.empty() ? &zero_capacity : devices.data()) : nullptr); result = static_cast(status); if (status == VK_SUCCESS || status == VK_INCOMPLETE) { if (arguments[2]) { for (size_t index = 0; index < count; ++index) { const auto existing = std::find_if(physical.begin(), physical.end(), [&](const auto &item) { return item.second.instance == arguments[0] && item.second.handle == devices.at(index); }); uint64_t token = existing == physical.end() ? next_handle++ : existing->first; if (existing == physical.end()) physical.emplace(token, Physical{devices.at(index), arguments[0]}); write(arguments[2] + index * 8, token); } } write(arguments[1], count); } } else if (name == "vkGetPhysicalDeviceMemoryProperties" || name == "vkGetPhysicalDeviceFeatures") { const auto found = physical.find(arguments[0]); if (found == physical.end()) throw std::runtime_error("invalid Vulkan physical device handle"); if (name == "vkGetPhysicalDeviceMemoryProperties") { writable(arguments[1], sizeof(VkPhysicalDeviceMemoryProperties)); VkPhysicalDeviceMemoryProperties memory; std::memset(&memory, 0, sizeof(memory)); vkGetPhysicalDeviceMemoryProperties(found->second.handle, &memory); write(arguments[1], memory); } else { writable(arguments[1], sizeof(VkPhysicalDeviceFeatures)); VkPhysicalDeviceFeatures features; std::memset(&features, 0, sizeof(features)); vkGetPhysicalDeviceFeatures(found->second.handle, &features); write(arguments[1], features); } } else if (name == "vkGetPhysicalDeviceQueueFamilyProperties") { const auto found = physical.find(arguments[0]); if (found == physical.end()) throw std::runtime_error("invalid Vulkan physical device handle"); writable(arguments[1], 4); uint32_t count = 0; if (arguments[2]) { count = read.template operator()(arguments[1]); if (count > 4096) throw std::runtime_error("Vulkan queue family capacity exceeds limit"); writable(arguments[2], size_t(count) * sizeof(VkQueueFamilyProperties)); } std::vector families(count); vkGetPhysicalDeviceQueueFamilyProperties(found->second.handle, &count, arguments[2] ? families.data() : nullptr); if (arguments[2]) for (size_t index = 0; index < count && index < families.size(); ++index) write(arguments[2] + index * sizeof(VkQueueFamilyProperties), families[index]); write(arguments[1], count); } else if (name == "vkCreateDevice") { const auto found = physical.find(arguments[0]); if (found == physical.end()) throw std::runtime_error("invalid Vulkan physical device handle"); if (arguments[2]) throw std::runtime_error("Vulkan guest allocation callbacks are not implemented"); auto info = read.template operator()(arguments[1]); if (info.sType != VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO || info.flags || info.enabledLayerCount || info.enabledExtensionCount > 96 || !info.queueCreateInfoCount || info.queueCreateInfoCount > 16) throw std::runtime_error("Vulkan device flags or layers are not implemented"); // Feature structures chained to the create info are plain blocks of bytes that travel to the driver unchanged. auto device_chain = mirror_chain(reinterpret_cast(info.pNext), true); info.pNext = link_chain(device_chain); std::vector guest_extensions, host_extensions; for (uint32_t index = 0; index < info.enabledExtensionCount; ++index) guest_extensions.push_back(string(read.template operator()(reinterpret_cast(info.ppEnabledExtensionNames) + uint64_t(index) * 8))); if (!host_extension_names(guest_extensions, true, host_extensions)) throw std::runtime_error("Vulkan device extension is not offered"); std::vector host_pointers; for (const auto &extension : host_extensions) host_pointers.push_back(extension.c_str()); std::vector queue_infos(info.queueCreateInfoCount); std::vector> priorities(info.queueCreateInfoCount); std::vector queue_chains; queue_chains.reserve(info.queueCreateInfoCount); for (uint32_t index = 0; index < info.queueCreateInfoCount; ++index) { queue_infos[index] = read.template operator()( reinterpret_cast(info.pQueueCreateInfos) + uint64_t(index) * sizeof(VkDeviceQueueCreateInfo)); auto &queue = queue_infos[index]; if (queue.sType != VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO || !queue.queueCount || queue.queueCount > 64) throw std::runtime_error("unsupported Vulkan queue create info"); // A chain on the queue's create info, such as a global priority request, travels like the device's own. if (queue.pNext) { queue_chains.push_back(mirror_chain(reinterpret_cast(queue.pNext), true)); queue.pNext = link_chain(queue_chains.back()); } priorities[index].resize(queue.queueCount); const auto bytes = process.read_memory(reinterpret_cast(queue.pQueuePriorities), size_t(queue.queueCount) * 4); std::memcpy(priorities[index].data(), bytes.data(), bytes.size()); queue.pQueuePriorities = priorities[index].data(); } VkPhysicalDeviceFeatures features{}; if (info.pEnabledFeatures) { features = read.template operator()(reinterpret_cast(info.pEnabledFeatures)); info.pEnabledFeatures = &features; } info.pQueueCreateInfos = queue_infos.data(); info.ppEnabledLayerNames = nullptr; info.enabledExtensionCount = uint32_t(host_pointers.size()); info.ppEnabledExtensionNames = host_pointers.empty() ? nullptr : host_pointers.data(); writable(arguments[3], 8); VkDevice device = VK_NULL_HANDLE; const auto status = vkCreateDevice(found->second.handle, &info, nullptr, &device); result = static_cast(status); if (status == VK_SUCCESS) { const auto token = next_handle++; Device record{device, arguments[0], {}}; try { devices.emplace(token, record); write(arguments[3], token); } catch (...) { devices.erase(token); vkDestroyDevice(device, nullptr); throw; } } } else if (name == "vkDestroyDevice") { if (arguments[1]) throw std::runtime_error("Vulkan guest allocation callbacks are not implemented"); if (arguments[0]) { if (!devices.contains(arguments[0])) throw std::runtime_error("invalid Vulkan device handle"); destroy_device(arguments[0]); } } else if (name == "vkGetDeviceQueue") { const auto found = devices.find(arguments[0]); if (found == devices.end()) throw std::runtime_error("invalid Vulkan device handle"); writable(arguments[3], 8); VkQueue handle = VK_NULL_HANDLE; vkGetDeviceQueue(found->second.handle, uint32_t(arguments[1]), uint32_t(arguments[2]), &handle); if (!handle) throw std::runtime_error("Vulkan queue was not requested at device creation"); const auto existing = std::find_if(queues.begin(), queues.end(), [&](const auto &item) { return item.second.device == arguments[0] && item.second.family == uint32_t(arguments[1]) && item.second.index == uint32_t(arguments[2]); }); uint64_t token = existing == queues.end() ? next_handle++ : existing->first; if (existing == queues.end()) queues.emplace(token, Queue{handle, arguments[0], uint32_t(arguments[1]), uint32_t(arguments[2])}); write(arguments[3], token); } else if (name == "vkDeviceWaitIdle") { const auto found = devices.find(arguments[0]); if (found == devices.end()) throw std::runtime_error("invalid Vulkan device handle"); const auto status = vkDeviceWaitIdle(found->second.handle); result = static_cast(status); if (status == VK_SUCCESS) shadow_from_device(arguments[0]); } else if (name == "vkGetPhysicalDeviceProperties") { const auto found = physical.find(arguments[0]); if (found == physical.end()) throw std::runtime_error("invalid Vulkan physical device handle"); writable(arguments[1], sizeof(VkPhysicalDeviceProperties)); VkPhysicalDeviceProperties properties; std::memset(&properties, 0, sizeof(properties)); vkGetPhysicalDeviceProperties(found->second.handle, &properties); write(arguments[1], properties); } process.return_api(result); return true; } }