diff --git a/camera.css b/camera.css deleted file mode 100644 index e897a85..0000000 --- a/camera.css +++ /dev/null @@ -1,12 +0,0 @@ -.black { - background: #000000; -} - -.errorbox { - background: #dd0000; - color: #ffffff; -} - -.controlbox { - background: rgba(0,0,0,0.2); -} diff --git a/camera.glade b/camera.glade deleted file mode 100644 index 8fb7e67..0000000 --- a/camera.glade +++ /dev/null @@ -1,592 +0,0 @@ - - - - - - 100 - 1 - 10 - - - False - Camera - 360 - 640 - - - True - False - - - True - False - vertical - - - True - False - vertical - - - True - False - - - True - True - 0 - - - - - - True - True - 0 - - - - - False - - - True - False - 10 - 10 - 10 - 10 - - - True - False - 10 - ISO - - - False - True - 0 - - - - - True - True - control_adj - 1 - False - - - True - True - 1 - - - - - Auto - True - True - True - 10 - - - False - True - end - 2 - - - - - True - True - 0 - - - - - - False - True - 1 - - - - - True - False - - - 48 - 48 - True - True - True - True - - - True - False - /org/postmarketos/Megapixels/shutter-button.svg - - - - - - False - True - 2 - - - - - True - False - 8 - 8 - 10 - - - True - True - True - - - True - False - /org/postmarketos/Megapixels/settings-symbolic.svg - - - - - False - True - 0 - - - - - True - True - True - - - True - False - /org/postmarketos/Megapixels/switch-camera.svg - - - - - False - True - 1 - - - - - - - - True - True - 10 - 0 - - - - - True - False - 8 - 8 - 10 - - - - - - True - True - True - - - True - False - /org/postmarketos/Megapixels/folder-symbolic.svg - - - - - False - True - end - 1 - - - - - True - True - True - - - True - False - - - 24 - 24 - True - False - - - - - 24 - 24 - True - False - - - - - - - False - True - end - 2 - - - - - True - True - 10 - end - 1 - - - - - False - True - 10 - end - 1 - - - - - False - - - True - False - 10 - 10 - 10 - 10 - 10 - - - True - False - start - No error - - - True - True - 0 - - - - - gtk-close - True - True - True - True - True - - - False - True - 1 - - - - - True - True - 0 - - - - - - False - True - 2 - - - - - main - page0 - - - - - True - True - in - - - True - False - none - - - True - False - 10 - 10 - 10 - 10 - vertical - 10 - - - True - False - - - Back - True - True - True - 10 - 10 - - - - False - True - 0 - - - - - True - False - Settings aren't functional yet - - - False - True - 1 - - - - - False - True - 0 - - - - - True - False - vertical - 4 - - - True - False - start - Photos - - - - False - True - 0 - - - - - True - False - 0 - in - - - True - False - 12 - - - True - False - vertical - 6 - - - True - False - start - Resolution - - - False - True - 0 - - - - - True - False - - - False - True - 1 - - - - - - - - - - - True - False - start - Storage mode - - - False - True - 4 - - - - - True - False - vertical - - - Debayer with VNG (slowest) - True - True - False - True - True - - - False - True - 0 - - - - - Debayer with linear interpolation - True - True - False - True - store_vng - - - False - True - 1 - - - - - Raw - True - True - False - True - store_vng - - - False - True - 2 - - - - - False - True - 5 - - - - - - - - - - - - - False - True - 1 - - - - - False - True - 1 - - - - - - - - - - - - settings - page1 - 1 - - - - - - diff --git a/config/pine64,pinephone-1.0.ini b/config/pine64,pinephone-1.0.ini index cafbb77..901ccc7 100644 --- a/config/pine64,pinephone-1.0.ini +++ b/config/pine64,pinephone-1.0.ini @@ -11,7 +11,7 @@ capture-rate=10 capture-fmt=BGGR8 preview-width=1280 preview-height=720 -preview-rate=20 +preview-rate=30 preview-fmt=BGGR8 rotate=270 colormatrix=1.384,-0.3203,-0.0124,-0.2728,1.049,0.1556,-0.0506,0.2577,0.8050 @@ -29,11 +29,11 @@ driver=gc2145 media-driver=sun6i-csi capture-width=1280 capture-height=960 -capture-rate=30 +capture-rate=60 capture-fmt=BGGR8 preview-width=1280 preview-height=960 -preview-rate=30 +preview-rate=60 preview-fmt=BGGR8 rotate=90 mirrored=true diff --git a/config/pine64,pinephone-1.1.ini b/config/pine64,pinephone-1.1.ini index cafbb77..901ccc7 100644 --- a/config/pine64,pinephone-1.1.ini +++ b/config/pine64,pinephone-1.1.ini @@ -11,7 +11,7 @@ capture-rate=10 capture-fmt=BGGR8 preview-width=1280 preview-height=720 -preview-rate=20 +preview-rate=30 preview-fmt=BGGR8 rotate=270 colormatrix=1.384,-0.3203,-0.0124,-0.2728,1.049,0.1556,-0.0506,0.2577,0.8050 @@ -29,11 +29,11 @@ driver=gc2145 media-driver=sun6i-csi capture-width=1280 capture-height=960 -capture-rate=30 +capture-rate=60 capture-fmt=BGGR8 preview-width=1280 preview-height=960 -preview-rate=30 +preview-rate=60 preview-fmt=BGGR8 rotate=90 mirrored=true diff --git a/config/pine64,pinephone-1.2.ini b/config/pine64,pinephone-1.2.ini index 74ce9d2..e295f87 100644 --- a/config/pine64,pinephone-1.2.ini +++ b/config/pine64,pinephone-1.2.ini @@ -11,7 +11,7 @@ capture-rate=10 capture-fmt=BGGR8 preview-width=1280 preview-height=720 -preview-rate=20 +preview-rate=30 preview-fmt=BGGR8 rotate=270 mirrored=false @@ -30,11 +30,11 @@ driver=gc2145 media-driver=sun6i-csi capture-width=1280 capture-height=960 -capture-rate=30 +capture-rate=60 capture-fmt=BGGR8 preview-width=1280 preview-height=960 -preview-rate=30 +preview-rate=60 preview-fmt=BGGR8 rotate=90 mirrored=true diff --git a/data/blit.frag b/data/blit.frag new file mode 100644 index 0000000..58df011 --- /dev/null +++ b/data/blit.frag @@ -0,0 +1,11 @@ +#ifdef GL_ES +precision mediump float; +#endif + +uniform sampler2D texture; + +varying vec2 uv; + +void main() { + gl_FragColor = vec4(texture2D(texture, uv).rgb, 1); +} diff --git a/data/blit.vert b/data/blit.vert new file mode 100644 index 0000000..e6e75d4 --- /dev/null +++ b/data/blit.vert @@ -0,0 +1,16 @@ +#ifdef GL_ES +precision mediump float; +#endif + +attribute vec2 vert; +attribute vec2 tex_coord; + +uniform mat3 transform; + +varying vec2 uv; + +void main() { + uv = tex_coord; + + gl_Position = vec4(transform * vec3(vert, 1), 1); +} diff --git a/data/camera.css b/data/camera.css new file mode 100644 index 0000000..85c65e3 --- /dev/null +++ b/data/camera.css @@ -0,0 +1,13 @@ +.errorbox { + background: #dd0000; + color: #ffffff; +} + +.controlbox { + background: rgba(0, 0, 0, 0.2); +} + +.button-overlay { + opacity: 0.9; + background-color: rgba(0, 0, 0, 0.2); +} diff --git a/data/camera.ui b/data/camera.ui new file mode 100644 index 0000000..23dd058 --- /dev/null +++ b/data/camera.ui @@ -0,0 +1,250 @@ + + + + + 0 + 360 + 640 + 0 + + + 0 + + + main + + + 0 + + + 1 + 0 + 1 + + + + + vertical + fill + start + 5 + 0 + + + + 1 + 1 + start + 5 + 5 + 5 + 5 + 0 + 5 + + + start + ISO + + + + + + start + shutter-symbolic + + + + + + + + 0 + start + 5 + 5 + 5 + 5 + + + horizontal + 10 + + + Error + + + + + dialog-warning-symbolic + + + + + + + + + + + + + horizontal + fill + end + + + + 1 + 1 + center + center + 5 + 5 + 5 + 5 + 5 + + + app.open-settings + settings-symbolic + + + + + app.switch-camera + switch-camera-symbolic + + + + + + + 5 + 5 + app.capture + + + 60 + 0 + shutter-button-symbolic + + + + + + + + + 1 + 1 + center + center + 5 + 5 + 5 + 5 + 5 + + + app.open-last + + + 0 + + + 24 + 24 + 0 + + + + + 24 + 24 + 0 + + + + + + + + + app.open-photos + folder-symbolic + + + + + + + + + + + + + + settings + page1 + + + + + 0 + + + 0 + 10 + 10 + 10 + 10 + vertical + 10 + + + 0 + + + Back + 10 + 10 + app.close-settings + + + + + + 0 + Settings aren't functional yet + + + + + + + + + + + + + + + + diff --git a/data/controls-popover.ui b/data/controls-popover.ui new file mode 100644 index 0000000..140ef1a --- /dev/null +++ b/data/controls-popover.ui @@ -0,0 +1,42 @@ + + + + + + + vertical + 1 + 5 + + + + + + + horizontal + 1 + 5 + + + horizontal + 0 + 150 + + + + + + + + + center + auto + 5 + + + + + + + + diff --git a/data/debayer.frag b/data/debayer.frag new file mode 100644 index 0000000..9a98c10 --- /dev/null +++ b/data/debayer.frag @@ -0,0 +1,34 @@ +#ifdef GL_ES +precision mediump float; +#endif + +uniform sampler2D texture; +uniform mat3 color_matrix; + +varying vec2 top_left_uv; +varying vec2 top_right_uv; +varying vec2 bottom_left_uv; +varying vec2 bottom_right_uv; + +void main() { + // Note the coordinates for texture samples need to be a varying, as the + // Mali-400 has this as a fast path allowing 32-bit floats. Otherwise + // they end up as 16-bit floats and that's not accurate enough. + vec4 samples = vec4( + texture2D(texture, top_left_uv).r, + texture2D(texture, top_right_uv).r, + texture2D(texture, bottom_left_uv).r, + texture2D(texture, bottom_right_uv).r); + + // Assume BGGR for now. Currently this just takes 3 of the four samples + // for each pixel, there's room here to do some better debayering. + vec3 color = vec3(samples.w, (samples.y + samples.w) / 2.0, samples.x); + + // Fast SRGB estimate. See https://mimosa-pudica.net/fast-gamma/ + vec3 srgb_color = (vec3(1.138) * inversesqrt(color) - vec3(0.138)) * color; + + // Slow SRGB estimate + // vec3 srgb_color = pow(color, vec3(1.0 / 2.2)); + + gl_FragColor = vec4(color_matrix * srgb_color, 1); +} diff --git a/data/debayer.vert b/data/debayer.vert new file mode 100644 index 0000000..49af8c1 --- /dev/null +++ b/data/debayer.vert @@ -0,0 +1,23 @@ +#ifdef GL_ES +precision mediump float; +#endif + +attribute vec2 vert; +attribute vec2 tex_coord; + +uniform mat3 transform; +uniform vec2 pixel_size; + +varying vec2 top_left_uv; +varying vec2 top_right_uv; +varying vec2 bottom_left_uv; +varying vec2 bottom_right_uv; + +void main() { + top_left_uv = tex_coord - pixel_size / 2.0; + bottom_right_uv = tex_coord + pixel_size / 2.0; + top_right_uv = vec2(top_left_uv.x, bottom_right_uv.y); + bottom_left_uv = vec2(bottom_right_uv.x, top_left_uv.y); + + gl_Position = vec4(transform * vec3(vert, 1), 1); +} diff --git a/folder-symbolic.svg b/data/folder-symbolic.svg similarity index 100% rename from folder-symbolic.svg rename to data/folder-symbolic.svg diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 0000000..c0de055 --- /dev/null +++ b/data/meson.build @@ -0,0 +1,15 @@ +resources = gnome.compile_resources('megapixels-resources', + 'org.postmarketos.Megapixels.gresource.xml') + +install_data(['org.postmarketos.Megapixels.desktop'], + install_dir: get_option('datadir') / 'applications') + +install_data(['org.postmarketos.Megapixels.metainfo.xml'], + install_dir: get_option('datadir') / 'metainfo') + +install_data('org.postmarketos.Megapixels.svg', + install_dir: join_paths(get_option('datadir'), 'icons/hicolor/scalable/apps')) + +install_data(['postprocess.sh'], + install_dir: get_option('datadir') / 'megapixels/', + install_mode: 'rwxr-xr-x') diff --git a/data/org.postmarketos.Megapixels.gresource.xml b/data/org.postmarketos.Megapixels.gresource.xml new file mode 100644 index 0000000..0243024 --- /dev/null +++ b/data/org.postmarketos.Megapixels.gresource.xml @@ -0,0 +1,19 @@ + + + + camera.ui + controls-popover.ui + camera.css + switch-camera-symbolic.svg + shutter-button-symbolic.svg + folder-symbolic.svg + settings-symbolic.svg + shutter-symbolic.svg + blit.vert + blit.frag + solid.vert + solid.frag + debayer.vert + debayer.frag + + diff --git a/postprocess.sh b/data/postprocess.sh similarity index 100% rename from postprocess.sh rename to data/postprocess.sh diff --git a/settings-symbolic.svg b/data/settings-symbolic.svg similarity index 100% rename from settings-symbolic.svg rename to data/settings-symbolic.svg diff --git a/shutter-button.svg b/data/shutter-button-symbolic.svg similarity index 100% rename from shutter-button.svg rename to data/shutter-button-symbolic.svg diff --git a/data/shutter-symbolic.svg b/data/shutter-symbolic.svg new file mode 100644 index 0000000..bcfec02 --- /dev/null +++ b/data/shutter-symbolic.svg @@ -0,0 +1,32 @@ + + + + + + + image/svg+xml + + + + + + + diff --git a/data/solid.frag b/data/solid.frag new file mode 100644 index 0000000..bb167f7 --- /dev/null +++ b/data/solid.frag @@ -0,0 +1,9 @@ +#ifdef GL_ES +precision mediump float; +#endif + +uniform vec4 color; + +void main() { + gl_FragColor = color; +} diff --git a/data/solid.vert b/data/solid.vert new file mode 100644 index 0000000..f578403 --- /dev/null +++ b/data/solid.vert @@ -0,0 +1,9 @@ +#ifdef GL_ES +precision mediump float; +#endif + +attribute vec2 vert; + +void main() { + gl_Position = vec4(vert, 0, 1); +} diff --git a/switch-camera.svg b/data/switch-camera-symbolic.svg similarity index 100% rename from switch-camera.svg rename to data/switch-camera-symbolic.svg diff --git a/main.c b/main.c deleted file mode 100644 index 63471bc..0000000 --- a/main.c +++ /dev/null @@ -1,792 +0,0 @@ -#include "main.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "camera_config.h" -#include "quickpreview.h" -#include "io_pipeline.h" - -enum user_control { USER_CONTROL_ISO, USER_CONTROL_SHUTTER }; - -static bool camera_is_initialized = false; -static const struct mp_camera_config *camera = NULL; -static MPCameraMode mode; - -static int preview_width = -1; -static int preview_height = -1; - -static bool gain_is_manual = false; -static int gain; -static int gain_max; - -static bool exposure_is_manual = false; -static int exposure; - -static bool has_auto_focus_continuous; -static bool has_auto_focus_start; - -static cairo_surface_t *surface = NULL; -static cairo_surface_t *status_surface = NULL; -static char last_path[260] = ""; - -static MPZBarScanResult *zbar_result = NULL; - -static int burst_length = 3; - -static enum user_control current_control; - -// Widgets -GtkWidget *preview; -GtkWidget *error_box; -GtkWidget *error_message; -GtkWidget *main_stack; -GtkWidget *open_last_stack; -GtkWidget *thumb_last; -GtkWidget *process_spinner; -GtkWidget *control_box; -GtkWidget *control_name; -GtkAdjustment *control_slider; -GtkWidget *control_auto; - -int -remap(int value, int input_min, int input_max, int output_min, int output_max) -{ - const long long factor = 1000000000; - long long output_spread = output_max - output_min; - long long input_spread = input_max - input_min; - - long long zero_value = value - input_min; - zero_value *= factor; - long long percentage = zero_value / input_spread; - - long long zero_output = percentage * output_spread / factor; - - long long result = output_min + zero_output; - return (int)result; -} - -static void -update_io_pipeline() -{ - struct mp_io_pipeline_state io_state = { - .camera = camera, - .burst_length = burst_length, - .preview_width = preview_width, - .preview_height = preview_height, - .gain_is_manual = gain_is_manual, - .gain = gain, - .exposure_is_manual = exposure_is_manual, - .exposure = exposure, - }; - mp_io_pipeline_update_state(&io_state); -} - -static bool -update_state(const struct mp_main_state *state) -{ - if (!camera_is_initialized) { - camera_is_initialized = true; - } - - if (camera == state->camera) { - mode = state->mode; - - if (!gain_is_manual) { - gain = state->gain; - } - gain_max = state->gain_max; - - if (!exposure_is_manual) { - exposure = state->exposure; - } - - has_auto_focus_continuous = state->has_auto_focus_continuous; - has_auto_focus_start = state->has_auto_focus_start; - } - - return false; -} - -void -mp_main_update_state(const struct mp_main_state *state) -{ - struct mp_main_state *state_copy = malloc(sizeof(struct mp_main_state)); - *state_copy = *state; - - g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, - (GSourceFunc)update_state, state_copy, free); -} - -static bool set_zbar_result(MPZBarScanResult *result) -{ - if (zbar_result) { - for (uint8_t i = 0; i < zbar_result->size; ++i) { - free(zbar_result->codes[i].data); - } - - free(zbar_result); - } - - zbar_result = result; - gtk_widget_queue_draw(preview); - - return false; -} - -void mp_main_set_zbar_result(MPZBarScanResult *result) -{ - g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, - (GSourceFunc)set_zbar_result, result, NULL); -} - -static bool -set_preview(cairo_surface_t *image) -{ - if (surface) { - cairo_surface_destroy(surface); - } - surface = image; - gtk_widget_queue_draw(preview); - return false; -} - -void -mp_main_set_preview(cairo_surface_t *image) -{ - g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, - (GSourceFunc)set_preview, image, NULL); -} - -static void transform_centered(cairo_t *cr, uint32_t dst_width, uint32_t dst_height, - int src_width, int src_height) -{ - cairo_translate(cr, dst_width / 2, dst_height / 2); - - double scale = MIN(dst_width / (double)src_width, dst_height / (double)src_height); - cairo_scale(cr, scale, scale); - - cairo_translate(cr, -src_width / 2, -src_height / 2); -} - -void -draw_surface_scaled_centered(cairo_t *cr, uint32_t dst_width, uint32_t dst_height, - cairo_surface_t *surface) -{ - cairo_save(cr); - - int width = cairo_image_surface_get_width(surface); - int height = cairo_image_surface_get_height(surface); - transform_centered(cr, dst_width, dst_height, width, height); - - cairo_set_source_surface(cr, surface, 0, 0); - cairo_paint(cr); - cairo_restore(cr); -} - -struct capture_completed_args { - cairo_surface_t *thumb; - char *fname; -}; - -static bool -capture_completed(struct capture_completed_args *args) -{ - strncpy(last_path, args->fname, 259); - - gtk_image_set_from_surface(GTK_IMAGE(thumb_last), args->thumb); - - gtk_spinner_stop(GTK_SPINNER(process_spinner)); - gtk_stack_set_visible_child(GTK_STACK(open_last_stack), thumb_last); - - cairo_surface_destroy(args->thumb); - g_free(args->fname); - - return false; -} - -void -mp_main_capture_completed(cairo_surface_t *thumb, const char *fname) -{ - struct capture_completed_args *args = malloc(sizeof(struct capture_completed_args)); - args->thumb = thumb; - args->fname = g_strdup(fname); - g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, - (GSourceFunc)capture_completed, args, free); -} - -static void -draw_controls() -{ - cairo_t *cr; - char iso[6]; - int temp; - char shutterangle[6]; - - if (exposure_is_manual) { - temp = (int)((float)exposure / (float)camera->capture_mode.height * - 360); - sprintf(shutterangle, "%d\u00b0", temp); - } else { - sprintf(shutterangle, "auto"); - } - - if (gain_is_manual) { - temp = remap(gain - 1, 0, gain_max, camera->iso_min, - camera->iso_max); - sprintf(iso, "%d", temp); - } else { - sprintf(iso, "auto"); - } - - if (status_surface) - cairo_surface_destroy(status_surface); - - // Make a service to show status of controls, 32px high - if (gtk_widget_get_window(preview) == NULL) { - return; - } - status_surface = - gdk_window_create_similar_surface(gtk_widget_get_window(preview), - CAIRO_CONTENT_COLOR_ALPHA, - preview_width, 32); - - cr = cairo_create(status_surface); - cairo_set_source_rgba(cr, 0, 0, 0, 0.0); - cairo_paint(cr); - - // Draw the outlines for the headings - cairo_select_font_face(cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, - CAIRO_FONT_WEIGHT_BOLD); - cairo_set_font_size(cr, 9); - cairo_set_source_rgba(cr, 0, 0, 0, 1); - - cairo_move_to(cr, 16, 16); - cairo_text_path(cr, "ISO"); - cairo_stroke(cr); - - cairo_move_to(cr, 60, 16); - cairo_text_path(cr, "Shutter"); - cairo_stroke(cr); - - // Draw the fill for the headings - cairo_set_source_rgba(cr, 1, 1, 1, 1); - cairo_move_to(cr, 16, 16); - cairo_show_text(cr, "ISO"); - cairo_move_to(cr, 60, 16); - cairo_show_text(cr, "Shutter"); - - // Draw the outlines for the values - cairo_select_font_face(cr, "sans-serif", CAIRO_FONT_SLANT_NORMAL, - CAIRO_FONT_WEIGHT_NORMAL); - cairo_set_font_size(cr, 11); - cairo_set_source_rgba(cr, 0, 0, 0, 1); - - cairo_move_to(cr, 16, 26); - cairo_text_path(cr, iso); - cairo_stroke(cr); - - cairo_move_to(cr, 60, 26); - cairo_text_path(cr, shutterangle); - cairo_stroke(cr); - - // Draw the fill for the values - cairo_set_source_rgba(cr, 1, 1, 1, 1); - cairo_move_to(cr, 16, 26); - cairo_show_text(cr, iso); - cairo_move_to(cr, 60, 26); - cairo_show_text(cr, shutterangle); - - cairo_destroy(cr); - - gtk_widget_queue_draw_area(preview, 0, 0, preview_width, 32); -} - -static gboolean -preview_draw(GtkWidget *widget, cairo_t *cr, gpointer data) -{ - if (!camera_is_initialized) { - return FALSE; - } - - // Clear preview area with black - cairo_paint(cr); - - if (surface) { - // Draw camera preview - cairo_save(cr); - - int width = cairo_image_surface_get_width(surface); - int height = cairo_image_surface_get_height(surface); - transform_centered(cr, preview_width, preview_height, width, height); - - cairo_set_source_surface(cr, surface, 0, 0); - cairo_paint(cr); - - // Draw zbar image - if (zbar_result) { - for (uint8_t i = 0; i < zbar_result->size; ++i) { - MPZBarCode *code = &zbar_result->codes[i]; - - cairo_set_line_width(cr, 3.0); - cairo_set_source_rgba(cr, 0, 0.5, 1, 0.75); - cairo_new_path(cr); - cairo_move_to(cr, code->bounds_x[0], code->bounds_y[0]); - for (uint8_t i = 0; i < 4; ++i) { - cairo_line_to(cr, code->bounds_x[i], code->bounds_y[i]); - } - cairo_close_path(cr); - cairo_stroke(cr); - - cairo_save(cr); - cairo_translate(cr, code->bounds_x[0], code->bounds_y[0]); - cairo_show_text(cr, code->data); - cairo_restore(cr); - } - } - - cairo_restore(cr); - } - - // Draw control overlay - cairo_set_source_surface(cr, status_surface, 0, 0); - cairo_paint(cr); - return FALSE; -} - -static gboolean -preview_configure(GtkWidget *widget, GdkEventConfigure *event) -{ - int new_preview_width = gtk_widget_get_allocated_width(widget); - int new_preview_height = gtk_widget_get_allocated_height(widget); - - if (preview_width != new_preview_width || - preview_height != new_preview_height) { - preview_width = new_preview_width; - preview_height = new_preview_height; - update_io_pipeline(); - } - - draw_controls(); - - return TRUE; -} - -void -on_open_last_clicked(GtkWidget *widget, gpointer user_data) -{ - char uri[275]; - GError *error = NULL; - - if (strlen(last_path) == 0) { - return; - } - sprintf(uri, "file://%s", last_path); - if (!g_app_info_launch_default_for_uri(uri, NULL, &error)) { - g_printerr("Could not launch image viewer for '%s': %s\n", uri, error->message); - } -} - -void -on_open_directory_clicked(GtkWidget *widget, gpointer user_data) -{ - char uri[270]; - GError *error = NULL; - sprintf(uri, "file://%s", g_get_user_special_dir(G_USER_DIRECTORY_PICTURES)); - if (!g_app_info_launch_default_for_uri(uri, NULL, &error)) { - g_printerr("Could not launch image viewer: %s\n", error->message); - } -} - -void -on_shutter_clicked(GtkWidget *widget, gpointer user_data) -{ - gtk_spinner_start(GTK_SPINNER(process_spinner)); - gtk_stack_set_visible_child(GTK_STACK(open_last_stack), process_spinner); - mp_io_pipeline_capture(); -} - -void -on_capture_shortcut(void) -{ - on_shutter_clicked(NULL, NULL); -} - -static bool -check_point_inside_bounds(int x, int y, int *bounds_x, int *bounds_y) -{ - bool right = false, left = false, top = false, bottom = false; - - for (int i = 0; i < 4; ++i) { - if (x <= bounds_x[i]) - left = true; - if (x >= bounds_x[i]) - right = true; - if (y <= bounds_y[i]) - top = true; - if (y >= bounds_y[i]) - bottom = true; - } - - return right && left && top && bottom; -} - -static void -on_zbar_code_tapped(GtkWidget *widget, const MPZBarCode *code) -{ - GtkWidget *dialog; - GtkDialogFlags flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT; - bool data_is_url = g_uri_is_valid( - code->data, G_URI_FLAGS_PARSE_RELAXED, NULL); - - char* data = strdup(code->data); - - if (data_is_url) { - dialog = gtk_message_dialog_new( - GTK_WINDOW(gtk_widget_get_toplevel(widget)), - flags, - GTK_MESSAGE_QUESTION, - GTK_BUTTONS_NONE, - "Found a URL '%s' encoded in a %s code.", - code->data, - code->type); - gtk_dialog_add_buttons( - GTK_DIALOG(dialog), - "_Open URL", - GTK_RESPONSE_YES, - NULL); - } else { - dialog = gtk_message_dialog_new( - GTK_WINDOW(gtk_widget_get_toplevel(widget)), - flags, - GTK_MESSAGE_QUESTION, - GTK_BUTTONS_NONE, - "Found '%s' encoded in a %s code.", - code->data, - code->type); - } - gtk_dialog_add_buttons( - GTK_DIALOG(dialog), - "_Copy", - GTK_RESPONSE_ACCEPT, - "_Cancel", - GTK_RESPONSE_CANCEL, - NULL); - - int result = gtk_dialog_run(GTK_DIALOG(dialog)); - - GError *error = NULL; - switch (result) { - case GTK_RESPONSE_YES: - if (!g_app_info_launch_default_for_uri(data, - NULL, &error)) { - g_printerr("Could not launch application: %s\n", - error->message); - } - case GTK_RESPONSE_ACCEPT: - gtk_clipboard_set_text( - gtk_clipboard_get(GDK_SELECTION_PRIMARY), - data, -1); - case GTK_RESPONSE_CANCEL: - break; - default: - g_printerr("Wrong dialog result: %d\n", result); - } - gtk_widget_destroy(dialog); -} - -void -on_preview_tap(GtkWidget *widget, GdkEventButton *event, gpointer user_data) -{ - if (event->type != GDK_BUTTON_PRESS) - return; - - // Handle taps on the controls - if (event->y < 32) { - if (gtk_widget_is_visible(control_box)) { - gtk_widget_hide(control_box); - return; - } else { - gtk_widget_show(control_box); - } - - if (event->x < 60) { - // ISO - current_control = USER_CONTROL_ISO; - gtk_label_set_text(GTK_LABEL(control_name), "ISO"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(control_auto), - !gain_is_manual); - gtk_adjustment_set_lower(control_slider, 0.0); - gtk_adjustment_set_upper(control_slider, (float)gain_max); - gtk_adjustment_set_value(control_slider, (double)gain); - - } else if (event->x > 60 && event->x < 120) { - // Shutter angle - current_control = USER_CONTROL_SHUTTER; - gtk_label_set_text(GTK_LABEL(control_name), "Shutter"); - gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(control_auto), - !exposure_is_manual); - gtk_adjustment_set_lower(control_slider, 1.0); - gtk_adjustment_set_upper(control_slider, 360.0); - gtk_adjustment_set_value(control_slider, (double)exposure); - } - - return; - } - - // Tapped zbar result - if (zbar_result) { - // Transform the event coordinates to the image - int width = cairo_image_surface_get_width(surface); - int height = cairo_image_surface_get_height(surface); - double scale = MIN(preview_width / (double)width, preview_height / (double)height); - int x = (event->x - preview_width / 2) / scale + width / 2; - int y = (event->y - preview_height / 2) / scale + height / 2; - - for (uint8_t i = 0; i < zbar_result->size; ++i) { - MPZBarCode *code = &zbar_result->codes[i]; - - if (check_point_inside_bounds(x, y, code->bounds_x, code->bounds_y)) { - on_zbar_code_tapped(widget, code); - return; - } - } - } - - // Tapped preview image itself, try focussing - if (has_auto_focus_start) { - mp_io_pipeline_focus(); - } -} - -void -on_error_close_clicked(GtkWidget *widget, gpointer user_data) -{ - gtk_widget_hide(error_box); -} - -void -on_camera_switch_clicked(GtkWidget *widget, gpointer user_data) -{ - size_t next_index = camera->index + 1; - const struct mp_camera_config *next_camera = - mp_get_camera_config(next_index); - - if (!next_camera) { - next_index = 0; - next_camera = mp_get_camera_config(next_index); - } - - camera = next_camera; - update_io_pipeline(); -} - -void -on_settings_btn_clicked(GtkWidget *widget, gpointer user_data) -{ - gtk_stack_set_visible_child_name(GTK_STACK(main_stack), "settings"); -} - -void -on_back_clicked(GtkWidget *widget, gpointer user_data) -{ - gtk_stack_set_visible_child_name(GTK_STACK(main_stack), "main"); -} - -void -on_control_auto_toggled(GtkToggleButton *widget, gpointer user_data) -{ - bool is_manual = gtk_toggle_button_get_active(widget) ? false : true; - bool has_changed; - - switch (current_control) { - case USER_CONTROL_ISO: - if (gain_is_manual != is_manual) { - gain_is_manual = is_manual; - has_changed = true; - } - break; - case USER_CONTROL_SHUTTER: - if (exposure_is_manual != is_manual) { - exposure_is_manual = is_manual; - has_changed = true; - } - break; - } - - if (has_changed) { - // The slider might have been moved while Auto mode is active. When entering - // Manual mode, first read the slider value to sync with those changes. - double value = gtk_adjustment_get_value(control_slider); - switch (current_control) { - case USER_CONTROL_ISO: - if (value != gain) { - gain = (int)value; - } - break; - case USER_CONTROL_SHUTTER: { - // So far all sensors use exposure time in number of sensor rows - int new_exposure = - (int)(value / 360.0 * camera->capture_mode.height); - if (new_exposure != exposure) { - exposure = new_exposure; - } - break; - } - } - - update_io_pipeline(); - draw_controls(); - } -} - -void -on_control_slider_changed(GtkAdjustment *widget, gpointer user_data) -{ - double value = gtk_adjustment_get_value(widget); - - bool has_changed = false; - switch (current_control) { - case USER_CONTROL_ISO: - if (value != gain) { - gain = (int)value; - has_changed = true; - } - break; - case USER_CONTROL_SHUTTER: { - // So far all sensors use exposure time in number of sensor rows - int new_exposure = - (int)(value / 360.0 * camera->capture_mode.height); - if (new_exposure != exposure) { - exposure = new_exposure; - has_changed = true; - } - break; - } - } - - if (has_changed) { - update_io_pipeline(); - draw_controls(); - } -} - -int -main(int argc, char *argv[]) -{ - if (!mp_load_config()) - return 1; - - setenv("LC_NUMERIC", "C", 1); - - gtk_init(&argc, &argv); - g_object_set(gtk_settings_get_default(), "gtk-application-prefer-dark-theme", - TRUE, NULL); - GtkBuilder *builder = gtk_builder_new_from_resource( - "/org/postmarketos/Megapixels/camera.glade"); - - GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "window")); - GtkWidget *shutter = GTK_WIDGET(gtk_builder_get_object(builder, "shutter")); - GtkWidget *switch_btn = - GTK_WIDGET(gtk_builder_get_object(builder, "switch_camera")); - GtkWidget *settings_btn = - GTK_WIDGET(gtk_builder_get_object(builder, "settings")); - GtkWidget *settings_back = - GTK_WIDGET(gtk_builder_get_object(builder, "settings_back")); - GtkWidget *error_close = - GTK_WIDGET(gtk_builder_get_object(builder, "error_close")); - GtkWidget *open_last = - GTK_WIDGET(gtk_builder_get_object(builder, "open_last")); - GtkWidget *open_directory = - GTK_WIDGET(gtk_builder_get_object(builder, "open_directory")); - preview = GTK_WIDGET(gtk_builder_get_object(builder, "preview")); - error_box = GTK_WIDGET(gtk_builder_get_object(builder, "error_box")); - error_message = GTK_WIDGET(gtk_builder_get_object(builder, "error_message")); - main_stack = GTK_WIDGET(gtk_builder_get_object(builder, "main_stack")); - open_last_stack = GTK_WIDGET(gtk_builder_get_object(builder, "open_last_stack")); - thumb_last = GTK_WIDGET(gtk_builder_get_object(builder, "thumb_last")); - process_spinner = GTK_WIDGET(gtk_builder_get_object(builder, "process_spinner")); - control_box = GTK_WIDGET(gtk_builder_get_object(builder, "control_box")); - control_name = GTK_WIDGET(gtk_builder_get_object(builder, "control_name")); - control_slider = - GTK_ADJUSTMENT(gtk_builder_get_object(builder, "control_adj")); - control_auto = GTK_WIDGET(gtk_builder_get_object(builder, "control_auto")); - g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL); - g_signal_connect(shutter, "clicked", G_CALLBACK(on_shutter_clicked), NULL); - g_signal_connect(error_close, "clicked", G_CALLBACK(on_error_close_clicked), - NULL); - g_signal_connect(switch_btn, "clicked", G_CALLBACK(on_camera_switch_clicked), - NULL); - g_signal_connect(settings_btn, "clicked", - G_CALLBACK(on_settings_btn_clicked), NULL); - g_signal_connect(settings_back, "clicked", G_CALLBACK(on_back_clicked), - NULL); - g_signal_connect(open_last, "clicked", G_CALLBACK(on_open_last_clicked), - NULL); - g_signal_connect(open_directory, "clicked", - G_CALLBACK(on_open_directory_clicked), NULL); - g_signal_connect(preview, "draw", G_CALLBACK(preview_draw), NULL); - g_signal_connect(preview, "configure-event", G_CALLBACK(preview_configure), - NULL); - gtk_widget_set_events(preview, gtk_widget_get_events(preview) | - GDK_BUTTON_PRESS_MASK | - GDK_POINTER_MOTION_MASK); - g_signal_connect(preview, "button-press-event", G_CALLBACK(on_preview_tap), - NULL); - g_signal_connect(control_auto, "toggled", - G_CALLBACK(on_control_auto_toggled), NULL); - g_signal_connect(control_slider, "value-changed", - G_CALLBACK(on_control_slider_changed), NULL); - - GtkCssProvider *provider = gtk_css_provider_new(); - if (access("camera.css", F_OK) != -1) { - gtk_css_provider_load_from_path(provider, "camera.css", NULL); - } else { - gtk_css_provider_load_from_resource( - provider, "/org/postmarketos/Megapixels/camera.css"); - } - GtkStyleContext *context = gtk_widget_get_style_context(error_box); - gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), - GTK_STYLE_PROVIDER_PRIORITY_USER); - context = gtk_widget_get_style_context(control_box); - gtk_style_context_add_provider(context, GTK_STYLE_PROVIDER(provider), - GTK_STYLE_PROVIDER_PRIORITY_USER); - - GClosure* capture_shortcut = g_cclosure_new(on_capture_shortcut, 0, 0); - - GtkAccelGroup* accel_group = gtk_accel_group_new(); - gtk_accel_group_connect(accel_group, - GDK_KEY_space, - 0, - 0, - capture_shortcut); - - gtk_window_add_accel_group(GTK_WINDOW(window), accel_group); - - mp_io_pipeline_start(); - - camera = mp_get_camera_config(0); - update_io_pipeline(); - - gtk_widget_show(window); - gtk_main(); - - mp_io_pipeline_stop(); - - return 0; -} diff --git a/meson.build b/meson.build index 56df6fe..163fc20 100644 --- a/meson.build +++ b/meson.build @@ -1,73 +1,76 @@ project('megapixels', 'c') + gnome = import('gnome') -gtkdep = dependency('gtk+-3.0') +gtkdep = dependency('gtk4') tiff = dependency('libtiff-4') zbar = dependency('zbar') threads = dependency('threads') +# gl = dependency('gl') +epoxy = dependency('epoxy') cc = meson.get_compiler('c') libm = cc.find_library('m', required: false) -resources = gnome.compile_resources('megapixels-resources', 'org.postmarketos.Megapixels.gresource.xml') +subdir('data') conf = configuration_data() conf.set_quoted('DATADIR', join_paths(get_option('prefix'), get_option('datadir'))) conf.set_quoted('SYSCONFDIR', get_option('sysconfdir')) configure_file( output: 'config.h', - configuration: conf ) + configuration: conf) # 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 -# Workaround for libtiff having ABI changes but not changing the internal version number +# Workaround for libtiff having ABI changes but not changing the internal +# version number if get_option('tiffcfapattern') add_global_arguments('-DLIBTIFF_CFA_PATTERN', 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', - 'zbar_pipeline.c', - 'matrix.c', - resources, - dependencies : [gtkdep, libm, tiff, zbar, threads], - install : true) + 'src/main.c', + 'src/ini.c', + 'src/gles2_debayer.c', + 'src/gl_util.c', + 'src/camera.c', + 'src/device.c', + 'src/pipeline.c', + 'src/camera_config.c', + 'src/io_pipeline.c', + 'src/process_pipeline.c', + 'src/zbar_pipeline.c', + 'src/matrix.c', + resources, + include_directories: 'src/', + dependencies: [gtkdep, libm, tiff, zbar, threads, epoxy], + install: true, + link_args: '-Wl,-ldl') -install_data(['data/org.postmarketos.Megapixels.desktop'], - install_dir : get_option('datadir') / 'applications') - -install_data(['data/org.postmarketos.Megapixels.metainfo.xml'], - install_dir : get_option('datadir') / 'metainfo') - -install_data('data/org.postmarketos.Megapixels.svg', - install_dir: join_paths(get_option('datadir'), 'icons/hicolor/scalable/apps') -) - -install_data([ - 'config/pine64,pinephone-1.0.ini', - 'config/pine64,pinephone-1.1.ini', - 'config/pine64,pinephone-1.2.ini', - 'config/pine64,pinetab.ini', +install_data( + [ + 'config/pine64,pinephone-1.0.ini', + 'config/pine64,pinephone-1.1.ini', + 'config/pine64,pinephone-1.2.ini', + 'config/pine64,pinetab.ini', ], - install_dir : get_option('datadir') / 'megapixels/config/') - -install_data(['postprocess.sh'], - install_dir : get_option('datadir') / 'megapixels/', - install_mode: 'rwxr-xr-x') + install_dir: get_option('datadir') / 'megapixels/config/') # Tools -executable('megapixels-list-devices', 'tools/list_devices.c', 'device.c', dependencies: [gtkdep], install: true) -executable('megapixels-camera-test', 'tools/camera_test.c', 'camera.c', 'device.c', dependencies: [gtkdep], install: true) +executable('megapixels-list-devices', + 'tools/list_devices.c', + 'src/device.c', + include_directories: 'src/', + dependencies: [gtkdep], + install: true) -test_quickpreview = executable('test_quickpreview', 'tests/test_quickpreview.c', 'quickpreview.c', 'camera.c', dependencies: [gtkdep]) -test('quickpreview', test_quickpreview) +executable('megapixels-camera-test', + 'tools/camera_test.c', + 'src/camera.c', + 'src/device.c', + include_directories: 'src/', + dependencies: [gtkdep], + install: true) diff --git a/org.postmarketos.Megapixels.gresource.xml b/org.postmarketos.Megapixels.gresource.xml deleted file mode 100644 index d9938c1..0000000 --- a/org.postmarketos.Megapixels.gresource.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - camera.glade - camera.css - switch-camera.svg - shutter-button.svg - folder-symbolic.svg - settings-symbolic.svg - - diff --git a/quickpreview.c b/quickpreview.c deleted file mode 100644 index f8aed4f..0000000 --- a/quickpreview.c +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Fast but bad debayer method that scales and rotates by skipping source - * pixels and doesn't interpolate any values at all - */ - -#include "quickpreview.h" -#include -#include - -/* Linear -> sRGB lookup table */ -static const int srgb[] = { - 0, 12, 21, 28, 33, 38, 42, 46, 49, 52, 55, 58, 61, 63, 66, 68, 70, - 73, 75, 77, 79, 81, 82, 84, 86, 88, 89, 91, 93, 94, 96, 97, 99, 100, - 102, 103, 104, 106, 107, 109, 110, 111, 112, 114, 115, 116, 117, 118, - 120, 121, 122, 123, 124, 125, 126, 127, 129, 130, 131, 132, 133, 134, - 135, 136, 137, 138, 139, 140, 141, 142, 142, 143, 144, 145, 146, 147, - 148, 149, 150, 151, 151, 152, 153, 154, 155, 156, 157, 157, 158, 159, - 160, 161, 161, 162, 163, 164, 165, 165, 166, 167, 168, 168, 169, 170, - 171, 171, 172, 173, 174, 174, 175, 176, 176, 177, 178, 179, 179, 180, - 181, 181, 182, 183, 183, 184, 185, 185, 186, 187, 187, 188, 189, 189, - 190, 191, 191, 192, 193, 193, 194, 194, 195, 196, 196, 197, 197, 198, - 199, 199, 200, 201, 201, 202, 202, 203, 204, 204, 205, 205, 206, 206, - 207, 208, 208, 209, 209, 210, 210, 211, 212, 212, 213, 213, 214, 214, - 215, 215, 216, 217, 217, 218, 218, 219, 219, 220, 220, 221, 221, 222, - 222, 223, 223, 224, 224, 225, 226, 226, 227, 227, 228, 228, 229, 229, - 230, 230, 231, 231, 232, 232, 233, 233, 234, 234, 235, 235, 236, 236, - 237, 237, 237, 238, 238, 239, 239, 240, 240, 241, 241, 242, 242, 243, - 243, 244, 244, 245, 245, 245, 246, 246, 247, 247, 248, 248, 249, 249, - 250, 250, 251, 251, 251, 252, 252, 253, 253, 254, 254, 255 -}; - -static inline uint32_t -pack_rgb(uint8_t r, uint8_t g, uint8_t b) -{ - return (r << 16) | (g << 8) | b; -} - -static inline uint32_t -convert_yuv_to_srgb(uint8_t y, uint8_t u, uint8_t v) -{ - uint32_t r = 1.164f * y + 1.596f * (v - 128); - uint32_t g = 1.164f * y - 0.813f * (v - 128) - 0.391f * (u - 128); - uint32_t b = 1.164f * y + 2.018f * (u - 128); - return pack_rgb(r, g, b); -} - -static inline uint32_t -apply_colormatrix(uint32_t color, const float *colormatrix) -{ - if (!colormatrix) { - return color; - } - - uint32_t r = (color >> 16) * colormatrix[0] + - ((color >> 8) & 0xFF) * colormatrix[1] + - (color & 0xFF) * colormatrix[2]; - uint32_t g = (color >> 16) * colormatrix[3] + - ((color >> 8) & 0xFF) * colormatrix[4] + - (color & 0xFF) * colormatrix[5]; - uint32_t b = (color >> 16) * colormatrix[6] + - ((color >> 8) & 0xFF) * colormatrix[7] + - (color & 0xFF) * colormatrix[8]; - - // Clip colors - if (r > 0xFF) - r = 0xFF; - if (g > 0xFF) - g = 0xFF; - if (b > 0xFF) - b = 0xFF; - return pack_rgb(r, g, b); -} - -static inline uint32_t -coord_map(uint32_t x, uint32_t y, uint32_t width, uint32_t height, int rotation, - bool mirrored) -{ - uint32_t x_r, y_r; - if (rotation == 0) { - x_r = x; - y_r = y; - } else if (rotation == 90) { - x_r = y; - y_r = height - x - 1; - } else if (rotation == 270) { - x_r = width - y - 1; - y_r = x; - } else { - x_r = width - x - 1; - y_r = height - y - 1; - } - - if (mirrored) { - x_r = width - x_r - 1; - } - - uint32_t index = y_r * width + x_r; -#ifdef DEBUG - assert(index < width * height); -#endif - return index; -} - -static void -quick_preview_rggb8(uint32_t *dst, const uint32_t dst_width, - const uint32_t dst_height, const uint8_t *src, - const uint32_t src_width, const uint32_t src_height, - const MPPixelFormat format, const uint32_t rotation, - const bool mirrored, const float *colormatrix, - const uint8_t blacklevel, const uint32_t skip) -{ - uint32_t src_y = 0, dst_y = 0; - while (src_y < src_height) { - uint32_t src_x = 0, dst_x = 0; - while (src_x < src_width) { - uint32_t src_i = src_y * src_width + src_x; - - uint8_t b0 = srgb[src[src_i] - blacklevel]; - uint8_t b1 = srgb[src[src_i + 1] - blacklevel]; - uint8_t b2 = srgb[src[src_i + src_width + 1] - blacklevel]; - - uint32_t color; - switch (format) { - case MP_PIXEL_FMT_BGGR8: - color = pack_rgb(b2, b1, b0); - break; - case MP_PIXEL_FMT_GBRG8: - color = pack_rgb(b2, b0, b1); - break; - case MP_PIXEL_FMT_GRBG8: - color = pack_rgb(b1, b0, b2); - break; - case MP_PIXEL_FMT_RGGB8: - color = pack_rgb(b0, b1, b2); - break; - default: - assert(false); - } - - color = apply_colormatrix(color, colormatrix); - - dst[coord_map(dst_x, dst_y, dst_width, dst_height, rotation, - mirrored)] = color; - - src_x += 2 + 2 * skip; - ++dst_x; - } - - src_y += 2 + 2 * skip; - ++dst_y; - } -} - -static void -quick_preview_rggb10(uint32_t *dst, const uint32_t dst_width, - const uint32_t dst_height, const uint8_t *src, - const uint32_t src_width, const uint32_t src_height, - const MPPixelFormat format, const uint32_t rotation, - const bool mirrored, const float *colormatrix, - const uint8_t blacklevel, const uint32_t skip) -{ - assert(src_width % 2 == 0); - - uint32_t width_bytes = mp_pixel_format_width_to_bytes(format, src_width); - - uint32_t src_y = 0, dst_y = 0; - while (src_y < src_height) { - uint32_t src_x = 0, dst_x = 0; - while (src_x < width_bytes) { - uint32_t src_i = src_y * width_bytes + src_x; - - uint8_t b0 = srgb[src[src_i] - blacklevel]; - uint8_t b1 = srgb[src[src_i + 1] - blacklevel]; - uint8_t b2 = srgb[src[src_i + width_bytes + 1] - blacklevel]; - - uint32_t color; - switch (format) { - case MP_PIXEL_FMT_BGGR10P: - color = pack_rgb(b2, b1, b0); - break; - case MP_PIXEL_FMT_GBRG10P: - color = pack_rgb(b2, b0, b1); - break; - case MP_PIXEL_FMT_GRBG10P: - color = pack_rgb(b1, b0, b2); - break; - case MP_PIXEL_FMT_RGGB10P: - color = pack_rgb(b0, b1, b2); - break; - default: - assert(false); - } - - color = apply_colormatrix(color, colormatrix); - - dst[coord_map(dst_x, dst_y, dst_width, dst_height, rotation, - mirrored)] = color; - - uint32_t advance = 1 + skip; - if (src_x % 5 == 0) { - src_x += 2 * (advance % 2) + 5 * (advance / 2); - } else { - src_x += 3 * (advance % 2) + 5 * (advance / 2); - } - ++dst_x; - } - - src_y += 2 + 2 * skip; - ++dst_y; - } -} - -static void -quick_preview_yuv(uint32_t *dst, const uint32_t dst_width, const uint32_t dst_height, - const uint8_t *src, const uint32_t src_width, - const uint32_t src_height, const MPPixelFormat format, - const uint32_t rotation, const bool mirrored, - const float *colormatrix, const uint32_t skip) -{ - assert(src_width % 2 == 0); - - uint32_t width_bytes = src_width * 2; - - uint32_t unrot_dst_width = dst_width; - if (rotation != 0 && rotation != 180) { - unrot_dst_width = dst_height; - } - - uint32_t src_y = 0, dst_y = 0; - while (src_y < src_height) { - uint32_t src_x = 0, dst_x = 0; - while (src_x < width_bytes) { - uint32_t src_i = src_y * width_bytes + src_x; - - uint8_t b0 = src[src_i]; - uint8_t b1 = src[src_i + 1]; - uint8_t b2 = src[src_i + 2]; - uint8_t b3 = src[src_i + 3]; - - uint32_t color1, color2; - switch (format) { - case MP_PIXEL_FMT_UYVY: - color1 = convert_yuv_to_srgb(b1, b0, b2); - color2 = convert_yuv_to_srgb(b3, b0, b2); - break; - case MP_PIXEL_FMT_YUYV: - color1 = convert_yuv_to_srgb(b0, b1, b3); - color2 = convert_yuv_to_srgb(b2, b1, b3); - break; - default: - assert(false); - } - - color1 = apply_colormatrix(color1, colormatrix); - color2 = apply_colormatrix(color2, colormatrix); - - uint32_t dst_i1 = coord_map(dst_x, dst_y, dst_width, - dst_height, rotation, mirrored); - dst[dst_i1] = color1; - ++dst_x; - - // The last pixel needs to be skipped if we have an odd un-rotated width - if (dst_x < unrot_dst_width) { - uint32_t dst_i2 = - coord_map(dst_x, dst_y, dst_width, - dst_height, rotation, mirrored); - dst[dst_i2] = color2; - ++dst_x; - } - - src_x += 4 + 4 * skip; - } - - src_y += 1 + skip; - ++dst_y; - } -} - -void -quick_preview(uint32_t *dst, const uint32_t dst_width, const uint32_t dst_height, - const uint8_t *src, const uint32_t src_width, - const uint32_t src_height, const MPPixelFormat format, - const uint32_t rotation, const bool mirrored, const float *colormatrix, - const uint8_t blacklevel, const uint32_t skip) -{ - switch (format) { - case MP_PIXEL_FMT_BGGR8: - case MP_PIXEL_FMT_GBRG8: - case MP_PIXEL_FMT_GRBG8: - case MP_PIXEL_FMT_RGGB8: - quick_preview_rggb8(dst, dst_width, dst_height, src, src_width, - src_height, format, rotation, mirrored, - colormatrix, blacklevel, skip); - break; - case MP_PIXEL_FMT_BGGR10P: - case MP_PIXEL_FMT_GBRG10P: - case MP_PIXEL_FMT_GRBG10P: - case MP_PIXEL_FMT_RGGB10P: - quick_preview_rggb10(dst, dst_width, dst_height, src, src_width, - src_height, format, rotation, mirrored, - colormatrix, blacklevel, skip); - break; - case MP_PIXEL_FMT_UYVY: - case MP_PIXEL_FMT_YUYV: - quick_preview_yuv(dst, dst_width, dst_height, src, src_width, - src_height, format, rotation, mirrored, - colormatrix, skip); - break; - default: - assert(false); - } -} - -static uint32_t -div_ceil(uint32_t x, uint32_t y) -{ - return x / y + !!(x % y); -} - -void -quick_preview_size(uint32_t *dst_width, uint32_t *dst_height, uint32_t *skip, - const uint32_t preview_width, const uint32_t preview_height, - const uint32_t src_width, const uint32_t src_height, - const MPPixelFormat format, const int rotation) -{ - uint32_t colors_x = mp_pixel_format_width_to_colors(format, src_width); - uint32_t colors_y = mp_pixel_format_height_to_colors(format, src_height); - - if (rotation != 0 && rotation != 180) { - uint32_t tmp = colors_x; - colors_x = colors_y; - colors_y = tmp; - } - - uint32_t scale_x = colors_x / preview_width; - uint32_t scale_y = colors_y / preview_height; - - if (scale_x > 0) - --scale_x; - if (scale_y > 0) - --scale_y; - *skip = scale_x > scale_y ? scale_x : scale_y; - - *dst_width = div_ceil(colors_x, (1 + *skip)); - if (*dst_width <= 0) - *dst_width = 1; - - *dst_height = div_ceil(colors_y, (1 + *skip)); - if (*dst_height <= 0) - *dst_height = 1; -} diff --git a/quickpreview.h b/quickpreview.h deleted file mode 100644 index 1815215..0000000 --- a/quickpreview.h +++ /dev/null @@ -1,14 +0,0 @@ -#include "camera.h" -#include - -void quick_preview(uint32_t *dst, const uint32_t dst_width, - const uint32_t dst_height, const uint8_t *src, - const uint32_t src_width, const uint32_t src_height, - const MPPixelFormat format, const uint32_t rotation, - const bool mirrored, const float *colormatrix, - const uint8_t blacklevel, const uint32_t skip); - -void quick_preview_size(uint32_t *dst_width, uint32_t *dst_height, uint32_t *skip, - const uint32_t preview_width, const uint32_t preview_height, - const uint32_t src_width, const uint32_t src_height, - const MPPixelFormat format, const int rotation); diff --git a/camera.c b/src/camera.c similarity index 96% rename from camera.c rename to src/camera.c index 6e80781..6bc3e36 100644 --- a/camera.c +++ b/src/camera.c @@ -6,6 +6,7 @@ #include #include #include +#include #define MAX_VIDEO_BUFFERS 20 @@ -205,6 +206,7 @@ xioctl(int fd, int request, void *arg) struct video_buffer { uint32_t length; uint8_t *data; + int fd; }; struct _MPCamera { @@ -465,6 +467,17 @@ mp_camera_start_capture(MPCamera *camera) break; } + struct v4l2_exportbuffer expbuf = { + .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, + .index = i, + }; + if (xioctl(camera->video_fd, VIDIOC_EXPBUF, &expbuf) == -1) { + errno_printerr("VIDIOC_EXPBUF"); + break; + } + + camera->buffers[i].fd = expbuf.fd; + ++camera->num_buffers; } @@ -510,6 +523,10 @@ error: -1) { errno_printerr("munmap"); } + + if (close(camera->buffers[i].fd) == -1) { + errno_printerr("close"); + } } // Reset allocated buffers @@ -543,6 +560,10 @@ mp_camera_stop_capture(MPCamera *camera) -1) { errno_printerr("munmap"); } + + if (close(camera->buffers[i].fd) == -1) { + errno_printerr("close"); + } } camera->num_buffers = 0; @@ -565,8 +586,7 @@ mp_camera_is_capturing(MPCamera *camera) } bool -mp_camera_capture_image(MPCamera *camera, void (*callback)(MPImage, void *), - void *user_data) +mp_camera_capture_buffer(MPCamera *camera, MPBuffer *buffer) { struct v4l2_buffer buf = {}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; @@ -587,6 +607,7 @@ mp_camera_capture_image(MPCamera *camera, void (*callback)(MPImage, void *), /* fallthrough */ default: errno_printerr("VIDIOC_DQBUF"); + exit(1); return false; } } @@ -606,24 +627,23 @@ mp_camera_capture_image(MPCamera *camera, void (*callback)(MPImage, void *), mp_pixel_format_width_to_bytes(pixel_format, width) * height); assert(bytesused == camera->buffers[buf.index].length); - MPImage image = { - .pixel_format = pixel_format, - .width = width, - .height = height, - .data = camera->buffers[buf.index].data, - }; + buffer->index = buf.index; + buffer->data = camera->buffers[buf.index].data; + buffer->fd = camera->buffers[buf.index].fd; - callback(image, user_data); + return true; +} - // The callback may have stopped the capture, only queue the buffer if we're - // still capturing. - if (mp_camera_is_capturing(camera)) { - if (xioctl(camera->video_fd, VIDIOC_QBUF, &buf) == -1) { - errno_printerr("VIDIOC_QBUF"); - return false; - } +bool mp_camera_release_buffer(MPCamera *camera, uint32_t buffer_index) +{ + struct v4l2_buffer buf = {}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; + buf.memory = V4L2_MEMORY_MMAP; + buf.index = buffer_index; + if (xioctl(camera->video_fd, VIDIOC_QBUF, &buf) == -1) { + errno_printerr("VIDIOC_QBUF"); + return false; } - return true; } diff --git a/camera.h b/src/camera.h similarity index 95% rename from camera.h rename to src/camera.h index 3004e8e..323ac63 100644 --- a/camera.h +++ b/src/camera.h @@ -45,11 +45,11 @@ typedef struct { bool mp_camera_mode_is_equivalent(const MPCameraMode *m1, const MPCameraMode *m2); typedef struct { - uint32_t pixel_format; - uint32_t width; - uint32_t height; + uint32_t index; + uint8_t *data; -} MPImage; + int fd; +} MPBuffer; typedef struct _MPCamera MPCamera; @@ -67,8 +67,8 @@ bool mp_camera_set_mode(MPCamera *camera, MPCameraMode *mode); bool mp_camera_start_capture(MPCamera *camera); bool mp_camera_stop_capture(MPCamera *camera); bool mp_camera_is_capturing(MPCamera *camera); -bool mp_camera_capture_image(MPCamera *camera, void (*callback)(MPImage, void *), - void *user_data); +bool mp_camera_capture_buffer(MPCamera *camera, MPBuffer *buffer); +bool mp_camera_release_buffer(MPCamera *camera, uint32_t buffer_index); typedef struct _MPCameraModeList MPCameraModeList; diff --git a/camera_config.c b/src/camera_config.c similarity index 95% rename from camera_config.c rename to src/camera_config.c index 1e52d62..f8ecf05 100644 --- a/camera_config.c +++ b/src/camera_config.c @@ -3,7 +3,6 @@ #include "ini.h" #include "config.h" #include "matrix.h" -#include #include #include #include @@ -21,18 +20,8 @@ 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 @@ -48,7 +37,7 @@ find_config(char *conffile) } // Check for a config file in XDG_CONFIG_HOME - sprintf(conffile, "%s/megapixels/config/%s.ini", xdg_config_home, + sprintf(conffile, "%s/megapixels/config/%s.ini", g_get_user_config_dir (), buf); if (access(conffile, F_OK) != -1) { printf("Found config file at %s\n", conffile); diff --git a/camera_config.h b/src/camera_config.h similarity index 100% rename from camera_config.h rename to src/camera_config.h diff --git a/device.c b/src/device.c similarity index 100% rename from device.c rename to src/device.c diff --git a/device.h b/src/device.h similarity index 100% rename from device.h rename to src/device.h diff --git a/src/gl_util.c b/src/gl_util.c new file mode 100644 index 0000000..c4c76ca --- /dev/null +++ b/src/gl_util.c @@ -0,0 +1,220 @@ +#include "gl_util.h" + +#include +#include +#include +#include +#include +#include +#include + +void gl_util_check_error(const char *file, int line) +{ + GLenum error = glGetError(); + + const char *name; + switch (error) { + case GL_NO_ERROR: + return; // no error + case GL_INVALID_ENUM: + name = "GL_INVALID_ENUM"; + break; + case GL_INVALID_VALUE: + name = "GL_INVALID_VALUE"; + break; + case GL_INVALID_OPERATION: + name = "GL_INVALID_OPERATION"; + break; + case GL_INVALID_FRAMEBUFFER_OPERATION: + name = "GL_INVALID_FRAMEBUFFER_OPERATION"; + break; + case GL_OUT_OF_MEMORY: + name = "GL_OUT_OF_MEMORY"; + break; + default: + name = "UNKNOWN ERROR!"; + break; + } + + printf("GL error at %s:%d - %s\n", file, line, name); + + // raise(SIGTRAP); +} + +GLuint +gl_util_load_shader(const char *resource, GLenum type, const char **extra_sources, size_t num_extra) +{ + GdkGLContext *context = gdk_gl_context_get_current(); + assert(context); + + GLuint shader = glCreateShader(type); + if (shader == 0) { + return 0; + } + + GBytes *bytes = g_resources_lookup_data(resource, 0, NULL); + if (!bytes) { + printf("Failed to load shader resource %s\n", resource); + return 0; + } + + // Build #define for OpenGL context information + gboolean is_es = gdk_gl_context_get_use_es(context); + int major, minor; + gdk_gl_context_get_version(context, &major, &minor); + char context_info_buf[128]; + snprintf(context_info_buf, 128, "#define %s\n#define GL_%d\n#define GL_%d_%d\n", is_es ? "GL_ES" : "GL_NO_ES", major, major, minor); + + gsize glib_size = 0; + const GLchar *source = g_bytes_get_data(bytes, &glib_size); + if (glib_size == 0 || glib_size > INT_MAX) { + printf("Invalid size for resource\n"); + return 0; + } + + const GLchar **sources = malloc((num_extra + 1) * sizeof(GLchar *)); + GLint *sizes = malloc((num_extra + 1) * sizeof(GLint)); + + for (size_t i = 0; i < num_extra; ++i) { + sources[i] = extra_sources[i]; + sizes[i] = -1; + } + sources[num_extra] = source; + sizes[num_extra] = glib_size; + + glShaderSource(shader, num_extra + 1, sources, sizes); + glCompileShader(shader); + check_gl(); + + free(sources); + free(sizes); + + g_bytes_unref(bytes); + + // Check compile status + GLint success; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (success == GL_FALSE) { + printf("Shader compilation failed for %s\n", resource); + + glDeleteShader(shader); + return 0; + } + + GLint log_length; + glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_length); + if (log_length > 0) { + char *log = malloc(sizeof(char) * log_length); + glGetShaderInfoLog(shader, log_length - 1, &log_length, log); + + printf("Shader %s log: %s\n", resource, log); + free(log); + + glDeleteShader(shader); + return 0; + } + + return shader; +} + +GLuint +gl_util_link_program(GLuint *shaders, size_t num_shaders) +{ + GLuint program = glCreateProgram(); + + for (size_t i = 0; i < num_shaders; ++i) { + glAttachShader(program, shaders[i]); + } + + glLinkProgram(program); + check_gl(); + + GLint success; + glGetProgramiv(program, GL_LINK_STATUS, &success); + if (success == GL_FALSE) { + printf("Program linking failed\n"); + } + + GLint log_length; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &log_length); + if (log_length > 0) { + char *log = malloc(sizeof(char) * log_length); + glGetProgramInfoLog(program, log_length - 1, &log_length, log); + + printf("Program log: %s\n", log); + free(log); + } + check_gl(); + + return program; +} + +static const GLfloat quad_data[] = { + // Vertices + -1, -1, + 1, -1, + -1, 1, + 1, 1, + // Texcoords + 0, 0, + 1, 0, + 0, 1, + 1, 1, +}; + +GLuint gl_util_new_quad() +{ + GdkGLContext *context = gdk_gl_context_get_current(); + assert(context); + + if (gdk_gl_context_get_use_es(context)) { + return 0; + } else { + GLuint buffer; + glGenBuffers(1, &buffer); + + glBindBuffer(GL_ARRAY_BUFFER, buffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(quad_data), quad_data, GL_STATIC_DRAW); + check_gl(); + + glBindBuffer(GL_ARRAY_BUFFER, 0); + check_gl(); + + return buffer; + } +} + +void gl_util_bind_quad(GLuint buffer) +{ + GdkGLContext *context = gdk_gl_context_get_current(); + assert(context); + + if (gdk_gl_context_get_use_es(context)) { + glVertexAttribPointer(GL_UTIL_VERTEX_ATTRIBUTE, 2, GL_FLOAT, 0, 0, quad_data); + check_gl(); + glEnableVertexAttribArray(GL_UTIL_VERTEX_ATTRIBUTE); + check_gl(); + + glVertexAttribPointer(GL_UTIL_TEX_COORD_ATTRIBUTE, 2, GL_FLOAT, 0, 0, quad_data + 8); + check_gl(); + glEnableVertexAttribArray(GL_UTIL_TEX_COORD_ATTRIBUTE); + check_gl(); + } else { + glBindBuffer(GL_ARRAY_BUFFER, buffer); + check_gl(); + + glVertexAttribPointer(GL_UTIL_VERTEX_ATTRIBUTE, 2, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(GL_UTIL_VERTEX_ATTRIBUTE); + check_gl(); + + glVertexAttribPointer(GL_UTIL_TEX_COORD_ATTRIBUTE, 2, GL_FLOAT, GL_FALSE, 0, (void*) (8 * sizeof(float))); + glEnableVertexAttribArray(GL_UTIL_TEX_COORD_ATTRIBUTE); + check_gl(); + } +} + +void gl_util_draw_quad(GLuint buffer) +{ + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + check_gl(); +} diff --git a/src/gl_util.h b/src/gl_util.h new file mode 100644 index 0000000..f1e8451 --- /dev/null +++ b/src/gl_util.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#define GL_UTIL_VERTEX_ATTRIBUTE 0 +#define GL_UTIL_TEX_COORD_ATTRIBUTE 1 + +#define check_gl() gl_util_check_error(__FILE__, __LINE__) +void gl_util_check_error(const char *file, int line); + +GLuint gl_util_load_shader(const char *resource, GLenum type, const char **extra_sources, size_t num_extra); +GLuint gl_util_link_program(GLuint *shaders, size_t num_shaders); + +GLuint gl_util_new_quad(); +void gl_util_bind_quad(GLuint buffer); +void gl_util_draw_quad(GLuint buffer); diff --git a/src/gles2_debayer.c b/src/gles2_debayer.c new file mode 100644 index 0000000..df31fc4 --- /dev/null +++ b/src/gles2_debayer.c @@ -0,0 +1,141 @@ +#include "gles2_debayer.h" + +#include "camera.h" +#include "gl_util.h" +#include + +#define VERTEX_ATTRIBUTE 0 +#define TEX_COORD_ATTRIBUTE 1 + +struct _GLES2Debayer { + GLuint frame_buffer; + GLuint program; + GLuint uniform_transform; + GLuint uniform_pixel_size; + GLuint uniform_texture; + GLuint uniform_color_matrix; + + GLuint quad; +}; + +GLES2Debayer * +gles2_debayer_new(MPPixelFormat format) +{ + if (format != MP_PIXEL_FMT_BGGR8) { + return NULL; + } + + GLuint frame_buffer; + glGenFramebuffers(1, &frame_buffer); + check_gl(); + + GLuint shaders[] = { + gl_util_load_shader("/org/postmarketos/Megapixels/debayer.vert", GL_VERTEX_SHADER, NULL, 0), + gl_util_load_shader("/org/postmarketos/Megapixels/debayer.frag", GL_FRAGMENT_SHADER, NULL, 0), + }; + + GLuint program = gl_util_link_program(shaders, 2); + glBindAttribLocation(program, VERTEX_ATTRIBUTE, "vert"); + glBindAttribLocation(program, TEX_COORD_ATTRIBUTE, "tex_coord"); + check_gl(); + + GLES2Debayer *self = malloc(sizeof(GLES2Debayer)); + self->frame_buffer = frame_buffer; + self->program = program; + + self->uniform_transform = glGetUniformLocation(self->program, "transform"); + self->uniform_pixel_size = glGetUniformLocation(self->program, "pixel_size"); + self->uniform_texture = glGetUniformLocation(self->program, "texture"); + self->uniform_color_matrix = glGetUniformLocation(self->program, "color_matrix"); + check_gl(); + + self->quad = gl_util_new_quad(); + + return self; +} + +void +gles2_debayer_free(GLES2Debayer *self) +{ + glDeleteFramebuffers(1, &self->frame_buffer); + + glDeleteProgram(self->program); + + free(self); +} + +void +gles2_debayer_use(GLES2Debayer *self) +{ + glUseProgram(self->program); + check_gl(); + + gl_util_bind_quad(self->quad); +} + +void +gles2_debayer_configure(GLES2Debayer *self, + const uint32_t dst_width, const uint32_t dst_height, + const uint32_t src_width, const uint32_t src_height, + const uint32_t rotation, + const bool mirrored, + const float *colormatrix, + const uint8_t blacklevel) +{ + glViewport(0, 0, dst_width, dst_height); + check_gl(); + + GLfloat rotation_list[4] = { 0, -1, 0, 1 }; + int rotation_index = 4 - rotation / 90; + + GLfloat sin_rot = rotation_list[rotation_index]; + GLfloat cos_rot = rotation_list[(rotation_index + 1) % 4]; + GLfloat scale_x = mirrored ? 1 : -1; + GLfloat matrix[9] = { + cos_rot * scale_x, sin_rot, 0, + -sin_rot * scale_x, cos_rot, 0, + 0, 0, 1, + }; + glUniformMatrix3fv(self->uniform_transform, 1, GL_FALSE, matrix); + check_gl(); + + GLfloat pixel_size_x = 1.0f / src_width; + GLfloat pixel_size_y = 1.0f / src_height; + glUniform2f(self->uniform_pixel_size, pixel_size_x, pixel_size_y); + check_gl(); + + if (colormatrix) { + GLfloat transposed[9]; + for (int i = 0; i < 3; ++i) + for (int j = 0; j < 3; ++j) + transposed[i + j * 3] = colormatrix[j + i * 3]; + + glUniformMatrix3fv(self->uniform_color_matrix, 1, GL_FALSE, transposed); + } else { + static const GLfloat identity[9] = { + 1, 0, 0, + 0, 1, 0, + 0, 0, 1, + }; + glUniformMatrix3fv(self->uniform_color_matrix, 1, GL_FALSE, identity); + } + check_gl(); +} + +void +gles2_debayer_process(GLES2Debayer *self, GLuint dst_id, GLuint source_id) +{ + glBindFramebuffer(GL_FRAMEBUFFER, self->frame_buffer); + glBindTexture(GL_TEXTURE_2D, dst_id); + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, dst_id, 0); + check_gl(); + + assert(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, source_id); + glUniform1i(self->uniform_texture, 0); + check_gl(); + + gl_util_draw_quad(self->quad); +} diff --git a/src/gles2_debayer.h b/src/gles2_debayer.h new file mode 100644 index 0000000..0028749 --- /dev/null +++ b/src/gles2_debayer.h @@ -0,0 +1,21 @@ +#include "camera.h" +#include "gl_util.h" +#include +#include + +typedef struct _GLES2Debayer GLES2Debayer; + +GLES2Debayer* gles2_debayer_new(MPPixelFormat format); +void gles2_debayer_free(GLES2Debayer *self); + +void gles2_debayer_use(GLES2Debayer *self); + +void gles2_debayer_configure(GLES2Debayer *self, + const uint32_t dst_width, const uint32_t dst_height, + const uint32_t src_width, const uint32_t src_height, + const uint32_t rotation, + const bool mirrored, + const float *colormatrix, + const uint8_t blacklevel); + +void gles2_debayer_process(GLES2Debayer *self, GLuint dst_id, GLuint source_id); diff --git a/ini.c b/src/ini.c similarity index 100% rename from ini.c rename to src/ini.c diff --git a/ini.h b/src/ini.h similarity index 100% rename from ini.h rename to src/ini.h diff --git a/io_pipeline.c b/src/io_pipeline.c similarity index 92% rename from io_pipeline.c rename to src/io_pipeline.c index 980a722..1281df3 100644 --- a/io_pipeline.c +++ b/src/io_pipeline.c @@ -75,6 +75,8 @@ static int captures_remaining = 0; static int preview_width; static int preview_height; +static int device_rotation; + struct control_state { bool gain_is_manual; int gain; @@ -185,6 +187,12 @@ setup_camera(MPDeviceList **device_list, const struct mp_camera_config *config) info->camera = mp_camera_new(dev_info->video_fd, info->fd); + // Start with the capture format, this works around a bug with + // the ov5640 driver where it won't allow setting the preview + // format initially. + MPCameraMode mode = config->capture_mode; + mp_camera_set_mode(info->camera, &mode); + // Trigger continuous auto focus if the sensor supports it if (mp_camera_query_control(info->camera, V4L2_CID_FOCUS_AUTO, NULL)) { @@ -269,6 +277,7 @@ update_process_pipeline() .burst_length = burst_length, .preview_width = preview_width, .preview_height = preview_height, + .device_rotation = device_rotation, .gain_is_manual = current_controls.gain_is_manual, .gain = current_controls.gain, .gain_max = info->gain_max, @@ -305,6 +314,7 @@ capture(MPPipeline *pipeline, const void *data) V4L2_EXPOSURE_MANUAL); // Change camera mode for capturing + mp_process_pipeline_sync(); mp_camera_stop_capture(info->camera); mode = camera->capture_mode; @@ -324,6 +334,19 @@ mp_io_pipeline_capture() mp_pipeline_invoke(pipeline, capture, NULL, 0); } +static void +release_buffer(MPPipeline *pipeline, const uint32_t *buffer_index) +{ + struct camera_info *info = &cameras[camera->index]; + + mp_camera_release_buffer(info->camera, *buffer_index); +} + +void mp_io_pipeline_release_buffer(uint32_t buffer_index) +{ + mp_pipeline_invoke(pipeline, (MPPipelineCallback) release_buffer, &buffer_index, sizeof(uint32_t)); +} + static void update_controls() { @@ -375,7 +398,7 @@ update_controls() } static void -on_frame(MPImage image, void *data) +on_frame(MPBuffer buffer, void * _data) { // Only update controls right after a frame was captured update_controls(); @@ -384,13 +407,13 @@ on_frame(MPImage image, void *data) // presumably from buffers made ready during the switch. Ignore these. if (just_switched_mode) { if (blank_frame_count < 20) { - // Only check a 50x50 area + // Only check a 10x10 area size_t test_size = - MIN(50, image.width) * MIN(50, image.height); + MIN(10, mode.width) * MIN(10, mode.height); bool image_is_blank = true; for (size_t i = 0; i < test_size; ++i) { - if (image.data[i] != 0) { + if (buffer.data[i] != 0) { image_is_blank = false; } } @@ -407,17 +430,8 @@ on_frame(MPImage image, void *data) 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); + mp_process_pipeline_process_image(buffer); if (captures_remaining > 0) { --captures_remaining; @@ -438,6 +452,7 @@ on_frame(MPImage image, void *data) } // Go back to preview mode + mp_process_pipeline_sync(); mp_camera_stop_capture(info->camera); mode = camera->preview_mode; @@ -465,6 +480,7 @@ update_state(MPPipeline *pipeline, const struct mp_io_pipeline_state *state) struct camera_info *info = &cameras[camera->index]; struct device_info *dev_info = &devices[info->device_index]; + mp_process_pipeline_sync(); mp_camera_stop_capture(info->camera); mp_device_setup_link(dev_info->device, info->pad_id, dev_info->interface_pad_id, false); @@ -492,15 +508,15 @@ update_state(MPPipeline *pipeline, const struct mp_io_pipeline_state *state) pipeline, info->camera, on_frame, NULL); current_controls.gain_is_manual = - mp_camera_control_get_int32( - info->camera, V4L2_CID_EXPOSURE_AUTO) == - V4L2_EXPOSURE_MANUAL; + mp_camera_control_get_bool(info->camera, + V4L2_CID_AUTOGAIN) == 0; current_controls.gain = mp_camera_control_get_int32( info->camera, info->gain_ctrl); current_controls.exposure_is_manual = - mp_camera_control_get_bool(info->camera, - V4L2_CID_AUTOGAIN) == 0; + mp_camera_control_get_int32( + info->camera, V4L2_CID_EXPOSURE_AUTO) == + V4L2_EXPOSURE_MANUAL; current_controls.exposure = mp_camera_control_get_int32( info->camera, V4L2_CID_EXPOSURE); } @@ -508,11 +524,13 @@ update_state(MPPipeline *pipeline, const struct mp_io_pipeline_state *state) has_changed = has_changed || burst_length != state->burst_length || preview_width != state->preview_width || - preview_height != state->preview_height; + preview_height != state->preview_height || + device_rotation != state->device_rotation; burst_length = state->burst_length; preview_width = state->preview_width; preview_height = state->preview_height; + device_rotation = state->device_rotation; if (camera) { struct control_state previous_desired = desired_controls; diff --git a/io_pipeline.h b/src/io_pipeline.h similarity index 84% rename from io_pipeline.h rename to src/io_pipeline.h index 10a6298..62b0c8b 100644 --- a/io_pipeline.h +++ b/src/io_pipeline.h @@ -10,6 +10,8 @@ struct mp_io_pipeline_state { int preview_width; int preview_height; + int device_rotation; + bool gain_is_manual; int gain; @@ -23,4 +25,6 @@ void mp_io_pipeline_stop(); void mp_io_pipeline_focus(); void mp_io_pipeline_capture(); +void mp_io_pipeline_release_buffer(uint32_t buffer_index); + void mp_io_pipeline_update_state(const struct mp_io_pipeline_state *state); diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..b096e8e --- /dev/null +++ b/src/main.c @@ -0,0 +1,963 @@ +#include "main.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gl_util.h" +#include "camera_config.h" +#include "io_pipeline.h" +#include "process_pipeline.h" + +#define RENDERDOC + +#ifdef RENDERDOC +#include +#include +RENDERDOC_API_1_1_2 *rdoc_api = NULL; +#endif + +enum user_control { USER_CONTROL_ISO, USER_CONTROL_SHUTTER }; + +static bool camera_is_initialized = false; +static const struct mp_camera_config *camera = NULL; +static MPCameraMode mode; + +static int preview_width = -1; +static int preview_height = -1; + +static int device_rotation = 0; + +static bool gain_is_manual = false; +static int gain; +static int gain_max; + +static bool exposure_is_manual = false; +static int exposure; + +static bool has_auto_focus_continuous; +static bool has_auto_focus_start; + +static MPProcessPipelineBuffer *current_preview_buffer = NULL; +static int preview_buffer_width = -1; +static int preview_buffer_height = -1; + +static char last_path[260] = ""; + +static MPZBarScanResult *zbar_result = NULL; + +static int burst_length = 3; + +// Widgets +GtkWidget *preview; +GtkWidget *main_stack; +GtkWidget *open_last_stack; +GtkWidget *thumb_last; +GtkWidget *process_spinner; +GtkWidget *scanned_codes; +GtkWidget *preview_top_box; +GtkWidget *preview_bottom_box; + +int +remap(int value, int input_min, int input_max, int output_min, int output_max) +{ + const long long factor = 1000000000; + long long output_spread = output_max - output_min; + long long input_spread = input_max - input_min; + + long long zero_value = value - input_min; + zero_value *= factor; + long long percentage = zero_value / input_spread; + + long long zero_output = percentage * output_spread / factor; + + long long result = output_min + zero_output; + return (int)result; +} + +static void +update_io_pipeline() +{ + struct mp_io_pipeline_state io_state = { + .camera = camera, + .burst_length = burst_length, + .preview_width = preview_width, + .preview_height = preview_height, + .device_rotation = device_rotation, + .gain_is_manual = gain_is_manual, + .gain = gain, + .exposure_is_manual = exposure_is_manual, + .exposure = exposure, + }; + mp_io_pipeline_update_state(&io_state); +} + +static bool +update_state(const struct mp_main_state *state) +{ + if (!camera_is_initialized) { + camera_is_initialized = true; + } + + if (camera == state->camera) { + mode = state->mode; + + if (!gain_is_manual) { + gain = state->gain; + } + gain_max = state->gain_max; + + if (!exposure_is_manual) { + exposure = state->exposure; + } + + has_auto_focus_continuous = state->has_auto_focus_continuous; + has_auto_focus_start = state->has_auto_focus_start; + } + + preview_buffer_width = state->image_width; + preview_buffer_height = state->image_height; + + return false; +} + +void +mp_main_update_state(const struct mp_main_state *state) +{ + struct mp_main_state *state_copy = malloc(sizeof(struct mp_main_state)); + *state_copy = *state; + + g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, + (GSourceFunc)update_state, state_copy, free); +} + +static bool set_zbar_result(MPZBarScanResult *result) +{ + if (zbar_result) { + for (uint8_t i = 0; i < zbar_result->size; ++i) { + free(zbar_result->codes[i].data); + } + + free(zbar_result); + } + + zbar_result = result; + gtk_widget_queue_draw(preview); + + return false; +} + +void mp_main_set_zbar_result(MPZBarScanResult *result) +{ + g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, + (GSourceFunc)set_zbar_result, result, NULL); +} + +static bool +set_preview(MPProcessPipelineBuffer *buffer) +{ + if (current_preview_buffer) { + mp_process_pipeline_buffer_unref(current_preview_buffer); + } + current_preview_buffer = buffer; + gtk_widget_queue_draw(preview); + return false; +} + +void +mp_main_set_preview(MPProcessPipelineBuffer *buffer) +{ + g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, + (GSourceFunc)set_preview, buffer, NULL); +} + +struct capture_completed_args { + GdkTexture *thumb; + char *fname; +}; + +static bool +capture_completed(struct capture_completed_args *args) +{ + strncpy(last_path, args->fname, 259); + + gtk_image_set_from_paintable(GTK_IMAGE(thumb_last), + GDK_PAINTABLE(args->thumb)); + + gtk_spinner_stop(GTK_SPINNER(process_spinner)); + gtk_stack_set_visible_child(GTK_STACK(open_last_stack), thumb_last); + + g_object_unref(args->thumb); + g_free(args->fname); + + return false; +} + +void +mp_main_capture_completed(GdkTexture *thumb, const char *fname) +{ + struct capture_completed_args *args = malloc(sizeof(struct capture_completed_args)); + args->thumb = thumb; + args->fname = g_strdup(fname); + g_main_context_invoke_full(g_main_context_default(), G_PRIORITY_DEFAULT_IDLE, + (GSourceFunc)capture_completed, args, free); +} + +static GLuint blit_program; +static GLuint blit_uniform_transform; +static GLuint blit_uniform_texture; +static GLuint solid_program; +static GLuint solid_uniform_color; +static GLuint quad; + +static void +preview_realize(GtkGLArea *area) +{ + gtk_gl_area_make_current(area); + + if (gtk_gl_area_get_error(area) != NULL) { + return; + } + + // Make a VAO for OpenGL + if (!gtk_gl_area_get_use_es(area)) { + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + check_gl(); + } + + GLuint blit_shaders[] = { + gl_util_load_shader("/org/postmarketos/Megapixels/blit.vert", GL_VERTEX_SHADER, NULL, 0), + gl_util_load_shader("/org/postmarketos/Megapixels/blit.frag", GL_FRAGMENT_SHADER, NULL, 0), + }; + + blit_program = gl_util_link_program(blit_shaders, 2); + glBindAttribLocation(blit_program, GL_UTIL_VERTEX_ATTRIBUTE, "vert"); + glBindAttribLocation(blit_program, GL_UTIL_TEX_COORD_ATTRIBUTE, "tex_coord"); + check_gl(); + + blit_uniform_transform = glGetUniformLocation(blit_program, "transform"); + blit_uniform_texture = glGetUniformLocation(blit_program, "texture"); + + GLuint solid_shaders[] = { + gl_util_load_shader("/org/postmarketos/Megapixels/solid.vert", GL_VERTEX_SHADER, NULL, 0), + gl_util_load_shader("/org/postmarketos/Megapixels/solid.frag", GL_FRAGMENT_SHADER, NULL, 0), + }; + + solid_program = gl_util_link_program(solid_shaders, 2); + glBindAttribLocation(solid_program, GL_UTIL_VERTEX_ATTRIBUTE, "vert"); + check_gl(); + + solid_uniform_color = glGetUniformLocation(solid_program, "color"); + + quad = gl_util_new_quad(); +} + +static void +position_preview(float *offset_x, float *offset_y, float *size_x, float *size_y) +{ + int buffer_width, buffer_height; + if (device_rotation == 0 || device_rotation == 180) { + buffer_width = preview_buffer_width; + buffer_height = preview_buffer_height; + } else { + buffer_width = preview_buffer_height; + buffer_height = preview_buffer_width; + } + + int scale_factor = gtk_widget_get_scale_factor(preview); + int top_height = gtk_widget_get_allocated_height(preview_top_box) * scale_factor; + int bottom_height = gtk_widget_get_allocated_height(preview_bottom_box) * scale_factor; + int inner_height = preview_height - top_height - bottom_height; + + double scale = MIN(preview_width / (float) buffer_width, preview_height / (float) buffer_height); + + *size_x = scale * buffer_width; + *size_y = scale * buffer_height; + + *offset_x = (preview_width - *size_x) / 2.0; + + if (*size_y > inner_height) { + *offset_y = (preview_height - *size_y) / 2.0; + } else { + *offset_y = top_height + (inner_height - *size_y) / 2.0; + } + +} + +static gboolean +preview_draw(GtkGLArea *area, GdkGLContext *ctx, gpointer data) +{ + if (gtk_gl_area_get_error(area) != NULL) { + return FALSE; + } + + if (!camera_is_initialized) { + return FALSE; + } + +#ifdef RENDERDOC + if (rdoc_api) rdoc_api->StartFrameCapture(NULL, NULL); +#endif + + glClearColor(0, 0, 0, 1); + glClear(GL_COLOR_BUFFER_BIT); + + float offset_x, offset_y, size_x, size_y; + position_preview(&offset_x, &offset_y, &size_x, &size_y); + glViewport(offset_x, + preview_height - size_y - offset_y, + size_x, + size_y); + + if (current_preview_buffer) { + glUseProgram(blit_program); + + GLfloat rotation_list[4] = { 0, -1, 0, 1 }; + int rotation_index = device_rotation / 90; + + GLfloat sin_rot = rotation_list[rotation_index]; + GLfloat cos_rot = rotation_list[(4 + rotation_index - 1) % 4]; + GLfloat matrix[9] = { + cos_rot, sin_rot, 0, + -sin_rot, cos_rot, 0, + 0, 0, 1, + }; + glUniformMatrix3fv(blit_uniform_transform, 1, GL_FALSE, matrix); + check_gl(); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, mp_process_pipeline_buffer_get_texture_id(current_preview_buffer)); + glUniform1i(blit_uniform_texture, 0); + check_gl(); + + gl_util_bind_quad(quad); + gl_util_draw_quad(quad); + } + + if (zbar_result) { + GLuint buffer; + if (!gtk_gl_area_get_use_es(area)) { + glGenBuffers(1, &buffer); + glBindBuffer(GL_ARRAY_BUFFER, buffer); + check_gl(); + } + + glUseProgram(solid_program); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + glUniform4f(solid_uniform_color, 1, 0, 0, 0.5); + + for (uint8_t i = 0; i < zbar_result->size; ++i) { + MPZBarCode *code = &zbar_result->codes[i]; + + GLfloat vertices[] = { + code->bounds_x[0], code->bounds_y[0], + code->bounds_x[1], code->bounds_y[1], + code->bounds_x[3], code->bounds_y[3], + code->bounds_x[2], code->bounds_y[2], + }; + + for (int i = 0; i < 4; ++i) { + vertices[i * 2] = 2 * vertices[i * 2] / preview_buffer_width - 1.0; + vertices[i * 2 + 1] = 1.0 - 2 * vertices[i * 2 + 1] / preview_buffer_height; + } + + if (gtk_gl_area_get_use_es(area)) { + glVertexAttribPointer(GL_UTIL_VERTEX_ATTRIBUTE, 2, GL_FLOAT, 0, 0, vertices); + check_gl(); + glEnableVertexAttribArray(GL_UTIL_VERTEX_ATTRIBUTE); + check_gl(); + } else { + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STREAM_DRAW); + check_gl(); + + glVertexAttribPointer(GL_UTIL_VERTEX_ATTRIBUTE, 2, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(GL_UTIL_VERTEX_ATTRIBUTE); + check_gl(); + } + + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + check_gl(); + } + + glDisable(GL_BLEND); + glBindBuffer(GL_ARRAY_BUFFER, 0); + } + + glFlush(); + +#ifdef RENDERDOC + if(rdoc_api) rdoc_api->EndFrameCapture(NULL, NULL); +#endif + + return FALSE; +} + +static gboolean +preview_resize(GtkWidget *widget, int width, int height, gpointer data) +{ + if (preview_width != width || preview_height != height) { + preview_width = width; + preview_height = height; + update_io_pipeline(); + } + + return TRUE; +} + +void +run_open_last_action(GSimpleAction *action, GVariant *param, gpointer user_data) +{ + char uri[275]; + GError *error = NULL; + + if (strlen(last_path) == 0) { + return; + } + sprintf(uri, "file://%s", last_path); + if (!g_app_info_launch_default_for_uri(uri, NULL, &error)) { + g_printerr("Could not launch image viewer for '%s': %s\n", uri, error->message); + } +} + +void +run_open_photos_action(GSimpleAction *action, GVariant *param, gpointer user_data) +{ + char uri[270]; + GError *error = NULL; + sprintf(uri, "file://%s", g_get_user_special_dir(G_USER_DIRECTORY_PICTURES)); + if (!g_app_info_launch_default_for_uri(uri, NULL, &error)) { + g_printerr("Could not launch image viewer: %s\n", error->message); + } +} + +void +run_capture_action(GSimpleAction *action, GVariant *param, gpointer user_data) +{ + gtk_spinner_start(GTK_SPINNER(process_spinner)); + gtk_stack_set_visible_child(GTK_STACK(open_last_stack), process_spinner); + mp_io_pipeline_capture(); +} + +void +run_quit_action(GSimpleAction *action, GVariant *param, GApplication *app) +{ + g_application_quit(app); +} + +static bool +check_point_inside_bounds(int x, int y, int *bounds_x, int *bounds_y) +{ + bool right = false, left = false, top = false, bottom = false; + + for (int i = 0; i < 4; ++i) { + if (x <= bounds_x[i]) + left = true; + if (x >= bounds_x[i]) + right = true; + if (y <= bounds_y[i]) + top = true; + if (y >= bounds_y[i]) + bottom = true; + } + + return right && left && top && bottom; +} + +static void +on_zbar_dialog_response(GtkDialog *dialog, int response, char *data) +{ + GError *error = NULL; + switch (response) { + case GTK_RESPONSE_YES: + if (!g_app_info_launch_default_for_uri(data, + NULL, &error)) { + g_printerr("Could not launch application: %s\n", + error->message); + } + case GTK_RESPONSE_ACCEPT: + { + GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(dialog)); + gdk_clipboard_set_text( + gdk_display_get_clipboard(display), + data); + } + case GTK_RESPONSE_CANCEL: + break; + default: + g_printerr("Wrong dialog response: %d\n", response); + } + + g_free(data); + gtk_window_destroy(GTK_WINDOW(dialog)); +} + +static void +on_zbar_code_tapped(GtkWidget *widget, const MPZBarCode *code) +{ + GtkWidget *dialog; + GtkDialogFlags flags = GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT; + bool data_is_url = g_uri_is_valid( + code->data, G_URI_FLAGS_PARSE_RELAXED, NULL); + + char* data = strdup(code->data); + + if (data_is_url) { + dialog = gtk_message_dialog_new( + GTK_WINDOW(gtk_widget_get_root(widget)), + flags, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + "Found a URL '%s' encoded in a %s code.", + code->data, + code->type); + gtk_dialog_add_buttons( + GTK_DIALOG(dialog), + "_Open URL", + GTK_RESPONSE_YES, + NULL); + } else { + dialog = gtk_message_dialog_new( + GTK_WINDOW(gtk_widget_get_root(widget)), + flags, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_NONE, + "Found '%s' encoded in a %s code.", + code->data, + code->type); + } + gtk_dialog_add_buttons( + GTK_DIALOG(dialog), + "_Copy", + GTK_RESPONSE_ACCEPT, + "_Cancel", + GTK_RESPONSE_CANCEL, + NULL); + + g_signal_connect(dialog, "response", G_CALLBACK(on_zbar_dialog_response), data); + + gtk_widget_show(GTK_WIDGET(dialog)); +} + +static void +preview_pressed(GtkGestureClick *gesture, int n_press, double x, double y) +{ + GtkWidget *widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture)); + int scale_factor = gtk_widget_get_scale_factor(widget); + + // Tapped zbar result + if (zbar_result) { + // Transform the event coordinates to the image + float offset_x, offset_y, size_x, size_y; + position_preview(&offset_x, &offset_y, &size_x, &size_y); + + int zbar_x = (x - offset_x) * scale_factor / size_x * preview_buffer_width; + int zbar_y = (y - offset_y) * scale_factor / size_y * preview_buffer_height; + + for (uint8_t i = 0; i < zbar_result->size; ++i) { + MPZBarCode *code = &zbar_result->codes[i]; + + if (check_point_inside_bounds(zbar_x, zbar_y, code->bounds_x, code->bounds_y)) { + on_zbar_code_tapped(widget, code); + return; + } + } + } + + // Tapped preview image itself, try focussing + if (has_auto_focus_start) { + mp_io_pipeline_focus(); + } +} + +static void +run_camera_switch_action(GSimpleAction *action, GVariant *param, gpointer user_data) +{ + size_t next_index = camera->index + 1; + const struct mp_camera_config *next_camera = + mp_get_camera_config(next_index); + + if (!next_camera) { + next_index = 0; + next_camera = mp_get_camera_config(next_index); + } + + camera = next_camera; + update_io_pipeline(); +} + +static void +run_open_settings_action(GSimpleAction *action, GVariant *param, gpointer user_data) +{ + gtk_stack_set_visible_child_name(GTK_STACK(main_stack), "settings"); +} + +static void +run_close_settings_action(GSimpleAction *action, GVariant *param, gpointer user_data) +{ + gtk_stack_set_visible_child_name(GTK_STACK(main_stack), "main"); +} + +static void +on_controls_scale_changed(GtkAdjustment *adjustment, void (*set_fn)(double)) +{ + set_fn(gtk_adjustment_get_value(adjustment)); +} + +static void +update_value(GtkAdjustment *adjustment, GtkLabel *label) +{ + char buf[12]; + snprintf(buf, 12, "%.0f", gtk_adjustment_get_value(adjustment)); + gtk_label_set_label(label, buf); +} + +static void +on_auto_controls_toggled(GtkToggleButton *button, void (*set_auto_fn)(bool)) +{ + set_auto_fn(gtk_toggle_button_get_active(button)); +} + +static void +update_scale(GtkToggleButton *button, GtkScale *scale) +{ + gtk_widget_set_sensitive(GTK_WIDGET(scale), !gtk_toggle_button_get_active(button)); +} + +static void +open_controls(GtkWidget *parent, const char *title_name, + double min_value, double max_value, double current, + bool auto_enabled, + void (*set_fn)(double), + void (*set_auto_fn)(bool)) +{ + GtkBuilder *builder = gtk_builder_new_from_resource( + "/org/postmarketos/Megapixels/controls-popover.ui"); + GtkPopover *popover = GTK_POPOVER(gtk_builder_get_object(builder, "controls")); + GtkScale *scale = GTK_SCALE(gtk_builder_get_object(builder, "scale")); + GtkLabel *title = GTK_LABEL(gtk_builder_get_object(builder, "title")); + GtkLabel *value_label = GTK_LABEL(gtk_builder_get_object(builder, "value-label")); + GtkToggleButton *auto_button = GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "auto-button")); + + gtk_label_set_label(title, title_name); + + GtkAdjustment *adjustment = gtk_range_get_adjustment(GTK_RANGE(scale)); + gtk_adjustment_set_lower(adjustment, min_value); + gtk_adjustment_set_upper(adjustment, max_value); + gtk_adjustment_set_value(adjustment, current); + update_value(adjustment, value_label); + + gtk_toggle_button_set_active(auto_button, auto_enabled); + update_scale(auto_button, scale); + + g_signal_connect(adjustment, "value-changed", G_CALLBACK(on_controls_scale_changed), set_fn); + g_signal_connect(adjustment, "value-changed", G_CALLBACK(update_value), value_label); + g_signal_connect(auto_button, "toggled", G_CALLBACK(on_auto_controls_toggled), set_auto_fn); + g_signal_connect(auto_button, "toggled", G_CALLBACK(update_scale), scale); + + gtk_widget_set_parent(GTK_WIDGET(popover), parent); + gtk_popover_popup(popover); + // g_object_unref(popover); +} + +static void +set_gain(double value) +{ + if (gain != (int)value) { + gain = value; + update_io_pipeline(); + } +} + +static void +set_gain_auto(bool is_auto) +{ + if (gain_is_manual != !is_auto) { + gain_is_manual = !is_auto; + update_io_pipeline(); + } +} + +static void +open_iso_controls(GtkWidget *button, gpointer user_data) +{ + open_controls(button, "ISO", 0, gain_max, gain, !gain_is_manual, set_gain, set_gain_auto); +} + +static void +set_shutter(double value) +{ + int new_exposure = + (int)(value / 360.0 * camera->capture_mode.height); + if (new_exposure != exposure) { + exposure = new_exposure; + update_io_pipeline(); + } +} + +static void +set_shutter_auto(bool is_auto) +{ + if (exposure_is_manual != !is_auto) { + exposure_is_manual = !is_auto; + update_io_pipeline(); + } +} + +static void +open_shutter_controls(GtkWidget *button, gpointer user_data) +{ + open_controls(button, "Shutter", 1.0, 360.0, exposure, !exposure_is_manual, set_shutter, set_shutter_auto); +} + +static void +on_realize(GtkWidget *window, gpointer *data) +{ + GtkNative *native = gtk_widget_get_native(window); + mp_process_pipeline_init_gl(gtk_native_get_surface(native)); + + camera = mp_get_camera_config(0); + update_io_pipeline(); +} + +static GSimpleAction * +create_simple_action(GtkApplication *app, const char *name, GCallback callback) +{ + GSimpleAction *action = g_simple_action_new(name, NULL); + g_signal_connect(action, "activate", callback, app); + g_action_map_add_action(G_ACTION_MAP(app), G_ACTION(action)); + return action; +} + +static void update_ui_rotation() +{ + if (device_rotation == 0 || device_rotation == 180) { + // Portrait + gtk_widget_set_halign(preview_top_box, GTK_ALIGN_FILL); + gtk_orientable_set_orientation(GTK_ORIENTABLE(preview_top_box), GTK_ORIENTATION_VERTICAL); + + gtk_widget_set_halign(preview_bottom_box, GTK_ALIGN_FILL); + gtk_orientable_set_orientation(GTK_ORIENTABLE(preview_bottom_box), GTK_ORIENTATION_HORIZONTAL); + + if (device_rotation == 0) { + gtk_widget_set_valign(preview_top_box, GTK_ALIGN_START); + gtk_widget_set_valign(preview_bottom_box, GTK_ALIGN_END); + } else { + gtk_widget_set_valign(preview_top_box, GTK_ALIGN_END); + gtk_widget_set_valign(preview_bottom_box, GTK_ALIGN_START); + } + } else { + // Landscape + gtk_widget_set_valign(preview_top_box, GTK_ALIGN_FILL); + gtk_orientable_set_orientation(GTK_ORIENTABLE(preview_top_box), GTK_ORIENTATION_HORIZONTAL); + + gtk_widget_set_valign(preview_bottom_box, GTK_ALIGN_FILL); + gtk_orientable_set_orientation(GTK_ORIENTABLE(preview_bottom_box), GTK_ORIENTATION_VERTICAL); + + if (device_rotation == 90) { + gtk_widget_set_halign(preview_top_box, GTK_ALIGN_END); + gtk_widget_set_halign(preview_bottom_box, GTK_ALIGN_START); + } else { + gtk_widget_set_halign(preview_top_box, GTK_ALIGN_START); + gtk_widget_set_halign(preview_bottom_box, GTK_ALIGN_END); + } + } +} + +static void display_config_received(GDBusConnection *conn, GAsyncResult *res, gpointer user_data) +{ + GError *error = NULL; + GVariant *result = g_dbus_connection_call_finish(conn, res, &error); + + if (!result) { + printf("Failed to get display configuration: %s\n", error->message); + return; + } + + GVariant *configs = g_variant_get_child_value(result, 1); + if (g_variant_n_children(configs) == 0) { + return; + } + + GVariant *config = g_variant_get_child_value(configs, 0); + GVariant *rot_config = g_variant_get_child_value(config, 7); + uint32_t rotation_index = g_variant_get_uint32(rot_config); + + assert(rotation_index < 4); + int new_rotation = rotation_index * 90; + + if (new_rotation != device_rotation) { + device_rotation = new_rotation; + update_io_pipeline(); + update_ui_rotation(); + } +} + +static void update_screen_rotation(GDBusConnection *conn) +{ + g_dbus_connection_call(conn, + "org.gnome.Mutter.DisplayConfig", + "/org/gnome/Mutter/DisplayConfig", + "org.gnome.Mutter.DisplayConfig", + "GetResources", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NO_AUTO_START, + -1, + NULL, + (GAsyncReadyCallback)display_config_received, + NULL); +} + +static void on_screen_rotate( + GDBusConnection *conn, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + update_screen_rotation(conn); +} + +static void +activate(GtkApplication *app, gpointer data) +{ + g_object_set(gtk_settings_get_default(), "gtk-application-prefer-dark-theme", + TRUE, NULL); + + GdkDisplay *display = gdk_display_get_default(); + GtkIconTheme *icon_theme = gtk_icon_theme_get_for_display(display); + gtk_icon_theme_add_resource_path(icon_theme, "/org/postmarketos/Megapixels"); + + GtkCssProvider *provider = gtk_css_provider_new(); + gtk_css_provider_load_from_resource( + provider, "/org/postmarketos/Megapixels/camera.css"); + gtk_style_context_add_provider_for_display( + display, GTK_STYLE_PROVIDER(provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + GtkBuilder *builder = gtk_builder_new_from_resource( + "/org/postmarketos/Megapixels/camera.ui"); + + GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "window")); + GtkWidget *iso_button = GTK_WIDGET(gtk_builder_get_object(builder, "iso-controls-button")); + GtkWidget *shutter_button = GTK_WIDGET(gtk_builder_get_object(builder, "shutter-controls-button")); + preview = GTK_WIDGET(gtk_builder_get_object(builder, "preview")); + main_stack = GTK_WIDGET(gtk_builder_get_object(builder, "main_stack")); + open_last_stack = GTK_WIDGET(gtk_builder_get_object(builder, "open_last_stack")); + thumb_last = GTK_WIDGET(gtk_builder_get_object(builder, "thumb_last")); + process_spinner = GTK_WIDGET(gtk_builder_get_object(builder, "process_spinner")); + scanned_codes = GTK_WIDGET(gtk_builder_get_object(builder, "scanned-codes")); + preview_top_box = GTK_WIDGET(gtk_builder_get_object(builder, "top-box")); + preview_bottom_box = GTK_WIDGET(gtk_builder_get_object(builder, "bottom-box")); + + g_signal_connect(window, "realize", G_CALLBACK(on_realize), NULL); + + g_signal_connect(preview, "realize", G_CALLBACK(preview_realize), NULL); + g_signal_connect(preview, "render", G_CALLBACK(preview_draw), NULL); + g_signal_connect(preview, "resize", G_CALLBACK(preview_resize), NULL); + GtkGesture *click = gtk_gesture_click_new(); + g_signal_connect(click, "pressed", G_CALLBACK(preview_pressed), NULL); + gtk_widget_add_controller(preview, GTK_EVENT_CONTROLLER(click)); + + g_signal_connect(iso_button, "clicked", G_CALLBACK(open_iso_controls), NULL); + g_signal_connect(shutter_button, "clicked", G_CALLBACK(open_shutter_controls), NULL); + + // Setup actions + create_simple_action(app, "capture", G_CALLBACK(run_capture_action)); + create_simple_action(app, "switch-camera", G_CALLBACK(run_camera_switch_action)); + create_simple_action(app, "open-settings", G_CALLBACK(run_open_settings_action)); + create_simple_action(app, "close-settings", G_CALLBACK(run_close_settings_action)); + create_simple_action(app, "open-last", G_CALLBACK(run_open_last_action)); + create_simple_action(app, "open-photos", G_CALLBACK(run_open_photos_action)); + create_simple_action(app, "quit", G_CALLBACK(run_quit_action)); + + // Setup shortcuts + const char *capture_accels[] = { "space", NULL }; + gtk_application_set_accels_for_action(app, "app.capture", capture_accels); + + const char *quit_accels[] = { "q", "w", NULL }; + gtk_application_set_accels_for_action(app, "app.quit", quit_accels); + + // Listen for phosh rotation + GDBusConnection *conn = g_application_get_dbus_connection(G_APPLICATION(app)); + g_dbus_connection_signal_subscribe( + conn, + NULL, + "org.gnome.Mutter.DisplayConfig", + "MonitorsChanged", + "/org/gnome/Mutter/DisplayConfig", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + &on_screen_rotate, + NULL, + NULL); + update_screen_rotation(conn); + + mp_io_pipeline_start(); + + gtk_application_add_window(app, GTK_WINDOW(window)); + gtk_widget_show(window); +} + +static void +shutdown(GApplication *app, gpointer data) +{ + // Only do cleanup in development, let the OS clean up otherwise +#ifdef DEBUG + mp_io_pipeline_stop(); +#endif +} + +int +main(int argc, char *argv[]) +{ +#ifdef RENDERDOC + { + void *mod = dlopen("librenderdoc.so", RTLD_NOW | RTLD_NOLOAD); + if (mod) + { + pRENDERDOC_GetAPI RENDERDOC_GetAPI = (pRENDERDOC_GetAPI)dlsym(mod, "RENDERDOC_GetAPI"); + int ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_1_2, (void **)&rdoc_api); + assert(ret == 1); + } + else + { + printf("Renderdoc not found\n"); + } + } +#endif + + if (!mp_load_config()) + return 1; + + setenv("LC_NUMERIC", "C", 1); + + GtkApplication *app = gtk_application_new("org.postmarketos.Megapixels", 0); + + g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); + g_signal_connect(app, "shutdown", G_CALLBACK(shutdown), NULL); + + g_application_run(G_APPLICATION(app), argc, argv); + + return 0; +} diff --git a/main.h b/src/main.h similarity index 65% rename from main.h rename to src/main.h index 2995993..bbebca9 100644 --- a/main.h +++ b/src/main.h @@ -2,14 +2,16 @@ #include "camera_config.h" #include "zbar_pipeline.h" +#include "process_pipeline.h" #include "gtk/gtk.h" -#define MP_MAIN_THUMB_SIZE 24 - struct mp_main_state { const struct mp_camera_config *camera; MPCameraMode mode; + int image_width; + int image_height; + bool gain_is_manual; int gain; int gain_max; @@ -23,12 +25,9 @@ struct mp_main_state { 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(cairo_surface_t *thumb, const char *fname); +void mp_main_set_preview(MPProcessPipelineBuffer *buffer); +void mp_main_capture_completed(GdkTexture *thumb, const char *fname); void mp_main_set_zbar_result(MPZBarScanResult *result); int remap(int value, int input_min, int input_max, int output_min, int output_max); - -void draw_surface_scaled_centered(cairo_t *cr, uint32_t dst_width, uint32_t dst_height, - cairo_surface_t *surface); diff --git a/matrix.c b/src/matrix.c similarity index 100% rename from matrix.c rename to src/matrix.c diff --git a/matrix.h b/src/matrix.h similarity index 100% rename from matrix.h rename to src/matrix.h diff --git a/pipeline.c b/src/pipeline.c similarity index 79% rename from pipeline.c rename to src/pipeline.c index fcfa4c2..80715f0 100644 --- a/pipeline.c +++ b/src/pipeline.c @@ -65,6 +65,27 @@ mp_pipeline_invoke(MPPipeline *pipeline, MPPipelineCallback callback, } } +static bool +unlock_mutex(GMutex *mutex) +{ + g_mutex_unlock(mutex); + return false; +} + +void +mp_pipeline_sync(MPPipeline *pipeline) +{ + GMutex mutex; + g_mutex_init(&mutex); + g_mutex_lock(&mutex); + + g_main_context_invoke_full(pipeline->main_context, G_PRIORITY_LOW, (GSourceFunc)unlock_mutex, &mutex, NULL); + g_mutex_lock(&mutex); + g_mutex_unlock(&mutex); + + g_mutex_clear(&mutex); +} + void mp_pipeline_free(MPPipeline *pipeline) { @@ -80,21 +101,24 @@ mp_pipeline_free(MPPipeline *pipeline) struct capture_source_args { MPCamera *camera; - void (*callback)(MPImage, void *); + void (*callback)(MPBuffer, void *); void *user_data; }; static bool on_capture(int fd, GIOCondition condition, struct capture_source_args *args) { - mp_camera_capture_image(args->camera, args->callback, args->user_data); + MPBuffer buffer; + if (mp_camera_capture_buffer(args->camera, &buffer)) { + args->callback(buffer, args->user_data); + } return true; } // Not thread safe GSource * mp_pipeline_add_capture_source(MPPipeline *pipeline, MPCamera *camera, - void (*callback)(MPImage, void *), void *user_data) + void (*callback)(MPBuffer, void *), void *user_data) { int video_fd = mp_camera_get_video_fd(camera); GSource *video_source = g_unix_fd_source_new(video_fd, G_IO_IN); diff --git a/pipeline.h b/src/pipeline.h similarity index 77% rename from pipeline.h rename to src/pipeline.h index 6f71faa..67eaeff 100644 --- a/pipeline.h +++ b/src/pipeline.h @@ -11,8 +11,10 @@ typedef void (*MPPipelineCallback)(MPPipeline *, const void *); MPPipeline *mp_pipeline_new(); void mp_pipeline_invoke(MPPipeline *pipeline, MPPipelineCallback callback, const void *data, size_t size); +// Wait until all pending tasks have completed +void mp_pipeline_sync(MPPipeline *pipeline); void mp_pipeline_free(MPPipeline *pipeline); GSource *mp_pipeline_add_capture_source(MPPipeline *pipeline, MPCamera *camera, - void (*callback)(MPImage, void *), + void (*callback)(MPBuffer, void *), void *user_data); diff --git a/process_pipeline.c b/src/process_pipeline.c similarity index 55% rename from process_pipeline.c rename to src/process_pipeline.c index 64003a2..dd5e4f4 100644 --- a/process_pipeline.c +++ b/src/process_pipeline.c @@ -2,15 +2,18 @@ #include "pipeline.h" #include "zbar_pipeline.h" +#include "io_pipeline.h" #include "main.h" #include "config.h" -#include "quickpreview.h" +#include "gles2_debayer.h" #include #include #include -#include #include +#include "gl_util.h" +#include + #define TIFFTAG_FORWARDMATRIX1 50964 static const float colormatrix_srgb[] = { 3.2409, -1.5373, -0.4986, -0.9692, 1.8759, @@ -26,6 +29,7 @@ static volatile int frames_processed = 0; static volatile int frames_received = 0; static const struct mp_camera_config *camera; +static int camera_rotation; static MPCameraMode mode; @@ -35,6 +39,11 @@ static int captures_remaining = 0; static int preview_width; static int preview_height; +static int device_rotation; + +static int output_buffer_width = -1; +static int output_buffer_height = -1; + // static bool gain_is_manual; static int gain; static int gain_max; @@ -60,28 +69,18 @@ register_custom_tiff_tags(TIFF *tif) 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); + // Check postprocess.sh in the current working directory + sprintf(script, "./data/%s", filename); if (access(script, F_OK) != -1) { - sprintf(script, "./%s", filename); + sprintf(script, "./data/%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); + sprintf(script, "%s/megapixels/%s", g_get_user_config_dir(), filename); if (access(script, F_OK) != -1) { printf("Found postprocessor script at %s\n", script); return true; @@ -134,47 +133,208 @@ mp_process_pipeline_stop() mp_zbar_pipeline_stop(); } -static cairo_surface_t * -process_image_for_preview(const MPImage *image) +void +mp_process_pipeline_sync() { - 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); + mp_pipeline_sync(pipeline); +} - cairo_surface_t *surface = cairo_image_surface_create( - CAIRO_FORMAT_RGB24, surface_width, surface_height); +#define NUM_BUFFERS 4 - uint8_t *pixels = cairo_image_surface_get_data(surface); +struct _MPProcessPipelineBuffer { + GLuint texture_id; - quick_preview((uint32_t *)pixels, surface_width, surface_height, image->data, - image->width, image->height, image->pixel_format, - camera->rotate, camera->mirrored, - camera->previewmatrix[0] == 0 ? NULL : camera->previewmatrix, - camera->blacklevel, skip); + _Atomic(int) refcount; +}; +static MPProcessPipelineBuffer output_buffers[NUM_BUFFERS]; - // Create a thumbnail from the preview for the last capture - cairo_surface_t *thumb = NULL; - if (captures_remaining == 1) { - printf("Making thumbnail\n"); - thumb = cairo_image_surface_create( - CAIRO_FORMAT_ARGB32, MP_MAIN_THUMB_SIZE, MP_MAIN_THUMB_SIZE); +void +mp_process_pipeline_buffer_ref(MPProcessPipelineBuffer *buf) +{ + ++buf->refcount; +} - cairo_t *cr = cairo_create(thumb); - draw_surface_scaled_centered( - cr, MP_MAIN_THUMB_SIZE, MP_MAIN_THUMB_SIZE, surface); - cairo_destroy(cr); +void +mp_process_pipeline_buffer_unref(MPProcessPipelineBuffer *buf) +{ + --buf->refcount; +} + +uint32_t +mp_process_pipeline_buffer_get_texture_id(MPProcessPipelineBuffer *buf) +{ + return buf->texture_id; +} + +static GLES2Debayer *gles2_debayer = NULL; + +static GdkGLContext *context; + +#define RENDERDOC + +#ifdef RENDERDOC +#include +extern RENDERDOC_API_1_1_2 *rdoc_api; +#endif + +static void +init_gl(MPPipeline *pipeline, GdkSurface **surface) +{ + GError *error = NULL; + context = gdk_surface_create_gl_context(*surface, &error); + if (context == NULL) { + printf("Failed to initialize OpenGL context: %s\n", error->message); + g_clear_error(&error); + return; } - // Pass processed preview to main and zbar - mp_zbar_pipeline_process_image(cairo_surface_reference(surface)); - mp_main_set_preview(surface); + gdk_gl_context_set_use_es(context, true); + gdk_gl_context_set_required_version(context, 2, 0); + gdk_gl_context_set_forward_compatible(context, false); +#ifdef DEBUG + gdk_gl_context_set_debug_enabled(context, true); +#else + gdk_gl_context_set_debug_enabled(context, false); +#endif + + gdk_gl_context_realize(context, &error); + if (error != NULL) { + printf("Failed to create OpenGL context: %s\n", error->message); + g_clear_object(&context); + g_clear_error(&error); + return; + } + + gdk_gl_context_make_current(context); + check_gl(); + + // Make a VAO for OpenGL + if (!gdk_gl_context_get_use_es(context)) { + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + check_gl(); + } + + gles2_debayer = gles2_debayer_new(MP_PIXEL_FMT_BGGR8); + check_gl(); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + check_gl(); + + gles2_debayer_use(gles2_debayer); + + for (size_t i = 0; i < NUM_BUFFERS; ++i) { + glGenTextures(1, &output_buffers[i].texture_id); + glBindTexture(GL_TEXTURE_2D, output_buffers[i].texture_id); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + } + + glBindTexture(GL_TEXTURE_2D, 0); + + gboolean is_es = gdk_gl_context_get_use_es(context); + int major, minor; + gdk_gl_context_get_version(context, &major, &minor); + + printf("Initialized %s %d.%d\n", is_es ? "OpenGL ES" : "OpenGL", major, minor); +} + +void +mp_process_pipeline_init_gl(GdkSurface *surface) +{ + mp_pipeline_invoke(pipeline, (MPPipelineCallback) init_gl, &surface, sizeof(GdkSurface *)); +} + +static GdkTexture * +process_image_for_preview(const uint8_t *image) +{ +#ifdef PROFILE_DEBAYER + clock_t t1 = clock(); +#endif + + // Pick an available buffer + MPProcessPipelineBuffer *output_buffer = NULL; + for (size_t i = 0; i < NUM_BUFFERS; ++i) { + if (output_buffers[i].refcount == 0) { + output_buffer = &output_buffers[i]; + } + } + + if (output_buffer == NULL) { + return NULL; + } + assert(output_buffer != NULL); + +#ifdef RENDERDOC + if (rdoc_api) rdoc_api->StartFrameCapture(NULL, NULL); +#endif + + // Copy image to a GL texture. TODO: This can be avoided + GLuint input_texture; + glGenTextures(1, &input_texture); + glBindTexture(GL_TEXTURE_2D, input_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, mode.width, mode.height, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, image); + check_gl(); + + gles2_debayer_process( + gles2_debayer, output_buffer->texture_id, input_texture); + check_gl(); + + glFinish(); + + glDeleteTextures(1, &input_texture); + +#ifdef PROFILE_DEBAYER + clock_t t2 = clock(); + printf("process_image_for_preview %fms\n", (float)(t2 - t1) / CLOCKS_PER_SEC * 1000); +#endif + +#ifdef RENDERDOC + if(rdoc_api) rdoc_api->EndFrameCapture(NULL, NULL); +#endif + + mp_process_pipeline_buffer_ref(output_buffer); + mp_main_set_preview(output_buffer); + + // Create a thumbnail from the preview for the last capture + GdkTexture *thumb = NULL; + if (captures_remaining == 1) { + printf("Making thumbnail\n"); + + size_t size = output_buffer_width * output_buffer_height * sizeof(uint32_t); + + uint32_t *data = g_malloc_n(size, 1); + + glReadPixels(0, 0, output_buffer_width, output_buffer_height, GL_RGBA, GL_UNSIGNED_BYTE, data); + check_gl(); + + // Flip vertically + for (size_t y = 0; y < output_buffer_height / 2; ++y) { + for (size_t x = 0; x < output_buffer_width; ++x) { + uint32_t tmp = data[(output_buffer_height - y - 1) * output_buffer_width + x]; + data[(output_buffer_height - y - 1) * output_buffer_width + x] = data[y * output_buffer_width + x]; + data[y * output_buffer_width + x] = tmp; + } + } + + thumb = gdk_memory_texture_new( + output_buffer_width, + output_buffer_height, + GDK_MEMORY_R8G8B8A8, + g_bytes_new_take(data, size), + output_buffer_width * sizeof(uint32_t)); + } return thumb; } static void -process_image_for_capture(const MPImage *image, int count) +process_image_for_capture(const uint8_t *image, int count) { time_t rawtime; time(&rawtime); @@ -193,21 +353,21 @@ process_image_for_capture(const MPImage *image, int count) // 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_IMAGEWIDTH, mode.width >> 4); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, mode.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) { + if (camera_rotation == 0) { orientation = camera->mirrored ? ORIENTATION_TOPRIGHT : ORIENTATION_TOPLEFT; - } else if (camera->rotate == 90) { + } else if (camera_rotation == 90) { orientation = camera->mirrored ? ORIENTATION_RIGHTBOT : ORIENTATION_LEFTBOT; - } else if (camera->rotate == 180) { + } else if (camera_rotation == 180) { orientation = camera->mirrored ? ORIENTATION_BOTLEFT : ORIENTATION_BOTRIGHT; } else { @@ -241,8 +401,8 @@ process_image_for_capture(const MPImage *image, int count) // 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++) { + (unsigned char *)calloc(1, (int)mode.width >> 4); + for (int row = 0; row < (mode.height >> 4); row++) { TIFFWriteScanline(tif, buf, row, 0); } free(buf); @@ -251,8 +411,8 @@ process_image_for_capture(const MPImage *image, int count) // Define main photo TIFFSetField(tif, TIFFTAG_SUBFILETYPE, 0); - TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, image->width); - TIFFSetField(tif, TIFFTAG_IMAGELENGTH, image->height); + TIFFSetField(tif, TIFFTAG_IMAGEWIDTH, mode.width); + TIFFSetField(tif, TIFFTAG_IMAGELENGTH, mode.height); TIFFSetField(tif, TIFFTAG_BITSPERSAMPLE, 8); TIFFSetField(tif, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_CFA); TIFFSetField(tif, TIFFTAG_SAMPLESPERPIXEL, 1); @@ -274,9 +434,9 @@ process_image_for_capture(const MPImage *image, int count) 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); + unsigned char *pLine = (unsigned char *)malloc(mode.width); + for (int row = 0; row < mode.height; row++) { + TIFFWriteScanline(tif, (void *) image + (row * mode.width), row, 0); } free(pLine); TIFFWriteDirectory(tif); @@ -293,7 +453,7 @@ process_image_for_capture(const MPImage *image, int count) TIFFSetField(tif, EXIFTAG_EXPOSURETIME, (mode.frame_interval.numerator / (float)mode.frame_interval.denominator) / - ((float)image->height / (float)exposure)); + ((float)mode.height / (float)exposure)); uint16_t isospeed[1]; isospeed[0] = (uint16_t)remap(gain - 1, 0, gain_max, camera->iso_min, camera->iso_max); @@ -325,7 +485,7 @@ process_image_for_capture(const MPImage *image, int count) } static void -post_process_finished(GSubprocess *proc, GAsyncResult *res, cairo_surface_t *thumb) +post_process_finished(GSubprocess *proc, GAsyncResult *res, GdkTexture *thumb) { char *stdout; g_subprocess_communicate_utf8_finish(proc, res, &stdout, NULL, NULL); @@ -348,7 +508,7 @@ post_process_finished(GSubprocess *proc, GAsyncResult *res, cairo_surface_t *thu } static void -process_capture_burst(cairo_surface_t *thumb) +process_capture_burst(GdkTexture *thumb) { time_t rawtime; time(&rawtime); @@ -401,11 +561,27 @@ process_capture_burst(cairo_surface_t *thumb) } static void -process_image(MPPipeline *pipeline, const MPImage *image) +process_image(MPPipeline *pipeline, const MPBuffer *buffer) { - assert(image->width == mode.width && image->height == mode.height); +#ifdef PROFILE_PROCESS + clock_t t1 = clock(); +#endif - cairo_surface_t *thumb = process_image_for_preview(image); + size_t size = + mp_pixel_format_width_to_bytes(mode.pixel_format, mode.width) * + mode.height; + uint8_t *image = malloc(size); + memcpy(image, buffer->data, size); + mp_io_pipeline_release_buffer(buffer->index); + + MPZBarImage *zbar_image = mp_zbar_image_new(image, mode.pixel_format, mode.width, mode.height, camera_rotation, camera->mirrored); + mp_zbar_pipeline_process_image(mp_zbar_image_ref(zbar_image)); + +#ifdef PROFILE_PROCESS + clock_t t2 = clock(); +#endif + + GdkTexture *thumb = process_image_for_preview(image); if (captures_remaining > 0) { int count = burst_length - captures_remaining; @@ -423,28 +599,35 @@ process_image(MPPipeline *pipeline, const MPImage *image) assert(!thumb); } - free(image->data); + mp_zbar_image_unref(zbar_image); ++frames_processed; if (captures_remaining == 0) { is_capturing = false; } + +#ifdef PROFILE_PROCESS + clock_t t3 = clock(); + printf("process_image %fms, step 1:%fms, step 2:%fms\n", + (float)(t3 - t1) / CLOCKS_PER_SEC * 1000, + (float)(t2 - t1) / CLOCKS_PER_SEC * 1000, + (float)(t3 - t2) / CLOCKS_PER_SEC * 1000); +#endif } void -mp_process_pipeline_process_image(MPImage image) +mp_process_pipeline_process_image(MPBuffer buffer) { // If we haven't processed the previous frame yet, drop this one if (frames_received != frames_processed && !is_capturing) { - printf("Dropped frame at capture\n"); - free(image.data); + mp_io_pipeline_release_buffer(buffer.index); return; } ++frames_received; - mp_pipeline_invoke(pipeline, (MPPipelineCallback)process_image, &image, - sizeof(MPImage)); + mp_pipeline_invoke(pipeline, (MPPipelineCallback)process_image, &buffer, + sizeof(MPBuffer)); } static void @@ -472,17 +655,60 @@ mp_process_pipeline_capture() mp_pipeline_invoke(pipeline, capture, NULL, 0); } +static void +on_output_changed() +{ + output_buffer_width = mode.width / 2; + output_buffer_height = mode.height / 2; + + if (camera->rotate != 0 || camera->rotate != 180) { + int tmp = output_buffer_width; + output_buffer_width = output_buffer_height; + output_buffer_height = tmp; + } + + for (size_t i = 0; i < NUM_BUFFERS; ++i) { + glBindTexture(GL_TEXTURE_2D, output_buffers[i].texture_id); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, output_buffer_width, output_buffer_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); + } + + glBindTexture(GL_TEXTURE_2D, 0); + + gles2_debayer_configure( + gles2_debayer, + output_buffer_width, output_buffer_height, + mode.width, mode.height, + camera->rotate, camera->mirrored, + camera->previewmatrix[0] == 0 ? NULL : camera->previewmatrix, + camera->blacklevel); +} + +static int +mod(int a, int b) +{ + int r = a % b; + return r < 0 ? r + b : r; +} + static void update_state(MPPipeline *pipeline, const struct mp_process_pipeline_state *state) { + const bool output_changed = + !mp_camera_mode_is_equivalent(&mode, &state->mode) + || preview_width != state->preview_width + || preview_height != state->preview_height + || device_rotation != state->device_rotation; + camera = state->camera; mode = state->mode; - burst_length = state->burst_length; - preview_width = state->preview_width; preview_height = state->preview_height; + device_rotation = state->device_rotation; + + burst_length = state->burst_length; + // gain_is_manual = state->gain_is_manual; gain = state->gain; gain_max = state->gain_max; @@ -490,9 +716,17 @@ update_state(MPPipeline *pipeline, const struct mp_process_pipeline_state *state exposure_is_manual = state->exposure_is_manual; exposure = state->exposure; + if (output_changed) { + camera_rotation = mod(camera->rotate - device_rotation, 360); + + on_output_changed(); + } + struct mp_main_state main_state = { .camera = camera, .mode = mode, + .image_width = output_buffer_width, + .image_height = output_buffer_height, .gain_is_manual = state->gain_is_manual, .gain = gain, .gain_max = gain_max, @@ -510,3 +744,6 @@ mp_process_pipeline_update_state(const struct mp_process_pipeline_state *new_sta mp_pipeline_invoke(pipeline, (MPPipelineCallback)update_state, new_state, sizeof(struct mp_process_pipeline_state)); } + +// GTK4 seems to require this +void pango_fc_font_get_languages() {} diff --git a/process_pipeline.h b/src/process_pipeline.h similarity index 52% rename from process_pipeline.h rename to src/process_pipeline.h index 40cadd7..2413d35 100644 --- a/process_pipeline.h +++ b/src/process_pipeline.h @@ -2,6 +2,8 @@ #include "camera_config.h" +typedef struct _GdkSurface GdkSurface; + struct mp_process_pipeline_state { const struct mp_camera_config *camera; MPCameraMode mode; @@ -11,6 +13,8 @@ struct mp_process_pipeline_state { int preview_width; int preview_height; + int device_rotation; + bool gain_is_manual; int gain; int gain_max; @@ -24,7 +28,16 @@ struct mp_process_pipeline_state { void mp_process_pipeline_start(); void mp_process_pipeline_stop(); +void mp_process_pipeline_sync(); -void mp_process_pipeline_process_image(MPImage image); +void mp_process_pipeline_init_gl(GdkSurface *window); + +void mp_process_pipeline_process_image(MPBuffer buffer); void mp_process_pipeline_capture(); void mp_process_pipeline_update_state(const struct mp_process_pipeline_state *state); + +typedef struct _MPProcessPipelineBuffer MPProcessPipelineBuffer; + +void mp_process_pipeline_buffer_ref(MPProcessPipelineBuffer *buf); +void mp_process_pipeline_buffer_unref(MPProcessPipelineBuffer *buf); +uint32_t mp_process_pipeline_buffer_get_texture_id(MPProcessPipelineBuffer *buf); diff --git a/src/renderdoc/app.h b/src/renderdoc/app.h new file mode 100644 index 0000000..4e1fec2 --- /dev/null +++ b/src/renderdoc/app.h @@ -0,0 +1,688 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2019-2021 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#pragma once + +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Documentation for the API is available at https://renderdoc.org/docs/in_application_api.html +// + +#if !defined(RENDERDOC_NO_STDINT) +#include +#endif + +#if defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) +#define RENDERDOC_CC __cdecl +#elif defined(__linux__) +#define RENDERDOC_CC +#elif defined(__APPLE__) +#define RENDERDOC_CC +#else +#error "Unknown platform" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Constants not used directly in below API + +// This is a GUID/magic value used for when applications pass a path where shader debug +// information can be found to match up with a stripped shader. +// the define can be used like so: const GUID RENDERDOC_ShaderDebugMagicValue = +// RENDERDOC_ShaderDebugMagicValue_value +#define RENDERDOC_ShaderDebugMagicValue_struct \ + { \ + 0xeab25520, 0x6670, 0x4865, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff \ + } + +// as an alternative when you want a byte array (assuming x86 endianness): +#define RENDERDOC_ShaderDebugMagicValue_bytearray \ + { \ + 0x20, 0x55, 0xb2, 0xea, 0x70, 0x66, 0x65, 0x48, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff \ + } + +// truncated version when only a uint64_t is available (e.g. Vulkan tags): +#define RENDERDOC_ShaderDebugMagicValue_truncated 0x48656670eab25520ULL + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc capture options +// + +typedef enum RENDERDOC_CaptureOption { + // Allow the application to enable vsync + // + // Default - enabled + // + // 1 - The application can enable or disable vsync at will + // 0 - vsync is force disabled + eRENDERDOC_Option_AllowVSync = 0, + + // Allow the application to enable fullscreen + // + // Default - enabled + // + // 1 - The application can enable or disable fullscreen at will + // 0 - fullscreen is force disabled + eRENDERDOC_Option_AllowFullscreen = 1, + + // Record API debugging events and messages + // + // Default - disabled + // + // 1 - Enable built-in API debugging features and records the results into + // the capture, which is matched up with events on replay + // 0 - no API debugging is forcibly enabled + eRENDERDOC_Option_APIValidation = 2, + eRENDERDOC_Option_DebugDeviceMode = 2, // deprecated name of this enum + + // Capture CPU callstacks for API events + // + // Default - disabled + // + // 1 - Enables capturing of callstacks + // 0 - no callstacks are captured + eRENDERDOC_Option_CaptureCallstacks = 3, + + // When capturing CPU callstacks, only capture them from drawcalls. + // This option does nothing without the above option being enabled + // + // Default - disabled + // + // 1 - Only captures callstacks for drawcall type API events. + // Ignored if CaptureCallstacks is disabled + // 0 - Callstacks, if enabled, are captured for every event. + eRENDERDOC_Option_CaptureCallstacksOnlyDraws = 4, + + // Specify a delay in seconds to wait for a debugger to attach, after + // creating or injecting into a process, before continuing to allow it to run. + // + // 0 indicates no delay, and the process will run immediately after injection + // + // Default - 0 seconds + // + eRENDERDOC_Option_DelayForDebugger = 5, + + // Verify buffer access. This includes checking the memory returned by a Map() call to + // detect any out-of-bounds modification, as well as initialising buffers with undefined contents + // to a marker value to catch use of uninitialised memory. + // + // NOTE: This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do + // not do the same kind of interception & checking and undefined contents are really undefined. + // + // Default - disabled + // + // 1 - Verify buffer access + // 0 - No verification is performed, and overwriting bounds may cause crashes or corruption in + // RenderDoc. + eRENDERDOC_Option_VerifyBufferAccess = 6, + + // The old name for eRENDERDOC_Option_VerifyBufferAccess was eRENDERDOC_Option_VerifyMapWrites. + // This option now controls the filling of uninitialised buffers with 0xdddddddd which was + // previously always enabled + eRENDERDOC_Option_VerifyMapWrites = eRENDERDOC_Option_VerifyBufferAccess, + + // Hooks any system API calls that create child processes, and injects + // RenderDoc into them recursively with the same options. + // + // Default - disabled + // + // 1 - Hooks into spawned child processes + // 0 - Child processes are not hooked by RenderDoc + eRENDERDOC_Option_HookIntoChildren = 7, + + // By default RenderDoc only includes resources in the final capture necessary + // for that frame, this allows you to override that behaviour. + // + // Default - disabled + // + // 1 - all live resources at the time of capture are included in the capture + // and available for inspection + // 0 - only the resources referenced by the captured frame are included + eRENDERDOC_Option_RefAllResources = 8, + + // **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or + // getting it will be ignored, to allow compatibility with older versions. + // In v1.1 the option acts as if it's always enabled. + // + // By default RenderDoc skips saving initial states for resources where the + // previous contents don't appear to be used, assuming that writes before + // reads indicate previous contents aren't used. + // + // Default - disabled + // + // 1 - initial contents at the start of each captured frame are saved, even if + // they are later overwritten or cleared before being used. + // 0 - unless a read is detected, initial contents will not be saved and will + // appear as black or empty data. + eRENDERDOC_Option_SaveAllInitials = 9, + + // In APIs that allow for the recording of command lists to be replayed later, + // RenderDoc may choose to not capture command lists before a frame capture is + // triggered, to reduce overheads. This means any command lists recorded once + // and replayed many times will not be available and may cause a failure to + // capture. + // + // NOTE: This is only true for APIs where multithreading is difficult or + // discouraged. Newer APIs like Vulkan and D3D12 will ignore this option + // and always capture all command lists since the API is heavily oriented + // around it and the overheads have been reduced by API design. + // + // 1 - All command lists are captured from the start of the application + // 0 - Command lists are only captured if their recording begins during + // the period when a frame capture is in progress. + eRENDERDOC_Option_CaptureAllCmdLists = 10, + + // Mute API debugging output when the API validation mode option is enabled + // + // Default - enabled + // + // 1 - Mute any API debug messages from being displayed or passed through + // 0 - API debugging is displayed as normal + eRENDERDOC_Option_DebugOutputMute = 11, + + // Option to allow vendor extensions to be used even when they may be + // incompatible with RenderDoc and cause corrupted replays or crashes. + // + // Default - inactive + // + // No values are documented, this option should only be used when absolutely + // necessary as directed by a RenderDoc developer. + eRENDERDOC_Option_AllowUnsupportedVendorExtensions = 12, + +} RENDERDOC_CaptureOption; + +// Sets an option that controls how RenderDoc behaves on capture. +// +// Returns 1 if the option and value are valid +// Returns 0 if either is invalid and the option is unchanged +typedef int(RENDERDOC_CC *pRENDERDOC_SetCaptureOptionU32)(RENDERDOC_CaptureOption opt, uint32_t val); +typedef int(RENDERDOC_CC *pRENDERDOC_SetCaptureOptionF32)(RENDERDOC_CaptureOption opt, float val); + +// Gets the current value of an option as a uint32_t +// +// If the option is invalid, 0xffffffff is returned +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetCaptureOptionU32)(RENDERDOC_CaptureOption opt); + +// Gets the current value of an option as a float +// +// If the option is invalid, -FLT_MAX is returned +typedef float(RENDERDOC_CC *pRENDERDOC_GetCaptureOptionF32)(RENDERDOC_CaptureOption opt); + +typedef enum RENDERDOC_InputButton { + // '0' - '9' matches ASCII values + eRENDERDOC_Key_0 = 0x30, + eRENDERDOC_Key_1 = 0x31, + eRENDERDOC_Key_2 = 0x32, + eRENDERDOC_Key_3 = 0x33, + eRENDERDOC_Key_4 = 0x34, + eRENDERDOC_Key_5 = 0x35, + eRENDERDOC_Key_6 = 0x36, + eRENDERDOC_Key_7 = 0x37, + eRENDERDOC_Key_8 = 0x38, + eRENDERDOC_Key_9 = 0x39, + + // 'A' - 'Z' matches ASCII values + eRENDERDOC_Key_A = 0x41, + eRENDERDOC_Key_B = 0x42, + eRENDERDOC_Key_C = 0x43, + eRENDERDOC_Key_D = 0x44, + eRENDERDOC_Key_E = 0x45, + eRENDERDOC_Key_F = 0x46, + eRENDERDOC_Key_G = 0x47, + eRENDERDOC_Key_H = 0x48, + eRENDERDOC_Key_I = 0x49, + eRENDERDOC_Key_J = 0x4A, + eRENDERDOC_Key_K = 0x4B, + eRENDERDOC_Key_L = 0x4C, + eRENDERDOC_Key_M = 0x4D, + eRENDERDOC_Key_N = 0x4E, + eRENDERDOC_Key_O = 0x4F, + eRENDERDOC_Key_P = 0x50, + eRENDERDOC_Key_Q = 0x51, + eRENDERDOC_Key_R = 0x52, + eRENDERDOC_Key_S = 0x53, + eRENDERDOC_Key_T = 0x54, + eRENDERDOC_Key_U = 0x55, + eRENDERDOC_Key_V = 0x56, + eRENDERDOC_Key_W = 0x57, + eRENDERDOC_Key_X = 0x58, + eRENDERDOC_Key_Y = 0x59, + eRENDERDOC_Key_Z = 0x5A, + + // leave the rest of the ASCII range free + // in case we want to use it later + eRENDERDOC_Key_NonPrintable = 0x100, + + eRENDERDOC_Key_Divide, + eRENDERDOC_Key_Multiply, + eRENDERDOC_Key_Subtract, + eRENDERDOC_Key_Plus, + + eRENDERDOC_Key_F1, + eRENDERDOC_Key_F2, + eRENDERDOC_Key_F3, + eRENDERDOC_Key_F4, + eRENDERDOC_Key_F5, + eRENDERDOC_Key_F6, + eRENDERDOC_Key_F7, + eRENDERDOC_Key_F8, + eRENDERDOC_Key_F9, + eRENDERDOC_Key_F10, + eRENDERDOC_Key_F11, + eRENDERDOC_Key_F12, + + eRENDERDOC_Key_Home, + eRENDERDOC_Key_End, + eRENDERDOC_Key_Insert, + eRENDERDOC_Key_Delete, + eRENDERDOC_Key_PageUp, + eRENDERDOC_Key_PageDn, + + eRENDERDOC_Key_Backspace, + eRENDERDOC_Key_Tab, + eRENDERDOC_Key_PrtScrn, + eRENDERDOC_Key_Pause, + + eRENDERDOC_Key_Max, +} RENDERDOC_InputButton; + +// Sets which key or keys can be used to toggle focus between multiple windows +// +// If keys is NULL or num is 0, toggle keys will be disabled +typedef void(RENDERDOC_CC *pRENDERDOC_SetFocusToggleKeys)(RENDERDOC_InputButton *keys, int num); + +// Sets which key or keys can be used to capture the next frame +// +// If keys is NULL or num is 0, captures keys will be disabled +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureKeys)(RENDERDOC_InputButton *keys, int num); + +typedef enum RENDERDOC_OverlayBits { + // This single bit controls whether the overlay is enabled or disabled globally + eRENDERDOC_Overlay_Enabled = 0x1, + + // Show the average framerate over several seconds as well as min/max + eRENDERDOC_Overlay_FrameRate = 0x2, + + // Show the current frame number + eRENDERDOC_Overlay_FrameNumber = 0x4, + + // Show a list of recent captures, and how many captures have been made + eRENDERDOC_Overlay_CaptureList = 0x8, + + // Default values for the overlay mask + eRENDERDOC_Overlay_Default = (eRENDERDOC_Overlay_Enabled | eRENDERDOC_Overlay_FrameRate | + eRENDERDOC_Overlay_FrameNumber | eRENDERDOC_Overlay_CaptureList), + + // Enable all bits + eRENDERDOC_Overlay_All = ~0U, + + // Disable all bits + eRENDERDOC_Overlay_None = 0, +} RENDERDOC_OverlayBits; + +// returns the overlay bits that have been set +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetOverlayBits)(); +// sets the overlay bits with an and & or mask +typedef void(RENDERDOC_CC *pRENDERDOC_MaskOverlayBits)(uint32_t And, uint32_t Or); + +// this function will attempt to remove RenderDoc's hooks in the application. +// +// Note: that this can only work correctly if done immediately after +// the module is loaded, before any API work happens. RenderDoc will remove its +// injected hooks and shut down. Behaviour is undefined if this is called +// after any API functions have been called, and there is still no guarantee of +// success. +typedef void(RENDERDOC_CC *pRENDERDOC_RemoveHooks)(); + +// DEPRECATED: compatibility for code compiled against pre-1.4.1 headers. +typedef pRENDERDOC_RemoveHooks pRENDERDOC_Shutdown; + +// This function will unload RenderDoc's crash handler. +// +// If you use your own crash handler and don't want RenderDoc's handler to +// intercede, you can call this function to unload it and any unhandled +// exceptions will pass to the next handler. +typedef void(RENDERDOC_CC *pRENDERDOC_UnloadCrashHandler)(); + +// Sets the capture file path template +// +// pathtemplate is a UTF-8 string that gives a template for how captures will be named +// and where they will be saved. +// +// Any extension is stripped off the path, and captures are saved in the directory +// specified, and named with the filename and the frame number appended. If the +// directory does not exist it will be created, including any parent directories. +// +// If pathtemplate is NULL, the template will remain unchanged +// +// Example: +// +// SetCaptureFilePathTemplate("my_captures/example"); +// +// Capture #1 -> my_captures/example_frame123.rdc +// Capture #2 -> my_captures/example_frame456.rdc +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFilePathTemplate)(const char *pathtemplate); + +// returns the current capture path template, see SetCaptureFileTemplate above, as a UTF-8 string +typedef const char *(RENDERDOC_CC *pRENDERDOC_GetCaptureFilePathTemplate)(); + +// DEPRECATED: compatibility for code compiled against pre-1.1.2 headers. +typedef pRENDERDOC_SetCaptureFilePathTemplate pRENDERDOC_SetLogFilePathTemplate; +typedef pRENDERDOC_GetCaptureFilePathTemplate pRENDERDOC_GetLogFilePathTemplate; + +// returns the number of captures that have been made +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetNumCaptures)(); + +// This function returns the details of a capture, by index. New captures are added +// to the end of the list. +// +// filename will be filled with the absolute path to the capture file, as a UTF-8 string +// pathlength will be written with the length in bytes of the filename string +// timestamp will be written with the time of the capture, in seconds since the Unix epoch +// +// Any of the parameters can be NULL and they'll be skipped. +// +// The function will return 1 if the capture index is valid, or 0 if the index is invalid +// If the index is invalid, the values will be unchanged +// +// Note: when captures are deleted in the UI they will remain in this list, so the +// capture path may not exist anymore. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetCapture)(uint32_t idx, char *filename, + uint32_t *pathlength, uint64_t *timestamp); + +// Sets the comments associated with a capture file. These comments are displayed in the +// UI program when opening. +// +// filePath should be a path to the capture file to add comments to. If set to NULL or "" +// the most recent capture file created made will be used instead. +// comments should be a NULL-terminated UTF-8 string to add as comments. +// +// Any existing comments will be overwritten. +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFileComments)(const char *filePath, + const char *comments); + +// returns 1 if the RenderDoc UI is connected to this application, 0 otherwise +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsTargetControlConnected)(); + +// DEPRECATED: compatibility for code compiled against pre-1.1.1 headers. +// This was renamed to IsTargetControlConnected in API 1.1.1, the old typedef is kept here for +// backwards compatibility with old code, it is castable either way since it's ABI compatible +// as the same function pointer type. +typedef pRENDERDOC_IsTargetControlConnected pRENDERDOC_IsRemoteAccessConnected; + +// This function will launch the Replay UI associated with the RenderDoc library injected +// into the running application. +// +// if connectTargetControl is 1, the Replay UI will be launched with a command line parameter +// to connect to this application +// cmdline is the rest of the command line, as a UTF-8 string. E.g. a captures to open +// if cmdline is NULL, the command line will be empty. +// +// returns the PID of the replay UI if successful, 0 if not successful. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_LaunchReplayUI)(uint32_t connectTargetControl, + const char *cmdline); + +// RenderDoc can return a higher version than requested if it's backwards compatible, +// this function returns the actual version returned. If a parameter is NULL, it will be +// ignored and the others will be filled out. +typedef void(RENDERDOC_CC *pRENDERDOC_GetAPIVersion)(int *major, int *minor, int *patch); + +////////////////////////////////////////////////////////////////////////// +// Capturing functions +// + +// A device pointer is a pointer to the API's root handle. +// +// This would be an ID3D11Device, HGLRC/GLXContext, ID3D12Device, etc +typedef void *RENDERDOC_DevicePointer; + +// A window handle is the OS's native window handle +// +// This would be an HWND, GLXDrawable, etc +typedef void *RENDERDOC_WindowHandle; + +// A helper macro for Vulkan, where the device handle cannot be used directly. +// +// Passing the VkInstance to this macro will return the RENDERDOC_DevicePointer to use. +// +// Specifically, the value needed is the dispatch table pointer, which sits as the first +// pointer-sized object in the memory pointed to by the VkInstance. Thus we cast to a void** and +// indirect once. +#define RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(inst) (*((void **)(inst))) + +// This sets the RenderDoc in-app overlay in the API/window pair as 'active' and it will +// respond to keypresses. Neither parameter can be NULL +typedef void(RENDERDOC_CC *pRENDERDOC_SetActiveWindow)(RENDERDOC_DevicePointer device, + RENDERDOC_WindowHandle wndHandle); + +// capture the next frame on whichever window and API is currently considered active +typedef void(RENDERDOC_CC *pRENDERDOC_TriggerCapture)(); + +// capture the next N frames on whichever window and API is currently considered active +typedef void(RENDERDOC_CC *pRENDERDOC_TriggerMultiFrameCapture)(uint32_t numFrames); + +// When choosing either a device pointer or a window handle to capture, you can pass NULL. +// Passing NULL specifies a 'wildcard' match against anything. This allows you to specify +// any API rendering to a specific window, or a specific API instance rendering to any window, +// or in the simplest case of one window and one API, you can just pass NULL for both. +// +// In either case, if there are two or more possible matching (device,window) pairs it +// is undefined which one will be captured. +// +// Note: for headless rendering you can pass NULL for the window handle and either specify +// a device pointer or leave it NULL as above. + +// Immediately starts capturing API calls on the specified device pointer and window handle. +// +// If there is no matching thing to capture (e.g. no supported API has been initialised), +// this will do nothing. +// +// The results are undefined (including crashes) if two captures are started overlapping, +// even on separate devices and/oror windows. +typedef void(RENDERDOC_CC *pRENDERDOC_StartFrameCapture)(RENDERDOC_DevicePointer device, + RENDERDOC_WindowHandle wndHandle); + +// Returns whether or not a frame capture is currently ongoing anywhere. +// +// This will return 1 if a capture is ongoing, and 0 if there is no capture running +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsFrameCapturing)(); + +// Ends capturing immediately. +// +// This will return 1 if the capture succeeded, and 0 if there was an error capturing. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_EndFrameCapture)(RENDERDOC_DevicePointer device, + RENDERDOC_WindowHandle wndHandle); + +// Ends capturing immediately and discard any data stored without saving to disk. +// +// This will return 1 if the capture was discarded, and 0 if there was an error or no capture +// was in progress +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_DiscardFrameCapture)(RENDERDOC_DevicePointer device, + RENDERDOC_WindowHandle wndHandle); + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc API versions +// + +// RenderDoc uses semantic versioning (http://semver.org/). +// +// MAJOR version is incremented when incompatible API changes happen. +// MINOR version is incremented when functionality is added in a backwards-compatible manner. +// PATCH version is incremented when backwards-compatible bug fixes happen. +// +// Note that this means the API returned can be higher than the one you might have requested. +// e.g. if you are running against a newer RenderDoc that supports 1.0.1, it will be returned +// instead of 1.0.0. You can check this with the GetAPIVersion entry point +typedef enum RENDERDOC_Version { + eRENDERDOC_API_Version_1_0_0 = 10000, // RENDERDOC_API_1_0_0 = 1 00 00 + eRENDERDOC_API_Version_1_0_1 = 10001, // RENDERDOC_API_1_0_1 = 1 00 01 + eRENDERDOC_API_Version_1_0_2 = 10002, // RENDERDOC_API_1_0_2 = 1 00 02 + eRENDERDOC_API_Version_1_1_0 = 10100, // RENDERDOC_API_1_1_0 = 1 01 00 + eRENDERDOC_API_Version_1_1_1 = 10101, // RENDERDOC_API_1_1_1 = 1 01 01 + eRENDERDOC_API_Version_1_1_2 = 10102, // RENDERDOC_API_1_1_2 = 1 01 02 + eRENDERDOC_API_Version_1_2_0 = 10200, // RENDERDOC_API_1_2_0 = 1 02 00 + eRENDERDOC_API_Version_1_3_0 = 10300, // RENDERDOC_API_1_3_0 = 1 03 00 + eRENDERDOC_API_Version_1_4_0 = 10400, // RENDERDOC_API_1_4_0 = 1 04 00 + eRENDERDOC_API_Version_1_4_1 = 10401, // RENDERDOC_API_1_4_1 = 1 04 01 +} RENDERDOC_Version; + +// API version changelog: +// +// 1.0.0 - initial release +// 1.0.1 - Bugfix: IsFrameCapturing() was returning false for captures that were triggered +// by keypress or TriggerCapture, instead of Start/EndFrameCapture. +// 1.0.2 - Refactor: Renamed eRENDERDOC_Option_DebugDeviceMode to eRENDERDOC_Option_APIValidation +// 1.1.0 - Add feature: TriggerMultiFrameCapture(). Backwards compatible with 1.0.x since the new +// function pointer is added to the end of the struct, the original layout is identical +// 1.1.1 - Refactor: Renamed remote access to target control (to better disambiguate from remote +// replay/remote server concept in replay UI) +// 1.1.2 - Refactor: Renamed "log file" in function names to just capture, to clarify that these +// are captures and not debug logging files. This is the first API version in the v1.0 +// branch. +// 1.2.0 - Added feature: SetCaptureFileComments() to add comments to a capture file that will be +// displayed in the UI program on load. +// 1.3.0 - Added feature: New capture option eRENDERDOC_Option_AllowUnsupportedVendorExtensions +// which allows users to opt-in to allowing unsupported vendor extensions to function. +// Should be used at the user's own risk. +// Refactor: Renamed eRENDERDOC_Option_VerifyMapWrites to +// eRENDERDOC_Option_VerifyBufferAccess, which now also controls initialisation to +// 0xdddddddd of uninitialised buffer contents. +// 1.4.0 - Added feature: DiscardFrameCapture() to discard a frame capture in progress and stop +// capturing without saving anything to disk. +// 1.4.1 - Refactor: Renamed Shutdown to RemoveHooks to better clarify what is happening + +typedef struct RENDERDOC_API_1_4_1 +{ + pRENDERDOC_GetAPIVersion GetAPIVersion; + + pRENDERDOC_SetCaptureOptionU32 SetCaptureOptionU32; + pRENDERDOC_SetCaptureOptionF32 SetCaptureOptionF32; + + pRENDERDOC_GetCaptureOptionU32 GetCaptureOptionU32; + pRENDERDOC_GetCaptureOptionF32 GetCaptureOptionF32; + + pRENDERDOC_SetFocusToggleKeys SetFocusToggleKeys; + pRENDERDOC_SetCaptureKeys SetCaptureKeys; + + pRENDERDOC_GetOverlayBits GetOverlayBits; + pRENDERDOC_MaskOverlayBits MaskOverlayBits; + + // Shutdown was renamed to RemoveHooks in 1.4.1. + // These unions allow old code to continue compiling without changes + union + { + pRENDERDOC_Shutdown Shutdown; + pRENDERDOC_RemoveHooks RemoveHooks; + }; + pRENDERDOC_UnloadCrashHandler UnloadCrashHandler; + + // Get/SetLogFilePathTemplate was renamed to Get/SetCaptureFilePathTemplate in 1.1.2. + // These unions allow old code to continue compiling without changes + union + { + // deprecated name + pRENDERDOC_SetLogFilePathTemplate SetLogFilePathTemplate; + // current name + pRENDERDOC_SetCaptureFilePathTemplate SetCaptureFilePathTemplate; + }; + union + { + // deprecated name + pRENDERDOC_GetLogFilePathTemplate GetLogFilePathTemplate; + // current name + pRENDERDOC_GetCaptureFilePathTemplate GetCaptureFilePathTemplate; + }; + + pRENDERDOC_GetNumCaptures GetNumCaptures; + pRENDERDOC_GetCapture GetCapture; + + pRENDERDOC_TriggerCapture TriggerCapture; + + // IsRemoteAccessConnected was renamed to IsTargetControlConnected in 1.1.1. + // This union allows old code to continue compiling without changes + union + { + // deprecated name + pRENDERDOC_IsRemoteAccessConnected IsRemoteAccessConnected; + // current name + pRENDERDOC_IsTargetControlConnected IsTargetControlConnected; + }; + pRENDERDOC_LaunchReplayUI LaunchReplayUI; + + pRENDERDOC_SetActiveWindow SetActiveWindow; + + pRENDERDOC_StartFrameCapture StartFrameCapture; + pRENDERDOC_IsFrameCapturing IsFrameCapturing; + pRENDERDOC_EndFrameCapture EndFrameCapture; + + // new function in 1.1.0 + pRENDERDOC_TriggerMultiFrameCapture TriggerMultiFrameCapture; + + // new function in 1.2.0 + pRENDERDOC_SetCaptureFileComments SetCaptureFileComments; + + // new function in 1.4.0 + pRENDERDOC_DiscardFrameCapture DiscardFrameCapture; +} RENDERDOC_API_1_4_1; + +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_0_0; +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_0_1; +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_0_2; +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_1_0; +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_1_1; +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_1_2; +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_2_0; +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_3_0; +typedef RENDERDOC_API_1_4_1 RENDERDOC_API_1_4_0; + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc API entry point +// +// This entry point can be obtained via GetProcAddress/dlsym if RenderDoc is available. +// +// The name is the same as the typedef - "RENDERDOC_GetAPI" +// +// This function is not thread safe, and should not be called on multiple threads at once. +// Ideally, call this once as early as possible in your application's startup, before doing +// any API work, since some configuration functionality etc has to be done also before +// initialising any APIs. +// +// Parameters: +// version is a single value from the RENDERDOC_Version above. +// +// outAPIPointers will be filled out with a pointer to the corresponding struct of function +// pointers. +// +// Returns: +// 1 - if the outAPIPointers has been filled with a pointer to the API struct requested +// 0 - if the requested version is not supported or the arguments are invalid. +// +typedef int(RENDERDOC_CC *pRENDERDOC_GetAPI)(RENDERDOC_Version version, void **outAPIPointers); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/zbar_pipeline.c b/src/zbar_pipeline.c similarity index 57% rename from zbar_pipeline.c rename to src/zbar_pipeline.c index e4c9257..0fa5223 100644 --- a/zbar_pipeline.c +++ b/src/zbar_pipeline.c @@ -6,6 +6,17 @@ #include #include +struct _MPZBarImage { + uint8_t *data; + MPPixelFormat pixel_format; + int width; + int height; + int rotation; + bool mirrored; + + _Atomic int ref_count; +}; + static MPPipeline *pipeline; static volatile int frames_processed = 0; @@ -33,7 +44,8 @@ mp_zbar_pipeline_stop() mp_pipeline_free(pipeline); } -static bool is_3d_code(zbar_symbol_type_t type) +static bool +is_3d_code(zbar_symbol_type_t type) { switch (type) { case ZBAR_EAN2: @@ -62,9 +74,41 @@ static bool is_3d_code(zbar_symbol_type_t type) } } -static MPZBarCode -process_symbol(const zbar_symbol_t *symbol) +static inline void +map_coords(int *x, int *y, int width, int height, int rotation, bool mirrored) { + int x_r, y_r; + if (rotation == 0) { + x_r = *x; + y_r = *y; + } else if (rotation == 90) { + x_r = *y; + y_r = height - *x - 1; + } else if (rotation == 270) { + x_r = width - *y - 1; + y_r = *x; + } else { + x_r = width - *x - 1; + y_r = height - *y - 1; + } + + if (mirrored) { + x_r = width - x_r - 1; + } + + *x = x_r; + *y = y_r; +} + +static MPZBarCode +process_symbol(const MPZBarImage *image, int width, int height, const zbar_symbol_t *symbol) +{ + if (image->rotation == 90 || image->rotation == 270) { + int tmp = width; + width = height; + height = tmp; + } + MPZBarCode code; unsigned loc_size = zbar_symbol_get_loc_size(symbol); @@ -100,6 +144,10 @@ process_symbol(const zbar_symbol_t *symbol) code.bounds_y[3] = max_y; } + for (uint8_t i = 0; i < 4; ++i) { + map_coords(&code.bounds_x[i], &code.bounds_y[i], width, height, image->rotation, image->mirrored); + } + const char *data = zbar_symbol_get_data(symbol); unsigned int data_size = zbar_symbol_get_data_length(symbol); code.type = zbar_get_symbol_name(type); @@ -110,25 +158,33 @@ process_symbol(const zbar_symbol_t *symbol) } static void -process_surface(MPPipeline *pipeline, cairo_surface_t **_surface) +process_image(MPPipeline *pipeline, MPZBarImage **_image) { - cairo_surface_t *surface = *_surface; + MPZBarImage *image = *_image; - int width = cairo_image_surface_get_width(surface); - int height = cairo_image_surface_get_height(surface); - const uint32_t *surface_data = (const uint32_t *)cairo_image_surface_get_data(surface); + assert(image->pixel_format == MP_PIXEL_FMT_BGGR8 + || image->pixel_format == MP_PIXEL_FMT_GBRG8 + || image->pixel_format == MP_PIXEL_FMT_GRBG8 + || image->pixel_format == MP_PIXEL_FMT_RGGB8); + + // Create a grayscale image for scanning from the current preview. + // Rotate/mirror correctly. + int width = image->width / 2; + int height = image->height / 2; - // Create a grayscale image for scanning from the current preview uint8_t *data = malloc(width * height * sizeof(uint8_t)); - for (size_t i = 0; i < width * height; ++i) { - data[i] = (surface_data[i] >> 16) & 0xff; + size_t i = 0; + for (int y = 0; y < image->height; y += 2) { + for (int x = 0; x < image->width; x += 2) { + data[++i] = image->data[x + image->width * y]; + } } // Create image for zbar zbar_image_t *zbar_image = zbar_image_create(); zbar_image_set_format(zbar_image, zbar_fourcc('Y', '8', '0', '0')); zbar_image_set_size(zbar_image, width, height); - zbar_image_set_data(zbar_image, data, width * height * sizeof(uint8_t), zbar_image_free_data); + zbar_image_set_data(zbar_image, data, width * height * sizeof(uint8_t), NULL); int res = zbar_scan_image(scanner, zbar_image); assert(res >= 0); @@ -140,7 +196,7 @@ process_surface(MPPipeline *pipeline, cairo_surface_t **_surface) const zbar_symbol_t *symbol = zbar_image_first_symbol(zbar_image); for (int i = 0; i < MIN(res, 8); ++i) { assert(symbol != NULL); - result->codes[i] = process_symbol(symbol); + result->codes[i] = process_symbol(image, width, height, symbol); symbol = zbar_symbol_next(symbol); } @@ -150,22 +206,57 @@ process_surface(MPPipeline *pipeline, cairo_surface_t **_surface) } zbar_image_destroy(zbar_image); - cairo_surface_destroy(surface); + mp_zbar_image_unref(image); ++frames_processed; } void -mp_zbar_pipeline_process_image(cairo_surface_t *surface) +mp_zbar_pipeline_process_image(MPZBarImage *image) { // If we haven't processed the previous frame yet, drop this one if (frames_received != frames_processed) { - cairo_surface_destroy(surface); + mp_zbar_image_unref(image); return; } ++frames_received; - mp_pipeline_invoke(pipeline, (MPPipelineCallback)process_surface, &surface, - sizeof(cairo_surface_t *)); + mp_pipeline_invoke(pipeline, (MPPipelineCallback)process_image, &image, + sizeof(MPZBarImage *)); +} + +MPZBarImage * +mp_zbar_image_new(uint8_t *data, + MPPixelFormat pixel_format, + int width, + int height, + int rotation, + bool mirrored) +{ + MPZBarImage *image = malloc(sizeof(MPZBarImage)); + image->data = data; + image->pixel_format = pixel_format; + image->width = width; + image->height = height; + image->rotation = rotation; + image->mirrored = mirrored; + image->ref_count = 1; + return image; +} + +MPZBarImage * +mp_zbar_image_ref(MPZBarImage *image) +{ + ++image->ref_count; + return image; +} + +void +mp_zbar_image_unref(MPZBarImage *image) +{ + if (--image->ref_count == 0) { + free(image->data); + free(image); + } } diff --git a/src/zbar_pipeline.h b/src/zbar_pipeline.h new file mode 100644 index 0000000..410e906 --- /dev/null +++ b/src/zbar_pipeline.h @@ -0,0 +1,31 @@ +#pragma once + +#include "camera_config.h" + +typedef struct _MPZBarImage MPZBarImage; + +typedef struct { + int bounds_x[4]; + int bounds_y[4]; + char *data; + const char *type; +} MPZBarCode; + +typedef struct { + MPZBarCode codes[8]; + uint8_t size; +} MPZBarScanResult; + +void mp_zbar_pipeline_start(); +void mp_zbar_pipeline_stop(); + +void mp_zbar_pipeline_process_image(MPZBarImage *image); + +MPZBarImage *mp_zbar_image_new(uint8_t *data, + MPPixelFormat pixel_format, + int width, + int height, + int rotation, + bool mirrored); +MPZBarImage *mp_zbar_image_ref(MPZBarImage *image); +void mp_zbar_image_unref(MPZBarImage *image); diff --git a/tests/test_quickpreview.c b/tests/test_quickpreview.c deleted file mode 100644 index 9b43778..0000000 --- a/tests/test_quickpreview.c +++ /dev/null @@ -1,120 +0,0 @@ -#include "quickpreview.h" -#include -#include -#include - -static void -test_quick_preview_fuzz() -{ - for (size_t i = 0; i < 10000; ++i) { - uint32_t width = rand() % 127 + 1; - uint32_t height = rand() % 127 + 1; - uint32_t format = rand() % (MP_PIXEL_FMT_MAX - 1) + 1; - - assert(width > 0 && height > 0); - - switch (format) { - case MP_PIXEL_FMT_BGGR8: - case MP_PIXEL_FMT_GBRG8: - case MP_PIXEL_FMT_GRBG8: - case MP_PIXEL_FMT_RGGB8: - width += width % 2; - height += height % 2; - break; - case MP_PIXEL_FMT_BGGR10P: - case MP_PIXEL_FMT_GBRG10P: - case MP_PIXEL_FMT_GRBG10P: - case MP_PIXEL_FMT_RGGB10P: - width += width % 2; - height += height % 2; - break; - case MP_PIXEL_FMT_UYVY: - case MP_PIXEL_FMT_YUYV: - width += width % 4; - break; - default: - assert(false); - } - - int rotation; - switch (rand() % 3) { - case 0: - rotation = 0; - break; - case 1: - rotation = 90; - break; - case 2: - rotation = 180; - break; - default: - rotation = 270; - break; - } - - bool mirrored = rand() % 2; - - float matbuf[9]; - float *colormatrix = NULL; - if (rand() % 2) { - for (int j = 0; j < 9; ++j) { - matbuf[j] = (double)rand() / RAND_MAX * 2.0; - } - colormatrix = matbuf; - } - - uint8_t blacklevel = rand() % 3; - - size_t src_size = mp_pixel_format_width_to_bytes(format, width) * height; - uint8_t *src = malloc(src_size); - for (int j = 0; j < src_size; ++j) { - src[j] = rand(); - } - - uint32_t preview_width = mp_pixel_format_width_to_colors(format, width); - uint32_t preview_height = mp_pixel_format_height_to_colors(format, height); - if (preview_width > 32 && preview_height > 32) { - preview_width /= (1 + rand() % 2); - preview_height /= (1 + rand() % 2); - } - - uint32_t dst_width, dst_height, skip; - quick_preview_size( - &dst_width, - &dst_height, - &skip, - preview_width, - preview_height, - width, - height, - format, - rotation); - - uint32_t *dst = malloc(dst_width * dst_height * sizeof(uint32_t)); - - quick_preview( - dst, - dst_width, - dst_height, - src, - width, - height, - format, - rotation, - mirrored, - colormatrix, - blacklevel, - skip); - - free(dst); - free(src); - } -} - -int -main(int argc, char *argv[]) -{ - g_test_init(&argc, &argv, NULL); - g_test_add_func("/quick_preview/fuzz", test_quick_preview_fuzz); - return g_test_run(); -} diff --git a/tools/camera_test.c b/tools/camera_test.c index df88dbf..df7d923 100644 --- a/tools/camera_test.c +++ b/tools/camera_test.c @@ -15,20 +15,6 @@ get_time() return t.tv_sec + t.tv_usec * 1e-6; } -void -on_capture(MPImage image, void *user_data) -{ - 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); - - printf(" first byte: %d.", data[0]); - - free(data); -} - int main(int argc, char *argv[]) { @@ -184,7 +170,22 @@ main(int argc, char *argv[]) (last - start_capture) * 1000); for (int i = 0; i < 10; ++i) { - mp_camera_capture_image(camera, on_capture, NULL); + MPBuffer buffer; + if (!mp_camera_capture_buffer(camera, &buffer)) { + printf(" Failed to capture buffer\n"); + } + + size_t num_bytes = + mp_pixel_format_width_to_bytes(m->pixel_format, m->width) * + m->height; + uint8_t *data = malloc(num_bytes); + memcpy(data, buffer.data, num_bytes); + + printf(" first byte: %d.", data[0]); + + free(data); + + mp_camera_release_buffer(camera, buffer.index); double now = get_time(); printf(" capture took %fms\n", (now - last) * 1000); diff --git a/zbar_pipeline.h b/zbar_pipeline.h deleted file mode 100644 index 150861a..0000000 --- a/zbar_pipeline.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include "camera_config.h" - -typedef struct _cairo_surface cairo_surface_t; - -typedef struct { - int bounds_x[4]; - int bounds_y[4]; - char *data; - const char *type; -} MPZBarCode; - -typedef struct { - MPZBarCode codes[8]; - uint8_t size; -} MPZBarScanResult; - -void mp_zbar_pipeline_start(); -void mp_zbar_pipeline_stop(); - -void mp_zbar_pipeline_process_image(cairo_surface_t *surface);