mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-24 13:01:48 +02:00
# Conflicts: # artifacts/SHA256SUMS # artifacts/arm64-v8a/libopenxr_loader_generic.so # artifacts/armeabi-v7a/libopenxr_loader_generic.so # catalog/triage.yaml # tests/test_triage.py
1466 lines
69 KiB
C++
1466 lines
69 KiB
C++
// SPDX-License-Identifier: GPL-3.0-only
|
|
#include "runtime.h"
|
|
|
|
#include <EGL/egl.h>
|
|
#include <GLES3/gl32.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <limits>
|
|
#include <new>
|
|
|
|
namespace {
|
|
constexpr uint32_t kMaxBuffers = 16;
|
|
constexpr uint32_t kMaxLayers = 16;
|
|
constexpr uint32_t kMaxCopyJobs = kMaxLayers * 2;
|
|
constexpr uint32_t kMaxXrFormats = 64;
|
|
constexpr uint64_t kFrameWaitNs = 1000000000ULL;
|
|
constexpr uint64_t kShutdownWaitNs = 5000000000ULL;
|
|
constexpr uintptr_t kDefaultSwapchain = 1;
|
|
constexpr uintptr_t kLoadingSwapchain = 2;
|
|
|
|
constexpr int32_t kTexture2D = 0;
|
|
constexpr int32_t kTexture2DArray = 2;
|
|
constexpr int32_t kProjectionLayer = 1;
|
|
constexpr uint32_t kLayerChromatic = 1u << 1;
|
|
constexpr uint32_t kFrameFlush = 1u << 1;
|
|
constexpr uint32_t kFrameInhibitVolume = 1u << 6;
|
|
constexpr uint32_t kFrameShowTextureDensity = 1u << 8; // obsolete Quest compositor debug overlay
|
|
constexpr uint32_t kLayerFixedToView = 1u << 2;
|
|
constexpr uint32_t kLayerClipRect = 1u << 4;
|
|
constexpr uint32_t kLayerInhibitSrgb = 1u << 8;
|
|
constexpr int32_t kBlendZero = 0;
|
|
constexpr int32_t kBlendOne = 1;
|
|
constexpr int32_t kBlendSrcAlpha = 2;
|
|
constexpr int32_t kBlendOneMinusSrcAlpha = 5;
|
|
|
|
struct AppImage {
|
|
VkImage image = VK_NULL_HANDLE;
|
|
VkDeviceMemory memory = VK_NULL_HANDLE;
|
|
};
|
|
}
|
|
|
|
struct ovrTextureSwapChain {
|
|
ovrTextureSwapChain* next = nullptr;
|
|
int32_t type = kTexture2D;
|
|
VkFormat format = VK_FORMAT_UNDEFINED;
|
|
uint32_t width = 0;
|
|
uint32_t height = 0;
|
|
uint32_t levels = 0;
|
|
uint32_t bufferCount = 0;
|
|
uint32_t arrayLayers = 1;
|
|
std::array<AppImage, kMaxBuffers> appImages{};
|
|
AppImage fullRateImage{};
|
|
bool fullRateInitialized = false;
|
|
|
|
XrSwapchain output = XR_NULL_HANDLE;
|
|
uint32_t outputImageCount = 0;
|
|
std::array<XrSwapchainImageVulkanKHR, kMaxBuffers> outputImages{};
|
|
std::array<bool, kMaxBuffers> outputInitialized{};
|
|
|
|
// GLES applications: the app renders into these textures; frames are copied to glOutputImages.
|
|
bool gl = false;
|
|
GLenum glInternalFormat = 0;
|
|
void* glContext = nullptr; // EGL context current when the textures were created
|
|
std::array<GLuint, kMaxBuffers> glTextures{};
|
|
std::array<XrSwapchainImageOpenGLESKHR, kMaxBuffers> glOutputImages{};
|
|
};
|
|
|
|
namespace {
|
|
ovrTextureSwapChain* gChains = nullptr;
|
|
ovrTextureSwapChain* gRetired = nullptr;
|
|
|
|
struct CopyContext {
|
|
VkDevice device = VK_NULL_HANDLE;
|
|
VkQueue queue = VK_NULL_HANDLE;
|
|
uint32_t queueFamily = 0;
|
|
VkCommandPool pool = VK_NULL_HANDLE;
|
|
VkCommandBuffer command = VK_NULL_HANDLE;
|
|
VkFence fence = VK_NULL_HANDLE;
|
|
bool submitted = false;
|
|
uint32_t pendingCount = 0;
|
|
std::array<XrSwapchain, kMaxCopyJobs> pending{};
|
|
};
|
|
CopyContext gCopy;
|
|
|
|
bool isBuiltin(const ovrTextureSwapChain* chain) {
|
|
const uintptr_t value = reinterpret_cast<uintptr_t>(chain);
|
|
return value == kDefaultSwapchain || value == kLoadingSwapchain;
|
|
}
|
|
|
|
ovrTextureSwapChain* findChain(const ovrTextureSwapChain* candidate) {
|
|
for (ovrTextureSwapChain* chain = gChains; chain; chain = chain->next) {
|
|
if (chain == candidate) return chain;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool supportedFormat(VkFormat format) {
|
|
switch (format) {
|
|
case VK_FORMAT_R8G8B8A8_UNORM:
|
|
case VK_FORMAT_R8G8B8A8_SRGB:
|
|
case VK_FORMAT_B8G8R8A8_UNORM:
|
|
case VK_FORMAT_B8G8R8A8_SRGB:
|
|
case VK_FORMAT_R16G16B16A16_SFLOAT:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool findMemoryType(VkPhysicalDevice physicalDevice, uint32_t bits,
|
|
VkMemoryPropertyFlags required, uint32_t& index) {
|
|
VkPhysicalDeviceMemoryProperties properties{};
|
|
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &properties);
|
|
for (uint32_t i = 0; i < properties.memoryTypeCount; ++i) {
|
|
if ((bits & (1u << i)) != 0 &&
|
|
(properties.memoryTypes[i].propertyFlags & required) == required) {
|
|
index = i;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void destroyAppImage(AppImage& image) {
|
|
const VkDevice device = ovp::runtime().vk.Device;
|
|
if (device == VK_NULL_HANDLE) return;
|
|
if (image.image != VK_NULL_HANDLE) vkDestroyImage(device, image.image, nullptr);
|
|
if (image.memory != VK_NULL_HANDLE) vkFreeMemory(device, image.memory, nullptr);
|
|
image = {};
|
|
}
|
|
|
|
void destroyAppImages(ovrTextureSwapChain* chain) {
|
|
if (chain->gl) {
|
|
glDeleteTextures(static_cast<GLsizei>(chain->bufferCount), chain->glTextures.data());
|
|
chain->glTextures.fill(0);
|
|
return;
|
|
}
|
|
for (uint32_t i = 0; i < chain->bufferCount; ++i) destroyAppImage(chain->appImages[i]);
|
|
destroyAppImage(chain->fullRateImage);
|
|
chain->fullRateInitialized = false;
|
|
}
|
|
|
|
void destroyOutput(ovrTextureSwapChain* chain) {
|
|
if (chain->output != XR_NULL_HANDLE) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
if (s.xr.xrDestroySwapchain) {
|
|
ovp::xrOk(s.xr.xrDestroySwapchain(chain->output), "xrDestroySwapchain");
|
|
}
|
|
chain->output = XR_NULL_HANDLE;
|
|
}
|
|
chain->outputImageCount = 0;
|
|
chain->outputInitialized.fill(false);
|
|
for (auto& image : chain->outputImages) {
|
|
image = {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR};
|
|
}
|
|
}
|
|
|
|
void destroyChain(ovrTextureSwapChain* chain) {
|
|
destroyOutput(chain);
|
|
destroyAppImages(chain);
|
|
delete chain;
|
|
}
|
|
|
|
void releasePending() {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
for (uint32_t i = 0; i < gCopy.pendingCount; ++i) {
|
|
if (gCopy.pending[i] != XR_NULL_HANDLE && s.xr.xrReleaseSwapchainImage) {
|
|
const XrSwapchainImageReleaseInfo release{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
|
ovp::xrOk(s.xr.xrReleaseSwapchainImage(gCopy.pending[i], &release),
|
|
"xrReleaseSwapchainImage");
|
|
}
|
|
gCopy.pending[i] = XR_NULL_HANDLE;
|
|
}
|
|
gCopy.pendingCount = 0;
|
|
gCopy.submitted = false;
|
|
}
|
|
|
|
bool settleCopy(uint64_t timeoutNs, bool allowQueueIdle) {
|
|
if (!gCopy.submitted) return true;
|
|
VkResult result = vkWaitForFences(gCopy.device, 1, &gCopy.fence, VK_TRUE, timeoutNs);
|
|
if (result == VK_TIMEOUT && allowQueueIdle) {
|
|
result = vkQueueWaitIdle(gCopy.queue);
|
|
}
|
|
if (result != VK_SUCCESS) {
|
|
OVP_ERROR("Vulkan copy completion failed: %d", static_cast<int>(result));
|
|
return false;
|
|
}
|
|
releasePending();
|
|
return true;
|
|
}
|
|
|
|
void reapRetired() {
|
|
if (gCopy.submitted) return;
|
|
while (gRetired) {
|
|
ovrTextureSwapChain* chain = gRetired;
|
|
gRetired = chain->next;
|
|
destroyChain(chain);
|
|
}
|
|
}
|
|
|
|
void destroyCopyContext() {
|
|
if (gCopy.device != VK_NULL_HANDLE) {
|
|
if (gCopy.fence != VK_NULL_HANDLE) vkDestroyFence(gCopy.device, gCopy.fence, nullptr);
|
|
if (gCopy.pool != VK_NULL_HANDLE) vkDestroyCommandPool(gCopy.device, gCopy.pool, nullptr);
|
|
}
|
|
gCopy = {};
|
|
}
|
|
|
|
bool ensureCopyContext() {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
if (!ovp::ensureGraphicsQueue()) return false;
|
|
if (gCopy.device == s.vk.Device && gCopy.queue == s.queue &&
|
|
gCopy.queueFamily == s.queueFamily && gCopy.command != VK_NULL_HANDLE) {
|
|
return true;
|
|
}
|
|
if (gCopy.submitted && !settleCopy(kShutdownWaitNs, true)) return false;
|
|
destroyCopyContext();
|
|
|
|
gCopy.device = s.vk.Device;
|
|
gCopy.queue = s.queue;
|
|
gCopy.queueFamily = s.queueFamily;
|
|
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
|
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
|
poolInfo.queueFamilyIndex = s.queueFamily;
|
|
if (vkCreateCommandPool(s.vk.Device, &poolInfo, nullptr, &gCopy.pool) != VK_SUCCESS) {
|
|
destroyCopyContext();
|
|
return false;
|
|
}
|
|
VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
|
allocate.commandPool = gCopy.pool;
|
|
allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
|
allocate.commandBufferCount = 1;
|
|
if (vkAllocateCommandBuffers(s.vk.Device, &allocate, &gCopy.command) != VK_SUCCESS) {
|
|
destroyCopyContext();
|
|
return false;
|
|
}
|
|
const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
|
if (vkCreateFence(s.vk.Device, &fenceInfo, nullptr, &gCopy.fence) != VK_SUCCESS) {
|
|
destroyCopyContext();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ensureFullRateImage(ovrTextureSwapChain* chain) {
|
|
if (!settleCopy(kFrameWaitNs, false)) return false;
|
|
if (chain->fullRateInitialized) return true;
|
|
if (!ensureCopyContext()) return false;
|
|
const auto& s = ovp::runtime();
|
|
AppImage& resource = chain->fullRateImage;
|
|
if (resource.image == VK_NULL_HANDLE) {
|
|
VkFormatProperties properties{};
|
|
vkGetPhysicalDeviceFormatProperties(s.vk.PhysicalDevice, VK_FORMAT_R8G8_UNORM, &properties);
|
|
const VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
|
VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
|
|
if ((properties.optimalTilingFeatures & needed) != needed) return false;
|
|
VkImageCreateInfo image{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
|
image.imageType = VK_IMAGE_TYPE_2D;
|
|
image.format = VK_FORMAT_R8G8_UNORM;
|
|
image.extent = {1, 1, 1};
|
|
image.mipLevels = 1;
|
|
image.arrayLayers = chain->arrayLayers;
|
|
image.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
image.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
image.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT |
|
|
VK_IMAGE_USAGE_FRAGMENT_DENSITY_MAP_BIT_EXT;
|
|
// Some engines bind this resource as a fragment-density attachment even when our VrApi capability says
|
|
// foveation is off (The Climb, GitHub #238). Turnip samples density maps on the CPU: this usage makes it
|
|
// choose linear storage and map the backing memory at bind time. An ordinary sampled image has a NULL
|
|
// image->map and crashes in vkCmdEndRenderPass. Keep the constant full-rate contents in either case.
|
|
// This API supplies a density attachment, so never substitute an ordinary sampled image when a layer
|
|
// filters the advertised format features. vkCreateImage can reject unsupported usage without publishing it.
|
|
image.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
image.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
const VkResult created = vkCreateImage(s.vk.Device, &image, nullptr, &resource.image);
|
|
OVP_LOG("Full-rate image: format features=0x%x usage=0x%x create=%d",
|
|
properties.optimalTilingFeatures, image.usage, created);
|
|
if (created != VK_SUCCESS) return false;
|
|
VkMemoryRequirements requirements{};
|
|
vkGetImageMemoryRequirements(s.vk.Device, resource.image, &requirements);
|
|
uint32_t memoryType = 0;
|
|
if (!findMemoryType(s.vk.PhysicalDevice, requirements.memoryTypeBits,
|
|
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, memoryType)) {
|
|
destroyAppImage(resource);
|
|
return false;
|
|
}
|
|
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
|
allocate.allocationSize = requirements.size;
|
|
allocate.memoryTypeIndex = memoryType;
|
|
if (vkAllocateMemory(s.vk.Device, &allocate, nullptr, &resource.memory) != VK_SUCCESS ||
|
|
vkBindImageMemory(s.vk.Device, resource.image, resource.memory, 0) != VK_SUCCESS) {
|
|
destroyAppImage(resource);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (vkResetCommandBuffer(gCopy.command, 0) != VK_SUCCESS) return false;
|
|
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
|
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
if (vkBeginCommandBuffer(gCopy.command, &begin) != VK_SUCCESS) return false;
|
|
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
|
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
barrier.image = resource.image;
|
|
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, chain->arrayLayers};
|
|
vkCmdPipelineBarrier(gCopy.command, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
// Constant (1,1) density means full-rate shading, including when the engine binds it as a density attachment.
|
|
const VkClearColorValue fullRate{{1.0f, 1.0f, 1.0f, 1.0f}};
|
|
vkCmdClearColorImage(gCopy.command, resource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
|
&fullRate, 1, &barrier.subresourceRange);
|
|
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
|
vkCmdPipelineBarrier(gCopy.command, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
|
if (vkEndCommandBuffer(gCopy.command) != VK_SUCCESS ||
|
|
vkResetFences(gCopy.device, 1, &gCopy.fence) != VK_SUCCESS) return false;
|
|
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
|
submit.commandBufferCount = 1;
|
|
submit.pCommandBuffers = &gCopy.command;
|
|
if (vkQueueSubmit(gCopy.queue, 1, &submit, gCopy.fence) != VK_SUCCESS) return false;
|
|
gCopy.submitted = true;
|
|
gCopy.pendingCount = 0;
|
|
// Keep ownership if the wait times out; retirement already waits for this
|
|
// fence, and a later query cannot publish the image until it has completed.
|
|
chain->fullRateInitialized = true;
|
|
if (!settleCopy(kFrameWaitNs, false)) return false;
|
|
OVP_LOG("Created full-rate compatibility texture (%u layers); hardware foveation disabled",
|
|
chain->arrayLayers);
|
|
return true;
|
|
}
|
|
|
|
bool ensureOutputGl(ovrTextureSwapChain* chain) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
uint32_t formatCount = 0;
|
|
std::array<int64_t, kMaxXrFormats> formats{};
|
|
if (XR_FAILED(s.xr.xrEnumerateSwapchainFormats(s.session, 0, &formatCount, nullptr)) || !formatCount ||
|
|
formatCount > kMaxXrFormats ||
|
|
XR_FAILED(s.xr.xrEnumerateSwapchainFormats(s.session, formatCount, &formatCount, formats.data()))) {
|
|
OVP_ERROR("xrEnumerateSwapchainFormats(GLES) failed");
|
|
return false;
|
|
}
|
|
auto exposed = [&](int64_t f) { return std::find(formats.begin(), formats.begin() + formatCount, f) != formats.begin() + formatCount; };
|
|
int64_t wanted = chain->glInternalFormat;
|
|
if (!exposed(wanted) && (wanted == GL_RGBA8 || wanted == GL_SRGB8_ALPHA8)) {
|
|
// glCopyImageSubData copies raw texels between these size-compatible formats.
|
|
const int64_t twin = wanted == GL_RGBA8 ? GL_SRGB8_ALPHA8 : GL_RGBA8;
|
|
if (exposed(twin)) {
|
|
OVP_LOG("Runtime lacks GL format 0x%llx; using compatible 0x%llx", (long long)wanted, (long long)twin);
|
|
wanted = twin;
|
|
}
|
|
}
|
|
if (!exposed(wanted)) {
|
|
OVP_ERROR("OpenXR runtime does not expose GL format 0x%llx", (long long)wanted);
|
|
return false;
|
|
}
|
|
XrSwapchainCreateInfo create{XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
|
create.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT | XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
|
|
create.format = wanted;
|
|
create.sampleCount = 1;
|
|
create.width = chain->width;
|
|
create.height = chain->height;
|
|
create.faceCount = 1;
|
|
create.arraySize = chain->arrayLayers;
|
|
create.mipCount = 1;
|
|
if (!ovp::xrOk(s.xr.xrCreateSwapchain(s.session, &create, &chain->output), "xrCreateSwapchain(GLES)")) {
|
|
chain->output = XR_NULL_HANDLE;
|
|
return false;
|
|
}
|
|
uint32_t count = 0;
|
|
if (XR_FAILED(s.xr.xrEnumerateSwapchainImages(chain->output, 0, &count, nullptr)) || !count || count > kMaxBuffers) {
|
|
destroyOutput(chain);
|
|
return false;
|
|
}
|
|
for (uint32_t i = 0; i < count; ++i) chain->glOutputImages[i] = {XR_TYPE_SWAPCHAIN_IMAGE_OPENGL_ES_KHR};
|
|
if (XR_FAILED(s.xr.xrEnumerateSwapchainImages(chain->output, count, &count,
|
|
reinterpret_cast<XrSwapchainImageBaseHeader*>(chain->glOutputImages.data())))) {
|
|
destroyOutput(chain);
|
|
return false;
|
|
}
|
|
chain->outputImageCount = count;
|
|
OVP_LOG("Created GLES output swapchain %ux%u x%u format 0x%llx", chain->width, chain->height,
|
|
chain->arrayLayers, (long long)wanted);
|
|
return true;
|
|
}
|
|
|
|
bool ensureOutput(ovrTextureSwapChain* chain) {
|
|
if (chain->output != XR_NULL_HANDLE) return true;
|
|
ovp::Runtime& s = ovp::runtime();
|
|
if (s.session == XR_NULL_HANDLE) return false;
|
|
if (chain->gl) return ensureOutputGl(chain);
|
|
|
|
uint32_t formatCount = 0;
|
|
XrResult result = s.xr.xrEnumerateSwapchainFormats(s.session, 0, &formatCount, nullptr);
|
|
if (XR_FAILED(result) || formatCount == 0 || formatCount > kMaxXrFormats) {
|
|
ovp::xrOk(result, "xrEnumerateSwapchainFormats(count)");
|
|
return false;
|
|
}
|
|
std::array<int64_t, kMaxXrFormats> formats{};
|
|
result = s.xr.xrEnumerateSwapchainFormats(s.session, formatCount, &formatCount,
|
|
formats.data());
|
|
if (XR_FAILED(result)) {
|
|
ovp::xrOk(result, "xrEnumerateSwapchainFormats");
|
|
return false;
|
|
}
|
|
int64_t wanted = static_cast<int64_t>(chain->format);
|
|
auto exposed = [&](int64_t f) {
|
|
return std::find(formats.begin(), formats.begin() + formatCount, f) != formats.begin() + formatCount;
|
|
};
|
|
if (!exposed(wanted)) {
|
|
// Steam Frame exposes only the sRGB variants. The output is filled with vkCmdCopyImage, a raw
|
|
// copy that is valid between size-compatible UNORM/SRGB twins, so use the twin instead.
|
|
int64_t twin = 0;
|
|
switch (wanted) {
|
|
case VK_FORMAT_R8G8B8A8_UNORM: twin = VK_FORMAT_R8G8B8A8_SRGB; break;
|
|
case VK_FORMAT_R8G8B8A8_SRGB: twin = VK_FORMAT_R8G8B8A8_UNORM; break;
|
|
case VK_FORMAT_B8G8R8A8_UNORM: twin = VK_FORMAT_B8G8R8A8_SRGB; break;
|
|
case VK_FORMAT_B8G8R8A8_SRGB: twin = VK_FORMAT_B8G8R8A8_UNORM; break;
|
|
default: break;
|
|
}
|
|
if (!twin || !exposed(twin)) {
|
|
OVP_ERROR("OpenXR runtime does not expose requested VkFormat %lld",
|
|
static_cast<long long>(wanted));
|
|
return false;
|
|
}
|
|
OVP_LOG("Runtime lacks VkFormat %lld; using size-compatible VkFormat %lld for the output swapchain",
|
|
static_cast<long long>(wanted), static_cast<long long>(twin));
|
|
wanted = twin;
|
|
}
|
|
|
|
XrSwapchainCreateInfo create{XR_TYPE_SWAPCHAIN_CREATE_INFO};
|
|
create.usageFlags = XR_SWAPCHAIN_USAGE_COLOR_ATTACHMENT_BIT |
|
|
XR_SWAPCHAIN_USAGE_TRANSFER_DST_BIT;
|
|
create.format = wanted;
|
|
create.sampleCount = 1;
|
|
create.width = chain->width;
|
|
create.height = chain->height;
|
|
create.faceCount = 1;
|
|
create.arraySize = chain->arrayLayers;
|
|
create.mipCount = chain->levels;
|
|
// Per-game opt-in: Valve's injection caches density maps by maximum extent.
|
|
// Keep mono overlays from replacing the stereo map at the same extent.
|
|
// Only output storage is padded; copies and submitted subrects retain the
|
|
// original dimensions, and all application-owned images remain unchanged.
|
|
const char* isolate = std::getenv("FRAMEPORT_VRAPI_FDM_ISOLATE_MONO");
|
|
if (isolate && std::strcmp(isolate, "1") == 0 && chain->arrayLayers == 1) {
|
|
create.width += 32;
|
|
create.height += 32;
|
|
OVP_LOG("Foveation separate mono output %ux%u -> %ux%u", chain->width, chain->height,
|
|
create.width, create.height);
|
|
}
|
|
result = s.xr.xrCreateSwapchain(s.session, &create, &chain->output);
|
|
if (XR_FAILED(result)) {
|
|
ovp::xrOk(result, "xrCreateSwapchain");
|
|
chain->output = XR_NULL_HANDLE;
|
|
return false;
|
|
}
|
|
|
|
uint32_t imageCount = 0;
|
|
result = s.xr.xrEnumerateSwapchainImages(chain->output, 0, &imageCount, nullptr);
|
|
if (XR_FAILED(result) || imageCount == 0 || imageCount > kMaxBuffers) {
|
|
ovp::xrOk(result, "xrEnumerateSwapchainImages(count)");
|
|
destroyOutput(chain);
|
|
return false;
|
|
}
|
|
for (uint32_t i = 0; i < imageCount; ++i) {
|
|
chain->outputImages[i] = {XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR};
|
|
}
|
|
result = s.xr.xrEnumerateSwapchainImages(
|
|
chain->output, imageCount, &imageCount,
|
|
reinterpret_cast<XrSwapchainImageBaseHeader*>(chain->outputImages.data()));
|
|
if (XR_FAILED(result)) {
|
|
ovp::xrOk(result, "xrEnumerateSwapchainImages");
|
|
destroyOutput(chain);
|
|
return false;
|
|
}
|
|
chain->outputImageCount = imageCount;
|
|
return true;
|
|
}
|
|
|
|
bool finite(float value) { return std::isfinite(value); }
|
|
|
|
bool allColor(const ovrVector4f& color, float value) {
|
|
return color.x == value && color.y == value && color.z == value && color.w == value;
|
|
}
|
|
|
|
XrQuaternionf normalize(XrQuaternionf q) {
|
|
const float length = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
|
if (!(length > 0.000001f) || !finite(length)) return {0, 0, 0, 0};
|
|
const float inverse = 1.0f / length;
|
|
return {q.x * inverse, q.y * inverse, q.z * inverse, q.w * inverse};
|
|
}
|
|
|
|
XrQuaternionf conjugate(const XrQuaternionf& q) { return {-q.x, -q.y, -q.z, q.w}; }
|
|
|
|
XrQuaternionf multiply(const XrQuaternionf& a, const XrQuaternionf& b) {
|
|
return {
|
|
a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
|
|
a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
|
|
a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
|
|
a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
|
|
};
|
|
}
|
|
|
|
XrVector3f rotate(const XrQuaternionf& q, const XrVector3f& v) {
|
|
const XrQuaternionf p{v.x, v.y, v.z, 0};
|
|
const XrQuaternionf r = multiply(multiply(q, p), conjugate(q));
|
|
return {r.x, r.y, r.z};
|
|
}
|
|
|
|
bool eyePose(const ovrPosef& headPose, uint32_t eye, XrPosef& result) {
|
|
const ovp::Runtime& s = ovp::runtime();
|
|
if (!finite(headPose.Position.x) || !finite(headPose.Position.y) ||
|
|
!finite(headPose.Position.z)) return false;
|
|
XrQuaternionf head = normalize(ovp::toXrPose(headPose).orientation);
|
|
XrQuaternionf q0 = normalize(s.views[0].pose.orientation);
|
|
XrQuaternionf q1 = normalize(s.views[1].pose.orientation);
|
|
if (head.w == 0 && head.x == 0 && head.y == 0 && head.z == 0) return false;
|
|
if (q0.w == 0 && q0.x == 0 && q0.y == 0 && q0.z == 0) return false;
|
|
if (q1.w == 0 && q1.x == 0 && q1.y == 0 && q1.z == 0) return false;
|
|
if (q0.x * q1.x + q0.y * q1.y + q0.z * q1.z + q0.w * q1.w < 0) {
|
|
q1 = {-q1.x, -q1.y, -q1.z, -q1.w};
|
|
}
|
|
const XrQuaternionf center = normalize(
|
|
{q0.x + q1.x, q0.y + q1.y, q0.z + q1.z, q0.w + q1.w});
|
|
if (center.w == 0 && center.x == 0 && center.y == 0 && center.z == 0) return false;
|
|
const XrVector3f centerPosition{
|
|
(s.views[0].pose.position.x + s.views[1].pose.position.x) * 0.5f,
|
|
(s.views[0].pose.position.y + s.views[1].pose.position.y) * 0.5f,
|
|
(s.views[0].pose.position.z + s.views[1].pose.position.z) * 0.5f,
|
|
};
|
|
const XrVector3f worldOffset{
|
|
s.views[eye].pose.position.x - centerPosition.x,
|
|
s.views[eye].pose.position.y - centerPosition.y,
|
|
s.views[eye].pose.position.z - centerPosition.z,
|
|
};
|
|
const XrVector3f localOffset = rotate(conjugate(center), worldOffset);
|
|
const XrVector3f offset = rotate(head, localOffset);
|
|
const XrQuaternionf localOrientation = multiply(conjugate(center),
|
|
eye == 0 ? q0 : q1);
|
|
result.orientation = normalize(multiply(head, localOrientation));
|
|
result.position = {headPose.Position.x + offset.x,
|
|
headPose.Position.y + offset.y,
|
|
headPose.Position.z + offset.z};
|
|
return true;
|
|
}
|
|
|
|
bool projectionGeometry(const ovrMatrix4f& matrix, const ovrRectf& rect,
|
|
uint32_t width, uint32_t height, XrFovf& fov,
|
|
XrRect2Di& imageRect) {
|
|
const float sx = matrix.M[0][0];
|
|
const float sy = matrix.M[1][1];
|
|
const float tx = matrix.M[0][2];
|
|
const float ty = matrix.M[1][2];
|
|
if (!finite(sx) || !finite(sy) || !finite(tx) || !finite(ty) ||
|
|
sx <= 0.000001f || std::fabs(sy) <= 0.000001f ||
|
|
std::fabs(matrix.M[0][1]) > 0.000001f ||
|
|
std::fabs(matrix.M[1][0]) > 0.000001f ||
|
|
!finite(rect.x) || !finite(rect.y) || !finite(rect.width) ||
|
|
!finite(rect.height) || rect.width <= 0 || rect.height <= 0 ||
|
|
rect.x < 0 || rect.y < 0 || rect.x + rect.width > 1.00001f ||
|
|
rect.y + rect.height > 1.00001f) {
|
|
return false;
|
|
}
|
|
const float x0 = (rect.x + tx) / sx;
|
|
const float x1 = (rect.x + rect.width + tx) / sx;
|
|
const float y0 = (rect.y + ty) / sy;
|
|
const float y1 = (rect.y + rect.height + ty) / sy;
|
|
fov.angleLeft = std::atan(std::min(x0, x1));
|
|
fov.angleRight = std::atan(std::max(x0, x1));
|
|
// Vulkan images start at the top-left. Preserve the tangent direction at
|
|
// each image edge: OpenXR reverses the image when angleDown > angleUp.
|
|
fov.angleUp = std::atan(y0);
|
|
fov.angleDown = std::atan(y1);
|
|
|
|
const int32_t left = std::max(0, static_cast<int32_t>(std::floor(rect.x * width)));
|
|
const int32_t top = std::max(0, static_cast<int32_t>(std::floor(rect.y * height)));
|
|
const int32_t right = std::min(static_cast<int32_t>(width),
|
|
static_cast<int32_t>(std::ceil((rect.x + rect.width) * width)));
|
|
const int32_t bottom = std::min(static_cast<int32_t>(height),
|
|
static_cast<int32_t>(std::ceil((rect.y + rect.height) * height)));
|
|
if (right <= left || bottom <= top) return false;
|
|
imageRect.offset = {left, top};
|
|
imageRect.extent = {right - left, bottom - top};
|
|
return true;
|
|
}
|
|
|
|
// VrApi cylinder -> flat OpenXR quad (the Frame runtime lacks XR_KHR_composition_layer_cylinder).
|
|
// TexCoordsFromTanAngles is the inverse of the cylinder's eye-space model-view matrix; the unit
|
|
// cylinder spans 180 degrees of u, so the central angle is pi / TextureMatrix.M[0][0].
|
|
bool invert4(const ovrMatrix4f& in, double out[4][4]) {
|
|
double a[4][8];
|
|
for (int r = 0; r < 4; ++r)
|
|
for (int c = 0; c < 8; ++c) a[r][c] = c < 4 ? in.M[r][c] : (c - 4 == r ? 1.0 : 0.0);
|
|
for (int c = 0; c < 4; ++c) {
|
|
int pivot = c;
|
|
for (int r = c + 1; r < 4; ++r)
|
|
if (std::fabs(a[r][c]) > std::fabs(a[pivot][c])) pivot = r;
|
|
if (std::fabs(a[pivot][c]) < 1e-12) return false;
|
|
if (pivot != c)
|
|
for (int k = 0; k < 8; ++k) std::swap(a[c][k], a[pivot][k]);
|
|
const double d = a[c][c];
|
|
for (int k = 0; k < 8; ++k) a[c][k] /= d;
|
|
for (int r = 0; r < 4; ++r) {
|
|
if (r == c) continue;
|
|
const double f = a[r][c];
|
|
for (int k = 0; k < 8; ++k) a[r][k] -= f * a[c][k];
|
|
}
|
|
}
|
|
for (int r = 0; r < 4; ++r)
|
|
for (int c = 0; c < 4; ++c) out[r][c] = a[r][c + 4];
|
|
return true;
|
|
}
|
|
|
|
XrQuaternionf quatFromBasis(const double m[3][3]) {
|
|
XrQuaternionf q{};
|
|
const double trace = m[0][0] + m[1][1] + m[2][2];
|
|
if (trace > 0) {
|
|
const double s = std::sqrt(trace + 1.0) * 2;
|
|
q.w = static_cast<float>(0.25 * s);
|
|
q.x = static_cast<float>((m[2][1] - m[1][2]) / s);
|
|
q.y = static_cast<float>((m[0][2] - m[2][0]) / s);
|
|
q.z = static_cast<float>((m[1][0] - m[0][1]) / s);
|
|
} else if (m[0][0] > m[1][1] && m[0][0] > m[2][2]) {
|
|
const double s = std::sqrt(1.0 + m[0][0] - m[1][1] - m[2][2]) * 2;
|
|
q.w = static_cast<float>((m[2][1] - m[1][2]) / s);
|
|
q.x = static_cast<float>(0.25 * s);
|
|
q.y = static_cast<float>((m[0][1] + m[1][0]) / s);
|
|
q.z = static_cast<float>((m[0][2] + m[2][0]) / s);
|
|
} else if (m[1][1] > m[2][2]) {
|
|
const double s = std::sqrt(1.0 + m[1][1] - m[0][0] - m[2][2]) * 2;
|
|
q.w = static_cast<float>((m[0][2] - m[2][0]) / s);
|
|
q.x = static_cast<float>((m[0][1] + m[1][0]) / s);
|
|
q.y = static_cast<float>(0.25 * s);
|
|
q.z = static_cast<float>((m[1][2] + m[2][1]) / s);
|
|
} else {
|
|
const double s = std::sqrt(1.0 + m[2][2] - m[0][0] - m[1][1]) * 2;
|
|
q.w = static_cast<float>((m[1][0] - m[0][1]) / s);
|
|
q.x = static_cast<float>((m[0][2] + m[2][0]) / s);
|
|
q.y = static_cast<float>((m[1][2] + m[2][1]) / s);
|
|
q.z = static_cast<float>(0.25 * s);
|
|
}
|
|
return normalize(q);
|
|
}
|
|
|
|
// Eye-space pose and size of a flat quad approximating the cylinder seen by `eye`.
|
|
bool cylinderAsQuad(const ovrLayerCylinder2& layer, uint32_t eye, uint32_t texWidth, uint32_t texHeight,
|
|
XrPosef& local, XrExtent2Df& size) {
|
|
const auto& t = layer.Textures[eye];
|
|
double mv[4][4];
|
|
if (!invert4(t.TexCoordsFromTanAngles, mv)) return false;
|
|
double basis[3][3], len[3];
|
|
for (int c = 0; c < 3; ++c) {
|
|
len[c] = std::sqrt(mv[0][c] * mv[0][c] + mv[1][c] * mv[1][c] + mv[2][c] * mv[2][c]);
|
|
if (!(len[c] > 1e-6)) return false;
|
|
for (int r = 0; r < 3; ++r) basis[r][c] = mv[r][c] / len[c];
|
|
}
|
|
const double radius = len[0];
|
|
const float uScale = t.TextureMatrix.M[0][0];
|
|
double angle = uScale > 0.01f ? M_PI / uScale : M_PI / 2;
|
|
if (angle > 2 * M_PI) angle = 2 * M_PI;
|
|
const float rw = t.TextureRect.width > 0 ? t.TextureRect.width : 1.0f;
|
|
const float rh = t.TextureRect.height > 0 ? t.TextureRect.height : 1.0f;
|
|
size.width = static_cast<float>(radius * angle);
|
|
size.height = size.width * (static_cast<float>(texHeight) * rh) / (static_cast<float>(texWidth) * rw);
|
|
local.orientation = quatFromBasis(basis);
|
|
const XrVector3f forward = rotate(local.orientation, {0, 0, static_cast<float>(-radius)});
|
|
local.position = {static_cast<float>(mv[0][3]) + forward.x, static_cast<float>(mv[1][3]) + forward.y,
|
|
static_cast<float>(mv[2][3]) + forward.z};
|
|
return finite(size.width) && finite(size.height) && size.width > 0 && size.height > 0 &&
|
|
finite(local.position.x) && finite(local.position.y) && finite(local.position.z);
|
|
}
|
|
|
|
XrPosef composePose(const XrPosef& a, const XrPosef& b) {
|
|
XrPosef r;
|
|
r.orientation = normalize(multiply(a.orientation, b.orientation));
|
|
const XrVector3f p = rotate(a.orientation, b.position);
|
|
r.position = {a.position.x + p.x, a.position.y + p.y, a.position.z + p.z};
|
|
return r;
|
|
}
|
|
|
|
struct PreparedLayer {
|
|
const ovrLayerProjection2* source = nullptr;
|
|
const ovrLayerCylinder2* cylinder = nullptr;
|
|
XrCompositionLayerQuad quad{XR_TYPE_COMPOSITION_LAYER_QUAD};
|
|
bool black = false;
|
|
XrCompositionLayerProjection projection{XR_TYPE_COMPOSITION_LAYER_PROJECTION};
|
|
std::array<XrCompositionLayerProjectionView, 2> views{{
|
|
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
|
|
{XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW},
|
|
}};
|
|
};
|
|
|
|
struct CopyJob {
|
|
ovrTextureSwapChain* chain = nullptr;
|
|
uint32_t sourceIndex = 0;
|
|
uint32_t outputIndex = 0;
|
|
bool acquired = false;
|
|
};
|
|
|
|
ovrResult failFrame(ovrResult error) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
if (s.frameBegun) {
|
|
const XrResult end = ovp::endFrame(nullptr, 0);
|
|
if (XR_FAILED(end)) return DeviceUnavailable;
|
|
}
|
|
return error;
|
|
}
|
|
|
|
void releaseJobs(std::array<CopyJob, kMaxCopyJobs>& jobs, uint32_t count) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
if (!jobs[i].acquired) continue;
|
|
const XrSwapchainImageReleaseInfo release{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
|
ovp::xrOk(s.xr.xrReleaseSwapchainImage(jobs[i].chain->output, &release),
|
|
"xrReleaseSwapchainImage(error cleanup)");
|
|
jobs[i].acquired = false;
|
|
}
|
|
}
|
|
|
|
bool acquireJobs(std::array<CopyJob, kMaxCopyJobs>& jobs, uint32_t count) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
const XrSwapchainImageAcquireInfo acquire{XR_TYPE_SWAPCHAIN_IMAGE_ACQUIRE_INFO};
|
|
XrResult result = s.xr.xrAcquireSwapchainImage(jobs[i].chain->output, &acquire,
|
|
&jobs[i].outputIndex);
|
|
if (result != XR_SUCCESS ||
|
|
jobs[i].outputIndex >= jobs[i].chain->outputImageCount) {
|
|
ovp::xrOk(result, "xrAcquireSwapchainImage");
|
|
releaseJobs(jobs, i);
|
|
return false;
|
|
}
|
|
jobs[i].acquired = true;
|
|
XrSwapchainImageWaitInfo wait{XR_TYPE_SWAPCHAIN_IMAGE_WAIT_INFO};
|
|
wait.timeout = static_cast<XrDuration>(kFrameWaitNs);
|
|
result = s.xr.xrWaitSwapchainImage(jobs[i].chain->output, &wait);
|
|
if (result != XR_SUCCESS) {
|
|
ovp::xrOk(result, "xrWaitSwapchainImage");
|
|
releaseJobs(jobs, i + 1);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool submitCopiesGl(std::array<CopyJob, kMaxCopyJobs>& jobs, uint32_t count) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
#ifdef OVP_GL_DIAG // build with -DOVP_GL_DIAG for app GL state + eye image readback probes
|
|
{ // Diagnostic: what did the app's rendering leave behind this frame?
|
|
static int frames;
|
|
if (frames++ % 360 == 0 && frames < 4000) {
|
|
GLenum appErrors[4] = {0};
|
|
int n = 0;
|
|
for (GLenum e; (e = glGetError()) != GL_NO_ERROR && n < 4;) appErrors[n++] = e;
|
|
GLint drawFbo = 0;
|
|
glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFbo);
|
|
GLenum status = drawFbo ? glCheckFramebufferStatus(GL_DRAW_FRAMEBUFFER) : 0;
|
|
GLint type = 0, name = 0, layer = 0, views = 0;
|
|
if (drawFbo) {
|
|
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE, &type);
|
|
if (type == GL_TEXTURE) {
|
|
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME, &name);
|
|
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER, &layer);
|
|
glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
|
|
0x9630 /* GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_NUM_VIEWS_OVR */, &views);
|
|
}
|
|
}
|
|
glGetError(); // the NUM_VIEWS query errors without GL_OVR_multiview
|
|
OVP_LOG("GL app state: errors=[0x%x 0x%x 0x%x 0x%x] drawFBO=%d status=0x%x attach type=0x%x name=%d layer=%d views=%d",
|
|
appErrors[0], appErrors[1], appErrors[2], appErrors[3], drawFbo, status, type, name, layer, views);
|
|
}
|
|
}
|
|
#endif
|
|
if (count == 0) return true;
|
|
if (!acquireJobs(jobs, count)) return false;
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
ovrTextureSwapChain* chain = jobs[i].chain;
|
|
const GLuint source = chain->glTextures[jobs[i].sourceIndex];
|
|
const GLuint destination = chain->glOutputImages[jobs[i].outputIndex].image;
|
|
const GLenum target = chain->arrayLayers > 1 ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_2D;
|
|
while (glGetError() != GL_NO_ERROR) {} // don't blame the copy for errors the app left behind
|
|
glCopyImageSubData(source, target, 0, 0, 0, 0, destination, target, 0, 0, 0, 0,
|
|
static_cast<GLsizei>(chain->width), static_cast<GLsizei>(chain->height),
|
|
static_cast<GLsizei>(chain->arrayLayers));
|
|
const GLenum error = glGetError();
|
|
static int errors;
|
|
if (error != GL_NO_ERROR && errors++ < 5) OVP_ERROR("glCopyImageSubData failed: 0x%x", error);
|
|
#ifdef OVP_GL_DIAG
|
|
static int probes;
|
|
if (probes < 4000 && (probes++ % 360) == 0) {
|
|
// Diagnostic: centre pixel of the app image and of the runtime image after the copy.
|
|
GLint oldRead = 0;
|
|
glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &oldRead);
|
|
GLuint fbo = 0;
|
|
glGenFramebuffers(1, &fbo);
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
|
|
unsigned char px[2][4] = {{0}};
|
|
unsigned int nonblack = 0;
|
|
const GLuint names[2] = {source, destination};
|
|
for (int k = 0; k < 2; ++k) {
|
|
if (target == GL_TEXTURE_2D_ARRAY) glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, names[k], 0, 0);
|
|
else glFramebufferTexture2D(GL_READ_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, names[k], 0);
|
|
glReadPixels(static_cast<GLint>(chain->width / 2), static_cast<GLint>(chain->height / 2), 1, 1, GL_RGBA,
|
|
GL_UNSIGNED_BYTE, px[k]);
|
|
if (k == 0) // also scan a coarse 8x8 grid of the app image for any non-black pixel
|
|
for (int gy = 1; gy < 8; ++gy)
|
|
for (int gx = 1; gx < 8; ++gx) {
|
|
unsigned char p[4] = {0};
|
|
glReadPixels(static_cast<GLint>(chain->width * gx / 8), static_cast<GLint>(chain->height * gy / 8), 1, 1,
|
|
GL_RGBA, GL_UNSIGNED_BYTE, p);
|
|
if (p[0] | p[1] | p[2]) ++nonblack;
|
|
}
|
|
}
|
|
const GLenum readError = glGetError();
|
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(oldRead));
|
|
glDeleteFramebuffers(1, &fbo);
|
|
OVP_LOG("GL probe: src tex %u idx %u = %u,%u,%u,%u (%u/49 grid non-black); dst tex %u = %u,%u,%u,%u (err 0x%x) ctx=%p created-in=%p",
|
|
source, jobs[i].sourceIndex, px[0][0], px[0][1], px[0][2], px[0][3], nonblack, destination, px[1][0], px[1][1],
|
|
px[1][2], px[1][3], readError, eglGetCurrentContext(), chain->glContext);
|
|
}
|
|
#endif
|
|
}
|
|
glFlush();
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
const XrSwapchainImageReleaseInfo release{XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO};
|
|
ovp::xrOk(s.xr.xrReleaseSwapchainImage(jobs[i].chain->output, &release), "xrReleaseSwapchainImage(GLES)");
|
|
jobs[i].acquired = false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool submitCopies(std::array<CopyJob, kMaxCopyJobs>& jobs, uint32_t count) {
|
|
if (ovp::runtime().gles) return submitCopiesGl(jobs, count);
|
|
if (!settleCopy(kFrameWaitNs, false)) return false;
|
|
reapRetired();
|
|
if (count == 0) return true;
|
|
if (!ensureCopyContext()) return false;
|
|
if (!acquireJobs(jobs, count)) return false;
|
|
|
|
if (vkResetCommandBuffer(gCopy.command, 0) != VK_SUCCESS) {
|
|
releaseJobs(jobs, count);
|
|
return false;
|
|
}
|
|
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
|
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
|
if (vkBeginCommandBuffer(gCopy.command, &begin) != VK_SUCCESS) {
|
|
releaseJobs(jobs, count);
|
|
return false;
|
|
}
|
|
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
ovrTextureSwapChain* chain = jobs[i].chain;
|
|
const VkImage source = chain->appImages[jobs[i].sourceIndex].image;
|
|
const VkImage destination = chain->outputImages[jobs[i].outputIndex].image;
|
|
const bool initialized = chain->outputInitialized[jobs[i].outputIndex];
|
|
|
|
std::array<VkImageMemoryBarrier, 2> before{};
|
|
before[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
before[0].srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
before[0].dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
before[0].oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
before[0].newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
|
before[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
before[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
before[0].image = source;
|
|
before[0].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, chain->levels,
|
|
0, chain->arrayLayers};
|
|
before[1].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
before[1].srcAccessMask = initialized ?
|
|
(VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT) : 0;
|
|
before[1].dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
before[1].oldLayout = initialized ? VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL :
|
|
VK_IMAGE_LAYOUT_UNDEFINED;
|
|
before[1].newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
before[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
before[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
before[1].image = destination;
|
|
before[1].subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, chain->levels,
|
|
0, chain->arrayLayers};
|
|
vkCmdPipelineBarrier(gCopy.command, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
|
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr,
|
|
static_cast<uint32_t>(before.size()), before.data());
|
|
|
|
for (uint32_t level = 0; level < chain->levels; ++level) {
|
|
VkImageCopy copy{};
|
|
copy.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, chain->arrayLayers};
|
|
copy.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, 0, chain->arrayLayers};
|
|
copy.extent = {std::max(1u, chain->width >> level),
|
|
std::max(1u, chain->height >> level), 1};
|
|
vkCmdCopyImage(gCopy.command, source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
|
destination, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
|
}
|
|
|
|
std::array<VkImageMemoryBarrier, 2> after{};
|
|
after[0].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
after[0].srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
|
after[0].dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
|
after[0].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
|
after[0].newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
|
after[0].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
after[0].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
after[0].image = source;
|
|
after[0].subresourceRange = before[0].subresourceRange;
|
|
after[1].sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
|
after[1].srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
|
after[1].dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
|
|
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
|
|
after[1].oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
|
after[1].newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
|
after[1].srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
after[1].dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
|
after[1].image = destination;
|
|
after[1].subresourceRange = before[1].subresourceRange;
|
|
vkCmdPipelineBarrier(gCopy.command, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
|
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr,
|
|
static_cast<uint32_t>(after.size()), after.data());
|
|
}
|
|
|
|
if (vkEndCommandBuffer(gCopy.command) != VK_SUCCESS ||
|
|
vkResetFences(gCopy.device, 1, &gCopy.fence) != VK_SUCCESS) {
|
|
releaseJobs(jobs, count);
|
|
return false;
|
|
}
|
|
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
|
submit.commandBufferCount = 1;
|
|
submit.pCommandBuffers = &gCopy.command;
|
|
const VkResult submitted = vkQueueSubmit(gCopy.queue, 1, &submit, gCopy.fence);
|
|
if (submitted != VK_SUCCESS) {
|
|
OVP_ERROR("vkQueueSubmit failed: %d", static_cast<int>(submitted));
|
|
releaseJobs(jobs, count);
|
|
return false;
|
|
}
|
|
gCopy.submitted = true;
|
|
gCopy.pendingCount = count;
|
|
for (uint32_t i = 0; i < count; ++i) {
|
|
gCopy.pending[i] = jobs[i].chain->output;
|
|
jobs[i].chain->outputInitialized[jobs[i].outputIndex] = true;
|
|
jobs[i].acquired = false;
|
|
}
|
|
return settleCopy(kFrameWaitNs, false);
|
|
}
|
|
} // namespace
|
|
|
|
VRAPI ovrTextureSwapChain* vrapi_CreateTextureSwapChain3(int32_t type, int64_t format,
|
|
int width, int height,
|
|
int levels, int bufferCount) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
if (s.vk.Device == VK_NULL_HANDLE || s.vk.PhysicalDevice == VK_NULL_HANDLE ||
|
|
(type != kTexture2D && type != kTexture2DArray) || width <= 0 || height <= 0 ||
|
|
width > 16384 || height > 16384 || levels <= 0 || bufferCount <= 0 ||
|
|
bufferCount > static_cast<int>(kMaxBuffers) ||
|
|
format < std::numeric_limits<int32_t>::min() ||
|
|
format > std::numeric_limits<int32_t>::max()) {
|
|
OVP_ERROR("Invalid Vulkan texture swapchain parameters");
|
|
return nullptr;
|
|
}
|
|
const uint32_t maxDimension = static_cast<uint32_t>(std::max(width, height));
|
|
uint32_t maxLevels = 1;
|
|
for (uint32_t dimension = maxDimension; dimension > 1; dimension >>= 1) ++maxLevels;
|
|
const VkFormat vkFormat = static_cast<VkFormat>(format);
|
|
if (levels > static_cast<int>(maxLevels) || !supportedFormat(vkFormat)) {
|
|
OVP_ERROR("Unsupported Vulkan texture swapchain type/format/mip count");
|
|
return nullptr;
|
|
}
|
|
VkFormatProperties properties{};
|
|
vkGetPhysicalDeviceFormatProperties(s.vk.PhysicalDevice, vkFormat, &properties);
|
|
const VkFormatFeatureFlags needed = VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT |
|
|
VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT |
|
|
VK_FORMAT_FEATURE_TRANSFER_SRC_BIT |
|
|
VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
|
|
if ((properties.optimalTilingFeatures & needed) != needed) {
|
|
OVP_ERROR("VkFormat %d lacks required attachment, sampling, or transfer support",
|
|
static_cast<int>(vkFormat));
|
|
return nullptr;
|
|
}
|
|
|
|
ovrTextureSwapChain* chain = new (std::nothrow) ovrTextureSwapChain();
|
|
if (!chain) return nullptr;
|
|
chain->type = type;
|
|
chain->format = vkFormat;
|
|
chain->width = static_cast<uint32_t>(width);
|
|
chain->height = static_cast<uint32_t>(height);
|
|
chain->levels = static_cast<uint32_t>(levels);
|
|
chain->bufferCount = static_cast<uint32_t>(bufferCount);
|
|
chain->arrayLayers = type == kTexture2DArray ? 2u : 1u;
|
|
|
|
for (uint32_t i = 0; i < chain->bufferCount; ++i) {
|
|
VkImageCreateInfo image{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
|
image.imageType = VK_IMAGE_TYPE_2D;
|
|
image.format = chain->format;
|
|
image.extent = {chain->width, chain->height, 1};
|
|
image.mipLevels = chain->levels;
|
|
image.arrayLayers = chain->arrayLayers;
|
|
image.samples = VK_SAMPLE_COUNT_1_BIT;
|
|
image.tiling = VK_IMAGE_TILING_OPTIMAL;
|
|
image.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
|
|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
image.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
|
image.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
|
if (vkCreateImage(s.vk.Device, &image, nullptr, &chain->appImages[i].image) !=
|
|
VK_SUCCESS) {
|
|
destroyAppImages(chain);
|
|
delete chain;
|
|
return nullptr;
|
|
}
|
|
VkMemoryRequirements requirements{};
|
|
vkGetImageMemoryRequirements(s.vk.Device, chain->appImages[i].image, &requirements);
|
|
uint32_t memoryType = 0;
|
|
if (!findMemoryType(s.vk.PhysicalDevice, requirements.memoryTypeBits,
|
|
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, memoryType)) {
|
|
destroyAppImages(chain);
|
|
delete chain;
|
|
return nullptr;
|
|
}
|
|
VkMemoryAllocateInfo allocate{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
|
allocate.allocationSize = requirements.size;
|
|
allocate.memoryTypeIndex = memoryType;
|
|
if (vkAllocateMemory(s.vk.Device, &allocate, nullptr,
|
|
&chain->appImages[i].memory) != VK_SUCCESS ||
|
|
vkBindImageMemory(s.vk.Device, chain->appImages[i].image,
|
|
chain->appImages[i].memory, 0) != VK_SUCCESS) {
|
|
destroyAppImages(chain);
|
|
delete chain;
|
|
return nullptr;
|
|
}
|
|
}
|
|
chain->next = gChains;
|
|
gChains = chain;
|
|
reapRetired();
|
|
return chain;
|
|
}
|
|
|
|
VRAPI int vrapi_GetTextureSwapChainLength(ovrTextureSwapChain* candidate) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
ovrTextureSwapChain* chain = findChain(candidate);
|
|
return chain ? static_cast<int>(chain->bufferCount) : 0;
|
|
}
|
|
|
|
VRAPI VkImage vrapi_GetTextureSwapChainBufferVulkan(ovrTextureSwapChain* candidate,
|
|
int index) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
ovrTextureSwapChain* chain = findChain(candidate);
|
|
if (!chain || index < 0 || index >= static_cast<int>(chain->bufferCount)) {
|
|
return VK_NULL_HANDLE;
|
|
}
|
|
return chain->appImages[static_cast<uint32_t>(index)].image;
|
|
}
|
|
|
|
VRAPI ovrResult vrapi_GetTextureSwapChainBufferFoveationVulkan(
|
|
ovrTextureSwapChain* candidate, int index, VkImage* image,
|
|
uint32_t* imageWidth, uint32_t* imageHeight) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
if (image) *image = VK_NULL_HANDLE;
|
|
if (imageWidth) *imageWidth = 0;
|
|
if (imageHeight) *imageHeight = 0;
|
|
ovrTextureSwapChain* chain = findChain(candidate);
|
|
if (!chain || index < 0 || index >= static_cast<int>(chain->bufferCount) ||
|
|
!image || !imageWidth || !imageHeight) {
|
|
return InvalidParameter;
|
|
}
|
|
if (!ensureFullRateImage(chain)) {
|
|
OVP_ERROR("Failed to initialize full-rate compatibility texture");
|
|
return InvalidOperation;
|
|
}
|
|
*image = chain->fullRateImage.image;
|
|
*imageWidth = *imageHeight = 1;
|
|
return Success;
|
|
}
|
|
|
|
VRAPI void vrapi_DestroyTextureSwapChain(ovrTextureSwapChain* candidate) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
ovrTextureSwapChain** link = &gChains;
|
|
while (*link && *link != candidate) link = &(*link)->next;
|
|
if (!*link) {
|
|
if (candidate && !isBuiltin(candidate)) OVP_ERROR("Destroying unknown texture swapchain");
|
|
return;
|
|
}
|
|
ovrTextureSwapChain* chain = *link;
|
|
*link = chain->next;
|
|
if (gCopy.submitted && !settleCopy(kFrameWaitNs, false)) {
|
|
chain->next = gRetired;
|
|
gRetired = chain;
|
|
return;
|
|
}
|
|
reapRetired();
|
|
destroyChain(chain);
|
|
}
|
|
|
|
VRAPI ovrResult vrapi_SubmitFrame2(ovrMobile* mobile,
|
|
const ovrSubmitFrameDescription2* description) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
if (!ovp::validMobile(mobile) || !description ||
|
|
description->LayerCount > kMaxLayers ||
|
|
(description->LayerCount != 0 && !description->Layers)) {
|
|
return failFrame(InvalidParameter);
|
|
}
|
|
// Volume suppression and the old texture-density visualization only affect Quest's compositor overlays.
|
|
// OpenXR has neither overlay; accepting those flags must still submit the application's actual layers.
|
|
if ((description->Flags & ~(kFrameFlush | kFrameInhibitVolume | kFrameShowTextureDensity)) != 0 ||
|
|
description->SwapInterval != 1) {
|
|
OVP_ERROR("Unsupported frame flags or swap interval: flags=0x%x interval=%u",
|
|
description->Flags, description->SwapInterval);
|
|
return failFrame(Unsupported);
|
|
}
|
|
|
|
std::array<PreparedLayer, kMaxLayers> prepared{};
|
|
for (uint32_t i = 0; i < description->LayerCount; ++i) {
|
|
const ovrLayerHeader2* header = description->Layers[i];
|
|
if (!header) return failFrame(InvalidParameter);
|
|
if (header->Type == 6) { // VRAPI_LAYER_TYPE_LOADING_ICON2: nothing to show on OpenXR
|
|
prepared[i].black = true;
|
|
continue;
|
|
}
|
|
if (header->Type == 3) { // VRAPI_LAYER_TYPE_CYLINDER2 -> flat quad
|
|
const auto* cylinder = reinterpret_cast<const ovrLayerCylinder2*>(header);
|
|
ovrTextureSwapChain* chain = findChain(cylinder->Textures[0].ColorSwapChain);
|
|
const int index = cylinder->Textures[0].SwapChainIndex;
|
|
if (!chain || index < 0 || index >= static_cast<int>(chain->bufferCount) ||
|
|
header->ColorScale.w <= 0.0f) {
|
|
prepared[i].black = true;
|
|
continue;
|
|
}
|
|
prepared[i].cylinder = cylinder;
|
|
if (header->SrcBlend == kBlendOne && header->DstBlend == kBlendZero) prepared[i].quad.layerFlags = 0;
|
|
else if (header->SrcBlend == kBlendOne && header->DstBlend == kBlendOneMinusSrcAlpha)
|
|
prepared[i].quad.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
|
else
|
|
prepared[i].quad.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT |
|
|
XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT;
|
|
continue;
|
|
}
|
|
if (header->Type != kProjectionLayer) {
|
|
OVP_ERROR("Unsupported VrApi layer type %d", header->Type);
|
|
return failFrame(Unsupported);
|
|
}
|
|
if (header->Reserved ||
|
|
(header->Flags & ~(kLayerChromatic | kLayerFixedToView |
|
|
kLayerClipRect | kLayerInhibitSrgb)) != 0) {
|
|
OVP_ERROR("Unsupported projection layer flags=0x%x reserved=%p",
|
|
header->Flags, header->Reserved);
|
|
return failFrame(Unsupported);
|
|
}
|
|
PreparedLayer& output = prepared[i];
|
|
output.source = reinterpret_cast<const ovrLayerProjection2*>(header);
|
|
const bool black = allColor(header->ColorScale, 0.0f);
|
|
const bool normalColor = allColor(header->ColorScale, 1.0f);
|
|
if (!black && !normalColor) {
|
|
OVP_ERROR("Projection color scaling is unsupported");
|
|
return failFrame(Unsupported);
|
|
}
|
|
if (header->SrcBlend == kBlendOne && header->DstBlend == kBlendZero) {
|
|
output.projection.layerFlags = 0;
|
|
} else if (header->SrcBlend == kBlendSrcAlpha &&
|
|
header->DstBlend == kBlendOneMinusSrcAlpha) {
|
|
output.projection.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT |
|
|
XR_COMPOSITION_LAYER_UNPREMULTIPLIED_ALPHA_BIT;
|
|
} else if (header->SrcBlend == kBlendOne &&
|
|
header->DstBlend == kBlendOneMinusSrcAlpha) {
|
|
output.projection.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT;
|
|
} else {
|
|
OVP_ERROR("Unsupported projection blend function");
|
|
return failFrame(Unsupported);
|
|
}
|
|
|
|
bool allDefault = true;
|
|
for (uint32_t eye = 0; eye < 2; ++eye) {
|
|
const ovrTextureSwapChain* candidate = output.source->Textures[eye].ColorSwapChain;
|
|
if (reinterpret_cast<uintptr_t>(candidate) == kLoadingSwapchain) {
|
|
OVP_ERROR("Built-in loading-icon swapchain has no OpenXR equivalent");
|
|
return failFrame(Unsupported);
|
|
}
|
|
if (reinterpret_cast<uintptr_t>(candidate) == kDefaultSwapchain) continue;
|
|
allDefault = false;
|
|
ovrTextureSwapChain* chain = findChain(candidate);
|
|
const int index = output.source->Textures[eye].SwapChainIndex;
|
|
if (!chain || index < 0 || index >= static_cast<int>(chain->bufferCount)) {
|
|
return failFrame(InvalidParameter);
|
|
}
|
|
}
|
|
if (allDefault) {
|
|
if (!black) {
|
|
OVP_ERROR("Default swapchain is only supported for an explicit black layer");
|
|
return failFrame(Unsupported);
|
|
}
|
|
output.black = true;
|
|
} else if (black) {
|
|
OVP_ERROR("Color-scaled application swapchains are unsupported");
|
|
return failFrame(Unsupported);
|
|
} else {
|
|
for (uint32_t eye = 0; eye < 2; ++eye) {
|
|
if (isBuiltin(output.source->Textures[eye].ColorSwapChain)) {
|
|
OVP_ERROR("Mixed built-in and application projection textures unsupported");
|
|
return failFrame(Unsupported);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!ovp::ensureSession(s.queue) || !ovp::beginFrame(static_cast<int64_t>(description->FrameIndex))) {
|
|
return failFrame(DeviceUnavailable);
|
|
}
|
|
// A short runtime visibility dip can be held by FrameBridge's focus_hold.
|
|
// Continue supplying the real image while the application still has focus;
|
|
// otherwise an empty submission replaces it with the runtime's black image.
|
|
// shouldRender is an advisory; actual focus loss keeps the ordinary path.
|
|
const char* keepFocused = std::getenv("FRAMEPORT_VRAPI_KEEP_FOCUSED_IMAGE");
|
|
const bool retainImage = keepFocused && std::strcmp(keepFocused, "1") == 0 &&
|
|
s.sessionState == XR_SESSION_STATE_FOCUSED;
|
|
if (!s.frame.shouldRender && !retainImage) {
|
|
const XrResult result = ovp::endFrame(nullptr, 0);
|
|
return XR_SUCCEEDED(result) ? Success : DeviceUnavailable;
|
|
}
|
|
if (!s.frame.shouldRender && retainImage) {
|
|
static unsigned held = 0;
|
|
if (held++ % 72 == 0) OVP_LOG("CLIMB visibility: retaining image during held focus dip (%u)", held);
|
|
}
|
|
|
|
std::array<CopyJob, kMaxCopyJobs> jobs{};
|
|
uint32_t jobCount = 0;
|
|
std::array<const XrCompositionLayerBaseHeader*, kMaxLayers> xrLayers{};
|
|
uint32_t xrLayerCount = 0;
|
|
std::array<XrView, 2> headLockedViews{{{XR_TYPE_VIEW}, {XR_TYPE_VIEW}}};
|
|
bool haveHeadLockedViews = false;
|
|
for (uint32_t i = 0; i < description->LayerCount; ++i) {
|
|
PreparedLayer& output = prepared[i];
|
|
if (output.black) continue;
|
|
if (output.cylinder) {
|
|
const auto& tex = output.cylinder->Textures[0];
|
|
ovrTextureSwapChain* chain = findChain(tex.ColorSwapChain);
|
|
if (!chain || !ensureOutput(chain)) return failFrame(DeviceUnavailable);
|
|
XrPosef local;
|
|
XrExtent2Df size;
|
|
if (!cylinderAsQuad(*output.cylinder, 0, chain->width, chain->height, local, size)) {
|
|
static int bad;
|
|
if (bad++ < 5) OVP_ERROR("Cylinder layer has an invalid transform; skipped");
|
|
continue;
|
|
}
|
|
const bool cylHeadLocked = (output.cylinder->Header.Flags & kLayerFixedToView) != 0;
|
|
XrPosef eye0;
|
|
if (cylHeadLocked) {
|
|
output.quad.space = s.viewSpace;
|
|
output.quad.pose = local;
|
|
} else if (eyePose(output.cylinder->HeadPose.Pose, 0, eye0)) {
|
|
output.quad.space = s.appSpace;
|
|
output.quad.pose = composePose(eye0, local);
|
|
} else {
|
|
continue;
|
|
}
|
|
static int logged;
|
|
if (logged++ < 3)
|
|
OVP_LOG("Cylinder->quad: tex %ux%u uScale=%g size=%.2fx%.2f m local pos=(%.2f,%.2f,%.2f) headLocked=%d",
|
|
chain->width, chain->height, tex.TextureMatrix.M[0][0], size.width, size.height,
|
|
local.position.x, local.position.y, local.position.z, cylHeadLocked);
|
|
output.quad.eyeVisibility = XR_EYE_VISIBILITY_BOTH;
|
|
output.quad.size = size;
|
|
output.quad.subImage.swapchain = chain->output;
|
|
output.quad.subImage.imageArrayIndex = 0;
|
|
const float rx = tex.TextureRect.width > 0 ? tex.TextureRect.x : 0.0f;
|
|
const float ry = tex.TextureRect.height > 0 ? tex.TextureRect.y : 0.0f;
|
|
const float rw = tex.TextureRect.width > 0 ? tex.TextureRect.width : 1.0f;
|
|
const float rh = tex.TextureRect.height > 0 ? tex.TextureRect.height : 1.0f;
|
|
output.quad.subImage.imageRect.offset = {static_cast<int32_t>(rx * chain->width),
|
|
static_cast<int32_t>(ry * chain->height)};
|
|
output.quad.subImage.imageRect.extent = {std::max(1, static_cast<int32_t>(rw * chain->width)),
|
|
std::max(1, static_cast<int32_t>(rh * chain->height))};
|
|
uint32_t job = 0;
|
|
for (; job < jobCount && jobs[job].chain != chain; ++job) {}
|
|
if (job == jobCount) {
|
|
if (jobCount == kMaxCopyJobs) return failFrame(Unsupported);
|
|
jobs[jobCount].chain = chain;
|
|
jobs[jobCount].sourceIndex = static_cast<uint32_t>(tex.SwapChainIndex);
|
|
++jobCount;
|
|
}
|
|
xrLayers[xrLayerCount++] = reinterpret_cast<const XrCompositionLayerBaseHeader*>(&output.quad);
|
|
continue;
|
|
}
|
|
const bool headLocked = (output.source->Header.Flags & kLayerFixedToView) != 0;
|
|
if (headLocked && !haveHeadLockedViews) {
|
|
XrViewLocateInfo locate{XR_TYPE_VIEW_LOCATE_INFO};
|
|
locate.viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO;
|
|
locate.displayTime = s.frame.predictedDisplayTime;
|
|
locate.space = s.viewSpace;
|
|
XrViewState state{XR_TYPE_VIEW_STATE};
|
|
uint32_t count = 0;
|
|
constexpr XrViewStateFlags validPose =
|
|
XR_VIEW_STATE_ORIENTATION_VALID_BIT | XR_VIEW_STATE_POSITION_VALID_BIT;
|
|
if (!ovp::xrOk(s.xr.xrLocateViews(s.session, &locate, &state, 2, &count,
|
|
headLockedViews.data()), "xrLocateViews(VIEW)") ||
|
|
count != 2 || (state.viewStateFlags & validPose) != validPose) {
|
|
return failFrame(DeviceUnavailable);
|
|
}
|
|
haveHeadLockedViews = true;
|
|
}
|
|
output.projection.space = headLocked ? s.viewSpace : s.appSpace;
|
|
output.projection.viewCount = 2;
|
|
output.projection.views = output.views.data();
|
|
for (uint32_t eye = 0; eye < 2; ++eye) {
|
|
const auto& sourceTexture = output.source->Textures[eye];
|
|
ovrTextureSwapChain* chain = findChain(sourceTexture.ColorSwapChain);
|
|
if (!chain || !ensureOutput(chain)) return failFrame(DeviceUnavailable);
|
|
// Fixed-to-view images are not timewarped using the application's
|
|
// render HeadPose. Keep the runtime's actual eye offsets/cant in VIEW.
|
|
if (headLocked) output.views[eye].pose = headLockedViews[eye].pose;
|
|
if ((!headLocked && !eyePose(output.source->HeadPose.Pose, eye, output.views[eye].pose)) ||
|
|
!projectionGeometry(sourceTexture.TexCoordsFromTanAngles,
|
|
sourceTexture.TextureRect, chain->width, chain->height,
|
|
output.views[eye].fov,
|
|
output.views[eye].subImage.imageRect)) {
|
|
const auto& matrix = sourceTexture.TexCoordsFromTanAngles;
|
|
const auto& rect = sourceTexture.TextureRect;
|
|
OVP_ERROR("Invalid projection geometry or pose: eye=%u flags=0x%x "
|
|
"scale=(%g,%g) shear=(%g,%g) offset=(%g,%g) rect=(%g,%g,%g,%g)",
|
|
eye, output.source->Header.Flags, matrix.M[0][0], matrix.M[1][1],
|
|
matrix.M[0][1], matrix.M[1][0], matrix.M[0][2], matrix.M[1][2],
|
|
rect.x, rect.y, rect.width, rect.height);
|
|
return failFrame(InvalidParameter);
|
|
}
|
|
if (chain->gl) { // GL images start at the bottom row: reverse the vertical tangent mapping
|
|
std::swap(output.views[eye].fov.angleUp, output.views[eye].fov.angleDown);
|
|
}
|
|
output.views[eye].subImage.swapchain = chain->output;
|
|
output.views[eye].subImage.imageArrayIndex =
|
|
chain->type == kTexture2DArray ? eye : 0;
|
|
|
|
uint32_t job = 0;
|
|
for (; job < jobCount; ++job) {
|
|
if (jobs[job].chain != chain) continue;
|
|
if (jobs[job].sourceIndex != static_cast<uint32_t>(sourceTexture.SwapChainIndex)) {
|
|
OVP_ERROR("One XR swapchain cannot present multiple app indices in one frame");
|
|
return failFrame(Unsupported);
|
|
}
|
|
break;
|
|
}
|
|
if (job == jobCount) {
|
|
if (jobCount == kMaxCopyJobs) return failFrame(Unsupported);
|
|
jobs[jobCount].chain = chain;
|
|
jobs[jobCount].sourceIndex =
|
|
static_cast<uint32_t>(sourceTexture.SwapChainIndex);
|
|
++jobCount;
|
|
}
|
|
}
|
|
xrLayers[xrLayerCount++] = reinterpret_cast<const XrCompositionLayerBaseHeader*>(
|
|
&output.projection);
|
|
}
|
|
|
|
if (!submitCopies(jobs, jobCount)) return failFrame(DeviceUnavailable);
|
|
const XrResult result = ovp::endFrame(xrLayers.data(), xrLayerCount);
|
|
return XR_SUCCEEDED(result) ? Success : DeviceUnavailable;
|
|
}
|
|
|
|
namespace ovp::graphics {
|
|
void shutdownSession() {
|
|
Runtime& s = runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
settleCopy(kShutdownWaitNs, true);
|
|
for (ovrTextureSwapChain* chain = gChains; chain; chain = chain->next) destroyOutput(chain);
|
|
for (ovrTextureSwapChain* chain = gRetired; chain; chain = chain->next) destroyOutput(chain);
|
|
}
|
|
|
|
void shutdown() {
|
|
Runtime& s = runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
settleCopy(kShutdownWaitNs, true);
|
|
while (gChains) {
|
|
ovrTextureSwapChain* chain = gChains;
|
|
gChains = chain->next;
|
|
destroyChain(chain);
|
|
}
|
|
while (gRetired) {
|
|
ovrTextureSwapChain* chain = gRetired;
|
|
gRetired = chain->next;
|
|
destroyChain(chain);
|
|
}
|
|
destroyCopyContext();
|
|
}
|
|
} // namespace ovp::graphics
|
|
|
|
// ---------------------------------------------------------------- GLES texture swapchains
|
|
namespace {
|
|
constexpr int64_t kFormat8888 = 4, kFormat8888Srgb = 5, kFormatRgba16f = 6;
|
|
}
|
|
|
|
// format: VrApi's 32-bit ovrTextureFormat enum (only vrapi_CreateTextureSwapChain3 takes a 64-bit format)
|
|
VRAPI ovrTextureSwapChain* vrapi_CreateTextureSwapChain2(int32_t type, int32_t format, int width, int height,
|
|
int levels, int bufferCount) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
GLenum internal = 0;
|
|
if (format == kFormat8888) internal = GL_RGBA8;
|
|
else if (format == kFormat8888Srgb) internal = GL_SRGB8_ALPHA8;
|
|
else if (format == kFormatRgba16f) internal = GL_RGBA16F;
|
|
if (!internal || (type != kTexture2D && type != kTexture2DArray) || width <= 0 || height <= 0 ||
|
|
width > 16384 || height > 16384 || levels <= 0 || bufferCount <= 0 ||
|
|
bufferCount > static_cast<int>(kMaxBuffers)) {
|
|
OVP_ERROR("Unsupported GLES texture swapchain type=%d format=%lld %dx%d levels=%d buffers=%d", type,
|
|
(long long)format, width, height, levels, bufferCount);
|
|
return nullptr;
|
|
}
|
|
ovrTextureSwapChain* chain = new (std::nothrow) ovrTextureSwapChain();
|
|
if (!chain) return nullptr;
|
|
chain->gl = true;
|
|
chain->glInternalFormat = internal;
|
|
chain->glContext = eglGetCurrentContext();
|
|
chain->type = type;
|
|
chain->width = static_cast<uint32_t>(width);
|
|
chain->height = static_cast<uint32_t>(height);
|
|
chain->levels = 1;
|
|
chain->bufferCount = static_cast<uint32_t>(bufferCount);
|
|
chain->arrayLayers = type == kTexture2DArray ? 2u : 1u;
|
|
const GLenum target = chain->arrayLayers > 1 ? GL_TEXTURE_2D_ARRAY : GL_TEXTURE_2D;
|
|
glGenTextures(static_cast<GLsizei>(chain->bufferCount), chain->glTextures.data());
|
|
for (uint32_t i = 0; i < chain->bufferCount; ++i) {
|
|
glBindTexture(target, chain->glTextures[i]);
|
|
if (target == GL_TEXTURE_2D_ARRAY) glTexStorage3D(target, 1, internal, width, height, 2);
|
|
else glTexStorage2D(target, 1, internal, width, height);
|
|
glTexParameteri(target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
|
glTexParameteri(target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
|
glTexParameteri(target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
glTexParameteri(target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
}
|
|
glBindTexture(target, 0);
|
|
const GLenum error = glGetError();
|
|
if (error != GL_NO_ERROR) {
|
|
OVP_ERROR("GLES texture swapchain creation failed: 0x%x (is a GL context current?)", error);
|
|
destroyChain(chain);
|
|
return nullptr;
|
|
}
|
|
chain->next = gChains;
|
|
gChains = chain;
|
|
OVP_LOG("Created GLES texture swapchain %dx%d x%u buffers=%d format=0x%x ctx=%p names=%u..%u", width, height,
|
|
chain->arrayLayers, bufferCount, internal, chain->glContext, chain->glTextures[0],
|
|
chain->glTextures[chain->bufferCount - 1]);
|
|
return chain;
|
|
}
|
|
|
|
VRAPI ovrTextureSwapChain* vrapi_CreateTextureSwapChain(int32_t type, int32_t format, int width, int height,
|
|
int levels, bool buffered) {
|
|
return vrapi_CreateTextureSwapChain2(type, format, width, height, levels, buffered ? 3 : 1);
|
|
}
|
|
|
|
VRAPI unsigned int vrapi_GetTextureSwapChainHandle(ovrTextureSwapChain* candidate, int index) {
|
|
ovp::Runtime& s = ovp::runtime();
|
|
std::lock_guard<std::recursive_mutex> lock(s.mutex);
|
|
ovrTextureSwapChain* chain = findChain(candidate);
|
|
if (!chain || !chain->gl || index < 0 || index >= static_cast<int>(chain->bufferCount)) return 0;
|
|
return chain->glTextures[static_cast<uint32_t>(index)];
|
|
}
|