1f2b1dedccdf21b629c45ece80b4ca32f6653296

Author
Nick Brassel <nick@tzarc.org>
Committer
GitHub <noreply@github.com>
Date

Message

Quantum Painter (#10174)

* Install dependencies before executing unit tests.

* Split out UTF-8 decoder.

* Fixup python formatting rules.

* Add documentation for QGF/QFF and the RLE format used.

* Add CLI commands for converting images and fonts.

* Add stub rules.mk for QP.

* Add stream type.

* Add base driver and comms interfaces.

* Add support for SPI, SPI+D/C comms drivers.

* Include <qp.h> when enabled.

* Add base support for SPI+D/C+RST panels, as well as concrete implementation of ST7789.

* Add support for GC9A01.

* Add support for ILI9341.

* Add support for ILI9163.

* Add support for SSD1351.

* Implement qp_setpixel, including pixdata buffer management.

* Implement qp_line.

* Implement qp_rect.

* Implement qp_circle.

* Implement qp_ellipse.

* Implement palette interpolation.

* Allow for streams to work with either flash or RAM.

* Image loading.

* Font loading.

* QGF palette loading.

* Progressive decoder of pixel data supporting Raw+RLE, 1-,2-,4-,8-bpp monochrome and palette-based images.

* Image drawing.

* Animations.

* Font rendering.

* Check against 256 colours, dump out the loaded palette if debugging enabled.

* Fix build.

* AVR is not the intended audience.

* `qmk format-c`

* Generation fix.

* First batch of docs.

* More docs and examples.

* Review comments.

* Public API documentation.

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