Commit Graph
28 Commits
Author SHA1 Message Date
DeeJanuzandClaude Opus 5.5 b99fb97f32 Turn the displays off while the headset isn't used, and keep it awake on a charger
A display mount that covers the proximity sensor makes the headset seem
worn, so SteamVR never turned its displays off and they stayed on all
night. The new power service, ft-powerd (frametop-power.service), goes by
use instead: after DISPLAY_OFF_MIN minutes in which the headset and
controllers didn't move and no input device was used, it turns the
backlight off, and the next movement or input turns it back on.

The new Power tab in Frametop Display Settings sets that time and has a
Stay awake while plugged in switch. The switch sets Steam's own "When
Plugged In and Idle -> Sleep after" to Never through Steam's UI, so the
Frame stays reachable remotely while the power button still works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 09:34:34 -06:00
DeeJanuzandClaude Opus 5.5 f01c84f9a3 Use the other eye when the tracker loses one, and add a headset fit check
SteamVR's combined gaze keeps going on one eye, but it holds the lost
eye's yaw, so the gaze moves half as far sideways as the eyes do.
ft-gaze now reads each eye's tracking uncertainty and raw measurement
from eye-server.mmap. When the tracker loses an eye, ft-gazed takes the
gaze from the other one, plus the offset that eye usually shows
against both, learned while both are seen. On a recording, one eye
alone came out a median 0.8 degrees from both eyes' gaze.

Glances down at the keyboard, past every screen, aren't sent. The
pointer stays put, and eyes lost there don't count as lost.

The gaze probe gets a Headset fit mode. It shows per-eye tracking,
openness and confidence, maps where each eye gets lost, gives hints,
and has a guided check. The settings app opens it from the Gaze page
and shows how often each eye is lost. The probe can also test each eye
alone, and its side panel now collapses to a title bar so the dot
isn't hidden behind it.

Snapping to UI elements is deferred; the mouse drag is the correction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:07:41 -06:00
DeeJanuzandClaude Opus 5.5 e60cb2f28a Add a report script for keys that stick or leak
scripts/keys-report.py records what happens to the modifiers, Tab, and
Esc while someone reproduces a key problem: each press, release, and
autorepeat as the relay reads it from a physical keyboard, and as it
comes out of the relay's virtual keyboard to gamescope and SteamVR. It
adds the relay's view of every device (role, grabbed), which programs
have each input node open, and the relay, pointer helper, and desktop
logs for the same time. Other keys show only as "other key", so nothing
typed ends up in the report, and Bluetooth addresses are masked.

For #2: Shift+Tab in the Frametop desktop opened the SteamVR dashboard
and then stayed held, which doesn't happen here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:35:16 -06:00
DeeJanuzandClaude Opus 5.5 18aa0fec3a Put clicks where the cursor is on scaled desktop screens
With a screen's scale set to anything but 100%, clicks landed away from
the cursor, further off the further from the top left (#3). ft-screens
hands KWin panel positions in buffer pixels, and KWin's nested Wayland
backend (6.2.5, WaylandInputDevice) adds surface coordinates to its
output's logical position without dividing by the output's scale. At
125% a click at the middle of a 3440x1440 screen, (1720, 720), reached
KWin as logical (1720, 720), pixel (2150, 900).

ft-screens now keeps a scale per screen and divides pointer positions by
it. ft-layout sends each screen's scale, as KWin reports it after
applying, with a new "scale N s" command, whenever it applies scales:
at desktop start and from Frametop Display Settings. Screens default to
1, so an ft-layout that never sends it keeps the old behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:33:03 -06:00
DeeJanuzandClaude Opus 5.5 0857a54fab Don't let resting controllers take the laser from the mouse
A controller released the 3D mouse's pointer on a single pose sample
faster than 0.35 m/s or 2 rad/s, once the mouse had been still for
500 ms. The helper polls about every 8 ms, so one noisy sample was
enough: a knock on the desk, or a tracking jump when the headset's
cameras pick a resting controller up again (#1).

A controller now has to stay over the limit for 100 ms in a row, and
only samples with a normal tracking result (Running_OK) count. The new
POINTER_CONTROLLER_PICKUP setting (1 by default, 0.5 to 5) scales both
limits; it's a slider on the Pointer page of Frametop Input Settings
and applies live. A controller picked up for real still gets the laser
back through SteamVR's hand role, which follows its touch sensors. The
release log now records the speed and spin that triggered it, to tune
the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:31:22 -06:00
DeeJanuzandClaude Opus 5.5 14879023f3 Let the 3D mouse click all of SteamVR's Settings page
Most of SteamVR's Settings page took no clicks from the 3D mouse: they
went through to a desktop screen behind it, and the page covered the dot.
Steam's pages (Library and the rest) are drawn in
valve.steam.gamepadui.main, which ComputeOverlayIntersection hits exactly.
For SteamVR Settings that overlay is hidden and the page is drawn by the
dashboard's scene-graph panel, whose shape OpenVR doesn't give out. The
helper guessed a plane and a 0.5 m circle from the panel's transform, but
its origin is at the page's left edge and the page's surface is nearer
than that plane, so the laser, starting a few cm in front of the guess,
started behind the page.

On that page only (dashboard open, the main overlay hidden, the line of
sight crossing the page's measured area), the laser now starts 25 cm from
the eye so SteamVR's own hit test finds the page. The dot is drawn 0.6 m
out, in front of the page, and the laser-catching dot sits 8 m out,
invisible, with SteamVR's hit dot hidden on it. The beam and SteamVR's hit
dot show there, like a controller's; everywhere else nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 17:17:15 -06:00
DeeJanuzandClaude Opus 5.5 ed3c360f0d Add a Global input switch to the Controllers page
SteamVR's "Enable global input from overlays (Experimental)" could only
be turned on from the settings app: its notice, with the button, hid once
the setting was on. It's now a switch, and the notice shows only when
buttons are mapped and the setting is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:59:56 -06:00
DeeJanuzandClaude Opus 5.5 0d50efec45 Map the Frame controllers' buttons; make gaze clicks correctable
The Frame controllers aren't input devices on the host, so the pointer
helper reads them with SteamVR input (vrbuttons.h, actions/), one action
set per button, and sends presses to the relay, which does the mapped
action like for a mouse button. Only mapped buttons are taken, at an
overlay-global priority (SteamVR's experimental "Enable global input from
overlays"), and only outside games unless In games is on. Frametop Input
Settings gets a Controllers page to map them.

Gaze mode: a left press while the gaze has the pointer isn't sent at
once. The pointer stops, you drag it onto what you meant with the button
held, and the release clicks there; a press held still for
POINTER_GAZE_HOLD (0.5 s) becomes a real press, for drags. The dot shows
only while the mouse moves it (POINTER_GAZE_SHOW), while a press is held,
and as a pulse per click. Outside games the pointer stays on while gaze
mode is on. ft-gazed takes a third of each lesson's offset instead of all
of it: in the first live test one 6 degree lesson moved everything and put
the next target 7 degrees off. Frametop Input Settings gets a Gaze page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 16:26:24 -06:00
DeeJanuzandClaude Opus 5.5 10f21273a6 Add gaze mode to the pointer: it goes where you look
MAGIC pointing (Zhai et al. 1999), off by default: with POINTER_GAZE=1,
"gaze on", or a button mapped to the new gaze_toggle action, the cursor ray
is the corrected gaze from ft-gazed while the gaze has the pointer. Moving
the mouse takes the pointer from where the gaze left it; looking more than
POINTER_GAZE_RETAKE (5 deg) away with the mouse still gives it back. The
pointer is aimed at the gaze, never steered toward it, and only fresh gaze
moves it, so a stopped service or a blink leaves it where it is. A mouse
nudge of up to POINTER_GAZE_NUDGE_MAX (8 deg) before a click is sent to
ft-gazed as a lesson in the eye tracker's error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:51:51 -06:00
DeeJanuzandClaude Opus 5.5 e84ac22313 Arrange the desktop's outputs the way the screens are around you
KWin's output positions now follow where the Frametop screens are in the room
instead of their numbers: a screen you see to the left of another is to its
left in Plasma, so the pointer and dragged windows cross straight to it.
Screens one above the other stack, and wrist-pinned screens go last.
ft-layout scale works this out from ft-screens' head pose and screen poses,
and runs after arranging, capturing, or pinning; ft-screens runs it half a
second after a screen is let go. With no head pose (headset off) KWin's
order is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 22:51:07 -06:00
DeeJanuzandClaude Opus 5.5 c43418085d Add a potential hazards doc for the input changes
It lists what can go wrong with the relay taking the volume keys (keymaps
left remapped after a crash, the headset's click button on the same
device, wpctl stepping the default output), with key releases (a key left
held in the desktop when its release never arrives), and with keyboards
grabbed for typing (hotkey tools lose them; SHARE_KEYS is off by default).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:52:21 -06:00
DeeJanuzandClaude Opus 5.5 cfe5990b12 Mark head follow as experimental
It works but is only lightly tested, and what's left is mostly tuning its
settings. Frametop Input Settings now says so under the Head follow switch,
and the README, design notes, and button action name say it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:52:21 -06:00
DeeJanuzandClaude Opus 5.5 9addd8c669 Move the head-follow cursor only after the head passes the leash
Easing the reference toward the head all the time moved the cursor on
every small head movement, so a cursor parked in a corner of the view
drifted. Now nothing moves while the head stays within the leash. Once
the head has been past it for POINTER_LEASH_DELAY (0.2 s, so a glance
out and back doesn't count), the reference eases all the way to where
you face (POINTER_LEASH_RETURN) and the cursor lands back in its place
in the view, then the leash waits again. Leaning inside the leash no
longer moves the cursor either. Leash 0 stays head-locked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:52:21 -06:00
DeeJanuzandClaude Opus 5.5 9f4b61729d Settle head follow back on where you look; let the mouse reach 70 degrees
The leash only moved its reference when the head reached the leash's end,
so after turning back it sat up to the leash off, and recentring meant
overshooting with the head. The reference now eases toward the head's
facing (POINTER_LEASH_RETURN, 0.2 s) and never lags more than the leash,
so the cursor settles back to its place in the view once the head stops.
The mouse can now move the cursor up to POINTER_FOLLOW_REACH (70 degrees)
from the middle of the view, up from a fixed 40. Both are sliders on the
Pointer page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:52:21 -06:00
DeeJanuzandClaude Opus 5.5 137705cbfd Add head follow: the pointer comes along when you turn your head
Off by default. With POINTER_FOLLOW=1 (a switch on the Pointer page of
Frametop Input Settings, or a mouse button mapped to Head follow on/off),
the cursor rides on a reference direction leashed POINTER_LEASH_DEG (10)
from where you face. Within the leash it stays put in the room; past it,
it turns with your head and keeps its offset. A leash of 0 locks it to
your view. Head roll is ignored, the cursor stays within 40 degrees of
the reference, and it holds still in the room while the left button is
down so the head can't nudge a click or a drag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:52:21 -06:00
DeeJanuzandClaude Opus 5.5 8f051db083 Send typing to the panel clicked last
An ungrabbed keyboard reached both sides at once: gamescope, which in VR
reads every input device itself (the SteamOS build's InputStealer), typed
into its focused app, and ft-screens typed into the desktop. Space in the
desktop paused Spotify on the dashboard, including every space frame-voice
dictated. And while the SteamVR dashboard was open, the desktop got no
keys at all.

Typing now follows the last click. ft-screens sees clicks on its own
screens; the pointer helper reports the panel under the dot on each mouse
press, so a click on any other panel sends typing to Steam. While typing
goes to the desktop and the screens are showing, ft-screens tells the
relay every second, and the relay grabs pass-through keyboards. A grab
waits until no key is down, and the relay lets go if ft-screens stops
reporting. Controller clicks on other panels aren't visible to overlay
apps, so they don't move typing (noted in the README).

A program that reads every keyboard for a hotkey loses a grabbed one.
Repeating the keys on another input device doesn't work, since gamescope
reads that too, so with SHARE_KEYS=1 the relay sends them to
@frametop_keys as datagrams with the device name. It's off by default:
any local process that binds that name first would get every key typed
into the desktop.

The docs now say gamescope reads keyboards and the headset's buttons
itself; they said SteamVR passed keys on to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:52:21 -06:00
DeeJanuzandClaude Opus 5.5 705b434cf3 Take over the volume keys so they can't crash gamescope
gamescope sends volume up and down to Steam by moving keyboard focus to
Steam for the key and back. When nothing had focus, it moves focus back to
null and wlroots aborts, which ends the whole VR session. One press of the
headset's volume button did that while working in the Frametop desktop.

The input relay now handles volume keys from every device that has them,
the headset's buttons included, and steps the default output with wpctl
(5%, repeating while held). SteamVR, which passes keys on to gamescope,
never sees a volume key: devices with a keymap (gpio-keys, USB and
Bluetooth keyboards) get only their volume entries remapped to unused
codes, so the headset's click button keeps working, and pmic_resin, which
has only volume down, is grabbed. The keymaps go back when the relay
stops, and --no-grab leaves the volume keys alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 09:10:28 -06:00
DeeJanuzandClaude Opus 5.5 de2823c360 Turn off the Meta tap dashboard shortcut by default
A Meta tap on a pass-through keyboard toggled the SteamVR dashboard, which
got in the way of using Meta on its own. It's now off unless
META_DASHBOARD=1 is in ~/.config/frametop.conf. Meta as a modifier
(Meta+Shift+R, Meta+Shift+H) is unaffected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 08:12:14 -06:00
DeeJanuzandClaude Opus 5.5 f70073b140 Leave an unchanged SteamVR driver in place when reinstalling
Re-running the installer replaced the ft_pointer driver's files under a
running SteamVR. SteamVR then kept giving the virtual controller its hand
role but ignored its laser claim, so the mouse moved its dot without
SteamVR's laser until SteamVR restarted. The driver is now staged and only
replaced when it changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 20:07:35 -06:00
DeeJanuzandClaude Opus 5.5 a4499c1c16 Reveal a screen's controls when a laser lands on them
Hidden controls were taken out of SteamVR, so only ft-screens' own
proximity test could bring them back, and that test didn't match every
controller's laser. They now stay in place fully transparent while
hidden, and SteamVR's hover event on one reveals them, for any laser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:59:38 -06:00
DeeJanuzandClaude Opus 5.5 54b1e5cd4b Aim controller rays along the laser, not the controller
The Frame controllers' laser comes from their render model's tip, 40
degrees below the controller pose's forward axis. ft-screens tested rays
along the pose, so a controller's laser near a screen's controls didn't
reveal them, and dragging the resize tab or the roll knob with a
controller followed the wrong point. Rays now start from the tip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:42:30 -06:00
DeeJanuzandClaude Opus 5.5 bc74097331 Fix the installer; start the desktop without the Steam client's environment
install.sh: a comment with an apostrophe inside a single-quoted command
(from the distrobox pin) ended the quote and broke the script at step 1.

The VR launcher started the desktop with the Steam client's environment,
including LD_LIBRARY_PATH to Steam's runtime, whose libavcodec has no
H.264 decoder: VLC in the desktop couldn't play most videos. The session
script now drops the client's variables before starting anything, so apps
use the system's libraries and codecs.

The README and the installer now recommend setting Steam's Sleep after
(When Plugged In and Idle) to Never; Steam suspends the Frame after an
hour otherwise, even while charging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:16:35 -06:00
DeeJanuzandClaude Opus 5.5 b0036cdeae Rewrite the README and docs in plainer prose
The README drops the bold headlines on every item and reads as prose. The
reference is brought up to date and loses its bold labels. The design doc
is reorganized by topic instead of as a dated work log, keeping the
technical findings. The setup guide uses subheadings for troubleshooting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 19:06:36 -06:00
DeeJanuzandClaude Opus 5.5 4d845492e7 Let the headset sleep with the helper idle; keep the container out of our units
With the pointer released, the helper still listed overlays by running
vrcmd every second, a new SteamVR client each time, which kept SteamVR
from going to standby. The list now pauses while the pointer is off.

Starting the dev container from one of our services made that service own
it, so stopping the service stopped the container and the desktop's
compositor with it. scripts/container-up.sh starts the container in a
systemd scope of its own before every distrobox enter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:56:45 -06:00
DeeJanuzandClaude Opus 5.5 b9dc805e75 Hide the screens during VR games unless the dashboard is open
A new setting, During VR games: with Always, the screens hide while a VR
game runs and show while the SteamVR dashboard is open (the default), or
stay visible over the game. The hotkey still shows them; a game starting or
stopping resets it. Socket command: ingames hide|visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:06:42 -06:00
DeeJanuzandClaude Opus 5.5 6122313fae Record the screens-over-games test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 17:04:55 -06:00
DeeJanuzandClaude Opus 5.5 c18f6c732a Keep the controllers in VR games while the screens stay up
Visible screens kept SteamVR's laser mouse on, which takes the controllers
away from a VR game. Now, by default, that's off while a scene app runs:
the screens stay over the game and the 3D mouse or the dashboard works
them. Frametop Display Settings has the choice (always, except during VR
games, only with the dashboard open); the socket command is controllers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 16:54:31 -06:00
DeeJanuzandClaude Opus 5.5 d439bc3f25 Frametop: a multi-screen desktop and universal 3D mouse for the Steam Frame
Several KDE Plasma screens floating in SteamVR, each a real monitor of any
resolution and shape, shown by our own compositor (ft-screens), with a
layout, wrist pinning, and visibility modes; a Bluetooth mouse that drives
all of SteamVR as a room-anchored 3D pointer (input relay, ft-pointer
helper, ft_pointer SteamVR driver); two settings apps; and Bluetooth LE
fixes. Installs on the headset with ./install.sh.

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