Files
Matthias JanetschekandClaude Opus 5.5 bde675f1b8 Steam Frame build, packaging and deployment scripts; render test
- build_scripts/steamframe/: Debian trixie (Qt 6.8) containers for the
  aarch64 cross build, native build and tests; toolchain; scripts to build,
  test (Vulkan validation layers), package (system Qt, ~3 MB) and deploy over
  SSH. Adapted from the vrdashboard template.
- test/render/: render test for the OpenGL and Vulkan backends, driven like
  OverlayController (option OVRAS_BUILD_TESTS).
- RPATH only $ORIGIN, without the build machine's link directories.
- Ignore dist/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 13:50:57 +02:00

184 lines
6.3 KiB
C++

// 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 <QQmlComponent>
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickRenderControl>
#include <QQuickWindow>
#include <QtTest>
#include <memory>
#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<const vr::VRVulkanTextureData_t*>(
texture.handle );
QVERIFY( vk->m_nImage != 0 && vk->m_pDevice && vk->m_pQueue
&& vk->m_pInstance );
QCOMPARE( vk->m_nWidth, static_cast<uint32_t>( 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<QQuickWindow>( &renderControl );
window->setGeometry( 0, 0, size.width(), size.height() );
QQmlComponent component( &engine );
component.loadFromModule( "RenderTest", "TwoTone" );
root.reset( qobject_cast<QQuickItem*>( 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<QQuickWindow> window;
std::unique_ptr<QQuickItem> root;
QString error;
};
std::unique_ptr<RenderBackend> m_backend;
};
QTEST_MAIN( RenderTest )
#include "tst_render.moc"