mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 01:00:06 +02:00
ft-camd: keep colour trouble away from the mono cameras, and idle colour
A colour camera probes at most 4 buffers a frame (each probe syncs a ~9 MB buffer's cache, which made the mono cameras miss frames), and one that goes stale twice in a row is paused (10 s, doubling to 160 s) and learned again instead of ft-camd exiting. The colour cameras run at 2 fps until a reader asks for more in frametop-hands/color-fps, which saves most of their decoding while only their brightness is needed. Each mono camera's latest near-black frame's mean goes in the ring (dark_mean), a measure of the room's IR light. The service now starts with --with-color; HANDS_CAMERAS=mono leaves the colour cameras out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8bc6739dca
commit
40a2f39b2a
3 files changed
+156
-15
No files matched your search
+148
-13
@@ -67,6 +67,17 @@
|
|||||||
#define SEAM_LIMIT 2.0 /* seam_score above this: the frame carries the half-size copy */
|
#define SEAM_LIMIT 2.0 /* seam_score above this: the frame carries the half-size copy */
|
||||||
#define STALE_RELEARN 30 /* consecutive unchanged frames: the mapping changed */
|
#define STALE_RELEARN 30 /* consecutive unchanged frames: the mapping changed */
|
||||||
#define MAX_RELEARNS 5 /* then assume XRService has new buffers, and exit */
|
#define MAX_RELEARNS 5 /* then assume XRService has new buffers, and exit */
|
||||||
|
/*
|
||||||
|
* The color cameras never take the mono ones down with them. Each color buffer is ~9 MB,
|
||||||
|
* and every probe for a fresh one syncs a whole buffer's cache, so a color camera looks
|
||||||
|
* at no more than COLOR_PROBES buffers a frame. One that keeps going stale (on 2026-09-30
|
||||||
|
* it did while the headset was worn, never while it lay unused) is paused for
|
||||||
|
* COLOR_PAUSE_S, doubling each time up to COLOR_PAUSE_MAX_S, then learned again; ft-camd
|
||||||
|
* doesn't exit over it.
|
||||||
|
*/
|
||||||
|
#define COLOR_PROBES 4
|
||||||
|
#define COLOR_PAUSE_S 10
|
||||||
|
#define COLOR_PAUSE_MAX_S 160
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
xr_camera_t *cam;
|
xr_camera_t *cam;
|
||||||
@@ -105,6 +116,10 @@ typedef struct {
|
|||||||
unsigned out_w, out_h; /* image size in the ring */
|
unsigned out_w, out_h; /* image size in the ring */
|
||||||
uint64_t last_pub_ns; /* --color-fps pacing */
|
uint64_t last_pub_ns; /* --color-fps pacing */
|
||||||
uint64_t paced, seams; /* color frames skipped: pacing, the half-size copy */
|
uint64_t paced, seams; /* color frames skipped: pacing, the half-size copy */
|
||||||
|
uint64_t paused_until; /* color: going stale, left alone until then */
|
||||||
|
unsigned pause_s; /* ... for this long the next time */
|
||||||
|
uint64_t pauses;
|
||||||
|
unsigned good_run; /* color frames in a row that were fresh */
|
||||||
uint8_t *ring_dark;
|
uint8_t *ring_dark;
|
||||||
uint64_t dark_no;
|
uint64_t dark_no;
|
||||||
|
|
||||||
@@ -128,6 +143,13 @@ static bool opt_with_dark; /* also publish the near-black frames */
|
|||||||
static bool opt_with_color; /* also publish the Arcturus color cameras */
|
static bool opt_with_color; /* also publish the Arcturus color cameras */
|
||||||
static unsigned opt_color_scale = 2; /* ... at 1/N size */
|
static unsigned opt_color_scale = 2; /* ... at 1/N size */
|
||||||
static double opt_color_fps = 30; /* ... at most this often */
|
static double opt_color_fps = 30; /* ... at most this often */
|
||||||
|
static double opt_color_idle = 2; /* ... and this often while no reader asks for more */
|
||||||
|
/*
|
||||||
|
* The color rate readers ask for (see color_want): ft-hands tracks with the color cameras
|
||||||
|
* only in bright light, and otherwise needs just their brightness now and then, which
|
||||||
|
* saves most of the decoding (about 11% of a core at 30 fps).
|
||||||
|
*/
|
||||||
|
static double color_fps = 2;
|
||||||
static double opt_status = 10.0;
|
static double opt_status = 10.0;
|
||||||
|
|
||||||
static volatile sig_atomic_t stop;
|
static volatile sig_atomic_t stop;
|
||||||
@@ -509,7 +531,7 @@ static int find_fresh(cam_t *c, uint64_t *out)
|
|||||||
int t = order[j]; order[j] = order[j - 1]; order[j - 1] = t;
|
int t = order[j]; order[j] = order[j - 1]; order[j - 1] = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int k = 0; k < c->nslots; k++) {
|
for (int k = 0; k < c->nslots && (!c->color || k < COLOR_PROBES); k++) {
|
||||||
|
|
||||||
int s = order[k];
|
int s = order[k];
|
||||||
|
|
||||||
@@ -640,6 +662,9 @@ static void on_frame(cam_t *c, int64_t index, uint32_t seq, uint64_t ts, uint64_
|
|||||||
if (index < 0 || index >= MAX_INDEX || c->nslots == 0)
|
if (index < 0 || index >= MAX_INDEX || c->nslots == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (c->color && evtime < c->paused_until)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!c->mapped) {
|
if (!c->mapped) {
|
||||||
learn(c, (int)index);
|
learn(c, (int)index);
|
||||||
return;
|
return;
|
||||||
@@ -652,8 +677,8 @@ static void on_frame(cam_t *c, int64_t index, uint32_t seq, uint64_t ts, uint64_
|
|||||||
|
|
||||||
int slot = c->slot_of[index];
|
int slot = c->slot_of[index];
|
||||||
|
|
||||||
/* color runs at 60 fps: skip frames early enough that --color-fps holds, before any sync */
|
/* color runs at 60 fps: skip frames early enough that the asked rate holds, before any sync */
|
||||||
if (c->color && opt_color_fps > 0 && evtime - c->last_pub_ns < (uint64_t)(1e9 / opt_color_fps) - 3000000) {
|
if (c->color && color_fps > 0 && evtime - c->last_pub_ns < (uint64_t)(1e9 / color_fps) - 3000000) {
|
||||||
c->paced++;
|
c->paced++;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -700,7 +725,19 @@ static void on_frame(cam_t *c, int64_t index, uint32_t seq, uint64_t ts, uint64_
|
|||||||
}
|
}
|
||||||
c->stale++;
|
c->stale++;
|
||||||
c->rc->dropped++;
|
c->rc->dropped++;
|
||||||
if (++c->stale_run >= STALE_RELEARN) {
|
c->good_run = 0;
|
||||||
|
if (++c->stale_run >= STALE_RELEARN && c->color && c->relearns >= 1) {
|
||||||
|
/* the second time in a row: leave it alone for a while (see COLOR_PAUSE_S) */
|
||||||
|
c->pause_s = c->pause_s ? (c->pause_s * 2 > COLOR_PAUSE_MAX_S ? COLOR_PAUSE_MAX_S : c->pause_s * 2)
|
||||||
|
: COLOR_PAUSE_S;
|
||||||
|
c->paused_until = evtime + (uint64_t)c->pause_s * 1000000000ull;
|
||||||
|
c->pauses++;
|
||||||
|
fprintf(stderr, "%s: %d frames in a row unchanged again; color paused for %u s\n", c->slug,
|
||||||
|
c->stale_run, c->pause_s);
|
||||||
|
c->stale_run = 0;
|
||||||
|
c->relearns = 0;
|
||||||
|
reset_learning(c);
|
||||||
|
} else if (c->stale_run >= STALE_RELEARN) {
|
||||||
if (++c->relearns > MAX_RELEARNS) {
|
if (++c->relearns > MAX_RELEARNS) {
|
||||||
fprintf(stderr, "%s: its buffers keep going stale; XRService must have new ones\n", c->slug);
|
fprintf(stderr, "%s: its buffers keep going stale; XRService must have new ones\n", c->slug);
|
||||||
exit(3);
|
exit(3);
|
||||||
@@ -713,6 +750,8 @@ static void on_frame(cam_t *c, int64_t index, uint32_t seq, uint64_t ts, uint64_
|
|||||||
}
|
}
|
||||||
|
|
||||||
c->stale_run = 0;
|
c->stale_run = 0;
|
||||||
|
if (c->color && ++c->good_run >= 600) /* about a minute at the idle rate: trouble forgotten */
|
||||||
|
c->relearns = 0, c->pause_s = 0, c->good_run = 0;
|
||||||
memcpy(c->samp[slot], cur, sizeof(cur));
|
memcpy(c->samp[slot], cur, sizeof(cur));
|
||||||
memcpy(c->hist[1], c->hist[0], sizeof(cur));
|
memcpy(c->hist[1], c->hist[0], sizeof(cur));
|
||||||
memcpy(c->hist[0], cur, sizeof(cur));
|
memcpy(c->hist[0], cur, sizeof(cur));
|
||||||
@@ -749,6 +788,7 @@ static void on_frame(cam_t *c, int64_t index, uint32_t seq, uint64_t ts, uint64_
|
|||||||
if (mean < opt_dark * peak) {
|
if (mean < opt_dark * peak) {
|
||||||
c->dark++;
|
c->dark++;
|
||||||
c->rc->dropped++;
|
c->rc->dropped++;
|
||||||
|
c->rc->dark_mean = (float)(mean > 0.01 ? mean : 0.01);
|
||||||
if (c->rc_dark)
|
if (c->rc_dark)
|
||||||
publish(c, c->rc_dark, c->ring_dark, &c->dark_no, c->map[slot], seq, ts, evtime, mean);
|
publish(c, c->rc_dark, c->ring_dark, &c->dark_no, c->map[slot], seq, ts, evtime, mean);
|
||||||
} else {
|
} else {
|
||||||
@@ -800,6 +840,81 @@ static int ring_layout(int cam_of[], bool dark_of[], int max)
|
|||||||
|
|
||||||
static char ring_dir[64], ring_file[96];
|
static char ring_dir[64], ring_file[96];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The color rate readers want: a number of frames a second in ring_dir/color-fps, which
|
||||||
|
* ft-hands rewrites every second while it tracks with the color cameras. A file newer than
|
||||||
|
* 3 s holds (up to --color-fps); otherwise the color cameras run at --color-idle.
|
||||||
|
*/
|
||||||
|
static void color_want(void)
|
||||||
|
{
|
||||||
|
char path[128], buf[32] = "";
|
||||||
|
struct stat st;
|
||||||
|
struct timespec now;
|
||||||
|
double want = opt_color_idle;
|
||||||
|
|
||||||
|
snprintf(path, sizeof(path), "%s/color-fps", ring_dir);
|
||||||
|
clock_gettime(CLOCK_REALTIME, &now);
|
||||||
|
|
||||||
|
if (stat(path, &st) == 0 && now.tv_sec - st.st_mtim.tv_sec <= 3) {
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
if (f) {
|
||||||
|
if (fgets(buf, sizeof(buf), f) && atof(buf) > 0)
|
||||||
|
want = atof(buf);
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (want > opt_color_fps)
|
||||||
|
want = opt_color_fps;
|
||||||
|
|
||||||
|
if (want != color_fps) {
|
||||||
|
printf("color cameras: %.0f fps\n", want);
|
||||||
|
fflush(stdout);
|
||||||
|
color_fps = want;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A setting from ~/.config/frametop.conf, or FT_<key> from the environment; "" if unset. */
|
||||||
|
static void setting(const char *key, char *out, size_t n)
|
||||||
|
{
|
||||||
|
char env[64], path[512], line[512];
|
||||||
|
const char *home = getenv("HOME");
|
||||||
|
|
||||||
|
snprintf(env, sizeof(env), "FT_%s", key);
|
||||||
|
out[0] = 0;
|
||||||
|
|
||||||
|
if (getenv(env)) {
|
||||||
|
snprintf(out, n, "%s", getenv(env));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(path, sizeof(path), "%s/.config/frametop.conf", home ? home : "");
|
||||||
|
FILE *f = fopen(path, "r");
|
||||||
|
|
||||||
|
if (!f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
size_t klen = strlen(key);
|
||||||
|
|
||||||
|
while (fgets(line, sizeof(line), f)) {
|
||||||
|
char *p = line;
|
||||||
|
while (*p == ' ' || *p == '\t')
|
||||||
|
p++;
|
||||||
|
if (strncmp(p, key, klen) || (p[klen] != '=' && p[klen] != ' '))
|
||||||
|
continue;
|
||||||
|
p = strchr(p, '=');
|
||||||
|
if (!p)
|
||||||
|
continue;
|
||||||
|
p++;
|
||||||
|
while (*p == ' ' || *p == '"' || *p == '\'')
|
||||||
|
p++;
|
||||||
|
size_t len = strcspn(p, " \t\"'#\n");
|
||||||
|
snprintf(out, n, "%.*s", (int)len, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
/* Created once privileges are gone, as the user, in a folder only the user can write. */
|
/* Created once privileges are gone, as the user, in a folder only the user can write. */
|
||||||
static uint8_t *ring_create(size_t *len_out)
|
static uint8_t *ring_create(size_t *len_out)
|
||||||
{
|
{
|
||||||
@@ -950,18 +1065,20 @@ static void status(double secs)
|
|||||||
for (int i = 0; i < ncams; i++) {
|
for (int i = 0; i < ncams; i++) {
|
||||||
cam_t *c = &cams[i];
|
cam_t *c = &cams[i];
|
||||||
if (c->color) {
|
if (c->color) {
|
||||||
printf(" %s %s%.1f fps (paced %llu half-size copy %llu stale %llu torn %llu, sync %.2f ms decode %.2f ms)",
|
printf(" %s %s%s%.1f fps (paced %llu half-size copy %llu stale %llu torn %llu paused %llu, sync %.2f ms "
|
||||||
c->slug, c->mapped ? "" : "learning ", (double)(c->bright - c->last_bright) / opt_status,
|
"decode %.2f ms)",
|
||||||
(unsigned long long)c->paced, (unsigned long long)c->seams, (unsigned long long)c->stale,
|
c->slug, mono_ns() < c->paused_until ? "PAUSED " : "", c->mapped ? "" : "learning ",
|
||||||
(unsigned long long)c->torn, c->nsync ? c->sync_ns / 1e6 / c->nsync : 0.0,
|
(double)(c->bright - c->last_bright) / opt_status, (unsigned long long)c->paced,
|
||||||
|
(unsigned long long)c->seams, (unsigned long long)c->stale, (unsigned long long)c->torn,
|
||||||
|
(unsigned long long)c->pauses, c->nsync ? c->sync_ns / 1e6 / c->nsync : 0.0,
|
||||||
c->bright ? c->copy_ns / 1e6 / c->bright : 0.0);
|
c->bright ? c->copy_ns / 1e6 / c->bright : 0.0);
|
||||||
c->last_bright = c->bright;
|
c->last_bright = c->bright;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
printf(" %s %s%.1f fps (dark %llu stale %llu torn %llu remapped %llu, sync %.2f ms copy %.2f ms)",
|
printf(" %s %s%.1f fps (dark %llu at %.1f stale %llu torn %llu remapped %llu, sync %.2f ms copy %.2f ms)",
|
||||||
c->slug, c->mapped ? "" : "learning ", (double)(c->bright - c->last_bright) / opt_status,
|
c->slug, c->mapped ? "" : "learning ", (double)(c->bright - c->last_bright) / opt_status,
|
||||||
(unsigned long long)c->dark, (unsigned long long)c->stale, (unsigned long long)c->torn,
|
(unsigned long long)c->dark, c->rc->dark_mean, (unsigned long long)c->stale,
|
||||||
(unsigned long long)c->repairs,
|
(unsigned long long)c->torn, (unsigned long long)c->repairs,
|
||||||
c->nsync ? c->sync_ns / 1e6 / c->nsync : 0.0, c->bright ? c->copy_ns / 1e6 / c->bright : 0.0);
|
c->nsync ? c->sync_ns / 1e6 / c->nsync : 0.0, c->bright ? c->copy_ns / 1e6 / c->bright : 0.0);
|
||||||
c->last_bright = c->bright;
|
c->last_bright = c->bright;
|
||||||
}
|
}
|
||||||
@@ -985,8 +1102,11 @@ static void usage(const char *argv0)
|
|||||||
" --with-dark also publish the dark frames, as extra ring cameras flagged FH_CAM_DARK\n"
|
" --with-dark also publish the dark frames, as extra ring cameras flagged FH_CAM_DARK\n"
|
||||||
" --with-color also publish the Arcturus color cameras' luma, flagged FH_CAM_COLOR\n"
|
" --with-color also publish the Arcturus color cameras' luma, flagged FH_CAM_COLOR\n"
|
||||||
" --color-scale N ... at 1/N size (default 2: 986x1232)\n"
|
" --color-scale N ... at 1/N size (default 2: 986x1232)\n"
|
||||||
" --color-fps F ... at most F frames a second (default 30; they run at 60)\n"
|
" --color-fps F ... at most F frames a second (default 30; they run at 60), as readers ask\n"
|
||||||
|
" in /run/user/UID/frametop-hands/color-fps (ft-hands does when it tracks with them)\n"
|
||||||
|
" --color-idle F ... and F a second while no reader asks (default 2: enough to tell the light)\n"
|
||||||
" --status S print a status line every S seconds, 0 for never (default 10)\n"
|
" --status S print a status line every S seconds, 0 for never (default 10)\n"
|
||||||
|
"HANDS_CAMERAS=mono in ~/.config/frametop.conf (or FT_HANDS_CAMERAS) turns --with-color off.\n"
|
||||||
"Frames go to /run/user/UID/" FH_RING_NAME " (layout in fhring.h).\n", argv0);
|
"Frames go to /run/user/UID/" FH_RING_NAME " (layout in fhring.h).\n", argv0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1006,6 +1126,8 @@ int main(int argc, char **argv)
|
|||||||
opt_color_scale = (unsigned)atoi(argv[++i]);
|
opt_color_scale = (unsigned)atoi(argv[++i]);
|
||||||
else if (!strcmp(argv[i], "--color-fps") && i + 1 < argc)
|
else if (!strcmp(argv[i], "--color-fps") && i + 1 < argc)
|
||||||
opt_color_fps = atof(argv[++i]);
|
opt_color_fps = atof(argv[++i]);
|
||||||
|
else if (!strcmp(argv[i], "--color-idle") && i + 1 < argc)
|
||||||
|
opt_color_idle = atof(argv[++i]);
|
||||||
else if (!strcmp(argv[i], "--status") && i + 1 < argc)
|
else if (!strcmp(argv[i], "--status") && i + 1 < argc)
|
||||||
opt_status = atof(argv[++i]);
|
opt_status = atof(argv[++i]);
|
||||||
else {
|
else {
|
||||||
@@ -1014,6 +1136,14 @@ int main(int argc, char **argv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char cams_setting[32];
|
||||||
|
setting("HANDS_CAMERAS", cams_setting, sizeof(cams_setting));
|
||||||
|
if (opt_with_color && !strcmp(cams_setting, "mono")) {
|
||||||
|
printf("HANDS_CAMERAS=mono: no color cameras\n");
|
||||||
|
opt_with_color = false;
|
||||||
|
}
|
||||||
|
color_fps = opt_color_idle < opt_color_fps ? opt_color_idle : opt_color_fps;
|
||||||
|
|
||||||
bool root = geteuid() == 0;
|
bool root = geteuid() == 0;
|
||||||
uid_t uid = getuid();
|
uid_t uid = getuid();
|
||||||
gid_t gid = getgid();
|
gid_t gid = getgid();
|
||||||
@@ -1093,7 +1223,7 @@ int main(int argc, char **argv)
|
|||||||
signal(SIGINT, on_signal);
|
signal(SIGINT, on_signal);
|
||||||
signal(SIGTERM, on_signal);
|
signal(SIGTERM, on_signal);
|
||||||
|
|
||||||
uint64_t start = mono_ns(), last_status = start;
|
uint64_t start = mono_ns(), last_status = start, last_want = 0;
|
||||||
int rc = 0;
|
int rc = 0;
|
||||||
|
|
||||||
while (!stop) {
|
while (!stop) {
|
||||||
@@ -1106,6 +1236,11 @@ int main(int argc, char **argv)
|
|||||||
uint64_t now = mono_ns();
|
uint64_t now = mono_ns();
|
||||||
__atomic_store_n(&ring->heartbeat_ns, now, __ATOMIC_RELEASE);
|
__atomic_store_n(&ring->heartbeat_ns, now, __ATOMIC_RELEASE);
|
||||||
|
|
||||||
|
if (opt_with_color && now - last_want >= 500000000ull) {
|
||||||
|
color_want();
|
||||||
|
last_want = now;
|
||||||
|
}
|
||||||
|
|
||||||
struct pollfd pf = { .fd = pidfd, .events = POLLIN };
|
struct pollfd pf = { .fd = pidfd, .events = POLLIN };
|
||||||
|
|
||||||
if (poll(&pf, 1, 0) > 0) {
|
if (poll(&pf, 1, 0) > 0) {
|
||||||
|
|||||||
+5
-1
@@ -55,7 +55,11 @@ typedef struct {
|
|||||||
uint64_t published; /* frames published */
|
uint64_t published; /* frames published */
|
||||||
uint64_t dropped; /* dark, stale or torn frames not published */
|
uint64_t dropped; /* dark, stale or torn frames not published */
|
||||||
uint32_t flags; /* FH_CAM_* */
|
uint32_t flags; /* FH_CAM_* */
|
||||||
uint8_t reserved[28];
|
float dark_mean; /* mono: mean luma of its latest near-black */
|
||||||
|
/* frame (a short fixed exposure, so it follows */
|
||||||
|
/* the room's IR light, sunlight above all); */
|
||||||
|
/* 0 before the first */
|
||||||
|
uint8_t reserved[24];
|
||||||
} fh_ring_cam_t; /* 160 bytes */
|
} fh_ring_cam_t; /* 160 bytes */
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ PartOf=steamvr.service
|
|||||||
# On the host: the dev container can't reach XRService. Its file capabilities (set by
|
# On the host: the dev container can't reach XRService. Its file capabilities (set by
|
||||||
# hands/run.sh install) let it borrow the buffers; it drops them once set up. It exits when
|
# hands/run.sh install) let it borrow the buffers; it drops them once set up. It exits when
|
||||||
# XRService restarts, and comes back to attach to the new one.
|
# XRService restarts, and comes back to attach to the new one.
|
||||||
ExecStart=@REPO@/hands/build/ft-camd --status 60
|
# The colour cameras run at 2 fps (their brightness picks the tracking cameras) until
|
||||||
|
# ft-hands asks for more; HANDS_CAMERAS=mono leaves them out.
|
||||||
|
ExecStart=@REPO@/hands/build/ft-camd --with-color --status 60
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
TimeoutStopSec=5
|
TimeoutStopSec=5
|
||||||
|
|||||||
Reference in new issue
Block a user