From a11cef49bad187e1e036a228985eed6997fe4f23 Mon Sep 17 00:00:00 2001 From: DeeJanuz <45082401+DeeJanuz@users.noreply.github.com> Date: Sat, 3 Oct 2026 09:11:15 -0600 Subject: [PATCH] Gaze: ft-eyes and ft-gaze below SteamVR's priority ft-eyes and ft-gaze run in the dev container through distrobox, so they live in podman's libpod scope: frametop-gaze.service's limits never reach them, and they ran at nice 0 next to vrcompositor and vrserver, also at nice 0. - ft-eyes sets itself to nice 10 and SCHED_BATCH at start, before its threads. Batch turns off wakeup preemption, so a frame ft-eyes wakes up for can wait out a running compositor's turn; a few ms late costs the gaze little. - ft-gaze sets nice 5 before its threads start, but stays SCHED_OTHER: each sample goes on to the pointer, and batch would add the same wait to every one of them. - Both only ever lower their priority (a higher nice already set wins), and a failure is logged and ignored. Checked in the dev container: nice 0 -> 10, policy 0 -> 3 (SCHED_BATCH) without any capability. Co-Authored-By: Claude Opus 5.5 --- gaze/README.md | 2 +- gaze/ft-gaze.cpp | 10 ++++++++++ gaze/tracker/ft-eyes | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/gaze/README.md b/gaze/README.md index 3346530..2f47612 100644 --- a/gaze/README.md +++ b/gaze/README.md @@ -63,7 +63,7 @@ Ground rules, for anyone changing it: - **Clean room.** Nothing of Valve's goes in: we don't decompile, disassemble, or patch the `eyetracking` binary or its network weights, and we don't copy their code or weights. Its public output (eye-server.mmap, read-only) is fair game as a baseline and as labels, and so are published papers and openly licensed pupil detectors (check each one's license: PuRe, PuReST, ElSe, and ExCuSe are non-commercial only). - **Root only reads.** ft-eyegrab never writes to, stops, or signals the `eyetracking` process, vrserver, or vrcompositor, never opens `/dev/adsp`, `/dev/cdsp`, or `/dev/spidev0.1`, and never writes to `/dev/shm/eye-server.mmap` (it also carries calibration clicks into SteamVR's tracker), `/opt`, or `/persist`. - **Eye images are biometric data.** Recordings live outside the repo, in `~/.local/share/frametop/eyes/captures` (0700), and `.gitignore` catches stray frame dumps. They go nowhere but the machine that runs your offline jobs. -- **Mind the headset's budget.** Finding a pupil takes about 0.4 ms a frame while ft-eyes follows it, and 1.4-2.1 ms when it searches the whole frame. ft-eyes keeps OpenCV and numpy to one thread: their pools of one per core spun idle workers at about a quarter of a core, for frames this small. Replays, scoring, and training go to a PC. +- **Mind the headset's budget.** Finding a pupil takes about 0.4 ms a frame while ft-eyes follows it, and 1.4-2.1 ms when it searches the whole frame. ft-eyes keeps OpenCV and numpy to one thread: their pools of one per core spun idle workers at about a quarter of a core, for frames this small. It also runs at nice 10 with SCHED_BATCH, and ft-gaze at nice 5 (not batch, since each sample goes on to the pointer): both run in the dev container's podman scope, out of reach of the gaze service's unit, on the cores vrcompositor and vrserver use at nice 0. Replays, scoring, and training go to a PC. ## Headset fit diff --git a/gaze/ft-gaze.cpp b/gaze/ft-gaze.cpp index 2d4a107..f11dd4d 100644 --- a/gaze/ft-gaze.cpp +++ b/gaze/ft-gaze.cpp @@ -58,6 +58,7 @@ #include #include +#include #include #include #include @@ -72,6 +73,7 @@ #include #include +#include #include #include #include @@ -430,6 +432,14 @@ int main(int argc, char **argv) { if (std::strcmp(argv[i], "-v") == 0) verbose = true; if (std::strcmp(argv[i], "--watch-stdin") == 0) watchStdin = true; } + // Nice 5, before any thread starts (they inherit it): we run in the dev container's podman + // scope, beside vrcompositor and vrserver at nice 0, and the gaze service's unit doesn't + // reach us. Not SCHED_BATCH, as ft-eyes is: that would let each wakeup wait out another + // task's turn, and each sample goes on to the pointer. + errno = 0; + const int nice0 = getpriority(PRIO_PROCESS, 0); + if (errno == 0 && nice0 < 5 && setpriority(PRIO_PROCESS, 0, 5) != 0) + std::fprintf(stderr, "ft-gaze: nice: %s\n", std::strerror(errno)); // --watch-stdin: quit when stdin closes. The probe runs us through distrobox, which // passes neither its signals nor a closed stdout on to us, but does pass stdin's end. std::atomic stdinClosed{false}; diff --git a/gaze/tracker/ft-eyes b/gaze/tracker/ft-eyes index 66bbff7..f716f77 100755 --- a/gaze/tracker/ft-eyes +++ b/gaze/tracker/ft-eyes @@ -377,7 +377,24 @@ def serve(sock, tracker): pass +def below_steamvr(): + """Nice 10 and SCHED_BATCH, for this thread and those it starts. ft-eyes runs in the dev + container's podman scope, where the gaze service's unit doesn't reach it, so it ran at + nice 0 on the cores vrcompositor and vrserver use. Batch also lets a waking ft-eyes wait + for the running task's turn instead of taking the core: a frame a few ms late costs the + gaze little, a late compositor frame costs a dropped frame in the headset.""" + try: + os.setpriority(os.PRIO_PROCESS, 0, max(os.getpriority(os.PRIO_PROCESS, 0), 10)) + except OSError as e: + print(f"ft-eyes: nice: {e}", file=sys.stderr, flush=True) + try: + os.sched_setscheduler(0, os.SCHED_BATCH, os.sched_param(0)) + except (OSError, AttributeError) as e: + print(f"ft-eyes: SCHED_BATCH: {e}", file=sys.stderr, flush=True) + + def main(): + below_steamvr() verbose = "-v" in sys.argv if "--watch-stdin" in sys.argv: # Run by ft-gazed through distrobox, which doesn't pass a stop on: quit when our