Browse Source

First pass at integrating pipeline with application

Benjamin Schaaf 4 years ago
parent
commit
dd45690d47
11 changed files with 1924 additions and 783 deletions
  1. 266 0
      camera_config.c
  2. 49 0
      camera_config.h
  3. 18 8
      config/pine64,pinephone-1.2.ini
  4. 584 0
      io_pipeline.c
  5. 26 0
      io_pipeline.h
  6. 447 773
      main.c
  7. 27 0
      main.h
  8. 9 1
      meson.build
  9. 467 0
      process_pipeline.c
  10. 30 0
      process_pipeline.h
  11. 1 1
      tools/test_camera.c

+ 266 - 0
camera_config.c

@@ -0,0 +1,266 @@
+#include "camera_config.h"
+
+#include "ini.h"
+#include "config.h"
+#include <wordexp.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <glib.h>
+#include <assert.h>
+
+static struct mp_camera_config cameras[MP_MAX_CAMERAS];
+static size_t num_cameras = 0;
+
+static char *exif_make;
+static char *exif_model;
+
+static bool
+find_config(char *conffile)
+{
+    char buf[512];
+    char *xdg_config_home;
+    wordexp_t exp_result;
+    FILE *fp;
+
+    // Resolve XDG stuff
+    if ((xdg_config_home = getenv("XDG_CONFIG_HOME")) == NULL) {
+        xdg_config_home = "~/.config";
+    }
+    wordexp(xdg_config_home, &exp_result, 0);
+    xdg_config_home = strdup(exp_result.we_wordv[0]);
+    wordfree(&exp_result);
+
+    if (access("/proc/device-tree/compatible", F_OK) != -1) {
+        // Reads to compatible string of the current device tree, looks like:
+        // pine64,pinephone-1.2\0allwinner,sun50i-a64\0
+        fp = fopen("/proc/device-tree/compatible", "r");
+        fgets(buf, 512, fp);
+        fclose(fp);
+
+        // Check config/%dt.ini in the current working directory
+        sprintf(conffile, "config/%s.ini", buf);
+        if(access(conffile, F_OK) != -1) {
+            printf("Found config file at %s\n", conffile);
+            return true;
+        }
+
+        // Check for a config file in XDG_CONFIG_HOME
+        sprintf(conffile, "%s/megapixels/config/%s.ini", xdg_config_home, buf);
+        if(access(conffile, F_OK) != -1) {
+            printf("Found config file at %s\n", conffile);
+            return true;
+        }
+
+        // Check user overridden /etc/megapixels/config/$dt.ini
+        sprintf(conffile, "%s/megapixels/config/%s.ini", SYSCONFDIR, buf);
+        if(access(conffile, F_OK) != -1) {
+            printf("Found config file at %s\n", conffile);
+            return true;
+        }
+        // Check packaged /usr/share/megapixels/config/$dt.ini
+        sprintf(conffile, "%s/megapixels/config/%s.ini", DATADIR, buf);
+        if(access(conffile, F_OK) != -1) {
+            printf("Found config file at %s\n", conffile);
+            return true;
+        }
+        printf("%s not found\n", conffile);
+    } else {
+        printf("Could not read device name from device tree\n");
+    }
+
+    // If all else fails, fall back to /etc/megapixels.ini
+    sprintf(conffile, "/etc/megapixels.ini");
+    if (access(conffile, F_OK) != -1) {
+        printf("Found config file at %s\n", conffile);
+        return true;
+    }
+
+    return false;
+}
+
+static int
+strtoint(const char *nptr, char **endptr, int base)
+{
+    long x = strtol(nptr, endptr, base);
+    assert(x <= INT_MAX);
+    return (int) x;
+}
+
+static bool
+config_handle_camera_mode(const char *prefix, MPCameraMode * mode, const char *name, const char *value)
+{
+    int prefix_length = strlen(prefix);
+    if (strncmp(prefix, name, prefix_length) != 0)
+        return false;
+
+    name += prefix_length;
+
+    if (strcmp(name, "width") == 0) {
+        mode->width = strtoint(value, NULL, 10);
+    } else if (strcmp(name, "height") == 0) {
+        mode->height = strtoint(value, NULL, 10);
+    } else if (strcmp(name, "rate") == 0) {
+        mode->frame_interval.numerator = 1;
+        mode->frame_interval.denominator = strtoint(value, NULL, 10);
+    } else if (strcmp(name, "fmt") == 0) {
+        mode->pixel_format = mp_pixel_format_from_str(value);
+        if (mode->pixel_format == MP_PIXEL_FMT_UNSUPPORTED) {
+            g_printerr("Unsupported pixelformat %s\n", value);
+            exit(1);
+        }
+    } else {
+        return false;
+    }
+    return true;
+}
+
+static int
+config_ini_handler(void *user, const char *section, const char *name,
+    const char *value)
+{
+    if (strcmp(section, "device") == 0) {
+        if (strcmp(name, "make") == 0) {
+            exif_make = strdup(value);
+        } else if (strcmp(name, "model") == 0) {
+            exif_model = strdup(value);
+        } else {
+            g_printerr("Unknown key '%s' in [device]\n", name);
+            exit(1);
+        }
+    } else {
+        if (num_cameras == MP_MAX_CAMERAS) {
+            g_printerr("More cameras defined than NUM_CAMERAS\n");
+            exit(1);
+        }
+
+        size_t index = 0;
+        for (; index < num_cameras; ++index) {
+            if (strcmp(cameras[index].cfg_name, section) == 0) {
+                break;
+            }
+        }
+
+        if (index == num_cameras) {
+            printf("Adding camera %s from config\n", section);
+            ++num_cameras;
+
+            cameras[index].index = index;
+            strcpy(cameras[index].cfg_name, section);
+        }
+
+        struct mp_camera_config *cc = &cameras[index];
+
+        if (config_handle_camera_mode("capture-", &cc->capture_mode, name, value)) {
+        } else if (config_handle_camera_mode("preview-", &cc->preview_mode, name, value)) {
+        } else if (strcmp(name, "rotate") == 0) {
+            cc->rotate = strtoint(value, NULL, 10);
+        } else if (strcmp(name, "mirrored") == 0) {
+            cc->mirrored = strcmp(value, "true") == 0;
+        } else if (strcmp(name, "driver") == 0) {
+            strcpy(cc->dev_name, value);
+        } else if (strcmp(name, "media-driver") == 0) {
+            strcpy(cc->media_dev_name, value);
+        } else if (strcmp(name, "media-links") == 0) {
+            char **linkdefs = g_strsplit(value, ",", 0);
+
+            for (int i = 0; i < MP_MAX_LINKS && linkdefs[i] != NULL; ++i) {
+                char **linkdef = g_strsplit(linkdefs[i], "->", 2);
+                char **porta = g_strsplit(linkdef[0], ":", 2);
+                char **portb = g_strsplit(linkdef[1], ":", 2);
+
+                strcpy(cc->media_links[i].source_name, porta[0]);
+                strcpy(cc->media_links[i].target_name, portb[0]);
+                cc->media_links[i].source_port = strtoint(porta[1], NULL, 10);
+                cc->media_links[i].target_port = strtoint(portb[1], NULL, 10);
+
+                g_strfreev(portb);
+                g_strfreev(porta);
+                g_strfreev(linkdef);
+                ++cc->num_media_links;
+            }
+            g_strfreev(linkdefs);
+        } else if (strcmp(name, "colormatrix") == 0) {
+            sscanf(value, "%f,%f,%f,%f,%f,%f,%f,%f,%f",
+                    cc->colormatrix+0,
+                    cc->colormatrix+1,
+                    cc->colormatrix+2,
+                    cc->colormatrix+3,
+                    cc->colormatrix+4,
+                    cc->colormatrix+5,
+                    cc->colormatrix+6,
+                    cc->colormatrix+7,
+                    cc->colormatrix+8
+                    );
+        } else if (strcmp(name, "forwardmatrix") == 0) {
+            sscanf(value, "%f,%f,%f,%f,%f,%f,%f,%f,%f",
+                    cc->forwardmatrix+0,
+                    cc->forwardmatrix+1,
+                    cc->forwardmatrix+2,
+                    cc->forwardmatrix+3,
+                    cc->forwardmatrix+4,
+                    cc->forwardmatrix+5,
+                    cc->forwardmatrix+6,
+                    cc->forwardmatrix+7,
+                    cc->forwardmatrix+8
+                    );
+        } else if (strcmp(name, "whitelevel") == 0) {
+            cc->whitelevel = strtoint(value, NULL, 10);
+        } else if (strcmp(name, "blacklevel") == 0) {
+            cc->blacklevel = strtoint(value, NULL, 10);
+        } else if (strcmp(name, "focallength") == 0) {
+            cc->focallength = strtof(value, NULL);
+        } else if (strcmp(name, "cropfactor") == 0) {
+            cc->cropfactor = strtof(value, NULL);
+        } else if (strcmp(name, "fnumber") == 0) {
+            cc->fnumber = strtod(value, NULL);
+        } else if (strcmp(name, "iso-min") == 0) {
+            cc->iso_min = strtod(value, NULL);
+        } else if (strcmp(name, "iso-max") == 0) {
+            cc->iso_max = strtod(value, NULL);
+        } else {
+            g_printerr("Unknown key '%s' in [%s]\n", name, section);
+            exit(1);
+        }
+    }
+    return 1;
+}
+
+bool mp_load_config() {
+    char file[512];
+    if (!find_config(file)) {
+        g_printerr("Could not find any config file\n");
+        return false;
+    }
+
+    int result = ini_parse(file, config_ini_handler, NULL);
+    if (result == -1) {
+        g_printerr("Config file not found\n");
+    } else if (result == -2) {
+        g_printerr("Could not allocate memory to parse config file\n");
+    } else if (result != 0) {
+        g_printerr("Could not parse config file\n");
+    } else {
+        return true;
+    }
+    return false;
+}
+
+const char * mp_get_device_make()
+{
+    return exif_make;
+}
+
+const char * mp_get_device_model()
+{
+    return exif_model;
+}
+
+const struct mp_camera_config * mp_get_camera_config(size_t index)
+{
+    if (index >= num_cameras)
+        return NULL;
+
+    return &cameras[index];
+}

+ 49 - 0
camera_config.h

@@ -0,0 +1,49 @@
+#pragma once
+
+#include "camera.h"
+
+#include <stdbool.h>
+#include <stddef.h>
+
+#define MP_MAX_CAMERAS 5
+#define MP_MAX_LINKS 10
+
+struct mp_media_link_config {
+    char source_name[100];
+    char target_name[100];
+    int source_port;
+    int target_port;
+};
+
+struct mp_camera_config {
+    size_t index;
+
+    char cfg_name[100];
+    char dev_name[260];
+    char media_dev_name[260];
+
+    MPCameraMode capture_mode;
+    MPCameraMode preview_mode;
+    int rotate;
+    bool mirrored;
+
+    struct mp_media_link_config media_links[MP_MAX_LINKS];
+    int num_media_links;
+
+    float colormatrix[9];
+    float forwardmatrix[9];
+    int blacklevel;
+    int whitelevel;
+
+    float focallength;
+    float cropfactor;
+    double fnumber;
+    int iso_min;
+    int iso_max;
+};
+
+bool mp_load_config();
+
+const char *mp_get_device_make();
+const char *mp_get_device_model();
+const struct mp_camera_config * mp_get_camera_config(size_t index);

+ 18 - 8
config/pine64,pinephone-1.2.ini

@@ -5,11 +5,16 @@ model=PinePhone
 [rear]
 driver=ov5640
 media-driver=sun6i-csi
-width=2592
-height=1944
-rate=15
-fmt=BGGR8
+capture-width=2592
+capture-height=1944
+capture-rate=15
+capture-fmt=BGGR8
+preview-width=1280
+preview-height=720
+preview-rate=30
+preview-fmt=BGGR8
 rotate=270
+mirrored=false
 colormatrix=1.384,-0.3203,-0.0124,-0.2728,1.049,0.1556,-0.0506,0.2577,0.8050
 forwardmatrix=0.7331,0.1294,0.1018,0.3039,0.6698,0.0263,0.0002,0.0556,0.7693
 blacklevel=3
@@ -23,11 +28,16 @@ iso-max=64000
 [front]
 driver=gc2145
 media-driver=sun6i-csi
-width=1280
-height=960
-rate=30
-fmt=BGGR8
+capture-width=1280
+capture-height=960
+capture-rate=30
+capture-fmt=BGGR8
+preview-width=1280
+preview-height=960
+preview-rate=30
+preview-fmt=BGGR8
 rotate=90
+mirrored=true
 focallength=2.6
 cropfactor=12.7
 fnumber=2.8

+ 584 - 0
io_pipeline.c

@@ -0,0 +1,584 @@
+#include "io_pipeline.h"
+
+#include "device.h"
+#include "camera.h"
+#include "pipeline.h"
+#include "process_pipeline.h"
+#include <string.h>
+#include <glib.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <errno.h>
+#include <assert.h>
+#include <stdio.h>
+
+struct media_link_info {
+    unsigned int source_entity_id;
+    unsigned int target_entity_id;
+    char source_fname[260];
+    char target_fname[260];
+};
+
+struct camera_info {
+    size_t device_index;
+
+    unsigned int pad_id;
+
+    char dev_fname[260];
+    int fd;
+
+    MPCamera *camera;
+
+    int gain_ctrl;
+    int gain_max;
+
+    bool has_auto_focus_continuous;
+    bool has_auto_focus_start;
+
+
+    // unsigned int entity_id;
+    // enum v4l2_buf_type type;
+
+    // char media_dev_fname[260];
+    // char video_dev_fname[260];
+    // int media_fd;
+
+
+    // struct mp_media_link media_links[MP_MAX_LINKS];
+    // int num_media_links;
+
+    // int gain_ctrl;
+};
+
+struct device_info {
+    const char *media_dev_name; // owned by camera config
+
+    MPDevice *device;
+
+    unsigned int interface_pad_id;
+
+    int video_fd;
+};
+
+static struct camera_info cameras[MP_MAX_CAMERAS];
+
+static struct device_info devices[MP_MAX_CAMERAS];
+static size_t num_devices = 0;
+
+static const struct mp_camera_config *camera = NULL;
+static MPCameraMode mode;
+
+static bool just_switched_mode = false;
+static int blank_frame_count = 0;
+
+static int burst_length;
+static int captures_remaining = 0;
+
+static int preview_width;
+static int preview_height;
+
+struct control_state {
+    bool gain_is_manual;
+    int gain;
+
+    bool exposure_is_manual;
+    int exposure;
+};
+
+static struct control_state desired_controls = {};
+static struct control_state current_controls = {};
+
+static bool want_focus = false;
+
+static MPPipeline *pipeline;
+static GSource *capture_source;
+
+static int
+xioctl(int fd, int request, void *arg)
+{
+    int r;
+    do {
+        r = ioctl(fd, request, arg);
+    } while (r == -1 && errno == EINTR);
+    return r;
+}
+
+static int
+v4l2_ctrl_set(int fd, uint32_t id, int val)
+{
+   struct v4l2_control ctrl = {0};
+   ctrl.id = id;
+   ctrl.value = val;
+
+   if (xioctl(fd, VIDIOC_S_CTRL, &ctrl) == -1) {
+       g_printerr("Failed to set control %d to %d\n", id, val);
+       return -1;
+   }
+   return 0;
+}
+
+static int
+v4l2_ctrl_get(int fd, uint32_t id)
+{
+   struct v4l2_control ctrl = {0};
+   ctrl.id = id;
+
+   if (xioctl(fd, VIDIOC_G_CTRL, &ctrl) == -1) {
+       g_printerr("Failed to get control %d\n", id);
+       return -1;
+   }
+   return ctrl.value;
+}
+
+static int
+v4l2_ctrl_get_max(int fd, uint32_t id)
+{
+   struct v4l2_queryctrl queryctrl;
+   int ret;
+
+   memset(&queryctrl, 0, sizeof(queryctrl));
+
+   queryctrl.id = id;
+   ret = xioctl(fd, VIDIOC_QUERYCTRL, &queryctrl);
+   if (ret)
+       return 0;
+
+   if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) {
+       return 0;
+   }
+
+   return queryctrl.maximum;
+}
+
+static int
+v4l2_has_control(int fd, int control_id)
+{
+   struct v4l2_queryctrl queryctrl;
+   int ret;
+
+   memset(&queryctrl, 0, sizeof(queryctrl));
+
+   queryctrl.id = control_id;
+   ret = xioctl(fd, VIDIOC_QUERYCTRL, &queryctrl);
+   if (ret)
+       return 0;
+
+   if (queryctrl.flags & V4L2_CTRL_FLAG_DISABLED) {
+       return 0;
+   }
+
+   return 1;
+}
+
+static void setup_camera(MPDeviceList **device_list, const struct mp_camera_config *config)
+{
+    // Find device info
+    size_t device_index = 0;
+    for (; device_index < num_devices; ++device_index) {
+        if (strcmp(config->media_dev_name, devices[device_index].media_dev_name) == 0) {
+            break;
+        }
+    }
+
+    if (device_index == num_devices)
+    {
+        device_index = num_devices;
+
+        // Initialize new device
+        struct device_info *info = &devices[device_index];
+        info->media_dev_name = config->media_dev_name;
+        info->device = mp_device_list_find_remove(device_list, info->media_dev_name);
+        if (!info->device) {
+            g_printerr("Could not find /dev/media* node matching '%s'\n", info->media_dev_name);
+            exit(EXIT_FAILURE);
+        }
+
+        const struct media_v2_entity *entity = mp_device_find_entity(info->device, info->media_dev_name);
+        if (!entity) {
+            g_printerr("Count not find device video entity\n");
+            exit(EXIT_FAILURE);
+        }
+
+        const struct media_v2_pad *pad = mp_device_get_pad_from_entity(info->device, entity->id);
+        info->interface_pad_id = pad->id;
+
+        const struct media_v2_interface *interface = mp_device_find_entity_interface(info->device, entity->id);
+        char dev_name[260];
+        if (!mp_find_device_path(interface->devnode, dev_name, 260)) {
+            g_printerr("Count not find video path\n");
+            exit(EXIT_FAILURE);
+        }
+
+        info->video_fd = open(dev_name, O_RDWR);
+        if (info->video_fd == -1) {
+            g_printerr("Could not open %s\n", dev_name);
+            exit(EXIT_FAILURE);
+        }
+
+        ++num_devices;
+    }
+
+    {
+        struct camera_info *info = &cameras[config->index];
+        struct device_info *dev_info = &devices[device_index];
+
+        info->device_index = device_index;
+
+        const struct media_v2_entity *entity = mp_device_find_entity(dev_info->device, config->dev_name);
+        if (!entity) {
+            g_printerr("Count not find camera entity matching '%s'\n", config->dev_name);
+            exit(EXIT_FAILURE);
+        }
+
+        const struct media_v2_pad *pad = mp_device_get_pad_from_entity(dev_info->device, entity->id);
+
+        info->pad_id = pad->id;
+
+        // Make sure the camera starts out as disabled
+        mp_device_setup_link(
+            dev_info->device,
+            info->pad_id,
+            dev_info->interface_pad_id,
+            false);
+
+        const struct media_v2_interface *interface = mp_device_find_entity_interface(dev_info->device, entity->id);
+
+        if (!mp_find_device_path(interface->devnode, info->dev_fname, 260)) {
+            g_printerr("Count not find camera device path\n");
+            exit(EXIT_FAILURE);
+        }
+
+        info->fd = open(info->dev_fname, O_RDWR);
+        if (info->fd == -1) {
+            g_printerr("Could not open %s\n", info->dev_fname);
+            exit(EXIT_FAILURE);
+        }
+
+        info->camera = mp_camera_new(dev_info->video_fd, info->fd);
+
+        // Trigger continuous auto focus if the sensor supports it
+        if (v4l2_has_control(info->fd, V4L2_CID_FOCUS_AUTO)) {
+            info->has_auto_focus_continuous = true;
+            v4l2_ctrl_set(info->fd, V4L2_CID_FOCUS_AUTO, 1);
+        }
+        if (v4l2_has_control(info->fd, V4L2_CID_AUTO_FOCUS_START)) {
+            info->has_auto_focus_start = true;
+        }
+
+        if (v4l2_has_control(info->fd, V4L2_CID_GAIN)) {
+            info->gain_ctrl = V4L2_CID_GAIN;
+            info->gain_max = v4l2_ctrl_get_max(info->fd, V4L2_CID_GAIN);
+        }
+
+        if (v4l2_has_control(info->fd, V4L2_CID_ANALOGUE_GAIN)) {
+            info->gain_ctrl = V4L2_CID_ANALOGUE_GAIN;
+            info->gain_max = v4l2_ctrl_get_max(info->fd, V4L2_CID_ANALOGUE_GAIN);
+        }
+    }
+}
+
+static void setup(MPPipeline *pipeline, const void *data)
+{
+    MPDeviceList *device_list = mp_device_list_new();
+
+    for (size_t i = 0; i < MP_MAX_CAMERAS; ++i) {
+        const struct mp_camera_config *config = mp_get_camera_config(i);
+        if (!config) {
+            break;
+        }
+
+        setup_camera(&device_list, config);
+    }
+
+    mp_device_list_free(device_list);
+}
+
+void mp_io_pipeline_start()
+{
+    mp_process_pipeline_start();
+
+    pipeline = mp_pipeline_new();
+
+    mp_pipeline_invoke(pipeline, setup, NULL, 0);
+}
+
+void mp_io_pipeline_stop()
+{
+    if (capture_source) {
+        g_source_destroy(capture_source);
+    }
+
+    mp_pipeline_free(pipeline);
+
+    mp_process_pipeline_stop();
+}
+
+static void
+update_process_pipeline()
+{
+    struct camera_info *info = &cameras[camera->index];
+
+    // Grab the latest control values
+    if (!current_controls.gain_is_manual) {
+        current_controls.gain = v4l2_ctrl_get(info->fd, info->gain_ctrl);
+    }
+    if (!current_controls.exposure_is_manual) {
+        current_controls.exposure = v4l2_ctrl_get(info->fd, V4L2_CID_EXPOSURE);
+    }
+
+    struct mp_process_pipeline_state pipeline_state = {
+        .camera = camera,
+        .mode = mode,
+        .burst_length = burst_length,
+        .preview_width = preview_width,
+        .preview_height = preview_height,
+        .gain_is_manual = current_controls.gain_is_manual,
+        .gain = current_controls.gain,
+        .gain_max = info->gain_max,
+        .exposure_is_manual = current_controls.exposure_is_manual,
+        .exposure = current_controls.exposure,
+        .has_auto_focus_continuous = info->has_auto_focus_continuous,
+        .has_auto_focus_start = info->has_auto_focus_start,
+    };
+    mp_process_pipeline_update_state(&pipeline_state);
+}
+
+static void
+focus(MPPipeline *pipeline, const void *data)
+{
+    want_focus = true;
+}
+
+void mp_io_pipeline_focus()
+{
+    mp_pipeline_invoke(pipeline, focus, NULL, 0);
+}
+
+static void
+capture(MPPipeline *pipeline, const void *data)
+{
+    struct camera_info *info = &cameras[camera->index];
+
+    captures_remaining = burst_length;
+
+    // Disable the autogain/exposure while taking the burst
+    v4l2_ctrl_set(info->fd, V4L2_CID_AUTOGAIN, 0);
+    v4l2_ctrl_set(info->fd, V4L2_CID_EXPOSURE_AUTO, V4L2_EXPOSURE_MANUAL);
+
+    // Change camera mode for capturing
+    mp_camera_stop_capture(info->camera);
+
+    mode = camera->capture_mode;
+    mp_camera_set_mode(info->camera, &mode);
+    just_switched_mode = true;
+
+    mp_camera_start_capture(info->camera);
+
+    update_process_pipeline();
+
+    mp_process_pipeline_capture();
+}
+
+void mp_io_pipeline_capture()
+{
+    mp_pipeline_invoke(pipeline, capture, NULL, 0);
+}
+
+static void
+update_controls()
+{
+    // Don't update controls while capturing
+    if (captures_remaining > 0) {
+        return;
+    }
+
+    struct camera_info *info = &cameras[camera->index];
+
+    if (want_focus) {
+        if (info->has_auto_focus_continuous) {
+            v4l2_ctrl_set(info->fd, V4L2_CID_FOCUS_AUTO, 1);
+        } else if (info->has_auto_focus_start) {
+            v4l2_ctrl_set(info->fd, V4L2_CID_AUTO_FOCUS_START, 1);
+        }
+
+        want_focus = false;
+    }
+
+    if (current_controls.gain_is_manual != desired_controls.gain_is_manual) {
+        v4l2_ctrl_set(info->fd, V4L2_CID_AUTOGAIN, desired_controls.gain_is_manual ? 0 : 1);
+    }
+
+    if (!desired_controls.gain_is_manual && current_controls.gain != desired_controls.gain) {
+        v4l2_ctrl_set(info->fd, info->gain_ctrl, desired_controls.gain);
+    }
+
+    if (current_controls.exposure_is_manual != desired_controls.exposure_is_manual) {
+        v4l2_ctrl_set(
+            info->fd,
+            V4L2_CID_EXPOSURE_AUTO,
+            desired_controls.exposure_is_manual ? V4L2_EXPOSURE_MANUAL : V4L2_EXPOSURE_AUTO);
+    }
+
+    if (!desired_controls.exposure_is_manual && current_controls.exposure != desired_controls.exposure) {
+        printf("Setting exposure to %d\n", desired_controls.exposure);
+        v4l2_ctrl_set(info->fd, V4L2_CID_EXPOSURE, desired_controls.exposure);
+    }
+
+    current_controls = desired_controls;
+}
+
+static void
+on_frame(MPImage image, void *data)
+{
+    // Only update controls right after a frame was captured
+    update_controls();
+
+    // When the mode is switched while capturing we get a couple blank frames,
+    // presumably from buffers made ready during the switch. Ignore these.
+    if (just_switched_mode)
+    {
+        if (blank_frame_count < 20) {
+            // Only check a 50x50 area
+            size_t test_size = MIN(50, image.width) * MIN(50, image.height);
+
+            bool image_is_blank = true;
+            for (size_t i = 0; i < test_size; ++i) {
+                if (image.data[i] != 0) {
+                    image_is_blank = false;
+                }
+            }
+
+            if (image_is_blank) {
+                printf("Skipping blank image\n");
+                ++blank_frame_count;
+                return;
+            }
+        } else {
+            printf("Blank image limit reached, resulting capture may be blank\n");
+        }
+
+        just_switched_mode = false;
+        blank_frame_count = 0;
+    }
+
+    // Copy from the camera buffer
+    size_t size = mp_pixel_format_width_to_bytes(image.pixel_format, image.width) * image.height;
+    uint8_t *buffer = malloc(size);
+    memcpy(buffer, image.data, size);
+
+    image.data = buffer;
+
+    // Send the image off for processing
+    mp_process_pipeline_process_image(image);
+
+    if (captures_remaining > 0) {
+        --captures_remaining;
+
+        if (captures_remaining == 0) {
+            struct camera_info *info = &cameras[camera->index];
+
+            // Restore the auto exposure and gain if needed
+            if (!current_controls.exposure_is_manual) {
+                v4l2_ctrl_set(info->fd, V4L2_CID_EXPOSURE_AUTO, V4L2_EXPOSURE_AUTO);
+            }
+
+            if (!current_controls.gain_is_manual) {
+                v4l2_ctrl_set(info->fd, V4L2_CID_AUTOGAIN, 1);
+            }
+
+            // Go back to preview mode
+            mp_camera_stop_capture(info->camera);
+
+            mode = camera->preview_mode;
+            mp_camera_set_mode(info->camera, &mode);
+            just_switched_mode = true;
+
+            mp_camera_start_capture(info->camera);
+
+            update_process_pipeline();
+        }
+    }
+}
+
+static void
+update_state(MPPipeline *pipeline, const struct mp_io_pipeline_state *state)
+{
+    // Make sure the state isn't updated more than it needs to be by checking
+    // whether this state change actually changes anything.
+    bool has_changed = false;
+
+    if (camera != state->camera) {
+        has_changed = true;
+
+        if (camera) {
+            struct camera_info *info = &cameras[camera->index];
+            struct device_info *dev_info = &devices[info->device_index];
+
+            mp_camera_stop_capture(info->camera);
+            mp_device_setup_link(
+                dev_info->device,
+                info->pad_id,
+                dev_info->interface_pad_id,
+                false);
+        }
+
+        if (capture_source) {
+            g_source_destroy(capture_source);
+            capture_source = NULL;
+        }
+
+        camera = state->camera;
+
+        if (camera) {
+            struct camera_info *info = &cameras[camera->index];
+            struct device_info *dev_info = &devices[info->device_index];
+
+            mp_device_setup_link(
+                dev_info->device,
+                info->pad_id,
+                dev_info->interface_pad_id,
+                true);
+
+            mode = camera->preview_mode;
+            mp_camera_set_mode(info->camera, &mode);
+
+            mp_camera_start_capture(info->camera);
+            capture_source = mp_pipeline_add_capture_source(pipeline, info->camera, on_frame, NULL);
+
+            current_controls.gain_is_manual = v4l2_ctrl_get(info->fd, V4L2_CID_EXPOSURE_AUTO) == V4L2_EXPOSURE_MANUAL;
+            current_controls.gain = v4l2_ctrl_get(info->fd, info->gain_ctrl);
+
+            current_controls.exposure_is_manual = v4l2_ctrl_get(info->fd, V4L2_CID_AUTOGAIN) == 0;
+            current_controls.exposure = v4l2_ctrl_get(info->fd, V4L2_CID_EXPOSURE);
+        }
+    }
+
+    has_changed = has_changed
+        || burst_length != state->burst_length
+        || preview_width != state->preview_width
+        || preview_height != state->preview_height;
+
+    burst_length = state->burst_length;
+    preview_width = state->preview_width;
+    preview_height = state->preview_height;
+
+    if (camera) {
+        struct control_state previous_desired = desired_controls;
+
+        desired_controls.gain_is_manual = state->gain_is_manual;
+        desired_controls.gain = state->gain;
+        desired_controls.exposure_is_manual = state->exposure_is_manual;
+        desired_controls.exposure = state->exposure;
+
+        has_changed = has_changed || memcmp(&previous_desired, &desired_controls, sizeof(struct control_state)) != 0;
+    }
+
+    assert(has_changed);
+
+    update_process_pipeline();
+}
+
+void mp_io_pipeline_update_state(const struct mp_io_pipeline_state *state)
+{
+    mp_pipeline_invoke(pipeline, (MPPipelineCallback)update_state, state, sizeof(struct mp_io_pipeline_state));
+}

+ 26 - 0
io_pipeline.h

@@ -0,0 +1,26 @@
+#pragma once
+
+#include "camera_config.h"
+
+struct mp_io_pipeline_state {
+    const struct mp_camera_config *camera;
+
+    int burst_length;
+
+    int preview_width;
+    int preview_height;
+
+    bool gain_is_manual;
+    int gain;
+
+    bool exposure_is_manual;
+    int exposure;
+};
+
+void mp_io_pipeline_start();
+void mp_io_pipeline_stop();
+
+void mp_io_pipeline_focus();
+void mp_io_pipeline_capture();
+
+void mp_io_pipeline_update_state(const struct mp_io_pipeline_state *state);

File diff suppressed because it is too large
+ 447 - 773
main.c


+ 27 - 0
main.h

@@ -0,0 +1,27 @@
+#pragma once
+
+#include "camera_config.h"
+#include "gtk/gtk.h"
+
+struct mp_main_state {
+    const struct mp_camera_config *camera;
+    MPCameraMode mode;
+
+    bool gain_is_manual;
+    int gain;
+    int gain_max;
+
+    bool exposure_is_manual;
+    int exposure;
+
+    bool has_auto_focus_continuous;
+    bool has_auto_focus_start;
+};
+
+void mp_main_update_state(const struct mp_main_state *state);
+
+void mp_main_set_preview(cairo_surface_t *image);
+void mp_main_capture_completed(const char *fname);
+
+int
+remap(int value, int input_min, int input_max, int output_min, int output_max);

+ 9 - 1
meson.build

@@ -16,7 +16,12 @@ configure_file(
   output: 'config.h',
   configuration: conf )
 
-executable('megapixels', 'main.c', 'ini.c', 'quickpreview.c', 'camera.c', 'device.c', 'pipeline.c', resources, dependencies : [gtkdep, libm, tiff, threads], install : true)
+# Define DEBUG for debug builds only (debugoptimized is not included on this one)
+if get_option('buildtype') == 'debug'
+  add_global_arguments('-DDEBUG', language: 'c')
+endif
+
+executable('megapixels', 'main.c', 'ini.c', 'quickpreview.c', 'camera.c', 'device.c', 'pipeline.c', 'camera_config.c', 'io_pipeline.c', 'process_pipeline.c', resources, dependencies : [gtkdep, libm, tiff, threads], install : true)
 
 install_data(['data/org.postmarketos.Megapixels.desktop'],
              install_dir : get_option('datadir') / 'applications')
@@ -42,3 +47,6 @@ install_data(['postprocess.sh'],
 
 executable('list_devices', 'tools/list_devices.c', 'device.c', dependencies: [gtkdep])
 executable('test_camera', 'tools/test_camera.c', 'camera.c', 'device.c', dependencies: [gtkdep])
+
+test_quickpreview = executable('test_quickpreview', 'tests/test_quickpreview.c', 'quickpreview.c', 'camera.c', dependencies: [gtkdep])
+test('quickpreview', test_quickpreview)

+ 467 - 0
process_pipeline.c

@@ -0,0 +1,467 @@
+#include "process_pipeline.h"
+
+#include "pipeline.h"
+#include "main.h"
+#include "config.h"
+#include "quickpreview.h"
+#include <tiffio.h>
+#include <assert.h>
+#include <math.h>
+#include <wordexp.h>
+#include <gtk/gtk.h>
+
+#define TIFFTAG_FORWARDMATRIX1 50964
+
+static const float colormatrix_srgb[] = {
+    3.2409, -1.5373, -0.4986,
+    -0.9692, 1.8759, 0.0415,
+    0.0556, -0.2039, 1.0569
+};
+
+static MPPipeline *pipeline;
+
+static char burst_dir[23];
+static char processing_script[512];
+
+static volatile bool is_capturing = false;
+static volatile int frames_processed = 0;
+static volatile int frames_received = 0;
+
+static const struct mp_camera_config *camera;
+
+static MPCameraMode mode;
+
+static int burst_length;
+static int captures_remaining = 0;
+
+static int preview_width;
+static int preview_height;
+
+// static bool gain_is_manual;
+static int gain;
+static int gain_max;
+
+static bool exposure_is_manual;
+static int exposure;
+
+static char capture_fname[255];
+
+// static void
+// process_image(const int *p, int size)
+// {
+//     time_t rawtime;
+//     char datetime[20] = {0};
+//     struct tm tim;
+//     uint8_t *pixels;
+//     char fname[255];
+//     char fname_target[255];
+//     char command[1024];
+//     char timestamp[30];
+//     char uniquecameramodel[255];
+//     GdkPixbuf *pixbuf;
+//     GdkPixbuf *pixbufrot;
+//     GdkPixbuf *thumb;
+//     GError *error = NULL;
+//     double scale;
+//     cairo_t *cr;
+//     TIFF *tif;
+//     int skip = 2;
+//     long sub_offset = 0;
+//     uint64 exif_offset = 0;
+//     static const short cfapatterndim[] = {2, 2};
+//     static const float neutral[] = {1.0, 1.0, 1.0};
+//     static uint16_t isospeed[] = {0};
+
+//     // Only process preview frames when not capturing
+//     if (capture == 0) {
+
+//     } else {
+
+
+//         if (capture == 0) {
+
+
+//             // Restore the auto exposure and gain if needed
+//             // if (auto_exposure) {
+//             //     v4l2_ctrl_set(current.fd, V4L2_CID_EXPOSURE_AUTO, V4L2_EXPOSURE_AUTO);
+//             // }
+//             // if (auto_gain) {
+//             //     v4l2_ctrl_set(current.fd, V4L2_CID_AUTOGAIN, 1);
+//             // }
+//         }
+//     }
+// }
+
+static void
+register_custom_tiff_tags(TIFF *tif)
+{
+    static const TIFFFieldInfo custom_fields[] = {
+        {TIFFTAG_FORWARDMATRIX1, -1, -1, TIFF_SRATIONAL, FIELD_CUSTOM, 1, 1, "ForwardMatrix1"},
+    };
+
+    // Add missing dng fields
+    TIFFMergeFieldInfo(tif, custom_fields, sizeof(custom_fields) / sizeof(custom_fields[0]));
+}
+
+static bool
+find_processor(char *script)
+{
+    char *xdg_config_home;
+    char filename[] = "postprocess.sh";
+    wordexp_t exp_result;
+
+    // Resolve XDG stuff
+    if ((xdg_config_home = getenv("XDG_CONFIG_HOME")) == NULL) {
+        xdg_config_home = "~/.config";
+    }
+    wordexp(xdg_config_home, &exp_result, 0);
+    xdg_config_home = strdup(exp_result.we_wordv[0]);
+    wordfree(&exp_result);
+
+    // Check postprocess.h in the current working directory
+    sprintf(script, "%s", filename);
+    if(access(script, F_OK) != -1) {
+        sprintf(script, "./%s", filename);
+        printf("Found postprocessor script at %s\n", script);
+        return true;
+    }
+
+    // Check for a script in XDG_CONFIG_HOME
+    sprintf(script, "%s/megapixels/%s", xdg_config_home, filename);
+    if(access(script, F_OK) != -1) {
+        printf("Found postprocessor script at %s\n", script);
+        return true;
+    }
+
+    // Check user overridden /etc/megapixels/postprocessor.sh
+    sprintf(script, "%s/megapixels/%s", SYSCONFDIR, filename);
+    if(access(script, F_OK) != -1) {
+        printf("Found postprocessor script at %s\n", script);
+        return true;
+    }
+
+    // Check packaged /usr/share/megapixels/postprocessor.sh
+    sprintf(script, "%s/megapixels/%s", DATADIR, filename);
+    if(access(script, F_OK) != -1) {
+        printf("Found postprocessor script at %s\n", script);
+        return true;
+    }
+
+    return false;
+}
+
+static void setup(MPPipeline *pipeline, const void *data)
+{
+    TIFFSetTagExtender(register_custom_tiff_tags);
+
+    if (!find_processor(processing_script)) {
+        g_printerr("Could not find any post-process script\n");
+        exit(1);
+    }
+}
+
+void mp_process_pipeline_start()
+{
+    pipeline = mp_pipeline_new();
+
+    mp_pipeline_invoke(pipeline, setup, NULL, 0);
+}
+
+void mp_process_pipeline_stop()
+{
+    mp_pipeline_free(pipeline);
+}
+
+static void
+process_image_for_preview(const MPImage *image)
+{
+    uint32_t surface_width, surface_height, skip;
+    quick_preview_size(
+        &surface_width,
+        &surface_height,
+        &skip,
+        preview_width,
+        preview_height,
+        image->width,
+        image->height,
+        image->pixel_format,
+        camera->rotate);
+
+    cairo_surface_t *surface = cairo_image_surface_create(
+        CAIRO_FORMAT_RGB24,
+        surface_width,
+        surface_height);
+
+    uint8_t *pixels = cairo_image_surface_get_data(surface);
+
+    quick_preview(
+        (uint32_t *)pixels,
+        surface_width,
+        surface_height,
+        image->data,
+        image->width,
+        image->height,
+        image->pixel_format,
+        camera->rotate,
+        camera->mirrored,
+        camera->colormatrix[0] == 0 ? NULL : camera->colormatrix,
+        camera->blacklevel,
+        skip);
+
+    mp_main_set_preview(surface);
+}
+
+static void
+process_image_for_capture(const MPImage *image, int count)
+{
+    time_t rawtime;
+    time(&rawtime);
+    struct tm tim = *(localtime(&rawtime));
+
+    char datetime[20] = {0};
+    strftime(datetime, 20, "%Y:%m:%d %H:%M:%S", &tim);
+
+    char fname[255];
+    sprintf(fname, "%s/%d.dng", burst_dir, count);
+
+    TIFF *tif = TIFFOpen(fname, "w");
+    if(!tif) {
+        printf("Could not open tiff\n");
+    }
+
+    // Define TIFF thumbnail
+    TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 1);
+    TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, image->width >> 4);
+    TIFFSetField(tif, TIFFTAG_IMAGELENGTH, image->height >> 4);
+    TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
+    TIFFSetField(tif, TIFFTAG_COMPRESSION, COMPRESSION_NONE);
+    TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
+    TIFFSetField(tif, TIFFTAG_MAKE, mp_get_device_make());
+    TIFFSetField(tif, TIFFTAG_MODEL, mp_get_device_model());
+    uint16_t orientation;
+    if (camera->rotate == 0) {
+        orientation = camera->mirrored ? ORIENTATION_TOPRIGHT : ORIENTATION_TOPLEFT;
+    } else if (camera->rotate == 90) {
+        orientation = camera->mirrored ? ORIENTATION_RIGHTBOT : ORIENTATION_LEFTBOT;
+    } else if (camera->rotate == 180) {
+        orientation = camera->mirrored ? ORIENTATION_BOTLEFT : ORIENTATION_BOTRIGHT;
+    } else {
+        orientation = camera->mirrored ? ORIENTATION_LEFTTOP : ORIENTATION_RIGHTTOP;
+    }
+    TIFFSetField(tif, TIFFTAG_ORIENTATION, orientation);
+    TIFFSetField(tif, TIFFTAG_DATETIME, datetime);
+    TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 3);
+    TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
+    TIFFSetField(tif, TIFFTAG_SOFTWARE, "Megapixels");
+    long sub_offset = 0;
+    TIFFSetField(tif, TIFFTAG_SUBIFD, 1, &sub_offset);
+    TIFFSetField(tif, TIFFTAG_DNGVERSION, "\001\001\0\0");
+    TIFFSetField(tif, TIFFTAG_DNGBACKWARDVERSION, "\001\0\0\0");
+    char uniquecameramodel[255];
+    sprintf(uniquecameramodel, "%s %s", mp_get_device_make(), mp_get_device_model());
+    TIFFSetField(tif, TIFFTAG_UNIQUECAMERAMODEL, uniquecameramodel);
+    if(camera->colormatrix[0]) {
+        TIFFSetField(tif, TIFFTAG_COLORMATRIX1, 9, camera->colormatrix);
+    } else {
+        TIFFSetField(tif, TIFFTAG_COLORMATRIX1, 9, colormatrix_srgb);
+    }
+    if(camera->forwardmatrix[0]) {
+        TIFFSetField(tif, TIFFTAG_FORWARDMATRIX1, 9, camera->forwardmatrix);
+    }
+    static const float neutral[] = {1.0, 1.0, 1.0};
+    TIFFSetField(tif, TIFFTAG_ASSHOTNEUTRAL, 3, neutral);
+    TIFFSetField(tif, TIFFTAG_CALIBRATIONILLUMINANT1, 21);
+    // Write black thumbnail, only windows uses this
+    {
+        unsigned char *buf = (unsigned char *)calloc(1, (int)image->width >> 4);
+        for (int row = 0; row < image->height>>4; row++) {
+            TIFFWriteScanline(tif, buf, row, 0);
+        }
+        free(buf);
+    }
+    TIFFWriteDirectory(tif);
+
+    // Define main photo
+    TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 0);
+    TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, image->width);
+    TIFFSetField(tif, TIFFTAG_IMAGELENGTH, image->height);
+    TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8);
+    TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_CFA);
+    TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1);
+    TIFFSetField(tif, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
+    static const short cfapatterndim[] = {2, 2};
+    TIFFSetField(tif, TIFFTAG_CFAREPEATPATTERNDIM, cfapatterndim);
+    TIFFSetField(tif, TIFFTAG_CFAPATTERN, "\002\001\001\000"); // BGGR
+    if(camera->whitelevel) {
+        TIFFSetField(tif, TIFFTAG_WHITELEVEL, 1, &camera->whitelevel);
+    }
+    if(camera->blacklevel) {
+        TIFFSetField(tif, TIFFTAG_BLACKLEVEL, 1, &camera->blacklevel);
+    }
+    TIFFCheckpointDirectory(tif);
+    printf("Writing frame to %s\n", fname);
+
+    unsigned char *pLine = (unsigned char*)malloc(image->width);
+    for(int row = 0; row < image->height; row++){
+        TIFFWriteScanline(tif, image->data + (row * image->width), row, 0);
+    }
+    free(pLine);
+    TIFFWriteDirectory(tif);
+
+    // Add an EXIF block to the tiff
+    TIFFCreateEXIFDirectory(tif);
+    // 1 = manual, 2 = full auto, 3 = aperture priority, 4 = shutter priority
+    if (!exposure_is_manual) {
+        TIFFSetField(tif, EXIFTAG_EXPOSUREPROGRAM, 2);
+    } else {
+        TIFFSetField(tif, EXIFTAG_EXPOSUREPROGRAM, 1);
+    }
+
+    TIFFSetField(tif, EXIFTAG_EXPOSURETIME, (mode.frame_interval.numerator / (float)mode.frame_interval.denominator) / ((float)image->height / (float)exposure));
+    uint16_t isospeed[1];
+    isospeed[0] = (uint16_t)remap(gain - 1, 0, gain_max, camera->iso_min, camera->iso_max);
+    TIFFSetField(tif, EXIFTAG_ISOSPEEDRATINGS, 1, isospeed);
+    TIFFSetField(tif, EXIFTAG_FLASH, 0);
+
+    TIFFSetField(tif, EXIFTAG_DATETIMEORIGINAL, datetime);
+    TIFFSetField(tif, EXIFTAG_DATETIMEDIGITIZED, datetime);
+    if(camera->fnumber) {
+        TIFFSetField(tif, EXIFTAG_FNUMBER, camera->fnumber);
+    }
+    if(camera->focallength) {
+        TIFFSetField(tif, EXIFTAG_FOCALLENGTH, camera->focallength);
+    }
+    if(camera->focallength && camera->cropfactor) {
+        TIFFSetField(tif, EXIFTAG_FOCALLENGTHIN35MMFILM, (short)(camera->focallength * camera->cropfactor));
+    }
+    uint64_t exif_offset = 0;
+    TIFFWriteCustomDirectory(tif, &exif_offset);
+    TIFFFreeDirectory(tif);
+
+    // Update exif pointer
+    TIFFSetDirectory(tif, 0);
+    TIFFSetField(tif, TIFFTAG_EXIFIFD, exif_offset);
+    TIFFRewriteDirectory(tif);
+
+    TIFFClose(tif);
+}
+
+static void
+process_capture_burst()
+{
+    time_t rawtime;
+    time(&rawtime);
+    struct tm tim = *(localtime(&rawtime));
+
+    char timestamp[30];
+    strftime(timestamp, 30, "%Y%m%d%H%M%S", &tim);
+
+    sprintf(capture_fname, "%s/Pictures/IMG%s", getenv("HOME"), timestamp);
+
+    // Start post-processing the captured burst
+    g_print("Post process %s to %s.ext\n", burst_dir, capture_fname);
+    char command[1024];
+    sprintf(command, "%s %s %s &", processing_script, burst_dir, capture_fname);
+    system(command);
+}
+
+static void
+process_image(MPPipeline *pipeline, const MPImage *image)
+{
+    assert(image->width == mode.width && image->height == mode.height);
+
+    process_image_for_preview(image);
+
+    if (captures_remaining > 0) {
+        int count = burst_length - captures_remaining;
+        --captures_remaining;
+
+        process_image_for_capture(image, count);
+
+        if (captures_remaining == 0) {
+            process_capture_burst();
+
+            mp_main_capture_completed(capture_fname);
+        }
+    }
+
+    free(image->data);
+
+    ++frames_processed;
+    if (captures_remaining == 0) {
+        is_capturing = false;
+    }
+}
+
+void mp_process_pipeline_process_image(MPImage image)
+{
+    // If we haven't processed the previous frame yet, drop this one
+    if (frames_received != frames_processed && !is_capturing) {
+        printf("Dropped frame at capture %d %d\n", frames_received, frames_processed);
+        return;
+    }
+
+    ++frames_received;
+
+    mp_pipeline_invoke(pipeline, (MPPipelineCallback)process_image, &image, sizeof(MPImage));
+}
+
+static void capture()
+{
+    char template[] = "/tmp/megapixels.XXXXXX";
+    char *tempdir;
+    tempdir = mkdtemp(template);
+
+    if (tempdir == NULL) {
+        g_printerr("Could not make capture directory %s\n", template);
+        exit (EXIT_FAILURE);
+    }
+
+    strcpy(burst_dir, tempdir);
+
+    captures_remaining = burst_length;
+}
+
+void mp_process_pipeline_capture()
+{
+    is_capturing = true;
+
+    mp_pipeline_invoke(pipeline, capture, NULL, 0);
+}
+
+static void
+update_state(MPPipeline *pipeline, const struct mp_process_pipeline_state *state)
+{
+    camera = state->camera;
+    mode = state->mode;
+
+    burst_length = state->burst_length;
+
+    preview_width = state->preview_width;
+    preview_height = state->preview_height;
+
+    // gain_is_manual = state->gain_is_manual;
+    gain = state->gain;
+    gain_max = state->gain_max;
+
+    exposure_is_manual = state->exposure_is_manual;
+    exposure = state->exposure;
+
+    struct mp_main_state main_state = {
+        .camera = camera,
+        .mode = mode,
+        .gain_is_manual = state->gain_is_manual,
+        .gain = gain,
+        .gain_max = gain_max,
+        .exposure_is_manual = exposure_is_manual,
+        .exposure = exposure,
+        .has_auto_focus_continuous = state->has_auto_focus_continuous,
+        .has_auto_focus_start = state->has_auto_focus_start,
+    };
+    mp_main_update_state(&main_state);
+}
+
+void mp_process_pipeline_update_state(const struct mp_process_pipeline_state *new_state)
+{
+    mp_pipeline_invoke(pipeline, (MPPipelineCallback)update_state, new_state, sizeof(struct mp_process_pipeline_state));
+}

+ 30 - 0
process_pipeline.h

@@ -0,0 +1,30 @@
+#pragma once
+
+#include "camera_config.h"
+
+struct mp_process_pipeline_state {
+    const struct mp_camera_config *camera;
+    MPCameraMode mode;
+
+    int burst_length;
+
+    int preview_width;
+    int preview_height;
+
+    bool gain_is_manual;
+    int gain;
+    int gain_max;
+
+    bool exposure_is_manual;
+    int exposure;
+
+    bool has_auto_focus_continuous;
+    bool has_auto_focus_start;
+};
+
+void mp_process_pipeline_start();
+void mp_process_pipeline_stop();
+
+void mp_process_pipeline_process_image(MPImage image);
+void mp_process_pipeline_capture();
+void mp_process_pipeline_update_state(const struct mp_process_pipeline_state *state);

+ 1 - 1
tools/test_camera.c

@@ -16,7 +16,7 @@ double get_time()
 
 void on_capture(MPImage image, void *user_data)
 {
-    size_t num_bytes = mp_pixel_format_bytes_per_pixel(image.pixel_format) * image.width * image.height;
+    size_t num_bytes = mp_pixel_format_width_to_bytes(image.pixel_format, image.width) * image.height;
     uint8_t *data = malloc(num_bytes);
     memcpy(data, image.data, num_bytes);
 

Some files were not shown because too many files changed in this diff