diff options
Diffstat (limited to 'quantum/painter/qp_draw_image.c')
| -rw-r--r-- | quantum/painter/qp_draw_image.c | 382 | 
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..5822758dce --- /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(); + +    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; +    } + +    // 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 (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); +} | 
