File stereo_renderer.cpp
File List > openxr_interface > stereo_renderer.cpp
Go to the documentation of this file
#include "stereo_renderer.hpp"
#include "color_frag_spv.h"
#include "color_vert_spv.h"
#include "depth_frag_spv.h"
#include "motion_vec_frag_spv.h"
#ifdef ILLIXR_ENABLE_BOBA
# include "modal_frag_spv.h"
# include "modal_vert_spv.h"
# include "overlay_frag_spv.h"
# include "overlay_vert_spv.h"
#endif
#include
#include
#include
#include
#include
#include
using namespace ILLIXR;
using namespace ILLIXR::data_format;
//
// Helpers
//
#define VK_CHECK(expr) \
do { \
VkResult _vk_result = (expr); \
if (_vk_result != VK_SUCCESS) { \
spdlog::get("illixr")->error("[stereo_renderer] Vulkan error {} at {}:{}", static_cast(_vk_result), __FILE__, \
__LINE__); \
return false; \
} \
} while (0)
stereo_renderer::~stereo_renderer() {
cleanup();
}
void stereo_renderer::set_crop_region(int original_width, int original_height, int padded_width, int padded_height) {
crop_scale_x_ = static_cast<float>(original_width) / static_cast<float>(padded_width);
crop_scale_y_ = static_cast<float>(original_height) / static_cast<float>(padded_height);
spdlog::get("illixr")->info("[stereo_renderer] Crop {}x{} → {}x{} (scale {:.4f},{:.4f})", padded_width, padded_height,
original_width, original_height, crop_scale_x_, crop_scale_y_);
}
//
// Initialization
//
bool stereo_renderer::initialize(VkInstance instance, VkPhysicalDevice physical_device, VkDevice device, VkQueue queue,
uint32_t queue_family, VkFormat swapchain_format) {
if (initialized_) {
spdlog::get("illixr")->warn("stereo_renderer already initialized");
return true;
}
instance_ = instance;
physical_device_ = physical_device;
device_ = device;
queue_ = queue;
queue_family_ = queue_family;
swapchain_format_ = swapchain_format;
image_cache_.reserve(32);
// Resolve the Android hardware buffer properties extension.
vk_get_ahb_properties_ = reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(
vkGetDeviceProcAddr(device_, "vkGetAndroidHardwareBufferPropertiesANDROID"));
if (!vk_get_ahb_properties_) {
spdlog::get("illixr")->error("[stereo_renderer] VK_ANDROID_external_memory_android_hardware_buffer "
"extension not available");
return false;
}
if (!create_render_pass())
return false;
if (!create_command_pool())
return false;
if (!allocate_command_buffers())
return false;
if (!create_descriptor_pool())
return false;
#ifdef ILLIXR_ENABLE_BOBA
if (!create_boba_overlay_resources())
return false;
#endif
// Fences for render completion
VkFenceCreateInfo fence_info{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT; // start signalled (nothing in flight)
for (int i = 0; i < 2; i++) {
VK_CHECK(vkCreateFence(device_, &fence_info, nullptr, &render_fences_[i]));
}
initialized_ = true;
spdlog::get("illixr")->info("[stereo_renderer] Vulkan renderer initialized");
return true;
}
void stereo_renderer::destroy_imported_image(imported_image& img) {
if (img.image_view != VK_NULL_HANDLE) {
vkDestroyImageView(device_, img.image_view, nullptr);
img.image_view = VK_NULL_HANDLE;
}
if (img.image != VK_NULL_HANDLE) {
vkDestroyImage(device_, img.image, nullptr);
img.image = VK_NULL_HANDLE;
}
if (img.memory != VK_NULL_HANDLE) {
vkFreeMemory(device_, img.memory, nullptr);
img.memory = VK_NULL_HANDLE;
}
if (img.sampler != VK_NULL_HANDLE) {
vkDestroySampler(device_, img.sampler, nullptr);
img.sampler = VK_NULL_HANDLE;
}
if (img.ycbcr_conv != VK_NULL_HANDLE) {
vkDestroySamplerYcbcrConversion(device_, img.ycbcr_conv, nullptr);
img.ycbcr_conv = VK_NULL_HANDLE;
}
}
//
// Render pass
//
bool stereo_renderer::create_render_pass() {
VkAttachmentDescription color_attachment{};
color_attachment.format = swapchain_format_;
color_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
color_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
color_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
color_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
// OpenXR expects COLOR_ATTACHMENT_OPTIMAL when it acquires the image back.
color_attachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference color_ref{};
color_ref.attachment = 0;
color_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_ref;
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.srcAccessMask = 0;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rp_info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
rp_info.attachmentCount = 1;
rp_info.pAttachments = &color_attachment;
rp_info.subpassCount = 1;
rp_info.pSubpasses = &subpass;
rp_info.dependencyCount = 1;
rp_info.pDependencies = &dependency;
VK_CHECK(vkCreateRenderPass(device_, &rp_info, nullptr, &render_pass_));
return true;
}
//
// Pipeline (deferred — created on first frame, once we have a prototype image
// from which we can extract the external format and build the sampler)
//
bool stereo_renderer::create_pipeline(const imported_image& prototype) {
if (pipeline_created_)
return true;
// Descriptor set layout with immutable YCbCr sampler
// YCbCr combined-image-samplers MUST use immutable samplers in the layout.
VkDescriptorSetLayoutBinding binding{};
binding.binding = 0;
binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
binding.descriptorCount = 1;
binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
binding.pImmutableSamplers = &prototype.sampler;
VkDescriptorSetLayoutCreateInfo layout_info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layout_info.bindingCount = 1;
layout_info.pBindings = &binding;
VK_CHECK(vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &desc_set_layout_));
// Allocate descriptor sets (one per eye)
std::vector<VkDescriptorSetLayout> layouts(2, desc_set_layout_);
VkDescriptorSetAllocateInfo alloc_info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc_info.descriptorPool = descriptor_pool_;
alloc_info.descriptorSetCount = 2;
alloc_info.pSetLayouts = layouts.data();
VK_CHECK(vkAllocateDescriptorSets(device_, &alloc_info, descriptor_sets_.data()));
// Push constants (crop scale + u_offset)
VkPushConstantRange push_range{};
push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
push_range.offset = 0;
#ifdef COMBINED_ENCODING
push_range.size = sizeof(float) * 3; // crop_scale_x, crop_scale_y, u_offset
#else
push_range.size = sizeof(float) * 2; // crop_scale_x, crop_scale_y
#endif
VkPipelineLayoutCreateInfo pl_info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pl_info.setLayoutCount = 1;
pl_info.pSetLayouts = &desc_set_layout_;
pl_info.pushConstantRangeCount = 1;
pl_info.pPushConstantRanges = &push_range;
VK_CHECK(vkCreatePipelineLayout(device_, &pl_info, nullptr, &pipeline_layout_));
// Shaders
VkShaderModule vert_module = create_shader_module(device_, color_vert_spv, sizeof(color_vert_spv) / sizeof(uint32_t));
VkShaderModule frag_module = create_shader_module(device_, color_frag_spv, sizeof(color_frag_spv) / sizeof(uint32_t));
if (vert_module == VK_NULL_HANDLE || frag_module == VK_NULL_HANDLE) {
spdlog::get("illixr")->error("[stereo_renderer] Shader compilation failed");
return false;
}
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = vert_module;
stages[0].pName = "main";
stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = frag_module;
stages[1].pName = "main";
// Fixed-function state
VkPipelineVertexInputStateCreateInfo vertex_input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
// No vertex buffers — we generate vertices from gl_VertexIndex.
VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo vp_state{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
vp_state.viewportCount = 1;
vp_state.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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState blend_attachment{};
blend_attachment.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = 1;
blend.pAttachments = &blend_attachment;
// Viewport and scissor are dynamic so we can change them per-frame.
constexpr VkDynamicState dyn_states[] = {VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dyn{VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO};
dyn.dynamicStateCount = 2;
dyn.pDynamicStates = dyn_states;
VkGraphicsPipelineCreateInfo gp_info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gp_info.stageCount = 2;
gp_info.pStages = stages;
gp_info.pVertexInputState = &vertex_input;
gp_info.pInputAssemblyState = &ia;
gp_info.pViewportState = &vp_state;
gp_info.pRasterizationState = &raster;
gp_info.pMultisampleState = &ms;
gp_info.pColorBlendState = &blend;
gp_info.pDynamicState = &dyn;
gp_info.layout = pipeline_layout_;
gp_info.renderPass = render_pass_;
gp_info.subpass = 0;
VkResult result = vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &gp_info, nullptr, &pipeline_);
vkDestroyShaderModule(device_, vert_module, nullptr);
vkDestroyShaderModule(device_, frag_module, nullptr);
if (result != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] vkCreateGraphicsPipelines failed: {}", static_cast<int>(result));
// Release all resources allocated in this attempt so that:
// (a) the descriptor pool does not fill up on repeated failures
// (pool exhaustion returns VK_ERROR_OUT_OF_POOL_MEMORY = -1000069000
// from vkAllocateDescriptorSets on the next call), and
// (b) the next attempt starts from a clean slate.
vkFreeDescriptorSets(device_, descriptor_pool_, 2, descriptor_sets_.data());
descriptor_sets_.fill(VK_NULL_HANDLE);
vkDestroyPipelineLayout(device_, pipeline_layout_, nullptr);
pipeline_layout_ = VK_NULL_HANDLE;
vkDestroyDescriptorSetLayout(device_, desc_set_layout_, nullptr);
desc_set_layout_ = VK_NULL_HANDLE;
return false;
}
pipeline_created_ = true;
spdlog::get("illixr")->info("[stereo_renderer] Pipeline created");
return true;
}
#ifdef ILLIXR_ENABLE_BOBA
// ---- Boba vector and modal overlay resources -------------------------------
std::uint32_t stereo_renderer::find_memory_type(std::uint32_t type_filter, VkMemoryPropertyFlags properties) const {
VkPhysicalDeviceMemoryProperties memory_properties{};
vkGetPhysicalDeviceMemoryProperties(physical_device_, &memory_properties);
for (std::uint32_t index = 0; index < memory_properties.memoryTypeCount; ++index) {
if ((type_filter & (1U << index)) != 0 &&
(memory_properties.memoryTypes[index].propertyFlags & properties) == properties) {
return index;
}
}
return UINT32_MAX;
}
bool stereo_renderer::create_host_visible_buffer(VkDeviceSize size, VkBufferUsageFlags usage, VkBuffer* buffer,
VkDeviceMemory* memory, void** mapped) {
VkBufferCreateInfo buffer_info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
buffer_info.size = size;
buffer_info.usage = usage;
buffer_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(device_, &buffer_info, nullptr, buffer) != VK_SUCCESS) {
return false;
}
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(device_, *buffer, &requirements);
const std::uint32_t memory_type = find_memory_type(
requirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
if (memory_type == UINT32_MAX) {
vkDestroyBuffer(device_, *buffer, nullptr);
*buffer = VK_NULL_HANDLE;
return false;
}
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = memory_type;
if (vkAllocateMemory(device_, &allocation, nullptr, memory) != VK_SUCCESS) {
vkDestroyBuffer(device_, *buffer, nullptr);
*buffer = VK_NULL_HANDLE;
return false;
}
if (vkBindBufferMemory(device_, *buffer, *memory, 0) != VK_SUCCESS ||
vkMapMemory(device_, *memory, 0, size, 0, mapped) != VK_SUCCESS) {
vkDestroyBuffer(device_, *buffer, nullptr);
vkFreeMemory(device_, *memory, nullptr);
*memory = VK_NULL_HANDLE;
*buffer = VK_NULL_HANDLE;
return false;
}
return true;
}
bool stereo_renderer::create_overlay_pipeline() {
VkPushConstantRange push_range{};
push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
push_range.size = 2 * sizeof(float);
VkPipelineLayoutCreateInfo layout_info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
layout_info.pushConstantRangeCount = 1;
layout_info.pPushConstantRanges = &push_range;
VK_CHECK(vkCreatePipelineLayout(device_, &layout_info, nullptr, &overlay_pipeline_layout_));
VkShaderModule vert = create_shader_module(device_, overlay_vert_spv, sizeof(overlay_vert_spv) / sizeof(uint32_t));
VkShaderModule frag = create_shader_module(device_, overlay_frag_spv, sizeof(overlay_frag_spv) / sizeof(uint32_t));
if (vert == VK_NULL_HANDLE || frag == VK_NULL_HANDLE) {
return false;
}
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0] = {
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_VERTEX_BIT, vert, "main", nullptr};
stages[1] = {
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_FRAGMENT_BIT, frag, "main", nullptr};
VkVertexInputBindingDescription binding{};
binding.binding = 0;
binding.stride = sizeof(overlay_vertex);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
std::array<VkVertexInputAttributeDescription, 2> attributes{};
attributes[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(overlay_vertex, x)};
attributes[1] = {1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(overlay_vertex, red)};
VkPipelineVertexInputStateCreateInfo vertex_input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
vertex_input.vertexBindingDescriptionCount = 1;
vertex_input.pVertexBindingDescriptions = &binding;
vertex_input.vertexAttributeDescriptionCount = static_cast<std::uint32_t>(attributes.size());
vertex_input.pVertexAttributeDescriptions = attributes.data();
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 = 1;
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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0F;
VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState blend_attachment{};
blend_attachment.blendEnable = VK_TRUE;
blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
blend_attachment.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = 1;
blend.pAttachments = &blend_attachment;
constexpr 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 pipeline_info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
pipeline_info.stageCount = 2;
pipeline_info.pStages = stages;
pipeline_info.pVertexInputState = &vertex_input;
pipeline_info.pInputAssemblyState = &assembly;
pipeline_info.pViewportState = &viewport;
pipeline_info.pRasterizationState = &raster;
pipeline_info.pMultisampleState = &multisample;
pipeline_info.pColorBlendState = &blend;
pipeline_info.pDynamicState = &dynamic;
pipeline_info.layout = overlay_pipeline_layout_;
pipeline_info.renderPass = render_pass_;
pipeline_info.subpass = 0;
const VkResult result = vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &overlay_pipeline_);
vkDestroyShaderModule(device_, vert, nullptr);
vkDestroyShaderModule(device_, frag, nullptr);
return result == VK_SUCCESS;
}
bool stereo_renderer::create_modal_pipeline() {
VkSamplerCreateInfo sampler_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_info.magFilter = VK_FILTER_LINEAR;
sampler_info.minFilter = VK_FILTER_LINEAR;
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.unnormalizedCoordinates = VK_FALSE;
VK_CHECK(vkCreateSampler(device_, &sampler_info, nullptr, &modal_sampler_));
VkDescriptorSetLayoutBinding descriptor_binding{};
descriptor_binding.binding = 0;
descriptor_binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptor_binding.descriptorCount = 1;
descriptor_binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
VkDescriptorSetLayoutCreateInfo descriptor_layout{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
descriptor_layout.bindingCount = 1;
descriptor_layout.pBindings = &descriptor_binding;
VK_CHECK(vkCreateDescriptorSetLayout(device_, &descriptor_layout, nullptr, &modal_desc_set_layout_));
VkDescriptorPoolSize pool_size{};
pool_size.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
pool_size.descriptorCount = 1;
VkDescriptorPoolCreateInfo pool_info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool_info.maxSets = 1;
pool_info.poolSizeCount = 1;
pool_info.pPoolSizes = &pool_size;
VK_CHECK(vkCreateDescriptorPool(device_, &pool_info, nullptr, &modal_descriptor_pool_));
VkDescriptorSetAllocateInfo descriptor_allocation{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
descriptor_allocation.descriptorPool = modal_descriptor_pool_;
descriptor_allocation.descriptorSetCount = 1;
descriptor_allocation.pSetLayouts = &modal_desc_set_layout_;
VK_CHECK(vkAllocateDescriptorSets(device_, &descriptor_allocation, &modal_descriptor_set_));
VkPushConstantRange push_range{};
push_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
push_range.size = 2 * sizeof(float);
VkPipelineLayoutCreateInfo layout_info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
layout_info.setLayoutCount = 1;
layout_info.pSetLayouts = &modal_desc_set_layout_;
layout_info.pushConstantRangeCount = 1;
layout_info.pPushConstantRanges = &push_range;
VK_CHECK(vkCreatePipelineLayout(device_, &layout_info, nullptr, &modal_pipeline_layout_));
VkShaderModule vert = create_shader_module(device_, modal_vert_spv, sizeof(modal_vert_spv) / sizeof(uint32_t));
VkShaderModule frag = create_shader_module(device_, modal_frag_spv, sizeof(modal_frag_spv) / sizeof(uint32_t));
if (vert == VK_NULL_HANDLE || frag == VK_NULL_HANDLE) {
return false;
}
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0] = {
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_VERTEX_BIT, vert, "main", nullptr};
stages[1] = {
VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, nullptr, 0, VK_SHADER_STAGE_FRAGMENT_BIT, frag, "main", nullptr};
VkVertexInputBindingDescription binding{};
binding.binding = 0;
binding.stride = sizeof(modal_vertex);
binding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
std::array<VkVertexInputAttributeDescription, 2> attributes{};
attributes[0] = {0, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(modal_vertex, x)};
attributes[1] = {1, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(modal_vertex, u)};
VkPipelineVertexInputStateCreateInfo vertex_input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
vertex_input.vertexBindingDescriptionCount = 1;
vertex_input.pVertexBindingDescriptions = &binding;
vertex_input.vertexAttributeDescriptionCount = static_cast<std::uint32_t>(attributes.size());
vertex_input.pVertexAttributeDescriptions = attributes.data();
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 = 1;
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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0F;
VkPipelineMultisampleStateCreateInfo multisample{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
multisample.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineColorBlendAttachmentState blend_attachment{};
blend_attachment.blendEnable = VK_TRUE;
blend_attachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
blend_attachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment.colorBlendOp = VK_BLEND_OP_ADD;
blend_attachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
blend_attachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
blend_attachment.alphaBlendOp = VK_BLEND_OP_ADD;
blend_attachment.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = 1;
blend.pAttachments = &blend_attachment;
constexpr 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 pipeline_info{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
pipeline_info.stageCount = 2;
pipeline_info.pStages = stages;
pipeline_info.pVertexInputState = &vertex_input;
pipeline_info.pInputAssemblyState = &assembly;
pipeline_info.pViewportState = &viewport;
pipeline_info.pRasterizationState = &raster;
pipeline_info.pMultisampleState = &multisample;
pipeline_info.pColorBlendState = &blend;
pipeline_info.pDynamicState = &dynamic;
pipeline_info.layout = modal_pipeline_layout_;
pipeline_info.renderPass = render_pass_;
pipeline_info.subpass = 0;
const VkResult result = vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &pipeline_info, nullptr, &modal_pipeline_);
vkDestroyShaderModule(device_, vert, nullptr);
vkDestroyShaderModule(device_, frag, nullptr);
return result == VK_SUCCESS;
}
bool stereo_renderer::create_boba_overlay_resources() {
if (!create_overlay_pipeline() || !create_modal_pipeline()) {
spdlog::get("illixr")->error("[stereo_renderer] Could not create Boba overlay pipelines");
return false;
}
constexpr VkDeviceSize overlay_buffer_bytes =
sizeof(overlay_vertex) * data_format::boba_frame_overlay::max_commands_per_eye * 6ULL;
constexpr VkDeviceSize modal_buffer_bytes = sizeof(modal_vertex) * 6ULL;
for (int eye = 0; eye < 2; ++eye) {
if (!create_host_visible_buffer(overlay_buffer_bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, &overlay_vertex_buffers_[eye],
&overlay_vertex_memories_[eye], &overlay_vertex_mapped_[eye]) ||
!create_host_visible_buffer(modal_buffer_bytes, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, &modal_vertex_buffers_[eye],
&modal_vertex_memories_[eye], &modal_vertex_mapped_[eye])) {
spdlog::get("illixr")->error("[stereo_renderer] Could not allocate Boba overlay vertex buffers");
return false;
}
overlay_vertices_[eye].reserve(data_format::boba_frame_overlay::max_commands_per_eye * 6ULL);
}
spdlog::get("illixr")->info("[stereo_renderer] Boba overlay pipelines initialized");
return true;
}
void stereo_renderer::destroy_modal_texture() {
if (modal_image_view_ != VK_NULL_HANDLE) {
vkDestroyImageView(device_, modal_image_view_, nullptr);
modal_image_view_ = VK_NULL_HANDLE;
}
if (modal_image_ != VK_NULL_HANDLE) {
vkDestroyImage(device_, modal_image_, nullptr);
modal_image_ = VK_NULL_HANDLE;
}
if (modal_image_memory_ != VK_NULL_HANDLE) {
vkFreeMemory(device_, modal_image_memory_, nullptr);
modal_image_memory_ = VK_NULL_HANDLE;
}
modal_texture_id_ = 0;
}
bool stereo_renderer::upload_modal_texture(std::uint64_t texture_id, std::uint32_t width, std::uint32_t height,
const std::vector<std::uint8_t>& rgba) {
const std::uint64_t expected_size = static_cast<std::uint64_t>(width) * height * 4ULL;
if (texture_id == 0 || width == 0 || height == 0 || expected_size != rgba.size()) {
return false;
}
// Stage host pixels and build a replacement image without disturbing the
// currently renderable modal. Ownership is swapped only after upload ends.
VkBuffer staging_buffer = VK_NULL_HANDLE;
VkDeviceMemory staging_memory = VK_NULL_HANDLE;
void* staging_mapped = nullptr;
if (!create_host_visible_buffer(expected_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, &staging_buffer, &staging_memory,
&staging_mapped)) {
spdlog::get("illixr")->error("[stereo_renderer] Could not allocate Boba modal staging buffer");
return false;
}
std::memcpy(staging_mapped, rgba.data(), rgba.size());
vkUnmapMemory(device_, staging_memory);
staging_mapped = nullptr;
VkImage new_image = VK_NULL_HANDLE;
VkDeviceMemory new_memory = VK_NULL_HANDLE;
VkImageView new_view = VK_NULL_HANDLE;
VkCommandBuffer upload_command = VK_NULL_HANDLE;
const auto release_new_resources = [&] {
if (upload_command != VK_NULL_HANDLE) {
vkFreeCommandBuffers(device_, command_pool_, 1, &upload_command);
upload_command = VK_NULL_HANDLE;
}
if (new_view != VK_NULL_HANDLE) {
vkDestroyImageView(device_, new_view, nullptr);
new_view = VK_NULL_HANDLE;
}
if (new_image != VK_NULL_HANDLE) {
vkDestroyImage(device_, new_image, nullptr);
new_image = VK_NULL_HANDLE;
}
if (new_memory != VK_NULL_HANDLE) {
vkFreeMemory(device_, new_memory, nullptr);
new_memory = VK_NULL_HANDLE;
}
if (staging_buffer != VK_NULL_HANDLE) {
vkDestroyBuffer(device_, staging_buffer, nullptr);
staging_buffer = VK_NULL_HANDLE;
}
if (staging_memory != VK_NULL_HANDLE) {
vkFreeMemory(device_, staging_memory, nullptr);
staging_memory = VK_NULL_HANDLE;
}
};
VkImageCreateInfo image_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
image_info.imageType = VK_IMAGE_TYPE_2D;
image_info.format = VK_FORMAT_R8G8B8A8_UNORM;
image_info.extent = {width, height, 1};
image_info.mipLevels = 1;
image_info.arrayLayers = 1;
image_info.samples = VK_SAMPLE_COUNT_1_BIT;
image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
image_info.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vkCreateImage(device_, &image_info, nullptr, &new_image) != VK_SUCCESS) {
release_new_resources();
return false;
}
VkMemoryRequirements image_requirements{};
vkGetImageMemoryRequirements(device_, new_image, &image_requirements);
const std::uint32_t image_memory_type =
find_memory_type(image_requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (image_memory_type == UINT32_MAX) {
release_new_resources();
return false;
}
VkMemoryAllocateInfo image_allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
image_allocation.allocationSize = image_requirements.size;
image_allocation.memoryTypeIndex = image_memory_type;
if (vkAllocateMemory(device_, &image_allocation, nullptr, &new_memory) != VK_SUCCESS ||
vkBindImageMemory(device_, new_image, new_memory, 0) != VK_SUCCESS) {
release_new_resources();
return false;
}
VkCommandBufferAllocateInfo command_allocation{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
command_allocation.commandPool = command_pool_;
command_allocation.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
command_allocation.commandBufferCount = 1;
if (vkAllocateCommandBuffers(device_, &command_allocation, &upload_command) != VK_SUCCESS) {
release_new_resources();
return false;
}
VkCommandBufferBeginInfo command_begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
command_begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
if (vkBeginCommandBuffer(upload_command, &command_begin) != VK_SUCCESS) {
release_new_resources();
return false;
}
VkImageMemoryBarrier to_transfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
to_transfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
to_transfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
to_transfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_transfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_transfer.image = new_image;
to_transfer.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
to_transfer.subresourceRange.levelCount = 1;
to_transfer.subresourceRange.layerCount = 1;
to_transfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
vkCmdPipelineBarrier(upload_command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0,
nullptr, 1, &to_transfer);
VkBufferImageCopy copy_region{};
copy_region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
copy_region.imageSubresource.layerCount = 1;
copy_region.imageExtent = {width, height, 1};
vkCmdCopyBufferToImage(upload_command, staging_buffer, new_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©_region);
VkImageMemoryBarrier to_shader_read{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
to_shader_read.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
to_shader_read.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
to_shader_read.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_shader_read.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_shader_read.image = new_image;
to_shader_read.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
to_shader_read.subresourceRange.levelCount = 1;
to_shader_read.subresourceRange.layerCount = 1;
to_shader_read.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
to_shader_read.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(upload_command, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr,
0, nullptr, 1, &to_shader_read);
if (vkEndCommandBuffer(upload_command) != VK_SUCCESS) {
release_new_resources();
return false;
}
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &upload_command;
if (vkQueueSubmit(queue_, 1, &submit, VK_NULL_HANDLE) != VK_SUCCESS || vkQueueWaitIdle(queue_) != VK_SUCCESS) {
release_new_resources();
return false;
}
VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.image = new_image;
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = VK_FORMAT_R8G8B8A8_UNORM;
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view_info.subresourceRange.baseMipLevel = 0;
view_info.subresourceRange.levelCount = 1;
view_info.subresourceRange.baseArrayLayer = 0;
view_info.subresourceRange.layerCount = 1;
if (vkCreateImageView(device_, &view_info, nullptr, &new_view) != VK_SUCCESS) {
release_new_resources();
return false;
}
// Queue-idle above guarantees the previous modal is no longer referenced.
destroy_modal_texture();
modal_image_ = new_image;
modal_image_memory_ = new_memory;
modal_image_view_ = new_view;
modal_texture_id_ = texture_id;
new_image = VK_NULL_HANDLE;
new_memory = VK_NULL_HANDLE;
new_view = VK_NULL_HANDLE;
VkDescriptorImageInfo descriptor_image{};
descriptor_image.sampler = modal_sampler_;
descriptor_image.imageView = modal_image_view_;
descriptor_image.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet descriptor_write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
descriptor_write.dstSet = modal_descriptor_set_;
descriptor_write.dstBinding = 0;
descriptor_write.descriptorCount = 1;
descriptor_write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptor_write.pImageInfo = &descriptor_image;
vkUpdateDescriptorSets(device_, 1, &descriptor_write, 0, nullptr);
release_new_resources();
spdlog::get("illixr")->info("[stereo_renderer] Uploaded Boba modal texture id={} size={}x{}", texture_id, width, height);
return true;
}
void stereo_renderer::update_boba_overlay_state(const data_format::dual_frames& frame) {
overlay_source_width_ = frame.boba_overlay.source_width;
overlay_source_height_ = frame.boba_overlay.source_height;
render_boba_overlays_ = frame.presentation_mode == data_format::stereo_presentation_mode::stereo_fullscreen &&
overlay_source_width_ > 0 && overlay_source_height_ > 0;
active_modal_ = frame.boba_modal;
// Boba sends compact 14-float commands. Expand lines and rectangles into
// triangles once per decoded frame so command buffers need only draw them.
const auto append_vertex = [](std::vector<overlay_vertex>& vertices, float x, float y, float red, float green, float blue,
float alpha) {
vertices.push_back({x, y, std::clamp(red / 255.0F, 0.0F, 1.0F), std::clamp(green / 255.0F, 0.0F, 1.0F),
std::clamp(blue / 255.0F, 0.0F, 1.0F), std::clamp(alpha, 0.0F, 1.0F)});
};
const auto append_triangle = [&](std::vector<overlay_vertex>& vertices, float x0, float y0, float x1, float y1, float x2,
float y2, float red, float green, float blue, float alpha) {
append_vertex(vertices, x0, y0, red, green, blue, alpha);
append_vertex(vertices, x1, y1, red, green, blue, alpha);
append_vertex(vertices, x2, y2, red, green, blue, alpha);
};
const std::array<const std::vector<float>*, 2> command_lists = {&frame.boba_overlay.left_commands,
&frame.boba_overlay.right_commands};
for (int eye = 0; eye < 2; ++eye) {
auto& vertices = overlay_vertices_[eye];
vertices.clear();
modal_vertex_counts_[eye] = 0;
if (!render_boba_overlays_) {
continue;
}
const auto& commands = *command_lists[eye];
const std::size_t command_count =
std::min<std::size_t>(commands.size() / data_format::boba_frame_overlay::command_stride_floats,
data_format::boba_frame_overlay::max_commands_per_eye);
for (std::size_t command_index = 0; command_index < command_count; ++command_index) {
const float* command = commands.data() + command_index * data_format::boba_frame_overlay::command_stride_floats;
if (!std::all_of(command, command + 10, [](float value) {
return std::isfinite(value);
})) {
continue;
}
const int command_type = static_cast<int>(std::round(command[0]));
const float alpha = command[6];
const float red = command[7];
const float green = command[8];
const float blue = command[9];
if (command_type == 0) {
const float start_x = command[1];
const float start_y = command[2];
const float end_x = command[3];
const float end_y = command[4];
const float radius = std::max(0.5F, command[5]);
const float delta_x = end_x - start_x;
const float delta_y = end_y - start_y;
const float length = std::sqrt(delta_x * delta_x + delta_y * delta_y);
if (length <= 1.0e-4F) {
continue;
}
const float normal_x = -delta_y / length * radius;
const float normal_y = delta_x / length * radius;
append_triangle(vertices, start_x + normal_x, start_y + normal_y, end_x + normal_x, end_y + normal_y,
end_x - normal_x, end_y - normal_y, red, green, blue, alpha);
append_triangle(vertices, start_x + normal_x, start_y + normal_y, end_x - normal_x, end_y - normal_y,
start_x - normal_x, start_y - normal_y, red, green, blue, alpha);
} else if (command_type == 1) {
const float center_x = command[1];
const float center_y = command[2];
const float radius = std::max(1.0F, command[5]);
const float x0 = center_x - radius;
const float y0 = center_y - radius;
const float x1 = center_x + radius;
const float y1 = center_y + radius;
append_triangle(vertices, x0, y0, x1, y0, x1, y1, red, green, blue, alpha);
append_triangle(vertices, x0, y0, x1, y1, x0, y1, red, green, blue, alpha);
}
}
const bool eye_valid = eye == 0 ? active_modal_.left_valid : active_modal_.right_valid;
const auto& quad = eye == 0 ? active_modal_.left_quad_pixels : active_modal_.right_quad_pixels;
if (active_modal_.visible && eye_valid && std::all_of(quad.begin(), quad.end(), [](float value) {
return std::isfinite(value);
})) {
modal_vertices_[eye] = {{{quad[0], quad[1], 0.0F, 0.0F},
{quad[2], quad[3], 1.0F, 0.0F},
{quad[4], quad[5], 1.0F, 1.0F},
{quad[0], quad[1], 0.0F, 0.0F},
{quad[4], quad[5], 1.0F, 1.0F},
{quad[6], quad[7], 0.0F, 1.0F}}};
modal_vertex_counts_[eye] = 6;
}
}
if (render_boba_overlays_ && active_modal_.visible && active_modal_.texture_id != 0 &&
active_modal_.texture_id != modal_texture_id_ && frame.boba_modal_rgba != nullptr) {
const std::uint64_t expected_size = static_cast<std::uint64_t>(active_modal_.width) * active_modal_.height * 4ULL;
if (expected_size == frame.boba_modal_rgba->size() &&
!upload_modal_texture(active_modal_.texture_id, active_modal_.width, active_modal_.height,
*frame.boba_modal_rgba)) {
spdlog::get("illixr")->warn("[stereo_renderer] Could not upload Boba modal texture id={}",
active_modal_.texture_id);
}
}
}
void stereo_renderer::record_boba_overlays(VkCommandBuffer command_buffer, int eye) {
if (!render_boba_overlays_ || eye < 0 || eye > 1) {
return;
}
const float source_size[2] = {static_cast<float>(overlay_source_width_), static_cast<float>(overlay_source_height_)};
const auto& overlay = overlay_vertices_[eye];
if (!overlay.empty()) {
std::memcpy(overlay_vertex_mapped_[eye], overlay.data(), overlay.size() * sizeof(overlay_vertex));
const VkDeviceSize offset = 0;
vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, overlay_pipeline_);
vkCmdPushConstants(command_buffer, overlay_pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(source_size),
source_size);
vkCmdBindVertexBuffers(command_buffer, 0, 1, &overlay_vertex_buffers_[eye], &offset);
vkCmdDraw(command_buffer, static_cast<std::uint32_t>(overlay.size()), 1, 0, 0);
}
if (active_modal_.visible && modal_vertex_counts_[eye] == 6 && modal_texture_id_ == active_modal_.texture_id &&
modal_image_view_ != VK_NULL_HANDLE) {
std::memcpy(modal_vertex_mapped_[eye], modal_vertices_[eye].data(), sizeof(modal_vertices_[eye]));
const VkDeviceSize offset = 0;
vkCmdBindPipeline(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, modal_pipeline_);
vkCmdBindDescriptorSets(command_buffer, VK_PIPELINE_BIND_POINT_GRAPHICS, modal_pipeline_layout_, 0, 1,
&modal_descriptor_set_, 0, nullptr);
vkCmdPushConstants(command_buffer, modal_pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(source_size),
source_size);
vkCmdBindVertexBuffers(command_buffer, 0, 1, &modal_vertex_buffers_[eye], &offset);
vkCmdDraw(command_buffer, 6, 1, 0, 0);
}
}
#endif
//
// Shader module
//
VkShaderModule stereo_renderer::create_shader_module(VkDevice device, const uint32_t* spv, size_t word_count) {
VkShaderModuleCreateInfo ci{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
ci.codeSize = word_count * sizeof(uint32_t);
ci.pCode = spv;
VkShaderModule mod = VK_NULL_HANDLE;
vkCreateShaderModule(device, &ci, nullptr, &mod);
return mod;
}
//
// Command pool and buffers
//
bool stereo_renderer::create_command_pool() {
VkCommandPoolCreateInfo info{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
info.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
info.queueFamilyIndex = queue_family_;
VK_CHECK(vkCreateCommandPool(device_, &info, nullptr, &command_pool_));
return true;
}
bool stereo_renderer::allocate_command_buffers() {
VkCommandBufferAllocateInfo alloc{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
alloc.commandPool = command_pool_;
alloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
alloc.commandBufferCount = 2;
VK_CHECK(vkAllocateCommandBuffers(device_, &alloc, command_buffers_.data()));
return true;
}
bool stereo_renderer::create_descriptor_pool() {
// Two eyes × one descriptor per eye. Extra capacity for re-allocation
// when the pipeline is rebuilt after the first frame.
VkDescriptorPoolSize pool_size{};
pool_size.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
pool_size.descriptorCount = 8;
VkDescriptorPoolCreateInfo info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
info.maxSets = 8;
info.poolSizeCount = 1;
info.pPoolSizes = &pool_size;
VK_CHECK(vkCreateDescriptorPool(device_, &info, nullptr, &descriptor_pool_));
return true;
}
//
// AHardwareBuffer → VkImage import
//
stereo_renderer::imported_image* stereo_renderer::import_hardware_buffer(AHardwareBuffer* hw_buffer) {
auto it = image_cache_.find(hw_buffer);
if (it != image_cache_.end()) {
return it->second.get();
}
imported_image img{};
img.hw_buffer = hw_buffer;
// Query AHardwareBuffer properties
VkAndroidHardwareBufferFormatPropertiesANDROID fmt_props{
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID};
VkAndroidHardwareBufferPropertiesANDROID ahb_props{VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
ahb_props.pNext = &fmt_props;
if (vk_get_ahb_properties_(device_, hw_buffer, &ahb_props) != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] vkGetAndroidHardwareBufferPropertiesANDROID failed");
return nullptr;
}
img.external_fmt = fmt_props.externalFormat;
spdlog::get("illixr")->debug("[stereo_renderer] AHardwareBuffer imported: "
"externalFormat=0x{:X} memTypeBits=0x{:X}",
img.external_fmt, ahb_props.memoryTypeBits);
// YCbCr conversion
// For AIMAGE_FORMAT_PRIVATE buffers the externalFormat is non-zero.
// We must attach a VkSamplerYcbcrConversion so the driver performs
// the YUV→RGB transform in the sampler.
VkExternalFormatANDROID ext_fmt_info{VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID};
ext_fmt_info.externalFormat = img.external_fmt;
VkSamplerYcbcrConversionCreateInfo ycbcr_info{VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO};
ycbcr_info.pNext = &ext_fmt_info;
ycbcr_info.format = VK_FORMAT_UNDEFINED; // required for external format
ycbcr_info.ycbcrModel = fmt_props.suggestedYcbcrModel;
ycbcr_info.ycbcrRange = fmt_props.suggestedYcbcrRange;
ycbcr_info.components = fmt_props.samplerYcbcrConversionComponents;
ycbcr_info.xChromaOffset = fmt_props.suggestedXChromaOffset;
ycbcr_info.yChromaOffset = fmt_props.suggestedYChromaOffset;
ycbcr_info.chromaFilter = VK_FILTER_LINEAR;
ycbcr_info.forceExplicitReconstruction = VK_FALSE;
if (vkCreateSamplerYcbcrConversion(device_, &ycbcr_info, nullptr, &img.ycbcr_conv) != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] vkCreateSamplerYcbcrConversion failed");
return nullptr;
}
// Sampler
VkSamplerYcbcrConversionInfo conv_info{VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO};
conv_info.conversion = img.ycbcr_conv;
VkSamplerCreateInfo sampler_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_info.pNext = &conv_info;
sampler_info.magFilter = VK_FILTER_LINEAR;
sampler_info.minFilter = VK_FILTER_LINEAR;
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.unnormalizedCoordinates = VK_FALSE;
if (vkCreateSampler(device_, &sampler_info, nullptr, &img.sampler) != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] vkCreateSampler failed");
destroy_imported_image(img);
return nullptr;
}
// VkImage
AHardwareBuffer_Desc ahb_desc{};
AHardwareBuffer_describe(hw_buffer, &ahb_desc);
VkExternalMemoryImageCreateInfo ext_mem_img{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
ext_mem_img.pNext = &ext_fmt_info;
ext_mem_img.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
VkImageCreateInfo img_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
img_info.pNext = &ext_mem_img;
img_info.imageType = VK_IMAGE_TYPE_2D;
img_info.format = VK_FORMAT_UNDEFINED; // external format
img_info.extent = {ahb_desc.width, ahb_desc.height, 1};
img_info.mipLevels = 1;
img_info.arrayLayers = 1;
img_info.samples = VK_SAMPLE_COUNT_1_BIT;
img_info.tiling = VK_IMAGE_TILING_OPTIMAL;
img_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
img_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
img_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
if (vkCreateImage(device_, &img_info, nullptr, &img.image) != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] vkCreateImage (AHB) failed");
destroy_imported_image(img);
return nullptr;
}
// VkDeviceMemory (imported from AHardwareBuffer)
VkImportAndroidHardwareBufferInfoANDROID import_info{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
import_info.buffer = hw_buffer;
VkMemoryDedicatedAllocateInfo ded_alloc{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
ded_alloc.pNext = &import_info;
ded_alloc.image = img.image;
// Find a memory type that satisfies the AHardwareBuffer requirements.
uint32_t mem_type_idx = 0;
VkPhysicalDeviceMemoryProperties mem_props{};
vkGetPhysicalDeviceMemoryProperties(physical_device_, &mem_props);
bool found = false;
for (uint32_t i = 0; i < mem_props.memoryTypeCount; i++) {
if ((ahb_props.memoryTypeBits & (1u << i)) &&
(mem_props.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
mem_type_idx = i;
found = true;
break;
}
}
if (!found) {
// Fallback: use any matching type
for (uint32_t i = 0; i < mem_props.memoryTypeCount; i++) {
if (ahb_props.memoryTypeBits & (1u << i)) {
mem_type_idx = i;
break;
}
}
}
VkMemoryAllocateInfo mem_alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
mem_alloc.pNext = &ded_alloc;
mem_alloc.allocationSize = ahb_props.allocationSize;
mem_alloc.memoryTypeIndex = mem_type_idx;
if (vkAllocateMemory(device_, &mem_alloc, nullptr, &img.memory) != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] vkAllocateMemory (AHB import) failed");
destroy_imported_image(img);
return nullptr;
}
if (vkBindImageMemory(device_, img.image, img.memory, 0) != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] vkBindImageMemory failed");
destroy_imported_image(img);
return nullptr;
}
// VkImageView
VkSamplerYcbcrConversionInfo view_conv{VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO};
view_conv.conversion = img.ycbcr_conv;
VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
view_info.pNext = &view_conv;
view_info.image = img.image;
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = VK_FORMAT_UNDEFINED;
view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view_info.subresourceRange.baseMipLevel = 0;
view_info.subresourceRange.levelCount = 1;
view_info.subresourceRange.baseArrayLayer = 0;
view_info.subresourceRange.layerCount = 1;
if (vkCreateImageView(device_, &view_info, nullptr, &img.image_view) != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] vkCreateImageView failed");
destroy_imported_image(img);
return nullptr;
}
image_cache_[hw_buffer] = std::make_unique<imported_image>(std::move(img));
return image_cache_[hw_buffer].get();
}
stereo_renderer::imported_image* stereo_renderer::import_mv_hardware_buffer(AHardwareBuffer* hw_buffer) {
// ── Identical to import_hardware_buffer() up to the YCbCr model ───────────
VkAndroidHardwareBufferPropertiesANDROID props{VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
VkAndroidHardwareBufferFormatPropertiesANDROID fmt_props{
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID};
props.pNext = &fmt_props;
if (vkGetAndroidHardwareBufferPropertiesANDROID(device_, hw_buffer, &props) != VK_SUCCESS) {
return nullptr;
}
VkExternalFormatANDROID ext_fmt_info{VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID};
ext_fmt_info.externalFormat = fmt_props.externalFormat;
VkSamplerYcbcrConversionCreateInfo ycbcr_info{VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_CREATE_INFO};
ycbcr_info.pNext = &ext_fmt_info;
ycbcr_info.format = VK_FORMAT_UNDEFINED;
// ── KEY DIFFERENCE: force RGB_IDENTITY, do NOT use suggestedYcbcrModel ──
// The motion-vector buffer contains quantised float data, not a colour
// video signal. With RGB_IDENTITY the sampler performs a passthrough:
// R ← Y_norm (Vx channel), G ← U_norm (Vy channel), B ← V_norm (Vz channel).
// This lets motion_vec.frag dequantise directly without inverting BT.601.
ycbcr_info.ycbcrModel = VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY;
ycbcr_info.ycbcrRange = VK_SAMPLER_YCBCR_RANGE_ITU_FULL;
ycbcr_info.components = fmt_props.samplerYcbcrConversionComponents;
ycbcr_info.xChromaOffset = fmt_props.suggestedXChromaOffset;
ycbcr_info.yChromaOffset = fmt_props.suggestedYChromaOffset;
ycbcr_info.chromaFilter = VK_FILTER_LINEAR;
ycbcr_info.forceExplicitReconstruction = VK_FALSE;
// ── Identical to import_hardware_buffer() from here ───────────────────────
imported_image img{};
if (vkCreateSamplerYcbcrConversion(device_, &ycbcr_info, nullptr, &img.ycbcr_conv) != VK_SUCCESS) {
return nullptr;
}
VkSamplerYcbcrConversionInfo conv_info{VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO};
conv_info.conversion = img.ycbcr_conv;
VkSamplerCreateInfo sampler_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_info.pNext = &conv_info;
sampler_info.magFilter = VK_FILTER_LINEAR;
sampler_info.minFilter = VK_FILTER_LINEAR;
sampler_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_info.unnormalizedCoordinates = VK_FALSE;
if (vkCreateSampler(device_, &sampler_info, nullptr, &img.sampler) != VK_SUCCESS) {
vkDestroySamplerYcbcrConversion(device_, img.ycbcr_conv, nullptr);
return nullptr;
}
// External image memory import — identical to import_hardware_buffer().
VkImportAndroidHardwareBufferInfoANDROID import_info{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
import_info.buffer = hw_buffer;
VkExternalMemoryImageCreateInfo ext_mem_img{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
ext_mem_img.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
VkImageCreateInfo img_info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
img_info.pNext = &ext_mem_img;
img_info.imageType = VK_IMAGE_TYPE_2D;
img_info.format = VK_FORMAT_UNDEFINED;
img_info.extent = {static_cast<uint32_t>(props.allocationSize > 0 ? 1 : 1), 1, 1}; // Driver fills actual extent
img_info.mipLevels = 1;
img_info.arrayLayers = 1;
img_info.samples = VK_SAMPLE_COUNT_1_BIT;
img_info.tiling = VK_IMAGE_TILING_OPTIMAL;
img_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
img_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
img_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
VkExternalFormatANDROID ext_fmt{VK_STRUCTURE_TYPE_EXTERNAL_FORMAT_ANDROID};
ext_fmt.externalFormat = fmt_props.externalFormat;
img_info.pNext = &ext_fmt;
ext_fmt.pNext = &ext_mem_img;
if (vkCreateImage(device_, &img_info, nullptr, &img.image) != VK_SUCCESS) {
destroy_imported_image(img);
return nullptr;
}
VkMemoryRequirements mem_reqs{};
vkGetImageMemoryRequirements(device_, img.image, &mem_reqs);
VkMemoryAllocateInfo alloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
alloc.allocationSize = props.allocationSize;
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
dedicated.image = img.image;
dedicated.pNext = nullptr;
alloc.pNext = &dedicated;
dedicated.pNext = const_cast<void*>(static_cast<const void*>(&import_info));
uint32_t type_idx = 0;
for (uint32_t i = 0; i < 32; i++) {
if ((mem_reqs.memoryTypeBits >> i) & 1) {
type_idx = i;
break;
}
}
alloc.memoryTypeIndex = type_idx;
if (vkAllocateMemory(device_, &alloc, nullptr, &img.memory) != VK_SUCCESS) {
destroy_imported_image(img);
return nullptr;
}
VkBindImageMemoryInfo bind{VK_STRUCTURE_TYPE_BIND_IMAGE_MEMORY_INFO};
bind.image = img.image;
bind.memory = img.memory;
bind.memoryOffset = 0;
if (vkBindImageMemory2(device_, 1, &bind) != VK_SUCCESS) {
destroy_imported_image(img);
return nullptr;
}
VkImageViewCreateInfo view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
VkSamplerYcbcrConversionInfo view_conv{VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO};
view_conv.conversion = img.ycbcr_conv;
view_info.pNext = &view_conv;
view_info.image = img.image;
view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
view_info.format = VK_FORMAT_UNDEFINED;
view_info.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
if (vkCreateImageView(device_, &view_info, nullptr, &img.image_view) != VK_SUCCESS) {
destroy_imported_image(img);
return nullptr;
}
auto [it, ok] = mv_image_cache_.emplace(hw_buffer, std::make_unique<imported_image>(std::move(img)));
if (!ok) {
return nullptr;
}
return it->second.get();
}
//
// receive_frame — import the new AHardwareBuffers
//
void stereo_renderer::receive_frame(const dual_frames& frame) {
if (!initialized_) {
spdlog::get("illixr")->warn("stereo_renderer: receive_frame called before initialize");
return;
}
if (frame.format != data_format::frame_format::hardware_buffer) {
spdlog::get("illixr")->warn("[stereo_renderer] Received non-hardware_buffer frame, ignoring");
return;
}
#ifdef ILLIXR_ENABLE_BOBA
// Overlay metadata is tied to this exact decoded frame. Build its
// per-eye geometry before either the combined or separate-eye path can
// branch, so the compositor never reuses commands from another image.
update_boba_overlay_state(frame);
#endif
AHardwareBuffer* bufs[2] = {frame.left_eye.hw_buffer, frame.right_eye.hw_buffer};
#ifdef COMBINED_ENCODING
if (combined_encoding_) {
// Both eyes are encoded side-by-side in left_eye.hw_buffer.
// Import it once; assign the same imported_image to both eyes.
AHardwareBuffer* combined_buf = frame.left_eye.hw_buffer;
if (combined_buf == nullptr) {
spdlog::get("illixr")->error("[stereo_renderer] COMBINED_ENCODING: combined buffer is null");
has_valid_frame_ = false;
return;
}
imported_image* img = import_hardware_buffer(combined_buf);
if (!img) {
spdlog::get("illixr")->error("[stereo_renderer] COMBINED_ENCODING: import failed");
has_valid_frame_ = false;
return;
}
current_images_[0] = img;
current_images_[1] = img;
if (!pipeline_created_) {
if (!create_pipeline(*img)) {
spdlog::get("illixr")->error("[stereo_renderer] Pipeline creation failed");
return;
}
}
// Both eyes share the same image; update both descriptor sets.
// update_descriptor_set(0, *img);
// update_descriptor_set(1, *img);
has_valid_frame_ = true;
// Depth and motion vectors are still per-eye — fall through to the
// existing depth/MV import below.
goto handle_depth_mv;
}
#endif // COMBINED_ENCODING
for (int eye = 0; eye < 2; eye++) {
if (bufs[eye] == nullptr)
continue;
imported_image* img = import_hardware_buffer(bufs[eye]);
if (!img) {
spdlog::get("illixr")->error("[stereo_renderer] Failed to import AHardwareBuffer for eye {}", eye);
current_images_[eye] = nullptr;
continue;
}
current_images_[eye] = img;
// Lazily create the pipeline once we have a prototype image with
// the correct external format and sampler.
if (!pipeline_created_) {
if (!create_pipeline(*img)) {
spdlog::get("illixr")->error("[stereo_renderer] Pipeline creation failed");
return;
}
}
// Update descriptor set for this eye.
// update_descriptor_set(eye, *img);
}
has_valid_frame_ = (current_images_[0] != nullptr && current_images_[1] != nullptr);
#ifdef COMBINED_ENCODING
handle_depth_mv:
#endif
// current_format_ = frame.format;
// frame_width_ = frame.width;
// frame_height_ = frame.height; TODO:
/*if (frame.format == frame_format::external_oes) {
// Store texture handles (owned by decoder)
external_textures_[0] = frame.left_eye.texture_id;
external_textures_[1] = frame.right_eye.texture_id;
// Copy transform matrices
std::copy(frame.left_eye.texture_transform.begin(),
frame.left_eye.texture_transform.end(),
texture_transforms_[0].begin());
std::copy(frame.right_eye.texture_transform.begin(),
frame.right_eye.texture_transform.end(),
texture_transforms_[1].begin());
} else if (frame.format == frame_format::nv12) {
// Upload NV12 data to our textures
upload_nv12_data(0, frame.left_eye, frame.width, frame.height);
upload_nv12_data(1, frame.right_eye, frame.width, frame.height);
}*/
// Depth frames
has_depth_frame_ = false;
if (frame.has_valid_depth()) {
AHardwareBuffer* depth_bufs[2] = {frame.left_depth.hw_buffer, frame.right_depth.hw_buffer};
for (int eye = 0; eye < 2; eye++) {
if (depth_bufs[eye] == nullptr)
continue;
// Import into the separate depth cache so color and depth images
// never collide (they come from different AImageReaders).
auto it = depth_image_cache_.find(depth_bufs[eye]);
if (it == depth_image_cache_.end()) {
imported_image* img = import_hardware_buffer(depth_bufs[eye]);
if (!img) {
spdlog::get("illixr")->error("[stereo_renderer] Failed to import depth AHardwareBuffer eye {}", eye);
current_depth_images_[eye] = nullptr;
continue;
}
// Move from color cache to depth cache — import_hardware_buffer
// inserts into image_cache_; move it over.
auto node = image_cache_.extract(depth_bufs[eye]);
depth_image_cache_.insert(std::move(node));
current_depth_images_[eye] = depth_image_cache_.at(depth_bufs[eye]).get();
} else {
current_depth_images_[eye] = it->second.get();
}
// if (current_depth_images_[eye]) {
// update_depth_descriptor_set(eye, *current_depth_images_[eye]);
// }
}
has_depth_frame_ = (current_depth_images_[0] != nullptr && current_depth_images_[1] != nullptr);
spdlog::get("illixr")->debug("stereo_renderer: Received depth");
}
// Motion-vector import
if (frame.has_valid_motion_vectors()) {
for (int eye = 0; eye < 2; eye++) {
AHardwareBuffer* mv_buf = (eye == 0) ? frame.left_motion_vec.hw_buffer : frame.right_motion_vec.hw_buffer;
auto it = mv_image_cache_.find(mv_buf);
if (it == mv_image_cache_.end()) {
imported_image* img = import_mv_hardware_buffer(mv_buf);
if (!img) {
spdlog::get("illixr")->error("[stereo_renderer] Failed to import MV AHardwareBuffer eye {}", eye);
current_mv_images_[eye] = nullptr;
continue;
}
current_mv_images_[eye] = img;
if (!mv_pipeline_created_) {
if (!create_mv_render_pass())
return;
if (!create_mv_pipeline(*img))
return;
if (!create_mv_descriptor_pool())
return;
if (!allocate_mv_command_buffers())
return;
}
// update_mv_descriptor_set(eye, *img);
} else {
current_mv_images_[eye] = it->second.get();
// if (mv_pipeline_created_) {
// update_mv_descriptor_set(eye, it->second);
// }
}
}
has_mv_frame_ = true;
} else {
has_mv_frame_ = false;
}
}
//
// render_eye — record and submit command buffer for one eye
//
bool stereo_renderer::render_eye(int eye, VkImage swapchain_image, uint32_t swapchain_width, uint32_t swapchain_height,
VkSemaphore signal_semaphore) {
if (!initialized_) {
spdlog::get("illixr")->error("stereo_renderer: Not initialized");
return false;
}
if (!pipeline_created_) {
spdlog::get("illixr")->error("stereo_renderer: Not initialized");
return false;
}
if (current_images_[eye] == nullptr) {
return false;
}
// Wait for any previous submission on this eye's command buffer to finish,
// then destroy the transient framebuffer and image view from that previous
// submission. These objects must not be freed while the GPU is still
// reading them — destroying them immediately after vkQueueSubmit (before the
// fence signals) is a spec violation that can silently corrupt rendering on
// some drivers.
vkWaitForFences(device_, 1, &render_fences_[eye], VK_TRUE, UINT64_MAX);
vkResetFences(device_, 1, &render_fences_[eye]);
// Safe to update here — fence confirms GPU is done with frame N-1
if (current_images_[eye]) {
update_descriptor_set(eye, *current_images_[eye]);
}
// Destroy the transient objects from the previous frame for this eye now
// that the fence has confirmed the GPU is finished with them.
if (prev_framebuffers_[eye] != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, prev_framebuffers_[eye], nullptr);
prev_framebuffers_[eye] = VK_NULL_HANDLE;
}
if (prev_swapchain_views_[eye] != VK_NULL_HANDLE) {
vkDestroyImageView(device_, prev_swapchain_views_[eye], nullptr);
prev_swapchain_views_[eye] = VK_NULL_HANDLE;
}
VkCommandBuffer cmd = command_buffers_[eye];
vkResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(cmd, &begin);
// Transition swapchain image to color attachment
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = swapchain_image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.layerCount = 1;
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr,
0, nullptr, 1, &barrier);
// Transition decoder image to shader read
VkImageMemoryBarrier src_barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
src_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
src_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
src_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
src_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
src_barrier.image = current_images_[eye]->image;
src_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
src_barrier.subresourceRange.levelCount = 1;
src_barrier.subresourceRange.layerCount = 1;
src_barrier.srcAccessMask = 0;
src_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0,
nullptr, 1, &src_barrier);
// Create transient framebuffer for this swapchain image
// We create a VkImageView for the swapchain image on the fly.
// In production you would cache these per swapchain image index.
VkImageViewCreateInfo sc_view_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
sc_view_info.image = swapchain_image;
sc_view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
sc_view_info.format = swapchain_format_;
sc_view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
sc_view_info.subresourceRange.levelCount = 1;
sc_view_info.subresourceRange.layerCount = 1;
VkImageView sc_view = VK_NULL_HANDLE;
vkCreateImageView(device_, &sc_view_info, nullptr, &sc_view);
VkFramebufferCreateInfo fb_info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fb_info.renderPass = render_pass_;
fb_info.attachmentCount = 1;
fb_info.pAttachments = &sc_view;
fb_info.width = swapchain_width;
fb_info.height = swapchain_height;
fb_info.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
vkCreateFramebuffer(device_, &fb_info, nullptr, &framebuffer);
// Render pass
VkClearValue clear_value{};
clear_value.color = {{0.0f, 0.0f, 0.0f, 1.0f}};
VkRenderPassBeginInfo rp_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
rp_begin.renderPass = render_pass_;
rp_begin.framebuffer = framebuffer;
rp_begin.renderArea.extent = {swapchain_width, swapchain_height};
rp_begin.clearValueCount = 1;
rp_begin.pClearValues = &clear_value;
vkCmdBeginRenderPass(cmd, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout_, 0, 1, &descriptor_sets_[eye], 0, nullptr);
// Push crop scale constants (and u_offset under COMBINED_ENCODING).
#ifdef COMBINED_ENCODING
// crop_scale_x covers one eye's half of the combined buffer (0.5 of full width
// after padding correction). u_offset shifts eye 1 to the right half.
const float u_offset = combined_encoding_ ? (eye == 0 ? 0.0f : 0.5f) : 0.0f;
float push_data[3] = {crop_scale_x_ * (combined_encoding_ ? 0.5f : 1.0f), crop_scale_y_, u_offset};
vkCmdPushConstants(cmd, pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(push_data), push_data);
#else
float push_data[2] = {crop_scale_x_, crop_scale_y_};
vkCmdPushConstants(cmd, pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(push_data), push_data);
#endif
VkViewport viewport{};
viewport.width = static_cast<float>(swapchain_width);
viewport.height = static_cast<float>(swapchain_height);
viewport.maxDepth = 1.0f;
vkCmdSetViewport(cmd, 0, 1, &viewport);
VkRect2D scissor{{0, 0}, {swapchain_width, swapchain_height}};
vkCmdSetScissor(cmd, 0, 1, &scissor);
// Three vertices generate a full-screen triangle (no vertex buffer).
vkCmdDraw(cmd, 3, 1, 0, 0);
#ifdef ILLIXR_ENABLE_BOBA
// Compose Boba's view-dependent vectors and optional bitmap card at the
// Quest swapchain resolution, after the decoded base image.
record_boba_overlays(cmd, eye);
#endif
vkCmdEndRenderPass(cmd);
vkEndCommandBuffer(cmd);
// Submit
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
if (signal_semaphore != VK_NULL_HANDLE) {
submit.signalSemaphoreCount = 1;
submit.pSignalSemaphores = &signal_semaphore;
}
vkQueueSubmit(queue_, 1, &submit, render_fences_[eye]);
// Stash the transient objects for destruction at the top of the NEXT call
// to render_eye() for this eye, after the fence has signalled.
prev_framebuffers_[eye] = framebuffer;
prev_swapchain_views_[eye] = sc_view;
return true;
}
//
// render_eye_depth — write decoded depth into OpenXR depth swapchain image
//
bool stereo_renderer::render_eye_depth(int eye, VkImage depth_swapchain_image, VkFormat depth_format, uint32_t swapchain_width,
uint32_t swapchain_height) {
if (!has_depth_frame_ || current_depth_images_[eye] == nullptr)
return false;
// Lazily build the depth pipeline on first call (needs depth_format).
if (!depth_pipeline_created_) {
if (!create_depth_render_pass(depth_format))
return false;
if (!create_depth_descriptor_pool())
return false;
if (!allocate_depth_command_buffers())
return false;
if (!create_depth_pipeline(*current_depth_images_[eye], depth_format))
return false;
}
// Wait for previous depth submission on this eye.
vkWaitForFences(device_, 1, &depth_fences_[eye], VK_TRUE, UINT64_MAX);
vkResetFences(device_, 1, &depth_fences_[eye]);
if (current_depth_images_[eye])
update_depth_descriptor_set(eye, *current_depth_images_[eye]);
// Destroy transient objects from the previous depth submission for this eye.
if (prev_depth_framebuffers_[eye] != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, prev_depth_framebuffers_[eye], nullptr);
prev_depth_framebuffers_[eye] = VK_NULL_HANDLE;
}
if (prev_depth_views_[eye] != VK_NULL_HANDLE) {
vkDestroyImageView(device_, prev_depth_views_[eye], nullptr);
prev_depth_views_[eye] = VK_NULL_HANDLE;
}
VkCommandBuffer cmd = depth_command_buffers_[eye];
vkResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(cmd, &begin);
// Transition depth swapchain image to depth attachment
VkImageMemoryBarrier depth_barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
depth_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depth_barrier.newLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
depth_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
depth_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
depth_barrier.image = depth_swapchain_image;
depth_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
depth_barrier.subresourceRange.levelCount = 1;
depth_barrier.subresourceRange.layerCount = 1;
depth_barrier.srcAccessMask = 0;
depth_barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT, 0, 0, nullptr,
0, nullptr, 1, &depth_barrier);
// Transition decoded depth texture to shader read
VkImageMemoryBarrier src_barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
src_barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
src_barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
src_barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
src_barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
src_barrier.image = current_depth_images_[eye]->image;
src_barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
src_barrier.subresourceRange.levelCount = 1;
src_barrier.subresourceRange.layerCount = 1;
src_barrier.srcAccessMask = 0;
src_barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0,
nullptr, 1, &src_barrier);
// Create transient depth image view and framebuffer
VkImageViewCreateInfo dv_info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
dv_info.image = depth_swapchain_image;
dv_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
dv_info.format = depth_format;
dv_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
dv_info.subresourceRange.levelCount = 1;
dv_info.subresourceRange.layerCount = 1;
VkImageView depth_view = VK_NULL_HANDLE;
vkCreateImageView(device_, &dv_info, nullptr, &depth_view);
VkFramebufferCreateInfo fb_info{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fb_info.renderPass = depth_render_pass_;
fb_info.attachmentCount = 1;
fb_info.pAttachments = &depth_view;
fb_info.width = swapchain_width;
fb_info.height = swapchain_height;
fb_info.layers = 1;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
vkCreateFramebuffer(device_, &fb_info, nullptr, &framebuffer);
// Depth render pass
VkClearValue clear_depth{};
clear_depth.depthStencil = {1.0f, 0};
VkRenderPassBeginInfo rp_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
rp_begin.renderPass = depth_render_pass_;
rp_begin.framebuffer = framebuffer;
rp_begin.renderArea.extent = {swapchain_width, swapchain_height};
rp_begin.clearValueCount = 1;
rp_begin.pClearValues = &clear_depth;
vkCmdBeginRenderPass(cmd, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, depth_pipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, depth_pipeline_layout_, 0, 1, &depth_descriptor_sets_[eye], 0,
nullptr);
float push_data[2] = {crop_scale_x_, crop_scale_y_};
vkCmdPushConstants(cmd, depth_pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(push_data), push_data);
VkViewport viewport{};
viewport.width = static_cast<float>(swapchain_width);
viewport.height = static_cast<float>(swapchain_height);
viewport.maxDepth = 1.0f;
vkCmdSetViewport(cmd, 0, 1, &viewport);
VkRect2D scissor{{0, 0}, {swapchain_width, swapchain_height}};
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdDraw(cmd, 3, 1, 0, 0);
vkCmdEndRenderPass(cmd);
vkEndCommandBuffer(cmd);
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
vkQueueSubmit(queue_, 1, &submit, depth_fences_[eye]);
prev_depth_framebuffers_[eye] = framebuffer;
prev_depth_views_[eye] = depth_view;
return true;
}
bool stereo_renderer::update_depth_descriptor_set(int eye, const imported_image& img) {
if (depth_descriptor_sets_[eye] == VK_NULL_HANDLE)
return false;
VkDescriptorImageInfo img_info{};
img_info.sampler = img.sampler;
img_info.imageView = img.image_view;
img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = depth_descriptor_sets_[eye];
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &img_info;
vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr);
return true;
}
//
// Depth render pass, descriptor pool, pipeline, command buffers
//
bool stereo_renderer::create_depth_render_pass(VkFormat depth_format) {
// Depth-only render pass: no colour attachment, one depth attachment.
VkAttachmentDescription depth_attachment{};
depth_attachment.format = depth_format;
depth_attachment.samples = VK_SAMPLE_COUNT_1_BIT;
depth_attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depth_attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
depth_attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
depth_attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depth_attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depth_attachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkAttachmentReference depth_ref{};
depth_ref.attachment = 0;
depth_ref.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.pDepthStencilAttachment = &depth_ref;
VkSubpassDependency dep{};
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
dep.dstSubpass = 0;
dep.srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
dep.dstStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT;
dep.srcAccessMask = 0;
dep.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rp_info{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
rp_info.attachmentCount = 1;
rp_info.pAttachments = &depth_attachment;
rp_info.subpassCount = 1;
rp_info.pSubpasses = &subpass;
rp_info.dependencyCount = 1;
rp_info.pDependencies = &dep;
VK_CHECK(vkCreateRenderPass(device_, &rp_info, nullptr, &depth_render_pass_));
return true;
}
bool stereo_renderer::create_depth_descriptor_pool() {
// Pool for 2 combined-image-sampler descriptors (one per eye).
VkDescriptorPoolSize pool_size{};
pool_size.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
pool_size.descriptorCount = 2;
VkDescriptorPoolCreateInfo pool_info{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool_info.maxSets = 2;
pool_info.poolSizeCount = 1;
pool_info.pPoolSizes = &pool_size;
VK_CHECK(vkCreateDescriptorPool(device_, &pool_info, nullptr, &depth_descriptor_pool_));
return true;
}
bool stereo_renderer::allocate_depth_command_buffers() {
VkCommandBufferAllocateInfo alloc{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
alloc.commandPool = command_pool_; // reuse the same pool
alloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
alloc.commandBufferCount = 2;
VK_CHECK(vkAllocateCommandBuffers(device_, &alloc, depth_command_buffers_.data()));
return true;
}
bool stereo_renderer::create_depth_pipeline(const imported_image& prototype, VkFormat depth_format) {
// Descriptor set layout
// Immutable sampler required for YCbCr (same constraint as colour pipeline).
VkSamplerYcbcrConversionInfo ycbcr_info{VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO};
ycbcr_info.conversion = prototype.ycbcr_conv;
VkSamplerCreateInfo samp_info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
samp_info.pNext = &ycbcr_info;
samp_info.magFilter = VK_FILTER_LINEAR;
samp_info.minFilter = VK_FILTER_LINEAR;
samp_info.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samp_info.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samp_info.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
samp_info.unnormalizedCoordinates = VK_FALSE;
VkSampler depth_immutable_sampler = VK_NULL_HANDLE;
if (vkCreateSampler(device_, &samp_info, nullptr, &depth_immutable_sampler) != VK_SUCCESS) {
return false;
}
VkDescriptorSetLayoutBinding binding{};
binding.binding = 0;
binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
binding.descriptorCount = 1;
binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
binding.pImmutableSamplers = &depth_immutable_sampler;
VkDescriptorSetLayoutCreateInfo layout_info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layout_info.bindingCount = 1;
layout_info.pBindings = &binding;
if (vkCreateDescriptorSetLayout(device_, &layout_info, nullptr, &depth_desc_set_layout_) != VK_SUCCESS) {
vkDestroySampler(device_, depth_immutable_sampler, nullptr);
return false;
}
// Transfer ownership to the member so the sampler outlives the layout and
// pipeline. The Vulkan spec requires the immutable sampler to remain valid
// until the descriptor set layout is destroyed (cleanup() handles this).
depth_immutable_sampler_ = depth_immutable_sampler;
// Allocate depth descriptor sets (one per eye)
std::array<VkDescriptorSetLayout, 2> layouts = {depth_desc_set_layout_, depth_desc_set_layout_};
VkDescriptorSetAllocateInfo alloc{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc.descriptorPool = depth_descriptor_pool_;
alloc.descriptorSetCount = 2;
alloc.pSetLayouts = layouts.data();
VK_CHECK(vkAllocateDescriptorSets(device_, &alloc, depth_descriptor_sets_.data()));
// Depth fences
VkFenceCreateInfo fence_info{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fence_info.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (int i = 0; i < 2; i++) {
if (depth_fences_[i] == VK_NULL_HANDLE) {
VK_CHECK(vkCreateFence(device_, &fence_info, nullptr, &depth_fences_[i]));
}
}
// Pipeline layout (push constants: 2 floats for crop scale)
VkPushConstantRange pc{};
pc.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pc.size = sizeof(float) * 2;
VkPipelineLayoutCreateInfo pl_info{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pl_info.setLayoutCount = 1;
pl_info.pSetLayouts = &depth_desc_set_layout_;
pl_info.pushConstantRangeCount = 1;
pl_info.pPushConstantRanges = &pc;
VK_CHECK(vkCreatePipelineLayout(device_, &pl_info, nullptr, &depth_pipeline_layout_));
// Shaders
VkShaderModule vert_mod = create_shader_module(device_, color_vert_spv, sizeof(color_vert_spv) / sizeof(uint32_t));
VkShaderModule frag_mod = create_shader_module(device_, depth_frag_spv, sizeof(depth_frag_spv) / sizeof(uint32_t));
VkPipelineShaderStageCreateInfo stages[2] = {};
stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = vert_mod;
stages[0].pName = "main";
stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = frag_mod;
stages[1].pName = "main";
// Fixed-function state
VkPipelineVertexInputStateCreateInfo vertex_input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkPipelineInputAssemblyStateCreateInfo input_asm{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
input_asm.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo viewport_state{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
viewport_state.viewportCount = 1;
viewport_state.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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
// Depth pipeline: write depth values written by gl_FragDepth.
VkPipelineDepthStencilStateCreateInfo depth_stencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depth_stencil.depthTestEnable = VK_FALSE; // we write our own depth, no test
depth_stencil.depthWriteEnable = VK_TRUE;
depth_stencil.depthCompareOp = VK_COMPARE_OP_ALWAYS;
// No colour blend state — depth-only render pass has no colour attachment.
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = 0; // no colour attachments
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 gp{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gp.stageCount = 2;
gp.pStages = stages;
gp.pVertexInputState = &vertex_input;
gp.pInputAssemblyState = &input_asm;
gp.pViewportState = &viewport_state;
gp.pRasterizationState = &raster;
gp.pMultisampleState = &ms;
gp.pDepthStencilState = &depth_stencil;
gp.pColorBlendState = &blend;
gp.pDynamicState = &dynamic;
gp.layout = depth_pipeline_layout_;
gp.renderPass = depth_render_pass_;
VkResult result = vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &gp, nullptr, &depth_pipeline_);
vkDestroyShaderModule(device_, vert_mod, nullptr);
vkDestroyShaderModule(device_, frag_mod, nullptr);
if (result != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] Depth pipeline creation failed: {}", static_cast<int>(result));
return false;
}
depth_pipeline_created_ = true;
spdlog::get("illixr")->info("[stereo_renderer] Depth pipeline created");
return true;
}
bool stereo_renderer::update_descriptor_set(int eye, const imported_image& img) {
if (descriptor_sets_[eye] == VK_NULL_HANDLE)
return false;
VkDescriptorImageInfo img_info{};
img_info.sampler = img.sampler;
img_info.imageView = img.image_view;
img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = descriptor_sets_[eye];
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &img_info;
vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr);
return true;
}
void stereo_renderer::cleanup() {
if (device_ == VK_NULL_HANDLE)
return;
vkDeviceWaitIdle(device_);
// Destroy any transient per-frame objects that were stashed for deferred
// destruction (they are now safe to destroy after vkDeviceWaitIdle).
for (int i = 0; i < 2; i++) {
if (prev_framebuffers_[i] != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, prev_framebuffers_[i], nullptr);
prev_framebuffers_[i] = VK_NULL_HANDLE;
}
if (prev_swapchain_views_[i] != VK_NULL_HANDLE) {
vkDestroyImageView(device_, prev_swapchain_views_[i], nullptr);
prev_swapchain_views_[i] = VK_NULL_HANDLE;
}
if (prev_depth_framebuffers_[i] != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, prev_depth_framebuffers_[i], nullptr);
prev_depth_framebuffers_[i] = VK_NULL_HANDLE;
}
if (prev_depth_views_[i] != VK_NULL_HANDLE) {
vkDestroyImageView(device_, prev_depth_views_[i], nullptr);
prev_depth_views_[i] = VK_NULL_HANDLE;
}
}
// Destroy cached color imported images
for (auto& [key, img] : image_cache_) {
destroy_imported_image(*img);
}
image_cache_.clear();
// Destroy cached depth imported images
for (auto& [key, img] : depth_image_cache_) {
destroy_imported_image(*img);
}
depth_image_cache_.clear();
for (int i = 0; i < 2; i++) {
if (render_fences_[i] != VK_NULL_HANDLE) {
vkDestroyFence(device_, render_fences_[i], nullptr);
render_fences_[i] = VK_NULL_HANDLE;
}
if (depth_fences_[i] != VK_NULL_HANDLE) {
vkDestroyFence(device_, depth_fences_[i], nullptr);
depth_fences_[i] = VK_NULL_HANDLE;
}
}
#ifdef ILLIXR_ENABLE_BOBA
for (int i = 0; i < 2; ++i) {
if (overlay_vertex_mapped_[i] != nullptr) {
vkUnmapMemory(device_, overlay_vertex_memories_[i]);
overlay_vertex_mapped_[i] = nullptr;
}
if (overlay_vertex_buffers_[i] != VK_NULL_HANDLE) {
vkDestroyBuffer(device_, overlay_vertex_buffers_[i], nullptr);
overlay_vertex_buffers_[i] = VK_NULL_HANDLE;
}
if (overlay_vertex_memories_[i] != VK_NULL_HANDLE) {
vkFreeMemory(device_, overlay_vertex_memories_[i], nullptr);
overlay_vertex_memories_[i] = VK_NULL_HANDLE;
}
if (modal_vertex_mapped_[i] != nullptr) {
vkUnmapMemory(device_, modal_vertex_memories_[i]);
modal_vertex_mapped_[i] = nullptr;
}
if (modal_vertex_buffers_[i] != VK_NULL_HANDLE) {
vkDestroyBuffer(device_, modal_vertex_buffers_[i], nullptr);
modal_vertex_buffers_[i] = VK_NULL_HANDLE;
}
if (modal_vertex_memories_[i] != VK_NULL_HANDLE) {
vkFreeMemory(device_, modal_vertex_memories_[i], nullptr);
modal_vertex_memories_[i] = VK_NULL_HANDLE;
}
overlay_vertices_[i].clear();
modal_vertex_counts_[i] = 0;
}
if (overlay_pipeline_ != VK_NULL_HANDLE) {
vkDestroyPipeline(device_, overlay_pipeline_, nullptr);
overlay_pipeline_ = VK_NULL_HANDLE;
}
if (overlay_pipeline_layout_ != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(device_, overlay_pipeline_layout_, nullptr);
overlay_pipeline_layout_ = VK_NULL_HANDLE;
}
if (modal_pipeline_ != VK_NULL_HANDLE) {
vkDestroyPipeline(device_, modal_pipeline_, nullptr);
modal_pipeline_ = VK_NULL_HANDLE;
}
if (modal_pipeline_layout_ != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(device_, modal_pipeline_layout_, nullptr);
modal_pipeline_layout_ = VK_NULL_HANDLE;
}
if (modal_descriptor_pool_ != VK_NULL_HANDLE) {
vkDestroyDescriptorPool(device_, modal_descriptor_pool_, nullptr);
modal_descriptor_pool_ = VK_NULL_HANDLE;
modal_descriptor_set_ = VK_NULL_HANDLE;
}
if (modal_desc_set_layout_ != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayout(device_, modal_desc_set_layout_, nullptr);
modal_desc_set_layout_ = VK_NULL_HANDLE;
}
destroy_modal_texture();
if (modal_sampler_ != VK_NULL_HANDLE) {
vkDestroySampler(device_, modal_sampler_, nullptr);
modal_sampler_ = VK_NULL_HANDLE;
}
active_modal_ = {};
render_boba_overlays_ = false;
#endif
if (command_pool_ != VK_NULL_HANDLE) {
vkDestroyCommandPool(device_, command_pool_, nullptr);
command_pool_ = VK_NULL_HANDLE;
}
if (pipeline_ != VK_NULL_HANDLE) {
vkDestroyPipeline(device_, pipeline_, nullptr);
pipeline_ = VK_NULL_HANDLE;
}
if (pipeline_layout_ != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(device_, pipeline_layout_, nullptr);
pipeline_layout_ = VK_NULL_HANDLE;
}
if (render_pass_ != VK_NULL_HANDLE) {
vkDestroyRenderPass(device_, render_pass_, nullptr);
render_pass_ = VK_NULL_HANDLE;
}
if (descriptor_pool_ != VK_NULL_HANDLE) {
vkDestroyDescriptorPool(device_, descriptor_pool_, nullptr);
descriptor_pool_ = VK_NULL_HANDLE;
}
if (desc_set_layout_ != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayout(device_, desc_set_layout_, nullptr);
desc_set_layout_ = VK_NULL_HANDLE;
}
// Depth pipeline
if (depth_pipeline_ != VK_NULL_HANDLE) {
vkDestroyPipeline(device_, depth_pipeline_, nullptr);
depth_pipeline_ = VK_NULL_HANDLE;
}
if (depth_pipeline_layout_ != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(device_, depth_pipeline_layout_, nullptr);
depth_pipeline_layout_ = VK_NULL_HANDLE;
}
if (depth_render_pass_ != VK_NULL_HANDLE) {
vkDestroyRenderPass(device_, depth_render_pass_, nullptr);
depth_render_pass_ = VK_NULL_HANDLE;
}
if (depth_descriptor_pool_ != VK_NULL_HANDLE) {
vkDestroyDescriptorPool(device_, depth_descriptor_pool_, nullptr);
depth_descriptor_pool_ = VK_NULL_HANDLE;
}
if (depth_desc_set_layout_ != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayout(device_, depth_desc_set_layout_, nullptr);
depth_desc_set_layout_ = VK_NULL_HANDLE;
}
// Must be destroyed AFTER depth_desc_set_layout_ since the layout holds a
// reference to this sampler handle (pImmutableSamplers).
if (depth_immutable_sampler_ != VK_NULL_HANDLE) {
vkDestroySampler(device_, depth_immutable_sampler_, nullptr);
depth_immutable_sampler_ = VK_NULL_HANDLE;
}
// Motion-vector pipeline
for (int i = 0; i < 2; i++) {
if (prev_mv_framebuffers_[i] != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, prev_mv_framebuffers_[i], nullptr);
prev_mv_framebuffers_[i] = VK_NULL_HANDLE;
}
if (prev_mv_swapchain_views_[i] != VK_NULL_HANDLE) {
vkDestroyImageView(device_, prev_mv_swapchain_views_[i], nullptr);
prev_mv_swapchain_views_[i] = VK_NULL_HANDLE;
}
if (mv_fences_[i] != VK_NULL_HANDLE) {
vkDestroyFence(device_, mv_fences_[i], nullptr);
mv_fences_[i] = VK_NULL_HANDLE;
}
}
for (auto& [key, img] : mv_image_cache_) {
destroy_imported_image(*img);
}
mv_image_cache_.clear();
if (mv_pipeline_ != VK_NULL_HANDLE) {
vkDestroyPipeline(device_, mv_pipeline_, nullptr);
mv_pipeline_ = VK_NULL_HANDLE;
}
if (mv_pipeline_layout_ != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(device_, mv_pipeline_layout_, nullptr);
mv_pipeline_layout_ = VK_NULL_HANDLE;
}
if (mv_render_pass_ != VK_NULL_HANDLE) {
vkDestroyRenderPass(device_, mv_render_pass_, nullptr);
mv_render_pass_ = VK_NULL_HANDLE;
}
if (mv_descriptor_pool_ != VK_NULL_HANDLE) {
vkDestroyDescriptorPool(device_, mv_descriptor_pool_, nullptr);
mv_descriptor_pool_ = VK_NULL_HANDLE;
}
if (mv_desc_set_layout_ != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayout(device_, mv_desc_set_layout_, nullptr);
mv_desc_set_layout_ = VK_NULL_HANDLE;
}
// Must be destroyed AFTER mv_desc_set_layout_.
if (mv_immutable_sampler_ != VK_NULL_HANDLE) {
vkDestroySampler(device_, mv_immutable_sampler_, nullptr);
mv_immutable_sampler_ = VK_NULL_HANDLE;
}
mv_pipeline_created_ = false;
has_mv_frame_ = false;
initialized_ = false;
has_valid_frame_ = false;
pipeline_created_ = false;
depth_pipeline_created_ = false;
has_depth_frame_ = false;
}
void stereo_renderer::wait_idle() {
if (device_ != VK_NULL_HANDLE) {
vkDeviceWaitIdle(device_);
}
}
//
// Motion-vector render pass
//
bool stereo_renderer::create_mv_render_pass() {
// Single R16G16B16A16_SFLOAT colour attachment — no depth, no stencil.
VkAttachmentDescription color_att{};
color_att.format = VK_FORMAT_R16G16B16A16_SFLOAT;
color_att.samples = VK_SAMPLE_COUNT_1_BIT;
color_att.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color_att.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
color_att.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
color_att.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
color_att.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
color_att.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkAttachmentReference color_ref{};
color_ref.attachment = 0;
color_ref.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &color_ref;
VkSubpassDependency dep{};
dep.srcSubpass = VK_SUBPASS_EXTERNAL;
dep.dstSubpass = 0;
dep.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dep.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dep.srcAccessMask = 0;
dep.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo rp{VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO};
rp.attachmentCount = 1;
rp.pAttachments = &color_att;
rp.subpassCount = 1;
rp.pSubpasses = &subpass;
rp.dependencyCount = 1;
rp.pDependencies = &dep;
VK_CHECK(vkCreateRenderPass(device_, &rp, nullptr, &mv_render_pass_));
spdlog::get("illixr")->info("[stereo_renderer] MV render pass created");
return true;
}
//
// Motion-vector graphics pipeline
//
bool stereo_renderer::create_mv_pipeline(const imported_image& prototype) {
// Immutable YCbCr sampler
// The pipeline's immutable sampler MUST use the same VkSamplerYcbcrConversion
// as the imported image's view (Vulkan spec requirement for external-format
// images). import_mv_hardware_buffer() created that conversion with
// VK_SAMPLER_YCBCR_MODEL_CONVERSION_RGB_IDENTITY so that the motion-vector
// channels pass through as (R=Y_norm, G=U_norm, B=V_norm) without a BT.601
// colour matrix being applied to the quantised velocity data.
// Use prototype.ycbcr_conv directly — exactly as create_depth_pipeline() does.
VkSamplerYcbcrConversionInfo conv_info{VK_STRUCTURE_TYPE_SAMPLER_YCBCR_CONVERSION_INFO};
conv_info.conversion = prototype.ycbcr_conv;
VkSamplerCreateInfo sampler_ci{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
sampler_ci.pNext = &conv_info;
sampler_ci.magFilter = VK_FILTER_LINEAR;
sampler_ci.minFilter = VK_FILTER_LINEAR;
sampler_ci.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_ci.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_ci.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
sampler_ci.unnormalizedCoordinates = VK_FALSE;
VK_CHECK(vkCreateSampler(device_, &sampler_ci, nullptr, &mv_immutable_sampler_));
// Descriptor set layout
VkDescriptorSetLayoutBinding binding{};
binding.binding = 0;
binding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
binding.descriptorCount = 1;
binding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
binding.pImmutableSamplers = &mv_immutable_sampler_;
VkDescriptorSetLayoutCreateInfo layout_ci{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO};
layout_ci.bindingCount = 1;
layout_ci.pBindings = &binding;
VK_CHECK(vkCreateDescriptorSetLayout(device_, &layout_ci, nullptr, &mv_desc_set_layout_));
// Pipeline layout (push constants for crop scale)
VkPushConstantRange pc_range{};
pc_range.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pc_range.offset = 0;
pc_range.size = 2 * sizeof(float); // crop_scale_x, crop_scale_y
VkPipelineLayoutCreateInfo pl_ci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO};
pl_ci.setLayoutCount = 1;
pl_ci.pSetLayouts = &mv_desc_set_layout_;
pl_ci.pushConstantRangeCount = 1;
pl_ci.pPushConstantRanges = &pc_range;
VK_CHECK(vkCreatePipelineLayout(device_, &pl_ci, nullptr, &mv_pipeline_layout_));
// Shader stages
// Reuse the color vertex shader (full-screen triangle + crop push constants)
// and use the new motion-vector fragment shader.
VkShaderModule vert_mod = create_shader_module(device_, color_vert_spv, sizeof(color_vert_spv) / sizeof(uint32_t));
VkShaderModule frag_mod =
create_shader_module(device_, motion_vec_frag_spv, sizeof(motion_vec_frag_spv) / sizeof(uint32_t));
VkPipelineShaderStageCreateInfo stages[2]{};
stages[0].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[0].stage = VK_SHADER_STAGE_VERTEX_BIT;
stages[0].module = vert_mod;
stages[0].pName = "main";
stages[1].sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
stages[1].stage = VK_SHADER_STAGE_FRAGMENT_BIT;
stages[1].module = frag_mod;
stages[1].pName = "main";
// Fixed-function state
VkPipelineVertexInputStateCreateInfo vertex_input{VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO};
VkPipelineInputAssemblyStateCreateInfo input_asm{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
input_asm.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
VkPipelineViewportStateCreateInfo viewport_state{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
viewport_state.viewportCount = 1;
viewport_state.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.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
raster.lineWidth = 1.0f;
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
// Single R16G16B16A16_SFLOAT colour attachment — standard alpha blend.
VkPipelineColorBlendAttachmentState blend_att{};
blend_att.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.attachmentCount = 1;
blend.pAttachments = &blend_att;
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 gp{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gp.stageCount = 2;
gp.pStages = stages;
gp.pVertexInputState = &vertex_input;
gp.pInputAssemblyState = &input_asm;
gp.pViewportState = &viewport_state;
gp.pRasterizationState = &raster;
gp.pMultisampleState = &ms;
gp.pDepthStencilState = nullptr; // no depth attachment
gp.pColorBlendState = &blend;
gp.pDynamicState = &dynamic;
gp.layout = mv_pipeline_layout_;
gp.renderPass = mv_render_pass_;
VkResult result = vkCreateGraphicsPipelines(device_, VK_NULL_HANDLE, 1, &gp, nullptr, &mv_pipeline_);
vkDestroyShaderModule(device_, vert_mod, nullptr);
vkDestroyShaderModule(device_, frag_mod, nullptr);
if (result != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] MV pipeline creation failed: {}", static_cast<int>(result));
return false;
}
// Fence start state: signalled (nothing in flight).
VkFenceCreateInfo fence_ci{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
fence_ci.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (int i = 0; i < 2; i++) {
VK_CHECK(vkCreateFence(device_, &fence_ci, nullptr, &mv_fences_[i]));
}
mv_pipeline_created_ = true;
spdlog::get("illixr")->info("[stereo_renderer] Motion-vector pipeline created");
return true;
}
//
bool stereo_renderer::create_mv_descriptor_pool() {
VkDescriptorPoolSize pool_size{};
pool_size.type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
pool_size.descriptorCount = 2; // one per eye
VkDescriptorPoolCreateInfo pool_ci{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
pool_ci.maxSets = 2;
pool_ci.poolSizeCount = 1;
pool_ci.pPoolSizes = &pool_size;
VK_CHECK(vkCreateDescriptorPool(device_, &pool_ci, nullptr, &mv_descriptor_pool_));
std::array<VkDescriptorSetLayout, 2> layouts{mv_desc_set_layout_, mv_desc_set_layout_};
VkDescriptorSetAllocateInfo alloc_info{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO};
alloc_info.descriptorPool = mv_descriptor_pool_;
alloc_info.descriptorSetCount = 2;
alloc_info.pSetLayouts = layouts.data();
VK_CHECK(vkAllocateDescriptorSets(device_, &alloc_info, mv_descriptor_sets_.data()));
return true;
}
//
bool stereo_renderer::allocate_mv_command_buffers() {
VkCommandBufferAllocateInfo alloc{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
alloc.commandPool = command_pool_; // reuse the existing pool
alloc.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
alloc.commandBufferCount = 2;
VK_CHECK(vkAllocateCommandBuffers(device_, &alloc, mv_command_buffers_.data()));
return true;
}
//
bool stereo_renderer::update_mv_descriptor_set(int eye, const imported_image& img) {
if (mv_descriptor_sets_[eye] == VK_NULL_HANDLE)
return false;
VkDescriptorImageInfo img_info{};
img_info.sampler = img.sampler;
img_info.imageView = img.image_view;
img_info.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkWriteDescriptorSet write{VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET};
write.dstSet = mv_descriptor_sets_[eye];
write.dstBinding = 0;
write.descriptorCount = 1;
write.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
write.pImageInfo = &img_info;
vkUpdateDescriptorSets(device_, 1, &write, 0, nullptr);
return true;
}
//
// Motion-vector render
//
bool stereo_renderer::render_eye_motion_vec(int eye, VkImage mv_swapchain_image, uint32_t swapchain_width,
uint32_t swapchain_height) {
if (!initialized_ || !mv_pipeline_created_) {
spdlog::get("illixr")->warn("[stereo_renderer] render_eye_motion_vec: not ready (eye {})", eye);
return false;
}
if (!has_mv_frame_ || current_mv_images_[eye] == nullptr) {
spdlog::get("illixr")->debug("[stereo_renderer] render_eye_motion_vec: no MV frame (eye {})", eye);
return false;
}
// Wait for the previous MV submission on this eye to finish.
vkWaitForFences(device_, 1, &mv_fences_[eye], VK_TRUE, UINT64_MAX);
vkResetFences(device_, 1, &mv_fences_[eye]);
if (current_mv_images_[eye])
update_mv_descriptor_set(eye, *current_mv_images_[eye]);
// Destroy the transient framebuffer and image view from the previous frame.
if (prev_mv_framebuffers_[eye] != VK_NULL_HANDLE) {
vkDestroyFramebuffer(device_, prev_mv_framebuffers_[eye], nullptr);
prev_mv_framebuffers_[eye] = VK_NULL_HANDLE;
}
if (prev_mv_swapchain_views_[eye] != VK_NULL_HANDLE) {
vkDestroyImageView(device_, prev_mv_swapchain_views_[eye], nullptr);
prev_mv_swapchain_views_[eye] = VK_NULL_HANDLE;
}
// Create swapchain image view
VkImageViewCreateInfo iv_ci{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
iv_ci.image = mv_swapchain_image;
iv_ci.viewType = VK_IMAGE_VIEW_TYPE_2D;
iv_ci.format = VK_FORMAT_R16G16B16A16_SFLOAT;
iv_ci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
VkImageView mv_view = VK_NULL_HANDLE;
VK_CHECK(vkCreateImageView(device_, &iv_ci, nullptr, &mv_view));
prev_mv_swapchain_views_[eye] = mv_view;
// Create framebuffer
VkFramebufferCreateInfo fb_ci{VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO};
fb_ci.renderPass = mv_render_pass_;
fb_ci.attachmentCount = 1;
fb_ci.pAttachments = &mv_view;
fb_ci.width = swapchain_width;
fb_ci.height = swapchain_height;
fb_ci.layers = 1;
VkFramebuffer fb = VK_NULL_HANDLE;
VK_CHECK(vkCreateFramebuffer(device_, &fb_ci, nullptr, &fb));
prev_mv_framebuffers_[eye] = fb;
// Record command buffer
VkCommandBuffer cmd = mv_command_buffers_[eye];
vkResetCommandBuffer(cmd, 0);
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkBeginCommandBuffer(cmd, &begin));
// Transition swapchain image from UNDEFINED to COLOR_ATTACHMENT_OPTIMAL.
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = mv_swapchain_image;
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr,
0, nullptr, 1, &barrier);
// Begin render pass.
VkRenderPassBeginInfo rp_begin{VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO};
rp_begin.renderPass = mv_render_pass_;
rp_begin.framebuffer = fb;
rp_begin.renderArea.offset = {0, 0};
rp_begin.renderArea.extent = {swapchain_width, swapchain_height};
// No clear colour needed (loadOp = DONT_CARE).
vkCmdBeginRenderPass(cmd, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, mv_pipeline_);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, mv_pipeline_layout_, 0, 1, &mv_descriptor_sets_[eye], 0,
nullptr);
// Push crop scale (motion-vector buffers are already at their native
// resolution with no padding, so scale = 1.0).
float push[2] = {1.0f, 1.0f};
vkCmdPushConstants(cmd, mv_pipeline_layout_, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(push), push);
VkViewport viewport{0.0f, 0.0f, static_cast<float>(swapchain_width), static_cast<float>(swapchain_height), 0.0f, 1.0f};
VkRect2D scissor{{0, 0}, {swapchain_width, swapchain_height}};
vkCmdSetViewport(cmd, 0, 1, &viewport);
vkCmdSetScissor(cmd, 0, 1, &scissor);
vkCmdDraw(cmd, 3, 1, 0, 0);
vkCmdEndRenderPass(cmd);
// Transition swapchain image to SHADER_READ_ONLY_OPTIMAL for OpenXR.
barrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
barrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0,
nullptr, 0, nullptr, 1, &barrier);
VK_CHECK(vkEndCommandBuffer(cmd));
// Submit
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
VkResult result = vkQueueSubmit(queue_, 1, &submit, mv_fences_[eye]);
if (result != VK_SUCCESS) {
spdlog::get("illixr")->error("[stereo_renderer] MV render submit failed eye {}: {}", eye, static_cast<int>(result));
return false;
}
return true;
}