Files
DeeJanuz--frametop/hands/camd/xrcams.c
T
DeeJanuz 98bd6ce21c Hand recorder: headsets without the colour module
Without the Arcturus module, XRService runs the side cameras through the
ISP ("ISP enabled for tracking cameras (main VFE available)"): NV12 on
vfe0 and vfe1, pitch 1152. ft-camd published only grey cameras, so it
dropped them, and the recorder stopped at "ft-camd publishes only 2 of 4
mono cameras" whatever was restarted.

- ft-camd reads a tracking camera's NV12 luma as its grey image.
- ft-hands and check_sides name the cameras by XRService's numbering in
  its log (TrackingCameraInit index 0-3), not by capture pipe, which
  moves with the module; a camera whose size isn't its calibration's is
  left out. sides.json says which device each camera was.
- camcheck reports the camera map, the ISP routing and which cameras
  ft-camd is missing; the recorder says so instead of "restart it",
  restarts its own ft-camd once when that's the only problem, and keeps
  the map in session.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4db5266cf7)
2026-10-05 13:55:17 -06:00

801 lines
21 KiB
C

/*
* xrcams - find the headset cameras and the DMA-BUF queues XRService feeds them.
*
* Adapted from framecap.c in FrameEyeCameraFeed (vendor/FrameEyeCameraFeed),
* MIT License, Copyright (c) 2026 Curtis English. See LICENSE.FrameEyeCameraFeed.
*
* Everything is discovered rather than hardcoded:
* - XRService is found by scanning /proc for its cmdline.
* - The V4L2 nodes and sensor subdevs it holds open come from /proc/<pid>/fd.
* - Each node's geometry comes from VIDIOC_G_FMT on our own handle.
* - Each node is traced back to its sensor through MEDIA_IOC_G_TOPOLOGY.
* - Buffers are split into queues by allocation order: XRService opens a
* sensor subdev, then allocates that camera's buffers.
*/
#define _GNU_SOURCE
#include "xrcams.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <unistd.h>
#include <linux/media.h>
#ifndef MEDIA_ENT_F_CAM_SENSOR
#define MEDIA_ENT_F_CAM_SENSOR 0x00020001
#endif
#define MAX_FDENTS 4096
#define MAX_TOPOS 8
enum fdkind { FD_DMABUF, FD_SUBDEV_SENSOR, FD_VIDEO };
typedef struct {
int xfd;
enum fdkind kind;
size_t size;
unsigned long ino;
char sensor[XR_SENSOR_LEN];
char path[64];
} fdent_t;
typedef struct {
struct media_v2_entity *ents;
struct media_v2_interface *intfs;
struct media_v2_pad *pads;
struct media_v2_link *links;
__u32 nents, nintfs, npads, nlinks;
} topo_t;
static fdent_t fdents[MAX_FDENTS];
static int nfdents;
static topo_t topos[MAX_TOPOS];
static int ntopos;
static void set_err(char *err, size_t n, const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
vsnprintf(err, n, fmt, ap);
va_end(ap);
}
void xr_slugify(const char *in, char *out, size_t n)
{
size_t i = 0;
for (; in[i] && i + 1 < n; i++)
out[i] = (in[i] == ' ' || in[i] == '/') ? '_' : in[i];
out[i] = 0;
}
/* --------------------------------------------------- media graph handling */
static void topo_free_all(void)
{
for (int i = 0; i < ntopos; i++) {
free(topos[i].ents);
free(topos[i].intfs);
free(topos[i].pads);
free(topos[i].links);
}
ntopos = 0;
}
static void topo_load_all(void)
{
for (int mi = 0; mi < MAX_TOPOS; mi++) {
char mpath[32];
snprintf(mpath, sizeof(mpath), "/dev/media%d", mi);
int mfd = open(mpath, O_RDWR | O_CLOEXEC);
if (mfd < 0)
continue;
struct media_v2_topology t;
memset(&t, 0, sizeof(t));
if (ioctl(mfd, MEDIA_IOC_G_TOPOLOGY, &t) < 0) {
close(mfd);
continue;
}
topo_t *o = &topos[ntopos];
memset(o, 0, sizeof(*o));
o->nents = t.num_entities;
o->nintfs = t.num_interfaces;
o->npads = t.num_pads;
o->nlinks = t.num_links;
o->ents = calloc(o->nents ? o->nents : 1, sizeof(*o->ents));
o->intfs = calloc(o->nintfs ? o->nintfs : 1, sizeof(*o->intfs));
o->pads = calloc(o->npads ? o->npads : 1, sizeof(*o->pads));
o->links = calloc(o->nlinks ? o->nlinks : 1, sizeof(*o->links));
t.ptr_entities = (__u64)(uintptr_t)o->ents;
t.ptr_interfaces = (__u64)(uintptr_t)o->intfs;
t.ptr_pads = (__u64)(uintptr_t)o->pads;
t.ptr_links = (__u64)(uintptr_t)o->links;
bool ok = o->ents && o->intfs && o->pads && o->links &&
ioctl(mfd, MEDIA_IOC_G_TOPOLOGY, &t) == 0;
close(mfd);
if (!ok) {
free(o->ents); free(o->intfs); free(o->pads); free(o->links);
continue;
}
ntopos++;
}
}
static struct media_v2_entity *topo_entity(topo_t *t, __u32 id)
{
for (__u32 i = 0; i < t->nents; i++)
if (t->ents[i].id == id)
return &t->ents[i];
return NULL;
}
static struct media_v2_pad *topo_pad(topo_t *t, __u32 id)
{
for (__u32 i = 0; i < t->npads; i++)
if (t->pads[i].id == id)
return &t->pads[i];
return NULL;
}
static __u32 topo_entity_for_devnode(topo_t *t, dev_t rdev)
{
__u32 intf_id = 0;
for (__u32 i = 0; i < t->nintfs; i++)
if (t->intfs[i].devnode.major == major(rdev) &&
t->intfs[i].devnode.minor == minor(rdev)) {
intf_id = t->intfs[i].id;
break;
}
if (!intf_id)
return 0;
for (__u32 i = 0; i < t->nlinks; i++)
if ((t->links[i].flags & MEDIA_LNK_FL_LINK_TYPE) == MEDIA_LNK_FL_INTERFACE_LINK &&
t->links[i].source_id == intf_id)
return t->links[i].sink_id;
return 0;
}
/*
* Walk upstream across enabled data links until a sensor is reached. A CSIPHY
* carries two sensors on separate (sink, source) pad pairs, so re-enter on the
* sink pad paired with the source pad we left through.
*/
static bool topo_walk_to_sensor(topo_t *t, __u32 ent_id, char *out, size_t outn)
{
int exit_pad_index = -1;
for (int hop = 0; hop < 32 && ent_id; hop++) {
struct media_v2_entity *e = topo_entity(t, ent_id);
if (!e)
return false;
if (e->function == MEDIA_ENT_F_CAM_SENSOR) {
snprintf(out, outn, "%s", e->name);
return true;
}
__u32 first_sink = 0, paired = 0;
int nsinks = 0;
for (__u32 p = 0; p < t->npads; p++) {
if (t->pads[p].entity_id != ent_id || !(t->pads[p].flags & MEDIA_PAD_FL_SINK))
continue;
nsinks++;
if (!first_sink)
first_sink = t->pads[p].id;
if (exit_pad_index >= 1 && (int)t->pads[p].index == exit_pad_index - 1)
paired = t->pads[p].id;
}
__u32 sink_pad = (nsinks == 1) ? first_sink : (paired ? paired : first_sink);
if (!sink_pad)
return false;
__u32 src_pad = 0;
for (__u32 i = 0; i < t->nlinks; i++) {
if ((t->links[i].flags & MEDIA_LNK_FL_LINK_TYPE) != MEDIA_LNK_FL_DATA_LINK)
continue;
if (!(t->links[i].flags & MEDIA_LNK_FL_ENABLED))
continue;
if (t->links[i].sink_id == sink_pad) {
src_pad = t->links[i].source_id;
break;
}
}
struct media_v2_pad *sp = src_pad ? topo_pad(t, src_pad) : NULL;
if (!sp)
return false;
ent_id = sp->entity_id;
exit_pad_index = (int)sp->index;
}
return false;
}
static bool sensor_for_video(dev_t rdev, char *out, size_t outn)
{
for (int i = 0; i < ntopos; i++) {
__u32 ent = topo_entity_for_devnode(&topos[i], rdev);
if (ent && topo_walk_to_sensor(&topos[i], ent, out, outn))
return true;
}
return false;
}
static bool sensor_for_subdev(dev_t rdev, char *out, size_t outn)
{
for (int i = 0; i < ntopos; i++) {
__u32 id = topo_entity_for_devnode(&topos[i], rdev);
struct media_v2_entity *e = id ? topo_entity(&topos[i], id) : NULL;
if (e && e->function == MEDIA_ENT_F_CAM_SENSOR) {
snprintf(out, outn, "%s", e->name);
return true;
}
}
return false;
}
static const char *role_for_sensor(const char *sensor)
{
if (strstr(sensor, "og01a1b"))
return "tracking"; /* 1056x1024 side fisheye */
if (strstr(sensor, "og0ve10"))
return "tracking"; /* 640x480 upper */
if (strstr(sensor, "imx616"))
return "passthrough"; /* 2464x2464 Arcturus color */
return "unknown";
}
/* ------------------------------------------------- XRService / proc scan */
static pid_t find_process(const char *needle)
{
DIR *d = opendir("/proc");
if (!d)
return 0;
struct dirent *e;
pid_t found = 0;
while ((e = readdir(d))) {
if (e->d_name[0] < '0' || e->d_name[0] > '9')
continue;
char path[288];
snprintf(path, sizeof(path), "/proc/%s/cmdline", e->d_name);
FILE *f = fopen(path, "rb");
if (!f)
continue;
char buf[512] = {0};
size_t got = fread(buf, 1, sizeof(buf) - 1, f);
fclose(f);
if (got == 0)
continue;
const char *base = strrchr(buf, '/');
base = base ? base + 1 : buf;
if (strstr(base, needle)) {
found = (pid_t)atoi(e->d_name);
break;
}
}
closedir(d);
return found;
}
static bool read_dmabuf_size(pid_t pid, int fd, size_t *size, unsigned long *ino)
{
char path[64];
snprintf(path, sizeof(path), "/proc/%d/fdinfo/%d", pid, fd);
FILE *f = fopen(path, "r");
if (!f)
return false;
bool have = false;
char line[256];
*ino = 0;
while (fgets(line, sizeof(line), f)) {
unsigned long long v;
if (sscanf(line, "size: %llu", &v) == 1) {
*size = (size_t)v;
have = true;
} else if (sscanf(line, "ino: %llu", &v) == 1) {
*ino = (unsigned long)v;
}
}
fclose(f);
return have;
}
static int cmp_int(const void *a, const void *b)
{
return *(const int *)a - *(const int *)b;
}
static bool scan_xr_fds(pid_t pid, char *err, size_t errn)
{
char dirpath[64];
snprintf(dirpath, sizeof(dirpath), "/proc/%d/fd", pid);
DIR *d = opendir(dirpath);
if (!d) {
set_err(err, errn, "opendir(%s): %s (are you root?)", dirpath, strerror(errno));
return false;
}
static int fds[8192];
int nfds = 0;
struct dirent *e;
while ((e = readdir(d)) && nfds < (int)(sizeof(fds) / sizeof(fds[0])))
if (e->d_name[0] >= '0' && e->d_name[0] <= '9')
fds[nfds++] = atoi(e->d_name);
closedir(d);
qsort(fds, nfds, sizeof(int), cmp_int);
nfdents = 0;
for (int i = 0; i < nfds && nfdents < MAX_FDENTS; i++) {
char link[64], target[256];
snprintf(link, sizeof(link), "/proc/%d/fd/%d", pid, fds[i]);
ssize_t n = readlink(link, target, sizeof(target) - 1);
if (n < 0)
continue;
target[n] = 0;
fdent_t ent;
memset(&ent, 0, sizeof(ent));
ent.xfd = fds[i];
if (strstr(target, "dmabuf")) {
if (!read_dmabuf_size(pid, fds[i], &ent.size, &ent.ino))
continue;
ent.kind = FD_DMABUF;
} else if (strncmp(target, "/dev/video", 10) == 0) {
ent.kind = FD_VIDEO;
snprintf(ent.path, sizeof(ent.path), "%.63s", target);
} else if (strncmp(target, "/dev/v4l-subdev", 15) == 0) {
struct stat st;
if (stat(target, &st) < 0 || !sensor_for_subdev(st.st_rdev, ent.sensor, sizeof(ent.sensor)))
continue;
ent.kind = FD_SUBDEV_SENSOR;
} else {
continue;
}
fdents[nfdents++] = ent;
}
return true;
}
/* ------------------------------------------------------ camera discovery */
static void probe_cameras(xr_state_t *st)
{
int seen[64];
int nseen = 0;
for (int i = 0; i < nfdents; i++) {
if (fdents[i].kind != FD_VIDEO)
continue;
const char *path = fdents[i].path;
int node = atoi(path + 10);
bool dup = false;
for (int k = 0; k < nseen; k++)
if (seen[k] == node)
dup = true;
if (dup || st->ncameras >= XR_MAX_CAMERAS || nseen >= 64)
continue;
seen[nseen++] = node;
int fd = open(path, O_RDWR | O_CLOEXEC);
if (fd < 0)
continue;
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
xr_camera_t *c = &st->cameras[st->ncameras];
memset(c, 0, sizeof(*c));
if (ioctl(fd, VIDIOC_G_FMT, &fmt) == 0) {
c->width = fmt.fmt.pix_mp.width;
c->height = fmt.fmt.pix_mp.height;
c->pixfmt = fmt.fmt.pix_mp.pixelformat;
c->nplanes = fmt.fmt.pix_mp.num_planes;
c->bytesperline = fmt.fmt.pix_mp.plane_fmt[0].bytesperline;
for (unsigned p = 0; p < c->nplanes && p < VIDEO_MAX_PLANES; p++)
c->planesize[p] = fmt.fmt.pix_mp.plane_fmt[p].sizeimage;
} else {
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
if (ioctl(fd, VIDIOC_G_FMT, &fmt) < 0) {
close(fd);
continue;
}
c->width = fmt.fmt.pix.width;
c->height = fmt.fmt.pix.height;
c->pixfmt = fmt.fmt.pix.pixelformat;
c->nplanes = 1;
c->bytesperline = fmt.fmt.pix.bytesperline;
c->planesize[0] = fmt.fmt.pix.sizeimage;
}
struct stat sb;
if (fstat(fd, &sb) == 0) {
c->minor = minor(sb.st_rdev);
sensor_for_video(sb.st_rdev, c->sensor, sizeof(c->sensor));
}
close(fd);
if (!c->sensor[0])
snprintf(c->sensor, sizeof(c->sensor), "unknown");
c->node = node;
snprintf(c->path, sizeof(c->path), "%s", path);
c->role = role_for_sensor(c->sensor);
st->ncameras++;
}
}
/*
* qcom-camss can report bytesperline as the visible width while the VFE
* writes a larger aligned pitch. sizeimage is right, so derive the pitch.
* For NV12, plane 0 normally holds the chroma rows after the luma (the side
* cameras through the ISP: 1056 wide, 1152 bytes a row); if it's too small for
* that, it holds the luma alone.
*/
unsigned xr_camera_stride(const xr_camera_t *c)
{
if (!c->height || !c->planesize[0])
return c->bytesperline ? c->bytesperline : c->width;
bool yuv = c->pixfmt == V4L2_PIX_FMT_NV12 || c->pixfmt == V4L2_PIX_FMT_NV21;
unsigned s = (unsigned)((double)c->planesize[0] / ((double)c->height * (yuv ? 1.5 : 1.0)));
if (s >= c->width && s <= c->width * 4)
return s;
s = (unsigned)(c->planesize[0] / c->height);
if (yuv && s >= c->width && s <= c->width * 4)
return s;
return c->bytesperline ? c->bytesperline : c->width;
}
/*
* The Arcturus color cameras (arcimx616) claim 2464x2464 NV12, but measured on
* 2026-09-28 their plane 0 holds 10-bit MIPI-packed YUV 4:2:0: 2464 luma rows
* then 1232 rows of interleaved UV, each row 2464 packed pixels (3080 bytes)
* padded to a 256-byte pitch (3328). Only the first 1972 pixels of a row carry
* image; the rest are zero.
*/
#define IMX616_VALID_WIDTH 1972
void xr_camera_layout(const xr_camera_t *c, xr_layout_t *l)
{
memset(l, 0, sizeof(*l));
l->height = c->height;
if (c->pixfmt == V4L2_PIX_FMT_NV12 && strstr(c->sensor, "imx616")) {
unsigned packed = (c->width * 5 + 3) / 4;
l->fmt = XR_FMT_YUV420_10P;
l->pitch = (packed + 255) & ~255u;
l->rows = c->height + c->height / 2;
l->width = IMX616_VALID_WIDTH < c->width ? IMX616_VALID_WIDTH : c->width;
return;
}
l->pitch = xr_camera_stride(c);
l->width = c->width < l->pitch ? c->width : l->pitch;
/*
* Without the colour module, XRService runs the side cameras through the
* ISP ("ISP enabled for tracking cameras (main VFE available)" in its log),
* and they come out NV12. They're mono sensors, so the luma is the image.
*/
bool yuv = c->pixfmt == V4L2_PIX_FMT_NV12 || c->pixfmt == V4L2_PIX_FMT_NV21;
if (yuv && c->role && !strcmp(c->role, "tracking")) {
l->fmt = XR_FMT_GREY8;
l->rows = c->height;
return;
}
if (yuv) {
l->fmt = XR_FMT_NV12;
l->rows = c->height + c->height / 2;
} else {
l->fmt = XR_FMT_GREY8;
l->rows = c->height;
}
}
const char *xr_fmt_name(xr_fmt_t f)
{
switch (f) {
case XR_FMT_GREY8: return "grey8";
case XR_FMT_NV12: return "nv12";
case XR_FMT_YUV420_10P: return "yuv420_10p";
}
return "?";
}
/* ------------------------------------------------------- buffer grouping */
/*
* XRService allocates one udmabuf per plane, plane 0 then plane 1, a whole
* queue at a time right after opening the sensor's subdev. Plane 1 matches
* VIDIOC_G_FMT exactly; plane 0 has slack, so it is matched with >=.
*/
static void build_groups(xr_state_t *st)
{
char current_sensor[XR_SENSOR_LEN] = "";
for (int i = 0; i < nfdents; i++) {
if (fdents[i].kind == FD_SUBDEV_SENSOR) {
snprintf(current_sensor, sizeof(current_sensor), "%s", fdents[i].sensor);
continue;
}
if (fdents[i].kind != FD_DMABUF)
continue;
if (i + 1 >= nfdents || fdents[i + 1].kind != FD_DMABUF)
continue;
size_t s0 = fdents[i].size;
size_t s1 = fdents[i + 1].size;
bool match = false;
for (int c = 0; c < st->ncameras; c++) {
xr_camera_t *cam = &st->cameras[c];
if (cam->nplanes >= 2 && s1 == cam->planesize[1] && s0 >= cam->planesize[0]) {
match = true;
break;
}
}
if (!match)
continue;
xr_group_t *g = NULL;
if (st->ngroups > 0) {
xr_group_t *last = &st->groups[st->ngroups - 1];
if (last->planesize[0] == s0 && last->planesize[1] == s1 &&
!strcmp(last->sensor, current_sensor))
g = last;
}
if (!g) {
if (st->ngroups >= XR_MAX_GROUPS)
break;
g = &st->groups[st->ngroups++];
memset(g, 0, sizeof(*g));
g->planesize[0] = s0;
g->planesize[1] = s1;
snprintf(g->sensor, sizeof(g->sensor), "%s", current_sensor);
}
if (g->nbufs < XR_MAX_RUNBUFS) {
g->buf[g->nbufs].xfd = fdents[i].xfd;
g->buf[g->nbufs].xfd1 = fdents[i + 1].xfd;
g->buf[g->nbufs].size = s0;
g->buf[g->nbufs].size1 = s1;
g->nbufs++;
}
i++; /* consume the plane 1 descriptor */
}
int keep = 0;
for (int i = 0; i < st->ngroups; i++)
if (st->groups[i].nbufs >= 4)
st->groups[keep++] = st->groups[i];
st->ngroups = keep;
/*
* Bind each run to a camera. The sensor marker alone can be wrong: XRService
* sometimes opens another sensor's subdev (e.g. the idle color camera)
* between an upper camera's subdev and its buffers, and two upper cameras
* can resolve to the same sensor name. So a marker match must also fit the
* camera's plane sizes, and each camera takes at most one run.
*/
for (int pass = 0; pass < 2; pass++)
for (int i = 0; i < st->ngroups; i++) {
xr_group_t *g = &st->groups[i];
for (int c = 0; c < st->ncameras && !g->cam; c++) {
xr_camera_t *cam = &st->cameras[c];
if (pass == 0 && (!g->sensor[0] || strcmp(cam->sensor, g->sensor)))
continue;
if (cam->nplanes < 2 || g->planesize[1] != cam->planesize[1] ||
g->planesize[0] < cam->planesize[0])
continue;
bool taken = false;
for (int k = 0; k < st->ngroups; k++)
if (k != i && st->groups[k].cam == cam)
taken = true;
if (!taken)
g->cam = cam;
}
}
}
bool xr_discover(xr_state_t *st, const char *process, char *err, size_t errn)
{
memset(st, 0, sizeof(*st));
st->pid = find_process(process);
if (!st->pid) {
set_err(err, errn, "%s is not running; start SteamVR on the headset first", process);
return false;
}
topo_load_all();
bool ok = scan_xr_fds(st->pid, err, errn);
if (ok) {
probe_cameras(st);
build_groups(st);
}
topo_free_all();
return ok;
}
void xr_print(const xr_state_t *st, FILE *f)
{
fprintf(f, "XRService pid %d\n", st->pid);
for (int i = 0; i < st->ncameras; i++) {
const xr_camera_t *c = &st->cameras[i];
char fcc[5] = {
(char)(c->pixfmt & 0xff), (char)((c->pixfmt >> 8) & 0xff),
(char)((c->pixfmt >> 16) & 0xff), (char)((c->pixfmt >> 24) & 0xff), 0
};
fprintf(f, " camera %-12s minor %-3u %-16s %ux%u %s pitch %u planes %zu %zu role=%s\n",
c->path, c->minor, c->sensor, c->width, c->height, fcc,
xr_camera_stride(c), c->planesize[0], c->planesize[1], c->role);
}
for (int i = 0; i < st->ngroups; i++) {
const xr_group_t *g = &st->groups[i];
fprintf(f, " queue %d: %d buffers plane0=%zu plane1=%zu fds %d..%d sensor '%s' -> %s\n",
i, g->nbufs, g->planesize[0], g->planesize[1],
g->buf[0].xfd, g->buf[g->nbufs - 1].xfd1, g->sensor,
g->cam ? g->cam->path : "(unbound)");
}
}