/* SPDX-License-Identifier: BSD-2-Clause */ /* * chardev_client.c — kernel-bridge client for the daedalus-v4l2 daemon. */ #include "chardev_client.h" #include "decoder.h" #include "dmabuf_capture.h" #include "ffmpeg_loader.h" #include "log.h" #include #include #include #include #include #include #define CHARDEV_PATH "/dev/daedalus-v4l2" #define CHARDEV_READ_BUFSZ (sizeof(struct daedalus_msg_hdr) + \ DAEDALUS_PROTO_MAX_PAYLOAD) int chardev_client_open(struct chardev_client *cli, struct ffmpeg_loader *loader, volatile sig_atomic_t *stop_flag) { int fd, rc; memset(cli, 0, sizeof(*cli)); cli->fd = -1; cli->loader = loader; cli->stop_flag = stop_flag; fd = open(CHARDEV_PATH, O_RDWR | O_CLOEXEC); if (fd < 0) { rc = -errno; log_err("open(%s): %s", CHARDEV_PATH, strerror(errno)); return rc; } cli->decoder = calloc(1, sizeof(*cli->decoder)); if (!cli->decoder) { close(fd); return -ENOMEM; } rc = daedalus_decoder_init(cli->decoder, loader); if (rc < 0) { free(cli->decoder); cli->decoder = NULL; close(fd); log_err("daedalus_decoder_init: %d", rc); return rc; } cli->fd = fd; log_info("chardev: opened %s (fd %d)", CHARDEV_PATH, fd); return 0; } void chardev_client_close(struct chardev_client *cli) { if (!cli) return; if (cli->decoder) { daedalus_decoder_cleanup(cli->decoder); free(cli->decoder); cli->decoder = NULL; } if (cli->fd >= 0) { close(cli->fd); cli->fd = -1; } } static int send_response(struct chardev_client *cli, uint32_t type, uint32_t cookie, const void *payload, size_t payload_len) { struct daedalus_msg_hdr hdr; int rc; if (payload_len > DAEDALUS_PROTO_MAX_PAYLOAD) return -EMSGSIZE; hdr.magic = DAEDALUS_PROTO_MAGIC; hdr.version = DAEDALUS_PROTO_VERSION; hdr.type = type; hdr.cookie = cookie; hdr.payload_len = (uint32_t) payload_len; hdr.reserved = 0; /* * The kernel's write() path validates count == sizeof(hdr) * + hdr.payload_len in a single call, and only implements * .write (not .write_iter), so a writev() lands as -EINVAL. * Marshal the message into a single buffer and write() it. * * Response payloads are small (struct daedalus_resp_frame = * 36 bytes; PONG echoes <= 64 KiB). A short-lived heap * allocation per response is fine; per-loop reuse can come * later if profiling demands it. */ { size_t total = sizeof(hdr) + payload_len; uint8_t *out = malloc(total); ssize_t n; if (!out) return -ENOMEM; memcpy(out, &hdr, sizeof(hdr)); if (payload_len) memcpy(out + sizeof(hdr), payload, payload_len); for (;;) { n = write(cli->fd, out, total); if (n >= 0) { if ((size_t) n != total) { log_err("chardev: short write %zd != %zu", n, total); rc = -EIO; } else { rc = 0; } break; } if (errno == EINTR) continue; rc = -errno; log_err("chardev: write: %s", strerror(errno)); break; } free(out); } return rc; } static int handle_req_decode(struct chardev_client *cli, const struct daedalus_msg_hdr *hdr, const uint8_t *payload) { struct daedalus_req_decode req; struct daedalus_resp_frame resp; struct daedalus_capture_planes planes; const struct daedalus_h264_meta *h264_meta = NULL; size_t meta_off, meta_len = 0; int rc; int decoded = 0; if (hdr->payload_len < sizeof(req)) { log_err("REQ_DECODE cookie=%u: payload too short %u < %zu", hdr->cookie, hdr->payload_len, sizeof(req)); memset(&resp, 0, sizeof(resp)); resp.status = DAEDALUS_DECODE_ERR_RECV; return send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie, &resp, sizeof(resp)); } memcpy(&req, payload, sizeof(req)); /* Optional H.264 meta block follows req when the flag is set; * bitstream comes after meta. */ if (req.flags & DAEDALUS_REQ_FLAG_H264_META) meta_len = sizeof(struct daedalus_h264_meta); meta_off = sizeof(req); if ((size_t) req.bitstream_len + sizeof(req) + meta_len != hdr->payload_len) { log_err("REQ_DECODE cookie=%u: bitstream_len %u + meta %zu inconsistent with payload_len %u", hdr->cookie, req.bitstream_len, meta_len, hdr->payload_len); memset(&resp, 0, sizeof(resp)); resp.status = DAEDALUS_DECODE_ERR_RECV; return send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie, &resp, sizeof(resp)); } if (meta_len) h264_meta = (const struct daedalus_h264_meta *) (payload + meta_off); log_info("REQ_DECODE cookie=%u codec=%u bitstream=%u bytes meta=%s capture=%ux%u %u planes", hdr->cookie, req.codec_id, req.bitstream_len, h264_meta ? "h264" : "none", req.capture_width, req.capture_height, req.capture_num_planes); /* * Open dmabuf-fds for every CAPTURE plane and mmap them. * If this fails we still attempt the decode (so the kernel * gets a structured error response) — but we pass NULL * planes so pixels aren't written anywhere. */ rc = daedalus_capture_planes_open(cli->fd, hdr->cookie, &req, &planes); if (rc < 0) { log_warn("REQ_DECODE cookie=%u: GET_DMABUF/mmap failed (%d); decode metadata-only", hdr->cookie, rc); /* planes is already zeroed by capture_planes_open */ } rc = daedalus_decoder_run_request(cli->decoder, &req, payload + meta_off + meta_len, h264_meta, &resp, planes.nr ? &planes : NULL); decoded = (rc >= 0); daedalus_capture_planes_close(&planes); if (!decoded) return rc; /* * RESP_FRAME is metadata-only in Phase 8.6 — pixels already * live in the V4L2 client's CAPTURE buffer via the dmabuf * the daemon wrote to in pack_nv12_to_planes. */ return send_response(cli, DAEDALUS_MSG_RESP_FRAME, hdr->cookie, &resp, sizeof(resp)); } static int handle_ping(struct chardev_client *cli, const struct daedalus_msg_hdr *hdr, const uint8_t *payload) { log_info("PING cookie=%u plen=%u — echoing PONG", hdr->cookie, hdr->payload_len); return send_response(cli, DAEDALUS_MSG_PONG, hdr->cookie, payload, hdr->payload_len); } static int handle_one_message(struct chardev_client *cli, uint8_t *buf) { struct daedalus_msg_hdr hdr; ssize_t n; /* * The kernel chardev delivers exactly one message per * read(). Pass a buffer that can hold any legal message. */ for (;;) { n = read(cli->fd, buf, CHARDEV_READ_BUFSZ); if (n >= 0) break; if (errno == EINTR) { if (*cli->stop_flag) return 0; continue; } log_err("chardev: read: %s", strerror(errno)); return -errno; } if (n == 0) return -EIO; /* EOF / device unplugged */ if ((size_t) n < sizeof(hdr)) { log_err("chardev: short read %zd < hdr", n); return -EBADMSG; } memcpy(&hdr, buf, sizeof(hdr)); if (hdr.magic != DAEDALUS_PROTO_MAGIC) { log_err("chardev: bad magic 0x%08x", hdr.magic); return -EBADMSG; } if (hdr.version != DAEDALUS_PROTO_VERSION) { log_err("chardev: unsupported version %u", hdr.version); return -EPROTO; } if ((size_t) n != sizeof(hdr) + hdr.payload_len) { log_err("chardev: framing mismatch n=%zd expected %zu", n, sizeof(hdr) + hdr.payload_len); return -EBADMSG; } switch (hdr.type) { case DAEDALUS_MSG_PING: return handle_ping(cli, &hdr, buf + sizeof(hdr)); case DAEDALUS_MSG_REQ_DECODE: return handle_req_decode(cli, &hdr, buf + sizeof(hdr)); default: log_warn("chardev: unknown request type 0x%08x cookie=%u", hdr.type, hdr.cookie); return 0; /* skip, don't bail the loop */ } } int chardev_client_run(struct chardev_client *cli) { uint8_t *buf; int rc = 0; buf = malloc(CHARDEV_READ_BUFSZ); if (!buf) return -ENOMEM; log_info("daemon loop started; waiting for kernel requests"); while (!*cli->stop_flag) { rc = handle_one_message(cli, buf); if (rc < 0) { if (rc == -EINTR) continue; log_err("chardev: handle_one_message: %d", rc); break; } } log_info("daemon loop exiting (stop=%d rc=%d)", *cli->stop_flag, rc); free(buf); return rc; }