#include #include #include #include "render/OffscreenQuickRenderer.h" #include "render/RenderBackend.h" // Renders a known scene through the real offscreen pipeline (backend + // QQuickRenderControl) and checks the texture content via readback. The backend // comes from VRDASH_TEST_API (opengl / vulkan): the graphics API is // process-global, so CTest runs one process each. class RenderTest : public QObject { Q_OBJECT private slots: void initTestCase() { GraphicsApi api = GraphicsApi::OpenGL; const QString apiName = qEnvironmentVariable("VRDASH_TEST_API", QStringLiteral("opengl")); QVERIFY2(parseGraphicsApi(apiName, &api), qPrintable(apiName)); m_backend = createRenderBackend(api); if (!m_backend) QSKIP(qPrintable(apiName + QStringLiteral(" not compiled in"))); QQuickWindow::setGraphicsApi(m_backend->graphicsApi()); QString error; if (!m_backend->initialize({}, &error)) QSKIP(qPrintable( QStringLiteral("No %1 context: %2").arg(m_backend->name(), error))); } void rendersSceneUpright() { QQmlEngine engine; const QSize size(64, 32); OffscreenQuickRenderer renderer(*m_backend, size); QString error; QVERIFY2(renderer.initialize(engine, "RenderTest", "TwoTone", &error), qPrintable(error)); int sinkCalls = 0; vr::Texture_t submitted{}; renderer.setFrameSink( [&](const vr::Texture_t& texture, const vr::VRTextureBounds_t&) { ++sinkCalls; submitted = texture; }); renderer.renderNow(); QCOMPARE(sinkCalls, 1); QVERIFY(submitted.handle != nullptr); const auto expectedType = m_backend->graphicsApi() == QSGRendererInterface::Vulkan ? vr::TextureType_Vulkan : vr::TextureType_OpenGL; QCOMPARE(submitted.eType, expectedType); if (submitted.eType == vr::TextureType_Vulkan) { const auto* vk = static_cast(submitted.handle); QVERIFY(vk->m_nImage != 0 && vk->m_pDevice && vk->m_pQueue && vk->m_pInstance); QCOMPARE(vk->m_nWidth, uint32_t(size.width())); } 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. // Vulkan layout transitions between Qt's frames and the SteamVR // hand-off). void rendersRepeatedly() { QQmlEngine engine; OffscreenQuickRenderer renderer(*m_backend, {32, 32}); QString error; QVERIFY2(renderer.initialize(engine, "RenderTest", "TwoTone", &error), qPrintable(error)); for (int i = 0; i < 5; ++i) renderer.renderNow(); QCOMPARE(m_backend->readback().pixelColor(16, 2), QColor(255, 0, 0)); } // An animated scene must render at the fps cap even when each frame // takes noticeable time: pacing is measured from frame start, not added // after the frame. void pacingHoldsFrameRateWithSlowFrames() { QQmlEngine engine; OffscreenQuickRenderer renderer(*m_backend, {32, 32}); QString error; QVERIFY2(renderer.initialize(engine, "RenderTest", "Spinner", &error), qPrintable(error)); int frames = 0; renderer.setFrameSink( [&](const vr::Texture_t&, const vr::VRTextureBounds_t&) { ++frames; QThread::usleep(3000); // e.g. a slow SteamVR hand-off }); renderer.setMaxFps(60); renderer.setActive(true); QTest::qWait(300); // warm-up frames = 0; QElapsedTimer timer; timer.start(); QTest::qWait(2000); const double fps = frames * 1000.0 / timer.elapsed(); renderer.setActive(false); qInfo("measured %.1f fps", fps); QVERIFY2(fps > 57.0 && fps < 63.0, qPrintable(QString::number(fps))); } void cleanupTestCase() { m_backend.reset(); } private: std::unique_ptr m_backend; }; QTEST_MAIN(RenderTest) #include "tst_render.moc"