mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
frame-eyes, a separate project until now, becomes gaze/tracker:
- ft-eyegrab (was fe-bufprobe) copies the eye-camera frames, read-only, out of
SteamVR's eyetracking process. It runs as the system service
frametop-eyegrab.service, which gaze/tracker/install.sh installs to
/etc/frametop with sudo. It keeps only CAP_SYS_PTRACE, CAP_DAC_READ_SEARCH, and
CAP_CHOWN, and copies frames only while /dev/shm/frametop-eyes-want is fresh,
holding none of the tracker's buffers otherwise.
- ft-eyes (was fe-trackd) runs under ft-gazed in the dev container, with
build/venv's pinned numpy and OpenCV: while Eye tracker is Own tracker, or on
the probe's lease ("eyes SECONDS"). No sudo password or fe-live script at
run time any more.
- lab/ holds the research tools (ft-eyes-score, -e2e, -record, -replay,
-session) and findings.md. Recordings live outside the repo, in
~/.local/share/frametop/eyes/captures; .gitignore catches stray frame dumps.
Its socket is now @ft_eyes, its output /dev/shm/frametop-eyes-gaze, and its state
~/.local/state/frametop/gaze/eyes. On practice1 -> practice2 the whole live path
(ft-eyes-e2e) gives 1.30 deg median and 3.18 for the worst tenth, as before the
move (1.30, 3.21).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
606 lines
22 KiB
C
606 lines
22 KiB
C
/*
|
|
* ft-eyegrab: the eye-camera frames for our own eye tracker (ft-eyes), copied read-only out
|
|
* of the DMA-BUFs SteamVR's eyetracking process holds. Runs as root (pidfd_getfd needs
|
|
* CAP_SYS_PTRACE; ptrace_scope is 1): the system service frametop-eyegrab.service runs
|
|
* --share, installed by gaze/tracker/install.sh. The other modes are for finding the frames
|
|
* again after a SteamVR update (run them with sudo).
|
|
*
|
|
* ft-eyegrab --share PATH [--owner UID:GID] [--want FILE]
|
|
* keep the latest frames of both cameras in PATH (shared memory,
|
|
* 0600, owned by UID:GID, or the sudo user) for ft-eyes; follows
|
|
* the tracker through SteamVR restarts. With --want, only while
|
|
* FILE (a regular file owned by that user) was touched in the
|
|
* last WANT_FRESH seconds: ft-eyes and ft-eyes-record touch it
|
|
* every second, so nothing is copied, and none of the tracker's
|
|
* buffers are held, while nobody reads the frames
|
|
* ft-eyegrab list the buffers
|
|
* ft-eyegrab --scan [N] N snapshots (default 40) about 11 ms apart: which 4 KiB pages
|
|
* change, merged into regions, with byte statistics for each
|
|
* ft-eyegrab --dump I OFF LEN FILE
|
|
* copy LEN bytes at OFF of buffer I (from the list) to FILE
|
|
* ft-eyegrab --seq I OFF LEN FRAMES DIR
|
|
* FRAMES copies of that region, one each time it changes, to
|
|
* DIR/NNNN.raw, with DIR/times.txt (CLOCK_MONOTONIC_RAW)
|
|
* ft-eyegrab --rec SECONDS DIR
|
|
* every new eye-camera frame for SECONDS: DIR/frames.raw (512x400
|
|
* 8-bit frames back to back) and DIR/index.txt, one line per frame:
|
|
* "<n> <slot> <camera 0|1> <CLOCK_MONOTONIC_RAW time seen>"
|
|
* (lab/ft-eyes-record does the same from the shared frames,
|
|
* without root)
|
|
*
|
|
* The eye frames (found with --scan): in the 16 MiB buffer, eight slots 0x40000 apart from
|
|
* 0x230000, four per camera (slots 0-3, 4-7). Each slot starts with a small block, then a
|
|
* 512x400 8-bit image at 0x40c0 + 0x40 per slot, and one more 0x40 for the second camera's.
|
|
*
|
|
* Only reads the tracker's buffers. They are borrowed with pidfd_getfd and mapped PROT_READ; nothing is
|
|
* written, and the process isn't stopped or signalled. Reads can tear while the DSP writes.
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/syscall.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#define MAXBUF 32
|
|
#define PAGE 4096
|
|
|
|
typedef struct {
|
|
int xfd, fd;
|
|
size_t size;
|
|
unsigned long ino;
|
|
const uint8_t *p;
|
|
} buf_t;
|
|
|
|
static buf_t bufs[MAXBUF];
|
|
static int nbufs;
|
|
|
|
static double now(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
|
|
return ts.tv_sec + ts.tv_nsec * 1e-9;
|
|
}
|
|
|
|
static int find_tracker(void) {
|
|
DIR *d = opendir("/proc");
|
|
struct dirent *e;
|
|
int pid = -1;
|
|
while (d && (e = readdir(d))) {
|
|
char path[300], cmd[512];
|
|
if (e->d_name[0] < '0' || e->d_name[0] > '9') continue;
|
|
snprintf(path, sizeof path, "/proc/%s/cmdline", e->d_name);
|
|
FILE *f = fopen(path, "r");
|
|
if (!f) continue;
|
|
size_t n = fread(cmd, 1, sizeof cmd - 1, f);
|
|
fclose(f);
|
|
cmd[n] = 0;
|
|
if (strstr(cmd, "tools/eyetracking/bin/") && strstr(cmd, "/eyetracking")) {
|
|
pid = atoi(e->d_name);
|
|
break;
|
|
}
|
|
}
|
|
if (d) closedir(d);
|
|
return pid;
|
|
}
|
|
|
|
static int open_bufs(int pid) {
|
|
int pidfd = syscall(SYS_pidfd_open, pid, 0);
|
|
if (pidfd < 0) {
|
|
perror("pidfd_open");
|
|
return -1;
|
|
}
|
|
char dir[64];
|
|
snprintf(dir, sizeof dir, "/proc/%d/fd", pid);
|
|
DIR *d = opendir(dir);
|
|
struct dirent *e;
|
|
while (d && (e = readdir(d)) && nbufs < MAXBUF) {
|
|
char link[320], target[256];
|
|
if (e->d_name[0] == '.') continue;
|
|
snprintf(link, sizeof link, "%s/%s", dir, e->d_name);
|
|
ssize_t n = readlink(link, target, sizeof target - 1);
|
|
if (n <= 0) continue;
|
|
target[n] = 0;
|
|
if (strncmp(target, "/dmabuf:", 8) != 0) continue;
|
|
int xfd = atoi(e->d_name);
|
|
int fd = syscall(SYS_pidfd_getfd, pidfd, xfd, 0);
|
|
if (fd < 0) {
|
|
fprintf(stderr, "pidfd_getfd %d: %s\n", xfd, strerror(errno));
|
|
continue;
|
|
}
|
|
struct stat st;
|
|
fstat(fd, &st);
|
|
off_t size = lseek(fd, 0, SEEK_END);
|
|
void *p = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
|
|
if (p == MAP_FAILED) {
|
|
fprintf(stderr, "mmap fd %d (%lld bytes): %s\n", xfd, (long long)size, strerror(errno));
|
|
close(fd);
|
|
continue;
|
|
}
|
|
bufs[nbufs++] = (buf_t){xfd, fd, (size_t)size, (unsigned long)st.st_ino, p};
|
|
}
|
|
if (d) closedir(d);
|
|
close(pidfd);
|
|
return nbufs;
|
|
}
|
|
|
|
static uint64_t page_hash(const uint8_t *p) {
|
|
const uint64_t *q = (const uint64_t *)p;
|
|
uint64_t h = 1469598103934665603ull;
|
|
for (int i = 0; i < PAGE / 8; i += 4) h = (h ^ q[i]) * 1099511628211ull; // every 4th word
|
|
return h;
|
|
}
|
|
|
|
static void stats(const uint8_t *p, size_t n, double *mean, int *lo, int *hi, double *nonzero) {
|
|
uint64_t sum = 0, nz = 0;
|
|
int a = 255, b = 0;
|
|
for (size_t i = 0; i < n; i++) {
|
|
sum += p[i];
|
|
nz += p[i] != 0;
|
|
if (p[i] < a) a = p[i];
|
|
if (p[i] > b) b = p[i];
|
|
}
|
|
*mean = n ? (double)sum / n : 0;
|
|
*lo = a, *hi = b, *nonzero = n ? (double)nz / n : 0;
|
|
}
|
|
|
|
static void scan(int snaps) {
|
|
for (int b = 0; b < nbufs; b++) {
|
|
size_t pages = bufs[b].size / PAGE;
|
|
uint64_t *prev = calloc(pages, 8), *cur = calloc(pages, 8);
|
|
int *changes = calloc(pages, sizeof(int));
|
|
for (size_t i = 0; i < pages; i++) prev[i] = page_hash(bufs[b].p + i * PAGE);
|
|
double t0 = now();
|
|
for (int s = 1; s < snaps; s++) {
|
|
usleep(11000);
|
|
for (size_t i = 0; i < pages; i++) {
|
|
cur[i] = page_hash(bufs[b].p + i * PAGE);
|
|
if (cur[i] != prev[i]) changes[i]++;
|
|
prev[i] = cur[i];
|
|
}
|
|
}
|
|
double dt = now() - t0;
|
|
printf("buffer %d (fd %d, %zu bytes, ino %lu): %d snapshots over %.2f s\n", b, bufs[b].xfd, bufs[b].size,
|
|
bufs[b].ino, snaps, dt);
|
|
// Regions: runs of pages that changed at least once (gaps of up to 2 pages merged).
|
|
size_t i = 0;
|
|
int regions = 0;
|
|
while (i < pages) {
|
|
if (!changes[i]) {
|
|
i++;
|
|
continue;
|
|
}
|
|
size_t start = i, end = i, gap = 0;
|
|
int most = 0;
|
|
long total = 0;
|
|
for (; i < pages; i++) {
|
|
if (changes[i]) {
|
|
end = i, gap = 0;
|
|
total += changes[i];
|
|
if (changes[i] > most) most = changes[i];
|
|
} else if (++gap > 2) {
|
|
break;
|
|
}
|
|
}
|
|
size_t off = start * PAGE, len = (end - start + 1) * PAGE;
|
|
double mean, nz;
|
|
int lo, hi;
|
|
stats(bufs[b].p + off, len, &mean, &lo, &hi, &nz);
|
|
printf(" region 0x%08zx +0x%zx (%zu KiB): changed in up to %d of %d intervals (avg %.1f); "
|
|
"bytes mean %.1f min %d max %d nonzero %.0f%%\n",
|
|
off, len, len / 1024, most, snaps - 1, (double)total / (end - start + 1), mean, lo, hi, nz * 100);
|
|
regions++;
|
|
}
|
|
if (!regions) {
|
|
double mean, nz;
|
|
int lo, hi;
|
|
stats(bufs[b].p, bufs[b].size, &mean, &lo, &hi, &nz);
|
|
printf(" no change; bytes mean %.1f min %d max %d nonzero %.1f%%\n", mean, lo, hi, nz * 100);
|
|
}
|
|
free(prev), free(cur), free(changes);
|
|
}
|
|
}
|
|
|
|
#define EYE_W 512
|
|
#define EYE_H 400
|
|
#define EYE_SLOTS 8
|
|
|
|
static size_t slot_start(int k) {
|
|
return 0x230000 + (size_t)k * 0x40000 + 0x40c0 + (size_t)k * 0x40 + (k >= 4 ? 0x40 : 0);
|
|
}
|
|
|
|
// A cheap fingerprint of a frame: 256 words spread over it (a new frame changes nearly all).
|
|
static uint64_t frame_sig(const uint8_t *p) {
|
|
uint64_t h = 1469598103934665603ull, w;
|
|
for (int i = 0; i < 256; i++) {
|
|
memcpy(&w, p + (size_t)i * (EYE_W * EYE_H / 256), 8);
|
|
h = (h ^ w) * 1099511628211ull;
|
|
}
|
|
return h;
|
|
}
|
|
|
|
static volatile sig_atomic_t stop_rec;
|
|
static void on_stop(int sig) { (void)sig; stop_rec = 1; }
|
|
|
|
// The poller copies finished frames into a ring; a writer thread saves them, so a slow disk
|
|
// write never delays the polling.
|
|
#define RING 128
|
|
static struct {
|
|
uint8_t *frames;
|
|
int slot[RING];
|
|
double time[RING];
|
|
size_t head, tail, dropped; // head: next to fill (poller); tail: next to save (writer)
|
|
int done;
|
|
pthread_mutex_t mu;
|
|
pthread_cond_t cv;
|
|
FILE *f, *ix;
|
|
} ring = {.mu = PTHREAD_MUTEX_INITIALIZER, .cv = PTHREAD_COND_INITIALIZER};
|
|
|
|
static void *ring_writer(void *arg) {
|
|
(void)arg;
|
|
size_t fsize = EYE_W * EYE_H, n = 0;
|
|
pthread_mutex_lock(&ring.mu);
|
|
for (;;) {
|
|
while (ring.tail == ring.head && !ring.done) pthread_cond_wait(&ring.cv, &ring.mu);
|
|
if (ring.tail == ring.head) break;
|
|
size_t i = ring.tail % RING;
|
|
pthread_mutex_unlock(&ring.mu);
|
|
fwrite(ring.frames + i * fsize, 1, fsize, ring.f);
|
|
fprintf(ring.ix, "%zu %d %d %.6f\n", n++, ring.slot[i], ring.slot[i] >= 4, ring.time[i]);
|
|
pthread_mutex_lock(&ring.mu);
|
|
ring.tail++;
|
|
}
|
|
pthread_mutex_unlock(&ring.mu);
|
|
return NULL;
|
|
}
|
|
|
|
static void ring_put(const uint8_t *frame, int slot, double t) {
|
|
size_t fsize = EYE_W * EYE_H;
|
|
pthread_mutex_lock(&ring.mu);
|
|
int full = ring.head - ring.tail >= RING;
|
|
pthread_mutex_unlock(&ring.mu);
|
|
if (full) {
|
|
ring.dropped++;
|
|
return;
|
|
}
|
|
size_t i = ring.head % RING;
|
|
memcpy(ring.frames + i * fsize, frame, fsize);
|
|
ring.slot[i] = slot, ring.time[i] = t;
|
|
pthread_mutex_lock(&ring.mu);
|
|
ring.head++;
|
|
pthread_cond_signal(&ring.cv);
|
|
pthread_mutex_unlock(&ring.mu);
|
|
}
|
|
|
|
static int eye_buffer(void) {
|
|
for (int i = 0; i < nbufs; i++)
|
|
if (bufs[i].size == 16777216) return i;
|
|
return -1;
|
|
}
|
|
|
|
// Calls done(frame, slot, time) for every complete eye-camera frame until `seconds` pass
|
|
// (forever if negative), a stop signal comes, the tracker process goes away, or keep()
|
|
// (checked about every 0.25 s, when given) says to stop.
|
|
//
|
|
// A frame lands over several milliseconds, in bursts, and its last bursts can come after
|
|
// the camera has started its next frame. A slot isn't rewritten until at least three frames
|
|
// later (camera 0 cycles 3,0,1,2; camera 1 7,5,4,6,5,7,6,4), so a frame is passed on when
|
|
// its camera starts the frame after next. Changes to the slot just finished are late bursts,
|
|
// not a new frame. A frame's time is when its slot first changed.
|
|
static void poll_frames(int b, double seconds, int pid, void (*done)(const uint8_t *, int, double),
|
|
int (*keep)(void)) {
|
|
uint64_t sig[EYE_SLOTS];
|
|
double first[EYE_SLOTS];
|
|
int cur[2] = {-1, -1}, prev[2] = {-1, -1};
|
|
for (int k = 0; k < EYE_SLOTS; k++) sig[k] = frame_sig(bufs[b].p + slot_start(k)), first[k] = 0;
|
|
double start = now(), checked = start, kept = start;
|
|
char proc[64];
|
|
snprintf(proc, sizeof proc, "/proc/%d", pid);
|
|
while ((seconds < 0 || now() - start < seconds) && !stop_rec) {
|
|
double t = now();
|
|
if (t - checked > 1.0) { // the tracker restarted: its buffers are stale
|
|
struct stat st;
|
|
if (stat(proc, &st) != 0) return;
|
|
checked = t;
|
|
}
|
|
if (keep && t - kept > 0.25) {
|
|
if (!keep()) return;
|
|
kept = t;
|
|
}
|
|
for (int k = 0; k < EYE_SLOTS; k++) {
|
|
uint64_t s = frame_sig(bufs[b].p + slot_start(k));
|
|
if (s == sig[k]) continue;
|
|
sig[k] = s;
|
|
int cam = k >= 4;
|
|
if (k == cur[cam] || k == prev[cam]) continue; // landing, or a late burst
|
|
if (prev[cam] >= 0) done(bufs[b].p + slot_start(prev[cam]), prev[cam], first[prev[cam]]);
|
|
prev[cam] = cur[cam];
|
|
cur[cam] = k;
|
|
first[k] = t;
|
|
}
|
|
usleep(300);
|
|
}
|
|
}
|
|
|
|
static void rec_frame(const uint8_t *frame, int slot, double t) { ring_put(frame, slot, t); }
|
|
|
|
static int rec(double seconds, const char *dir, int pid) {
|
|
int b = eye_buffer();
|
|
if (b < 0) {
|
|
fprintf(stderr, "no 16 MiB buffer\n");
|
|
return 1;
|
|
}
|
|
// Frames stream to disk (about 37 MB/s), so a long recording doesn't fill memory.
|
|
// Ctrl-C or SIGTERM ends it early and keeps what was recorded.
|
|
char path[512];
|
|
snprintf(path, sizeof path, "%s/frames.raw", dir);
|
|
ring.f = fopen(path, "wb");
|
|
snprintf(path, sizeof path, "%s/index.txt", dir);
|
|
ring.ix = fopen(path, "w");
|
|
ring.frames = malloc((size_t)RING * EYE_W * EYE_H);
|
|
if (!ring.f || !ring.ix || !ring.frames) {
|
|
perror(dir);
|
|
return 1;
|
|
}
|
|
setvbuf(ring.f, NULL, _IOFBF, 4 << 20);
|
|
signal(SIGINT, on_stop);
|
|
signal(SIGTERM, on_stop);
|
|
pthread_t writer;
|
|
pthread_create(&writer, NULL, ring_writer, NULL);
|
|
double start = now();
|
|
poll_frames(b, seconds, pid, rec_frame, NULL);
|
|
pthread_mutex_lock(&ring.mu);
|
|
ring.done = 1;
|
|
pthread_cond_signal(&ring.cv);
|
|
pthread_mutex_unlock(&ring.mu);
|
|
pthread_join(writer, NULL);
|
|
fclose(ring.f), fclose(ring.ix);
|
|
printf("%zu frames in %.1f s to %s", ring.head, now() - start, dir);
|
|
if (ring.dropped) printf(" (%zu dropped: disk too slow)", ring.dropped);
|
|
printf("\n");
|
|
free(ring.frames);
|
|
return 0;
|
|
}
|
|
|
|
// --- --share: the latest frames in shared memory for the live tracker ---
|
|
//
|
|
// The file (SHARE_PATH, mode 0600, owned by the --owner user) is a header, then SHARE_SLOTS
|
|
// entries per camera. Each entry is a 64-byte head and one 512x400 frame. Frame n of camera
|
|
// c goes in entry c * SHARE_SLOTS + n % SHARE_SLOTS. The head's `seq` is odd while it's
|
|
// written (read it before and after copying, and retry if it changed or was odd), and
|
|
// count[c] is how many frames camera c has published. tracker_pid is 0 while no frames
|
|
// come (nobody wants them, or SteamVR's tracker isn't running).
|
|
//
|
|
// This keeps the tracker's own buffers behind root: the user side only ever sees copies.
|
|
#define SHARE_SLOTS 8
|
|
#define SHARE_MAGIC 0x31434546u // "FEC1"
|
|
#define WANT_FRESH 3.0 // seconds a touch of the --want file lasts
|
|
|
|
typedef struct {
|
|
uint32_t magic, version, width, height, slots, entry_size;
|
|
volatile uint64_t count[2];
|
|
uint32_t tracker_pid, pad0;
|
|
uint8_t pad[16];
|
|
} share_head_t;
|
|
|
|
typedef struct {
|
|
volatile uint64_t seq;
|
|
double t;
|
|
uint64_t n;
|
|
uint32_t cam, slot;
|
|
uint8_t pad[32];
|
|
} share_entry_t;
|
|
|
|
_Static_assert(sizeof(share_head_t) == 64, "share header");
|
|
_Static_assert(sizeof(share_entry_t) == 64, "share entry");
|
|
|
|
static uint8_t *share;
|
|
static const char *want_path;
|
|
static uid_t owner_uid = (uid_t)-1;
|
|
static gid_t owner_gid = (gid_t)-1;
|
|
|
|
static void share_frame(const uint8_t *frame, int slot, double t) {
|
|
share_head_t *h = (share_head_t *)share;
|
|
int cam = slot >= 4;
|
|
uint64_t n = h->count[cam];
|
|
size_t esize = sizeof(share_entry_t) + EYE_W * EYE_H;
|
|
share_entry_t *e = (share_entry_t *)(share + sizeof *h + (cam * SHARE_SLOTS + n % SHARE_SLOTS) * esize);
|
|
e->seq++;
|
|
__atomic_thread_fence(__ATOMIC_RELEASE);
|
|
memcpy((uint8_t *)(e + 1), frame, EYE_W * EYE_H);
|
|
e->t = t, e->n = n, e->cam = cam, e->slot = slot;
|
|
__atomic_thread_fence(__ATOMIC_RELEASE);
|
|
e->seq++;
|
|
__atomic_thread_fence(__ATOMIC_RELEASE);
|
|
h->count[cam] = n + 1;
|
|
}
|
|
|
|
// Someone reads the frames: the want file was touched lately. It must be a regular file
|
|
// (lstat: a link isn't followed) owned by the frames' owner, so no one else can turn this on.
|
|
static int wanted(void) {
|
|
if (!want_path) return 1;
|
|
struct stat st;
|
|
if (lstat(want_path, &st) != 0 || !S_ISREG(st.st_mode)) return 0;
|
|
if (owner_uid != (uid_t)-1 && st.st_uid != owner_uid) return 0;
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_REALTIME, &ts);
|
|
double age = (ts.tv_sec - st.st_mtim.tv_sec) + (ts.tv_nsec - st.st_mtim.tv_nsec) * 1e-9;
|
|
return age < WANT_FRESH;
|
|
}
|
|
|
|
static void close_bufs(void) {
|
|
for (int i = 0; i < nbufs; i++) munmap((void *)bufs[i].p, bufs[i].size), close(bufs[i].fd);
|
|
nbufs = 0;
|
|
}
|
|
|
|
static int share_loop(const char *path) {
|
|
size_t esize = sizeof(share_entry_t) + EYE_W * EYE_H;
|
|
size_t size = sizeof(share_head_t) + 2 * SHARE_SLOTS * esize;
|
|
unlink(path);
|
|
int fd = open(path, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
|
|
if (fd < 0 || ftruncate(fd, size) != 0) {
|
|
perror(path);
|
|
return 1;
|
|
}
|
|
if (owner_uid != (uid_t)-1 && fchown(fd, owner_uid, owner_gid) != 0) perror("fchown");
|
|
share = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
|
close(fd);
|
|
if (share == MAP_FAILED) {
|
|
perror("mmap");
|
|
return 1;
|
|
}
|
|
share_head_t *h = (share_head_t *)share;
|
|
*h = (share_head_t){.magic = SHARE_MAGIC, .version = 1, .width = EYE_W, .height = EYE_H,
|
|
.slots = SHARE_SLOTS, .entry_size = (uint32_t)esize};
|
|
signal(SIGINT, on_stop);
|
|
signal(SIGTERM, on_stop);
|
|
fprintf(stderr, "ft-eyegrab: sharing frames in %s%s%s\n", path, want_path ? " while wanted by " : "",
|
|
want_path ? want_path : "");
|
|
int pid = -1, idle = -1, missing = 0;
|
|
while (!stop_rec) {
|
|
if (!wanted()) {
|
|
// Nobody reads the frames: copy nothing, and let go of the tracker's buffers.
|
|
if (idle != 1) fprintf(stderr, "ft-eyegrab: idle (nobody wants frames)\n"), idle = 1;
|
|
close_bufs();
|
|
pid = -1;
|
|
h->tracker_pid = 0;
|
|
usleep(250000);
|
|
continue;
|
|
}
|
|
if (nbufs == 0 && ((pid = find_tracker()) < 0 || open_bufs(pid) <= 0 || eye_buffer() < 0)) {
|
|
// SteamVR's tracker isn't running (yet, or again).
|
|
if (!missing) fprintf(stderr, "ft-eyegrab: waiting for SteamVR's eyetracking\n"), missing = 1;
|
|
close_bufs();
|
|
h->tracker_pid = 0;
|
|
sleep(2);
|
|
continue;
|
|
}
|
|
if (idle != 0 || missing) fprintf(stderr, "ft-eyegrab: copying frames from eyetracking %d\n", pid);
|
|
idle = 0, missing = 0;
|
|
h->tracker_pid = pid;
|
|
poll_frames(eye_buffer(), -1, pid, share_frame, wanted);
|
|
struct stat st;
|
|
char proc[64];
|
|
snprintf(proc, sizeof proc, "/proc/%d", pid);
|
|
if (!stop_rec && stat(proc, &st) != 0) {
|
|
fprintf(stderr, "ft-eyegrab: eyetracking %d went away; waiting for it\n", pid);
|
|
close_bufs();
|
|
h->tracker_pid = 0;
|
|
}
|
|
}
|
|
close_bufs();
|
|
unlink(path);
|
|
return 0;
|
|
}
|
|
|
|
static int dump(int b, size_t off, size_t len, const char *file) {
|
|
if (b < 0 || b >= nbufs || off + len > bufs[b].size) {
|
|
fprintf(stderr, "out of range\n");
|
|
return 1;
|
|
}
|
|
FILE *f = fopen(file, "wb");
|
|
if (!f) {
|
|
perror(file);
|
|
return 1;
|
|
}
|
|
fwrite(bufs[b].p + off, 1, len, f);
|
|
fclose(f);
|
|
printf("wrote %zu bytes to %s\n", len, file);
|
|
return 0;
|
|
}
|
|
|
|
static int seq(int b, size_t off, size_t len, int frames, const char *dir) {
|
|
if (b < 0 || b >= nbufs || off + len > bufs[b].size) {
|
|
fprintf(stderr, "out of range\n");
|
|
return 1;
|
|
}
|
|
char path[512];
|
|
snprintf(path, sizeof path, "%s/times.txt", dir);
|
|
FILE *times = fopen(path, "w");
|
|
if (!times) {
|
|
perror(path);
|
|
return 1;
|
|
}
|
|
uint8_t *copy = malloc(len);
|
|
uint64_t last = 0;
|
|
int got = 0;
|
|
double start = now();
|
|
while (got < frames && now() - start < 30) {
|
|
uint64_t h = 0;
|
|
for (size_t i = 0; i + PAGE <= len; i += PAGE * 8) h ^= page_hash(bufs[b].p + off + i) + i;
|
|
if (h != last) {
|
|
last = h;
|
|
double t = now();
|
|
memcpy(copy, bufs[b].p + off, len);
|
|
snprintf(path, sizeof path, "%s/%04d.raw", dir, got);
|
|
FILE *f = fopen(path, "wb");
|
|
if (f) fwrite(copy, 1, len, f), fclose(f);
|
|
fprintf(times, "%d %.6f\n", got, t);
|
|
got++;
|
|
}
|
|
usleep(1000);
|
|
}
|
|
fclose(times);
|
|
free(copy);
|
|
printf("%d frames in %s\n", got, dir);
|
|
return 0;
|
|
}
|
|
|
|
static void usage(void) {
|
|
fprintf(stderr, "usage: ft-eyegrab [--share PATH [--owner UID:GID] [--want FILE] | --scan [N] | --dump I OFF LEN FILE |\n"
|
|
" --seq I OFF LEN FRAMES DIR | --rec SECONDS DIR]\n");
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc >= 3 && strcmp(argv[1], "--share") == 0) {
|
|
const char *uid = getenv("SUDO_UID"), *gid = getenv("SUDO_GID");
|
|
if (uid && gid) owner_uid = (uid_t)atoi(uid), owner_gid = (gid_t)atoi(gid);
|
|
for (int i = 3; i < argc; i++) {
|
|
unsigned u, g;
|
|
if (strcmp(argv[i], "--owner") == 0 && i + 1 < argc && sscanf(argv[i + 1], "%u:%u", &u, &g) == 2) {
|
|
owner_uid = u, owner_gid = g, i++;
|
|
} else if (strcmp(argv[i], "--want") == 0 && i + 1 < argc) {
|
|
want_path = argv[++i];
|
|
} else {
|
|
usage();
|
|
return 2;
|
|
}
|
|
}
|
|
return share_loop(argv[2]);
|
|
}
|
|
int pid = find_tracker();
|
|
if (pid < 0) {
|
|
fprintf(stderr, "SteamVR's eyetracking process isn't running\n");
|
|
return 1;
|
|
}
|
|
if (open_bufs(pid) <= 0) {
|
|
fprintf(stderr, "no buffers (run as root)\n");
|
|
return 1;
|
|
}
|
|
if (argc >= 2 && strcmp(argv[1], "--scan") == 0) {
|
|
scan(argc >= 3 ? atoi(argv[2]) : 40);
|
|
} else if (argc == 6 && strcmp(argv[1], "--dump") == 0) {
|
|
return dump(atoi(argv[2]), strtoul(argv[3], NULL, 0), strtoul(argv[4], NULL, 0), argv[5]);
|
|
} else if (argc == 4 && strcmp(argv[1], "--rec") == 0) {
|
|
return rec(atof(argv[2]), argv[3], pid);
|
|
} else if (argc == 7 && strcmp(argv[1], "--seq") == 0) {
|
|
return seq(atoi(argv[2]), strtoul(argv[3], NULL, 0), strtoul(argv[4], NULL, 0), atoi(argv[5]), argv[6]);
|
|
} else if (argc == 1) {
|
|
printf("eyetracking pid %d\n", pid);
|
|
for (int b = 0; b < nbufs; b++)
|
|
printf("buffer %d: fd %d, %zu bytes, ino %lu\n", b, bufs[b].xfd, bufs[b].size, bufs[b].ino);
|
|
} else {
|
|
usage();
|
|
return 2;
|
|
}
|
|
return 0;
|
|
}
|