Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquick3dxrmanager_visionos.mm
Go to the documentation of this file.
1// Copyright (C) 2024 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3
9
10#include "../qquick3dxrinputmanager_p.h"
11#include "../qquick3dxranimationdriver_p.h"
12
13#include <QtQuick3D/private/qquick3dviewport_p.h>
14#include <QtQuick3D/private/qquick3dnode_p_p.h>
15
16#include <QtQuick3DUtils/private/qssgassert_p.h>
17
18#include <QQuickGraphicsDevice>
19#include <rhi/qrhi.h>
20
21#include <CompositorServices/CompositorServices.h>
22#include <QtGui/qguiapplication_platform.h>
23
24#include <QtCore/qoperatingsystemversion.h>
25#include <QtCore/qloggingcategory.h>
26#include <QtCore/qvarlengtharray.h>
27
28#include <TargetConditionals.h>
29
30#include <cmath>
31
33
35
36static const char s_renderThreadName[] = "QQuick3DXrRenderThread";
37
38// NOTE: This only affects visionOS
40{
41 static bool foveationDisabled = qEnvironmentVariableIntValue("QT_QUICK3D_XR_DISABLE_FOVEATION") != 0;
42 return foveationDisabled;
43}
44
45// Qt Quick and Qt Quick 3D produce display-referred, sRGB-encoded output, just like they do for a
46// window swapchain; the tonemapper is the last step of the 3D pass and the 2D scenegraph writes
47// its colors as they are. The compositor treats a RGBA16Float layer as linear light, so such
48// output would be gamma-encoded a second time when it is displayed. Prefer the 8-bit sRGB
49// formats instead and render into them through a non-sRGB view (see qssgNonSrgbFormat()), which
50// stores Qt's output as it is. This matches what the OpenXR backends do.
51static MTLPixelFormat qssgPreferredColorFormat(cp_layer_renderer_capabilities_t capabilities)
52{
53 static const MTLPixelFormat preferredFormats[] = { MTLPixelFormatBGRA8Unorm_sRGB, MTLPixelFormatRGBA8Unorm_sRGB };
54
55 const size_t count = cp_layer_renderer_capabilities_supported_color_formats_count_with_options(capabilities, cp_supported_color_formats_options_none);
56 QVarLengthArray<MTLPixelFormat, 8> supportedFormats;
57 for (size_t i = 0; i < count; ++i)
58 supportedFormats.append(cp_layer_renderer_capabilities_supported_color_format_with_options(capabilities, cp_supported_color_formats_options_none, i));
59
60 for (MTLPixelFormat format : preferredFormats) {
61 if (supportedFormats.contains(format))
62 return format;
63 }
64
65 qCWarning(lcQuick3DXr, "The compositor layer supports no 8-bit sRGB color format, falling back to RGBA16Float. "
66 "Colors will be gamma-encoded twice and appear too bright.");
67 return MTLPixelFormatRGBA16Float;
68}
69
70static MTLPixelFormat qssgNonSrgbFormat(MTLPixelFormat format)
71{
72 switch (format) {
73 case MTLPixelFormatBGRA8Unorm_sRGB:
74 return MTLPixelFormatBGRA8Unorm;
75 case MTLPixelFormatRGBA8Unorm_sRGB:
76 return MTLPixelFormatRGBA8Unorm;
77 default:
78 return format;
79 }
80}
81
82// Default clip distances used when the compositor does not report usable ones.
83// These match the defaults used by the OpenXR backend.
84//
85// NOTE: CompositorServices nominally takes the depth range in meters, while these
86// are Quick3D scene units, i.e. centimeters, so 1 cm to 100 m. It works out for
87// us, because the eye transforms are scaled to centimeters as well and the
88// projection comes from cp_drawable_compute_projection(), built from the very
89// same numbers, which leaves the depth range acting as scene units throughout.
90// It does mean the compositor itself is told the far plane sits at 10 km. That
91// was already the case when we took the range from the compositor's default and
92// handed it to the eye cameras unchanged.
93static constexpr float s_defaultClipNear = 1.0f;
94static constexpr float s_defaultClipFar = 10000.0f;
95
96// The compositor reports the depth range in reverse-Z order (x = far, y = near),
97// and the default far plane it reports is infinite. That is what visionOS 27
98// reports, and the same has been observed as far back as visionOS 1.0. An
99// infinite far plane is fine for the projection matrix, but Quick3D also uses
100// the clip distances as plain values for shadow cascades, fog and depth
101// linearization, which all break down with inf. Make sure we always end up with
102// finite values that are in the right order.
103static void qssgSanitizeDepthRange(simd_float2 depthRange, float &near, float &far)
104{
105 near = depthRange.y;
106 far = depthRange.x;
107
108 if (!std::isfinite(near) || near <= 0.0f)
109 near = s_defaultClipNear;
110 if (!std::isfinite(far) || far <= near)
111 far = qMax(s_defaultClipFar, near * 2.0f);
112}
113
115{
117public:
118 CompositorLayer() = default;
120 {
121 m_xrManager = nullptr;
122 [m_layerRenderer release];
123 m_layerRenderer = nullptr;
124 m_worldTrackingProvider = nullptr;
125 m_arSession = nullptr;
126 m_initialized = false;
127 }
128
137
138 static constexpr QEvent::Type asQEvent(CompositorLayer::Event event) { return static_cast<QEvent::Type>(event); }
139
140 static bool supportsLayoutType(cp_layer_renderer_capabilities_t capabilities, cp_layer_renderer_layout layout)
141 {
142 const size_t layoutCount = cp_layer_renderer_capabilities_supported_layouts_count(capabilities, cp_supported_layouts_options_none);
143 bool found = false;
144 for (size_t i = 0; i < layoutCount && !found; ++i)
145 found = (layout == cp_layer_renderer_capabilities_supported_layout(capabilities, cp_supported_layouts_options_none, i));
146
147 return found;
148 }
149
150 void configure(cp_layer_renderer_capabilities_t capabilities, cp_layer_renderer_configuration_t configuration) const override
151 {
152 QMutexLocker locker(&m_compositorLayerMtx);
153
155
156 // If multiview isn't disabled we need to check if the target supports it.
157 // NOTE: We're only doing layered or dedicated rendering, so shared is not supported (untested).
158 m_multiviewSupported = supportsLayoutType(capabilities, cp_layer_renderer_layout_layered);
159 m_multiviewEnabled = m_multiviewEnabled && m_multiviewSupported;
160
161 // NOTE: If foviation is supported we enable it.
162 m_foveationSupported = cp_layer_renderer_capabilities_supports_foveation(capabilities);
163 // ... but we can disable it with an environment variable.
164 m_foveationEnabled = m_foveationSupported && !qssgDisableFoveation();
165
166 // NOTE: We're only doing layered or dedicated rendering, so shared is not supported, even though we technically
167 // could support it. Since the only target we currently have (visionOS on the Vision Pro) support all these
168 // modes anyways we only need to care about these two (dedicated is required for the emulator).
169 cp_layer_renderer_layout textureLayout = m_multiviewEnabled ? cp_layer_renderer_layout_layered
170 : cp_layer_renderer_layout_dedicated;
171
172 cp_layer_renderer_configuration_set_layout(configuration, textureLayout);
173 cp_layer_renderer_configuration_set_foveation_enabled(configuration, m_foveationEnabled);
174 m_colorFormat = qssgPreferredColorFormat(capabilities);
175 cp_layer_renderer_configuration_set_color_format(configuration, m_colorFormat);
176
177 // NOTE: The depth range is inverted for VisionOS (x = far, y = near)
178 const simd_float2 systemDepthRange = cp_layer_renderer_configuration_get_default_depth_range(configuration);
179 qssgSanitizeDepthRange(systemDepthRange, m_depthRange[0], m_depthRange[1]);
180 // Don't rely on the system default, which is undocumented and not finite; set the
181 // range we're going to use explicitly so the drawables report the same finite values.
182 cp_layer_renderer_configuration_set_default_depth_range(configuration, simd_make_float2(m_depthRange[1], m_depthRange[0]));
183
184 qCDebug(lcQuick3DXr) << "Configuring with the following settings:"
185 << "\n\tMultiview supported: " << m_multiviewSupported
186 << "\n\tMultiview enabled: " << m_multiviewEnabled
187 << "\n\tFoveation supported: " << m_foveationSupported
188 << "\n\tFoveation enabled: " << m_foveationEnabled
189 << "\n\tColor format: " << NSUInteger(m_colorFormat)
190 << "\n\tSystem depth range: " << systemDepthRange.y << " to: " << systemDepthRange.x
191 << "\n\tDepth Range: " << m_depthRange[0] << " to: " << m_depthRange[1];
192 }
193
194 void render(cp_layer_renderer_t renderer) override
195 {
196 if (m_layerRenderer != renderer) {
197 m_layerRenderer = renderer;
198 emit layerRendererChanged();
199 }
200
201 if (m_layerRenderer) {
202 QMutexLocker locker(&m_mutex);
203 // NOTE: The layer renderer is our handle to the compositor layer, so we need to ensure that
204 // we need to ensure that it stays alive long enough for us to know if we should tear down
205 // the compositor layer or not.
206 [m_layerRenderer retain];
207 checkRenderState();
208 emit layerRendererReady();
209 }
210 }
211
212 // Listen for spatial events (these are native gestures like pinch click/drag coming from SwiftUI)
213 void handleSpatialEvents(const QJsonObject &events) override
214 {
215 if (m_xrManager)
216 m_xrManager->processSpatialEvents(events);
217 }
218
219 bool isInitialized() const { return m_initialized; }
220
221 bool isMultiviewSupported() const { return m_multiviewSupported; }
222 bool isMultiviewEnabled() const { return m_multiviewEnabled; }
223 bool isFoveationSupported() const { return m_foveationSupported; }
224 bool isFoveationEnabled() const { return m_foveationEnabled; }
225
227 {
228 return m_layerRenderer;
229 }
230
231 void getDefaultDepthRange(float &near, float &far) const
232 {
233 near = m_depthRange[0];
234 far = m_depthRange[1];
235 }
236
237 // Must be called from the GUI thread
239 {
240 Q_ASSERT(qApp->thread() == QThread::currentThread());
241 QSSG_ASSERT(!m_initialized, return);
242
243 m_xrManager = xrManager;
244 runWorldTrackingARSession();
245 m_initialized = true;
246 }
247
248 // Called from the gui thread while holding the render lock.
249 // For now we only let the thread know that it should stop rendering,
250 // as we're going to tear down the compositor layer on the render thread.
251 void teardown() { m_teardown = true; }
252
254 {
255 Q_ASSERT(qApp->thread() == QThread::currentThread());
256
257 QMutexLocker locker(&m_arSessionMtx);
258
259 if (m_arSession) {
260 qCDebug(lcQuick3DXr, "Stopping AR session");
261 // NOTE: We're stopping the AR session, so we need to ensure that the AR session
262 // doesn't get posted at some later point, so we change the dispatch queue to
263 // use the main queue (which is the GUI thread) before calling stop.
264 ar_session_set_data_provider_state_change_handler_f(m_arSession, dispatch_get_main_queue(), this, &onArStateChanged);
265 ar_session_stop(m_arSession);
266 m_arSession = nullptr;
268 }
269 }
270
271 QMutex &renderLock() { return m_mutex; }
273 {
274 return m_waitCondition.wait(&m_mutex);
275 }
276
277 Q_INVOKABLE static void destroy(QQuick3DXrManagerPrivate *mngr, QQuickWindow *window, CompositorLayer *compositorLayer)
278 {
279 QSSG_ASSERT(mngr != nullptr, return);
280
281 qCDebug(lcQuick3DXr) << "Destroying compositor layer";
282
283 if (auto *visionOSApplicaton = qGuiApp->nativeInterface<QNativeInterface::QVisionOSApplication>())
284 visionOSApplicaton->setImmersiveSpaceCompositorLayer(nullptr);
285
286 // NOTE: This is a bit of a hack, but we need to cleanup the nodes
287 // on the render thread while the GUI thread is blocked (as documented.)
288 // Since we cannot destruct the window on the render thread, and we cannot
289 // cleanup all nodes on the GUI thread due to dependencies on the render
290 // thread and resources created on it. Instead we just invalidate the render
291 // control and cleanup the nodes on the render thread. This is not ideal,
292 // but it's what we have for now...
293 if (window) { // If we never got past the setup we don't have a window.
294 auto *d = QQuickWindowPrivate::get(window);
295 d->cleanupNodesOnShutdown();
296 mngr->releaseResources();
297 if (auto *rc = d->renderControl)
298 rc->invalidate();
299 }
300
301 // We're holding onto the lock, so defer the delete until we're done here.
302 // All reasources should be released at this point anyways.
303 compositorLayer->deleteLater();
304 }
305
311
312protected:
313 bool event(QEvent *event) override
314 {
315 {
316 // NOTE: Intentionally scoped to avoid locking the mutex for events we're not handling
317 // and that has side-effects (e.g. cleanup due to a deferred delete).
318 QMutexLocker locker(&m_mutex);
319
320 if (m_teardown) {
321 qCDebug(lcQuick3DXr) << "Teardown in progress, skipping event handling";
322 // If we're tearing down we're about to be destroyed, so we don't want to
323 // ensure that the mutex is unlocked, as we're going to be destroyed anyways.
324 if (event->type() == QEvent::DeferredDelete)
325 locker.unlock();
326 return QObject::event(event);
327 }
328
329 switch (static_cast<Event>(event->type())) {
330 case Event::Render:
331 {
332 const bool success = renderFrame(locker);
333 // Check if we successfully rendered the frame, if not the GUI thread
334 // is likely waiting for the render thread to complete rendering, so we
335 // need to wake it up.
336 if (!success) {
337 qCDebug(lcQuick3DXr) << "Waking up the GUI thread, rendering returned early...";
338 m_xrManager->m_syncDone = false;
339 m_waitCondition.wakeOne();
340 }
341 }
342 return true;
343 case Event::Stop:
344 cleanup();
345 return true;
346 case Event::Pause:
347 pause();
348 return true;
349 case Event::Pulse:
350 checkRenderState();
351 return true;
352 }
353 }
354
355 return QObject::event(event);
356 }
357
358private:
359 friend bool QQuick3DXrManagerPrivate::renderFrameImpl(QMutexLocker<QMutex> &locker, QWaitCondition &waitCondition);
361
362 static void onArStateChanged(void *context,
363 ar_data_providers_t data_providers,
364 ar_data_provider_state_t new_state,
365 ar_error_t error,
366 ar_data_provider_t failed_data_provider)
367 {
368 Q_UNUSED(context);
369 Q_UNUSED(data_providers);
370 Q_UNUSED(error);
371 Q_UNUSED(failed_data_provider);
372
373 auto *that = reinterpret_cast<CompositorLayer *>(context);
374
375 QMutexLocker lock(&that->m_arSessionMtx);
376
377 // If the ar session is null we're called after we tore down the session.
378 QSSG_ASSERT_X(that->m_arSession != nullptr, "AR session is not running, skipping state change!", return);
379
380 const auto oldState = that->m_arTrackingState;
381 switch (new_state) {
382 case ar_data_provider_state_initialized:
384 break;
385 case ar_data_provider_state_running:
387 break;
388 case ar_data_provider_state_paused:
390 break;
391 case ar_data_provider_state_stopped:
393 break;
394 }
395
396
397 if (oldState != that->m_arTrackingState) {
398 QMutexLocker renderLocker(&that->m_mutex);
399 emit that->arStateChanged(that->m_arTrackingState);
400 }
401 }
402
403 enum class RenderStateAction
404 {
405 CheckOnly,
406 WaitUntilRunning,
407 };
408
409 template<RenderStateAction Action = RenderStateAction::WaitUntilRunning>
410 void checkRenderState()
411 {
412 QSSG_ASSERT(m_layerRenderer != nullptr, return);
413
414 const auto oldState = m_renderState;
415 switch (cp_layer_renderer_get_state(m_layerRenderer)) {
416 case cp_layer_renderer_state_paused:
417 m_renderState = QQuick3DXrManagerPrivate::RenderState::Paused;
418 break;
419 case cp_layer_renderer_state_running:
420 m_renderState = QQuick3DXrManagerPrivate::RenderState::Running;
421 break;
422 case cp_layer_renderer_state_invalidated:
423 m_renderState = QQuick3DXrManagerPrivate::RenderState::Invalidated;
424 break;
425 }
426
427 if (oldState != m_renderState)
428 emit renderStateChanged(m_renderState);
429
430 if (m_renderState == QQuick3DXrManagerPrivate::RenderState::Paused) {
431 if constexpr (Action == RenderStateAction::WaitUntilRunning) {
432 qCDebug(lcQuick3DXr) << "Waiting for rendering to resume...";
433 cp_layer_renderer_wait_until_running(m_layerRenderer);
434 QCoreApplication::postEvent(this, new QEvent(asQEvent(CompositorLayer::Event::Pulse)));
435 }
436 }
437 }
438
439 ar_device_anchor_t createPoseForTiming(cp_frame_timing_t timing)
440 {
441 QSSG_ASSERT(m_worldTrackingProvider != nullptr, return nullptr);
442
443 ar_device_anchor_t outAnchor = ar_device_anchor_create();
444 cp_time_t presentationTime = cp_frame_timing_get_presentation_time(timing);
445 CFTimeInterval queryTime = cp_time_to_cf_time_interval(presentationTime);
446 ar_device_anchor_query_status_t status = ar_world_tracking_provider_query_device_anchor_at_timestamp(m_worldTrackingProvider, queryTime, outAnchor);
447 if (status != ar_device_anchor_query_status_success) {
448 NSLog(@"Failed to get estimated pose from world tracking provider for presentation timestamp %0.3f", queryTime);
449 }
450 return outAnchor;
451 }
452
453 void runWorldTrackingARSession()
454 {
455 QMutexLocker locker(&m_arSessionMtx);
456
457 ar_world_tracking_configuration_t worldTrackingConfiguration = ar_world_tracking_configuration_create();
458 m_worldTrackingProvider = ar_world_tracking_provider_create(worldTrackingConfiguration);
459
460 ar_data_providers_t dataProviders = ar_data_providers_create();
461 ar_data_providers_add_data_provider(dataProviders, m_worldTrackingProvider);
462
464 QQuick3DXrAnchorManager *anchorManager = QQuick3DXrAnchorManager::instance();
465
466 // 1. prepare
467 QQuick3DXrInputManagerPrivate *pim = nullptr;
468 if (QSSG_GUARD_X(inputManager != nullptr, "No InputManager available!")) {
469 pim = QQuick3DXrInputManagerPrivate::get(inputManager);
470 if (QSSG_GUARD(pim != nullptr))
471 pim->prepareHandtracking(dataProviders);
472 }
473
474 if (QSSG_GUARD_X(anchorManager != nullptr, "No AnchorManager available!"))
475 QQuick3DXrManagerPrivate::prepareAnchorManager(anchorManager, dataProviders);
476
477 m_arSession = ar_session_create();
478 ar_session_set_data_provider_state_change_handler_f(m_arSession, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), this, &onArStateChanged);
479 ar_session_run(m_arSession, dataProviders);
480
481 // 2. initialize
482 QQuick3DXrManagerPrivate::initInputManager(inputManager);
483 QQuick3DXrManagerPrivate::initAnchorManager(anchorManager);
484 }
485
486 void pause()
487 {
488 qCDebug(lcQuick3DXr, "Pausing rendering");
489 }
490
491 void cleanup()
492 {
493 qCDebug(lcQuick3DXr, "Cleaning up");
494 if (m_xrManager)
495 m_xrManager->releaseResources();
496 }
497
498 [[nodiscard]] bool renderFrame(QMutexLocker<QMutex> &locker)
499 {
500 Q_ASSERT(locker.isLocked());
501 checkRenderState();
502
503 if (m_renderState != QQuick3DXrManagerPrivate::RenderState::Running) {
504 qCDebug(lcQuick3DXr, "Rendering is not running, skipping frame rendering!");
505 return false;
506 }
507
508 // Lock the AR session mutex and render the frame. We hold onto the lock until we're done rendering
509 // to ensure that the AR session isn't stopped while we're rendering (this can happen!)
510 QMutexLocker arLocker(&m_arSessionMtx);
512 qCDebug(lcQuick3DXr, "AR tracking is not running, skipping frame rendering!");
513 return false;
514 }
515
516 // Lock the compositor layer mutex and render the frame. We hold onto the lock until we're done rendering
517 // as the configure call comes from the GUI thread and we need to ensure that the configuration is not
518 // changed while we're rendering.
519 QMutexLocker compositorLayer(&m_compositorLayerMtx);
520 return m_xrManager->renderFrameImpl(locker, m_waitCondition);
521 }
522
523 ar_world_tracking_provider_t worldTrackingProvider() const
524 {
525 return m_worldTrackingProvider;
526 }
527
528 // Rendering
529 mutable QMutex m_mutex;
530 QWaitCondition m_waitCondition;
531
532 mutable QMutex m_compositorLayerMtx;
533
534 // AR Session
535 mutable QMutex m_arSessionMtx;
536
537 QQuick3DXrManagerPrivate *m_xrManager = nullptr;
538
539 cp_layer_renderer_t m_layerRenderer = nullptr;
540 ar_world_tracking_provider_t m_worldTrackingProvider = nullptr;
541 ar_session_t m_arSession = nullptr;
542 mutable float m_depthRange[2] {1.0f, 10000.0f}; // NOTE: Near, Far
543 QQuick3DXrManagerPrivate::RenderState m_renderState = QQuick3DXrManagerPrivate::RenderState::Uninitialized;
545 bool m_initialized = false;
546 bool m_teardown = false;
547 mutable bool m_multiviewSupported = true;
548 mutable bool m_multiviewEnabled = true;
549 mutable bool m_foveationSupported = true;
550 mutable bool m_foveationEnabled = true;
551 mutable MTLPixelFormat m_colorFormat = MTLPixelFormatBGRA8Unorm_sRGB;
552};
553
558
559Q_GLOBAL_STATIC(QSSGCompositionLayerInstance, s_compositorLayer)
560
561// Sanitize the depth range the drawable reports and, if it needed fixing up, write it
562// back on the drawable. The projection is computed from the drawable's own range, so
563// without the write-back the projection and the clip distances the cameras get, which
564// Quick3D also uses for shadows, fog and depth linearization, could disagree.
565static void qssgUpdateDrawableDepthRange(cp_drawable_t drawable, float &clipNear, float &clipFar)
566{
567 const simd_float2 reportedRange = cp_drawable_get_depth_range(drawable);
568 qssgSanitizeDepthRange(reportedRange, clipNear, clipFar);
569
570 // NOTE: The depth range is inverted for visionOS (x = far, y = near)
571 if (reportedRange.y != clipNear || reportedRange.x != clipFar)
572 cp_drawable_set_depth_range(drawable, simd_make_float2(clipFar, clipNear));
573}
574
575// FIXME: Maybe unify with the openxr implementation?!
576void QQuick3DXrManagerPrivate::updateCameraImp(simd_float4x4 headTransform, cp_drawable_t drawable, QQuick3DXrOrigin *xrOrigin, int i)
577{
578 cp_view_t view = cp_drawable_get_view(drawable, i);
579 // The drawable normally reports the range set on the layer configuration, but
580 // make sure we never hand non-finite clip distances to the cameras.
581 float clipNear, clipFar;
582 qssgUpdateDrawableDepthRange(drawable, clipNear, clipFar);
583
584 xrOrigin->eyeCamera(i)->setClipNear(clipNear);
585 xrOrigin->eyeCamera(i)->setClipFar(clipFar);
586
587 // NOTE: Must be queried after the depth range has been set on the drawable.
588 simd_float4x4 projection = cp_drawable_compute_projection(drawable, cp_axis_direction_convention_right_up_forward, i);
589 QMatrix4x4 proj{projection.columns[0].x, projection.columns[1].x, projection.columns[2].x, projection.columns[3].x,
590 projection.columns[0].y, projection.columns[1].y, projection.columns[2].y, projection.columns[3].y,
591 projection.columns[0].z, projection.columns[1].z, projection.columns[2].z, projection.columns[3].z,
592 projection.columns[0].w, projection.columns[1].w, projection.columns[2].w, projection.columns[3].w};
593 xrOrigin->eyeCamera(i)->setProjection(proj);
594
595 simd_float4x4 localEyeTransform = cp_view_get_transform(view);
596 simd_float4x4 eyeCameraTransform = simd_mul(headTransform, localEyeTransform);
597 // NOTE: We need to convert from meters to centimeters here
598 QMatrix4x4 transform{eyeCameraTransform.columns[0].x, eyeCameraTransform.columns[1].x, eyeCameraTransform.columns[2].x, eyeCameraTransform.columns[3].x * 100,
599 eyeCameraTransform.columns[0].y, eyeCameraTransform.columns[1].y, eyeCameraTransform.columns[2].y, eyeCameraTransform.columns[3].y * 100,
600 eyeCameraTransform.columns[0].z, eyeCameraTransform.columns[1].z, eyeCameraTransform.columns[2].z, eyeCameraTransform.columns[3].z * 100,
601 0.0f, 0.0f, 0.0f, 1.0f};
602 QQuick3DNodePrivate::get(xrOrigin->eyeCamera(i))->setLocalTransform(transform);
603}
604
605void QQuick3DXrManagerPrivate::updateCamera(QQuick3DViewport *xrViewport, simd_float4x4 headTransform, cp_drawable_t drawable, QQuick3DXrOrigin *xrOrigin, int i)
606{
607 updateCameraImp(headTransform, drawable, xrOrigin, i);
608 xrViewport->setCamera(xrOrigin->eyeCamera(i));
609}
610
611void QQuick3DXrManagerPrivate::updateCameraMultiview(QQuick3DViewport *xrViewport, simd_float4x4 headTransform, cp_drawable_t drawable, QQuick3DXrOrigin *xrOrigin)
612{
613 QQuick3DCamera *cameras[2] {xrOrigin->eyeCamera(0), xrOrigin->eyeCamera(1)};
614
615 for (int i = 0; i < 2; ++i)
616 updateCameraImp(headTransform, drawable, xrOrigin, i);
617
618 xrViewport->setMultiViewCameras(cameras);
619}
620
621
623 : q_ptr(&manager)
624{
625}
626
628{
629}
630
631QQuick3DXrManagerPrivate *QQuick3DXrManagerPrivate::get(QQuick3DXrManager *manager)
632{
633 QSSG_ASSERT(manager != nullptr, return nullptr);
634 return manager->d_func();
635}
636
637bool QQuick3DXrManagerPrivate::initialize()
638{
639 Q_Q(QQuick3DXrManager);
640
641 // NOTE: Check if there's a global instance of the compositor layer
642 // already created, if not create one. Should probably move this over
643 // to the native interface.
644 if (!m_compositorLayer) {
645 m_compositorLayer = s_compositorLayer->instance;
646 if (!m_compositorLayer) {
647 m_compositorLayer = new CompositorLayer;
648 s_compositorLayer->instance = m_compositorLayer;
649 }
650 }
651
652 if (!m_inputManager)
653 m_inputManager = QQuick3DXrInputManager::instance();
654 if (!m_anchorManager)
655 m_anchorManager = QQuick3DXrAnchorManager::instance();
656
657 // NOTE: Check if the compository layer proxy is already active.
658 if (!m_compositorLayer->isInitialized()) {
659 if (auto *visionOSApplicaton = qGuiApp->nativeInterface<QNativeInterface::QVisionOSApplication>()) {
660 visionOSApplicaton->setImmersiveSpaceCompositorLayer(m_compositorLayer);
661 // FIXME: We don't actually handle the case where the rendere changes or we get multiple calls should do something.
662 QObject::connect(m_compositorLayer, &CompositorLayer::layerRendererReady, q, &QQuick3DXrManager::initialized, Qt::ConnectionType(Qt::SingleShotConnection | Qt::QueuedConnection));
663 QObject::connect(m_compositorLayer, &CompositorLayer::renderStateChanged, q, [q](QQuick3DXrManagerPrivate::RenderState state) {
664 switch (state) {
665 case QQuick3DXrManagerPrivate::RenderState::Uninitialized:
666 qCDebug(lcQuick3DXr, "Render state: Uninitialized");
667 QQuick3DXrManagerPrivate::get(q)->m_running = false;
668 break;
669 case QQuick3DXrManagerPrivate::RenderState::Running:
670 qCDebug(lcQuick3DXr, "Render state: Running");
671 QQuick3DXrManagerPrivate::get(q)->m_running = true;
672 QCoreApplication::postEvent(q, new QEvent(QEvent::UpdateRequest));
673 break;
674 case QQuick3DXrManagerPrivate::RenderState::Invalidated:
675 qCDebug(lcQuick3DXr, "Render state: Invalidated");
676 QQuick3DXrManagerPrivate::get(q)->m_running = false;
677 emit q->sessionEnded();
678 break;
679 case QQuick3DXrManagerPrivate::RenderState::Paused:
680 QQuick3DXrManagerPrivate::get(q)->m_running = false;
681 qCDebug(lcQuick3DXr, "Render state: Paused");
682 break;
683 }
684 }, Qt::DirectConnection);
685
686 QObject::connect(m_compositorLayer, &CompositorLayer::arStateChanged, q, [q](QQuick3DXrManagerPrivate::ArTrackingState state) {
687 switch (state) {
688 case QQuick3DXrManagerPrivate::ArTrackingState::Uninitialized:
689 qCDebug(lcQuick3DXr, "AR state: Uninitialized");
690 QQuick3DXrManagerPrivate::get(q)->m_arRunning = false;
691 break;
692 case QQuick3DXrManagerPrivate::ArTrackingState::Initialized:
693 qCDebug(lcQuick3DXr, "AR state: Initialized");
694 QQuick3DXrManagerPrivate::get(q)->m_arRunning = false;
695 break;
696 case QQuick3DXrManagerPrivate::ArTrackingState::Running:
697 qCDebug(lcQuick3DXr, "AR state: Running");
698 QQuick3DXrManagerPrivate::get(q)->m_arRunning = true;
699 QCoreApplication::postEvent(q, new QEvent(QEvent::UpdateRequest));
700 break;
701 case QQuick3DXrManagerPrivate::ArTrackingState::Paused:
702 qCDebug(lcQuick3DXr, "AR state: Paused");
703 QQuick3DXrManagerPrivate::get(q)->m_arRunning = false;
704 break;
705 case QQuick3DXrManagerPrivate::ArTrackingState::Stopped:
706 qCDebug(lcQuick3DXr, "AR state: Stopped");
707 QQuick3DXrManagerPrivate::get(q)->m_arRunning = false;
708 break;
709 }
710 }, Qt::DirectConnection);
711
712 m_compositorLayer->init(this);
713 }
714 return false;
715 }
716
717 return true;
718}
719
720void QQuick3DXrManagerPrivate::setupWindow(QQuickWindow *window)
721{
722 if (!window) {
723 qWarning("QQuick3DXrManagerPrivate: Window is null!");
724 return;
725 }
726
727 QSSG_ASSERT_X(m_compositorLayer != nullptr, "No composition layer!", return);
728
729 cp_layer_renderer_t renderer = m_compositorLayer->layerRenderer();
730 if (!renderer) {
731 qWarning("QQuick3DXrManagerPrivate: Layer renderer is not available.");
732 return;
733 }
734
735 auto device = cp_layer_renderer_get_device(renderer);
736 auto commandQueue = [device newCommandQueue];
737
738 auto qqGraphicsDevice = QQuickGraphicsDevice::fromDeviceAndCommandQueue(static_cast<MTLDevice*>(device), static_cast<MTLCommandQueue *>(commandQueue));
739
740 window->setGraphicsDevice(qqGraphicsDevice);
741}
742
743bool QQuick3DXrManagerPrivate::finalizeGraphics(QRhi *rhi)
744{
745 Q_UNUSED(rhi);
746
747 m_isGraphicsInitialized = true;
748 return m_isGraphicsInitialized;
749}
750
752{
753 return m_compositorLayer && (m_compositorLayer->layerRenderer() != nullptr);
754}
755
757{
758 return m_isGraphicsInitialized;
759}
760
761bool QQuick3DXrManagerPrivate::setupGraphics(QQuickWindow *window)
762{
763 QSSG_ASSERT(window != nullptr && m_compositorLayer != nullptr, return false);
764
765 if (m_compositorLayer->isMultiviewEnabled()) {
766 if (!m_renderThread) {
767 m_renderThread = new QThread;
768 m_renderThread->setObjectName(QLatin1StringView(s_renderThreadName));
769 m_renderThread->start();
770 }
771
772 if (m_compositorLayer->thread() != m_renderThread)
773 m_compositorLayer->moveToThread(m_renderThread);
774
775 Q_ASSERT(m_renderThread != nullptr);
776 QQuickWindowPrivate::get(window)->renderControl->prepareThread(m_renderThread);
777 }
778
779 // NOTE: See QQuick3DXrManager::setupGraphics() for emission of the value changed signal
780 m_multiviewRenderingEnabled = m_compositorLayer->isMultiviewEnabled();
781
782 return true;
783}
784
785void QQuick3DXrManagerPrivate::getDefaultClipDistances(float &nearClip, float &farClip) const
786{
787 m_compositorLayer->getDefaultDepthRange(nearClip, farClip);
788}
789
790void QQuick3DXrManagerPrivate::teardown()
791{
792 Q_Q(QQuick3DXrManager);
793
794 qCDebug(lcQuick3DXr) << "Tearing down XR session";
795
796 QMutexLocker<QMutex> locker { nullptr };
797 if (m_compositorLayer)
798 locker = QMutexLocker<QMutex>{ &m_compositorLayer->renderLock() };
799
800 m_running = false;
801
802 if (m_inputManager)
803 QQuick3DXrInputManagerPrivate::get(m_inputManager)->teardown();
804
805 if (m_anchorManager)
806 m_anchorManager->teardown();
807
808 if (m_compositorLayer) {
809 m_compositorLayer->stopArSession();
810 m_compositorLayer->teardown();
811
812 // NOTE: Unlock the render mutex. The compositor layer will be destroyed on the render thread now!
813 // Also, be aware of the race condition here once the lock is released (see: teardownStarted())...
814 locker.unlock();
815
816 Qt::ConnectionType connection = (m_compositorLayer->thread() == QThread::currentThread())
817 ? Qt::DirectConnection : Qt::BlockingQueuedConnection;
818 QMetaObject::invokeMethod(m_compositorLayer, &CompositorLayer::destroy, connection, this, q->m_quickWindow, m_compositorLayer);
819 m_compositorLayer = nullptr;
820 }
821
822 if (m_renderThread) {
823 m_renderThread->quit();
824 m_renderThread->wait();
825 delete m_renderThread;
826 m_renderThread = nullptr;
827 }
828}
829
831{
832 Q_UNUSED(enable);
833 qCWarning(lcQuick3DXr) << "Changing multiview rendering is not supported at runtime on visionOS!";
834}
835
837{
838 return m_multiviewRenderingEnabled;
839}
840
842{
843 Q_UNUSED(enable);
844 qCWarning(lcQuick3DXr) << "Changing passthrough is not supported at runtime on visionOS!";
846}
847
849{
850 // FIXME: Not sure exactly what reference space is default or what is supported etc.
851 return QtQuick3DXr::ReferenceSpace::ReferenceSpaceLocalFloor;
852}
853
854void QQuick3DXrManagerPrivate::setReferenceSpace(QtQuick3DXr::ReferenceSpace newReferenceSpace)
855{
856 Q_UNUSED(newReferenceSpace);
857 qCWarning(lcQuick3DXr) << "Changing reference space is not supported at runtime on visionOS!";
858}
859
861{
862 if (!enable)
863 qCWarning(lcQuick3DXr, "Depth submission is required on visionOS");
864}
865
867{
868 Q_Q(QQuick3DXrManager);
869
870 // Lock the render mutex and request a new frame to be rendered.
871 QMutexLocker<QMutex> locker { &m_compositorLayer->renderLock() };
872
873 if (!m_running || !m_arRunning || !m_isGraphicsInitialized) {
874 qCDebug(lcQuick3DXr, "Not running, skipping update");
875 return;
876 }
877
878 if (m_compositorLayer->isMultiviewEnabled()) {
879 // polish (GUI thread)
880 q->m_renderControl->polishItems();
881 m_syncDone = false;
882 QCoreApplication::postEvent(m_compositorLayer, new QEvent(CompositorLayer::asQEvent(CompositorLayer::Event::Render)));
883 // Wait for the sync to complete.
884 bool waitCompleted = m_compositorLayer->waitForSyncToComplete();
885 // The gui thread can now continue.
886
887 QQuick3DXrAnimationDriver *animationDriver = q->m_animationDriver;
888
889 if (Q_LIKELY(waitCompleted && m_syncDone && animationDriver)) {
890 animationDriver->setStep(m_nextStepSize);
891 animationDriver->advance();
892 } else {
893 qCDebug(lcQuick3DXr) << "Failed to wait for sync to complete" << "\nWaitCompleted:" << waitCompleted << "\nsyncDone" << m_syncDone;
894 }
895 } else {
896 [[maybe_unused]] bool ret = m_compositorLayer->renderFrame(locker);
897 }
898
899 QCoreApplication::postEvent(q, new QEvent(QEvent::UpdateRequest));
900}
901
903{
904 // NOTE: This is not used on visionOS
905}
906
907void QQuick3DXrManagerPrivate::processSpatialEvents(const QJsonObject &events)
908{
909 Q_Q(QQuick3DXrManager);
910 QSSG_ASSERT(q->m_vrViewport != nullptr, return);
911 QQuick3DXrInputManagerPrivate::processSpatialEvents(*q->m_vrViewport, events);
912}
913
914void QQuick3DXrManagerPrivate::prepareAnchorManager(QQuick3DXrAnchorManager *anchorManager, ar_data_providers_t dataProviders)
915{
916 anchorManager->prepareAnchorManager(dataProviders);
917}
918
919void QQuick3DXrManagerPrivate::initAnchorManager(QQuick3DXrAnchorManager *anchorManager)
920{
921 anchorManager->initAnchorManager();
922}
923
924void QQuick3DXrManagerPrivate::initInputManager(QQuick3DXrInputManager *im)
925{
927}
928
929void QQuick3DXrManagerPrivate::setSamples(int samples)
930{
931 Q_UNUSED(samples);
932 qCWarning(lcQuick3DXr) << "Changing sample count is not supported on visionOS!";
933}
934
936{
937 return QStringLiteral("visionOS");
938}
939
941{
942 static const auto versionNumber = QOperatingSystemVersion::current().version();
943 return versionNumber;
944}
945
947{
948 return QString();
949}
950
951void QQuick3DXrManagerPrivate::setupShadingRateMap(QQuickWindow *window, QRhiShadingRateMap *srm)
952{
953 if (QRhiSwapChain *swapchain = window->swapChain()) {
954 swapchain->setShadingRateMap(srm);
955 if (!m_srmRenderPassDesc) {
956 qCDebug(lcQuick3DXr) << "Creating render pass descriptor suitable for shading rate map use";
957 m_srmRenderPassDesc = swapchain->newCompatibleRenderPassDescriptor();
958 swapchain->setRenderPassDescriptor(m_srmRenderPassDesc);
959 }
960 } else {
961 QSGRendererInterface *rif = window->rendererInterface();
962 QRhiTextureRenderTarget *rt = static_cast<QRhiTextureRenderTarget *>(rif->getResource(window, QSGRendererInterface::RhiRedirectRenderTarget));
963 if (QSSG_GUARD_X(rt, "No render target!")) {
964 QRhiTextureRenderTargetDescription desc = rt->description();
965 desc.setShadingRateMap(srm);
966 rt->setDescription(desc);
967 if (!m_srmRenderPassDesc) {
968 qCDebug(lcQuick3DXr) << "Creating render pass descriptor suitable for shading rate map use";
969 QRhiRenderPassDescriptor *rpd = rt->renderPassDescriptor();
970 m_srmRenderPassDesc = rpd->newCompatibleRenderPassDescriptor();
971 rt->setRenderPassDescriptor(m_srmRenderPassDesc);
972 rt->create();
973 }
974 }
975 }
976}
977
978void QQuick3DXrManagerPrivate::releaseResources()
979{
980 qCDebug(lcQuick3DXr) << "Releasing resources";
981 Q_ASSERT((m_renderThread == nullptr) || QThread::currentThread() == m_renderThread);
982 delete m_srmRenderPassDesc;
983 m_srmRenderPassDesc = nullptr;
984 delete m_rhiDepthTexture;
985 m_rhiDepthTexture = nullptr;
986 for (size_t i = 0, end = std::size(m_srm); i < end; ++i) {
987 delete m_srm[i];
988 m_srm[i] = nullptr;
989 }
990}
991
992bool QQuick3DXrManagerPrivate::renderFrameImpl(QMutexLocker<QMutex> &locker, QWaitCondition &waitCondition)
993{
994 Q_Q(QQuick3DXrManager);
995
996 // NOTE: The GUI thread is locked at this point
997 QQuickWindow *window = q->m_quickWindow;
998 QQuickRenderControl *renderControl = q->m_renderControl;
999 QQuick3DXrOrigin *xrOrigin = q->m_xrOrigin;
1000 QQuick3DViewport *xrViewport = q->m_vrViewport;
1001 QQuick3DXrAnimationDriver *animationDriver = q->m_animationDriver;
1002
1003 QSSG_ASSERT_X(window && renderControl && xrViewport && xrOrigin && animationDriver, "Invalid state, rendering aborted", return false);
1004
1005 const bool multiviewRenderingEnabled = isMultiViewRenderingEnabled();
1006
1007 Q_ASSERT(!multiviewRenderingEnabled || QThread::currentThread() != window->thread());
1008
1009 auto layerRenderer = m_compositorLayer->layerRenderer();
1010 cp_frame_t frame = cp_layer_renderer_query_next_frame(layerRenderer);
1011 if (Q_UNLIKELY(frame == nullptr)) {
1012 qWarning("Failed to get next frame");
1013 return false;
1014 }
1015
1016 cp_frame_timing_t timing = cp_frame_predict_timing(frame);
1017 if (Q_UNLIKELY(timing == nullptr)) {
1018 qWarning("Failed to get timing for frame");
1019 return false;
1020 }
1021
1022 cp_frame_start_update(frame);
1023
1024 cp_frame_end_update(frame);
1025
1026 cp_time_t optimalInputTime = cp_frame_timing_get_optimal_input_time(timing);
1027 cp_time_wait_until(optimalInputTime);
1028
1029 cp_frame_start_submission(frame);
1030
1031 // A frame can come with more than one drawable since visionOS 26, for example an additional
1032 // one for capture (recording or streaming). We render the one shown on the device and, for
1033 // now, leave the others alone; they are never encoded or presented.
1034 //
1035 // NOTE: That is also what we got from the deprecated cp_frame_query_drawable(), which only
1036 // ever handed out one drawable, so an app had no way of presenting a capture drawable to
1037 // begin with. The assumption is therefore that the compositor sources the capture from what
1038 // we do present, as it must have done before. None of this is documented, so if a capture
1039 // ever turns out to be empty or stale, the fix is to encode and present the remaining
1040 // drawables as well, at the very least as a cleared pass.
1041 cp_drawable_array_t drawables = cp_frame_query_drawables(frame);
1042 const size_t drawableCount = cp_drawable_array_get_count(drawables);
1043 if (Q_UNLIKELY(drawableCount == 0)) {
1044 // The frame has been cancelled (the layer is paused or invalidated) and must be discarded.
1045 qCDebug(lcQuick3DXr, "No drawables for frame, skipping");
1046 return false;
1047 }
1048
1049 cp_drawable_t drawable = nullptr;
1050 for (size_t d = 0; d < drawableCount && !drawable; ++d) {
1051 cp_drawable_t candidate = cp_drawable_array_get_drawable(drawables, d);
1052 if (cp_drawable_get_target(candidate) == cp_drawable_target_built_in)
1053 drawable = candidate;
1054 }
1055 if (Q_UNLIKELY(!drawable)) {
1056 qCWarning(lcQuick3DXr, "No built-in drawable for frame, using the first one");
1057 drawable = cp_drawable_array_get_drawable(drawables, 0);
1058 }
1059 if (drawableCount > 1) {
1060 static bool extraDrawablesWarningShown = false;
1061 if (!extraDrawablesWarningShown) {
1062 extraDrawablesWarningShown = true;
1063 qCWarning(lcQuick3DXr) << "Frame has" << drawableCount
1064 << "drawables; only the built-in one is rendered";
1065 }
1066 }
1067
1068 cp_frame_timing_t actualTiming = cp_drawable_get_frame_timing(drawable);
1069 ar_device_anchor_t anchor = m_compositorLayer->createPoseForTiming(actualTiming);
1070 cp_drawable_set_device_anchor(drawable, anchor);
1071
1072 // Get the pose transform from the anchor
1073 simd_float4x4 headTransform = ar_anchor_get_origin_from_anchor_transform(anchor);
1074
1075 // NOTE: We need to convert from meters to centimeters here
1076 QMatrix4x4 qtHeadTransform{headTransform.columns[0].x, headTransform.columns[1].x, headTransform.columns[2].x, headTransform.columns[3].x * 100,
1077 headTransform.columns[0].y, headTransform.columns[1].y, headTransform.columns[2].y, headTransform.columns[3].y * 100,
1078 headTransform.columns[0].z, headTransform.columns[1].z, headTransform.columns[2].z, headTransform.columns[3].z * 100,
1079 0.0f, 0.0f, 0.0f, 1.0f};
1080 xrOrigin->updateTrackedCamera(qtHeadTransform);
1081
1082 // Update the hands
1083 if (QSSG_GUARD(m_inputManager != nullptr))
1084 QQuick3DXrInputManagerPrivate::get(m_inputManager)->updateHandtracking();
1085
1086 // Animation driver
1087 // Convert the cp_frame_timing_t ticks to milliseconds
1088 enum : size_t { DisplayPeriod = 0, DisplayDelta };
1089 qint64 stepSizes[2] {0, 0};
1090 auto &[displayPeriodMS, displayDeltaMS] = stepSizes;
1091 displayPeriodMS = qint64(cp_time_to_cf_time_interval(optimalInputTime) * 1000.0);
1092 displayDeltaMS = ((qint64(cp_time_to_cf_time_interval(cp_frame_timing_get_optimal_input_time(actualTiming)) * 1000.0)) - m_previousTime);
1093 const size_t selector = ((m_previousTime == 0) || (displayDeltaMS > displayPeriodMS)) ? DisplayPeriod : DisplayDelta;
1094 m_nextStepSize = stepSizes[selector];
1095 m_previousTime = displayPeriodMS;
1096
1097 if (!multiviewRenderingEnabled) {
1098 animationDriver->setStep(m_nextStepSize);
1099 animationDriver->advance();
1100 }
1101
1102 QRhi *rhi = renderControl->rhi();
1103
1104 const auto viewCount = cp_drawable_get_view_count(drawable);
1105 const auto textureCount = cp_drawable_get_texture_count(drawable);
1106 const auto renderCalls = textureCount; // To make it less confusing...
1107
1108 bool foveationEnabled = m_compositorLayer->isFoveationEnabled();
1109 Q_ASSERT(!foveationEnabled || rhi->isFeatureSupported(QRhi::VariableRateShadingMap));
1110 // We still should make sure we have a rate map!
1111 if (foveationEnabled) {
1112 const size_t rrmapcount = cp_drawable_get_rasterization_rate_map_count(drawable);
1113 const bool validMapCount = (rrmapcount == 1 || rrmapcount == 2);
1114 QSSG_GUARD_X(validMapCount, "Invalid rate map count!");
1115 foveationEnabled = validMapCount;
1116
1117 if (foveationEnabled) {
1118 for (size_t i = 0; i < rrmapcount; ++i) {
1119 if (!m_srm[i])
1120 m_srm[i] = rhi->newShadingRateMap();
1121 id<MTLRasterizationRateMap> rrm = cp_drawable_get_rasterization_rate_map(drawable, i);
1122 const bool wasCreated = m_srm[i]->createFrom({ quint64(rrm) });
1123 QSSG_GUARD_X(wasCreated, "Failed to create shading rate map!");
1124 foveationEnabled = (foveationEnabled && wasCreated);
1125 }
1126 }
1127 }
1128
1129 // NOTE: With the layered layout (multiview rendering enabled) the drawable has a single texture array and each
1130 // view/eye is rendered to a slice in that array in one pass. With the dedicated layout (multiview rendering
1131 // disabled) the drawable has one texture per view and each view is rendered to its own texture.
1132 //
1133 // The simulator accepts the layered layout but delivers a single view, so the texture array has one slice.
1134 // In that case there is nothing to render with multiview and the texture is treated as a plain 2D render
1135 // target. The render target, depth texture and cameras therefore follow the number of views the drawable
1136 // holds per texture rather than the configured layout. NOTE: We don't do threaded rendering when multiview
1137 // rendering is disabled!
1138 const int viewsPerTexture = multiviewRenderingEnabled ? int(viewCount) : 1;
1139 const bool multiviewTarget = viewsPerTexture >= 2;
1140 QSSG_ASSERT_X(viewsPerTexture <= 2, "Unsupported number of views per texture", return false);
1141
1142 for (size_t i = 0, end = renderCalls; i != end ; ++i) {
1143 // Setup the RenderTarget based on the current drawable
1144 id<MTLTexture> colorMetalTexture = cp_drawable_get_color_texture(drawable, i);
1145 // Qt Quick and Qt Quick 3D write sRGB-encoded colors, so render into an sRGB texture through
1146 // a non-sRGB view; otherwise the GPU would encode the output a second time on write. This is
1147 // what the OpenXR backends do with their sRGB swapchain images as well. See
1148 // qssgPreferredColorFormat() for the format choice.
1149 const MTLPixelFormat colorFormat = [colorMetalTexture pixelFormat];
1150 const MTLPixelFormat colorViewFormat = qssgNonSrgbFormat(colorFormat);
1151
1152 // The size everything is laid out and rendered at: the view's viewport in the drawable's
1153 // texture, which is the source description of where the view's content goes.
1154 // Without foveation it matches the texture size. With foveation the rasterization rate
1155 // map maps this larger logical (screen space) size onto the physical texture, and Metal
1156 // interprets viewports and scissors in that logical space.
1157 cp_view_t view = cp_drawable_get_view(drawable, i);
1158 cp_view_texture_map_t textureMap = cp_view_get_view_texture_map(view);
1159 const MTLViewport viewport = cp_view_texture_map_get_viewport(textureMap);
1160 const QSize renderSize(int(viewport.width), int(viewport.height));
1161
1162 // The render target wrappers use the actual texture size. With foveation that is smaller
1163 // than the render size; QRhi derives the logical size from the shading rate map.
1164 const QSize textureSize = QSize([colorMetalTexture width], [colorMetalTexture height]);
1165
1166 // With one view per texture this sets up a plain 2D render target.
1167 QQuickRenderTarget renderTarget = QQuickRenderTarget::fromMetalTexture(static_cast<MTLTexture*>(colorMetalTexture),
1168 colorFormat,
1169 colorViewFormat,
1170 textureSize,
1171 1 /*sampleCount*/,
1172 viewsPerTexture,
1173 {});
1174
1175 auto depthMetalTexture = cp_drawable_get_depth_texture(drawable, i);
1176 const QSize depthTextureSize = QSize([depthMetalTexture width], [depthMetalTexture height]);
1177 MTLPixelFormat depthTextureFormat = [depthMetalTexture pixelFormat];
1178 static const auto convertFormat = [](MTLPixelFormat format) -> QRhiTexture::Format {
1179 switch (format) {
1180 case MTLPixelFormatDepth16Unorm:
1181 return QRhiTexture::D16;
1182 case MTLPixelFormatDepth32Float:
1183 return QRhiTexture::D32F;
1184 default:
1185 qWarning("Unsupported depth texture format");
1186 return QRhiTexture::UnknownFormat;
1187 }
1188 };
1189 auto depthFormat = convertFormat(depthTextureFormat);
1190 if (depthFormat != QRhiTexture::UnknownFormat) {
1191 if (m_rhiDepthTexture && (m_rhiDepthTexture->format() != depthFormat || m_rhiDepthTexture->pixelSize() != depthTextureSize)) {
1192 delete m_rhiDepthTexture;
1193 m_rhiDepthTexture = nullptr;
1194 }
1195
1196 if (!m_rhiDepthTexture) {
1197 if (multiviewTarget)
1198 m_rhiDepthTexture = rhi->newTextureArray(depthFormat, viewsPerTexture, depthTextureSize, 1, QRhiTexture::RenderTarget);
1199 else
1200 m_rhiDepthTexture = rhi->newTexture(depthFormat, depthTextureSize, 1, QRhiTexture::RenderTarget);
1201 }
1202
1203
1204 m_rhiDepthTexture->createFrom({ quint64(static_cast<MTLTexture*>(depthMetalTexture)), 0});
1205 renderTarget.setDepthTexture(m_rhiDepthTexture);
1206 }
1207
1208 window->setRenderTarget(renderTarget);
1209
1210 // The window size follows the (logical) render size computed above.
1211 window->setGeometry(0,
1212 0,
1213 renderSize.width(),
1214 renderSize.height());
1215 window->contentItem()->setSize(renderSize);
1216 // NOTE: We set the size here as we need to make sure the window size is correct before
1217 // the sync, or the xrView's size will be incorrect. This happens because we're on the
1218 // render thread (GUI thread is blocked) and any updates to the contentItem size will
1219 // not be updated (see: QQuick3DXrView::init)...
1220 xrViewport->setSize(renderSize);
1221
1222 // Update the camera pose
1223 if (QSSG_GUARD(xrOrigin)) {
1224 if (multiviewTarget)
1225 updateCameraMultiview(xrViewport, headTransform, drawable, xrOrigin);
1226 else
1227 updateCamera(xrViewport, headTransform, drawable, xrOrigin, i);
1228 }
1229
1230 // We only do this if we are rendering on a separate thread and with multiview rendering enabled,
1231 // or else the beginFrame and sync will be done on the GUI thread after both eyes have been
1232 // rendered...
1233 if (multiviewRenderingEnabled) {
1234 // Marks the start of the frame.
1235 renderControl->beginFrame();
1236 // Synchronization happens here on the render thread (with the GUI thread locked)
1237 m_syncDone = renderControl->sync();
1238
1239 if (Q_UNLIKELY(!m_syncDone))
1240 return false;
1241
1242 if (foveationEnabled)
1243 setupShadingRateMap(window, m_srm[i]);
1244
1245 // Signal the GUI thread that the sync is done, so it can continue.
1246 waitCondition.wakeOne();
1247 locker.unlock();
1248
1249 // Render the frame
1250 renderControl->render();
1251 // Marks the end of the frame.
1252 renderControl->endFrame();
1253 } else {
1254 renderControl->polishItems();
1255 renderControl->beginFrame();
1256 renderControl->sync();
1257 if (foveationEnabled)
1258 setupShadingRateMap(window, m_srm[i]);
1259 renderControl->render();
1260 renderControl->endFrame();
1261 }
1262
1263#if TARGET_OS_SIMULATOR == 1
1264 // With multiview this indicates that the frame with both eyes is ready from
1265 // the 3D APIs perspective. Without multiview this is done - and so the
1266 // signal is emitted - multiple times (twice) per "frame" (eye).
1267 QRhiRenderTarget *rt = QQuickWindowPrivate::get(window)->activeCustomRhiRenderTarget();
1268 if (rt->resourceType() == QRhiResource::TextureRenderTarget && static_cast<QRhiTextureRenderTarget *>(rt)->description().colorAttachmentAt(0)->texture())
1269 emit q->frameReady();
1270#endif // TARGET_OS_SIMULATOR
1271 }
1272
1273 id<MTLCommandBuffer> commandBuffer = [static_cast<const QRhiMetalNativeHandles*>(renderControl->rhi()->nativeHandles())->cmdQueue commandBuffer];
1274
1275 cp_drawable_encode_present(drawable, commandBuffer);
1276 [commandBuffer commit];
1277
1278 cp_frame_end_submission(frame);
1279
1280 return true;
1281}
1282
1283QT_END_NAMESPACE
1284
1285#include "qquick3dxrmanager_visionos.moc"
void arStateChanged(QQuick3DXrManagerPrivate::ArTrackingState)
void configure(cp_layer_renderer_capabilities_t capabilities, cp_layer_renderer_configuration_t configuration) const override
static constexpr QEvent::Type asQEvent(CompositorLayer::Event event)
friend bool QQuick3DXrManagerPrivate::renderFrameImpl(QMutexLocker< QMutex > &locker, QWaitCondition &waitCondition)
void getDefaultDepthRange(float &near, float &far) const
static Q_INVOKABLE void destroy(QQuick3DXrManagerPrivate *mngr, QQuickWindow *window, CompositorLayer *compositorLayer)
void layerRendererChanged()
static bool supportsLayoutType(cp_layer_renderer_capabilities_t capabilities, cp_layer_renderer_layout layout)
bool event(QEvent *event) override
This virtual function receives events to an object and should return true if the event e was recogniz...
void renderStateChanged(QQuick3DXrManagerPrivate::RenderState)
cp_layer_renderer_t layerRenderer() const
void render(cp_layer_renderer_t renderer) override
void handleSpatialEvents(const QJsonObject &events) override
std::underlying_type_t< QEvent::Type > EventT
void init(QQuick3DXrManagerPrivate *xrManager)
\inmodule QtCore
Definition qmutex.h:347
static QQuick3DXrAnchorManager * instance()
void prepareHandtracking(ar_data_providers_t dataProviders)
static QQuick3DXrInputManagerPrivate * get(QQuick3DXrInputManager *inputManager)
static QQuick3DXrInputManager * instance()
QQuick3DXrManagerPrivate(QQuick3DXrManager &manager)
void setupWindow(QQuickWindow *window)
QtQuick3DXr::ReferenceSpace getReferenceSpace() const
void setReferenceSpace(QtQuick3DXr::ReferenceSpace newReferenceSpace)
bool setupGraphics(QQuickWindow *window)
void processSpatialEvents(const QJsonObject &events)
void getDefaultClipDistances(float &nearClip, float &farClip) const
static bool isMultiviewRenderingDisabled()
\inmodule QtGuiPrivate \inheaderfile rhi/qrhi.h
Definition qrhi.h:650
QT_BEGIN_NAMESPACE Q_DECLARE_LOGGING_CATEGORY(lcQIORing)
static constexpr float s_defaultClipNear
static constexpr float s_defaultClipFar
static void qssgUpdateDrawableDepthRange(cp_drawable_t drawable, float &clipNear, float &clipFar)
static const char s_renderThreadName[]
static MTLPixelFormat qssgNonSrgbFormat(MTLPixelFormat format)
static MTLPixelFormat qssgPreferredColorFormat(cp_layer_renderer_capabilities_t capabilities)
static bool qssgDisableFoveation()
static void qssgSanitizeDepthRange(simd_float2 depthRange, float &near, float &far)