Initial FrameAssistant baseline from the vrdashboard template

Template stripped to the overlay platform (examples, demo UI and Frame
test package removed), renamed to FrameAssistant, and working rules for
the project recorded in CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 00:06:58 +02:00
co-authored by Claude Opus 5.5
commit 4a5b09053e
63 changed files with 5193 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
#include <QQmlEngine>
#include <QQuickWindow>
#include <QtTest>
#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<const vr::VRVulkanTextureData_t*>(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<RenderBackend> m_backend;
};
QTEST_MAIN(RenderTest)
#include "tst_render.moc"