mirror of
https://github.com/mirkobrombin/raven.git
synced 2026-10-30 21:35:06 +00:00
328 lines
12 KiB
C++
328 lines
12 KiB
C++
#include "vulkan_bridge.h"
|
|
#include "shared_gpu_memory.h"
|
|
#include "nt/runtime_profile.h"
|
|
#include <chrono>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <stdexcept>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
namespace raven {
|
|
|
|
// Names are kept in byte order because the guest provider binary searches its export table.
|
|
static constexpr std::array<std::string_view, 8> names{
|
|
"vkAllocateCommandBuffers", "vkDestroyCommandPool", "vkFlushMappedMemoryRanges", "vkFreeCommandBuffers",
|
|
"vkFreeMemory", "vkInvalidateMappedMemoryRanges", "vkMapMemory", "vkUnmapMemory"
|
|
};
|
|
|
|
std::span<const std::string_view> vulkan_resource_names()
|
|
{
|
|
static const auto sorted = [] {
|
|
std::array<std::string_view, 8> copy = names;
|
|
std::sort(copy.begin(), copy.end());
|
|
return copy;
|
|
}();
|
|
return sorted;
|
|
}
|
|
|
|
void VulkanBridge::check_writable(uint64_t address, size_t size) const
|
|
{
|
|
process.write_memory(address, process.read_memory(address, size));
|
|
}
|
|
|
|
uint64_t VulkanBridge::make_shadow(Memory &memory, size_t size)
|
|
{
|
|
const auto rounded = (size + 4095) & ~size_t(4095);
|
|
const int file = memfd_create("raven-vulkan-shadow", MFD_CLOEXEC);
|
|
if (file < 0 || ftruncate(file, off_t(rounded)) != 0) {
|
|
if (file >= 0)
|
|
close(file);
|
|
throw std::runtime_error("cannot create a shared Vulkan memory shadow");
|
|
}
|
|
void *view = mmap(nullptr, rounded, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0);
|
|
if (view == MAP_FAILED) {
|
|
close(file);
|
|
throw std::runtime_error("cannot map a shared Vulkan memory shadow");
|
|
}
|
|
uint64_t guest = 0;
|
|
try {
|
|
guest = process.map_shared(file, rounded);
|
|
} catch (...) {
|
|
munmap(view, rounded);
|
|
close(file);
|
|
throw;
|
|
}
|
|
memory.shadow_file = file;
|
|
memory.mapping_base = guest;
|
|
memory.view = static_cast<uint8_t *>(view);
|
|
memory.view_size = rounded;
|
|
return guest;
|
|
}
|
|
|
|
void VulkanBridge::drop_shadow(Memory &memory)
|
|
{
|
|
if (memory.mapping_base)
|
|
process.unmap_memory(memory.mapping_base);
|
|
if (memory.view)
|
|
munmap(memory.view, memory.view_size);
|
|
if (memory.shadow_file >= 0)
|
|
close(memory.shadow_file);
|
|
memory.shadow = 0;
|
|
memory.mapping_base = 0;
|
|
memory.view = nullptr;
|
|
memory.view_size = 0;
|
|
memory.shadow_file = -1;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Calls visit(first, last) for each run of bytes where a and b differ, comparing a word at a time and looking at single bytes only inside
|
|
// a word that differs.
|
|
template<typename Visit> void each_difference(const uint8_t *a, const uint8_t *b, size_t size, Visit visit)
|
|
{
|
|
size_t index = 0;
|
|
while (index < size) {
|
|
// Whole blocks that are equal (nearly all of a large mapping) are skipped with the library's vectorised comparison.
|
|
if (index % 4096 == 0 && index + 4096 <= size && std::memcmp(a + index, b + index, 4096) == 0) {
|
|
index += 4096;
|
|
continue;
|
|
}
|
|
if (index + 8 <= size) {
|
|
uint64_t left, right;
|
|
std::memcpy(&left, a + index, 8);
|
|
std::memcpy(&right, b + index, 8);
|
|
if (left == right) {
|
|
index += 8;
|
|
continue;
|
|
}
|
|
}
|
|
const size_t limit = std::min(size, index + 8);
|
|
for (; index < limit; ++index)
|
|
if (a[index] != b[index])
|
|
visit(index);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void VulkanBridge::shadow_to_device(uint64_t device)
|
|
{
|
|
const auto started = std::chrono::steady_clock::now();
|
|
struct Timed {
|
|
std::chrono::steady_clock::time_point started;
|
|
~Timed() { if (RuntimeProfile::instance().enabled()) RuntimeProfile::instance().api("(shadow to device)", std::chrono::steady_clock::now() - started); }
|
|
} timed{started};
|
|
for (auto &[token, memory] : memories) {
|
|
if (memory.device != device || !memory.host || !memory.view)
|
|
continue;
|
|
auto *host = static_cast<uint8_t *>(memory.host);
|
|
each_difference(memory.view, memory.snapshot.data(), size_t(memory.mapped), [&](size_t index) {
|
|
host[index] = memory.view[index];
|
|
memory.snapshot[index] = memory.view[index];
|
|
});
|
|
}
|
|
}
|
|
|
|
void VulkanBridge::shadow_from_device(uint64_t device)
|
|
{
|
|
const auto started = std::chrono::steady_clock::now();
|
|
struct Timed {
|
|
std::chrono::steady_clock::time_point started;
|
|
~Timed() { if (RuntimeProfile::instance().enabled()) RuntimeProfile::instance().api("(shadow from device)", std::chrono::steady_clock::now() - started); }
|
|
} timed{started};
|
|
for (auto &[token, memory] : memories) {
|
|
if (memory.device != device || !memory.host || !memory.view)
|
|
continue;
|
|
const auto *host = static_cast<const uint8_t *>(memory.host);
|
|
each_difference(host, memory.snapshot.data(), size_t(memory.mapped), [&](size_t index) {
|
|
memory.view[index] = host[index];
|
|
memory.snapshot[index] = host[index];
|
|
});
|
|
}
|
|
}
|
|
|
|
bool VulkanBridge::dispatch_resource(std::string_view name)
|
|
{
|
|
if (std::find(names.begin(), names.end(), name) == names.end())
|
|
return false;
|
|
static const std::map<std::string_view, size_t, std::less<>> words{
|
|
{"vkAllocateCommandBuffers", 3}, {"vkDestroyCommandPool", 3}, {"vkFlushMappedMemoryRanges", 3},
|
|
{"vkFreeCommandBuffers", 4}, {"vkFreeMemory", 3}, {"vkInvalidateMappedMemoryRanges", 3}, {"vkMapMemory", 6}, {"vkUnmapMemory", 2}};
|
|
auto a = process.api_arguments(words.at(name));
|
|
// A 32-bit argument leaves the upper half of its register or stack slot undefined, so those are narrowed before use.
|
|
static const std::map<std::string_view, std::vector<size_t>, std::less<>> narrow{
|
|
{"vkMapMemory", {4}}, {"vkFlushMappedMemoryRanges", {1}}, {"vkInvalidateMappedMemoryRanges", {1}}, {"vkFreeCommandBuffers", {2}}};
|
|
if (const auto found = narrow.find(name); found != narrow.end())
|
|
for (const auto index : found->second)
|
|
a[index] = uint32_t(a[index]);
|
|
const auto device_of = [&](uint64_t token) -> Device & {
|
|
const auto found = devices.find(token);
|
|
if (found == devices.end())
|
|
throw std::runtime_error("invalid Vulkan device handle");
|
|
return found->second;
|
|
};
|
|
const auto no_allocator = [&](uint64_t pointer) {
|
|
if (pointer)
|
|
throw std::runtime_error("Vulkan guest allocation callbacks are not implemented");
|
|
};
|
|
const auto memory_of = [&](uint64_t token, uint64_t device) -> Memory & {
|
|
const auto found = memories.find(token);
|
|
if (found == memories.end() || found->second.device != device)
|
|
throw std::runtime_error("invalid Vulkan memory handle");
|
|
return found->second;
|
|
};
|
|
const auto buffer_of = [&](uint64_t token, uint64_t device) -> Buffer & {
|
|
const auto found = buffers.find(token);
|
|
if (found == buffers.end() || found->second.device != device)
|
|
throw std::runtime_error("invalid Vulkan buffer handle");
|
|
return found->second;
|
|
};
|
|
const auto pool_of = [&](uint64_t token, uint64_t device) -> Pool & {
|
|
const auto found = pools.find(token);
|
|
if (found == pools.end() || found->second.device != device)
|
|
throw std::runtime_error("invalid Vulkan command pool handle");
|
|
return found->second;
|
|
};
|
|
const auto command_of = [&](uint64_t token) -> CommandBuffer & {
|
|
const auto found = command_buffers.find(token);
|
|
if (found == command_buffers.end())
|
|
throw std::runtime_error("invalid Vulkan command buffer handle");
|
|
return found->second;
|
|
};
|
|
const auto fence_of = [&](uint64_t token, uint64_t device) -> Fence & {
|
|
const auto found = fences.find(token);
|
|
if (found == fences.end() || found->second.device != device)
|
|
throw std::runtime_error("invalid Vulkan fence handle");
|
|
return found->second;
|
|
};
|
|
uint64_t result = 0;
|
|
if (name == "vkFreeMemory") {
|
|
auto &device = device_of(a[0]);
|
|
no_allocator(a[2]);
|
|
if (a[1]) {
|
|
auto &memory = memory_of(a[1], a[0]);
|
|
if (memory.host) {
|
|
vkUnmapMemory(device.handle, memory.handle);
|
|
drop_shadow(memory);
|
|
}
|
|
vkFreeMemory(device.handle, memory.handle, nullptr);
|
|
memories.erase(a[1]);
|
|
}
|
|
} else if (name == "vkMapMemory") {
|
|
auto &device = device_of(a[0]);
|
|
auto &memory = memory_of(a[1], a[0]);
|
|
const uint64_t offset = a[2], size = a[3] == VK_WHOLE_SIZE ? memory.size - std::min<uint64_t>(offset, memory.size) : a[3];
|
|
if (a[4] || memory.host || offset >= memory.size || !size || size > memory.size - offset || size > (256ULL << 20))
|
|
throw std::runtime_error("unsupported Vulkan memory mapping (offset " + std::to_string(offset) + ", size " + std::to_string(size) +
|
|
", memory " + std::to_string(memory.size) + (memory.host ? ", already mapped" : "") + ", flags " + std::to_string(a[4]) + ")");
|
|
check_writable(a[5], 8);
|
|
void *host = nullptr;
|
|
const auto status = vkMapMemory(device.handle, memory.handle, offset, size, 0, &host);
|
|
result = static_cast<uint32_t>(status);
|
|
if (status == VK_SUCCESS) {
|
|
uint64_t shadow = 0;
|
|
try {
|
|
if (memory.coherent)
|
|
if (const auto mapping = map_gpu_memory(process, host, size_t(size))) {
|
|
shadow = mapping->address;
|
|
memory.mapping_base = mapping->base;
|
|
}
|
|
if (!shadow) {
|
|
memory.snapshot.assign(static_cast<const uint8_t *>(host), static_cast<const uint8_t *>(host) + size);
|
|
shadow = make_shadow(memory, size_t(size));
|
|
std::memcpy(memory.view, memory.snapshot.data(), memory.snapshot.size());
|
|
}
|
|
} catch (...) {
|
|
memory.snapshot.clear();
|
|
vkUnmapMemory(device.handle, memory.handle);
|
|
throw;
|
|
}
|
|
memory.host = host;
|
|
memory.shadow = shadow;
|
|
memory.offset = offset;
|
|
memory.mapped = size;
|
|
store(a[5], shadow);
|
|
}
|
|
} else if (name == "vkUnmapMemory") {
|
|
auto &device = device_of(a[0]);
|
|
auto &memory = memory_of(a[1], a[0]);
|
|
if (!memory.host)
|
|
throw std::runtime_error("Vulkan memory is not mapped");
|
|
shadow_to_device(a[0]);
|
|
vkUnmapMemory(device.handle, memory.handle);
|
|
drop_shadow(memory);
|
|
memory.host = nullptr;
|
|
memory.snapshot.clear();
|
|
} else if (name == "vkFlushMappedMemoryRanges" || name == "vkInvalidateMappedMemoryRanges") {
|
|
device_of(a[0]);
|
|
if (a[1] > 64)
|
|
throw std::runtime_error("too many Vulkan mapped memory ranges");
|
|
for (uint64_t index = 0; index < a[1]; ++index) {
|
|
const auto range = load<VkMappedMemoryRange>(a[2] + index * sizeof(VkMappedMemoryRange));
|
|
if (range.sType != VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE || range.pNext)
|
|
throw std::runtime_error("unsupported Vulkan mapped memory range");
|
|
if (!memory_of(reinterpret_cast<uint64_t>(range.memory), a[0]).host)
|
|
throw std::runtime_error("Vulkan memory is not mapped");
|
|
}
|
|
if (name == "vkFlushMappedMemoryRanges")
|
|
shadow_to_device(a[0]);
|
|
else
|
|
shadow_from_device(a[0]);
|
|
} else if (name == "vkDestroyCommandPool") {
|
|
auto &device = device_of(a[0]);
|
|
no_allocator(a[2]);
|
|
if (a[1]) {
|
|
const auto &pool = pool_of(a[1], a[0]);
|
|
vkDestroyCommandPool(device.handle, pool.handle, nullptr);
|
|
std::erase_if(command_buffers, [&](const auto &item) { return item.second.pool == a[1]; });
|
|
pools.erase(a[1]);
|
|
}
|
|
} else if (name == "vkAllocateCommandBuffers") {
|
|
auto &device = device_of(a[0]);
|
|
auto info = load<VkCommandBufferAllocateInfo>(a[1]);
|
|
if (info.sType != VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO || info.pNext || !info.commandBufferCount || info.commandBufferCount > 64)
|
|
throw std::runtime_error("unsupported Vulkan command buffer allocate info");
|
|
const auto pool_token = reinterpret_cast<uint64_t>(info.commandPool);
|
|
const auto &pool = pool_of(pool_token, a[0]);
|
|
info.commandPool = pool.handle;
|
|
check_writable(a[2], size_t(info.commandBufferCount) * 8);
|
|
std::vector<VkCommandBuffer> created(info.commandBufferCount);
|
|
const auto status = vkAllocateCommandBuffers(device.handle, &info, created.data());
|
|
result = static_cast<uint32_t>(status);
|
|
if (status == VK_SUCCESS)
|
|
for (size_t index = 0; index < created.size(); ++index) {
|
|
const auto token = next_handle++;
|
|
command_buffers.emplace(token, CommandBuffer{created[index], a[0], pool_token});
|
|
store(a[2] + index * 8, token);
|
|
}
|
|
} else if (name == "vkFreeCommandBuffers") {
|
|
auto &device = device_of(a[0]);
|
|
const auto &pool = pool_of(a[1], a[0]);
|
|
if (a[2] > 64)
|
|
throw std::runtime_error("too many Vulkan command buffers");
|
|
std::vector<VkCommandBuffer> handles;
|
|
std::vector<uint64_t> tokens;
|
|
for (uint64_t index = 0; index < a[2]; ++index) {
|
|
const auto token = load<uint64_t>(a[3] + index * 8);
|
|
if (!token)
|
|
continue;
|
|
const auto &buffer = command_of(token);
|
|
if (buffer.pool != a[1])
|
|
throw std::runtime_error("Vulkan command buffer belongs to another pool");
|
|
handles.push_back(buffer.handle);
|
|
tokens.push_back(token);
|
|
}
|
|
if (!handles.empty())
|
|
vkFreeCommandBuffers(device.handle, pool.handle, uint32_t(handles.size()), handles.data());
|
|
for (const auto token : tokens)
|
|
command_buffers.erase(token);
|
|
}
|
|
process.return_api(result);
|
|
return true;
|
|
}
|
|
|
|
}
|