summaryrefslogtreecommitdiff
path: root/quantum/painter/qp_draw_image.c
diff options
context:
space:
mode:
authorNick Brassel <nick@tzarc.org>2022-04-13 18:00:18 +1000
committerGitHub <noreply@github.com>2022-04-13 18:00:18 +1000
commit1f2b1dedccdf21b629c45ece80b4ca32f6653296 (patch)
treea4283b928fe11c6662be10067314531f12774152 /quantum/painter/qp_draw_image.c
parent1dbbd2b6b068b9f921ebc0341c890df16a491007 (diff)
downloadqmk_firmware-1f2b1dedccdf21b629c45ece80b4ca32f6653296.tar.gz
qmk_firmware-1f2b1dedccdf21b629c45ece80b4ca32f6653296.zip
Quantum Painter (#10174)
* Install dependencies before executing unit tests. * Split out UTF-8 decoder. * Fixup python formatting rules. * Add documentation for QGF/QFF and the RLE format used. * Add CLI commands for converting images and fonts. * Add stub rules.mk for QP. * Add stream type. * Add base driver and comms interfaces. * Add support for SPI, SPI+D/C comms drivers. * Include <qp.h> when enabled. * Add base support for SPI+D/C+RST panels, as well as concrete implementation of ST7789. * Add support for GC9A01. * Add support for ILI9341. * Add support for ILI9163. * Add support for SSD1351. * Implement qp_setpixel, including pixdata buffer management. * Implement qp_line. * Implement qp_rect. * Implement qp_circle. * Implement qp_ellipse. * Implement palette interpolation. * Allow for streams to work with either flash or RAM. * Image loading. * Font loading. * QGF palette loading. * Progressive decoder of pixel data supporting Raw+RLE, 1-,2-,4-,8-bpp monochrome and palette-based images. * Image drawing. * Animations. * Font rendering. * Check against 256 colours, dump out the loaded palette if debugging enabled. * Fix build. * AVR is not the intended audience. * `qmk format-c` * Generation fix. * First batch of docs. * More docs and examples. * Review comments. * Public API documentation.
Diffstat (limited to 'quantum/painter/qp_draw_image.c')
-rw-r--r--quantum/painter/qp_draw_image.c382
1 files changed, 382 insertions, 0 deletions
diff --git a/quantum/painter/qp_draw_image.c b/quantum/painter/qp_draw_image.c
new file mode 100644
index 0000000000..5134ae7e99
--- /dev/null
+++ b/quantum/painter/qp_draw_image.c
@@ -0,0 +1,382 @@
+// Copyright 2021 Nick Brassel (@tzarc)
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "qp_internal.h"
+#include "qp_draw.h"
+#include "qp_comms.h"
+#include "qgf.h"
+#include "deferred_exec.h"
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// QGF image handles
+
+typedef struct qgf_image_handle_t {
+ painter_image_desc_t base;
+ bool validate_ok;
+ union {
+ qp_stream_t stream;
+ qp_memory_stream_t mem_stream;
+#ifdef QP_STREAM_HAS_FILE_IO
+ qp_file_stream_t file_stream;
+#endif // QP_STREAM_HAS_FILE_IO
+ };
+} qgf_image_handle_t;
+
+static qgf_image_handle_t image_descriptors[QUANTUM_PAINTER_NUM_IMAGES] = {0};
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_load_image_mem
+
+painter_image_handle_t qp_load_image_mem(const void *buffer) {
+ qp_dprintf("qp_load_image_mem: entry\n");
+ qgf_image_handle_t *image = NULL;
+
+ // Find a free slot
+ for (int i = 0; i < QUANTUM_PAINTER_NUM_IMAGES; ++i) {
+ if (!image_descriptors[i].validate_ok) {
+ image = &image_descriptors[i];
+ break;
+ }
+ }
+
+ // Drop out if not found
+ if (!image) {
+ qp_dprintf("qp_load_image_mem: fail (no free slot)\n");
+ return NULL;
+ }
+
+ // Assume we can read the graphics descriptor
+ image->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qgf_graphics_descriptor_v1_t));
+
+ // Update the length of the stream to match, and rewind to the start
+ image->mem_stream.length = qgf_get_total_size(&image->stream);
+ image->mem_stream.position = 0;
+
+ // Now that we know the length, validate the input data
+ if (!qgf_validate_stream(&image->stream)) {
+ qp_dprintf("qp_load_image_mem: fail (failed validation)\n");
+ return NULL;
+ }
+
+ // Fill out the QP image descriptor
+ qgf_read_graphics_descriptor(&image->stream, &image->base.width, &image->base.height, &image->base.frame_count, NULL);
+
+ // Validation success, we can return the handle
+ image->validate_ok = true;
+ qp_dprintf("qp_load_image_mem: ok\n");
+ return (painter_image_handle_t)image;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_close_image
+
+bool qp_close_image(painter_image_handle_t image) {
+ qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
+ if (!qgf_image->validate_ok) {
+ qp_dprintf("qp_close_image: fail (invalid image)\n");
+ return false;
+ }
+
+ // Free up this image for use elsewhere.
+ qgf_image->validate_ok = false;
+ return true;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_drawimage
+
+bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
+ return qp_drawimage_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_drawimage_recolor
+
+typedef struct qgf_frame_info_t {
+ painter_compression_t compression_scheme;
+ uint8_t bpp;
+ bool has_palette;
+ bool is_delta;
+ uint16_t left;
+ uint16_t top;
+ uint16_t right;
+ uint16_t bottom;
+ uint16_t delay;
+} qgf_frame_info_t;
+
+static bool qp_drawimage_prepare_frame_for_stream_read(painter_device_t device, qgf_image_handle_t *qgf_image, uint16_t frame_number, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qgf_frame_info_t *info) {
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+
+ // Drop out if we can't actually place the data we read out anywhere
+ if (!info) {
+ qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n");
+ return false;
+ }
+
+ // Seek to the frame
+ qgf_seek_to_frame_descriptor(&qgf_image->stream, frame_number);
+
+ // Read the frame descriptor
+ qgf_frame_v1_t frame_descriptor;
+ if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, &qgf_image->stream) != 1) {
+ qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t));
+ return false;
+ }
+
+ // Parse out the frame info
+ if (!qgf_parse_frame_descriptor(&frame_descriptor, &info->bpp, &info->has_palette, &info->is_delta, &info->compression_scheme, &info->delay)) {
+ return false;
+ }
+
+ // Ensure we aren't reusing any palette
+ qp_internal_invalidate_palette();
+
+ // Handle palette if needed
+ const uint16_t palette_entries = 1u << info->bpp;
+ bool needs_pixconvert = false;
+ if (info->has_palette) {
+ // Load the palette from the stream
+ if (!qp_internal_load_qgf_palette((qp_stream_t *)&qgf_image->stream, info->bpp)) {
+ return false;
+ }
+
+ needs_pixconvert = true;
+ } else {
+ // Interpolate from fg/bg
+ needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries);
+ }
+
+ if (!qp_internal_bpp_capable(info->bpp)) {
+ qp_dprintf("qp_drawimage_recolor: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\n", (int)info->bpp);
+ qp_comms_stop(device);
+ return false;
+ }
+
+ if (needs_pixconvert) {
+ // Convert the palette to native format
+ if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) {
+ qp_dprintf("qp_drawimage_recolor: fail (could not convert pixels to native)\n");
+ qp_comms_stop(device);
+ return false;
+ }
+ }
+
+ // Handle delta if needed
+ if (info->is_delta) {
+ qgf_delta_v1_t delta_descriptor;
+ if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, &qgf_image->stream) != 1) {
+ qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t));
+ return false;
+ }
+
+ info->left = delta_descriptor.left;
+ info->top = delta_descriptor.top;
+ info->right = delta_descriptor.right;
+ info->bottom = delta_descriptor.bottom;
+ }
+
+ // Read the data block
+ qgf_data_v1_t data_descriptor;
+ if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, &qgf_image->stream) != 1) {
+ qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t));
+ return false;
+ }
+
+ // Stream is now at the point of being able to read pixdata
+ return true;
+}
+
+static bool qp_drawimage_recolor_impl(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, int frame_number, qgf_frame_info_t *frame_info, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888) {
+ qp_dprintf("qp_drawimage_recolor: entry\n");
+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
+ if (!driver->validate_ok) {
+ qp_dprintf("qp_drawimage_recolor: fail (validation_ok == false)\n");
+ return false;
+ }
+
+ qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
+ if (!qgf_image->validate_ok) {
+ qp_dprintf("qp_drawimage_recolor: fail (invalid image)\n");
+ return false;
+ }
+
+ // Read the frame info
+ if (!qp_drawimage_prepare_frame_for_stream_read(device, qgf_image, frame_number, fg_hsv888, bg_hsv888, frame_info)) {
+ qp_dprintf("qp_drawimage_recolor: fail (could not read frame %d)\n", frame_number);
+ return false;
+ }
+
+ if (!qp_comms_start(device)) {
+ qp_dprintf("qp_drawimage_recolor: fail (could not start comms)\n");
+ return false;
+ }
+
+ uint16_t l, t, r, b;
+ if (frame_info->is_delta) {
+ l = x + frame_info->left;
+ t = y + frame_info->top;
+ r = x + frame_info->right - 1;
+ b = y + frame_info->bottom - 1;
+ } else {
+ l = x;
+ t = y;
+ r = x + image->width - 1;
+ b = y + image->height - 1;
+ }
+ uint32_t pixel_count = ((uint32_t)(r - l + 1)) * (b - t + 1);
+
+ // Configure where we're going to be rendering to
+ if (!driver->driver_vtable->viewport(device, l, t, r, b)) {
+ qp_dprintf("qp_drawimage_recolor: fail (could not set viewport)\n");
+ qp_comms_stop(device);
+ return false;
+ }
+
+ // Set up the input state
+ struct qp_internal_byte_input_state input_state = {.device = device, .src_stream = &qgf_image->stream};
+ qp_internal_byte_input_callback input_callback = qp_internal_prepare_input_state(&input_state, frame_info->compression_scheme);
+ if (input_callback == NULL) {
+ qp_dprintf("qp_drawimage_recolor: fail (invalid image compression scheme)\n");
+ qp_comms_stop(device);
+ return false;
+ }
+
+ // Set up the output state
+ struct qp_internal_pixel_output_state output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)};
+
+ // Decode the pixel data and stream to the display
+ bool ret = qp_internal_decode_palette(device, pixel_count, frame_info->bpp, input_callback, &input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, &output_state);
+
+ // Any leftovers need transmission as well.
+ if (ret && output_state.pixel_write_pos > 0) {
+ ret &= driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, output_state.pixel_write_pos);
+ }
+
+ qp_dprintf("qp_drawimage_recolor: %s\n", ret ? "ok" : "fail");
+ qp_comms_stop(device);
+ return ret;
+}
+
+bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
+ qgf_frame_info_t frame_info = {0};
+ qp_pixel_t fg_hsv888 = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
+ qp_pixel_t bg_hsv888 = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
+ return qp_drawimage_recolor_impl(device, x, y, image, 0, &frame_info, fg_hsv888, bg_hsv888);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_animate
+
+deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
+ return qp_animate_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_animate_recolor
+
+typedef struct animation_state_t {
+ painter_device_t device;
+ uint16_t x;
+ uint16_t y;
+ painter_image_handle_t image;
+ qp_pixel_t fg_hsv888;
+ qp_pixel_t bg_hsv888;
+ uint16_t frame_number;
+ deferred_token defer_token;
+} animation_state_t;
+
+static deferred_executor_t animation_executors[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
+static animation_state_t animation_states[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
+
+static deferred_token qp_render_animation_state(animation_state_t *state, uint16_t *delay_ms) {
+ qgf_frame_info_t frame_info = {0};
+ qp_dprintf("qp_render_animation_state: entry (frame #%d)\n", (int)state->frame_number);
+ bool ret = qp_drawimage_recolor_impl(state->device, state->x, state->y, state->image, state->frame_number, &frame_info, state->fg_hsv888, state->bg_hsv888);
+ if (ret) {
+ ++state->frame_number;
+ if (state->frame_number >= state->image->frame_count) {
+ state->frame_number = 0;
+ }
+ *delay_ms = frame_info.delay;
+ }
+ qp_dprintf("qp_render_animation_state: %s (delay %dms)\n", ret ? "ok" : "fail", (int)(*delay_ms));
+ return ret;
+}
+
+static uint32_t animation_callback(uint32_t trigger_time, void *cb_arg) {
+ animation_state_t *state = (animation_state_t *)cb_arg;
+ uint16_t delay_ms;
+ bool ret = qp_render_animation_state(state, &delay_ms);
+ if (!ret) {
+ // Setting the device to NULL clears the animation slot
+ state->device = NULL;
+ }
+ // If we're successful, keep animating -- returning 0 cancels the deferred execution
+ return ret ? delay_ms : 0;
+}
+
+deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
+ qp_dprintf("qp_animate_recolor: entry\n");
+
+ animation_state_t *anim_state = NULL;
+ for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
+ if (animation_states[i].device == NULL) {
+ anim_state = &animation_states[i];
+ break;
+ }
+ }
+
+ if (!anim_state) {
+ qp_dprintf("qp_animate_recolor: fail (could not find free animation slot)\n");
+ return INVALID_DEFERRED_TOKEN;
+ }
+
+ // Prepare the animation state
+ anim_state->device = device;
+ anim_state->x = x;
+ anim_state->y = y;
+ anim_state->image = image;
+ anim_state->fg_hsv888 = (qp_pixel_t){.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
+ anim_state->bg_hsv888 = (qp_pixel_t){.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
+ anim_state->frame_number = 0;
+
+ // Draw the first frame
+ uint16_t delay_ms;
+ if (!qp_render_animation_state(anim_state, &delay_ms)) {
+ anim_state->device = NULL; // disregard the allocated animation slot
+ qp_dprintf("qp_animate_recolor: fail (could not render first frame)\n");
+ return INVALID_DEFERRED_TOKEN;
+ }
+
+ // Set up the timer
+ anim_state->defer_token = defer_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, delay_ms, animation_callback, anim_state);
+ if (anim_state->defer_token == INVALID_DEFERRED_TOKEN) {
+ anim_state->device = NULL; // disregard the allocated animation slot
+ qp_dprintf("qp_animate_recolor: fail (could not set up animation executor)\n");
+ return INVALID_DEFERRED_TOKEN;
+ }
+
+ qp_dprintf("qp_animate_recolor: ok (deferred token = %d)\n", (int)anim_state->defer_token);
+ return anim_state->defer_token;
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter External API: qp_stop_animation
+
+void qp_stop_animation(deferred_token anim_token) {
+ for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
+ if (animation_states[i].defer_token == anim_token) {
+ cancel_deferred_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, anim_token);
+ animation_states[i].device = NULL;
+ return;
+ }
+ }
+}
+
+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+// Quantum Painter Core API: qp_internal_animation_tick
+
+void qp_internal_animation_tick(void) {
+ static uint32_t last_anim_exec = 0;
+ deferred_exec_advanced_task(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, &last_anim_exec);
+}