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:
@@ -0,0 +1,42 @@
|
||||
#include "vr/DashboardOverlay.h"
|
||||
|
||||
DashboardOverlay::DashboardOverlay(VrRuntime& runtime, QObject* parent)
|
||||
: Overlay(runtime, parent) {}
|
||||
|
||||
DashboardOverlay::~DashboardOverlay() { destroy(); }
|
||||
|
||||
void DashboardOverlay::destroyExtras() {
|
||||
// Explicitly: OpenVR doesn't document whether destroying the main overlay
|
||||
// takes the thumbnail with it.
|
||||
if (m_thumbnail != vr::k_ulOverlayHandleInvalid)
|
||||
vr::VROverlay()->DestroyOverlay(m_thumbnail);
|
||||
m_thumbnail = vr::k_ulOverlayHandleInvalid;
|
||||
}
|
||||
|
||||
bool DashboardOverlay::create(const QString& key, const QString& name,
|
||||
const QImage& thumbnail, QQuickWindow* window,
|
||||
QSize textureSize, QString* error) {
|
||||
vr::VROverlayHandle_t handle = vr::k_ulOverlayHandleInvalid;
|
||||
const vr::EVROverlayError err = vr::VROverlay()->CreateDashboardOverlay(
|
||||
key.toUtf8().constData(), name.toUtf8().constData(), &handle,
|
||||
&m_thumbnail);
|
||||
if (err != vr::VROverlayError_None) {
|
||||
*error = QStringLiteral("CreateDashboardOverlay(%1): %2")
|
||||
.arg(key, errorName(err));
|
||||
if (err == vr::VROverlayError_KeyInUse)
|
||||
*error += QStringLiteral(
|
||||
" - already running? (Use --app-key for a second instance.)");
|
||||
return false;
|
||||
}
|
||||
setUp(handle, window, textureSize);
|
||||
// The dashboard sizes its tabs itself; this only matters once floated.
|
||||
vr::VROverlay()->SetOverlayWidthInMeters(handle, 1.6f);
|
||||
|
||||
if (!thumbnail.isNull()) {
|
||||
QImage rgba = thumbnail.convertToFormat(QImage::Format_RGBA8888);
|
||||
vr::VROverlay()->SetOverlayRaw(m_thumbnail, rgba.bits(),
|
||||
uint32_t(rgba.width()),
|
||||
uint32_t(rgba.height()), 4);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QString>
|
||||
|
||||
#include "vr/Overlay.h"
|
||||
|
||||
// A SteamVR dashboard tab (CreateDashboardOverlay) with its thumbnail icon.
|
||||
//
|
||||
// The user can float/pin it from the SteamVR dashboard (SteamVR then owns its
|
||||
// placement and keeps it visible in games), so rendering follows the
|
||||
// overlay's own visibility (VREvent_OverlayShown/Hidden), NOT whether the
|
||||
// dashboard is open.
|
||||
class DashboardOverlay : public Overlay {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DashboardOverlay(VrRuntime& runtime, QObject* parent = nullptr);
|
||||
~DashboardOverlay() override;
|
||||
|
||||
// `key` must be unique within SteamVR; `window` receives the input.
|
||||
bool create(const QString& key, const QString& name, const QImage& thumbnail,
|
||||
QQuickWindow* window, QSize textureSize, QString* error);
|
||||
|
||||
protected:
|
||||
void destroyExtras() override;
|
||||
|
||||
private:
|
||||
vr::VROverlayHandle_t m_thumbnail = vr::k_ulOverlayHandleInvalid;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "vr/Overlay.h"
|
||||
|
||||
#include "input/OverlayInputBridge.h"
|
||||
#include "vr/VrRuntime.h"
|
||||
|
||||
Overlay::Overlay(VrRuntime& runtime, QObject* parent)
|
||||
: QObject(parent), m_runtime(runtime) {}
|
||||
|
||||
Overlay::~Overlay() {
|
||||
// Subclasses call destroy() in their destructors (destroyExtras() is
|
||||
// virtual); this is the safety net for the main overlay.
|
||||
destroy();
|
||||
}
|
||||
|
||||
QString Overlay::errorName(vr::EVROverlayError err) {
|
||||
return QString::fromUtf8(vr::VROverlay()->GetOverlayErrorNameFromEnum(err));
|
||||
}
|
||||
|
||||
void Overlay::setUp(vr::VROverlayHandle_t handle, QQuickWindow* window,
|
||||
QSize textureSize) {
|
||||
m_handle = handle;
|
||||
vr::IVROverlay* overlay = vr::VROverlay();
|
||||
const vr::HmdVector2_t mouseScale{
|
||||
{float(textureSize.width()), float(textureSize.height())}};
|
||||
overlay->SetOverlayMouseScale(m_handle, &mouseScale);
|
||||
overlay->SetOverlayInputMethod(m_handle, vr::VROverlayInputMethod_Mouse);
|
||||
overlay->SetOverlayFlag(m_handle, vr::VROverlayFlags_SendVRSmoothScrollEvents,
|
||||
true);
|
||||
overlay->SetOverlayFlag(m_handle, vr::VROverlayFlags_EnableClickStabilization,
|
||||
true);
|
||||
|
||||
m_input = std::make_unique<OverlayInputBridge>(window, textureSize, m_handle);
|
||||
connect(m_input.get(), &OverlayInputBridge::focusEntered, this,
|
||||
&Overlay::pointerEntered);
|
||||
m_runtime.registerOverlay(
|
||||
m_handle, [this](const vr::VREvent_t& event) { handleEvent(event); });
|
||||
}
|
||||
|
||||
void Overlay::destroy() {
|
||||
if (m_handle == vr::k_ulOverlayHandleInvalid) return;
|
||||
m_runtime.unregisterOverlay(m_handle);
|
||||
// After VR_Shutdown there is no IVROverlay any more (nothing to destroy).
|
||||
if (vr::VROverlay()) {
|
||||
destroyExtras();
|
||||
vr::VROverlay()->DestroyOverlay(m_handle);
|
||||
}
|
||||
m_handle = vr::k_ulOverlayHandleInvalid;
|
||||
}
|
||||
|
||||
bool Overlay::isVisible() const {
|
||||
return m_handle != vr::k_ulOverlayHandleInvalid &&
|
||||
vr::VROverlay()->IsOverlayVisible(m_handle);
|
||||
}
|
||||
|
||||
void Overlay::submitFrame(const vr::Texture_t& texture,
|
||||
const vr::VRTextureBounds_t& bounds) {
|
||||
if (m_handle == vr::k_ulOverlayHandleInvalid) return;
|
||||
vr::VROverlay()->SetOverlayTextureBounds(m_handle, &bounds);
|
||||
vr::Texture_t tex = texture;
|
||||
const vr::EVROverlayError err =
|
||||
vr::VROverlay()->SetOverlayTexture(m_handle, &tex);
|
||||
if (err != vr::VROverlayError_None)
|
||||
qWarning("SetOverlayTexture failed: %s", qPrintable(errorName(err)));
|
||||
}
|
||||
|
||||
void Overlay::handleEvent(const vr::VREvent_t& event) {
|
||||
switch (event.eventType) {
|
||||
case vr::VREvent_OverlayShown:
|
||||
emit visibilityChanged(true);
|
||||
return;
|
||||
case vr::VREvent_OverlayHidden:
|
||||
emit visibilityChanged(false);
|
||||
return;
|
||||
default:
|
||||
m_input->handleEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <openvr.h>
|
||||
|
||||
#include <QObject>
|
||||
#include <QSize>
|
||||
#include <memory>
|
||||
|
||||
class OverlayInputBridge;
|
||||
class QQuickWindow;
|
||||
class VrRuntime;
|
||||
|
||||
// Base of the SteamVR overlays a surface can show: creation is up to the
|
||||
// subclass (DashboardOverlay; later e.g. a world overlay), everything else is
|
||||
// shared: laser/scroll/keyboard input into the surface's window, visibility
|
||||
// events and frame submission.
|
||||
class Overlay : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
~Overlay() override;
|
||||
|
||||
vr::VROverlayHandle_t handle() const { return m_handle; }
|
||||
OverlayInputBridge* inputBridge() const { return m_input.get(); }
|
||||
bool isVisible() const;
|
||||
|
||||
void submitFrame(const vr::Texture_t& texture,
|
||||
const vr::VRTextureBounds_t& bounds);
|
||||
|
||||
signals:
|
||||
void visibilityChanged(bool visible);
|
||||
// The laser entered the overlay.
|
||||
void pointerEntered();
|
||||
|
||||
protected:
|
||||
explicit Overlay(VrRuntime& runtime, QObject* parent = nullptr);
|
||||
|
||||
// For subclasses, once they created the overlay as `handle`: mouse input
|
||||
// scaled to `textureSize`, smooth scrolling, event routing to `window`.
|
||||
void setUp(vr::VROverlayHandle_t handle, QQuickWindow* window,
|
||||
QSize textureSize);
|
||||
// Subclasses destroy extra overlays (e.g. thumbnails) here; called while
|
||||
// the runtime is still up, before the main overlay is destroyed.
|
||||
virtual void destroyExtras() {}
|
||||
void destroy();
|
||||
|
||||
static QString errorName(vr::EVROverlayError err);
|
||||
|
||||
private:
|
||||
void handleEvent(const vr::VREvent_t& event);
|
||||
|
||||
VrRuntime& m_runtime;
|
||||
vr::VROverlayHandle_t m_handle = vr::k_ulOverlayHandleInvalid;
|
||||
std::unique_ptr<OverlayInputBridge> m_input;
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "VrRuntime.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QFile>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace {
|
||||
|
||||
// OpenVR returns extension lists as one space-separated string.
|
||||
template <typename Query>
|
||||
QByteArrayList queryExtensionList(Query query) {
|
||||
const uint32_t size = query(nullptr, 0);
|
||||
if (size == 0) return {};
|
||||
QByteArray buffer(int(size), '\0');
|
||||
query(buffer.data(), size);
|
||||
QByteArrayList result;
|
||||
for (const QByteArray& ext : buffer.left(buffer.indexOf('\0')).split(' ')) {
|
||||
if (!ext.isEmpty()) result.append(ext);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
VrRuntime::VrRuntime(QObject* parent) : QObject(parent) {
|
||||
// Events are cheap to poll; 90 Hz keeps laser hover/drag responsive.
|
||||
m_pollTimer.setInterval(11);
|
||||
m_pollTimer.setTimerType(Qt::PreciseTimer);
|
||||
connect(&m_pollTimer, &QTimer::timeout, this, &VrRuntime::pollEvents);
|
||||
}
|
||||
|
||||
VrRuntime::~VrRuntime() { shutdown(); }
|
||||
|
||||
bool VrRuntime::initialize(QString* error) {
|
||||
vr::EVRInitError initError = vr::VRInitError_None;
|
||||
vr::VR_Init(&initError, vr::VRApplication_Overlay);
|
||||
if (initError != vr::VRInitError_None) {
|
||||
const char* reason = vr::VR_GetVRInitErrorAsEnglishDescription(initError);
|
||||
*error =
|
||||
QStringLiteral("VR_Init failed: %1").arg(QString::fromUtf8(reason));
|
||||
return false;
|
||||
}
|
||||
if (!vr::VROverlay()) {
|
||||
*error = QStringLiteral("IVROverlay interface unavailable");
|
||||
vr::VR_Shutdown();
|
||||
return false;
|
||||
}
|
||||
m_initialized = true;
|
||||
m_pollTimer.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
void VrRuntime::shutdown() {
|
||||
if (!m_initialized) return;
|
||||
m_pollTimer.stop();
|
||||
m_overlays.clear();
|
||||
vr::VR_Shutdown();
|
||||
m_initialized = false;
|
||||
}
|
||||
|
||||
void VrRuntime::registerOverlay(vr::VROverlayHandle_t handle,
|
||||
OverlayEventHandler handler) {
|
||||
m_overlays.insert(handle, std::move(handler));
|
||||
}
|
||||
|
||||
void VrRuntime::unregisterOverlay(vr::VROverlayHandle_t handle) {
|
||||
m_overlays.remove(handle);
|
||||
}
|
||||
|
||||
void VrRuntime::pollEvents() {
|
||||
vr::VREvent_t event{};
|
||||
|
||||
while (vr::VRSystem()->PollNextEvent(&event, sizeof(event))) {
|
||||
switch (event.eventType) {
|
||||
case vr::VREvent_Quit:
|
||||
// SteamVR is shutting down; overlays must exit promptly
|
||||
// or they get killed.
|
||||
vr::VRSystem()->AcknowledgeQuit_Exiting();
|
||||
emit quitRequested();
|
||||
break;
|
||||
case vr::VREvent_DashboardActivated:
|
||||
emit dashboardActivated();
|
||||
break;
|
||||
case vr::VREvent_DashboardDeactivated:
|
||||
emit dashboardDeactivated();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
emit systemEvent(event);
|
||||
}
|
||||
|
||||
// Copy: a handler may (un)register overlays.
|
||||
const auto overlays = m_overlays;
|
||||
for (auto it = overlays.cbegin(); it != overlays.cend(); ++it) {
|
||||
while (
|
||||
vr::VROverlay()->PollNextOverlayEvent(it.key(), &event, sizeof(event)))
|
||||
it.value()(event);
|
||||
}
|
||||
}
|
||||
|
||||
RenderInterop VrRuntime::renderInterop() const {
|
||||
RenderInterop interop;
|
||||
interop.vulkanInstanceExtensions = [] {
|
||||
vr::IVRCompositor* compositor = vr::VRCompositor();
|
||||
if (!compositor) {
|
||||
qWarning(
|
||||
"IVRCompositor unavailable: cannot query required Vulkan instance "
|
||||
"extensions");
|
||||
return QByteArrayList();
|
||||
}
|
||||
return queryExtensionList([&](char* buf, uint32_t size) {
|
||||
return compositor->GetVulkanInstanceExtensionsRequired(buf, size);
|
||||
});
|
||||
};
|
||||
interop.vulkanDeviceExtensions = [](VkPhysicalDevice_T* device) {
|
||||
vr::IVRCompositor* compositor = vr::VRCompositor();
|
||||
if (!compositor) return QByteArrayList();
|
||||
return queryExtensionList([&](char* buf, uint32_t size) {
|
||||
return compositor->GetVulkanDeviceExtensionsRequired(device, buf, size);
|
||||
});
|
||||
};
|
||||
interop.vulkanOutputDevice = [](VkInstance_T* instance) {
|
||||
uint64_t device = 0;
|
||||
vr::VRSystem()->GetOutputDevice(&device, vr::TextureType_Vulkan, instance);
|
||||
return reinterpret_cast<VkPhysicalDevice_T*>(device);
|
||||
};
|
||||
return interop;
|
||||
}
|
||||
|
||||
bool VrRuntime::installManifest(const QString& manifestPath,
|
||||
QStringList* appKeys, QString* error) {
|
||||
QFile file(manifestPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
*error = QStringLiteral("Cannot read %1").arg(manifestPath);
|
||||
return false;
|
||||
}
|
||||
appKeys->clear();
|
||||
const QJsonArray applications = QJsonDocument::fromJson(file.readAll())
|
||||
.object()
|
||||
.value("applications")
|
||||
.toArray();
|
||||
for (const QJsonValue& app : applications)
|
||||
appKeys->append(app.toObject().value("app_key").toString());
|
||||
|
||||
vr::IVRApplications* apps = vr::VRApplications();
|
||||
const QByteArray path = manifestPath.toUtf8();
|
||||
auto errorName = [&](vr::EVRApplicationError err) {
|
||||
return QString::fromUtf8(apps->GetApplicationsErrorNameFromEnum(err));
|
||||
};
|
||||
// Re-register (path or content may have changed)
|
||||
apps->RemoveApplicationManifest(path.constData());
|
||||
|
||||
vr::EVRApplicationError err =
|
||||
apps->AddApplicationManifest(path.constData(), false);
|
||||
if (err != vr::VRApplicationError_None) {
|
||||
*error = QStringLiteral("AddApplicationManifest(%1): %2")
|
||||
.arg(manifestPath, errorName(err));
|
||||
return false;
|
||||
}
|
||||
qInfo().noquote() << "Added manifest" << manifestPath;
|
||||
// SteamVR silently skips entries it can't use (e.g. no binary path for this
|
||||
// platform); report each key's state so such problems are visible.
|
||||
for (const QString& key : std::as_const(*appKeys)) {
|
||||
const QByteArray k = key.toUtf8();
|
||||
const bool known = apps->IsApplicationInstalled(k.constData());
|
||||
qInfo().noquote() << " " << key
|
||||
<< (known ? "known to SteamVR" : "NOT known to SteamVR");
|
||||
err = apps->SetApplicationAutoLaunch(k.constData(), true);
|
||||
if (err != vr::VRApplicationError_None) {
|
||||
*error = QStringLiteral("SetApplicationAutoLaunch(%1): %2")
|
||||
.arg(key, errorName(err));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VrRuntime::launchDashboardOverlay(const QString& appKey, QString* error) {
|
||||
vr::IVRApplications* apps = vr::VRApplications();
|
||||
const vr::EVRApplicationError err =
|
||||
apps->LaunchDashboardOverlay(appKey.toUtf8().constData());
|
||||
if (err != vr::VRApplicationError_None) {
|
||||
*error = QStringLiteral("LaunchDashboardOverlay(%1): %2")
|
||||
.arg(appKey, QString::fromUtf8(
|
||||
apps->GetApplicationsErrorNameFromEnum(err)));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VrRuntime::uninstallManifest(const QString& manifestPath, QString* error) {
|
||||
vr::IVRApplications* apps = vr::VRApplications();
|
||||
const vr::EVRApplicationError err =
|
||||
apps->RemoveApplicationManifest(manifestPath.toUtf8().constData());
|
||||
if (err != vr::VRApplicationError_None) {
|
||||
*error = QString::fromUtf8(apps->GetApplicationsErrorNameFromEnum(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <openvr.h>
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <functional>
|
||||
|
||||
#include "render/RenderBackend.h"
|
||||
|
||||
// Owns the OpenVR connection (as VRApplication_Overlay) and pumps events on the
|
||||
// Qt event loop. Overlay-specific events are routed to the handler registered
|
||||
// for the overlay handle; global events are emitted as signals.
|
||||
class VrRuntime : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
using OverlayEventHandler = std::function<void(const vr::VREvent_t&)>;
|
||||
|
||||
explicit VrRuntime(QObject* parent = nullptr);
|
||||
~VrRuntime() override;
|
||||
|
||||
bool initialize(QString* error);
|
||||
void shutdown();
|
||||
|
||||
void registerOverlay(vr::VROverlayHandle_t handle,
|
||||
OverlayEventHandler handler);
|
||||
void unregisterOverlay(vr::VROverlayHandle_t handle);
|
||||
|
||||
// What SteamVR requires from the graphics device (Vulkan extensions,
|
||||
// HMD GPU).
|
||||
RenderInterop renderInterop() const;
|
||||
|
||||
// Registers the .vrmanifest with SteamVR and enables auto-launch for
|
||||
// every app key in it (returned in *appKeys). Requires an initialized
|
||||
// VR connection (any app type).
|
||||
static bool installManifest(const QString& manifestPath, QStringList* appKeys,
|
||||
QString* error);
|
||||
static bool uninstallManifest(const QString& manifestPath, QString* error);
|
||||
// Starts a registered dashboard overlay through SteamVR (with SteamVR's
|
||||
// environment).
|
||||
static bool launchDashboardOverlay(const QString& appKey, QString* error);
|
||||
|
||||
signals:
|
||||
void quitRequested();
|
||||
void dashboardActivated();
|
||||
void dashboardDeactivated();
|
||||
// Every global (non-overlay) event, for features that need more than
|
||||
// the above.
|
||||
void systemEvent(const vr::VREvent_t& event);
|
||||
|
||||
private:
|
||||
void pollEvents();
|
||||
|
||||
QTimer m_pollTimer;
|
||||
QHash<vr::VROverlayHandle_t, OverlayEventHandler> m_overlays;
|
||||
bool m_initialized = false;
|
||||
};
|
||||
Reference in New Issue
Block a user