| 1 | // Copyright (C) 2024 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
| 3 | |
| 4 | #include "qopenxrgraphics_vulkan_p.h" |
| 5 | |
| 6 | #include "qopenxrhelpers_p.h" |
| 7 | #include <QtQuick/QQuickWindow> |
| 8 | #include <QtQuick/QQuickGraphicsDevice> |
| 9 | #include <QtQuick/QQuickGraphicsConfiguration> |
| 10 | #include <QtQuick/private/qquickrendertarget_p.h> |
| 11 | |
| 12 | #include <rhi/qrhi.h> |
| 13 | |
| 14 | //#define XR_USE_GRAPHICS_API_VULKAN |
| 15 | |
| 16 | QT_BEGIN_NAMESPACE |
| 17 | |
| 18 | QOpenXRGraphicsVulkan::QOpenXRGraphicsVulkan() |
| 19 | { |
| 20 | m_graphicsBinding.type = XR_TYPE_GRAPHICS_BINDING_VULKAN_KHR; |
| 21 | } |
| 22 | |
| 23 | |
| 24 | bool QOpenXRGraphicsVulkan::isExtensionSupported(const QVector<XrExtensionProperties> &extensions) const |
| 25 | { |
| 26 | for (const auto &extension : extensions) { |
| 27 | if (!strcmp(XR_KHR_VULKAN_ENABLE_EXTENSION_NAME, |
| 28 | s2: extension.extensionName)) |
| 29 | return true; |
| 30 | } |
| 31 | return false; |
| 32 | } |
| 33 | |
| 34 | |
| 35 | const char *QOpenXRGraphicsVulkan::extensionName() const |
| 36 | { |
| 37 | return XR_KHR_VULKAN_ENABLE_EXTENSION_NAME; |
| 38 | } |
| 39 | |
| 40 | |
| 41 | const XrBaseInStructure *QOpenXRGraphicsVulkan::handle() const |
| 42 | { |
| 43 | return reinterpret_cast<const XrBaseInStructure*>(&m_graphicsBinding); |
| 44 | } |
| 45 | |
| 46 | |
| 47 | bool QOpenXRGraphicsVulkan::setupGraphics(const XrInstance &instance, XrSystemId &systemId, const QQuickGraphicsConfiguration &quickConfig) |
| 48 | { |
| 49 | // Setup Vulkan Instance. |
| 50 | |
| 51 | // In hybrid applications that also show Qt Quick windows on the desktop, it |
| 52 | // is not ideal to create multiple VkInstances (as the on-screen |
| 53 | // QQuickWindow(s) will have another one), but there is nothing we can do |
| 54 | // due to the forced upfront nature of Vulkan API design. And we need to do |
| 55 | // OpenXR API calls to get the things we need to create the instance. This |
| 56 | // is hard to reconcile with Quick, that knows nothing about XrView and |
| 57 | // such, and cannot predict the future either (i.e., "guess" if the user is |
| 58 | // ever going to instantiate an XRView, and so on). |
| 59 | // |
| 60 | // This has no relevance for XR-only apps, and even the hybrid case this |
| 61 | // works in practice, so we might just live with this for now. |
| 62 | |
| 63 | PFN_xrGetVulkanGraphicsRequirementsKHR pfnGetVulkanGraphicsRequirementsKHR = nullptr; |
| 64 | OpenXRHelpers::checkXrResult(result: xrGetInstanceProcAddr(instance, |
| 65 | name: "xrGetVulkanGraphicsRequirementsKHR" , |
| 66 | function: reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetVulkanGraphicsRequirementsKHR)), |
| 67 | instance); |
| 68 | |
| 69 | if (!pfnGetVulkanGraphicsRequirementsKHR) { |
| 70 | qWarning(msg: "Could not resolve xrGetVulkanGraphicsRequirementsKHR; perhaps the OpenXR implementation does not support Vulkan?" ); |
| 71 | return false; |
| 72 | } |
| 73 | |
| 74 | PFN_xrGetVulkanInstanceExtensionsKHR pfnGetVulkanInstanceExtensionsKHR = nullptr; |
| 75 | OpenXRHelpers::checkXrResult(result: xrGetInstanceProcAddr(instance, |
| 76 | name: "xrGetVulkanInstanceExtensionsKHR" , |
| 77 | function: reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetVulkanInstanceExtensionsKHR)), |
| 78 | instance); |
| 79 | |
| 80 | XrGraphicsRequirementsVulkanKHR graphicsRequirements{}; |
| 81 | graphicsRequirements.type = XR_TYPE_GRAPHICS_REQUIREMENTS_VULKAN_KHR; |
| 82 | OpenXRHelpers::checkXrResult(result: pfnGetVulkanGraphicsRequirementsKHR(instance, |
| 83 | systemId, |
| 84 | &graphicsRequirements), |
| 85 | instance); |
| 86 | |
| 87 | quint32 extensionNamesSize = 0; |
| 88 | OpenXRHelpers::checkXrResult(result: pfnGetVulkanInstanceExtensionsKHR(instance, |
| 89 | systemId, |
| 90 | 0, |
| 91 | &extensionNamesSize, |
| 92 | nullptr), |
| 93 | instance); |
| 94 | |
| 95 | QByteArray extensionNames; |
| 96 | extensionNames.resize(size: extensionNamesSize); |
| 97 | OpenXRHelpers::checkXrResult(result: pfnGetVulkanInstanceExtensionsKHR(instance, |
| 98 | systemId, |
| 99 | extensionNamesSize, |
| 100 | &extensionNamesSize, |
| 101 | extensionNames.data()), |
| 102 | instance); |
| 103 | |
| 104 | // The last extension could have extra null characters but |
| 105 | // the way we handle extenions doesn't handle null terminated |
| 106 | // strings well, so we have to strip them ourselves |
| 107 | auto stripNullChars = [](const QByteArray &string) { |
| 108 | auto begin = string.begin(); |
| 109 | auto end = string.end(); |
| 110 | while (begin < end && end[-1] == '\x00') |
| 111 | --end; |
| 112 | return QByteArray(begin, end - begin); |
| 113 | }; |
| 114 | |
| 115 | QByteArrayList extensions = extensionNames.split(sep: ' '); |
| 116 | for (auto &ext : extensions) |
| 117 | ext = stripNullChars(ext); |
| 118 | |
| 119 | for (auto &rhiExt : QRhiVulkanInitParams::preferredInstanceExtensions()) { |
| 120 | if (!extensions.contains(t: rhiExt)) |
| 121 | extensions.append(t: rhiExt); |
| 122 | } |
| 123 | |
| 124 | m_vulkanInstance.setExtensions(extensions); |
| 125 | |
| 126 | // Multiview is a Vulkan 1.1 feature and won't work without setting up the instance accordingly. |
| 127 | const QVersionNumber supportedVersion = m_vulkanInstance.supportedApiVersion(); |
| 128 | if (supportedVersion >= QVersionNumber(1, 3)) |
| 129 | m_vulkanInstance.setApiVersion(QVersionNumber(1, 3)); |
| 130 | else if (supportedVersion >= QVersionNumber(1, 2)) |
| 131 | m_vulkanInstance.setApiVersion(QVersionNumber(1, 2)); |
| 132 | else if (supportedVersion >= QVersionNumber(1, 1)) |
| 133 | m_vulkanInstance.setApiVersion(QVersionNumber(1, 1)); |
| 134 | |
| 135 | if (quickConfig.isDebugLayerEnabled()) |
| 136 | m_vulkanInstance.setLayers({ "VK_LAYER_LUNARG_standard_validation" }); |
| 137 | |
| 138 | if (!m_vulkanInstance.create()) { |
| 139 | qWarning(msg: "Quick 3D XR: Failed to create Vulkan instance" ); |
| 140 | return false; |
| 141 | } |
| 142 | |
| 143 | // Get Vulkan device extensions |
| 144 | PFN_xrGetVulkanDeviceExtensionsKHR pfnGetVulkanDeviceExtensionsKHR = nullptr; |
| 145 | OpenXRHelpers::checkXrResult(result: xrGetInstanceProcAddr(instance, |
| 146 | name: "xrGetVulkanDeviceExtensionsKHR" , |
| 147 | function: reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetVulkanDeviceExtensionsKHR)), |
| 148 | instance); |
| 149 | |
| 150 | uint32_t deviceExtensionNamesSize = 0; |
| 151 | OpenXRHelpers::checkXrResult(result: pfnGetVulkanDeviceExtensionsKHR(instance, |
| 152 | systemId, |
| 153 | 0, |
| 154 | &deviceExtensionNamesSize, |
| 155 | nullptr), |
| 156 | instance); |
| 157 | QByteArray deviceExtensionNames; |
| 158 | deviceExtensionNames.resize(size: deviceExtensionNamesSize); |
| 159 | OpenXRHelpers::checkXrResult(result: pfnGetVulkanDeviceExtensionsKHR(instance, |
| 160 | systemId, |
| 161 | deviceExtensionNamesSize, |
| 162 | &deviceExtensionNamesSize, |
| 163 | deviceExtensionNames.data()), |
| 164 | instance); |
| 165 | |
| 166 | auto deviceExtensions = deviceExtensionNames.split(sep: ' '); |
| 167 | for (auto &ext : deviceExtensions) { |
| 168 | ext = stripNullChars(ext); |
| 169 | } |
| 170 | m_graphicsConfiguration.setDeviceExtensions(deviceExtensions); |
| 171 | |
| 172 | // Get the Vulkan Graphics Device |
| 173 | PFN_xrGetVulkanGraphicsDeviceKHR pfnGetVulkanGraphicsDeviceKHR = nullptr; |
| 174 | OpenXRHelpers::checkXrResult(result: xrGetInstanceProcAddr(instance, name: "xrGetVulkanGraphicsDeviceKHR" , |
| 175 | function: reinterpret_cast<PFN_xrVoidFunction*>(&pfnGetVulkanGraphicsDeviceKHR)), instance); |
| 176 | |
| 177 | OpenXRHelpers::checkXrResult(result: pfnGetVulkanGraphicsDeviceKHR(instance, systemId, m_vulkanInstance.vkInstance(), &m_vulkanPhysicalDevice), instance); |
| 178 | |
| 179 | return true; |
| 180 | } |
| 181 | |
| 182 | bool QOpenXRGraphicsVulkan::finializeGraphics(QRhi *rhi) |
| 183 | { |
| 184 | const QRhiVulkanNativeHandles *vulkanRhi = static_cast<const QRhiVulkanNativeHandles *>(rhi->nativeHandles()); |
| 185 | m_vulkanDevice = vulkanRhi->dev; |
| 186 | Q_ASSERT(m_vulkanPhysicalDevice == vulkanRhi->physDev); |
| 187 | m_vulkanCommandQueue = vulkanRhi->gfxQueue; |
| 188 | m_queueFamilyIndex = vulkanRhi->gfxQueueFamilyIdx; |
| 189 | |
| 190 | m_graphicsBinding.instance = m_vulkanInstance.vkInstance(); |
| 191 | m_graphicsBinding.physicalDevice = m_vulkanPhysicalDevice; |
| 192 | m_graphicsBinding.device = m_vulkanDevice; |
| 193 | m_graphicsBinding.queueFamilyIndex = m_queueFamilyIndex; |
| 194 | m_graphicsBinding.queueIndex = 0; |
| 195 | |
| 196 | m_rhi = rhi; |
| 197 | |
| 198 | return true; |
| 199 | } |
| 200 | |
| 201 | |
| 202 | int64_t QOpenXRGraphicsVulkan::colorSwapchainFormat(const QVector<int64_t> &swapchainFormats) const |
| 203 | { |
| 204 | // List of supported color swapchain formats. |
| 205 | constexpr int64_t supportedColorSwapchainFormats[] = { |
| 206 | VK_FORMAT_B8G8R8A8_SRGB, |
| 207 | VK_FORMAT_R8G8B8A8_SRGB, |
| 208 | VK_FORMAT_B8G8R8A8_UNORM, |
| 209 | VK_FORMAT_R8G8B8A8_UNORM |
| 210 | }; |
| 211 | |
| 212 | auto swapchainFormatIt = std::find_first_of(first1: std::begin(arr: supportedColorSwapchainFormats), |
| 213 | last1: std::end(arr: supportedColorSwapchainFormats), |
| 214 | first2: swapchainFormats.begin(), |
| 215 | last2: swapchainFormats.end()); |
| 216 | return *swapchainFormatIt; |
| 217 | } |
| 218 | |
| 219 | int64_t QOpenXRGraphicsVulkan::depthSwapchainFormat(const QVector<int64_t> &swapchainFormats) const |
| 220 | { |
| 221 | // in order of preference |
| 222 | QVarLengthArray<int64_t, 4> supportedDepthSwapchainFormats; |
| 223 | constexpr std::pair<int64_t, QRhiTexture::Format> nativeDepthSwapchainFormats[] = { |
| 224 | { VK_FORMAT_D24_UNORM_S8_UINT, QRhiTexture::D24S8 }, |
| 225 | { VK_FORMAT_D32_SFLOAT_S8_UINT, QRhiTexture::D32F }, |
| 226 | { VK_FORMAT_D32_SFLOAT, QRhiTexture::D32F }, |
| 227 | { VK_FORMAT_D16_UNORM, QRhiTexture::D16 } |
| 228 | }; |
| 229 | for (const auto &format : nativeDepthSwapchainFormats) { |
| 230 | if (m_rhi->isTextureFormatSupported(format: format.second)) |
| 231 | supportedDepthSwapchainFormats.append(t: format.first); |
| 232 | } |
| 233 | |
| 234 | if (supportedDepthSwapchainFormats.isEmpty()) |
| 235 | return 0; |
| 236 | |
| 237 | // order matters, we prefer D24S8 above all the others |
| 238 | return *std::find_first_of(first1: std::begin(cont&: supportedDepthSwapchainFormats), |
| 239 | last1: std::end(cont&: supportedDepthSwapchainFormats), |
| 240 | first2: swapchainFormats.begin(), |
| 241 | last2: swapchainFormats.end()); |
| 242 | } |
| 243 | |
| 244 | QVector<XrSwapchainImageBaseHeader*> QOpenXRGraphicsVulkan::allocateSwapchainImages(int count, XrSwapchain swapchain) |
| 245 | { |
| 246 | QVector<XrSwapchainImageBaseHeader*> swapchainImages; |
| 247 | QVector<XrSwapchainImageVulkanKHR> swapchainImageBuffer(count); |
| 248 | for (XrSwapchainImageVulkanKHR& image : swapchainImageBuffer) { |
| 249 | image.type = XR_TYPE_SWAPCHAIN_IMAGE_VULKAN_KHR; |
| 250 | swapchainImages.push_back(t: reinterpret_cast<XrSwapchainImageBaseHeader*>(&image)); |
| 251 | } |
| 252 | m_swapchainImageBuffer.insert(key: swapchain, value: swapchainImageBuffer); |
| 253 | return swapchainImages; |
| 254 | } |
| 255 | |
| 256 | |
| 257 | QQuickRenderTarget QOpenXRGraphicsVulkan::(const XrSwapchainSubImage &subImage, |
| 258 | const XrSwapchainImageBaseHeader *swapchainImage, |
| 259 | quint64 swapchainFormat, |
| 260 | int samples, |
| 261 | int arraySize, |
| 262 | const XrSwapchainImageBaseHeader *depthSwapchainImage, |
| 263 | quint64 depthSwapchainFormat) const |
| 264 | { |
| 265 | VkImage colorTexture = reinterpret_cast<const XrSwapchainImageVulkanKHR*>(swapchainImage)->image; |
| 266 | |
| 267 | VkFormat viewFormat = VkFormat(swapchainFormat); |
| 268 | switch (swapchainFormat) { |
| 269 | case VK_FORMAT_R8G8B8A8_SRGB: |
| 270 | viewFormat = VK_FORMAT_R8G8B8A8_UNORM; |
| 271 | break; |
| 272 | case VK_FORMAT_B8G8R8A8_SRGB: |
| 273 | viewFormat = VK_FORMAT_B8G8R8A8_UNORM; |
| 274 | break; |
| 275 | default: |
| 276 | break; |
| 277 | } |
| 278 | |
| 279 | QQuickRenderTarget::Flags flags; |
| 280 | if (samples > 1) |
| 281 | flags |= QQuickRenderTarget::Flag::MultisampleResolve; |
| 282 | |
| 283 | const QSize pixelSize(subImage.imageRect.extent.width, subImage.imageRect.extent.height); |
| 284 | QQuickRenderTarget rt = QQuickRenderTarget::fromVulkanImage(image: colorTexture, |
| 285 | layout: VK_IMAGE_LAYOUT_UNDEFINED, |
| 286 | format: VkFormat(swapchainFormat), |
| 287 | viewFormat, |
| 288 | pixelSize, |
| 289 | sampleCount: samples, |
| 290 | arraySize, |
| 291 | flags); |
| 292 | if (depthSwapchainImage) { |
| 293 | // There might be issues with stencil when MSAA is not used and the |
| 294 | // format is D16 or D32F or the half-unsupported D32FS8 (because then |
| 295 | // the OpenXR-provided texture is the one and only depth-stencil buffer, |
| 296 | // perhaps without stencil). But we prefer D24S8 whenever that's |
| 297 | // available, so hopefully this problem won't come up in practice. |
| 298 | QRhiTexture::Format format = QRhiTexture::D24S8; |
| 299 | switch (depthSwapchainFormat) { |
| 300 | case VK_FORMAT_D32_SFLOAT_S8_UINT: |
| 301 | case VK_FORMAT_D32_SFLOAT: |
| 302 | format = QRhiTexture::D32F; |
| 303 | break; |
| 304 | case VK_FORMAT_D16_UNORM: |
| 305 | format = QRhiTexture::D16; |
| 306 | break; |
| 307 | } |
| 308 | VkImage depthImage = reinterpret_cast<const XrSwapchainImageVulkanKHR*>(depthSwapchainImage)->image; |
| 309 | if (m_depthTexture && (m_depthTexture->format() != format || m_depthTexture->pixelSize() != pixelSize || m_depthTexture->arraySize() != arraySize)) { |
| 310 | delete m_depthTexture; |
| 311 | m_depthTexture = nullptr; |
| 312 | } |
| 313 | if (!m_depthTexture) { |
| 314 | // this is never multisample, QQuickRt takes care of resolving depth-stencil |
| 315 | if (arraySize > 1) |
| 316 | m_depthTexture = m_rhi->newTextureArray(format, arraySize, pixelSize, sampleCount: 1, flags: QRhiTexture::RenderTarget); |
| 317 | else |
| 318 | m_depthTexture = m_rhi->newTexture(format, pixelSize, sampleCount: 1, flags: QRhiTexture::RenderTarget); |
| 319 | } |
| 320 | m_depthTexture->createFrom(src: { .object: quint64(depthImage), .layout: VK_IMAGE_LAYOUT_UNDEFINED }); |
| 321 | rt.setDepthTexture(m_depthTexture); |
| 322 | } |
| 323 | return rt; |
| 324 | } |
| 325 | |
| 326 | void QOpenXRGraphicsVulkan::setupWindow(QQuickWindow *quickWindow) |
| 327 | { |
| 328 | quickWindow->setGraphicsDevice(QQuickGraphicsDevice::fromPhysicalDevice(physicalDevice: m_vulkanPhysicalDevice)); |
| 329 | quickWindow->setGraphicsConfiguration(m_graphicsConfiguration); |
| 330 | quickWindow->setVulkanInstance(&m_vulkanInstance); |
| 331 | } |
| 332 | |
| 333 | void QOpenXRGraphicsVulkan::releaseResources() |
| 334 | { |
| 335 | delete m_depthTexture; |
| 336 | m_depthTexture = nullptr; |
| 337 | } |
| 338 | |
| 339 | QT_END_NAMESPACE |
| 340 | |