- The runtime files move to /run/user/UID/frametop-hands/: the desktop session deletes /run/user/UID/frametop at every start. - The cutout copy shader runs at highp: mediump (16-bit on Adreno) stepped 1.7 texels across a 3440-pixel screen. - One hand no longer pinches both sides after its left/right call flips mid-pinch, and --pinch-palm-down (0.6) holds back pinches with the palm facing down (typing on a lap keyboard). - ft-camd judges a colour frame fresh by its luma rows only, and logs per-buffer changes at stale colour frames with FT_CAMD_DEBUG=1. - hands/run.sh caps skips the setcap when ft-camd already has them. - The replay tool dumps poses (--poses), and its pinch events carry the hand id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 KiB
Hands in Frametop: migration plan
Hand tracking from the headset's own cameras has been built as a separate project, frame-hands (~/Desktop/Projects/frame-hands on the Frame, a local git repo with no remote). The plan is to make it a native Frametop component, like gaze/ and power/, instead of a separate module. The work happens on branch hands-migration (worktree frametop-hands/ in the PC workspace) and is merged into experimental after it's been tested in the headset.
Builds from this worktree must sync to their own folder on the Frame, never ~/dev/frametop: run every script with FRAME_REPO=/home/steamos/dev/frametop-hands.
Status (2026-09-30)
Steps 1-5 are done:
- frame-hands' pending work was committed there (6c63c9e).
- Its filtered history was merged under
hands/(1a76d15), then laid out (trackd/totrack/). - The renames, the Frametop paths, and ft-camd's file capabilities are done. So are
hands/Makefile,build.sh,run.sh, the two units, the README, the settings, the installer step, and ft-screens on the shared header. - Built in the dev container on the Frame, and on the 7i.
- Checked without the headset:
ft-handreplayagainst frame-hands'fh-replay, both x86 with--cost, on the whole dim recording and the first 60 s of the bright one: identical summaries and byte-identical depth dumps. The Makefile's own ncnn build is included in that.ft-ringplayintoft-handson the 7i tracked, pinched, and wrote/run/user/UID/frametop-hands/{hands,gestures}.- On the Frame, ft-hands in the container finds the calibration through
/run/host/persist, and ft-camd without its capabilities refuses with a clear message.
Step 6 has started (2026-09-30 10:30):
hands/run.sh installis done, and both services run from this worktree.- The files moved to
/run/user/UID/frametop-hands/, because/run/user/UID/frametopis the desktop session's own runtime folder, deleted at every desktop start. - Until the desktop restarts from a build with this branch's ft-screens, the link
/run/user/UID/frame-hands -> frametop-handsfeeds the running one. It's tmpfs, so it's gone at reboot.
Found in the headset:
- The side cameras were swapped (
HANDS_SWAP_SIDES=1). - The cutout copy shader lost resolution at
mediump(nowhighp). - Colour capture isn't reliable (see the README).
- Two pinch fixes: one hand no longer pinches both sides, and the palm-down limit stops typing pinches.
What frame-hands is today
| Part | What it is | Size |
|---|---|---|
camd/ |
fh-camd, the camera broker (C). It borrows XRService's camera DMA-BUFs read-only with pidfd_getfd, times them with the v4l2_dqbuf tracepoint, and publishes the four IR cameras (and optionally the two colour cameras) to a shared-memory ring. It starts as root and drops to the user after setup. Adapted in part from FrameEyeCameraFeed (MIT, licence file kept). fh-camprobe is its discovery and recording probe. |
camd 1.1k lines, tp 0.4k, xrcams 0.8k, camprobe 1.1k |
trackd/ |
fh-tracker (C++): the tracker, the models on ncnn, the calibration (jsoncpp), the pinch detector, the publisher, and the recorder. Also fh-replay (offline replay and scoring), fh-ringplay (plays a recording into a ring), and nettest. |
2.9k lines |
include/ |
The hands file (fh_hands.h, read by ft-screens) and the gestures file (fh_gestures.h, pinches). |
|
models/ncnn/ |
MediaPipe's palm detector and hand landmark model, from the OpenCV Zoo ONNX ports (Apache-2.0), converted to ncnn in float and int8. | 5.9 MB |
tools/ |
Python analysis: side-camera check, colour calibration check, frame viewer, gesture watcher, depth report, model comparison, int8 calibration, model conversion. | ~1.1k lines |
tracker/ |
The Python prototype of the tracker. Some tools import its calib.py and models.py. |
1.3k lines |
probes/, notes/, re/, shim/ |
One-off experiments, reverse-engineering notes on SteamVR's passthrough internals, a disassembly (not in git), and a header from an abandoned XRService shim approach. | |
vendor/, captures/ |
ncnn and FrameEyeCameraFeed clones, and recordings of the user's hands and room (tens of GB). Neither is in git. |
Today it runs by hand: sudo camd/fh-camd, then trackd/fh-tracker. There are no units and no installer. Files: /run/frame-hands/ir-ring (the ring, in a root-owned folder), and $XDG_RUNTIME_DIR/frame-hands/hands and gestures.
Frametop already has the consumer side on experimental: screens/handcut.{h,cpp} cuts the hands out of the screens, with its own copy of the hands file layout, and screens/handtest.cpp tries it on a test panel.
Where it goes
A top-level hands/ folder, laid out like gaze/:
hands/
README.md # from trackd/README.md and camd/README.md
build.sh # ft-camd, ft-hands; --tools also builds the replay tools
run.sh # install|uninstall|start|stop|restart|status|log
frametop-camd.service # user units (templates, @REPO@)
frametop-hands.service
include/ # fhring.h, fh_hands.h, fh_gestures.h: shared with screens/ and pointer/
camd/ # ft-camd: camd.c tp.c xrcams.c, LICENSE.FrameEyeCameraFeed
track/ # ft-hands: tracker, nets, calib, pinch, publish, record; replay.cpp
# (ft-handreplay) and ringplay.cpp (ft-ringplay) for recordings
models/ # the ncnn models, with NOTICE (Apache-2.0, MediaPipe / OpenCV Zoo)
tools/ # the Python checks, watch_gestures, depth_report, calib.py, ring.py
Left behind in frame-hands, which stays as the lab: the recordings, the Python prototype (the tools that need calib.py or models.py get a trimmed copy in hands/tools/), probes/, notes/, re/, shim/, camprobe, and vendor/. The reverse-engineering notes don't belong in a public repo, and recordings are images of the user's hands and room, so they never go into git.
Names
Programs within 15 characters, ft- prefix; files under frametop:
| Now | In Frametop |
|---|---|
fh-camd |
ft-camd |
fh-tracker |
ft-hands |
fh-replay, fh-ringplay |
ft-handreplay, ft-ringplay |
/run/frame-hands/ir-ring |
/run/user/UID/frametop-hands/cam-ring |
$XDG_RUNTIME_DIR/frame-hands/hands, gestures |
/run/user/UID/frametop-hands/hands, gestures |
The source keeps its fh_ identifiers and header names (fh_hands.h, fh_gestures.h, fhring.h), and the file formats keep their magic strings, so recordings and tools from frame-hands keep working. Programs, units and runtime paths change.
Build
hands/build.shbuilds in the dev container throughscripts/frame.sh --build, intohands/build/, like the other components.FRAME_BUILDER=pccan take the ncnn build.- ncnn: fetched at a pinned tag (20260526, as now) into
hands/build/ncnnand built once, the wayscreens/build.shfetches the OpenVR header, with frame-hands' options so results match.NCNN=points the build at an existing install instead. Every net runs single-threaded (num_threads = 1), with the tracker spreading nets over its own pinned threads, so OpenMP could go later. - ft-hands runs in the dev container like ft-pointer and ft-powerd (
distrobox enter dev --, afterscripts/container-up.sh). Today's fh-tracker runs on the host and works only because the host happens to have the samelibjsoncpp.so.25and libgomp as the container. Inside the container the calibration is at/run/host/persist, and calib.cpp (andtools/calib.py) fall back to it when/persistisn't there. - ft-camd has to run on the host (below), so it's linked statically (only libc and libm;
glibc-staticgoes intosetup/dev-container.sh). The host has an older glibc than the container.
Running it
ft-camd needs privileges, only while it sets up: pidfd_getfd on XRService (the Frame has ptrace_scope=1), system-wide tracepoints (perf_event_paranoid=2), and the tracepoint files, which are root-only (/sys/kernel/tracing/events/v4l2/v4l2_dqbuf/{id,format} are mode 0440). A rootless container's root can't do any of that, so it runs on the host. Two ways:
- A. File capabilities (chosen, 2026-09-30). The installer runs
sudo setcap cap_sys_ptrace,cap_perfmon,cap_dac_read_search+ep hands/build/ft-camdonce. ft-camd then runs as the user, in a user unitPartOf=steamvr.service, so it starts and stops with SteamVR, and its ring lives in the user's runtime folder. It drops all capabilities after setup, as it drops root today. Nothing ever runs as root. Writing the file clears its capabilities, so a rebuilt ft-camd needs the setcap again. It changes rarely./homeon the Frame is ext4 withoutnosuid, so file capabilities work there. - B. Root system service, like the Bluetooth fixes: a root-owned copy in
/var/lib/frametop/, a unit in/etc/systemd/system/. It would have to watch for XRService itself, because a system unit can't follow the user'ssteamvr.service.
Either way the password is needed once at install, through the same sudo -S path the Bluetooth fixes use, and only after asking.
ft-hands is a user unit, frametop-hands.service: after frametop-camd.service, PartOf=steamvr.service, nice 5, model threads on CPUs 5-7 (measured best on 2026-09-29).
Settings in ~/.config/frametop.conf: HANDS_SWAP_SIDES=1 and HANDS_CPUS=5,6,7, read by ft-hands. It's on while its services are installed (hands/run.sh install, uninstall), so there's no HANDS switch. There's no setting for colour yet. Later, a switch in Frametop Display Settings.
Installer: an optional last step in install.sh, off by default, which asks first because it needs sudo.
Interfaces
screens/handcut.cppincludeshands/include/ft_hands.hinstead of its own copy of the layout, and reads the new path. ft-screens and ft-hands change together on this branch.- Pinches go to the pointer helper. It maps the gestures file and checks the begin and end counters each tick. A begin is a press, an end the release, and the pinch point's movement a drag. In gaze mode, the press lands where you look. The counters mean a quick tap between two ticks isn't missed. The tracker knows nothing about the pointer.
Open items that aren't part of the move
These block shipping hands to other people, not the migration:
- The side-camera swap. After some XRService restarts, fh-camd publishes the two side cameras under each other's names. Today it's caught by hand (
tools/check_sides.py --ring, then--swap-sides). It needs fixing at the source (tell the buffers apart by thedqbuftracepoint's device, the way the colour pair is split), or at least an automatic check at start-up. - The colour cameras' calibration mapping (
tools/check_color.pyon a recording with texture). - Depth when one camera loses the hand. From the 2026-09-30 replay measurements: drifting 10% per update toward the one-camera guess (
kMonoDepthGain) makes the depth worse than keeping the last distance. Try 0.02.
Public repo
Frametop is public. Not pushed to GitHub until the user says it's ready (user decision, 2026-09-30). When it is, it publishes:
- The camera borrowing (
pidfd_getfdon XRService's buffers) and the tracepoint timing. FrameEyeCameraFeed already does the same publicly. Its MIT licence and credit stay with the code. - The models, under Apache-2.0, with a NOTICE.
It doesn't publish the reverse-engineering notes, the probes, or any recording. They stay in frame-hands.
History
frame-hands' work was committed there first (6c63c9e, its 4th commit). Its history was then filtered to drop what stays behind (notes/, probes/, shim/, camd/camprobe.c, the camprobe tools, the Python prototype except calib.py and models.py, and .frame-job) from every commit. It was merged into this branch under hands/ (a subtree merge), so blame still leads to where each line came from. The renames come after, as their own commits.
Steps
- In frame-hands: commit the pending work, as its last state before the move (needs the user's OK).
- On this branch: import it under
hands/, then rename the programs and paths. The behaviour stays identical. hands/build.sh,run.sh, the two units, the README, the settings, and the installer step.- ft-screens' hand cutouts on the shared header and the new path.
- Check without the headset.
ft-handreplayon the 2026-09-29 recordings with--costis repeatable, so its summary must matchfh-replay's exactly. Andft-ringplayinto ft-hands must publish the same hands as into fh-tracker. - In the headset, with the user and after asking: stop fh-camd and fh-tracker, install the services from
~/dev/frametop-hands, and restart the desktop from this branch so ft-screens reads the new path. - Pinch into the pointer helper (it can also follow the merge). The gaze work is on the Frame's
~/frametopmain: on 2026-09-30 that branch had 4 commitsexperimentaldoesn't have, plus uncommitted work in the pointer helper's gaze mode. Build this step on wherever that work lands, not on this branch's older copy. - Merge into
experimental. It's checked out in a worktree on the Frame (~/frametop/.worktrees/experimental, where the live desktop runs), so the merge happens there, orexperimentalis switched away first.