mirror of
https://github.com/MichaelScottsman/LiquidAss-Frame.git
synced 2026-10-17 18:02:42 +02:00
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.
This commit is contained in:
1 parent
5af91fc1bd
commit
af4e73c1d3
10 files changed
+290
-66
No files matched your search
+46
-4
@@ -148,6 +148,7 @@
|
||||
const MODE_APPEAR_MS = 1500; // a mode's cover reported this soon after the mode changed carries that time (appearAt)
|
||||
const EXIT_MAX_MS = 2000; // a plate's content fade-out longer than this is not followed (exitMs)
|
||||
const PLATE_GONE_MS = 90; // a plate whose element left the DOM: its glass fades this fast (exitMs)
|
||||
const PAGE_OUT_MS = 150; // --lgs-d-page-out: a page the shell stamped leaving (data-lgs-leaving) fades this long
|
||||
// spring tokens a morphing cover may name (native/shared/motion_tokens.h; glassd evaluates them)
|
||||
const MORPH_TOKENS = ['interactive', 'hover-in', 'fade', 'snappy', 'morph-open', 'morph-close', 'sheet-in', 'sheet-out', 'page', 'depth'];
|
||||
const PING_TTL_MS = 12000; // no ping for this long (once pinged): stop
|
||||
@@ -641,8 +642,6 @@
|
||||
try { return C.Instance ? !!C.Instance.IsInGamepadNav : null; } catch (_) { return null; }
|
||||
}
|
||||
|
||||
// Is the dashboard frame that shows this overlay visible? (A dashboard tab
|
||||
// for another app hides Steam's window.) Unknown layouts don't block.
|
||||
// Does a shown Steam frame show one of SteamVR's own pages? Its frame action for that page (VR Settings)
|
||||
// is then "active" (the frame menu highlights it).
|
||||
function frameOnVrPage() {
|
||||
@@ -656,6 +655,8 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is the dashboard frame that shows this overlay visible? (A dashboard tab
|
||||
// for another app hides Steam's window.) Unknown layouts don't block.
|
||||
function frameVisible(key) {
|
||||
try {
|
||||
const frames = SteamUIStore.WindowStore.VRGamepadUIMainWindowInstance.VRFrameStore.frames;
|
||||
@@ -756,7 +757,7 @@
|
||||
const INPUT_ATTRS = ['class', 'style', 'hidden', 'open', 'data-lgs-plate', 'data-lgs-plate-id',
|
||||
'data-lgs-plate-phase', 'data-lgs-plate-appear', 'data-lgs-plate-tint', 'data-lgs-plate-fill',
|
||||
'data-lgs-plate-occluder', 'data-lgs-plate-r', 'data-lgs-plate-inset', 'data-lgs-mosaic', 'data-lgs-nopop',
|
||||
'data-lgs-destructive', 'data-lgs-media', 'data-lgs-window-dim', 'data-lgs-window-recede'];
|
||||
'data-lgs-destructive', 'data-lgs-media', 'data-lgs-window-dim', 'data-lgs-window-recede', 'data-lgs-leaving'];
|
||||
const SPOT_RE = /--h[xy]\s*:[^;]*;?/g;
|
||||
|
||||
function spotOnly(r) {
|
||||
@@ -937,6 +938,15 @@
|
||||
bump('mutation');
|
||||
ent.domGen++;
|
||||
ent.dirty = true;
|
||||
// reported plates whose elements this mutation took out (a route unmounting: Home's discs as Library
|
||||
// commits) left with their content in this very frame: the report says so now, each plate fading
|
||||
// from this moment (exitAt), not after the next frame-driven tick, which the new route's render
|
||||
// starves (~270 ms measured), followed by the 350 ms dematerialize ramp. Nothing is measured here.
|
||||
if (S.dash && !S.paused && ent.plateLast && ent.plateLast.size
|
||||
&& records.some((r) => r.type === 'childList' && r.removedNodes.length) && platesGone(ent)) {
|
||||
bump('plates-gone');
|
||||
emit();
|
||||
}
|
||||
// a glass mode change is measured and reported right here, before Steam renders the new route
|
||||
// (a tick would wait for that render), so glassd starts materializing the window at once, from
|
||||
// the moment the mode changed (modeAt: the cover's appearAt)
|
||||
@@ -1597,7 +1607,12 @@
|
||||
const goneAt = Date.now();
|
||||
const curIds = new Set(plates.map((p) => p.id));
|
||||
for (const [pid, last] of ent.plateLast || []) {
|
||||
if (curIds.has(pid) || last.p.exitAt || !last.el || last.el.isConnected) continue;
|
||||
if (curIds.has(pid) || last.p.exitAt || !last.el) continue;
|
||||
// gone from the DOM, or still in it but no longer rendered (Steam hides the old route a frame
|
||||
// before it unmounts it)
|
||||
let shown = last.el.isConnected;
|
||||
if (shown) { try { shown = last.el.checkVisibility({ visibilityProperty: true }); } catch (_) { shown = true; } }
|
||||
if (shown) continue;
|
||||
plates.push(Object.assign({}, last.p, { exitAt: goneAt, exitMs: PLATE_GONE_MS }));
|
||||
}
|
||||
if (!why) ent.plateLast = new Map(out.plateEls.map((pe) => [pe.p.id, { p: pe.p, el: pe.el }]));
|
||||
@@ -1759,6 +1774,13 @@
|
||||
if (ctx.held > held) exit = fadeOutOf(ctx, el);
|
||||
}
|
||||
}
|
||||
// its page is leaving (the shell's stamp, epoch ms, on an ancestor): the glass leaves on that clock
|
||||
if (!appear && !exit) {
|
||||
let lv = null;
|
||||
try { lv = el.closest('[data-lgs-leaving]'); } catch (_) { lv = null; }
|
||||
const at = lv ? +lv.getAttribute('data-lgs-leaving') : 0;
|
||||
if (at > 0 && Date.now() - at < EXIT_MAX_MS) exit = { at, ms: PAGE_OUT_MS };
|
||||
}
|
||||
const p = { id, x: t.x, y: t.y, w: t.w, h: t.h, r: t.r, material: mat };
|
||||
if (appear) { p.appear = 'materialize'; p.phaseMs = Math.max(0, Math.min(2000, Math.round(appear.ms))); p.appearAt = Math.round(appear.at); }
|
||||
if (exit) { p.exitAt = Math.round(exit.at); p.exitMs = Math.round(exit.ms); }
|
||||
@@ -2607,6 +2629,26 @@
|
||||
ent.applied = plan;
|
||||
}
|
||||
|
||||
// The last report's plates whose element is no longer in the DOM, marked leaving from now (exitAt, over
|
||||
// PLATE_GONE_MS) in the current report, without measuring anything. True when one was.
|
||||
function platesGone(ent) {
|
||||
const surf = ent.cur && ent.cur.surface;
|
||||
if (!surf || !Array.isArray(surf.plates) || !surf.visible) return false;
|
||||
const at = Date.now();
|
||||
let n = 0;
|
||||
const plates = surf.plates.map((p) => {
|
||||
const last = ent.plateLast.get(p.id);
|
||||
if (!last || !last.el || last.el.isConnected || p.exitAt) return p;
|
||||
n++;
|
||||
ent.plateLast.delete(p.id); // left: the next compute does not mark it again
|
||||
return Object.assign({}, p, { exitAt: at, exitMs: PLATE_GONE_MS });
|
||||
});
|
||||
if (!n) return false;
|
||||
ent.cur = Object.assign({}, ent.cur, { surface: Object.assign({}, surf, { plates }) });
|
||||
ent.sig = JSON.stringify(ent.cur.surface);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A plate a route just inserted, in a native window: glassd's at once (data-lgs-plate-ack, so its CSS
|
||||
// plate never paints), until it is measured and acked, or GLASS_WAIT_MS pass without that.
|
||||
function platePending(ent, el) {
|
||||
|
||||
+55
-9
@@ -55,6 +55,7 @@
|
||||
const SLAB_OVER_COVER = 0.0003; // and at least this far in front of the cover
|
||||
const MIN_PIECE = 1; // px; smaller mosaic slivers are dropped
|
||||
const SINK_MAX_MS = 600; // glassd keeps a retired slab cell drawn this long
|
||||
const STEREO_PARALLEL = 1; // SteamVR's EStereoscopy: Mono 0, Parallel 1 (left eye: left half)
|
||||
const ROUTE_OUT_MS = 150; // --lgs-d-page-out: a pop of a page that is leaving goes after this
|
||||
const ROUTE_ADOPT_MS = 400; // ... and one still lifted this long after the mode change stays, as the page's
|
||||
const ROOT_ID = GLOBAL === '__LGS_SG' ? 'lgs-sg-root' : 'lgs-sg-root-' + GLOBAL;
|
||||
@@ -501,6 +502,9 @@
|
||||
delete p.curvature;
|
||||
}
|
||||
Object.assign(p, d.flags || {});
|
||||
// glassd's per-eye texture (left eye's layout on the left half): each eye sees its own half, at the
|
||||
// same uv (SteamVR's stereoscopy "Parallel"), so the room behind the glass has its depth
|
||||
if (d.stereo) p.stereoscopy = STEREO_PARALLEL;
|
||||
// Wearer profile only (PLAN 1.7, SP 2.6): the crop takes the laser itself.
|
||||
if (d.interactive) Object.assign(p, { interactive: true, 'steam-input-appid': 769, 'can-take-keyboard-focus': true });
|
||||
return p;
|
||||
@@ -556,7 +560,8 @@
|
||||
// z of animated items is not part of the signature (applyAnimated sets it).
|
||||
function sigOf(d) {
|
||||
return [d.parentKey, r6(d.u), r6(d.v), d.popKey ? 'anim' : r6(d.z), d.key, d.uv.map(r6).join(','), d.mpp, d.name, d.curvOrigin || '',
|
||||
JSON.stringify(d.flags || {}), d.interactive ? 1 : 0, d.dimKey || '', d.popKey ? r6(d.baseDz) + '/' + r6(d.coverDz) : ''].join('|');
|
||||
JSON.stringify(d.flags || {}), d.interactive ? 1 : 0, d.dimKey || '', d.popKey ? r6(d.baseDz) + '/' + r6(d.coverDz) : '',
|
||||
d.stereo ? 'st' : ''].join('|');
|
||||
}
|
||||
|
||||
// Update in place; true if anything changed, 'rebuild' if the wrapper changes.
|
||||
@@ -929,10 +934,14 @@
|
||||
for (const [pk, ps] of st.pops) if (ps.surface === key && c.seen.has(pk) && !routeOut(ps, windowless)) { popped = true; break; }
|
||||
const behind = COVER_BEHIND && (!popped || windowless) && W.__LGS_SG_FRONT !== true;
|
||||
// __LGS_SG_FRONT: lab A/B, the layout with crops
|
||||
// a stereo surface (glassd: the left eye's backdrop, then the right eye's): the panel spans the pair,
|
||||
// which SteamVR splits per eye; shown whole only (a crop of a pair would not split at its seam)
|
||||
const cuv = [b0 + (b2 - b0) * R.x0 / Wd, b1 + (b3 - b1) * R.y0 / Ht, b0 + (b2 - b0) * R.x1 / Wd, b1 + (b3 - b1) * R.y1 / Ht];
|
||||
const coverStereo = g.stereo === true && R.x0 === 0 && R.x1 === Wd;
|
||||
if (coverStereo) cuv[2] = cuv[0] + 2 * (cuv[2] - cuv[0]);
|
||||
add('cover', {
|
||||
kind: 'cover', id: null, px: [R.x1 - R.x0, R.y1 - R.y0], u: au((R.x0 + R.x1) / 2), v: av((R.y0 + R.y1) / 2), z: behind ? COVER_BEHIND_DZ : coverDz,
|
||||
key: g.key, uv: [b0 + (b2 - b0) * R.x0 / Wd, b1 + (b3 - b1) * R.y0 / Ht, b0 + (b2 - b0) * R.x1 / Wd, b1 + (b3 - b1) * R.y1 / Ht],
|
||||
mpp: M / scale, name: 'lgs:cover:' + short, dimKey,
|
||||
key: g.key, uv: cuv, mpp: M / scale, name: 'lgs:cover:' + short, dimKey, stereo: coverStereo,
|
||||
});
|
||||
|
||||
const holes = [];
|
||||
@@ -961,10 +970,14 @@
|
||||
// The slab is the element's full rect; crop it like the element was.
|
||||
const [s0, t0, s1, t1] = ps.slab.map(Number);
|
||||
const fx0 = (x0 - x) / w, fx1 = (x1 - x) / w, fy0 = (y0 - y) / h, fy1 = (y1 - y) / h;
|
||||
add('slab:' + id, {
|
||||
const suv = [s0 + (s1 - s0) * fx0, t0 + (t1 - t0) * fy0, s0 + (s1 - s0) * fx1, t0 + (t1 - t0) * fy1];
|
||||
// stereo: the panel spans the cell's pair; clipped sideways (an element scrolled half out) the pair
|
||||
// cannot be split at its seam, so the slab is left out while it is
|
||||
const sideClip = fx0 > 0.001 || fx1 < 0.999;
|
||||
if (g.stereo === true) suv[2] = suv[0] + 2 * (suv[2] - suv[0]);
|
||||
if (!(g.stereo === true && sideClip)) add('slab:' + id, {
|
||||
kind: 'slab', id, ghost: ps.ghost, px: [x1 - x0, y1 - y0], u, v, z: ps.c.target, popKey: pk, baseDz, coverDz,
|
||||
key: g.key, uv: [s0 + (s1 - s0) * fx0, t0 + (t1 - t0) * fy0, s0 + (s1 - s0) * fx1, t0 + (t1 - t0) * fy1],
|
||||
mpp: M / scale, name: 'lgs:slab:' + short + ':' + id,
|
||||
key: g.key, uv: suv, mpp: M / scale, name: 'lgs:slab:' + short + ':' + id, stereo: g.stereo === true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -980,8 +993,10 @@
|
||||
const k = Math.max(1, num(o.scaleUp, ROOMDIM_SCALE));
|
||||
add('roomdim:' + o.id, {
|
||||
kind: 'roomdim', id: String(o.id), px: [Math.round(w * k), Math.round(h * k)], u: au(x + w / 2), v: av(y + h / 2),
|
||||
z: Math.min(num(o.dz, ROOMDIM_DZ), ROOMDIM_DZ), key: g.key, uv: o.slab.map(Number), mpp: (M / scale) * k,
|
||||
name: 'lgs:roomdim:' + short + ':' + o.id,
|
||||
z: Math.min(num(o.dz, ROOMDIM_DZ), ROOMDIM_DZ), key: g.key, mpp: (M / scale) * k,
|
||||
// stereo: the cell's pair (the same flat tone for both eyes)
|
||||
uv: (() => { const q = o.slab.map(Number); if (g.stereo === true) q[2] = q[0] + 2 * (q[2] - q[0]); return q; })(),
|
||||
name: 'lgs:roomdim:' + short + ':' + o.id, stereo: g.stereo === true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1098,8 +1113,11 @@
|
||||
for (const k of Object.keys(sp)) if (!VR_SKIP.includes(k)) props[k] = sp[k];
|
||||
// a panel sized by metres per pixel: our texture has `scale` px per panel px
|
||||
if (num(sp['meters-per-pixel'], 0) > 0) props['meters-per-pixel'] = num(sp['meters-per-pixel'], 0) / scale;
|
||||
Object.assign(props, { key: g.key, uv_min: [r6(b0), r6(b1)], uv_max: [r6(b2), r6(b3)], interactive: false, reflect: 0,
|
||||
// stereo: the panel spans the left eye's backdrop and the right eye's after it
|
||||
const vb2 = g.stereo === true ? b0 + 2 * (b2 - b0) : b2;
|
||||
Object.assign(props, { key: g.key, uv_min: [r6(b0), r6(b1)], uv_max: [r6(vb2), r6(b3)], interactive: false, reflect: 0,
|
||||
debug_name: 'lgs:vr:' + d.name });
|
||||
if (g.stereo === true) props.stereoscopy = STEREO_PARALLEL;
|
||||
let c = VRC.get(d.name);
|
||||
if (!c || c.el !== p || !c.xf || !c.xf.isConnected) {
|
||||
if (c) vrCoverDrop(d.name);
|
||||
@@ -1724,6 +1742,34 @@
|
||||
pushNow();
|
||||
return { yaw: d, applied: ov.applied, missing: ov.missing };
|
||||
},
|
||||
// A test panel on Steam's main window showing a crop of an overlay's texture with a stereoscopy mode
|
||||
// (0 mono, 1 side-by-side parallel): how SteamVR maps a cropped panel per eye. probe(null) removes it.
|
||||
// o: {key, uv: [u0, v0, u1, v1], stereo, u, v (anchor on main), z (m), mpp}
|
||||
stereoProbe(o) {
|
||||
if (st.destroyed) return { error: 'destroyed' };
|
||||
contact();
|
||||
if (st.stereoProbe) {
|
||||
const p = st.stereoProbe;
|
||||
st.stereoProbe = null;
|
||||
try { p.remove(); } catch (_) { /* gone */ }
|
||||
retire([p, ...p.querySelectorAll('[sgid]')]);
|
||||
}
|
||||
if (o && typeof o.key === 'string' && Array.isArray(o.uv) && o.uv.length === 4) {
|
||||
const top = vnode('reparent-to-panel', { 'parent-overlay-key': STEAM_KEY });
|
||||
const anchor = vnode('panel-anchor', { 'anchor-u': num(o.u, 0.5), 'anchor-v': num(o.v, 0.5) });
|
||||
const xf = vtransform(num(o.z, 0.05));
|
||||
const panel = vnode('panel', { key: o.key, uv_min: [o.uv[0], o.uv[1]], uv_max: [o.uv[2], o.uv[3]],
|
||||
'meters-per-pixel': num(o.mpp, 0.0009), origin: [0, 0], interactive: false, visibility: 0, reflect: 0,
|
||||
stereoscopy: Math.round(num(o.stereo, 0)), debug_name: 'lgs:stereo-probe' });
|
||||
xf.appendChild(panel);
|
||||
anchor.appendChild(xf);
|
||||
top.appendChild(anchor);
|
||||
rootEl().appendChild(top);
|
||||
st.stereoProbe = top;
|
||||
}
|
||||
pushNow();
|
||||
return { on: !!st.stereoProbe };
|
||||
},
|
||||
};
|
||||
|
||||
// ------------------------------------------------------------ inspection
|
||||
|
||||
+19
-3
@@ -1308,6 +1308,10 @@ class Shell:
|
||||
self.set_tune(lgs.clean_tune(t), save=True)
|
||||
return {"tune": self.tune}
|
||||
|
||||
def stereo_on(self):
|
||||
"""Per-eye glass unless the flag glassStereo is false."""
|
||||
return (self.rt_flags or {}).get("glassStereo") is not False
|
||||
|
||||
def set_tune(self, t, save):
|
||||
"""A new tune (the panel's, or tune.json changed by `lgs dial`): glassd.json, the bridge, and
|
||||
(save) tune.json plus the dial file a moment later."""
|
||||
@@ -1505,6 +1509,10 @@ class Shell:
|
||||
surf["masks"] = clean_masks(s.get("masks"))
|
||||
if "scaleFrom" in caps and s.get("scaleFrom") in ("main", "overlay"):
|
||||
surf["scaleFrom"] = s["scaleFrom"]
|
||||
# per-eye glass where the scene graph shows the surface whole (Steam's window; the bar, the frame
|
||||
# menu and the popups show crops of theirs, which a stereo panel cannot split per eye)
|
||||
if "stereo" in caps and name == "main" and self.stereo_on():
|
||||
surf["stereo"] = True
|
||||
# materialize: a new slab or plate id appears with materialize (GM §5.4)
|
||||
for kind, items in (("slab", surf["slabs"]), ("plate", surf.get("plates") or [])):
|
||||
for it in items:
|
||||
@@ -1564,8 +1572,13 @@ class Shell:
|
||||
on = vn in self.vr_shown
|
||||
surfaces.append({"name": "vr." + vn, "overlayKey": STEAM_MAIN_KEY, "texW": w, "texH": h,
|
||||
"radius": gr, "material": vp["material"], "visible": on, "armed": not on, "slabs": [],
|
||||
**({"stereo": True} if "stereo" in caps and self.stereo_on() else {}),
|
||||
"shapes": [{"x": gx, "y": gy, "w": gw, "h": gh, "r": gr}]})
|
||||
cfg = {"dial": self.dial_v, "reduceMotion": bool(self.reduce_motion), "surfaces": surfaces}
|
||||
# per-eye glass (glassd renders each eye's view side by side; the scene graph shows each eye its half):
|
||||
# on unless the flag glassStereo is false (contracts/glassd.md §1.1)
|
||||
if "stereo" in caps:
|
||||
cfg["stereo"] = self.stereo_on()
|
||||
if "tune" in caps: # the wearer's tune (glassd eases to it; contracts/glassd.md §1.1)
|
||||
t = self.tune
|
||||
cfg["tune"] = {"refract": t["refract"], "frost": t["frost"], "light": t["light"]}
|
||||
@@ -1774,7 +1787,8 @@ class Shell:
|
||||
p["sink"] = L["sink"]
|
||||
popped.append(p)
|
||||
sp = {"steamKey": s["overlayKey"], "texW": texW, "texH": texH, "visible": True,
|
||||
"glassd": {"key": g.get("key") or "glassd." + s["name"], "backdrop": backdrop, "scale": scale},
|
||||
"glassd": {"key": g.get("key") or "glassd." + s["name"], "backdrop": backdrop, "scale": scale,
|
||||
"stereo": g.get("stereo") is True},
|
||||
"coverDz": COVER_DZ, "baseDz": BASE_DZ, "popped": popped}
|
||||
if _num(s.get("coverDz"), -1, 1) is not None:
|
||||
sp["coverDz"] = _num(s["coverDz"], -1, 1)
|
||||
@@ -1811,7 +1825,8 @@ class Shell:
|
||||
if isinstance(gv, dict) and _nums(gv.get("backdrop"), 4) is not None:
|
||||
vr.append({"name": vn, "glassd": {"key": gv.get("key") or "glassd.vr." + vn,
|
||||
"backdrop": _nums(gv.get("backdrop"), 4),
|
||||
"scale": _num(gv.get("backdropScale") or 0.75, 0.05, 4)}})
|
||||
"scale": _num(gv.get("backdropScale") or 0.75, 0.05, 4),
|
||||
"stereo": gv.get("stereo") is True}})
|
||||
if vr:
|
||||
spec["vr"] = vr
|
||||
flags = self.read_flags()
|
||||
@@ -1826,7 +1841,8 @@ class Shell:
|
||||
if None in (texW, texH, scale) or backdrop is None:
|
||||
return None
|
||||
sp = {"steamKey": s["overlayKey"], "texW": texW, "texH": texH, "visible": False, "standby": True,
|
||||
"glassd": {"key": g.get("key") or "glassd." + s["name"], "backdrop": backdrop, "scale": scale},
|
||||
"glassd": {"key": g.get("key") or "glassd." + s["name"], "backdrop": backdrop, "scale": scale,
|
||||
"stereo": g.get("stereo") is True},
|
||||
"coverDz": COVER_DZ, "baseDz": BASE_DZ, "popped": []}
|
||||
if _num(s.get("coverDz"), -1, 1) is not None:
|
||||
sp["coverDz"] = _num(s["coverDz"], -1, 1)
|
||||
|
||||
@@ -123,6 +123,29 @@
|
||||
const f = q(m.doc, S.footerSel);
|
||||
return !!(f && q(f, S.legendSel));
|
||||
}
|
||||
// Leaving our Home (Home, a folder) for another route: Home has no route exit when the new route shares
|
||||
// its top-level route (Home -> Library, both under /library): Steam swaps the content in one frame once
|
||||
// Library has rendered, 100-500 ms later. The Home view that is leaving is stamped with the moment it
|
||||
// started (epoch ms, before the glass mode flips, so the reporter's tick on that flip already sees it):
|
||||
// it fades on the route exit (41-home.css, --lgs-d-page-out) and the reporter lets its plates go on the
|
||||
// same clock (exitAt), however late its next tick comes while the new route renders. Only the views
|
||||
// present now carry it (never the new page); back to Home clears it.
|
||||
function leaving(path) {
|
||||
const m = S.main;
|
||||
if (!m || typeof path !== 'string') return;
|
||||
const root = q(m.doc, S.rootSel);
|
||||
const home = (k) => k === 'home' || k === 'folder';
|
||||
let views = [];
|
||||
try { views = [...m.doc.querySelectorAll('.lgs-home')]; } catch (_) { return; }
|
||||
if (home(mapRoute(path).key)) {
|
||||
for (const v of views) if (v.hasAttribute('data-lgs-leaving')) v.removeAttribute('data-lgs-leaving');
|
||||
return;
|
||||
}
|
||||
// Steam's redirect right after (/library -> its tab) is a second history change: the stamp stays
|
||||
if (!root || !home(S.route && S.route.key) || root.getAttribute('data-lgs-glass') !== 'windowless') return;
|
||||
const at = String(Date.now());
|
||||
for (const v of views) if (!v.hasAttribute('data-lgs-leaving')) v.setAttribute('data-lgs-leaving', at);
|
||||
}
|
||||
// The new route's glass mode at once, in the history change itself, when the map alone decides it (no
|
||||
// hook of the route's own that reads its not-yet-rendered page): the reporter sees the mode flip before
|
||||
// Steam renders the route, so the window glass materializes with the route change (Home -> Library),
|
||||
@@ -808,6 +831,7 @@
|
||||
e.html.style.removeProperty('--lgs-c1a-tab-wx');
|
||||
e.html.classList.remove('lgs-modal');
|
||||
for (const n of e.doc.querySelectorAll('.lgs-tabcap')) n.remove();
|
||||
for (const n of e.doc.querySelectorAll('.lgs-home[data-lgs-leaving]')) n.removeAttribute('data-lgs-leaving');
|
||||
} catch (_) { /* closed */ }
|
||||
}
|
||||
}
|
||||
@@ -825,6 +849,7 @@
|
||||
if (!S) return;
|
||||
try { trackHistory(loc, action); } catch (_) { /* keep going */ }
|
||||
S.otherMode = null; setTarget(null);
|
||||
try { leaving(loc && loc.pathname); } catch (_) { /* no fade: Steam's swap */ }
|
||||
try { earlyMode(loc && loc.pathname); } catch (_) { /* applyRoute below */ }
|
||||
S.rt.setTimeout(() => S && applyRoute('history'), 0);
|
||||
});
|
||||
|
||||
+18
-9
@@ -123,13 +123,17 @@ __LGS_RT.define({
|
||||
// ---- the panel (the bar popup's contents)
|
||||
const contentsClass = () => { try { const s = rt.sel('%{DashboardBarPopupContents}'); return /^\.[\w-]+$/.test(s) ? s.slice(1) : ''; } catch (_) { return ''; } };
|
||||
const pct = (v) => Math.round(v * 100);
|
||||
// one row: the name and the value on one line, Steam's bare slider below (its own gamepad and laser
|
||||
// handling; no label of its own, so nothing wraps in the 300 pp column)
|
||||
function Slider(props) {
|
||||
if (!c.SliderField) return null;
|
||||
return jsx('div', { className: 'lgs-tune-row' + (props.disabled ? ' is-disabled' : ''), children: jsx(c.SliderField, {
|
||||
label: props.label, description: props.description, value: props.value, min: props.min || 0, max: props.max,
|
||||
step: 1, showValue: true, valueSuffix: '%', disabled: !!props.disabled, bottomSeparator: 'none',
|
||||
resetValue: props.reset, onChange: props.onChange,
|
||||
}) });
|
||||
return jsxs('div', { className: 'lgs-tune-row' + (props.disabled ? ' is-disabled' : ''), children: [
|
||||
jsx('div', { className: 'lgs-tune-label', children: [props.label, jsx('span', { children: props.value + '%' }, 'v')] }, 'l'),
|
||||
jsx(c.SliderField, {
|
||||
value: props.value, min: props.min || 0, max: props.max, step: 1, disabled: !!props.disabled,
|
||||
bottomSeparator: 'none', resetValue: props.reset, onChange: props.onChange,
|
||||
}, 's'),
|
||||
] });
|
||||
}
|
||||
function TunePanel() {
|
||||
const [, bump] = React.useState(0);
|
||||
@@ -138,7 +142,6 @@ __LGS_RT.define({
|
||||
const d = daemon();
|
||||
const cls = contentsClass();
|
||||
const hueName = (HUES.find((h) => h[0] === t.hue) || HUES[0])[1];
|
||||
const nativeOnly = d.native ? undefined : 'Native glass only';
|
||||
return jsxs(Focusable, {
|
||||
className: 'lgs-tune-panel' + (cls ? ' ' + cls : ''),
|
||||
'flow-children': 'column',
|
||||
@@ -146,6 +149,7 @@ __LGS_RT.define({
|
||||
children: [
|
||||
jsx('div', { className: 'lgs-tune-title', children: 'Glass' }, 'title'),
|
||||
d.live ? null : jsx('div', { className: 'lgs-tune-note', children: 'Glass service not running' }, 'note'),
|
||||
d.live && !d.native ? jsx('div', { className: 'lgs-tune-note', children: 'Refraction and Highlights need native glass' }, 'nn') : null,
|
||||
jsx('div', { className: 'lgs-tune-label', children: ['Colour', jsx('span', { children: hueName }, 'v')] }, 'cl'),
|
||||
jsx(Focusable, {
|
||||
className: 'lgs-tune-swatches', 'flow-children': 'row',
|
||||
@@ -158,13 +162,13 @@ __LGS_RT.define({
|
||||
}, 'sw'),
|
||||
jsx(Slider, { label: 'Tint strength', value: pct(t.hueK), max: 100, reset: 50, disabled: !t.hue,
|
||||
onChange: (v) => set({ hueK: v / 100 }) }, 'k'),
|
||||
jsx(Slider, { label: 'Glass intensity', description: 'Clear to frosted', value: pct(t.dial), max: 100, reset: 50,
|
||||
jsx(Slider, { label: 'Glass intensity', value: pct(t.dial), max: 100, reset: 50,
|
||||
onChange: (v) => set({ dial: v / 100 }) }, 'dial'),
|
||||
jsx(Slider, { label: 'Refraction', description: nativeOnly, value: pct(t.refract), max: 200, reset: 100, disabled: !d.native,
|
||||
jsx(Slider, { label: 'Refraction', value: pct(t.refract), max: 200, reset: 100, disabled: !d.native,
|
||||
onChange: (v) => set({ refract: v / 100 }) }, 'ref'),
|
||||
jsx(Slider, { label: 'Frost', value: pct(t.frost), max: 200, reset: 100,
|
||||
onChange: (v) => set({ frost: v / 100 }) }, 'frost'),
|
||||
jsx(Slider, { label: 'Highlights', description: nativeOnly, value: pct(t.light), max: 200, reset: 100, disabled: !d.native,
|
||||
jsx(Slider, { label: 'Highlights', value: pct(t.light), max: 200, reset: 100, disabled: !d.native,
|
||||
onChange: (v) => set({ light: v / 100 }) }, 'light'),
|
||||
c.DialogButton ? jsx(c.DialogButton, { className: 'lgs-tune-reset', onClick: reset, children: 'Reset to default' }, 'reset') : null,
|
||||
],
|
||||
@@ -185,6 +189,7 @@ __LGS_RT.define({
|
||||
function TuneButton() {
|
||||
const handle = React.useRef(undefined);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
S.btnHandle = handle; // status, and the lab's open() / close()
|
||||
const Btn = findBtn();
|
||||
if (!Btn) return null;
|
||||
const bc = barClasses();
|
||||
@@ -244,6 +249,10 @@ __LGS_RT.define({
|
||||
shown: (() => { try { return rt.windows.byKind('bar').some((e) => !!e.doc.querySelector('.lgs-tune')); } catch (_) { return null; } })() }),
|
||||
set: (patch) => set(patch || {}),
|
||||
reset,
|
||||
// the popup, for tests (the wearer opens it with the paintbrush)
|
||||
open: () => { const h = S.btnHandle && S.btnHandle.current; if (!h) return false; h.openPopup(); return true; },
|
||||
close: () => { const h = S.btnHandle && S.btnHandle.current; if (!h) return false; h.closePopup(); return true; },
|
||||
isOpen: () => { const h = S.btnHandle && S.btnHandle.current; return !!(h && h.BPopupOpen && h.BPopupOpen()); },
|
||||
};
|
||||
},
|
||||
remove() {
|
||||
|
||||
@@ -24,6 +24,7 @@ Units, unless a row says otherwise:
|
||||
|---|---|---|---|---|
|
||||
| `seq` | int | 0 | Echoed in the output | live |
|
||||
| `dial` | 0..1 | 0.5 | Transparency dial (GM §2). Eased (time constant 40 ms) when it changes, like `tune` | live |
|
||||
| **`stereo`** | bool | true | false: every surface mono (P8: the flag `glassStereo`). Cap `stereo` | built |
|
||||
| **`tune`** | `{refract, frost, light, hue?, hueK?}` | all 1, no hue | The wearer's glass tune (the bar's paintbrush panel, `lgs dial`; P8 sends it from `~/.config/glass-shell/tune.json`). Multipliers 0..2 on every material after its preset, size and the dial: `refract` × lens deviation and dispersion, `frost` × interior and edge frost (at most mip 6), `light` × key specular, Fresnel and sheen (not on occluders). `hue` (a colour) with `hueK` 0..1 moves the glass body's neutral toward that colour (`uHue`; the tint deepens by 0.2 × `hueK`); per-piece `tint`s stay on top. glassd eases every value (40 ms) and renders while it moves; `glassd-out.json` `tune` shows the values drawn. Cap `tune` | built |
|
||||
| `reduceMotion` | bool | false | Every phase ramp is a 180 ms coverage fade (MO C8) | live |
|
||||
| **`roomDim`** | 0..0.9 | 0 | G7 (SM-D6 "surroundings dim", behind C7's flag `roomDim`, off by default): every cover and plate sees the room darkened by this much (folded into the glass's backdrop dimming), so the glass stays consistent with a dimmed room behind it. glassd animates it itself on `sheet-in` up and `sheet-out` down (180 ms fade under `reduceMotion`); the first spec applies it at once. The dark panel around the window is a `dim` slab (§1.4) that P7 stretches behind the window | live |
|
||||
@@ -45,6 +46,7 @@ Units, unless a row says otherwise:
|
||||
| **`plates`** | `[plate]` | [] | §1.3. **≤ 32**; extra plates are dropped and listed (§3) | live |
|
||||
| **`coverDz`** | units | 0.001 | Where the cover and its plates sit relative to the surface plane, for glassd's optics only (the scene graph places the panel; P7 must use the same value as its spec `coverDz`). K-G6: `-0.027` (−10 mm at r = 1) puts the keyboard platter behind the keys | live |
|
||||
| **`masks`** | `[{x, y, w, h, dz?}]` | [] | Extra rects of **this surface** cut out of the room map (G4), in its Steam px (they may lie outside the texture, e.g. an ornament beside the window at negative x), at `dz` units. ≤ 8 per surface | live |
|
||||
| **`stereo`** | bool | false | Per-eye glass (with top-level `stereo`, unless glassd runs `--mono`): every region of the surface's texture is a pair, the left eye's copy then the right eye's, each rendered from that eye (its own rays through the glass and its own highlights), so the room behind the glass has its stereo depth instead of lying on the glass like a picture. Every x doubles: the backdrop's copies at [0, bw) and [bw, 2bw), a slab cell at mono x at [2x, 2x + w) and [2x + w, 2x + 2w); the texture is twice as wide, the mouse scale stays one copy's size (the scene graph sizes panels from it). `glassd-out.json` reports the left copy's uv (`backdrop`, slabs) and `stereo: true`; a scene-graph panel spans the pair (u1 = u0 + 2 (u1 - u0)) with `stereoscopy: 1` (Parallel), which SteamVR splits per eye **at the middle of the panel's own uv rect** (measured 2026-10-09, not the texture's halves). So only a surface the scene graph shows whole can be stereo: P8 sets it for `main` and the SteamVR panels (`vr.*`), not for the bar, the frame menu or popups (their panels are crops). A slab clipped sideways is left out while it is. Cost: the GPU time grew from about 2.9 to 3.5 ms a frame (main in stereo). Cap `stereo` | built |
|
||||
| **`scaleFrom`** | `"main"` \| `"overlay"` | `"main"` for non-main surfaces | Which metres-per-pixel glassd trusts for a surface without `quad`: Steam's popups report transforms at the wrong scale, so glassd rescales them to the window's (README *Geometry*). `"overlay"` keeps the overlay's own transform (for overlays that report it right, e.g. a keyboard if K-G2 shows so) | live |
|
||||
|
||||
### 1.3 `plates[]` (G1): opaque per-shape glass at the cover's depth
|
||||
|
||||
@@ -100,6 +100,7 @@ struct Options {
|
||||
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
|
||||
@@ -188,6 +189,7 @@ bool parseArgs(int argc, char **argv, Options &o) {
|
||||
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()); }
|
||||
@@ -531,6 +533,7 @@ struct SurfSpec {
|
||||
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
|
||||
@@ -544,6 +547,7 @@ 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)
|
||||
@@ -708,6 +712,7 @@ bool parseSpec(const std::string &text, Spec &out, std::string &err) {
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -858,6 +863,7 @@ bool parseSpec(const std::string &text, Spec &out, std::string &err) {
|
||||
}
|
||||
}
|
||||
ss.scaleFromOverlay = s.str("scaleFrom", "main") == "overlay";
|
||||
ss.stereo = s.boolean("stereo", false);
|
||||
out.surfaces.push_back(std::move(ss));
|
||||
}
|
||||
return true;
|
||||
@@ -898,6 +904,7 @@ struct Surface {
|
||||
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;
|
||||
@@ -1110,6 +1117,8 @@ class Glassd {
|
||||
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;
|
||||
@@ -1401,8 +1410,10 @@ class Glassd {
|
||||
// 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) {
|
||||
if (s.gw == gw && s.gh == gh) return;
|
||||
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});
|
||||
@@ -1410,6 +1421,7 @@ class Glassd {
|
||||
}
|
||||
s.gw = gw;
|
||||
s.gh = gh;
|
||||
s.eyes = eyes;
|
||||
s.next = 0;
|
||||
s.last = -1;
|
||||
s.rendered = false;
|
||||
@@ -1486,7 +1498,7 @@ class Glassd {
|
||||
// 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) {
|
||||
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;
|
||||
@@ -1767,10 +1779,10 @@ class Glassd {
|
||||
bool ensureBuffers(Surface &s) {
|
||||
for (auto &b : s.bufs) {
|
||||
if (b.bo) continue;
|
||||
if (!b.create(gfx, s.gw, s.gh)) return false;
|
||||
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);
|
||||
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;
|
||||
@@ -2606,10 +2618,17 @@ class Glassd {
|
||||
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.gh);
|
||||
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.
|
||||
@@ -2618,8 +2637,24 @@ class Glassd {
|
||||
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(0, 0, s.bw, s.bh);
|
||||
glScissor(ox, 0, s.bw, s.bh);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
const Geometry &g = s.geo;
|
||||
const float mpp = designMpp();
|
||||
@@ -2685,8 +2720,8 @@ class Glassd {
|
||||
progGlass.set("uLowScale", lowScale);
|
||||
progGlass.set("uInner", inner);
|
||||
}
|
||||
glScissor(sx0, sy0, sx1 - sx0, sy1 - sy0);
|
||||
drawGlass(s, g, head, cm, cph, 0, 0, s.bw, s.bh, {0, 0, 0}, shapes, coverDzM, false, casters, twoPass ? 2 : 0);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2745,9 +2780,9 @@ class Glassd {
|
||||
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(x0, y0, x1 - x0, y1 - y0);
|
||||
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), 0, 0, s.bw, s.bh, {0, 0, 0},
|
||||
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;
|
||||
}
|
||||
@@ -2768,37 +2803,38 @@ class Glassd {
|
||||
auto drawSlab = [&](const SlabSlot &sl) {
|
||||
float ex, ey, sw, sh;
|
||||
slabRect(s, sl, ex, ey, sw, sh);
|
||||
const int gx0 = std::max(0, sl.x - 1), gy0 = std::max(s.bh, sl.y - 1);
|
||||
const int gx1 = std::min(s.gw, sl.x + sl.w + 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
|
||||
// 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(sl.x, sl.y, sl.w, sl.h);
|
||||
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), sl.x, sl.y, sl.w, sl.h, {ex, ey, 0},
|
||||
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(sl.x, sl.y, sl.w, sl.h);
|
||||
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), sl.x, sl.y, sl.w, sl.h,
|
||||
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;
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
s.last = s.next;
|
||||
s.next = (s.next + 1) % 3;
|
||||
s.rendered = true;
|
||||
}
|
||||
|
||||
// Renders every visible surface, waits for the GPU, then hands the new
|
||||
@@ -2814,6 +2850,15 @@ class Glassd {
|
||||
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;
|
||||
@@ -2865,7 +2910,8 @@ class Glassd {
|
||||
// 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) {
|
||||
vr::HmdVector2_t mouse = {float(b.w), float(b.h)};
|
||||
// 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;
|
||||
@@ -2931,7 +2977,7 @@ class Glassd {
|
||||
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\"]";
|
||||
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));
|
||||
@@ -2950,7 +2996,7 @@ class Glassd {
|
||||
: 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->bh / gh,
|
||||
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:
|
||||
@@ -2965,11 +3011,14 @@ class Glassd {
|
||||
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];
|
||||
std::snprintf(b, sizeof b, "%s%s: [%.6f, %.6f, %.6f, %.6f]", i ? ", " : "", jsonEscape(sl.s.id).c_str(), sl.x / gw, sl.y / gh,
|
||||
(sl.x + sl.w) / gw, (sl.y + sl.h) / gh);
|
||||
// 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 += "}";
|
||||
|
||||
+38
-14
@@ -43,35 +43,40 @@
|
||||
}
|
||||
|
||||
|
||||
/* 2. The panel */
|
||||
/* 2. The panel: about 560 pp tall: Steam's bar popup contents stop at 600 (max-height, overflow hidden) */
|
||||
.lgs-tune-panel {
|
||||
box-sizing: border-box;
|
||||
width: 300px;
|
||||
max-width: 100%;
|
||||
padding: 14px 12px 18px;
|
||||
padding: 10px 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* the theme's 64 pp slider capsule, at 40 in this compact column (knob and fill follow it) */
|
||||
--lgs-slider: 40px;
|
||||
}
|
||||
.lgs-tune-title {
|
||||
font: var(--lgs-type-title3);
|
||||
font: var(--lgs-type-headline);
|
||||
color: var(--lgs-text-1);
|
||||
padding: 4px 12px 8px;
|
||||
padding: 0 12px 4px;
|
||||
}
|
||||
.lgs-tune-note {
|
||||
font: var(--lgs-type-footnote);
|
||||
color: var(--lgs-text-2);
|
||||
padding: 0 12px 8px;
|
||||
padding: 0 12px 6px;
|
||||
}
|
||||
/* a row's name and value on one line */
|
||||
.lgs-tune-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
font: var(--lgs-type-body);
|
||||
gap: 12px;
|
||||
font: var(--lgs-type-subhead);
|
||||
color: var(--lgs-text-1);
|
||||
padding: 6px 12px 4px;
|
||||
white-space: nowrap;
|
||||
padding: 2px 12px 0;
|
||||
}
|
||||
.lgs-tune-label > span {
|
||||
font: var(--lgs-type-subhead);
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--lgs-text-2);
|
||||
}
|
||||
|
||||
@@ -79,7 +84,7 @@
|
||||
.lgs-tune-swatches {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px 14px;
|
||||
padding: 6px 12px 8px;
|
||||
}
|
||||
.lgs-tune-swatch {
|
||||
position: relative;
|
||||
@@ -129,17 +134,36 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 2.2 Sliders (Steam's SliderField): the panel's own padding, no separators */
|
||||
/* 2.2 Sliders: Steam's own, bare (no label of theirs), fitted like the Control Center's (31-cc.css) */
|
||||
.lgs-tune-row {
|
||||
padding: 0 4px;
|
||||
padding: 0 0 4px;
|
||||
}
|
||||
.lgs-tune-row %{*GamepadDialogContent>Field} {
|
||||
padding: 0 4px !important;
|
||||
margin: 0 !important;
|
||||
min-height: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.lgs-tune-row %{*GamepadDialogContent>Field}::after {
|
||||
display: none;
|
||||
}
|
||||
.lgs-tune-row %{*GamepadDialogContent>FieldLeftColumn},
|
||||
.lgs-tune-row %{*GamepadDialogContent>FieldLeftColumn} > %{*GamepadDialogContent>FieldChildrenWithIcon} {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
.lgs-tune-row %{*SliderControlAndNotches>SliderControlPanelGroup} {
|
||||
padding-block: 0;
|
||||
}
|
||||
.lgs-tune-row.is-disabled {
|
||||
opacity: .5;
|
||||
opacity: .45;
|
||||
}
|
||||
|
||||
/* 2.3 Reset */
|
||||
.lgs-tune-reset {
|
||||
margin: 12px 12px 0;
|
||||
min-height: 56px;
|
||||
margin: 6px 8px 0;
|
||||
min-height: 48px;
|
||||
border-radius: var(--lgs-r-capsule);
|
||||
}
|
||||
@@ -559,6 +559,15 @@
|
||||
linear-gradient(180deg, rgb(255 255 255 / .14), rgb(255 255 255 / .03)), rgb(0 0 0 / .66));
|
||||
}
|
||||
|
||||
/* leaving Home for a route under the same top-level route (Home -> Library): the route exit's fade that
|
||||
Steam gives every other route change (23-transitions.css: 150 ms linear). The shell stamps the leaving
|
||||
view (data-lgs-leaving, rt 20-shell.js) and the reporter lets Home's plates go on the same clock. */
|
||||
.lgs-home[data-lgs-leaving] {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity var(--lgs-d-page-out, 150ms) linear;
|
||||
}
|
||||
|
||||
/* mosaic bands (HA §10.2): invisible boxes the reporter reads (data-lgs-mosaic) */
|
||||
.lgs-home-band {
|
||||
position: absolute;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"The ornament is a liquid plate (shell-ornament, the T2 node #Footer > .lgs-orn-plate, also the data-lgs-nopop box); on windowless routes Back and the search field are liquid plates (shell-back, shell-search). These are DOM attributes from device/rt/20-shell.js, not rules.",
|
||||
"frame.menu: liquid (PLAN §1.6), and its cover is the tab bar's two capsules (the T2 nodes .lgs-tabcap), not the menu's box (which also holds the gap and the band under the bar). Without the runtime the cover falls back to the box. The cover's radius is the element's own border-radius (no r field), which 20-shell.css §4 sets from --lgs-c1a-tab-r: collapsed a capsule (half the column), expanded a 30 px rounded rectangle (a capsule radius on the expanded panel drew an oval blob); the fallback box gets the same radius, so it is never a square slab (REQ P7->C1a, review R2 M2).",
|
||||
"The tab bar opens and closes by animating its width (20-shell.css §4, on snappy out and fade back): cover.morph has the reporter send the end shape with the motion's start time and spring, and glassd morphs the two capsules' glass along with the page (no glass trailing the labels).",
|
||||
"Not laser-only (00-base.json's laserOnly is overridden): SteamVR shows the frame menu in gamepad mode too (laser mouse off, and beside SteamVR's own pages), and its panel reads only-visible-with-laser false whatever Steam asks (P6 RP-7). Keyed on the gamepad flag, the menu's native glass went off whenever the laser did and its CSS glass showed (2026-10-09).",
|
||||
"The tab bar's +25 mm is P7's tabBarDepth rule (theme/sg/00-base.json) and the always-visible flag is theme/popups/20-shell.json (tabBarAlways): never a crop."
|
||||
],
|
||||
"flag": "wp.c1a",
|
||||
@@ -15,6 +16,7 @@
|
||||
"surfaces": {
|
||||
"frame.menu": {
|
||||
"material": "liquid",
|
||||
"laserOnly": false,
|
||||
"cover": {
|
||||
"sel": "%{DashboardMenu}%{Variant_FrameMenu} > .lgs-tabcap, %{DashboardMenu}%{Variant_FrameMenu}:not(:has(> .lgs-tabcap))",
|
||||
"all": true,
|
||||
|
||||
Reference in new issue
Block a user