mirror of
https://github.com/lbgos/rift-s-linux.git
synced 2026-10-22 21:00:42 +02:00
Compare layout changes against the size the swapchain was requested for, not the extent the surface caps may clamp it to, so a clamped surface cannot recreate the swapchain every frame. Add a focused test for the /proc process lookup that replaced pgrep.
1014 lines
41 KiB
C++
1014 lines
41 KiB
C++
// Copyright 2026, lbgos
|
|
// SPDX-License-Identifier: BSL-1.0
|
|
//
|
|
// Zero-copy headset preview.
|
|
//
|
|
// SteamVR renders its headset view into the public `system.HeadsetView`
|
|
// overlay. Once per compositor vsync the preview thread re-acquires that
|
|
// overlay through IVROverlayView into our Vulkan device on SteamVR's GPU and
|
|
// draws it into a Vulkan swapchain on a Wayland subsurface that sits over the
|
|
// preview box of the webview. Pixels never leave the GPU.
|
|
//
|
|
// Re-acquiring with the same VROverlayView_t (as the OpenVR header advises)
|
|
// reuses the client's imported images: ~25 us per call and no vrclient log
|
|
// line, versus ~1 ms and one log line for acquire+release.
|
|
//
|
|
// Threads:
|
|
// - rifts_surface_place: GTK main thread, owns the subsurface position.
|
|
// - rifts_headset_preview_step/_shutdown: preview thread with the OpenVR
|
|
// client lock held, owns Vulkan and the subsurface contents.
|
|
// - rifts_preview_status: any thread.
|
|
#define VK_USE_PLATFORM_WAYLAND_KHR
|
|
#include "headset_preview.hpp"
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <vulkan/vulkan.h>
|
|
#include <wayland-client.h>
|
|
|
|
// The few GTK/GDK calls needed to find the toplevel wl_surface. Declared here
|
|
// to keep GTK headers out of the build; the binary links GTK through Tauri.
|
|
extern "C" {
|
|
struct _GtkWidget;
|
|
struct _GdkWindow;
|
|
struct _GdkDisplay;
|
|
_GtkWidget *gtk_widget_get_toplevel(_GtkWidget *);
|
|
_GdkWindow *gtk_widget_get_window(_GtkWidget *);
|
|
int gtk_widget_translate_coordinates(_GtkWidget *, _GtkWidget *, int, int,
|
|
int *, int *);
|
|
void gtk_widget_queue_draw(_GtkWidget *);
|
|
_GdkDisplay *gdk_window_get_display(_GdkWindow *);
|
|
int gdk_window_get_scale_factor(_GdkWindow *);
|
|
unsigned long gdk_wayland_window_get_type();
|
|
int g_type_check_instance_is_a(void *, unsigned long);
|
|
wl_display *gdk_wayland_display_get_wl_display(_GdkDisplay *);
|
|
wl_surface *gdk_wayland_window_get_wl_surface(_GdkWindow *);
|
|
}
|
|
|
|
namespace {
|
|
constexpr int64_t retry_us = 500000;
|
|
|
|
// ---- Shared state ---------------------------------------------------------
|
|
|
|
struct Layout {
|
|
int x{}, y{}, width{}, height{}, scale{1};
|
|
bool visible{};
|
|
};
|
|
// Wayland objects live for the process. `surface` is ours; the subsurface
|
|
// role ties it to the GTK toplevel. Only the main thread changes the role and
|
|
// position; only the preview thread attaches buffers.
|
|
struct Wayland {
|
|
wl_display *display{};
|
|
wl_event_queue *queue{};
|
|
wl_compositor *compositor{};
|
|
wl_subcompositor *subcompositor{};
|
|
wl_surface *parent{};
|
|
wl_surface *surface{};
|
|
wl_subsurface *subsurface{};
|
|
bool unavailable{};
|
|
};
|
|
std::mutex layout_lock; // guards layout and the publication of wayland
|
|
Layout layout;
|
|
Wayland wayland;
|
|
|
|
std::mutex status_lock;
|
|
std::string status_error = "Starting preview";
|
|
uint64_t status_presented{};
|
|
uint32_t status_width{}, status_height{};
|
|
void set_error(const std::string &error) {
|
|
std::lock_guard guard(status_lock);
|
|
status_error = error;
|
|
}
|
|
|
|
void registry_global(void *, wl_registry *registry, uint32_t name,
|
|
const char *interface, uint32_t version) {
|
|
if (!std::strcmp(interface, wl_compositor_interface.name))
|
|
wayland.compositor = static_cast<wl_compositor *>(wl_registry_bind(
|
|
registry, name, &wl_compositor_interface, std::min(version, 4u)));
|
|
else if (!std::strcmp(interface, wl_subcompositor_interface.name))
|
|
wayland.subcompositor = static_cast<wl_subcompositor *>(
|
|
wl_registry_bind(registry, name, &wl_subcompositor_interface, 1));
|
|
}
|
|
void registry_remove(void *, wl_registry *, uint32_t) {}
|
|
const wl_registry_listener registry_listener{registry_global, registry_remove};
|
|
|
|
// Binds globals on a private queue so GTK's queue is never dispatched here.
|
|
bool connect_wayland(wl_display *display) {
|
|
wayland.display = display;
|
|
wayland.queue = wl_display_create_queue(display);
|
|
auto *wrapper = static_cast<wl_display *>(wl_proxy_create_wrapper(display));
|
|
wl_proxy_set_queue(reinterpret_cast<wl_proxy *>(wrapper), wayland.queue);
|
|
wl_registry *registry = wl_display_get_registry(wrapper);
|
|
wl_proxy_wrapper_destroy(wrapper);
|
|
wl_registry_add_listener(registry, ®istry_listener, nullptr);
|
|
wl_display_roundtrip_queue(display, wayland.queue);
|
|
wl_registry_destroy(registry);
|
|
if (!wayland.compositor || !wayland.subcompositor)
|
|
return false;
|
|
wayland.surface = wl_compositor_create_surface(wayland.compositor);
|
|
// Pointer input falls through to the webview (double-click, cursor).
|
|
wl_region *empty = wl_compositor_create_region(wayland.compositor);
|
|
wl_surface_set_input_region(wayland.surface, empty);
|
|
wl_region_destroy(empty);
|
|
return true;
|
|
}
|
|
|
|
// ---- Vulkan renderer ------------------------------------------------------
|
|
|
|
void check(VkResult result, const char *what) {
|
|
if (result != VK_SUCCESS)
|
|
throw std::runtime_error(std::string("Headset preview ") + what +
|
|
" failed (" + 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;
|
|
}
|
|
// Space-separated extension list from OpenVR plus our own.
|
|
std::vector<std::string> extensions(std::string required, const char *extra) {
|
|
auto names = split(required);
|
|
if (std::find(names.begin(), names.end(), extra) == names.end())
|
|
names.push_back(extra);
|
|
return names;
|
|
}
|
|
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;
|
|
}
|
|
bool srgb(VkFormat format) {
|
|
return format == VK_FORMAT_R8G8B8A8_SRGB ||
|
|
format == VK_FORMAT_B8G8R8A8_SRGB ||
|
|
format == VK_FORMAT_A8B8G8R8_SRGB_PACK32;
|
|
}
|
|
|
|
struct Renderer {
|
|
VkInstance instance{};
|
|
VkPhysicalDevice physical{};
|
|
VkDevice device{};
|
|
VkQueue queue{};
|
|
uint32_t family{};
|
|
VkCommandPool pool{};
|
|
VkCommandBuffer command{};
|
|
VkFence fence{};
|
|
VkSemaphore acquired{};
|
|
VkSampler sampler{};
|
|
VkDescriptorSetLayout set_layout{};
|
|
VkDescriptorPool descriptor_pool{};
|
|
VkPipelineLayout pipeline_layout{};
|
|
VkRenderPass pass{};
|
|
VkPipeline pipeline{};
|
|
VkFormat format{VK_FORMAT_UNDEFINED};
|
|
double period{1.0 / 80};
|
|
|
|
VkSurfaceKHR surface{};
|
|
VkSwapchainKHR swapchain{};
|
|
VkExtent2D extent{};
|
|
// Layout size the swapchain was made for; extent may be clamped by caps.
|
|
VkExtent2D requested{};
|
|
int scale{1};
|
|
bool stale{};
|
|
bool mapped{};
|
|
struct Target {
|
|
VkImageView view{};
|
|
VkFramebuffer framebuffer{};
|
|
VkSemaphore rendered{};
|
|
};
|
|
std::vector<VkImage> images;
|
|
std::vector<Target> targets;
|
|
|
|
// Overlay images seen through the held view. The producer cycles through a
|
|
// few; anything beyond the cache means it reallocated, so start over.
|
|
vr::IVROverlayView *api{};
|
|
vr::VROverlayView_t view{};
|
|
bool holding{};
|
|
struct Source {
|
|
VkImage image{};
|
|
VkImageView view{};
|
|
VkDescriptorSet set{};
|
|
};
|
|
std::array<Source, 8> sources{};
|
|
size_t source_count{};
|
|
uint32_t source_width{}, source_height{};
|
|
VkFormat source_format{VK_FORMAT_UNDEFINED};
|
|
uint64_t last_frame{~0ull};
|
|
uint32_t last_index{};
|
|
std::chrono::steady_clock::time_point last_present{};
|
|
|
|
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 = extensions(required.c_str(), "VK_KHR_wayland_surface");
|
|
auto list = 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 = list.size();
|
|
create.ppEnabledExtensionNames = list.data();
|
|
check(vkCreateInstance(&create, nullptr, &instance), "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) ||
|
|
!vkGetPhysicalDeviceWaylandPresentationSupportKHR(
|
|
physical, family, wayland.display)))
|
|
++family;
|
|
if (family == count)
|
|
throw std::runtime_error("Headset preview cannot present on this GPU");
|
|
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 = extensions(required.c_str(), VK_KHR_SWAPCHAIN_EXTENSION_NAME);
|
|
list = 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.pNext = &timeline;
|
|
device_create.queueCreateInfoCount = 1;
|
|
device_create.pQueueCreateInfos = &queue_create;
|
|
device_create.enabledExtensionCount = list.size();
|
|
device_create.ppEnabledExtensionNames = list.data();
|
|
check(vkCreateDevice(physical, &device_create, nullptr, &device), "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), "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), "commands");
|
|
VkFenceCreateInfo fence_create{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
|
fence_create.flags = VK_FENCE_CREATE_SIGNALED_BIT;
|
|
check(vkCreateFence(device, &fence_create, nullptr, &fence), "fence");
|
|
VkSemaphoreCreateInfo semaphore{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
|
check(vkCreateSemaphore(device, &semaphore, nullptr, &acquired),
|
|
"semaphore");
|
|
|
|
VkSamplerCreateInfo sample{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
|
sample.magFilter = sample.minFilter = VK_FILTER_LINEAR;
|
|
sample.addressModeU = sample.addressModeV = sample.addressModeW =
|
|
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
|
check(vkCreateSampler(device, &sample, nullptr, &sampler), "sampler");
|
|
VkDescriptorSetLayoutBinding binding{
|
|
0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1,
|
|
VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
|
VkDescriptorSetLayoutCreateInfo set_create{
|
|
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
|
|
set_create.bindingCount = 1;
|
|
set_create.pBindings = &binding;
|
|
check(vkCreateDescriptorSetLayout(device, &set_create, nullptr,
|
|
&set_layout),
|
|
"descriptor layout");
|
|
VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
|
|
uint32_t(sources.size())};
|
|
VkDescriptorPoolCreateInfo descriptors{
|
|
VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
|
descriptors.maxSets = sources.size();
|
|
descriptors.poolSizeCount = 1;
|
|
descriptors.pPoolSizes = &size;
|
|
check(vkCreateDescriptorPool(device, &descriptors, nullptr,
|
|
&descriptor_pool),
|
|
"descriptor pool");
|
|
constexpr size_t slots = std::tuple_size_v<decltype(sources)>;
|
|
std::array<VkDescriptorSetLayout, slots> layouts;
|
|
layouts.fill(set_layout);
|
|
std::array<VkDescriptorSet, slots> sets{};
|
|
VkDescriptorSetAllocateInfo set_allocate{
|
|
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
|
|
set_allocate.descriptorPool = descriptor_pool;
|
|
set_allocate.descriptorSetCount = layouts.size();
|
|
set_allocate.pSetLayouts = layouts.data();
|
|
check(vkAllocateDescriptorSets(device, &set_allocate, sets.data()),
|
|
"descriptor sets");
|
|
for (size_t i = 0; i < sources.size(); ++i)
|
|
sources[i].set = sets[i];
|
|
VkPushConstantRange push{VK_SHADER_STAGE_VERTEX_BIT |
|
|
VK_SHADER_STAGE_FRAGMENT_BIT,
|
|
0, 20};
|
|
VkPipelineLayoutCreateInfo pipeline_create{
|
|
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
|
|
pipeline_create.setLayoutCount = 1;
|
|
pipeline_create.pSetLayouts = &set_layout;
|
|
pipeline_create.pushConstantRangeCount = 1;
|
|
pipeline_create.pPushConstantRanges = &push;
|
|
check(vkCreatePipelineLayout(device, &pipeline_create, nullptr,
|
|
&pipeline_layout),
|
|
"pipeline layout");
|
|
|
|
vr::ETrackedPropertyError error{};
|
|
float hz = system->GetFloatTrackedDeviceProperty(
|
|
vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float,
|
|
&error);
|
|
if (error == vr::TrackedProp_Success && hz >= 30 && hz <= 240)
|
|
period = 1.0 / hz;
|
|
}
|
|
|
|
// Render pass and pipeline depend on the swapchain format only.
|
|
void build_pipeline(VkFormat target) {
|
|
if (pipeline && target == format)
|
|
return;
|
|
if (pipeline)
|
|
vkDestroyPipeline(device, pipeline, nullptr);
|
|
if (pass)
|
|
vkDestroyRenderPass(device, pass, nullptr);
|
|
pipeline = {};
|
|
pass = {};
|
|
format = target;
|
|
VkAttachmentDescription color{};
|
|
color.format = format;
|
|
color.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
|
color.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
|
|
color.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
|
color.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
|
|
color.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
color.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
|
|
VkAttachmentReference reference{0,
|
|
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
|
|
VkSubpassDescription subpass{};
|
|
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
|
|
subpass.colorAttachmentCount = 1;
|
|
subpass.pColorAttachments = &reference;
|
|
// The swapchain image is ready once the acquire semaphore wait at this
|
|
// stage completes.
|
|
VkSubpassDependency dependency{};
|
|
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
|
|
dependency.dstSubpass = 0;
|
|
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
VkRenderPassCreateInfo pass_create{
|
|
VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
|
|
pass_create.attachmentCount = 1;
|
|
pass_create.pAttachments = &color;
|
|
pass_create.subpassCount = 1;
|
|
pass_create.pSubpasses = &subpass;
|
|
pass_create.dependencyCount = 1;
|
|
pass_create.pDependencies = &dependency;
|
|
check(vkCreateRenderPass(device, &pass_create, nullptr, &pass),
|
|
"render pass");
|
|
|
|
// Generated from headset_preview.vert/.frag by build.rs (glslc -mfmt=c).
|
|
static constexpr uint32_t vertex_code[] =
|
|
#include "headset_preview_vert.inc"
|
|
;
|
|
static constexpr uint32_t fragment_code[] =
|
|
#include "headset_preview_frag.inc"
|
|
;
|
|
auto module = [&](const uint32_t *code, size_t bytes) {
|
|
VkShaderModuleCreateInfo create{
|
|
VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
|
|
create.codeSize = bytes;
|
|
create.pCode = code;
|
|
VkShaderModule out{};
|
|
check(vkCreateShaderModule(device, &create, nullptr, &out), "shader");
|
|
return out;
|
|
};
|
|
VkShaderModule vertex = module(vertex_code, sizeof(vertex_code));
|
|
VkShaderModule fragment = module(fragment_code, sizeof(fragment_code));
|
|
VkPipelineShaderStageCreateInfo stages[2]{};
|
|
for (auto &stage : stages) {
|
|
stage.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
|
|
stage.pName = "main";
|
|
}
|
|
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
|
|
stages[0].module = vertex;
|
|
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
|
|
stages[1].module = fragment;
|
|
VkPipelineVertexInputStateCreateInfo input{
|
|
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
|
|
VkPipelineInputAssemblyStateCreateInfo assembly{
|
|
VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
|
assembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
|
VkPipelineViewportStateCreateInfo viewport{
|
|
VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
|
viewport.viewportCount = viewport.scissorCount = 1;
|
|
VkPipelineRasterizationStateCreateInfo raster{
|
|
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
|
raster.polygonMode = VK_POLYGON_MODE_FILL;
|
|
raster.cullMode = VK_CULL_MODE_NONE;
|
|
raster.lineWidth = 1;
|
|
VkPipelineMultisampleStateCreateInfo multisample{
|
|
VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
|
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
|
VkPipelineColorBlendAttachmentState blend{};
|
|
blend.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
|
|
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
|
|
VkPipelineColorBlendStateCreateInfo blending{
|
|
VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
|
|
blending.attachmentCount = 1;
|
|
blending.pAttachments = &blend;
|
|
VkDynamicState dynamic_states[] = {VK_DYNAMIC_STATE_VIEWPORT,
|
|
VK_DYNAMIC_STATE_SCISSOR};
|
|
VkPipelineDynamicStateCreateInfo dynamic{
|
|
VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
|
|
dynamic.dynamicStateCount = 2;
|
|
dynamic.pDynamicStates = dynamic_states;
|
|
VkGraphicsPipelineCreateInfo create{
|
|
VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
|
create.stageCount = 2;
|
|
create.pStages = stages;
|
|
create.pVertexInputState = &input;
|
|
create.pInputAssemblyState = &assembly;
|
|
create.pViewportState = &viewport;
|
|
create.pRasterizationState = &raster;
|
|
create.pMultisampleState = &multisample;
|
|
create.pColorBlendState = &blending;
|
|
create.pDynamicState = &dynamic;
|
|
create.layout = pipeline_layout;
|
|
create.renderPass = pass;
|
|
auto created = vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &create,
|
|
nullptr, &pipeline);
|
|
vkDestroyShaderModule(device, vertex, nullptr);
|
|
vkDestroyShaderModule(device, fragment, nullptr);
|
|
check(created, "pipeline");
|
|
}
|
|
|
|
void destroy_targets() {
|
|
for (auto &target : targets) {
|
|
vkDestroyFramebuffer(device, target.framebuffer, nullptr);
|
|
vkDestroyImageView(device, target.view, nullptr);
|
|
vkDestroySemaphore(device, target.rendered, nullptr);
|
|
}
|
|
targets.clear();
|
|
images.clear();
|
|
}
|
|
// Drops the swapchain and unmaps the subsurface so the webview below shows
|
|
// status text. The VkSurfaceKHR is kept for the next swapchain.
|
|
void hide() {
|
|
if (swapchain) {
|
|
vkDeviceWaitIdle(device);
|
|
destroy_targets();
|
|
vkDestroySwapchainKHR(device, swapchain, nullptr);
|
|
swapchain = {};
|
|
}
|
|
if (mapped) {
|
|
wl_surface_attach(wayland.surface, nullptr, 0, 0);
|
|
wl_surface_commit(wayland.surface);
|
|
wl_display_flush(wayland.display);
|
|
mapped = false;
|
|
}
|
|
}
|
|
|
|
// Returns true when a new swapchain was created.
|
|
bool ensure_swapchain(const Layout &target) {
|
|
VkExtent2D want{uint32_t(target.width * target.scale),
|
|
uint32_t(target.height * target.scale)};
|
|
if (swapchain && !stale && want.width == requested.width &&
|
|
want.height == requested.height && target.scale == scale)
|
|
return false;
|
|
if (!surface) {
|
|
VkWaylandSurfaceCreateInfoKHR create{
|
|
VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR};
|
|
create.display = wayland.display;
|
|
create.surface = wayland.surface;
|
|
check(vkCreateWaylandSurfaceKHR(instance, &create, nullptr, &surface),
|
|
"surface");
|
|
VkBool32 supported = VK_FALSE;
|
|
vkGetPhysicalDeviceSurfaceSupportKHR(physical, family, surface,
|
|
&supported);
|
|
if (!supported)
|
|
throw std::runtime_error("Headset preview surface unsupported");
|
|
}
|
|
vkDeviceWaitIdle(device);
|
|
if (target.scale != scale) {
|
|
// Applies with the first commit of the new swapchain, whose extent is a
|
|
// multiple of the scale.
|
|
wl_surface_set_buffer_scale(wayland.surface, target.scale);
|
|
scale = target.scale;
|
|
}
|
|
VkSurfaceCapabilitiesKHR caps{};
|
|
check(vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physical, surface, &caps),
|
|
"surface capabilities");
|
|
uint32_t count = 0;
|
|
vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &count, nullptr);
|
|
std::vector<VkSurfaceFormatKHR> formats(count);
|
|
vkGetPhysicalDeviceSurfaceFormatsKHR(physical, surface, &count,
|
|
formats.data());
|
|
if (formats.empty())
|
|
throw std::runtime_error("Headset preview surface has no formats");
|
|
// A UNORM target receives sRGB-encoded values from the shader.
|
|
auto chosen = formats[0];
|
|
for (auto candidate : formats)
|
|
if ((candidate.format == VK_FORMAT_B8G8R8A8_UNORM ||
|
|
candidate.format == VK_FORMAT_R8G8B8A8_UNORM) &&
|
|
candidate.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
|
|
chosen = candidate;
|
|
break;
|
|
}
|
|
vkGetPhysicalDeviceSurfacePresentModesKHR(physical, surface, &count,
|
|
nullptr);
|
|
std::vector<VkPresentModeKHR> modes(count);
|
|
vkGetPhysicalDeviceSurfacePresentModesKHR(physical, surface, &count,
|
|
modes.data());
|
|
// Never block the paced thread in present: the newest frame wins.
|
|
auto mode = VK_PRESENT_MODE_FIFO_KHR;
|
|
for (auto preferred :
|
|
{VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_IMMEDIATE_KHR})
|
|
if (std::find(modes.begin(), modes.end(), preferred) != modes.end()) {
|
|
mode = preferred;
|
|
break;
|
|
}
|
|
requested = want;
|
|
if (caps.currentExtent.width != UINT32_MAX)
|
|
want = caps.currentExtent;
|
|
want.width = std::clamp(want.width, caps.minImageExtent.width,
|
|
caps.maxImageExtent.width);
|
|
want.height = std::clamp(want.height, caps.minImageExtent.height,
|
|
caps.maxImageExtent.height);
|
|
uint32_t image_count = std::max(caps.minImageCount, 3u);
|
|
if (caps.maxImageCount)
|
|
image_count = std::min(image_count, caps.maxImageCount);
|
|
VkSwapchainCreateInfoKHR create{
|
|
VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR};
|
|
create.surface = surface;
|
|
create.minImageCount = image_count;
|
|
create.imageFormat = chosen.format;
|
|
create.imageColorSpace = chosen.colorSpace;
|
|
create.imageExtent = want;
|
|
create.imageArrayLayers = 1;
|
|
create.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
|
create.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
create.preTransform = caps.currentTransform;
|
|
create.compositeAlpha =
|
|
(caps.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
|
|
? VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR
|
|
: VkCompositeAlphaFlagBitsKHR(caps.supportedCompositeAlpha &
|
|
-caps.supportedCompositeAlpha);
|
|
create.presentMode = mode;
|
|
create.clipped = VK_TRUE;
|
|
create.oldSwapchain = swapchain;
|
|
VkSwapchainKHR fresh{};
|
|
check(vkCreateSwapchainKHR(device, &create, nullptr, &fresh), "swapchain");
|
|
destroy_targets();
|
|
if (swapchain)
|
|
vkDestroySwapchainKHR(device, swapchain, nullptr);
|
|
swapchain = fresh;
|
|
extent = want;
|
|
stale = false;
|
|
build_pipeline(chosen.format);
|
|
vkGetSwapchainImagesKHR(device, swapchain, &count, nullptr);
|
|
images.resize(count);
|
|
vkGetSwapchainImagesKHR(device, swapchain, &count, images.data());
|
|
targets.resize(count);
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
VkImageViewCreateInfo view_create{
|
|
VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
view_create.image = images[i];
|
|
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, &targets[i].view),
|
|
"target view");
|
|
VkFramebufferCreateInfo framebuffer{
|
|
VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
|
|
framebuffer.renderPass = pass;
|
|
framebuffer.attachmentCount = 1;
|
|
framebuffer.pAttachments = &targets[i].view;
|
|
framebuffer.width = extent.width;
|
|
framebuffer.height = extent.height;
|
|
framebuffer.layers = 1;
|
|
check(vkCreateFramebuffer(device, &framebuffer, nullptr,
|
|
&targets[i].framebuffer),
|
|
"framebuffer");
|
|
VkSemaphoreCreateInfo semaphore{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
|
|
check(vkCreateSemaphore(device, &semaphore, nullptr,
|
|
&targets[i].rendered),
|
|
"semaphore");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void forget_sources() {
|
|
for (size_t i = 0; i < source_count; ++i) {
|
|
vkDestroyImageView(device, sources[i].view, nullptr);
|
|
sources[i].view = {};
|
|
sources[i].image = {};
|
|
}
|
|
source_count = 0;
|
|
}
|
|
// GPU work must be finished (fence waited) before this frees the imports.
|
|
void release() {
|
|
forget_sources();
|
|
if (holding && api)
|
|
api->ReleaseOverlayView(&view);
|
|
holding = false;
|
|
view = {};
|
|
}
|
|
const Source *source_for(const vr::VRVulkanTextureData_t &texture) {
|
|
auto image = reinterpret_cast<VkImage>(texture.m_nImage);
|
|
for (size_t i = 0; i < source_count; ++i)
|
|
if (sources[i].image == image)
|
|
return &sources[i];
|
|
if (source_count == sources.size())
|
|
return nullptr;
|
|
auto &source = sources[source_count];
|
|
VkImageViewCreateInfo view_create{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
|
view_create.image = image;
|
|
view_create.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
|
view_create.format = VkFormat(texture.m_nFormat);
|
|
view_create.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
check(vkCreateImageView(device, &view_create, nullptr, &source.view),
|
|
"source view");
|
|
source.image = image;
|
|
VkDescriptorImageInfo sampled{sampler, source.view,
|
|
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL};
|
|
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
|
|
write.dstSet = source.set;
|
|
write.descriptorCount = 1;
|
|
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
|
|
write.pImageInfo = &sampled;
|
|
vkUpdateDescriptorSets(device, 1, &write, 0, nullptr);
|
|
++source_count;
|
|
return &source;
|
|
}
|
|
|
|
// Microseconds until shortly after the next compositor vsync, when the
|
|
// headset view for the new frame is ready.
|
|
int64_t until_next_frame(vr::IVRSystem *system) {
|
|
float since = 0;
|
|
uint64_t frame = 0;
|
|
if (!system->GetTimeSinceLastVsync(&since, &frame))
|
|
return int64_t(period * 1e6);
|
|
double wait = period - since + 0.001;
|
|
while (wait < 0.0005)
|
|
wait += period;
|
|
return int64_t(wait * 1e6);
|
|
}
|
|
|
|
int64_t step(vr::IVRSystem *system, vr::IVROverlay *overlay,
|
|
vr::IVRCompositor *compositor, const Layout &target) {
|
|
check(vkWaitForFences(device, 1, &fence, VK_TRUE, 500000000),
|
|
"frame wait");
|
|
float since = 0;
|
|
uint64_t frame = 0;
|
|
bool timed = system->GetTimeSinceLastVsync(&since, &frame);
|
|
bool resized = !swapchain || stale ||
|
|
requested.width != uint32_t(target.width * target.scale) ||
|
|
requested.height != uint32_t(target.height * target.scale);
|
|
// One present per compositor frame. Layout wake-ups mid-frame only
|
|
// present when the surface changed.
|
|
if (timed && frame == last_frame && !resized)
|
|
return until_next_frame(system);
|
|
// Any GPU submit at display rate costs vrcompositor CPU (NVIDIA), so skip
|
|
// frames that cannot differ: in Standby the compositor stops rendering
|
|
// and the headset view is static. Refresh once a second regardless.
|
|
// Both reads are shared memory.
|
|
vr::Compositor_FrameTiming timing{};
|
|
timing.m_nSize = sizeof(timing);
|
|
bool new_index = !compositor->GetFrameTiming(&timing, 0) ||
|
|
timing.m_nFrameIndex != last_index;
|
|
auto now = std::chrono::steady_clock::now();
|
|
if (!resized && !new_index && now - last_present < std::chrono::seconds(1) &&
|
|
system->GetTrackedDeviceActivityLevel(vr::k_unTrackedDeviceIndex_Hmd) ==
|
|
vr::k_EDeviceActivityLevel_Standby)
|
|
return until_next_frame(system);
|
|
|
|
vr::VROverlayHandle_t handle{};
|
|
if (overlay->FindOverlay(vr::k_pchHeadsetViewOverlayKey, &handle) !=
|
|
vr::VROverlayError_None) {
|
|
release();
|
|
hide();
|
|
set_error("Open VR View to preview");
|
|
return retry_us;
|
|
}
|
|
if (holding && view.overlayHandle != handle)
|
|
release();
|
|
if (!holding && !api->IsViewingPermitted(handle)) {
|
|
hide();
|
|
set_error("Headset preview access unavailable");
|
|
return retry_us;
|
|
}
|
|
vr::VRVulkanDevice_t vulkan{instance, device, physical, queue, family};
|
|
vr::VRNativeDevice_t native{&vulkan, vr::DeviceType_Vulkan};
|
|
auto acquired_view =
|
|
api->AcquireOverlayView(handle, &native, &view, sizeof(view));
|
|
// Even a failed or empty view may own native resources.
|
|
holding = true;
|
|
if (acquired_view != vr::VROverlayError_None) {
|
|
release();
|
|
hide();
|
|
set_error("Headset preview acquisition error " +
|
|
std::to_string(acquired_view));
|
|
return retry_us;
|
|
}
|
|
if (!view.texture.handle) {
|
|
hide();
|
|
set_error(system->GetTrackedDeviceActivityLevel(
|
|
vr::k_unTrackedDeviceIndex_Hmd) ==
|
|
vr::k_EDeviceActivityLevel_Standby
|
|
? "Put on the headset to preview"
|
|
: "Headset view has no frame");
|
|
return retry_us;
|
|
}
|
|
if (view.texture.eType != vr::TextureType_Vulkan)
|
|
throw std::runtime_error("Headset preview texture type unsupported");
|
|
const auto &texture =
|
|
*static_cast<vr::VRVulkanTextureData_t *>(view.texture.handle);
|
|
const auto &bounds = view.textureBounds;
|
|
if (texture.m_pDevice != device || texture.m_nQueueFamilyIndex != family ||
|
|
texture.m_nSampleCount != 1 || !texture.m_nWidth ||
|
|
!texture.m_nHeight || texture.m_nWidth > 16384 ||
|
|
texture.m_nHeight > 16384)
|
|
throw std::runtime_error("Headset preview texture unsupported");
|
|
for (float value : {bounds.uMin, bounds.uMax, bounds.vMin, bounds.vMax})
|
|
if (!std::isfinite(value) || value < 0 || value > 1)
|
|
throw std::runtime_error("Headset preview bounds invalid");
|
|
auto texture_format = VkFormat(texture.m_nFormat);
|
|
if (texture.m_nWidth != source_width ||
|
|
texture.m_nHeight != source_height || texture_format != source_format) {
|
|
// A new size or format means the producer reallocated. Free the old
|
|
// imports, then pick up the new image.
|
|
VkFormatProperties properties{};
|
|
vkGetPhysicalDeviceFormatProperties(physical, texture_format,
|
|
&properties);
|
|
if (!(properties.optimalTilingFeatures &
|
|
VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT))
|
|
throw std::runtime_error("Headset preview format unsupported");
|
|
forget_sources();
|
|
source_width = texture.m_nWidth;
|
|
source_height = texture.m_nHeight;
|
|
source_format = texture_format;
|
|
}
|
|
const Source *source = source_for(texture);
|
|
if (!source) {
|
|
release();
|
|
return 0;
|
|
}
|
|
|
|
resized = ensure_swapchain(target) || resized;
|
|
uint32_t index = 0;
|
|
auto next = vkAcquireNextImageKHR(device, swapchain, 0, acquired,
|
|
VK_NULL_HANDLE, &index);
|
|
if (next == VK_ERROR_OUT_OF_DATE_KHR) {
|
|
stale = true;
|
|
return 0;
|
|
}
|
|
if (next == VK_NOT_READY || next == VK_TIMEOUT)
|
|
return until_next_frame(system);
|
|
if (next == VK_SUBOPTIMAL_KHR)
|
|
stale = true;
|
|
else
|
|
check(next, "image acquire");
|
|
|
|
float du = std::abs(bounds.uMax - bounds.uMin) * texture.m_nWidth;
|
|
float dv = std::abs(bounds.vMax - bounds.vMin) * texture.m_nHeight;
|
|
float fit = std::min(extent.width / du, extent.height / dv);
|
|
VkViewport viewport{};
|
|
viewport.width = std::round(du * fit);
|
|
viewport.height = std::round(dv * fit);
|
|
viewport.x = std::floor((extent.width - viewport.width) / 2);
|
|
viewport.y = std::floor((extent.height - viewport.height) / 2);
|
|
viewport.maxDepth = 1;
|
|
VkRect2D scissor{{int32_t(viewport.x), int32_t(viewport.y)},
|
|
{uint32_t(viewport.width), uint32_t(viewport.height)}};
|
|
|
|
check(vkResetCommandBuffer(command, 0), "command reset");
|
|
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
|
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
check(vkBeginCommandBuffer(command, &begin), "command begin");
|
|
// The acquired image is sampleable and stable. Never write or transition
|
|
// it; the fence wait keeps reads ahead of the next acquire/release.
|
|
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_FRAGMENT_SHADER_BIT, 0, 1, &readable,
|
|
0, nullptr, 0, nullptr);
|
|
VkClearValue black{};
|
|
black.color.float32[3] = 1;
|
|
VkRenderPassBeginInfo pass_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
|
|
pass_begin.renderPass = pass;
|
|
pass_begin.framebuffer = targets[index].framebuffer;
|
|
pass_begin.renderArea.extent = extent;
|
|
pass_begin.clearValueCount = 1;
|
|
pass_begin.pClearValues = &black;
|
|
vkCmdBeginRenderPass(command, &pass_begin, VK_SUBPASS_CONTENTS_INLINE);
|
|
vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
|
vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS,
|
|
pipeline_layout, 0, 1, &source->set, 0, nullptr);
|
|
vkCmdSetViewport(command, 0, 1, &viewport);
|
|
vkCmdSetScissor(command, 0, 1, &scissor);
|
|
struct {
|
|
float bounds[4];
|
|
uint32_t srgb;
|
|
} push{{bounds.uMin, bounds.vMin, bounds.uMax, bounds.vMax},
|
|
uint32_t(srgb(texture_format))};
|
|
vkCmdPushConstants(command, pipeline_layout,
|
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
|
0, sizeof(push), &push);
|
|
vkCmdDraw(command, 3, 1, 0, 0);
|
|
vkCmdEndRenderPass(command);
|
|
check(vkEndCommandBuffer(command), "command end");
|
|
check(vkResetFences(device, 1, &fence), "fence reset");
|
|
VkPipelineStageFlags wait_stage =
|
|
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
|
|
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
|
submit.waitSemaphoreCount = 1;
|
|
submit.pWaitSemaphores = &acquired;
|
|
submit.pWaitDstStageMask = &wait_stage;
|
|
submit.commandBufferCount = 1;
|
|
submit.pCommandBuffers = &command;
|
|
submit.signalSemaphoreCount = 1;
|
|
submit.pSignalSemaphores = &targets[index].rendered;
|
|
check(vkQueueSubmit(queue, 1, &submit, fence), "submit");
|
|
VkPresentInfoKHR present{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
|
|
present.waitSemaphoreCount = 1;
|
|
present.pWaitSemaphores = &targets[index].rendered;
|
|
present.swapchainCount = 1;
|
|
present.pSwapchains = &swapchain;
|
|
present.pImageIndices = &index;
|
|
auto presented = vkQueuePresentKHR(queue, &present);
|
|
if (presented == VK_ERROR_OUT_OF_DATE_KHR ||
|
|
presented == VK_SUBOPTIMAL_KHR)
|
|
stale = true;
|
|
else
|
|
check(presented, "present");
|
|
mapped = true;
|
|
last_frame = frame;
|
|
last_index = timing.m_nFrameIndex;
|
|
last_present = now;
|
|
{
|
|
std::lock_guard guard(status_lock);
|
|
status_error.clear();
|
|
++status_presented;
|
|
status_width = texture.m_nWidth;
|
|
status_height = texture.m_nHeight;
|
|
}
|
|
return until_next_frame(system);
|
|
}
|
|
|
|
~Renderer() {
|
|
if (device)
|
|
vkDeviceWaitIdle(device);
|
|
release();
|
|
hide();
|
|
if (surface)
|
|
vkDestroySurfaceKHR(instance, surface, nullptr);
|
|
if (pipeline)
|
|
vkDestroyPipeline(device, pipeline, nullptr);
|
|
if (pass)
|
|
vkDestroyRenderPass(device, pass, nullptr);
|
|
if (pipeline_layout)
|
|
vkDestroyPipelineLayout(device, pipeline_layout, nullptr);
|
|
if (descriptor_pool)
|
|
vkDestroyDescriptorPool(device, descriptor_pool, nullptr);
|
|
if (set_layout)
|
|
vkDestroyDescriptorSetLayout(device, set_layout, nullptr);
|
|
if (sampler)
|
|
vkDestroySampler(device, sampler, nullptr);
|
|
if (acquired)
|
|
vkDestroySemaphore(device, acquired, 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<Renderer> renderer;
|
|
} // namespace
|
|
|
|
extern "C" void rifts_surface_place(void *webview, int x, int y, int width,
|
|
int height, bool visible) {
|
|
auto *widget = static_cast<_GtkWidget *>(webview);
|
|
_GtkWidget *top = gtk_widget_get_toplevel(widget);
|
|
_GdkWindow *window = top ? gtk_widget_get_window(top) : nullptr;
|
|
std::lock_guard guard(layout_lock);
|
|
if (!window) {
|
|
// Not realized yet; the frontend reports again on the next resize.
|
|
layout.visible = false;
|
|
return;
|
|
}
|
|
if (!g_type_check_instance_is_a(window, gdk_wayland_window_get_type()))
|
|
wayland.unavailable = true;
|
|
wl_surface *parent =
|
|
wayland.unavailable ? nullptr : gdk_wayland_window_get_wl_surface(window);
|
|
if (parent && !wayland.display)
|
|
wayland.unavailable = !connect_wayland(gdk_wayland_display_get_wl_display(
|
|
gdk_window_get_display(window)));
|
|
if (!parent || wayland.unavailable) {
|
|
layout.visible = false;
|
|
return;
|
|
}
|
|
wl_display_dispatch_queue_pending(wayland.display, wayland.queue);
|
|
if (parent != wayland.parent) {
|
|
// GTK recreates the toplevel surface when the window is re-shown.
|
|
if (wayland.subsurface)
|
|
wl_subsurface_destroy(wayland.subsurface);
|
|
wayland.subsurface = wl_subcompositor_get_subsurface(
|
|
wayland.subcompositor, wayland.surface, parent);
|
|
wl_subsurface_set_desync(wayland.subsurface);
|
|
wayland.parent = parent;
|
|
}
|
|
int origin_x = 0, origin_y = 0;
|
|
gtk_widget_translate_coordinates(widget, top, 0, 0, &origin_x, &origin_y);
|
|
wl_subsurface_set_position(wayland.subsurface, origin_x + x, origin_y + y);
|
|
// Position is parent state; make GTK commit the toplevel once more.
|
|
gtk_widget_queue_draw(top);
|
|
layout = {x, y, width, height, std::max(1, gdk_window_get_scale_factor(window)),
|
|
visible && width > 0 && height > 0};
|
|
}
|
|
|
|
int64_t rifts_headset_preview_step(vr::IVRSystem *system,
|
|
vr::IVROverlay *overlay,
|
|
vr::IVROverlayView *api,
|
|
vr::IVRCompositor *compositor) {
|
|
Layout target;
|
|
bool unavailable;
|
|
{
|
|
std::lock_guard guard(layout_lock);
|
|
target = layout;
|
|
unavailable = wayland.unavailable;
|
|
}
|
|
if (!target.visible) {
|
|
if (renderer) {
|
|
renderer->release();
|
|
renderer->hide();
|
|
}
|
|
if (unavailable)
|
|
set_error("Headset preview needs a Wayland session");
|
|
return -1;
|
|
}
|
|
if (!system || !overlay || !api || !compositor) {
|
|
rifts_headset_preview_shutdown();
|
|
set_error("SteamVR is not running");
|
|
return retry_us;
|
|
}
|
|
try {
|
|
if (!renderer) {
|
|
auto fresh = std::make_unique<Renderer>();
|
|
fresh->initialize(system, compositor);
|
|
renderer = std::move(fresh);
|
|
}
|
|
renderer->api = api;
|
|
return renderer->step(system, overlay, compositor, target);
|
|
} catch (const std::exception &failure) {
|
|
set_error(failure.what());
|
|
// Start from a clean device on the next attempt.
|
|
renderer.reset();
|
|
return retry_us;
|
|
}
|
|
}
|
|
|
|
void rifts_headset_preview_shutdown() { renderer.reset(); }
|
|
|
|
extern "C" size_t rifts_preview_status(uint64_t *presented, uint32_t *width,
|
|
uint32_t *height, char *error,
|
|
size_t capacity) {
|
|
std::lock_guard guard(status_lock);
|
|
*presented = status_presented;
|
|
*width = status_width;
|
|
*height = status_height;
|
|
size_t length = std::min(status_error.size(), capacity);
|
|
std::memcpy(error, status_error.data(), length);
|
|
return length;
|
|
}
|