Files
MichaelScottsman--LiquidAss…/native/glassd/src/glassd.cpp
T
MichaelScottsman af4e73c1d3 Stereo glass: each eye sees the room behind the window glass from where it is; tuner fits its popup; the sidebar keeps native glass without the laser; Home leaves on the route exit
Stereo (glassd cap stereo, flag glassStereo, on)
- The window glass was one image both eyes saw on the glass plane, so the room behind it read as a picture
  pasted on the glass. glassd now renders each eye's view (SteamVR's eye-to-head offsets, refreshed every
  5 s): every region of a stereo surface's texture is a pair, left eye's copy then right eye's, and the
  scene graph's panel spans the pair with stereoscopy Parallel.
- Measured on the device with a lab probe (lgs_sg test.stereoProbe): SteamVR splits a Parallel panel at
  the middle of the panel's own uv rect, not at the texture's halves; hence the pair layout, and stereo only
  where the scene graph shows a surface whole (main, SteamVR's panels). The bar, the frame menu and popups
  show crops and stay mono. A slab clipped sideways is left out while it is.
- GPU about 2.9 -> 3.5 ms a frame.

Sidebar without the laser
- frame.menu is no longer laser-only: SteamVR shows it in gamepad mode (laser mouse off) and beside its own
  pages; keyed on the gamepad flag its native glass went off and the CSS glass showed. Verified with the
  flag forced to gamepad on Home and in VR Settings: the cover stays.

Home -> Library
- The shell stamps the leaving Home view with the moment it starts leaving (only on a way out of Home, kept
  across Steam's /library redirect); Home fades on the route exit and the reporter lets its plates go on
  that clock. Measured: all Home discs reported leaving in the first report, gone with the page.
- Plates whose element leaves the DOM are reported leaving from the mutation itself (no measuring), not
  after a frame-driven tick the new route's render starves.

Glass tuner
- The popup fits Steam's 600 px bar popup cap (rows 76 px: label and value on one line over a 40 px slider);
  open()/close() for tests.
2026-10-09 14:51:41 -04:00

3510 lines
174 KiB
C++

// glassd: the native glass renderer of Glass Shell (see docs/NATIVE.md).
//
// For every Steam surface listed in /dev/shm/lgs/glassd.json it publishes one
// SteamVR overlay "glassd.<name>" whose texture is a triple-buffered dmabuf
// (GBM + surfaceless EGL + GLES 3). The texture holds the surface's window
// glass (the "cover": frosted and lensed room, tint, sheen, rims) on top and an
// atlas of Liquid Glass slabs for popped elements underneath. The room comes
// from an equirectangular room map built from the passthrough feed
// (/dev/video99) with every Steam surface masked out. The daemon's scene graph
// shows these overlays by key; glassd never calls ShowOverlay.
//
// Writes the layout (UV rects, backdropScale) plus fps / gpu_ms / health to
// /dev/shm/lgs/glassd-out.json. Renders only while the dashboard is visible.
//
// Exit codes: 0 stopped (signal, --timeout, parent gone), 1 setup error,
// 2 bad arguments, 75 temporary (another glassd holds the lock, SteamVR is not
// running or went away, overlays could not be kept alive): not a crash.
#include <openvr.h>
#include <dirent.h>
#include <signal.h>
#include <sys/file.h>
#include <sys/inotify.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <deque>
#include <fstream>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "atlas.h"
#include "feed.h"
#include "gfx.h"
#include "json.h"
#include "room.h"
#include "vmath.h"
// Motion tokens (P5, generated by docs/phase2/research/springs.py): the phase
// ramps below use them, so glassd's materialize equals the token table (MO-5).
#include "motion_tokens.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
static_assert(sizeof(vr::SharedTextureHandle_t) == sizeof(uint64_t), "shared handle size");
static const std::pair<const char *, const char *> kShaders[] = {
#include "shaders.inc"
};
namespace {
constexpr int kExitTemp = 75; // EX_TEMPFAIL: not a crash, try again later
constexpr const char *kMainKey = "valve.steam.gamepadui.main";
constexpr const char *kDefaultPrefix = "glassd.";
constexpr const char *kShmDir = "/dev/shm/lgs";
constexpr int kMaxShapes = 8;
constexpr int kMaxPlates = 32; // per surface (docs/phase2/contracts/glassd.md §1.3)
constexpr int kMaxListedDrops = 32; // dropped plate ids listed in glassd-out.json (all are counted)
constexpr float kPlateLiftM = 0.002f; // R1: optics of a plate over a cover sit this far in front of it
constexpr int kMaxCasters = 16; // slab shadows and holes on one piece of glass (glass.frag uShBox[])
constexpr int kMaxMasks = 24; // feed mask quads (room_update.frag uQO[])
constexpr int kMaxExtraMasks = 8; // per surface and at the top level
constexpr uint64_t kMs = 1000000ull;
constexpr uint64_t kGraceNs = 400 * kMs; // no room integration this long after a dashboard change
constexpr uint64_t kGhostNs = 600 * kMs; // a removed slab's cell stays drawn (and reserved) this long
constexpr uint64_t kMaskPeriodNs = 250 * kMs; // feed masks are rebuilt (with fresh geometry) this often
const v3 kTestEye{0, 1.1f, 0}; // --test-backdrop: head position (standing space)
std::atomic<bool> g_quit{false};
std::atomic<bool> g_dumpRequest{false}, g_loseRequest{false};
std::atomic<int> g_exitCode{0};
void onSignal(int s) {
if (s == SIGUSR1) g_dumpRequest = true;
else if (s == SIGUSR2) g_loseRequest = true;
else g_quit = true;
}
void quitWith(int code) {
g_exitCode = code;
g_quit = true;
}
// ------------------------------------------------------------------ options
struct Options {
std::string spec = "/dev/shm/lgs/glassd.json";
std::string out; // default: /dev/shm/lgs/glassd-out.json, or /dev/shm/lgs/<prefix>out.json for another prefix
std::string dumpDir, dumpRoom, shaderDir;
std::string keyPrefix = kDefaultPrefix;
std::string feedDev = "/dev/video99";
bool once = false, force = false, demo = false, noFeed = false, noMask = false, orphanOk = false;
bool mono = false; // --mono: one copy per texture, from the head (no per-eye stereo)
float zone[3] = {0.15f, 0.08f, 0.25f}; // main window mask zone: side, top, bottom (m)
int debugView = 0;
int dashOverride = -1; // --dash on|off (tests): -1 = ask SteamVR
float scale = 0.75f; // backdropScale
float fps = 72.f; // max render rate while visible
float feedHz = 60.f; // room map updates while visible (the feed streams): what is behind the glass moves at this rate
float idleHz = 0.2f; // one-frame feed shots per second otherwise (0 = none)
float warmup = 3.f; // seconds of feed before --once / --dump output
float margin = 0.06f; // metres around Steam surfaces in the feed mask
float roomDepth = 2.2f; // radius of the room sphere
float predictMs = 20.f; // head pose prediction for rendering
float timeout = 0.f; // exit after N seconds (0 = run until stopped)
int feedDownsample = 4;
// Verification without a wearer (README "Verifying without the headset"):
// a procedural room instead of the feed, a fixed head, composite views.
int testPattern = -1; // --test-backdrop: 0 room, 1 stripes; -1 off
bool testHole = false; // room-hole: the room behind the UI stays unknown
float testHead[3] = {0, 0, 0}; // eye offset from the test head (m)
bool dumpView = false; // also write <name>-view.png (room + cover + slabs)
bool bench = false; // render every frame (GPU timing)
float phasePin = -1; // --phase M: every piece of glass at materialize progress M (dumps)
std::string selftest; // --selftest phase: offline checks, no SteamVR
int viewContent = 0; // --view-content hero: dump views draw a stand-in page (art + crops)
};
void usage() {
std::printf(
"glassd [options]\n"
" --spec FILE layer spec to follow (default /dev/shm/lgs/glassd.json; re-read on change)\n"
" --out FILE layout + stats output (default /dev/shm/lgs/glassd-out.json; with another\n"
" --key-prefix P: /dev/shm/lgs/<P>out.json)\n"
" --demo use a built-in spec (main window + 3 slabs) instead of --spec\n"
" --once integrate the feed for --warmup s, render one frame of every surface, exit\n"
" --dump DIR write every surface texture to DIR/<name>.png after one frame\n"
" --dump-room PATH write the filled room map PNG (and PATH-known.png); delete after looking\n"
" --force render even while the dashboard is hidden\n"
" --scale S backdropScale, glassd px per Steam px (default 0.75)\n"
" --fps N max render rate while visible (default 72)\n"
" --feed-hz N room map updates per second while the glass shows (default 36)\n"
" --idle-hz N one-frame feed shots per second otherwise (default 0.2, 0 = none)\n"
" --warmup S feed seconds before dumps (default 3)\n"
" --margin M feed mask margin around Steam surfaces in metres (default 0.06)\n"
" --zone S,T,B extra mask around the main window: side, top, bottom (default 0.15,0.08,0.25)\n"
" --no-mask debug: integrate the feed without masking the UI\n"
" --debug-view N debug shading: 1 backdrop, 2 light, 3 bezel, 4 room uv\n"
" --dash on|off|auto test override for SteamVR's dashboard visibility (default auto)\n"
" --key-prefix P overlay key prefix (default glassd.)\n"
" --shaders DIR load shaders from DIR instead of the built-in copies\n"
" --feed DEV passthrough feed device (default /dev/video99); --no-feed to skip it\n"
" --timeout S exit after S seconds\n"
" --orphan-ok keep running when the parent process exits (detached lab runs)\n"
" --test-backdrop P no camera: a procedural room (room, room-hole, stripes), a fixed head and\n"
" fixed geometry (the spec's quad, else the window 1.43 m ahead); dumps are safe to keep\n"
" --test-head X,Y,Z with --test-backdrop: move the eye by X,Y,Z metres (off-axis views)\n"
" --dump-view with --dump: also write DIR/<name>-view.png, the room with the cover and slabs on it\n"
" --bench render every frame at --fps (GPU timing in the status line)\n"
" --phase M pin every cover and slab at materialize progress M (0..1), for dumps\n"
" --selftest phase check the materialize ramps against motion_tokens.h (no SteamVR), exit 0 = pass\n"
" --view-content C with --dump-view: none (default) or hero, a stand-in page in front of the\n"
" cover (opaque art with the popped rects cut out, and the popped crops);\n"
" hero-art: the same without the cut-outs (GL-2's reference for a hole's dL)\n"
"SIGUSR1 dumps the room map and surfaces (to the --dump paths, else /tmp/lgs/glassd-dump/).\n"
"SIGUSR2 (test aid) treats every overlay as lost: destroys and recreates it.\n"
"Exit codes: 0 stopped, 1 setup error, 2 bad arguments, 75 temporary (lock held, SteamVR absent or gone).\n");
}
bool parseArgs(int argc, char **argv, Options &o) {
for (int i = 1; i < argc; i++) {
std::string a = argv[i];
auto next = [&](std::string &v) {
if (i + 1 >= argc) return false;
v = argv[++i];
return true;
};
auto nextf = [&](float &v) {
if (i + 1 >= argc) return false;
v = std::strtof(argv[++i], nullptr);
return true;
};
bool ok = true;
if (a == "--spec") ok = next(o.spec);
else if (a == "--out") ok = next(o.out);
else if (a == "--dump") ok = next(o.dumpDir);
else if (a == "--dump-room") ok = next(o.dumpRoom);
else if (a == "--shaders") ok = next(o.shaderDir);
else if (a == "--key-prefix") ok = next(o.keyPrefix);
else if (a == "--feed") ok = next(o.feedDev);
else if (a == "--no-feed") o.noFeed = true;
else if (a == "--mono") o.mono = true;
else if (a == "--no-mask") o.noMask = true;
else if (a == "--orphan-ok") o.orphanOk = true;
else if (a == "--debug-view") { std::string v; ok = next(v); o.debugView = std::atoi(v.c_str()); }
else if (a == "--dash") {
std::string v;
ok = next(v) && (v == "on" || v == "off" || v == "auto");
o.dashOverride = v == "on" ? 1 : v == "off" ? 0 : -1;
}
else if (a == "--zone") {
std::string v;
ok = next(v) && std::sscanf(v.c_str(), "%f,%f,%f", &o.zone[0], &o.zone[1], &o.zone[2]) == 3;
}
else if (a == "--test-backdrop") {
std::string v;
ok = next(v) && (v == "room" || v == "room-hole" || v == "stripes");
o.testPattern = v == "stripes" ? 1 : 0;
o.testHole = v == "room-hole";
}
else if (a == "--test-head") {
std::string v;
ok = next(v) && std::sscanf(v.c_str(), "%f,%f,%f", &o.testHead[0], &o.testHead[1], &o.testHead[2]) == 3;
}
else if (a == "--dump-view") o.dumpView = true;
else if (a == "--bench") o.bench = true;
else if (a == "--phase") ok = nextf(o.phasePin);
else if (a == "--selftest") ok = next(o.selftest) && o.selftest == "phase";
else if (a == "--view-content") {
std::string v;
ok = next(v) && (v == "none" || v == "hero" || v == "hero-art");
o.viewContent = v == "hero" ? 1 : v == "hero-art" ? 2 : 0;
}
else if (a == "--once") o.once = true;
else if (a == "--force") o.force = true;
else if (a == "--demo") o.demo = true;
else if (a == "--scale") ok = nextf(o.scale);
else if (a == "--fps") ok = nextf(o.fps);
else if (a == "--feed-hz") ok = nextf(o.feedHz);
else if (a == "--idle-hz") ok = nextf(o.idleHz);
else if (a == "--warmup") ok = nextf(o.warmup);
else if (a == "--margin") ok = nextf(o.margin);
else if (a == "--room-depth") ok = nextf(o.roomDepth);
else if (a == "--predict-ms") ok = nextf(o.predictMs);
else if (a == "--timeout") ok = nextf(o.timeout);
else if (a == "-h" || a == "--help") { usage(); std::exit(0); }
else { std::fprintf(stderr, "unknown option %s\n", a.c_str()); return false; }
if (!ok) { std::fprintf(stderr, "%s needs a value\n", a.c_str()); return false; }
}
if (o.keyPrefix.empty()) { std::fprintf(stderr, "--key-prefix must not be empty\n"); return false; }
// A test instance (another key prefix) never shares the daemon's output file.
if (o.out.empty())
o.out = o.keyPrefix == kDefaultPrefix ? std::string(kShmDir) + "/glassd-out.json"
: std::string(kShmDir) + "/" + o.keyPrefix + "out.json";
if (o.testPattern >= 0) o.noFeed = true; // the test room replaces the camera
o.scale = std::clamp(o.scale, 0.25f, 1.5f);
o.fps = std::clamp(o.fps, 1.f, 144.f);
o.feedHz = std::clamp(o.feedHz, 1.f, 60.f);
o.idleHz = std::clamp(o.idleHz, 0.f, 60.f);
return true;
}
// ---------------------------------------------------------------- materials
// Material v2 (docs/phase2/glassd-material.md). Every edge cue is physical:
// no term draws a line of constant width.
struct Material {
float thetaRef; // thickness the preset is tuned for (see thickness())
float tintA; // mix toward a neutral of the band luminance
float frost; // interior frost, room-map mip (one map texel = 0.35 deg)
float edgeFrost; // frost where the bezel bends the most (the bend must show)
float edgeClear; // how much the lens band drops tint and tone compression (a polished bevel)
float bezelM; // squircle bezel width, metres (clamped to the shape's half size and corner radius)
float thick; // slab thickness / bezel width as the light sees it: where the highlight sits, how thin it is
float lensThick; // the same for refraction: deeper, so the bend spans the lens band
float lensDeg; // ray deviation at the rim, degrees (lensing strength)
float disp; // dispersion: R bends (1 - disp) x, B (1 + disp) x
float bandK; // share of the room's luminance swing kept (rest pulled to the band)
float dim; // backdrop dimming (clear glass over media)
float spec; // key specular (crescent on the light-facing bezel)
float gloss; // key specular exponent
float fill; // opposite transmitted highlight, fraction of the key
float fres; // Fresnel reflection of the room on grazing bezel
float sheen; // top-down sheen
float dark; // darkened band inside the edge (E4)
float darkW; // its width, metres
float occ; // occlusion inside the lower edge (E5)
float shadow; // contact shadow outside a cover's shapes (needs room in the texture)
float slabShadow; // shadow a slab of this material casts on the cover behind it
};
// Presets at dial 0.5 and their reference thickness (DESIGN2 §6.1).
Material materialPreset(const std::string &name) {
// thRef tint frost edgeF eClr bezel thick lensT lens disp bandK dim spec gloss fill fres sheen dark darkW occ shadow slabSh
if (name == "liquid") return {0.25f, 0.14f, 1.3f, 0.3f, 0.60f, 0.016f, 0.50f, 1.5f, 7.0f, 0.020f, 0.60f, 0.00f, 0.90f, 40.f, 0.07f, 0.20f, 0.8f, 0.05f, 0.006f, 0.06f, 0.10f, 0.22f};
if (name == "clear") return {0.25f, 0.06f, 0.6f, 0.1f, 0.60f, 0.016f, 0.50f, 1.6f, 9.0f, 0.025f, 0.75f, 0.35f, 0.90f, 40.f, 0.07f, 0.20f, 0.6f, 0.04f, 0.006f, 0.05f, 0.08f, 0.20f};
if (name == "panel") return {0.60f, 0.40f, 3.4f, 1.0f, 0.40f, 0.012f, 0.40f, 1.2f, 3.0f, 0.020f, 0.35f, 0.00f, 0.80f, 50.f, 0.07f, 0.15f, 1.0f, 0.08f, 0.008f, 0.08f, 0.12f, 0.24f};
if (name == "thick") return {0.85f, 0.30f, 4.0f, 1.2f, 0.35f, 0.014f, 0.40f, 1.2f, 2.5f, 0.020f, 0.32f, 0.00f, 0.75f, 50.f, 0.07f, 0.14f, 1.0f, 0.09f, 0.010f, 0.09f, 0.12f, 0.28f};
/* window */ return {1.00f, 0.50f, 3.6f, 1.2f, 0.30f, 0.020f, 0.35f, 1.2f, 2.0f, 0.020f, 0.30f, 0.00f, 0.70f, 60.f, 0.05f, 0.12f, 1.0f, 0.10f, 0.014f, 0.07f, 0.00f, 0.00f};
}
// Thickness from the shorter side (Steam px at the window's scale): 0 at
// 44 px, 0.5 at 176 px, 1 at >= 704 px. DESIGN2 §6.1 defines θ from the
// longer side; the shorter side is used because it is what bounds a slab's
// bezel, and a 600 px wide, 64 px tall tab bar must stay thin liquid glass.
float thickness(float minSide) { return std::clamp(std::log2(std::max(minSide, 1.f) / 44.f) / 4.f, 0.f, 1.f); }
// The preset for this size (P5 "size changes the material": bigger glass is
// thicker, frostier and lenses less; small glass is thinner and lenses more)
// and the dial (tint x0.7 -> x1.3, frost -1 -> +1 mip, the band tighter).
Material materialFor(const std::string &name, float dial, float minSide) {
Material m = materialPreset(name);
const float dt = thickness(minSide) - m.thetaRef; // -1..1
m.frost = std::max(0.f, m.frost + 0.8f * dt);
m.edgeFrost = std::max(0.f, m.edgeFrost + 0.6f * dt);
m.tintA = std::clamp(m.tintA + 0.12f * dt, 0.f, 0.9f);
m.lensDeg *= std::clamp(1.f - 0.55f * dt, 0.4f, 1.6f);
m.spec *= std::clamp(1.f - 0.25f * dt, 0.6f, 1.3f);
m.dark *= std::clamp(1.f + 0.5f * dt, 0.5f, 1.5f);
m.occ *= std::clamp(1.f + 0.5f * dt, 0.5f, 1.5f);
m.slabShadow *= std::clamp(1.f + 0.6f * dt, 0.5f, 1.6f);
dial = std::clamp(dial, 0.f, 1.f);
m.tintA = std::min(0.95f, m.tintA * (0.7f + 0.6f * dial));
m.frost = std::max(0.f, m.frost - 1.f + 2.f * dial);
m.edgeFrost = std::min(m.frost, std::max(0.f, m.edgeFrost - 0.5f + dial));
m.bandK = std::clamp(m.bandK * (1.3f - 0.6f * dial), 0.f, 1.f);
return m;
}
// Materialize (MO §9): one progress m per piece of glass; the optics come in
// in order: light first, then the bend, then frost and tint, then shading.
// The ranges are P5's (motion_tokens.h).
float ramp(float x, float a, float b) { return std::clamp((x - a) / (b - a), 0.f, 1.f); }
struct Phased {
float light, lens, frost, tint, shade, shadow, alpha;
};
Phased phaseMap(float m, bool reduceMotion) {
namespace mt = lgs_motion;
m = std::clamp(m, 0.f, 1.f);
if (reduceMotion) return {1, 1, 1, 1, 1, 1, m}; // coverage only
return {mt::ramp(m, mt::kRampLight), mt::ramp(m, mt::kRampLens), mt::ramp(m, mt::kRampFrost), mt::ramp(m, mt::kRampTint),
mt::ramp(m, mt::kRampShade), mt::ramp(m, mt::kRampShadow), mt::coverage(m)};
}
// Displayed materialize progress of one piece of glass, animated toward the
// spec's target (docs/phase2/glassd-material.md, "Materialize"). Large glass
// (covers, window and thick slabs and plates) rides lgs_motion::kPhaseUp /
// kPhaseDown (sheet-in / sheet-out), small glass the linear kMatInMs /
// kMatOutMs ramps, Reduce Motion a kReduceMs coverage fade.
struct PhaseAnim {
bool init = false;
float target = 1, x = 1, v = 0; // target, value and velocity now
float y0 = 0, v0 = 0; // spring start (relative to target)
float lin0 = 1; // linear ramp start value
uint64_t t0 = 0;
bool spring = false; // large glass rides a spring, small glass a linear ramp
float ms = -1; // linear ramp duration override (ms), -1 = by size
// Sets the target; first sight starts at `from` (0 = materialize) or at the target.
void set(float tgt, bool springMode, float msOverride, float from, uint64_t now) {
tgt = std::clamp(tgt, 0.f, 1.f);
if (!init) {
init = true;
x = from >= 0 ? std::clamp(from, 0.f, 1.f) : tgt;
target = x;
v = 0;
t0 = now;
}
spring = springMode;
ms = msOverride;
if (tgt != target) {
target = tgt;
y0 = x - tgt;
v0 = v;
lin0 = x;
t0 = now;
}
}
// The value at t seconds after the last retarget (no state change).
float valueAt(float t, bool reduceMotion, float *vel = nullptr) const {
namespace mt = lgs_motion;
const bool up = target > lin0;
if (reduceMotion || !spring || ms >= 0) {
// a full 0 <-> 1 sweep takes dur; a retarget keeps that speed
const float dur = ms >= 0 ? ms / 1000.f : (reduceMotion ? mt::kReduceMs : up ? mt::kMatInMs : mt::kMatOutMs) / 1000.f;
if (vel) *vel = 0;
if (dur <= 0) return target;
return up ? std::min(target, lin0 + t / dur) : std::max(target, lin0 - t / dur);
}
const mt::Token &k = up ? mt::kPhaseUp : mt::kPhaseDown;
float y, vv;
mt::spring(k, y0, v0, t, y, vv);
if (vel) *vel = vv;
if (mt::settled(y, vv, std::max(std::fabs(y0), 0.01f), k.d)) {
if (vel) *vel = 0;
return target;
}
return std::clamp(target + y, 0.f, 1.f);
}
// Advances to `now`; returns true while still moving.
bool step(uint64_t now, bool reduceMotion) {
if (!init) return false;
// (t0 may be a little later than `now`: the spec is applied mid-iteration)
const float t = now > t0 ? float(double(now - t0) / 1e9) : 0.f;
if (x == target && v == 0) return false;
x = valueAt(t, reduceMotion, &v);
return !(x == target && v == 0);
}
};
// --selftest phase (MO-5, GL-4): PhaseAnim against motion_tokens.h at 1 ms
// steps, for a full 0 -> 1 and 1 -> 0 sweep of both curves and Reduce Motion.
// No SteamVR needed. Prints the largest difference; exit 0 when <= 0.002.
int selftestPhase() {
namespace mt = lgs_motion;
struct Case {
const char *name;
bool spring, rm;
float from, to;
} cases[] = {{"spring up (sheet-in)", true, false, 0, 1}, {"spring down (sheet-out)", true, false, 1, 0},
{"linear up (materialize-in)", false, false, 0, 1}, {"linear down (materialize-out)", false, false, 1, 0},
{"reduce motion up", true, true, 0, 1}, {"reduce motion down", false, true, 1, 0}};
float worst = 0;
std::printf("selftest phase: motion_tokens.h %s (sheet-in d %.2f settle %.0f ms, sheet-out d %.2f settle %.0f ms, "
"linear %.0f / %.0f ms, reduce %.0f ms)\n",
mt::kVersion, double(mt::kSheetIn.d), double(mt::kSheetIn.settleMs), double(mt::kSheetOut.d),
double(mt::kSheetOut.settleMs), double(mt::kMatInMs), double(mt::kMatOutMs), double(mt::kReduceMs));
for (const Case &c : cases) {
PhaseAnim a;
a.set(c.from, c.spring, -1, c.from, 0);
a.set(c.to, c.spring, -1, -1, 0);
float err = 0, settleMs = -1;
for (int ms = 0; ms <= 1200; ms++) {
a.step(uint64_t(ms) * kMs, c.rm);
float expect;
if (c.rm) {
expect = c.to > c.from ? std::min(1.f, ms / mt::kReduceMs) : std::max(0.f, 1.f - ms / mt::kReduceMs);
} else if (!c.spring) {
expect = c.to > c.from ? std::min(1.f, ms / mt::kMatInMs) : std::max(0.f, 1.f - ms / mt::kMatOutMs);
} else {
const mt::Token &k = c.to > c.from ? mt::kSheetIn : mt::kSheetOut;
const float p = mt::sample(k, float(ms)); // 0 -> 1 progress
expect = c.to > c.from ? p : 1.f - p;
}
err = std::max(err, std::fabs(a.x - expect));
if (settleMs < 0 && a.x == c.to) settleMs = float(ms);
}
std::printf(" %-30s max |glassd - table| %.4f, settles at %.0f ms\n", c.name, double(err), double(settleMs));
worst = std::max(worst, err);
}
std::printf("selftest phase: %s (worst %.4f, limit 0.002)\n", worst <= 0.002f ? "PASS" : "FAIL", double(worst));
return worst <= 0.002f ? 0 : 1;
}
// --------------------------------------------------------------------- spec
// Materialize fields, optional on a surface and on a slab (README "Phase").
struct PhaseSpec {
float phase = 1; // target materialize progress 0..1
bool materialize = false; // "appear": "materialize": first sight starts at 0
float ms = -1; // "phaseMs": linear ramp duration (ms) instead of the size's default
double atMs = 0; // "appearAt": epoch ms the materialize started (the page's own open animation)
};
// A colour from the spec: sRGB, straight alpha (v3: tint, fill, hole fill).
struct Color {
bool set = false;
float r = 0, g = 0, b = 0, a = 0;
};
// v3 hole treatment under a popped element (G2): its contact shadow, not
// excluded under the slab, clipped to the crop's rect, over a fill tone.
constexpr int kMaxEdgeSamples = 8; // per hole edge (glass.frag uHoleTex)
struct HoleSpec {
bool on = false;
float shadow = 0.35f, y = 9.f, blur = 27.f; // Steam px (6 / 18 CSS px at 1.5x)
Color fill;
// R1: tones just outside each edge of the crop (top, right, bottom, left),
// 0..8 samples along it (left to right, top to bottom); none = `fill`
std::vector<Color> edges[4];
bool hasEdges() const { return !edges[0].empty() || !edges[1].empty() || !edges[2].empty() || !edges[3].empty(); }
bool hasClip = false;
float cx0 = 0, cy0 = 0, cx1 = 0, cy1 = 0; // crop rect, Steam px
};
struct SlabSpec {
std::string id, material = "liquid";
float w = 0, h = 0, r = 0;
bool hasPos = false;
float x = 0, y = 0;
float dz = 0.015f; // scene units (metres = dz x unitM)
float ox = 0, oy = 0; // v3: world-point offset of a moved crop (Steam px)
Color tint; // v3 (G3)
Color fill; // v3 G7: a "dim" slab's tone (default black .30)
HoleSpec hole; // v3 (G2)
PhaseSpec ph;
bool none() const { return material == "none"; }
bool dim() const { return material == "dim"; } // v3 G7: a flat dark cell, no optics
};
struct ShapeSpec {
float x = 0, y = 0, w = 0, h = 0, r = 0;
};
// v3 plate (G1): one rounded rect of glass at the cover's depth, own material,
// phase, tint, fill; the occluder variant sits under a pop over its own plate.
struct PlateSpec {
std::string id, material = "liquid";
float x = 0, y = 0, w = 0, h = 0, r = 0;
PhaseSpec ph;
Color tint, fill;
bool occluder = false;
float shadow = -1; // contact shadow alpha, -1 = the material's
// "exitAt"/"exitMs": its content fades out (a route or sheet leaving) from that epoch ms over that
// long; the glass fades with it, linearly, its optics unchanged (cap "exitAt")
double exitAtMs = 0;
float exitMs = 0;
};
// v3 extra feed mask on a surface (Steam px, may lie outside the texture).
struct MaskSpec {
float x = 0, y = 0, w = 0, h = 0, dz = 0;
};
struct WorldMask {
v3 O, U, V; // a corner and the two full edge vectors (metres, standing)
};
struct SurfSpec {
std::string name, overlayKey, material = "window";
int texW = 0, texH = 0;
float radius = 0;
bool visible = true;
bool armed = false; // hidden, but its content is ready: draw its glass ahead (a pooled popup about to show)
// Cover shapes. Explicit (hasShapes, possibly empty = no cover), or absent:
// then a "window" surface is covered whole and any other gets no cover.
bool hasShapes = false;
std::vector<ShapeSpec> shapes;
// Optional world quad from the daemon (overrides the overlay transform):
// world position of Steam px (0,0) and world steps per Steam px right / down.
bool hasQuad = false;
v3 qO, qU, qV;
std::vector<SlabSpec> slabs;
PhaseSpec ph;
// "morph" (optional): the reporter's cover.morph. The shapes shown move toward `shapes` on the
// named spring (motion_tokens.h), starting at epoch ms `morphAtMs`: with the page's own motion
bool hasMorph = false;
std::string morphToken;
double morphAtMs = 0;
// v3
std::vector<PlateSpec> plates; // at most kMaxPlates
std::vector<std::string> droppedPlates; // beyond kMaxPlates or empty after clipping (the first kMaxListedDrops)
size_t droppedPlatesN = 0; // all of them
float coverDz = 0.001f; // scene units: the cover and plates relative to the surface
std::vector<MaskSpec> masks;
bool scaleFromOverlay = false; // "scaleFrom": "overlay": no rescale to the window's mpp
bool stereo = false; // "stereo": per-eye layout (a surface the scene graph shows whole)
};
// The wearer's glass tune (the bar's paintbrush panel; cap "tune"): multipliers on every material's
// refraction (lens and dispersion), frost and light (key specular, Fresnel, sheen), and a colour for the
// glass body (the neutral the tint pulls toward), applied in drawGlass on top of the presets and the dial.
struct Tune {
float refract = 1, frost = 1, light = 1; // 0..2
float hueK = 0; // 0..1: how far the body colour moves from neutral to `hue`
Color hue; // sRGB; unset = neutral
};
struct Spec {
long long seq = 0;
float dial = 0.5f;
Tune tune;
bool stereo = true; // "stereo": false = every surface mono (cap stereo)
bool reduceMotion = false;
float unitM = 0.369f; // v3: metres per scene unit (S x r)
float roomDim = 0; // v3 G7: every piece of glass sees the room darkened by this (0..1)
std::vector<WorldMask> masks; // v3: extra world mask quads
std::vector<SurfSpec> surfaces;
};
// v3 colours: [r, g, b(, a)] in 0..1, "#rgb", "#rrggbb", "#rrggbbaa",
// "rgb(r g b / a)", "rgba(r, g, b, a)" (0..255 channels, alpha 0..1 or %),
// and the reporter's names "green", "blue", "red", "scrim".
bool parseColorValue(const JVal *v, Color &c);
bool parseColor(const JVal *v, Color &c) {
if (parseColorValue(v, c)) return true;
if (v && v->type != JVal::Null) {
// ignored (treated as absent) and logged once per value, at most 16 values
static std::set<std::string> logged;
std::string what = v->type == JVal::Str ? "\"" + v->s.substr(0, 60) + "\"" : v->type == JVal::Arr ? "an array" : "a non-colour";
if (logged.size() < 16 && logged.insert(what).second) std::printf("spec: unparsable colour %s ignored\n", what.c_str());
}
return false;
}
bool parseColorValue(const JVal *v, Color &c) {
c = Color{};
if (!v) return false;
float ch[4] = {0, 0, 0, 1};
if (v->type == JVal::Arr) {
if (v->a.size() < 3 || v->a.size() > 4) return false;
for (size_t i = 0; i < v->a.size(); i++) {
if (v->a[i].type != JVal::Num || !std::isfinite(v->a[i].n)) return false;
ch[i] = float(v->a[i].n);
}
} else if (v->type == JVal::Str) {
const std::string &s = v->s;
// the reporter's named tones (DESIGN2 tokens): --lgs-green, --lgs-blue, --lgs-red, the .35 scrim
if (s == "green") { ch[0] = 48 / 255.f; ch[1] = 209 / 255.f; ch[2] = 88 / 255.f; }
else if (s == "blue") { ch[0] = 0; ch[1] = 145 / 255.f; ch[2] = 1; }
else if (s == "red") { ch[0] = 1; ch[1] = 66 / 255.f; ch[2] = 69 / 255.f; }
else if (s == "scrim") { ch[3] = 0.35f; }
else if (!s.empty() && s[0] == '#') {
std::string h = s.substr(1);
for (char k : h)
if (!std::isxdigit((unsigned char)k)) return false;
auto hx = [&](size_t i, size_t n) { return float(std::strtoul(h.substr(i, n).c_str(), nullptr, 16)); };
if (h.size() == 3 || h.size() == 4) {
for (size_t i = 0; i < h.size(); i++) ch[i] = hx(i, 1) * 17.f / 255.f;
} else if (h.size() == 6 || h.size() == 8) {
for (size_t i = 0; i < h.size() / 2; i++) ch[i] = hx(i * 2, 2) / 255.f;
} else {
return false;
}
} else if (s.rfind("rgb", 0) == 0) {
size_t open = s.find('('), close = s.rfind(')');
if (open == std::string::npos || close == std::string::npos || close < open) return false;
std::string body = s.substr(open + 1, close - open - 1);
for (char &k : body)
if (k == ',' || k == '/') k = ' ';
std::stringstream ss(body);
std::string tok;
int n = 0;
while (ss >> tok && n < 4) {
bool pct = !tok.empty() && tok.back() == '%';
float f = std::strtof(tok.c_str(), nullptr);
if (!std::isfinite(f)) return false;
ch[n] = n < 3 ? (pct ? f / 100.f : f / 255.f) : (pct ? f / 100.f : f);
n++;
}
if (n < 3) return false;
} else {
return false;
}
} else {
return false;
}
c.set = true;
c.r = std::clamp(ch[0], 0.f, 1.f);
c.g = std::clamp(ch[1], 0.f, 1.f);
c.b = std::clamp(ch[2], 0.f, 1.f);
c.a = std::clamp(ch[3], 0.f, 1.f);
return true;
}
float srgbToLin1(float c) { return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f); }
// A numeric id as text: integers exactly, anything beyond 2^53 (or not
// finite) as %.17g, never through an overflowing integer cast (R1 m7).
std::string numId(double n) {
if (std::isfinite(n) && std::fabs(n) < 9.0e15 && n == std::floor(n)) return std::to_string((long long)n);
char b[40];
std::snprintf(b, sizeof b, "%.17g", n);
return b;
}
// Hole edge tones (R1): {"top": c | [c, ...], "right": ..., "bottom": ..., "left": ...},
// each a colour or 1..8 colours along that edge; unparsable entries are skipped.
void readEdges(const JVal *e, HoleSpec &h) {
if (!e || e->type != JVal::Obj) return;
static const char *names[4] = {"top", "right", "bottom", "left"};
for (int k = 0; k < 4; k++) {
const JVal *v = e->get(names[k]);
if (!v) continue;
// a list of colours, or one colour (a string, or [r, g, b(, a)] numbers)
const bool list = v->type == JVal::Arr && !v->a.empty() && v->a[0].type != JVal::Num;
if (list) {
for (const JVal &c : v->a) {
Color col;
if (h.edges[k].size() < size_t(kMaxEdgeSamples) && parseColor(&c, col)) h.edges[k].push_back(col);
}
} else {
Color col;
if (parseColor(v, col)) h.edges[k].push_back(col);
}
}
}
void readPhase(const JVal &o, PhaseSpec &ph) {
ph.phase = std::clamp(float(o.num("phase", 1.0)), 0.f, 1.f);
if (!std::isfinite(ph.phase)) ph.phase = 1;
ph.materialize = o.str("appear", "") == "materialize";
ph.ms = o.has("phaseMs") ? std::clamp(float(o.num("phaseMs", -1)), 0.f, 10000.f) : -1.f;
ph.atMs = o.has("appearAt") ? o.num("appearAt", 0) : 0;
if (!std::isfinite(ph.atMs) || ph.atMs < 0) ph.atMs = 0;
}
const char *kDemoSpec = R"({"seq": 1, "dial": 0.5, "surfaces": [
{"name": "main", "overlayKey": "valve.steam.gamepadui.main", "texW": 1920, "texH": 1080, "radius": 48,
"material": "window", "visible": true,
"slabs": [{"id": "hdr-back", "w": 210, "h": 60, "r": 30, "material": "liquid", "x": 18, "y": 6, "dz": 0.015},
{"id": "tabs", "w": 600, "h": 64, "r": 32, "material": "liquid", "x": 900, "y": 70, "dz": 0.012},
{"id": "play", "w": 260, "h": 72, "r": 36, "material": "liquid", "x": 120, "y": 840, "dz": 0.015}]}
]})";
bool readVec3(const JVal *v, v3 &out) {
if (!v || v->type != JVal::Arr || v->a.size() != 3) return false;
for (const JVal &e : v->a)
if (e.type != JVal::Num || !std::isfinite(e.n)) return false;
out = {float(v->a[0].n), float(v->a[1].n), float(v->a[2].n)};
return true;
}
bool parseSpec(const std::string &text, Spec &out, std::string &err) {
JVal root;
if (!JParser(text).parse(root, err)) return false;
if (root.type != JVal::Obj) { err = "spec is not an object"; return false; }
out = Spec{};
{
const double sq = root.num("seq", 0);
out.seq = std::isfinite(sq) ? (long long)std::clamp(sq, -9.0e18, 9.0e18) : 0;
}
out.dial = float(root.num("dial", 0.5));
if (!std::isfinite(out.dial)) out.dial = 0.5f;
out.dial = std::clamp(out.dial, 0.f, 1.f);
if (const JVal *t = root.get("tune"); t && t->type == JVal::Obj) {
auto mult = [&](const char *k) {
const float v = float(t->num(k, 1));
return std::isfinite(v) ? std::clamp(v, 0.f, 2.f) : 1.f;
};
out.tune.refract = mult("refract");
out.tune.frost = mult("frost");
out.tune.light = mult("light");
if (parseColor(t->get("hue"), out.tune.hue)) {
const float k = float(t->num("hueK", 0.5));
out.tune.hueK = std::isfinite(k) ? std::clamp(k, 0.f, 1.f) : 0.f;
}
}
out.reduceMotion = root.boolean("reduceMotion", false);
out.stereo = root.boolean("stereo", true);
{
const float d = float(root.num("roomDim", 0));
out.roomDim = std::isfinite(d) ? std::clamp(d, 0.f, 0.9f) : 0.f;
}
{
const float u = float(root.num("unitM", 0.369));
out.unitM = std::isfinite(u) && u > 0.01f && u < 10.f ? u : 0.369f;
}
auto finite = [](float v) { return std::isfinite(v) ? v : 0.f; };
if (const JVal *m = root.get("masks"); m && m->type == JVal::Arr) {
for (const JVal &q : m->a) {
WorldMask wm;
if (q.type == JVal::Obj && readVec3(q.get("O"), wm.O) && readVec3(q.get("U"), wm.U) && readVec3(q.get("V"), wm.V) &&
length(wm.U) > 1e-4f && length(wm.V) > 1e-4f && length(wm.U) < 20.f && length(wm.V) < 20.f &&
out.masks.size() < size_t(kMaxExtraMasks))
out.masks.push_back(wm);
}
}
const JVal *surfs = root.get("surfaces");
if (!surfs || surfs->type != JVal::Arr) return true;
for (const JVal &s : surfs->a) {
if (s.type != JVal::Obj) continue;
SurfSpec ss;
ss.name = s.str("name", "");
ss.overlayKey = s.str("overlayKey", "");
ss.texW = int(s.num("texW", 0));
ss.texH = int(s.num("texH", 0));
ss.radius = float(s.num("radius", 0));
ss.material = s.str("material", "window");
ss.visible = s.boolean("visible", true);
ss.armed = !ss.visible && s.boolean("armed", false);
readPhase(s, ss.ph);
if (const JVal *m = s.get("morph"); m && m->type == JVal::Obj) {
ss.morphToken = m->str("token", "");
ss.morphAtMs = m->num("at", 0);
ss.hasMorph = !ss.morphToken.empty() && std::isfinite(ss.morphAtMs) && ss.morphAtMs > 0;
}
if (ss.name.empty() || ss.name.size() > 128 || ss.texW <= 0 || ss.texH <= 0 || ss.texW > 8192 || ss.texH > 8192) continue;
if (const JVal *sh = s.get("shapes"); sh && sh->type == JVal::Arr) {
ss.hasShapes = true;
for (const JVal &q : sh->a) {
ShapeSpec p{float(q.num("x", 0)), float(q.num("y", 0)), float(q.num("w", 0)), float(q.num("h", 0)),
float(q.num("r", ss.radius))};
// clip to the texture
const float x0 = std::max(0.f, p.x), y0 = std::max(0.f, p.y);
const float x1 = std::min(float(ss.texW), p.x + p.w), y1 = std::min(float(ss.texH), p.y + p.h);
if (x1 - x0 < 1 || y1 - y0 < 1) continue;
p = {x0, y0, x1 - x0, y1 - y0, std::max(0.f, p.r)};
ss.shapes.push_back(p);
}
}
if (const JVal *q = s.get("quad"); q && q->type == JVal::Obj)
ss.hasQuad = readVec3(q->get("O"), ss.qO) && readVec3(q->get("U"), ss.qU) && readVec3(q->get("V"), ss.qV);
if (const JVal *sl = s.get("slabs"); sl && sl->type == JVal::Arr) {
for (const JVal &q : sl->a) {
SlabSpec b;
b.id = q.str("id", "");
if (const JVal *idn = q.get("id"); idn && idn->type == JVal::Num) b.id = numId(idn->n);
b.w = float(q.num("w", 0));
b.h = float(q.num("h", 0));
b.r = float(q.num("r", 0));
b.material = q.str("material", "liquid");
b.hasPos = q.has("x") && q.has("y");
b.x = float(q.num("x", 0));
b.y = float(q.num("y", 0));
b.dz = finite(float(q.num("dz", 0.015)));
b.ox = finite(float(q.num("ox", 0)));
b.oy = finite(float(q.num("oy", 0)));
parseColor(q.get("tint"), b.tint);
parseColor(q.get("fill"), b.fill);
if (const JVal *h = q.get("hole")) {
if (h->type == JVal::Bool) {
b.hole.on = h->b;
} else if (h->type == JVal::Obj) {
b.hole.on = true;
b.hole.shadow = std::clamp(finite(float(h->num("shadow", 0.35))), 0.f, 1.f);
b.hole.y = std::clamp(finite(float(h->num("y", 9))), -200.f, 200.f);
b.hole.blur = std::clamp(finite(float(h->num("blur", 27))), 0.f, 400.f);
parseColor(h->get("fill"), b.hole.fill);
readEdges(h->get("edges"), b.hole);
if (const JVal *c = h->get("clip"); c && c->type == JVal::Arr && c->a.size() == 4) {
bool ok = true;
float v[4];
for (int i = 0; i < 4; i++) {
ok &= c->a[size_t(i)].type == JVal::Num && std::isfinite(c->a[size_t(i)].n);
v[i] = ok ? float(c->a[size_t(i)].n) : 0.f;
}
if (ok && v[2] > v[0] && v[3] > v[1]) {
b.hole.hasClip = true;
b.hole.cx0 = v[0]; b.hole.cy0 = v[1]; b.hole.cx1 = v[2]; b.hole.cy1 = v[3];
}
}
}
}
readPhase(q, b.ph);
if (b.id.empty() || b.id.size() > 128 || b.w < 1 || b.h < 1) continue;
ss.slabs.push_back(b);
}
}
// v3 plates (G1): at most kMaxPlates, clipped to the texture
if (const JVal *pl = s.get("plates"); pl && pl->type == JVal::Arr) {
int index = 0;
for (const JVal &q : pl->a) {
if (q.type != JVal::Obj) { index++; continue; }
PlateSpec p;
p.id = q.str("id", "");
if (const JVal *idn = q.get("id"); idn && idn->type == JVal::Num) p.id = numId(idn->n);
if (p.id.empty()) p.id = "p" + std::to_string(index);
if (p.id.size() > 128) p.id = p.id.substr(0, 128);
index++;
p.material = q.str("material", "liquid");
p.x = finite(float(q.num("x", 0)));
p.y = finite(float(q.num("y", 0)));
p.w = finite(float(q.num("w", 0)));
p.h = finite(float(q.num("h", 0)));
p.r = q.has("r") ? std::max(0.f, finite(float(q.num("r", 0)))) : 0.5f * std::min(p.w, p.h);
readPhase(q, p.ph);
parseColor(q.get("tint"), p.tint);
parseColor(q.get("fill"), p.fill);
p.occluder = q.boolean("occluder", false);
if (q.has("shadow")) p.shadow = std::clamp(finite(float(q.num("shadow", -1))), 0.f, 1.f);
if (q.has("exitAt")) {
const double at = q.num("exitAt", 0);
if (std::isfinite(at) && at > 0) {
p.exitAtMs = at;
p.exitMs = std::clamp(finite(float(q.num("exitMs", 150))), 0.f, 2000.f);
}
}
const float x0 = std::max(0.f, p.x), y0 = std::max(0.f, p.y);
const float x1 = std::min(float(ss.texW), p.x + p.w), y1 = std::min(float(ss.texH), p.y + p.h);
if (x1 - x0 < 1 || y1 - y0 < 1 || ss.plates.size() >= size_t(kMaxPlates)) {
if (ss.droppedPlates.size() < size_t(kMaxListedDrops)) ss.droppedPlates.push_back(p.id);
ss.droppedPlatesN++;
continue;
}
p.r = std::min(p.r, 0.5f * std::min(x1 - x0, y1 - y0));
p.x = x0; p.y = y0; p.w = x1 - x0; p.h = y1 - y0;
ss.plates.push_back(p);
}
}
if (s.has("coverDz")) ss.coverDz = std::clamp(finite(float(s.num("coverDz", 0.001))), -0.5f, 0.5f);
if (const JVal *m = s.get("masks"); m && m->type == JVal::Arr) {
for (const JVal &q : m->a) {
if (q.type != JVal::Obj || ss.masks.size() >= size_t(kMaxExtraMasks)) continue;
MaskSpec k{finite(float(q.num("x", 0))), finite(float(q.num("y", 0))), finite(float(q.num("w", 0))),
finite(float(q.num("h", 0))), finite(float(q.num("dz", 0)))};
if (k.w >= 1 && k.h >= 1 && k.w < 20000 && k.h < 20000) ss.masks.push_back(k);
}
}
ss.scaleFromOverlay = s.str("scaleFrom", "main") == "overlay";
ss.stereo = s.boolean("stereo", false);
out.surfaces.push_back(std::move(ss));
}
return true;
}
std::string sanitizeKey(const std::string &s) {
std::string o;
for (char c : s) o += (std::isalnum((unsigned char)c) || c == '.' || c == '_' || c == '-') ? c : '_';
return o.substr(0, 100);
}
// ----------------------------------------------------------------- surfaces
struct Geometry {
bool valid = false, fallback = false;
v3 O, U, V, N; // world: Steam pixel (0,0) top-left; per-pixel right; per-pixel down; unit normal
float mpp = 0;
};
struct SlabSlot {
SlabSpec s;
int x = 0, y = 0, w = 0, h = 0; // cell in the glassd texture
uint64_t ghostUntilNs = 0; // ghosts: removed from the spec, drawn until then
uint64_t stillSinceNs = 0; // its element's x/y last changed then (its shadow waits for it to settle)
PhaseAnim anim; // materialize progress
};
// A plate's state across specs (its materialize progress follows its id).
struct PlateSlot {
PlateSpec s;
PhaseAnim anim;
uint64_t exitT0 = 0; // its content's fade-out started then (monotonic ns); 0 = not leaving
};
struct Surface {
SurfSpec spec;
std::vector<PlateSlot> plates; // v3 plates, in spec order
std::string key;
vr::VROverlayHandle_t ov = vr::k_ulOverlayHandleInvalid;
vr::VROverlayHandle_t steam = vr::k_ulOverlayHandleInvalid;
int gw = 0, gh = 0, bw = 0, bh = 0;
int eyes = 1; // 2: the buffers hold the layout twice, left eye's on the left (stereo)
float scale = 0.75f;
Atlas atlas;
std::vector<SlabSlot> slots, ghosts;
std::set<std::string> dropped; // slabs that did not fit (logged once)
DmaTarget bufs[3];
bool cleared[3] = {false, false, false}; // buffer fully cleared once since it was created
int next = 0, last = -1;
// geometry: geo is the last good quad (or a fallback); geoOk = the latest
// fetch succeeded and was plausible; rawGeo = the overlay's own quad when
// geo was rescaled to the main window's scale
Geometry geo, rawGeo;
bool geoOk = false, rescaled = false;
uint64_t geoOkNs = 0, findNs = 0;
std::string geoNote = "none", geoLogged;
bool rendered = false, announced = false, warnedShapes = false, atlasOn = false, warnedHoles = false;
PhaseAnim anim; // the cover's materialize progress (0 while it has no cover)
// cover morph: the shapes shown move from morphFrom toward the spec's on morphTok's spring from
// morphT0 (monotonic ns); fadeShapes: the shapes a cover that went away dematerializes with
std::vector<ShapeSpec> morphFrom, fadeShapes;
const lgs_motion::Token *morphTok = nullptr;
uint64_t morphT0 = 0;
bool clearPending = false; // hidden since the last frame: one transparent frame, so no stale glass shows on the next open
Target lowTex; // the cover at 1/4 resolution, mipmapped (glass.frag pass 1): its flat
// interior for pass 2, and what slabs see behind them
Target coverTex; // R1: a copy of lowTex with the cover only (no plates), mipmapped:
// what plates over a cover see behind them (glass over glass)
int submittedW = 0, submittedH = 0; // what SteamVR holds (mouse scale follows it)
int submitFails = 0, createTries = 0;
uint64_t createNs = 0, submitFailSinceNs = 0, lastSubmitNs = 0;
};
struct Retired {
DmaTarget buf;
uint64_t freeAtNs;
};
// --------------------------------------------------------------- GPU timer
struct GpuTimer {
Gfx *g = nullptr;
GLuint q[4] = {0, 0, 0, 0};
bool busy[4] = {false, false, false, false};
int head = 0;
bool active = false;
float ms = 0; // EMA
float last = 0; // the latest frame's
bool have = false;
bool keep = false; // --bench: keep every frame's time
std::vector<float> samples;
void init(Gfx &gfx) {
g = &gfx;
if (g->timerQuery) glGenQueries(4, q);
}
void begin() {
if (!g || !g->timerQuery || busy[head]) return;
glBeginQuery(GL_TIME_ELAPSED_EXT, q[head]);
active = true;
}
void end() {
if (!active) return;
glEndQuery(GL_TIME_ELAPSED_EXT);
busy[head] = true;
head = (head + 1) % 4;
active = false;
}
void poll() {
if (!g || !g->timerQuery) return;
GLint disjoint = 0;
glGetIntegerv(GL_GPU_DISJOINT_EXT, &disjoint);
for (int i = 0; i < 4; i++) {
if (!busy[i]) continue;
GLuint avail = 0;
glGetQueryObjectuiv(q[i], GL_QUERY_RESULT_AVAILABLE, &avail);
if (!avail) continue;
GLuint64 ns = 0;
g->getQueryObjectui64v(q[i], GL_QUERY_RESULT, &ns);
busy[i] = false;
if (disjoint) continue;
float v = float(ns) / 1e6f;
last = v;
if (keep) samples.push_back(v);
ms = have ? ms + (v - ms) * 0.1f : v;
have = true;
}
}
};
// First process whose comm is `name` (comm is at most 15 chars), or -1.
int findPid(const char *name) {
DIR *d = opendir("/proc");
if (!d) return -1;
int found = -1;
while (dirent *e = readdir(d)) {
if (!std::isdigit((unsigned char)e->d_name[0])) continue;
std::string p = std::string("/proc/") + e->d_name + "/comm";
FILE *f = std::fopen(p.c_str(), "r");
if (!f) continue;
char comm[64] = {};
if (std::fgets(comm, sizeof comm, f)) {
comm[std::strcspn(comm, "\n")] = 0;
if (std::strcmp(comm, name) == 0) found = std::atoi(e->d_name);
}
std::fclose(f);
if (found > 0) break;
}
closedir(d);
return found;
}
bool pidIs(int pid, const char *name) {
std::string p = "/proc/" + std::to_string(pid) + "/comm";
FILE *f = std::fopen(p.c_str(), "r");
if (!f) return false;
char comm[64] = {};
bool ok = std::fgets(comm, sizeof comm, f) != nullptr;
std::fclose(f);
comm[std::strcspn(comm, "\n")] = 0;
return ok && std::strcmp(comm, name) == 0;
}
void mkdirs(const std::string &dir) {
std::string cur;
std::stringstream ss(dir);
std::string part;
if (!dir.empty() && dir[0] == '/') cur = "/";
while (std::getline(ss, part, '/')) {
if (part.empty()) continue;
cur += part + "/";
mkdir(cur.c_str(), 0755);
}
}
std::string dirname(const std::string &p) {
size_t s = p.rfind('/');
return s == std::string::npos ? "." : s == 0 ? "/" : p.substr(0, s);
}
std::string basename(const std::string &p) {
size_t s = p.rfind('/');
return s == std::string::npos ? p : p.substr(s + 1);
}
bool readFile(const std::string &p, std::string &out) {
std::ifstream f(p, std::ios::binary);
if (!f) return false;
std::stringstream ss;
ss << f.rdbuf();
out = ss.str();
return true;
}
bool writeAtomic(const std::string &p, const std::string &text) {
std::string tmp = p + ".tmp";
FILE *f = std::fopen(tmp.c_str(), "w");
if (!f) return false;
std::fwrite(text.data(), 1, text.size(), f);
std::fclose(f);
return std::rename(tmp.c_str(), p.c_str()) == 0;
}
// Exclusive, non-blocking flock on `path` (created). Returns the fd or -1.
int lockPath(const std::string &path) {
mkdirs(dirname(path));
int fd = open(path.c_str(), O_CREAT | O_RDWR | O_CLOEXEC, 0644);
if (fd < 0) return -1;
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
close(fd);
return -1;
}
return fd;
}
int roundUp(int v, int m) { return ((v + m - 1) / m) * m; }
// A slab's shadow on the cover (and plates) behind it (glass.frag uShBox /
// uShPar); with a hole (v3 G2) also its crop rect and fill tone.
struct Caster {
float cx, cy, hw, hh, r, alpha, off, soft;
bool hole = false;
float x0 = 0, y0 = 0, x1 = 0, y1 = 0; // crop rect (holes)
Color fill; // hole fill (alpha already phased)
const HoleSpec *spec = nullptr; // its edge tones (R1), alpha phased by `edgeA`
float edgeA = 1;
};
// v3 per-piece look: colour tint, flat fill, occluder variant, dim plate.
struct Look {
Color tint, fill;
bool occluder = false, flat = false;
float shadow = -1; // contact shadow alpha override (-1 = material)
};
// ===================================================================== glassd
class Glassd {
public:
explicit Glassd(const Options &o) : opt(o) {}
int run() {
int rc = 1;
if (!setup(rc)) {
teardown();
return rc;
}
loop();
teardown();
return g_exitCode.load();
}
private:
Options opt;
Gfx gfx;
Program progGlass, progUpdate, progPush, progPull, progRow, progHfill, progTest, progView;
bool testRoomDone = false;
GLuint vao = 0;
GLuint holeTex = 0; // hole edge tones of the casters being drawn (glass.frag uHoleTex, unit 4)
Room room;
FeedCapture feed;
FeedFrame frame;
uint64_t haveSeq = 0;
FeedCalib cal;
Pose eyeToHead;
v3 eyeOffset[2] = {{-0.032f, 0, 0}, {0.032f, 0, 0}}; // each eye in head space (SteamVR's IPD)
uint64_t eyeOffsetAt = 0;
std::deque<std::pair<uint64_t, Pose>> hist;
Spec spec;
bool haveSpec = false;
struct stat specStat {};
std::string specError;
std::vector<std::unique_ptr<Surface>> surfaces;
std::vector<Retired> retired;
std::vector<MaskQuad> masks;
uint64_t masksNs = 0, masksBuiltNs = 0;
bool masksComplete = false, masksOverflowLogged = false;
int lockOut = -1, lockKey = -1;
int inoFd = -1, inoWd = -1;
uint64_t inoRetryNs = 0;
GpuTimer renderTimer, roomTimer;
float envLuma = 0.25f;
bool envSet = false;
float mainMpp = 0; // metres per Steam px of the main window, from its overlay (0 = unknown)
// health
int origPpid = 0, vrserverPid = -1, compositorPid = -1;
uint64_t lastHealthNs = 0, lastSubmitNs = 0;
std::deque<uint64_t> overlayLosses;
// dashboard / room gating
uint64_t dashChangeNs = 0, lastShotNs = 0;
uint64_t hideShotNs[2] = {0, 0}; // extra feed shots right after the dashboard hides
bool graceDone = true;
uint64_t roomSkipped = 0;
// stats
uint64_t startNs = 0, frames = 0, framesWindow = 0, roomWindow = 0, lastStatusNs = 0, lastOutNs = 0;
uint64_t feedSeenAtStatus = 0, feedAttachedAtStatus = 0, firstRoomNs = 0;
float fpsNow = 0, cpuMs = 0, roomCpuMs = 0;
bool lastDash = false;
Pose lastRenderHead;
bool roomDirty = true, layoutDirty = true, outPending = false;
static constexpr const char *kRoomKeep = "/dev/shm/lgs/glassd-room.bin"; // the room map across restarts
uint64_t roomKeptAt = 0, lastKeepNs = 0;
uint64_t outPendingSinceNs = 0;
Pose lastHead;
bool haveLastHead = false;
uint64_t nextRenderNs = 0, lastLoopNs = 0, lastRenderNs = 0;
bool dumpedOnce = false;
uint64_t lastPhaseNs = 0; // when the phases were last stepped (dump lines)
PhaseAnim roomDimAnim; // v3 G7: the room dim, on sheet-in up and sheet-out down
// The dial and the tune as drawn: eased toward the spec's (time constant kTuneTau), so a slider dragged
// in the paintbrush panel moves the glass smoothly instead of in steps; the body colour is linear rgb
// at luminance 1
struct TuneLive {
bool init = false;
float dial = 0.5f, refract = 1, frost = 1, light = 1, hueK = 0, hr = 1, hg = 1, hb = 1;
} tuneLive;
uint64_t tuneNs = 0;
static constexpr float kTuneTau = 0.04f; // s: 95 % of a change in 120 ms
// ------------------------------------------------------------- shaders
std::string shaderSource(const std::string &name) {
if (!opt.shaderDir.empty()) {
std::string s;
if (readFile(opt.shaderDir + "/" + name, s)) return s;
std::fprintf(stderr, "%s/%s missing; using the built-in copy\n", opt.shaderDir.c_str(), name.c_str());
}
for (auto &kv : kShaders)
if (name == kv.first) return kv.second;
std::fprintf(stderr, "no shader %s\n", name.c_str());
return "";
}
bool loadPrograms() {
const std::string head = "#version 300 es\nprecision highp float;\nprecision highp int;\nprecision highp sampler2D;\n";
const std::string common = shaderSource("common.glsl");
const std::string vs = head + common + "\n" + shaderSource("fullscreen.vert");
auto fs = [&](const char *n) { return head + common + "\n" + shaderSource(n); };
const std::string testLib = shaderSource("testroom.glsl");
auto fsTest = [&](const char *n) { return head + common + "\n" + testLib + "\n" + shaderSource(n); };
return linkProgram(progGlass, vs, fs("glass.frag"), "glass") &&
linkProgram(progUpdate, vs, fs("room_update.frag"), "room_update") &&
linkProgram(progPush, vs, fs("push.frag"), "push") && linkProgram(progPull, vs, fs("pull.frag"), "pull") &&
linkProgram(progRow, vs, fs("row.frag"), "row") && linkProgram(progHfill, vs, fs("hfill.frag"), "hfill") &&
linkProgram(progTest, vs, fsTest("testroom.frag"), "testroom") &&
linkProgram(progView, vs, fsTest("view.frag"), "view");
}
// --------------------------------------------------------------- setup
bool setup(int &rc) {
startNs = monoNowNs();
// Stop with the parent (lgs_shell): an orphan would keep its overlays
// and the lock, and block its successor.
origPpid = getppid();
if (!opt.orphanOk && origPpid > 1) {
prctl(PR_SET_PDEATHSIG, SIGTERM);
if (getppid() != origPpid) {
std::fprintf(stderr, "parent already gone\n");
rc = 0;
return false;
}
}
// One instance per output file and per overlay key prefix.
mkdirs(dirname(opt.out));
const std::string outLock = opt.out + ".lock";
const std::string keyLock = std::string(kShmDir) + "/" + sanitizeKey(opt.keyPrefix) + "lock";
if ((lockOut = lockPath(outLock)) < 0) {
std::fprintf(stderr, "another glassd holds %s\n", outLock.c_str());
rc = kExitTemp;
return false;
}
if ((lockKey = lockPath(keyLock)) < 0) {
std::fprintf(stderr, "another glassd holds %s (key prefix %s)\n", keyLock.c_str(), opt.keyPrefix.c_str());
rc = kExitTemp;
return false;
}
vrserverPid = findPid("vrserver");
if (vrserverPid <= 0) {
std::fprintf(stderr, "SteamVR (vrserver) is not running; not starting it\n");
rc = kExitTemp;
return false;
}
compositorPid = findPid("vrcompositor");
vr::EVRInitError err = vr::VRInitError_None;
vr::VR_Init(&err, vr::VRApplication_Overlay);
if (err != vr::VRInitError_None) {
std::fprintf(stderr, "VR_Init: %s\n", vr::VR_GetVRInitErrorAsEnglishDescription(err));
rc = kExitTemp;
return false;
}
if (!vr::VROverlay() || !vr::VRSystem() || !vr::VRIPCResourceManager()) {
std::fprintf(stderr, "OpenVR interfaces missing\n");
return false;
}
if (!gfx.init()) return false;
std::printf("GL %s | %s | timer queries %s | half-float targets %s\n", gfx.version.c_str(), gfx.renderer.c_str(),
gfx.timerQuery ? "yes" : "no", gfx.halfFloatTargets ? "yes" : "no");
std::printf("vrserver pid %d, vrcompositor pid %d, parent pid %d%s\n", vrserverPid, compositorPid, origPpid,
opt.orphanOk ? " (orphan ok)" : "");
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
if (!loadPrograms()) return false;
room.radius = opt.roomDepth;
if (!room.init(gfx, progUpdate, progPush, progPull, progRow, progHfill, vao)) return false;
// the room this glassd's predecessor knew (Room::save): glass refracts the room from the first frame
if (opt.testPattern < 0 && room.load(kRoomKeep)) {
float known = 0, lum = 0;
room.stats(known, lum);
std::printf("room map restored from %s (%.0f%% known)\n", kRoomKeep, double(known) * 100.0);
}
renderTimer.init(gfx);
roomTimer.init(gfx);
renderTimer.keep = opt.bench;
cal.load();
std::printf("feed calibration %s: u=%.4fx%+.4f v=%.4fy%+.4f latency %.1f ms, %s eye\n", cal.source.c_str(), cal.a, cal.b,
cal.c, cal.d, cal.latencyMs, cal.eye ? "right" : "left");
eyeToHead = fromVR(vr::VRSystem()->GetEyeToHeadTransform(cal.eye ? vr::Eye_Right : vr::Eye_Left));
if (!opt.noFeed) feed.start(opt.feedDev, opt.feedDownsample);
if (opt.testPattern >= 0)
std::printf("test backdrop: no camera, fixed head at %.2f %.2f %.2f (eye offset %.2f %.2f %.2f), window 1.43 m ahead\n",
double(kTestEye.x), double(kTestEye.y), double(kTestEye.z), double(opt.testHead[0]), double(opt.testHead[1]),
double(opt.testHead[2]));
if (opt.demo) {
Spec s;
std::string e;
if (parseSpec(kDemoSpec, s, e)) applySpec(s);
std::printf("using the built-in demo spec\n");
} else {
watchSpec();
}
return true;
}
void keepRoom() {
if (opt.testPattern >= 0 || room.updates == roomKeptAt) return;
mkdirs(dirname(kRoomKeep));
if (room.save(kRoomKeep)) roomKeptAt = room.updates;
}
void teardown() {
benchSummary();
keepRoom();
feed.stop();
if (vr::VRSystem()) writeOut(true); // fps 0, "exiting": the layout stays readable
if (vr::VROverlay()) {
for (auto &s : surfaces) releaseSurface(*s, true);
for (auto &r : retired) releaseBuffer(r.buf);
}
surfaces.clear();
retired.clear();
if (gfx.dpy != EGL_NO_DISPLAY) gfx.shutdown();
if (vr::VRSystem()) vr::VR_Shutdown();
if (inoFd >= 0) close(inoFd);
if (lockKey >= 0) close(lockKey);
if (lockOut >= 0) close(lockOut);
}
// ------------------------------------------------------- spec watching
// inotify on the spec's directory, so a new spec is picked up at once
// (the 10 Hz stat poll stays as a fallback).
void watchSpec() {
if (inoFd < 0) inoFd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (inoFd < 0 || inoWd >= 0) return;
inoWd = inotify_add_watch(inoFd, dirname(opt.spec).c_str(), IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE | IN_DELETE_SELF | IN_MOVE_SELF);
inoRetryNs = monoNowNs() + 2000000000ull;
}
// True when an event concerns the spec file (or the watch needs renewing).
bool drainSpecEvents() {
if (inoFd < 0) return false;
bool hit = false;
alignas(inotify_event) char buf[4096];
const std::string base = basename(opt.spec);
while (true) {
ssize_t n = read(inoFd, buf, sizeof buf);
if (n <= 0) break;
for (ssize_t i = 0; i < n;) {
auto *e = reinterpret_cast<inotify_event *>(buf + i);
if (e->mask & (IN_IGNORED | IN_DELETE_SELF | IN_MOVE_SELF)) inoWd = -1;
if (e->len && base == e->name) hit = true;
i += ssize_t(sizeof(inotify_event) + e->len);
}
}
return hit;
}
// ---------------------------------------------------------- overlays
void releaseBuffer(DmaTarget &b) {
if (b.vrHandle) vr::VRIPCResourceManager()->UnrefResource(vr::SharedTextureHandle_t(b.vrHandle));
b.destroy(gfx);
}
void releaseSurface(Surface &s, bool destroyOverlay) {
if (destroyOverlay && s.ov != vr::k_ulOverlayHandleInvalid) {
vr::VROverlay()->DestroyOverlay(s.ov);
s.ov = vr::k_ulOverlayHandleInvalid;
}
if (destroyOverlay) { // the surface goes away: its copies too
s.lowTex.destroy();
s.coverTex.destroy();
}
for (auto &b : s.bufs) releaseBuffer(b);
}
bool ensureOverlay(Surface &s) {
if (s.ov != vr::k_ulOverlayHandleInvalid) return true;
const uint64_t now = monoNowNs();
if (s.createTries && now - s.createNs < 5000000000ull) return false; // retry every 5 s
s.createNs = now;
std::string friendly = "Glass Shell glass (" + s.spec.name + ")";
auto e = vr::VROverlay()->CreateOverlay(s.key.c_str(), friendly.c_str(), &s.ov);
if (e != vr::VROverlayError_None) {
if (s.createTries++ == 0)
std::printf("CreateOverlay(%s): %s (retrying every 5 s)\n", s.key.c_str(), vr::VROverlay()->GetOverlayErrorNameFromEnum(e));
s.ov = vr::k_ulOverlayHandleInvalid;
return false;
}
s.createTries = 0;
s.submittedW = s.submittedH = 0;
vr::VROverlay()->SetOverlayFlag(s.ov, vr::VROverlayFlags_IsPremultiplied, true);
vr::VROverlay()->SetOverlayWidthInMeters(s.ov, 1.0f);
std::printf("created overlay %s\n", s.key.c_str());
return true;
}
// Our overlay is gone or refuses textures (e.g. vrcompositor restarted):
// drop it and its imports; the next render recreates both. Three losses
// within 30 s end glassd (exit 75) so the daemon starts a fresh one.
void loseOverlay(Surface &s, const char *why) {
const uint64_t now = monoNowNs();
std::printf("overlay %s lost (%s); recreating it\n", s.key.c_str(), why);
if (s.ov != vr::k_ulOverlayHandleInvalid) vr::VROverlay()->DestroyOverlay(s.ov);
s.ov = vr::k_ulOverlayHandleInvalid;
for (auto &b : s.bufs) releaseBuffer(b);
for (bool &c : s.cleared) c = false;
s.next = 0;
s.last = -1;
s.rendered = s.announced = false;
s.submittedW = s.submittedH = 0;
s.submitFails = 0;
s.createTries = 0;
layoutDirty = outPending = true;
outPendingSinceNs = now;
// losses within 1 s of each other (all surfaces at once) are one incident
if (!overlayLosses.empty() && now - overlayLosses.back() < 1000000000ull) return;
overlayLosses.push_back(now);
while (!overlayLosses.empty() && now - overlayLosses.front() > 30000000000ull) overlayLosses.pop_front();
if (overlayLosses.size() >= 3) {
std::printf("overlays keep failing (%zu losses in 30 s); exiting for a fresh start\n", overlayLosses.size());
quitWith(kExitTemp);
}
}
// ------------------------------------------------------------ layout
// Texture = backdrop (bw x bh) on top, slab atlas below. The atlas has its
// final height from the start (the backdrop's height, 256..1024 rows), so
// slabs coming and going (focus moves, a sheet opening) never change the
// texture size, the backdrop UV or the mouse scale while the Steam window
// keeps its size. Rendering touches only the backdrop and live cells, so
// the headroom costs memory, not bandwidth.
static int atlasHeight(int bh) { return roundUp(bh + Atlas::kGutter + std::clamp(bh, 256, 1024), 32); }
int wantEyes(const Surface &s) const { return !opt.mono && spec.stereo && s.spec.stereo ? 2 : 1; }
void resizeTexture(Surface &s, int gw, int gh) {
const int eyes = wantEyes(s);
if (s.gw == gw && s.gh == gh && s.eyes == eyes) return;
const uint64_t now = monoNowNs();
for (auto &b : s.bufs) {
if (b.bo) retired.push_back({b, now + 500000000ull});
b = DmaTarget{};
}
s.gw = gw;
s.gh = gh;
s.eyes = eyes;
s.next = 0;
s.last = -1;
s.rendered = false;
for (bool &c : s.cleared) c = false;
std::printf("layout %s -> %s %dx%d, backdrop %dx%d (scale %.4f)\n", s.spec.name.c_str(), s.key.c_str(), gw, gh, s.bw, s.bh,
double(s.scale));
}
void dropSlab(Surface &s, const SlabSpec &b, int w, int h, const char *why) {
if (s.dropped.insert(b.id).second)
std::printf("slab %s/%s (%dx%d px) %s; it stays flat\n", s.spec.name.c_str(), b.id.c_str(), w, h, why);
}
// Re-packs every live slab plus `extra` into the atlas. Commits (moving
// live cells: their UVs change, logged) only if everything fits.
bool repack(Surface &s, SlabSlot *extra) {
std::vector<SlabSlot *> items;
for (auto &sl : s.slots) items.push_back(&sl);
if (extra) items.push_back(extra);
std::stable_sort(items.begin(), items.end(), [](const SlabSlot *a, const SlabSlot *b) {
if (a->h != b->h) return a->h > b->h;
return a->s.id < b->s.id;
});
Atlas a;
a.reset(s.bw, s.bh + Atlas::kGutter, s.gh);
a.tallHint = s.atlas.tallHint;
std::vector<std::pair<int, int>> pos;
for (SlabSlot *it : items) {
int x, y;
if (!a.alloc(it->w, it->h, x, y)) return false;
pos.push_back({x, y});
}
for (size_t i = 0; i < items.size(); i++) {
items[i]->x = pos[i].first;
items[i]->y = pos[i].second;
}
s.atlas = a;
std::printf("atlas %s re-packed (%zu slabs placed again)\n", s.spec.name.c_str(), items.size());
return true;
}
bool place(Surface &s, SlabSlot &sl) {
int x, y;
if (s.atlas.alloc(sl.w, sl.h, x, y)) {
sl.x = x;
sl.y = y;
return true;
}
// reclaim the ghosts' cells early
if (!s.ghosts.empty()) {
for (auto &g : s.ghosts) s.atlas.release(g.x, g.y);
s.ghosts.clear();
if (s.atlas.alloc(sl.w, sl.h, x, y)) {
sl.x = x;
sl.y = y;
return true;
}
}
// compact (the texture never grows)
return repack(s, &sl);
}
// Brings the surface's slab cells in line with its spec. Existing cells
// never move; removed or resized slabs become ghosts for kGhostNs (still
// drawn, so a scene graph that lags behind shows the right glass).
// Returns true when the layout (texture size or announced UVs) changed.
bool syncSlots(Surface &s, uint64_t now) {
const SurfSpec &sp = s.spec;
const int bw = std::max(1, int(std::lround(sp.texW * opt.scale)));
const float scale = float(bw) / float(sp.texW);
const int bh = std::max(1, int(std::lround(sp.texH * scale)));
bool changed = false;
// The window (main) has its atlas from the start; other surfaces get
// one with their first slab (one texture resize) and keep it. Popups
// that never pop anything stay backdrop-sized.
const bool wantAtlas = s.atlasOn || sp.material == "window" || !sp.slabs.empty();
if (bw != s.bw || bh != s.bh || s.gh == 0 || wantAtlas != s.atlasOn || s.eyes != wantEyes(s)) {
s.bw = bw;
s.bh = bh;
s.scale = scale;
s.slots.clear();
s.ghosts.clear();
s.dropped.clear();
s.atlasOn = wantAtlas;
resizeTexture(s, bw, wantAtlas ? atlasHeight(bh) : bh);
s.atlas.reset(bw, bh + Atlas::kGutter, s.gh);
changed = true;
}
auto cellSize = [&](const SlabSpec &b, int &w, int &h) {
w = std::max(1, int(std::lround(b.w * s.scale)));
h = std::max(1, int(std::lround(b.h * s.scale)));
};
auto findSpec = [&](const std::string &id) -> const SlabSpec * {
for (const SlabSpec &b : sp.slabs)
if (b.id == id) return &b;
return nullptr;
};
// removed or resized -> ghosts; the rest take the new positions
for (size_t i = 0; i < s.slots.size();) {
SlabSlot &sl = s.slots[i];
const SlabSpec *b = findSpec(sl.s.id);
int w = 0, h = 0;
if (b) cellSize(*b, w, h);
if (b && w == sl.w && h == sl.h) {
if (b->x != sl.s.x || b->y != sl.s.y || b->hasPos != sl.s.hasPos) sl.stillSinceNs = now;
sl.s = *b;
setSlabPhase(sl, now);
i++;
continue;
}
sl.ghostUntilNs = now + kGhostNs;
s.ghosts.push_back(sl);
s.slots.erase(s.slots.begin() + long(i));
changed = true;
}
// new slabs, tallest first
std::vector<const SlabSpec *> fresh;
for (const SlabSpec &b : sp.slabs) {
bool have = false;
for (auto &sl : s.slots) have |= sl.s.id == b.id;
if (!have) fresh.push_back(&b);
}
std::stable_sort(fresh.begin(), fresh.end(), [](const SlabSpec *a, const SlabSpec *b) {
if (a->h != b->h) return a->h > b->h;
return a->id < b->id;
});
const int maxH = s.gh - (s.bh + Atlas::kGutter);
for (const SlabSpec *b : fresh) {
SlabSlot sl;
sl.s = *b;
sl.stillSinceNs = now;
setSlabPhase(sl, now);
cellSize(*b, sl.w, sl.h);
// a ghost of the same id and size comes back to life
bool revived = false;
for (size_t k = 0; k < s.ghosts.size(); k++) {
if (s.ghosts[k].s.id == b->id && s.ghosts[k].w == sl.w && s.ghosts[k].h == sl.h) {
SlabSlot g = s.ghosts[k];
g.s = *b;
g.ghostUntilNs = 0;
g.stillSinceNs = now;
setSlabPhase(g, now);
s.ghosts.erase(s.ghosts.begin() + long(k));
s.slots.push_back(g);
revived = changed = true;
break;
}
}
if (revived) continue;
if (sl.w > s.bw) { dropSlab(s, *b, sl.w, sl.h, "is wider than the texture"); continue; }
if (sl.h > maxH) { dropSlab(s, *b, sl.w, sl.h, "is taller than the atlas"); continue; }
if (!place(s, sl)) { dropSlab(s, *b, sl.w, sl.h, "does not fit the atlas"); continue; }
s.dropped.erase(b->id);
s.slots.push_back(sl);
changed = true;
}
// forget drop notes of slabs that left the spec
for (auto it = s.dropped.begin(); it != s.dropped.end();) it = findSpec(*it) ? std::next(it) : s.dropped.erase(it);
return changed;
}
// Materialize targets: covers and thick slabs (menus, sheets) ride the
// sheet springs, other slabs the linear 250/350 ms ramps.
void setSlabPhase(SlabSlot &sl, uint64_t now) {
sl.anim.set(sl.s.ph.phase, sl.s.material == "thick" || sl.s.dim(), sl.s.ph.ms, sl.s.ph.materialize ? 0.f : -1.f, now);
}
// Does this spec draw a cover (README, *Input*: shapes, or a window surface without shapes)?
static bool specCover(const SurfSpec &sp) { return sp.hasShapes ? !sp.shapes.empty() : sp.material == "window"; }
static bool sameShapes(const std::vector<ShapeSpec> &a, const std::vector<ShapeSpec> &b) {
if (a.size() != b.size()) return false;
for (size_t i = 0; i < a.size(); i++)
if (std::fabs(a[i].x - b[i].x) > .5f || std::fabs(a[i].y - b[i].y) > .5f || std::fabs(a[i].w - b[i].w) > .5f ||
std::fabs(a[i].h - b[i].h) > .5f || std::fabs(a[i].r - b[i].r) > .5f)
return false;
return true;
}
static const lgs_motion::Token *motionToken(const std::string &name) {
for (const lgs_motion::Token &k : lgs_motion::kTokens)
if (name == k.name) return &k;
return nullptr;
}
// An epoch time (ms, the reporter's Date.now()) on the monotonic clock; within 2 s of now, else now.
static uint64_t epochToMono(double ms, uint64_t now) {
timespec rt;
clock_gettime(CLOCK_REALTIME, &rt);
const double d = ms - (double(rt.tv_sec) * 1e3 + double(rt.tv_nsec) / 1e6);
if (!(std::fabs(d) < 2000.0)) return now;
return uint64_t(int64_t(now) + int64_t(d * 1e6));
}
// Morph progress (0 -> 1, with the spring's overshoot) at `now`.
float morphProgress(const Surface &s, uint64_t now) const {
const float ms = now > s.morphT0 ? float(double(now - s.morphT0) / 1e6) : 0.f;
if (spec.reduceMotion) return std::clamp(ms / lgs_motion::kReduceMs, 0.f, 1.f);
return lgs_motion::sample(*s.morphTok, ms);
}
// The cover's materialize target follows whether it has a cover: on a surface that stays on screen
// (main between a windowless route and a window route) the window glass materializes in on sheet-in
// and dematerializes out on sheet-out with the shapes it had, instead of popping. A surface that
// appears or goes as a whole is at its target at once (a phaseMs of 0 makes any change instant).
// A spec with "morph" moves the shapes from what is shown toward the new ones (morphProgress).
void setCoverPhase(Surface &s, const SurfSpec &ns, uint64_t now) {
const bool init = s.anim.init;
const bool wasOn = s.spec.visible || s.spec.armed, on = ns.visible || ns.armed;
const bool wasShown = init && wasOn && specCover(s.spec);
const bool shown = on && specCover(ns);
const bool kept = init && wasOn && on;
std::vector<ShapeSpec> before;
if (init && wasShown) before = coverShapes(s);
const std::vector<ShapeSpec> oldTarget = s.spec.shapes;
const bool wasVisible = init && wasOn;
s.spec = ns;
if (kept && wasShown && !shown) s.fadeShapes = before;
else if (shown || !kept) s.fadeShapes.clear();
if (!kept) s.anim.init = false;
if (wasVisible && !on) s.clearPending = true;
s.anim.set(shown ? ns.ph.phase : 0.f, true, ns.ph.ms, ns.ph.materialize ? 0.f : -1.f, now);
// appearing with the page's open animation: the ramp runs from when that started. Also on a
// surface that stays on screen and gains its cover (main: Home -> Library), so the window glass
// springs in from the route change, not from when the report reached glassd
if ((!kept || !wasShown) && shown && ns.ph.materialize && ns.ph.atMs > 0) s.anim.t0 = epochToMono(ns.ph.atMs, now);
if (!(kept && wasShown && shown) || sameShapes(oldTarget, ns.shapes)) {
if (!(kept && wasShown && shown)) s.morphTok = nullptr;
return; // nothing moved: a running morph goes on
}
const lgs_motion::Token *tok = ns.hasMorph ? motionToken(ns.morphToken) : nullptr;
if (tok && before.size() == ns.shapes.size() && !before.empty()) {
s.morphFrom = before;
s.morphTok = tok;
s.morphT0 = epochToMono(ns.morphAtMs, now);
} else {
s.morphTok = nullptr; // the shapes jump
}
}
// Plates: window and thick plates ride the sheet springs like covers,
// the others (liquid discs, panel tiles, dim) the linear ramps like slabs.
static bool plateSprings(const PlateSpec &p) { return p.material == "window" || p.material == "thick"; }
// Brings the surface's plate states in line with its spec (by id, in spec order).
void syncPlates(Surface &s, uint64_t now) {
std::vector<PlateSlot> next;
next.reserve(s.spec.plates.size());
for (const PlateSpec &p : s.spec.plates) {
PlateSlot slot;
double exitWas = 0;
for (auto &old : s.plates)
if (old.s.id == p.id) {
slot.anim = old.anim;
slot.exitT0 = old.exitT0;
exitWas = old.s.exitAtMs;
break;
}
const bool fresh = !slot.anim.init;
slot.s = p;
slot.anim.set(p.ph.phase, plateSprings(p), p.ph.ms, p.ph.materialize ? 0.f : -1.f, now);
// a new plate materializing with the page's own animation: the ramp runs from when that started
if (fresh && p.ph.materialize && p.ph.atMs > 0) slot.anim.t0 = epochToMono(p.ph.atMs, now);
// its content fading out: from when that started (kept while the spec repeats the same fade)
if (p.exitAtMs <= 0) slot.exitT0 = 0;
else if (!slot.exitT0 || exitWas != p.exitAtMs) slot.exitT0 = epochToMono(p.exitAtMs, now);
next.push_back(slot);
}
s.plates.swap(next);
}
// Advances every materialize animation; true while any still moves.
bool stepPhases(uint64_t now) {
bool moving = false;
for (auto &s : surfaces) {
moving |= s->anim.step(now, spec.reduceMotion);
if (s->morphTok) {
const float ms = now > s->morphT0 ? float(double(now - s->morphT0) / 1e6) : 0.f;
if (ms >= (spec.reduceMotion ? lgs_motion::kReduceMs : s->morphTok->settleMs)) {
s->morphTok = nullptr;
s->morphFrom.clear();
} else {
moving = true;
}
}
for (auto &sl : s->slots) moving |= sl.anim.step(now, spec.reduceMotion);
for (auto &sl : s->ghosts) moving |= sl.anim.step(now, spec.reduceMotion);
for (auto &p : s->plates) {
moving |= p.anim.step(now, spec.reduceMotion);
if (p.exitT0 && now < p.exitT0 + uint64_t(double(p.s.exitMs) * 1e6)) moving = true;
}
}
moving |= roomDimAnim.step(now, spec.reduceMotion);
moving |= stepTune(now);
return moving;
}
// A colour as the glass body's hue: linear rgb at luminance 1 (glass.frag's luma), no channel over 4.
static void hueLin(const Color &c, float &r, float &g, float &b) {
r = srgbToLin1(c.r), g = srgbToLin1(c.g), b = srgbToLin1(c.b);
const float l = std::max(1e-3f, 0.2126f * r + 0.7152f * g + 0.0722f * b);
r /= l, g /= l, b /= l;
const float mx = std::max({r, g, b});
if (mx > 4.f) r *= 4.f / mx, g *= 4.f / mx, b *= 4.f / mx;
}
// Eases the drawn dial and tune toward the spec's; true while they still move. The first spec, and any
// under Reduce Motion, applies at once.
bool stepTune(uint64_t now) {
TuneLive &L = tuneLive;
const Tune &T = spec.tune;
float hr = L.hr, hg = L.hg, hb = L.hb;
if (T.hue.set) hueLin(T.hue, hr, hg, hb);
const float tgt[8] = {std::clamp(spec.dial, 0.f, 1.f), T.refract, T.frost, T.light, T.hue.set ? T.hueK : 0.f, hr, hg, hb};
float *cur[8] = {&L.dial, &L.refract, &L.frost, &L.light, &L.hueK, &L.hr, &L.hg, &L.hb};
const float dt = tuneNs && now > tuneNs ? std::min(0.1f, float(double(now - tuneNs) / 1e9)) : 0.f;
tuneNs = now;
if (!L.init || spec.reduceMotion) {
for (int i = 0; i < 8; i++) *cur[i] = tgt[i];
L.init = true;
return false;
}
const float a = 1.f - std::exp(-dt / kTuneTau);
bool moving = false;
for (int i = 0; i < 8; i++) {
const float d = tgt[i] - *cur[i];
if (std::fabs(d) < 1e-3f) {
*cur[i] = tgt[i];
} else {
*cur[i] += d * a;
moving = true;
}
}
return moving;
}
float dialNow() const { return tuneLive.init ? tuneLive.dial : std::clamp(spec.dial, 0.f, 1.f); }
float phaseOf(const PhaseAnim &a) const { return opt.phasePin >= 0 ? std::clamp(opt.phasePin, 0.f, 1.f) : a.init ? a.x : 1.f; }
// A leaving plate's share of coverage at `now`: 1 -> 0 linearly over its content's fade-out.
static float exitAlpha(const PlateSlot &p, uint64_t now) {
if (!p.exitT0 || now <= p.exitT0) return 1.f;
if (p.s.exitMs <= 0.f) return 0.f;
return std::clamp(1.f - float(double(now - p.exitT0) / 1e6) / p.s.exitMs, 0.f, 1.f);
}
// A plate's materialize ramps with its exit fade folded into coverage and shadow.
Phased platePhase(const PlateSlot &p, uint64_t now) const {
Phased ph = phaseMap(phaseOf(p.anim), spec.reduceMotion);
const float e = exitAlpha(p, now);
ph.alpha *= e;
ph.shadow *= e;
return ph;
}
float roomDimNow() const { return roomDimAnim.init ? std::clamp(roomDimAnim.x, 0.f, 0.9f) : 0.f; }
void expireGhosts(uint64_t now) {
for (auto &s : surfaces)
for (size_t i = 0; i < s->ghosts.size();) {
if (now >= s->ghosts[i].ghostUntilNs) {
s->atlas.release(s->ghosts[i].x, s->ghosts[i].y);
s->ghosts.erase(s->ghosts.begin() + long(i));
} else {
i++;
}
}
}
bool ensureBuffers(Surface &s) {
for (auto &b : s.bufs) {
if (b.bo) continue;
if (!b.create(gfx, s.gw * s.eyes, s.gh)) return false;
vr::DmabufAttributes_t a{};
a.pNext = nullptr;
a.unWidth = uint32_t(s.gw * s.eyes);
a.unHeight = uint32_t(s.gh);
a.unDepth = a.unMipLevels = a.unArrayLayers = a.unSampleCount = 1;
a.unFormat = GBM_FORMAT_ABGR8888;
a.ulModifier = DRM_FORMAT_MOD_LINEAR;
a.unPlaneCount = 1;
a.plane[0].unOffset = b.offset;
a.plane[0].unStride = b.stride;
a.plane[0].nFd = b.fd;
vr::SharedTextureHandle_t h = 0;
if (!vr::VRIPCResourceManager()->ImportDmabuf(vr::VRApplication_Overlay, &a, &h) || !h) {
std::printf("ImportDmabuf failed for %s (%dx%d)\n", s.key.c_str(), s.gw, s.gh);
b.destroy(gfx);
return false;
}
b.vrHandle = h;
}
// The mouse scale is set together with the first texture of this size
// (renderAll), not here: the scene graph sizes the panel from it.
return true;
}
void applySpec(const Spec &ns) {
spec = ns;
haveSpec = true;
const uint64_t now = monoNowNs();
// test runs (dumps): when each spec took effect, for tools/test_phase.sh
if (!opt.dumpDir.empty()) std::printf("spec seq %lld applied (t=%.4f)\n", ns.seq, double(now) / 1e9);
// v3 G7: the room dim rides sheet-in / sheet-out (a first spec applies it at once)
roomDimAnim.set(ns.roomDim, true, -1, -1, now);
// drop surfaces that left the spec
for (size_t i = 0; i < surfaces.size();) {
bool keep = false;
for (auto &s : ns.surfaces) keep |= s.name == surfaces[i]->spec.name;
if (!keep) {
std::printf("surface %s removed\n", surfaces[i]->spec.name.c_str());
releaseSurface(*surfaces[i], true);
surfaces.erase(surfaces.begin() + long(i));
} else {
i++;
}
}
for (const SurfSpec &ss : ns.surfaces) {
Surface *s = nullptr;
for (auto &e : surfaces)
if (e->spec.name == ss.name) s = e.get();
if (!s) {
surfaces.push_back(std::make_unique<Surface>());
s = surfaces.back().get();
s->key = opt.keyPrefix + sanitizeKey(ss.name);
}
if (s->spec.overlayKey != ss.overlayKey) {
s->steam = vr::k_ulOverlayHandleInvalid;
s->findNs = 0;
s->geo = s->rawGeo = Geometry{};
s->geoOk = false;
}
if (s->spec.hasShapes != ss.hasShapes || s->spec.material != ss.material || s->spec.shapes.size() != ss.shapes.size())
s->warnedShapes = false;
if (!ss.droppedPlates.empty() && ss.droppedPlates != s->spec.droppedPlates)
std::printf("surface %s: %zu plates not drawn (more than %d, or outside the texture)\n", ss.name.c_str(),
ss.droppedPlatesN, kMaxPlates);
setCoverPhase(*s, ss, now); // sets s->spec
syncPlates(*s, now);
syncSlots(*s, now);
ensureOverlay(*s);
ensureBuffers(*s);
}
// The new layout is announced after the next (priming) render, so the
// scene graph never points at buffers that hold nothing yet.
layoutDirty = true;
roomDirty = true;
outPending = true;
outPendingSinceNs = now;
}
void checkSpec() {
if (opt.demo) return;
struct stat st {};
if (stat(opt.spec.c_str(), &st) != 0) return;
if (haveSpec && st.st_mtim.tv_sec == specStat.st_mtim.tv_sec && st.st_mtim.tv_nsec == specStat.st_mtim.tv_nsec &&
st.st_size == specStat.st_size && st.st_ino == specStat.st_ino)
return;
std::string text, err;
if (!readFile(opt.spec, text)) return;
Spec ns;
if (!parseSpec(text, ns, err)) {
if (err != specError) std::printf("%s: %s (keeping the previous spec)\n", opt.spec.c_str(), err.c_str());
specError = err;
return; // retried on the next check (a writer may be mid-write)
}
specError.clear();
specStat = st;
applySpec(ns);
}
// ---------------------------------------------------------- geometry
bool fetchGeometry(vr::VROverlayHandle_t h, float texW, float texH, Geometry &g) {
vr::HmdVector2_t ms{};
if (vr::VROverlay()->GetOverlayMouseScale(h, &ms) != vr::VROverlayError_None) return false;
const float mw = ms.v[0] > 1 ? ms.v[0] : texW, mh = ms.v[1] > 1 ? ms.v[1] : texH;
vr::HmdMatrix34_t a{}, b{}, c{};
auto *ov = vr::VROverlay();
if (ov->GetTransformForOverlayCoordinates(h, vr::TrackingUniverseStanding, {0.f, 0.f}, &a) != vr::VROverlayError_None ||
ov->GetTransformForOverlayCoordinates(h, vr::TrackingUniverseStanding, {mw, 0.f}, &b) != vr::VROverlayError_None ||
ov->GetTransformForOverlayCoordinates(h, vr::TrackingUniverseStanding, {0.f, mh}, &c) != vr::VROverlayError_None)
return false;
// origin bottom-left, y up: a = bottom-left, b = bottom-right, c = top-left
v3 BL{a.m[0][3], a.m[1][3], a.m[2][3]}, BR{b.m[0][3], b.m[1][3], b.m[2][3]}, TL{c.m[0][3], c.m[1][3], c.m[2][3]};
g.O = TL;
g.U = (BR - BL) * (1.f / texW);
g.V = (BL - TL) * (1.f / texH);
g.mpp = length(g.U);
if (!std::isfinite(g.mpp) || g.mpp < 1e-6f || length(g.V) < 1e-6f || g.mpp > 0.05f) return false;
g.N = normalize(cross(g.V, g.U));
g.valid = true;
g.fallback = false;
return true;
}
// Why a quad cannot be where Steam's panel shows, or nullptr. An overlay
// without a world placement reports overlay-local coordinates (a quad at
// the origin), and a hidden one can sit anywhere.
const char *implausible(const Geometry &g, float texW, float texH, const Pose &head) const {
const v3 mid = g.O + g.U * (texW * 0.5f) + g.V * (texH * 0.5f);
if (length(mid) < 0.05f) return "quad at the origin";
if (!haveLastHead) return nullptr;
const v3 toHead = head.t - mid;
const float dist = length(toHead);
if (dist < 0.3f || dist > 5.f) return "quad not 0.3-5 m from the head";
if (std::fabs(dot(normalize(toHead), normalize(cross(g.V, g.U)))) < 0.2f) return "quad edge-on to the head";
return nullptr;
}
// In front of the head at 1.43 m (the measured dashboard distance); used
// when Steam's overlay can't be read, e.g. for headless dumps.
Geometry fallbackGeometry(const Surface &s, const Pose &head) {
Geometry g;
v3 fwd = head.rotate({0, 0, -1});
fwd.y = 0;
fwd = length(fwd) > 1e-3f ? normalize(fwd) : v3{0, 0, -1};
v3 right = normalize(cross(fwd, {0, 1, 0}));
const float mpp = 0.98f / 1920.f;
v3 centre = head.t + fwd * 1.43f;
g.U = right * mpp;
g.V = v3{0, -1, 0} * mpp;
g.O = centre - g.U * (s.spec.texW * 0.5f) - g.V * (s.spec.texH * 0.5f);
g.N = -fwd;
g.mpp = mpp;
g.valid = true;
g.fallback = true;
return g;
}
// Steam's popups (bar, footer, ...) report overlay transforms whose scale
// disagrees with where the scene graph draws them: the bar claims
// 0.2 mm/px against main's 0.51, although its scene-graph panel uses the
// same metres-per-pixel as the window. Keep the reported centre and
// orientation, but use the main window's scale. Returns the ratio found.
float rescaleToMain(Geometry &g, float texW, float texH) const {
if (mainMpp <= 0) return 1.f;
const float ratio = g.mpp / mainMpp;
if (ratio > 0.92f && ratio < 1.087f) return 1.f;
const v3 mid = g.O + g.U * (texW * 0.5f) + g.V * (texH * 0.5f);
g.U = normalize(g.U) * mainMpp;
g.V = normalize(g.V) * mainMpp;
g.O = mid - g.U * (texW * 0.5f) - g.V * (texH * 0.5f);
g.mpp = mainMpp;
return ratio;
}
void faceHead(Geometry &g, float texW, float texH, const Pose &head) {
const v3 mid = g.O + g.U * (texW * 0.5f) + g.V * (texH * 0.5f);
if (dot(g.N, head.t - mid) < 0) g.N = -g.N;
}
// Brings s.geo up to date. With fetch = false (priming renders while the
// dashboard is hidden) it never asks SteamVR and only fills a fallback.
void refreshGeometry(Surface &s, const Pose &head, uint64_t now, bool fetch) {
const float tw = float(s.spec.texW), th = float(s.spec.texH);
if (s.spec.hasQuad) {
Geometry g;
g.O = s.spec.qO;
g.U = s.spec.qU;
g.V = s.spec.qV;
g.mpp = length(g.U);
g.N = normalize(cross(g.V, g.U));
g.valid = g.mpp > 1e-6f && length(g.V) > 1e-6f;
if (g.valid) {
s.geo = s.rawGeo = g;
s.geoOk = true;
s.rescaled = false;
s.geoOkNs = now;
s.geoNote = "spec";
faceHead(s.geo, tw, th, head);
return;
}
}
if (opt.testPattern >= 0) {
// Test backdrop: the window 1.43 m ahead of the fixed test head,
// at the window's nominal 0.98 m width (moving the eye with
// --test-head does not move the window).
Geometry g;
const float mpp = 0.98f / 1920.f;
g.U = {mpp, 0, 0};
g.V = {0, -mpp, 0};
g.N = {0, 0, 1};
g.O = kTestEye + v3{0, 0, -1.43f} - g.U * (tw * 0.5f) - g.V * (th * 0.5f);
g.mpp = mpp;
g.valid = true;
s.geo = s.rawGeo = g;
s.geoOk = true;
s.rescaled = false;
s.geoOkNs = now;
s.geoNote = "test";
if (s.spec.overlayKey == kMainKey) mainMpp = mpp;
return;
}
if (!fetch) {
if (!s.geo.valid) {
s.geo = fallbackGeometry(s, head);
s.geoNote = "fallback(not fetched)";
}
faceHead(s.geo, tw, th, head);
return;
}
// (Re)find Steam's overlay: every 1 s while unknown, and every 2 s
// re-validate the handle (Steam's overlays come back with new handles
// after a Steam restart).
if (!s.spec.overlayKey.empty() && now - s.findNs > (s.steam == vr::k_ulOverlayHandleInvalid ? 1000000000ull : 2000000000ull)) {
s.findNs = now;
vr::VROverlayHandle_t h = vr::k_ulOverlayHandleInvalid;
if (vr::VROverlay()->FindOverlay(s.spec.overlayKey.c_str(), &h) != vr::VROverlayError_None) h = vr::k_ulOverlayHandleInvalid;
if (h != s.steam && s.steam != vr::k_ulOverlayHandleInvalid && h != vr::k_ulOverlayHandleInvalid)
std::printf("%s: Steam overlay %s has a new handle\n", s.spec.name.c_str(), s.spec.overlayKey.c_str());
s.steam = h;
}
Geometry g;
const char *why = nullptr;
if (s.spec.overlayKey.empty()) why = "no overlay key";
else if (s.steam == vr::k_ulOverlayHandleInvalid) why = "no overlay";
else if (!fetchGeometry(s.steam, tw, th, g)) {
why = "no transform";
// a stale handle errors out: find it again (rate-limited above)
s.steam = vr::k_ulOverlayHandleInvalid;
} else {
why = implausible(g, tw, th, head); // the handle is fine, the overlay is not placed
}
if (!why) {
const bool isMain = s.spec.overlayKey == kMainKey;
if (isMain) mainMpp = g.mpp;
s.rawGeo = g;
const float ratio = isMain || s.spec.scaleFromOverlay ? 1.f : rescaleToMain(g, tw, th);
s.rescaled = ratio != 1.f;
s.geo = g;
s.geoOk = true;
s.geoOkNs = now;
if (s.rescaled) {
char b[64];
std::snprintf(b, sizeof b, "steam(rescaled x%.2f)", double(1.f / ratio));
s.geoNote = b;
} else {
s.geoNote = "steam";
}
} else {
s.geoOk = false;
if (s.geo.valid && !s.geo.fallback) {
s.geoNote = std::string("stale(") + why + ")";
} else {
s.geo = fallbackGeometry(s, head);
s.geoNote = std::string("fallback(") + why + ")";
}
}
if (s.geoNote != s.geoLogged) {
const v3 mid = s.geo.O + s.geo.U * (tw * 0.5f) + s.geo.V * (th * 0.5f);
std::printf("geometry %s: %s (centre %.2f %.2f %.2f, %.2f x %.2f m)\n", s.spec.name.c_str(), s.geoNote.c_str(), mid.x,
mid.y, mid.z, length(s.geo.U) * tw, length(s.geo.V) * th);
s.geoLogged = s.geoNote;
}
faceHead(s.geo, tw, th, head);
}
// A surface quad as a feed mask; zone = extra metres beyond the margin:
// side, top, bottom.
// Returns false when the quad did not fit (kMaxMasks): the map must not
// integrate then, or it would take in UI.
static bool addMask(std::vector<MaskQuad> &out, const Geometry &g, float texW, float texH, const float zone[3], float mg) {
if (!g.valid || g.fallback) return true;
if (out.size() >= size_t(kMaxMasks)) return false;
MaskQuad m;
float lu = length(g.U), lv = length(g.V);
m.O = g.O;
m.U = g.U * (1.f / lu);
m.V = g.V * (1.f / lv);
m.u0 = -mg - zone[0];
m.u1 = texW * lu + mg + zone[0];
m.v0 = -mg - zone[1];
m.v1 = texH * lv + mg + zone[2];
out.push_back(m);
return true;
}
// v3 extra masks: a rect of a surface (Steam px, at dz units toward the
// viewer), or a world quad (corner plus full edge vectors).
static bool addRectMask(std::vector<MaskQuad> &out, const Geometry &g, const MaskSpec &k, float dzM, float mg) {
if (!g.valid || g.fallback) return true;
Geometry r = g;
r.O = g.O + g.U * k.x + g.V * k.y + g.N * dzM;
const float none[3] = {0, 0, 0};
return addMask(out, r, k.w, k.h, none, mg);
}
static bool addWorldMask(std::vector<MaskQuad> &out, const WorldMask &w, float mg) {
Geometry g;
g.valid = true;
g.O = w.O;
g.U = w.U;
g.V = w.V;
const float none[3] = {0, 0, 0};
return addMask(out, g, 1.f, 1.f, none, mg);
}
// --test-backdrop: fill the room map with the procedural room once the
// spec is known; room-hole leaves what the UI would hide from the feed
// unknown (the same masks as live, seen from the map centre).
void makeTestRoom(const Pose &head, uint64_t now) {
std::vector<MaskQuad> hole;
const float none[3] = {0, 0, 0};
for (auto &s : surfaces) {
if (!s->spec.visible) continue;
refreshGeometry(*s, head, now, false);
const bool isMain = s->spec.overlayKey == kMainKey;
addMask(hole, s->geo, float(s->spec.texW), float(s->spec.texH), isMain ? opt.zone : none, opt.margin);
for (const MaskSpec &k : s->spec.masks) addRectMask(hole, s->geo, k, k.dz * spec.unitM, opt.margin);
}
for (const WorldMask &w : spec.masks) addWorldMask(hole, w, opt.margin);
room.generate(progTest, opt.testPattern, opt.testHole, hole);
float known = 0, lum = 0;
room.stats(known, lum);
envLuma = std::max(0.06f, lum);
envSet = true;
std::printf("test room %s%s: %.0f%% known, mean luma %.2f\n", opt.testPattern == 1 ? "stripes" : "room",
opt.testHole ? " (hole behind the UI)" : "", double(known * 100), double(lum));
testRoomDone = true;
roomDirty = true;
if (!firstRoomNs) firstRoomNs = now;
}
// Steam surfaces as seen by the feed, from geometry fetched now. Only while
// the dashboard shows them. masksComplete says whether every visible spec
// surface got a mask; the room map does not integrate while it is false.
void updateMasks(bool dash, const Pose &head, uint64_t now) {
if (now - masksNs < kMaskPeriodNs) return;
masksNs = now;
masks.clear();
masksBuiltNs = now;
masksComplete = true;
if (!dash || opt.noMask) return;
bool fits = true;
auto add = [&](const Geometry &g, float texW, float texH, const float zone[3], float marginScale) {
fits &= addMask(masks, g, texW, texH, zone, opt.margin * marginScale);
};
// The main window gets a wider "dashboard zone": SteamVR's own panels
// (grab bar, frame controls, frame menus, side panels) sit around it and
// glassd cannot see them. A rescaled popup is masked at both its
// reported and its rescaled quad, with twice the margin.
const float none[3] = {0, 0, 0};
auto addSurface = [&](const Geometry &g, const Geometry &raw, bool rescaled, const std::string &key, float tw, float th) {
const bool isMain = key == kMainKey;
add(g, tw, th, isMain ? opt.zone : none, rescaled ? 2.f : 1.f);
if (rescaled) add(raw, tw, th, none, 2.f);
};
std::vector<std::string> done;
int missing = 0;
// main first, so popups rescale to this moment's main scale
std::vector<Surface *> order;
for (auto &s : surfaces)
if (s->spec.overlayKey == kMainKey) order.insert(order.begin(), s.get());
else order.push_back(s.get());
for (Surface *s : order) {
if (!s->spec.visible) continue;
refreshGeometry(*s, head, now, true);
if (!s->geoOk) {
missing++;
continue;
}
addSurface(s->geo, s->rawGeo, s->rescaled, s->spec.overlayKey, float(s->spec.texW), float(s->spec.texH));
done.push_back(s->spec.overlayKey);
}
// Steam's fixed surfaces, if visible and not masked above.
static const char *kSteam[] = {kMainKey, "valve.steam.gamepadui.bar", "valve.steam.gamepadui.floatingfooter",
"valve.steam.gamepadui.keyboard", "valve.steam.gamepadui.notifications",
"valve.steam.gamepadui.volumelevel"};
for (const char *k : kSteam) {
if (std::find(done.begin(), done.end(), k) != done.end()) continue;
vr::VROverlayHandle_t h;
if (vr::VROverlay()->FindOverlay(k, &h) != vr::VROverlayError_None || !vr::VROverlay()->IsOverlayVisible(h)) continue;
vr::HmdVector2_t ms{};
vr::VROverlay()->GetOverlayMouseScale(h, &ms);
if (ms.v[0] < 2 || ms.v[1] < 2) continue;
Geometry g;
if (!fetchGeometry(h, ms.v[0], ms.v[1], g) || implausible(g, ms.v[0], ms.v[1], head)) continue;
const bool isMain = std::strcmp(k, kMainKey) == 0;
if (isMain) mainMpp = g.mpp;
Geometry raw = g;
const bool rescaled = !isMain && rescaleToMain(g, ms.v[0], ms.v[1]) != 1.f;
addSurface(g, raw, rescaled, k, ms.v[0], ms.v[1]);
done.push_back(k);
// a spec surface that failed above but is covered here is fine
for (Surface *s : order)
if (s->spec.visible && !s->geoOk && s->spec.overlayKey == k) missing--;
}
// v3 extra masks: rects of the spec surfaces (ornaments beside the
// window), then world quads
for (Surface *s : order) {
if (!s->spec.visible || !s->geoOk) continue;
for (const MaskSpec &k : s->spec.masks) fits &= addRectMask(masks, s->geo, k, k.dz * spec.unitM, opt.margin);
}
for (const WorldMask &w : spec.masks) fits &= addWorldMask(masks, w, opt.margin);
if (!fits && !masksOverflowLogged) {
std::printf("more than %d feed mask quads; the room map waits until they fit\n", kMaxMasks);
masksOverflowLogged = true;
}
masksComplete = missing <= 0 && fits;
}
// -------------------------------------------------------------- poses
bool samplePose(float predictS, Pose &out) {
vr::TrackedDevicePose_t p[1];
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, predictS, p, 1);
if (!p[0].bPoseIsValid) return false;
out = fromVR(p[0].mDeviceToAbsoluteTracking);
return true;
}
bool poseAt(uint64_t ns, Pose &out) {
if (hist.empty()) return false;
if (ns <= hist.front().first) { out = hist.front().second; return true; }
if (ns >= hist.back().first) { out = hist.back().second; return true; }
for (size_t i = 1; i < hist.size(); i++) {
if (hist[i].first >= ns) {
const auto &a = hist[i - 1], &b = hist[i];
float t = float(double(ns - a.first) / double(std::max<uint64_t>(1, b.first - a.first)));
out = poseLerp(a.second, b.second, t);
return true;
}
}
out = hist.back().second;
return true;
}
// ---------------------------------------------------------- room map
void updateRoom(bool dash, uint64_t now) {
if (!feed.latest(frame, haveSeq)) return;
haveSeq = frame.seq;
envLuma = envSet ? envLuma + (std::max(0.06f, frame.meanLuma) - envLuma) * 0.1f : std::max(0.06f, frame.meanLuma);
envSet = true;
// Not around a dashboard change (the UI fades in or out and SteamVR
// places the window again), and while the dashboard shows only with
// masks built from geometry fetched after that.
const uint64_t settled = dashChangeNs + kGraceNs;
if (now < settled || frame.recvNs < settled ||
(dash && !opt.noMask && !(masksComplete && masksBuiltNs >= settled))) {
roomSkipped++;
return;
}
Pose hmd;
const uint64_t target = frame.recvNs - uint64_t(cal.latencyMs * 1e6);
if (!poseAt(target, hmd)) return;
const uint64_t t0 = monoNowNs();
roomTimer.begin();
// While streaming, frames come 30 times a second: blend slowly. While
// the dashboard is hidden they are sparse shots: each one counts more,
// so a changed room (lights on) is caught within a few shots.
// while the glass shows, a new frame carries most of the texel: at 0.22 a moving hand or a change behind
// the glass took ~200 ms to come through, smeared (the background lagged); 0.45 still averages sensor noise
room.ema = dash ? 0.45f : 0.6f;
room.uploadFeed(frame);
room.integrate(hmd * eyeToHead, cal, masks);
room.fill();
roomTimer.end();
roomCpuMs = float(double(monoNowNs() - t0) / 1e6);
roomDirty = true;
roomWindow++;
if (!firstRoomNs) firstRoomNs = now;
}
// ------------------------------------------------------------ render
// Metres per Steam texture pixel: the main window's, as SteamVR reports it.
float designMpp() const { return mainMpp > 0 ? mainMpp : 0.98f / 1920.f; }
// The cover's shapes in Steam px; empty = no cover.
std::vector<ShapeSpec> coverShapes(Surface &s) {
std::vector<ShapeSpec> v;
if (s.spec.hasShapes) {
v = s.spec.shapes;
if (v.size() > size_t(kMaxShapes)) {
if (!s.warnedShapes) std::printf("surface %s: %zu cover shapes, drawing the first %d\n", s.spec.name.c_str(), v.size(), kMaxShapes);
s.warnedShapes = true;
v.resize(size_t(kMaxShapes));
}
} else if (s.spec.material == "window") {
v.push_back({0, 0, float(s.spec.texW), float(s.spec.texH), s.spec.radius});
} else if (!s.warnedShapes) {
std::printf("surface %s (material %s) came without cover shapes; drawing no cover\n", s.spec.name.c_str(),
s.spec.material.c_str());
s.warnedShapes = true;
}
// a cover that went away dematerializes with the shapes it had
if (v.empty() && !s.fadeShapes.empty()) {
if (phaseOf(s.anim) > 0.002f) return s.fadeShapes;
s.fadeShapes.clear();
}
// a morph: from the shapes shown when it began toward the spec's
if (s.morphTok && s.morphFrom.size() == v.size()) {
const float t = morphProgress(s, monoNowNs());
for (size_t i = 0; i < v.size(); i++) {
const ShapeSpec &a = s.morphFrom[i], &b = v[i];
v[i] = {a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, std::max(1.f, a.w + (b.w - a.w) * t),
std::max(1.f, a.h + (b.h - a.h) * t), std::max(0.f, a.r + (b.r - a.r) * t)};
}
}
return v;
}
// One piece of glass: the union of `shapes` (region-local Steam px) in the
// region (rx, ry, rw, rh) of the current target. m is already sized
// (materialFor), ph the materialize ramps. under: a slab that sees the
// cover copy behind it. casters: slab shadows and holes to draw on it.
// dz in metres.
void drawGlass(Surface &s, const Geometry &g, const Pose &head, const Material &m, const Phased &ph, int rx, int ry, int rw,
int rh, v3 elem, const std::vector<ShapeSpec> &shapes, float dz, bool under, const std::vector<Caster> &casters,
int pass = 0, float scale = 0.f, const Look &look = Look{}) {
if (shapes.empty()) return;
Program &p = progGlass;
glViewport(rx, ry, rw, rh);
p.set("uOrigin", float(rx), float(ry));
p.set("uScale", scale > 0 ? scale : s.scale);
p.set("uPass", pass);
p.set("uElemOff", elem.x, elem.y);
const int n = std::min(int(shapes.size()), kMaxShapes);
float sh[4 * kMaxShapes] = {}, rads[kMaxShapes] = {};
float x0 = 1e9f, y0 = 1e9f, x1 = -1e9f, y1 = -1e9f, minHalf = 1e9f;
for (int i = 0; i < n; i++) {
const ShapeSpec &q = shapes[size_t(i)];
sh[i * 4] = q.x + q.w * 0.5f;
sh[i * 4 + 1] = q.y + q.h * 0.5f;
sh[i * 4 + 2] = q.w * 0.5f;
sh[i * 4 + 3] = q.h * 0.5f;
rads[i] = std::min(q.r, 0.5f * std::min(q.w, q.h));
x0 = std::min(x0, q.x);
y0 = std::min(y0, q.y);
x1 = std::max(x1, q.x + q.w);
y1 = std::max(y1, q.y + q.h);
minHalf = std::min(minHalf, 0.5f * std::min(q.w, q.h));
}
glUniform1i(p.loc("uNS"), n);
glUniform4fv(p.loc("uShapes"), n, sh);
glUniform1fv(p.loc("uRads"), n, rads);
p.set("uShapeC", 0.5f * (x0 + x1), 0.5f * (y0 + y1));
p.set("uHalf", 0.5f * (x1 - x0), 0.5f * (y1 - y0));
p.set("uO", g.O.x, g.O.y, g.O.z);
p.set("uU", g.U.x, g.U.y, g.U.z);
p.set("uV", g.V.x, g.V.y, g.V.z);
p.set("uN", g.N.x, g.N.y, g.N.z);
p.set("uDz", dz);
p.set("uCoverDz", s.spec.coverDz * spec.unitM);
p.set("uEye", head.t.x, head.t.y, head.t.z);
// Material widths are converted with one design scale for all surfaces:
// Steam's panels share a metres-per-pixel in the scene graph.
const float mpp = designMpp();
p.set("uBezel", std::max(1.f, std::min(m.bezelM / mpp, minHalf)));
p.set("uThick", m.thick);
p.set("uLensThick", m.lensThick);
// the wearer's tune (eased): refraction, frost (at most mip 6: 6.5 is the coarse tone read), light,
// and the body colour, which also deepens the tint a little so coloured glass reads as coloured
const TuneLive &tu = tuneLive;
const float light = look.occluder ? 1.f : tu.light;
p.set("uLens", m.lensDeg * tu.refract * ph.lens * 0.0174533f);
p.set("uDisp", m.disp * tu.refract);
p.set("uFrost", std::min(6.f, m.frost * tu.frost) * ph.frost);
p.set("uEdgeFrost", std::min(6.f, m.edgeFrost * tu.frost) * ph.frost);
p.set("uEdgeClear", m.edgeClear);
p.set("uTintA", std::min(0.95f, m.tintA + 0.2f * tu.hueK) * ph.tint);
p.set("uHue", tu.hr, tu.hg, tu.hb, tu.hueK);
p.set("uBandMid", 0.314f); // L 80 of 255: the middle of text-bearing glass (DESIGN2 §6.3)
p.set("uBandK", 1.f + (m.bandK - 1.f) * ph.tint);
// v3 G7: the room dim folds into the backdrop dimming of glass that sees the room; a slab
// over its cover sees the cover, which is dimmed already
p.set("uDim", 1.f - (1.f - m.dim * ph.tint) * (1.f - (under ? 0.f : roomDimNow())));
p.set("uSpec", m.spec * light * ph.light);
p.set("uGloss", m.gloss);
p.set("uFill", m.fill);
p.set("uFres", m.fres * light * ph.light);
p.set("uSheen", m.sheen * light * ph.tint);
p.set("uDark", m.dark * ph.shade);
p.set("uDarkW", std::max(2.f, m.darkW / mpp));
p.set("uOcc", m.occ * ph.shade);
const float contact = look.occluder ? 0.f : look.shadow >= 0 ? look.shadow : m.shadow;
p.set("uShadow", contact * ph.shadow);
p.set("uShadowW", 0.006f / mpp);
p.set("uShadowY", 0.003f / mpp);
p.set("uAlpha", ph.alpha);
p.set("uUnder", under ? 1 : 0);
// v3 looks
if (look.tint.set && look.tint.a > 0)
p.set("uTintC", srgbToLin1(look.tint.r), srgbToLin1(look.tint.g), srgbToLin1(look.tint.b), std::min(0.9f, look.tint.a) * ph.tint); // never fully opaque paint: the glass stays glass
else
p.set("uTintC", 0.f, 0.f, 0.f, 0.f);
const float fa = look.fill.set ? look.fill.a * (look.flat ? 1.f : ph.tint) : look.flat ? 0.30f : 0.f;
p.set("uFillC", look.fill.set ? look.fill.r : 0.f, look.fill.set ? look.fill.g : 0.f, look.fill.set ? look.fill.b : 0.f, fa);
p.set("uOccl", look.occluder ? 1 : 0);
p.set("uFlat", look.flat ? 1 : 0);
p.set("uFeather", 8.f);
if (under) {
// room-map mip -> cover mip: a map texel (0.35 deg) over a cover texel's angle
const float lowScale = s.scale / 4.f;
const v3 mid = g.O + g.U * (s.spec.texW * 0.5f) + g.V * (s.spec.texH * 0.5f);
const float dist = std::max(0.3f, length(mid - head.t));
const float coverDeg = mpp / lowScale / dist * 57.2958f;
p.set("uCoverLod", std::log2((360.f / float(room.W)) / coverDeg));
p.set("uCoverSize", float(s.lowTex.w), float(s.lowTex.h));
p.set("uCoverScale", lowScale);
p.set("uCoverTaps", coverTaps);
p.set("uSteamSize", float(s.spec.texW), float(s.spec.texH));
}
const int nc = std::min<int>(int(casters.size()), kMaxCasters);
glUniform1i(p.loc("uNSh"), nc);
if (nc) {
float box[4 * kMaxCasters], par[4 * kMaxCasters], clip[4 * kMaxCasters], fill[4 * kMaxCasters];
for (int i = 0; i < nc; i++) {
const Caster &c = casters[size_t(i)];
box[i * 4] = c.cx; box[i * 4 + 1] = c.cy; box[i * 4 + 2] = c.hw; box[i * 4 + 3] = c.hh;
par[i * 4] = c.r; par[i * 4 + 1] = c.alpha; par[i * 4 + 2] = c.off; par[i * 4 + 3] = c.soft;
clip[i * 4] = c.hole ? c.x0 : 0.f; clip[i * 4 + 1] = c.hole ? c.y0 : 0.f;
clip[i * 4 + 2] = c.hole ? c.x1 : 0.f; clip[i * 4 + 3] = c.hole ? c.y1 : 0.f;
fill[i * 4] = c.fill.r; fill[i * 4 + 1] = c.fill.g; fill[i * 4 + 2] = c.fill.b;
fill[i * 4 + 3] = c.hole && c.fill.set ? c.fill.a : 0.f;
}
glUniform4fv(p.loc("uShBox"), nc, box);
glUniform4fv(p.loc("uShPar"), nc, par);
glUniform4fv(p.loc("uShClip"), nc, clip);
glUniform4fv(p.loc("uShFill"), nc, fill);
}
glDrawArrays(GL_TRIANGLES, 0, 3);
}
// Where a slab's element sits on its surface (Steam px).
static void slabRect(const Surface &s, const SlabSlot &sl, float &ex, float &ey, float &sw, float &sh) {
ex = sl.s.hasPos ? sl.s.x : (s.spec.texW - sl.s.w) * 0.5f;
ey = sl.s.hasPos ? sl.s.y : (s.spec.texH - sl.s.h) * 0.5f;
sw = sl.w / s.scale; // the cell is sl.w x sl.h glassd px = (w, h) Steam px at this scale
sh = sl.h / s.scale;
}
// Slab shadows and holes for the cover and plates of a surface: only for
// slabs whose element has held still for 150 ms (the daemon's pop rule;
// while an element moves it stays flat, so its shadow and hole wait).
// Holes come first, so a crowded surface never loses one.
std::vector<Caster> buildCasters(Surface &s, uint64_t now) {
const float mpp = designMpp();
std::vector<Caster> holes, shadows;
for (const SlabSlot &sl : s.slots) {
if (sl.s.dim() && !sl.s.hole.on) continue; // v3 G7: a dim cell casts nothing
const float settle = ramp(float(double(now - sl.stillSinceNs) / 1e6), 150.f, 300.f);
const Phased ph = phaseMap(phaseOf(sl.anim), spec.reduceMotion);
float ex, ey, sw, sh;
slabRect(s, sl, ex, ey, sw, sh);
Caster c;
c.cx = ex + sw * 0.5f;
c.cy = ey + sh * 0.5f;
c.hw = sw * 0.5f;
c.hh = sh * 0.5f;
c.r = std::min(sl.s.r, 0.5f * std::min(sw, sh));
if (sl.s.hole.on) {
const HoleSpec &h = sl.s.hole;
c.hole = true;
c.alpha = h.shadow * settle * ph.shadow;
c.off = h.y;
c.soft = std::max(1.f, h.blur);
c.x0 = h.hasClip ? h.cx0 : ex;
c.y0 = h.hasClip ? h.cy0 : ey;
c.x1 = h.hasClip ? h.cx1 : ex + sw;
c.y1 = h.hasClip ? h.cy1 : ey + sh;
c.fill = h.fill;
c.fill.a *= settle * ph.alpha;
c.spec = &h;
c.edgeA = settle * ph.alpha;
if (c.alpha < 0.005f && (!c.fill.set || c.fill.a < 0.005f) && (!h.hasEdges() || c.edgeA < 0.005f)) continue;
holes.push_back(c);
} else {
const Material sm = materialFor(sl.s.none() ? "liquid" : sl.s.material, dialNow(), std::min(sl.s.w, sl.s.h));
c.alpha = sm.slabShadow * settle * ph.shadow;
if (c.alpha < 0.005f) continue;
const float dzM = std::max(0.f, sl.s.dz * spec.unitM);
// VP P-48: offset 0.4 CSS px per mm of depth (= 0.3 m per m on the
// surface at 0.77 mm per CSS px; R1 m4: was 2 mm + 0.4 dz, about
// twice that), softness 3 mm + 0.4 dz (P-48's 1.2 px/mm within its band)
c.off = (0.3f * dzM) / mpp;
c.soft = (0.003f + dzM * 0.4f) / mpp;
shadows.push_back(c);
}
}
if (holes.size() > size_t(kMaxCasters) && !s.warnedHoles) {
std::printf("surface %s: %zu holes, drawing the first %d\n", s.spec.name.c_str(), holes.size(), kMaxCasters);
s.warnedHoles = true;
}
for (const Caster &c : shadows) holes.push_back(c);
if (holes.size() > size_t(kMaxCasters)) holes.resize(size_t(kMaxCasters));
return holes;
}
// How a piece of glass reads the copy behind it (glass.frag uCoverTaps): the
// round nine-tap blur only where something small and sharp lies behind it
// (a plate, or a hole's fill tone), which one coarse tap would turn into a
// square (R1 M3); one tap over plain cover glass, where it looks the same
// and costs a ninth (about 0.3 ms in the library scene).
int coverTaps = 9;
bool copyHasDetail(const Surface &s, const std::vector<Caster> &casters, float x0, float y0, float x1, float y1,
bool plates) const {
const float pad = 48.f; // Steam px: the blur's reach plus the bend
x0 -= pad, y0 -= pad, x1 += pad, y1 += pad;
auto hit = [&](float a0, float b0, float a1, float b1) { return a0 < x1 && x0 < a1 && b0 < y1 && y0 < b1; };
if (plates)
for (const PlateSlot &p : s.plates)
if (phaseOf(p.anim) > 0.002f && hit(p.s.x, p.s.y, p.s.x + p.s.w, p.s.y + p.s.h)) return true;
for (const Caster &c : casters)
if (c.hole && ((c.fill.set && c.fill.a > 0.01f) || (c.spec && c.spec->hasEdges())) && hit(c.x0, c.y0, c.x1, c.y1))
return true;
return false;
}
// The casters' hole edge tones for glass.frag (uHoleTex, unit 4): row i =
// caster i, texel e * 8 + k = sample k of edge e (sRGB bytes, straight
// alpha, phased), texel 32 = the sample count of each edge (0 = the flat
// fill). Uploaded only while some hole has edge tones (and once after).
bool holeTexUsed = true;
void uploadHoles(const std::vector<Caster> &casters) {
constexpr int W = 4 * kMaxEdgeSamples + 1;
if (!holeTex) {
glGenTextures(1, &holeTex);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, holeTex);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, kMaxCasters);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glActiveTexture(GL_TEXTURE0);
holeTexUsed = true; // its first contents are undefined: upload zeros at least once
}
bool any = false;
for (const Caster &c : casters) any |= c.hole && c.spec && c.spec->hasEdges();
if (!any && !holeTexUsed) return; // every count is zero already
static uint8_t px[kMaxCasters][W][4];
std::memset(px, 0, sizeof px);
auto q8 = [](float v) { return uint8_t(std::lround(std::clamp(v, 0.f, 1.f) * 255.f)); };
const size_t n = std::min(casters.size(), size_t(kMaxCasters));
for (size_t i = 0; i < n; i++) {
const Caster &c = casters[i];
if (!c.hole || !c.spec) continue;
for (int e = 0; e < 4; e++) {
const std::vector<Color> &v = c.spec->edges[e];
const size_t m = std::min(v.size(), size_t(kMaxEdgeSamples));
px[i][W - 1][e] = uint8_t(m);
for (size_t k = 0; k < m; k++) {
uint8_t *t = px[i][size_t(e) * kMaxEdgeSamples + k];
t[0] = q8(v[k].r);
t[1] = q8(v[k].g);
t[2] = q8(v[k].b);
t[3] = q8(v[k].a * c.edgeA);
}
}
}
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, holeTex);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, W, kMaxCasters, GL_RGBA, GL_UNSIGNED_BYTE, px);
glActiveTexture(GL_TEXTURE0);
holeTexUsed = any;
}
// The quarter-resolution copy of the backdrop (covers and plates): pass 1
// of the cover's interior, and what slabs see behind them.
void ensureLow(Surface &s) {
const int lw = (s.bw + 3) / 4, lh = (s.bh + 3) / 4;
if (s.lowTex.w == lw && s.lowTex.h == lh && s.lowTex.tex) return;
s.lowTex.destroy();
s.lowTex.create(lw, lh, gfx.halfFloatTargets ? GL_RGBA16F : GL_RGBA8, true);
// transparent border: where a bent ray leaves the cover, its edge blurs into the room
glBindTexture(GL_TEXTURE_2D, s.lowTex.tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 6); // slabs sample up to mip 4.5
}
// R1: the cover alone, copied from lowTex after the cover's pass 1, for
// plates drawn over that cover (they sample it as slabs sample lowTex).
// (Drawing pass 1 a second time instead of the blit cost 0.1 ms more.)
void copyCover(Surface &s) {
if (s.coverTex.w != s.lowTex.w || s.coverTex.h != s.lowTex.h || !s.coverTex.tex) {
s.coverTex.destroy();
s.coverTex.create(s.lowTex.w, s.lowTex.h, s.lowTex.fmt, true);
glBindTexture(GL_TEXTURE_2D, s.coverTex.tex);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 6);
}
glBindFramebuffer(GL_READ_FRAMEBUFFER, s.lowTex.fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, s.coverTex.fbo);
glBlitFramebuffer(0, 0, s.lowTex.w, s.lowTex.h, 0, 0, s.lowTex.w, s.lowTex.h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glActiveTexture(GL_TEXTURE2);
s.coverTex.mipmap();
glActiveTexture(GL_TEXTURE0);
}
// Nothing may sample the low copy while it is the render target.
static void unbindLow() {
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
}
// A plate as one piece of glass (v3 G1).
Look plateLook(const PlateSlot &p) const {
Look k;
k.tint = p.s.tint;
k.fill = p.s.fill;
k.occluder = p.s.occluder;
k.flat = p.s.material == "dim";
k.shadow = p.s.shadow;
return k;
}
// Steam px around a plate that its draw may touch (contact shadow).
float platePad(const PlateSlot &p, const Material &m) const {
if (p.s.material == "dim" || p.s.occluder) return 2.f;
const float a = p.s.shadow >= 0 ? p.s.shadow : m.shadow;
return a > 0 ? (0.006f + 0.003f) / designMpp() + 2.f : 2.f;
}
// Stereo (s.eyes 2): each region of the layout is a pair, the left eye's copy and then the right eye's
// (every x doubled: the backdrop at [0, bw) and [bw, 2bw), a slab cell at x at [2x, 2x + w) and
// [2x + w, 2x + 2w)), each rendered from that eye (its own rays through the glass, its own highlights).
// SteamVR shows a "Parallel" panel's uv rect split in half, the left half to the left eye (measured on
// the device 2026-10-09: it splits the panel's own crop, not the texture), so a panel over a whole pair
// gives each eye its view and the room behind the glass has its own depth instead of lying on the glass
// like a picture. Mono: one copy, from the head.
void renderSurface(Surface &s, const Pose &head, uint64_t now) {
DmaTarget &b = s.bufs[s.next];
glBindFramebuffer(GL_FRAMEBUFFER, b.fbo);
glViewport(0, 0, s.gw * s.eyes, s.gh);
glClearColor(0, 0, 0, 0);
// Only the backdrop and the live cells (plus their gutters) are
// cleared and drawn; the rest of a buffer is cleared once.
if (!s.cleared[s.next]) {
glDisable(GL_SCISSOR_TEST);
glClear(GL_COLOR_BUFFER_BIT);
s.cleared[s.next] = true;
}
for (int e = 0; e < s.eyes; e++) {
Pose eye = head;
if (s.eyes == 2) eye.t = head.t + head.rotate(eyeOffset[e]);
renderEye(s, b, eye, now, e);
}
glDisable(GL_SCISSOR_TEST);
s.last = s.next;
s.next = (s.next + 1) % 3;
s.rendered = true;
}
// Eye e's copy of the surface's layout (e 0 in mono).
void renderEye(Surface &s, DmaTarget &b, const Pose &head, uint64_t now, int e) {
const int ox = e * s.bw; // the backdrop's copy
glBindFramebuffer(GL_FRAMEBUFFER, b.fbo);
glClearColor(0, 0, 0, 0);
glEnable(GL_SCISSOR_TEST);
glScissor(ox, 0, s.bw, s.bh);
glClear(GL_COLOR_BUFFER_BIT);
const Geometry &g = s.geo;
const float mpp = designMpp();
const float coverDzM = s.spec.coverDz * spec.unitM;
const bool hasSlabs = !s.slots.empty() || !s.ghosts.empty();
const std::vector<Caster> casters = buildCasters(s, now);
uploadHoles(casters);
// cover: the union of its shapes, one pass, in the backdrop region,
// with the shadows and holes of the settled slabs in front of it
const std::vector<ShapeSpec> shapes = coverShapes(s);
std::vector<const PlateSlot *> live;
for (const PlateSlot &p : s.plates)
if (phaseOf(p.anim) > 0.002f && exitAlpha(p, now) > 0.002f) live.push_back(&p);
// R1 (m1): plates over a cover see that cover behind them (glass over
// glass, like a slab), so pass 1 also runs to give them its copy
const bool platesOverCover = !shapes.empty() && !live.empty();
bool coverDrawn = false, lowReady = false, coverCopied = false;
if (!shapes.empty()) {
float x0 = 1e9f, y0 = 1e9f, x1 = -1e9f, y1 = -1e9f;
for (const ShapeSpec &q : shapes) {
x0 = std::min(x0, q.x);
y0 = std::min(y0, q.y);
x1 = std::max(x1, q.x + q.w);
y1 = std::max(y1, q.y + q.h);
}
float minSide = 1e9f;
for (const ShapeSpec &q : shapes) minSide = std::min(minSide, std::min(q.w, q.h));
const Material cm = materialFor(s.spec.material, dialNow(), minSide);
// the contact shadow falls outside the shapes: widen the scissor
const float pad = cm.shadow > 0 ? (0.006f + 0.003f) / mpp : 0.f;
const int sx0 = std::max(0, int(std::floor((x0 - pad) * s.scale)) - 1);
const int sy0 = std::max(0, int(std::floor((y0 - pad) * s.scale)) - 1);
const int sx1 = std::min(s.bw, int(std::ceil((x1 + pad) * s.scale)) + 1);
const int sy1 = std::min(s.bh, int(std::ceil((y1 + pad) * s.scale)) + 1);
if (sx1 > sx0 && sy1 > sy0) {
const Phased cph = phaseMap(phaseOf(s.anim), spec.reduceMotion);
// Pass 1: the flat interior at 1/4 resolution (it is frosted by
// several degrees); pass 2: the edge band in full, pass 1 inside.
// Every edge term ends within `inner` of the edge: the bezel,
// the shading bands (1.6 x their width), plus 1.5 low texels.
const float lowScale = s.scale / 4.f;
const float inner = std::max(std::min(cm.bezelM / mpp, 0.5f * minSide), 1.6f * std::max(2.f, cm.darkW / mpp)) +
1.5f / lowScale + 2.f;
const bool twoPass = opt.debugView == 0 && minSide > 2.f * inner + 8.f / lowScale;
if (twoPass || hasSlabs || platesOverCover) {
ensureLow(s);
unbindLow();
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
s.lowTex.bind();
drawGlass(s, g, head, cm, cph, 0, 0, s.lowTex.w, s.lowTex.h, {0, 0, 0}, shapes, coverDzM, false, casters, 1, lowScale);
lowReady = true;
if (platesOverCover) {
copyCover(s); // scissor off: a blit obeys it
coverCopied = true;
}
glBindFramebuffer(GL_FRAMEBUFFER, b.fbo);
glEnable(GL_SCISSOR_TEST);
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, s.lowTex.tex);
glActiveTexture(GL_TEXTURE0);
progGlass.set("uLowSize", float(s.lowTex.w), float(s.lowTex.h));
progGlass.set("uLowScale", lowScale);
progGlass.set("uInner", inner);
}
glScissor(ox + sx0, sy0, sx1 - sx0, sy1 - sy0);
drawGlass(s, g, head, cm, cph, ox, 0, s.bw, s.bh, {0, 0, 0}, shapes, coverDzM, false, casters, twoPass ? 2 : 0);
coverDrawn = true;
}
}
// v3 plates, in spec order, over the cover (premultiplied "over"); the
// low copy gets them too when slabs need to see them. Over a cover a
// plate samples the cover's copy (unit 2) and its optics sit
// kPlateLiftM in front of it, so its bezel bends the window glass, not
// the room behind the window (R1 m1: a closed rim of bent room)
bool platesDrawn = false;
const bool plateUnder = coverCopied;
const float plateDz = coverDzM + (plateUnder ? kPlateLiftM : 0.f);
auto bindCoverCopy = [&] {
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, s.coverTex.tex);
glActiveTexture(GL_TEXTURE0);
};
if (!live.empty()) {
glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
if (hasSlabs) {
ensureLow(s);
unbindLow();
if (plateUnder) bindCoverCopy();
glDisable(GL_SCISSOR_TEST);
s.lowTex.bind();
if (!lowReady) {
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT);
lowReady = true;
}
glEnable(GL_SCISSOR_TEST);
const float lowScale = s.scale / 4.f;
for (const PlateSlot *p : live) {
// pass 1 has no coverage alpha: a leaving plate leaves the slabs' view halfway through
if (exitAlpha(*p, now) < 0.5f) continue;
const Material pm = materialFor(p->s.material, dialNow(), std::min(p->s.w, p->s.h));
const float pad = platePad(*p, pm);
const int x0 = std::max(0, int(std::floor((p->s.x - pad) * lowScale)) - 1);
const int y0 = std::max(0, int(std::floor((p->s.y - pad) * lowScale)) - 1);
const int x1 = std::min(s.lowTex.w, int(std::ceil((p->s.x + p->s.w + pad) * lowScale)) + 1);
const int y1 = std::min(s.lowTex.h, int(std::ceil((p->s.y + p->s.h + pad) * lowScale)) + 1);
if (x1 <= x0 || y1 <= y0) continue;
glScissor(x0, y0, x1 - x0, y1 - y0);
coverTaps = copyHasDetail(s, casters, p->s.x, p->s.y, p->s.x + p->s.w, p->s.y + p->s.h, false) ? 9 : 1;
drawGlass(s, g, head, pm, phaseMap(phaseOf(p->anim), spec.reduceMotion), 0, 0, s.lowTex.w, s.lowTex.h, {0, 0, 0},
{{p->s.x, p->s.y, p->s.w, p->s.h, p->s.r}}, plateDz, plateUnder, casters, 1, lowScale, plateLook(*p));
}
glBindFramebuffer(GL_FRAMEBUFFER, b.fbo);
}
if (plateUnder) bindCoverCopy();
for (const PlateSlot *p : live) {
const Material pm = materialFor(p->s.material, dialNow(), std::min(p->s.w, p->s.h));
const float pad = platePad(*p, pm);
const int x0 = std::max(0, int(std::floor((p->s.x - pad) * s.scale)) - 1);
const int y0 = std::max(0, int(std::floor((p->s.y - pad) * s.scale)) - 1);
const int x1 = std::min(s.bw, int(std::ceil((p->s.x + p->s.w + pad) * s.scale)) + 1);
const int y1 = std::min(s.bh, int(std::ceil((p->s.y + p->s.h + pad) * s.scale)) + 1);
if (x1 <= x0 || y1 <= y0) continue;
glScissor(ox + x0, y0, x1 - x0, y1 - y0);
coverTaps = copyHasDetail(s, casters, p->s.x, p->s.y, p->s.x + p->s.w, p->s.y + p->s.h, false) ? 9 : 1;
drawGlass(s, g, head, pm, platePhase(*p, now), ox, 0, s.bw, s.bh, {0, 0, 0},
{{p->s.x, p->s.y, p->s.w, p->s.h, p->s.r}}, plateDz, plateUnder, casters, 0, 0.f, plateLook(*p));
platesDrawn = true;
}
glDisable(GL_BLEND);
}
// what the slabs see behind them: the quarter-resolution cover and
// plates (mipmapped; the cover's highlights are left out, its frost hides them)
const bool under = (coverDrawn || platesDrawn) && lowReady && hasSlabs;
if (under) {
glActiveTexture(GL_TEXTURE3);
s.lowTex.mipmap();
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, s.lowTex.tex);
glActiveTexture(GL_TEXTURE0);
}
// slabs (and fading ghosts): the element's exact size; world point =
// element position (+ the moved crop's offset) + dz
auto drawSlab = [&](const SlabSlot &sl) {
float ex, ey, sw, sh;
slabRect(s, sl, ex, ey, sw, sh);
// the cell's copy for this eye; the pair (and its gutter) is cleared once, before the left eye
const int cx = s.eyes == 2 ? 2 * sl.x + e * sl.w : sl.x;
if (e == 0) {
const int px = s.eyes == 2 ? 2 * sl.x : sl.x, pw = s.eyes == 2 ? 2 * sl.w : sl.w;
const int gx0 = std::max(0, px - 1), gy0 = std::max(s.bh, sl.y - 1);
const int gx1 = std::min(s.gw * s.eyes, px + pw + 1), gy1 = std::min(s.gh, sl.y + sl.h + 1);
glScissor(gx0, gy0, gx1 - gx0, gy1 - gy0);
glClear(GL_COLOR_BUFFER_BIT); // the cell's gutter may hold an older cell's pixels
}
if (sl.s.none()) return; // v3: a cell that draws nothing (its hole is on the cover)
if (sl.s.dim()) {
// v3 G7: a flat dark cell (the room dim the scene graph stretches behind the window)
Look flat;
flat.flat = true;
flat.fill = sl.s.fill;
glScissor(cx, sl.y, sl.w, sl.h);
drawGlass(s, g, head, materialFor("liquid", dialNow(), std::min(sl.s.w, sl.s.h)),
phaseMap(phaseOf(sl.anim), spec.reduceMotion), cx, sl.y, sl.w, sl.h, {ex, ey, 0},
{{0, 0, sw, sh, sl.s.r}}, 0.f, false, {}, 0, 0.f, flat);
return;
}
const Material m = materialFor(sl.s.material, dialNow(), std::min(sl.s.w, sl.s.h));
Look look;
look.tint = sl.s.tint;
glScissor(cx, sl.y, sl.w, sl.h);
coverTaps = copyHasDetail(s, casters, ex + sl.s.ox, ey + sl.s.oy, ex + sl.s.ox + sw, ey + sl.s.oy + sh, true) ? 9 : 1;
drawGlass(s, g, head, m, phaseMap(phaseOf(sl.anim), spec.reduceMotion), cx, sl.y, sl.w, sl.h,
{ex + sl.s.ox, ey + sl.s.oy, 0}, {{0, 0, sw, sh, sl.s.r}}, sl.s.dz * spec.unitM - 0.0008f, under, {}, 0, 0.f, look);
};
for (const SlabSlot &sl : s.slots) drawSlab(sl);
for (const SlabSlot &sl : s.ghosts) drawSlab(sl);
coverTaps = 9;
}
// Renders every visible surface, waits for the GPU, then hands the new
// buffers to SteamVR. fetchGeo: ask SteamVR for the surfaces' quads.
int renderAll(const Pose &head, bool force, bool fetchGeo, uint64_t now) {
std::vector<Surface *> todo;
for (auto &s : surfaces) {
if (!(s->spec.visible || s->spec.armed || force || s->clearPending)) continue;
if (s->ov == vr::k_ulOverlayHandleInvalid && !ensureOverlay(*s)) continue;
if (!s->bufs[0].bo && !ensureBuffers(*s)) continue;
if (!s->bufs[s->next].bo) continue;
refreshGeometry(*s, head, now, fetchGeo);
todo.push_back(s.get());
}
if (todo.empty()) return 0;
// where each eye sits in the head (the wearer's IPD), every few seconds
if (!eyeOffsetAt || now - eyeOffsetAt > 5000000000ull) {
eyeOffsetAt = now;
for (int e = 0; e < 2; e++) {
const vr::HmdMatrix34_t m = vr::VRSystem()->GetEyeToHeadTransform(e ? vr::Eye_Right : vr::Eye_Left);
const v3 o{m.m[0][3], m.m[1][3], m.m[2][3]};
if (std::isfinite(o.x) && std::fabs(o.x) > 0.015f && std::fabs(o.x) < 0.05f) eyeOffset[e] = o;
}
}
const uint64_t t0 = monoNowNs();
renderTimer.begin();
Program &p = progGlass;
p.use();
glBindVertexArray(vao);
glDisable(GL_BLEND);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, room.push.back().tex); // 1x1: the known room's mean
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, room.filled.tex);
p.set("uRoom", 0);
p.set("uRoomAvg", 1);
p.set("uCover", 2);
p.set("uLow", 3);
p.set("uHoleTex", 4);
p.set("uRoomSize", float(room.W), float(room.H));
p.set("uCenter", room.center.x, room.center.y, room.center.z);
p.set("uRadius", room.radius);
// One key light for every surface, fixed in the world: from above,
// about 20 deg left of vertical, a little in front (DESIGN2 §6.2 R2).
const v3 key = normalize(v3{-0.30f, 0.90f, 0.32f});
p.set("uKeyL", key.x, key.y, key.z);
p.set("uDebug", opt.debugView);
for (Surface *s : todo) {
renderSurface(*s, head, now);
s->clearPending = false;
}
renderTimer.end();
glFinish(); // SteamVR samples the dmabufs from another process
cpuMs = cpuMs * 0.9f + float(double(monoNowNs() - t0) / 1e6) * 0.1f;
renderTimer.poll();
for (Surface *s : todo) {
const DmaTarget &b = s->bufs[s->last];
vr::SharedTextureHandle_t h = vr::SharedTextureHandle_t(b.vrHandle);
vr::Texture_t tex = {&h, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
auto e = vr::VROverlay()->SetOverlayTexture(s->ov, &tex);
if (e != vr::VROverlayError_None) {
if (s->submitFails++ == 0) s->submitFailSinceNs = now;
if (s->submitFails <= 3)
std::printf("SetOverlayTexture(%s): %s\n", s->key.c_str(), vr::VROverlay()->GetOverlayErrorNameFromEnum(e));
if (e == vr::VROverlayError_UnknownOverlay || e == vr::VROverlayError_InvalidHandle)
loseOverlay(*s, vr::VROverlay()->GetOverlayErrorNameFromEnum(e));
else if (s->submitFails >= 5 && now - s->submitFailSinceNs > 1000000000ull)
loseOverlay(*s, "texture submits keep failing");
continue;
}
s->submitFails = 0;
s->lastSubmitNs = lastSubmitNs = now;
// The scene graph sizes the panel from the mouse scale: change it
// together with the first texture of a new size.
if (b.w != s->submittedW || b.h != s->submittedH) {
// one eye's size: the panel is sized from it, and a stereo texture holds two
vr::HmdVector2_t mouse = {float(b.w / std::max(1, s->eyes)), float(b.h)};
vr::VROverlay()->SetOverlayMouseScale(s->ov, &mouse);
s->submittedW = b.w;
s->submittedH = b.h;
}
if (!s->announced) {
// what SteamVR now holds for this overlay
uint32_t tw = 0, th = 0;
auto te = vr::VROverlay()->GetOverlayTextureSize(s->ov, &tw, &th);
vr::HmdVector2_t ms{};
vr::VROverlay()->GetOverlayMouseScale(s->ov, &ms);
std::printf("first texture on %s: SteamVR reports %ux%u (%s), mouse scale %.0fx%.0f\n", s->key.c_str(), tw, th,
vr::VROverlay()->GetOverlayErrorNameFromEnum(te), double(ms.v[0]), double(ms.v[1]));
s->announced = true;
}
}
frames++;
framesWindow++;
return int(todo.size());
}
// -------------------------------------------------------------- health
// Every 2 s: SteamVR's processes are still the ones we connected to, and
// our overlays still exist. A vrserver or vrcompositor that went away
// (crash, respawn) ends glassd with 75; the daemon starts a fresh one.
void healthCheck(uint64_t now) {
if (now - lastHealthNs < 2000000000ull) return;
lastHealthNs = now;
if (vrserverPid > 0 && !pidIs(vrserverPid, "vrserver")) {
std::printf("SteamVR (vrserver %d) went away; exiting\n", vrserverPid);
quitWith(kExitTemp);
return;
}
if (compositorPid > 0 && !pidIs(compositorPid, "vrcompositor")) {
std::printf("vrcompositor %d went away; exiting so a fresh glassd can reconnect\n", compositorPid);
quitWith(kExitTemp);
return;
}
for (auto &s : surfaces) {
if (s->ov == vr::k_ulOverlayHandleInvalid) continue;
vr::VROverlayHandle_t h = vr::k_ulOverlayHandleInvalid;
auto e = vr::VROverlay()->FindOverlay(s->key.c_str(), &h);
if (e != vr::VROverlayError_None || h != s->ov) loseOverlay(*s, "overlay vanished");
}
}
// Visible surfaces all have an overlay that accepts textures.
bool healthy() const {
for (auto &s : surfaces) {
if (!s->spec.visible) continue;
if (s->ov == vr::k_ulOverlayHandleInvalid || s->submitFails >= 3) return false;
}
return true;
}
// -------------------------------------------------------------- output
void writeOut(bool exiting) {
std::string o;
char b[1024]; // parseSpec caps names and slab ids at 128 chars, sanitizeKey keys at 100
timespec rt;
clock_gettime(CLOCK_REALTIME, &rt);
const uint64_t now = monoNowNs();
std::snprintf(b, sizeof b, "{\"seq\": %lld, \"pid\": %d, \"updated\": %.3f, \"healthy\": %s, \"fps\": %.1f, \"gpu_ms\": %.2f, \"frames\": %llu, \"dashboard\": %s",
spec.seq, int(getpid()), double(rt.tv_sec) + double(rt.tv_nsec) / 1e9, !exiting && healthy() ? "true" : "false",
exiting ? 0.0 : double(fpsNow), double(gpuMs()), (unsigned long long)frames, lastDash ? "true" : "false");
o += b;
o += ", \"version\": 3, \"caps\": [\"plates\", \"holes\", \"tint\", \"masks\", \"coverDz\", \"none\", \"offset\", \"dim\", \"scaleFrom\", \"unitM\", \"roomDim\", \"dimSlab\", \"holeEdges\", \"morph\", \"appearAt\", \"armed\", \"exitAt\", \"tune\", \"stereo\"]";
// the dial and the tune as drawn (eased toward the spec's)
std::snprintf(b, sizeof b, ", \"tune\": {\"dial\": %.3f, \"refract\": %.3f, \"frost\": %.3f, \"light\": %.3f, \"hueK\": %.3f}",
double(dialNow()), double(tuneLive.refract), double(tuneLive.frost), double(tuneLive.light), double(tuneLive.hueK));
o += b;
std::snprintf(b, sizeof b, ", \"last_submit_s\": %.1f, \"room_ms\": %.2f, \"room_updates\": %llu, \"feed\": %s%s",
lastSubmitNs ? double(now - lastSubmitNs) / 1e9 : -1.0, double(roomTimer.have ? roomTimer.ms : roomCpuMs),
(unsigned long long)room.updates, jsonEscape(feedState()).c_str(), exiting ? ", \"exiting\": true" : "");
o += b;
o += ", \"surfaces\": {";
bool first = true;
for (auto &s : surfaces) {
if (!first) o += ", ";
first = false;
const float gw = float(std::max(1, s->gw)), gh = float(std::max(1, s->gh));
const size_t cover = !specCover(s->spec) ? 0
: s->spec.hasShapes ? std::min(s->spec.shapes.size(), size_t(kMaxShapes)) : size_t(1);
std::snprintf(b, sizeof b,
"%s: {\"key\": %s, \"texW\": %d, \"texH\": %d, \"backdrop\": [0, 0, %.6f, %.6f], \"backdropScale\": %.6f, \"cover\": %zu",
jsonEscape(s->spec.name).c_str(), jsonEscape(s->key).c_str(), s->gw, s->gh, s->bw / (gw * s->eyes), s->bh / gh,
double(s->scale), cover);
o += b;
// where the surface is from the head: the angle off the view direction and the distance (lab:
// `glass.py hv` waits for the wearer to face the dashboard before it grabs)
if (haveLastHead && s->spec.texW > 0) {
const Geometry &g = s->geo;
const v3 mid = g.O + g.U * (s->spec.texW * 0.5f) + g.V * (s->spec.texH * 0.5f);
const v3 to = mid - lastHead.t;
const float dist = length(to);
const v3 fwd = lastHead.rotate({0, 0, -1});
const float off = dist > 1e-4f ? std::acos(std::clamp(dot(to * (1.f / dist), fwd), -1.f, 1.f)) * 57.2958f : 180.f;
std::snprintf(b, sizeof b, ", \"view\": {\"offDeg\": %.1f, \"distM\": %.2f}", double(off), double(dist));
o += b;
}
if (s->eyes == 2) o += ", \"stereo\": true";
o += ", \"geometry\": " + jsonEscape(s->geoNote) + ", \"slabs\": {";
for (size_t i = 0; i < s->slots.size(); i++) {
const SlabSlot &sl = s->slots[i];
// the cell (stereo: its left eye's copy; the right eye's follows it)
const float sx = float(s->eyes == 2 ? 2 * sl.x : sl.x), tw = gw * float(s->eyes);
std::snprintf(b, sizeof b, "%s%s: [%.6f, %.6f, %.6f, %.6f]", i ? ", " : "", jsonEscape(sl.s.id).c_str(), sx / tw, sl.y / gh,
(sx + sl.w) / tw, (sl.y + sl.h) / gh);
o += b;
}
o += "}";
if (!s->dropped.empty()) {
o += ", \"dropped\": [";
size_t n = 0; // the first kMaxListedDrops (counts.dropped has them all)
for (const std::string &id : s->dropped) {
if (n == size_t(kMaxListedDrops)) break;
o += (n++ ? ", " : "") + jsonEscape(id);
}
o += "]";
}
// v3: the plates drawn (in the backdrop region) and those left out
o += ", \"plates\": [";
for (size_t i = 0; i < s->plates.size(); i++) o += (i ? ", " : "") + jsonEscape(s->plates[i].s.id);
o += "]";
if (!s->spec.droppedPlates.empty()) {
o += ", \"droppedPlates\": [";
for (size_t i = 0; i < s->spec.droppedPlates.size(); i++) o += (i ? ", " : "") + jsonEscape(s->spec.droppedPlates[i]);
o += "]";
}
size_t holes = 0;
for (const SlabSlot &sl : s->slots) holes += sl.s.hole.on;
std::snprintf(b, sizeof b,
", \"counts\": {\"shapes\": %zu, \"plates\": %zu, \"slabs\": %zu, \"holes\": %zu, \"dropped\": %zu, \"droppedPlates\": %zu}",
cover, s->plates.size(), s->slots.size(), holes, s->dropped.size(), s->spec.droppedPlatesN);
o += b;
o += "}";
}
o += "}}\n";
if (!writeAtomic(opt.out, o)) std::printf("cannot write %s\n", opt.out.c_str());
lastOutNs = monoNowNs();
}
float gpuMs() const { return renderTimer.have ? renderTimer.ms : cpuMs; }
std::string feedState() const {
if (opt.noFeed) return "off";
int st = feed.state.load();
if (st == -1) return "failed: " + feed.error();
if (feed.attached()) return (feed.isStreaming() ? "live " : "shot ") + feed.mode();
return "standby";
}
// --------------------------------------------------------------- dumps
void dumpSurfaces(const std::string &dir) {
mkdirs(dir);
for (auto &s : surfaces) {
if (s->last < 0) continue;
// materialize progress of every piece of glass at this frame
char tb[48];
std::snprintf(tb, sizeof tb, " (t=%.4f, spec seq %lld)", double(lastPhaseNs) / 1e9, spec.seq);
std::string ph = "phase " + s->spec.name + "=" + std::to_string(phaseOf(s->anim)).substr(0, 5);
for (const SlabSlot &sl : s->slots) ph += " " + sl.s.id + "=" + std::to_string(phaseOf(sl.anim)).substr(0, 5);
for (const PlateSlot &pl : s->plates) ph += " plate:" + pl.s.id + "=" + std::to_string(phaseOf(pl.anim)).substr(0, 5);
std::printf("%s%s\n", ph.c_str(), tb);
DmaTarget &b = s->bufs[s->last];
std::vector<uint8_t> px = readPixels(b.fbo, s->gw, s->gh);
for (size_t i = 0; i < px.size(); i += 4) { // premultiplied -> straight for PNG
unsigned a = px[i + 3];
if (a > 0 && a < 255)
for (int c = 0; c < 3; c++) px[i + c] = uint8_t(std::min(255u, (px[i + c] * 255u + a / 2) / a));
}
std::string path = dir + "/" + sanitizeKey(s->spec.name) + ".png";
stbi_write_png(path.c_str(), s->gw, s->gh, 4, px.data(), s->gw * 4);
const Geometry &g = s->geo;
const v3 mid = g.O + g.U * (s->spec.texW * 0.5f) + g.V * (s->spec.texH * 0.5f);
const v3 toHead = lastRenderHead.t - mid;
const float ang = std::acos(std::clamp(dot(normalize(toHead), g.N), -1.f, 1.f)) * 57.2958f;
std::printf("dumped %s (%dx%d, geometry %s: centre %.2f %.2f %.2f, %.2f x %.2f m, head %.2f m away at %.0f deg off-normal)\n",
path.c_str(), s->gw, s->gh, s->geoNote.c_str(), mid.x, mid.y, mid.z, length(g.U) * s->spec.texW,
length(g.V) * s->spec.texH, length(toHead), ang);
if (opt.dumpView) dumpView(*s, px, dir + "/" + sanitizeKey(s->spec.name) + "-view.png");
}
}
// What the wearer would see of one surface (view.frag): the room around
// it, the cover and the slabs at their world positions, from the current
// head. `straight` is the surface texture as dumped (straight alpha).
void dumpView(Surface &s, const std::vector<uint8_t> &straight, const std::string &path) {
if (!s.geo.valid) return;
// back to premultiplied for the compositing shader
std::vector<uint8_t> px = straight;
for (size_t i = 0; i < px.size(); i += 4)
for (int c = 0; c < 3; c++) px[i + c] = uint8_t((px[i + c] * unsigned(px[i + 3]) + 127) / 255);
GLuint tex = 0;
glGenTextures(1, &tex);
glBindTexture(GL_TEXTURE_2D, tex);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, s.gw, s.gh, 0, GL_RGBA, GL_UNSIGNED_BYTE, px.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
const float tw = float(s.spec.texW), th = float(s.spec.texH);
const float m = std::clamp(0.08f * std::max(tw, th), 48.f, 200.f);
const float x0 = -m, y0 = -m, x1 = tw + m, y1 = th + m;
const int ow = std::max(1, int(std::lround((x1 - x0) * s.scale))), oh = std::max(1, int(std::lround((y1 - y0) * s.scale)));
Target out;
if (!out.create(ow, oh, GL_RGBA8, false)) {
std::printf("view target %dx%d failed\n", ow, oh);
out.destroy();
glDeleteTextures(1, &tex);
return;
}
out.bind();
glDisable(GL_BLEND);
glDisable(GL_SCISSOR_TEST);
Program &p = progView;
p.use();
glBindVertexArray(vao);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, room.filled.tex);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, tex);
p.set("uRoom", 0);
p.set("uTex", 1);
p.set("uRect", x0, y0, x1, y1);
const Geometry &g = s.geo;
p.set("uO", g.O.x, g.O.y, g.O.z);
p.set("uU", g.U.x, g.U.y, g.U.z);
p.set("uV", g.V.x, g.V.y, g.V.z);
p.set("uN", g.N.x, g.N.y, g.N.z);
p.set("uEye", lastRenderHead.t.x, lastRenderHead.t.y, lastRenderHead.t.z);
p.set("uCenter", room.center.x, room.center.y, room.center.z);
p.set("uRadius", room.radius);
p.set("uPattern", opt.testPattern);
p.set("uTexSize", float(s.gw), float(s.gh));
p.set("uScale", s.scale);
p.set("uSteamSize", tw, th);
p.set("uCoverDz", s.spec.coverDz * spec.unitM);
std::vector<const SlabSlot *> sl;
for (const SlabSlot &q : s.slots) sl.push_back(&q);
std::stable_sort(sl.begin(), sl.end(), [](const SlabSlot *a, const SlabSlot *b) { return a->s.dz < b->s.dz; });
if (sl.size() > 16) sl.resize(16);
float rect[64] = {}, cell[64] = {}, dz[16] = {}, hole[64] = {}, info[64] = {}, col[64] = {};
for (size_t i = 0; i < sl.size(); i++) {
const SlabSlot &q = *sl[i];
const float ex = q.s.hasPos ? q.s.x : (tw - q.s.w) * 0.5f, ey = q.s.hasPos ? q.s.y : (th - q.s.h) * 0.5f;
rect[i * 4] = ex + q.s.ox;
rect[i * 4 + 1] = ey + q.s.oy;
rect[i * 4 + 2] = q.w / s.scale;
rect[i * 4 + 3] = q.h / s.scale;
cell[i * 4] = float(q.x);
cell[i * 4 + 1] = float(q.y);
cell[i * 4 + 2] = float(q.w);
cell[i * 4 + 3] = float(q.h);
dz[i] = q.s.dz * spec.unitM;
// hero stand-in: the hole the pop leaves in the mosaic, the crop's
// offset, radius and colour (its tint, else a light grey capsule)
const HoleSpec &h = q.s.hole;
hole[i * 4] = h.hasClip ? h.cx0 : ex;
hole[i * 4 + 1] = h.hasClip ? h.cy0 : ey;
hole[i * 4 + 2] = h.hasClip ? h.cx1 : ex + q.w / s.scale;
hole[i * 4 + 3] = h.hasClip ? h.cy1 : ey + q.h / s.scale;
info[i * 4] = q.s.ox;
info[i * 4 + 1] = q.s.oy;
info[i * 4 + 2] = q.s.r;
info[i * 4 + 3] = q.s.none() ? 1.f : 0.f;
const bool tinted = q.s.tint.set && q.s.tint.a > 0;
col[i * 4] = tinted ? q.s.tint.r : 0.78f;
col[i * 4 + 1] = tinted ? q.s.tint.g : 0.80f;
col[i * 4 + 2] = tinted ? q.s.tint.b : 0.84f;
col[i * 4 + 3] = 1.f;
}
p.set("uContent", opt.viewContent);
p.set("uRoomDim", roomDimNow()); // v3 G7: the dim panel behind the UI, as the wearer would see it
glUniform1i(p.loc("uNSlab"), int(sl.size()));
if (!sl.empty()) {
glUniform4fv(p.loc("uSlabRect"), GLsizei(sl.size()), rect);
glUniform4fv(p.loc("uSlabCell"), GLsizei(sl.size()), cell);
glUniform1fv(p.loc("uSlabDz"), GLsizei(sl.size()), dz);
glUniform4fv(p.loc("uPopHole"), GLsizei(sl.size()), hole);
glUniform4fv(p.loc("uPopInfo"), GLsizei(sl.size()), info);
glUniform4fv(p.loc("uPopCol"), GLsizei(sl.size()), col);
}
glDrawArrays(GL_TRIANGLES, 0, 3);
std::vector<uint8_t> o = readPixels(out.fbo, ow, oh), rgb(size_t(ow) * oh * 3);
for (size_t i = 0, n = size_t(ow) * oh; i < n; i++)
for (int c = 0; c < 3; c++) rgb[i * 3 + c] = o[i * 4 + c];
stbi_write_png(path.c_str(), ow, oh, 3, rgb.data(), ow * 3);
std::printf("dumped %s (%dx%d: the surface plus %.0f px around, eye %.2f %.2f %.2f)\n", path.c_str(), ow, oh, double(m),
double(lastRenderHead.t.x), double(lastRenderHead.t.y), double(lastRenderHead.t.z));
glActiveTexture(GL_TEXTURE0);
out.destroy();
glDeleteTextures(1, &tex);
}
void dumpRoom(const std::string &path) {
mkdirs(dirname(path));
std::vector<uint8_t> px = readPixels(room.filled.fbo, room.W, room.H);
std::vector<uint8_t> rgb(size_t(room.W) * room.H * 3), known(size_t(room.W) * room.H);
for (size_t i = 0, n = size_t(room.W) * room.H; i < n; i++)
for (int c = 0; c < 3; c++) rgb[i * 3 + c] = px[i * 4 + c];
stbi_write_png(path.c_str(), room.W, room.H, 3, rgb.data(), room.W * 3);
std::vector<uint8_t> mp = readPixels(room.map[room.cur].fbo, room.W, room.H);
size_t seen = 0;
for (size_t i = 0, n = size_t(room.W) * room.H; i < n; i++) {
known[i] = mp[i * 4 + 3];
seen += known[i] > 127;
}
std::string kp = path;
size_t dot = kp.rfind(".png");
kp = (dot == std::string::npos ? kp : kp.substr(0, dot)) + "-known.png";
stbi_write_png(kp.c_str(), room.W, room.H, 1, known.data(), room.W);
std::printf("dumped room map %s and %s (%.1f%% of texels seen, %llu updates)\n", path.c_str(), kp.c_str(),
100.0 * double(seen) / double(size_t(room.W) * room.H), (unsigned long long)room.updates);
}
// ---------------------------------------------------------------- loop
void status(uint64_t now, bool dash) {
const double dt = double(now - lastStatusNs) / 1e9;
float known = 0, lum = 0;
room.stats(known, lum);
const uint64_t seen = feed.framesSeen.load(), att = feed.attachedTotalNs();
std::string geo;
int vis = 0;
size_t slabs = 0, ghosts = 0, dropped = 0, plates = 0;
for (auto &s : surfaces) {
geo += (geo.empty() ? "" : ",") + s->spec.name + ":" + s->geoNote;
vis += s->spec.visible;
plates += s->plates.size();
slabs += s->slots.size();
ghosts += s->ghosts.size();
dropped += s->dropped.size();
}
std::printf("dash=%d surfaces=%d/%zu fps=%.1f gpu=%.2fms%s cpu=%.2fms room=%.1f/s (%.2fms, %llu skipped) feed=%s %.0f/s attached=%.0f%% "
"known=%.0f%% lum=%.2f env=%.2f masks=%zu%s plates=%zu slabs=%zu+%zu fading, %zu dropped geo=%s\n",
dash, vis, surfaces.size(), double(framesWindow) / dt, double(gpuMs()), renderTimer.have ? "" : "(cpu+finish)",
double(cpuMs), double(roomWindow) / dt, double(roomTimer.have ? roomTimer.ms : roomCpuMs),
(unsigned long long)roomSkipped, feedState().c_str(), double(seen - feedSeenAtStatus) / dt,
100.0 * double(att - feedAttachedAtStatus) / 1e9 / dt, double(known * 100), double(lum), double(envLuma), masks.size(),
masksComplete ? "" : "(incomplete)", plates, slabs, ghosts, dropped, geo.empty() ? "-" : geo.c_str());
feedSeenAtStatus = seen;
feedAttachedAtStatus = att;
framesWindow = 0;
roomWindow = 0;
lastStatusNs = now;
}
// --bench: GPU clock samples (MHz) from devfreq, for the summary
std::vector<float> benchClock;
uint64_t benchClockNs = 0;
void benchSample(uint64_t now) {
if (!opt.bench || now - benchClockNs < 100 * kMs) return;
benchClockNs = now;
if (FILE *f = std::fopen("/sys/class/devfreq/3d00000.gpu/cur_freq", "r")) {
long long hz = 0;
if (std::fscanf(f, "%lld", &hz) == 1 && hz > 0) benchClock.push_back(float(hz / 1e6));
std::fclose(f);
}
}
void benchSummary() {
std::vector<float> v = renderTimer.samples;
if (v.size() < 10) return;
v.erase(v.begin(), v.begin() + long(v.size() / 10)); // skip the warm-up tenth
std::sort(v.begin(), v.end());
std::vector<float> c = benchClock;
std::sort(c.begin(), c.end());
const float med = v[v.size() / 2], p90 = v[v.size() * 9 / 10], mhz = c.empty() ? 0.f : c[c.size() / 2];
std::printf("bench: %zu frames, gpu median %.2f ms, p90 %.2f ms, min %.2f ms; GPU clock median %.0f MHz (%.2f ms at 903 MHz)\n",
v.size(), double(med), double(p90), double(v.front()), double(mhz), mhz > 0 ? double(med * mhz / 903.f) : 0.0);
// Frames that overlap the compositor's own GPU work read longer (the
// timer counts the time the GPU spends on it in between): a second mode.
int hist[8] = {};
for (float x : v) hist[std::min(7, int(x / 0.5f))]++;
std::printf("bench: frames per 0.5 ms bin:");
for (int i = 0; i < 8; i++) std::printf(" %s%.1f:%d", i == 7 ? ">=" : "<", double((i + 1) * 0.5f - (i == 7 ? 0.5f : 0.f)), hist[i]);
std::printf("\n");
}
void loop() {
lastStatusNs = lastOutNs = lastLoopNs = lastHealthNs = monoNowNs();
uint64_t fpsWindowNs = lastStatusNs, fpsFrames = 0, lastSpecCheckNs = 0;
const bool wantDump = !opt.dumpDir.empty() || !opt.dumpRoom.empty() || opt.once;
while (!g_quit) {
const uint64_t now = monoNowNs();
const float dt = float(double(now - lastLoopNs) / 1e9);
lastLoopNs = now;
if (opt.timeout > 0 && now - startNs > uint64_t(opt.timeout * 1e9)) break;
if (!opt.orphanOk && origPpid > 1 && getppid() != origPpid) {
std::printf("parent %d exited; stopping\n", origPpid);
break;
}
vr::VREvent_t ev;
while (vr::VRSystem()->PollNextEvent(&ev, sizeof ev)) {
if (ev.eventType == vr::VREvent_Quit) {
std::printf("SteamVR is quitting\n");
vr::VRSystem()->AcknowledgeQuit_Exiting();
quitWith(kExitTemp);
}
}
if (g_quit) break;
if (g_loseRequest.exchange(false))
for (auto &s : surfaces)
if (s->ov != vr::k_ulOverlayHandleInvalid) loseOverlay(*s, "SIGUSR2 test");
healthCheck(now);
if (g_quit) break;
benchSample(now);
Pose hmd;
const bool test = opt.testPattern >= 0;
if (test) hmd.t = kTestEye; // fixed test head, looking down -z
if (test || samplePose(0.f, hmd)) {
hist.emplace_back(now, hmd);
while (hist.size() > 2 && now - hist.front().first > 1500000000ull) hist.pop_front();
room.follow(hmd.t, dt);
}
// the spec: at once on an inotify event, else polled at 10 Hz
if (!opt.demo) {
if (inoWd < 0 && now >= inoRetryNs) watchSpec();
const bool touched = drainSpecEvents();
if (touched || now - lastSpecCheckNs > 100000000ull) {
lastSpecCheckNs = now;
checkSpec();
}
}
for (size_t i = 0; i < retired.size();) {
if (now >= retired[i].freeAtNs) {
releaseBuffer(retired[i].buf);
retired.erase(retired.begin() + long(i));
} else {
i++;
}
}
expireGhosts(now);
const bool dash = opt.dashOverride >= 0 ? opt.dashOverride == 1 : test || vr::VROverlay()->IsDashboardVisible();
if (dash != lastDash) {
std::printf("dashboard %s\n", dash ? "visible" : "hidden");
lastDash = dash;
dashChangeNs = now;
graceDone = false;
masksNs = 0;
// Right after the dashboard hides, the wearer still faces where
// the window was: two shots then see the room it hid.
hideShotNs[0] = dash ? 0 : now + 600 * kMs;
hideShotNs[1] = dash ? 0 : now + 2000 * kMs;
}
if (!graceDone && now - dashChangeNs >= kGraceNs) {
graceDone = true;
masksNs = 0; // masks from geometry fetched after the window settled
}
const bool warm = opt.noFeed || feed.state.load() == -1 ? now - startNs > uint64_t((opt.warmup + 1) * 1e9)
: firstRoomNs && now - firstRoomNs > uint64_t(opt.warmup * 1e9);
const bool dumpDue = (wantDump && !dumpedOnce && warm) || g_dumpRequest.load();
const bool active = dash || opt.force || opt.once;
bool anyVisible = false;
for (auto &s : surfaces) anyVisible |= s->spec.visible;
// The feed streams only while glass is on screen; otherwise it is
// attached for one frame every 1/idle-hz s (v4l2cam works only
// while a reader is attached).
if (!opt.noFeed) {
const bool stream = active && (anyVisible || opt.force || opt.once);
feed.setStreaming(stream);
feed.minIntervalNs = uint64_t(1e9 / opt.feedHz);
if (!stream && opt.idleHz > 0 && (!lastShotNs || now - lastShotNs >= uint64_t(1e9 / opt.idleHz))) {
feed.requestShot();
lastShotNs = now;
}
for (uint64_t &t : hideShotNs)
if (t && now >= t) {
if (!stream && opt.idleHz > 0) feed.requestShot();
t = 0;
}
}
// Head pose for rendering; while tracking is lost keep the last one
// (or a standing head at the origin) so dumps and priming still work.
Pose head;
if (test) {
head.t = kTestEye + v3{opt.testHead[0], opt.testHead[1], opt.testHead[2]};
lastHead = head;
haveLastHead = true;
} else if (samplePose(opt.predictMs / 1000.f, head)) {
lastHead = head;
haveLastHead = true;
} else if (haveLastHead) {
head = lastHead;
} else {
head.t = {0, 1.6f, 0};
}
if (test) {
if (!testRoomDone && !surfaces.empty()) makeTestRoom(head, now);
} else {
updateMasks(dash, head, now);
updateRoom(dash, now);
}
roomTimer.poll();
// 0.08 deg / 1 mm: well under a frosted texel, above tracking jitter
const bool moved = poseAngle(head, lastRenderHead) > 0.0014f || length(head.t - lastRenderHead.t) > 0.001f;
const bool due = now >= nextRenderNs;
const bool animating = stepPhases(now); // materialize ramps render at full rate
lastPhaseNs = now;
// What is behind the glass moves (hands, people, the room's light): every room update is shown, at the
// feed's rate (within the fps cap), so the background behind the glass moves fluidly. At ~6 Hz it stepped.
const bool roomDue = roomDirty;
// Continuous rendering only while the dashboard is visible; one priming
// frame after a layout change so new buffers never sit empty.
const bool renderNow = !surfaces.empty() &&
((active && (anyVisible || opt.force || opt.once) && due && (moved || roomDue || layoutDirty || animating || opt.bench)) ||
(layoutDirty && due) || dumpDue);
if (renderNow) {
const bool forced = opt.force || opt.once || dumpDue || !active;
// priming renders while hidden don't ask SteamVR for quads
const bool fetchGeo = dash || opt.force || opt.once || dumpDue;
const uint64_t before = frames;
if (renderAll(head, forced, fetchGeo, now) > 0) {
lastRenderHead = head;
lastRenderNs = now;
roomDirty = layoutDirty = false;
fpsFrames++;
}
nextRenderNs = now + uint64_t(1e9 / opt.fps);
if (outPending || (before == 0 && frames > 0)) {
writeOut(false);
outPending = false;
}
}
if (outPending && now - outPendingSinceNs > 500000000ull) {
writeOut(false); // nothing could be rendered; announce the layout anyway
outPending = false;
}
if (dumpDue && (renderNow || surfaces.empty())) {
const bool sig = g_dumpRequest.exchange(false);
std::string dir = !opt.dumpDir.empty() ? opt.dumpDir : (sig ? "/tmp/lgs/glassd-dump" : "");
std::string roomPath = !opt.dumpRoom.empty() ? opt.dumpRoom : (sig ? "/tmp/lgs/glassd-dump/room.png" : "");
renderTimer.poll();
std::printf("dump frame: gpu %.2f ms%s\n", double(renderTimer.last), renderTimer.have ? "" : " (no timer)");
if (!roomPath.empty()) dumpRoom(roomPath);
if (!dir.empty()) dumpSurfaces(dir);
dumpedOnce = true;
if (opt.once) {
writeOut(false);
break;
}
}
if (now - fpsWindowNs >= 1000000000ull) {
fpsNow = float(double(fpsFrames) * 1e9 / double(now - fpsWindowNs));
fpsFrames = 0;
fpsWindowNs = now;
}
if (now - lastOutNs >= 2000000000ull) writeOut(false);
if (now - lastStatusNs >= 5000000000ull) {
status(now, dash);
if (now - lastKeepNs >= 10000000000ull) { keepRoom(); lastKeepNs = now; }
}
// ~250 Hz while active (pose history + frame pacing), ~100 Hz idle;
// a spec change wakes the loop at once
const int waitMs = active ? 4 : 10;
if (inoFd >= 0) {
pollfd p{inoFd, POLLIN, 0};
poll(&p, 1, waitMs);
} else {
usleep(useconds_t(waitMs) * 1000);
}
}
}
};
} // namespace
int main(int argc, char **argv) {
setvbuf(stdout, nullptr, _IOLBF, 0);
Options opt;
if (!parseArgs(argc, argv, opt)) {
usage();
return 2;
}
struct sigaction sa {};
sa.sa_handler = onSignal;
sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGHUP, &sa, nullptr);
sigaction(SIGUSR1, &sa, nullptr);
sigaction(SIGUSR2, &sa, nullptr);
signal(SIGPIPE, SIG_IGN);
if (opt.selftest == "phase") return selftestPhase();
std::printf("starting (spec %s, out %s, key prefix %s, scale %.2f, pid %d)\n", opt.demo ? "<demo>" : opt.spec.c_str(), opt.out.c_str(),
opt.keyPrefix.c_str(), double(opt.scale), int(getpid()));
Glassd g(opt);
int rc = g.run();
std::printf("exit %d\n", rc);
return rc;
}