Files
DeeJanuz--frametop/screens/vr.cpp
T
DeeJanuzandClaude Opus 5.5 d439bc3f25 Frametop: a multi-screen desktop and universal 3D mouse for the Steam Frame
Several KDE Plasma screens floating in SteamVR, each a real monitor of any
resolution and shape, shown by our own compositor (ft-screens), with a
layout, wrist pinning, and visibility modes; a Bluetooth mouse that drives
all of SteamVR as a room-anchored 3D pointer (input relay, ft-pointer
helper, ft_pointer SteamVR driver); two settings apps; and Bluetooth LE
fixes. Installs on the headset with ./install.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 16:14:13 -06:00

1276 lines
59 KiB
C++

// The OpenVR side of ft-screens: one overlay per screen, client DMA-BUFs imported with
// IVRIPCResourceManagerClient::ImportDmabuf (no copy, no size limit), panel mouse events
// turned into ft_events for the compositor, and the panels' own handling:
// - a grab bar under each screen: press it with any laser (a controller, or the 3D
// mouse's virtual controller) and the screen follows that device rigidly until the
// release, so the 3D mouse's tilt (right button while dragging) turns it; scrolling
// while dragging pushes it away or pulls it closer (along the line from the head).
// - a curve button next to the bar: bends the screen into a cylinder around you (its
// radius: your distance to it when pressed), or flat again.
// - a roll button next to that: drag it sideways like a knob to roll the screen about
// its centre (it snaps level within kRollSnap), or scroll on it for kRollStep steps.
// - a resize tab on the bottom right corner: drag it to set the width (the height
// follows the screen's resolution).
// The controls are translucent, like SteamVR's own, and brighten under a laser. They
// are invisible until a laser (a controller's, or the 3D mouse's) passes very close to
// one of them (UpdateControls).
// - pin to a wrist: while carrying a screen, sweep the laser (the line from the carrying
// device to the bar) across your other controller. A ring around each controller
// shows the target and a dot where the laser passes it; crossing the ring arms the pin
// (ring and bar turn blue), crossing it again disarms it. Let go while armed and the
// screen rides on that controller as it is then, at any size and distance, so you can
// arm it and then turn it the way you want before letting go. Grabbing a pinned
// screen keeps it armed for its wrist: move it, let go, and it's re-pinned there
// (sweep across the ring to take it off). A pinned screen shows only while you see
// its front, within the wrist angle (and fades out over the last kFade degrees).
// - visibility modes: always (the hide hotkey toggles), only with the SteamVR dashboard
// open, while you look at a chosen controller (the wrist gesture), or toggle only
// (hidden until the hotkey shows them). While visible the screens keep SteamVR's laser
// mouse on (VROverlayFlags_MakeOverlaysInteractiveIfVisible), so controllers can use
// them with the dashboard closed; hidden, VR games get their triggers back.
// OpenVR has no overlay-relative transforms here (openvr v2.15.6), so the bar, button,
// and handle are placed whenever their screen moves.
#include "vr.h"
#include <openvr.h>
#include <linux/input-event-codes.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <map>
#include <string>
#include <vector>
namespace {
using Mat = vr::HmdMatrix34_t;
Mat Identity() {
Mat m{};
m.m[0][0] = m.m[1][1] = m.m[2][2] = 1;
return m;
}
Mat Mul(const Mat &a, const Mat &b) {
Mat r{};
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
double v = j == 3 ? a.m[i][3] : 0;
for (int k = 0; k < 3; ++k) v += a.m[i][k] * b.m[k][j];
r.m[i][j] = float(v);
}
}
return r;
}
Mat Inverse(const Mat &a) { // rigid: R^T, -R^T t
Mat r{};
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j) r.m[i][j] = a.m[j][i];
for (int i = 0; i < 3; ++i) r.m[i][3] = -(r.m[i][0] * a.m[0][3] + r.m[i][1] * a.m[1][3] + r.m[i][2] * a.m[2][3]);
return r;
}
Mat Translation(double x, double y, double z) {
Mat m = Identity();
m.m[0][3] = float(x), m.m[1][3] = float(y), m.m[2][3] = float(z);
return m;
}
double Dot3(const double a[3], const double b[3]) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
void Column(const Mat &m, int c, double out[3]) { out[0] = m.m[0][c], out[1] = m.m[1][c], out[2] = m.m[2][c]; }
// A panel pose from a centre and the direction its front is seen from (yaw, pitch; see
// layout: the front faces back along that direction), turned by roll.
Mat PanelPose(double x, double y, double z, double yawDeg, double pitchDeg, double rollDeg) {
const double yw = yawDeg * M_PI / 180, pt = pitchDeg * M_PI / 180, rl = rollDeg * M_PI / 180;
const double fx = -std::sin(yw) * std::cos(pt), fy = std::sin(pt), fz = -std::cos(yw) * std::cos(pt);
const double Z[3] = {-fx, -fy, -fz}; // the front
double X[3] = {Z[2], 0, -Z[0]}; // up x Z: horizontal right
const double n = std::sqrt(X[0] * X[0] + X[2] * X[2]) + 1e-12;
X[0] /= n, X[2] /= n;
const double Y[3] = {Z[1] * X[2] - Z[2] * X[1], Z[2] * X[0] - Z[0] * X[2], Z[0] * X[1] - Z[1] * X[0]};
const double c = std::cos(rl), s = std::sin(rl);
Mat m{};
for (int i = 0; i < 3; ++i) {
m.m[i][0] = float(X[i] * c + Y[i] * s);
m.m[i][1] = float(Y[i] * c - X[i] * s);
m.m[i][2] = float(Z[i]);
}
m.m[0][3] = float(x), m.m[1][3] = float(y), m.m[2][3] = float(z);
return m;
}
// Device poses, read once per tick (ft_vr_poll) or per command.
vr::TrackedDevicePose_t g_poses[vr::k_unMaxTrackedDeviceCount];
void RefreshPoses() {
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, 0, g_poses,
vr::k_unMaxTrackedDeviceCount);
}
bool DevicePose(vr::TrackedDeviceIndex_t dev, Mat *out) {
if (dev >= vr::k_unMaxTrackedDeviceCount || !g_poses[dev].bPoseIsValid) return false;
*out = g_poses[dev].mDeviceToAbsoluteTracking;
return true;
}
bool IsHandController(vr::TrackedDeviceIndex_t i) {
if (vr::VRSystem()->GetTrackedDeviceClass(i) != vr::TrackedDeviceClass_Controller) return false;
char type[64] = "";
vr::VRSystem()->GetStringTrackedDeviceProperty(i, vr::Prop_ControllerType_String, type, sizeof type);
return std::strcmp(type, "ft_pointer") != 0; // not the 3D mouse's virtual controller
}
vr::TrackedDeviceIndex_t HandDevice(const char *hand) {
return vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(
std::strcmp(hand, "right") == 0 ? vr::TrackedControllerRole_RightHand : vr::TrackedControllerRole_LeftHand);
}
const char *HandName(vr::TrackedDeviceIndex_t i) {
switch (vr::VRSystem()->GetControllerRoleForTrackedDeviceIndex(i)) {
case vr::TrackedControllerRole_LeftHand: return "left";
case vr::TrackedControllerRole_RightHand: return "right";
default: return "none";
}
}
enum class Drag { None, Move, Resize, Roll };
enum class Mode { Always, Dashboard, Gesture, Toggle };
constexpr double kWristZone = 0.06; // the laser passing this close to a controller is on its wrist
constexpr double kWristLeave = 0.09; // ...and has left it beyond this (so it doesn't flicker)
constexpr double kDotRange = 0.35; // the guide dot shows while the laser is this close
constexpr double kMinWidth = 0.15;
constexpr double kFade = 10; // degrees over which a pinned screen fades out
constexpr double kRollSnap = 2.5; // degrees from level where rolling snaps level
constexpr double kRollStep = 5; // degrees per scroll notch on the roll button
constexpr float kChromeIdle = 0.55f; // the controls' opacity without a laser on them
constexpr long kControlsLinger = 35; // ticks (~0.4 s) the controls stay after a laser leaves
long g_tick = 0; // ft_vr_poll calls
constexpr vr::TrackedDeviceIndex_t kNone = vr::k_unTrackedDeviceIndexInvalid;
struct Screen {
vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid, bar = vr::k_ulOverlayHandleInvalid,
handle = vr::k_ulOverlayHandleInvalid, curveButton = vr::k_ulOverlayHandleInvalid,
rollButton = vr::k_ulOverlayHandleInvalid;
int width = 0, height = 0; // current buffer size (mouse scale)
double metres = 1;
double curve = 0; // cylinder radius in metres; 0 = flat
const void *shown = nullptr; // a frame arrived
bool visible = false; // shown in VR right now
float alpha = 1;
vr::TrackedDeviceIndex_t pinned = kNone; // riding on this controller
Mat pinRel = Identity(); // controller -> screen
Drag drag = Drag::None;
vr::TrackedDeviceIndex_t dragDevice = kNone;
Mat dragRel = Identity(); // device -> screen, while moving
double grabX = 0, grabY = 0; // resize: the grab point relative to the corner
Mat rollFrom = Identity(); // roll: the pose at the press (pinRel when pinned)
double rollAngle = 0; // roll: the laser's angle around the centre then
bool hover[4] = {}; // a laser is on the bar, curve, roll, resize control
float controls = 0; // the controls' fade, 0 (hidden) .. 1
bool controlsUp = false; // the controls' overlays are shown
long nearUntil = 0; // a laser was near the controls until this tick
vr::TrackedDeviceIndex_t pinTarget = kNone; // moving: rides on this controller when let go
vr::TrackedDeviceIndex_t onWrist = kNone; // moving: the laser is in this controller's ring
bool barLit = false;
double chrome = 0.3; // the bar's width; the other controls follow it (ChromeSize)
double grip = 0.04; // the corner tab's and the round buttons' size
double heightMetres() const { return width > 0 ? metres * height / width : metres * 9 / 16; }
std::array<vr::VROverlayHandle_t, 4> Controls() const { return {bar, curveButton, rollButton, handle}; }
std::array<vr::VROverlayHandle_t, 5> All() const { return {overlay, bar, curveButton, rollButton, handle}; }
};
std::map<int, Screen> g_screens;
std::map<const void *, vr::SharedTextureHandle_t> g_imports;
// Visibility (see the top). g_manual is the hide/show switch: in the always mode it hides
// the screens, in the others it shows them anyway.
Mode g_mode = Mode::Always;
bool g_manual = false;
double g_wristAngle = 60; // a pinned screen shows while you see its front within this
double g_gestureAngle = 20; // gesture: look within this of the controller
std::string g_gestureHand = "left";
// ---------------------------------------------------------------- chrome (bar, button, handle)
// The controls look like SteamVR's own: a light translucent pill for the bar, dark
// translucent discs with white glyphs for the buttons (the overlay alpha, kChromeIdle,
// dims them further until a laser is on them).
std::vector<uint8_t> PillTexture(int w, int h, uint8_t red, uint8_t green, uint8_t blue, uint8_t alpha) {
std::vector<uint8_t> px(size_t(w) * h * 4, 0);
const double r = h / 2.0 - 1;
for (int y = 0; y < h; ++y)
for (int x = 0; x < w; ++x) {
const double cx = std::clamp(double(x), r + 1, w - r - 1), cy = h / 2.0;
const double d = std::hypot(x + 0.5 - cx, y + 0.5 - cy);
uint8_t *p = &px[(size_t(y) * w + x) * 4];
p[0] = red, p[1] = green, p[2] = blue;
p[3] = uint8_t(std::clamp(r - d + 0.5, 0.0, 1.0) * alpha);
}
return px;
}
const std::vector<uint8_t> &BarTexture(bool lit) {
static const auto normal = PillTexture(256, 24, 235, 235, 235, 210), glow = PillTexture(256, 24, 90, 170, 255, 240);
return lit ? glow : normal;
}
// Paint a control: dark translucent inside `inside(u, v)`, white where `glyph(u, v)`, a
// faint light rim where `rim(u, v)`. u, v: -1..1 across the texture, v up.
template <typename In, typename Glyph, typename Rim>
std::vector<uint8_t> ControlTexture(int n, In inside, Glyph glyph, Rim rim) {
std::vector<uint8_t> px(size_t(n) * n * 4, 0);
const int ss = 3; // supersampling, for smooth edges
for (int y = 0; y < n; ++y)
for (int x = 0; x < n; ++x) {
double in = 0, g = 0, e = 0;
for (int j = 0; j < ss; ++j)
for (int i = 0; i < ss; ++i) {
const double u = (x + (i + 0.5) / ss) / n * 2 - 1, v = 1 - (y + (j + 0.5) / ss) / n * 2;
if (!inside(u, v)) continue;
in += 1;
if (glyph(u, v)) g += 1;
else if (rim(u, v)) e += 1;
}
const double k = ss * ss;
in /= k, g /= k, e /= k;
uint8_t *p = &px[(size_t(y) * n + x) * 4];
const double bg = in - g - e; // dark part
const double a = bg * 0.72 + e * 0.6 + g * 1.0;
if (a <= 0) continue;
const double shade = (bg * 0.72 * 38 + e * 0.6 * 200 + g * 255) / a;
p[0] = p[1] = p[2] = uint8_t(std::clamp(shade, 0.0, 255.0));
p[3] = uint8_t(std::clamp(a * 255, 0.0, 255.0));
}
return px;
}
bool InDisc(double u, double v) { return u * u + v * v <= 1; }
bool DiscRim(double u, double v) { return u * u + v * v > 0.86 * 0.86; }
std::vector<uint8_t> CornerTexture(int n) {
// A quarter disc whose corner (the texture's top left) sits on the screen's bottom
// right corner, with two grip arcs: "drag this corner".
auto r = [](double u, double v) { return std::hypot(u + 1, v - 1) / 2; }; // 0..1 from the corner
return ControlTexture(
n, [&](double u, double v) { return r(u, v) <= 1; },
[&](double u, double v) {
const double d = r(u, v);
return std::fabs(d - 0.5) < 0.035 || std::fabs(d - 0.75) < 0.035;
},
[&](double u, double v) { return r(u, v) > 0.93; });
}
std::vector<uint8_t> CurveTexture(int n) {
// An arc: "curve this screen".
return ControlTexture(
n, InDisc,
[](double u, double v) { return std::fabs(std::hypot(u, -v - 1.9) - 1.7) < 0.11 && std::fabs(u) < 0.6; },
DiscRim);
}
std::vector<uint8_t> RollTexture(int n) {
// A circular arrow, counterclockwise: "roll this screen".
return ControlTexture(
n, InDisc,
[](double u, double v) {
const double r = std::hypot(u, v);
double ang = std::atan2(v, u) * 180 / M_PI;
if (ang < 0) ang += 360;
if (std::fabs(r - 0.48) < 0.085 && ang >= 100) return true; // the arc, 100..360 degrees
// The head at 0 degrees, pointing up (the way the arc turns there).
const double hx = u - 0.48, hy = v + 0.02;
return hy >= 0 && hy <= 0.3 && std::fabs(hx) <= 0.24 * (1 - hy / 0.3);
},
DiscRim);
}
vr::VROverlayHandle_t MakeChrome(const char *key, const char *name, const std::vector<uint8_t> &px, int w, int h) {
vr::VROverlayHandle_t o = vr::k_ulOverlayHandleInvalid;
if (vr::VROverlay()->CreateOverlay(key, name, &o) != vr::VROverlayError_None) return o;
vr::VROverlay()->SetOverlayRaw(o, const_cast<uint8_t *>(px.data()), uint32_t(w), uint32_t(h), 4);
vr::VROverlay()->SetOverlayInputMethod(o, vr::VROverlayInputMethod_Mouse);
vr::VROverlay()->SetOverlaySortOrder(o, 10);
return o;
}
void LightBar(Screen &s, bool lit) {
if (s.barLit == lit) return;
s.barLit = lit;
const auto &px = BarTexture(lit);
vr::VROverlay()->SetOverlayRaw(s.bar, const_cast<uint8_t *>(px.data()), 256, 24, 4);
}
// ---------------------------------------------------------------- wrist guides
// While a screen is carried, each other controller gets a ring (its wrist zone, facing
// you) and a dot where the laser passes closest to it. Blue: armed / in the ring.
std::vector<uint8_t> DiscTexture(int n, double stroke, uint8_t red, uint8_t green, uint8_t blue, uint8_t fill,
uint8_t rimShade) {
std::vector<uint8_t> px(size_t(n) * n * 4, 0);
const double c = n / 2.0, r = n / 2.0 - 1;
for (int y = 0; y < n; ++y)
for (int x = 0; x < n; ++x) {
const double d = std::hypot(x + 0.5 - c, y + 0.5 - c);
const double a = std::clamp(r - d + 0.5, 0.0, 1.0);
uint8_t *p = &px[(size_t(y) * n + x) * 4];
const bool rim = d > r - stroke;
const bool edge = d > r - 2 || (rim && d < r - stroke + 2); // a dark line each side of the rim
p[0] = edge ? rimShade : red, p[1] = edge ? rimShade : green, p[2] = edge ? rimShade : blue;
p[3] = uint8_t(a * (rim ? 235 : fill));
}
return px;
}
const std::vector<uint8_t> &RingTexture(bool lit) {
static const auto normal = DiscTexture(128, 9, 240, 240, 240, 40, 60),
glow = DiscTexture(128, 12, 90, 170, 255, 110, 30);
return lit ? glow : normal;
}
const std::vector<uint8_t> &DotTexture(bool lit) {
static const auto normal = DiscTexture(32, 16, 250, 250, 250, 250, 50),
glow = DiscTexture(32, 16, 90, 170, 255, 250, 30);
return lit ? glow : normal;
}
struct GuidePart {
vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid;
int lit = -1; // the texture on it (-1: none yet)
bool shown = false;
void Show(bool on) {
if (on == shown || overlay == vr::k_ulOverlayHandleInvalid) return;
shown = on;
if (on) vr::VROverlay()->ShowOverlay(overlay);
else vr::VROverlay()->HideOverlay(overlay);
}
void Light(bool on, const std::vector<uint8_t> &px, int n) {
if (int(on) == lit) return;
lit = on;
vr::VROverlay()->SetOverlayRaw(overlay, const_cast<uint8_t *>(px.data()), uint32_t(n), uint32_t(n), 4);
}
};
struct Guide { GuidePart ring, dot; };
std::map<vr::TrackedDeviceIndex_t, Guide> g_guides;
Guide &GuideFor(vr::TrackedDeviceIndex_t dev) {
auto it = g_guides.find(dev);
if (it != g_guides.end()) return it->second;
Guide &g = g_guides[dev];
char key[64];
std::snprintf(key, sizeof key, "frametop.guide.%u.ring", dev);
if (vr::VROverlay()->CreateOverlay(key, "Wrist pin target", &g.ring.overlay) == vr::VROverlayError_None) {
vr::VROverlay()->SetOverlayWidthInMeters(g.ring.overlay, float(2 * kWristZone));
vr::VROverlay()->SetOverlaySortOrder(g.ring.overlay, 20);
}
std::snprintf(key, sizeof key, "frametop.guide.%u.dot", dev);
if (vr::VROverlay()->CreateOverlay(key, "Wrist pin laser", &g.dot.overlay) == vr::VROverlayError_None) {
vr::VROverlay()->SetOverlayWidthInMeters(g.dot.overlay, 0.022f);
vr::VROverlay()->SetOverlaySortOrder(g.dot.overlay, 21);
}
return g;
}
// A pose at pt facing the head (upright).
Mat FacingPose(const double pt[3], const Mat &head) {
double z[3] = {head.m[0][3] - pt[0], head.m[1][3] - pt[1], head.m[2][3] - pt[2]};
const double zl = std::sqrt(Dot3(z, z)) + 1e-9;
for (double &v : z) v /= zl;
double x[3] = {z[2], 0, -z[0]}; // up x z
const double xl = std::sqrt(x[0] * x[0] + x[2] * x[2]);
if (xl < 1e-6) x[0] = 1, x[2] = 0;
else x[0] /= xl, x[2] /= xl;
const double y[3] = {z[1] * x[2] - z[2] * x[1], z[2] * x[0] - z[0] * x[2], z[0] * x[1] - z[1] * x[0]};
Mat m{};
for (int i = 0; i < 3; ++i) m.m[i][0] = float(x[i]), m.m[i][1] = float(y[i]), m.m[i][2] = float(z[i]), m.m[i][3] = float(pt[i]);
return m;
}
void ApplyCurve(const Screen &s) {
// OpenVR's curvature: the fraction of a full cylinder the overlay's width covers.
const double c = s.curve > 0 ? std::clamp(s.metres / (2 * M_PI * s.curve), 0.0, 1.0) : 0.0;
vr::VROverlay()->SetOverlayCurvature(s.overlay, float(c));
}
// The screen's pose in the room (a pinned one: its controller's pose times pinRel).
bool ScreenPose(const Screen &s, Mat *out) {
if (s.pinned != kNone) {
Mat d;
if (!DevicePose(s.pinned, &d)) return false;
*out = Mul(d, s.pinRel);
return true;
}
vr::ETrackingUniverseOrigin origin;
return vr::VROverlay()->GetOverlayTransformAbsolute(s.overlay, &origin, out) == vr::VROverlayError_None;
}
// The controls' size from both the screen's width and its distance from the head (the
// geometric mean of 12% of the width and 10% of the distance), so a small screen near you
// gets small controls and a big or far one gets big ones, never under about 1.7 degrees.
void ChromeSize(Screen &s) {
Mat head, p;
double dist = 2;
if (DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head) && ScreenPose(s, &p)) {
const double d[3] = {p.m[0][3] - head.m[0][3], p.m[1][3] - head.m[1][3], p.m[2][3] - head.m[2][3]};
dist = std::max(0.2, std::sqrt(Dot3(d, d)));
}
const double least = dist * 0.03;
s.chrome = std::clamp(std::sqrt(0.012 * dist * s.metres), least, std::max(least, s.metres * 0.5));
s.grip = std::max(s.chrome * 0.13, dist * 0.018);
}
// A point on the screen's surface, u metres along it from the centre (along the arc when
// curved), v up, dz out of it, facing the way the surface does there. OpenVR curves a
// screen into a cylinder toward its front, with its centre line where the flat one was.
Mat OnSurface(const Screen &s, double u, double v, double dz) {
if (s.curve <= 0) return Translation(u, v, dz);
const double r = s.curve, a = u / r, c = std::cos(a), sn = std::sin(a);
Mat m = Identity();
m.m[0][0] = float(c), m.m[0][2] = float(-sn);
m.m[2][0] = float(sn), m.m[2][2] = float(c);
m.m[0][3] = float(r * sn - dz * sn), m.m[1][3] = float(v), m.m[2][3] = float(r - r * c + dz * c);
return m;
}
double BarY(const Screen &s) { return -(s.heightMetres() / 2 + s.chrome * 0.06 + s.chrome * 12 / 256); }
Mat BarOffset(const Screen &s) { return OnSurface(s, 0, BarY(s), 0.003); }
// Put the bar, the curve button, and the corner tab under the screen (same parent: the
// room or the controller), sized for the screen and its distance, and on its surface.
// Where each control sits, relative to the screen: bar, curve, roll, resize tab.
std::array<Mat, 4> ControlOffsets(const Screen &s) {
const double h = s.heightMetres(), bar = s.chrome, button = s.grip, gap = bar * 0.06;
return {BarOffset(s), OnSurface(s, bar / 2 + gap + button / 2, BarY(s), 0.003),
OnSurface(s, bar / 2 + gap * 2 + button * 1.5, BarY(s), 0.003),
// The tab's top left corner is the screen's bottom right corner.
OnSurface(s, s.metres / 2 + s.grip / 2, -(h / 2 + s.grip / 2), 0.003)};
}
void PlaceChrome(Screen &s) {
ChromeSize(s);
const double bar = s.chrome, button = s.grip;
const auto offsets = ControlOffsets(s);
vr::VROverlay()->SetOverlayWidthInMeters(s.bar, float(bar));
vr::VROverlay()->SetOverlayWidthInMeters(s.curveButton, float(button));
vr::VROverlay()->SetOverlayWidthInMeters(s.rollButton, float(button));
vr::VROverlay()->SetOverlayWidthInMeters(s.handle, float(s.grip));
// Curved, the bar bends with the screen's bottom edge.
vr::VROverlay()->SetOverlayCurvature(s.bar, s.curve > 0 ? float(std::min(1.0, bar / (2 * M_PI * s.curve))) : 0.f);
const std::pair<vr::VROverlayHandle_t, Mat> parts[] = {
{s.bar, offsets[0]}, {s.curveButton, offsets[1]}, {s.rollButton, offsets[2]}, {s.handle, offsets[3]}};
if (s.pinned != kNone) {
for (const auto &[o, off] : parts) {
const Mat m = Mul(s.pinRel, off);
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(o, s.pinned, &m);
}
return;
}
Mat p;
if (!ScreenPose(s, &p)) return;
for (const auto &[o, off] : parts) {
const Mat m = Mul(p, off);
vr::VROverlay()->SetOverlayTransformAbsolute(o, vr::TrackingUniverseStanding, &m);
}
}
// Screens you walk up to (or pinned ones you bring close) get their controls resized now
// and then, not every frame.
void RefreshChrome() {
static int tick = 0;
if (++tick % 45) return;
for (auto &[i, s] : g_screens) {
if (s.drag != Drag::None) continue;
const double before = s.chrome;
ChromeSize(s);
if (std::fabs(s.chrome - before) > before * 0.08) PlaceChrome(s);
else s.chrome = before;
}
}
void SetAbsolute(Screen &s, const Mat &pose) {
s.pinned = kNone;
vr::VROverlay()->SetOverlayTransformAbsolute(s.overlay, vr::TrackingUniverseStanding, &pose);
PlaceChrome(s);
}
void Pin(Screen &s, vr::TrackedDeviceIndex_t dev, const Mat &rel) {
s.pinned = dev;
s.pinRel = rel;
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(s.overlay, dev, &s.pinRel);
PlaceChrome(s);
}
void SetWidth(Screen &s, double metres) {
s.metres = std::clamp(metres, kMinWidth, 12.0);
vr::VROverlay()->SetOverlayWidthInMeters(s.overlay, float(s.metres));
ApplyCurve(s); // same radius, so the curvature fraction changes with the width
PlaceChrome(s);
}
// Curve toward the head: the radius is the head's distance to the screen now.
void ToggleCurve(Screen &s) {
Mat head, p;
if (s.curve > 0 || !DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head) || !ScreenPose(s, &p)) {
s.curve = 0;
} else {
const double dx = p.m[0][3] - head.m[0][3], dy = p.m[1][3] - head.m[1][3], dz = p.m[2][3] - head.m[2][3];
s.curve = std::max(0.5, std::sqrt(dx * dx + dy * dy + dz * dz));
}
ApplyCurve(s);
PlaceChrome(s);
}
// ---------------------------------------------------------------- visibility
// Angle in degrees between a panel's front and the direction from it to the head.
double FacingAngle(const Mat &p, const Mat &head) {
double n[3], to[3] = {head.m[0][3] - p.m[0][3], head.m[1][3] - p.m[1][3], head.m[2][3] - p.m[2][3]};
Column(p, 2, n);
const double len = std::sqrt(Dot3(to, to)) + 1e-9;
return std::acos(std::clamp(Dot3(n, to) / len, -1.0, 1.0)) * 180 / M_PI;
}
// The screens' shared visibility for the mode (before a pinned screen's own facing rule).
bool ModeVisible() {
switch (g_mode) {
case Mode::Always: return !g_manual;
case Mode::Toggle: return g_manual;
case Mode::Dashboard: return g_manual || vr::VROverlay()->IsDashboardVisible();
case Mode::Gesture: {
if (g_manual) return true;
// Looking at the chosen controller: it's within the gesture angle of the gaze.
Mat head, c;
if (!DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head) ||
!DevicePose(HandDevice(g_gestureHand.c_str()), &c))
return false;
double f[3], to[3] = {c.m[0][3] - head.m[0][3], c.m[1][3] - head.m[1][3], c.m[2][3] - head.m[2][3]};
Column(head, 2, f); // the head's +Z points backward
const double len = std::sqrt(Dot3(to, to)) + 1e-9;
return std::acos(std::clamp(-Dot3(f, to) / len, -1.0, 1.0)) * 180 / M_PI <= g_gestureAngle;
}
}
return true;
}
// The screen at its alpha; each control dimmer (kChromeIdle) unless a laser is on it or
// it's being dragged.
void ApplyAlpha(const Screen &s) {
vr::VROverlay()->SetOverlayAlpha(s.overlay, s.alpha);
const bool active[4] = {s.hover[0] || s.drag == Drag::Move, s.hover[1], s.hover[2] || s.drag == Drag::Roll,
s.hover[3] || s.drag == Drag::Resize};
const auto controls = s.Controls();
for (int k = 0; k < 4; ++k)
vr::VROverlay()->SetOverlayAlpha(controls[k], s.alpha * s.controls * (active[k] ? 1.f : kChromeIdle));
}
void SetVisible(Screen &s, bool visible, float alpha) {
if (visible && std::fabs(alpha - s.alpha) > 0.01f) {
s.alpha = alpha;
ApplyAlpha(s);
}
if (visible == s.visible) return;
s.visible = visible;
if (visible) {
vr::VROverlay()->ShowOverlay(s.overlay);
return;
}
// Hidden: the controls go at once (UpdateControls brings them back).
vr::VROverlay()->HideOverlay(s.overlay);
for (auto o : s.Controls()) vr::VROverlay()->HideOverlay(o);
s.controls = 0, s.controlsUp = false;
}
void UpdateVisibility() {
const bool shared = ModeVisible();
Mat head;
const bool haveHead = DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head);
for (auto &[i, s] : g_screens) {
bool visible = s.shown && (shared || s.drag != Drag::None);
float alpha = 1;
Mat p;
if (visible && s.pinned != kNone && s.drag == Drag::None && haveHead && ScreenPose(s, &p)) {
// A pinned screen shows while you see its front: fully inside the wrist angle,
// fading out over the last kFade degrees, gone beyond it (and from behind).
const double a = FacingAngle(p, head);
alpha = float(std::clamp((g_wristAngle - a) / kFade, 0.0, 1.0));
visible = alpha > 0.02f;
}
SetVisible(s, visible, alpha);
}
}
// The controls are invisible until a laser passes very close to one of them (within
// `reach`, about 1.5 times a button's size); they stay kControlsLinger ticks after it
// leaves, and while in use.
void UpdateControls() {
std::vector<Mat> lasers;
for (vr::TrackedDeviceIndex_t i = 1; i < vr::k_unMaxTrackedDeviceCount; ++i) {
Mat d;
if (vr::VRSystem()->GetTrackedDeviceClass(i) == vr::TrackedDeviceClass_Controller && DevicePose(i, &d))
lasers.push_back(d);
}
for (auto &[i, s] : g_screens) {
Mat p;
if (s.visible && ScreenPose(s, &p)) {
// Points along the bar and at each button and the tab; a laser passing within
// `reach` of one of them is close.
std::vector<Mat> spots;
const auto offsets = ControlOffsets(s);
for (double f : {-0.5, -0.25, 0.0, 0.25, 0.5})
spots.push_back(Mul(p, Mul(offsets[0], Translation(f * s.chrome, 0, 0))));
for (int k = 1; k < 4; ++k) spots.push_back(Mul(p, offsets[k]));
const double reach = std::max(s.grip * 1.5, s.chrome * 0.12);
for (const Mat &d : lasers) {
const double o[3] = {d.m[0][3], d.m[1][3], d.m[2][3]}, dir[3] = {-d.m[0][2], -d.m[1][2], -d.m[2][2]};
bool close = false;
for (const Mat &c : spots) {
const double v[3] = {c.m[0][3] - o[0], c.m[1][3] - o[1], c.m[2][3] - o[2]};
const double t = Dot3(v, dir);
if (t <= 0) continue;
const double q[3] = {v[0] - dir[0] * t, v[1] - dir[1] * t, v[2] - dir[2] * t};
if (Dot3(q, q) <= reach * reach) close = true;
}
if (close) {
s.nearUntil = g_tick + kControlsLinger;
break;
}
}
}
const bool inUse = s.drag != Drag::None || s.hover[0] || s.hover[1] || s.hover[2] || s.hover[3];
const bool want = s.visible && (inUse || g_tick < s.nearUntil);
const float before = s.controls;
s.controls = std::clamp(s.controls + (want ? 0.2f : -0.1f), 0.f, 1.f);
if (s.controls == before) continue;
if (s.controls > 0 && !s.controlsUp) {
for (auto o : s.Controls()) vr::VROverlay()->ShowOverlay(o);
s.controlsUp = true;
}
ApplyAlpha(s);
if (s.controls == 0 && s.controlsUp) {
for (auto o : s.Controls()) vr::VROverlay()->HideOverlay(o);
s.controlsUp = false;
}
}
}
// ---------------------------------------------------------------- moving, resizing, pinning
// Where a device's ray meets the screen's plane, in the screen's x (right) and y (up),
// metres from its centre.
bool RayOnPlane(const Mat &p, const Mat &d, double *x, double *y) {
const double o[3] = {d.m[0][3], d.m[1][3], d.m[2][3]}, dir[3] = {-d.m[0][2], -d.m[1][2], -d.m[2][2]};
const double c[3] = {p.m[0][3], p.m[1][3], p.m[2][3]};
double n[3], ax[3], ay[3];
Column(p, 2, n), Column(p, 0, ax), Column(p, 1, ay);
const double denom = Dot3(dir, n);
if (std::fabs(denom) < 1e-4) return false;
const double co[3] = {c[0] - o[0], c[1] - o[1], c[2] - o[2]};
const double t = Dot3(co, n) / denom;
if (t <= 0) return false;
const double rel[3] = {o[0] + dir[0] * t - c[0], o[1] + dir[1] * t - c[1], o[2] + dir[2] * t - c[2]};
*x = Dot3(rel, ax), *y = Dot3(rel, ay);
return true;
}
bool RayOnScreen(const Screen &s, const Mat &d, double *x, double *y) {
Mat p;
return ScreenPose(s, &p) && RayOnPlane(p, d, x, y);
}
// Roll: a rotation about the screen's own front axis (counterclockwise as you see it).
Mat RollZ(double rad) {
Mat m = Identity();
m.m[0][0] = m.m[1][1] = float(std::cos(rad));
m.m[1][0] = float(std::sin(rad)), m.m[0][1] = float(-std::sin(rad));
return m;
}
// Roll the screen to `rad` from its pose at the press, snapping level within kRollSnap.
void ApplyRoll(Screen &s, double rad) {
const bool pinned = s.pinned != kNone;
Mat c = Identity();
if (pinned && !DevicePose(s.pinned, &c)) return;
const Mat base = pinned ? Mul(c, s.rollFrom) : s.rollFrom;
const Mat p = Mul(base, RollZ(rad));
const double tilt = std::asin(std::clamp(double(p.m[1][0]), -1.0, 1.0)); // the right edge's slope
if (std::fabs(tilt) < kRollSnap * M_PI / 180) rad -= tilt;
if (pinned) Pin(s, s.pinned, Mul(s.rollFrom, RollZ(rad)));
else SetAbsolute(s, Mul(base, RollZ(rad)));
}
// The laser's angle around the screen's centre, in the frame of its pose at the press.
bool RollLaserAngle(const Screen &s, const Mat &d, double *rad) {
Mat c = Identity();
if (s.pinned != kNone && !DevicePose(s.pinned, &c)) return false;
const Mat base = s.pinned != kNone ? Mul(c, s.rollFrom) : s.rollFrom;
double hx, hy;
if (!RayOnPlane(base, d, &hx, &hy)) return false;
*rad = std::atan2(hy, hx);
return true;
}
// The laser while moving a screen: from the carrying device to the bar.
void Laser(const Screen &s, const Mat &d, const Mat &p, double a[3], double b[3]) {
const Mat bar = Mul(p, BarOffset(s));
for (int k = 0; k < 3; ++k) a[k] = d.m[k][3], b[k] = bar.m[k][3];
}
// The point q on the segment a-b closest to pt, and its distance.
double SegmentClosest(const double pt[3], const double a[3], const double b[3], double q[3]) {
const double ab[3] = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}, ap[3] = {pt[0] - a[0], pt[1] - a[1], pt[2] - a[2]};
const double t = std::clamp(Dot3(ap, ab) / (Dot3(ab, ab) + 1e-12), 0.0, 1.0);
for (int k = 0; k < 3; ++k) q[k] = a[k] + ab[k] * t;
const double v[3] = {q[0] - pt[0], q[1] - pt[1], q[2] - pt[2]};
return std::sqrt(Dot3(v, v));
}
double LaserDistance(const Screen &s, const Mat &d, const Mat &p, vr::TrackedDeviceIndex_t dev, double q[3]) {
Mat c;
if (!DevicePose(dev, &c)) return 1e9;
double a[3], b[3];
Laser(s, d, p, a, b);
const double pt[3] = {c.m[0][3], c.m[1][3], c.m[2][3]};
return SegmentClosest(pt, a, b, q);
}
// The hand controller (not the carrying device) whose ring the laser is in, or kNone.
vr::TrackedDeviceIndex_t WristOnLaser(const Screen &s, const Mat &d, const Mat &p) {
for (vr::TrackedDeviceIndex_t i = 1; i < vr::k_unMaxTrackedDeviceCount; ++i) {
double q[3];
if (i != s.dragDevice && IsHandController(i) && LaserDistance(s, d, p, i, q) <= kWristZone) return i;
}
return kNone;
}
void StartDrag(Screen &s, Drag mode, vr::TrackedDeviceIndex_t dev) {
Mat d, p;
if (dev == kNone || !DevicePose(dev, &d) || !ScreenPose(s, &p)) return;
s.pinTarget = kNone;
if (s.pinned != kNone && mode == Drag::Move) {
// Carried freely; let go, it goes back on the same wrist (unless disarmed).
s.pinTarget = s.pinned;
SetAbsolute(s, p);
}
s.drag = mode;
s.dragDevice = dev;
s.dragRel = Mul(Inverse(d), p);
// Already in a ring when grabbed: that doesn't count as crossing it.
s.onWrist = mode == Drag::Move ? WristOnLaser(s, d, p) : kNone;
LightBar(s, s.pinTarget != kNone);
if (mode == Drag::Resize) {
double hx, hy;
if (RayOnScreen(s, d, &hx, &hy)) s.grabX = hx - s.metres / 2, s.grabY = hy + s.heightMetres() / 2;
else s.grabX = s.grabY = 0;
}
if (mode == Drag::Roll) {
s.rollFrom = s.pinned != kNone ? s.pinRel : p;
if (!RollLaserAngle(s, d, &s.rollAngle)) s.drag = Drag::None, s.dragDevice = kNone;
}
ApplyAlpha(s);
}
// Stop moving where it is (a command took over).
void EndDrag(Screen &s) {
s.drag = Drag::None;
s.dragDevice = kNone;
s.pinTarget = s.onWrist = kNone;
LightBar(s, false);
ApplyAlpha(s);
}
// Let go: pin to the armed wrist, as the screen is now.
void FinishDrag(Screen &s, int index) {
const bool moved = s.drag == Drag::Move;
const vr::TrackedDeviceIndex_t target = s.pinTarget;
EndDrag(s);
Mat c, p;
if (!moved) return;
if (target != kNone && DevicePose(target, &c) && ScreenPose(s, &p)) {
Pin(s, target, Mul(Inverse(c), p));
std::printf("screen %d: pinned to the %s controller\n", index + 1, HandName(target));
}
}
// A button release on any of our panels ends that device's drags (it may be over another
// screen by then).
void EndDragsBy(vr::TrackedDeviceIndex_t dev) {
for (auto &[index, s] : g_screens)
if (s.drag != Drag::None && s.dragDevice == dev) FinishDrag(s, index);
}
// While moving: the laser entering a controller's ring flips whether the screen pins to
// it when let go (so sweeping across arms it, sweeping back disarms it).
void CheckWristAim(Screen &s, const Mat &d, const Mat &p) {
double q[3];
if (s.onWrist != kNone && LaserDistance(s, d, p, s.onWrist, q) > kWristLeave) s.onWrist = kNone;
if (s.onWrist == kNone) {
s.onWrist = WristOnLaser(s, d, p);
if (s.onWrist != kNone) s.pinTarget = s.pinTarget == s.onWrist ? kNone : s.onWrist;
}
LightBar(s, s.pinTarget != kNone);
}
// Show the rings and dots for the screen being carried (hide them otherwise).
void UpdateGuides() {
const Screen *carried = nullptr;
Mat d, p, head;
for (auto &[i, s] : g_screens)
if (s.drag == Drag::Move && DevicePose(s.dragDevice, &d) && ScreenPose(s, &p)) {
carried = &s;
break;
}
if (!carried || !DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head)) {
for (auto &[dev, g] : g_guides) g.ring.Show(false), g.dot.Show(false);
return;
}
for (vr::TrackedDeviceIndex_t i = 1; i < vr::k_unMaxTrackedDeviceCount; ++i) {
Mat c;
const bool want = i != carried->dragDevice && IsHandController(i) && DevicePose(i, &c);
if (!want) {
auto it = g_guides.find(i);
if (it != g_guides.end()) it->second.ring.Show(false), it->second.dot.Show(false);
continue;
}
Guide &g = GuideFor(i);
const double pt[3] = {c.m[0][3], c.m[1][3], c.m[2][3]};
Mat m = FacingPose(pt, head);
vr::VROverlay()->SetOverlayTransformAbsolute(g.ring.overlay, vr::TrackingUniverseStanding, &m);
g.ring.Light(carried->pinTarget == i, RingTexture(carried->pinTarget == i), 128);
g.ring.Show(true);
double q[3];
const double dist = LaserDistance(*carried, d, p, i, q);
if (dist <= kDotRange) {
m = FacingPose(q, head);
vr::VROverlay()->SetOverlayTransformAbsolute(g.dot.overlay, vr::TrackingUniverseStanding, &m);
g.dot.Light(dist <= kWristZone, DotTexture(dist <= kWristZone), 32);
}
g.dot.Show(dist <= kDotRange);
}
}
void UpdateDrag(Screen &s, int index) {
Mat d;
if (!DevicePose(s.dragDevice, &d)) return;
if (s.drag == Drag::Move) {
const Mat p = Mul(d, s.dragRel);
SetAbsolute(s, p);
CheckWristAim(s, d, p);
return;
}
if (s.drag == Drag::Roll) {
// Like turning a knob: the screen turns as far as the laser has gone around its centre.
double a;
if (!RollLaserAngle(s, d, &a)) return;
ApplyRoll(s, std::remainder(a - s.rollAngle, 2 * M_PI));
return;
}
// Resize: the corner follows the ray along the screen's diagonal (so it shrinks and
// grows from any direction), keeping where on the handle it was grabbed.
double hx, hy;
if (!RayOnScreen(s, d, &hx, &hy)) return;
const double a = s.width > 0 ? double(s.height) / s.width : 9.0 / 16;
const double cx = hx - s.grabX, cy = hy - s.grabY; // where the corner should be
SetWidth(s, 2 * (cx - a * cy) / (1 + a * a));
}
// Scroll while moving: push the screen away (up) or pull it closer, along the line from
// the head (not from the carrying device: the 3D mouse's device sits just in front of the
// bar, below the screen's centre, so that line points mostly up).
void Push(Screen &s, double notches) {
Mat d, head;
if (!DevicePose(s.dragDevice, &d) || !DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head)) return;
Mat p = Mul(d, s.dragRel);
const double to[3] = {p.m[0][3] - head.m[0][3], p.m[1][3] - head.m[1][3], p.m[2][3] - head.m[2][3]};
const double len = std::sqrt(Dot3(to, to));
const double next = std::clamp(len * (1 + 0.08 * notches), 0.3, 10.0);
for (int k = 0; k < 3; ++k) p.m[k][3] = float(head.m[k][3] + to[k] / (len + 1e-9) * next);
s.dragRel = Mul(Inverse(d), p);
}
Screen *Find(int one_based) {
auto it = g_screens.find(one_based - 1);
return it == g_screens.end() ? nullptr : &it->second;
}
uint32_t LinuxButton(uint32_t vrButton) {
switch (vrButton) {
case vr::VRMouseButton_Right: return BTN_RIGHT;
case vr::VRMouseButton_Middle: return BTN_MIDDLE;
default: return BTN_LEFT;
}
}
const char *ModeName() {
switch (g_mode) {
case Mode::Dashboard: return "dashboard";
case Mode::Gesture: return "gesture";
case Mode::Toggle: return "toggle";
default: return "always";
}
}
} // namespace
extern "C" {
bool ft_vr_init(void) {
vr::EVRInitError err = vr::VRInitError_None;
vr::VR_Init(&err, vr::VRApplication_Overlay);
if (err != vr::VRInitError_None) {
std::fprintf(stderr, "openvr: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err));
return false;
}
if (!vr::VRIPCResourceManager()) {
std::fprintf(stderr, "openvr: no IVRIPCResourceManagerClient (SteamVR too old?)\n");
return false;
}
RefreshPoses();
return true;
}
void ft_vr_shutdown(void) {
for (auto &[i, s] : g_screens)
for (auto o : s.All()) vr::VROverlay()->DestroyOverlay(o);
for (auto &[dev, g] : g_guides)
for (auto o : {g.ring.overlay, g.dot.overlay}) vr::VROverlay()->DestroyOverlay(o);
for (auto &[k, h] : g_imports) vr::VRIPCResourceManager()->UnrefResource(h);
g_guides.clear();
g_screens.clear();
g_imports.clear();
vr::VR_Shutdown();
}
int ft_vr_modifiers(uint32_t format, uint64_t *out, int max) {
uint32_t n = uint32_t(max);
if (!vr::VRIPCResourceManager()->GetDmabufModifiers(vr::VRApplication_Overlay, format, &n, out)) return 0;
return int(n < uint32_t(max) ? n : uint32_t(max));
}
bool ft_vr_dashboard_visible(void) { return vr::VROverlay()->IsDashboardVisible(); }
void ft_vr_screen_create(int index, double metres, int count) {
Screen &s = g_screens[index];
s.metres = metres;
char key[64], name[64];
std::snprintf(key, sizeof key, "frametop.screen.%d", index + 1);
std::snprintf(name, sizeof name, "Screen %d", index + 1);
if (vr::VROverlay()->CreateOverlay(key, name, &s.overlay) != vr::VROverlayError_None) {
std::fprintf(stderr, "openvr: can't create overlay %s\n", key);
return;
}
vr::VROverlay()->SetOverlayWidthInMeters(s.overlay, float(metres));
vr::VROverlay()->SetOverlayInputMethod(s.overlay, vr::VROverlayInputMethod_Mouse);
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_IgnoreTextureAlpha, true);
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, true);
static const auto corner = CornerTexture(64);
static const auto curve = CurveTexture(64);
static const auto roll = RollTexture(64);
std::snprintf(key, sizeof key, "frametop.screen.%d.bar", index + 1);
std::snprintf(name, sizeof name, "Screen %d: move", index + 1);
s.bar = MakeChrome(key, name, BarTexture(false), 256, 24);
vr::VROverlay()->SetOverlayFlag(s.bar, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
std::snprintf(key, sizeof key, "frametop.screen.%d.curve", index + 1);
std::snprintf(name, sizeof name, "Screen %d: curve", index + 1);
s.curveButton = MakeChrome(key, name, curve, 64, 64);
std::snprintf(key, sizeof key, "frametop.screen.%d.roll", index + 1);
std::snprintf(name, sizeof name, "Screen %d: roll", index + 1);
s.rollButton = MakeChrome(key, name, roll, 64, 64);
vr::VROverlay()->SetOverlayFlag(s.rollButton, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
std::snprintf(key, sizeof key, "frametop.screen.%d.resize", index + 1);
std::snprintf(name, sizeof name, "Screen %d: resize", index + 1);
s.handle = MakeChrome(key, name, corner, 64, 64);
ApplyAlpha(s);
// Until the layout places it: 2 m ahead of the head, in a row, screen 1 on the left.
RefreshPoses();
Mat head;
if (!DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head)) head = Identity();
const double heading = std::atan2(head.m[0][2], head.m[2][2]) * 180 / M_PI;
const double yaw = heading + (double(count - 1) / 2 - index) * 35;
const double dx = -std::sin(yaw * M_PI / 180), dz = -std::cos(yaw * M_PI / 180);
SetAbsolute(s, PanelPose(head.m[0][3] + dx * 2, head.m[1][3], head.m[2][3] + dz * 2, yaw, 0, 0));
}
void ft_vr_screen_destroy(int index) {
auto it = g_screens.find(index);
if (it == g_screens.end()) return;
for (auto o : it->second.All()) vr::VROverlay()->DestroyOverlay(o);
g_screens.erase(it);
}
bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *b) {
auto sit = g_screens.find(index);
if (sit == g_screens.end()) return false;
Screen &s = sit->second;
auto it = g_imports.find(key);
if (it == g_imports.end()) {
vr::DmabufAttributes_t a{};
a.unWidth = uint32_t(b->width);
a.unHeight = uint32_t(b->height);
a.unDepth = a.unMipLevels = a.unArrayLayers = a.unSampleCount = 1;
a.unFormat = b->format;
a.ulModifier = b->modifier;
a.unPlaneCount = uint32_t(b->n_planes);
for (int i = 0; i < b->n_planes && i < int(vr::MaxDmabufPlaneCount); ++i) {
a.plane[i].unOffset = b->offset[i];
a.plane[i].unStride = b->stride[i];
a.plane[i].nFd = b->fd[i];
}
vr::SharedTextureHandle_t h = 0;
if (!vr::VRIPCResourceManager()->ImportDmabuf(vr::VRApplication_Overlay, &a, &h)) {
std::fprintf(stderr, "openvr: ImportDmabuf failed: %dx%d format 0x%x modifier 0x%llx\n", b->width,
b->height, b->format, (unsigned long long)b->modifier);
return false;
}
it = g_imports.emplace(key, h).first;
}
if (b->width != s.width || b->height != s.height) {
s.width = b->width, s.height = b->height;
vr::HmdVector2_t scale = {float(s.width), float(s.height)};
vr::VROverlay()->SetOverlayMouseScale(s.overlay, &scale);
PlaceChrome(s); // the height changed
std::printf("screen %d: %dx%d\n", index + 1, s.width, s.height);
}
vr::SharedTextureHandle_t handle = it->second;
vr::Texture_t tex = {&handle, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
vr::VROverlay()->SetOverlayTexture(s.overlay, &tex);
s.shown = key; // UpdateVisibility shows it on the next tick
return true;
}
void ft_vr_forget(const void *key) {
auto it = g_imports.find(key);
if (it == g_imports.end()) return;
vr::VRIPCResourceManager()->UnrefResource(it->second);
g_imports.erase(it);
}
void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
RefreshPoses();
for (auto &[index, s] : g_screens) {
vr::VREvent_t ev;
// The screen itself: input for KWin.
while (vr::VROverlay()->PollNextOverlayEvent(s.overlay, &ev, sizeof ev)) {
ft_event e{};
e.screen = index;
switch (ev.eventType) {
case vr::VREvent_MouseMove:
e.type = FT_MOTION;
e.x = ev.data.mouse.x;
e.y = s.height - ev.data.mouse.y; // OpenVR's mouse origin is bottom left
break;
case vr::VREvent_MouseButtonDown:
case vr::VREvent_MouseButtonUp:
if (ev.eventType == vr::VREvent_MouseButtonUp) EndDragsBy(ev.trackedDeviceIndex);
e.type = FT_BUTTON;
e.button = LinuxButton(ev.data.mouse.button);
e.pressed = ev.eventType == vr::VREvent_MouseButtonDown;
e.x = ev.data.mouse.x;
e.y = s.height - ev.data.mouse.y;
break;
case vr::VREvent_ScrollDiscrete:
e.type = FT_SCROLL;
e.dx = -ev.data.scroll.xdelta;
e.dy = -ev.data.scroll.ydelta;
break;
case vr::VREvent_FocusLeave:
e.type = FT_LEAVE;
break;
default:
continue;
}
handle(&e, data);
}
// The controls light up under a laser.
auto hover = [&](int k) {
const bool on = ev.eventType == vr::VREvent_MouseMove || ev.eventType == vr::VREvent_FocusEnter;
if (!on && ev.eventType != vr::VREvent_FocusLeave) return;
if (s.hover[k] != on) s.hover[k] = on, ApplyAlpha(s);
};
// The bar: move (and push/pull with the wheel while moving).
while (vr::VROverlay()->PollNextOverlayEvent(s.bar, &ev, sizeof ev)) {
hover(0);
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left)
StartDrag(s, Drag::Move, ev.trackedDeviceIndex);
else if (ev.eventType == vr::VREvent_MouseButtonUp)
EndDragsBy(ev.trackedDeviceIndex);
else if (ev.eventType == vr::VREvent_ScrollDiscrete && s.drag == Drag::Move)
Push(s, ev.data.scroll.ydelta);
}
// The corner: resize.
while (vr::VROverlay()->PollNextOverlayEvent(s.handle, &ev, sizeof ev)) {
hover(3);
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left)
StartDrag(s, Drag::Resize, ev.trackedDeviceIndex);
else if (ev.eventType == vr::VREvent_MouseButtonUp)
EndDragsBy(ev.trackedDeviceIndex);
}
// The curve button.
while (vr::VROverlay()->PollNextOverlayEvent(s.curveButton, &ev, sizeof ev)) {
hover(1);
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left)
ToggleCurve(s);
else if (ev.eventType == vr::VREvent_MouseButtonUp)
EndDragsBy(ev.trackedDeviceIndex);
}
// The roll button: drag around like a knob, or scroll.
while (vr::VROverlay()->PollNextOverlayEvent(s.rollButton, &ev, sizeof ev)) {
hover(2);
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left)
StartDrag(s, Drag::Roll, ev.trackedDeviceIndex);
else if (ev.eventType == vr::VREvent_MouseButtonUp)
EndDragsBy(ev.trackedDeviceIndex);
else if (ev.eventType == vr::VREvent_ScrollDiscrete && s.drag == Drag::None) {
Mat p;
if (!ScreenPose(s, &p)) continue;
s.rollFrom = s.pinned != kNone ? s.pinRel : p;
ApplyRoll(s, ev.data.scroll.ydelta * kRollStep * M_PI / 180);
}
}
if (s.drag != Drag::None) UpdateDrag(s, index);
}
RefreshChrome();
vr::VREvent_t ev;
while (vr::VRSystem()->PollNextEvent(&ev, sizeof ev)) {
if (ev.eventType == vr::VREvent_Quit) {
ft_event e{};
e.type = FT_QUIT;
handle(&e, data);
}
// A carrying controller that goes away drops its screen.
if (ev.eventType == vr::VREvent_TrackedDeviceDeactivated) EndDragsBy(ev.trackedDeviceIndex);
}
++g_tick;
UpdateVisibility();
UpdateControls();
UpdateGuides();
}
// Control commands (datagrams on @ft_screens, replies to the sender):
// place <screen> <x> <y> <z> <yaw> <pitch> <roll> centre (standing universe) and facing
// width <screen> <metres>
// curve <screen> <radius> cylinder radius in metres; 0 = flat
// curve <screen> on|off on: the radius is the head's distance to it now -> "ok <radius>"
// pin <screen|all> <left|right> [12 numbers] pin to that hand's controller: as it is now,
// or at the given controller->screen transform (rows of a 3x4)
// unpin <screen|all>
// get <screen> -> "ok x y z xx xy xz yx yy yz zx zy zz width height curve hand
// [12 numbers: controller->screen, when pinned]"
// screens -> "ok <count> <index>:<pixels w>x<h>:<metres> ..."
// head -> "ok x y z yaw"
// visibility always|dashboard|gesture|toggle
// wrist <degrees> a pinned screen shows while you see its front within this
// gesture <left|right> <degrees> the gesture mode: look within this of that controller
// hide | show | toggle the manual switch (see g_manual)
// state -> "ok <mode> <manual 0|1> <wrist deg> <gesture hand> <gesture deg>"
// (size <screen> <w> <h> and key <code> <value> are handled in compositor.c.) Screens are
// numbered from 1 here, like everywhere the user sees them.
void ft_vr_command(const char *cmd, char *reply, int size) {
RefreshPoses();
int n;
double x, y, z, yaw, pitch, roll, w;
char word[16], hand[16];
float r[12];
auto each = [&](const char *which, auto fn) -> bool { // "all" or a screen number
if (std::strcmp(which, "all") == 0) {
for (auto &[i, s] : g_screens) fn(s);
return true;
}
Screen *s = Find(std::atoi(which));
if (s) fn(*s);
return s != nullptr;
};
if (std::sscanf(cmd, "place %d %lf %lf %lf %lf %lf %lf", &n, &x, &y, &z, &yaw, &pitch, &roll) == 7) {
Screen *s = Find(n);
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
EndDrag(*s);
SetAbsolute(*s, PanelPose(x, y, z, yaw, pitch, roll));
std::snprintf(reply, size, "ok");
} else if (std::sscanf(cmd, "width %d %lf", &n, &w) == 2) {
Screen *s = Find(n);
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
SetWidth(*s, w);
std::snprintf(reply, size, "ok");
} else if (std::sscanf(cmd, "curve %d %7s", &n, word) == 2 && (!std::strcmp(word, "on") || !std::strcmp(word, "off"))) {
Screen *s = Find(n);
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
if ((s->curve > 0) != (word[1] == 'n')) ToggleCurve(*s);
std::snprintf(reply, size, "ok %.3f", s->curve);
} else if (std::sscanf(cmd, "curve %d %lf", &n, &w) == 2) {
Screen *s = Find(n);
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
s->curve = w > 0 ? std::max(0.5, w) : 0;
ApplyCurve(*s);
PlaceChrome(*s);
std::snprintf(reply, size, "ok");
} else if (const int got = std::sscanf(cmd, "pin %15s %15s %f %f %f %f %f %f %f %f %f %f %f %f", word, hand,
&r[0], &r[1], &r[2], &r[3], &r[4], &r[5], &r[6], &r[7], &r[8], &r[9],
&r[10], &r[11]);
got >= 2) {
const vr::TrackedDeviceIndex_t dev = HandDevice(hand);
Mat c;
if (dev == kNone || !DevicePose(dev, &c))
return (void)std::snprintf(reply, size, "error no %s controller tracked", hand);
Mat rel = Identity();
for (int k = 0; k < 12; ++k) rel.m[k / 4][k % 4] = r[k];
const bool found = each(word, [&](Screen &s) {
Mat p;
EndDrag(s);
if (got == 14) Pin(s, dev, rel);
else if (ScreenPose(s, &p)) Pin(s, dev, Mul(Inverse(c), p));
});
std::snprintf(reply, size, found ? "ok" : "error no such screen");
} else if (std::sscanf(cmd, "unpin %15s", word) == 1) {
const bool found = each(word, [&](Screen &s) {
Mat p;
if (s.pinned != kNone && ScreenPose(s, &p)) SetAbsolute(s, p);
});
std::snprintf(reply, size, found ? "ok" : "error no such screen");
} else if (std::sscanf(cmd, "get %d", &n) == 1) {
Screen *s = Find(n);
Mat m;
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
if (!ScreenPose(*s, &m)) return (void)std::snprintf(reply, size, "error screen %d has no pose", n);
int len = std::snprintf(reply, size,
"ok %.4f %.4f %.4f %.5f %.5f %.5f %.5f %.5f %.5f %.5f %.5f %.5f %.4f %.4f %.3f %s",
m.m[0][3], m.m[1][3], m.m[2][3], m.m[0][0], m.m[1][0], m.m[2][0], m.m[0][1], m.m[1][1],
m.m[2][1], m.m[0][2], m.m[1][2], m.m[2][2], s->metres, s->heightMetres(), s->curve,
s->pinned == kNone ? "none" : HandName(s->pinned));
if (s->pinned != kNone)
for (int k = 0; k < 12 && len < size; ++k)
len += std::snprintf(reply + len, size - len, " %.5f", s->pinRel.m[k / 4][k % 4]);
} else if (std::strncmp(cmd, "screens", 7) == 0) {
int len = std::snprintf(reply, size, "ok %zu", g_screens.size());
for (auto &[i, s] : g_screens)
if (len < size)
len += std::snprintf(reply + len, size - len, " %d:%dx%d:%.3f", i + 1, s.width, s.height, s.metres);
} else if (std::strncmp(cmd, "head", 4) == 0) {
Mat m;
if (!DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &m))
return (void)std::snprintf(reply, size, "error no head pose (headset off?)");
std::snprintf(reply, size, "ok %.4f %.4f %.4f %.2f", m.m[0][3], m.m[1][3], m.m[2][3],
std::atan2(m.m[0][2], m.m[2][2]) * 180 / M_PI);
} else if (std::sscanf(cmd, "visibility %15s", word) == 1) {
const std::string m = word;
if (m == "always") g_mode = Mode::Always;
else if (m == "dashboard") g_mode = Mode::Dashboard;
else if (m == "gesture") g_mode = Mode::Gesture;
else if (m == "toggle") g_mode = Mode::Toggle;
else return (void)std::snprintf(reply, size, "error modes: always dashboard gesture toggle");
g_manual = false;
std::snprintf(reply, size, "ok %s", ModeName());
} else if (std::sscanf(cmd, "wrist %lf", &w) == 1) {
g_wristAngle = std::clamp(w, 10.0, 180.0);
std::snprintf(reply, size, "ok");
} else if (std::sscanf(cmd, "gesture %15s %lf", hand, &w) == 2) {
g_gestureHand = std::strcmp(hand, "right") == 0 ? "right" : "left";
g_gestureAngle = std::clamp(w, 5.0, 90.0);
std::snprintf(reply, size, "ok");
} else if (!std::strncmp(cmd, "hide", 4) || !std::strncmp(cmd, "show", 4) || !std::strncmp(cmd, "toggle", 6)) {
const bool shownNow = g_mode == Mode::Always ? !g_manual : g_manual;
const bool want = cmd[0] == 's' ? true : cmd[0] == 'h' ? false : !shownNow;
g_manual = g_mode == Mode::Always ? !want : want;
UpdateVisibility();
std::snprintf(reply, size, "ok %s", want ? "shown" : "hidden");
} else if (std::strncmp(cmd, "state", 5) == 0) {
std::snprintf(reply, size, "ok %s %d %.0f %s %.0f", ModeName(), g_manual ? 1 : 0, g_wristAngle,
g_gestureHand.c_str(), g_gestureAngle);
} else {
std::snprintf(reply, size, "error unknown command");
}
}
} // extern "C"