1
0
Fork 0

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.
This commit is contained in:
Nick Brassel 2022-04-13 18:00:18 +10:00 committed by GitHub
parent 1dbbd2b6b0
commit 1f2b1dedcc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 7561 additions and 35 deletions

137
quantum/painter/qff.c Normal file
View file

@ -0,0 +1,137 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
// Quantum Font File "QFF" File Format.
// See https://docs.qmk.fm/#/quantum_painter_qff for more information.
#include "qff.h"
#include "qp_draw.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QFF API
bool qff_read_font_descriptor(qp_stream_t *stream, uint8_t *line_height, bool *has_ascii_table, uint16_t *num_unicode_glyphs, uint8_t *bpp, bool *has_palette, painter_compression_t *compression_scheme, uint32_t *total_bytes) {
// Seek to the start
qp_stream_setpos(stream, 0);
// Read and validate the font descriptor
qff_font_descriptor_v1_t font_descriptor;
if (qp_stream_read(&font_descriptor, sizeof(qff_font_descriptor_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read font_descriptor, expected length was not %d\n", (int)sizeof(qff_font_descriptor_v1_t));
return false;
}
// Make sure this block is valid
if (!qgf_validate_block_header(&font_descriptor.header, QFF_FONT_DESCRIPTOR_TYPEID, (sizeof(qff_font_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) {
return false;
}
// Make sure the magic and version are correct
if (font_descriptor.magic != QFF_MAGIC || font_descriptor.qff_version != 0x01) {
qp_dprintf("Failed to validate font_descriptor, expected magic 0x%06X was 0x%06X, expected version = 0x%02X was 0x%02X\n", (int)QFF_MAGIC, (int)font_descriptor.magic, (int)0x01, (int)font_descriptor.qff_version);
return false;
}
// Make sure the file length is valid
if (font_descriptor.neg_total_file_size != ~font_descriptor.total_file_size) {
qp_dprintf("Failed to validate font_descriptor, expected negated length 0x%08X was 0x%08X\n", (int)(~font_descriptor.total_file_size), (int)font_descriptor.neg_total_file_size);
return false;
}
// Copy out the required info
if (line_height) {
*line_height = font_descriptor.line_height;
}
if (has_ascii_table) {
*has_ascii_table = font_descriptor.has_ascii_table;
}
if (num_unicode_glyphs) {
*num_unicode_glyphs = font_descriptor.num_unicode_glyphs;
}
if (bpp || has_palette) {
if (!qgf_parse_format(font_descriptor.format, bpp, has_palette)) {
return false;
}
}
if (compression_scheme) {
*compression_scheme = font_descriptor.compression_scheme;
}
if (total_bytes) {
*total_bytes = font_descriptor.total_file_size;
}
return true;
}
static bool qff_validate_ascii_descriptor(qp_stream_t *stream) {
// Read the raw descriptor
qff_ascii_glyph_table_v1_t ascii_descriptor;
if (qp_stream_read(&ascii_descriptor, sizeof(qff_ascii_glyph_table_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read ascii_descriptor, expected length was not %d\n", (int)sizeof(qff_ascii_glyph_table_v1_t));
return false;
}
// Make sure this block is valid
if (!qgf_validate_block_header(&ascii_descriptor.header, QFF_ASCII_GLYPH_DESCRIPTOR_TYPEID, (sizeof(qff_ascii_glyph_table_v1_t) - sizeof(qgf_block_header_v1_t)))) {
return false;
}
return true;
}
static bool qff_validate_unicode_descriptor(qp_stream_t *stream, uint16_t num_unicode_glyphs) {
// Read the raw descriptor
qff_unicode_glyph_table_v1_t unicode_descriptor;
if (qp_stream_read(&unicode_descriptor, sizeof(qff_unicode_glyph_table_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read unicode_descriptor, expected length was not %d\n", (int)sizeof(qff_unicode_glyph_table_v1_t));
return false;
}
// Make sure this block is valid
if (!qgf_validate_block_header(&unicode_descriptor.header, QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID, num_unicode_glyphs * 6)) {
return false;
}
// Skip the necessary amount of data to get to the next block
qp_stream_seek(stream, num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t), SEEK_CUR);
return true;
}
bool qff_validate_stream(qp_stream_t *stream) {
bool has_ascii_table;
uint16_t num_unicode_glyphs;
if (!qff_read_font_descriptor(stream, NULL, &has_ascii_table, &num_unicode_glyphs, NULL, NULL, NULL, NULL)) {
return false;
}
if (has_ascii_table) {
if (!qff_validate_ascii_descriptor(stream)) {
return false;
}
}
if (num_unicode_glyphs > 0) {
if (!qff_validate_unicode_descriptor(stream, num_unicode_glyphs)) {
return false;
}
}
return true;
}
uint32_t qff_get_total_size(qp_stream_t *stream) {
// Get the original location
uint32_t oldpos = qp_stream_tell(stream);
// Read the font descriptor, grabbing the size
uint32_t total_size;
if (!qff_read_font_descriptor(stream, NULL, NULL, NULL, NULL, NULL, NULL, &total_size)) {
return false;
}
// Restore the original location
qp_stream_setpos(stream, oldpos);
return total_size;
}

88
quantum/painter/qff.h Normal file
View file

@ -0,0 +1,88 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// Quantum Font File "QFF" File Format.
// See https://docs.qmk.fm/#/quantum_painter_qff for more information.
#include <stdint.h>
#include <stdbool.h>
#include "qp_stream.h"
#include "qp_internal.h"
#include "qgf.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QFF structures
/////////////////////////////////////////
// Font descriptor
#define QFF_FONT_DESCRIPTOR_TYPEID 0x00
typedef struct __attribute__((packed)) qff_font_descriptor_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 }
uint32_t magic : 24; // constant, equal to 0x464651 ("QFF")
uint8_t qff_version; // constant, equal to 0x01
uint32_t total_file_size; // total size of the entire file, starting at offset zero
uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors
uint8_t line_height; // glyph height in pixels
bool has_ascii_table; // whether the font has an ascii table of glyphs (0x20...0x7E)
uint16_t num_unicode_glyphs; // the number of glyphs in the unicode table -- no table specified if zero
qp_image_format_t format : 8; // Frame format, see qp.h.
uint8_t flags; // frame flags, see below.
uint8_t compression_scheme; // compression scheme, see below.
uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
} qff_font_descriptor_v1_t;
_Static_assert(sizeof(qff_font_descriptor_v1_t) == (sizeof(qgf_block_header_v1_t) + 20), "qff_font_descriptor_v1_t must be 25 bytes in v1 of QFF");
#define QFF_MAGIC 0x464651
/////////////////////////////////////////
// ASCII glyph table descriptor
#define QFF_ASCII_GLYPH_DESCRIPTOR_TYPEID 0x01
#define QFF_GLYPH_WIDTH_BITS 6
#define QFF_GLYPH_WIDTH_MASK ((1 << QFF_GLYPH_WIDTH_BITS) - 1)
#define QFF_GLYPH_OFFSET_BITS 18
#define QFF_GLYPH_OFFSET_MASK (((1 << QFF_GLYPH_OFFSET_BITS) - 1) << QFF_GLYPH_WIDTH_BITS)
typedef struct __attribute__((packed)) qff_ascii_glyph_v1_t {
uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined
} qff_ascii_glyph_v1_t;
_Static_assert(sizeof(qff_ascii_glyph_v1_t) == 3, "qff_ascii_glyph_v1_t must be 3 bytes in v1 of QFF");
typedef struct __attribute__((packed)) qff_ascii_glyph_table_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 }
qff_ascii_glyph_v1_t glyph[95]; // 95 glyphs, 0x20..0x7E
} qff_ascii_glyph_table_v1_t;
_Static_assert(sizeof(qff_ascii_glyph_table_v1_t) == (sizeof(qgf_block_header_v1_t) + (95 * sizeof(qff_ascii_glyph_v1_t))), "qff_ascii_glyph_table_v1_t must be 290 bytes in v1 of QFF");
/////////////////////////////////////////
// Unicode glyph table descriptor
#define QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID 0x02
typedef struct __attribute__((packed)) qff_unicode_glyph_v1_t {
uint32_t code_point : 24;
uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined
} qff_unicode_glyph_v1_t;
_Static_assert(sizeof(qff_unicode_glyph_v1_t) == 6, "qff_unicode_glyph_v1_t must be 6 bytes in v1 of QFF");
typedef struct __attribute__((packed)) qff_unicode_glyph_table_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = (N * 6) }
qff_unicode_glyph_v1_t glyph[0]; // Extent of '0' signifies that this struct is immediately followed by the glyph data
} qff_unicode_glyph_table_v1_t;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QFF API
bool qff_validate_stream(qp_stream_t *stream);
uint32_t qff_get_total_size(qp_stream_t *stream);
bool qff_read_font_descriptor(qp_stream_t *stream, uint8_t *line_height, bool *has_ascii_table, uint16_t *num_unicode_glyphs, uint8_t *bpp, bool *has_palette, painter_compression_t *compression_scheme, uint32_t *total_bytes);

292
quantum/painter/qgf.c Normal file
View file

@ -0,0 +1,292 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
// Quantum Graphics File "QGF" File Format.
// See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
#include "qgf.h"
#include "qp_draw.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QGF API
bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length) {
if (desc->type_id != expected_typeid || desc->neg_type_id != ((~expected_typeid) & 0xFF)) {
qp_dprintf("Failed to validate header, expected typeid 0x%02X, was 0x%02X, expected negated typeid 0x%02X, was 0x%02X\n", (int)expected_typeid, (int)desc->type_id, (int)((~desc->type_id) & 0xFF), (int)desc->neg_type_id);
return false;
}
if (expected_length >= 0 && desc->length != expected_length) {
qp_dprintf("Failed to validate header (typeid 0x%02X), expected length %d, was %d\n", (int)desc->type_id, (int)expected_length, (int)desc->length);
return false;
}
return true;
}
bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette) {
// clang-format off
static const struct QP_PACKED {
uint8_t bpp;
bool has_palette;
} formats[] = {
[GRAYSCALE_1BPP] = {.bpp = 1, .has_palette = false},
[GRAYSCALE_2BPP] = {.bpp = 2, .has_palette = false},
[GRAYSCALE_4BPP] = {.bpp = 4, .has_palette = false},
[GRAYSCALE_8BPP] = {.bpp = 8, .has_palette = false},
[PALETTE_1BPP] = {.bpp = 1, .has_palette = true},
[PALETTE_2BPP] = {.bpp = 2, .has_palette = true},
[PALETTE_4BPP] = {.bpp = 4, .has_palette = true},
[PALETTE_8BPP] = {.bpp = 8, .has_palette = true},
};
// clang-format on
// Copy out the required info
if (format > PALETTE_8BPP) {
qp_dprintf("Failed to parse frame_descriptor, invalid format 0x%02X\n", (int)format);
return false;
}
// Copy out the required info
if (bpp) {
*bpp = formats[format].bpp;
}
if (has_palette) {
*has_palette = formats[format].has_palette;
}
return true;
}
bool qgf_parse_frame_descriptor(qgf_frame_v1_t *frame_descriptor, uint8_t *bpp, bool *has_palette, bool *is_delta, painter_compression_t *compression_scheme, uint16_t *delay) {
// Decode the format
qgf_parse_format(frame_descriptor->format, bpp, has_palette);
// Copy out the required info
if (is_delta) {
*is_delta = (frame_descriptor->flags & QGF_FRAME_FLAG_DELTA) == QGF_FRAME_FLAG_DELTA;
}
if (compression_scheme) {
*compression_scheme = frame_descriptor->compression_scheme;
}
if (delay) {
*delay = frame_descriptor->delay;
}
return true;
}
bool qgf_read_graphics_descriptor(qp_stream_t *stream, uint16_t *image_width, uint16_t *image_height, uint16_t *frame_count, uint32_t *total_bytes) {
// Seek to the start
qp_stream_setpos(stream, 0);
// Read and validate the graphics descriptor
qgf_graphics_descriptor_v1_t graphics_descriptor;
if (qp_stream_read(&graphics_descriptor, sizeof(qgf_graphics_descriptor_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read graphics_descriptor, expected length was not %d\n", (int)sizeof(qgf_graphics_descriptor_v1_t));
return false;
}
// Make sure this block is valid
if (!qgf_validate_block_header(&graphics_descriptor.header, QGF_GRAPHICS_DESCRIPTOR_TYPEID, (sizeof(qgf_graphics_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) {
return false;
}
// Make sure the magic and version are correct
if (graphics_descriptor.magic != QGF_MAGIC || graphics_descriptor.qgf_version != 0x01) {
qp_dprintf("Failed to validate graphics_descriptor, expected magic 0x%06X was 0x%06X, expected version = 0x%02X was 0x%02X\n", (int)QGF_MAGIC, (int)graphics_descriptor.magic, (int)0x01, (int)graphics_descriptor.qgf_version);
return false;
}
// Make sure the file length is valid
if (graphics_descriptor.neg_total_file_size != ~graphics_descriptor.total_file_size) {
qp_dprintf("Failed to validate graphics_descriptor, expected negated length 0x%08X was 0x%08X\n", (int)(~graphics_descriptor.total_file_size), (int)graphics_descriptor.neg_total_file_size);
return false;
}
// Copy out the required info
if (image_width) {
*image_width = graphics_descriptor.image_width;
}
if (image_height) {
*image_height = graphics_descriptor.image_height;
}
if (frame_count) {
*frame_count = graphics_descriptor.frame_count;
}
if (total_bytes) {
*total_bytes = graphics_descriptor.total_file_size;
}
return true;
}
static bool qgf_read_frame_offset(qp_stream_t *stream, uint16_t frame_number, uint32_t *frame_offset) {
uint16_t frame_count;
if (!qgf_read_graphics_descriptor(stream, NULL, NULL, &frame_count, NULL)) {
return false;
}
// Read the frame offsets descriptor
qgf_frame_offsets_v1_t frame_offsets;
if (qp_stream_read(&frame_offsets, sizeof(qgf_frame_offsets_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read frame_offsets, expected length was not %d\n", (int)sizeof(qgf_frame_offsets_v1_t));
return false;
}
// Make sure this block is valid
if (!qgf_validate_block_header(&frame_offsets.header, QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID, (frame_count * sizeof(uint32_t)))) {
return false;
}
if (frame_number >= frame_count) {
qp_dprintf("Invalid frame number, was %d but only %d frames in image\n", (int)frame_number, (int)frame_count);
return false;
}
// Skip the necessary amount of data to get to the requested frame offset
qp_stream_seek(stream, frame_number * sizeof(uint32_t), SEEK_CUR);
// Read the frame offset
uint32_t offset = 0;
if (qp_stream_read(&offset, sizeof(uint32_t), 1, stream) != 1) {
qp_dprintf("Failed to read frame offset, expected length was not %d\n", (int)sizeof(uint32_t));
return false;
}
// Copy out the required info
if (frame_offset) {
*frame_offset = offset;
}
return true;
}
void qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number) {
// Read the offset
uint32_t offset = 0;
qgf_read_frame_offset(stream, frame_number, &offset);
// Move to the offset
qp_stream_setpos(stream, offset);
}
bool qgf_validate_frame_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t *bpp, bool *has_palette, bool *is_delta) {
// Seek to the correct location
qgf_seek_to_frame_descriptor(stream, frame_number);
// Read the raw descriptor
qgf_frame_v1_t frame_descriptor;
if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t));
return false;
}
// Make sure this block is valid
if (!qgf_validate_block_header(&frame_descriptor.header, QGF_FRAME_DESCRIPTOR_TYPEID, (sizeof(qgf_frame_v1_t) - sizeof(qgf_block_header_v1_t)))) {
return false;
}
return qgf_parse_frame_descriptor(&frame_descriptor, bpp, has_palette, is_delta, NULL, NULL);
}
bool qgf_validate_palette_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t bpp) {
// Read the palette descriptor
qgf_palette_v1_t palette_descriptor;
if (qp_stream_read(&palette_descriptor, sizeof(qgf_palette_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read palette_descriptor, expected length was not %d\n", (int)sizeof(qgf_palette_v1_t));
return false;
}
// Make sure this block is valid
uint32_t expected_length = (1 << bpp) * 3 * sizeof(uint8_t);
if (!qgf_validate_block_header(&palette_descriptor.header, QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID, expected_length)) {
return false;
}
// Move forward in the stream to the next block
qp_stream_seek(stream, expected_length, SEEK_CUR);
return true;
}
bool qgf_validate_delta_descriptor(qp_stream_t *stream, uint16_t frame_number) {
// Read the delta descriptor
qgf_delta_v1_t delta_descriptor;
if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t));
return false;
}
// Make sure this block is valid
if (!qgf_validate_block_header(&delta_descriptor.header, QGF_FRAME_DELTA_DESCRIPTOR_TYPEID, (sizeof(qgf_delta_v1_t) - sizeof(qgf_block_header_v1_t)))) {
return false;
}
return true;
}
bool qgf_validate_frame_data_descriptor(qp_stream_t *stream, uint16_t frame_number) {
// Read and validate the data block
qgf_data_v1_t data_descriptor;
if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t));
return false;
}
if (!qgf_validate_block_header(&data_descriptor.header, QGF_FRAME_DATA_DESCRIPTOR_TYPEID, -1)) {
return false;
}
return true;
}
bool qgf_validate_stream(qp_stream_t *stream) {
uint16_t frame_count;
if (!qgf_read_graphics_descriptor(stream, NULL, NULL, &frame_count, NULL)) {
return false;
}
// Read and validate all the frames (automatically validates the frame offset descriptor in the process)
for (uint16_t i = 0; i < frame_count; ++i) {
// Validate the frame descriptor block
uint8_t bpp;
bool has_palette;
bool has_delta;
if (!qgf_validate_frame_descriptor(stream, i, &bpp, &has_palette, &has_delta)) {
return false;
}
// If we've got a palette block, check it
if (has_palette && !qgf_validate_palette_descriptor(stream, i, bpp)) {
return false;
}
// If we've got a delta block, check it
if (has_delta && !qgf_validate_delta_descriptor(stream, i)) {
return false;
}
// Check the data block
if (!qgf_validate_frame_data_descriptor(stream, i)) {
return false;
}
}
return true;
}
// Work out the total size of an image definition, assuming we can read far enough into the file
uint32_t qgf_get_total_size(qp_stream_t *stream) {
// Get the original location
uint32_t oldpos = qp_stream_tell(stream);
// Read the graphics descriptor, grabbing the size
uint32_t total_size;
if (!qgf_read_graphics_descriptor(stream, NULL, NULL, NULL, &total_size)) {
return false;
}
// Restore the original location
qp_stream_setpos(stream, oldpos);
return total_size;
}

136
quantum/painter/qgf.h Normal file
View file

@ -0,0 +1,136 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
// Quantum Graphics File "QGF" File Format.
// See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
#include <stdint.h>
#include <stdbool.h>
#include "qp_stream.h"
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QGF structures
/////////////////////////////////////////
// Common block header
typedef struct QP_PACKED qgf_block_header_v1_t {
uint8_t type_id; // See each respective block type below.
uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors.
uint32_t length : 24; // 24-bit blob length, allowing for block sizes of a maximum of 16MB.
} qgf_block_header_v1_t;
_Static_assert(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF");
/////////////////////////////////////////
// Graphics descriptor
#define QGF_GRAPHICS_DESCRIPTOR_TYPEID 0x00
typedef struct QP_PACKED qgf_graphics_descriptor_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 }
uint32_t magic : 24; // constant, equal to 0x464751 ("QGF")
uint8_t qgf_version; // constant, equal to 0x01
uint32_t total_file_size; // total size of the entire file, starting at offset zero
uint32_t neg_total_file_size; // negated value of total_file_size
uint16_t image_width; // in pixels
uint16_t image_height; // in pixels
uint16_t frame_count; // minimum of 1
} qgf_graphics_descriptor_v1_t;
_Static_assert(sizeof(qgf_graphics_descriptor_v1_t) == (sizeof(qgf_block_header_v1_t) + 18), "qgf_graphics_descriptor_v1_t must be 23 bytes in v1 of QGF");
#define QGF_MAGIC 0x464751
/////////////////////////////////////////
// Frame offset descriptor
#define QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID 0x01
typedef struct QP_PACKED qgf_frame_offsets_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = (N * sizeof(uint32_t)) }
uint32_t offset[0]; // '0' signifies that this struct is immediately followed by the frame offsets
} qgf_frame_offsets_v1_t;
_Static_assert(sizeof(qgf_frame_offsets_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_frame_offsets_v1_t must only contain qgf_block_header_v1_t in v1 of QGF");
/////////////////////////////////////////
// Frame descriptor
#define QGF_FRAME_DESCRIPTOR_TYPEID 0x02
typedef struct QP_PACKED qgf_frame_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 6 }
qp_image_format_t format : 8; // Frame format, see qp.h.
uint8_t flags; // Frame flags, see below.
painter_compression_t compression_scheme : 8; // Compression scheme, see qp.h.
uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
uint16_t delay; // frame delay time for animations (in units of milliseconds)
} qgf_frame_v1_t;
_Static_assert(sizeof(qgf_frame_v1_t) == (sizeof(qgf_block_header_v1_t) + 6), "qgf_frame_v1_t must be 11 bytes in v1 of QGF");
#define QGF_FRAME_FLAG_DELTA 0x02
#define QGF_FRAME_FLAG_TRANSPARENT 0x01
/////////////////////////////////////////
// Frame palette descriptor
#define QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID 0x03
typedef struct QP_PACKED qgf_palette_entry_v1_t {
uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t.
uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t.
uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t.
} qgf_palette_entry_v1_t;
_Static_assert(sizeof(qgf_palette_entry_v1_t) == 3, "Palette entry is not 3 bytes in size");
typedef struct QP_PACKED qgf_palette_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) }
qgf_palette_entry_v1_t hsv[0]; // N * hsv, where N is the number of palette entries depending on the frame format in the descriptor
} qgf_palette_v1_t;
_Static_assert(sizeof(qgf_palette_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_palette_v1_t must only contain qgf_block_header_v1_t in v1 of QGF");
/////////////////////////////////////////
// Frame delta descriptor
#define QGF_FRAME_DELTA_DESCRIPTOR_TYPEID 0x04
typedef struct QP_PACKED qgf_delta_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x04, .neg_type_id = (~0x04), .length = 8 }
uint16_t left; // The left pixel location to draw the delta image
uint16_t top; // The top pixel location to draw the delta image
uint16_t right; // The right pixel location to to draw the delta image
uint16_t bottom; // The bottom pixel location to to draw the delta image
} qgf_delta_v1_t;
_Static_assert(sizeof(qgf_delta_v1_t) == (sizeof(qgf_block_header_v1_t) + 8), "qgf_delta_v1_t must be 13 bytes in v1 of QGF");
/////////////////////////////////////////
// Frame data descriptor
#define QGF_FRAME_DATA_DESCRIPTOR_TYPEID 0x05
typedef struct QP_PACKED qgf_data_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x05, .neg_type_id = (~0x05), .length = N }
uint8_t data[0]; // 0 signifies that this struct is immediately followed by the length of data specified in the header
} qgf_data_v1_t;
_Static_assert(sizeof(qgf_data_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_data_v1_t must only contain qgf_block_header_v1_t in v1 of QGF");
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QGF API
uint32_t qgf_get_total_size(qp_stream_t *stream);
bool qgf_validate_stream(qp_stream_t *stream);
bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length);
bool qgf_read_graphics_descriptor(qp_stream_t *stream, uint16_t *image_width, uint16_t *image_height, uint16_t *frame_count, uint32_t *total_bytes);
bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette);
void qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number);
bool qgf_parse_frame_descriptor(qgf_frame_v1_t *frame_descriptor, uint8_t *bpp, bool *has_palette, bool *is_delta, painter_compression_t *compression_scheme, uint16_t *delay);

228
quantum/painter/qp.c Normal file
View file

@ -0,0 +1,228 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include <quantum.h>
#include <utf8.h>
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_draw.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal driver validation
static bool validate_driver_vtable(struct painter_driver_t *driver) {
return (driver->driver_vtable && driver->driver_vtable->init && driver->driver_vtable->power && driver->driver_vtable->clear && driver->driver_vtable->viewport && driver->driver_vtable->pixdata && driver->driver_vtable->palette_convert && driver->driver_vtable->append_pixels) ? true : false;
}
static bool validate_comms_vtable(struct painter_driver_t *driver) {
return (driver->comms_vtable && driver->comms_vtable->comms_init && driver->comms_vtable->comms_start && driver->comms_vtable->comms_stop && driver->comms_vtable->comms_send) ? true : false;
}
static bool validate_driver_integrity(struct painter_driver_t *driver) {
return validate_driver_vtable(driver) && validate_comms_vtable(driver);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_init
bool qp_init(painter_device_t device, painter_rotation_t rotation) {
qp_dprintf("qp_init: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
driver->validate_ok = false;
if (!validate_driver_integrity(driver)) {
qp_dprintf("Failed to validate driver integrity in qp_init\n");
return false;
}
driver->validate_ok = true;
if (!qp_comms_init(device)) {
driver->validate_ok = false;
qp_dprintf("qp_init: fail (could not init comms)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("qp_init: fail (could not start comms)\n");
return false;
}
// Set the rotation before init
driver->rotation = rotation;
// Invoke init
bool ret = driver->driver_vtable->init(device, rotation);
qp_comms_stop(device);
qp_dprintf("qp_init: %s\n", ret ? "ok" : "fail");
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_power
bool qp_power(painter_device_t device, bool power_on) {
qp_dprintf("qp_power: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_power: fail (validation_ok == false)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("qp_power: fail (could not start comms)\n");
return false;
}
bool ret = driver->driver_vtable->power(device, power_on);
qp_comms_stop(device);
qp_dprintf("qp_power: %s\n", ret ? "ok" : "fail");
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_clear
bool qp_clear(painter_device_t device) {
qp_dprintf("qp_clear: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_clear: fail (validation_ok == false)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("qp_clear: fail (could not start comms)\n");
return false;
}
bool ret = driver->driver_vtable->clear(device);
qp_comms_stop(device);
qp_dprintf("qp_clear: %s\n", ret ? "ok" : "fail");
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_flush
bool qp_flush(painter_device_t device) {
qp_dprintf("qp_flush: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_flush: fail (validation_ok == false)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("qp_flush: fail (could not start comms)\n");
return false;
}
bool ret = driver->driver_vtable->flush(device);
qp_comms_stop(device);
qp_dprintf("qp_flush: %s\n", ret ? "ok" : "fail");
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_get_geometry
void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y) {
qp_dprintf("qp_geometry: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
switch (driver->rotation) {
default:
case QP_ROTATION_0:
case QP_ROTATION_180:
if (width) {
*width = driver->panel_width;
}
if (height) {
*height = driver->panel_height;
}
break;
case QP_ROTATION_90:
case QP_ROTATION_270:
if (width) {
*width = driver->panel_height;
}
if (height) {
*height = driver->panel_width;
}
break;
}
if (rotation) {
*rotation = driver->rotation;
}
if (offset_x) {
*offset_x = driver->offset_x;
}
if (offset_y) {
*offset_y = driver->offset_y;
}
qp_dprintf("qp_geometry: ok\n");
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_set_viewport_offsets
void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y) {
qp_dprintf("qp_set_viewport_offsets: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
driver->offset_x = offset_x;
driver->offset_y = offset_y;
qp_dprintf("qp_set_viewport_offsets: ok\n");
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_viewport
bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
qp_dprintf("qp_viewport: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_viewport: fail (validation_ok == false)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("qp_viewport: fail (could not start comms)\n");
return false;
}
// Set the viewport
bool ret = driver->driver_vtable->viewport(device, left, top, right, bottom);
qp_dprintf("qp_viewport: %s\n", ret ? "ok" : "fail");
qp_comms_stop(device);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_pixdata
bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
qp_dprintf("qp_pixdata: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_pixdata: fail (validation_ok == false)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("qp_pixdata: fail (could not start comms)\n");
return false;
}
bool ret = driver->driver_vtable->pixdata(device, pixel_data, native_pixel_count);
qp_dprintf("qp_pixdata: %s\n", ret ? "ok" : "fail");
qp_comms_stop(device);
return ret;
}

453
quantum/painter/qp.h Normal file
View file

@ -0,0 +1,453 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "deferred_exec.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter global configurables (add to your keyboard's config.h)
#ifndef QUANTUM_PAINTER_NUM_IMAGES
/**
* @def This controls the maximum number of images that Quantum Painter can load at any one time. Images can be loaded
* using \ref qp_load_image_mem, and can be unloaded by calling \ref qp_close_image. Increasing this number in
* order to load more images increases the amount of RAM required. Image data is not held in RAM, just metadata.
*/
# define QUANTUM_PAINTER_NUM_IMAGES 8
#endif // QUANTUM_PAINTER_NUM_IMAGES
#ifndef QUANTUM_PAINTER_NUM_FONTS
/**
* @def This controls the maximum number of fonts that Quantum Painter can load. Fonts can be loaded using
* \ref qp_load_font_mem, and can be unloaded by calling \ref qp_close_font. Increasing this number in order to
* load more fonts increases the amount of RAM required. Font data is not held in RAM, unless
* \ref QUANTUM_PAINTER_LOAD_FONTS_TO_RAM is set to TRUE.
*/
# define QUANTUM_PAINTER_NUM_FONTS 4
#endif // QUANTUM_PAINTER_NUM_FONTS
#ifndef QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
/**
* @def This controls whether or not fonts should be cached in RAM. Under normal circumstances, fonts can have quite
* random access patterns, and due to timing of flash memory or external storage, it may be a significant speedup
* moving the font into RAM before use. Defaults to "off", but if it's enabled it will fallback to reading from the
* original location if corresponding RAM could not be allocated (such as being too large).
*/
# define QUANTUM_PAINTER_LOAD_FONTS_TO_RAM FALSE
#endif
#ifndef QUANTUM_PAINTER_CONCURRENT_ANIMATIONS
/**
* @def This controls the maximum number of animations that Quantum Painter can play simultaneously. Increasing this
* number in order to play more animations at the same time increases the amount of RAM required.
*/
# define QUANTUM_PAINTER_CONCURRENT_ANIMATIONS 4
#endif // QUANTUM_PAINTER_CONCURRENT_ANIMATIONS
#ifndef QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE
/**
* @def This controls the maximum size of the pixel data buffer used for single blocks of transmission. Larger buffers
* means more data is processed at one time, with less frequent transmissions, at the cost of RAM.
*/
# define QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE 32
#endif
#ifndef QUANTUM_PAINTER_SUPPORTS_256_PALETTE
/**
* @def This controls whether 256-color palettes are supported. This has relatively hefty requirements on RAM -- at
* least 1kB extra is required just to store the palette information, with more required for other metadata.
*/
# define QUANTUM_PAINTER_SUPPORTS_256_PALETTE FALSE
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter types
/**
* @typedef A handle to a Quantum Painter device, such as an LCD or OLED. Most Quantum Painter APIs require this
* argument in order to perform operations on the display.
*/
typedef const void *painter_device_t;
/**
* @typedef The desired rotation of a panel. Used as a parameter to \ref qp_init, and can be queried by
* \ref qp_get_geometry.
*/
typedef enum { QP_ROTATION_0, QP_ROTATION_90, QP_ROTATION_180, QP_ROTATION_270 } painter_rotation_t;
/**
* @typedef A descriptor for a Quantum Painter image.
*/
typedef struct painter_image_desc_t {
uint16_t width; ///< Image width
uint16_t height; ///< Image height
uint16_t frame_count; ///< Number of frames in this image
} painter_image_desc_t;
/**
* @typedef A handle to a Quantum Painter image.
*/
typedef const painter_image_desc_t *painter_image_handle_t;
/**
* @typedef A descriptor for a Quantum Painter font.
*/
typedef struct painter_font_desc_t {
uint8_t line_height; ///< The number of pixels in height for each line
} painter_font_desc_t;
/**
* @typedef A handle to a Quantum Painter font.
*/
typedef const painter_font_desc_t *painter_font_handle_t;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API
/**
* Initialize a device and set its rotation.
*
* @param device[in] the handle of the device to initialize
* @param rotation[in] the rotation to use
* @return true if initialization succeeded
* @return false if initialization failed
*/
bool qp_init(painter_device_t device, painter_rotation_t rotation);
/**
* Controls whether a display is on or off.
*
* @note If backlighting is used to control brightness (such as for an LCD), it will need to be handled external to
* Quantum Painter.
*
* @param device[in] the handle of the device to control
* @param power_on[in] whether or not the device should be on
* @return true if controlling the power state succeeded
* @return false if controlling the power state failed
*/
bool qp_power(painter_device_t device, bool power_on);
/**
* Clears a device's screen.
*
* @param device[in] the handle of the device to control
* @return true if clearing the screen succeeded
* @return false if clearing the screen failed
*/
bool qp_clear(painter_device_t device);
/**
* Transmits any outstanding data to the screen in order to persist all changes to the display.
*
* @note Drivers without internal framebuffers will likely ignore this API.
*
* @param device[in] the handle of the device to control
* @return true if flushing changes to the screen succeeded
* @return false if flushing changes to the screen failed
*/
bool qp_flush(painter_device_t device);
/**
* Retrieves the size, rotation, and offsets for the display.
*
* @note Any arguments of NULL will be ignored.
*
* @param device[in] the handle of the device to control
* @param width[out] the device's width
* @param height[out] the device's height
* @param rotation[out] the device's rotation
* @param offset_x[out] the device's x-offset applied while drawing
* @param offset_y[out] the device's y-offset applied while drawing
*/
void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y);
/**
* Allows repositioning of the viewport if the panel geometry offsets are non-zero.
*
* @param device[in] the handle of the device to control
* @param offset_x[in] the device's x-offset applied while drawing
* @param offset_y[in] the device's y-offset applied while drawing
*/
void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y);
/**
* Sets a pixel to the specified color.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position to draw onto the device
* @param y[in] the y-position to draw onto the device
* @param hue[in] the hue to use, with 0-360 mapped to 0-255
* @param sat[in] the saturation to use, with 0-100% mapped to 0-255
* @param val[in] the value to use, with 0-100% mapped to 0-255
* @return true if setting the pixel succeeded
* @return false if setting the pixel failed
*/
bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val);
/**
* Draws a line using the specified color.
*
* @param device[in] the handle of the device to control
* @param x0[in] the device's x-position to start
* @param y0[in] the device's y-position to start
* @param x1[in] the device's x-position to finish
* @param y1[in] the device's y-position to finish
* @param hue[in] the hue to use, with 0-360 mapped to 0-255
* @param sat[in] the saturation to use, with 0-100% mapped to 0-255
* @param val[in] the value to use, with 0-100% mapped to 0-255
* @return true if drawing the line succeeded
* @return false if drawing the line failed
*/
bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val);
/**
* Draws a rectangle using the specified color, optionally filled.
*
* @param device[in] the handle of the device to control
* @param left[in] the device's x-position to start
* @param top[in] the device's y-position to start
* @param right[in] the device's x-position to finish
* @param bottom[in] the device's y-position to finish
* @param hue[in] the hue to use, with 0-360 mapped to 0-255
* @param sat[in] the saturation to use, with 0-100% mapped to 0-255
* @param val[in] the value to use, with 0-100% mapped to 0-255
* @param filled[in] whether the rectangle should be filled
* @return true if drawing the rectangle succeeded
* @return false if drawing the rectangle failed
*/
bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
/**
* Draws a circle using the specified color, optionally filled.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position of the centre of the circle to draw onto the device
* @param y[in] the y-position of the centre of the circle to draw onto the device
* @param radius[in] the radius of the circle to draw
* @param hue[in] the hue to use, with 0-360 mapped to 0-255
* @param sat[in] the saturation to use, with 0-100% mapped to 0-255
* @param val[in] the value to use, with 0-100% mapped to 0-255
* @param filled[in] whether the circle should be filled
* @return true if drawing the circle succeeded
* @return false if drawing the circle failed
*/
bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
/**
* Draws a ellipse using the specified color, optionally filled.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position of the centre of the ellipse to draw onto the device
* @param y[in] the y-position of the centre of the ellipse to draw onto the device
* @param sizex[in] the horizontal size of the ellipse
* @param sizey[in] the vertical size of the ellipse
* @param hue[in] the hue to use, with 0-360 mapped to 0-255
* @param sat[in] the saturation to use, with 0-100% mapped to 0-255
* @param val[in] the value to use, with 0-100% mapped to 0-255
* @param filled[in] whether the ellipse should be filled
* @return true if drawing the ellipse succeeded
* @return false if drawing the ellipse failed
*/
bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
/**
* Sets up the location on the display to stream raw pixel data to the display, using \ref qp_pixdata.
*
* @note This is for advanced uses only, and should not be required for normal Quantum Painter functionality.
*
* @param device[in] the handle of the device to control
* @param left[in] the device's x-position to start
* @param top[in] the device's y-position to start
* @param right[in] the device's x-position to finish
* @param bottom[in] the device's y-position to finish
* @return true if setting the viewport succeeded
* @return false if setting the viewport failed
*/
bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
/**
* Streams raw pixel data (in the native panel format) to the area previously set by \ref qp_viewport.
*
* @note This is for advanced uses only, and should not be required for normal Quantum Painter functionality.
*
* @param device[in] the handle of the device to control
* @param pixel_data[in] pointer to buffer data
* @param native_pixel_count[in] the number of pixels to transmit
* @return true if streaming of data succeeded
* @return false if streaming of data failed
*/
bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
/**
* Loads an image into memory.
*
* @note Images can be unloaded by calling \ref qp_close_image.
*
* @param buffer[in] the image data to load
* @return an image handle usable with \ref qp_drawimage, \ref qp_drawimage_recolor, \ref qp_animate, and
* \ref qp_animate_recolor.
* @return NULL if loading the image failed
*/
painter_image_handle_t qp_load_image_mem(const void *buffer);
/**
* Closes an image handle when no longer in use.
*
* @param image[in] the handle of the image to unload
* @return true if unloading the image succeeded
* @return false if unloading the image failed
*/
bool qp_close_image(painter_image_handle_t image);
/**
* Draws an image to the display.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position where the image should be drawn onto the device
* @param y[in] the y-position where the image should be drawn onto the device
* @param image[in] the handle of the image to draw
* @return true if drawing the image succeeded
* @return false if drawing the image failed
*/
bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
/**
* Draws an image to the display, recoloring monochrome images to the desired foreground/background.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position where the image should be drawn onto the device
* @param y[in] the y-position where the image should be drawn onto the device
* @param image[in] the handle of the image to draw
* @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
* @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
* @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
* @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
* @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
* @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
* @return true if drawing the image succeeded
* @return false if drawing the image failed
*/
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);
/**
* Draws an animation to the display.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position where the image should be drawn onto the device
* @param y[in] the y-position where the image should be drawn onto the device
* @param image[in] the handle of the image to draw
* @return the \ref deferred_token to use with \ref qp_stop_animation in order to stop animating
* @return INVALID_DEFERRED_TOKEN if animating the image failed
*/
deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
/**
* Draws an animation to the display, recoloring monochrome images to the desired foreground/background.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position where the image should be drawn onto the device
* @param y[in] the y-position where the image should be drawn onto the device
* @param image[in] the handle of the image to draw
* @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
* @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
* @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
* @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
* @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
* @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
* @return the \ref deferred_token to use with \ref qp_stop_animation in order to stop animating
* @return INVALID_DEFERRED_TOKEN if animating the image failed
*/
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);
/**
* Cancels a running animation.
*
* @param anim_token[in] the animation token returned by \ref qp_animate, or \ref qp_animate_recolor.
*/
void qp_stop_animation(deferred_token anim_token);
/**
* Loads a font into memory.
*
* @note Fonts can be unloaded by calling \ref qp_close_font.
*
* @param buffer[in] the font data to load
* @return an image handle usable with \ref qp_textwidth, \ref qp_drawtext, and \ref qp_drawtext_recolor.
* @return NULL if loading the font failed
*/
painter_font_handle_t qp_load_font_mem(const void *buffer);
/**
* Closes a font handle when no longer in use.
*
* @param font[in] the handle of the font to unload
* @return true if unloading the font succeeded
* @return false if unloading the font failed
*/
bool qp_close_font(painter_font_handle_t font);
/**
* Measures the width (in pixels) of the supplied string, given the specified font.
*
* @param font[in] the handle of the font
* @param str[in] the string to measure
* @return the width (in pixels) needed to draw the specified string
*/
int16_t qp_textwidth(painter_font_handle_t font, const char *str);
/**
* Draws text to the display.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position where the text should be drawn onto the device
* @param y[in] the y-position where the text should be drawn onto the device
* @param font[in] the handle of the font
* @param str[in] the string to draw
* @return the width (in pixels) used when drawing the specified string
*/
int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str);
/**
* Draws text to the display, recoloring monochrome fonts to the desired foreground/background.
*
* @param device[in] the handle of the device to control
* @param x[in] the x-position where the text should be drawn onto the device
* @param y[in] the y-position where the text should be drawn onto the device
* @param font[in] the handle of the font
* @param str[in] the string to draw
* @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
* @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
* @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
* @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
* @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
* @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
* @return the width (in pixels) used when drawing the specified string
*/
int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter Drivers
#ifdef QUANTUM_PAINTER_ILI9163_ENABLE
# include "qp_ili9163.h"
#endif // QUANTUM_PAINTER_ILI9163_ENABLE
#ifdef QUANTUM_PAINTER_ILI9341_ENABLE
# include "qp_ili9341.h"
#endif // QUANTUM_PAINTER_ILI9341_ENABLE
#ifdef QUANTUM_PAINTER_ST7789_ENABLE
# include "qp_st7789.h"
#endif // QUANTUM_PAINTER_ST7789_ENABLE
#ifdef QUANTUM_PAINTER_GC9A01_ENABLE
# include "qp_gc9a01.h"
#endif // QUANTUM_PAINTER_GC9A01_ENABLE
#ifdef QUANTUM_PAINTER_SSD1351_ENABLE
# include "qp_ssd1351.h"
#endif // QUANTUM_PAINTER_SSD1351_ENABLE

View file

@ -0,0 +1,72 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_comms.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Base comms APIs
bool qp_comms_init(painter_device_t device) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_comms_init: fail (validation_ok == false)\n");
return false;
}
return driver->comms_vtable->comms_init(device);
}
bool qp_comms_start(painter_device_t device) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_comms_start: fail (validation_ok == false)\n");
return false;
}
return driver->comms_vtable->comms_start(device);
}
void qp_comms_stop(painter_device_t device) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_comms_stop: fail (validation_ok == false)\n");
return;
}
driver->comms_vtable->comms_stop(device);
}
uint32_t qp_comms_send(painter_device_t device, const void *data, uint32_t byte_count) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_comms_send: fail (validation_ok == false)\n");
return false;
}
return driver->comms_vtable->comms_send(device, data, byte_count);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comms APIs that use a D/C pin
void qp_comms_command(painter_device_t device, uint8_t cmd) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable;
comms_vtable->send_command(device, cmd);
}
void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data) {
qp_comms_command(device, cmd);
qp_comms_send(device, &data, sizeof(data));
}
uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void *data, uint32_t byte_count) {
qp_comms_command(device, cmd);
return qp_comms_send(device, data, byte_count);
}
void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable;
comms_vtable->bulk_command_sequence(device, sequence, sequence_len);
}

View file

@ -0,0 +1,25 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdbool.h>
#include <stdlib.h>
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Base comms APIs
bool qp_comms_init(painter_device_t device);
bool qp_comms_start(painter_device_t device);
void qp_comms_stop(painter_device_t device);
uint32_t qp_comms_send(painter_device_t device, const void* data, uint32_t byte_count);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comms APIs that use a D/C pin
void qp_comms_command(painter_device_t device, uint8_t cmd);
void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data);
uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void* data, uint32_t byte_count);
void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len);

85
quantum/painter/qp_draw.h Normal file
View file

@ -0,0 +1,85 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "qp_internal.h"
#include "qp_stream.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter utility functions
// Global variable used for native pixel data streaming.
extern uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];
// Check if the supplied bpp is capable of being rendered
bool qp_internal_bpp_capable(uint8_t bits_per_pixel);
// Returns the number of pixels that can fit in the pixdata buffer
uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device);
// Fills the supplied buffer with equivalent native pixels matching the supplied HSV
void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val);
// qp_setpixel internal implementation, but uses the global pixdata buffer with pre-converted native pixel. Only the first pixel is used.
bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y);
// qp_rect internal implementation, but uses the global pixdata buffer with pre-converted native pixels.
bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t l, uint16_t t, uint16_t r, uint16_t b);
// Convert from input pixel data + palette to equivalent pixels
typedef int16_t (*qp_internal_byte_input_callback)(void* cb_arg);
typedef bool (*qp_internal_pixel_output_callback)(qp_pixel_t* palette, uint8_t index, void* cb_arg);
bool qp_internal_decode_palette(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t* palette, qp_internal_pixel_output_callback output_callback, void* output_arg);
bool qp_internal_decode_grayscale(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_internal_pixel_output_callback output_callback, void* output_arg);
bool qp_internal_decode_recolor(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qp_internal_pixel_output_callback output_callback, void* output_arg);
// Global variable used for interpolated pixel lookup table.
#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE
extern qp_pixel_t qp_internal_global_pixel_lookup_table[256];
#else
extern qp_pixel_t qp_internal_global_pixel_lookup_table[16];
#endif
// Generates a color-interpolated lookup table based off the number of items, from foreground to background, for use with monochrome image rendering.
// Returns true if a palette was created, false if the palette is reused.
// As this uses a global, this may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() below to reset.
bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps);
// Resets the global palette so that it can be regenerated. Only needed if the colors are identical, but a different display is used with a different internal pixel format.
void qp_internal_invalidate_palette(void);
// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header.
bool qp_internal_load_qgf_palette(qp_stream_t* stream, uint8_t bpp);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter codec functions
enum qp_internal_rle_mode_t {
MARKER_BYTE,
REPEATING_RUN,
NON_REPEATING_RUN,
};
struct qp_internal_byte_input_state {
painter_device_t device;
qp_stream_t* src_stream;
int16_t curr;
union {
// RLE-specific
struct {
enum qp_internal_rle_mode_t mode;
uint8_t remain; // number of bytes remaining in the current mode
} rle;
};
};
struct qp_internal_pixel_output_state {
painter_device_t device;
uint32_t pixel_write_pos;
uint32_t max_pixels;
};
bool qp_internal_pixel_appender(qp_pixel_t* palette, uint8_t index, void* cb_arg);
qp_internal_byte_input_callback qp_internal_prepare_input_state(struct qp_internal_byte_input_state* input_state, painter_compression_t compression);

View file

@ -0,0 +1,172 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp.h"
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_draw.h"
// Utilize 8-way symmetry to draw circles
static bool qp_circle_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) {
/*
Circles have the property of 8-way symmetry, so eight pixels can be drawn
for each computed [offsetx,offsety] given the center coordinates
represented by [centerx,centery].
For filled circles, we can draw horizontal lines between each pair of
pixels with the same final value of y.
Two special cases exist and have been optimized:
1) offsetx == offsety (the final point), makes half the coordinates
equivalent, so we can omit them (and the corresponding fill lines)
2) offsetx == 0 (the starting point) means that some horizontal lines
would be a single pixel in length, so we write individual pixels instead.
This also makes half the symmetrical points identical to their twins,
so we only need four points or two points and one line
*/
int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx);
int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx);
int16_t xpy = ((int16_t)centerx) + ((int16_t)offsety);
int16_t xmy = ((int16_t)centerx) - ((int16_t)offsety);
int16_t ypx = ((int16_t)centery) + ((int16_t)offsetx);
int16_t ymx = ((int16_t)centery) - ((int16_t)offsetx);
int16_t ypy = ((int16_t)centery) + ((int16_t)offsety);
int16_t ymy = ((int16_t)centery) - ((int16_t)offsety);
if (offsetx == 0) {
if (!qp_internal_setpixel_impl(device, centerx, ypy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, centerx, ymy)) {
return false;
}
if (filled) {
if (!qp_internal_fillrect_helper_impl(device, xpy, centery, xmy, centery)) {
return false;
}
} else {
if (!qp_internal_setpixel_impl(device, xpy, centery)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmy, centery)) {
return false;
}
}
} else if (offsetx == offsety) {
if (filled) {
if (!qp_internal_fillrect_helper_impl(device, xpy, ypy, xmy, ypy)) {
return false;
}
if (!qp_internal_fillrect_helper_impl(device, xpy, ymy, xmy, ymy)) {
return false;
}
} else {
if (!qp_internal_setpixel_impl(device, xpy, ypy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmy, ypy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xpy, ymy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmy, ymy)) {
return false;
}
}
} else {
if (filled) {
if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) {
return false;
}
if (!qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) {
return false;
}
if (!qp_internal_fillrect_helper_impl(device, xpy, ypx, xmy, ypx)) {
return false;
}
if (!qp_internal_fillrect_helper_impl(device, xpy, ymx, xmy, ymx)) {
return false;
}
} else {
if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmx, ypy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmx, ymy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xpy, ypx)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmy, ypx)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xpy, ymx)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmy, ymx)) {
return false;
}
}
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_circle
bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
qp_dprintf("qp_circle: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_circle: fail (validation_ok == false)\n");
return false;
}
// plot the initial set of points for x, y and r
int16_t xcalc = 0;
int16_t ycalc = (int16_t)radius;
int16_t err = ((5 - (radius >> 2)) >> 2);
qp_internal_fill_pixdata(device, (radius * 2) + 1, hue, sat, val);
if (!qp_comms_start(device)) {
qp_dprintf("qp_circle: fail (could not start comms)\n");
return false;
}
bool ret = true;
if (!qp_circle_helper_impl(device, x, y, xcalc, ycalc, filled)) {
ret = false;
}
if (ret) {
while (xcalc < ycalc) {
xcalc++;
if (err < 0) {
err += (xcalc << 1) + 1;
} else {
ycalc--;
err += ((xcalc - ycalc) << 1) + 1;
}
if (!qp_circle_helper_impl(device, x, y, xcalc, ycalc, filled)) {
ret = false;
break;
}
}
}
qp_dprintf("qp_circle: %s\n", ret ? "ok" : "fail");
qp_comms_stop(device);
return ret;
}

View file

@ -0,0 +1,142 @@
// 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"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Palette / Monochrome-format decoder
static const qp_pixel_t qp_pixel_white = {.hsv888 = {.h = 0, .s = 0, .v = 255}};
static const qp_pixel_t qp_pixel_black = {.hsv888 = {.h = 0, .s = 0, .v = 0}};
bool qp_internal_bpp_capable(uint8_t bits_per_pixel) {
#if !(QUANTUM_PAINTER_SUPPORTS_256_PALETTE)
if (bits_per_pixel > 4) {
qp_dprintf("qp_internal_decode_palette: image bpp greater than 4\n");
return false;
}
#endif
if (bits_per_pixel > 8) {
qp_dprintf("qp_internal_decode_palette: image bpp greater than 8\n");
return false;
}
return true;
}
bool qp_internal_decode_palette(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t* palette, qp_internal_pixel_output_callback output_callback, void* output_arg) {
const uint8_t pixel_bitmask = (1 << bits_per_pixel) - 1;
const uint8_t pixels_per_byte = 8 / bits_per_pixel;
uint32_t remaining_pixels = pixel_count; // don't try to derive from byte_count, we may not use an entire byte
while (remaining_pixels > 0) {
uint8_t byteval = input_callback(input_arg);
if (byteval < 0) {
return false;
}
uint8_t loop_pixels = remaining_pixels < pixels_per_byte ? remaining_pixels : pixels_per_byte;
for (uint8_t q = 0; q < loop_pixels; ++q) {
if (!output_callback(palette, byteval & pixel_bitmask, output_arg)) {
return false;
}
byteval >>= bits_per_pixel;
}
remaining_pixels -= loop_pixels;
}
return true;
}
bool qp_internal_decode_grayscale(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_internal_pixel_output_callback output_callback, void* output_arg) {
return qp_internal_decode_recolor(device, pixel_count, bits_per_pixel, input_callback, input_arg, qp_pixel_white, qp_pixel_black, output_callback, output_arg);
}
bool qp_internal_decode_recolor(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qp_internal_pixel_output_callback output_callback, void* output_arg) {
struct painter_driver_t* driver = (struct painter_driver_t*)device;
int16_t steps = 1 << bits_per_pixel; // number of items we need to interpolate
if (qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, steps)) {
if (!driver->driver_vtable->palette_convert(device, steps, qp_internal_global_pixel_lookup_table)) {
return false;
}
}
return qp_internal_decode_palette(device, pixel_count, bits_per_pixel, input_callback, input_arg, qp_internal_global_pixel_lookup_table, output_callback, output_arg);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Progressive pull of bytes, push of pixels
static inline int16_t qp_drawimage_byte_uncompressed_decoder(void* cb_arg) {
struct qp_internal_byte_input_state* state = (struct qp_internal_byte_input_state*)cb_arg;
state->curr = qp_stream_get(state->src_stream);
return state->curr;
}
static inline int16_t qp_drawimage_byte_rle_decoder(void* cb_arg) {
struct qp_internal_byte_input_state* state = (struct qp_internal_byte_input_state*)cb_arg;
// Work out if we're parsing the initial marker byte
if (state->rle.mode == MARKER_BYTE) {
uint8_t c = qp_stream_get(state->src_stream);
if (c >= 128) {
state->rle.mode = NON_REPEATING_RUN; // non-repeated run
state->rle.remain = c - 127;
} else {
state->rle.mode = REPEATING_RUN; // repeated run
state->rle.remain = c;
}
state->curr = qp_stream_get(state->src_stream);
}
// Work out which byte we're returning
uint8_t c = state->curr;
// Decrement the counter of the bytes remaining
state->rle.remain--;
if (state->rle.remain > 0) {
// If we're in a non-repeating run, queue up the next byte
if (state->rle.mode == NON_REPEATING_RUN) {
state->curr = qp_stream_get(state->src_stream);
}
} else {
// Swap back to querying the marker byte mode
state->rle.mode = MARKER_BYTE;
}
return c;
}
bool qp_internal_pixel_appender(qp_pixel_t* palette, uint8_t index, void* cb_arg) {
struct qp_internal_pixel_output_state* state = (struct qp_internal_pixel_output_state*)cb_arg;
struct painter_driver_t* driver = (struct painter_driver_t*)state->device;
if (!driver->driver_vtable->append_pixels(state->device, qp_internal_global_pixdata_buffer, palette, state->pixel_write_pos++, 1, &index)) {
return false;
}
// If we've hit the transmit limit, send out the entire buffer and reset the write position
if (state->pixel_write_pos == state->max_pixels) {
if (!driver->driver_vtable->pixdata(state->device, qp_internal_global_pixdata_buffer, state->pixel_write_pos)) {
return false;
}
state->pixel_write_pos = 0;
}
return true;
}
qp_internal_byte_input_callback qp_internal_prepare_input_state(struct qp_internal_byte_input_state* input_state, painter_compression_t compression) {
switch (compression) {
case IMAGE_UNCOMPRESSED:
return qp_drawimage_byte_uncompressed_decoder;
case IMAGE_COMPRESSED_RLE:
input_state->rle.mode = MARKER_BYTE;
input_state->rle.remain = 0;
return qp_drawimage_byte_rle_decoder;
default:
return NULL;
}
}

View file

@ -0,0 +1,294 @@
// Copyright 2021-2022 Nick Brassel (@tzarc)
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_draw.h"
#include "qgf.h"
_Static_assert((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE > 0) && (QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE % 16) == 0, "QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE needs to be a non-zero multiple of 16");
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Global variables
//
// NOTE: The variables in this section are intentionally outside a stack frame. They are able to be defined with larger
// sizes than the normal stack frames would allow, and as such need to be external.
//
// **** DO NOT refactor this and decide to place the variables inside the function calling them -- you will ****
// **** very likely get artifacts rendered to the screen as a result. ****
//
// Buffer used for transmitting native pixel data to the downstream device.
uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];
// Static buffer to contain a generated color palette
static bool generated_palette = false;
static int16_t generated_steps = -1;
static qp_pixel_t interpolated_fg_hsv888;
static qp_pixel_t interpolated_bg_hsv888;
#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE
qp_pixel_t qp_internal_global_pixel_lookup_table[256];
#else
qp_pixel_t qp_internal_global_pixel_lookup_table[16];
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
return ((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE * 8) / driver->native_bits_per_pixel);
}
// qp_setpixel internal implementation, but accepts a buffer with pre-converted native pixel. Only the first pixel is used.
bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
return driver->driver_vtable->viewport(device, x, y, x, y) && driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, 1);
}
// Fills the global native pixel buffer with equivalent pixels matching the supplied HSV
void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
uint32_t pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
num_pixels = QP_MIN(pixels_in_pixdata, num_pixels);
// Convert the color to native pixel format
qp_pixel_t color = {.hsv888 = {.h = hue, .s = sat, .v = val}};
driver->driver_vtable->palette_convert(device, 1, &color);
// Append the required number of pixels
uint8_t palette_idx = 0;
for (uint32_t i = 0; i < num_pixels; ++i) {
driver->driver_vtable->append_pixels(device, qp_internal_global_pixdata_buffer, &color, i, 1, &palette_idx);
}
}
// Resets the global palette so that it can be regenerated. Only needed if the colors are identical, but a different display is used with a different internal pixel format.
void qp_internal_invalidate_palette(void) {
generated_palette = false;
generated_steps = -1;
}
// Interpolates between two colors to generate a palette
bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps) {
// Check if we need to generate a new palette -- if the input parameters match then assume the palette can stay unchanged.
// This may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() to reset.
if (generated_palette == true && generated_steps == steps && memcmp(&interpolated_fg_hsv888, &fg_hsv888, sizeof(fg_hsv888)) == 0 && memcmp(&interpolated_bg_hsv888, &bg_hsv888, sizeof(bg_hsv888)) == 0) {
// We already have the correct palette, no point regenerating it.
return false;
}
// Save the parameters so we know whether we can skip generation
generated_palette = true;
generated_steps = steps;
interpolated_fg_hsv888 = fg_hsv888;
interpolated_bg_hsv888 = bg_hsv888;
int16_t hue_fg = fg_hsv888.hsv888.h;
int16_t hue_bg = bg_hsv888.hsv888.h;
// Make sure we take the "shortest" route from one hue to the other
if ((hue_fg - hue_bg) >= 128) {
hue_bg += 256;
} else if ((hue_fg - hue_bg) <= -128) {
hue_bg -= 256;
}
// Interpolate each of the lookup table entries
for (int16_t i = 0; i < steps; ++i) {
qp_internal_global_pixel_lookup_table[i].hsv888.h = (uint8_t)((hue_fg - hue_bg) * i / (steps - 1) + hue_bg);
qp_internal_global_pixel_lookup_table[i].hsv888.s = (uint8_t)((fg_hsv888.hsv888.s - bg_hsv888.hsv888.s) * i / (steps - 1) + bg_hsv888.hsv888.s);
qp_internal_global_pixel_lookup_table[i].hsv888.v = (uint8_t)((fg_hsv888.hsv888.v - bg_hsv888.hsv888.v) * i / (steps - 1) + bg_hsv888.hsv888.v);
qp_dprintf("qp_internal_interpolate_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)steps, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v);
}
return true;
}
// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header.
bool qp_internal_load_qgf_palette(qp_stream_t *stream, uint8_t bpp) {
qgf_palette_v1_t palette_descriptor;
if (qp_stream_read(&palette_descriptor, sizeof(qgf_palette_v1_t), 1, stream) != 1) {
qp_dprintf("Failed to read palette_descriptor, expected length was not %d\n", (int)sizeof(qgf_palette_v1_t));
return false;
}
// BPP determines the number of palette entries, each entry is a HSV888 triplet.
const uint16_t palette_entries = 1u << bpp;
// Ensure we aren't reusing any palette
qp_internal_invalidate_palette();
// Read the palette entries
for (uint16_t i = 0; i < palette_entries; ++i) {
// Read the palette entry
qgf_palette_entry_v1_t entry;
if (qp_stream_read(&entry, sizeof(qgf_palette_entry_v1_t), 1, stream) != 1) {
return false;
}
// Update the lookup table
qp_internal_global_pixel_lookup_table[i].hsv888.h = entry.h;
qp_internal_global_pixel_lookup_table[i].hsv888.s = entry.s;
qp_internal_global_pixel_lookup_table[i].hsv888.v = entry.v;
qp_dprintf("qp_internal_load_qgf_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)palette_entries, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v);
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_setpixel
bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_setpixel: fail (validation_ok == false)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("Failed to start comms in qp_setpixel\n");
return false;
}
qp_internal_fill_pixdata(device, 1, hue, sat, val);
bool ret = qp_internal_setpixel_impl(device, x, y);
qp_comms_stop(device);
qp_dprintf("qp_setpixel: %s\n", ret ? "ok" : "fail");
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_line
bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val) {
if (x0 == x1 || y0 == y1) {
qp_dprintf("qp_line(%d, %d, %d, %d): entry (deferring to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1);
bool ret = qp_rect(device, x0, y0, x1, y1, hue, sat, val, true);
qp_dprintf("qp_line(%d, %d, %d, %d): %s (deferred to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail");
return ret;
}
qp_dprintf("qp_line(%d, %d, %d, %d): entry\n", (int)x0, (int)y0, (int)x1, (int)y1);
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_line: fail (validation_ok == false)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("Failed to start comms in qp_line\n");
return false;
}
qp_internal_fill_pixdata(device, 1, hue, sat, val);
// draw angled line using Bresenham's algo
int16_t x = ((int16_t)x0);
int16_t y = ((int16_t)y0);
int16_t slopex = ((int16_t)x0) < ((int16_t)x1) ? 1 : -1;
int16_t slopey = ((int16_t)y0) < ((int16_t)y1) ? 1 : -1;
int16_t dx = abs(((int16_t)x1) - ((int16_t)x0));
int16_t dy = -abs(((int16_t)y1) - ((int16_t)y0));
int16_t e = dx + dy;
int16_t e2 = 2 * e;
bool ret = true;
while (x != x1 || y != y1) {
if (!qp_internal_setpixel_impl(device, x, y)) {
ret = false;
break;
}
e2 = 2 * e;
if (e2 >= dy) {
e += dy;
x += slopex;
}
if (e2 <= dx) {
e += dx;
y += slopey;
}
}
// draw the last pixel
if (!qp_internal_setpixel_impl(device, x, y)) {
ret = false;
}
qp_comms_stop(device);
qp_dprintf("qp_line(%d, %d, %d, %d): %s\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail");
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_rect
bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
uint32_t pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
struct painter_driver_t *driver = (struct painter_driver_t *)device;
uint16_t l = QP_MIN(left, right);
uint16_t r = QP_MAX(left, right);
uint16_t t = QP_MIN(top, bottom);
uint16_t b = QP_MAX(top, bottom);
uint16_t w = r - l + 1;
uint16_t h = b - t + 1;
uint32_t remaining = w * h;
driver->driver_vtable->viewport(device, l, t, r, b);
while (remaining > 0) {
uint32_t transmit = QP_MIN(remaining, pixels_in_pixdata);
if (!driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, transmit)) {
return false;
}
remaining -= transmit;
}
return true;
}
bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
qp_dprintf("qp_rect(%d, %d, %d, %d): entry\n", (int)left, (int)top, (int)right, (int)bottom);
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_rect: fail (validation_ok == false)\n");
return false;
}
// Cater for cases where people have submitted the coordinates backwards
uint16_t l = QP_MIN(left, right);
uint16_t r = QP_MAX(left, right);
uint16_t t = QP_MIN(top, bottom);
uint16_t b = QP_MAX(top, bottom);
uint16_t w = r - l + 1;
uint16_t h = b - t + 1;
bool ret = true;
if (!qp_comms_start(device)) {
qp_dprintf("Failed to start comms in qp_rect\n");
return false;
}
if (filled) {
// Fill up the pixdata buffer with the required number of native pixels
qp_internal_fill_pixdata(device, w * h, hue, sat, val);
// Perform the draw
ret = qp_internal_fillrect_helper_impl(device, l, t, r, b);
} else {
// Fill up the pixdata buffer with the required number of native pixels
qp_internal_fill_pixdata(device, QP_MAX(w, h), hue, sat, val);
// Draw 4x filled single-width rects to create an outline
if (!qp_internal_fillrect_helper_impl(device, l, t, r, t) || !qp_internal_fillrect_helper_impl(device, l, b, r, b) || !qp_internal_fillrect_helper_impl(device, l, t + 1, l, b - 1) || !qp_internal_fillrect_helper_impl(device, r, t + 1, r, b - 1)) {
ret = false;
}
}
qp_comms_stop(device);
qp_dprintf("qp_rect(%d, %d, %d, %d): %s\n", (int)l, (int)t, (int)r, (int)b, ret ? "ok" : "fail");
return ret;
}

View file

@ -0,0 +1,116 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_draw.h"
// Utilize 4-way symmetry to draw an ellipse
static bool qp_ellipse_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) {
/*
Ellipses have the property of 4-way symmetry, so four pixels can be drawn
for each computed [offsetx,offsety] given the center coordinates
represented by [centerx,centery].
For filled ellipses, we can draw horizontal lines between each pair of
pixels with the same final value of y.
When offsetx == 0 only two pixels can be drawn for filled or unfilled ellipses
*/
int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx);
int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx);
int16_t ypy = ((int16_t)centery) + ((int16_t)offsety);
int16_t ymy = ((int16_t)centery) - ((int16_t)offsety);
if (offsetx == 0) {
if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
return false;
}
} else if (filled) {
if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) {
return false;
}
if (offsety > 0 && !qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) {
return false;
}
} else {
if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmx, ypy)) {
return false;
}
if (!qp_internal_setpixel_impl(device, xmx, ymy)) {
return false;
}
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_ellipse
bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
qp_dprintf("qp_ellipse: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_ellipse: fail (validation_ok == false)\n");
return false;
}
int16_t aa = ((int16_t)sizex) * ((int16_t)sizex);
int16_t bb = ((int16_t)sizey) * ((int16_t)sizey);
int16_t fa = 4 * ((int16_t)aa);
int16_t fb = 4 * ((int16_t)bb);
int16_t dx = 0;
int16_t dy = ((int16_t)sizey);
qp_internal_fill_pixdata(device, QP_MAX(sizex, sizey), hue, sat, val);
if (!qp_comms_start(device)) {
qp_dprintf("qp_ellipse: fail (could not start comms)\n");
return false;
}
bool ret = true;
for (int16_t delta = (2 * bb) + (aa * (1 - (2 * sizey))); bb * dx <= aa * dy; dx++) {
if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) {
ret = false;
break;
}
if (delta >= 0) {
delta += fa * (1 - dy);
dy--;
}
delta += bb * (4 * dx + 6);
}
dx = sizex;
dy = 0;
for (int16_t delta = (2 * aa) + (bb * (1 - (2 * sizex))); aa * dy <= bb * dx; dy++) {
if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) {
ret = false;
break;
}
if (delta >= 0) {
delta += fb * (1 - dx);
dx--;
}
delta += aa * (4 * dy + 6);
}
qp_dprintf("qp_ellipse: %s\n", ret ? "ok" : "fail");
qp_comms_stop(device);
return ret;
}

View file

@ -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);
}

View file

@ -0,0 +1,444 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include <quantum.h>
#include <utf8.h>
#include "qp_internal.h"
#include "qp_draw.h"
#include "qp_comms.h"
#include "qff.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QFF font handles
typedef struct qff_font_handle_t {
painter_font_desc_t base;
bool validate_ok;
bool has_ascii_table;
uint16_t num_unicode_glyphs;
uint8_t bpp;
bool has_palette;
painter_compression_t compression_scheme;
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
};
#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
bool owns_buffer;
void *buffer;
#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
} qff_font_handle_t;
static qff_font_handle_t font_descriptors[QUANTUM_PAINTER_NUM_FONTS] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_load_font_mem
painter_font_handle_t qp_load_font_mem(const void *buffer) {
qp_dprintf("qp_load_font_mem: entry\n");
qff_font_handle_t *font = NULL;
// Find a free slot
for (int i = 0; i < QUANTUM_PAINTER_NUM_FONTS; ++i) {
if (!font_descriptors[i].validate_ok) {
font = &font_descriptors[i];
break;
}
}
// Drop out if not found
if (!font) {
qp_dprintf("qp_load_font_mem: fail (no free slot)\n");
return NULL;
}
// Assume we can read the graphics descriptor
font->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qff_font_descriptor_v1_t));
// Update the length of the stream to match, and rewind to the start
font->mem_stream.length = qff_get_total_size(&font->stream);
font->mem_stream.position = 0;
// Now that we know the length, validate the input data
if (!qff_validate_stream(&font->stream)) {
qp_dprintf("qp_load_font_mem: fail (failed validation)\n");
return NULL;
}
#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
// Clear out any existing data
font->owns_buffer = false;
font->buffer = NULL;
void *ram_buffer = malloc(font->mem_stream.length);
if (ram_buffer == NULL) {
qp_dprintf("qp_load_font_mem: could not allocate enough RAM for font, falling back to original\n");
} else {
do {
// Copy the data into RAM
if (qp_stream_read(ram_buffer, 1, font->mem_stream.length, &font->mem_stream) != font->mem_stream.length) {
qp_dprintf("qp_load_font_mem: could not copy from flash to RAM, falling back to original\n");
break;
}
// Create the new stream with the new buffer
font->buffer = ram_buffer;
font->owns_buffer = true;
font->mem_stream = qp_make_memory_stream(font->buffer, font->mem_stream.length);
} while (0);
}
// Free the buffer if we were unable to recreate the RAM copy.
if (ram_buffer != NULL && !font->owns_buffer) {
free(ram_buffer);
}
#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
// Read the info (parsing already successful above, no need to check return value)
qff_read_font_descriptor(&font->stream, &font->base.line_height, &font->has_ascii_table, &font->num_unicode_glyphs, &font->bpp, &font->has_palette, &font->compression_scheme, NULL);
if (!qp_internal_bpp_capable(font->bpp)) {
qp_dprintf("qp_load_font_mem: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\n", (int)font->bpp);
qp_close_font((painter_font_handle_t)font);
return NULL;
}
// Validation success, we can return the handle
font->validate_ok = true;
qp_dprintf("qp_load_font_mem: ok\n");
return (painter_font_handle_t)font;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_close_font
bool qp_close_font(painter_font_handle_t font) {
qff_font_handle_t *qff_font = (qff_font_handle_t *)font;
if (!qff_font->validate_ok) {
qp_dprintf("qp_close_font: fail (invalid font)\n");
return false;
}
#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
// Nuke the buffer, if required
if (qff_font->owns_buffer) {
free(qff_font->buffer);
qff_font->buffer = NULL;
qff_font->owns_buffer = false;
}
#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
// Free up this font for use elsewhere.
qff_font->validate_ok = false;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
// Callback to be invoked for each codepoint detected in the UTF8 input string
typedef bool (*code_point_handler)(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg);
// Helper that sets up the palette (if required) and returns the offset in the stream that the data starts
static inline bool qp_drawtext_prepare_font_for_render(painter_device_t device, qff_font_handle_t *qff_font, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, uint32_t *data_offset) {
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 (!data_offset) {
qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n");
return false;
}
// Work out where we're reading from
uint32_t offset = sizeof(qff_font_descriptor_v1_t);
if (qff_font->has_ascii_table) {
offset += sizeof(qff_ascii_glyph_table_v1_t);
}
if (qff_font->num_unicode_glyphs > 0) {
offset += sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * 6);
}
// Handle palette if needed
const uint16_t palette_entries = 1u << qff_font->bpp;
bool needs_pixconvert = false;
if (qff_font->has_palette) {
// If this font has a palette, we need to read it out and set up the pixel lookup table
qp_stream_setpos(&qff_font->stream, offset);
if (!qp_internal_load_qgf_palette(&qff_font->stream, qff_font->bpp)) {
return false;
}
// Skip this block, as far as offset calculations go
offset += sizeof(qgf_palette_v1_t) + (palette_entries * 3);
needs_pixconvert = true;
} else {
// Interpolate from fg/bg
int16_t palette_entries = 1 << qff_font->bpp;
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_drawtext_recolor: fail (could not convert pixels to native)\n");
qp_comms_stop(device);
return false;
}
}
*data_offset = offset;
return true;
}
static inline bool qp_drawtext_prepare_glyph_for_render(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t *width) {
if (code_point >= 0x20 && code_point < 0x7F && qff_font->has_ascii_table) {
// Do ascii table
qff_ascii_glyph_v1_t glyph_info;
uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor
+ sizeof(qgf_block_header_v1_t) // Skip the ascii table header
+ (code_point - 0x20) * sizeof(qff_ascii_glyph_v1_t); // Jump direct to the data offset based on the glyph index
if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) {
qp_dprintf("Failed to set stream position while reading ascii glyph info\n");
return false;
}
if (qp_stream_read(&glyph_info, sizeof(qff_ascii_glyph_v1_t), 1, &qff_font->stream) != 1) {
qp_dprintf("Failed to read glyph info\n");
return false;
}
uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK);
uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS);
uint32_t data_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor
+ sizeof(qff_ascii_glyph_table_v1_t) // Skip the ascii table
+ (qff_font->num_unicode_glyphs > 0 ? (sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t))) : 0) // Skip the unicode table
+ (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0) // Skip the palette
+ sizeof(qgf_block_header_v1_t) // Skip the data block header
+ glyph_offset; // Jump to the specified glyph offset
if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) {
qp_dprintf("Failed to set stream position while preparing ascii glyph data\n");
return false;
}
*width = glyph_width;
return true;
} else {
// Do unicode table, which may include singular ascii glyphs if full ascii table isn't specified
uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor
+ (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table
+ sizeof(qgf_block_header_v1_t); // Skip the unicode block header
if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) {
qp_dprintf("Failed to set stream position while preparing glyph data\n");
return false;
}
qff_unicode_glyph_v1_t glyph_info;
for (uint16_t i = 0; i < qff_font->num_unicode_glyphs; ++i) {
if (qp_stream_read(&glyph_info, sizeof(qff_unicode_glyph_v1_t), 1, &qff_font->stream) != 1) {
qp_dprintf("Failed to set stream position while reading unicode glyph info\n");
return false;
}
if (glyph_info.code_point == code_point) {
uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK);
uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS);
uint32_t data_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor
+ sizeof(qff_ascii_glyph_table_v1_t) // Skip the ascii table
+ (qff_font->num_unicode_glyphs > 0 ? (sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t))) : 0) // Skip the unicode table
+ (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0) // Skip the palette
+ sizeof(qgf_block_header_v1_t) // Skip the data block header
+ glyph_offset; // Jump to the specified glyph offset
if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) {
qp_dprintf("Failed to set stream position while preparing unicode glyph data\n");
return false;
}
*width = glyph_width;
return true;
}
}
// Not found
qp_dprintf("Failed to find unicode glyph info\n");
return false;
}
return false;
}
// Function to iterate over each UTF8 codepoint, invoking the callback for each decoded glyph
static inline bool qp_iterate_code_points(qff_font_handle_t *qff_font, const char *str, code_point_handler handler, void *cb_arg) {
while (*str) {
int32_t code_point = 0;
str = decode_utf8(str, &code_point);
if (code_point < 0) {
qp_dprintf("Invalid unicode code point decoded. Cannot render.\n");
return false;
}
uint8_t width;
if (!qp_drawtext_prepare_glyph_for_render(qff_font, code_point, &width)) {
qp_dprintf("Failed to prepare glyph for rendering.\n");
return false;
}
if (!handler(qff_font, code_point, width, qff_font->base.line_height, cb_arg)) {
qp_dprintf("Failed to execute glyph handler.\n");
return false;
}
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// String width calculation
// Callback state
struct code_point_iter_calcwidth_state {
int16_t width;
};
// Codepoint handler callback: width calc
static inline bool qp_font_code_point_handler_calcwidth(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg) {
struct code_point_iter_calcwidth_state *state = (struct code_point_iter_calcwidth_state *)cb_arg;
// Increment the overall width by this glyph's width
state->width += width;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// String drawing implementation
// Callback state
struct code_point_iter_drawglyph_state {
painter_device_t device;
int16_t xpos;
int16_t ypos;
qp_internal_byte_input_callback input_callback;
struct qp_internal_byte_input_state * input_state;
struct qp_internal_pixel_output_state *output_state;
};
// Codepoint handler callback: drawing
static inline bool qp_font_code_point_handler_drawglyph(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg) {
struct code_point_iter_drawglyph_state *state = (struct code_point_iter_drawglyph_state *)cb_arg;
struct painter_driver_t * driver = (struct painter_driver_t *)state->device;
// Reset the input state's RLE mode -- the stream should already be correctly positioned by qp_iterate_code_points()
state->input_state->rle.mode = MARKER_BYTE; // ignored if not using RLE
// Reset the output state
state->output_state->pixel_write_pos = 0;
// Configure where we're going to be rendering to
driver->driver_vtable->viewport(state->device, state->xpos, state->ypos, state->xpos + width - 1, state->ypos + height - 1);
// Move the x-position for the next glyph
state->xpos += width;
// Decode the pixel data for the glyph
uint32_t pixel_count = ((uint32_t)width) * height;
bool ret = qp_internal_decode_palette(state->device, pixel_count, qff_font->bpp, state->input_callback, state->input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, state->output_state);
// Any leftovers need transmission as well.
if (ret && state->output_state->pixel_write_pos > 0) {
ret &= driver->driver_vtable->pixdata(state->device, qp_internal_global_pixdata_buffer, state->output_state->pixel_write_pos);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_textwidth
int16_t qp_textwidth(painter_font_handle_t font, const char *str) {
qff_font_handle_t *qff_font = (qff_font_handle_t *)font;
if (!qff_font->validate_ok) {
qp_dprintf("qp_textwidth: fail (invalid font)\n");
return false;
}
// Create the codepoint iterator state
struct code_point_iter_calcwidth_state state = {.width = 0};
// Iterate each codepoint, return the calculated width if successful.
return qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_calcwidth, &state) ? state.width : 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_drawtext
int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str) {
// Offload to the recolor variant, substituting fg=white bg=black.
// Traditional LCDs with those colors will need to manually invoke qp_drawtext_recolor with the colors reversed.
return qp_drawtext_recolor(device, x, y, font, str, 0, 0, 255, 0, 0, 0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_drawtext_recolor
int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, 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_drawtext_recolor: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_drawtext_recolor: fail (validation_ok == false)\n");
return 0;
}
qff_font_handle_t *qff_font = (qff_font_handle_t *)font;
if (!qff_font->validate_ok) {
qp_dprintf("qp_drawtext_recolor: fail (invalid font)\n");
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("qp_drawtext_recolor: fail (could not start comms)\n");
return 0;
}
// Set up the byte input state and input callback
struct qp_internal_byte_input_state input_state = {.device = device, .src_stream = &qff_font->stream};
qp_internal_byte_input_callback input_callback = qp_internal_prepare_input_state(&input_state, qff_font->compression_scheme);
if (input_callback == NULL) {
qp_dprintf("qp_drawtext_recolor: fail (invalid font compression scheme)\n");
qp_comms_stop(device);
return false;
}
// Set up the pixel 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)};
// Set up the codepoint iteration state
struct code_point_iter_drawglyph_state state = {// Common
.device = device,
.xpos = x,
.ypos = y,
// Input
.input_callback = input_callback,
.input_state = &input_state,
// Output
.output_state = &output_state};
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}};
uint32_t data_offset;
if (!qp_drawtext_prepare_font_for_render(driver, qff_font, fg_hsv888, bg_hsv888, &data_offset)) {
qp_dprintf("qp_drawtext_recolor: fail (failed to prepare font for rendering)\n");
qp_comms_stop(device);
return false;
}
// Iterate the codepoints with the drawglyph callback
bool ret = qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_drawglyph, &state);
qp_dprintf("qp_drawtext_recolor: %s\n", ret ? "ok" : "fail");
qp_comms_stop(device);
return ret ? (state.xpos - x) : 0;
}

View file

@ -0,0 +1,33 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "quantum.h"
#include "qp.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
// Mark certain types that there should be no padding bytes between members.
#define QP_PACKED __attribute__((packed))
// Min/max defines
#define QP_MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
#define QP_MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
#ifdef QUANTUM_PAINTER_DEBUG
# include <debug.h>
# include <print.h>
# define qp_dprintf(...) dprintf(__VA_ARGS__)
#else
# define qp_dprintf(...) \
do { \
} while (0)
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Specific internal definitions
#include <qp_internal_formats.h>
#include <qp_internal_driver.h>

View file

@ -0,0 +1,82 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver callbacks
typedef bool (*painter_driver_init_func)(painter_device_t device, painter_rotation_t rotation);
typedef bool (*painter_driver_power_func)(painter_device_t device, bool power_on);
typedef bool (*painter_driver_clear_func)(painter_device_t device);
typedef bool (*painter_driver_flush_func)(painter_device_t device);
typedef bool (*painter_driver_viewport_func)(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
typedef bool (*painter_driver_pixdata_func)(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
typedef bool (*painter_driver_convert_palette_func)(painter_device_t device, int16_t palette_size, qp_pixel_t *palette);
typedef bool (*painter_driver_append_pixels)(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices);
// Driver vtable definition
struct painter_driver_vtable_t {
painter_driver_init_func init;
painter_driver_power_func power;
painter_driver_clear_func clear;
painter_driver_flush_func flush;
painter_driver_viewport_func viewport;
painter_driver_pixdata_func pixdata;
painter_driver_convert_palette_func palette_convert;
painter_driver_append_pixels append_pixels;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comms callbacks
typedef bool (*painter_driver_comms_init_func)(painter_device_t device);
typedef bool (*painter_driver_comms_start_func)(painter_device_t device);
typedef void (*painter_driver_comms_stop_func)(painter_device_t device);
typedef uint32_t (*painter_driver_comms_send_func)(painter_device_t device, const void *data, uint32_t byte_count);
struct painter_comms_vtable_t {
painter_driver_comms_init_func comms_init;
painter_driver_comms_start_func comms_start;
painter_driver_comms_stop_func comms_stop;
painter_driver_comms_send_func comms_send;
};
typedef void (*painter_driver_comms_send_command_func)(painter_device_t device, uint8_t cmd);
typedef void (*painter_driver_comms_bulk_command_sequence)(painter_device_t device, const uint8_t *sequence, size_t sequence_len);
struct painter_comms_with_command_vtable_t {
struct painter_comms_vtable_t base; // must be first, so this object can be cast from the painter_comms_vtable_t* type
painter_driver_comms_send_command_func send_command;
painter_driver_comms_bulk_command_sequence bulk_command_sequence;
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver base definition
struct painter_driver_t {
const struct painter_driver_vtable_t *driver_vtable;
const struct painter_comms_vtable_t * comms_vtable;
// Flag signifying if validation was successful
bool validate_ok;
// Panel geometry
uint16_t panel_width;
uint16_t panel_height;
// Target drawing rotation
painter_rotation_t rotation;
// Automated offsets for setting viewport
uint16_t offset_x;
uint16_t offset_y;
// Number of bits per pixel, used for determining how many pixels can be sent during a transmission of the pixdata buffer
uint8_t native_bits_per_pixel;
// Comms config pointer -- needs to point to an appropriate comms config if the comms driver requires it.
void *comms_config;
};

View file

@ -0,0 +1,49 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter pixel formats
// Datatype containing a pixel's color. The internal member used is dependent on the external context.
typedef union QP_PACKED qp_pixel_t {
uint8_t mono;
uint8_t palette_idx;
struct QP_PACKED {
uint8_t h;
uint8_t s;
uint8_t v;
} hsv888;
struct QP_PACKED {
uint8_t r;
uint8_t g;
uint8_t b;
} rgb888;
uint16_t rgb565;
uint32_t dummy;
} qp_pixel_t;
_Static_assert(sizeof(qp_pixel_t) == 4, "Invalid size for qp_pixel_t");
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter image format
typedef enum qp_image_format_t {
// Pixel formats available in the QGF frame format
GRAYSCALE_1BPP = 0x00,
GRAYSCALE_2BPP = 0x01,
GRAYSCALE_4BPP = 0x02,
GRAYSCALE_8BPP = 0x03,
PALETTE_1BPP = 0x04,
PALETTE_2BPP = 0x05,
PALETTE_4BPP = 0x06,
PALETTE_8BPP = 0x07,
} qp_image_format_t;
typedef enum painter_compression_t { IMAGE_UNCOMPRESSED, IMAGE_COMPRESSED_RLE } painter_compression_t;

171
quantum/painter/qp_stream.c Normal file
View file

@ -0,0 +1,171 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_stream.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Stream API
uint32_t qp_stream_read_impl(void *output_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream) {
uint8_t *output_ptr = (uint8_t *)output_buf;
uint32_t i;
for (i = 0; i < (num_members * member_size); ++i) {
int16_t c = qp_stream_get(stream);
if (c < 0) {
break;
}
output_ptr[i] = (uint8_t)(c & 0xFF);
}
return i / member_size;
}
uint32_t qp_stream_write_impl(const void *input_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream) {
uint8_t *input_ptr = (uint8_t *)input_buf;
uint32_t i;
for (i = 0; i < (num_members * member_size); ++i) {
if (!qp_stream_put(stream, input_ptr[i])) {
break;
}
}
return i / member_size;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Memory streams
int16_t mem_get(qp_stream_t *stream) {
qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
if (s->position >= s->length) {
s->is_eof = true;
return STREAM_EOF;
}
return s->buffer[s->position++];
}
bool mem_put(qp_stream_t *stream, uint8_t c) {
qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
if (s->position >= s->length) {
s->is_eof = true;
return false;
}
s->buffer[s->position++] = c;
return true;
}
int mem_seek(qp_stream_t *stream, int32_t offset, int origin) {
qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
// Handle as per fseek
int32_t position = s->position;
switch (origin) {
case SEEK_SET:
position = offset;
break;
case SEEK_CUR:
position += offset;
break;
case SEEK_END:
position = s->length + offset;
break;
default:
return -1;
}
// If we're before the start, ignore it.
if (position < 0) {
return -1;
}
// If we're at the end it's okay, we only care if we're after the end for failure purposes -- as per lseek()
if (position > s->length) {
return -1;
}
// Update the offset
s->position = position;
// Successful invocation of fseek() results in clearing of the EOF flag by default, mirror the same functionality
s->is_eof = false;
return 0;
}
int32_t mem_tell(qp_stream_t *stream) {
qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
return s->position;
}
bool mem_is_eof(qp_stream_t *stream) {
qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
return s->is_eof;
}
qp_memory_stream_t qp_make_memory_stream(void *buffer, int32_t length) {
qp_memory_stream_t stream = {
.base =
{
.get = mem_get,
.put = mem_put,
.seek = mem_seek,
.tell = mem_tell,
.is_eof = mem_is_eof,
},
.buffer = (uint8_t *)buffer,
.length = length,
.position = 0,
};
return stream;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FILE streams
#ifdef QP_STREAM_HAS_FILE_IO
int16_t file_get(qp_stream_t *stream) {
qp_file_stream_t *s = (qp_file_stream_t *)stream;
int c = fgetc(s->file);
if (c < 0 || feof(s->file)) return STREAM_EOF;
return (uint16_t)c;
}
bool file_put(qp_stream_t *stream, uint8_t c) {
qp_file_stream_t *s = (qp_file_stream_t *)stream;
return fputc(c, s->file) == c;
}
int file_seek(qp_stream_t *stream, int32_t offset, int origin) {
qp_file_stream_t *s = (qp_file_stream_t *)stream;
return fseek(s->file, offset, origin);
}
int32_t file_tell(qp_stream_t *stream) {
qp_file_stream_t *s = (qp_file_stream_t *)stream;
return (int32_t)ftell(s->file);
}
bool file_is_eof(qp_stream_t *stream) {
qp_file_stream_t *s = (qp_file_stream_t *)stream;
return (bool)feof(s->file);
}
qp_file_stream_t qp_make_file_stream(FILE *f) {
qp_file_stream_t stream = {
.base =
{
.get = file_get,
.put = file_put,
.seek = file_seek,
.tell = file_tell,
.is_eof = file_is_eof,
},
.file = f,
};
return stream;
}
#endif // QP_STREAM_HAS_FILE_IO

View file

@ -0,0 +1,82 @@
/* Copyright 2021 Nick Brassel (@tzarc)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Stream API
typedef struct qp_stream_t qp_stream_t;
#define qp_stream_get(stream_ptr) (((qp_stream_t *)(stream_ptr))->get((qp_stream_t *)(stream_ptr)))
#define qp_stream_put(stream_ptr, c) (((qp_stream_t *)(stream_ptr))->put((qp_stream_t *)(stream_ptr), (c)))
#define qp_stream_seek(stream_ptr, offset, origin) (((qp_stream_t *)(stream_ptr))->seek((qp_stream_t *)(stream_ptr), (offset), (origin)))
#define qp_stream_tell(stream_ptr) (((qp_stream_t *)(stream_ptr))->tell((qp_stream_t *)(stream_ptr)))
#define qp_stream_eof(stream_ptr) (((qp_stream_t *)(stream_ptr))->is_eof((qp_stream_t *)(stream_ptr)))
#define qp_stream_setpos(stream_ptr, offset) qp_stream_seek((stream_ptr), (offset), SEEK_SET)
#define qp_stream_getpos(stream_ptr) qp_stream_tell((stream_ptr))
#define qp_stream_read(output_buf, member_size, num_members, stream_ptr) qp_stream_read_impl((output_buf), (member_size), (num_members), (qp_stream_t *)(stream_ptr))
#define qp_stream_write(input_buf, member_size, num_members, stream_ptr) qp_stream_write_impl((input_buf), (member_size), (num_members), (qp_stream_t *)(stream_ptr))
uint32_t qp_stream_read_impl(void *output_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream);
uint32_t qp_stream_write_impl(const void *input_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream);
#define STREAM_EOF ((int16_t)(-1))
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Stream definition
struct qp_stream_t {
int16_t (*get)(qp_stream_t *stream);
bool (*put)(qp_stream_t *stream, uint8_t c);
int (*seek)(qp_stream_t *stream, int32_t offset, int origin);
int32_t (*tell)(qp_stream_t *stream);
bool (*is_eof)(qp_stream_t *stream);
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Memory streams
typedef struct qp_memory_stream_t {
qp_stream_t base;
uint8_t * buffer;
int32_t length;
int32_t position;
bool is_eof;
} qp_memory_stream_t;
qp_memory_stream_t qp_make_memory_stream(void *buffer, int32_t length);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FILE streams
#ifdef QP_STREAM_HAS_FILE_IO
typedef struct qp_file_stream_t {
qp_stream_t base;
FILE * file;
} qp_file_stream_t;
qp_file_stream_t qo_make_file_stream(FILE *f);
#endif // QP_STREAM_HAS_FILE_IO

116
quantum/painter/rules.mk Normal file
View file

@ -0,0 +1,116 @@
# Quantum Painter Configurables
QUANTUM_PAINTER_DRIVERS ?=
QUANTUM_PAINTER_ANIMATIONS_ENABLE ?= yes
# The list of permissible drivers that can be listed in QUANTUM_PAINTER_DRIVERS
VALID_QUANTUM_PAINTER_DRIVERS := ili9163_spi ili9341_spi st7789_spi gc9a01_spi ssd1351_spi
#-------------------------------------------------------------------------------
OPT_DEFS += -DQUANTUM_PAINTER_ENABLE
COMMON_VPATH += $(QUANTUM_DIR)/painter
SRC += \
$(QUANTUM_DIR)/utf8.c \
$(QUANTUM_DIR)/color.c \
$(QUANTUM_DIR)/painter/qp.c \
$(QUANTUM_DIR)/painter/qp_stream.c \
$(QUANTUM_DIR)/painter/qgf.c \
$(QUANTUM_DIR)/painter/qff.c \
$(QUANTUM_DIR)/painter/qp_draw_core.c \
$(QUANTUM_DIR)/painter/qp_draw_codec.c \
$(QUANTUM_DIR)/painter/qp_draw_circle.c \
$(QUANTUM_DIR)/painter/qp_draw_ellipse.c \
$(QUANTUM_DIR)/painter/qp_draw_image.c \
$(QUANTUM_DIR)/painter/qp_draw_text.c
# Check if people want animations... enable the defered exec if so.
ifeq ($(strip $(QUANTUM_PAINTER_ANIMATIONS_ENABLE)), yes)
DEFERRED_EXEC_ENABLE := yes
OPT_DEFS += -DQUANTUM_PAINTER_ANIMATIONS_ENABLE
endif
# Comms flags
QUANTUM_PAINTER_NEEDS_COMMS_SPI ?= no
# Handler for each driver
define handle_quantum_painter_driver
CURRENT_PAINTER_DRIVER := $1
ifeq ($$(filter $$(strip $$(CURRENT_PAINTER_DRIVER)),$$(VALID_QUANTUM_PAINTER_DRIVERS)),)
$$(error "$$(CURRENT_PAINTER_DRIVER)" is not a valid Quantum Painter driver)
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9163_spi)
QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
OPT_DEFS += -DQUANTUM_PAINTER_ILI9163_ENABLE -DQUANTUM_PAINTER_ILI9163_SPI_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/tft_panel \
$(DRIVER_PATH)/painter/ili9xxx
SRC += \
$(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
$(DRIVER_PATH)/painter/ili9xxx/qp_ili9163.c \
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9341_spi)
QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
OPT_DEFS += -DQUANTUM_PAINTER_ILI9341_ENABLE -DQUANTUM_PAINTER_ILI9341_SPI_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/tft_panel \
$(DRIVER_PATH)/painter/ili9xxx
SRC += \
$(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
$(DRIVER_PATH)/painter/ili9xxx/qp_ili9341.c \
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),st7789_spi)
QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
OPT_DEFS += -DQUANTUM_PAINTER_ST7789_ENABLE -DQUANTUM_PAINTER_ST7789_SPI_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/tft_panel \
$(DRIVER_PATH)/painter/st77xx
SRC += \
$(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
$(DRIVER_PATH)/painter/st77xx/qp_st7789.c
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),gc9a01_spi)
QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
OPT_DEFS += -DQUANTUM_PAINTER_GC9A01_ENABLE -DQUANTUM_PAINTER_GC9A01_SPI_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/tft_panel \
$(DRIVER_PATH)/painter/gc9a01
SRC += \
$(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
$(DRIVER_PATH)/painter/gc9a01/qp_gc9a01.c
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ssd1351_spi)
QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
OPT_DEFS += -DQUANTUM_PAINTER_SSD1351_ENABLE -DQUANTUM_PAINTER_SSD1351_SPI_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/tft_panel \
$(DRIVER_PATH)/painter/ssd1351
SRC += \
$(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
$(DRIVER_PATH)/painter/ssd1351/qp_ssd1351.c
endif
endef
# Iterate through the listed drivers for the build, including what's necessary
$(foreach qp_driver,$(QUANTUM_PAINTER_DRIVERS),$(eval $(call handle_quantum_painter_driver,$(qp_driver))))
# If SPI comms is needed, set up the required files
ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI)), yes)
OPT_DEFS += -DQUANTUM_PAINTER_SPI_ENABLE
QUANTUM_LIB_SRC += spi_master.c
VPATH += $(DRIVER_PATH)/painter/comms
SRC += \
$(QUANTUM_DIR)/painter/qp_comms.c \
$(DRIVER_PATH)/painter/comms/qp_comms_spi.c
ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET)), yes)
OPT_DEFS += -DQUANTUM_PAINTER_SPI_DC_RESET_ENABLE
endif
endif