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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b22c35ae29
commit
bde675f1b8
@@ -0,0 +1,35 @@
|
||||
# Render test for src/render/ (see tst_render.cpp). Runs without SteamVR or a
|
||||
# headset; skips itself without a GPU context. One CTest entry per graphics
|
||||
# API, because the API is process-wide.
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Test)
|
||||
|
||||
qt_add_executable(tst_render
|
||||
tst_render.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/render/RenderBackend.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/render/OpenGLRenderBackend.cpp
|
||||
)
|
||||
target_include_directories(tst_render PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/third-party/openvr/headers
|
||||
)
|
||||
# Only openvr.h's types are used: nothing links against libopenvr_api.
|
||||
target_link_libraries(tst_render PRIVATE Qt::Gui Qt::Quick Qt::Qml Qt6::Test)
|
||||
if (QT_FEATURE_vulkan)
|
||||
target_sources(tst_render PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src/render/VulkanRenderBackend.cpp
|
||||
)
|
||||
target_compile_definitions(tst_render PRIVATE OVRAS_HAS_VULKAN=1)
|
||||
else ()
|
||||
target_compile_definitions(tst_render PRIVATE OVRAS_HAS_VULKAN=0)
|
||||
endif ()
|
||||
qt_add_qml_module(tst_render
|
||||
URI RenderTest
|
||||
VERSION 1.0
|
||||
QML_FILES TwoTone.qml
|
||||
)
|
||||
|
||||
add_test(NAME render_opengl COMMAND tst_render)
|
||||
set_tests_properties(render_opengl PROPERTIES ENVIRONMENT "OVRAS_TEST_API=opengl")
|
||||
add_test(NAME render_vulkan COMMAND tst_render)
|
||||
set_tests_properties(render_vulkan PROPERTIES ENVIRONMENT "OVRAS_TEST_API=vulkan")
|
||||
@@ -0,0 +1,7 @@
|
||||
import QtQuick
|
||||
|
||||
// Top half red, bottom half blue: lets the render test check content and orientation.
|
||||
Item {
|
||||
Rectangle { width: parent.width; height: parent.height / 2; color: "#ff0000" }
|
||||
Rectangle { y: parent.height / 2; width: parent.width; height: parent.height / 2; color: "#0000ff" }
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// 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"
|
||||
Reference in New Issue
Block a user