1// Copyright (C) 2016 Klaralvdalens Datakonsult AB (KDAB).
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "scene3drenderer_p.h"
5
6#include <Qt3DCore/qaspectengine.h>
7#include <Qt3DRender/qrenderaspect.h>
8#include <QtCore/qthread.h>
9#include <qopenglcontext.h>
10#include <qopenglframebufferobject.h>
11#include <QtQuick/qquickwindow.h>
12#include <rhi/qrhi.h>
13
14#include <Qt3DRender/private/qrenderaspect_p.h>
15#include <Qt3DRender/private/abstractrenderer_p.h>
16#include <Qt3DCore/private/qaspectengine_p.h>
17#include <Qt3DCore/private/qaspectmanager_p.h>
18#include <Qt3DCore/private/qchangearbiter_p.h>
19#include <Qt3DCore/private/qservicelocator_p.h>
20
21#include <scene3ditem_p.h>
22#include <scene3dlogging_p.h>
23#include <scene3dsgnode_p.h>
24
25#include <QtQuick/private/qquickwindow_p.h>
26
27QT_BEGIN_NAMESPACE
28
29namespace Qt3DRender {
30
31class ContextSaver
32{
33public:
34 explicit ContextSaver(QOpenGLContext *context = QOpenGLContext::currentContext())
35 : m_context(context),
36 m_surface(context ? context->surface() : nullptr)
37 {
38 }
39
40 ~ContextSaver()
41 {
42 if (m_context && m_context->surface() != m_surface)
43 m_context->makeCurrent(surface: m_surface);
44 }
45
46 QOpenGLContext *context() const { return m_context; }
47 QSurface *surface() const { return m_surface; }
48
49private:
50 QOpenGLContext * const m_context;
51 QSurface * const m_surface;
52};
53
54/*!
55 \class Qt3DRender::Scene3DRenderer
56 \internal
57
58 \brief The Scene3DRenderer class takes care of rendering a Qt3D scene
59 within a Framebuffer object to be used by the QtQuick 2 renderer.
60
61 The Scene3DRenderer class renders a Qt3D scene as provided by a Scene3DItem.
62 It owns the aspectEngine even though it doesn't instantiate it.
63
64 The render loop goes as follows:
65 \list
66 \li The main thread runs, drives Animations, etc. and causes changes to be
67 reported to the Qt3D change arbiter. The first change reported will cause
68 the scene3drenderer to be marked dirty.
69 \li The QtQuick render thread starts a new frame, synchronizes the scene
70 graph and emits afterSynchronizing. This will trigger some preparational
71 steps for rendering and mark the QSGNode dirty if the Scene3DRenderer is
72 dirty.
73 \li The QtQuick render loop emits beforeRendering. If we're marked dirty or
74 if the renderPolicy is set to Always, we'll ask the Qt3D renderer aspect to
75 render. That call is blocking. If the aspect jobs are not done, yet, the
76 renderer will exit early and we skip a frame.
77 \endlist
78
79 The shutdown procedure is a two steps process that goes as follow:
80
81 \list
82 \li The window is closed
83
84 \li This triggers the windowsChanged signal which the Scene3DRenderer
85 uses to perform the necessary cleanups in the QSGRenderThread (destroys
86 DebugLogger ...) with the shutdown slot (queued connection).
87
88 \li The destroyed signal of the window is also connected to the
89 Scene3DRenderer. When triggered in the context of the main thread, the
90 cleanup slot is called.
91 \endlist
92
93 There is an alternate shutdown procedure in case the QQuickItem is
94 destroyed with an active window which can happen in the case where the
95 Scene3D is used with a QtQuick Loader
96
97 In that case the shutdown procedure goes the same except that the destroyed
98 signal of the window is not called. Therefore the cleanup method is invoked
99 to properly destroy the aspect engine.
100 */
101Scene3DRenderer::Scene3DRenderer()
102 : QObject()
103 , m_aspectEngine(nullptr)
104 , m_renderAspect(nullptr)
105 , m_node(nullptr)
106 , m_window(nullptr)
107 , m_needsShutdown(true)
108 , m_shouldRender(false)
109 , m_dirtyViews(false)
110 , m_skipFrame(false)
111 , m_allowRendering(0)
112 , m_compositingMode(Scene3DItem::FBO)
113{
114
115}
116
117void Scene3DRenderer::init(Qt3DCore::QAspectEngine *aspectEngine,
118 QRenderAspect *renderAspect)
119{
120 m_aspectEngine = aspectEngine;
121 m_renderAspect = renderAspect;
122 m_needsShutdown = true;
123
124 // Detect which Rendering backend Qt3D is using
125 Qt3DRender::QRenderAspectPrivate *aspectPriv = static_cast<QRenderAspectPrivate*>(QRenderAspectPrivate::get(q: m_renderAspect));
126 Qt3DRender::Render::AbstractRenderer *renderer = aspectPriv->m_renderer;
127
128 const bool isRHI = renderer->api() == API::RHI;
129 if (isRHI)
130 m_quickRenderer = new Scene3DRenderer::RHIRenderer;
131 else
132 m_quickRenderer = new Scene3DRenderer::GLRenderer;
133 m_quickRenderer->initialize(scene3DRenderer: this, renderer);
134}
135
136void Scene3DRenderer::setWindow(QQuickWindow *window)
137{
138 if (window == m_window)
139 return;
140
141 QObject::disconnect(receiver: m_window);
142 m_window = window;
143
144 if (m_window) {
145 QObject::connect(sender: m_window, signal: &QQuickWindow::beforeRendering, context: this,
146 slot: [this] () { m_quickRenderer->beforeRendering(scene3DRenderer: this); }, type: Qt::DirectConnection);
147 QObject::connect(sender: m_window, signal: &QQuickWindow::beforeRenderPassRecording, context: this,
148 slot: [this] () { m_quickRenderer->beforeRenderPassRecording(scene3DRenderer: this); }, type: Qt::DirectConnection);
149 } else {
150 shutdown();
151 }
152}
153
154Scene3DRenderer::~Scene3DRenderer()
155{
156 qCDebug(Scene3D) << Q_FUNC_INFO << QThread::currentThread();
157 shutdown();
158}
159
160Scene3DSGNode *Scene3DRenderer::sgNode() const
161{
162 return m_node;
163}
164
165bool Scene3DRenderer::isYUp() const
166{
167 return m_quickRenderer ? m_quickRenderer->isYUp() : true;
168}
169
170// Executed in the QtQuick render thread (which may even be the gui/main with QQuickWidget / RenderControl).
171void Scene3DRenderer::shutdown()
172{
173 if (!m_needsShutdown)
174 return;
175 m_needsShutdown = false;
176
177 m_quickRenderer->shutdown(sceneRenderer: this);
178 delete m_quickRenderer;
179 m_quickRenderer = nullptr;
180}
181
182// Render Thread, GUI locked
183void Scene3DRenderer::beforeSynchronize()
184{
185 m_quickRenderer->beforeSynchronize(scene3DRenderer: this);
186}
187
188void Scene3DRenderer::allowRender()
189{
190 m_allowRendering.release(n: 1);
191}
192
193void Scene3DRenderer::setCompositingMode(Scene3DItem::CompositingMode mode)
194{
195 m_compositingMode = mode;
196}
197
198void Scene3DRenderer::setSkipFrame(bool skip)
199{
200 m_skipFrame = skip;
201}
202
203void Scene3DRenderer::setMultisample(bool multisample)
204{
205 m_multisample = multisample;
206}
207
208void Scene3DRenderer::setBoundingSize(const QSize &size)
209{
210 m_boundingRectSize = size;
211}
212
213QOpenGLFramebufferObject *Scene3DRenderer::GLRenderer::createMultisampledFramebufferObject(const QSize &size)
214{
215 QOpenGLFramebufferObjectFormat format;
216 format.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
217 int samples = QSurfaceFormat::defaultFormat().samples();
218 if (samples == -1)
219 samples = 4;
220 format.setSamples(samples);
221 return new QOpenGLFramebufferObject(size, format);
222}
223
224QOpenGLFramebufferObject *Scene3DRenderer::GLRenderer::createFramebufferObject(const QSize &size)
225{
226 QOpenGLFramebufferObjectFormat format;
227 format.setAttachment(QOpenGLFramebufferObject::Depth);
228 return new QOpenGLFramebufferObject(size, format);
229}
230
231void Scene3DRenderer::GLRenderer::initialize(Scene3DRenderer *scene3DRenderer,
232 Qt3DRender::Render::AbstractRenderer *renderer)
233{
234 Q_UNUSED(scene3DRenderer);
235 Q_ASSERT(QOpenGLContext::currentContext());
236 m_renderer = renderer;
237 ContextSaver saver;
238 Q_ASSERT(renderer->api() == API::OpenGL);
239
240 m_renderer->setRenderDriver(Qt3DRender::Render::AbstractRenderer::Scene3D);
241 m_renderer->setOpenGLContext(saver.context());
242 m_renderer->initialize();
243}
244
245void Scene3DRenderer::GLRenderer::beforeSynchronize(Scene3DRenderer *scene3DRenderer)
246{
247 // Check size / multisampling
248 QQuickWindow *window = scene3DRenderer->m_window;
249
250 if (!window)
251 return;
252
253 // Only render if we are sure aspectManager->processFrame was called prior
254 // We could otherwise enter a deadlock state
255 if (!scene3DRenderer->m_allowRendering.tryAcquire(n: std::max(a: scene3DRenderer->m_allowRendering.available(), b: 1)))
256 return;
257
258 // In the case of OnDemand rendering, we still need to get to this
259 // point to ensure we have processed jobs for all aspects.
260 // We also still need to call render() to allow proceeding with the
261 // next frame. However it won't be performing any 3d rendering at all
262 // so we do it here and return early. This prevents a costly QtQuick
263 // SceneGraph update for nothing
264 if (scene3DRenderer->m_skipFrame) {
265 scene3DRenderer->m_skipFrame = false;
266 ContextSaver saver;
267 m_renderer->render(swapBuffers: false);
268 return;
269 }
270
271 scene3DRenderer->m_shouldRender = true;
272
273 m_multisample = scene3DRenderer->multisample();
274 const QSize boundingRectSize = scene3DRenderer->boundingSize();
275 const QSize currentSize = boundingRectSize * window->effectiveDevicePixelRatio();
276 const bool sizeHasChanged = currentSize != m_lastSize;
277 const bool multisampleHasChanged = m_multisample != m_lastMultisample;
278 const bool forceRecreate = sizeHasChanged || multisampleHasChanged;
279 // Store the current size as a comparison
280 // point for the next frame
281 m_lastSize = currentSize;
282 m_lastMultisample = m_multisample;
283
284 // Rebuild FBO if size/multisampling has changed
285 const bool usesFBO = scene3DRenderer->m_compositingMode == Scene3DItem::FBO;
286 auto node = scene3DRenderer->m_node;
287 if (node == nullptr) {
288 node = new Scene3DSGNode();
289 scene3DRenderer->m_node = node;
290 }
291 if (usesFBO) {
292 // Rebuild FBO and textures if never created or a resize has occurred
293 if ((m_multisampledFBO.isNull() || forceRecreate) && m_multisample) {
294 m_multisampledFBO.reset(other: createMultisampledFramebufferObject(size: m_lastSize));
295 if (m_multisampledFBO->format().samples() == 0 || !QOpenGLFramebufferObject::hasOpenGLFramebufferBlit()) {
296 m_multisample = false;
297 m_multisampledFBO.reset(other: nullptr);
298 }
299 }
300
301 const bool generateNewTexture = m_finalFBO.isNull() || forceRecreate;
302 if (generateNewTexture) {
303 m_finalFBO.reset(other: createFramebufferObject(size: m_lastSize));
304 m_textureId = m_finalFBO->texture();
305 m_texture.reset(other: QNativeInterface::QSGOpenGLTexture::fromNative(textureId: m_textureId, window, size: m_finalFBO->size(), options: QQuickWindow::TextureHasAlphaChannel));
306 }
307
308 // Set texture on node
309 if (!node->texture() || generateNewTexture)
310 node->setTexture(m_texture.data());
311 }
312
313 // Mark SGNodes as dirty so that QQuick will trigger some rendering
314 if (node)
315 node->markDirty(bits: QSGNode::DirtyMaterial);
316}
317
318void Scene3DRenderer::GLRenderer::beforeRendering(Scene3DRenderer *scene3DRenderer)
319{
320 Q_UNUSED(scene3DRenderer);
321}
322
323void Scene3DRenderer::GLRenderer::beforeRenderPassRecording(Scene3DRenderer *scene3DRenderer)
324{
325 // When this is fired, beforeRendering was fired and the RenderPass for QtQuick was
326 // started -> meaning the window was cleared but actual draw commands have yet to be
327 // recorded.
328 // When using the GL Renderer and FBO, that's the state we want to be in.
329
330 QMutexLocker l(&scene3DRenderer->m_windowMutex);
331 // Lock to ensure the window doesn't change while we are rendering
332 if (!scene3DRenderer->m_window || !scene3DRenderer->m_shouldRender)
333 return;
334 // Reset flag for next frame
335 scene3DRenderer->m_shouldRender = false;
336 ContextSaver saver;
337
338 // Create and bind FBO if using the FBO compositing mode
339 const bool usesFBO = scene3DRenderer->m_compositingMode == Scene3DItem::FBO;
340 if (usesFBO) {
341 // Bind FBO
342 if (m_multisample) //Only try to use MSAA when available
343 m_multisampledFBO->bind();
344 else
345 m_finalFBO->bind();
346 }
347
348 // Render Qt3D Scene
349 // Qt3D takes care of resetting the GL state to default values
350 m_renderer->render(swapBuffers: usesFBO);
351
352 // We may have called doneCurrent() so restore the context if the rendering surface was changed
353 // Note: keep in mind that the ContextSave also restores the surface when destroyed
354 if (saver.context()->surface() != saver.surface())
355 saver.context()->makeCurrent(surface: saver.surface());
356
357 if (usesFBO) {
358 if (m_multisample) {
359 // Blit multisampled FBO with non multisampled FBO with texture attachment
360 const QRect dstRect(QPoint(0, 0), m_finalFBO->size());
361 const QRect srcRect(QPoint(0, 0), m_multisampledFBO->size());
362 QOpenGLFramebufferObject::blitFramebuffer(target: m_finalFBO.data(), targetRect: dstRect,
363 source: m_multisampledFBO.data(), sourceRect: srcRect,
364 GL_COLOR_BUFFER_BIT,
365 GL_NEAREST,
366 readColorAttachmentIndex: 0, drawColorAttachmentIndex: 0,
367 restorePolicy: QOpenGLFramebufferObject::DontRestoreFramebufferBinding);
368 }
369
370 // Restore QtQuick FBO
371 QOpenGLFramebufferObject::bindDefault();
372
373 // Only show the node once Qt3D has rendered to it
374 // Avoids showing garbage on the first frame
375 if (scene3DRenderer->m_node)
376 scene3DRenderer->m_node->show();
377 }
378}
379
380void Scene3DRenderer::GLRenderer::shutdown(Scene3DRenderer *sceneRenderer)
381{
382 // Shutdown the Renderer Aspect while the OpenGL context
383 // is still valid
384 if (sceneRenderer->m_renderAspect)
385 m_renderer->shutdown();
386
387 m_finalFBO.reset();
388 m_multisampledFBO.reset();
389}
390
391void Scene3DRenderer::RHIRenderer::initialize(Scene3DRenderer *scene3DRenderer,
392 Qt3DRender::Render::AbstractRenderer *renderer)
393{
394 QQuickWindow *window = scene3DRenderer->m_window;
395 Q_ASSERT(window);
396 Q_ASSERT(renderer->api() == API::RHI);
397
398 // Retrieve RHI context
399 QSGRendererInterface *rif = window->rendererInterface();
400 const bool isRhi = QSGRendererInterface::isApiRhiBased(api: rif->graphicsApi());
401 Q_ASSERT(isRhi);
402 if (isRhi) {
403 m_rhi = static_cast<QRhi *>(rif->getResource(window, resource: QSGRendererInterface::RhiResource));
404 if (!m_rhi)
405 qFatal(msg: "No QRhi from QQuickWindow, this cannot happen");
406 m_renderer = renderer;
407 m_renderer->setRenderDriver(Qt3DRender::Render::AbstractRenderer::Scene3D);
408 m_renderer->setRHIContext(m_rhi);
409 m_renderer->initialize();
410 }
411}
412
413void Scene3DRenderer::RHIRenderer::beforeSynchronize(Scene3DRenderer *scene3DRenderer)
414{
415 // Check size / multisampling
416 QQuickWindow *window = scene3DRenderer->m_window;
417
418 if (!window)
419 return;
420
421 // Only render if we are sure aspectManager->processFrame was called prior
422 // We could otherwise enter a deadlock state
423 if (!scene3DRenderer->m_allowRendering.tryAcquire(n: std::max(a: scene3DRenderer->m_allowRendering.available(), b: 1)))
424 return;
425
426 // In the case of OnDemand rendering, we still need to get to this
427 // point to ensure we have processed jobs for all aspects.
428 // We also still need to call render() to allow proceeding with the
429 // next frame. However it won't be performing any 3d rendering at all
430 // so we do it here and return early. This prevents a costly QtQuick
431 // SceneGraph update for nothing
432 if (scene3DRenderer->m_skipFrame) {
433 scene3DRenderer->m_skipFrame = false;
434 m_renderer->render(swapBuffers: false);
435 return;
436 }
437
438 scene3DRenderer->m_shouldRender = true;
439
440 const QSize boundingRectSize = scene3DRenderer->boundingSize();
441 const QSize currentSize = boundingRectSize * window->effectiveDevicePixelRatio();
442 const bool sizeHasChanged = currentSize != m_lastSize;
443 const bool multisampleHasChanged = scene3DRenderer->multisample() != m_lastMultisample;
444 // Store the current size and multisample as a comparison point for the next frame
445 m_lastMultisample = scene3DRenderer->multisample();
446 m_lastSize = currentSize;
447
448 const bool forceRecreate = sizeHasChanged || multisampleHasChanged;
449 const bool usesFBO = scene3DRenderer->m_compositingMode == Scene3DItem::FBO;
450 // Not sure how we could support underlay rendering properly given Qt3D RHI will render into its own
451 // RHI RenderPasses prior to QtQuick and beginning a new RenderPass clears the screen
452 Q_ASSERT(usesFBO);
453 const bool generateNewTexture = m_texture.isNull() || forceRecreate;
454 const int samples = m_lastMultisample ? 4 : 1;
455
456 if (generateNewTexture) {
457 releaseRHIResources();
458
459 // Depth / Stencil
460 m_rhiDepthRenderBuffer = m_rhi->newRenderBuffer(type: QRhiRenderBuffer::DepthStencil, pixelSize: m_lastSize, sampleCount: samples);
461 m_rhiDepthRenderBuffer->create();
462
463 // Color Attachment or Color Resolutin texture (if using MSAA)
464 m_rhiTexture = m_rhi->newTexture(format: QRhiTexture::RGBA8, pixelSize: m_lastSize, sampleCount: 1, flags: QRhiTexture::RenderTarget|QRhiTexture::UsedAsTransferSource);
465 m_rhiTexture->create();
466
467 // Color Attachment description
468 QRhiTextureRenderTargetDescription renderTargetDesc;
469 renderTargetDesc.setDepthStencilBuffer(m_rhiDepthRenderBuffer);
470 if (samples > 1) {
471 m_rhiMSAARenderBuffer = m_rhi->newRenderBuffer(type: QRhiRenderBuffer::Color, pixelSize: m_lastSize, sampleCount: samples, flags: {}, backingFormatHint: m_rhiTexture->format());
472 m_rhiMSAARenderBuffer->create();
473 QRhiColorAttachment att;
474 att.setRenderBuffer(m_rhiMSAARenderBuffer);
475 att.setResolveTexture(m_rhiTexture);
476 renderTargetDesc.setColorAttachments({ att });
477 } else {
478 renderTargetDesc.setColorAttachments({ m_rhiTexture });
479 }
480
481 m_rhiRenderTarget = m_rhi->newTextureRenderTarget(desc: renderTargetDesc);
482 m_rhiRenderTargetPassDescriptor = m_rhiRenderTarget->newCompatibleRenderPassDescriptor();
483 m_rhiRenderTarget->setRenderPassDescriptor(m_rhiRenderTargetPassDescriptor);
484 m_rhiRenderTarget->create();
485
486 // Create QSGTexture from QRhiTexture
487 auto *windowPriv = QQuickWindowPrivate::get(c: window);
488 m_texture.reset(other: windowPriv->createTextureFromNativeTexture(nativeObjectHandle: m_rhiTexture->nativeTexture().object,
489 nativeLayoutOrState: 0, size: m_lastSize, options: QQuickWindow::TextureHasAlphaChannel));
490
491 // Set the Default RenderTarget to use on the RHI Renderer
492 // Note: this will release all pipelines using previousRenderTarget
493 m_renderer->setDefaultRHIRenderTarget(m_rhiRenderTarget);
494 }
495
496 Scene3DSGNode *node = scene3DRenderer->m_node;
497 if (node == nullptr) {
498 node = new Scene3DSGNode();
499 scene3DRenderer->m_node = node;
500 }
501
502 // Set texture on node
503 if (!node->texture() || generateNewTexture)
504 node->setTexture(m_texture.data());
505
506 // Mark SGNodes as dirty so that QQuick will trigger some rendering
507 node->markDirty(bits: QSGNode::DirtyMaterial);
508}
509
510void Scene3DRenderer::RHIRenderer::beforeRendering(Scene3DRenderer *scene3DRenderer)
511{
512 // This is fired before QtQuick starts its RenderPass but after a RHI Command Buffer
513 // has been created and after the RHI Frame has begun
514
515 // This means the swap chain has been set up and that we only need to record commands
516
517 // On the Qt3D side this also means we shouldn't be calling m_rhi->beginFrame/endFrame
518 // and we should only be rendering using the provided swap chain
519
520 // Therefore, if a QRenderSurfaceSelector is used in the FrameGraph to render to another surface,
521 // this will not be supported with Scene3D/RHI
522
523
524 QMutexLocker l(&scene3DRenderer->m_windowMutex);
525 // Lock to ensure the window doesn't change while we are rendering
526 if (!scene3DRenderer->m_window || !scene3DRenderer->m_shouldRender)
527 return;
528 // Reset flag for next frame
529 scene3DRenderer->m_shouldRender = false;
530
531 // TO DO: We need to check what we do about 3DViews and Composition Mode as those are not
532 // directly applicable with RHI
533
534 // Retrieve Command Buffer used by QtQuick
535 QRhiCommandBuffer *cb = nullptr;
536 QSGRendererInterface *rif = scene3DRenderer->m_window ->rendererInterface();
537 QRhiSwapChain *swapchain = static_cast<QRhiSwapChain *>(rif->getResource(window: scene3DRenderer->m_window, resource: QSGRendererInterface::RhiSwapchainResource));
538 if (swapchain) {
539 cb = swapchain->currentFrameCommandBuffer();
540 } else {
541 cb = static_cast<QRhiCommandBuffer *>(rif->getResource(window: scene3DRenderer->m_window, resource: QSGRendererInterface::RhiRedirectCommandBuffer));
542 }
543
544 Q_ASSERT(cb);
545 // Tell Qt3D renderer to use provided command buffer
546 m_renderer->setRHICommandBuffer(cb);
547 m_renderer->render(swapBuffers: false);
548
549 // Only show the node once Qt3D has rendered to it
550 // Avoids showing garbage on the first frame
551 if (scene3DRenderer->m_node)
552 scene3DRenderer->m_node->show();
553}
554
555void Scene3DRenderer::RHIRenderer::beforeRenderPassRecording(Scene3DRenderer *scene3DRenderer)
556{
557 Q_UNUSED(scene3DRenderer);
558}
559
560void Scene3DRenderer::RHIRenderer::shutdown(Scene3DRenderer *scene3DRenderer)
561{
562 if (scene3DRenderer->m_renderAspect)
563 m_renderer->shutdown();
564
565 releaseRHIResources();
566}
567
568bool Scene3DRenderer::RHIRenderer::isYUp() const
569{
570 return m_rhi->isYUpInFramebuffer();
571}
572
573void Scene3DRenderer::RHIRenderer::releaseRHIResources()
574{
575 delete m_rhiRenderTarget;
576 m_rhiRenderTarget = nullptr;
577
578 delete m_rhiTexture;
579 m_rhiTexture = nullptr;
580
581 delete m_rhiDepthRenderBuffer;
582 m_rhiDepthRenderBuffer = nullptr;
583
584 delete m_rhiMSAARenderBuffer;
585 m_rhiMSAARenderBuffer = nullptr;
586
587 delete m_rhiColorRenderBuffer;
588 m_rhiColorRenderBuffer = nullptr;
589
590 delete m_rhiRenderTargetPassDescriptor;
591 m_rhiRenderTargetPassDescriptor = nullptr;
592}
593
594Scene3DRenderer::QuickRenderer::QuickRenderer() {}
595Scene3DRenderer::QuickRenderer::~QuickRenderer() {}
596
597} // namespace Qt3DRender
598
599QT_END_NAMESPACE
600
601#include "moc_scene3drenderer_p.cpp"
602

source code of qt3d/src/quick3d/imports/scene3d/scene3drenderer.cpp