xnux.eu - site map - news

Interacting with cameras on PinePhone (from userspace)

Here you can find some sample code for interacting with cameras on PinePhone and TBS A711 tablet.

It will not compile by itself. It's provided as is in hopes it may help someone with understanding how to optimally use the cameras and HW acceleration of rotation on Allwinner A SoCs.

/*
 * PinePhone camera sample code
 *
 * Copyright (C) 2020  Ondřej Jirman <megous@megous.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/*
 * Camera interface for PP/TBS
 */

#pragma once

#include <stdint.h>
#include <stdbool.h>

#include <linux/videodev2.h>
#include <linux/v4l2-subdev.h>
#include <linux/media.h>

enum {
        BACK_CAM = 0,
        FRONT_CAM,
};

struct cam_app_setup
{
        int cam_location;

        int width;
        int height;
        int framerate;
        int rotate;
        bool hflip;
        bool vflip;

        uint32_t cam_bus_fmt;
        uint32_t cam_pix_fmt;
        uint32_t rot_out_fmt;

        int cam_buffers; // how many buffers to allocate
};

struct cam_app_buf
{
        uint8_t* start;
        size_t length;
        int index;

        int dmabuf_fd;
};

struct cam_control_menu_item {
        int index;
        char name[32];
        int64_t value;
};

struct cam_control {
        uint32_t id;
        uint32_t type;
        uint8_t disabled, grabbed, read_only, write_only, _volatile;
        int64_t min;
        int64_t max;
        int64_t step;
        int64_t def_val;
        struct cam_control_menu_item menu_items[64];
        int menu_items_count;
        int64_t val;
};

struct cam_app_subdev
{
        const char* name;
        int location;
        int fd;

        // media info for setting up links
        struct media_pad_desc pad;
};

struct cam_dev
{
        struct loop* loop;
        struct loop_listener csi_listener;
        struct loop_listener rot_listener;
        int media_fd;

        struct cam_app_subdev sd[2];

        struct cam_app_setup current_setup;

        int n_csi_bufs;
        struct cam_app_buf csi_bufs[8];
        struct v4l2_pix_format csi_out_pix_fmt;

        int n_rot_bufs;
        struct cam_app_buf rot_bufs[8];
        struct v4l2_pix_format rot_in_pix_fmt;
        struct v4l2_pix_format rot_out_pix_fmt;

        void (*on_buffer_ready)(struct cam_app_buf* buf);
        void (*on_request_release)(void);

        // media device info for csi ctl
        struct media_pad_desc csi_pad;
};

extern struct cam_dev cam_dev;

bool cam_app_init(struct loop* loop);
void cam_app_fini(void);
bool cam_app_switch_camera(int loc);
bool cam_app_setup_camera_pipeline(struct cam_app_setup* setup);
bool cam_app_return_buffer(int idx);
bool cam_app_start_stream(void);
bool cam_app_stop_stream(void);

bool cam_get_control(unsigned int id, struct cam_control* cc);
bool cam_set_control(unsigned int id, int64_t val);

void cam_sd_set_reg(uint16_t addr, uint8_t val);
uint8_t cam_sd_get_reg(uint16_t addr);
uint16_t cam_sd_get_reg16(uint16_t addr);

void cam_dump_info(void);
void cam_set_fps(int fps);
/*
 * PinePhone camera sample code
 *
 * Copyright (C) 2020  Ondřej Jirman <megous@megous.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <errno.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <inttypes.h>

#include <cbase/cbase.h>

#include "cam_dev.h"

struct cam_dev cam_dev = {};

static struct {
        const char* name;
        int location;
} cam_names[] = {
        {"hm5065", BACK_CAM},
        {"ov5640", BACK_CAM},
        {"gc2145", FRONT_CAM},
};

static bool read_sysfs_file(const char* path, char* buf, size_t buf_len)
{
        int fd;

        fd = open(path, O_RDONLY);
        if (fd < 0)
                return false;

        ssize_t read_bytes = read(fd, buf, buf_len);
        if (read_bytes > 0) {
                buf[read_bytes] = 0;

                if (read_bytes > 1 && buf[read_bytes - 1] == '\n')
                        buf[read_bytes - 1] = 0;
        }

        close(fd);
        return read_bytes > 0;
}

static bool set_video_format(int fd, int buf_type,
                             int width, int height, uint32_t format,
                             uint32_t bpl,
                             struct v4l2_pix_format* fmt_out,
                             const char* dev_name)
{
        int ret;
        struct v4l2_format fmt = {
                .type = buf_type,
                .fmt.pix.width = width,
                .fmt.pix.height = height,
                .fmt.pix.pixelformat = format,
                .fmt.pix.field = V4L2_FIELD_ANY,
                .fmt.pix.bytesperline = bpl,
        };

        ret = ioctl(fd, VIDIOC_S_FMT, &fmt);
        syscall_error(ret < 0, "VIDIOC_S_FMT failed");

        if (width != fmt.fmt.pix.width || height != fmt.fmt.pix.height)
                error("We need %dx%d, but %s insists on %dx%d",
                      width, height, dev_name,
                      fmt.fmt.pix.width, fmt.fmt.pix.height);

        if (fmt.fmt.pix.pixelformat != format)
                error("We need %.*s but %s insists on %.*s",
                      4, (char*)&format, dev_name,
                      4, (char*)&fmt.fmt.pix.pixelformat);

        if (bpl && fmt.fmt.pix.bytesperline != bpl)
                error("We need %d but %s insists on %d",
                      bpl, dev_name,
                      fmt.fmt.pix.bytesperline);

        if (fmt_out)
                *fmt_out = fmt.fmt.pix;

        return true;
}

static bool reserve_dambuf_buffers(int fd, int type, int count)
{
        int ret, i;

        struct v4l2_requestbuffers req = {
                .count = count,
                .type = type,
                .memory = V4L2_MEMORY_DMABUF,
        };
        ret = ioctl(fd, VIDIOC_REQBUFS, &req);
        syscall_error(ret < 0, "VIDIOC_REQBUFS failed");

        if (req.count != count)
                error("Requested %d buffers, got %d", count, req.count);

        //XXX: should we check length of the buffer so that it matches
        //the buffer length that will be imported?

        /*
        // allocate, map and export buffers
        for (i = 0; i < count; i++) {
                struct v4l2_buffer buf = {
                        .type = type,
                        .memory = V4L2_MEMORY_DMABUF,
                        .index = i,
                };

                ret = ioctl(fd, VIDIOC_QUERYBUF, &buf);
                syscall_error(ret < 0, "VIDIOC_QUERYBUF failed");

                bufs[i].index = i;
                bufs[i].length = buf.length;
        }
        */

        return true;
}

static bool alloc_buffers(int fd, int type, struct cam_app_buf* bufs, int count)
{
        int ret, i;

        struct v4l2_requestbuffers req = {
                .count = count,
                .type = type,
                .memory = V4L2_MEMORY_MMAP,
        };
        ret = ioctl(fd, VIDIOC_REQBUFS, &req);
        syscall_error(ret < 0, "VIDIOC_REQBUFS failed");

        if (req.count != count)
                error("Requested %d buffers, got %d", count, req.count);

        // allocate, map and export buffers
        for (i = 0; i < count; i++) {
                struct v4l2_buffer buf = {
                        .type = type,
                        .memory = V4L2_MEMORY_MMAP,
                        .index = i,
                };

                ret = ioctl(fd, VIDIOC_QUERYBUF, &buf);
                syscall_error(ret < 0, "VIDIOC_QUERYBUF failed");

                bufs[i].index = i;
                bufs[i].length = buf.length;

                // map the buffer to userspace
                bufs[i].start = mmap(NULL, buf.length,
                                     PROT_READ | PROT_WRITE,
                                     MAP_SHARED,
                                     fd, buf.m.offset);
                syscall_error(MAP_FAILED == bufs[i].start, "mmap failed");

                // export the buffer via dmabuf
                struct v4l2_exportbuffer ex = {
                        .type = type,
                        .index = i,
                        .flags = O_RDONLY | O_CLOEXEC,
                };

                ret = ioctl(fd, VIDIOC_EXPBUF, &ex);
                syscall_error(ret < 0, "VIDIOC_EXPBUF failed");

                bufs[i].dmabuf_fd = ex.fd;
        }

        return true;
}

static void queue_buf(int fd, int type, int idx)
{
        int ret;
        struct v4l2_buffer buf = {
                .type = type,
                .memory = V4L2_MEMORY_MMAP,
                .index = idx,
        };

        ret = ioctl(fd, VIDIOC_QBUF, &buf);
        syscall_error(ret < 0, "VIDIOC_QBUF failed");
}

static bool dequeue_buf(int fd, int type, int* idx)
{
        int ret;
        struct v4l2_buffer buf = {
                .type = type,
                .memory = V4L2_MEMORY_MMAP,
        };

        ret = ioctl(fd, VIDIOC_DQBUF, &buf);
        if (ret < 0) {
                if (errno == EAGAIN)
                        return false;

                syscall_error(true, "VIDIOC_DQBUF failed");
        }

        //printf("dequeued buf %u %s\n", buf.index, buf.flags & V4L2_BUF_FLAG_ERROR ? "E" : "G");

        *idx = buf.index;
        return true;
}

static void queue_dmabuf(int fd, int type, int idx, int dmabuf_fd)
{
        int ret;
        struct v4l2_buffer buf = {
                .type = type,
                .memory = V4L2_MEMORY_DMABUF,
                .index = idx,
                .m.fd = dmabuf_fd,
        };

        ret = ioctl(fd, VIDIOC_QBUF, &buf);
        syscall_error(ret < 0, "VIDIOC_QBUF failed");
}

static bool dequeue_dmabuf(int fd, int type, int* idx, int* dmabuf_fd)
{
        int ret;
        struct v4l2_buffer buf = {
                .type = type,
                .memory = V4L2_MEMORY_DMABUF,
        };

        ret = ioctl(fd, VIDIOC_DQBUF, &buf);
        if (ret < 0) {
                if (errno == EAGAIN)
                        return false;

                syscall_error(true, "VIDIOC_DQBUF failed");
        }

        if (idx)
                *idx = buf.index;
        if (dmabuf_fd)
                *dmabuf_fd = buf.m.fd;

        //printf("dequeued buf %u %s\n", buf.index, buf.flags & V4L2_BUF_FLAG_ERROR ? "E" : "G");
        return true;
}

static bool stream_set(int fd, int type, bool on)
{
        int ret;

        ret = ioctl(fd, on ? VIDIOC_STREAMON : VIDIOC_STREAMOFF, &type);
        syscall_error(ret < 0, "VIDIOC_STREAM%s failed", on ? "ON" : "OFF");

        return true;
}

static bool setup_link(int fd, struct media_pad_desc* src, struct media_pad_desc* sink, uint32_t flags)
{
        struct media_link_desc l = {
                .source = *src,
                .sink = *sink,
                .flags = flags,
        };

        return ioctl(fd, MEDIA_IOC_SETUP_LINK, &l) == 0;
}

static void on_csi_event(int event, void* data)
{
        int idx;

        // we may have a new frame from camera

        if (!dequeue_buf(cam_dev.csi_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, &idx))
                return;

        if (idx >= cam_dev.n_csi_bufs)
                error("Invalid cam buffer index");

        queue_dmabuf(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_OUTPUT,
                     idx, cam_dev.csi_bufs[idx].dmabuf_fd);
}

static void on_rot_event(int event, void* data)
{
        int rot_fd = cam_dev.rot_listener.fd;
        int idx;

        // we dequeue input buffers and return them to the csi
        while (dequeue_dmabuf(rot_fd, V4L2_BUF_TYPE_VIDEO_OUTPUT, &idx, NULL)) {
                if (idx >= cam_dev.n_rot_bufs)
                        error("Invalid rot[in] buffer index");

                queue_buf(cam_dev.csi_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, idx);
        }

        // then we dequeue output
        while (dequeue_buf(rot_fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, &idx)) {
                if (idx >= cam_dev.n_rot_bufs)
                        error("Invalid rot[out] buffer index");

                // we inform app about new buffer for display
                if (cam_dev.on_buffer_ready)
                        cam_dev.on_buffer_ready(&cam_dev.rot_bufs[idx]);
        }
}

bool cam_app_init(struct loop* loop)
{
        int ret, i, fd, csi_fd = -1, rot_fd = -1;
        struct v4l2_capability cap;
        struct stat st;
        char path[256];

        cam_dev.loop = loop;
        cam_dev.media_fd = -1;

        // detect and open camera devices

        for (i = 0; i < ARRAY_SIZE(cam_dev.sd); i++)
                cam_dev.sd[i].fd = -1;

        for (i = 0; i < 6; i++) {
                char subdev_name[256];

                snprintf(path, sizeof path, "/sys/class/video4linux/v4l-subdev%d/device/name", i);
                if (read_sysfs_file(path, subdev_name, sizeof subdev_name)) {
                        for (int j = 0; j < ARRAY_SIZE(cam_names); j++) {
                                if (!strcmp(subdev_name, cam_names[j].name) && cam_dev.sd[cam_names[j].location].fd < 0) {
                                        struct cam_app_subdev* sd = &cam_dev.sd[cam_names[j].location];

                                        snprintf(path, sizeof path, "/dev/v4l-subdev%d", i);
                                        fd = open(path, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
                                        if (fd < 0)
                                                continue;

                                        sd->name = cam_names[j].name;
                                        sd->location = cam_names[j].location;
                                        sd->fd = fd;
                                }
                        }
                }
        }

        if (cam_dev.sd[BACK_CAM].fd >= 0) {
                printf("back camera: %s\n", cam_dev.sd[BACK_CAM].name);
        }

        if (cam_dev.sd[FRONT_CAM].fd >= 0) {
                printf("front camera: %s\n", cam_dev.sd[FRONT_CAM].name);
        }

        if (cam_dev.sd[BACK_CAM].fd < 0 && cam_dev.sd[FRONT_CAM].fd < 0) {
                printf("no camera sensors found\n");
                return false;
        }

        // open media device

        for (i = 0; i < 6; i++) {
                snprintf(path, sizeof path, "/dev/media%d", i);

                fd = open(path, O_RDWR | O_CLOEXEC);
                if (fd < 0)
                        continue;

                struct media_device_info mi = {};

                ret = ioctl(fd, MEDIA_IOC_DEVICE_INFO, &mi);
                if (ret || strcmp(mi.driver, "sun6i-csi")) {
                        close(fd);
                        continue;
                }

                cam_dev.media_fd = fd;

                printf("media device: %s\n", mi.driver);

                uint32_t next_id = 0;
                while (true) {
                        struct media_entity_desc e = {
                                .id = next_id | MEDIA_ENT_ID_FLAG_NEXT,
                        };

                        ret = ioctl(fd, MEDIA_IOC_ENUM_ENTITIES, &e);
                        if (ret < 0)
                                break;

                        next_id = e.id;

                        struct media_pad_desc pads[e.pads];
                        struct media_link_desc links[e.links];
                        struct media_links_enum el = {
                                .entity = e.id,
                                .links = links,
                                .pads = pads,
                        };

                        ret = ioctl(fd, MEDIA_IOC_ENUM_LINKS, &el);
                        if (ret < 0)
                                break;

#if 0
                        printf("  entity: %d - %s [type=%x pads=%hu links=%hu]\n", e.id, e.name, e.type, e.pads, e.links);
                        for (int j = 0; j < e.pads; j++) {
                                printf("    pad: %d [flags=%x]\n", pads[j].index, pads[j].flags);
                        }
                        for (int j = 0; j < e.links; j++) {
                                struct media_link_desc* l = &links[j];
                                printf("    link: %d.%d => %d.%d [flags=%x]\n",
                                                l->source.entity, l->source.index,
                                                l->sink.entity, l->sink.index, l->flags);
                        }
#endif
                        // store data to cam_dev

                        if (!strcmp(e.name, "sun6i-csi")) {
                                printf("found media pad for %s (%d.0)\n", e.name, e.id);
                                cam_dev.csi_pad = pads[0];
                        }

                        for (i = 0; i < ARRAY_SIZE(cam_dev.sd); i++) {
                                struct cam_app_subdev* sd = &cam_dev.sd[i];
                                if (sd->fd >= 0 && strstr(e.name, sd->name)) {
                                        printf("found media pad for %s (%d.0)\n", sd->name, e.id);
                                        sd->pad = pads[0];
                                }
                        }
                }

                break;
        }

        if (cam_dev.media_fd < 0) {
                printf("media interface not found\n");
                goto err_close_subdevs;
        }

        // open platform devices

        for (i = 0; i < 6; i++) {
                snprintf(path, sizeof path, "/dev/video%d", i);

                fd = open(path, O_RDWR | O_NONBLOCK | O_CLOEXEC);
                if (fd < 0)
                        continue;

                ret = ioctl(fd, VIDIOC_QUERYCAP, &cap);
                if (ret) {
                        close(fd);
                        continue;
                }

                if (!strcmp(cap.card, "sun6i-csi")) {
                        csi_fd = fd;
                        continue;
                }

                if (!strcmp(cap.card, "sun8i-rotate")) {
                        rot_fd = fd;
                        continue;
                }

                close(fd);
        }

        if (csi_fd < 0) {
                printf("camera interface not found\n");
                goto err_close_media;
        }

        printf("sun6i-csi found\n");
        loop_listen_fd(loop, &cam_dev.csi_listener, csi_fd, on_csi_event, NULL);

        if (rot_fd >= 0) {
                printf("sun8i-rotate found\n");
                loop_listen_fd(loop, &cam_dev.rot_listener, rot_fd, on_rot_event, NULL);
        } else {
                printf("sun8i-rotate not found (rotation will not be available)\n");
        }

        return true;

err_close_media:
        close(cam_dev.media_fd);
err_close_subdevs:
        for (i = 0; i < ARRAY_SIZE(cam_dev.sd); i++) {
                struct cam_app_subdev* sd = &cam_dev.sd[i];
                if (sd->fd >= 0)
                        close(sd->fd);
        }

        return false;
}

void cam_app_fini(void)
{
        loop_listener_stop(&cam_dev.csi_listener, true);
        loop_listener_stop(&cam_dev.rot_listener, true);

        for (int i = 0; i < ARRAY_SIZE(cam_dev.sd); i++) {
                struct cam_app_subdev* sd = &cam_dev.sd[i];
                if (sd->fd >= 0)
                        close(sd->fd);
        }
}

bool cam_app_switch_camera(int loc)
{
        struct cam_app_subdev* sd;

        // switch off other links
        for (int i = 0; i < ARRAY_SIZE(cam_dev.sd); i++) {
                sd = &cam_dev.sd[i];
                if (sd->fd < 0 || i == loc)
                        continue;

                if (!setup_link(cam_dev.media_fd, &sd->pad, &cam_dev.csi_pad, 0))
                        return false;
        }

        // switch on selected link
        sd = &cam_dev.sd[loc];
        if (sd->fd >= 0)
                return setup_link(cam_dev.media_fd, &sd->pad, &cam_dev.csi_pad, MEDIA_LNK_FL_ENABLED);

        return false;
}

bool cam_app_setup_camera_pipeline(struct cam_app_setup* setup)
{
        int ret;
        int width = setup->width;
        int height = setup->height;
        uint32_t mbus = setup->cam_bus_fmt;
        struct cam_app_subdev* sd = &cam_dev.sd[setup->cam_location];

        // selected camera was not found
        if (sd->fd < 0)
                return false;

        // ask camera driver to set a new format
        struct v4l2_subdev_format sfmt = {
                .pad = 0,
                .which = V4L2_SUBDEV_FORMAT_ACTIVE,
                .format = {
                        .width = width,
                        .height = height,
                        .code = mbus,
                        .field = V4L2_FIELD_NONE,
                        .colorspace = V4L2_COLORSPACE_SRGB,
                },
        };

        ret = ioctl(sd->fd, VIDIOC_SUBDEV_S_FMT, &sfmt);
        syscall_error(ret < 0, "VIDIOC_SUBDEV_S_FMT failed");

        if (sfmt.format.code != mbus)
                error("We need 0x%x, but subdevice insists on 0x%x",
                      mbus, sfmt.format.code);

        if (width != sfmt.format.width || height != sfmt.format.height)
                error("We need %dx%d, but subdevice insists on %dx%d",
                      width, height,
                      sfmt.format.width, sfmt.format.height);

        // set camera framerate

        struct v4l2_subdev_frame_interval fi = {
                .pad = 0,
                .interval = {
                        .numerator = 1,
                        .denominator = setup->framerate,
                },
        };

        printf("Set frame interval: %u/%u\n", fi.interval.numerator,
                        fi.interval.denominator);

        ret = ioctl(sd->fd, VIDIOC_SUBDEV_S_FRAME_INTERVAL, &fi);
        syscall_error(ret < 0, "VIDIOC_SUBDEV_S_FRAME_INTERVAL failed");

        printf("Got frame interval: %u/%u\n", fi.interval.numerator,
                        fi.interval.denominator);

        // set rotation control first (because it affects format selection)

        struct v4l2_control ctrl = {
                .id = V4L2_CID_ROTATE,
                .value = setup->rotate,
        };

        ret = ioctl(cam_dev.rot_listener.fd, VIDIOC_S_CTRL, &ctrl);
        syscall_error(ret < 0, "VIDIOC_S_CTRL failed");

        ctrl = (struct v4l2_control){
                .id = V4L2_CID_HFLIP,
                .value = setup->hflip,
        };

        ret = ioctl(cam_dev.rot_listener.fd, VIDIOC_S_CTRL, &ctrl);
        syscall_error(ret < 0, "VIDIOC_S_CTRL failed");

        ctrl = (struct v4l2_control){
                .id = V4L2_CID_VFLIP,
                .value = setup->vflip,
        };

        ret = ioctl(cam_dev.rot_listener.fd, VIDIOC_S_CTRL, &ctrl);
        syscall_error(ret < 0, "VIDIOC_S_CTRL failed");

        // set format for the rotation engine next (because it determines bpl)

        if (!cam_dev.rot_listener.loop)
                error("Rotation engine is needed for now");

        set_video_format(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_OUTPUT,
                         width, height, setup->cam_pix_fmt, 0,
                         &cam_dev.rot_in_pix_fmt, "sun6i-rotate[in]");

        if (setup->rotate == 90 || setup->rotate == 270)
                set_video_format(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE,
                                 height, width, setup->rot_out_fmt, 0,
                                 &cam_dev.rot_out_pix_fmt, "sun6i-rotate[out]");
        else
                set_video_format(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE,
                                 width, height, setup->rot_out_fmt, 0,
                                 &cam_dev.rot_out_pix_fmt,  "sun6i-rotate[out]");

        // set format on the csi side

        set_video_format(cam_dev.csi_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE,
                         width, height, setup->cam_pix_fmt,
                         cam_dev.rot_in_pix_fmt.bytesperline,
                         &cam_dev.csi_out_pix_fmt, "sun6i-csi");

        // prepare buffers (for csi -> rot)

        alloc_buffers(cam_dev.csi_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE,
                      cam_dev.csi_bufs, setup->cam_buffers);
        cam_dev.n_csi_bufs = setup->cam_buffers;

        // prepare buffers (for rot -> out)

        reserve_dambuf_buffers(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_OUTPUT,
                               setup->cam_buffers);
        alloc_buffers(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE,
                      cam_dev.rot_bufs, setup->cam_buffers);
        cam_dev.n_rot_bufs = setup->cam_buffers;

        // queue buffers to csi
        for (int i = 0; i < cam_dev.n_csi_bufs; i++)
                queue_buf(cam_dev.csi_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, i);

        // queue output buffers to rot (input buffers will be shuffled
        // between csi out and rot in in the event callbacks)
        for (int i = 0; i < cam_dev.n_rot_bufs; i++)
                queue_buf(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, i);

        stream_set(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, true);
        stream_set(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_OUTPUT, true);

        cam_dev.current_setup = *setup;
        return true;
}

bool cam_app_return_buffer(int idx)
{
        if (idx >= cam_dev.n_rot_bufs)
                error("Invalid rot[out] buffer index");

        queue_buf(cam_dev.rot_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, idx);
        return true;
}

bool cam_app_start_stream(void)
{
        stream_set(cam_dev.csi_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, true);
        return true;
}

bool cam_app_stop_stream(void)
{
        stream_set(cam_dev.csi_listener.fd, V4L2_BUF_TYPE_VIDEO_CAPTURE, false);
        return true;
}

bool cam_get_control(unsigned int id, struct cam_control* cc)
{
        struct v4l2_ext_controls ctrls;
        struct v4l2_ext_control ctrl;
        struct v4l2_query_ext_ctrl query = {
                .id = id
        };
        int ret;
        int fd = cam_dev.sd[cam_dev.current_setup.cam_location].fd;

        memset(cc, 0, sizeof(*cc));

        ret = ioctl(fd, VIDIOC_QUERY_EXT_CTRL, &query);
        if (ret < 0)
                return false;

        cc->id = id;
        cc->disabled = query.flags & V4L2_CTRL_FLAG_DISABLED;
        cc->grabbed = query.flags & V4L2_CTRL_FLAG_GRABBED;
        cc->read_only = query.flags & V4L2_CTRL_FLAG_READ_ONLY;
        cc->write_only = query.flags & V4L2_CTRL_FLAG_WRITE_ONLY;
        cc->_volatile = query.flags & V4L2_CTRL_FLAG_VOLATILE;
        cc->type = query.type;
        cc->def_val = query.default_value;
        cc->min = query.minimum;
        cc->max = query.maximum;
        cc->step = query.step;
        cc->menu_items_count = 0;

        if (cc->type == V4L2_CTRL_TYPE_MENU || cc->type == V4L2_CTRL_TYPE_INTEGER_MENU) {
                for (uint32_t i = cc->min; i <= cc->max; i++) {
                        struct v4l2_querymenu query_menu = {
                                .id = id,
                                .index = i,
                        };

                        ret = ioctl(fd, VIDIOC_QUERYMENU, &query_menu);
                        syscall_error(ret < 0 && errno != EINVAL, "VIDIOC_QUERYMENU failed");
                        if (ret == 0) {
                                if (cc->menu_items_count >= 64)
                                        break;

                                struct cam_control_menu_item *mi =
                                        &cc->menu_items[cc->menu_items_count];

                                if (cc->type == V4L2_CTRL_TYPE_MENU) {
                                        snprintf(mi->name, sizeof(mi->name),
                                                 "%s", query_menu.name);
                                        mi->value = i;
                                } else {
                                        snprintf(mi->name, sizeof(mi->name),
                                                 "%" PRIi64, (int64_t)query_menu.value);
                                        mi->value = i;
                                }

                                cc->menu_items_count++;
                        }
                }

        }

        if (cc->disabled || cc->grabbed || cc->write_only)
                return true;

        memset(&ctrls, 0, sizeof(ctrls));
        memset(&ctrl, 0, sizeof(ctrl));

        ctrls.ctrl_class = V4L2_CTRL_ID2CLASS(id);
        ctrls.which = V4L2_CTRL_WHICH_CUR_VAL;
        ctrls.count = 1;
        ctrls.controls = &ctrl;
        ctrl.id = id;

        ret = ioctl(fd, VIDIOC_G_EXT_CTRLS, &ctrls);
        if (ret < 0)
                return true;

        if (cc->type == V4L2_CTRL_TYPE_INTEGER64)
                cc->val = ctrl.value64;
        else
                cc->val = ctrl.value;

        return true;
}

bool cam_set_control(unsigned int id, int64_t val)
{
        struct v4l2_control ctrl;
        struct v4l2_queryctrl query = {
                .id = id
        };
        int ret;
        int fd = cam_dev.sd[cam_dev.current_setup.cam_location].fd;

        ctrl.id = id;
        ctrl.value = val;

        return ioctl(fd, VIDIOC_S_CTRL, &ctrl) == 0;
}

// subdevice debugging api

void cam_sd_set_reg(uint16_t addr, uint8_t val)
{
        struct v4l2_dbg_register query = {
                .size = 1,
                .reg = addr,
                .val = val,
        };
        int ret;
        int fd = cam_dev.sd[cam_dev.current_setup.cam_location].fd;

        ret = ioctl(fd, VIDIOC_DBG_S_REGISTER, &query);
        syscall_error(ret < 0, "VIDIOC_DBG_S_REGISTER failed");
}

uint8_t cam_sd_get_reg(uint16_t addr)
{
        struct v4l2_dbg_register query = {
                .size = 1,
                .reg = addr,
                .val = 0,
        };
        int ret;
        int fd = cam_dev.sd[cam_dev.current_setup.cam_location].fd;

        ret = ioctl(fd, VIDIOC_DBG_G_REGISTER, &query);
        syscall_error(ret < 0, "VIDIOC_DBG_G_REGISTER failed");

        return query.val;
}

uint16_t cam_sd_get_reg16(uint16_t addr)
{
        struct v4l2_dbg_register query = {
                .size = 1,
                .reg = addr,
                .val = 0,
        };
        int ret;
        uint16_t val = 0;
        int fd = cam_dev.sd[cam_dev.current_setup.cam_location].fd;

        ret = ioctl(fd, VIDIOC_DBG_G_REGISTER, &query);
        syscall_error(ret < 0, "VIDIOC_DBG_G_REGISTER failed");

        val |= query.val << 8;
        query.size = 1;
        query.reg = addr + 1;

        ret = ioctl(fd, VIDIOC_DBG_G_REGISTER, &query);
        syscall_error(ret < 0, "VIDIOC_DBG_G_REGISTER failed");

        val |= query.val;
        return val;
}

// old potentialy useful code

void cam_dump_info(void)
{
        int ret, i;
        struct v4l2_fmtdesc fmt;
        struct v4l2_frmsizeenum size;
        struct v4l2_frmivalenum iv;
        int fd = cam_dev.csi_listener.fd;

        printf("Inputs:\n");

        for (i = 0; ; ++i) {
                struct v4l2_input input = {
                        .index = i,
                };

                ret = ioctl(fd, VIDIOC_ENUMINPUT, &input);
                if (ret < 0)
                        break;

                printf("\t%u: %s\n", i, input.name);
        }

        printf("Formats:\n");

        fmt.index = 0;
        fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
        while (ioctl(fd, VIDIOC_ENUM_FMT, &fmt) >= 0) {
                printf("\tidx=%d type=%d pixfmt=%.*s: %s\n", fmt.index, fmt.type,
                       4, (char*)&fmt.pixelformat, fmt.description);

                size.pixel_format = fmt.pixelformat;
                size.index = 0;
                while (ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &size) >= 0) {
                        if (size.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
                                printf("\t\tsize=%dx%d intervals: ",
                                       size.discrete.width,
                                       size.discrete.height);
                                iv.index = 0;
                                iv.pixel_format = fmt.pixelformat;
                                iv.width = size.discrete.width;
                                iv.height = size.discrete.height;
                                while (ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &iv) >= 0) {
                                        if (iv.type == V4L2_FRMIVAL_TYPE_DISCRETE)
                                                printf(" %d/%d", iv.discrete.numerator, iv.discrete.denominator);
                                        iv.index++;
                                }
                                printf("\n");
                        }
                        size.index++;
                }
                fmt.index++;
        }
}

void cam_set_fps(int fps)
{
        int ret;
        struct v4l2_streamparm parm = {
                .type = V4L2_BUF_TYPE_VIDEO_CAPTURE,
        };
        int fd = cam_dev.csi_listener.fd;

        ret = ioctl(fd, VIDIOC_G_PARM, &parm);
        syscall_error(ret < 0, "VIDIOC_G_PARM failed");
        if (parm.parm.capture.capability & V4L2_CAP_TIMEPERFRAME) {
                printf("Got frame interval: %u/%u\n",
                       parm.parm.capture.timeperframe.numerator,
                       parm.parm.capture.timeperframe.denominator);

                parm.parm.capture.timeperframe.numerator = 1;
                parm.parm.capture.timeperframe.denominator = fps;

                ret = ioctl(fd, VIDIOC_S_PARM, &parm);
                syscall_error(ret < 0, "VIDIOC_S_PARM failed");

                printf("Set frame interval: %u/%u\n",
                       parm.parm.capture.timeperframe.numerator,
                       parm.parm.capture.timeperframe.denominator);
        }
}