// ft-hands: hands in 3D from ft-camd's ring, published for Frametop's ft-screens (the hand // cutouts), and pinches for the pointer. It started as a port of frame-hands' Python // prototype: the same scheduling, with the models on a few threads. // // ft-hands [--seconds N] [--threads N] [--int8] [--status S] [--models DIR] [--nice N] // [--no-publish] [--record DIR] [--swap-sides] ... (--help lists them all) // // Settings in ~/.config/frametop.conf (FT_ in the environment overrides them, and // options override both): HANDS_SWAP_SIDES (1: as --swap-sides), HANDS_CPUS (as --cpus). #include "io.h" #include "pinch.h" #include "record.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace { volatile std::sig_atomic_t g_stop = 0, g_record = 0; // which calibrated camera each capture pipe carries (XRService's fixed routing) const char *camera_for_pipe(int node) { char path[64], name[64] = ""; std::snprintf(path, sizeof path, "/sys/class/video4linux/video%d/name", node); std::ifstream f(path); f.getline(name, sizeof name); if (!std::strcmp(name, "msm_vfe3_video0")) return "slam_left"; if (!std::strcmp(name, "msm_vfe4_video0")) return "slam_right"; if (!std::strcmp(name, "msm_vfe2_video0")) return "upper_left"; if (!std::strcmp(name, "msm_vfe2_video1")) return "upper_right"; return nullptr; } // A setting from ~/.config/frametop.conf, or FT_ from the environment; "" if unset. std::string setting(const std::string &key) { if (const char *v = std::getenv(("FT_" + key).c_str())) return v; const char *home = std::getenv("HOME"); std::ifstream in(std::string(home ? home : "") + "/.config/frametop.conf"); std::string line, value; auto trim = [](std::string s) { s.erase(0, s.find_first_not_of(" \t\"'")); s.erase(s.find_last_not_of(" \t\"'") + 1); return s; }; while (std::getline(in, line)) { line = line.substr(0, line.find('#')); const auto eq = line.find('='); if (eq != std::string::npos && trim(line.substr(0, eq)) == key) value = trim(line.substr(eq + 1)); } return value; } std::vector parse_cpus(const char *p) { std::vector out; while (*p) { char *end; const long c = std::strtol(p, &end, 10); if (end == p) break; out.push_back(int(c)); p = *end == ',' ? end + 1 : end; } return out; } // Where SIGUSR1 puts recordings: $XDG_DATA_HOME/frametop/hands (~/.local/share/...). std::string recordings_dir() { const char *data = std::getenv("XDG_DATA_HOME"), *home = std::getenv("HOME"); std::string dir = data && *data ? data : std::string(home ? home : "") + "/.local/share"; for (const char *part : {"/frametop", "/hands"}) mkdir((dir += part).c_str(), 0700); return dir; } double cpu_seconds() { rusage r; getrusage(RUSAGE_SELF, &r); return r.ru_utime.tv_sec + r.ru_stime.tv_sec + (r.ru_utime.tv_usec + r.ru_stime.tv_usec) / 1e6; } } // namespace int main(int argc, char **argv) { double seconds = 0, status = 5; int threads = 3, niceness = 5; bool int8 = false, publish = true, track = true, swap_sides = false; std::string models = std::string(argv[0]).substr(0, std::string(argv[0]).rfind('/') + 1) + "../models/ncnn"; std::string record, ring_path = "/run/user/" + std::to_string(getuid()) + "/" FH_RING_NAME; // SteamOS starts user processes on CPUs 0-4 and keeps 5-7 (two A720s and the X4) for // SteamVR's compositor, whose threads there run at real-time priority, so they always // win. XRService pins its head tracking to 2-3. frame-hands' probes/core_ab.py // (2026-09-29, headset on, 3 rounds): on 5-7 a step took 8.4 ms against 13.2 on 2-4, // latency 9.6 against 14.1 ms, and the compositor's late frames and CPU/GPU time didn't change. std::vector cpus = {5, 6, 7}; if (const auto c = parse_cpus(setting("HANDS_CPUS").c_str()); !c.empty()) cpus = c; swap_sides = setting("HANDS_SWAP_SIDES") == "1"; // How crops are equalized. CLAHE helps the palm search find hands (about 10% more in the // dim recording), but makes the landmarks jitter, so they get plain crops. Contrast palm_contrast, hand_contrast{Contrast::None}; double keep_presence = 0.5; // landmark presence a tracked view needs to stay PinchParams pinch_params; double record_for = 120; for (int i = 1; i < argc; ++i) { const std::string a = argv[i]; const bool more = i + 1 < argc; if (a == "--seconds" && more) seconds = std::atof(argv[++i]); else if (a == "--threads" && more) threads = std::max(1, std::atoi(argv[++i])); else if (a == "--status" && more) status = std::atof(argv[++i]); else if (a == "--models" && more) models = argv[++i]; else if (a == "--nice" && more) niceness = std::atoi(argv[++i]); else if (a == "--int8") int8 = true; else if (a == "--no-publish") publish = false; else if (a == "--pinch-begin" && more) pinch_params.begin_m = std::atof(argv[++i]); else if (a == "--pinch-end" && more) pinch_params.end_m = std::atof(argv[++i]); else if (a == "--pinch-triangulated") pinch_params.triangulated = true; else if (a == "--swap-sides") swap_sides = true; else if (a == "--record-only") track = publish = false; else if (a == "--ring" && more) ring_path = argv[++i]; else if (a == "--record" && more) record = argv[++i]; else if (a == "--record-for" && more) record_for = std::atof(argv[++i]); else if (a == "--keep-presence" && more) keep_presence = std::atof(argv[++i]); else if (a == "--contrast" && more) { if (!Contrast::parse_pair(argv[++i], palm_contrast, hand_contrast)) return std::fprintf(stderr, "--contrast MODE or PALM/HAND, each clahe[:CLIP]|none|stretch\n"), 1; } else if (a == "--cpus" && more) { cpus = parse_cpus(argv[++i]); if (cpus.empty()) cpus = {5, 6, 7}; } else { std::printf("usage: %s [--seconds N] [--threads N] [--int8] [--status S] [--models DIR] [--nice N] [--no-publish]\n" " [--record DIR] [--record-for S] [--record-only] [--cpus 5,6,7] [--swap-sides]\n" " [--keep-presence P] (0.5) [--ring PATH] (ft-camd's, or ft-ringplay's)\n" " [--pinch-begin M] (0.020) [--pinch-end M] (0.035) [--pinch-triangulated]\n" " [--contrast MODE|PALM/HAND] (clahe[:CLIP], none, stretch; default clahe:2/none)\n" "Recording saves every frame set for S seconds (120) to DIR/sets.bin, for ft-handreplay; SIGUSR1\n" "starts one in ~/.local/share/frametop/hands/rec-