1f2b1dedccdf21b629c45ece80b4ca32f6653296
- Author
- Nick Brassel <nick@tzarc.org>
- Committer
- GitHub <noreply@github.com>
- Date
Message
Diff
This diff is truncated to protect this page.
1diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml
2index 26bcb2f5110670ac2909414ba6589d83231edab8..726ce19f0c002104bc7ef04a1b767d6a5a65052c 100644
3--- a/.github/workflows/unit_test.yml
4+++ b/.github/workflows/unit_test.yml
5@@ -26,5 +26,7 @@ jobs:
6 - uses: actions/checkout@v2
7 with:
8 submodules: recursive
9+ - name: Install dependencies
10+ run: pip3 install -r requirements-dev.txt
11 - name: Run tests
12 run: make test:all
13diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk
14index a1793f91a57d28d066d901901d972d5562be7eae..c976b8296d5a894b86e2023449c62828be90f3d0 100644
15--- a/builddefs/common_features.mk
16+++ b/builddefs/common_features.mk
17@@ -149,6 +149,11 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
18 endif
19 endif
20
21+QUANTUM_PAINTER_ENABLE ?= no
22+ifeq ($(strip $(QUANTUM_PAINTER_ENABLE)), yes)
23+ include $(QUANTUM_DIR)/painter/rules.mk
24+endif
25+
26 VALID_EEPROM_DRIVER_TYPES := vendor custom transient i2c spi
27 EEPROM_DRIVER ?= vendor
28 ifeq ($(filter $(EEPROM_DRIVER),$(VALID_EEPROM_DRIVER_TYPES)),)
29@@ -696,7 +701,8 @@ endif
30
31 ifeq ($(strip $(UNICODE_COMMON)), yes)
32 OPT_DEFS += -DUNICODE_COMMON_ENABLE
33- SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c
34+ SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c \
35+ $(QUANTUM_DIR)/utf8.c
36 endif
37
38 MAGIC_ENABLE ?= yes
39diff --git a/docs/_summary.md b/docs/_summary.md
40index 249bfcd9edfad32aea7d9065e23cc8b5787e389f..786685eba47948a88ae266f0da92eaa63d2d02f7 100644
41--- a/docs/_summary.md
42+++ b/docs/_summary.md
43@@ -94,6 +94,7 @@
44
45 * Hardware Features
46 * Displays
47+ * [Quantum Painter](quantum_painter.md)
48 * [HD44780 LCD Driver](feature_hd44780.md)
49 * [ST7565 LCD Driver](feature_st7565.md)
50 * [OLED Driver](feature_oled_driver.md)
51diff --git a/docs/cli_commands.md b/docs/cli_commands.md
52index 463abcef12f255edb8533a18c73d654cfae4f12e..a380d3eb2f1d2507f0f280bf353ed1c7d1f87dd1 100644
53--- a/docs/cli_commands.md
54+++ b/docs/cli_commands.md
55@@ -515,3 +515,15 @@ Run single test:
56
57 qmk pytest -t qmk.tests.test_cli_commands.test_c2json
58 qmk pytest -t qmk.tests.test_qmk_path
59+
60+## `qmk painter-convert-graphics`
61+
62+This command converts images to a format usable by QMK, i.e. the QGF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.
63+
64+## `qmk painter-make-font-image`
65+
66+This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.
67+
68+## `qmk painter-convert-font-image`
69+
70+This command converts an intermediate font image to the QFF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.
71diff --git a/docs/quantum_painter.md b/docs/quantum_painter.md
72new file mode 100644
73index 0000000000000000000000000000000000000000..a3705b62ce7c3066a2ffb17019b1e8e663db2528
74--- /dev/null
75+++ b/docs/quantum_painter.md
76@@ -0,0 +1,705 @@
77+# Quantum Painter :id=quantum-painter
78+
79+Quantum Painter is the standardised API for graphical displays. It currently includes support for basic drawing primitives, as well as custom images, animations, and fonts.
80+
81+Due to the complexity, there is no support for Quantum Painter on AVR-based boards.
82+
83+To enable overall Quantum Painter to be built into your firmware, add the following to `rules.mk`:
84+
85+```make
86+QUANTUM_PAINTER_ENABLE = yes
87+QUANTUM_PAINTER_DRIVERS = ......
88+```
89+
90+You will also likely need to select an appropriate driver in `rules.mk`, which is listed below.
91+
92+!> Quantum Painter is not currently integrated with system-level operations such as disabling displays after a configurable timeout, or when the keyboard goes into suspend. Users will need to handle this manually at the current time.
93+
94+The QMK CLI can be used to convert from normal images such as PNG files or animated GIFs, as well as fonts from TTF files.
95+
96+Hardware supported:
97+
98+| Display Panel | Panel Type | Size | Comms Transport | Driver |
99+|---------------|--------------------|------------------|-----------------|-----------------------------------------|
100+| GC9A01 | RGB LCD (circular) | 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = gc9a01_spi` |
101+| ILI9163 | RGB LCD | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9163_spi` |
102+| ILI9341 | RGB LCD | 240x320 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9341_spi` |
103+| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ssd1351_spi` |
104+| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = st7789_spi` |
105+
106+## Quantum Painter Configuration :id=quantum-painter-config
107+
108+| Option | Default | Purpose |
109+|-----------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
110+| `QUANTUM_PAINTER_NUM_IMAGES` | `8` | The maximum number of images/animations that can be loaded at any one time. |
111+| `QUANTUM_PAINTER_NUM_FONTS` | `4` | The maximum number of fonts that can be loaded at any one time. |
112+| `QUANTUM_PAINTER_CONCURRENT_ANIMATIONS` | `4` | The maximum number of animations that can be executed at the same time. |
113+| `QUANTUM_PAINTER_LOAD_FONTS_TO_RAM` | `FALSE` | Whether or not fonts should be loaded to RAM. Relevant for fonts stored in off-chip persistent storage, such as external flash. |
114+| `QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE` | `32` | The limit of the amount of pixel data that can be transmitted in one transaction to the display. Higher values require more RAM on the MCU. |
115+| `QUANTUM_PAINTER_SUPPORTS_256_PALETTE` | `FALSE` | If 256-color palettes are supported. Requires significantly more RAM on the MCU. |
116+| `QUANTUM_PAINTER_DEBUG` | _unset_ | Prints out significant amounts of debugging information to CONSOLE output. Significant performance degradation, use only for debugging. |
117+
118+Drivers have their own set of configurable options, and are described in their respective sections.
119+
120+## Quantum Painter CLI Commands :id=quantum-painter-cli
121+
122+### `qmk painter-convert-graphics`
123+
124+This command converts images to a format usable by QMK, i.e. the QGF File Format.
125+
126+**Usage**:
127+
128+```
129+usage: qmk painter-convert-graphics [-h] [-d] [-r] -f FORMAT [-o OUTPUT] -i INPUT [-v]
130+
131+optional arguments:
132+ -h, --help show this help message and exit
133+ -d, --no-deltas Disables the use of delta frames when encoding animations.
134+ -r, --no-rle Disables the use of RLE when encoding images.
135+ -f FORMAT, --format FORMAT
136+ Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
137+ -o OUTPUT, --output OUTPUT
138+ Specify output directory. Defaults to same directory as input.
139+ -i INPUT, --input INPUT
140+ Specify input graphic file.
141+ -v, --verbose Turns on verbose output.
142+```
143+
144+The `INPUT` argument can be any image file loadable by Python's Pillow module. Common formats include PNG, or Animated GIF.
145+
146+The `OUTPUT` argument needs to be a directory, and will default to the same directory as the input argument.
147+
148+The `FORMAT` argument can be any of the following:
149+
150+| Format | Meaning |
151+|-----------|-----------------------------------------------------------------------|
152+| `pal256` | 256-color palette (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
153+| `pal16` | 16-color palette |
154+| `pal4` | 4-color palette |
155+| `pal2` | 2-color palette |
156+| `mono256` | 256-shade grayscale (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
157+| `mono16` | 16-shade grayscale |
158+| `mono4` | 4-shade grayscale |
159+| `mono2` | 2-shade grayscale |
160+
161+**Examples**:
162+
163+```
164+$ cd /home/qmk/qmk_firmware/keyboards/my_keeb
165+$ qmk painter-convert-graphics -f mono16 -i my_image.gif -o ./generated/
166+Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.h...
167+Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.c...
168+```
169+
170+### `qmk painter-make-font-image`
171+
172+This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format.
173+
174+**Usage**:
175+
176diff --git a/docs/quantum_painter_qff.md b/docs/quantum_painter_qff.md
177new file mode 100644
178index 0000000000000000000000000000000000000000..f62d59bdcb1ba98f6f7d4a59291e4e279c73f88c
179--- /dev/null
180+++ b/docs/quantum_painter_qff.md
181@@ -0,0 +1,103 @@
182+# QMK Font Format :id=qmk-font-format
183+
184+QMK uses a font format _("Quantum Font Format" - QFF)_ specifically for resource-constrained systems.
185+
186+This format is capable of encoding 1-, 2-, 4-, and 8-bit-per-pixel greyscale- and palette-based images into a font. It also includes RLE for pixel data for some basic compression.
187+
188+All integer values are in little-endian format.
189+
190diff --git a/docs/quantum_painter_qgf.md b/docs/quantum_painter_qgf.md
191new file mode 100644
192index 0000000000000000000000000000000000000000..caf6731e652b9773f13fa5782f4fbb46f79f1f0b
193--- /dev/null
194+++ b/docs/quantum_painter_qgf.md
195@@ -0,0 +1,178 @@
196+# QMK Graphics Format :id=qmk-graphics-format
197+
198+QMK uses a graphics format _("Quantum Graphics Format" - QGF)_ specifically for resource-constrained systems.
199+
200+This format is capable of encoding 1-, 2-, 4-, and 8-bit-per-pixel greyscale- and palette-based images. It also includes RLE for pixel data for some basic compression.
201+
202+All integer values are in little-endian format.
203+
204diff --git a/docs/quantum_painter_rle.md b/docs/quantum_painter_rle.md
205new file mode 100644
206index 0000000000000000000000000000000000000000..dcb9a1e1a7f49f0b7d0f04ef68583aa77c17d5da
207--- /dev/null
208+++ b/docs/quantum_painter_rle.md
209@@ -0,0 +1,29 @@
210+# QMK QGF/QFF RLE data schema :id=qmk-qp-rle-schema
211+
212+There are two "modes" to the RLE algorithm used in both [QGF](quantum_painter_qgf.md)/[QFF](quantum_painter_qff.md):
213+
214+* Non-repeating sections of octets, with associated length of up to `128` octets
215+ * `length` = `marker - 128`
216+ * A corresponding `length` number of octets follow directly after the marker octet
217+* Repeated octet with associated length, with associated length of up to `128`
218+ * `length` = `marker`
219+ * A single octet follows the marker that should be repeated `length` times.
220+
221+Decoder pseudocode:
222+```
223+while !EOF
224+ marker = READ_OCTET()
225+
226+ if marker >= 128
227+ length = marker - 128
228+ for i = 0 ... length-1
229+ c = READ_OCTET()
230+ WRITE_OCTET(c)
231+
232+ else
233+ length = marker
234+ c = READ_OCTET()
235+ for i = 0 ... length-1
236+ WRITE_OCTET(c)
237+
238+```
239diff --git a/drivers/painter/comms/qp_comms_spi.c b/drivers/painter/comms/qp_comms_spi.c
240new file mode 100644
241index 0000000000000000000000000000000000000000..e644ba9f84499ab41da14d0107abfec90ec2d50e
242--- /dev/null
243+++ b/drivers/painter/comms/qp_comms_spi.c
244@@ -0,0 +1,137 @@
245+// Copyright 2021 Nick Brassel (@tzarc)
246+// SPDX-License-Identifier: GPL-2.0-or-later
247+
248+#ifdef QUANTUM_PAINTER_SPI_ENABLE
249+
250+# include "spi_master.h"
251+# include "qp_comms_spi.h"
252+
253+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
254+// Base SPI support
255+
256+bool qp_comms_spi_init(painter_device_t device) {
257+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
258+ struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
259+
260+ // Initialize the SPI peripheral
261+ spi_init();
262+
263+ // Set up CS as output high
264+ setPinOutput(comms_config->chip_select_pin);
265+ writePinHigh(comms_config->chip_select_pin);
266+
267+ return true;
268+}
269+
270+bool qp_comms_spi_start(painter_device_t device) {
271+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
272+ struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
273+
274+ return spi_start(comms_config->chip_select_pin, comms_config->lsb_first, comms_config->mode, comms_config->divisor);
275+}
276+
277+uint32_t qp_comms_spi_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
278+ uint32_t bytes_remaining = byte_count;
279+ const uint8_t *p = (const uint8_t *)data;
280+ while (bytes_remaining > 0) {
281+ uint32_t bytes_this_loop = bytes_remaining < 1024 ? bytes_remaining : 1024;
282+ spi_transmit(p, bytes_this_loop);
283+ p += bytes_this_loop;
284+ bytes_remaining -= bytes_this_loop;
285+ }
286+
287+ return byte_count - bytes_remaining;
288+}
289+
290+void qp_comms_spi_stop(painter_device_t device) {
291+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
292+ struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
293+ spi_stop();
294+ writePinHigh(comms_config->chip_select_pin);
295+}
296+
297+const struct painter_comms_vtable_t spi_comms_vtable = {
298+ .comms_init = qp_comms_spi_init,
299+ .comms_start = qp_comms_spi_start,
300+ .comms_send = qp_comms_spi_send_data,
301+ .comms_stop = qp_comms_spi_stop,
302+};
303+
304+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
305+// SPI with D/C and RST pins
306+
307+# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
308+
309+bool qp_comms_spi_dc_reset_init(painter_device_t device) {
310+ if (!qp_comms_spi_init(device)) {
311+ return false;
312+ }
313+
314+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
315+ struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
316+
317+ // Set up D/C as output low, if specified
318+ if (comms_config->dc_pin != NO_PIN) {
319+ setPinOutput(comms_config->dc_pin);
320+ writePinLow(comms_config->dc_pin);
321+ }
322+
323+ // Set up RST as output, if specified, performing a reset in the process
324+ if (comms_config->reset_pin != NO_PIN) {
325+ setPinOutput(comms_config->reset_pin);
326+ writePinLow(comms_config->reset_pin);
327+ wait_ms(20);
328+ writePinHigh(comms_config->reset_pin);
329+ wait_ms(20);
330+ }
331+
332+ return true;
333+}
334+
335+uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
336+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
337+ struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
338+ writePinHigh(comms_config->dc_pin);
339+ return qp_comms_spi_send_data(device, data, byte_count);
340+}
341+
342+void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd) {
343+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
344diff --git a/drivers/painter/comms/qp_comms_spi.h b/drivers/painter/comms/qp_comms_spi.h
345new file mode 100644
346index 0000000000000000000000000000000000000000..99899873272ca30a56546444fcc47f1b79212e6d
347--- /dev/null
348+++ b/drivers/painter/comms/qp_comms_spi.h
349@@ -0,0 +1,51 @@
350+// Copyright 2021 Nick Brassel (@tzarc)
351+// SPDX-License-Identifier: GPL-2.0-or-later
352+
353+#pragma once
354+
355+#ifdef QUANTUM_PAINTER_SPI_ENABLE
356+
357+# include <stdint.h>
358+
359+# include "gpio.h"
360+# include "qp_internal.h"
361+
362+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
363+// Base SPI support
364+
365+struct qp_comms_spi_config_t {
366+ pin_t chip_select_pin;
367+ uint16_t divisor;
368+ bool lsb_first;
369+ int8_t mode;
370+};
371+
372+bool qp_comms_spi_init(painter_device_t device);
373+bool qp_comms_spi_start(painter_device_t device);
374+uint32_t qp_comms_spi_send_data(painter_device_t device, const void* data, uint32_t byte_count);
375+void qp_comms_spi_stop(painter_device_t device);
376+
377+extern const struct painter_comms_vtable_t spi_comms_vtable;
378+
379+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
380+// SPI with D/C and RST pins
381+
382+# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
383+
384+struct qp_comms_spi_dc_reset_config_t {
385+ struct qp_comms_spi_config_t spi_config;
386+ pin_t dc_pin;
387+ pin_t reset_pin;
388+};
389+
390+void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd);
391+uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void* data, uint32_t byte_count);
392+void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len);
393+
394+extern const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable;
395+
396+# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
397+
398+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
399+
400+#endif // QUANTUM_PAINTER_SPI_ENABLE
401diff --git a/drivers/painter/gc9a01/qp_gc9a01.c b/drivers/painter/gc9a01/qp_gc9a01.c
402new file mode 100644
403index 0000000000000000000000000000000000000000..ad76d58b078b00079b25c3ce33786d888d72a735
404--- /dev/null
405+++ b/drivers/painter/gc9a01/qp_gc9a01.c
406@@ -0,0 +1,150 @@
407+// Copyright 2021 Paul Cotter (@gr1mr3aver)
408+// SPDX-License-Identifier: GPL-2.0-or-later
409+
410+#include <wait.h>
411+#include "qp_internal.h"
412+#include "qp_comms.h"
413+#include "qp_gc9a01.h"
414+#include "qp_gc9a01_opcodes.h"
415+#include "qp_tft_panel.h"
416+
417+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
418+// Driver storage
419+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
420+
421+tft_panel_dc_reset_painter_device_t gc9a01_drivers[GC9A01_NUM_DEVICES] = {0};
422+
423+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
424+// Initialization
425+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
426+bool qp_gc9a01_init(painter_device_t device, painter_rotation_t rotation) {
427+ // A lot of these "unknown" opcodes are sourced from other OSS projects and are seemingly required for this display to function.
428+ // clang-format off
429+ const uint8_t gc9a01_init_sequence[] = {
430+ // Command, Delay, N, Data[N]
431+ GC9A01_SET_INTER_REG_ENABLE2, 0, 0,
432+ 0xEB, 0, 1, 0x14,
433+ GC9A01_SET_INTER_REG_ENABLE1, 0, 0,
434+ GC9A01_SET_INTER_REG_ENABLE2, 0, 0,
435+ 0xEB, 0, 1, 0x14,
436+ 0x84, 0, 1, 0x40,
437+ 0x85, 0, 1, 0xFF,
438+ 0x86, 0, 1, 0xFF,
439+ 0x87, 0, 1, 0xFF,
440+ 0x88, 0, 1, 0x0A,
441+ 0x89, 0, 1, 0x21,
442+ 0x8a, 0, 1, 0x00,
443+ 0x8b, 0, 1, 0x80,
444+ 0x8c, 0, 1, 0x01,
445+ 0x8d, 0, 1, 0x01,
446+ 0x8e, 0, 1, 0xFF,
447+ 0x8f, 0, 1, 0xFF,
448+ GC9A01_SET_FUNCTION_CTL, 0, 2, 0x00, 0x20,
449+ GC9A01_SET_PIX_FMT, 0, 1, 0x55,
450+ 0x90, 0, 4, 0x08, 0x08, 0x08, 0x08,
451+ 0xBD, 0, 1, 0x06,
452+ 0xBC, 0, 1, 0x00,
453+ 0xFF, 0, 3, 0x60, 0x01, 0x04,
454+ GC9A01_SET_POWER_CTL_2, 0, 1, 0x13,
455+ GC9A01_SET_POWER_CTL_3, 0, 1, 0x13,
456+ GC9A01_SET_POWER_CTL_4, 0, 1, 0x22,
457+ 0xBE, 0, 1, 0x11,
458+ 0xE1, 0, 2, 0x10, 0x0E,
459+ 0xDF, 0, 3, 0x21, 0x0C, 0x02,
460+ GC9A01_SET_GAMMA1, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A,
461+ GC9A01_SET_GAMMA2, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F,
462+ GC9A01_SET_GAMMA3, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A,
463+ GC9A01_SET_GAMMA4, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F,
464+ 0xED, 0, 2, 0x1B, 0x0B,
465+ 0xAE, 0, 1, 0x77,
466+ 0xCD, 0, 1, 0x63,
467+ 0x70, 0, 9, 0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03,
468+ GC9A01_SET_FRAME_RATE, 0, 1, 0x34,
469+ 0x62, 0, 12, 0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70,
470+ 0x63, 0, 12, 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70,
471+ 0x64, 0, 7, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07,
472+ 0x66, 0, 10, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00,
473+ 0x67, 0, 10, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98,
474+ 0x74, 0, 7, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00,
475+ 0x98, 0, 2, 0x3E, 0x07,
476+ GC9A01_CMD_TEARING_OFF, 0, 0,
477+ GC9A01_CMD_INVERT_OFF, 0, 0,
478+ GC9A01_CMD_SLEEP_OFF, 120, 0,
479+ GC9A01_CMD_DISPLAY_ON, 20, 0
480+ };
481+ // clang-format on
482+
483+ // clang-format on
484+ qp_comms_bulk_command_sequence(device, gc9a01_init_sequence, sizeof(gc9a01_init_sequence));
485+
486+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
487+ const uint8_t madctl[] = {
488+ [QP_ROTATION_0] = GC9A01_MADCTL_BGR,
489+ [QP_ROTATION_90] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MV,
490+ [QP_ROTATION_180] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MY,
491+ [QP_ROTATION_270] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MV | GC9A01_MADCTL_MY,
492+ };
493+ qp_comms_command_databyte(device, GC9A01_SET_MEM_ACS_CTL, madctl[rotation]);
494+
495+ return true;
496+}
497+
498+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
499+// Driver vtable
500+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
501+
502+const struct tft_panel_dc_reset_painter_driver_vtable_t gc9a01_driver_vtable = {
503+ .base =
504+ {
505+ .init = qp_gc9a01_init,
506diff --git a/drivers/painter/gc9a01/qp_gc9a01.h b/drivers/painter/gc9a01/qp_gc9a01.h
507new file mode 100644
508index 0000000000000000000000000000000000000000..e2b1939564052b3745f9ababa5590f858e9e27c3
509--- /dev/null
510+++ b/drivers/painter/gc9a01/qp_gc9a01.h
511@@ -0,0 +1,37 @@
512+// Copyright 2021 Paul Cotter (@gr1mr3aver)
513+// SPDX-License-Identifier: GPL-2.0-or-later
514+
515+#pragma once
516+
517+#include "gpio.h"
518+#include "qp_internal.h"
519+
520+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
521+// Quantum Painter GC9A01 configurables (add to your keyboard's config.h)
522+
523+#ifndef GC9A01_NUM_DEVICES
524+/**
525+ * @def This controls the maximum number of GC9A01 devices that Quantum Painter can communicate with at any one time.
526+ * Increasing this number allows for multiple displays to be used.
527+ */
528+# define GC9A01_NUM_DEVICES 1
529+#endif
530+
531+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
532+// Quantum Painter GC9A01 device factories
533+
534+#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE
535+/**
536+ * Factory method for an GC9A01 SPI LCD device.
537+ *
538+ * @param panel_width[in] the width of the display panel
539+ * @param panel_height[in] the height of the display panel
540+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
541+ * @param dc_pin[in] the GPIO pin used for D/C control
542+ * @param reset_pin[in] the GPIO pin used for RST
543+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
544+ * @param spi_mode[in] the SPI mode to use when communicating with the display
545+ * @return the device handle used with all drawing routines in Quantum Painter
546+ */
547+painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
548+#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE
549diff --git a/drivers/painter/gc9a01/qp_gc9a01_opcodes.h b/drivers/painter/gc9a01/qp_gc9a01_opcodes.h
550new file mode 100644
551index 0000000000000000000000000000000000000000..6ff4efe7a8ac4f3a0778460e6dba815e3a5f3f04
552--- /dev/null
553+++ b/drivers/painter/gc9a01/qp_gc9a01_opcodes.h
554@@ -0,0 +1,78 @@
555+// Copyright 2021 Paul Cotter (@gr1mr3aver)
556+// SPDX-License-Identifier: GPL-2.0-or-later
557+
558+#pragma once
559+
560+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
561+// Quantum Painter GC9A01 command opcodes
562+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
563+
564+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
565+// Level 1 command opcodes
566+
567+#define GC9A01_GET_ID_INFO 0x04 // Get ID information
568+#define GC9A01_GET_STATUS 0x09 // Get status
569+#define GC9A01_CMD_SLEEP_ON 0x10 // Enter sleep mode
570+#define GC9A01_CMD_SLEEP_OFF 0x11 // Exit sleep mode
571+#define GC9A01_CMD_PARTIAL_ON 0x12 // Enter partial mode
572+#define GC9A01_CMD_PARTIAL_OFF 0x13 // Exit partial mode
573+#define GC9A01_CMD_INVERT_ON 0x20 // Enter inverted mode
574+#define GC9A01_CMD_INVERT_OFF 0x21 // Exit inverted mode
575+#define GC9A01_CMD_DISPLAY_OFF 0x28 // Disable display
576+#define GC9A01_CMD_DISPLAY_ON 0x29 // Enable display
577+#define GC9A01_SET_COL_ADDR 0x2A // Set column address
578+#define GC9A01_SET_PAGE_ADDR 0x2B // Set page address
579+#define GC9A01_SET_MEM 0x2C // Set memory
580+#define GC9A01_SET_PARTIAL_AREA 0x30 // Set partial area
581+#define GC9A01_SET_VSCROLL 0x33 // Set vertical scroll def
582+#define GC9A01_CMD_TEARING_ON 0x34 // Tearing line enabled
583+#define GC9A01_CMD_TEARING_OFF 0x35 // Tearing line disabled
584+#define GC9A01_SET_MEM_ACS_CTL 0x36 // Set mem access ctl
585+#define GC9A01_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr
586+#define GC9A01_CMD_IDLE_OFF 0x38 // Exit idle mode
587+#define GC9A01_CMD_IDLE_ON 0x39 // Enter idle mode
588+#define GC9A01_SET_PIX_FMT 0x3A // Set pixel format
589+#define GC9A01_SET_MEM_CONT 0x3C // Set memory continue
590+#define GC9A01_SET_TEAR_SCANLINE 0x44 // Set tearing scanline
591+#define GC9A01_GET_TEAR_SCANLINE 0x45 // Get tearing scanline
592+#define GC9A01_SET_BRIGHTNESS 0x51 // Set brightness
593+#define GC9A01_SET_DISPLAY_CTL 0x53 // Set display ctl
594+#define GC9A01_GET_ID1 0xDA // Get ID1
595+#define GC9A01_GET_ID2 0xDB // Get ID2
596+#define GC9A01_GET_ID3 0xDC // Get ID3
597+
598+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
599+// Level 2 command opcodes
600+
601+#define GC9A01_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl
602+#define GC9A01_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl
603+#define GC9A01_SET_FUNCTION_CTL 0xB6 // Set function ctl
604+#define GC9A01_SET_TEARING_EFFECT 0xBA // Set backlight ctl 3
605+#define GC9A01_SET_IF_CTL 0xF6 // Set interface control
606+
607+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
608+// Level 3 command opcodes
609+
610+#define GC9A01_SET_FRAME_RATE 0xE8 // Set frame rate
611+#define GC9A01_SET_SPI_2DATA 0xE9 // Set frame rate
612+#define GC9A01_SET_POWER_CTL_1 0xC1 // Set power ctl 1
613+#define GC9A01_SET_POWER_CTL_2 0xC3 // Set power ctl 2
614+#define GC9A01_SET_POWER_CTL_3 0xC4 // Set power ctl 3
615+#define GC9A01_SET_POWER_CTL_4 0xC9 // Set power ctl 4
616+#define GC9A01_SET_POWER_CTL_7 0xA7 // Set power ctl 7
617+#define GC9A01_SET_INTER_REG_ENABLE1 0xFE // Enable Inter Register 1
618+#define GC9A01_SET_INTER_REG_ENABLE2 0xEF // Enable Inter Register 2
619+#define GC9A01_SET_GAMMA1 0xF0 //
620+#define GC9A01_SET_GAMMA2 0xF1
621+#define GC9A01_SET_GAMMA3 0xF2
622+#define GC9A01_SET_GAMMA4 0xF3
623+
624+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
625+// MADCTL Flags
626+#define GC9A01_MADCTL_MY 0b10000000
627+#define GC9A01_MADCTL_MX 0b01000000
628+#define GC9A01_MADCTL_MV 0b00100000
629+#define GC9A01_MADCTL_ML 0b00010000
630+#define GC9A01_MADCTL_RGB 0b00000000
631+#define GC9A01_MADCTL_BGR 0b00001000
632+#define GC9A01_MADCTL_MH 0b00000100
633diff --git a/drivers/painter/ili9xxx/qp_ili9163.c b/drivers/painter/ili9xxx/qp_ili9163.c
634new file mode 100644
635index 0000000000000000000000000000000000000000..beaac0fbb528994099d951bc8c60fca3a449e200
636--- /dev/null
637+++ b/drivers/painter/ili9xxx/qp_ili9163.c
638@@ -0,0 +1,121 @@
639+// Copyright 2021 Nick Brassel (@tzarc)
640+// SPDX-License-Identifier: GPL-2.0-or-later
641+
642+#include "qp_internal.h"
643+#include "qp_comms.h"
644+#include "qp_ili9163.h"
645+#include "qp_ili9xxx_opcodes.h"
646+#include "qp_tft_panel.h"
647+
648+#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
649+# include "qp_comms_spi.h"
650+#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
651+
652+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
653+// Common
654+
655+// Driver storage
656+tft_panel_dc_reset_painter_device_t ili9163_drivers[ILI9163_NUM_DEVICES] = {0};
657+
658+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
659+// Initialization
660+
661+bool qp_ili9163_init(painter_device_t device, painter_rotation_t rotation) {
662+ // clang-format off
663+ const uint8_t ili9163_init_sequence[] = {
664+ // Command, Delay, N, Data[N]
665+ ILI9XXX_CMD_RESET, 120, 0,
666+ ILI9XXX_CMD_SLEEP_OFF, 5, 0,
667+ ILI9XXX_SET_PIX_FMT, 0, 1, 0x55,
668+ ILI9XXX_SET_GAMMA, 0, 1, 0x04,
669+ ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x01,
670+ ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0xFF, 0x06,
671+ ILI9XXX_SET_PGAMMA, 0, 15, 0x36, 0x29, 0x12, 0x22, 0x1C, 0x15, 0x42, 0xB7, 0x2F, 0x13, 0x12, 0x0A, 0x11, 0x0B, 0x06,
672+ ILI9XXX_SET_NGAMMA, 0, 15, 0x09, 0x16, 0x2D, 0x0D, 0x13, 0x15, 0x40, 0x48, 0x53, 0x0C, 0x1D, 0x25, 0x2E, 0x34, 0x39,
673+ ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x08, 0x02,
674+ ILI9XXX_SET_POWER_CTL_1, 0, 2, 0x0A, 0x02,
675+ ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x02,
676+ ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x50, 0x63,
677+ ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0x00,
678+ ILI9XXX_CMD_PARTIAL_OFF, 0, 0,
679+ ILI9XXX_CMD_DISPLAY_ON, 20, 0
680+ };
681+ // clang-format on
682+ qp_comms_bulk_command_sequence(device, ili9163_init_sequence, sizeof(ili9163_init_sequence));
683+
684+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
685+ const uint8_t madctl[] = {
686+ [QP_ROTATION_0] = ILI9XXX_MADCTL_BGR,
687+ [QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV,
688+ [QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY,
689+ [QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY,
690+ };
691+ qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]);
692+
693+ return true;
694+}
695+
696+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
697+// Driver vtable
698+
699+const struct tft_panel_dc_reset_painter_driver_vtable_t ili9163_driver_vtable = {
700+ .base =
701+ {
702+ .init = qp_ili9163_init,
703+ .power = qp_tft_panel_power,
704+ .clear = qp_tft_panel_clear,
705+ .flush = qp_tft_panel_flush,
706+ .pixdata = qp_tft_panel_pixdata,
707+ .viewport = qp_tft_panel_viewport,
708+ .palette_convert = qp_tft_panel_palette_convert,
709+ .append_pixels = qp_tft_panel_append_pixels,
710+ },
711+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
712+ .num_window_bytes = 2,
713+ .swap_window_coords = false,
714+ .opcodes =
715+ {
716+ .display_on = ILI9XXX_CMD_DISPLAY_ON,
717+ .display_off = ILI9XXX_CMD_DISPLAY_OFF,
718+ .set_column_address = ILI9XXX_SET_COL_ADDR,
719+ .set_row_address = ILI9XXX_SET_PAGE_ADDR,
720+ .enable_writes = ILI9XXX_SET_MEM,
721+ },
722+};
723+
724+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
725+// SPI
726+
727+#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
728+
729+// Factory function for creating a handle to the ILI9163 device
730+painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
731+ for (uint32_t i = 0; i < ILI9163_NUM_DEVICES; ++i) {
732+ tft_panel_dc_reset_painter_device_t *driver = &ili9163_drivers[i];
733+ if (!driver->base.driver_vtable) {
734+ driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9163_driver_vtable;
735+ driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
736+ driver->base.panel_width = panel_width;
737+ driver->base.panel_height = panel_height;
738diff --git a/drivers/painter/ili9xxx/qp_ili9163.h b/drivers/painter/ili9xxx/qp_ili9163.h
739new file mode 100644
740index 0000000000000000000000000000000000000000..88d23629a92a854fb5e385b1b1531db38fcefd70
741--- /dev/null
742+++ b/drivers/painter/ili9xxx/qp_ili9163.h
743@@ -0,0 +1,37 @@
744+// Copyright 2021 Nick Brassel (@tzarc)
745+// SPDX-License-Identifier: GPL-2.0-or-later
746+
747+#pragma once
748+
749+#include "gpio.h"
750+#include "qp_internal.h"
751+
752+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
753+// Quantum Painter ILI9163 configurables (add to your keyboard's config.h)
754+
755+#ifndef ILI9163_NUM_DEVICES
756+/**
757+ * @def This controls the maximum number of ILI9163 devices that Quantum Painter can communicate with at any one time.
758+ * Increasing this number allows for multiple displays to be used.
759+ */
760+# define ILI9163_NUM_DEVICES 1
761+#endif
762+
763+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
764+// Quantum Painter ILI9163 device factories
765+
766+#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
767+/**
768+ * Factory method for an ILI9163 SPI LCD device.
769+ *
770+ * @param panel_width[in] the width of the display panel
771+ * @param panel_height[in] the height of the display panel
772+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
773+ * @param dc_pin[in] the GPIO pin used for D/C control
774+ * @param reset_pin[in] the GPIO pin used for RST
775+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
776+ * @param spi_mode[in] the SPI mode to use when communicating with the display
777+ * @return the device handle used with all drawing routines in Quantum Painter
778+ */
779+painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
780+#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
781diff --git a/drivers/painter/ili9xxx/qp_ili9341.c b/drivers/painter/ili9xxx/qp_ili9341.c
782new file mode 100644
783index 0000000000000000000000000000000000000000..1f41dcfc0bfb40c4a9213a190bd640c1eccc53de
784--- /dev/null
785+++ b/drivers/painter/ili9xxx/qp_ili9341.c
786@@ -0,0 +1,128 @@
787+// Copyright 2021 Nick Brassel (@tzarc)
788+// SPDX-License-Identifier: GPL-2.0-or-later
789+
790+#include "qp_internal.h"
791+#include "qp_comms.h"
792+#include "qp_ili9341.h"
793+#include "qp_ili9xxx_opcodes.h"
794+#include "qp_tft_panel.h"
795+
796+#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
797+# include <qp_comms_spi.h>
798+#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
799+
800+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
801+// Common
802+
803+// Driver storage
804+tft_panel_dc_reset_painter_device_t ili9341_drivers[ILI9341_NUM_DEVICES] = {0};
805+
806+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
807+// Initialization
808+
809+bool qp_ili9341_init(painter_device_t device, painter_rotation_t rotation) {
810+ // clang-format off
811+ const uint8_t ili9341_init_sequence[] = {
812+ // Command, Delay, N, Data[N]
813+ ILI9XXX_CMD_RESET, 120, 0,
814+ ILI9XXX_CMD_SLEEP_OFF, 5, 0,
815+ ILI9XXX_POWER_CTL_A, 0, 5, 0x39, 0x2C, 0x00, 0x34, 0x02,
816+ ILI9XXX_POWER_CTL_B, 0, 3, 0x00, 0xD9, 0x30,
817+ ILI9XXX_POWER_ON_SEQ_CTL, 0, 4, 0x64, 0x03, 0x12, 0x81,
818+ ILI9XXX_SET_PUMP_RATIO_CTL, 0, 1, 0x20,
819+ ILI9XXX_SET_POWER_CTL_1, 0, 1, 0x26,
820+ ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x11,
821+ ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x35, 0x3E,
822+ ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0xBE,
823+ ILI9XXX_DRV_TIMING_CTL_A, 0, 3, 0x85, 0x10, 0x7A,
824+ ILI9XXX_DRV_TIMING_CTL_B, 0, 2, 0x00, 0x00,
825+ ILI9XXX_SET_BRIGHTNESS, 0, 1, 0xFF,
826+ ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x00,
827+ ILI9XXX_SET_GAMMA, 0, 1, 0x01,
828+ ILI9XXX_SET_PGAMMA, 0, 15, 0x0F, 0x29, 0x24, 0x0C, 0x0E, 0x09, 0x4E, 0x78, 0x3C, 0x09, 0x13, 0x05, 0x17, 0x11, 0x00,
829+ ILI9XXX_SET_NGAMMA, 0, 15, 0x00, 0x16, 0x1B, 0x04, 0x11, 0x07, 0x31, 0x33, 0x42, 0x05, 0x0C, 0x0A, 0x28, 0x2F, 0x0F,
830+ ILI9XXX_SET_PIX_FMT, 0, 1, 0x05,
831+ ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x00, 0x1B,
832+ ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0x0A, 0xA2,
833+ ILI9XXX_CMD_PARTIAL_OFF, 0, 0,
834+ ILI9XXX_CMD_DISPLAY_ON, 20, 0
835+ };
836+ // clang-format on
837+ qp_comms_bulk_command_sequence(device, ili9341_init_sequence, sizeof(ili9341_init_sequence));
838+
839+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
840+ const uint8_t madctl[] = {
841+ [QP_ROTATION_0] = ILI9XXX_MADCTL_BGR,
842+ [QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV,
843+ [QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY,
844+ [QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY,
845+ };
846+ qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]);
847+
848+ return true;
849+}
850+
851+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
852+// Driver vtable
853+
854+const struct tft_panel_dc_reset_painter_driver_vtable_t ili9341_driver_vtable = {
855+ .base =
856+ {
857+ .init = qp_ili9341_init,
858+ .power = qp_tft_panel_power,
859+ .clear = qp_tft_panel_clear,
860+ .flush = qp_tft_panel_flush,
861+ .pixdata = qp_tft_panel_pixdata,
862+ .viewport = qp_tft_panel_viewport,
863+ .palette_convert = qp_tft_panel_palette_convert,
864+ .append_pixels = qp_tft_panel_append_pixels,
865+ },
866+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
867+ .num_window_bytes = 2,
868+ .swap_window_coords = false,
869+ .opcodes =
870+ {
871+ .display_on = ILI9XXX_CMD_DISPLAY_ON,
872+ .display_off = ILI9XXX_CMD_DISPLAY_OFF,
873+ .set_column_address = ILI9XXX_SET_COL_ADDR,
874+ .set_row_address = ILI9XXX_SET_PAGE_ADDR,
875+ .enable_writes = ILI9XXX_SET_MEM,
876+ },
877+};
878+
879+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
880+// SPI
881+
882+#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
883+
884+// Factory function for creating a handle to the ILI9341 device
885+painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
886diff --git a/drivers/painter/ili9xxx/qp_ili9341.h b/drivers/painter/ili9xxx/qp_ili9341.h
887new file mode 100644
888index 0000000000000000000000000000000000000000..28b0152a84cb5a88c13886710f009614210a7819
889--- /dev/null
890+++ b/drivers/painter/ili9xxx/qp_ili9341.h
891@@ -0,0 +1,37 @@
892+// Copyright 2021 Nick Brassel (@tzarc)
893+// SPDX-License-Identifier: GPL-2.0-or-later
894+
895+#pragma once
896+
897+#include "gpio.h"
898+#include "qp_internal.h"
899+
900+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
901+// Quantum Painter ILI9341 configurables (add to your keyboard's config.h)
902+
903+#ifndef ILI9341_NUM_DEVICES
904+/**
905+ * @def This controls the maximum number of ILI9341 devices that Quantum Painter can communicate with at any one time.
906+ * Increasing this number allows for multiple displays to be used.
907+ */
908+# define ILI9341_NUM_DEVICES 1
909+#endif
910+
911+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
912+// Quantum Painter ILI9341 device factories
913+
914+#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
915+/**
916+ * Factory method for an ILI9341 SPI LCD device.
917+ *
918+ * @param panel_width[in] the width of the display panel
919+ * @param panel_height[in] the height of the display panel
920+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
921+ * @param dc_pin[in] the GPIO pin used for D/C control
922+ * @param reset_pin[in] the GPIO pin used for RST
923+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
924+ * @param spi_mode[in] the SPI mode to use when communicating with the display
925+ * @return the device handle used with all drawing routines in Quantum Painter
926+ */
927+painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
928+#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
929diff --git a/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h b/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h
930new file mode 100644
931index 0000000000000000000000000000000000000000..1fa395cb898960898762a60c7620d78c19331394
932--- /dev/null
933+++ b/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h
934@@ -0,0 +1,100 @@
935+// Copyright 2021 Nick Brassel (@tzarc)
936+// SPDX-License-Identifier: GPL-2.0-or-later
937+
938+#pragma once
939+
940+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
941+// Quantum Painter ILI9xxx command opcodes
942+#define ILI9XXX_CMD_NOP 0x00 // No operation
943+#define ILI9XXX_CMD_RESET 0x01 // Software reset
944+#define ILI9XXX_GET_ID_INFO 0x04 // Get ID information
945+#define ILI9XXX_GET_STATUS 0x09 // Get status
946+#define ILI9XXX_GET_PWR_MODE 0x0A // Get power mode
947+#define ILI9XXX_GET_MADCTL 0x0B // Get MADCTL
948+#define ILI9XXX_GET_PIX_FMT 0x0C // Get pixel format
949+#define ILI9XXX_GET_IMG_FMT 0x0D // Get image format
950+#define ILI9XXX_GET_SIG_MODE 0x0E // Get signal mode
951+#define ILI9XXX_GET_SELF_DIAG 0x0F // Get self-diagnostics
952+#define ILI9XXX_CMD_SLEEP_ON 0x10 // Enter sleep mode
953+#define ILI9XXX_CMD_SLEEP_OFF 0x11 // Exist sleep mode
954+#define ILI9XXX_CMD_PARTIAL_ON 0x12 // Enter partial mode
955+#define ILI9XXX_CMD_PARTIAL_OFF 0x13 // Exit partial mode
956+#define ILI9XXX_CMD_INVERT_ON 0x20 // Enter inverted mode
957+#define ILI9XXX_CMD_INVERT_OFF 0x21 // Exit inverted mode
958+#define ILI9XXX_SET_GAMMA 0x26 // Set gamma params
959+#define ILI9XXX_CMD_DISPLAY_OFF 0x28 // Disable display
960+#define ILI9XXX_CMD_DISPLAY_ON 0x29 // Enable display
961+#define ILI9XXX_SET_COL_ADDR 0x2A // Set column address
962+#define ILI9XXX_SET_PAGE_ADDR 0x2B // Set page address
963+#define ILI9XXX_SET_MEM 0x2C // Set memory
964+#define ILI9XXX_SET_COLOR 0x2D // Set color
965+#define ILI9XXX_GET_MEM 0x2E // Get memory
966+#define ILI9XXX_SET_PARTIAL_AREA 0x30 // Set partial area
967+#define ILI9XXX_SET_VSCROLL 0x33 // Set vertical scroll def
968+#define ILI9XXX_CMD_TEARING_ON 0x34 // Tearing line enabled
969+#define ILI9XXX_CMD_TEARING_OFF 0x35 // Tearing line disabled
970+#define ILI9XXX_SET_MEM_ACS_CTL 0x36 // Set mem access ctl
971+#define ILI9XXX_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr
972+#define ILI9XXX_CMD_IDLE_OFF 0x38 // Exit idle mode
973+#define ILI9XXX_CMD_IDLE_ON 0x39 // Enter idle mode
974+#define ILI9XXX_SET_PIX_FMT 0x3A // Set pixel format
975+#define ILI9XXX_SET_MEM_CONT 0x3C // Set memory continue
976+#define ILI9XXX_GET_MEM_CONT 0x3E // Get memory continue
977+#define ILI9XXX_SET_TEAR_SCANLINE 0x44 // Set tearing scanline
978+#define ILI9XXX_GET_TEAR_SCANLINE 0x45 // Get tearing scanline
979+#define ILI9XXX_SET_BRIGHTNESS 0x51 // Set brightness
980+#define ILI9XXX_GET_BRIGHTNESS 0x52 // Get brightness
981+#define ILI9XXX_SET_DISPLAY_CTL 0x53 // Set display ctl
982+#define ILI9XXX_GET_DISPLAY_CTL 0x54 // Get display ctl
983+#define ILI9XXX_SET_CABC 0x55 // Set CABC
984+#define ILI9XXX_GET_CABC 0x56 // Get CABC
985+#define ILI9XXX_SET_CABC_MIN 0x5E // Set CABC min
986+#define ILI9XXX_GET_CABC_MIN 0x5F // Set CABC max
987+#define ILI9XXX_GET_ID1 0xDA // Get ID1
988+#define ILI9XXX_GET_ID2 0xDB // Get ID2
989+#define ILI9XXX_GET_ID3 0xDC // Get ID3
990+#define ILI9XXX_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl
991+#define ILI9XXX_SET_FRAME_CTL_NORMAL 0xB1 // Set frame ctl (normal)
992+#define ILI9XXX_SET_FRAME_CTL_IDLE 0xB2 // Set frame ctl (idle)
993+#define ILI9XXX_SET_FRAME_CTL_PARTIAL 0xB3 // Set frame ctl (partial)
994+#define ILI9XXX_SET_INVERSION_CTL 0xB4 // Set inversion ctl
995+#define ILI9XXX_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl
996+#define ILI9XXX_SET_FUNCTION_CTL 0xB6 // Set function ctl
997+#define ILI9XXX_SET_ENTRY_MODE 0xB7 // Set entry mode
998+#define ILI9XXX_SET_LIGHT_CTL_1 0xB8 // Set backlight ctl 1
999+#define ILI9XXX_SET_LIGHT_CTL_2 0xB9 // Set backlight ctl 2
1000+#define ILI9XXX_SET_LIGHT_CTL_3 0xBA // Set backlight ctl 3
1001+#define ILI9XXX_SET_LIGHT_CTL_4 0xBB // Set backlight ctl 4
1002+#define ILI9XXX_SET_LIGHT_CTL_5 0xBC // Set backlight ctl 5
1003+#define ILI9XXX_SET_LIGHT_CTL_7 0xBE // Set backlight ctl 7
1004+#define ILI9XXX_SET_LIGHT_CTL_8 0xBF // Set backlight ctl 8
1005+#define ILI9XXX_SET_POWER_CTL_1 0xC0 // Set power ctl 1
1006+#define ILI9XXX_SET_POWER_CTL_2 0xC1 // Set power ctl 2
1007+#define ILI9XXX_SET_VCOM_CTL_1 0xC5 // Set VCOM ctl 1
1008+#define ILI9XXX_SET_VCOM_CTL_2 0xC7 // Set VCOM ctl 2
1009+#define ILI9XXX_POWER_CTL_A 0xCB // Set power control A
1010+#define ILI9XXX_POWER_CTL_B 0xCF // Set power control B
1011+#define ILI9XXX_DRV_TIMING_CTL_A 0xE8 // Set driver timing control A
1012+#define ILI9XXX_DRV_TIMING_CTL_B 0xEA // Set driver timing control B
1013+#define ILI9XXX_POWER_ON_SEQ_CTL 0xED // Set Power on sequence control
1014+#define ILI9XXX_SET_NVMEM 0xD0 // Set NVMEM data
1015+#define ILI9XXX_GET_NVMEM_KEY 0xD1 // Get NVMEM protect key
1016+#define ILI9XXX_GET_NVMEM_STATUS 0xD2 // Get NVMEM status
1017+#define ILI9XXX_GET_ID4 0xD3 // Get ID4
1018+#define ILI9XXX_SET_PGAMMA 0xE0 // Set positive gamma
1019+#define ILI9XXX_SET_NGAMMA 0xE1 // Set negative gamma
1020+#define ILI9XXX_SET_DGAMMA_CTL_1 0xE2 // Set digital gamma ctl 1
1021+#define ILI9XXX_SET_DGAMMA_CTL_2 0xE3 // Set digital gamma ctl 2
1022+#define ILI9XXX_ENABLE_3_GAMMA 0xF2 // Enable 3 gamma
1023+#define ILI9XXX_SET_IF_CTL 0xF6 // Set interface control
1024+#define ILI9XXX_SET_PUMP_RATIO_CTL 0xF7 // Set pump ratio control
1025+
1026+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1027+// MADCTL Flags
1028+#define ILI9XXX_MADCTL_MY 0b10000000
1029+#define ILI9XXX_MADCTL_MX 0b01000000
1030+#define ILI9XXX_MADCTL_MV 0b00100000
1031+#define ILI9XXX_MADCTL_ML 0b00010000
1032+#define ILI9XXX_MADCTL_RGB 0b00000000
1033+#define ILI9XXX_MADCTL_BGR 0b00001000
1034diff --git a/drivers/painter/ssd1351/qp_ssd1351.c b/drivers/painter/ssd1351/qp_ssd1351.c
1035new file mode 100644
1036index 0000000000000000000000000000000000000000..970e7e67f31b22fca509d3351f5cc53fc46fafc4
1037--- /dev/null
1038+++ b/drivers/painter/ssd1351/qp_ssd1351.c
1039@@ -0,0 +1,125 @@
1040+// Copyright 2021 Nick Brassel (@tzarc)
1041+// SPDX-License-Identifier: GPL-2.0-or-later
1042+
1043+#include "qp_internal.h"
1044+#include "qp_comms.h"
1045+#include "qp_ssd1351.h"
1046+#include "qp_ssd1351_opcodes.h"
1047+#include "qp_tft_panel.h"
1048+
1049+#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
1050+# include "qp_comms_spi.h"
1051+#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
1052+
1053+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1054+// Common
1055+
1056+// Driver storage
1057+tft_panel_dc_reset_painter_device_t ssd1351_drivers[SSD1351_NUM_DEVICES] = {0};
1058+
1059+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1060+// Initialization
1061+
1062+bool qp_ssd1351_init(painter_device_t device, painter_rotation_t rotation) {
1063+ tft_panel_dc_reset_painter_device_t *driver = (tft_panel_dc_reset_painter_device_t *)device;
1064+
1065+ // clang-format off
1066+ const uint8_t ssd1351_init_sequence[] = {
1067+ // Command, Delay, N, Data[N]
1068+ SSD1351_COMMANDLOCK, 5, 1, 0x12,
1069+ SSD1351_COMMANDLOCK, 5, 1, 0xB1,
1070+ SSD1351_DISPLAYOFF, 5, 0,
1071+ SSD1351_CLOCKDIV, 5, 1, 0xF1,
1072+ SSD1351_MUXRATIO, 5, 1, 0x7F,
1073+ SSD1351_DISPLAYOFFSET, 5, 1, 0x00,
1074+ SSD1351_SETGPIO, 5, 1, 0x00,
1075+ SSD1351_FUNCTIONSELECT, 5, 1, 0x01,
1076+ SSD1351_PRECHARGE, 5, 1, 0x32,
1077+ SSD1351_VCOMH, 5, 1, 0x05,
1078+ SSD1351_NORMALDISPLAY, 5, 0,
1079+ SSD1351_CONTRASTABC, 5, 3, 0xC8, 0x80, 0xC8,
1080+ SSD1351_CONTRASTMASTER, 5, 1, 0x0F,
1081+ SSD1351_SETVSL, 5, 3, 0xA0, 0xB5, 0x55,
1082+ SSD1351_PRECHARGE2, 5, 1, 0x01,
1083+ SSD1351_DISPLAYON, 5, 0,
1084+ };
1085+ // clang-format on
1086+ qp_comms_bulk_command_sequence(device, ssd1351_init_sequence, sizeof(ssd1351_init_sequence));
1087+
1088+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
1089+ const uint8_t madctl[] = {
1090+ [QP_ROTATION_0] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MY,
1091+ [QP_ROTATION_90] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX | SSD1351_MADCTL_MY | SSD1351_MADCTL_MV,
1092+ [QP_ROTATION_180] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX,
1093+ [QP_ROTATION_270] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MV,
1094+ };
1095+ qp_comms_command_databyte(device, SSD1351_SETREMAP, madctl[rotation]);
1096+ qp_comms_command_databyte(device, SSD1351_STARTLINE, (rotation == QP_ROTATION_0 || rotation == QP_ROTATION_90) ? driver->base.panel_height : 0);
1097+
1098+ return true;
1099+}
1100+
1101+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1102+// Driver vtable
1103+
1104+const struct tft_panel_dc_reset_painter_driver_vtable_t ssd1351_driver_vtable = {
1105+ .base =
1106+ {
1107+ .init = qp_ssd1351_init,
1108+ .power = qp_tft_panel_power,
1109+ .clear = qp_tft_panel_clear,
1110+ .flush = qp_tft_panel_flush,
1111+ .pixdata = qp_tft_panel_pixdata,
1112+ .viewport = qp_tft_panel_viewport,
1113+ .palette_convert = qp_tft_panel_palette_convert,
1114+ .append_pixels = qp_tft_panel_append_pixels,
1115+ },
1116+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
1117+ .num_window_bytes = 1,
1118+ .swap_window_coords = true,
1119+ .opcodes =
1120+ {
1121+ .display_on = SSD1351_DISPLAYON,
1122+ .display_off = SSD1351_DISPLAYOFF,
1123+ .set_column_address = SSD1351_SETCOLUMN,
1124+ .set_row_address = SSD1351_SETROW,
1125+ .enable_writes = SSD1351_WRITERAM,
1126+ },
1127+};
1128+
1129+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1130+// SPI
1131+
1132+#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
1133+
1134+// Factory function for creating a handle to the SSD1351 device
1135+painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
1136+ for (uint32_t i = 0; i < SSD1351_NUM_DEVICES; ++i) {
1137+ tft_panel_dc_reset_painter_device_t *driver = &ssd1351_drivers[i];
1138+ if (!driver->base.driver_vtable) {
1139diff --git a/drivers/painter/ssd1351/qp_ssd1351.h b/drivers/painter/ssd1351/qp_ssd1351.h
1140new file mode 100644
1141index 0000000000000000000000000000000000000000..0df34f204d5c61f73da13930fc92eda35265d8f7
1142--- /dev/null
1143+++ b/drivers/painter/ssd1351/qp_ssd1351.h
1144@@ -0,0 +1,37 @@
1145+// Copyright 2021 Nick Brassel (@tzarc)
1146+// SPDX-License-Identifier: GPL-2.0-or-later
1147+
1148+#pragma once
1149+
1150+#include "gpio.h"
1151+#include "qp_internal.h"
1152+
1153+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1154+// Quantum Painter SSD1351 configurables (add to your keyboard's config.h)
1155+
1156+#ifndef SSD1351_NUM_DEVICES
1157+/**
1158+ * @def This controls the maximum number of SSD1351 devices that Quantum Painter can communicate with at any one time.
1159+ * Increasing this number allows for multiple displays to be used.
1160+ */
1161+# define SSD1351_NUM_DEVICES 1
1162+#endif
1163+
1164+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1165+// Quantum Painter SSD1351 device factories
1166+
1167+#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
1168+/**
1169+ * Factory method for an SSD1351 SPI OLED device.
1170+ *
1171+ * @param panel_width[in] the width of the display panel
1172+ * @param panel_height[in] the height of the display panel
1173+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
1174+ * @param dc_pin[in] the GPIO pin used for D/C control
1175+ * @param reset_pin[in] the GPIO pin used for RST
1176+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
1177+ * @param spi_mode[in] the SPI mode to use when communicating with the display
1178+ * @return the device handle used with all drawing routines in Quantum Painter
1179+ */
1180+painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
1181+#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
1182diff --git a/drivers/painter/ssd1351/qp_ssd1351_opcodes.h b/drivers/painter/ssd1351/qp_ssd1351_opcodes.h
1183new file mode 100644
1184index 0000000000000000000000000000000000000000..48ed2a3a7c2c6f9f1da6bf8c0cc44d24cd6fb094
1185--- /dev/null
1186+++ b/drivers/painter/ssd1351/qp_ssd1351_opcodes.h
1187@@ -0,0 +1,48 @@
1188+// Copyright 2021 Nick Brassel (@tzarc)
1189+// SPDX-License-Identifier: GPL-2.0-or-later
1190+
1191+#pragma once
1192+
1193+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1194+// Quantum Painter SSD1351 command opcodes
1195+
1196+// System function commands
1197+#define SSD1351_SETCOLUMN 0x15
1198+#define SSD1351_SETROW 0x75
1199+#define SSD1351_WRITERAM 0x5C
1200+#define SSD1351_READRAM 0x5D
1201+#define SSD1351_SETREMAP 0xA0
1202+#define SSD1351_STARTLINE 0xA1
1203+#define SSD1351_DISPLAYOFFSET 0xA2
1204+#define SSD1351_DISPLAYALLOFF 0xA4
1205+#define SSD1351_DISPLAYALLON 0xA5
1206+#define SSD1351_NORMALDISPLAY 0xA6
1207+#define SSD1351_INVERTDISPLAY 0xA7
1208+#define SSD1351_FUNCTIONSELECT 0xAB
1209+#define SSD1351_DISPLAYOFF 0xAE
1210+#define SSD1351_DISPLAYON 0xAF
1211+#define SSD1351_PRECHARGE 0xB1
1212+#define SSD1351_DISPLAYENHANCE 0xB2
1213+#define SSD1351_CLOCKDIV 0xB3
1214+#define SSD1351_SETVSL 0xB4
1215+#define SSD1351_SETGPIO 0xB5
1216+#define SSD1351_PRECHARGE2 0xB6
1217+#define SSD1351_SETGRAY 0xB8
1218+#define SSD1351_USELUT 0xB9
1219+#define SSD1351_PRECHARGELEVEL 0xBB
1220+#define SSD1351_VCOMH 0xBE
1221+#define SSD1351_CONTRASTABC 0xC1
1222+#define SSD1351_CONTRASTMASTER 0xC7
1223+#define SSD1351_MUXRATIO 0xCA
1224+#define SSD1351_COMMANDLOCK 0xFD
1225+#define SSD1351_HORIZSCROLL 0x96
1226+#define SSD1351_STOPSCROLL 0x9E
1227+#define SSD1351_STARTSCROLL 0x9F
1228+
1229+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1230+// SETREMAP (MADCTL) Flags
1231+#define SSD1351_MADCTL_MY 0b00010000
1232+#define SSD1351_MADCTL_MX 0b00000010
1233+#define SSD1351_MADCTL_MV 0b00000001
1234+#define SSD1351_MADCTL_RGB 0b01100000
1235+#define SSD1351_MADCTL_BGR 0b01100100
1236diff --git a/drivers/painter/st77xx/qp_st7789.c b/drivers/painter/st77xx/qp_st7789.c
1237new file mode 100644
1238index 0000000000000000000000000000000000000000..d005ece050e9e353309c7f56bf136342d192de3e
1239--- /dev/null
1240+++ b/drivers/painter/st77xx/qp_st7789.c
1241@@ -0,0 +1,144 @@
1242+// Copyright 2021 Paul Cotter (@gr1mr3aver)
1243+// Copyright 2021 Nick Brassel (@tzarc)
1244+// SPDX-License-Identifier: GPL-2.0-or-later
1245+
1246+#include "qp_internal.h"
1247+#include "qp_comms.h"
1248+#include "qp_st7789.h"
1249+#include "qp_st77xx_opcodes.h"
1250+#include "qp_st7789_opcodes.h"
1251+#include "qp_tft_panel.h"
1252+
1253+#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
1254+# include "qp_comms_spi.h"
1255+#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
1256+
1257+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1258+// Common
1259+
1260+// Driver storage
1261+tft_panel_dc_reset_painter_device_t st7789_drivers[ST7789_NUM_DEVICES] = {0};
1262+
1263+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1264+// Automatic viewport offsets
1265+
1266+#ifndef ST7789_NO_AUTOMATIC_OFFSETS
1267+static inline void st7789_automatic_viewport_offsets(painter_device_t device, painter_rotation_t rotation) {
1268+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
1269+
1270+ // clang-format off
1271+ const struct {
1272+ uint16_t offset_x;
1273+ uint16_t offset_y;
1274+ } rotation_offsets_240x240[] = {
1275+ [QP_ROTATION_0] = { .offset_x = 0, .offset_y = 0 },
1276+ [QP_ROTATION_90] = { .offset_x = 0, .offset_y = 0 },
1277+ [QP_ROTATION_180] = { .offset_x = 0, .offset_y = 80 },
1278+ [QP_ROTATION_270] = { .offset_x = 80, .offset_y = 0 },
1279+ };
1280+ // clang-format on
1281+
1282+ if (driver->panel_width == 240 && driver->panel_height == 240) {
1283+ driver->offset_x = rotation_offsets_240x240[rotation].offset_x;
1284+ driver->offset_y = rotation_offsets_240x240[rotation].offset_y;
1285+ }
1286+}
1287+#endif // ST7789_NO_AUTOMATIC_OFFSETS
1288+
1289+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1290+// Initialization
1291+
1292+bool qp_st7789_init(painter_device_t device, painter_rotation_t rotation) {
1293+ // clang-format off
1294+ const uint8_t st7789_init_sequence[] = {
1295+ // Command, Delay, N, Data[N]
1296+ ST77XX_CMD_RESET, 120, 0,
1297+ ST77XX_CMD_SLEEP_OFF, 5, 0,
1298+ ST77XX_SET_PIX_FMT, 0, 1, 0x55,
1299+ ST77XX_CMD_INVERT_ON, 0, 0,
1300+ ST77XX_CMD_NORMAL_ON, 0, 0,
1301+ ST77XX_CMD_DISPLAY_ON, 20, 0
1302+ };
1303+ // clang-format on
1304+ qp_comms_bulk_command_sequence(device, st7789_init_sequence, sizeof(st7789_init_sequence));
1305+
1306+ // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
1307+ const uint8_t madctl[] = {
1308+ [QP_ROTATION_0] = ST77XX_MADCTL_RGB,
1309+ [QP_ROTATION_90] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MV,
1310+ [QP_ROTATION_180] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MY,
1311+ [QP_ROTATION_270] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MY,
1312+ };
1313+ qp_comms_command_databyte(device, ST77XX_SET_MADCTL, madctl[rotation]);
1314+
1315+#ifndef ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
1316+ st7789_automatic_viewport_offsets(device, rotation);
1317+#endif // ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
1318+
1319+ return true;
1320+}
1321+
1322+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1323+// Driver vtable
1324+
1325+const struct tft_panel_dc_reset_painter_driver_vtable_t st7789_driver_vtable = {
1326+ .base =
1327+ {
1328+ .init = qp_st7789_init,
1329+ .power = qp_tft_panel_power,
1330+ .clear = qp_tft_panel_clear,
1331+ .flush = qp_tft_panel_flush,
1332+ .pixdata = qp_tft_panel_pixdata,
1333+ .viewport = qp_tft_panel_viewport,
1334+ .palette_convert = qp_tft_panel_palette_convert,
1335+ .append_pixels = qp_tft_panel_append_pixels,
1336+ },
1337+ .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
1338+ .num_window_bytes = 2,
1339+ .swap_window_coords = false,
1340+ .opcodes =
1341diff --git a/drivers/painter/st77xx/qp_st7789.h b/drivers/painter/st77xx/qp_st7789.h
1342new file mode 100644
1343index 0000000000000000000000000000000000000000..ec61f5d70b7e3a555bec4273272520ca3424743c
1344--- /dev/null
1345+++ b/drivers/painter/st77xx/qp_st7789.h
1346@@ -0,0 +1,44 @@
1347+// Copyright 2021 Paul Cotter (@gr1mr3aver)
1348+// Copyright 2021 Nick Brassel (@tzarc)
1349+// SPDX-License-Identifier: GPL-2.0-or-later
1350+
1351+#pragma once
1352+
1353+#include "gpio.h"
1354+#include "qp_internal.h"
1355+
1356+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1357+// Quantum Painter ST7789 configurables (add to your keyboard's config.h)
1358+
1359+#ifndef ST7789_NUM_DEVICES
1360+/**
1361+ * @def This controls the maximum number of ST7789 devices that Quantum Painter can communicate with at any one time.
1362+ * Increasing this number allows for multiple displays to be used.
1363+ */
1364+# define ST7789_NUM_DEVICES 1
1365+#endif
1366+
1367+// Additional configuration options to be copied to your keyboard's config.h (don't change here):
1368+
1369+// If you know exactly which offsets should be used on your panel with respect to selected rotation, then this config
1370+// option allows you to save some flash space -- you'll need to invoke qp_set_viewport_offsets() instead from your keyboard.
1371+// #define ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
1372+
1373+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1374+// Quantum Painter ST7789 device factories
1375+
1376+#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
1377+/**
1378+ * Factory method for an ST7789 SPI LCD device.
1379+ *
1380+ * @param panel_width[in] the width of the display panel
1381+ * @param panel_height[in] the height of the display panel
1382+ * @param chip_select_pin[in] the GPIO pin used for SPI chip select
1383+ * @param dc_pin[in] the GPIO pin used for D/C control
1384+ * @param reset_pin[in] the GPIO pin used for RST
1385+ * @param spi_divisor[in] the SPI divisor to use when communicating with the display
1386+ * @param spi_mode[in] the SPI mode to use when communicating with the display
1387+ * @return the device handle used with all drawing routines in Quantum Painter
1388+ */
1389+painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
1390+#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
1391diff --git a/drivers/painter/st77xx/qp_st7789_opcodes.h b/drivers/painter/st77xx/qp_st7789_opcodes.h
1392new file mode 100644
1393index 0000000000000000000000000000000000000000..b5baba718469d5bad47e640e6bffaa76de5889c0
1394--- /dev/null
1395+++ b/drivers/painter/st77xx/qp_st7789_opcodes.h
1396@@ -0,0 +1,64 @@
1397+// Copyright 2021 Paul Cotter (@gr1mr3aver)
1398+// SPDX-License-Identifier: GPL-2.0-or-later
1399+
1400+#pragma once
1401+
1402+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1403+// Quantum Painter ST7789 additional command opcodes
1404+
1405+// System function commands
1406+#define ST7789_GET_SELF_DIAG 0x0F // Get self-diagnostic result
1407+#define ST7789_SET_VERT_SCRL 0x33 // Set vertical scroll definition
1408+#define ST7789_SET_VERT_SCRL_ADDR 0x37 // SEt Vertical scroll start address
1409+#define ST7789_SET_MEM_CONT 0x3C // Memory Write continue
1410+#define ST7789_GET_MEM_CONT 0x3E // Memory Read continue
1411+#define ST7789_SET_TEAR_LINE 0x44 // Set tear scanline
1412+#define ST7789_GET_TEAR_LINE 0x45 // Get tear scanline
1413+#define ST7789_SET_BRIGHTNESS 0x51 // Set display brightness
1414+#define ST7789_GET_BRIGHTNESS 0x52 // Get display brightness
1415+#define ST7789_SET_CTRL 0x53 // Set CTRL display
1416+#define ST7789_GET_CTRL 0x54 // Get CTRL display value
1417+#define ST7789_SET_CAB_COLOR 0x55 // Set content adaptive brightness control and color enhancement
1418+#define ST7789_GET_CAB_COLOR 0x56 // Get content adaptive brightness control and color enhancement
1419+#define ST7789_SET_CAB_BRIGHTNESS 0x5E // Set content adaptive minimum brightness
1420+#define ST7789_GET_CAB_BRIGHTNESS 0x5F // Get content adaptive minimum brightness
1421+#define ST7789_GET_ABC_SELF_DIAG 0x68 // Get Auto brightness control self diagnostics
1422+
1423+// Panel Function Commands
1424+#define ST7789_SET_RAM_CTL 0xB0 // Set RAM control
1425+#define ST7789_SET_RGB_CTL 0xB1 // Set RGB control
1426+#define ST7789_SET_PORCH_CTL 0xB2 // Set Porch control
1427+#define ST7789_SET_FRAME_RATE_CTL_1 0xB3 // Set frame rate control 1
1428+#define ST7789_SET_PARTIAL_CTL 0xB5 // Set Partial control
1429+#define ST7789_SET_GATE_CTL 0xB7 // Set gate control
1430+#define ST7789_SET_GATE_ON_TIMING 0xB8 // Set gate on timing adjustment
1431+#define ST7789_SET_DIGITAL_GAMMA_ON 0xBA // Enable digital gamma
1432+#define ST7789_SET_VCOM 0xBB // Set VCOM
1433+#define ST7789_SET_POWER_SAVE 0xBC // Set power saving mode
1434+#define ST7789_SET_DISP_OFF_POWER 0xBD // Set display off power saving
1435+#define ST7789_SET_LCM_CTL 0xC0 // Set LCM control
1436+#define ST7789_SET_IDS 0xC1 // Set IDs
1437+#define ST7789_SET_VDV_VRH_ON 0xC2 // Set VDV and VRH command enable
1438+#define ST7789_SET_VRH 0xC3 // Set VRH
1439+#define ST7789_SET_VDV 0xC4 // Set VDV
1440+#define ST7789_SET_VCOM_OFFSET 0xC5 // Set VCOM offset ctl
1441+#define ST7789_SET_FRAME_RATE_CTL_2 0xC6 // Set frame rate control 2
1442+#define ST7789_SET_CABC_CTL 0xC7 // Set CABC Control
1443+#define ST7789_GET_REG_1 0xC8 // Get register value selection1
1444+#define ST7789_GET_REG_2 0xCA // Get register value selection2
1445+#define ST7789_SET_PWM_FREQ 0xCC // Set PWM frequency
1446+#define ST7789_SET_POWER_CTL_1 0xD0 // Set power ctl 1
1447+#define ST7789_SET_VAP_VAN_ON 0xD2 // Enable VAP/VAN signal output
1448+#define ST7789_SET_CMD2_ENABLE 0xDF // Enable command 2
1449+#define ST7789_SET_PGAMMA 0xE0 // Set positive gamma
1450+#define ST7789_SET_NGAMMA 0xE1 // Set negative gamma
1451+#define ST7789_SET_DIGITAL_GAMMA_RED 0xE2 // Set digital gamma lookup table for red
1452+#define ST7789_SET_DIGITAL_GAMMA_BLUE 0xE3 // Get digital gamma lookup table for blue
1453+#define ST7789_SET_GATE_CTL_2 0xE4 // Set gate control 2
1454+#define ST7789_SET_SPI2_ENABLE 0xE7 // Enable SPI2
1455+#define ST7789_SET_POWER_CTL_2 0xE8 // Set power ctl 2
1456+#define ST7789_SET_EQ_TIME_CTL 0xE9 // Set equalize time control
1457+#define ST7789_SET_PROG_CTL 0xEC // Set program control
1458+#define ST7789_SET_PROG_MODE_ENABLE 0xFA // Set program mode enable
1459+#define ST7789_SET_NVMEM 0xFC // Set NVMEM data
1460+#define ST7789_SET_PROG_ACTION 0xFE // Set program action
1461diff --git a/drivers/painter/st77xx/qp_st77xx_opcodes.h b/drivers/painter/st77xx/qp_st77xx_opcodes.h
1462new file mode 100644
1463index 0000000000000000000000000000000000000000..131378d8328e5d21debdbdfcb54519054377f138
1464--- /dev/null
1465+++ b/drivers/painter/st77xx/qp_st77xx_opcodes.h
1466@@ -0,0 +1,51 @@
1467+// Copyright 2021 Paul Cotter (@gr1mr3aver)
1468+// SPDX-License-Identifier: GPL-2.0-or-later
1469+
1470+#pragma once
1471+
1472+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1473+// Quantum Painter ST77XX command opcodes
1474+
1475+// System function commands
1476+#define ST77XX_CMD_NOP 0x00 // No operation
1477+#define ST77XX_CMD_RESET 0x01 // Software reset
1478+#define ST77XX_GET_ID_INFO 0x04 // Get ID information
1479+#define ST77XX_GET_STATUS 0x09 // Get status
1480+#define ST77XX_GET_PWR_MODE 0x0A // Get power mode
1481+#define ST77XX_GET_MADCTL 0x0B // Get mem access ctl
1482+#define ST77XX_GET_PIX_FMT 0x0C // Get pixel format
1483+#define ST77XX_GET_IMG_FMT 0x0D // Get image format
1484+#define ST77XX_GET_SIG_MODE 0x0E // Get signal mode
1485+#define ST77XX_CMD_SLEEP_ON 0x10 // Enter sleep mode
1486+#define ST77XX_CMD_SLEEP_OFF 0x11 // Exist sleep mode
1487+#define ST77XX_CMD_PARTIAL_ON 0x12 // Enter partial mode
1488+#define ST77XX_CMD_NORMAL_ON 0x13 // Exit partial mode
1489+#define ST77XX_CMD_INVERT_OFF 0x20 // Exit inverted mode
1490+#define ST77XX_CMD_INVERT_ON 0x21 // Enter inverted mode
1491+#define ST77XX_SET_GAMMA 0x26 // Set gamma params
1492+#define ST77XX_CMD_DISPLAY_OFF 0x28 // Disable display
1493+#define ST77XX_CMD_DISPLAY_ON 0x29 // Enable display
1494+#define ST77XX_SET_COL_ADDR 0x2A // Set column address
1495+#define ST77XX_SET_ROW_ADDR 0x2B // Set page (row) address
1496+#define ST77XX_SET_MEM 0x2C // Set memory
1497+#define ST77XX_GET_MEM 0x2E // Get memory
1498+#define ST77XX_SET_PARTIAL_AREA 0x30 // Set partial area
1499+#define ST77XX_CMD_TEARING_OFF 0x34 // Tearing line disabled
1500+#define ST77XX_CMD_TEARING_ON 0x35 // Tearing line enabled
1501+#define ST77XX_SET_MADCTL 0x36 // Set mem access ctl
1502+#define ST77XX_CMD_IDLE_OFF 0x38 // Exit idle mode
1503+#define ST77XX_CMD_IDLE_ON 0x39 // Enter idle mode
1504+#define ST77XX_SET_PIX_FMT 0x3A // Set pixel format
1505+#define ST77XX_GET_ID1 0xDA // Get ID1
1506+#define ST77XX_GET_ID2 0xDB // Get ID2
1507+#define ST77XX_GET_ID3 0xDC // Get ID3
1508+
1509+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1510+// MADCTL Flags
1511+#define ST77XX_MADCTL_MY 0b10000000
1512+#define ST77XX_MADCTL_MX 0b01000000
1513+#define ST77XX_MADCTL_MV 0b00100000
1514+#define ST77XX_MADCTL_ML 0b00010000
1515+#define ST77XX_MADCTL_RGB 0b00000000
1516+#define ST77XX_MADCTL_BGR 0b00001000
1517+#define ST77XX_MADCTL_MH 0b00000100
1518diff --git a/drivers/painter/tft_panel/qp_tft_panel.c b/drivers/painter/tft_panel/qp_tft_panel.c
1519new file mode 100644
1520index 0000000000000000000000000000000000000000..4d636c950966dd957b5cd799a478ab99e0033419
1521--- /dev/null
1522+++ b/drivers/painter/tft_panel/qp_tft_panel.c
1523@@ -0,0 +1,130 @@
1524+// Copyright 2021 Nick Brassel (@tzarc)
1525+// SPDX-License-Identifier: GPL-2.0-or-later
1526+
1527+#include "color.h"
1528+#include "qp_internal.h"
1529+#include "qp_comms.h"
1530+#include "qp_draw.h"
1531+#include "qp_tft_panel.h"
1532+
1533+#define BYTE_SWAP(x) (((((uint16_t)(x)) >> 8) & 0x00FF) | ((((uint16_t)(x)) << 8) & 0xFF00))
1534+
1535+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1536+// Native pixel format conversion
1537+
1538+uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
1539+ uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3);
1540+ return rgb565;
1541+}
1542+
1543+uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b) {
1544+ uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3);
1545+ return BYTE_SWAP(rgb565);
1546+}
1547+
1548+uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b) {
1549+ uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3);
1550+ return bgr565;
1551+}
1552+
1553+uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b) {
1554+ uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3);
1555+ return BYTE_SWAP(bgr565);
1556+}
1557+
1558+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1559+// Quantum Painter API implementations
1560+
1561+// Power control
1562+bool qp_tft_panel_power(painter_device_t device, bool power_on) {
1563+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
1564+ struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
1565+ qp_comms_command(device, power_on ? vtable->opcodes.display_on : vtable->opcodes.display_off);
1566+ return true;
1567+}
1568+
1569+// Screen clear
1570+bool qp_tft_panel_clear(painter_device_t device) {
1571+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
1572+ driver->driver_vtable->init(device, driver->rotation); // Re-init the LCD
1573+ return true;
1574+}
1575+
1576+// Screen flush
1577+bool qp_tft_panel_flush(painter_device_t device) {
1578+ // No-op, as there's no framebuffer in RAM for this device.
1579+ return true;
1580+}
1581+
1582+// Viewport to draw to
1583+bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
1584+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
1585+ struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
1586+
1587+ // Fix up the drawing location if required
1588+ left += driver->offset_x;
1589+ right += driver->offset_x;
1590+ top += driver->offset_y;
1591+ bottom += driver->offset_y;
1592+
1593+ // Check if we need to manually swap the window coordinates based on whether or not we're in a sideways rotation
1594+ if (vtable->swap_window_coords && (driver->rotation == QP_ROTATION_90 || driver->rotation == QP_ROTATION_270)) {
1595+ uint16_t temp;
1596+
1597+ temp = left;
1598+ left = top;
1599+ top = temp;
1600+
1601+ temp = right;
1602+ right = bottom;
1603+ bottom = temp;
1604+ }
1605+
1606+ if (vtable->num_window_bytes == 1) {
1607+ // Set up the x-window
1608+ uint8_t xbuf[2] = {left & 0xFF, right & 0xFF};
1609+ qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf));
1610+
1611+ // Set up the y-window
1612+ uint8_t ybuf[2] = {top & 0xFF, bottom & 0xFF};
1613+ qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf));
1614+ } else if (vtable->num_window_bytes == 2) {
1615+ // Set up the x-window
1616+ uint8_t xbuf[4] = {left >> 8, left & 0xFF, right >> 8, right & 0xFF};
1617+ qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf));
1618+
1619+ // Set up the y-window
1620+ uint8_t ybuf[4] = {top >> 8, top & 0xFF, bottom >> 8, bottom & 0xFF};
1621+ qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf));
1622+ }
1623diff --git a/drivers/painter/tft_panel/qp_tft_panel.h b/drivers/painter/tft_panel/qp_tft_panel.h
1624new file mode 100644
1625index 0000000000000000000000000000000000000000..6eddfc503d226eca1100d95d362d4e2ad2b1c460
1626--- /dev/null
1627+++ b/drivers/painter/tft_panel/qp_tft_panel.h
1628@@ -0,0 +1,67 @@
1629+// Copyright 2021 Nick Brassel (@tzarc)
1630+// SPDX-License-Identifier: GPL-2.0-or-later
1631+
1632+#include "color.h"
1633+#include "qp_internal.h"
1634+
1635+#ifdef QUANTUM_PAINTER_SPI_ENABLE
1636+# include "qp_comms_spi.h"
1637+#endif // QUANTUM_PAINTER_SPI_ENABLE
1638+
1639+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1640+// Common TFT panel implementation using D/C, and RST pins.
1641+
1642+typedef uint16_t (*rgb888_to_native_uint16_t)(uint8_t r, uint8_t g, uint8_t b);
1643+
1644+// Driver vtable with extras
1645+struct tft_panel_dc_reset_painter_driver_vtable_t {
1646+ struct painter_driver_vtable_t base; // must be first, so it can be cast to/from the painter_driver_vtable_t* type
1647+
1648+ // Conversion function for palette entries
1649+ rgb888_to_native_uint16_t rgb888_to_native16bit;
1650+
1651+ // Number of bytes for transmitting x/y coordinates
1652+ uint8_t num_window_bytes;
1653+
1654+ // Whether or not the x/y coords should be swapped on 90/270 rotation
1655+ bool swap_window_coords;
1656+
1657+ // Opcodes for normal display operation
1658+ struct {
1659+ uint8_t display_on;
1660+ uint8_t display_off;
1661+ uint8_t set_column_address;
1662+ uint8_t set_row_address;
1663+ uint8_t enable_writes;
1664+ } opcodes;
1665+};
1666+
1667+// Device definition
1668+typedef struct tft_panel_dc_reset_painter_device_t {
1669+ struct painter_driver_t base; // must be first, so it can be cast to/from the painter_device_t* type
1670+
1671+ union {
1672+#ifdef QUANTUM_PAINTER_SPI_ENABLE
1673+ // SPI-based configurables
1674+ struct qp_comms_spi_dc_reset_config_t spi_dc_reset_config;
1675+#endif // QUANTUM_PAINTER_SPI_ENABLE
1676+
1677+ // TODO: I2C/parallel etc.
1678+ };
1679+} tft_panel_dc_reset_painter_device_t;
1680+
1681+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
1682+// Forward declarations for injecting into concrete driver vtables
1683+
1684+bool qp_tft_panel_power(painter_device_t device, bool power_on);
1685+bool qp_tft_panel_clear(painter_device_t device);
1686+bool qp_tft_panel_flush(painter_device_t device);
1687+bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
1688+bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
1689+bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette);
1690+bool qp_tft_panel_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);
1691+
1692+uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b);
1693+uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b);
1694+uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b);
1695+uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b);
1696diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
1697index 5f65e677e50266e5c2080fda8d635fe2577cca10..85baa238a8d6bd204fddea208116cf8be35c8092 100644
1698--- a/lib/python/qmk/cli/__init__.py
1699+++ b/lib/python/qmk/cli/__init__.py
1700@@ -16,7 +16,8 @@ import_names = {
1701 # A mapping of package name to importable name
1702 'pep8-naming': 'pep8ext_naming',
1703 'pyusb': 'usb.core',
1704- 'qmk-dotty-dict': 'dotty_dict'
1705+ 'qmk-dotty-dict': 'dotty_dict',
1706+ 'pillow': 'PIL'
1707 }
1708
1709 safe_commands = [
1710@@ -67,6 +68,7 @@ subcommands = [
1711 'qmk.cli.multibuild',
1712 'qmk.cli.new.keyboard',
1713 'qmk.cli.new.keymap',
1714+ 'qmk.cli.painter',
1715 'qmk.cli.pyformat',
1716 'qmk.cli.pytest',
1717 'qmk.cli.via2json',
1718diff --git a/lib/python/qmk/cli/painter/__init__.py b/lib/python/qmk/cli/painter/__init__.py
1719new file mode 100644
1720index 0000000000000000000000000000000000000000..d1a225346ca23b8239b2374c1e0f069178c42149
1721--- /dev/null
1722+++ b/lib/python/qmk/cli/painter/__init__.py
1723@@ -0,0 +1,2 @@
1724+from . import convert_graphics
1725+from . import make_font
1726diff --git a/lib/python/qmk/cli/painter/convert_graphics.py b/lib/python/qmk/cli/painter/convert_graphics.py
1727new file mode 100644
1728index 0000000000000000000000000000000000000000..bbc30d26ff78f8e80dfe27bd13b90c13312d1ba4
1729--- /dev/null
1730+++ b/lib/python/qmk/cli/painter/convert_graphics.py
1731@@ -0,0 +1,86 @@
1732+"""This script tests QGF functionality.
1733+"""
1734+import re
1735+import datetime
1736+from io import BytesIO
1737+from qmk.path import normpath
1738+from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats
1739+from milc import cli
1740+from PIL import Image
1741+
1742+
1743+@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.')
1744+@cli.argument('-i', '--input', required=True, help='Specify input graphic file.')
1745+@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
1746+@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys())))
1747+@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.')
1748+@cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.')
1749+@cli.subcommand('Converts an input image to something QMK understands')
1750+def painter_convert_graphics(cli):
1751+ """Converts an image file to a format that Quantum Painter understands.
1752+
1753+ This command uses the `qmk.painter` module to generate a Quantum Painter image defintion from an image. The generated definitions are written to a files next to the input -- `INPUT.c` and `INPUT.h`.
1754+ """
1755+ # Work out the input file
1756+ if cli.args.input != '-':
1757+ cli.args.input = normpath(cli.args.input)
1758+
1759+ # Error checking
1760+ if not cli.args.input.exists():
1761+ cli.log.error('Input image file does not exist!')
1762+ cli.print_usage()
1763+ return False
1764+
1765+ # Work out the output directory
1766+ if len(cli.args.output) == 0:
1767+ cli.args.output = cli.args.input.parent
1768+ cli.args.output = normpath(cli.args.output)
1769+
1770+ # Ensure we have a valid format
1771+ if cli.args.format not in valid_formats.keys():
1772+ cli.log.error('Output format %s is invalid. Allowed values: %s' % (cli.args.format, ', '.join(valid_formats.keys())))
1773+ cli.print_usage()
1774+ return False
1775+
1776+ # Work out the encoding parameters
1777+ format = valid_formats[cli.args.format]
1778+
1779+ # Load the input image
1780+ input_img = Image.open(cli.args.input)
1781+
1782+ # Convert the image to QGF using PIL
1783+ out_data = BytesIO()
1784+ input_img.save(out_data, "QGF", use_deltas=(not cli.args.no_deltas), use_rle=(not cli.args.no_rle), qmk_format=format, verbose=cli.args.verbose)
1785+ out_bytes = out_data.getvalue()
1786+
1787+ # Work out the text substitutions for rendering the output data
1788+ subs = {
1789+ 'generated_type': 'image',
1790+ 'var_prefix': 'gfx',
1791+ 'generator_command': f'qmk painter-convert-graphics -i {cli.args.input.name} -f {cli.args.format}',
1792+ 'year': datetime.date.today().strftime("%Y"),
1793+ 'input_file': cli.args.input.name,
1794+ 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
1795+ 'byte_count': len(out_bytes),
1796+ 'bytes_lines': render_bytes(out_bytes),
1797+ 'format': cli.args.format,
1798+ }
1799+
1800+ # Render the license
1801+ subs.update({'license': render_license(subs)})
1802+
1803+ # Render and write the header file
1804+ header_text = render_header(subs)
1805+ header_file = cli.args.output / (cli.args.input.stem + ".qgf.h")
1806+ with open(header_file, 'w') as header:
1807+ print(f"Writing {header_file}...")
1808+ header.write(header_text)
1809+ header.close()
1810+
1811+ # Render and write the source file
1812+ source_text = render_source(subs)
1813+ source_file = cli.args.output / (cli.args.input.stem + ".qgf.c")
1814+ with open(source_file, 'w') as source:
1815+ print(f"Writing {source_file}...")
1816+ source.write(source_text)
1817+ source.close()
1818diff --git a/lib/python/qmk/cli/painter/make_font.py b/lib/python/qmk/cli/painter/make_font.py
1819new file mode 100644
1820index 0000000000000000000000000000000000000000..0762843fd3a046507c52ebdbb2ef972b1eb62670
1821--- /dev/null
1822+++ b/lib/python/qmk/cli/painter/make_font.py
1823@@ -0,0 +1,87 @@
1824+"""This script automates the conversion of font files into a format QMK firmware understands.
1825+"""
1826+
1827+import re
1828+import datetime
1829+from io import BytesIO
1830+from qmk.path import normpath
1831+from qmk.painter_qff import QFFFont
1832+from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats
1833+from milc import cli
1834+
1835+
1836+@cli.argument('-f', '--font', required=True, help='Specify input font file.')
1837+@cli.argument('-o', '--output', required=True, help='Specify output image path.')
1838+@cli.argument('-s', '--size', default=12, help='Specify font size. Default 12.')
1839+@cli.argument('-n', '--no-ascii', arg_only=True, action='store_true', help='Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.')
1840+@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
1841+@cli.argument('-a', '--no-aa', arg_only=True, action='store_true', help='Disable anti-aliasing on fonts.')
1842+@cli.subcommand('Converts an input font to something QMK understands')
1843+def painter_make_font_image(cli):
1844+ # Create the font object
1845+ font = QFFFont(cli)
1846+ # Read from the input file
1847+ cli.args.font = normpath(cli.args.font)
1848+ font.generate_image(cli.args.font, cli.args.size, include_ascii_glyphs=(not cli.args.no_ascii), unicode_glyphs=cli.args.unicode_glyphs, use_aa=(False if cli.args.no_aa else True))
1849+ # Render out the data
1850+ font.save_to_image(normpath(cli.args.output))
1851+
1852+
1853+@cli.argument('-i', '--input', help='Specify input graphic file.')
1854+@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
1855+@cli.argument('-n', '--no-ascii', arg_only=True, action='store_true', help='Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.')
1856+@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
1857+@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys())))
1858+@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.')
1859+@cli.subcommand('Converts an input font image to something QMK firmware understands')
1860+def painter_convert_font_image(cli):
1861+ # Work out the format
1862+ format = valid_formats[cli.args.format]
1863+
1864+ # Create the font object
1865+ font = QFFFont(cli.log)
1866+
1867+ # Read from the input file
1868+ cli.args.input = normpath(cli.args.input)
1869+ font.read_from_image(cli.args.input, include_ascii_glyphs=(not cli.args.no_ascii), unicode_glyphs=cli.args.unicode_glyphs)
1870+
1871+ # Work out the output directory
1872+ if len(cli.args.output) == 0:
1873+ cli.args.output = cli.args.input.parent
1874+ cli.args.output = normpath(cli.args.output)
1875+
1876+ # Render out the data
1877+ out_data = BytesIO()
1878+ font.save_to_qff(format, (False if cli.args.no_rle else True), out_data)
1879+
1880+ # Work out the text substitutions for rendering the output data
1881+ subs = {
1882+ 'generated_type': 'font',
1883+ 'var_prefix': 'font',
1884+ 'generator_command': f'qmk painter-convert-font-image -i {cli.args.input.name} -f {cli.args.format}',
1885+ 'year': datetime.date.today().strftime("%Y"),
1886+ 'input_file': cli.args.input.name,
1887+ 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
1888+ 'byte_count': out_data.getbuffer().nbytes,
1889+ 'bytes_lines': render_bytes(out_data.getbuffer().tobytes()),
1890+ 'format': cli.args.format,
1891+ }
1892+
1893+ # Render the license
1894+ subs.update({'license': render_license(subs)})
1895+
1896+ # Render and write the header file
1897+ header_text = render_header(subs)
1898+ header_file = cli.args.output / (cli.args.input.stem + ".qff.h")
1899+ with open(header_file, 'w') as header:
1900+ print(f"Writing {header_file}...")
1901+ header.write(header_text)
1902+ header.close()
1903+
1904+ # Render and write the source file
1905+ source_text = render_source(subs)
1906+ source_file = cli.args.output / (cli.args.input.stem + ".qff.c")
1907+ with open(source_file, 'w') as source:
1908+ print(f"Writing {source_file}...")
1909+ source.write(source_text)
1910+ source.close()
1911diff --git a/lib/python/qmk/painter.py b/lib/python/qmk/painter.py
1912new file mode 100644
1913index 0000000000000000000000000000000000000000..d0cc1dddec5592272f9419a3ff8c30435acf082e
1914--- /dev/null
1915+++ b/lib/python/qmk/painter.py
1916@@ -0,0 +1,268 @@
1917+"""Functions that help us work with Quantum Painter's file formats.
1918+"""
1919+import math
1920+import re
1921+from string import Template
1922+from PIL import Image, ImageOps
1923+
1924+# The list of valid formats Quantum Painter supports
1925+valid_formats = {
1926+ 'pal256': {
1927+ 'image_format': 'IMAGE_FORMAT_PALETTE',
1928+ 'bpp': 8,
1929+ 'has_palette': True,
1930+ 'num_colors': 256,
1931+ 'image_format_byte': 0x07, # see qp_internal_formats.h
1932+ },
1933+ 'pal16': {
1934+ 'image_format': 'IMAGE_FORMAT_PALETTE',
1935+ 'bpp': 4,
1936+ 'has_palette': True,
1937+ 'num_colors': 16,
1938+ 'image_format_byte': 0x06, # see qp_internal_formats.h
1939+ },
1940+ 'pal4': {
1941+ 'image_format': 'IMAGE_FORMAT_PALETTE',
1942+ 'bpp': 2,
1943+ 'has_palette': True,
1944+ 'num_colors': 4,
1945+ 'image_format_byte': 0x05, # see qp_internal_formats.h
1946+ },
1947+ 'pal2': {
1948+ 'image_format': 'IMAGE_FORMAT_PALETTE',
1949+ 'bpp': 1,
1950+ 'has_palette': True,
1951+ 'num_colors': 2,
1952+ 'image_format_byte': 0x04, # see qp_internal_formats.h
1953+ },
1954+ 'mono256': {
1955+ 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
1956+ 'bpp': 8,
1957+ 'has_palette': False,
1958+ 'num_colors': 256,
1959+ 'image_format_byte': 0x03, # see qp_internal_formats.h
1960+ },
1961+ 'mono16': {
1962+ 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
1963+ 'bpp': 4,
1964+ 'has_palette': False,
1965+ 'num_colors': 16,
1966+ 'image_format_byte': 0x02, # see qp_internal_formats.h
1967+ },
1968+ 'mono4': {
1969+ 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
1970+ 'bpp': 2,
1971+ 'has_palette': False,
1972+ 'num_colors': 4,
1973+ 'image_format_byte': 0x01, # see qp_internal_formats.h
1974+ },
1975+ 'mono2': {
1976+ 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
1977+ 'bpp': 1,
1978+ 'has_palette': False,
1979+ 'num_colors': 2,
1980+ 'image_format_byte': 0x00, # see qp_internal_formats.h
1981+ }
1982+}
1983+
1984+license_template = """\
1985+// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright
1986+// SPDX-License-Identifier: GPL-2.0-or-later
1987+
1988+// This file was auto-generated by `${generator_command}`
1989+"""
1990+
1991+
1992+def render_license(subs):
1993+ license_txt = Template(license_template)
1994+ return license_txt.substitute(subs)
1995+
1996+
1997+header_file_template = """\
1998+${license}
1999+#pragma once
2000+
2001+#include <qp.h>
2002+
2003+extern const uint32_t ${var_prefix}_${sane_name}_length;
2004+extern const uint8_t ${var_prefix}_${sane_name}[${byte_count}];
2005+"""
2006+
2007+
2008+def render_header(subs):
2009+ header_txt = Template(header_file_template)
2010+ return header_txt.substitute(subs)
2011+
2012+
2013+source_file_template = """\
2014+${license}
2015+#include <qp.h>
2016diff --git a/lib/python/qmk/painter_qff.py b/lib/python/qmk/painter_qff.py
2017new file mode 100644
2018index 0000000000000000000000000000000000000000..746bb166e52147d5c84ab20a30011a142f5f3bd8
2019--- /dev/null
2020+++ b/lib/python/qmk/painter_qff.py
2021@@ -0,0 +1,401 @@
2022+# Copyright 2021 Nick Brassel (@tzarc)
2023+# SPDX-License-Identifier: GPL-2.0-or-later
2024+
2025+# Quantum Font File "QFF" Font File Format.
2026+# See https://docs.qmk.fm/#/quantum_painter_qff for more information.
2027+
2028+from pathlib import Path
2029+from typing import Dict, Any
2030+from colorsys import rgb_to_hsv
2031+from PIL import Image, ImageDraw, ImageFont, ImageChops
2032+from PIL._binary import o8, o16le as o16, o32le as o32
2033+from qmk.painter_qgf import QGFBlockHeader, QGFFramePaletteDescriptorV1
2034+from milc.attrdict import AttrDict
2035+import qmk.painter
2036+
2037+
2038+def o24(i):
2039+ return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
2040+
2041+
2042+########################################################################################################################
2043+
2044+
2045+class QFFGlyphInfo(AttrDict):
2046+ def __init__(self, *args, **kwargs):
2047+ super().__init__()
2048+
2049+ for n, value in enumerate(args):
2050+ self[f'arg:{n}'] = value
2051+
2052+ for key, value in kwargs.items():
2053+ self[key] = value
2054+
2055+ def write(self, fp, include_code_point):
2056+ if include_code_point is True:
2057+ fp.write(o24(ord(self.code_point)))
2058+
2059+ value = ((self.data_offset << 6) & 0xFFFFC0) | (self.w & 0x3F)
2060+ fp.write(o24(value))
2061+
2062+
2063+########################################################################################################################
2064+
2065+
2066+class QFFFontDescriptor:
2067+ type_id = 0x00
2068+ length = 20
2069+ magic = 0x464651
2070+
2071+ def __init__(self):
2072+ self.header = QGFBlockHeader()
2073+ self.header.type_id = QFFFontDescriptor.type_id
2074+ self.header.length = QFFFontDescriptor.length
2075+ self.version = 1
2076+ self.total_file_size = 0
2077+ self.line_height = 0
2078+ self.has_ascii_table = False
2079+ self.unicode_glyph_count = 0
2080+ self.format = 0xFF
2081+ self.flags = 0
2082+ self.compression = 0xFF
2083+ self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
2084+
2085+ def write(self, fp):
2086+ self.header.write(fp)
2087+ fp.write(
2088+ b'' # start off with empty bytes...
2089+ + o24(QFFFontDescriptor.magic) # magic
2090+ + o8(self.version) # version
2091+ + o32(self.total_file_size) # file size
2092+ + o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size
2093+ + o8(self.line_height) # line height
2094+ + o8(1 if self.has_ascii_table is True else 0) # whether or not we have an ascii table present
2095+ + o16(self.unicode_glyph_count & 0xFFFF) # number of unicode glyphs present
2096+ + o8(self.format) # format
2097+ + o8(self.flags) # flags
2098+ + o8(self.compression) # compression
2099+ + o8(self.transparency_index) # transparency index
2100+ )
2101+
2102+ @property
2103+ def is_transparent(self):
2104+ return (self.flags & 0x01) == 0x01
2105+
2106+ @is_transparent.setter
2107+ def is_transparent(self, val):
2108+ if val:
2109+ self.flags |= 0x01
2110+ else:
2111+ self.flags &= ~0x01
2112+
2113+
2114+########################################################################################################################
2115+
2116+
2117+class QFFAsciiGlyphTableV1:
2118+ type_id = 0x01
2119+ length = 95 * 3 # We have 95 glyphs: [0x20...0x7E]
2120+
2121diff --git a/lib/python/qmk/painter_qgf.py b/lib/python/qmk/painter_qgf.py
2122new file mode 100644
2123index 0000000000000000000000000000000000000000..71ce1f5a02ce37f214339a423ff16b848841c5bf
2124--- /dev/null
2125+++ b/lib/python/qmk/painter_qgf.py
2126@@ -0,0 +1,408 @@
2127+# Copyright 2021 Nick Brassel (@tzarc)
2128+# SPDX-License-Identifier: GPL-2.0-or-later
2129+
2130+# Quantum Graphics File "QGF" Image File Format.
2131+# See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
2132+
2133+from colorsys import rgb_to_hsv
2134+from types import FunctionType
2135+from PIL import Image, ImageFile, ImageChops
2136+from PIL._binary import o8, o16le as o16, o32le as o32
2137+import qmk.painter
2138+
2139+
2140+def o24(i):
2141+ return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
2142+
2143+
2144+########################################################################################################################
2145+
2146+
2147+class QGFBlockHeader:
2148+ block_size = 5
2149+
2150+ def write(self, fp):
2151+ fp.write(b'' # start off with empty bytes...
2152+ + o8(self.type_id) # block type id
2153+ + o8((~self.type_id) & 0xFF) # negated block type id
2154+ + o24(self.length) # blob length
2155+ )
2156+
2157+
2158+########################################################################################################################
2159+
2160+
2161+class QGFGraphicsDescriptor:
2162+ type_id = 0x00
2163+ length = 18
2164+ magic = 0x464751
2165+
2166+ def __init__(self):
2167+ self.header = QGFBlockHeader()
2168+ self.header.type_id = QGFGraphicsDescriptor.type_id
2169+ self.header.length = QGFGraphicsDescriptor.length
2170+ self.version = 1
2171+ self.total_file_size = 0
2172+ self.image_width = 0
2173+ self.image_height = 0
2174+ self.frame_count = 0
2175+
2176+ def write(self, fp):
2177+ self.header.write(fp)
2178+ fp.write(
2179+ b'' # start off with empty bytes...
2180+ + o24(QGFGraphicsDescriptor.magic) # magic
2181+ + o8(self.version) # version
2182+ + o32(self.total_file_size) # file size
2183+ + o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size
2184+ + o16(self.image_width) # width
2185+ + o16(self.image_height) # height
2186+ + o16(self.frame_count) # frame count
2187+ )
2188+
2189+
2190+########################################################################################################################
2191+
2192+
2193+class QGFFrameOffsetDescriptorV1:
2194+ type_id = 0x01
2195+
2196+ def __init__(self, frame_count):
2197+ self.header = QGFBlockHeader()
2198+ self.header.type_id = QGFFrameOffsetDescriptorV1.type_id
2199+ self.frame_offsets = [0xFFFFFFFF] * frame_count
2200+ self.frame_count = frame_count
2201+
2202+ def write(self, fp):
2203+ self.header.length = len(self.frame_offsets) * 4
2204+ self.header.write(fp)
2205+ for offset in self.frame_offsets:
2206+ fp.write(b'' # start off with empty bytes...
2207+ + o32(offset) # offset
2208+ )
2209+
2210+
2211+########################################################################################################################
2212+
2213+
2214+class QGFFrameDescriptorV1:
2215+ type_id = 0x02
2216+ length = 6
2217+
2218+ def __init__(self):
2219+ self.header = QGFBlockHeader()
2220+ self.header.type_id = QGFFrameDescriptorV1.type_id
2221+ self.header.length = QGFFrameDescriptorV1.length
2222+ self.format = 0xFF
2223+ self.flags = 0
2224+ self.compression = 0xFF
2225+ self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
2226diff --git a/quantum/main.c b/quantum/main.c
2227index faba668056e1355572e1cd3deaa158da57d55d1c..2d5911b708df42f4bfcc330a1602f2f5d116bad7 100644
2228--- a/quantum/main.c
2229+++ b/quantum/main.c
2230@@ -43,10 +43,6 @@ void protocol_task(void) {
2231 protocol_post_task();
2232 }
2233
2234-#ifdef DEFERRED_EXEC_ENABLE
2235-void deferred_exec_task(void);
2236-#endif // DEFERRED_EXEC_ENABLE
2237-
2238 /** \brief Main
2239 *
2240 * FIXME: Needs doc
2241@@ -63,8 +59,15 @@ int main(void) {
2242 while (true) {
2243 protocol_task();
2244
2245+#ifdef QUANTUM_PAINTER_ENABLE
2246+ // Run Quantum Painter animations
2247+ void qp_internal_animation_tick(void);
2248+ qp_internal_animation_tick();
2249+#endif
2250+
2251 #ifdef DEFERRED_EXEC_ENABLE
2252 // Run deferred executions
2253+ void deferred_exec_task(void);
2254 deferred_exec_task();
2255 #endif // DEFERRED_EXEC_ENABLE
2256
2257diff --git a/quantum/painter/qff.c b/quantum/painter/qff.c
2258new file mode 100644
2259index 0000000000000000000000000000000000000000..cd6af788f9efab704932c70c41cf2dc82f1c8bf4
2260--- /dev/null
2261+++ b/quantum/painter/qff.c
2262@@ -0,0 +1,137 @@
2263+// Copyright 2021 Nick Brassel (@tzarc)
2264+// SPDX-License-Identifier: GPL-2.0-or-later
2265+
2266+// Quantum Font File "QFF" File Format.
2267+// See https://docs.qmk.fm/#/quantum_painter_qff for more information.
2268+
2269+#include "qff.h"
2270+#include "qp_draw.h"
2271+
2272+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2273+// QFF API
2274+
2275+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) {
2276+ // Seek to the start
2277+ qp_stream_setpos(stream, 0);
2278+
2279+ // Read and validate the font descriptor
2280+ qff_font_descriptor_v1_t font_descriptor;
2281+ if (qp_stream_read(&font_descriptor, sizeof(qff_font_descriptor_v1_t), 1, stream) != 1) {
2282+ qp_dprintf("Failed to read font_descriptor, expected length was not %d\n", (int)sizeof(qff_font_descriptor_v1_t));
2283+ return false;
2284+ }
2285+
2286+ // Make sure this block is valid
2287+ if (!qgf_validate_block_header(&font_descriptor.header, QFF_FONT_DESCRIPTOR_TYPEID, (sizeof(qff_font_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) {
2288+ return false;
2289+ }
2290+
2291+ // Make sure the magic and version are correct
2292+ if (font_descriptor.magic != QFF_MAGIC || font_descriptor.qff_version != 0x01) {
2293+ 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);
2294+ return false;
2295+ }
2296+
2297+ // Make sure the file length is valid
2298+ if (font_descriptor.neg_total_file_size != ~font_descriptor.total_file_size) {
2299+ 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);
2300+ return false;
2301+ }
2302+
2303+ // Copy out the required info
2304+ if (line_height) {
2305+ *line_height = font_descriptor.line_height;
2306+ }
2307+ if (has_ascii_table) {
2308+ *has_ascii_table = font_descriptor.has_ascii_table;
2309+ }
2310+ if (num_unicode_glyphs) {
2311+ *num_unicode_glyphs = font_descriptor.num_unicode_glyphs;
2312+ }
2313+ if (bpp || has_palette) {
2314+ if (!qgf_parse_format(font_descriptor.format, bpp, has_palette)) {
2315+ return false;
2316+ }
2317+ }
2318+ if (compression_scheme) {
2319+ *compression_scheme = font_descriptor.compression_scheme;
2320+ }
2321+ if (total_bytes) {
2322+ *total_bytes = font_descriptor.total_file_size;
2323+ }
2324+
2325+ return true;
2326+}
2327+
2328+static bool qff_validate_ascii_descriptor(qp_stream_t *stream) {
2329+ // Read the raw descriptor
2330+ qff_ascii_glyph_table_v1_t ascii_descriptor;
2331+ if (qp_stream_read(&ascii_descriptor, sizeof(qff_ascii_glyph_table_v1_t), 1, stream) != 1) {
2332+ qp_dprintf("Failed to read ascii_descriptor, expected length was not %d\n", (int)sizeof(qff_ascii_glyph_table_v1_t));
2333+ return false;
2334+ }
2335+
2336+ // Make sure this block is valid
2337+ 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)))) {
2338+ return false;
2339+ }
2340+
2341+ return true;
2342+}
2343+
2344+static bool qff_validate_unicode_descriptor(qp_stream_t *stream, uint16_t num_unicode_glyphs) {
2345+ // Read the raw descriptor
2346+ qff_unicode_glyph_table_v1_t unicode_descriptor;
2347+ if (qp_stream_read(&unicode_descriptor, sizeof(qff_unicode_glyph_table_v1_t), 1, stream) != 1) {
2348+ qp_dprintf("Failed to read unicode_descriptor, expected length was not %d\n", (int)sizeof(qff_unicode_glyph_table_v1_t));
2349+ return false;
2350+ }
2351+
2352+ // Make sure this block is valid
2353+ if (!qgf_validate_block_header(&unicode_descriptor.header, QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID, num_unicode_glyphs * 6)) {
2354+ return false;
2355+ }
2356+
2357+ // Skip the necessary amount of data to get to the next block
2358+ qp_stream_seek(stream, num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t), SEEK_CUR);
2359+
2360+ return true;
2361+}
2362diff --git a/quantum/painter/qff.h b/quantum/painter/qff.h
2363new file mode 100644
2364index 0000000000000000000000000000000000000000..6f1a1fd815603617221213cc039cd38a788c1d54
2365--- /dev/null
2366+++ b/quantum/painter/qff.h
2367@@ -0,0 +1,88 @@
2368+// Copyright 2021 Nick Brassel (@tzarc)
2369+// SPDX-License-Identifier: GPL-2.0-or-later
2370+
2371+#pragma once
2372+
2373+// Quantum Font File "QFF" File Format.
2374+// See https://docs.qmk.fm/#/quantum_painter_qff for more information.
2375+
2376+#include <stdint.h>
2377+#include <stdbool.h>
2378+
2379+#include "qp_stream.h"
2380+#include "qp_internal.h"
2381+#include "qgf.h"
2382+
2383+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2384+// QFF structures
2385+
2386+/////////////////////////////////////////
2387+// Font descriptor
2388+
2389+#define QFF_FONT_DESCRIPTOR_TYPEID 0x00
2390+
2391+typedef struct __attribute__((packed)) qff_font_descriptor_v1_t {
2392+ qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 }
2393+ uint32_t magic : 24; // constant, equal to 0x464651 ("QFF")
2394+ uint8_t qff_version; // constant, equal to 0x01
2395+ uint32_t total_file_size; // total size of the entire file, starting at offset zero
2396+ uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors
2397+ uint8_t line_height; // glyph height in pixels
2398+ bool has_ascii_table; // whether the font has an ascii table of glyphs (0x20...0x7E)
2399+ uint16_t num_unicode_glyphs; // the number of glyphs in the unicode table -- no table specified if zero
2400+ qp_image_format_t format : 8; // Frame format, see qp.h.
2401+ uint8_t flags; // frame flags, see below.
2402+ uint8_t compression_scheme; // compression scheme, see below.
2403+ uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
2404+} qff_font_descriptor_v1_t;
2405+
2406+_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");
2407+
2408+#define QFF_MAGIC 0x464651
2409+
2410+/////////////////////////////////////////
2411+// ASCII glyph table descriptor
2412+
2413+#define QFF_ASCII_GLYPH_DESCRIPTOR_TYPEID 0x01
2414+
2415+#define QFF_GLYPH_WIDTH_BITS 6
2416+#define QFF_GLYPH_WIDTH_MASK ((1 << QFF_GLYPH_WIDTH_BITS) - 1)
2417+#define QFF_GLYPH_OFFSET_BITS 18
2418+#define QFF_GLYPH_OFFSET_MASK (((1 << QFF_GLYPH_OFFSET_BITS) - 1) << QFF_GLYPH_WIDTH_BITS)
2419+
2420+typedef struct __attribute__((packed)) qff_ascii_glyph_v1_t {
2421+ uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined
2422+} qff_ascii_glyph_v1_t;
2423+
2424+_Static_assert(sizeof(qff_ascii_glyph_v1_t) == 3, "qff_ascii_glyph_v1_t must be 3 bytes in v1 of QFF");
2425+
2426+typedef struct __attribute__((packed)) qff_ascii_glyph_table_v1_t {
2427+ qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 }
2428+ qff_ascii_glyph_v1_t glyph[95]; // 95 glyphs, 0x20..0x7E
2429+} qff_ascii_glyph_table_v1_t;
2430+
2431+_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");
2432+
2433+/////////////////////////////////////////
2434+// Unicode glyph table descriptor
2435+
2436+#define QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID 0x02
2437+
2438+typedef struct __attribute__((packed)) qff_unicode_glyph_v1_t {
2439+ uint32_t code_point : 24;
2440+ uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined
2441+} qff_unicode_glyph_v1_t;
2442+
2443+_Static_assert(sizeof(qff_unicode_glyph_v1_t) == 6, "qff_unicode_glyph_v1_t must be 6 bytes in v1 of QFF");
2444+
2445+typedef struct __attribute__((packed)) qff_unicode_glyph_table_v1_t {
2446+ qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = (N * 6) }
2447+ qff_unicode_glyph_v1_t glyph[0]; // Extent of '0' signifies that this struct is immediately followed by the glyph data
2448+} qff_unicode_glyph_table_v1_t;
2449+
2450+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2451+// QFF API
2452+
2453+bool qff_validate_stream(qp_stream_t *stream);
2454+uint32_t qff_get_total_size(qp_stream_t *stream);
2455+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);
2456diff --git a/quantum/painter/qgf.c b/quantum/painter/qgf.c
2457new file mode 100644
2458index 0000000000000000000000000000000000000000..834837105bac6968c0eabac15cd606a7e5aea6fa
2459--- /dev/null
2460+++ b/quantum/painter/qgf.c
2461@@ -0,0 +1,292 @@
2462+// Copyright 2021 Nick Brassel (@tzarc)
2463+// SPDX-License-Identifier: GPL-2.0-or-later
2464+
2465+// Quantum Graphics File "QGF" File Format.
2466+// See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
2467+
2468+#include "qgf.h"
2469+#include "qp_draw.h"
2470+
2471+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2472+// QGF API
2473+
2474+bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length) {
2475+ if (desc->type_id != expected_typeid || desc->neg_type_id != ((~expected_typeid) & 0xFF)) {
2476+ 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);
2477+ return false;
2478+ }
2479+
2480+ if (expected_length >= 0 && desc->length != expected_length) {
2481+ 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);
2482+ return false;
2483+ }
2484+
2485+ return true;
2486+}
2487+
2488+bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette) {
2489+ // clang-format off
2490+ static const struct QP_PACKED {
2491+ uint8_t bpp;
2492+ bool has_palette;
2493+ } formats[] = {
2494+ [GRAYSCALE_1BPP] = {.bpp = 1, .has_palette = false},
2495+ [GRAYSCALE_2BPP] = {.bpp = 2, .has_palette = false},
2496+ [GRAYSCALE_4BPP] = {.bpp = 4, .has_palette = false},
2497+ [GRAYSCALE_8BPP] = {.bpp = 8, .has_palette = false},
2498+ [PALETTE_1BPP] = {.bpp = 1, .has_palette = true},
2499+ [PALETTE_2BPP] = {.bpp = 2, .has_palette = true},
2500+ [PALETTE_4BPP] = {.bpp = 4, .has_palette = true},
2501+ [PALETTE_8BPP] = {.bpp = 8, .has_palette = true},
2502+ };
2503+ // clang-format on
2504+
2505+ // Copy out the required info
2506+ if (format > PALETTE_8BPP) {
2507+ qp_dprintf("Failed to parse frame_descriptor, invalid format 0x%02X\n", (int)format);
2508+ return false;
2509+ }
2510+
2511+ // Copy out the required info
2512+ if (bpp) {
2513+ *bpp = formats[format].bpp;
2514+ }
2515+ if (has_palette) {
2516+ *has_palette = formats[format].has_palette;
2517+ }
2518+
2519+ return true;
2520+}
2521+
2522+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) {
2523+ // Decode the format
2524+ qgf_parse_format(frame_descriptor->format, bpp, has_palette);
2525+
2526+ // Copy out the required info
2527+ if (is_delta) {
2528+ *is_delta = (frame_descriptor->flags & QGF_FRAME_FLAG_DELTA) == QGF_FRAME_FLAG_DELTA;
2529+ }
2530+ if (compression_scheme) {
2531+ *compression_scheme = frame_descriptor->compression_scheme;
2532+ }
2533+ if (delay) {
2534+ *delay = frame_descriptor->delay;
2535+ }
2536+
2537+ return true;
2538+}
2539+
2540+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) {
2541+ // Seek to the start
2542+ qp_stream_setpos(stream, 0);
2543+
2544+ // Read and validate the graphics descriptor
2545+ qgf_graphics_descriptor_v1_t graphics_descriptor;
2546+ if (qp_stream_read(&graphics_descriptor, sizeof(qgf_graphics_descriptor_v1_t), 1, stream) != 1) {
2547+ qp_dprintf("Failed to read graphics_descriptor, expected length was not %d\n", (int)sizeof(qgf_graphics_descriptor_v1_t));
2548+ return false;
2549+ }
2550+
2551+ // Make sure this block is valid
2552+ if (!qgf_validate_block_header(&graphics_descriptor.header, QGF_GRAPHICS_DESCRIPTOR_TYPEID, (sizeof(qgf_graphics_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) {
2553+ return false;
2554+ }
2555+
2556+ // Make sure the magic and version are correct
2557+ if (graphics_descriptor.magic != QGF_MAGIC || graphics_descriptor.qgf_version != 0x01) {
2558+ 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);
2559+ return false;
2560+ }
2561diff --git a/quantum/painter/qgf.h b/quantum/painter/qgf.h
2562new file mode 100644
2563index 0000000000000000000000000000000000000000..54585edd0415091264121328a8a6ad1731bdc270
2564--- /dev/null
2565+++ b/quantum/painter/qgf.h
2566@@ -0,0 +1,136 @@
2567+// Copyright 2021 Nick Brassel (@tzarc)
2568+// SPDX-License-Identifier: GPL-2.0-or-later
2569+
2570+#pragma once
2571+
2572+// Quantum Graphics File "QGF" File Format.
2573+// See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
2574+
2575+#include <stdint.h>
2576+#include <stdbool.h>
2577+
2578+#include "qp_stream.h"
2579+#include "qp_internal.h"
2580+
2581+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2582+// QGF structures
2583+
2584+/////////////////////////////////////////
2585+// Common block header
2586+
2587+typedef struct QP_PACKED qgf_block_header_v1_t {
2588+ uint8_t type_id; // See each respective block type below.
2589+ uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors.
2590+ uint32_t length : 24; // 24-bit blob length, allowing for block sizes of a maximum of 16MB.
2591+} qgf_block_header_v1_t;
2592+
2593+_Static_assert(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF");
2594+
2595+/////////////////////////////////////////
2596+// Graphics descriptor
2597+
2598+#define QGF_GRAPHICS_DESCRIPTOR_TYPEID 0x00
2599+
2600+typedef struct QP_PACKED qgf_graphics_descriptor_v1_t {
2601+ qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 }
2602+ uint32_t magic : 24; // constant, equal to 0x464751 ("QGF")
2603+ uint8_t qgf_version; // constant, equal to 0x01
2604+ uint32_t total_file_size; // total size of the entire file, starting at offset zero
2605+ uint32_t neg_total_file_size; // negated value of total_file_size
2606+ uint16_t image_width; // in pixels
2607+ uint16_t image_height; // in pixels
2608+ uint16_t frame_count; // minimum of 1
2609+} qgf_graphics_descriptor_v1_t;
2610+
2611+_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");
2612+
2613+#define QGF_MAGIC 0x464751
2614+
2615+/////////////////////////////////////////
2616+// Frame offset descriptor
2617+
2618+#define QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID 0x01
2619+
2620+typedef struct QP_PACKED qgf_frame_offsets_v1_t {
2621+ qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = (N * sizeof(uint32_t)) }
2622+ uint32_t offset[0]; // '0' signifies that this struct is immediately followed by the frame offsets
2623+} qgf_frame_offsets_v1_t;
2624+
2625+_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");
2626+
2627+/////////////////////////////////////////
2628+// Frame descriptor
2629+
2630+#define QGF_FRAME_DESCRIPTOR_TYPEID 0x02
2631+
2632+typedef struct QP_PACKED qgf_frame_v1_t {
2633+ qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 6 }
2634+ qp_image_format_t format : 8; // Frame format, see qp.h.
2635+ uint8_t flags; // Frame flags, see below.
2636+ painter_compression_t compression_scheme : 8; // Compression scheme, see qp.h.
2637+ uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
2638+ uint16_t delay; // frame delay time for animations (in units of milliseconds)
2639+} qgf_frame_v1_t;
2640+
2641+_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");
2642+
2643+#define QGF_FRAME_FLAG_DELTA 0x02
2644+#define QGF_FRAME_FLAG_TRANSPARENT 0x01
2645+
2646+/////////////////////////////////////////
2647+// Frame palette descriptor
2648+
2649+#define QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID 0x03
2650+
2651+typedef struct QP_PACKED qgf_palette_entry_v1_t {
2652+ uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t.
2653+ uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t.
2654+ uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t.
2655+} qgf_palette_entry_v1_t;
2656+
2657+_Static_assert(sizeof(qgf_palette_entry_v1_t) == 3, "Palette entry is not 3 bytes in size");
2658+
2659+typedef struct QP_PACKED qgf_palette_v1_t {
2660+ qgf_block_header_v1_t header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) }
2661+ 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
2662+} qgf_palette_v1_t;
2663+
2664+_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");
2665+
2666diff --git a/quantum/painter/qp.c b/quantum/painter/qp.c
2667new file mode 100644
2668index 0000000000000000000000000000000000000000..e292ff64973c9890f2af4c4bc429c0a5a68f1590
2669--- /dev/null
2670+++ b/quantum/painter/qp.c
2671@@ -0,0 +1,228 @@
2672+// Copyright 2021 Nick Brassel (@tzarc)
2673+// SPDX-License-Identifier: GPL-2.0-or-later
2674+
2675+#include <quantum.h>
2676+#include <utf8.h>
2677+
2678+#include "qp_internal.h"
2679+#include "qp_comms.h"
2680+#include "qp_draw.h"
2681+
2682+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2683+// Internal driver validation
2684+
2685+static bool validate_driver_vtable(struct painter_driver_t *driver) {
2686diff --git a/quantum/painter/qp.h b/quantum/painter/qp.h
2687new file mode 100644
2688index 0000000000000000000000000000000000000000..e1c14d156cabb61277270e61c873675454920565
2689--- /dev/null
2690+++ b/quantum/painter/qp.h
2691@@ -0,0 +1,453 @@
2692+// Copyright 2021 Nick Brassel (@tzarc)
2693+// SPDX-License-Identifier: GPL-2.0-or-later
2694+
2695+#pragma once
2696+
2697+#include <stdint.h>
2698+#include <stdbool.h>
2699+
2700+#include "deferred_exec.h"
2701+
2702+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2703+// Quantum Painter global configurables (add to your keyboard's config.h)
2704+
2705+#ifndef QUANTUM_PAINTER_NUM_IMAGES
2706+/**
2707+ * @def This controls the maximum number of images that Quantum Painter can load at any one time. Images can be loaded
2708+ * using \ref qp_load_image_mem, and can be unloaded by calling \ref qp_close_image. Increasing this number in
2709+ * order to load more images increases the amount of RAM required. Image data is not held in RAM, just metadata.
2710+ */
2711+# define QUANTUM_PAINTER_NUM_IMAGES 8
2712+#endif // QUANTUM_PAINTER_NUM_IMAGES
2713+
2714+#ifndef QUANTUM_PAINTER_NUM_FONTS
2715+/**
2716+ * @def This controls the maximum number of fonts that Quantum Painter can load. Fonts can be loaded using
2717+ * \ref qp_load_font_mem, and can be unloaded by calling \ref qp_close_font. Increasing this number in order to
2718+ * load more fonts increases the amount of RAM required. Font data is not held in RAM, unless
2719+ * \ref QUANTUM_PAINTER_LOAD_FONTS_TO_RAM is set to TRUE.
2720+ */
2721+# define QUANTUM_PAINTER_NUM_FONTS 4
2722+#endif // QUANTUM_PAINTER_NUM_FONTS
2723+
2724+#ifndef QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
2725+/**
2726+ * @def This controls whether or not fonts should be cached in RAM. Under normal circumstances, fonts can have quite
2727+ * random access patterns, and due to timing of flash memory or external storage, it may be a significant speedup
2728+ * moving the font into RAM before use. Defaults to "off", but if it's enabled it will fallback to reading from the
2729+ * original location if corresponding RAM could not be allocated (such as being too large).
2730+ */
2731+# define QUANTUM_PAINTER_LOAD_FONTS_TO_RAM FALSE
2732+#endif
2733+
2734+#ifndef QUANTUM_PAINTER_CONCURRENT_ANIMATIONS
2735+/**
2736+ * @def This controls the maximum number of animations that Quantum Painter can play simultaneously. Increasing this
2737+ * number in order to play more animations at the same time increases the amount of RAM required.
2738+ */
2739+# define QUANTUM_PAINTER_CONCURRENT_ANIMATIONS 4
2740+#endif // QUANTUM_PAINTER_CONCURRENT_ANIMATIONS
2741+
2742+#ifndef QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE
2743+/**
2744+ * @def This controls the maximum size of the pixel data buffer used for single blocks of transmission. Larger buffers
2745+ * means more data is processed at one time, with less frequent transmissions, at the cost of RAM.
2746+ */
2747+# define QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE 32
2748+#endif
2749+
2750+#ifndef QUANTUM_PAINTER_SUPPORTS_256_PALETTE
2751+/**
2752+ * @def This controls whether 256-color palettes are supported. This has relatively hefty requirements on RAM -- at
2753+ * least 1kB extra is required just to store the palette information, with more required for other metadata.
2754+ */
2755+# define QUANTUM_PAINTER_SUPPORTS_256_PALETTE FALSE
2756+#endif
2757+
2758+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2759+// Quantum Painter types
2760+
2761+/**
2762+ * @typedef A handle to a Quantum Painter device, such as an LCD or OLED. Most Quantum Painter APIs require this
2763+ * argument in order to perform operations on the display.
2764+ */
2765+typedef const void *painter_device_t;
2766+
2767+/**
2768+ * @typedef The desired rotation of a panel. Used as a parameter to \ref qp_init, and can be queried by
2769+ * \ref qp_get_geometry.
2770+ */
2771+typedef enum { QP_ROTATION_0, QP_ROTATION_90, QP_ROTATION_180, QP_ROTATION_270 } painter_rotation_t;
2772+
2773+/**
2774+ * @typedef A descriptor for a Quantum Painter image.
2775+ */
2776+typedef struct painter_image_desc_t {
2777+ uint16_t width; ///< Image width
2778+ uint16_t height; ///< Image height
2779+ uint16_t frame_count; ///< Number of frames in this image
2780+} painter_image_desc_t;
2781+
2782+/**
2783+ * @typedef A handle to a Quantum Painter image.
2784+ */
2785+typedef const painter_image_desc_t *painter_image_handle_t;
2786+
2787+/**
2788+ * @typedef A descriptor for a Quantum Painter font.
2789+ */
2790+typedef struct painter_font_desc_t {
2791diff --git a/quantum/painter/qp_comms.c b/quantum/painter/qp_comms.c
2792new file mode 100644
2793index 0000000000000000000000000000000000000000..dc17b4946003d72cd1c7b1b8b6ecaf5d15f41e91
2794--- /dev/null
2795+++ b/quantum/painter/qp_comms.c
2796@@ -0,0 +1,72 @@
2797+// Copyright 2021 Nick Brassel (@tzarc)
2798+// SPDX-License-Identifier: GPL-2.0-or-later
2799+
2800+#include "qp_comms.h"
2801+
2802+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2803+// Base comms APIs
2804+
2805+bool qp_comms_init(painter_device_t device) {
2806+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
2807+ if (!driver->validate_ok) {
2808+ qp_dprintf("qp_comms_init: fail (validation_ok == false)\n");
2809+ return false;
2810+ }
2811+
2812+ return driver->comms_vtable->comms_init(device);
2813+}
2814+
2815+bool qp_comms_start(painter_device_t device) {
2816+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
2817+ if (!driver->validate_ok) {
2818+ qp_dprintf("qp_comms_start: fail (validation_ok == false)\n");
2819+ return false;
2820+ }
2821+
2822+ return driver->comms_vtable->comms_start(device);
2823+}
2824+
2825+void qp_comms_stop(painter_device_t device) {
2826+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
2827+ if (!driver->validate_ok) {
2828+ qp_dprintf("qp_comms_stop: fail (validation_ok == false)\n");
2829+ return;
2830+ }
2831+
2832+ driver->comms_vtable->comms_stop(device);
2833+}
2834+
2835+uint32_t qp_comms_send(painter_device_t device, const void *data, uint32_t byte_count) {
2836+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
2837+ if (!driver->validate_ok) {
2838+ qp_dprintf("qp_comms_send: fail (validation_ok == false)\n");
2839+ return false;
2840+ }
2841+
2842+ return driver->comms_vtable->comms_send(device, data, byte_count);
2843+}
2844+
2845+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2846+// Comms APIs that use a D/C pin
2847+
2848+void qp_comms_command(painter_device_t device, uint8_t cmd) {
2849+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
2850+ struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable;
2851+ comms_vtable->send_command(device, cmd);
2852+}
2853+
2854+void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data) {
2855+ qp_comms_command(device, cmd);
2856+ qp_comms_send(device, &data, sizeof(data));
2857+}
2858+
2859+uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void *data, uint32_t byte_count) {
2860+ qp_comms_command(device, cmd);
2861+ return qp_comms_send(device, data, byte_count);
2862+}
2863+
2864+void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
2865+ struct painter_driver_t * driver = (struct painter_driver_t *)device;
2866+ struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable;
2867+ comms_vtable->bulk_command_sequence(device, sequence, sequence_len);
2868+}
2869diff --git a/quantum/painter/qp_comms.h b/quantum/painter/qp_comms.h
2870new file mode 100644
2871index 0000000000000000000000000000000000000000..8fbf25c201cb766c9373e057274eb1f766bf491a
2872--- /dev/null
2873+++ b/quantum/painter/qp_comms.h
2874@@ -0,0 +1,25 @@
2875+// Copyright 2021 Nick Brassel (@tzarc)
2876+// SPDX-License-Identifier: GPL-2.0-or-later
2877+
2878+#pragma once
2879+
2880+#include <stdbool.h>
2881+#include <stdlib.h>
2882+
2883+#include "qp_internal.h"
2884+
2885+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2886+// Base comms APIs
2887+
2888+bool qp_comms_init(painter_device_t device);
2889+bool qp_comms_start(painter_device_t device);
2890+void qp_comms_stop(painter_device_t device);
2891+uint32_t qp_comms_send(painter_device_t device, const void* data, uint32_t byte_count);
2892+
2893+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2894+// Comms APIs that use a D/C pin
2895+
2896+void qp_comms_command(painter_device_t device, uint8_t cmd);
2897+void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data);
2898+uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void* data, uint32_t byte_count);
2899+void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len);
2900diff --git a/quantum/painter/qp_draw.h b/quantum/painter/qp_draw.h
2901new file mode 100644
2902index 0000000000000000000000000000000000000000..7094d80eaa6815980b14f76b2e11bc51ff9075dd
2903--- /dev/null
2904+++ b/quantum/painter/qp_draw.h
2905@@ -0,0 +1,85 @@
2906+// Copyright 2021 Nick Brassel (@tzarc)
2907+// SPDX-License-Identifier: GPL-2.0-or-later
2908+
2909+#pragma once
2910+
2911+#include "qp_internal.h"
2912+#include "qp_stream.h"
2913+
2914+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
2915+// Quantum Painter utility functions
2916+
2917+// Global variable used for native pixel data streaming.
2918+extern uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];
2919+
2920+// Check if the supplied bpp is capable of being rendered
2921+bool qp_internal_bpp_capable(uint8_t bits_per_pixel);
2922+
2923+// Returns the number of pixels that can fit in the pixdata buffer
2924+uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device);
2925+
2926+// Fills the supplied buffer with equivalent native pixels matching the supplied HSV
2927+void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val);
2928+
2929+// qp_setpixel internal implementation, but uses the global pixdata buffer with pre-converted native pixel. Only the first pixel is used.
2930+bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y);
2931+
2932+// qp_rect internal implementation, but uses the global pixdata buffer with pre-converted native pixels.
2933+bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t l, uint16_t t, uint16_t r, uint16_t b);
2934+
2935+// Convert from input pixel data + palette to equivalent pixels
2936+typedef int16_t (*qp_internal_byte_input_callback)(void* cb_arg);
2937+typedef bool (*qp_internal_pixel_output_callback)(qp_pixel_t* palette, uint8_t index, void* cb_arg);
2938diff --git a/quantum/painter/qp_draw_circle.c b/quantum/painter/qp_draw_circle.c
2939new file mode 100644
2940index 0000000000000000000000000000000000000000..edaae358356b758b36568fae1a1b43bed36416c8
2941--- /dev/null
2942+++ b/quantum/painter/qp_draw_circle.c
2943@@ -0,0 +1,172 @@
2944+// Copyright 2021 Paul Cotter (@gr1mr3aver)
2945+// Copyright 2021 Nick Brassel (@tzarc)
2946+// SPDX-License-Identifier: GPL-2.0-or-later
2947+
2948+#include "qp.h"
2949+#include "qp_internal.h"
2950+#include "qp_comms.h"
2951+#include "qp_draw.h"
2952+
2953+// Utilize 8-way symmetry to draw circles
2954+static bool qp_circle_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) {
2955+ /*
2956+ Circles have the property of 8-way symmetry, so eight pixels can be drawn
2957+ for each computed [offsetx,offsety] given the center coordinates
2958+ represented by [centerx,centery].
2959+
2960+ For filled circles, we can draw horizontal lines between each pair of
2961+ pixels with the same final value of y.
2962+
2963+ Two special cases exist and have been optimized:
2964+ 1) offsetx == offsety (the final point), makes half the coordinates
2965+ equivalent, so we can omit them (and the corresponding fill lines)
2966+ 2) offsetx == 0 (the starting point) means that some horizontal lines
2967+ would be a single pixel in length, so we write individual pixels instead.
2968+ This also makes half the symmetrical points identical to their twins,
2969+ so we only need four points or two points and one line
2970+ */
2971+
2972+ int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx);
2973+ int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx);
2974+ int16_t xpy = ((int16_t)centerx) + ((int16_t)offsety);
2975+ int16_t xmy = ((int16_t)centerx) - ((int16_t)offsety);
2976+ int16_t ypx = ((int16_t)centery) + ((int16_t)offsetx);
2977+ int16_t ymx = ((int16_t)centery) - ((int16_t)offsetx);
2978+ int16_t ypy = ((int16_t)centery) + ((int16_t)offsety);
2979+ int16_t ymy = ((int16_t)centery) - ((int16_t)offsety);
2980+
2981+ if (offsetx == 0) {
2982+ if (!qp_internal_setpixel_impl(device, centerx, ypy)) {
2983+ return false;
2984+ }
2985+ if (!qp_internal_setpixel_impl(device, centerx, ymy)) {
2986+ return false;
2987+ }
2988+ if (filled) {
2989+ if (!qp_internal_fillrect_helper_impl(device, xpy, centery, xmy, centery)) {
2990+ return false;
2991+ }
2992+ } else {
2993+ if (!qp_internal_setpixel_impl(device, xpy, centery)) {
2994+ return false;
2995+ }
2996+ if (!qp_internal_setpixel_impl(device, xmy, centery)) {
2997+ return false;
2998+ }
2999+ }
3000+ } else if (offsetx == offsety) {
3001+ if (filled) {
3002+ if (!qp_internal_fillrect_helper_impl(device, xpy, ypy, xmy, ypy)) {
3003+ return false;
3004+ }
3005+ if (!qp_internal_fillrect_helper_impl(device, xpy, ymy, xmy, ymy)) {
3006+ return false;
3007+ }
3008+ } else {
3009+ if (!qp_internal_setpixel_impl(device, xpy, ypy)) {
3010+ return false;
3011+ }
3012+ if (!qp_internal_setpixel_impl(device, xmy, ypy)) {
3013+ return false;
3014+ }
3015+ if (!qp_internal_setpixel_impl(device, xpy, ymy)) {
3016+ return false;
3017+ }
3018+ if (!qp_internal_setpixel_impl(device, xmy, ymy)) {
3019+ return false;
3020+ }
3021+ }
3022+
3023+ } else {
3024+ if (filled) {
3025+ if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) {
3026+ return false;
3027+ }
3028+ if (!qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) {
3029+ return false;
3030+ }
3031+ if (!qp_internal_fillrect_helper_impl(device, xpy, ypx, xmy, ypx)) {
3032+ return false;
3033+ }
3034+ if (!qp_internal_fillrect_helper_impl(device, xpy, ymx, xmy, ymx)) {
3035+ return false;
3036+ }
3037+ } else {
3038+ if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
3039+ return false;
3040+ }
3041+ if (!qp_internal_setpixel_impl(device, xmx, ypy)) {
3042+ return false;
3043diff --git a/quantum/painter/qp_draw_codec.c b/quantum/painter/qp_draw_codec.c
3044new file mode 100644
3045index 0000000000000000000000000000000000000000..438dce399460d7e9646e8d68ed42b5d3a6cf02a2
3046--- /dev/null
3047+++ b/quantum/painter/qp_draw_codec.c
3048@@ -0,0 +1,142 @@
3049+// Copyright 2021 Nick Brassel (@tzarc)
3050+// SPDX-License-Identifier: GPL-2.0-or-later
3051+
3052+#include "qp_internal.h"
3053+#include "qp_draw.h"
3054+#include "qp_comms.h"
3055+
3056+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3057+// Palette / Monochrome-format decoder
3058+
3059+static const qp_pixel_t qp_pixel_white = {.hsv888 = {.h = 0, .s = 0, .v = 255}};
3060+static const qp_pixel_t qp_pixel_black = {.hsv888 = {.h = 0, .s = 0, .v = 0}};
3061+
3062+bool qp_internal_bpp_capable(uint8_t bits_per_pixel) {
3063+#if !(QUANTUM_PAINTER_SUPPORTS_256_PALETTE)
3064+ if (bits_per_pixel > 4) {
3065+ qp_dprintf("qp_internal_decode_palette: image bpp greater than 4\n");
3066+ return false;
3067+ }
3068+#endif
3069+
3070+ if (bits_per_pixel > 8) {
3071+ qp_dprintf("qp_internal_decode_palette: image bpp greater than 8\n");
3072+ return false;
3073+ }
3074+
3075+ return true;
3076+}
3077+
3078diff --git a/quantum/painter/qp_draw_core.c b/quantum/painter/qp_draw_core.c
3079new file mode 100644
3080index 0000000000000000000000000000000000000000..c31c734132bf20fbe9548d0585cd62835d51896e
3081--- /dev/null
3082+++ b/quantum/painter/qp_draw_core.c
3083@@ -0,0 +1,294 @@
3084+// Copyright 2021-2022 Nick Brassel (@tzarc)
3085+// Copyright 2021 Paul Cotter (@gr1mr3aver)
3086+// SPDX-License-Identifier: GPL-2.0-or-later
3087+
3088+#include "qp_internal.h"
3089+#include "qp_comms.h"
3090+#include "qp_draw.h"
3091+#include "qgf.h"
3092+
3093+_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");
3094+
3095+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3096+// Global variables
3097+//
3098+// NOTE: The variables in this section are intentionally outside a stack frame. They are able to be defined with larger
3099+// sizes than the normal stack frames would allow, and as such need to be external.
3100+//
3101+// **** DO NOT refactor this and decide to place the variables inside the function calling them -- you will ****
3102+// **** very likely get artifacts rendered to the screen as a result. ****
3103+//
3104+
3105+// Buffer used for transmitting native pixel data to the downstream device.
3106+uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];
3107+
3108+// Static buffer to contain a generated color palette
3109+static bool generated_palette = false;
3110+static int16_t generated_steps = -1;
3111+static qp_pixel_t interpolated_fg_hsv888;
3112+static qp_pixel_t interpolated_bg_hsv888;
3113+#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE
3114+qp_pixel_t qp_internal_global_pixel_lookup_table[256];
3115+#else
3116+qp_pixel_t qp_internal_global_pixel_lookup_table[16];
3117+#endif
3118+
3119+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3120+// Helpers
3121+
3122+uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device) {
3123+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
3124+ return ((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE * 8) / driver->native_bits_per_pixel);
3125+}
3126+
3127+// qp_setpixel internal implementation, but accepts a buffer with pre-converted native pixel. Only the first pixel is used.
3128+bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y) {
3129+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
3130+ return driver->driver_vtable->viewport(device, x, y, x, y) && driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, 1);
3131+}
3132+
3133+// Fills the global native pixel buffer with equivalent pixels matching the supplied HSV
3134+void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val) {
3135+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
3136+ uint32_t pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
3137+ num_pixels = QP_MIN(pixels_in_pixdata, num_pixels);
3138+
3139+ // Convert the color to native pixel format
3140+ qp_pixel_t color = {.hsv888 = {.h = hue, .s = sat, .v = val}};
3141+ driver->driver_vtable->palette_convert(device, 1, &color);
3142+
3143+ // Append the required number of pixels
3144+ uint8_t palette_idx = 0;
3145+ for (uint32_t i = 0; i < num_pixels; ++i) {
3146+ driver->driver_vtable->append_pixels(device, qp_internal_global_pixdata_buffer, &color, i, 1, &palette_idx);
3147+ }
3148+}
3149+
3150+// 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.
3151+void qp_internal_invalidate_palette(void) {
3152+ generated_palette = false;
3153+ generated_steps = -1;
3154+}
3155+
3156+// Interpolates between two colors to generate a palette
3157+bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps) {
3158+ // Check if we need to generate a new palette -- if the input parameters match then assume the palette can stay unchanged.
3159+ // This may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() to reset.
3160+ 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) {
3161+ // We already have the correct palette, no point regenerating it.
3162+ return false;
3163+ }
3164+
3165+ // Save the parameters so we know whether we can skip generation
3166+ generated_palette = true;
3167+ generated_steps = steps;
3168+ interpolated_fg_hsv888 = fg_hsv888;
3169+ interpolated_bg_hsv888 = bg_hsv888;
3170+
3171+ int16_t hue_fg = fg_hsv888.hsv888.h;
3172+ int16_t hue_bg = bg_hsv888.hsv888.h;
3173+
3174+ // Make sure we take the "shortest" route from one hue to the other
3175+ if ((hue_fg - hue_bg) >= 128) {
3176+ hue_bg += 256;
3177+ } else if ((hue_fg - hue_bg) <= -128) {
3178+ hue_bg -= 256;
3179+ }
3180+
3181+ // Interpolate each of the lookup table entries
3182+ for (int16_t i = 0; i < steps; ++i) {
3183diff --git a/quantum/painter/qp_draw_ellipse.c b/quantum/painter/qp_draw_ellipse.c
3184new file mode 100644
3185index 0000000000000000000000000000000000000000..7f2f4abcfdd52e9ae3d46c79ce1c6f18d11aa248
3186--- /dev/null
3187+++ b/quantum/painter/qp_draw_ellipse.c
3188@@ -0,0 +1,116 @@
3189+// Copyright 2021 Paul Cotter (@gr1mr3aver)
3190+// Copyright 2021 Nick Brassel (@tzarc)
3191+// SPDX-License-Identifier: GPL-2.0-or-later
3192+
3193+#include "qp_internal.h"
3194+#include "qp_comms.h"
3195+#include "qp_draw.h"
3196+
3197+// Utilize 4-way symmetry to draw an ellipse
3198+static bool qp_ellipse_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) {
3199+ /*
3200+ Ellipses have the property of 4-way symmetry, so four pixels can be drawn
3201+ for each computed [offsetx,offsety] given the center coordinates
3202+ represented by [centerx,centery].
3203+
3204+ For filled ellipses, we can draw horizontal lines between each pair of
3205+ pixels with the same final value of y.
3206+
3207+ When offsetx == 0 only two pixels can be drawn for filled or unfilled ellipses
3208+ */
3209+
3210+ int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx);
3211+ int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx);
3212+ int16_t ypy = ((int16_t)centery) + ((int16_t)offsety);
3213+ int16_t ymy = ((int16_t)centery) - ((int16_t)offsety);
3214+
3215+ if (offsetx == 0) {
3216+ if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
3217+ return false;
3218+ }
3219+ if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
3220+ return false;
3221+ }
3222+ } else if (filled) {
3223+ if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) {
3224+ return false;
3225+ }
3226+ if (offsety > 0 && !qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) {
3227+ return false;
3228+ }
3229+ } else {
3230+ if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
3231+ return false;
3232+ }
3233+ if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
3234+ return false;
3235+ }
3236+ if (!qp_internal_setpixel_impl(device, xmx, ypy)) {
3237+ return false;
3238+ }
3239+ if (!qp_internal_setpixel_impl(device, xmx, ymy)) {
3240+ return false;
3241+ }
3242+ }
3243+
3244+ return true;
3245+}
3246+
3247+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3248+// Quantum Painter External API: qp_ellipse
3249+
3250+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) {
3251+ qp_dprintf("qp_ellipse: entry\n");
3252+ struct painter_driver_t *driver = (struct painter_driver_t *)device;
3253+ if (!driver->validate_ok) {
3254+ qp_dprintf("qp_ellipse: fail (validation_ok == false)\n");
3255+ return false;
3256+ }
3257+
3258+ int16_t aa = ((int16_t)sizex) * ((int16_t)sizex);
3259+ int16_t bb = ((int16_t)sizey) * ((int16_t)sizey);
3260+ int16_t fa = 4 * ((int16_t)aa);
3261+ int16_t fb = 4 * ((int16_t)bb);
3262+
3263+ int16_t dx = 0;
3264+ int16_t dy = ((int16_t)sizey);
3265+
3266+ qp_internal_fill_pixdata(device, QP_MAX(sizex, sizey), hue, sat, val);
3267+
3268+ if (!qp_comms_start(device)) {
3269+ qp_dprintf("qp_ellipse: fail (could not start comms)\n");
3270+ return false;
3271+ }
3272+
3273+ bool ret = true;
3274+ for (int16_t delta = (2 * bb) + (aa * (1 - (2 * sizey))); bb * dx <= aa * dy; dx++) {
3275+ if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) {
3276+ ret = false;
3277+ break;
3278+ }
3279+ if (delta >= 0) {
3280+ delta += fa * (1 - dy);
3281+ dy--;
3282+ }
3283+ delta += bb * (4 * dx + 6);
3284+ }
3285+
3286+ dx = sizex;
3287+ dy = 0;
3288diff --git a/quantum/painter/qp_draw_image.c b/quantum/painter/qp_draw_image.c
3289new file mode 100644
3290index 0000000000000000000000000000000000000000..5134ae7e99ae177b730eb12e7ce36b118ab87b69
3291--- /dev/null
3292+++ b/quantum/painter/qp_draw_image.c
3293@@ -0,0 +1,382 @@
3294+// Copyright 2021 Nick Brassel (@tzarc)
3295+// SPDX-License-Identifier: GPL-2.0-or-later
3296+
3297+#include "qp_internal.h"
3298+#include "qp_draw.h"
3299+#include "qp_comms.h"
3300+#include "qgf.h"
3301+#include "deferred_exec.h"
3302+
3303+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3304+// QGF image handles
3305+
3306+typedef struct qgf_image_handle_t {
3307+ painter_image_desc_t base;
3308+ bool validate_ok;
3309+ union {
3310+ qp_stream_t stream;
3311+ qp_memory_stream_t mem_stream;
3312+#ifdef QP_STREAM_HAS_FILE_IO
3313+ qp_file_stream_t file_stream;
3314+#endif // QP_STREAM_HAS_FILE_IO
3315+ };
3316+} qgf_image_handle_t;
3317+
3318+static qgf_image_handle_t image_descriptors[QUANTUM_PAINTER_NUM_IMAGES] = {0};
3319+
3320+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3321+// Quantum Painter External API: qp_load_image_mem
3322+
3323+painter_image_handle_t qp_load_image_mem(const void *buffer) {
3324+ qp_dprintf("qp_load_image_mem: entry\n");
3325+ qgf_image_handle_t *image = NULL;
3326+
3327+ // Find a free slot
3328+ for (int i = 0; i < QUANTUM_PAINTER_NUM_IMAGES; ++i) {
3329+ if (!image_descriptors[i].validate_ok) {
3330+ image = &image_descriptors[i];
3331+ break;
3332+ }
3333+ }
3334+
3335+ // Drop out if not found
3336+ if (!image) {
3337+ qp_dprintf("qp_load_image_mem: fail (no free slot)\n");
3338+ return NULL;
3339+ }
3340+
3341+ // Assume we can read the graphics descriptor
3342+ image->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qgf_graphics_descriptor_v1_t));
3343+
3344+ // Update the length of the stream to match, and rewind to the start
3345+ image->mem_stream.length = qgf_get_total_size(&image->stream);
3346+ image->mem_stream.position = 0;
3347+
3348+ // Now that we know the length, validate the input data
3349+ if (!qgf_validate_stream(&image->stream)) {
3350+ qp_dprintf("qp_load_image_mem: fail (failed validation)\n");
3351+ return NULL;
3352+ }
3353+
3354+ // Fill out the QP image descriptor
3355+ qgf_read_graphics_descriptor(&image->stream, &image->base.width, &image->base.height, &image->base.frame_count, NULL);
3356+
3357+ // Validation success, we can return the handle
3358+ image->validate_ok = true;
3359+ qp_dprintf("qp_load_image_mem: ok\n");
3360+ return (painter_image_handle_t)image;
3361+}
3362+
3363+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3364+// Quantum Painter External API: qp_close_image
3365+
3366+bool qp_close_image(painter_image_handle_t image) {
3367+ qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
3368+ if (!qgf_image->validate_ok) {
3369+ qp_dprintf("qp_close_image: fail (invalid image)\n");
3370+ return false;
3371+ }
3372+
3373+ // Free up this image for use elsewhere.
3374+ qgf_image->validate_ok = false;
3375+ return true;
3376+}
3377+
3378+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3379+// Quantum Painter External API: qp_drawimage
3380+
3381+bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
3382+ return qp_drawimage_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
3383+}
3384+
3385+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3386+// Quantum Painter External API: qp_drawimage_recolor
3387+
3388+typedef struct qgf_frame_info_t {
3389+ painter_compression_t compression_scheme;
3390+ uint8_t bpp;
3391+ bool has_palette;
3392+ bool is_delta;
3393diff --git a/quantum/painter/qp_draw_text.c b/quantum/painter/qp_draw_text.c
3394new file mode 100644
3395index 0000000000000000000000000000000000000000..f99e082cad26d44a75cd55eb1d90228cdc0695cf
3396--- /dev/null
3397+++ b/quantum/painter/qp_draw_text.c
3398@@ -0,0 +1,444 @@
3399+// Copyright 2021 Nick Brassel (@tzarc)
3400+// SPDX-License-Identifier: GPL-2.0-or-later
3401+
3402+#include <quantum.h>
3403+#include <utf8.h>
3404+
3405+#include "qp_internal.h"
3406+#include "qp_draw.h"
3407+#include "qp_comms.h"
3408+#include "qff.h"
3409+
3410+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3411+// QFF font handles
3412+
3413+typedef struct qff_font_handle_t {
3414+ painter_font_desc_t base;
3415+ bool validate_ok;
3416+ bool has_ascii_table;
3417+ uint16_t num_unicode_glyphs;
3418+ uint8_t bpp;
3419+ bool has_palette;
3420+ painter_compression_t compression_scheme;
3421+ union {
3422+ qp_stream_t stream;
3423+ qp_memory_stream_t mem_stream;
3424+#ifdef QP_STREAM_HAS_FILE_IO
3425+ qp_file_stream_t file_stream;
3426+#endif // QP_STREAM_HAS_FILE_IO
3427+ };
3428+#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
3429+ bool owns_buffer;
3430+ void *buffer;
3431+#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
3432+} qff_font_handle_t;
3433+
3434+static qff_font_handle_t font_descriptors[QUANTUM_PAINTER_NUM_FONTS] = {0};
3435+
3436+////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
3437+// Quantum Painter External API: qp_load_font_mem
3438+
3439+painter_font_handle_t qp_load_font_mem(const void *buffer) {
3440+ qp_dprintf("qp_load_font_mem: entry\n");
3441+ qff_font_handle_t *font = NULL;
3442+
3443+ // Find a free slot
3444+ for (int i = 0; i < QUANTUM_PAINTER_NUM_FONTS; ++i) {
3445+ if (!font_descriptors[i].validate_ok) {
3446+ font = &font_descriptors[i];
3447+ break;
3448+ }
3449+ }
3450+
3451+ // Drop out if not found
3452+ if (!font) {
3453+ qp_dprintf("qp_load_font_mem: fail (no free slot)\n");
3454+ return NULL;
3455+ }
3456+
3457+ // Assume we can read the graphics descriptor
3458+ font->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qff_font_descriptor_v1_t));
3459+
3460+ // Update the length of the stream to match, and rewind to the start
3461+ font->mem_stream.length = qff_get_total_size(&font->stream);
3462+ font->mem_stream.position = 0;
3463+
3464+ // Now that we know the length, validate the input data
3465+ if (!qff_validate_stream(&font->stream)) {
3466+ qp_dprintf("qp_load_font_mem: fail (failed validation)\n");
3467+ return NULL;
3468+ }
3469+
3470+#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
3471+ // Clear out any existing data
3472+ font->owns_buffer = false;
3473+ font->buffer = NULL;
3474+
3475+ void *ram_buffer = malloc(font->mem_stream.length);
3476+ if (ram_buffer == NULL) {
3477+ qp_dprintf("qp_load_font_mem: could not allocate enough RAM for font, falling back to original\n");
3478+ } else {
3479+ do {
3480+ // Copy the data into RAM
3481+ if (qp_stream_read(ram_buffer, 1, font->mem_stream.length, &font->mem_stream) != font->mem_stream.length) {
3482+ qp_dprintf("qp_load_font_mem: could not copy from flash to RAM, falling back to original\n");
3483+ break;
3484+ }
3485+
3486+ // Create the new stream with the new buffer
3487+ font->buffer = ram_buffer;
3488+ font->owns_buffer = true;
3489+ font->mem_stream = qp_make_memory_stream(font->buffer, font->mem_stream.length);
3490+ } while (0);
3491+ }
3492+
3493+ // Free the buffer if we were unable to recreate the RAM copy.
3494+ if (ram_buffer != NULL && !font->owns_buffer) {
3495+ free(ram_buffer);
3496+ }
3497+#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM