Files
lbgos--rift-s-linux/settings-app/src-tauri/native/overlay_preview.cpp
T

435 lines
19 KiB
C++

// Copyright 2026, lbgos
// SPDX-License-Identifier: BSL-1.0
#include "overlay_preview.hpp"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>
#include <vulkan/vulkan.h>
namespace {
void check(VkResult result) {
if (result != VK_SUCCESS)
throw std::runtime_error("Headset preview Vulkan error " +
std::to_string(result));
}
std::vector<std::string> split(const std::string &value) {
std::istringstream stream(value);
std::vector<std::string> out;
for (std::string part; stream >> part;)
out.push_back(part);
return out;
}
std::vector<const char *> pointers(const std::vector<std::string> &values) {
std::vector<const char *> out;
for (const auto &value : values)
out.push_back(value.c_str());
return out;
}
struct Preview {
VkInstance instance{};
VkPhysicalDevice physical{};
VkDevice device{};
VkQueue queue{};
uint32_t family{};
VkCommandPool pool{};
VkCommandBuffer command{};
VkFence fence{};
VkDeviceMemory buffer_memory{};
VkDescriptorSetLayout descriptor_layout{};
VkDescriptorPool descriptor_pool{};
VkDescriptorSet descriptor{};
VkPipelineLayout pipeline_layout{};
VkPipeline pipeline{};
VkSampler sampler{};
VkImageView source_view{};
VkBuffer buffer{};
uint32_t width{}, height{};
vr::IVROverlayView *api{};
vr::VROverlayView_t view{};
bool acquired{};
std::vector<uint8_t> pixels;
uint32_t memory_type(uint32_t bits, VkMemoryPropertyFlags flags) {
VkPhysicalDeviceMemoryProperties properties{};
vkGetPhysicalDeviceMemoryProperties(physical, &properties);
for (uint32_t i = 0; i < properties.memoryTypeCount; ++i)
if ((bits & (1u << i)) &&
(properties.memoryTypes[i].propertyFlags & flags) == flags)
return i;
throw std::runtime_error("Headset preview memory unavailable");
}
void initialize(vr::IVRSystem *system, vr::IVRCompositor *compositor) {
uint32_t length =
compositor->GetVulkanInstanceExtensionsRequired(nullptr, 0);
if (!length || length > 65536)
throw std::runtime_error("Headset preview extensions unavailable");
std::string required(length, '\0');
compositor->GetVulkanInstanceExtensionsRequired(required.data(), length);
auto names = split(required);
auto extensions = pointers(names);
VkApplicationInfo app{VK_STRUCTURE_TYPE_APPLICATION_INFO};
app.pApplicationName = "Rift S Settings";
app.apiVersion = VK_API_VERSION_1_1;
VkInstanceCreateInfo create{VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO};
create.pApplicationInfo = &app;
create.enabledExtensionCount = extensions.size();
create.ppEnabledExtensionNames = extensions.data();
check(vkCreateInstance(&create, nullptr, &instance));
uint64_t output = 0;
system->GetOutputDevice(&output, vr::TextureType_Vulkan, instance);
physical = reinterpret_cast<VkPhysicalDevice>(output);
if (!physical)
throw std::runtime_error("Headset preview GPU unavailable");
uint32_t count = 0;
vkGetPhysicalDeviceQueueFamilyProperties(physical, &count, nullptr);
std::vector<VkQueueFamilyProperties> families(count);
vkGetPhysicalDeviceQueueFamilyProperties(physical, &count, families.data());
while (family < count && (families[family].queueFlags &
(VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT)) !=
(VK_QUEUE_GRAPHICS_BIT | VK_QUEUE_COMPUTE_BIT))
++family;
if (family == count)
throw std::runtime_error("Headset preview queue unavailable");
length =
compositor->GetVulkanDeviceExtensionsRequired(physical, nullptr, 0);
if (!length || length > 65536)
throw std::runtime_error("Headset preview device extensions unavailable");
required.assign(length, '\0');
compositor->GetVulkanDeviceExtensionsRequired(physical, required.data(),
length);
names = split(required);
extensions = pointers(names);
float priority = 1;
VkDeviceQueueCreateInfo queue_create{
VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO};
queue_create.queueFamilyIndex = family;
queue_create.queueCount = 1;
queue_create.pQueuePriorities = &priority;
VkPhysicalDeviceTimelineSemaphoreFeatures timeline{
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_TIMELINE_SEMAPHORE_FEATURES};
VkPhysicalDeviceFeatures2 features{
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2};
features.pNext = &timeline;
vkGetPhysicalDeviceFeatures2(physical, &features);
VkDeviceCreateInfo device_create{VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO};
device_create.queueCreateInfoCount = 1;
device_create.pQueueCreateInfos = &queue_create;
device_create.enabledExtensionCount = extensions.size();
device_create.ppEnabledExtensionNames = extensions.data();
device_create.pNext = &timeline;
check(vkCreateDevice(physical, &device_create, nullptr, &device));
vkGetDeviceQueue(device, family, 0, &queue);
VkCommandPoolCreateInfo pool_create{
VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
pool_create.queueFamilyIndex = family;
pool_create.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
check(vkCreateCommandPool(device, &pool_create, nullptr, &pool));
VkCommandBufferAllocateInfo allocate{
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocate.commandPool = pool;
allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocate.commandBufferCount = 1;
check(vkAllocateCommandBuffers(device, &allocate, &command));
VkFenceCreateInfo fence_create{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
check(vkCreateFence(device, &fence_create, nullptr, &fence));
}
void initialize_sampling() {
VkDescriptorSetLayoutBinding bindings[] = {
{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1,
VK_SHADER_STAGE_COMPUTE_BIT, nullptr},
{1, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT,
nullptr}};
VkDescriptorSetLayoutCreateInfo layout{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layout.bindingCount = 2;
layout.pBindings = bindings;
check(vkCreateDescriptorSetLayout(device, &layout, nullptr,
&descriptor_layout));
VkDescriptorPoolSize sizes[] = {
{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1},
{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}};
VkDescriptorPoolCreateInfo descriptors{
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
descriptors.maxSets = 1;
descriptors.poolSizeCount = 2;
descriptors.pPoolSizes = sizes;
check(vkCreateDescriptorPool(device, &descriptors, nullptr,
&descriptor_pool));
VkDescriptorSetAllocateInfo allocate{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
allocate.descriptorPool = descriptor_pool;
allocate.descriptorSetCount = 1;
allocate.pSetLayouts = &descriptor_layout;
check(vkAllocateDescriptorSets(device, &allocate, &descriptor));
VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, 28};
VkPipelineLayoutCreateInfo pipeline_create{
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pipeline_create.setLayoutCount = 1;
pipeline_create.pSetLayouts = &descriptor_layout;
pipeline_create.pushConstantRangeCount = 1;
pipeline_create.pPushConstantRanges = &push;
check(vkCreatePipelineLayout(device, &pipeline_create, nullptr,
&pipeline_layout));
// Generated from overlay_preview.comp with glslc -fshader-stage=compute
// -mfmt=c.
static constexpr uint32_t code[] =
#include "overlay_preview_shader.inc"
;
VkShaderModuleCreateInfo module_create{
VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
module_create.codeSize = sizeof(code);
module_create.pCode = code;
VkShaderModule module{};
check(vkCreateShaderModule(device, &module_create, nullptr, &module));
VkComputePipelineCreateInfo compute{
VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO};
compute.layout = pipeline_layout;
compute.stage = {VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
nullptr,
0,
VK_SHADER_STAGE_COMPUTE_BIT,
module,
"main",
nullptr};
auto created = vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &compute,
nullptr, &pipeline);
vkDestroyShaderModule(device, module, nullptr);
check(created);
VkSamplerCreateInfo sample{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sample.magFilter = sample.minFilter = VK_FILTER_NEAREST;
sample.addressModeU = sample.addressModeV = sample.addressModeW =
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
check(vkCreateSampler(device, &sample, nullptr, &sampler));
}
bool release() {
auto result = acquired && api ? api->ReleaseOverlayView(&view)
: vr::VROverlayError_None;
acquired = false;
view = {};
return result == vr::VROverlayError_None;
}
void clear_buffers() {
if (buffer)
vkDestroyBuffer(device, buffer, nullptr);
if (buffer_memory)
vkFreeMemory(device, buffer_memory, nullptr);
buffer = {};
buffer_memory = {};
}
void resize(uint32_t w, uint32_t h) {
if (w == width && h == height && buffer)
return;
clear_buffers();
width = w;
height = h;
VkBufferCreateInfo create{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
create.size = size_t(w) * h * 4;
create.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
check(vkCreateBuffer(device, &create, nullptr, &buffer));
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device, buffer, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = memory_type(
requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
check(vkAllocateMemory(device, &allocation, nullptr, &buffer_memory));
check(vkBindBufferMemory(device, buffer, buffer_memory, 0));
pixels.resize(size_t(w) * h * 4);
}
void copy(const vr::VRVulkanTextureData_t &source,
const vr::VRTextureBounds_t &bounds) {
if (source.m_pDevice != device || source.m_nQueueFamilyIndex != family ||
source.m_nSampleCount != 1 || source.m_nWidth == 0 ||
source.m_nHeight == 0 || source.m_nWidth > 16384 ||
source.m_nHeight > 16384)
throw std::runtime_error("Headset preview texture unsupported");
const float coords[] = {bounds.uMin, bounds.uMax, bounds.vMin, bounds.vMax};
for (float value : coords)
if (!std::isfinite(value) || value < 0 || value > 1)
throw std::runtime_error("Headset preview bounds invalid");
uint32_t w =
std::lround(std::abs(bounds.uMax - bounds.uMin) * source.m_nWidth);
uint32_t h =
std::lround(std::abs(bounds.vMax - bounds.vMin) * source.m_nHeight);
if (!w || !h)
throw std::runtime_error("Headset preview bounds empty");
double scale = std::min(1.0, 512.0 / std::max(w, h));
resize(std::max(1u, uint32_t(w * scale)),
std::max(1u, uint32_t(h * scale)));
auto format = VkFormat(source.m_nFormat);
VkFormatProperties properties{};
vkGetPhysicalDeviceFormatProperties(physical, format, &properties);
if (!(properties.optimalTilingFeatures &
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT))
throw std::runtime_error("Headset preview format unsupported");
VkImageViewCreateInfo view_create{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_create.image = reinterpret_cast<VkImage>(source.m_nImage);
view_create.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_create.format = format;
view_create.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
check(vkCreateImageView(device, &view_create, nullptr, &source_view));
VkDescriptorImageInfo sampled{sampler, source_view,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
VkDescriptorBufferInfo output{buffer, 0, pixels.size()};
VkWriteDescriptorSet writes[2]{};
for (auto &write : writes) {
write.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
write.dstSet = descriptor;
write.descriptorCount = 1;
}
writes[0].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
writes[0].pImageInfo = &sampled;
writes[1].dstBinding = 1;
writes[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER;
writes[1].pBufferInfo = &output;
vkUpdateDescriptorSets(device, 2, writes, 0, nullptr);
check(vkResetCommandBuffer(command, 0));
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
check(vkBeginCommandBuffer(command, &begin));
// The acquired image is sampleable and stable. Never write or transition
// it, and finish all GPU reads before ReleaseOverlayView frees the import.
VkMemoryBarrier readable{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
readable.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
readable.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 1, &readable,
0, nullptr, 0, nullptr);
vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_COMPUTE,
pipeline_layout, 0, 1, &descriptor, 0, nullptr);
struct {
float bounds[4];
uint32_t width, height, srgb;
} push{{bounds.uMin, bounds.vMin, bounds.uMax, bounds.vMax},
width,
height,
uint32_t(format == VK_FORMAT_R8G8B8A8_SRGB ||
format == VK_FORMAT_B8G8R8A8_SRGB ||
format == VK_FORMAT_A8B8G8R8_SRGB_PACK32)};
vkCmdPushConstants(command, pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0,
sizeof(push), &push);
vkCmdDispatch(command, (width + 7) / 8, (height + 7) / 8, 1);
VkBufferMemoryBarrier host{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER};
host.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
host.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
host.srcQueueFamilyIndex = host.dstQueueFamilyIndex =
VK_QUEUE_FAMILY_IGNORED;
host.buffer = buffer;
host.size = VK_WHOLE_SIZE;
vkCmdPipelineBarrier(command, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &host, 0,
nullptr);
check(vkEndCommandBuffer(command));
check(vkResetFences(device, 1, &fence));
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &command;
check(vkQueueSubmit(queue, 1, &submit, fence));
check(vkWaitForFences(device, 1, &fence, VK_TRUE, 500000000));
vkDestroyImageView(device, source_view, nullptr);
source_view = {};
void *mapped{};
check(vkMapMemory(device, buffer_memory, 0, pixels.size(), 0, &mapped));
std::memcpy(pixels.data(), mapped, pixels.size());
vkUnmapMemory(device, buffer_memory);
}
~Preview() {
if (device)
vkDeviceWaitIdle(device);
if (source_view)
vkDestroyImageView(device, source_view, nullptr);
release();
clear_buffers();
if (pipeline)
vkDestroyPipeline(device, pipeline, nullptr);
if (pipeline_layout)
vkDestroyPipelineLayout(device, pipeline_layout, nullptr);
if (sampler)
vkDestroySampler(device, sampler, nullptr);
if (descriptor_pool)
vkDestroyDescriptorPool(device, descriptor_pool, nullptr);
if (descriptor_layout)
vkDestroyDescriptorSetLayout(device, descriptor_layout, nullptr);
if (fence)
vkDestroyFence(device, fence, nullptr);
if (pool)
vkDestroyCommandPool(device, pool, nullptr);
if (device)
vkDestroyDevice(device, nullptr);
if (instance)
vkDestroyInstance(instance, nullptr);
}
};
std::unique_ptr<Preview> preview;
std::string error;
} // namespace
const char *rifts_overlay_frame(vr::IVRSystem *system,
vr::IVRCompositor *compositor,
vr::IVROverlay *overlay,
vr::IVROverlayView *api, const uint8_t **data,
size_t *bytes, uint32_t *w, uint32_t *h) {
*data = nullptr;
*bytes = 0;
*w = *h = 0;
if (!system || !compositor || !overlay || !api)
return "Headset preview unavailable";
vr::VROverlayHandle_t handle{};
if (overlay->FindOverlay(vr::k_pchHeadsetViewOverlayKey, &handle) !=
vr::VROverlayError_None)
return "Open VR View to preview";
if (!api->IsViewingPermitted(handle))
return "Headset preview access unavailable";
try {
if (!preview) {
preview = std::make_unique<Preview>();
preview->initialize(system, compositor);
preview->initialize_sampling();
}
preview->api = api;
vr::VRVulkanDevice_t vulkan{preview->instance, preview->device,
preview->physical, preview->queue,
preview->family};
vr::VRNativeDevice_t native{&vulkan, vr::DeviceType_Vulkan};
auto acquired = api->AcquireOverlayView(handle, &native, &preview->view,
sizeof(preview->view));
// Even a view with no queued frame owns native resources and must be
// released.
preview->acquired = true;
if (acquired != vr::VROverlayError_None)
throw std::runtime_error("Headset preview acquisition error " +
std::to_string(acquired));
if (!preview->view.texture.handle) {
if (!preview->release())
throw std::runtime_error("Headset preview release failed");
auto activity = system->GetTrackedDeviceActivityLevel(0);
return activity == vr::k_EDeviceActivityLevel_Standby
? "Put on the headset to preview"
: "Headset view has no frame";
}
if (preview->view.texture.eType != vr::TextureType_Vulkan)
throw std::runtime_error("Headset preview texture type unsupported");
preview->copy(
*static_cast<vr::VRVulkanTextureData_t *>(preview->view.texture.handle),
preview->view.textureBounds);
if (!preview->release())
throw std::runtime_error("Headset preview release failed");
*data = preview->pixels.data();
*bytes = preview->pixels.size();
*w = preview->width;
*h = preview->height;
return nullptr;
} catch (const std::exception &failure) {
error = failure.what();
preview.reset();
return error.c_str();
}
}
void rifts_overlay_shutdown() { preview.reset(); }