// Render test for the render backends (src/render/), adapted from the // vrdashboard template's tst_render. It drives a backend and // QQuickRenderControl in the same order as OverlayController::SetWidget() and // renderOverlay(), then checks the texture handed to SteamVR and its content // (via readback). // // The backend comes from OVRAS_TEST_API (opengl / vulkan): the graphics API is // process-wide, so CTest runs one process per API. Without a GPU context the // test skips itself. With OVRAS_VULKAN_VALIDATION=1 the Vulkan validation // layers are enabled (build_scripts/steamframe/container-test.sh). #include #include #include #include #include #include #include #include "render/RenderBackend.h" class RenderTest : public QObject { Q_OBJECT private slots: void initTestCase() { GraphicsApi api = GraphicsApi::OpenGL; const QString apiName = qEnvironmentVariable( "OVRAS_TEST_API", "opengl" ); QVERIFY2( parseGraphicsApi( apiName, &api ), qPrintable( apiName ) ); m_backend = createRenderBackend( api ); if ( !m_backend ) { QSKIP( qPrintable( apiName + " not compiled in" ) ); } // Before any QQuickWindow exists, as in main(). QQuickWindow::setGraphicsApi( m_backend->graphicsApi() ); QString error; // No SteamVR here: empty interop, the backend picks defaults. if ( !m_backend->initialize( {}, &error ) ) { QSKIP( qPrintable( QString( "No %1 context: %2" ) .arg( m_backend->name(), error ) ) ); } } // One frame of a known scene (top red, bottom blue): texture type and // description for SteamVR, content and orientation. void rendersSceneUpright() { const QSize size( 64, 32 ); Scene scene( *m_backend, size ); QVERIFY2( scene.error.isEmpty(), qPrintable( scene.error ) ); const vr::Texture_t texture = scene.renderFrame(); QVERIFY( texture.handle != nullptr ); const auto expectedType = m_backend->graphicsApi() == QSGRendererInterface::Vulkan ? vr::TextureType_Vulkan : vr::TextureType_OpenGL; QCOMPARE( texture.eType, expectedType ); if ( texture.eType == vr::TextureType_Vulkan ) { const auto* vk = static_cast( texture.handle ); QVERIFY( vk->m_nImage != 0 && vk->m_pDevice && vk->m_pQueue && vk->m_pInstance ); QCOMPARE( vk->m_nWidth, static_cast( size.width() ) ); // VK_FORMAT_R8G8B8A8_UNORM, the format SteamVR gets told. QCOMPARE( vk->m_nFormat, 37u ); } QVERIFY( !m_backend->deviceName().isEmpty() ); const QImage image = m_backend->readback(); QCOMPARE( image.size(), size ); QCOMPARE( image.pixelColor( size.width() / 2, 4 ), QColor( 255, 0, 0 ) ); QCOMPARE( image.pixelColor( size.width() / 2, size.height() - 4 ), QColor( 0, 0, 255 ) ); } // Several frames in a row exercise the per-frame state handling (e.g. // the Vulkan layout transitions between Qt's frames and the SteamVR // hand-off). void rendersRepeatedly() { Scene scene( *m_backend, { 32, 32 } ); QVERIFY2( scene.error.isEmpty(), qPrintable( scene.error ) ); for ( int i = 0; i < 5; ++i ) { scene.renderFrame(); } QCOMPARE( m_backend->readback().pixelColor( 16, 2 ), QColor( 255, 0, 0 ) ); } void cleanupTestCase() { m_backend.reset(); } private: // The TwoTone scene in an offscreen window, set up like // OverlayController::SetWidget(). Tears down like // OverlayController::Shutdown() (minus VR_Shutdown). struct Scene { Scene( RenderBackend& backend, QSize size ) : backend( backend ) { window = std::make_unique( &renderControl ); window->setGeometry( 0, 0, size.width(), size.height() ); QQmlComponent component( &engine ); component.loadFromModule( "RenderTest", "TwoTone" ); root.reset( qobject_cast( component.create() ) ); if ( !root ) { error = component.errorString(); return; } root->setSize( size ); root->setParentItem( window->contentItem() ); backend.configureWindow( window.get() ); if ( !backend.makeCurrent() || !renderControl.initialize() ) { error = "QQuickRenderControl::initialize failed"; return; } if ( !backend.attachWindow( window.get(), &error ) ) { return; } const QQuickRenderTarget target = backend.createRenderTarget( size ); if ( target.isNull() ) { error = "createRenderTarget failed"; return; } window->setRenderTarget( target ); } ~Scene() { backend.makeCurrent(); root.reset(); backend.releaseRenderTarget(); backend.detachWindow(); window.reset(); } // One frame as in OverlayController::renderOverlay(); returns what // would be handed to IVROverlay::SetOverlayTexture. vr::Texture_t renderFrame() { backend.makeCurrent(); backend.beginFrame(); renderControl.polishItems(); renderControl.beginFrame(); renderControl.sync(); renderControl.render(); renderControl.endFrame(); backend.finishFrame(); return backend.vrTexture(); } RenderBackend& backend; QQmlEngine engine; // Destroyed after the window (declared before it): the window // belongs to this render control. QQuickRenderControl renderControl; std::unique_ptr window; std::unique_ptr root; QString error; }; std::unique_ptr m_backend; }; QTEST_MAIN( RenderTest ) #include "tst_render.moc"