summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNick Brassel <nick@tzarc.org>2022-04-13 18:00:18 +1000
committerGitHub <noreply@github.com>2022-04-13 18:00:18 +1000
commit1f2b1dedccdf21b629c45ece80b4ca32f6653296 (patch)
treea4283b928fe11c6662be10067314531f12774152
parent1dbbd2b6b068b9f921ebc0341c890df16a491007 (diff)
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.
-rw-r--r--.github/workflows/unit_test.yml2
-rw-r--r--builddefs/common_features.mk8
-rw-r--r--docs/_summary.md1
-rw-r--r--docs/cli_commands.md12
-rw-r--r--docs/quantum_painter.md705
-rw-r--r--docs/quantum_painter_qff.md103
-rw-r--r--docs/quantum_painter_qgf.md178
-rw-r--r--docs/quantum_painter_rle.md29
-rw-r--r--drivers/painter/comms/qp_comms_spi.c137
-rw-r--r--drivers/painter/comms/qp_comms_spi.h51
-rw-r--r--drivers/painter/gc9a01/qp_gc9a01.c150
-rw-r--r--drivers/painter/gc9a01/qp_gc9a01.h37
-rw-r--r--drivers/painter/gc9a01/qp_gc9a01_opcodes.h78
-rw-r--r--drivers/painter/ili9xxx/qp_ili9163.c121
-rw-r--r--drivers/painter/ili9xxx/qp_ili9163.h37
-rw-r--r--drivers/painter/ili9xxx/qp_ili9341.c128
-rw-r--r--drivers/painter/ili9xxx/qp_ili9341.h37
-rw-r--r--drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h100
-rw-r--r--drivers/painter/ssd1351/qp_ssd1351.c125
-rw-r--r--drivers/painter/ssd1351/qp_ssd1351.h37
-rw-r--r--drivers/painter/ssd1351/qp_ssd1351_opcodes.h48
-rw-r--r--drivers/painter/st77xx/qp_st7789.c144
-rw-r--r--drivers/painter/st77xx/qp_st7789.h44
-rw-r--r--drivers/painter/st77xx/qp_st7789_opcodes.h64
-rw-r--r--drivers/painter/st77xx/qp_st77xx_opcodes.h51
-rw-r--r--drivers/painter/tft_panel/qp_tft_panel.c130
-rw-r--r--drivers/painter/tft_panel/qp_tft_panel.h67
-rw-r--r--lib/python/qmk/cli/__init__.py4
-rw-r--r--lib/python/qmk/cli/painter/__init__.py2
-rw-r--r--lib/python/qmk/cli/painter/convert_graphics.py86
-rw-r--r--lib/python/qmk/cli/painter/make_font.py87
-rw-r--r--lib/python/qmk/painter.py268
-rw-r--r--lib/python/qmk/painter_qff.py401
-rw-r--r--lib/python/qmk/painter_qgf.py408
-rw-r--r--quantum/main.c11
-rw-r--r--quantum/painter/qff.c137
-rw-r--r--quantum/painter/qff.h88
-rw-r--r--quantum/painter/qgf.c292
-rw-r--r--quantum/painter/qgf.h136
-rw-r--r--quantum/painter/qp.c228
-rw-r--r--quantum/painter/qp.h453
-rw-r--r--quantum/painter/qp_comms.c72
-rw-r--r--quantum/painter/qp_comms.h25
-rw-r--r--quantum/painter/qp_draw.h85
-rw-r--r--quantum/painter/qp_draw_circle.c172
-rw-r--r--quantum/painter/qp_draw_codec.c142
-rw-r--r--quantum/painter/qp_draw_core.c294
-rw-r--r--quantum/painter/qp_draw_ellipse.c116
-rw-r--r--quantum/painter/qp_draw_image.c382
-rw-r--r--quantum/painter/qp_draw_text.c444
-rw-r--r--quantum/painter/qp_internal.h33
-rw-r--r--quantum/painter/qp_internal_driver.h82
-rw-r--r--quantum/painter/qp_internal_formats.h49
-rw-r--r--quantum/painter/qp_stream.c171
-rw-r--r--quantum/painter/qp_stream.h82
-rw-r--r--quantum/painter/rules.mk116
-rw-r--r--quantum/process_keycode/process_unicode_common.c30
-rw-r--r--quantum/quantum.h4
-rw-r--r--quantum/utf8.c46
-rw-r--r--quantum/utf8.h21
-rw-r--r--requirements.txt1
-rw-r--r--setup.cfg4
62 files changed, 7561 insertions, 35 deletions
diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml
index 26bcb2f511..726ce19f0c 100644
--- a/.github/workflows/unit_test.yml
+++ b/.github/workflows/unit_test.yml
@@ -26,5 +26,7 @@ jobs:
26 - uses: actions/checkout@v2 26 - uses: actions/checkout@v2
27 with: 27 with:
28 submodules: recursive 28 submodules: recursive
29 - name: Install dependencies
30 run: pip3 install -r requirements-dev.txt
29 - name: Run tests 31 - name: Run tests
30 run: make test:all 32 run: make test:all
diff --git a/builddefs/common_features.mk b/builddefs/common_features.mk
index a1793f91a5..c976b8296d 100644
--- a/builddefs/common_features.mk
+++ b/builddefs/common_features.mk
@@ -149,6 +149,11 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
149 endif 149 endif
150endif 150endif
151 151
152QUANTUM_PAINTER_ENABLE ?= no
153ifeq ($(strip $(QUANTUM_PAINTER_ENABLE)), yes)
154 include $(QUANTUM_DIR)/painter/rules.mk
155endif
156
152VALID_EEPROM_DRIVER_TYPES := vendor custom transient i2c spi 157VALID_EEPROM_DRIVER_TYPES := vendor custom transient i2c spi
153EEPROM_DRIVER ?= vendor 158EEPROM_DRIVER ?= vendor
154ifeq ($(filter $(EEPROM_DRIVER),$(VALID_EEPROM_DRIVER_TYPES)),) 159ifeq ($(filter $(EEPROM_DRIVER),$(VALID_EEPROM_DRIVER_TYPES)),)
@@ -696,7 +701,8 @@ endif
696 701
697ifeq ($(strip $(UNICODE_COMMON)), yes) 702ifeq ($(strip $(UNICODE_COMMON)), yes)
698 OPT_DEFS += -DUNICODE_COMMON_ENABLE 703 OPT_DEFS += -DUNICODE_COMMON_ENABLE
699 SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c 704 SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c \
705 $(QUANTUM_DIR)/utf8.c
700endif 706endif
701 707
702MAGIC_ENABLE ?= yes 708MAGIC_ENABLE ?= yes
diff --git a/docs/_summary.md b/docs/_summary.md
index 249bfcd9ed..786685eba4 100644
--- a/docs/_summary.md
+++ b/docs/_summary.md
@@ -94,6 +94,7 @@
94 94
95 * Hardware Features 95 * Hardware Features
96 * Displays 96 * Displays
97 * [Quantum Painter](quantum_painter.md)
97 * [HD44780 LCD Driver](feature_hd44780.md) 98 * [HD44780 LCD Driver](feature_hd44780.md)
98 * [ST7565 LCD Driver](feature_st7565.md) 99 * [ST7565 LCD Driver](feature_st7565.md)
99 * [OLED Driver](feature_oled_driver.md) 100 * [OLED Driver](feature_oled_driver.md)
diff --git a/docs/cli_commands.md b/docs/cli_commands.md
index 463abcef12..a380d3eb2f 100644
--- a/docs/cli_commands.md
+++ b/docs/cli_commands.md
@@ -515,3 +515,15 @@ Run single test:
515 515
516 qmk pytest -t qmk.tests.test_cli_commands.test_c2json 516 qmk pytest -t qmk.tests.test_cli_commands.test_c2json
517 qmk pytest -t qmk.tests.test_qmk_path 517 qmk pytest -t qmk.tests.test_qmk_path
518
519## `qmk painter-convert-graphics`
520
521This 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.
522
523## `qmk painter-make-font-image`
524
525This 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.
526
527## `qmk painter-convert-font-image`
528
529This 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.
diff --git a/docs/quantum_painter.md b/docs/quantum_painter.md
new file mode 100644
index 0000000000..a3705b62ce
--- /dev/null
+++ b/docs/quantum_painter.md
@@ -0,0 +1,705 @@
1# Quantum Painter :id=quantum-painter
2
3Quantum Painter is the standardised API for graphical displays. It currently includes support for basic drawing primitives, as well as custom images, animations, and fonts.
4
5Due to the complexity, there is no support for Quantum Painter on AVR-based boards.
6
7To enable overall Quantum Painter to be built into your firmware, add the following to `rules.mk`:
8
9```make
10QUANTUM_PAINTER_ENABLE = yes
11QUANTUM_PAINTER_DRIVERS = ......
12```
13
14You will also likely need to select an appropriate driver in `rules.mk`, which is listed below.
15
16!> 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.
17
18The QMK CLI can be used to convert from normal images such as PNG files or animated GIFs, as well as fonts from TTF files.
19
20Hardware supported:
21
22| Display Panel | Panel Type | Size | Comms Transport | Driver |
23|---------------|--------------------|------------------|-----------------|-----------------------------------------|
24| GC9A01 | RGB LCD (circular) | 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = gc9a01_spi` |
25| ILI9163 | RGB LCD | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9163_spi` |
26| ILI9341 | RGB LCD | 240x320 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9341_spi` |
27| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ssd1351_spi` |
28| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = st7789_spi` |
29
30## Quantum Painter Configuration :id=quantum-painter-config
31
32| Option | Default | Purpose |
33|-----------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
34| `QUANTUM_PAINTER_NUM_IMAGES` | `8` | The maximum number of images/animations that can be loaded at any one time. |
35| `QUANTUM_PAINTER_NUM_FONTS` | `4` | The maximum number of fonts that can be loaded at any one time. |
36| `QUANTUM_PAINTER_CONCURRENT_ANIMATIONS` | `4` | The maximum number of animations that can be executed at the same time. |
37| `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. |
38| `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. |
39| `QUANTUM_PAINTER_SUPPORTS_256_PALETTE` | `FALSE` | If 256-color palettes are supported. Requires significantly more RAM on the MCU. |
40| `QUANTUM_PAINTER_DEBUG` | _unset_ | Prints out significant amounts of debugging information to CONSOLE output. Significant performance degradation, use only for debugging. |
41
42Drivers have their own set of configurable options, and are described in their respective sections.
43
44## Quantum Painter CLI Commands :id=quantum-painter-cli
45
46### `qmk painter-convert-graphics`
47
48This command converts images to a format usable by QMK, i.e. the QGF File Format.
49
50**Usage**:
51
52```
53usage: qmk painter-convert-graphics [-h] [-d] [-r] -f FORMAT [-o OUTPUT] -i INPUT [-v]
54
55optional arguments:
56 -h, --help show this help message and exit
57 -d, --no-deltas Disables the use of delta frames when encoding animations.
58 -r, --no-rle Disables the use of RLE when encoding images.
59 -f FORMAT, --format FORMAT
60 Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
61 -o OUTPUT, --output OUTPUT
62 Specify output directory. Defaults to same directory as input.
63 -i INPUT, --input INPUT
64 Specify input graphic file.
65 -v, --verbose Turns on verbose output.
66```
67
68The `INPUT` argument can be any image file loadable by Python's Pillow module. Common formats include PNG, or Animated GIF.
69
70The `OUTPUT` argument needs to be a directory, and will default to the same directory as the input argument.
71
72The `FORMAT` argument can be any of the following:
73
74| Format | Meaning |
75|-----------|-----------------------------------------------------------------------|
76| `pal256` | 256-color palette (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
77| `pal16` | 16-color palette |
78| `pal4` | 4-color palette |
79| `pal2` | 2-color palette |
80| `mono256` | 256-shade grayscale (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
81| `mono16` | 16-shade grayscale |
82| `mono4` | 4-shade grayscale |
83| `mono2` | 2-shade grayscale |
84
85**Examples**:
86
87```
88$ cd /home/qmk/qmk_firmware/keyboards/my_keeb
89$ qmk painter-convert-graphics -f mono16 -i my_image.gif -o ./generated/
90Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.h...
91Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.c...
92```
93
94### `qmk painter-make-font-image`
95
96This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format.
97
98**Usage**:
99
100```
101usage: qmk painter-make-font-image [-h] [-a] [-u UNICODE_GLYPHS] [-n] [-s SIZE] -o OUTPUT -f FONT
102
103optional arguments:
104 -h, --help show this help message and exit
105 -a, --no-aa Disable anti-aliasing on fonts.
106 -u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS
107 Also generate the specified unicode glyphs.
108 -n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.
109 -s SIZE, --size SIZE Specify font size. Default 12.
110 -o OUTPUT, --output OUTPUT
111 Specify output image path.
112 -f FONT, --font FONT Specify input font file.
113```
114
115The `FONT` argument is generally a TrueType Font file (TTF).
116
117The `OUTPUT` argument is the output image to generate, generally something like `my_font.png`.
118
119The `UNICODE_GLYPHS` argument allows for specifying extra unicode glyphs to generate, and accepts a string.
120
121**Examples**:
122
123```
124$ qmk painter-make-font-image --font NotoSans-ExtraCondensedBold.ttf --size 11 -o noto11.png --unicode-glyphs "ĄȽɂɻɣɈʣ"
125```
126
127### `qmk painter-convert-font-image`
128
129This command converts an intermediate font image to the QFF File Format.
130
131This command expects an image that conforms to the following format:
132
133* Top-left pixel (at `0,0`) is the "delimiter" color:
134 * Each glyph in the font starts when a pixel of this color is found on the first row
135 * The first row is discarded when converting to the QFF format
136* The number of delimited glyphs must match the supplied arguments to the command:
137 * The full ASCII set `0x20..0x7E` (if `--no-ascii` was not specified)
138 * The corresponding number of unicode glyphs if any were specified with `--unicode-glyphs`
139* The order of the glyphs matches the ASCII set, if any, followed by the Unicode glyph set, if any.
140
141**Usage**:
142
143```
144usage: qmk painter-convert-font-image [-h] [-r] -f FORMAT [-u UNICODE_GLYPHS] [-n] [-o OUTPUT] [-i INPUT]
145
146optional arguments:
147 -h, --help show this help message and exit
148 -r, --no-rle Disable the use of RLE to minimise converted image size.
149 -f FORMAT, --format FORMAT
150 Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
151 -u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS
152 Also generate the specified unicode glyphs.
153 -n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.
154 -o OUTPUT, --output OUTPUT
155 Specify output directory. Defaults to same directory as input.
156 -i INPUT, --input INPUT
157 Specify input graphic file.
158```
159
160The same arguments for `--no-ascii` and `--unicode-glyphs` need to be specified, as per `qmk painter-make-font-image`.
161
162**Examples**:
163
164```
165$ cd /home/qmk/qmk_firmware/keyboards/my_keeb
166$ qmk painter-convert-font-image --input noto11.png -f mono4 --unicode-glyphs "ĄȽɂɻɣɈʣ"
167Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.h...
168Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.c...
169```
170
171## Quantum Painter Drawing API :id=quantum-painter-api
172
173All APIs require a `painter_device_t` object as their first parameter -- this object comes from the specific device initialisation, and instructions on creating it can be found in each driver's respective section.
174
175To use any of the APIs, you need to include `qp.h`:
176```c
177#include <qp.h>
178```
179
180### General Notes :id=quantum-painter-api-general
181
182The coordinate system used in Quantum Painter generally accepts `left`, `top`, `right`, and `bottom` instead of x/y/width/height, and each coordinate is inclusive of where pixels should be drawn. This is required as some datatypes used by display panels have a maximum value of `255` -- for any value or geometry extent that matches `256`, this would be represented as a `0`, instead.
183
184?> Drawing a horizontal line 8 pixels long, starting from 4 pixels inside the left side of the display, will need `left=4`, `right=11`.
185
186All color data matches the standard QMK HSV triplet definitions:
187
188* Hue is of the range `0...255` and is internally mapped to 0...360 degrees.
189* Saturation is of the range `0...255` and is internally mapped to 0...100% saturation.
190* Value is of the range `0...255` and is internally mapped to 0...100% brightness.
191
192?> Colors used in Quantum Painter are not subject to the RGB lighting CIE curve, if it is enabled.
193
194### Device Control :id=quantum-painter-api-device-control
195
196#### Display Initialisation :id=quantum-painter-api-init
197
198```c
199bool qp_init(painter_device_t device, painter_rotation_t rotation);
200```
201
202The `qp_init` function is used to initialise a display device after it has been created. This accepts a rotation parameter (`QP_ROTATION_0`, `QP_ROTATION_90`, `QP_ROTATION_180`, `QP_ROTATION_270`), which makes sure that the orientation of what's drawn on the display is correct.
203
204```c
205static painter_device_t display;
206void keyboard_post_init_kb(void) {
207 display = qp_make_.......; // Create the display
208 qp_init(display, QP_ROTATION_0); // Initialise the display
209}
210```
211
212#### Display Power :id=quantum-painter-api-power
213
214```c
215bool qp_power(painter_device_t device, bool power_on);
216```
217
218The `qp_power` function instructs the display whether or not the display panel should be on or off.
219
220!> If there is a separate backlight controlled through the normal QMK backlight API, this is not controlled by the `qp_power` function and needs to be manually handled elsewhere.
221
222```c
223static uint8_t last_backlight = 255;
224void suspend_power_down_user(void) {
225 if (last_backlight == 255) {
226 last_backlight = get_backlight_level();
227 }
228 backlight_set(0);
229 rgb_matrix_set_suspend_state(true);
230 qp_power(display, false);
231}
232
233void suspend_wakeup_init_user(void) {
234 qp_power(display, true);
235 rgb_matrix_set_suspend_state(false);
236 if (last_backlight != 255) {
237 backlight_set(last_backlight);
238 }
239 last_backlight = 255;
240}
241```
242
243#### Display Clear :id=quantum-painter-api-clear
244
245```c
246bool qp_clear(painter_device_t device);
247```
248
249The `qp_clear` function clears the display's screen.
250
251#### Display Flush :id=quantum-painter-api-flush
252
253```c
254bool qp_flush(painter_device_t device);
255```
256
257The `qp_flush` function ensures that all drawing operations are "pushed" to the display. This should be done as the last operation whenever a sequence of draws occur, and guarantees that any changes are applied.
258
259!> Some display panels may seem to work even without a call to `qp_flush` -- this may be because the driver cannot queue drawing operations and needs to display them immediately when invoked. In general, calling `qp_flush` at the end is still considered "best practice".
260
261```c
262void housekeeping_task_user(void) {
263 static uint32_t last_draw = 0;
264 if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
265 last_draw = timer_read32();
266 // Draw a rect based off the current RGB color
267 qp_rect(display, 0, 7, 0, 239, rgb_matrix_get_hue(), 255, 255);
268 qp_flush(display);
269 }
270}
271```
272
273### Drawing Primitives :id=quantum-painter-api-primitives
274
275#### Set Pixel :id=quantum-painter-api-setpixel
276
277```c
278bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val);
279```
280
281The `qp_setpixel` can be used to set a specific pixel on the screen to the supplied color.
282
283?> Using `qp_setpixel` for large amounts of drawing operations is inefficient and should be avoided unless they cannot be achieved with other drawing APIs.
284
285```c
286void housekeeping_task_user(void) {
287 static uint32_t last_draw = 0;
288 if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
289 last_draw = timer_read32();
290 // Draw a 240px high vertical rainbow line on X=0:
291 for (int i = 0; i < 239; ++i) {
292 qp_setpixel(display, 0, i, i, 255, 255);
293 }
294 qp_flush(display);
295 }
296}
297```
298
299#### Draw Line :id=quantum-painter-api-line
300
301```c
302bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val);
303```
304
305The `qp_line` can be used to draw lines on the screen with the supplied color.
306
307```c
308void housekeeping_task_user(void) {
309 static uint32_t last_draw = 0;
310 if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
311 last_draw = timer_read32();
312 // Draw 8px-wide rainbow down the left side of the display
313 for (int i = 0; i < 239; ++i) {
314 qp_line(display, 0, i, 7, i, i, 255, 255);
315 }
316 qp_flush(display);
317 }
318}
319```
320
321#### Draw Rect :id=quantum-painter-api-rect
322
323```c
324bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
325```
326
327The `qp_rect` can be used to draw rectangles on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the rectangle will be left as-is.
328
329```c
330void housekeeping_task_user(void) {
331 static uint32_t last_draw = 0;
332 if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
333 last_draw = timer_read32();
334 // Draw 8px-wide rainbow filled rectangles down the left side of the display
335 for (int i = 0; i < 239; i+=8) {
336 qp_rect(display, 0, i, 7, i+7, i, 255, 255, true);
337 }
338 qp_flush(display);
339 }
340}
341```
342
343#### Draw Circle :id=quantum-painter-api-circle
344
345```c
346bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
347```
348
349The `qp_circle` can be used to draw circles on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the circle will be left as-is.
350
351```c
352void housekeeping_task_user(void) {
353 static uint32_t last_draw = 0;
354 if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
355 last_draw = timer_read32();
356 // Draw r=4 filled circles down the left side of the display
357 for (int i = 0; i < 239; i+=8) {
358 qp_circle(display, 4, 4+i, 4, i, 255, 255, true);
359 }
360 qp_flush(display);
361 }
362}
363```
364
365#### Draw Ellipse :id=quantum-painter-api-ellipse
366
367```c
368bool 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);
369```
370
371The `qp_ellipse` can be used to draw ellipses on the screen with the supplied color, with or without a background fill. If not filled, any pixels inside the ellipses will be left as-is.
372
373```c
374void housekeeping_task_user(void) {
375 static uint32_t last_draw = 0;
376 if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
377 last_draw = timer_read32();
378 // Draw 16x8 filled ellipses down the left side of the display
379 for (int i = 0; i < 239; i+=8) {
380 qp_ellipse(display, 8, 4+i, 16, 8, i, 255, 255, true);
381 }
382 qp_flush(display);
383 }
384}
385```
386
387### Image Functions :id=quantum-painter-api-images
388
389#### Load Image :id=quantum-painter-api-load-image
390
391```c
392painter_image_handle_t qp_load_image_mem(const void *buffer);
393```
394
395The `qp_load_image_mem` function loads a QGF image from memory or flash.
396
397`qp_load_image_mem` returns a handle to the loaded image, which can then be used to draw to the screen using `qp_drawimage`, `qp_drawimage_recolor`, `qp_animate`, or `qp_animate_recolor`. If an image is no longer required, it can be unloaded by calling `qp_close_image` below.
398
399See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert images to [QGF](quantum_painter_qgf.md).
400
401?> The total number of images available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_IMAGES` in the table above. If more images are required, the number should be increased in `config.h`.
402
403Image information is available through accessing the handle:
404
405| Property | Accessor |
406|-------------|----------------------|
407| Width | `image->width` |
408| Height | `image->height` |
409| Frame Count | `image->frame_count` |
410
411#### Unload Image :id=quantum-painter-api-close-image
412
413```c
414bool qp_close_image(painter_image_handle_t image);
415```
416
417The `qp_close_image` function releases resources related to the loading of the supplied image.
418
419#### Draw image :id=quantum-painter-api-draw-image
420
421```c
422bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
423bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
424```
425
426The `qp_drawimage` and `qp_drawimage_recolor` functions draw the supplied image to the screen at the supplied location, with the latter function allowing for monochrome-based images to be recolored.
427
428```c
429// Draw an image on the bottom-right of the 240x320 display on initialisation
430static painter_image_handle_t my_image;
431void keyboard_post_init_kb(void) {
432 my_image = qp_load_image_mem(gfx_my_image);
433 if (my_image != NULL) {
434 qp_drawimage(display, (239 - my_image->width), (319 - my_image->height), my_image);
435 }
436}
437```
438
439#### Animate Image :id=quantum-painter-api-animate-image
440
441```c
442deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
443deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
444```
445
446The `qp_animate` and `qp_animate_recolor` functions draw the supplied image to the screen at the supplied location, with the latter function allowing for monochrome-based animations to be recolored. They also set up internal timing such that each frame is rendered at the correct time as per the animated image.
447
448Once an image has been set to animate, it will loop indefinitely until stopped, with no user intervention required.
449
450Both functions return a `deferred_token`, which can then be used to stop the animation, using `qp_stop_animation` below.
451
452```c
453// Animate an image on the bottom-right of the 240x320 display on initialisation
454static painter_image_handle_t my_image;
455static deferred_token my_anim;
456void keyboard_post_init_kb(void) {
457 my_image = qp_load_image_mem(gfx_my_image);
458 if (my_image != NULL) {
459 my_anim = qp_animate(display, (239 - my_image->width), (319 - my_image->height), my_image);
460 }
461}
462```
463
464#### Stop Animation :id=quantum-painter-api-stop-animation
465
466```c
467void qp_stop_animation(deferred_token anim_token);
468```
469
470The `qp_stop_animation` function stops the previously-started animation.
471```c
472void housekeeping_task_user(void) {
473 if (some_random_stop_reason) {
474 qp_stop_animation(my_anim);
475 }
476}
477```
478
479### Font Functions :id=quantum-painter-api-fonts
480
481#### Load Font :id=quantum-painter-api-load-font
482
483```c
484painter_font_handle_t qp_load_font_mem(const void *buffer);
485```
486
487The `qp_load_font_mem` function loads a QFF font from memory or flash.
488
489`qp_load_font_mem` returns a handle to the loaded font, which can then be measured using `qp_textwidth`, or drawn to the screen using `qp_drawtext`, or `qp_drawtext_recolor`. If a font is no longer required, it can be unloaded by calling `qp_close_font` below.
490
491See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert TTF fonts to [QFF](quantum_painter_qff.md).
492
493?> The total number of fonts available to load at any one time is controlled by the configurable option `QUANTUM_PAINTER_NUM_FONTS` in the table above. If more fonts are required, the number should be increased in `config.h`.
494
495Font information is available through accessing the handle:
496
497| Property | Accessor |
498|-------------|----------------------|
499| Line Height | `image->line_height` |
500
501#### Unload Font :id=quantum-painter-api-close-font
502
503```c
504bool qp_close_font(painter_font_handle_t font);
505```
506
507The `qp_close_font` function releases resources related to the loading of the supplied font.
508
509#### Measure Text :id=quantum-painter-api-textwidth
510
511```c
512int16_t qp_textwidth(painter_font_handle_t font, const char *str);
513```
514
515The `qp_textwidth` function allows measurement of how many pixels wide the supplied string would result in, for the given font.
516
517#### Draw Text :id=quantum-painter-api-drawtext
518
519```c
520int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str);
521int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
522```
523
524The `qp_drawtext` and `qp_drawtext_recolor` functions draw the supplied string to the screen at the given location using the font supplied, with the latter function allowing for monochrome-based fonts to be recolored.
525
526```c
527// Draw a text message on the bottom-right of the 240x320 display on initialisation
528static painter_font_handle_t my_font;
529void keyboard_post_init_kb(void) {
530 my_font = qp_load_font_mem(font_opensans);
531 if (my_font != NULL) {
532 static const char *text = "Hello from QMK!";
533 int16_t width = qp_textwidth(my_font, text);
534 qp_drawtext(display, (239 - width), (319 - my_font->line_height), my_font, text);
535 }
536}
537```
538
539### Advanced Functions :id=quantum-painter-api-advanced
540
541#### Get Geometry :id=quantum-painter-api-get-geometry
542
543```c
544void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y);
545```
546
547The `qp_get_geometry` function allows external code to retrieve the current width, height, rotation, and drawing offsets.
548
549#### Set Viewport Offsets :id=quantum-painter-api-set-viewport
550
551```c
552void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y);
553```
554
555The `qp_set_viewport_offsets` function can be used to offset all subsequent drawing operations. For example, if a display controller is internally 240x320, but the display panel is 240x240 and has a Y offset of 80 pixels, you could invoke `qp_set_viewport_offsets(display, 0, 80);` and the drawing positioning would be corrected.
556
557#### Set Viewport :id=quantum-painter-api-viewport
558
559```c
560bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
561```
562
563The `qp_viewport` function controls where raw pixel data is written to.
564
565#### Stream Pixel Data :id=quantum-painter-api-pixdata
566
567```c
568bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
569```
570
571The `qp_pixdata` function allows raw pixel data to be streamed to the display. It requires a native pixel count rather than the number of bytes to transfer, to ensure display panel data alignment is respected. E.g. for display panels using RGB565 internal format, sending 10 pixels will result in 20 bytes of transfer.
572
573!> Under normal circumstances, users will not need to manually call either `qp_viewport` or `qp_pixdata`. These allow for writing of raw pixel information, in the display panel's native format, to the area defined by the viewport.
574
575## Quantum Painter Display Drivers :id=quantum-painter-drivers
576
577### Common: Standard TFT (SPI + D/C + RST)
578
579Most TFT display panels use a 5-pin interface -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins.
580
581For these displays, QMK's `spi_master` must already be correctly configured for the platform you're building for.
582
583The pin assignments for SPI CS, D/C, and RST are specified during device construction.
584
585### GC9A01 :id=qp-driver-gc9a01
586
587Enabling support for the GC9A01 in Quantum Painter is done by adding the following to `rules.mk`:
588
589```make
590QUANTUM_PAINTER_ENABLE = yes
591QUANTUM_PAINTER_DRIVERS = gc9a01_spi
592```
593
594Creating a GC9A01 device in firmware can then be done with the following API:
595
596```c
597painter_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);
598```
599
600The device handle returned from the `qp_gc9a01_make_spi_device` function can be used to perform all other drawing operations.
601
602The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
603
604```c
605// 3 displays:
606#define GC9A01_NUM_DEVICES 3
607```
608
609### ILI9163 :id=qp-driver-ili9163
610
611Enabling support for the ILI9163 in Quantum Painter is done by adding the following to `rules.mk`:
612
613```make
614QUANTUM_PAINTER_ENABLE = yes
615QUANTUM_PAINTER_DRIVERS = ili9163_spi
616```
617
618Creating a ILI9163 device in firmware can then be done with the following API:
619
620```c
621painter_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);
622```
623
624The device handle returned from the `qp_ili9163_make_spi_device` function can be used to perform all other drawing operations.
625
626The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
627
628```c
629// 3 displays:
630#define ILI9163_NUM_DEVICES 3
631```
632
633### ILI9341 :id=qp-driver-ili9341
634
635Enabling support for the ILI9341 in Quantum Painter is done by adding the following to `rules.mk`:
636
637```make
638QUANTUM_PAINTER_ENABLE = yes
639QUANTUM_PAINTER_DRIVERS = ili9341_spi
640```
641
642Creating a ILI9341 device in firmware can then be done with the following API:
643
644```c
645painter_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);
646```
647
648The device handle returned from the `qp_ili9341_make_spi_device` function can be used to perform all other drawing operations.
649
650The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
651
652```c
653// 3 displays:
654#define ILI9341_NUM_DEVICES 3
655```
656
657### SSD1351 :id=qp-driver-ssd1351
658
659Enabling support for the SSD1351 in Quantum Painter is done by adding the following to `rules.mk`:
660
661```make
662QUANTUM_PAINTER_ENABLE = yes
663QUANTUM_PAINTER_DRIVERS = ssd1351_spi
664```
665
666Creating a SSD1351 device in firmware can then be done with the following API:
667
668```c
669painter_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);
670```
671
672The device handle returned from the `qp_ssd1351_make_spi_device` function can be used to perform all other drawing operations.
673
674The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
675
676```c
677// 3 displays:
678#define SSD1351_NUM_DEVICES 3
679```
680
681### ST7789 :id=qp-driver-st7789
682
683Enabling support for the ST7789 in Quantum Painter is done by adding the following to `rules.mk`:
684
685```make
686QUANTUM_PAINTER_ENABLE = yes
687QUANTUM_PAINTER_DRIVERS = st7789_spi
688```
689
690Creating a ST7789 device in firmware can then be done with the following API:
691
692```c
693painter_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);
694```
695
696The device handle returned from the `qp_st7789_make_spi_device` function can be used to perform all other drawing operations.
697
698The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
699
700```c
701// 3 displays:
702#define ST7789_NUM_DEVICES 3
703```
704
705!> Some ST7789 devices are known to have different drawing offsets -- despite being a 240x320 pixel display controller internally, some display panels are only 240x240, or smaller. These may require an offset to be applied; see `qp_set_viewport_offsets` above for information on how to override the offsets if they aren't correctly rendered. \ No newline at end of file
diff --git a/docs/quantum_painter_qff.md b/docs/quantum_painter_qff.md
new file mode 100644
index 0000000000..f62d59bdcb
--- /dev/null
+++ b/docs/quantum_painter_qff.md
@@ -0,0 +1,103 @@
1# QMK Font Format :id=qmk-font-format
2
3QMK uses a font format _("Quantum Font Format" - QFF)_ specifically for resource-constrained systems.
4
5This 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.
6
7All integer values are in little-endian format.
8
9The QFF is defined in terms of _blocks_ -- each _block_ contains a _header_ and an optional _blob_ of data. The _header_ contains the block's _typeid_, and the length of the _blob_ that follows. Each block type is denoted by a different _typeid_ has its own block definition below. All blocks are defined as packed structs, containing zero padding between fields.
10
11The general structure of the file is:
12
13* _Font descriptor block_
14* _ASCII glyph block_ (optional, only if ASCII glyphs are included)
15* _Unicode glyph block_ (optional, only if Unicode glyphs are included)
16* _Font palette block_ (optional, depending on frame format)
17* _Font data block_
18
19## Block Header :id=qff-block-header
20
21The block header is identical to [QGF's block header](quantum_painter_qgf.md#qgf-block-header), and is present for all blocks, including the font descriptor.
22
23## Font descriptor block :id=qff-font-descriptor
24
25* _typeid_ = 0x00
26* _length_ = 20
27
28This block must be located at the start of the file contents, and can exist a maximum of once in an entire QGF file. It is always followed by either the _ASCII glyph table_ or the _Unicode glyph table_, depending on which glyphs are included in the font.
29
30_Block_ format:
31
32```c
33typedef struct __attribute__((packed)) qff_font_descriptor_v1_t {
34 qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 }
35 uint24_t magic; // constant, equal to 0x464651 ("QFF")
36 uint8_t qff_version; // constant, equal to 0x01
37 uint32_t total_file_size; // total size of the entire file, starting at offset zero
38 uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors
39 uint8_t line_height; // glyph height in pixels
40 bool has_ascii_table; // whether the font has an ascii table of glyphs (0x20...0x7E)
41 uint16_t num_unicode_glyphs; // the number of glyphs in the unicode table -- no table specified if zero
42 uint8_t format; // frame format, see below.
43 uint8_t flags; // frame flags, see below.
44 uint8_t compression_scheme; // compression scheme, see below.
45 uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
46} qff_font_descriptor_v1_t;
47// _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");
48```
49
50The values for `format`, `flags`, `compression_scheme`, and `transparency_index` match [QGF's frame descriptor block](quantum_painter_qgf.md#qgf-frame-descriptor), with the exception that the `delta` flag is ignored by QFF.
51
52## ASCII glyph table :id=qff-ascii-table
53
54* _typeid_ = 0x01
55* _length_ = 290
56
57If the font contains ascii characters, the _ASCII glyph block_ must be located directly after the _font descriptor block_.
58
59```c
60#define QFF_GLYPH_WIDTH_BITS 6
61#define QFF_GLYPH_WIDTH_MASK ((1<<QFF_GLYPH_WIDTH_BITS)-1)
62#define QFF_GLYPH_OFFSET_BITS 18
63#define QFF_GLYPH_OFFSET_MASK (((1<<QFF_GLYPH_OFFSET_BITS)-1) << QFF_GLYPH_WIDTH_BITS)
64
65typedef struct __attribute__((packed)) qff_ascii_glyph_table_v1_t {
66 qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 }
67 uint24_t glyph[95]; // 95 glyphs, 0x20..0x7E, see bits/masks above for values
68} qff_ascii_glyph_table_v1_t;
69// _Static_assert(sizeof(qff_ascii_glyph_table_v1_t) == (sizeof(qgf_block_header_v1_t) + 285), "qff_ascii_glyph_table_v1_t must be 290 bytes in v1 of QFF");
70```
71
72## Unicode glyph table :id=qff-unicode-table
73
74* _typeid_ = 0x02
75* _length_ = variable
76
77If this font contains unicode characters, the _unicode glyph block_ must be located directly after the _ASCII glyph table block_, or the _font descriptor block_ if the font does not contain ASCII characters.
78
79```c
80typedef struct __attribute__((packed)) qff_unicode_glyph_table_v1_t {
81 qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = (N * 6) }
82 struct __attribute__((packed)) { // container for a single unicode glyph
83 uint24_t code_point; // the unicode code point
84 uint24_t glyph; // the glyph information, as per ASCII glyphs above
85 } glyph[N]; // N glyphs worth of data
86} qff_unicode_glyph_table_v1_t;
87```
88
89## Font palette block :id=qff-palette-descriptor
90
91* _typeid_ = 0x03
92* _length_ = variable
93
94The _font palette block_ is identical to [QGF's frame palette block](quantum_painter_qgf.md#qgf-frame-palette-descriptor), retaining the same _typeid_ of 0x03.
95
96It is only specified in the QFF if the font is palette-based, and follows the _unicode glyph block_ if the font contains any Unicode glyphs, or the _ASCII glyph block_ if the font contains only ASCII glyphs.
97
98## Font data block :id=qff-data-descriptor
99
100* _typeid_ = 0x04
101* _length_ = variable
102
103The _font data block_ is the last block in the file and is identical to [QGF's frame data block](quantum_painter_qgf.md#qgf-frame-data-descriptor), however has a different _typeid_ of 0x04 in QFF.
diff --git a/docs/quantum_painter_qgf.md b/docs/quantum_painter_qgf.md
new file mode 100644
index 0000000000..caf6731e65
--- /dev/null
+++ b/docs/quantum_painter_qgf.md
@@ -0,0 +1,178 @@
1# QMK Graphics Format :id=qmk-graphics-format
2
3QMK uses a graphics format _("Quantum Graphics Format" - QGF)_ specifically for resource-constrained systems.
4
5This 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.
6
7All integer values are in little-endian format.
8
9The QGF is defined in terms of _blocks_ -- each _block_ contains a _header_ and an optional _blob_ of data. The _header_ contains the block's _typeid_, and the length of the _blob_ that follows. Each block type is denoted by a different _typeid_ has its own block definition below. All blocks are defined as packed structs, containing zero padding between fields.
10
11The general structure of the file is:
12
13* _Graphics descriptor block_
14* _Frame offset block_
15* Repeating list of frames:
16 * _Frame descriptor block_
17 * _Frame palette block_ (optional, depending on frame format)
18 * _Frame delta block_ (optional, depending on delta flag)
19 * _Frame data block_
20
21Different frames within the file should be considered "isolated" and may have their own image format and/or palette.
22
23## Block Header :id=qgf-block-header
24
25This block header is present for all blocks, including the graphics descriptor.
26
27_Block header_ format:
28
29```c
30typedef struct __attribute__((packed)) qgf_block_header_v1_t {
31 uint8_t type_id; // See each respective block type
32 uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors
33 uint24_t length; // 24-bit blob length, allowing for block sizes of a maximum of 16MB
34} qgf_block_header_v1_t;
35// _Static_assert(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF");
36```
37The _length_ describes the number of octets in the data following the block header -- a block header may specify a _length_ of `0` if no blob is specified.
38
39## Graphics descriptor block :id=qgf-graphics-descriptor
40
41* _typeid_ = 0x00
42* _length_ = 18
43
44This block must be located at the start of the file contents, and can exist a maximum of once in an entire QGF file. It is always followed by the _frame offset block_.
45
46_Block_ format:
47
48```c
49typedef struct __attribute__((packed)) qgf_graphics_descriptor_v1_t {
50 qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 }
51 uint24_t magic; // constant, equal to 0x464751 ("QGF")
52 uint8_t qgf_version; // constant, equal to 0x01
53 uint32_t total_file_size; // total size of the entire file, starting at offset zero
54 uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors
55 uint16_t image_width; // in pixels
56 uint16_t image_height; // in pixels
57 uint16_t frame_count; // minimum of 1
58} qgf_graphics_descriptor_v1_t;
59// _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");
60```
61
62## Frame offset block :id=qgf-frame-offset-descriptor
63
64* _typeid_ = 0x01
65* _length_ = variable
66
67This block denotes the offsets within the file to each frame's _frame descriptor block_, relative to the start of the file. The _frame offset block_ always immediately follows the _graphics descriptor block_. The contents of this block are an array of U32's, with one entry for each frame.
68
69Duplicate frame offsets in this block are allowed, if a certain frame is to be shown multiple times during animation.
70
71_Block_ format:
72
73```c
74typedef struct __attribute__((packed)) qgf_frame_offsets_v1_t {
75 qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = (N * sizeof(uint32_t)) }
76 uint32_t offset[N]; // where 'N' is the number of frames in the file
77} qgf_frame_offsets_v1_t;
78```
79
80## Frame descriptor block :id=qgf-frame-descriptor
81
82* _typeid_ = 0x02
83* _length_ = 5
84
85This block denotes the start of a frame.
86
87_Block_ format:
88
89```c
90typedef struct __attribute__((packed)) qgf_frame_v1_t {
91 qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 5 }
92 uint8_t format; // Frame format, see below.
93 uint8_t flags; // Frame flags, see below.
94 uint8_t compression_scheme; // Compression scheme, see below.
95 uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
96 uint16_t delay; // frame delay time for animations (in units of milliseconds)
97} qgf_frame_v1_t;
98// _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");
99```
100
101If this frame is grayscale, the _frame descriptor block_ (or _frame delta block_ if flags denote a delta frame) is immediately followed by this frame's corresponding _frame data block_.
102
103If the frame uses an indexed palette, the _frame descriptor block_ (or _frame delta block_ if flags denote a delta frame) is immediately followed by this frame's corresponding _frame palette block_.
104
105Frame format possible values:
106
107* `0x00`: 1bpp grayscale, no palette, `0` = black, `1` = white, LSb first pixel
108* `0x01`: 2bpp grayscale, no palette, `0` = black, `3` = white, linear interpolation of brightness, LSb first pixel
109* `0x02`: 4bpp grayscale, no palette, `0` = black, `15` = white, linear interpolation of brightness, LSb first pixel
110* `0x03`: 8bpp grayscale, no palette, `0` = black, `255` = white, linear interpolation of brightness, LSb first pixel
111* `0x04`: 1bpp indexed palette, 2 colors, LSb first pixel
112* `0x05`: 2bpp indexed palette, 4 colors, LSb first pixel
113* `0x06`: 4bpp indexed palette, 16 colors, LSb first pixel
114* `0x07`: 8bpp indexed palette, 256 colors, LSb first pixel
115
116Frame flags is a bitmask with the following format:
117
118| `bit 7` | `bit 6` | `bit 5` | `bit 4` | `bit 3` | `bit 2` | `bit 1` | `bit 0` |
119|---------|---------|---------|---------|---------|---------|---------|--------------|
120| - | - | - | - | - | - | Delta | Transparency |
121
122* `[1]` -- Delta: Signifies that the current frame is a delta frame, which specifies only a sub-image. The _frame delta block_ follows the _frame palette block_ if the image format specifies a palette, otherwise it directly follows the _frame descriptor block_.
123* `[0]` -- Transparency: The transparent palette index in the _blob_ is considered valid and should be used when considering which pixels should be transparent during rendering this frame, if possible.
124
125Compression scheme possible values:
126
127* `0x00`: No compression
128* `0x01`: [QMK RLE](quantum_painter_rle.md)
129
130## Frame palette block :id=qgf-frame-palette-descriptor
131
132* _typeid_ = 0x03
133* _length_ = variable
134
135This block describes the palette used for the frame. The _blob_ contains an array of palette entries -- one palette entry is present for each color used -- each palette entry is in QMK HSV888 format:
136
137```c
138typedef struct __attribute__((packed)) qgf_palette_v1_t {
139 qgf_block_header_v1_t header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) }
140 struct { // container for a single HSV palette entry
141 uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t.
142 uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t.
143 uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t.
144 } hsv[N]; // N * hsv, where N is the number of palette entries depending on the frame format in the descriptor
145} qgf_palette_v1_t;
146```
147
148## Frame delta block :id=qgf-frame-delta-descriptor
149
150* _typeid_ = 0x04
151* _length_ = 8
152
153This block describes where the delta frame should be drawn, with respect to the top left location of the image.
154
155```c
156typedef struct __attribute__((packed)) qgf_delta_v1_t {
157 qgf_block_header_v1_t header; // = { .type_id = 0x04, .neg_type_id = (~0x04), .length = 8 }
158 uint16_t left; // The left pixel location to draw the delta image
159 uint16_t top; // The top pixel location to draw the delta image
160 uint16_t right; // The right pixel location to to draw the delta image
161 uint16_t bottom; // The bottom pixel location to to draw the delta image
162} qgf_delta_v1_t;
163// _Static_assert(sizeof(qgf_delta_v1_t) == 13, "qgf_delta_v1_t must be 13 bytes in v1 of QGF");
164```
165
166## Frame data block :id=qgf-frame-data-descriptor
167
168* _typeid_ = 0x05
169* _length_ = variable
170
171This block describes the data associated with the frame. The _blob_ contains an array of bytes containing the data corresponding to the frame's image format:
172
173```c
174typedef struct __attribute__((packed)) qgf_data_v1_t {
175 qgf_block_header_v1_t header; // = { .type_id = 0x05, .neg_type_id = (~0x05), .length = N }
176 uint8_t data[N]; // N data octets
177} qgf_data_v1_t;
178```
diff --git a/docs/quantum_painter_rle.md b/docs/quantum_painter_rle.md
new file mode 100644
index 0000000000..dcb9a1e1a7
--- /dev/null
+++ b/docs/quantum_painter_rle.md
@@ -0,0 +1,29 @@
1# QMK QGF/QFF RLE data schema :id=qmk-qp-rle-schema
2
3There are two "modes" to the RLE algorithm used in both [QGF](quantum_painter_qgf.md)/[QFF](quantum_painter_qff.md):
4
5* Non-repeating sections of octets, with associated length of up to `128` octets
6 * `length` = `marker - 128`
7 * A corresponding `length` number of octets follow directly after the marker octet
8* Repeated octet with associated length, with associated length of up to `128`
9 * `length` = `marker`
10 * A single octet follows the marker that should be repeated `length` times.
11
12Decoder pseudocode:
13```
14while !EOF
15 marker = READ_OCTET()
16
17 if marker >= 128
18 length = marker - 128
19 for i = 0 ... length-1
20 c = READ_OCTET()
21 WRITE_OCTET(c)
22
23 else
24 length = marker
25 c = READ_OCTET()
26 for i = 0 ... length-1
27 WRITE_OCTET(c)
28
29```
diff --git a/drivers/painter/comms/qp_comms_spi.c b/drivers/painter/comms/qp_comms_spi.c
new file mode 100644
index 0000000000..e644ba9f84
--- /dev/null
+++ b/drivers/painter/comms/qp_comms_spi.c
@@ -0,0 +1,137 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#ifdef QUANTUM_PAINTER_SPI_ENABLE
5
6# include "spi_master.h"
7# include "qp_comms_spi.h"
8
9////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
10// Base SPI support
11
12bool qp_comms_spi_init(painter_device_t device) {
13 struct painter_driver_t * driver = (struct painter_driver_t *)device;
14 struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
15
16 // Initialize the SPI peripheral
17 spi_init();
18
19 // Set up CS as output high
20 setPinOutput(comms_config->chip_select_pin);
21 writePinHigh(comms_config->chip_select_pin);
22
23 return true;
24}
25
26bool qp_comms_spi_start(painter_device_t device) {
27 struct painter_driver_t * driver = (struct painter_driver_t *)device;
28 struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
29
30 return spi_start(comms_config->chip_select_pin, comms_config->lsb_first, comms_config->mode, comms_config->divisor);
31}
32
33uint32_t qp_comms_spi_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
34 uint32_t bytes_remaining = byte_count;
35 const uint8_t *p = (const uint8_t *)data;
36 while (bytes_remaining > 0) {
37 uint32_t bytes_this_loop = bytes_remaining < 1024 ? bytes_remaining : 1024;
38 spi_transmit(p, bytes_this_loop);
39 p += bytes_this_loop;
40 bytes_remaining -= bytes_this_loop;
41 }
42
43 return byte_count - bytes_remaining;
44}
45
46void qp_comms_spi_stop(painter_device_t device) {
47 struct painter_driver_t * driver = (struct painter_driver_t *)device;
48 struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
49 spi_stop();
50 writePinHigh(comms_config->chip_select_pin);
51}
52
53const struct painter_comms_vtable_t spi_comms_vtable = {
54 .comms_init = qp_comms_spi_init,
55 .comms_start = qp_comms_spi_start,
56 .comms_send = qp_comms_spi_send_data,
57 .comms_stop = qp_comms_spi_stop,
58};
59
60////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
61// SPI with D/C and RST pins
62
63# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
64
65bool qp_comms_spi_dc_reset_init(painter_device_t device) {
66 if (!qp_comms_spi_init(device)) {
67 return false;
68 }
69
70 struct painter_driver_t * driver = (struct painter_driver_t *)device;
71 struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
72
73 // Set up D/C as output low, if specified
74 if (comms_config->dc_pin != NO_PIN) {
75 setPinOutput(comms_config->dc_pin);
76 writePinLow(comms_config->dc_pin);
77 }
78
79 // Set up RST as output, if specified, performing a reset in the process
80 if (comms_config->reset_pin != NO_PIN) {
81 setPinOutput(comms_config->reset_pin);
82 writePinLow(comms_config->reset_pin);
83 wait_ms(20);
84 writePinHigh(comms_config->reset_pin);
85 wait_ms(20);
86 }
87
88 return true;
89}
90
91uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
92 struct painter_driver_t * driver = (struct painter_driver_t *)device;
93 struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
94 writePinHigh(comms_config->dc_pin);
95 return qp_comms_spi_send_data(device, data, byte_count);
96}
97
98void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd) {
99 struct painter_driver_t * driver = (struct painter_driver_t *)device;
100 struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
101 writePinLow(comms_config->dc_pin);
102 spi_write(cmd);
103}
104
105void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
106 for (size_t i = 0; i < sequence_len;) {
107 uint8_t command = sequence[i];
108 uint8_t delay = sequence[i + 1];
109 uint8_t num_bytes = sequence[i + 2];
110 qp_comms_spi_dc_reset_send_command(device, command);
111 if (num_bytes > 0) {
112 qp_comms_spi_dc_reset_send_data(device, &sequence[i + 3], num_bytes);
113 }
114 if (delay > 0) {
115 wait_ms(delay);
116 }
117 i += (3 + num_bytes);
118 }
119}
120
121const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable = {
122 .base =
123 {
124 .comms_init = qp_comms_spi_dc_reset_init,
125 .comms_start = qp_comms_spi_start,
126 .comms_send = qp_comms_spi_dc_reset_send_data,
127 .comms_stop = qp_comms_spi_stop,
128 },
129 .send_command = qp_comms_spi_dc_reset_send_command,
130 .bulk_command_sequence = qp_comms_spi_dc_reset_bulk_command_sequence,
131};
132
133# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
134
135////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
136
137#endif // QUANTUM_PAINTER_SPI_ENABLE
diff --git a/drivers/painter/comms/qp_comms_spi.h b/drivers/painter/comms/qp_comms_spi.h
new file mode 100644
index 0000000000..9989987327
--- /dev/null
+++ b/drivers/painter/comms/qp_comms_spi.h
@@ -0,0 +1,51 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#ifdef QUANTUM_PAINTER_SPI_ENABLE
7
8# include <stdint.h>
9
10# include "gpio.h"
11# include "qp_internal.h"
12
13////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
14// Base SPI support
15
16struct qp_comms_spi_config_t {
17 pin_t chip_select_pin;
18 uint16_t divisor;
19 bool lsb_first;
20 int8_t mode;
21};
22
23bool qp_comms_spi_init(painter_device_t device);
24bool qp_comms_spi_start(painter_device_t device);
25uint32_t qp_comms_spi_send_data(painter_device_t device, const void* data, uint32_t byte_count);
26void qp_comms_spi_stop(painter_device_t device);
27
28extern const struct painter_comms_vtable_t spi_comms_vtable;
29
30////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
31// SPI with D/C and RST pins
32
33# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
34
35struct qp_comms_spi_dc_reset_config_t {
36 struct qp_comms_spi_config_t spi_config;
37 pin_t dc_pin;
38 pin_t reset_pin;
39};
40
41void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd);
42uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void* data, uint32_t byte_count);
43void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len);
44
45extern const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable;
46
47# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
48
49////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
50
51#endif // QUANTUM_PAINTER_SPI_ENABLE
diff --git a/drivers/painter/gc9a01/qp_gc9a01.c b/drivers/painter/gc9a01/qp_gc9a01.c
new file mode 100644
index 0000000000..ad76d58b07
--- /dev/null
+++ b/drivers/painter/gc9a01/qp_gc9a01.c
@@ -0,0 +1,150 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <wait.h>
5#include "qp_internal.h"
6#include "qp_comms.h"
7#include "qp_gc9a01.h"
8#include "qp_gc9a01_opcodes.h"
9#include "qp_tft_panel.h"
10
11////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
12// Driver storage
13////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
14
15tft_panel_dc_reset_painter_device_t gc9a01_drivers[GC9A01_NUM_DEVICES] = {0};
16
17////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
18// Initialization
19////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
20bool qp_gc9a01_init(painter_device_t device, painter_rotation_t rotation) {
21 // A lot of these "unknown" opcodes are sourced from other OSS projects and are seemingly required for this display to function.
22 // clang-format off
23 const uint8_t gc9a01_init_sequence[] = {
24 // Command, Delay, N, Data[N]
25 GC9A01_SET_INTER_REG_ENABLE2, 0, 0,
26 0xEB, 0, 1, 0x14,
27 GC9A01_SET_INTER_REG_ENABLE1, 0, 0,
28 GC9A01_SET_INTER_REG_ENABLE2, 0, 0,
29 0xEB, 0, 1, 0x14,
30 0x84, 0, 1, 0x40,
31 0x85, 0, 1, 0xFF,
32 0x86, 0, 1, 0xFF,
33 0x87, 0, 1, 0xFF,
34 0x88, 0, 1, 0x0A,
35 0x89, 0, 1, 0x21,
36 0x8a, 0, 1, 0x00,
37 0x8b, 0, 1, 0x80,
38 0x8c, 0, 1, 0x01,
39 0x8d, 0, 1, 0x01,
40 0x8e, 0, 1, 0xFF,
41 0x8f, 0, 1, 0xFF,
42 GC9A01_SET_FUNCTION_CTL, 0, 2, 0x00, 0x20,
43 GC9A01_SET_PIX_FMT, 0, 1, 0x55,
44 0x90, 0, 4, 0x08, 0x08, 0x08, 0x08,
45 0xBD, 0, 1, 0x06,
46 0xBC, 0, 1, 0x00,
47 0xFF, 0, 3, 0x60, 0x01, 0x04,
48 GC9A01_SET_POWER_CTL_2, 0, 1, 0x13,
49 GC9A01_SET_POWER_CTL_3, 0, 1, 0x13,
50 GC9A01_SET_POWER_CTL_4, 0, 1, 0x22,
51 0xBE, 0, 1, 0x11,
52 0xE1, 0, 2, 0x10, 0x0E,
53 0xDF, 0, 3, 0x21, 0x0C, 0x02,
54 GC9A01_SET_GAMMA1, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A,
55 GC9A01_SET_GAMMA2, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F,
56 GC9A01_SET_GAMMA3, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A,
57 GC9A01_SET_GAMMA4, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F,
58 0xED, 0, 2, 0x1B, 0x0B,
59 0xAE, 0, 1, 0x77,
60 0xCD, 0, 1, 0x63,
61 0x70, 0, 9, 0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03,
62 GC9A01_SET_FRAME_RATE, 0, 1, 0x34,
63 0x62, 0, 12, 0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70,
64 0x63, 0, 12, 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70,
65 0x64, 0, 7, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07,
66 0x66, 0, 10, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00,
67 0x67, 0, 10, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98,
68 0x74, 0, 7, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00,
69 0x98, 0, 2, 0x3E, 0x07,
70 GC9A01_CMD_TEARING_OFF, 0, 0,
71 GC9A01_CMD_INVERT_OFF, 0, 0,
72 GC9A01_CMD_SLEEP_OFF, 120, 0,
73 GC9A01_CMD_DISPLAY_ON, 20, 0
74 };
75 // clang-format on
76
77 // clang-format on
78 qp_comms_bulk_command_sequence(device, gc9a01_init_sequence, sizeof(gc9a01_init_sequence));
79
80 // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
81 const uint8_t madctl[] = {
82 [QP_ROTATION_0] = GC9A01_MADCTL_BGR,
83 [QP_ROTATION_90] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MV,
84 [QP_ROTATION_180] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MY,
85 [QP_ROTATION_270] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MV | GC9A01_MADCTL_MY,
86 };
87 qp_comms_command_databyte(device, GC9A01_SET_MEM_ACS_CTL, madctl[rotation]);
88
89 return true;
90}
91
92////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
93// Driver vtable
94////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
95
96const struct tft_panel_dc_reset_painter_driver_vtable_t gc9a01_driver_vtable = {
97 .base =
98 {
99 .init = qp_gc9a01_init,
100 .power = qp_tft_panel_power,
101 .clear = qp_tft_panel_clear,
102 .flush = qp_tft_panel_flush,
103 .pixdata = qp_tft_panel_pixdata,
104 .viewport = qp_tft_panel_viewport,
105 .palette_convert = qp_tft_panel_palette_convert,
106 .append_pixels = qp_tft_panel_append_pixels,
107 },
108 .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
109 .num_window_bytes = 2,
110 .swap_window_coords = false,
111 .opcodes =
112 {
113 .display_on = GC9A01_CMD_DISPLAY_ON,
114 .display_off = GC9A01_CMD_DISPLAY_OFF,
115 .set_column_address = GC9A01_SET_COL_ADDR,
116 .set_row_address = GC9A01_SET_PAGE_ADDR,
117 .enable_writes = GC9A01_SET_MEM,
118 },
119};
120
121#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE
122// Factory function for creating a handle to the ILI9341 device
123painter_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) {
124 for (uint32_t i = 0; i < GC9A01_NUM_DEVICES; ++i) {
125 tft_panel_dc_reset_painter_device_t *driver = &gc9a01_drivers[i];
126 if (!driver->base.driver_vtable) {
127 driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&gc9a01_driver_vtable;
128 driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
129 driver->base.native_bits_per_pixel = 16; // RGB565
130 driver->base.panel_width = panel_width;
131 driver->base.panel_height = panel_height;
132 driver->base.rotation = QP_ROTATION_0;
133 driver->base.offset_x = 0;
134 driver->base.offset_y = 0;
135
136 // SPI and other pin configuration
137 driver->base.comms_config = &driver->spi_dc_reset_config;
138 driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
139 driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
140 driver->spi_dc_reset_config.spi_config.lsb_first = false;
141 driver->spi_dc_reset_config.spi_config.mode = spi_mode;
142 driver->spi_dc_reset_config.dc_pin = dc_pin;
143 driver->spi_dc_reset_config.reset_pin = reset_pin;
144 return (painter_device_t)driver;
145 }
146 }
147 return NULL;
148}
149
150#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE
diff --git a/drivers/painter/gc9a01/qp_gc9a01.h b/drivers/painter/gc9a01/qp_gc9a01.h
new file mode 100644
index 0000000000..e2b1939564
--- /dev/null
+++ b/drivers/painter/gc9a01/qp_gc9a01.h
@@ -0,0 +1,37 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "gpio.h"
7#include "qp_internal.h"
8
9////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
10// Quantum Painter GC9A01 configurables (add to your keyboard's config.h)
11
12#ifndef GC9A01_NUM_DEVICES
13/**
14 * @def This controls the maximum number of GC9A01 devices that Quantum Painter can communicate with at any one time.
15 * Increasing this number allows for multiple displays to be used.
16 */
17# define GC9A01_NUM_DEVICES 1
18#endif
19
20////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
21// Quantum Painter GC9A01 device factories
22
23#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE
24/**
25 * Factory method for an GC9A01 SPI LCD device.
26 *
27 * @param panel_width[in] the width of the display panel
28 * @param panel_height[in] the height of the display panel
29 * @param chip_select_pin[in] the GPIO pin used for SPI chip select
30 * @param dc_pin[in] the GPIO pin used for D/C control
31 * @param reset_pin[in] the GPIO pin used for RST
32 * @param spi_divisor[in] the SPI divisor to use when communicating with the display
33 * @param spi_mode[in] the SPI mode to use when communicating with the display
34 * @return the device handle used with all drawing routines in Quantum Painter
35 */
36painter_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);
37#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE
diff --git a/drivers/painter/gc9a01/qp_gc9a01_opcodes.h b/drivers/painter/gc9a01/qp_gc9a01_opcodes.h
new file mode 100644
index 0000000000..6ff4efe7a8
--- /dev/null
+++ b/drivers/painter/gc9a01/qp_gc9a01_opcodes.h
@@ -0,0 +1,78 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7// Quantum Painter GC9A01 command opcodes
8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9
10////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
11// Level 1 command opcodes
12
13#define GC9A01_GET_ID_INFO 0x04 // Get ID information
14#define GC9A01_GET_STATUS 0x09 // Get status
15#define GC9A01_CMD_SLEEP_ON 0x10 // Enter sleep mode
16#define GC9A01_CMD_SLEEP_OFF 0x11 // Exit sleep mode
17#define GC9A01_CMD_PARTIAL_ON 0x12 // Enter partial mode
18#define GC9A01_CMD_PARTIAL_OFF 0x13 // Exit partial mode
19#define GC9A01_CMD_INVERT_ON 0x20 // Enter inverted mode
20#define GC9A01_CMD_INVERT_OFF 0x21 // Exit inverted mode
21#define GC9A01_CMD_DISPLAY_OFF 0x28 // Disable display
22#define GC9A01_CMD_DISPLAY_ON 0x29 // Enable display
23#define GC9A01_SET_COL_ADDR 0x2A // Set column address
24#define GC9A01_SET_PAGE_ADDR 0x2B // Set page address
25#define GC9A01_SET_MEM 0x2C // Set memory
26#define GC9A01_SET_PARTIAL_AREA 0x30 // Set partial area
27#define GC9A01_SET_VSCROLL 0x33 // Set vertical scroll def
28#define GC9A01_CMD_TEARING_ON 0x34 // Tearing line enabled
29#define GC9A01_CMD_TEARING_OFF 0x35 // Tearing line disabled
30#define GC9A01_SET_MEM_ACS_CTL 0x36 // Set mem access ctl
31#define GC9A01_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr
32#define GC9A01_CMD_IDLE_OFF 0x38 // Exit idle mode
33#define GC9A01_CMD_IDLE_ON 0x39 // Enter idle mode
34#define GC9A01_SET_PIX_FMT 0x3A // Set pixel format
35#define GC9A01_SET_MEM_CONT 0x3C // Set memory continue
36#define GC9A01_SET_TEAR_SCANLINE 0x44 // Set tearing scanline
37#define GC9A01_GET_TEAR_SCANLINE 0x45 // Get tearing scanline
38#define GC9A01_SET_BRIGHTNESS 0x51 // Set brightness
39#define GC9A01_SET_DISPLAY_CTL 0x53 // Set display ctl
40#define GC9A01_GET_ID1 0xDA // Get ID1
41#define GC9A01_GET_ID2 0xDB // Get ID2
42#define GC9A01_GET_ID3 0xDC // Get ID3
43
44////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
45// Level 2 command opcodes
46
47#define GC9A01_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl
48#define GC9A01_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl
49#define GC9A01_SET_FUNCTION_CTL 0xB6 // Set function ctl
50#define GC9A01_SET_TEARING_EFFECT 0xBA // Set backlight ctl 3
51#define GC9A01_SET_IF_CTL 0xF6 // Set interface control
52
53////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
54// Level 3 command opcodes
55
56#define GC9A01_SET_FRAME_RATE 0xE8 // Set frame rate
57#define GC9A01_SET_SPI_2DATA 0xE9 // Set frame rate
58#define GC9A01_SET_POWER_CTL_1 0xC1 // Set power ctl 1
59#define GC9A01_SET_POWER_CTL_2 0xC3 // Set power ctl 2
60#define GC9A01_SET_POWER_CTL_3 0xC4 // Set power ctl 3
61#define GC9A01_SET_POWER_CTL_4 0xC9 // Set power ctl 4
62#define GC9A01_SET_POWER_CTL_7 0xA7 // Set power ctl 7
63#define GC9A01_SET_INTER_REG_ENABLE1 0xFE // Enable Inter Register 1
64#define GC9A01_SET_INTER_REG_ENABLE2 0xEF // Enable Inter Register 2
65#define GC9A01_SET_GAMMA1 0xF0 //
66#define GC9A01_SET_GAMMA2 0xF1
67#define GC9A01_SET_GAMMA3 0xF2
68#define GC9A01_SET_GAMMA4 0xF3
69
70////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
71// MADCTL Flags
72#define GC9A01_MADCTL_MY 0b10000000
73#define GC9A01_MADCTL_MX 0b01000000
74#define GC9A01_MADCTL_MV 0b00100000
75#define GC9A01_MADCTL_ML 0b00010000
76#define GC9A01_MADCTL_RGB 0b00000000
77#define GC9A01_MADCTL_BGR 0b00001000
78#define GC9A01_MADCTL_MH 0b00000100
diff --git a/drivers/painter/ili9xxx/qp_ili9163.c b/drivers/painter/ili9xxx/qp_ili9163.c
new file mode 100644
index 0000000000..beaac0fbb5
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9163.c
@@ -0,0 +1,121 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "qp_internal.h"
5#include "qp_comms.h"
6#include "qp_ili9163.h"
7#include "qp_ili9xxx_opcodes.h"
8#include "qp_tft_panel.h"
9
10#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
11# include "qp_comms_spi.h"
12#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
13
14////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
15// Common
16
17// Driver storage
18tft_panel_dc_reset_painter_device_t ili9163_drivers[ILI9163_NUM_DEVICES] = {0};
19
20////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
21// Initialization
22
23bool qp_ili9163_init(painter_device_t device, painter_rotation_t rotation) {
24 // clang-format off
25 const uint8_t ili9163_init_sequence[] = {
26 // Command, Delay, N, Data[N]
27 ILI9XXX_CMD_RESET, 120, 0,
28 ILI9XXX_CMD_SLEEP_OFF, 5, 0,
29 ILI9XXX_SET_PIX_FMT, 0, 1, 0x55,
30 ILI9XXX_SET_GAMMA, 0, 1, 0x04,
31 ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x01,
32 ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0xFF, 0x06,
33 ILI9XXX_SET_PGAMMA, 0, 15, 0x36, 0x29, 0x12, 0x22, 0x1C, 0x15, 0x42, 0xB7, 0x2F, 0x13, 0x12, 0x0A, 0x11, 0x0B, 0x06,
34 ILI9XXX_SET_NGAMMA, 0, 15, 0x09, 0x16, 0x2D, 0x0D, 0x13, 0x15, 0x40, 0x48, 0x53, 0x0C, 0x1D, 0x25, 0x2E, 0x34, 0x39,
35 ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x08, 0x02,
36 ILI9XXX_SET_POWER_CTL_1, 0, 2, 0x0A, 0x02,
37 ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x02,
38 ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x50, 0x63,
39 ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0x00,
40 ILI9XXX_CMD_PARTIAL_OFF, 0, 0,
41 ILI9XXX_CMD_DISPLAY_ON, 20, 0
42 };
43 // clang-format on
44 qp_comms_bulk_command_sequence(device, ili9163_init_sequence, sizeof(ili9163_init_sequence));
45
46 // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
47 const uint8_t madctl[] = {
48 [QP_ROTATION_0] = ILI9XXX_MADCTL_BGR,
49 [QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV,
50 [QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY,
51 [QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY,
52 };
53 qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]);
54
55 return true;
56}
57
58////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
59// Driver vtable
60
61const struct tft_panel_dc_reset_painter_driver_vtable_t ili9163_driver_vtable = {
62 .base =
63 {
64 .init = qp_ili9163_init,
65 .power = qp_tft_panel_power,
66 .clear = qp_tft_panel_clear,
67 .flush = qp_tft_panel_flush,
68 .pixdata = qp_tft_panel_pixdata,
69 .viewport = qp_tft_panel_viewport,
70 .palette_convert = qp_tft_panel_palette_convert,
71 .append_pixels = qp_tft_panel_append_pixels,
72 },
73 .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
74 .num_window_bytes = 2,
75 .swap_window_coords = false,
76 .opcodes =
77 {
78 .display_on = ILI9XXX_CMD_DISPLAY_ON,
79 .display_off = ILI9XXX_CMD_DISPLAY_OFF,
80 .set_column_address = ILI9XXX_SET_COL_ADDR,
81 .set_row_address = ILI9XXX_SET_PAGE_ADDR,
82 .enable_writes = ILI9XXX_SET_MEM,
83 },
84};
85
86////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
87// SPI
88
89#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
90
91// Factory function for creating a handle to the ILI9163 device
92painter_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) {
93 for (uint32_t i = 0; i < ILI9163_NUM_DEVICES; ++i) {
94 tft_panel_dc_reset_painter_device_t *driver = &ili9163_drivers[i];
95 if (!driver->base.driver_vtable) {
96 driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9163_driver_vtable;
97 driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
98 driver->base.panel_width = panel_width;
99 driver->base.panel_height = panel_height;
100 driver->base.rotation = QP_ROTATION_0;
101 driver->base.offset_x = 0;
102 driver->base.offset_y = 0;
103 driver->base.native_bits_per_pixel = 16; // RGB565
104
105 // SPI and other pin configuration
106 driver->base.comms_config = &driver->spi_dc_reset_config;
107 driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
108 driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
109 driver->spi_dc_reset_config.spi_config.lsb_first = false;
110 driver->spi_dc_reset_config.spi_config.mode = spi_mode;
111 driver->spi_dc_reset_config.dc_pin = dc_pin;
112 driver->spi_dc_reset_config.reset_pin = reset_pin;
113 return (painter_device_t)driver;
114 }
115 }
116 return NULL;
117}
118
119#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
120
121////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/drivers/painter/ili9xxx/qp_ili9163.h b/drivers/painter/ili9xxx/qp_ili9163.h
new file mode 100644
index 0000000000..88d23629a9
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9163.h
@@ -0,0 +1,37 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "gpio.h"
7#include "qp_internal.h"
8
9////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
10// Quantum Painter ILI9163 configurables (add to your keyboard's config.h)
11
12#ifndef ILI9163_NUM_DEVICES
13/**
14 * @def This controls the maximum number of ILI9163 devices that Quantum Painter can communicate with at any one time.
15 * Increasing this number allows for multiple displays to be used.
16 */
17# define ILI9163_NUM_DEVICES 1
18#endif
19
20////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
21// Quantum Painter ILI9163 device factories
22
23#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
24/**
25 * Factory method for an ILI9163 SPI LCD device.
26 *
27 * @param panel_width[in] the width of the display panel
28 * @param panel_height[in] the height of the display panel
29 * @param chip_select_pin[in] the GPIO pin used for SPI chip select
30 * @param dc_pin[in] the GPIO pin used for D/C control
31 * @param reset_pin[in] the GPIO pin used for RST
32 * @param spi_divisor[in] the SPI divisor to use when communicating with the display
33 * @param spi_mode[in] the SPI mode to use when communicating with the display
34 * @return the device handle used with all drawing routines in Quantum Painter
35 */
36painter_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);
37#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
diff --git a/drivers/painter/ili9xxx/qp_ili9341.c b/drivers/painter/ili9xxx/qp_ili9341.c
new file mode 100644
index 0000000000..1f41dcfc0b
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9341.c
@@ -0,0 +1,128 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "qp_internal.h"
5#include "qp_comms.h"
6#include "qp_ili9341.h"
7#include "qp_ili9xxx_opcodes.h"
8#include "qp_tft_panel.h"
9
10#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
11# include <qp_comms_spi.h>
12#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
13
14////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
15// Common
16
17// Driver storage
18tft_panel_dc_reset_painter_device_t ili9341_drivers[ILI9341_NUM_DEVICES] = {0};
19
20////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
21// Initialization
22
23bool qp_ili9341_init(painter_device_t device, painter_rotation_t rotation) {
24 // clang-format off
25 const uint8_t ili9341_init_sequence[] = {
26 // Command, Delay, N, Data[N]
27 ILI9XXX_CMD_RESET, 120, 0,
28 ILI9XXX_CMD_SLEEP_OFF, 5, 0,
29 ILI9XXX_POWER_CTL_A, 0, 5, 0x39, 0x2C, 0x00, 0x34, 0x02,
30 ILI9XXX_POWER_CTL_B, 0, 3, 0x00, 0xD9, 0x30,
31 ILI9XXX_POWER_ON_SEQ_CTL, 0, 4, 0x64, 0x03, 0x12, 0x81,
32 ILI9XXX_SET_PUMP_RATIO_CTL, 0, 1, 0x20,
33 ILI9XXX_SET_POWER_CTL_1, 0, 1, 0x26,
34 ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x11,
35 ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x35, 0x3E,
36 ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0xBE,
37 ILI9XXX_DRV_TIMING_CTL_A, 0, 3, 0x85, 0x10, 0x7A,
38 ILI9XXX_DRV_TIMING_CTL_B, 0, 2, 0x00, 0x00,
39 ILI9XXX_SET_BRIGHTNESS, 0, 1, 0xFF,
40 ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x00,
41 ILI9XXX_SET_GAMMA, 0, 1, 0x01,
42 ILI9XXX_SET_PGAMMA, 0, 15, 0x0F, 0x29, 0x24, 0x0C, 0x0E, 0x09, 0x4E, 0x78, 0x3C, 0x09, 0x13, 0x05, 0x17, 0x11, 0x00,
43 ILI9XXX_SET_NGAMMA, 0, 15, 0x00, 0x16, 0x1B, 0x04, 0x11, 0x07, 0x31, 0x33, 0x42, 0x05, 0x0C, 0x0A, 0x28, 0x2F, 0x0F,
44 ILI9XXX_SET_PIX_FMT, 0, 1, 0x05,
45 ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x00, 0x1B,
46 ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0x0A, 0xA2,
47 ILI9XXX_CMD_PARTIAL_OFF, 0, 0,
48 ILI9XXX_CMD_DISPLAY_ON, 20, 0
49 };
50 // clang-format on
51 qp_comms_bulk_command_sequence(device, ili9341_init_sequence, sizeof(ili9341_init_sequence));
52
53 // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
54 const uint8_t madctl[] = {
55 [QP_ROTATION_0] = ILI9XXX_MADCTL_BGR,
56 [QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV,
57 [QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY,
58 [QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY,
59 };
60 qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]);
61
62 return true;
63}
64
65////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
66// Driver vtable
67
68const struct tft_panel_dc_reset_painter_driver_vtable_t ili9341_driver_vtable = {
69 .base =
70 {
71 .init = qp_ili9341_init,
72 .power = qp_tft_panel_power,
73 .clear = qp_tft_panel_clear,
74 .flush = qp_tft_panel_flush,
75 .pixdata = qp_tft_panel_pixdata,
76 .viewport = qp_tft_panel_viewport,
77 .palette_convert = qp_tft_panel_palette_convert,
78 .append_pixels = qp_tft_panel_append_pixels,
79 },
80 .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
81 .num_window_bytes = 2,
82 .swap_window_coords = false,
83 .opcodes =
84 {
85 .display_on = ILI9XXX_CMD_DISPLAY_ON,
86 .display_off = ILI9XXX_CMD_DISPLAY_OFF,
87 .set_column_address = ILI9XXX_SET_COL_ADDR,
88 .set_row_address = ILI9XXX_SET_PAGE_ADDR,
89 .enable_writes = ILI9XXX_SET_MEM,
90 },
91};
92
93////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
94// SPI
95
96#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
97
98// Factory function for creating a handle to the ILI9341 device
99painter_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) {
100 for (uint32_t i = 0; i < ILI9341_NUM_DEVICES; ++i) {
101 tft_panel_dc_reset_painter_device_t *driver = &ili9341_drivers[i];
102 if (!driver->base.driver_vtable) {
103 driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9341_driver_vtable;
104 driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
105 driver->base.native_bits_per_pixel = 16; // RGB565
106 driver->base.panel_width = panel_width;
107 driver->base.panel_height = panel_height;
108 driver->base.rotation = QP_ROTATION_0;
109 driver->base.offset_x = 0;
110 driver->base.offset_y = 0;
111
112 // SPI and other pin configuration
113 driver->base.comms_config = &driver->spi_dc_reset_config;
114 driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
115 driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
116 driver->spi_dc_reset_config.spi_config.lsb_first = false;
117 driver->spi_dc_reset_config.spi_config.mode = spi_mode;
118 driver->spi_dc_reset_config.dc_pin = dc_pin;
119 driver->spi_dc_reset_config.reset_pin = reset_pin;
120 return (painter_device_t)driver;
121 }
122 }
123 return NULL;
124}
125
126#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
127
128////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/drivers/painter/ili9xxx/qp_ili9341.h b/drivers/painter/ili9xxx/qp_ili9341.h
new file mode 100644
index 0000000000..28b0152a84
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9341.h
@@ -0,0 +1,37 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "gpio.h"
7#include "qp_internal.h"
8
9////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
10// Quantum Painter ILI9341 configurables (add to your keyboard's config.h)
11
12#ifndef ILI9341_NUM_DEVICES
13/**
14 * @def This controls the maximum number of ILI9341 devices that Quantum Painter can communicate with at any one time.
15 * Increasing this number allows for multiple displays to be used.
16 */
17# define ILI9341_NUM_DEVICES 1
18#endif
19
20////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
21// Quantum Painter ILI9341 device factories
22
23#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
24/**
25 * Factory method for an ILI9341 SPI LCD device.
26 *
27 * @param panel_width[in] the width of the display panel
28 * @param panel_height[in] the height of the display panel
29 * @param chip_select_pin[in] the GPIO pin used for SPI chip select
30 * @param dc_pin[in] the GPIO pin used for D/C control
31 * @param reset_pin[in] the GPIO pin used for RST
32 * @param spi_divisor[in] the SPI divisor to use when communicating with the display
33 * @param spi_mode[in] the SPI mode to use when communicating with the display
34 * @return the device handle used with all drawing routines in Quantum Painter
35 */
36painter_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);
37#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
diff --git a/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h b/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h
new file mode 100644
index 0000000000..1fa395cb89
--- /dev/null
+++ b/drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h
@@ -0,0 +1,100 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7// Quantum Painter ILI9xxx command opcodes
8#define ILI9XXX_CMD_NOP 0x00 // No operation
9#define ILI9XXX_CMD_RESET 0x01 // Software reset
10#define ILI9XXX_GET_ID_INFO 0x04 // Get ID information
11#define ILI9XXX_GET_STATUS 0x09 // Get status
12#define ILI9XXX_GET_PWR_MODE 0x0A // Get power mode
13#define ILI9XXX_GET_MADCTL 0x0B // Get MADCTL
14#define ILI9XXX_GET_PIX_FMT 0x0C // Get pixel format
15#define ILI9XXX_GET_IMG_FMT 0x0D // Get image format
16#define ILI9XXX_GET_SIG_MODE 0x0E // Get signal mode
17#define ILI9XXX_GET_SELF_DIAG 0x0F // Get self-diagnostics
18#define ILI9XXX_CMD_SLEEP_ON 0x10 // Enter sleep mode
19#define ILI9XXX_CMD_SLEEP_OFF 0x11 // Exist sleep mode
20#define ILI9XXX_CMD_PARTIAL_ON 0x12 // Enter partial mode
21#define ILI9XXX_CMD_PARTIAL_OFF 0x13 // Exit partial mode
22#define ILI9XXX_CMD_INVERT_ON 0x20 // Enter inverted mode
23#define ILI9XXX_CMD_INVERT_OFF 0x21 // Exit inverted mode
24#define ILI9XXX_SET_GAMMA 0x26 // Set gamma params
25#define ILI9XXX_CMD_DISPLAY_OFF 0x28 // Disable display
26#define ILI9XXX_CMD_DISPLAY_ON 0x29 // Enable display
27#define ILI9XXX_SET_COL_ADDR 0x2A // Set column address
28#define ILI9XXX_SET_PAGE_ADDR 0x2B // Set page address
29#define ILI9XXX_SET_MEM 0x2C // Set memory
30#define ILI9XXX_SET_COLOR 0x2D // Set color
31#define ILI9XXX_GET_MEM 0x2E // Get memory
32#define ILI9XXX_SET_PARTIAL_AREA 0x30 // Set partial area
33#define ILI9XXX_SET_VSCROLL 0x33 // Set vertical scroll def
34#define ILI9XXX_CMD_TEARING_ON 0x34 // Tearing line enabled
35#define ILI9XXX_CMD_TEARING_OFF 0x35 // Tearing line disabled
36#define ILI9XXX_SET_MEM_ACS_CTL 0x36 // Set mem access ctl
37#define ILI9XXX_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr
38#define ILI9XXX_CMD_IDLE_OFF 0x38 // Exit idle mode
39#define ILI9XXX_CMD_IDLE_ON 0x39 // Enter idle mode
40#define ILI9XXX_SET_PIX_FMT 0x3A // Set pixel format
41#define ILI9XXX_SET_MEM_CONT 0x3C // Set memory continue
42#define ILI9XXX_GET_MEM_CONT 0x3E // Get memory continue
43#define ILI9XXX_SET_TEAR_SCANLINE 0x44 // Set tearing scanline
44#define ILI9XXX_GET_TEAR_SCANLINE 0x45 // Get tearing scanline
45#define ILI9XXX_SET_BRIGHTNESS 0x51 // Set brightness
46#define ILI9XXX_GET_BRIGHTNESS 0x52 // Get brightness
47#define ILI9XXX_SET_DISPLAY_CTL 0x53 // Set display ctl
48#define ILI9XXX_GET_DISPLAY_CTL 0x54 // Get display ctl
49#define ILI9XXX_SET_CABC 0x55 // Set CABC
50#define ILI9XXX_GET_CABC 0x56 // Get CABC
51#define ILI9XXX_SET_CABC_MIN 0x5E // Set CABC min
52#define ILI9XXX_GET_CABC_MIN 0x5F // Set CABC max
53#define ILI9XXX_GET_ID1 0xDA // Get ID1
54#define ILI9XXX_GET_ID2 0xDB // Get ID2
55#define ILI9XXX_GET_ID3 0xDC // Get ID3
56#define ILI9XXX_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl
57#define ILI9XXX_SET_FRAME_CTL_NORMAL 0xB1 // Set frame ctl (normal)
58#define ILI9XXX_SET_FRAME_CTL_IDLE 0xB2 // Set frame ctl (idle)
59#define ILI9XXX_SET_FRAME_CTL_PARTIAL 0xB3 // Set frame ctl (partial)
60#define ILI9XXX_SET_INVERSION_CTL 0xB4 // Set inversion ctl
61#define ILI9XXX_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl
62#define ILI9XXX_SET_FUNCTION_CTL 0xB6 // Set function ctl
63#define ILI9XXX_SET_ENTRY_MODE 0xB7 // Set entry mode
64#define ILI9XXX_SET_LIGHT_CTL_1 0xB8 // Set backlight ctl 1
65#define ILI9XXX_SET_LIGHT_CTL_2 0xB9 // Set backlight ctl 2
66#define ILI9XXX_SET_LIGHT_CTL_3 0xBA // Set backlight ctl 3
67#define ILI9XXX_SET_LIGHT_CTL_4 0xBB // Set backlight ctl 4
68#define ILI9XXX_SET_LIGHT_CTL_5 0xBC // Set backlight ctl 5
69#define ILI9XXX_SET_LIGHT_CTL_7 0xBE // Set backlight ctl 7
70#define ILI9XXX_SET_LIGHT_CTL_8 0xBF // Set backlight ctl 8
71#define ILI9XXX_SET_POWER_CTL_1 0xC0 // Set power ctl 1
72#define ILI9XXX_SET_POWER_CTL_2 0xC1 // Set power ctl 2
73#define ILI9XXX_SET_VCOM_CTL_1 0xC5 // Set VCOM ctl 1
74#define ILI9XXX_SET_VCOM_CTL_2 0xC7 // Set VCOM ctl 2
75#define ILI9XXX_POWER_CTL_A 0xCB // Set power control A
76#define ILI9XXX_POWER_CTL_B 0xCF // Set power control B
77#define ILI9XXX_DRV_TIMING_CTL_A 0xE8 // Set driver timing control A
78#define ILI9XXX_DRV_TIMING_CTL_B 0xEA // Set driver timing control B
79#define ILI9XXX_POWER_ON_SEQ_CTL 0xED // Set Power on sequence control
80#define ILI9XXX_SET_NVMEM 0xD0 // Set NVMEM data
81#define ILI9XXX_GET_NVMEM_KEY 0xD1 // Get NVMEM protect key
82#define ILI9XXX_GET_NVMEM_STATUS 0xD2 // Get NVMEM status
83#define ILI9XXX_GET_ID4 0xD3 // Get ID4
84#define ILI9XXX_SET_PGAMMA 0xE0 // Set positive gamma
85#define ILI9XXX_SET_NGAMMA 0xE1 // Set negative gamma
86#define ILI9XXX_SET_DGAMMA_CTL_1 0xE2 // Set digital gamma ctl 1
87#define ILI9XXX_SET_DGAMMA_CTL_2 0xE3 // Set digital gamma ctl 2
88#define ILI9XXX_ENABLE_3_GAMMA 0xF2 // Enable 3 gamma
89#define ILI9XXX_SET_IF_CTL 0xF6 // Set interface control
90#define ILI9XXX_SET_PUMP_RATIO_CTL 0xF7 // Set pump ratio control
91
92////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
93// MADCTL Flags
94#define ILI9XXX_MADCTL_MY 0b10000000
95#define ILI9XXX_MADCTL_MX 0b01000000
96#define ILI9XXX_MADCTL_MV 0b00100000
97#define ILI9XXX_MADCTL_ML 0b00010000
98#define ILI9XXX_MADCTL_RGB 0b00000000
99#define ILI9XXX_MADCTL_BGR 0b00001000
100#define ILI9XXX_MADCTL_MH 0b00000100
diff --git a/drivers/painter/ssd1351/qp_ssd1351.c b/drivers/painter/ssd1351/qp_ssd1351.c
new file mode 100644
index 0000000000..970e7e67f3
--- /dev/null
+++ b/drivers/painter/ssd1351/qp_ssd1351.c
@@ -0,0 +1,125 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "qp_internal.h"
5#include "qp_comms.h"
6#include "qp_ssd1351.h"
7#include "qp_ssd1351_opcodes.h"
8#include "qp_tft_panel.h"
9
10#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
11# include "qp_comms_spi.h"
12#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
13
14////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
15// Common
16
17// Driver storage
18tft_panel_dc_reset_painter_device_t ssd1351_drivers[SSD1351_NUM_DEVICES] = {0};
19
20////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
21// Initialization
22
23bool qp_ssd1351_init(painter_device_t device, painter_rotation_t rotation) {
24 tft_panel_dc_reset_painter_device_t *driver = (tft_panel_dc_reset_painter_device_t *)device;
25
26 // clang-format off
27 const uint8_t ssd1351_init_sequence[] = {
28 // Command, Delay, N, Data[N]
29 SSD1351_COMMANDLOCK, 5, 1, 0x12,
30 SSD1351_COMMANDLOCK, 5, 1, 0xB1,
31 SSD1351_DISPLAYOFF, 5, 0,
32 SSD1351_CLOCKDIV, 5, 1, 0xF1,
33 SSD1351_MUXRATIO, 5, 1, 0x7F,
34 SSD1351_DISPLAYOFFSET, 5, 1, 0x00,
35 SSD1351_SETGPIO, 5, 1, 0x00,
36 SSD1351_FUNCTIONSELECT, 5, 1, 0x01,
37 SSD1351_PRECHARGE, 5, 1, 0x32,
38 SSD1351_VCOMH, 5, 1, 0x05,
39 SSD1351_NORMALDISPLAY, 5, 0,
40 SSD1351_CONTRASTABC, 5, 3, 0xC8, 0x80, 0xC8,
41 SSD1351_CONTRASTMASTER, 5, 1, 0x0F,
42 SSD1351_SETVSL, 5, 3, 0xA0, 0xB5, 0x55,
43 SSD1351_PRECHARGE2, 5, 1, 0x01,
44 SSD1351_DISPLAYON, 5, 0,
45 };
46 // clang-format on
47 qp_comms_bulk_command_sequence(device, ssd1351_init_sequence, sizeof(ssd1351_init_sequence));
48
49 // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
50 const uint8_t madctl[] = {
51 [QP_ROTATION_0] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MY,
52 [QP_ROTATION_90] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX | SSD1351_MADCTL_MY | SSD1351_MADCTL_MV,
53 [QP_ROTATION_180] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX,
54 [QP_ROTATION_270] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MV,
55 };
56 qp_comms_command_databyte(device, SSD1351_SETREMAP, madctl[rotation]);
57 qp_comms_command_databyte(device, SSD1351_STARTLINE, (rotation == QP_ROTATION_0 || rotation == QP_ROTATION_90) ? driver->base.panel_height : 0);
58
59 return true;
60}
61
62////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
63// Driver vtable
64
65const struct tft_panel_dc_reset_painter_driver_vtable_t ssd1351_driver_vtable = {
66 .base =
67 {
68 .init = qp_ssd1351_init,
69 .power = qp_tft_panel_power,
70 .clear = qp_tft_panel_clear,
71 .flush = qp_tft_panel_flush,
72 .pixdata = qp_tft_panel_pixdata,
73 .viewport = qp_tft_panel_viewport,
74 .palette_convert = qp_tft_panel_palette_convert,
75 .append_pixels = qp_tft_panel_append_pixels,
76 },
77 .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
78 .num_window_bytes = 1,
79 .swap_window_coords = true,
80 .opcodes =
81 {
82 .display_on = SSD1351_DISPLAYON,
83 .display_off = SSD1351_DISPLAYOFF,
84 .set_column_address = SSD1351_SETCOLUMN,
85 .set_row_address = SSD1351_SETROW,
86 .enable_writes = SSD1351_WRITERAM,
87 },
88};
89
90////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
91// SPI
92
93#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
94
95// Factory function for creating a handle to the SSD1351 device
96painter_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) {
97 for (uint32_t i = 0; i < SSD1351_NUM_DEVICES; ++i) {
98 tft_panel_dc_reset_painter_device_t *driver = &ssd1351_drivers[i];
99 if (!driver->base.driver_vtable) {
100 driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ssd1351_driver_vtable;
101 driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
102 driver->base.panel_width = panel_width;
103 driver->base.panel_height = panel_height;
104 driver->base.rotation = QP_ROTATION_0;
105 driver->base.offset_x = 0;
106 driver->base.offset_y = 0;
107 driver->base.native_bits_per_pixel = 16; // RGB565
108
109 // SPI and other pin configuration
110 driver->base.comms_config = &driver->spi_dc_reset_config;
111 driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
112 driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
113 driver->spi_dc_reset_config.spi_config.lsb_first = false;
114 driver->spi_dc_reset_config.spi_config.mode = spi_mode;
115 driver->spi_dc_reset_config.dc_pin = dc_pin;
116 driver->spi_dc_reset_config.reset_pin = reset_pin;
117 return (painter_device_t)driver;
118 }
119 }
120 return NULL;
121}
122
123#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
124
125////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/drivers/painter/ssd1351/qp_ssd1351.h b/drivers/painter/ssd1351/qp_ssd1351.h
new file mode 100644
index 0000000000..0df34f204d
--- /dev/null
+++ b/drivers/painter/ssd1351/qp_ssd1351.h
@@ -0,0 +1,37 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "gpio.h"
7#include "qp_internal.h"
8
9////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
10// Quantum Painter SSD1351 configurables (add to your keyboard's config.h)
11
12#ifndef SSD1351_NUM_DEVICES
13/**
14 * @def This controls the maximum number of SSD1351 devices that Quantum Painter can communicate with at any one time.
15 * Increasing this number allows for multiple displays to be used.
16 */
17# define SSD1351_NUM_DEVICES 1
18#endif
19
20////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
21// Quantum Painter SSD1351 device factories
22
23#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
24/**
25 * Factory method for an SSD1351 SPI OLED device.
26 *
27 * @param panel_width[in] the width of the display panel
28 * @param panel_height[in] the height of the display panel
29 * @param chip_select_pin[in] the GPIO pin used for SPI chip select
30 * @param dc_pin[in] the GPIO pin used for D/C control
31 * @param reset_pin[in] the GPIO pin used for RST
32 * @param spi_divisor[in] the SPI divisor to use when communicating with the display
33 * @param spi_mode[in] the SPI mode to use when communicating with the display
34 * @return the device handle used with all drawing routines in Quantum Painter
35 */
36painter_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);
37#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
diff --git a/drivers/painter/ssd1351/qp_ssd1351_opcodes.h b/drivers/painter/ssd1351/qp_ssd1351_opcodes.h
new file mode 100644
index 0000000000..48ed2a3a7c
--- /dev/null
+++ b/drivers/painter/ssd1351/qp_ssd1351_opcodes.h
@@ -0,0 +1,48 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7// Quantum Painter SSD1351 command opcodes
8
9// System function commands
10#define SSD1351_SETCOLUMN 0x15
11#define SSD1351_SETROW 0x75
12#define SSD1351_WRITERAM 0x5C
13#define SSD1351_READRAM 0x5D
14#define SSD1351_SETREMAP 0xA0
15#define SSD1351_STARTLINE 0xA1
16#define SSD1351_DISPLAYOFFSET 0xA2
17#define SSD1351_DISPLAYALLOFF 0xA4
18#define SSD1351_DISPLAYALLON 0xA5
19#define SSD1351_NORMALDISPLAY 0xA6
20#define SSD1351_INVERTDISPLAY 0xA7
21#define SSD1351_FUNCTIONSELECT 0xAB
22#define SSD1351_DISPLAYOFF 0xAE
23#define SSD1351_DISPLAYON 0xAF
24#define SSD1351_PRECHARGE 0xB1
25#define SSD1351_DISPLAYENHANCE 0xB2
26#define SSD1351_CLOCKDIV 0xB3
27#define SSD1351_SETVSL 0xB4
28#define SSD1351_SETGPIO 0xB5
29#define SSD1351_PRECHARGE2 0xB6
30#define SSD1351_SETGRAY 0xB8
31#define SSD1351_USELUT 0xB9
32#define SSD1351_PRECHARGELEVEL 0xBB
33#define SSD1351_VCOMH 0xBE
34#define SSD1351_CONTRASTABC 0xC1
35#define SSD1351_CONTRASTMASTER 0xC7
36#define SSD1351_MUXRATIO 0xCA
37#define SSD1351_COMMANDLOCK 0xFD
38#define SSD1351_HORIZSCROLL 0x96
39#define SSD1351_STOPSCROLL 0x9E
40#define SSD1351_STARTSCROLL 0x9F
41
42////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
43// SETREMAP (MADCTL) Flags
44#define SSD1351_MADCTL_MY 0b00010000
45#define SSD1351_MADCTL_MX 0b00000010
46#define SSD1351_MADCTL_MV 0b00000001
47#define SSD1351_MADCTL_RGB 0b01100000
48#define SSD1351_MADCTL_BGR 0b01100100
diff --git a/drivers/painter/st77xx/qp_st7789.c b/drivers/painter/st77xx/qp_st7789.c
new file mode 100644
index 0000000000..d005ece050
--- /dev/null
+++ b/drivers/painter/st77xx/qp_st7789.c
@@ -0,0 +1,144 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// Copyright 2021 Nick Brassel (@tzarc)
3// SPDX-License-Identifier: GPL-2.0-or-later
4
5#include "qp_internal.h"
6#include "qp_comms.h"
7#include "qp_st7789.h"
8#include "qp_st77xx_opcodes.h"
9#include "qp_st7789_opcodes.h"
10#include "qp_tft_panel.h"
11
12#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
13# include "qp_comms_spi.h"
14#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
15
16////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
17// Common
18
19// Driver storage
20tft_panel_dc_reset_painter_device_t st7789_drivers[ST7789_NUM_DEVICES] = {0};
21
22////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
23// Automatic viewport offsets
24
25#ifndef ST7789_NO_AUTOMATIC_OFFSETS
26static inline void st7789_automatic_viewport_offsets(painter_device_t device, painter_rotation_t rotation) {
27 struct painter_driver_t *driver = (struct painter_driver_t *)device;
28
29 // clang-format off
30 const struct {
31 uint16_t offset_x;
32 uint16_t offset_y;
33 } rotation_offsets_240x240[] = {
34 [QP_ROTATION_0] = { .offset_x = 0, .offset_y = 0 },
35 [QP_ROTATION_90] = { .offset_x = 0, .offset_y = 0 },
36 [QP_ROTATION_180] = { .offset_x = 0, .offset_y = 80 },
37 [QP_ROTATION_270] = { .offset_x = 80, .offset_y = 0 },
38 };
39 // clang-format on
40
41 if (driver->panel_width == 240 && driver->panel_height == 240) {
42 driver->offset_x = rotation_offsets_240x240[rotation].offset_x;
43 driver->offset_y = rotation_offsets_240x240[rotation].offset_y;
44 }
45}
46#endif // ST7789_NO_AUTOMATIC_OFFSETS
47
48////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
49// Initialization
50
51bool qp_st7789_init(painter_device_t device, painter_rotation_t rotation) {
52 // clang-format off
53 const uint8_t st7789_init_sequence[] = {
54 // Command, Delay, N, Data[N]
55 ST77XX_CMD_RESET, 120, 0,
56 ST77XX_CMD_SLEEP_OFF, 5, 0,
57 ST77XX_SET_PIX_FMT, 0, 1, 0x55,
58 ST77XX_CMD_INVERT_ON, 0, 0,
59 ST77XX_CMD_NORMAL_ON, 0, 0,
60 ST77XX_CMD_DISPLAY_ON, 20, 0
61 };
62 // clang-format on
63 qp_comms_bulk_command_sequence(device, st7789_init_sequence, sizeof(st7789_init_sequence));
64
65 // Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
66 const uint8_t madctl[] = {
67 [QP_ROTATION_0] = ST77XX_MADCTL_RGB,
68 [QP_ROTATION_90] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MV,
69 [QP_ROTATION_180] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MY,
70 [QP_ROTATION_270] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MY,
71 };
72 qp_comms_command_databyte(device, ST77XX_SET_MADCTL, madctl[rotation]);
73
74#ifndef ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
75 st7789_automatic_viewport_offsets(device, rotation);
76#endif // ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
77
78 return true;
79}
80
81////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
82// Driver vtable
83
84const struct tft_panel_dc_reset_painter_driver_vtable_t st7789_driver_vtable = {
85 .base =
86 {
87 .init = qp_st7789_init,
88 .power = qp_tft_panel_power,
89 .clear = qp_tft_panel_clear,
90 .flush = qp_tft_panel_flush,
91 .pixdata = qp_tft_panel_pixdata,
92 .viewport = qp_tft_panel_viewport,
93 .palette_convert = qp_tft_panel_palette_convert,
94 .append_pixels = qp_tft_panel_append_pixels,
95 },
96 .rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
97 .num_window_bytes = 2,
98 .swap_window_coords = false,
99 .opcodes =
100 {
101 .display_on = ST77XX_CMD_DISPLAY_ON,
102 .display_off = ST77XX_CMD_DISPLAY_OFF,
103 .set_column_address = ST77XX_SET_COL_ADDR,
104 .set_row_address = ST77XX_SET_ROW_ADDR,
105 .enable_writes = ST77XX_SET_MEM,
106 },
107};
108
109////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
110// SPI
111
112#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
113
114// Factory function for creating a handle to the ST7789 device
115painter_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) {
116 for (uint32_t i = 0; i < ST7789_NUM_DEVICES; ++i) {
117 tft_panel_dc_reset_painter_device_t *driver = &st7789_drivers[i];
118 if (!driver->base.driver_vtable) {
119 driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&st7789_driver_vtable;
120 driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
121 driver->base.panel_width = panel_width;
122 driver->base.panel_height = panel_height;
123 driver->base.rotation = QP_ROTATION_0;
124 driver->base.offset_x = 0;
125 driver->base.offset_y = 0;
126 driver->base.native_bits_per_pixel = 16; // RGB565
127
128 // SPI and other pin configuration
129 driver->base.comms_config = &driver->spi_dc_reset_config;
130 driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
131 driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
132 driver->spi_dc_reset_config.spi_config.lsb_first = false;
133 driver->spi_dc_reset_config.spi_config.mode = spi_mode;
134 driver->spi_dc_reset_config.dc_pin = dc_pin;
135 driver->spi_dc_reset_config.reset_pin = reset_pin;
136 return (painter_device_t)driver;
137 }
138 }
139 return NULL;
140}
141
142#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
143
144////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/drivers/painter/st77xx/qp_st7789.h b/drivers/painter/st77xx/qp_st7789.h
new file mode 100644
index 0000000000..ec61f5d70b
--- /dev/null
+++ b/drivers/painter/st77xx/qp_st7789.h
@@ -0,0 +1,44 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// Copyright 2021 Nick Brassel (@tzarc)
3// SPDX-License-Identifier: GPL-2.0-or-later
4
5#pragma once
6
7#include "gpio.h"
8#include "qp_internal.h"
9
10////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
11// Quantum Painter ST7789 configurables (add to your keyboard's config.h)
12
13#ifndef ST7789_NUM_DEVICES
14/**
15 * @def This controls the maximum number of ST7789 devices that Quantum Painter can communicate with at any one time.
16 * Increasing this number allows for multiple displays to be used.
17 */
18# define ST7789_NUM_DEVICES 1
19#endif
20
21// Additional configuration options to be copied to your keyboard's config.h (don't change here):
22
23// If you know exactly which offsets should be used on your panel with respect to selected rotation, then this config
24// option allows you to save some flash space -- you'll need to invoke qp_set_viewport_offsets() instead from your keyboard.
25// #define ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
26
27////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
28// Quantum Painter ST7789 device factories
29
30#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
31/**
32 * Factory method for an ST7789 SPI LCD device.
33 *
34 * @param panel_width[in] the width of the display panel
35 * @param panel_height[in] the height of the display panel
36 * @param chip_select_pin[in] the GPIO pin used for SPI chip select
37 * @param dc_pin[in] the GPIO pin used for D/C control
38 * @param reset_pin[in] the GPIO pin used for RST
39 * @param spi_divisor[in] the SPI divisor to use when communicating with the display
40 * @param spi_mode[in] the SPI mode to use when communicating with the display
41 * @return the device handle used with all drawing routines in Quantum Painter
42 */
43painter_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);
44#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
diff --git a/drivers/painter/st77xx/qp_st7789_opcodes.h b/drivers/painter/st77xx/qp_st7789_opcodes.h
new file mode 100644
index 0000000000..b5baba7184
--- /dev/null
+++ b/drivers/painter/st77xx/qp_st7789_opcodes.h
@@ -0,0 +1,64 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7// Quantum Painter ST7789 additional command opcodes
8
9// System function commands
10#define ST7789_GET_SELF_DIAG 0x0F // Get self-diagnostic result
11#define ST7789_SET_VERT_SCRL 0x33 // Set vertical scroll definition
12#define ST7789_SET_VERT_SCRL_ADDR 0x37 // SEt Vertical scroll start address
13#define ST7789_SET_MEM_CONT 0x3C // Memory Write continue
14#define ST7789_GET_MEM_CONT 0x3E // Memory Read continue
15#define ST7789_SET_TEAR_LINE 0x44 // Set tear scanline
16#define ST7789_GET_TEAR_LINE 0x45 // Get tear scanline
17#define ST7789_SET_BRIGHTNESS 0x51 // Set display brightness
18#define ST7789_GET_BRIGHTNESS 0x52 // Get display brightness
19#define ST7789_SET_CTRL 0x53 // Set CTRL display
20#define ST7789_GET_CTRL 0x54 // Get CTRL display value
21#define ST7789_SET_CAB_COLOR 0x55 // Set content adaptive brightness control and color enhancement
22#define ST7789_GET_CAB_COLOR 0x56 // Get content adaptive brightness control and color enhancement
23#define ST7789_SET_CAB_BRIGHTNESS 0x5E // Set content adaptive minimum brightness
24#define ST7789_GET_CAB_BRIGHTNESS 0x5F // Get content adaptive minimum brightness
25#define ST7789_GET_ABC_SELF_DIAG 0x68 // Get Auto brightness control self diagnostics
26
27// Panel Function Commands
28#define ST7789_SET_RAM_CTL 0xB0 // Set RAM control
29#define ST7789_SET_RGB_CTL 0xB1 // Set RGB control
30#define ST7789_SET_PORCH_CTL 0xB2 // Set Porch control
31#define ST7789_SET_FRAME_RATE_CTL_1 0xB3 // Set frame rate control 1
32#define ST7789_SET_PARTIAL_CTL 0xB5 // Set Partial control
33#define ST7789_SET_GATE_CTL 0xB7 // Set gate control
34#define ST7789_SET_GATE_ON_TIMING 0xB8 // Set gate on timing adjustment
35#define ST7789_SET_DIGITAL_GAMMA_ON 0xBA // Enable digital gamma
36#define ST7789_SET_VCOM 0xBB // Set VCOM
37#define ST7789_SET_POWER_SAVE 0xBC // Set power saving mode
38#define ST7789_SET_DISP_OFF_POWER 0xBD // Set display off power saving
39#define ST7789_SET_LCM_CTL 0xC0 // Set LCM control
40#define ST7789_SET_IDS 0xC1 // Set IDs
41#define ST7789_SET_VDV_VRH_ON 0xC2 // Set VDV and VRH command enable
42#define ST7789_SET_VRH 0xC3 // Set VRH
43#define ST7789_SET_VDV 0xC4 // Set VDV
44#define ST7789_SET_VCOM_OFFSET 0xC5 // Set VCOM offset ctl
45#define ST7789_SET_FRAME_RATE_CTL_2 0xC6 // Set frame rate control 2
46#define ST7789_SET_CABC_CTL 0xC7 // Set CABC Control
47#define ST7789_GET_REG_1 0xC8 // Get register value selection1
48#define ST7789_GET_REG_2 0xCA // Get register value selection2
49#define ST7789_SET_PWM_FREQ 0xCC // Set PWM frequency
50#define ST7789_SET_POWER_CTL_1 0xD0 // Set power ctl 1
51#define ST7789_SET_VAP_VAN_ON 0xD2 // Enable VAP/VAN signal output
52#define ST7789_SET_CMD2_ENABLE 0xDF // Enable command 2
53#define ST7789_SET_PGAMMA 0xE0 // Set positive gamma
54#define ST7789_SET_NGAMMA 0xE1 // Set negative gamma
55#define ST7789_SET_DIGITAL_GAMMA_RED 0xE2 // Set digital gamma lookup table for red
56#define ST7789_SET_DIGITAL_GAMMA_BLUE 0xE3 // Get digital gamma lookup table for blue
57#define ST7789_SET_GATE_CTL_2 0xE4 // Set gate control 2
58#define ST7789_SET_SPI2_ENABLE 0xE7 // Enable SPI2
59#define ST7789_SET_POWER_CTL_2 0xE8 // Set power ctl 2
60#define ST7789_SET_EQ_TIME_CTL 0xE9 // Set equalize time control
61#define ST7789_SET_PROG_CTL 0xEC // Set program control
62#define ST7789_SET_PROG_MODE_ENABLE 0xFA // Set program mode enable
63#define ST7789_SET_NVMEM 0xFC // Set NVMEM data
64#define ST7789_SET_PROG_ACTION 0xFE // Set program action
diff --git a/drivers/painter/st77xx/qp_st77xx_opcodes.h b/drivers/painter/st77xx/qp_st77xx_opcodes.h
new file mode 100644
index 0000000000..131378d832
--- /dev/null
+++ b/drivers/painter/st77xx/qp_st77xx_opcodes.h
@@ -0,0 +1,51 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7// Quantum Painter ST77XX command opcodes
8
9// System function commands
10#define ST77XX_CMD_NOP 0x00 // No operation
11#define ST77XX_CMD_RESET 0x01 // Software reset
12#define ST77XX_GET_ID_INFO 0x04 // Get ID information
13#define ST77XX_GET_STATUS 0x09 // Get status
14#define ST77XX_GET_PWR_MODE 0x0A // Get power mode
15#define ST77XX_GET_MADCTL 0x0B // Get mem access ctl
16#define ST77XX_GET_PIX_FMT 0x0C // Get pixel format
17#define ST77XX_GET_IMG_FMT 0x0D // Get image format
18#define ST77XX_GET_SIG_MODE 0x0E // Get signal mode
19#define ST77XX_CMD_SLEEP_ON 0x10 // Enter sleep mode
20#define ST77XX_CMD_SLEEP_OFF 0x11 // Exist sleep mode
21#define ST77XX_CMD_PARTIAL_ON 0x12 // Enter partial mode
22#define ST77XX_CMD_NORMAL_ON 0x13 // Exit partial mode
23#define ST77XX_CMD_INVERT_OFF 0x20 // Exit inverted mode
24#define ST77XX_CMD_INVERT_ON 0x21 // Enter inverted mode
25#define ST77XX_SET_GAMMA 0x26 // Set gamma params
26#define ST77XX_CMD_DISPLAY_OFF 0x28 // Disable display
27#define ST77XX_CMD_DISPLAY_ON 0x29 // Enable display
28#define ST77XX_SET_COL_ADDR 0x2A // Set column address
29#define ST77XX_SET_ROW_ADDR 0x2B // Set page (row) address
30#define ST77XX_SET_MEM 0x2C // Set memory
31#define ST77XX_GET_MEM 0x2E // Get memory
32#define ST77XX_SET_PARTIAL_AREA 0x30 // Set partial area
33#define ST77XX_CMD_TEARING_OFF 0x34 // Tearing line disabled
34#define ST77XX_CMD_TEARING_ON 0x35 // Tearing line enabled
35#define ST77XX_SET_MADCTL 0x36 // Set mem access ctl
36#define ST77XX_CMD_IDLE_OFF 0x38 // Exit idle mode
37#define ST77XX_CMD_IDLE_ON 0x39 // Enter idle mode
38#define ST77XX_SET_PIX_FMT 0x3A // Set pixel format
39#define ST77XX_GET_ID1 0xDA // Get ID1
40#define ST77XX_GET_ID2 0xDB // Get ID2
41#define ST77XX_GET_ID3 0xDC // Get ID3
42
43////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
44// MADCTL Flags
45#define ST77XX_MADCTL_MY 0b10000000
46#define ST77XX_MADCTL_MX 0b01000000
47#define ST77XX_MADCTL_MV 0b00100000
48#define ST77XX_MADCTL_ML 0b00010000
49#define ST77XX_MADCTL_RGB 0b00000000
50#define ST77XX_MADCTL_BGR 0b00001000
51#define ST77XX_MADCTL_MH 0b00000100
diff --git a/drivers/painter/tft_panel/qp_tft_panel.c b/drivers/painter/tft_panel/qp_tft_panel.c
new file mode 100644
index 0000000000..4d636c9509
--- /dev/null
+++ b/drivers/painter/tft_panel/qp_tft_panel.c
@@ -0,0 +1,130 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "color.h"
5#include "qp_internal.h"
6#include "qp_comms.h"
7#include "qp_draw.h"
8#include "qp_tft_panel.h"
9
10#define BYTE_SWAP(x) (((((uint16_t)(x)) >> 8) & 0x00FF) | ((((uint16_t)(x)) << 8) & 0xFF00))
11
12////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
13// Native pixel format conversion
14
15uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
16 uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3);
17 return rgb565;
18}
19
20uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b) {
21 uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3);
22 return BYTE_SWAP(rgb565);
23}
24
25uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b) {
26 uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3);
27 return bgr565;
28}
29
30uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b) {
31 uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3);
32 return BYTE_SWAP(bgr565);
33}
34
35////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
36// Quantum Painter API implementations
37
38// Power control
39bool qp_tft_panel_power(painter_device_t device, bool power_on) {
40 struct painter_driver_t * driver = (struct painter_driver_t *)device;
41 struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
42 qp_comms_command(device, power_on ? vtable->opcodes.display_on : vtable->opcodes.display_off);
43 return true;
44}
45
46// Screen clear
47bool qp_tft_panel_clear(painter_device_t device) {
48 struct painter_driver_t *driver = (struct painter_driver_t *)device;
49 driver->driver_vtable->init(device, driver->rotation); // Re-init the LCD
50 return true;
51}
52
53// Screen flush
54bool qp_tft_panel_flush(painter_device_t device) {
55 // No-op, as there's no framebuffer in RAM for this device.
56 return true;
57}
58
59// Viewport to draw to
60bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
61 struct painter_driver_t * driver = (struct painter_driver_t *)device;
62 struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
63
64 // Fix up the drawing location if required
65 left += driver->offset_x;
66 right += driver->offset_x;
67 top += driver->offset_y;
68 bottom += driver->offset_y;
69
70 // Check if we need to manually swap the window coordinates based on whether or not we're in a sideways rotation
71 if (vtable->swap_window_coords && (driver->rotation == QP_ROTATION_90 || driver->rotation == QP_ROTATION_270)) {
72 uint16_t temp;
73
74 temp = left;
75 left = top;
76 top = temp;
77
78 temp = right;
79 right = bottom;
80 bottom = temp;
81 }
82
83 if (vtable->num_window_bytes == 1) {
84 // Set up the x-window
85 uint8_t xbuf[2] = {left & 0xFF, right & 0xFF};
86 qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf));
87
88 // Set up the y-window
89 uint8_t ybuf[2] = {top & 0xFF, bottom & 0xFF};
90 qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf));
91 } else if (vtable->num_window_bytes == 2) {
92 // Set up the x-window
93 uint8_t xbuf[4] = {left >> 8, left & 0xFF, right >> 8, right & 0xFF};
94 qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf));
95
96 // Set up the y-window
97 uint8_t ybuf[4] = {top >> 8, top & 0xFF, bottom >> 8, bottom & 0xFF};
98 qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf));
99 }
100
101 // Lock in the window
102 qp_comms_command(device, vtable->opcodes.enable_writes);
103 return true;
104}
105
106// Stream pixel data to the current write position in GRAM
107bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
108 qp_comms_send(device, pixel_data, native_pixel_count * sizeof(uint16_t));
109 return true;
110}
111
112// Convert supplied palette entries into their native equivalents
113bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) {
114 struct painter_driver_t * driver = (struct painter_driver_t *)device;
115 struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
116 for (int16_t i = 0; i < palette_size; ++i) {
117 RGB rgb = hsv_to_rgb_nocie((HSV){palette[i].hsv888.h, palette[i].hsv888.s, palette[i].hsv888.v});
118 palette[i].rgb565 = vtable->rgb888_to_native16bit(rgb.r, rgb.g, rgb.b);
119 }
120 return true;
121}
122
123// Append pixels to the target location, keyed by the pixel index
124bool 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) {
125 uint16_t *buf = (uint16_t *)target_buffer;
126 for (uint32_t i = 0; i < pixel_count; ++i) {
127 buf[pixel_offset + i] = palette[palette_indices[i]].rgb565;
128 }
129 return true;
130}
diff --git a/drivers/painter/tft_panel/qp_tft_panel.h b/drivers/painter/tft_panel/qp_tft_panel.h
new file mode 100644
index 0000000000..6eddfc503d
--- /dev/null
+++ b/drivers/painter/tft_panel/qp_tft_panel.h
@@ -0,0 +1,67 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "color.h"
5#include "qp_internal.h"
6
7#ifdef QUANTUM_PAINTER_SPI_ENABLE
8# include "qp_comms_spi.h"
9#endif // QUANTUM_PAINTER_SPI_ENABLE
10
11////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
12// Common TFT panel implementation using D/C, and RST pins.
13
14typedef uint16_t (*rgb888_to_native_uint16_t)(uint8_t r, uint8_t g, uint8_t b);
15
16// Driver vtable with extras
17struct tft_panel_dc_reset_painter_driver_vtable_t {
18 struct painter_driver_vtable_t base; // must be first, so it can be cast to/from the painter_driver_vtable_t* type
19
20 // Conversion function for palette entries
21 rgb888_to_native_uint16_t rgb888_to_native16bit;
22
23 // Number of bytes for transmitting x/y coordinates
24 uint8_t num_window_bytes;
25
26 // Whether or not the x/y coords should be swapped on 90/270 rotation
27 bool swap_window_coords;
28
29 // Opcodes for normal display operation
30 struct {
31 uint8_t display_on;
32 uint8_t display_off;
33 uint8_t set_column_address;
34 uint8_t set_row_address;
35 uint8_t enable_writes;
36 } opcodes;
37};
38
39// Device definition
40typedef struct tft_panel_dc_reset_painter_device_t {
41 struct painter_driver_t base; // must be first, so it can be cast to/from the painter_device_t* type
42
43 union {
44#ifdef QUANTUM_PAINTER_SPI_ENABLE
45 // SPI-based configurables
46 struct qp_comms_spi_dc_reset_config_t spi_dc_reset_config;
47#endif // QUANTUM_PAINTER_SPI_ENABLE
48
49 // TODO: I2C/parallel etc.
50 };
51} tft_panel_dc_reset_painter_device_t;
52
53////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
54// Forward declarations for injecting into concrete driver vtables
55
56bool qp_tft_panel_power(painter_device_t device, bool power_on);
57bool qp_tft_panel_clear(painter_device_t device);
58bool qp_tft_panel_flush(painter_device_t device);
59bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
60bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
61bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette);
62bool 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);
63
64uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b);
65uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b);
66uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b);
67uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b);
diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py
index 5f65e677e5..85baa238a8 100644
--- a/lib/python/qmk/cli/__init__.py
+++ b/lib/python/qmk/cli/__init__.py
@@ -16,7 +16,8 @@ import_names = {
16 # A mapping of package name to importable name 16 # A mapping of package name to importable name
17 'pep8-naming': 'pep8ext_naming', 17 'pep8-naming': 'pep8ext_naming',
18 'pyusb': 'usb.core', 18 'pyusb': 'usb.core',
19 'qmk-dotty-dict': 'dotty_dict' 19 'qmk-dotty-dict': 'dotty_dict',
20 'pillow': 'PIL'
20} 21}
21 22
22safe_commands = [ 23safe_commands = [
@@ -67,6 +68,7 @@ subcommands = [
67 'qmk.cli.multibuild', 68 'qmk.cli.multibuild',
68 'qmk.cli.new.keyboard', 69 'qmk.cli.new.keyboard',
69 'qmk.cli.new.keymap', 70 'qmk.cli.new.keymap',
71 'qmk.cli.painter',
70 'qmk.cli.pyformat', 72 'qmk.cli.pyformat',
71 'qmk.cli.pytest', 73 'qmk.cli.pytest',
72 'qmk.cli.via2json', 74 'qmk.cli.via2json',
diff --git a/lib/python/qmk/cli/painter/__init__.py b/lib/python/qmk/cli/painter/__init__.py
new file mode 100644
index 0000000000..d1a225346c
--- /dev/null
+++ b/lib/python/qmk/cli/painter/__init__.py
@@ -0,0 +1,2 @@
1from . import convert_graphics
2from . import make_font
diff --git a/lib/python/qmk/cli/painter/convert_graphics.py b/lib/python/qmk/cli/painter/convert_graphics.py
new file mode 100644
index 0000000000..bbc30d26ff
--- /dev/null
+++ b/lib/python/qmk/cli/painter/convert_graphics.py
@@ -0,0 +1,86 @@
1"""This script tests QGF functionality.
2"""
3import re
4import datetime
5from io import BytesIO
6from qmk.path import normpath
7from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats
8from milc import cli
9from PIL import Image
10
11
12@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.')
13@cli.argument('-i', '--input', required=True, help='Specify input graphic file.')
14@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
15@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys())))
16@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.')
17@cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.')
18@cli.subcommand('Converts an input image to something QMK understands')
19def painter_convert_graphics(cli):
20 """Converts an image file to a format that Quantum Painter understands.
21
22 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`.
23 """
24 # Work out the input file
25 if cli.args.input != '-':
26 cli.args.input = normpath(cli.args.input)
27
28 # Error checking
29 if not cli.args.input.exists():
30 cli.log.error('Input image file does not exist!')
31 cli.print_usage()
32 return False
33
34 # Work out the output directory
35 if len(cli.args.output) == 0:
36 cli.args.output = cli.args.input.parent
37 cli.args.output = normpath(cli.args.output)
38
39 # Ensure we have a valid format
40 if cli.args.format not in valid_formats.keys():
41 cli.log.error('Output format %s is invalid. Allowed values: %s' % (cli.args.format, ', '.join(valid_formats.keys())))
42 cli.print_usage()
43 return False
44
45 # Work out the encoding parameters
46 format = valid_formats[cli.args.format]
47
48 # Load the input image
49 input_img = Image.open(cli.args.input)
50
51 # Convert the image to QGF using PIL
52 out_data = BytesIO()
53 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)
54 out_bytes = out_data.getvalue()
55
56 # Work out the text substitutions for rendering the output data
57 subs = {
58 'generated_type': 'image',
59 'var_prefix': 'gfx',
60 'generator_command': f'qmk painter-convert-graphics -i {cli.args.input.name} -f {cli.args.format}',
61 'year': datetime.date.today().strftime("%Y"),
62 'input_file': cli.args.input.name,
63 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
64 'byte_count': len(out_bytes),
65 'bytes_lines': render_bytes(out_bytes),
66 'format': cli.args.format,
67 }
68
69 # Render the license
70 subs.update({'license': render_license(subs)})
71
72 # Render and write the header file
73 header_text = render_header(subs)
74 header_file = cli.args.output / (cli.args.input.stem + ".qgf.h")
75 with open(header_file, 'w') as header:
76 print(f"Writing {header_file}...")
77 header.write(header_text)
78 header.close()
79
80 # Render and write the source file
81 source_text = render_source(subs)
82 source_file = cli.args.output / (cli.args.input.stem + ".qgf.c")
83 with open(source_file, 'w') as source:
84 print(f"Writing {source_file}...")
85 source.write(source_text)
86 source.close()
diff --git a/lib/python/qmk/cli/painter/make_font.py b/lib/python/qmk/cli/painter/make_font.py
new file mode 100644
index 0000000000..0762843fd3
--- /dev/null
+++ b/lib/python/qmk/cli/painter/make_font.py
@@ -0,0 +1,87 @@
1"""This script automates the conversion of font files into a format QMK firmware understands.
2"""
3
4import re
5import datetime
6from io import BytesIO
7from qmk.path import normpath
8from qmk.painter_qff import QFFFont
9from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats
10from milc import cli
11
12
13@cli.argument('-f', '--font', required=True, help='Specify input font file.')
14@cli.argument('-o', '--output', required=True, help='Specify output image path.')
15@cli.argument('-s', '--size', default=12, help='Specify font size. Default 12.')
16@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.')
17@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
18@cli.argument('-a', '--no-aa', arg_only=True, action='store_true', help='Disable anti-aliasing on fonts.')
19@cli.subcommand('Converts an input font to something QMK understands')
20def painter_make_font_image(cli):
21 # Create the font object
22 font = QFFFont(cli)
23 # Read from the input file
24 cli.args.font = normpath(cli.args.font)
25 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))
26 # Render out the data
27 font.save_to_image(normpath(cli.args.output))
28
29
30@cli.argument('-i', '--input', help='Specify input graphic file.')
31@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
32@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.')
33@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
34@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys())))
35@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.')
36@cli.subcommand('Converts an input font image to something QMK firmware understands')
37def painter_convert_font_image(cli):
38 # Work out the format
39 format = valid_formats[cli.args.format]
40
41 # Create the font object
42 font = QFFFont(cli.log)
43
44 # Read from the input file
45 cli.args.input = normpath(cli.args.input)
46 font.read_from_image(cli.args.input, include_ascii_glyphs=(not cli.args.no_ascii), unicode_glyphs=cli.args.unicode_glyphs)
47
48 # Work out the output directory
49 if len(cli.args.output) == 0:
50 cli.args.output = cli.args.input.parent
51 cli.args.output = normpath(cli.args.output)
52
53 # Render out the data
54 out_data = BytesIO()
55 font.save_to_qff(format, (False if cli.args.no_rle else True), out_data)
56
57 # Work out the text substitutions for rendering the output data
58 subs = {
59 'generated_type': 'font',
60 'var_prefix': 'font',
61 'generator_command': f'qmk painter-convert-font-image -i {cli.args.input.name} -f {cli.args.format}',
62 'year': datetime.date.today().strftime("%Y"),
63 'input_file': cli.args.input.name,
64 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
65 'byte_count': out_data.getbuffer().nbytes,
66 'bytes_lines': render_bytes(out_data.getbuffer().tobytes()),
67 'format': cli.args.format,
68 }
69
70 # Render the license
71 subs.update({'license': render_license(subs)})
72
73 # Render and write the header file
74 header_text = render_header(subs)
75 header_file = cli.args.output / (cli.args.input.stem + ".qff.h")
76 with open(header_file, 'w') as header:
77 print(f"Writing {header_file}...")
78 header.write(header_text)
79 header.close()
80
81 # Render and write the source file
82 source_text = render_source(subs)
83 source_file = cli.args.output / (cli.args.input.stem + ".qff.c")
84 with open(source_file, 'w') as source:
85 print(f"Writing {source_file}...")
86 source.write(source_text)
87 source.close()
diff --git a/lib/python/qmk/painter.py b/lib/python/qmk/painter.py
new file mode 100644
index 0000000000..d0cc1dddec
--- /dev/null
+++ b/lib/python/qmk/painter.py
@@ -0,0 +1,268 @@
1"""Functions that help us work with Quantum Painter's file formats.
2"""
3import math
4import re
5from string import Template
6from PIL import Image, ImageOps
7
8# The list of valid formats Quantum Painter supports
9valid_formats = {
10 'pal256': {
11 'image_format': 'IMAGE_FORMAT_PALETTE',
12 'bpp': 8,
13 'has_palette': True,
14 'num_colors': 256,
15 'image_format_byte': 0x07, # see qp_internal_formats.h
16 },
17 'pal16': {
18 'image_format': 'IMAGE_FORMAT_PALETTE',
19 'bpp': 4,
20 'has_palette': True,
21 'num_colors': 16,
22 'image_format_byte': 0x06, # see qp_internal_formats.h
23 },
24 'pal4': {
25 'image_format': 'IMAGE_FORMAT_PALETTE',
26 'bpp': 2,
27 'has_palette': True,
28 'num_colors': 4,
29 'image_format_byte': 0x05, # see qp_internal_formats.h
30 },
31 'pal2': {
32 'image_format': 'IMAGE_FORMAT_PALETTE',
33 'bpp': 1,
34 'has_palette': True,
35 'num_colors': 2,
36 'image_format_byte': 0x04, # see qp_internal_formats.h
37 },
38 'mono256': {
39 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
40 'bpp': 8,
41 'has_palette': False,
42 'num_colors': 256,
43 'image_format_byte': 0x03, # see qp_internal_formats.h
44 },
45 'mono16': {
46 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
47 'bpp': 4,
48 'has_palette': False,
49 'num_colors': 16,
50 'image_format_byte': 0x02, # see qp_internal_formats.h
51 },
52 'mono4': {
53 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
54 'bpp': 2,
55 'has_palette': False,
56 'num_colors': 4,
57 'image_format_byte': 0x01, # see qp_internal_formats.h
58 },
59 'mono2': {
60 'image_format': 'IMAGE_FORMAT_GRAYSCALE',
61 'bpp': 1,
62 'has_palette': False,
63 'num_colors': 2,
64 'image_format_byte': 0x00, # see qp_internal_formats.h
65 }
66}
67
68license_template = """\
69// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright
70// SPDX-License-Identifier: GPL-2.0-or-later
71
72// This file was auto-generated by `${generator_command}`
73"""
74
75
76def render_license(subs):
77 license_txt = Template(license_template)
78 return license_txt.substitute(subs)
79
80
81header_file_template = """\
82${license}
83#pragma once
84
85#include <qp.h>
86
87extern const uint32_t ${var_prefix}_${sane_name}_length;
88extern const uint8_t ${var_prefix}_${sane_name}[${byte_count}];
89"""
90
91
92def render_header(subs):
93 header_txt = Template(header_file_template)
94 return header_txt.substitute(subs)
95
96
97source_file_template = """\
98${license}
99#include <qp.h>
100
101const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count};
102
103// clang-format off
104const uint8_t ${var_prefix}_${sane_name}[${byte_count}] = {
105${bytes_lines}
106};
107// clang-format on
108"""
109
110
111def render_source(subs):
112 source_txt = Template(source_file_template)
113 return source_txt.substitute(subs)
114
115
116def render_bytes(bytes, newline_after=16):
117 lines = ''
118 for n in range(len(bytes)):
119 if n % newline_after == 0 and n > 0 and n != len(bytes):
120 lines = lines + "\n "
121 elif n == 0:
122 lines = lines + " "
123 lines = lines + " 0x{0:02X},".format(bytes[n])
124 return lines.rstrip()
125
126
127def clean_output(str):
128 str = re.sub(r'\r', '', str)
129 str = re.sub(r'[\n]{3,}', r'\n\n', str)
130 return str
131
132
133def rescale_byte(val, maxval):
134 """Rescales a byte value to the supplied range, i.e. [0,255] -> [0,maxval].
135 """
136 return int(round(val * maxval / 255.0))
137
138
139def convert_requested_format(im, format):
140 """Convert an image to the requested format.
141 """
142
143 # Work out the requested format
144 ncolors = format["num_colors"]
145 image_format = format["image_format"]
146
147 # Ensure we have a valid number of colors for the palette
148 if ncolors <= 0 or ncolors > 256 or (ncolors & (ncolors - 1) != 0):
149 raise ValueError("Number of colors must be 2, 4, 16, or 256.")
150
151 # Work out where we're getting the bytes from
152 if image_format == 'IMAGE_FORMAT_GRAYSCALE':
153 # If mono, convert input to grayscale, then to RGB, then grab the raw bytes corresponding to the intensity of the red channel
154 im = ImageOps.grayscale(im)
155 im = im.convert("RGB")
156 elif image_format == 'IMAGE_FORMAT_PALETTE':
157 # If color, convert input to RGB, palettize based on the supplied number of colors, then get the raw palette bytes
158 im = im.convert("RGB")
159 im = im.convert("P", palette=Image.ADAPTIVE, colors=ncolors)
160
161 return im
162
163
164def convert_image_bytes(im, format):
165 """Convert the supplied image to the equivalent bytes required by the QMK firmware.
166 """
167
168 # Work out the requested format
169 ncolors = format["num_colors"]
170 image_format = format["image_format"]
171 shifter = int(math.log2(ncolors))
172 pixels_per_byte = int(8 / math.log2(ncolors))
173 (width, height) = im.size
174 expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte
175
176 if image_format == 'IMAGE_FORMAT_GRAYSCALE':
177 # Take the red channel
178 image_bytes = im.tobytes("raw", "R")
179 image_bytes_len = len(image_bytes)
180
181 # No palette
182 palette = None
183
184 bytearray = []
185 for x in range(expected_byte_count):
186 byte = 0
187 for n in range(pixels_per_byte):
188 byte_offset = x * pixels_per_byte + n
189 if byte_offset < image_bytes_len:
190 # If mono, each input byte is a grayscale [0,255] pixel -- rescale to the range we want then pack together
191 byte = byte | (rescale_byte(image_bytes[byte_offset], ncolors - 1) << int(n * shifter))
192 bytearray.append(byte)
193
194 elif image_format == 'IMAGE_FORMAT_PALETTE':
195 # Convert each pixel to the palette bytes
196 image_bytes = im.tobytes("raw", "P")
197 image_bytes_len = len(image_bytes)
198
199 # Export the palette
200 palette = []
201 pal = im.getpalette()
202 for n in range(0, ncolors * 3, 3):
203 palette.append((pal[n + 0], pal[n + 1], pal[n + 2]))
204
205 bytearray = []
206 for x in range(expected_byte_count):
207 byte = 0
208 for n in range(pixels_per_byte):
209 byte_offset = x * pixels_per_byte + n
210 if byte_offset < image_bytes_len:
211 # If color, each input byte is the index into the color palette -- pack them together
212 byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter))
213 bytearray.append(byte)
214
215 if len(bytearray) != expected_byte_count:
216 raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}")
217
218 return (palette, bytearray)
219
220
221def compress_bytes_qmk_rle(bytearray):
222 debug_dump = False
223 output = []
224 temp = []
225 repeat = False
226
227 def append_byte(c):
228 if debug_dump:
229 print('Appending byte:', '0x{0:02X}'.format(int(c)), '=', c)
230 output.append(c)
231
232 def append_range(r):
233 append_byte(127 + len(r))
234 if debug_dump:
235 print('Appending {0} byte(s):'.format(len(r)), '[', ', '.join(['{0:02X}'.format(e) for e in r]), ']')
236 output.extend(r)
237
238 for n in range(0, len(bytearray) + 1):
239 end = True if n == len(bytearray) else False
240 if not end:
241 c = bytearray[n]
242 temp.append(c)
243 if len(temp) <= 1:
244 continue
245
246 if debug_dump:
247 print('Temp buffer state {0:3d} bytes:'.format(len(temp)), '[', ', '.join(['{0:02X}'.format(e) for e in temp]), ']')
248
249 if repeat:
250 if temp[-1] != temp[-2]:
251 repeat = False
252 if not repeat or len(temp) == 128 or end:
253 append_byte(len(temp) if end else len(temp) - 1)
254 append_byte(temp[0])
255 temp = [temp[-1]]
256 repeat = False
257 else:
258 if len(temp) >= 2 and temp[-1] == temp[-2]:
259 repeat = True
260 if len(temp) > 2:
261 append_range(temp[0:(len(temp) - 2)])
262 temp = [temp[-1], temp[-1]]
263 continue
264 if len(temp) == 128 or end:
265 append_range(temp)
266 temp = []
267 repeat = False
268 return output
diff --git a/lib/python/qmk/painter_qff.py b/lib/python/qmk/painter_qff.py
new file mode 100644
index 0000000000..746bb166e5
--- /dev/null
+++ b/lib/python/qmk/painter_qff.py
@@ -0,0 +1,401 @@
1# Copyright 2021 Nick Brassel (@tzarc)
2# SPDX-License-Identifier: GPL-2.0-or-later
3
4# Quantum Font File "QFF" Font File Format.
5# See https://docs.qmk.fm/#/quantum_painter_qff for more information.
6
7from pathlib import Path
8from typing import Dict, Any
9from colorsys import rgb_to_hsv
10from PIL import Image, ImageDraw, ImageFont, ImageChops
11from PIL._binary import o8, o16le as o16, o32le as o32
12from qmk.painter_qgf import QGFBlockHeader, QGFFramePaletteDescriptorV1
13from milc.attrdict import AttrDict
14import qmk.painter
15
16
17def o24(i):
18 return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
19
20
21########################################################################################################################
22
23
24class QFFGlyphInfo(AttrDict):
25 def __init__(self, *args, **kwargs):
26 super().__init__()
27
28 for n, value in enumerate(args):
29 self[f'arg:{n}'] = value
30
31 for key, value in kwargs.items():
32 self[key] = value
33
34 def write(self, fp, include_code_point):
35 if include_code_point is True:
36 fp.write(o24(ord(self.code_point)))
37
38 value = ((self.data_offset << 6) & 0xFFFFC0) | (self.w & 0x3F)
39 fp.write(o24(value))
40
41
42########################################################################################################################
43
44
45class QFFFontDescriptor:
46 type_id = 0x00
47 length = 20
48 magic = 0x464651
49
50 def __init__(self):
51 self.header = QGFBlockHeader()
52 self.header.type_id = QFFFontDescriptor.type_id
53 self.header.length = QFFFontDescriptor.length
54 self.version = 1
55 self.total_file_size = 0
56 self.line_height = 0
57 self.has_ascii_table = False
58 self.unicode_glyph_count = 0
59 self.format = 0xFF
60 self.flags = 0
61 self.compression = 0xFF
62 self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
63
64 def write(self, fp):
65 self.header.write(fp)
66 fp.write(
67 b'' # start off with empty bytes...
68 + o24(QFFFontDescriptor.magic) # magic
69 + o8(self.version) # version
70 + o32(self.total_file_size) # file size
71 + o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size
72 + o8(self.line_height) # line height
73 + o8(1 if self.has_ascii_table is True else 0) # whether or not we have an ascii table present
74 + o16(self.unicode_glyph_count & 0xFFFF) # number of unicode glyphs present
75 + o8(self.format) # format
76 + o8(self.flags) # flags
77 + o8(self.compression) # compression
78 + o8(self.transparency_index) # transparency index
79 )
80
81 @property
82 def is_transparent(self):
83 return (self.flags & 0x01) == 0x01
84
85 @is_transparent.setter
86 def is_transparent(self, val):
87 if val:
88 self.flags |= 0x01
89 else:
90 self.flags &= ~0x01
91
92
93########################################################################################################################
94
95
96class QFFAsciiGlyphTableV1:
97 type_id = 0x01
98 length = 95 * 3 # We have 95 glyphs: [0x20...0x7E]
99
100 def __init__(self):
101 self.header = QGFBlockHeader()
102 self.header.type_id = QFFAsciiGlyphTableV1.type_id
103 self.header.length = QFFAsciiGlyphTableV1.length
104
105 # Each glyph is key=code_point, value=QFFGlyphInfo
106 self.glyphs = {}
107
108 def add_glyph(self, glyph: QFFGlyphInfo):
109 self.glyphs[ord(glyph.code_point)] = glyph
110
111 def write(self, fp):
112 self.header.write(fp)
113
114 for n in range(0x20, 0x7F):
115 self.glyphs[n].write(fp, False)
116
117
118########################################################################################################################
119
120
121class QFFUnicodeGlyphTableV1:
122 type_id = 0x02
123
124 def __init__(self):
125 self.header = QGFBlockHeader()
126 self.header.type_id = QFFUnicodeGlyphTableV1.type_id
127 self.header.length = 0
128
129 # Each glyph is key=code_point, value=QFFGlyphInfo
130 self.glyphs = {}
131
132 def add_glyph(self, glyph: QFFGlyphInfo):
133 self.glyphs[ord(glyph.code_point)] = glyph
134
135 def write(self, fp):
136 self.header.length = len(self.glyphs.keys()) * 6
137 self.header.write(fp)
138
139 for n in sorted(self.glyphs.keys()):
140 self.glyphs[n].write(fp, True)
141
142
143########################################################################################################################
144
145
146class QFFFontDataDescriptorV1:
147 type_id = 0x04
148
149 def __init__(self):
150 self.header = QGFBlockHeader()
151 self.header.type_id = QFFFontDataDescriptorV1.type_id
152 self.data = []
153
154 def write(self, fp):
155 self.header.length = len(self.data)
156 self.header.write(fp)
157 fp.write(bytes(self.data))
158
159
160########################################################################################################################
161
162
163def _generate_font_glyphs_list(use_ascii, unicode_glyphs):
164 # The set of glyphs that we want to generate images for
165 glyphs = {}
166
167 # Add ascii charset if requested
168 if use_ascii is True:
169 for c in range(0x20, 0x7F): # does not include 0x7F!
170 glyphs[chr(c)] = True
171
172 # Append any extra unicode glyphs
173 unicode_glyphs = list(unicode_glyphs)
174 for c in unicode_glyphs:
175 glyphs[c] = True
176
177 return sorted(glyphs.keys())
178
179
180class QFFFont:
181 def __init__(self, logger):
182 self.logger = logger
183 self.image = None
184 self.glyph_data = {}
185 self.glyph_height = 0
186 return
187
188 def _extract_glyphs(self, format):
189 total_data_size = 0
190 total_rle_data_size = 0
191
192 converted_img = qmk.painter.convert_requested_format(self.image, format)
193 (self.palette, _) = qmk.painter.convert_image_bytes(converted_img, format)
194
195 # Work out how many bytes used for RLE vs. non-RLE
196 for _, glyph_entry in self.glyph_data.items():
197 glyph_img = converted_img.crop((glyph_entry.x, 1, glyph_entry.x + glyph_entry.w, 1 + self.glyph_height))
198 (_, this_glyph_image_bytes) = qmk.painter.convert_image_bytes(glyph_img, format)
199 this_glyph_rle_bytes = qmk.painter.compress_bytes_qmk_rle(this_glyph_image_bytes)
200 total_data_size += len(this_glyph_image_bytes)
201 total_rle_data_size += len(this_glyph_rle_bytes)
202 glyph_entry['image_uncompressed_bytes'] = this_glyph_image_bytes
203 glyph_entry['image_compressed_bytes'] = this_glyph_rle_bytes
204
205 return (total_data_size, total_rle_data_size)
206
207 def _parse_image(self, img, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''):
208 # Clear out any existing font metadata
209 self.image = None
210 # Each glyph is key=code_point, value={ x: ?, w: ? }
211 self.glyph_data = {}
212 self.glyph_height = 0
213
214 # Work out the list of glyphs required
215 glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs)
216
217 # Work out the geometry
218 (width, height) = img.size
219
220 # Work out the glyph offsets/widths
221 glyph_pixel_offsets = []
222 glyph_pixel_widths = []
223 pixels = img.load()
224
225 # Run through the markers and work out where each glyph starts/stops
226 glyph_split_color = pixels[0, 0] # top left pixel is the marker color we're going to use to split each glyph
227 glyph_pixel_offsets.append(0)
228 last_offset = 0
229 for x in range(1, width):
230 if pixels[x, 0] == glyph_split_color:
231 glyph_pixel_offsets.append(x)
232 glyph_pixel_widths.append(x - last_offset)
233 last_offset = x
234 glyph_pixel_widths.append(width - last_offset)
235
236 # Make sure the number of glyphs we're attempting to generate matches the input image
237 if len(glyph_pixel_offsets) != len(glyphs):
238 self.logger.error('The number of glyphs to generate doesn\'t match the number of detected glyphs in the input image.')
239 return
240
241 # Set up the required metadata for each glyph
242 for n in range(0, len(glyph_pixel_offsets)):
243 self.glyph_data[glyphs[n]] = QFFGlyphInfo(code_point=glyphs[n], x=glyph_pixel_offsets[n], w=glyph_pixel_widths[n])
244
245 # Parsing was successful, keep the image in this instance
246 self.image = img
247 self.glyph_height = height - 1 # subtract the line with the markers
248
249 def generate_image(self, ttf_file: Path, font_size: int, include_ascii_glyphs: bool = True, unicode_glyphs: str = '', include_before_left: bool = False, use_aa: bool = True):
250 # Load the font
251 font = ImageFont.truetype(str(ttf_file), int(font_size))
252 # Work out the max font size
253 max_font_size = font.font.ascent + abs(font.font.descent)
254 # Work out the list of glyphs required
255 glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs)
256
257 baseline_offset = 9999999
258 total_glyph_width = 0
259 max_glyph_height = -1
260
261 # Measure each glyph to determine the overall baseline offset required
262 for glyph in glyphs:
263 (ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls')
264 glyph_width = (ls_r - ls_l) if include_before_left else (ls_r)
265 glyph_height = font.getbbox(glyph, anchor='la')[3]
266 if max_glyph_height < glyph_height:
267 max_glyph_height = glyph_height
268 total_glyph_width += glyph_width
269 if baseline_offset > ls_t:
270 baseline_offset = ls_t
271
272 # Create the output image
273 img = Image.new("RGB", (total_glyph_width + 1, max_font_size * 2 + 1), (0, 0, 0, 255))
274 cur_x_pos = 0
275
276 # Loop through each glyph...
277 for glyph in glyphs:
278 # Work out this glyph's bounding box
279 (ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls')
280 glyph_width = (ls_r - ls_l) if include_before_left else (ls_r)
281 glyph_height = ls_b - ls_t
282 x_offset = -ls_l
283 y_offset = ls_t - baseline_offset
284
285 # Draw each glyph to its own image so we don't get anti-aliasing applied to the final image when straddling edges
286 glyph_img = Image.new("RGB", (glyph_width, max_font_size), (0, 0, 0, 255))
287 glyph_draw = ImageDraw.Draw(glyph_img)
288 if not use_aa:
289 glyph_draw.fontmode = "1"
290 glyph_draw.text((x_offset, y_offset), glyph, font=font, anchor='lt')
291
292 # Place the glyph-specific image in the correct location overall
293 img.paste(glyph_img, (cur_x_pos, 1))
294
295 # Set up the marker for start of each glyph
296 pixels = img.load()
297 pixels[cur_x_pos, 0] = (255, 0, 255)
298
299 # Increment for the next glyph's position
300 cur_x_pos += glyph_width
301
302 # Add the ending marker so that the difference/crop works
303 pixels = img.load()
304 pixels[cur_x_pos, 0] = (255, 0, 255)
305
306 # Determine the usable font area
307 dummy_img = Image.new("RGB", (total_glyph_width + 1, max_font_size + 1), (0, 0, 0, 255))
308 bbox = ImageChops.difference(img, dummy_img).getbbox()
309 bbox = (bbox[0], bbox[1], bbox[2] - 1, bbox[3]) # remove the unused end-marker
310
311 # Crop and re-parse the resulting image to ensure we're generating the correct format
312 self._parse_image(img.crop(bbox), include_ascii_glyphs, unicode_glyphs)
313
314 def save_to_image(self, img_file: Path):
315 # Drop out if there's no image loaded
316 if self.image is None:
317 self.logger.error('No image is loaded.')
318 return
319
320 # Save the image to the supplied file
321 self.image.save(str(img_file))
322
323 def read_from_image(self, img_file: Path, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''):
324 # Load and parse the supplied image file
325 self._parse_image(Image.open(str(img_file)), include_ascii_glyphs, unicode_glyphs)
326 return
327
328 def save_to_qff(self, format: Dict[str, Any], use_rle: bool, fp):
329 # Drop out if there's no image loaded
330 if self.image is None:
331 self.logger.error('No image is loaded.')
332 return
333
334 # Work out if we want to use RLE at all, skipping it if it's not any smaller (it's applied per-glyph)
335 (total_data_size, total_rle_data_size) = self._extract_glyphs(format)
336 if use_rle:
337 use_rle = (total_rle_data_size < total_data_size)
338
339 # For each glyph, work out which image data we want to use and append it to the image buffer, recording the byte-wise offset
340 img_buffer = bytes()
341 for _, glyph_entry in self.glyph_data.items():
342 glyph_entry['data_offset'] = len(img_buffer)
343 glyph_img_bytes = glyph_entry.image_compressed_bytes if use_rle else glyph_entry.image_uncompressed_bytes
344 img_buffer += bytes(glyph_img_bytes)
345
346 font_descriptor = QFFFontDescriptor()
347 ascii_table = QFFAsciiGlyphTableV1()
348 unicode_table = QFFUnicodeGlyphTableV1()
349 data_descriptor = QFFFontDataDescriptorV1()
350 data_descriptor.data = img_buffer
351
352 # Check if we have all the ASCII glyphs present
353 include_ascii_glyphs = all([chr(n) in self.glyph_data for n in range(0x20, 0x7F)])
354
355 # Helper for populating the blocks
356 for code_point, glyph_entry in self.glyph_data.items():
357 if ord(code_point) >= 0x20 and ord(code_point) <= 0x7E and include_ascii_glyphs:
358 ascii_table.add_glyph(glyph_entry)
359 else:
360 unicode_table.add_glyph(glyph_entry)
361
362 # Configure the font descriptor
363 font_descriptor.line_height = self.glyph_height
364 font_descriptor.has_ascii_table = include_ascii_glyphs
365 font_descriptor.unicode_glyph_count = len(unicode_table.glyphs.keys())
366 font_descriptor.is_transparent = False
367 font_descriptor.format = format['image_format_byte']
368 font_descriptor.compression = 0x01 if use_rle else 0x00
369
370 # Write a dummy font descriptor -- we'll have to come back and write it properly once we've rendered out everything else
371 font_descriptor_location = fp.tell()
372 font_descriptor.write(fp)
373
374 # Write out the ASCII table if required
375 if font_descriptor.has_ascii_table:
376 ascii_table.write(fp)
377
378 # Write out the unicode table if required
379 if font_descriptor.unicode_glyph_count > 0:
380 unicode_table.write(fp)
381
382 # Write out the palette if required
383 if format['has_palette']:
384 palette_descriptor = QGFFramePaletteDescriptorV1()
385
386 # Helper to convert from RGB888 to the QMK "dialect" of HSV888
387 def rgb888_to_qmk_hsv888(e):
388 hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
389 return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
390
391 # Convert all palette entries to HSV888 and write to the output
392 palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, self.palette))
393 palette_descriptor.write(fp)
394
395 # Write out the image data
396 data_descriptor.write(fp)
397
398 # Now fix up the overall font descriptor, then write it in the correct location
399 font_descriptor.total_file_size = fp.tell()
400 fp.seek(font_descriptor_location, 0)
401 font_descriptor.write(fp)
diff --git a/lib/python/qmk/painter_qgf.py b/lib/python/qmk/painter_qgf.py
new file mode 100644
index 0000000000..71ce1f5a02
--- /dev/null
+++ b/lib/python/qmk/painter_qgf.py
@@ -0,0 +1,408 @@
1# Copyright 2021 Nick Brassel (@tzarc)
2# SPDX-License-Identifier: GPL-2.0-or-later
3
4# Quantum Graphics File "QGF" Image File Format.
5# See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
6
7from colorsys import rgb_to_hsv
8from types import FunctionType
9from PIL import Image, ImageFile, ImageChops
10from PIL._binary import o8, o16le as o16, o32le as o32
11import qmk.painter
12
13
14def o24(i):
15 return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
16
17
18########################################################################################################################
19
20
21class QGFBlockHeader:
22 block_size = 5
23
24 def write(self, fp):
25 fp.write(b'' # start off with empty bytes...
26 + o8(self.type_id) # block type id
27 + o8((~self.type_id) & 0xFF) # negated block type id
28 + o24(self.length) # blob length
29 )
30
31
32########################################################################################################################
33
34
35class QGFGraphicsDescriptor:
36 type_id = 0x00
37 length = 18
38 magic = 0x464751
39
40 def __init__(self):
41 self.header = QGFBlockHeader()
42 self.header.type_id = QGFGraphicsDescriptor.type_id
43 self.header.length = QGFGraphicsDescriptor.length
44 self.version = 1
45 self.total_file_size = 0
46 self.image_width = 0
47 self.image_height = 0
48 self.frame_count = 0
49
50 def write(self, fp):
51 self.header.write(fp)
52 fp.write(
53 b'' # start off with empty bytes...
54 + o24(QGFGraphicsDescriptor.magic) # magic
55 + o8(self.version) # version
56 + o32(self.total_file_size) # file size
57 + o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size
58 + o16(self.image_width) # width
59 + o16(self.image_height) # height
60 + o16(self.frame_count) # frame count
61 )
62
63
64########################################################################################################################
65
66
67class QGFFrameOffsetDescriptorV1:
68 type_id = 0x01
69
70 def __init__(self, frame_count):
71 self.header = QGFBlockHeader()
72 self.header.type_id = QGFFrameOffsetDescriptorV1.type_id
73 self.frame_offsets = [0xFFFFFFFF] * frame_count
74 self.frame_count = frame_count
75
76 def write(self, fp):
77 self.header.length = len(self.frame_offsets) * 4
78 self.header.write(fp)
79 for offset in self.frame_offsets:
80 fp.write(b'' # start off with empty bytes...
81 + o32(offset) # offset
82 )
83
84
85########################################################################################################################
86
87
88class QGFFrameDescriptorV1:
89 type_id = 0x02
90 length = 6
91
92 def __init__(self):
93 self.header = QGFBlockHeader()
94 self.header.type_id = QGFFrameDescriptorV1.type_id
95 self.header.length = QGFFrameDescriptorV1.length
96 self.format = 0xFF
97 self.flags = 0
98 self.compression = 0xFF
99 self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
100 self.delay = 1000 # Placeholder until it gets read from the animation
101
102 def write(self, fp):
103 self.header.write(fp)
104 fp.write(b'' # start off with empty bytes...
105 + o8(self.format) # format
106 + o8(self.flags) # flags
107 + o8(self.compression) # compression
108 + o8(self.transparency_index) # transparency index
109 + o16(self.delay) # delay
110 )
111
112 @property
113 def is_transparent(self):
114 return (self.flags & 0x01) == 0x01
115
116 @is_transparent.setter
117 def is_transparent(self, val):
118 if val:
119 self.flags |= 0x01
120 else:
121 self.flags &= ~0x01
122
123 @property
124 def is_delta(self):
125 return (self.flags & 0x02) == 0x02
126
127 @is_delta.setter
128 def is_delta(self, val):
129 if val:
130 self.flags |= 0x02
131 else:
132 self.flags &= ~0x02
133
134
135########################################################################################################################
136
137
138class QGFFramePaletteDescriptorV1:
139 type_id = 0x03
140
141 def __init__(self):
142 self.header = QGFBlockHeader()
143 self.header.type_id = QGFFramePaletteDescriptorV1.type_id
144 self.header.length = 0
145 self.palette_entries = [(0xFF, 0xFF, 0xFF)] * 4
146
147 def write(self, fp):
148 self.header.length = len(self.palette_entries) * 3
149 self.header.write(fp)
150 for entry in self.palette_entries:
151 fp.write(b'' # start off with empty bytes...
152 + o8(entry[0]) # h
153 + o8(entry[1]) # s
154 + o8(entry[2]) # v
155 )
156
157
158########################################################################################################################
159
160
161class QGFFrameDeltaDescriptorV1:
162 type_id = 0x04
163 length = 8
164
165 def __init__(self):
166 self.header = QGFBlockHeader()
167 self.header.type_id = QGFFrameDeltaDescriptorV1.type_id
168 self.header.length = QGFFrameDeltaDescriptorV1.length
169 self.left = 0
170 self.top = 0
171 self.right = 0
172 self.bottom = 0
173
174 def write(self, fp):
175 self.header.write(fp)
176 fp.write(b'' # start off with empty bytes...
177 + o16(self.left) # left
178 + o16(self.top) # top
179 + o16(self.right) # right
180 + o16(self.bottom) # bottom
181 )
182
183
184########################################################################################################################
185
186
187class QGFFrameDataDescriptorV1:
188 type_id = 0x05
189
190 def __init__(self):
191 self.header = QGFBlockHeader()
192 self.header.type_id = QGFFrameDataDescriptorV1.type_id
193 self.data = []
194
195 def write(self, fp):
196 self.header.length = len(self.data)
197 self.header.write(fp)
198 fp.write(bytes(self.data))
199
200
201########################################################################################################################
202
203
204class QGFImageFile(ImageFile.ImageFile):
205
206 format = "QGF"
207 format_description = "Quantum Graphics File Format"
208
209 def _open(self):
210 raise NotImplementedError("Reading QGF files is not supported")
211
212
213########################################################################################################################
214
215
216def _accept(prefix):
217 """Helper method used by PIL to work out if it can parse an input file.
218
219 Currently unimplemented.
220 """
221 return False
222
223
224def _save(im, fp, filename):
225 """Helper method used by PIL to write to an output file.
226 """
227 # Work out from the parameters if we need to do anything special
228 encoderinfo = im.encoderinfo.copy()
229 append_images = list(encoderinfo.get("append_images", []))
230 verbose = encoderinfo.get("verbose", False)
231 use_deltas = encoderinfo.get("use_deltas", True)
232 use_rle = encoderinfo.get("use_rle", True)
233
234 # Helper for inline verbose prints
235 def vprint(s):
236 if verbose:
237 print(s)
238
239 # Helper to iterate through all frames in the input image
240 def _for_all_frames(x: FunctionType):
241 frame_num = 0
242 last_frame = None
243 for frame in [im] + append_images:
244 # Get number of of frames in this image
245 nfr = getattr(frame, "n_frames", 1)
246 for idx in range(nfr):
247 frame.seek(idx)
248 frame.load()
249 copy = frame.copy().convert("RGB")
250 x(frame_num, copy, last_frame)
251 last_frame = copy
252 frame_num += 1
253
254 # Collect all the frame sizes
255 frame_sizes = []
256 _for_all_frames(lambda idx, frame, last_frame: frame_sizes.append(frame.size))
257
258 # Make sure all frames are the same size
259 if len(list(set(frame_sizes))) != 1:
260 raise ValueError("Mismatching sizes on frames")
261
262 # Write out the initial graphics descriptor (and write a dummy value), so that we can come back and fill in the
263 # correct values once we've written all the frames to the output
264 graphics_descriptor_location = fp.tell()
265 graphics_descriptor = QGFGraphicsDescriptor()
266 graphics_descriptor.frame_count = len(frame_sizes)
267 graphics_descriptor.image_width = frame_sizes[0][0]
268 graphics_descriptor.image_height = frame_sizes[0][1]
269 vprint(f'{"Graphics descriptor block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
270 graphics_descriptor.write(fp)
271
272 # Work out the frame offset descriptor location (and write a dummy value), so that we can come back and fill in the
273 # correct offsets once we've written all the frames to the output
274 frame_offset_location = fp.tell()
275 frame_offsets = QGFFrameOffsetDescriptorV1(graphics_descriptor.frame_count)
276 vprint(f'{"Frame offsets block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
277 frame_offsets.write(fp)
278
279 # Helper function to save each frame to the output file
280 def _write_frame(idx, frame, last_frame):
281 # If we replace the frame we're going to output with a delta, we can override it here
282 this_frame = frame
283 location = (0, 0)
284 size = frame.size
285
286 # Work out the format we're going to use
287 format = encoderinfo["qmk_format"]
288
289 # Convert the original frame so we can do comparisons
290 converted = qmk.painter.convert_requested_format(this_frame, format)
291 graphic_data = qmk.painter.convert_image_bytes(converted, format)
292
293 # Convert the raw data to RLE-encoded if requested
294 raw_data = graphic_data[1]
295 if use_rle:
296 rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1])
297 use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data)
298 image_data = raw_data if use_raw_this_frame else rle_data
299
300 # Work out if a delta frame is smaller than injecting it directly
301 use_delta_this_frame = False
302 if use_deltas and last_frame is not None:
303 # If we want to use deltas, then find the difference
304 diff = ImageChops.difference(frame, last_frame)
305
306 # Get the bounding box of those differences
307 bbox = diff.getbbox()
308
309 # If we have a valid bounding box...
310 if bbox:
311 # ...create the delta frame by cropping the original.
312 delta_frame = frame.crop(bbox)
313 delta_location = (bbox[0], bbox[1])
314 delta_size = (bbox[2] - bbox[0], bbox[3] - bbox[1])
315
316 # Convert the delta frame to the requested format
317 delta_converted = qmk.painter.convert_requested_format(delta_frame, format)
318 delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format)
319
320 # Work out how large the delta frame is going to be with compression etc.
321 delta_raw_data = delta_graphic_data[1]
322 if use_rle:
323 delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1])
324 delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data)
325 delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data
326
327 # If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead
328 # This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash
329 # sizing constraints.
330 if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data):
331 # Copy across all the delta equivalents so that the rest of the processing acts on those
332 this_frame = delta_frame
333 location = delta_location
334 size = delta_size
335 converted = delta_converted
336 graphic_data = delta_graphic_data
337 raw_data = delta_raw_data
338 rle_data = delta_rle_data
339 use_raw_this_frame = delta_use_raw_this_frame
340 image_data = delta_image_data
341 use_delta_this_frame = True
342
343 # Write out the frame descriptor
344 frame_offsets.frame_offsets[idx] = fp.tell()
345 vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h')
346 frame_descriptor = QGFFrameDescriptorV1()
347 frame_descriptor.is_delta = use_delta_this_frame
348 frame_descriptor.is_transparent = False
349 frame_descriptor.format = format['image_format_byte']
350 frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01 # See qp.h, painter_compression_t
351 frame_descriptor.delay = frame.info['duration'] if 'duration' in frame.info else 1000 # If we're not an animation, just pretend we're delaying for 1000ms
352 frame_descriptor.write(fp)
353
354 # Write out the palette if required
355 if format['has_palette']:
356 palette = graphic_data[0]
357 palette_descriptor = QGFFramePaletteDescriptorV1()
358
359 # Helper to convert from RGB888 to the QMK "dialect" of HSV888
360 def rgb888_to_qmk_hsv888(e):
361 hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
362 return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
363
364 # Convert all palette entries to HSV888 and write to the output
365 palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette))
366 vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h')
367 palette_descriptor.write(fp)
368
369 # Write out the delta info if required
370 if use_delta_this_frame:
371 # Set up the rendering location of where the delta frame should be situated
372 delta_descriptor = QGFFrameDeltaDescriptorV1()
373 delta_descriptor.left = location[0]
374 delta_descriptor.top = location[1]
375 delta_descriptor.right = location[0] + size[0]
376 delta_descriptor.bottom = location[1] + size[1]
377
378 # Write the delta frame to the output
379 vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h')
380 delta_descriptor.write(fp)
381
382 # Write out the data for this frame to the output
383 data_descriptor = QGFFrameDataDescriptorV1()
384 data_descriptor.data = image_data
385 vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h')
386 data_descriptor.write(fp)
387
388 # Iterate over each if the input frames, writing it to the output in the process
389 _for_all_frames(_write_frame)
390
391 # Go back and update the graphics descriptor now that we can determine the final file size
392 graphics_descriptor.total_file_size = fp.tell()
393 fp.seek(graphics_descriptor_location, 0)
394 graphics_descriptor.write(fp)
395
396 # Go back and update the frame offsets now that they're written to the file
397 fp.seek(frame_offset_location, 0)
398 frame_offsets.write(fp)
399
400
401########################################################################################################################
402
403# Register with PIL so that it knows about the QGF format
404Image.register_open(QGFImageFile.format, QGFImageFile, _accept)
405Image.register_save(QGFImageFile.format, _save)
406Image.register_save_all(QGFImageFile.format, _save)
407Image.register_extension(QGFImageFile.format, f".{QGFImageFile.format.lower()}")
408Image.register_mime(QGFImageFile.format, f"image/{QGFImageFile.format.lower()}")
diff --git a/quantum/main.c b/quantum/main.c
index faba668056..2d5911b708 100644
--- a/quantum/main.c
+++ b/quantum/main.c
@@ -43,10 +43,6 @@ void protocol_task(void) {
43 protocol_post_task(); 43 protocol_post_task();
44} 44}
45 45
46#ifdef DEFERRED_EXEC_ENABLE
47void deferred_exec_task(void);
48#endif // DEFERRED_EXEC_ENABLE
49
50/** \brief Main 46/** \brief Main
51 * 47 *
52 * FIXME: Needs doc 48 * FIXME: Needs doc
@@ -63,8 +59,15 @@ int main(void) {
63 while (true) { 59 while (true) {
64 protocol_task(); 60 protocol_task();
65 61
62#ifdef QUANTUM_PAINTER_ENABLE
63 // Run Quantum Painter animations
64 void qp_internal_animation_tick(void);
65 qp_internal_animation_tick();
66#endif
67
66#ifdef DEFERRED_EXEC_ENABLE 68#ifdef DEFERRED_EXEC_ENABLE
67 // Run deferred executions 69 // Run deferred executions
70 void deferred_exec_task(void);
68 deferred_exec_task(); 71 deferred_exec_task();
69#endif // DEFERRED_EXEC_ENABLE 72#endif // DEFERRED_EXEC_ENABLE
70 73
diff --git a/quantum/painter/qff.c b/quantum/painter/qff.c
new file mode 100644
index 0000000000..cd6af788f9
--- /dev/null
+++ b/quantum/painter/qff.c
@@ -0,0 +1,137 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4// Quantum Font File "QFF" File Format.
5// See https://docs.qmk.fm/#/quantum_painter_qff for more information.
6
7#include "qff.h"
8#include "qp_draw.h"
9
10////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
11// QFF API
12
13bool 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) {
14 // Seek to the start
15 qp_stream_setpos(stream, 0);
16
17 // Read and validate the font descriptor
18 qff_font_descriptor_v1_t font_descriptor;
19 if (qp_stream_read(&font_descriptor, sizeof(qff_font_descriptor_v1_t), 1, stream) != 1) {
20 qp_dprintf("Failed to read font_descriptor, expected length was not %d\n", (int)sizeof(qff_font_descriptor_v1_t));
21 return false;
22 }
23
24 // Make sure this block is valid
25 if (!qgf_validate_block_header(&font_descriptor.header, QFF_FONT_DESCRIPTOR_TYPEID, (sizeof(qff_font_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) {
26 return false;
27 }
28
29 // Make sure the magic and version are correct
30 if (font_descriptor.magic != QFF_MAGIC || font_descriptor.qff_version != 0x01) {
31 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);
32 return false;
33 }
34
35 // Make sure the file length is valid
36 if (font_descriptor.neg_total_file_size != ~font_descriptor.total_file_size) {
37 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);
38 return false;
39 }
40
41 // Copy out the required info
42 if (line_height) {
43 *line_height = font_descriptor.line_height;
44 }
45 if (has_ascii_table) {
46 *has_ascii_table = font_descriptor.has_ascii_table;
47 }
48 if (num_unicode_glyphs) {
49 *num_unicode_glyphs = font_descriptor.num_unicode_glyphs;
50 }
51 if (bpp || has_palette) {
52 if (!qgf_parse_format(font_descriptor.format, bpp, has_palette)) {
53 return false;
54 }
55 }
56 if (compression_scheme) {
57 *compression_scheme = font_descriptor.compression_scheme;
58 }
59 if (total_bytes) {
60 *total_bytes = font_descriptor.total_file_size;
61 }
62
63 return true;
64}
65
66static bool qff_validate_ascii_descriptor(qp_stream_t *stream) {
67 // Read the raw descriptor
68 qff_ascii_glyph_table_v1_t ascii_descriptor;
69 if (qp_stream_read(&ascii_descriptor, sizeof(qff_ascii_glyph_table_v1_t), 1, stream) != 1) {
70 qp_dprintf("Failed to read ascii_descriptor, expected length was not %d\n", (int)sizeof(qff_ascii_glyph_table_v1_t));
71 return false;
72 }
73
74 // Make sure this block is valid
75 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)))) {
76 return false;
77 }
78
79 return true;
80}
81
82static bool qff_validate_unicode_descriptor(qp_stream_t *stream, uint16_t num_unicode_glyphs) {
83 // Read the raw descriptor
84 qff_unicode_glyph_table_v1_t unicode_descriptor;
85 if (qp_stream_read(&unicode_descriptor, sizeof(qff_unicode_glyph_table_v1_t), 1, stream) != 1) {
86 qp_dprintf("Failed to read unicode_descriptor, expected length was not %d\n", (int)sizeof(qff_unicode_glyph_table_v1_t));
87 return false;
88 }
89
90 // Make sure this block is valid
91 if (!qgf_validate_block_header(&unicode_descriptor.header, QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID, num_unicode_glyphs * 6)) {
92 return false;
93 }
94
95 // Skip the necessary amount of data to get to the next block
96 qp_stream_seek(stream, num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t), SEEK_CUR);
97
98 return true;
99}
100
101bool qff_validate_stream(qp_stream_t *stream) {
102 bool has_ascii_table;
103 uint16_t num_unicode_glyphs;
104
105 if (!qff_read_font_descriptor(stream, NULL, &has_ascii_table, &num_unicode_glyphs, NULL, NULL, NULL, NULL)) {
106 return false;
107 }
108
109 if (has_ascii_table) {
110 if (!qff_validate_ascii_descriptor(stream)) {
111 return false;
112 }
113 }
114
115 if (num_unicode_glyphs > 0) {
116 if (!qff_validate_unicode_descriptor(stream, num_unicode_glyphs)) {
117 return false;
118 }
119 }
120
121 return true;
122}
123
124uint32_t qff_get_total_size(qp_stream_t *stream) {
125 // Get the original location
126 uint32_t oldpos = qp_stream_tell(stream);
127
128 // Read the font descriptor, grabbing the size
129 uint32_t total_size;
130 if (!qff_read_font_descriptor(stream, NULL, NULL, NULL, NULL, NULL, NULL, &total_size)) {
131 return false;
132 }
133
134 // Restore the original location
135 qp_stream_setpos(stream, oldpos);
136 return total_size;
137}
diff --git a/quantum/painter/qff.h b/quantum/painter/qff.h
new file mode 100644
index 0000000000..6f1a1fd815
--- /dev/null
+++ b/quantum/painter/qff.h
@@ -0,0 +1,88 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6// Quantum Font File "QFF" File Format.
7// See https://docs.qmk.fm/#/quantum_painter_qff for more information.
8
9#include <stdint.h>
10#include <stdbool.h>
11
12#include "qp_stream.h"
13#include "qp_internal.h"
14#include "qgf.h"
15
16////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
17// QFF structures
18
19/////////////////////////////////////////
20// Font descriptor
21
22#define QFF_FONT_DESCRIPTOR_TYPEID 0x00
23
24typedef struct __attribute__((packed)) qff_font_descriptor_v1_t {
25 qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 }
26 uint32_t magic : 24; // constant, equal to 0x464651 ("QFF")
27 uint8_t qff_version; // constant, equal to 0x01
28 uint32_t total_file_size; // total size of the entire file, starting at offset zero
29 uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors
30 uint8_t line_height; // glyph height in pixels
31 bool has_ascii_table; // whether the font has an ascii table of glyphs (0x20...0x7E)
32 uint16_t num_unicode_glyphs; // the number of glyphs in the unicode table -- no table specified if zero
33 qp_image_format_t format : 8; // Frame format, see qp.h.
34 uint8_t flags; // frame flags, see below.
35 uint8_t compression_scheme; // compression scheme, see below.
36 uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
37} qff_font_descriptor_v1_t;
38
39_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");
40
41#define QFF_MAGIC 0x464651
42
43/////////////////////////////////////////
44// ASCII glyph table descriptor
45
46#define QFF_ASCII_GLYPH_DESCRIPTOR_TYPEID 0x01
47
48#define QFF_GLYPH_WIDTH_BITS 6
49#define QFF_GLYPH_WIDTH_MASK ((1 << QFF_GLYPH_WIDTH_BITS) - 1)
50#define QFF_GLYPH_OFFSET_BITS 18
51#define QFF_GLYPH_OFFSET_MASK (((1 << QFF_GLYPH_OFFSET_BITS) - 1) << QFF_GLYPH_WIDTH_BITS)
52
53typedef struct __attribute__((packed)) qff_ascii_glyph_v1_t {
54 uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined
55} qff_ascii_glyph_v1_t;
56
57_Static_assert(sizeof(qff_ascii_glyph_v1_t) == 3, "qff_ascii_glyph_v1_t must be 3 bytes in v1 of QFF");
58
59typedef struct __attribute__((packed)) qff_ascii_glyph_table_v1_t {
60 qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 }
61 qff_ascii_glyph_v1_t glyph[95]; // 95 glyphs, 0x20..0x7E
62} qff_ascii_glyph_table_v1_t;
63
64_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");
65
66/////////////////////////////////////////
67// Unicode glyph table descriptor
68
69#define QFF_UNICODE_GLYPH_DESCRIPTOR_TYPEID 0x02
70
71typedef struct __attribute__((packed)) qff_unicode_glyph_v1_t {
72 uint32_t code_point : 24;
73 uint32_t value : 24; // Uses QFF_GLYPH_*_(BITS|MASK) as bitfield ordering is compiler-defined
74} qff_unicode_glyph_v1_t;
75
76_Static_assert(sizeof(qff_unicode_glyph_v1_t) == 6, "qff_unicode_glyph_v1_t must be 6 bytes in v1 of QFF");
77
78typedef struct __attribute__((packed)) qff_unicode_glyph_table_v1_t {
79 qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = (N * 6) }
80 qff_unicode_glyph_v1_t glyph[0]; // Extent of '0' signifies that this struct is immediately followed by the glyph data
81} qff_unicode_glyph_table_v1_t;
82
83////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
84// QFF API
85
86bool qff_validate_stream(qp_stream_t *stream);
87uint32_t qff_get_total_size(qp_stream_t *stream);
88bool 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);
diff --git a/quantum/painter/qgf.c b/quantum/painter/qgf.c
new file mode 100644
index 0000000000..834837105b
--- /dev/null
+++ b/quantum/painter/qgf.c
@@ -0,0 +1,292 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4// Quantum Graphics File "QGF" File Format.
5// See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
6
7#include "qgf.h"
8#include "qp_draw.h"
9
10////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
11// QGF API
12
13bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length) {
14 if (desc->type_id != expected_typeid || desc->neg_type_id != ((~expected_typeid) & 0xFF)) {
15 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);
16 return false;
17 }
18
19 if (expected_length >= 0 && desc->length != expected_length) {
20 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);
21 return false;
22 }
23
24 return true;
25}
26
27bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette) {
28 // clang-format off
29 static const struct QP_PACKED {
30 uint8_t bpp;
31 bool has_palette;
32 } formats[] = {
33 [GRAYSCALE_1BPP] = {.bpp = 1, .has_palette = false},
34 [GRAYSCALE_2BPP] = {.bpp = 2, .has_palette = false},
35 [GRAYSCALE_4BPP] = {.bpp = 4, .has_palette = false},
36 [GRAYSCALE_8BPP] = {.bpp = 8, .has_palette = false},
37 [PALETTE_1BPP] = {.bpp = 1, .has_palette = true},
38 [PALETTE_2BPP] = {.bpp = 2, .has_palette = true},
39 [PALETTE_4BPP] = {.bpp = 4, .has_palette = true},
40 [PALETTE_8BPP] = {.bpp = 8, .has_palette = true},
41 };
42 // clang-format on
43
44 // Copy out the required info
45 if (format > PALETTE_8BPP) {
46 qp_dprintf("Failed to parse frame_descriptor, invalid format 0x%02X\n", (int)format);
47 return false;
48 }
49
50 // Copy out the required info
51 if (bpp) {
52 *bpp = formats[format].bpp;
53 }
54 if (has_palette) {
55 *has_palette = formats[format].has_palette;
56 }
57
58 return true;
59}
60
61bool 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) {
62 // Decode the format
63 qgf_parse_format(frame_descriptor->format, bpp, has_palette);
64
65 // Copy out the required info
66 if (is_delta) {
67 *is_delta = (frame_descriptor->flags & QGF_FRAME_FLAG_DELTA) == QGF_FRAME_FLAG_DELTA;
68 }
69 if (compression_scheme) {
70 *compression_scheme = frame_descriptor->compression_scheme;
71 }
72 if (delay) {
73 *delay = frame_descriptor->delay;
74 }
75
76 return true;
77}
78
79bool qgf_read_graphics_descriptor(qp_stream_t *stream, uint16_t *image_width, uint16_t *image_height, uint16_t *frame_count, uint32_t *total_bytes) {
80 // Seek to the start
81 qp_stream_setpos(stream, 0);
82
83 // Read and validate the graphics descriptor
84 qgf_graphics_descriptor_v1_t graphics_descriptor;
85 if (qp_stream_read(&graphics_descriptor, sizeof(qgf_graphics_descriptor_v1_t), 1, stream) != 1) {
86 qp_dprintf("Failed to read graphics_descriptor, expected length was not %d\n", (int)sizeof(qgf_graphics_descriptor_v1_t));
87 return false;
88 }
89
90 // Make sure this block is valid
91 if (!qgf_validate_block_header(&graphics_descriptor.header, QGF_GRAPHICS_DESCRIPTOR_TYPEID, (sizeof(qgf_graphics_descriptor_v1_t) - sizeof(qgf_block_header_v1_t)))) {
92 return false;
93 }
94
95 // Make sure the magic and version are correct
96 if (graphics_descriptor.magic != QGF_MAGIC || graphics_descriptor.qgf_version != 0x01) {
97 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);
98 return false;
99 }
100
101 // Make sure the file length is valid
102 if (graphics_descriptor.neg_total_file_size != ~graphics_descriptor.total_file_size) {
103 qp_dprintf("Failed to validate graphics_descriptor, expected negated length 0x%08X was 0x%08X\n", (int)(~graphics_descriptor.total_file_size), (int)graphics_descriptor.neg_total_file_size);
104 return false;
105 }
106
107 // Copy out the required info
108 if (image_width) {
109 *image_width = graphics_descriptor.image_width;
110 }
111 if (image_height) {
112 *image_height = graphics_descriptor.image_height;
113 }
114 if (frame_count) {
115 *frame_count = graphics_descriptor.frame_count;
116 }
117 if (total_bytes) {
118 *total_bytes = graphics_descriptor.total_file_size;
119 }
120
121 return true;
122}
123
124static bool qgf_read_frame_offset(qp_stream_t *stream, uint16_t frame_number, uint32_t *frame_offset) {
125 uint16_t frame_count;
126 if (!qgf_read_graphics_descriptor(stream, NULL, NULL, &frame_count, NULL)) {
127 return false;
128 }
129
130 // Read the frame offsets descriptor
131 qgf_frame_offsets_v1_t frame_offsets;
132 if (qp_stream_read(&frame_offsets, sizeof(qgf_frame_offsets_v1_t), 1, stream) != 1) {
133 qp_dprintf("Failed to read frame_offsets, expected length was not %d\n", (int)sizeof(qgf_frame_offsets_v1_t));
134 return false;
135 }
136
137 // Make sure this block is valid
138 if (!qgf_validate_block_header(&frame_offsets.header, QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID, (frame_count * sizeof(uint32_t)))) {
139 return false;
140 }
141
142 if (frame_number >= frame_count) {
143 qp_dprintf("Invalid frame number, was %d but only %d frames in image\n", (int)frame_number, (int)frame_count);
144 return false;
145 }
146
147 // Skip the necessary amount of data to get to the requested frame offset
148 qp_stream_seek(stream, frame_number * sizeof(uint32_t), SEEK_CUR);
149
150 // Read the frame offset
151 uint32_t offset = 0;
152 if (qp_stream_read(&offset, sizeof(uint32_t), 1, stream) != 1) {
153 qp_dprintf("Failed to read frame offset, expected length was not %d\n", (int)sizeof(uint32_t));
154 return false;
155 }
156
157 // Copy out the required info
158 if (frame_offset) {
159 *frame_offset = offset;
160 }
161
162 return true;
163}
164
165void qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number) {
166 // Read the offset
167 uint32_t offset = 0;
168 qgf_read_frame_offset(stream, frame_number, &offset);
169
170 // Move to the offset
171 qp_stream_setpos(stream, offset);
172}
173
174bool qgf_validate_frame_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t *bpp, bool *has_palette, bool *is_delta) {
175 // Seek to the correct location
176 qgf_seek_to_frame_descriptor(stream, frame_number);
177
178 // Read the raw descriptor
179 qgf_frame_v1_t frame_descriptor;
180 if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, stream) != 1) {
181 qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t));
182 return false;
183 }
184
185 // Make sure this block is valid
186 if (!qgf_validate_block_header(&frame_descriptor.header, QGF_FRAME_DESCRIPTOR_TYPEID, (sizeof(qgf_frame_v1_t) - sizeof(qgf_block_header_v1_t)))) {
187 return false;
188 }
189
190 return qgf_parse_frame_descriptor(&frame_descriptor, bpp, has_palette, is_delta, NULL, NULL);
191}
192
193bool qgf_validate_palette_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t bpp) {
194 // Read the palette descriptor
195 qgf_palette_v1_t palette_descriptor;
196 if (qp_stream_read(&palette_descriptor, sizeof(qgf_palette_v1_t), 1, stream) != 1) {
197 qp_dprintf("Failed to read palette_descriptor, expected length was not %d\n", (int)sizeof(qgf_palette_v1_t));
198 return false;
199 }
200
201 // Make sure this block is valid
202 uint32_t expected_length = (1 << bpp) * 3 * sizeof(uint8_t);
203 if (!qgf_validate_block_header(&palette_descriptor.header, QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID, expected_length)) {
204 return false;
205 }
206
207 // Move forward in the stream to the next block
208 qp_stream_seek(stream, expected_length, SEEK_CUR);
209 return true;
210}
211
212bool qgf_validate_delta_descriptor(qp_stream_t *stream, uint16_t frame_number) {
213 // Read the delta descriptor
214 qgf_delta_v1_t delta_descriptor;
215 if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, stream) != 1) {
216 qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t));
217 return false;
218 }
219
220 // Make sure this block is valid
221 if (!qgf_validate_block_header(&delta_descriptor.header, QGF_FRAME_DELTA_DESCRIPTOR_TYPEID, (sizeof(qgf_delta_v1_t) - sizeof(qgf_block_header_v1_t)))) {
222 return false;
223 }
224
225 return true;
226}
227
228bool qgf_validate_frame_data_descriptor(qp_stream_t *stream, uint16_t frame_number) {
229 // Read and validate the data block
230 qgf_data_v1_t data_descriptor;
231 if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, stream) != 1) {
232 qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t));
233 return false;
234 }
235
236 if (!qgf_validate_block_header(&data_descriptor.header, QGF_FRAME_DATA_DESCRIPTOR_TYPEID, -1)) {
237 return false;
238 }
239
240 return true;
241}
242
243bool qgf_validate_stream(qp_stream_t *stream) {
244 uint16_t frame_count;
245 if (!qgf_read_graphics_descriptor(stream, NULL, NULL, &frame_count, NULL)) {
246 return false;
247 }
248
249 // Read and validate all the frames (automatically validates the frame offset descriptor in the process)
250 for (uint16_t i = 0; i < frame_count; ++i) {
251 // Validate the frame descriptor block
252 uint8_t bpp;
253 bool has_palette;
254 bool has_delta;
255 if (!qgf_validate_frame_descriptor(stream, i, &bpp, &has_palette, &has_delta)) {
256 return false;
257 }
258
259 // If we've got a palette block, check it
260 if (has_palette && !qgf_validate_palette_descriptor(stream, i, bpp)) {
261 return false;
262 }
263
264 // If we've got a delta block, check it
265 if (has_delta && !qgf_validate_delta_descriptor(stream, i)) {
266 return false;
267 }
268
269 // Check the data block
270 if (!qgf_validate_frame_data_descriptor(stream, i)) {
271 return false;
272 }
273 }
274
275 return true;
276}
277
278// Work out the total size of an image definition, assuming we can read far enough into the file
279uint32_t qgf_get_total_size(qp_stream_t *stream) {
280 // Get the original location
281 uint32_t oldpos = qp_stream_tell(stream);
282
283 // Read the graphics descriptor, grabbing the size
284 uint32_t total_size;
285 if (!qgf_read_graphics_descriptor(stream, NULL, NULL, NULL, &total_size)) {
286 return false;
287 }
288
289 // Restore the original location
290 qp_stream_setpos(stream, oldpos);
291 return total_size;
292}
diff --git a/quantum/painter/qgf.h b/quantum/painter/qgf.h
new file mode 100644
index 0000000000..54585edd04
--- /dev/null
+++ b/quantum/painter/qgf.h
@@ -0,0 +1,136 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6// Quantum Graphics File "QGF" File Format.
7// See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
8
9#include <stdint.h>
10#include <stdbool.h>
11
12#include "qp_stream.h"
13#include "qp_internal.h"
14
15////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
16// QGF structures
17
18/////////////////////////////////////////
19// Common block header
20
21typedef struct QP_PACKED qgf_block_header_v1_t {
22 uint8_t type_id; // See each respective block type below.
23 uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors.
24 uint32_t length : 24; // 24-bit blob length, allowing for block sizes of a maximum of 16MB.
25} qgf_block_header_v1_t;
26
27_Static_assert(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF");
28
29/////////////////////////////////////////
30// Graphics descriptor
31
32#define QGF_GRAPHICS_DESCRIPTOR_TYPEID 0x00
33
34typedef struct QP_PACKED qgf_graphics_descriptor_v1_t {
35 qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 }
36 uint32_t magic : 24; // constant, equal to 0x464751 ("QGF")
37 uint8_t qgf_version; // constant, equal to 0x01
38 uint32_t total_file_size; // total size of the entire file, starting at offset zero
39 uint32_t neg_total_file_size; // negated value of total_file_size
40 uint16_t image_width; // in pixels
41 uint16_t image_height; // in pixels
42 uint16_t frame_count; // minimum of 1
43} qgf_graphics_descriptor_v1_t;
44
45_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");
46
47#define QGF_MAGIC 0x464751
48
49/////////////////////////////////////////
50// Frame offset descriptor
51
52#define QGF_FRAME_OFFSET_DESCRIPTOR_TYPEID 0x01
53
54typedef struct QP_PACKED qgf_frame_offsets_v1_t {
55 qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = (N * sizeof(uint32_t)) }
56 uint32_t offset[0]; // '0' signifies that this struct is immediately followed by the frame offsets
57} qgf_frame_offsets_v1_t;
58
59_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");
60
61/////////////////////////////////////////
62// Frame descriptor
63
64#define QGF_FRAME_DESCRIPTOR_TYPEID 0x02
65
66typedef struct QP_PACKED qgf_frame_v1_t {
67 qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 6 }
68 qp_image_format_t format : 8; // Frame format, see qp.h.
69 uint8_t flags; // Frame flags, see below.
70 painter_compression_t compression_scheme : 8; // Compression scheme, see qp.h.
71 uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
72 uint16_t delay; // frame delay time for animations (in units of milliseconds)
73} qgf_frame_v1_t;
74
75_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");
76
77#define QGF_FRAME_FLAG_DELTA 0x02
78#define QGF_FRAME_FLAG_TRANSPARENT 0x01
79
80/////////////////////////////////////////
81// Frame palette descriptor
82
83#define QGF_FRAME_PALETTE_DESCRIPTOR_TYPEID 0x03
84
85typedef struct QP_PACKED qgf_palette_entry_v1_t {
86 uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t.
87 uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t.
88 uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t.
89} qgf_palette_entry_v1_t;
90
91_Static_assert(sizeof(qgf_palette_entry_v1_t) == 3, "Palette entry is not 3 bytes in size");
92
93typedef struct QP_PACKED qgf_palette_v1_t {
94 qgf_block_header_v1_t header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) }
95 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
96} qgf_palette_v1_t;
97
98_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");
99
100/////////////////////////////////////////
101// Frame delta descriptor
102
103#define QGF_FRAME_DELTA_DESCRIPTOR_TYPEID 0x04
104
105typedef struct QP_PACKED qgf_delta_v1_t {
106 qgf_block_header_v1_t header; // = { .type_id = 0x04, .neg_type_id = (~0x04), .length = 8 }
107 uint16_t left; // The left pixel location to draw the delta image
108 uint16_t top; // The top pixel location to draw the delta image
109 uint16_t right; // The right pixel location to to draw the delta image
110 uint16_t bottom; // The bottom pixel location to to draw the delta image
111} qgf_delta_v1_t;
112
113_Static_assert(sizeof(qgf_delta_v1_t) == (sizeof(qgf_block_header_v1_t) + 8), "qgf_delta_v1_t must be 13 bytes in v1 of QGF");
114
115/////////////////////////////////////////
116// Frame data descriptor
117
118#define QGF_FRAME_DATA_DESCRIPTOR_TYPEID 0x05
119
120typedef struct QP_PACKED qgf_data_v1_t {
121 qgf_block_header_v1_t header; // = { .type_id = 0x05, .neg_type_id = (~0x05), .length = N }
122 uint8_t data[0]; // 0 signifies that this struct is immediately followed by the length of data specified in the header
123} qgf_data_v1_t;
124
125_Static_assert(sizeof(qgf_data_v1_t) == sizeof(qgf_block_header_v1_t), "qgf_data_v1_t must only contain qgf_block_header_v1_t in v1 of QGF");
126
127////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
128// QGF API
129
130uint32_t qgf_get_total_size(qp_stream_t *stream);
131bool qgf_validate_stream(qp_stream_t *stream);
132bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typeid, int32_t expected_length);
133bool qgf_read_graphics_descriptor(qp_stream_t *stream, uint16_t *image_width, uint16_t *image_height, uint16_t *frame_count, uint32_t *total_bytes);
134bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette);
135void qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number);
136bool 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);
diff --git a/quantum/painter/qp.c b/quantum/painter/qp.c
new file mode 100644
index 0000000000..e292ff6497
--- /dev/null
+++ b/quantum/painter/qp.c
@@ -0,0 +1,228 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <quantum.h>
5#include <utf8.h>
6
7#include "qp_internal.h"
8#include "qp_comms.h"
9#include "qp_draw.h"
10
11////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
12// Internal driver validation
13
14static bool validate_driver_vtable(struct painter_driver_t *driver) {
15 return (driver->driver_vtable && driver->driver_vtable->init && driver->driver_vtable->power && driver->driver_vtable->clear && driver->driver_vtable->viewport && driver->driver_vtable->pixdata && driver->driver_vtable->palette_convert && driver->driver_vtable->append_pixels) ? true : false;
16}
17
18static bool validate_comms_vtable(struct painter_driver_t *driver) {
19 return (driver->comms_vtable && driver->comms_vtable->comms_init && driver->comms_vtable->comms_start && driver->comms_vtable->comms_stop && driver->comms_vtable->comms_send) ? true : false;
20}
21
22static bool validate_driver_integrity(struct painter_driver_t *driver) {
23 return validate_driver_vtable(driver) && validate_comms_vtable(driver);
24}
25
26////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
27// Quantum Painter External API: qp_init
28
29bool qp_init(painter_device_t device, painter_rotation_t rotation) {
30 qp_dprintf("qp_init: entry\n");
31 struct painter_driver_t *driver = (struct painter_driver_t *)device;
32
33 driver->validate_ok = false;
34 if (!validate_driver_integrity(driver)) {
35 qp_dprintf("Failed to validate driver integrity in qp_init\n");
36 return false;
37 }
38
39 driver->validate_ok = true;
40
41 if (!qp_comms_init(device)) {
42 driver->validate_ok = false;
43 qp_dprintf("qp_init: fail (could not init comms)\n");
44 return false;
45 }
46
47 if (!qp_comms_start(device)) {
48 qp_dprintf("qp_init: fail (could not start comms)\n");
49 return false;
50 }
51
52 // Set the rotation before init
53 driver->rotation = rotation;
54
55 // Invoke init
56 bool ret = driver->driver_vtable->init(device, rotation);
57 qp_comms_stop(device);
58 qp_dprintf("qp_init: %s\n", ret ? "ok" : "fail");
59 return ret;
60}
61
62////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
63// Quantum Painter External API: qp_power
64
65bool qp_power(painter_device_t device, bool power_on) {
66 qp_dprintf("qp_power: entry\n");
67 struct painter_driver_t *driver = (struct painter_driver_t *)device;
68 if (!driver->validate_ok) {
69 qp_dprintf("qp_power: fail (validation_ok == false)\n");
70 return false;
71 }
72
73 if (!qp_comms_start(device)) {
74 qp_dprintf("qp_power: fail (could not start comms)\n");
75 return false;
76 }
77
78 bool ret = driver->driver_vtable->power(device, power_on);
79 qp_comms_stop(device);
80 qp_dprintf("qp_power: %s\n", ret ? "ok" : "fail");
81 return ret;
82}
83
84////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
85// Quantum Painter External API: qp_clear
86
87bool qp_clear(painter_device_t device) {
88 qp_dprintf("qp_clear: entry\n");
89 struct painter_driver_t *driver = (struct painter_driver_t *)device;
90 if (!driver->validate_ok) {
91 qp_dprintf("qp_clear: fail (validation_ok == false)\n");
92 return false;
93 }
94
95 if (!qp_comms_start(device)) {
96 qp_dprintf("qp_clear: fail (could not start comms)\n");
97 return false;
98 }
99
100 bool ret = driver->driver_vtable->clear(device);
101 qp_comms_stop(device);
102 qp_dprintf("qp_clear: %s\n", ret ? "ok" : "fail");
103 return ret;
104}
105
106////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
107// Quantum Painter External API: qp_flush
108
109bool qp_flush(painter_device_t device) {
110 qp_dprintf("qp_flush: entry\n");
111 struct painter_driver_t *driver = (struct painter_driver_t *)device;
112 if (!driver->validate_ok) {
113 qp_dprintf("qp_flush: fail (validation_ok == false)\n");
114 return false;
115 }
116
117 if (!qp_comms_start(device)) {
118 qp_dprintf("qp_flush: fail (could not start comms)\n");
119 return false;
120 }
121
122 bool ret = driver->driver_vtable->flush(device);
123 qp_comms_stop(device);
124 qp_dprintf("qp_flush: %s\n", ret ? "ok" : "fail");
125 return ret;
126}
127
128////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
129// Quantum Painter External API: qp_get_geometry
130
131void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y) {
132 qp_dprintf("qp_geometry: entry\n");
133 struct painter_driver_t *driver = (struct painter_driver_t *)device;
134
135 switch (driver->rotation) {
136 default:
137 case QP_ROTATION_0:
138 case QP_ROTATION_180:
139 if (width) {
140 *width = driver->panel_width;
141 }
142 if (height) {
143 *height = driver->panel_height;
144 }
145 break;
146 case QP_ROTATION_90:
147 case QP_ROTATION_270:
148 if (width) {
149 *width = driver->panel_height;
150 }
151 if (height) {
152 *height = driver->panel_width;
153 }
154 break;
155 }
156
157 if (rotation) {
158 *rotation = driver->rotation;
159 }
160
161 if (offset_x) {
162 *offset_x = driver->offset_x;
163 }
164
165 if (offset_y) {
166 *offset_y = driver->offset_y;
167 }
168
169 qp_dprintf("qp_geometry: ok\n");
170}
171
172////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
173// Quantum Painter External API: qp_set_viewport_offsets
174
175void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y) {
176 qp_dprintf("qp_set_viewport_offsets: entry\n");
177 struct painter_driver_t *driver = (struct painter_driver_t *)device;
178
179 driver->offset_x = offset_x;
180 driver->offset_y = offset_y;
181
182 qp_dprintf("qp_set_viewport_offsets: ok\n");
183}
184
185////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
186// Quantum Painter External API: qp_viewport
187
188bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
189 qp_dprintf("qp_viewport: entry\n");
190 struct painter_driver_t *driver = (struct painter_driver_t *)device;
191 if (!driver->validate_ok) {
192 qp_dprintf("qp_viewport: fail (validation_ok == false)\n");
193 return false;
194 }
195
196 if (!qp_comms_start(device)) {
197 qp_dprintf("qp_viewport: fail (could not start comms)\n");
198 return false;
199 }
200
201 // Set the viewport
202 bool ret = driver->driver_vtable->viewport(device, left, top, right, bottom);
203 qp_dprintf("qp_viewport: %s\n", ret ? "ok" : "fail");
204 qp_comms_stop(device);
205 return ret;
206}
207
208////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
209// Quantum Painter External API: qp_pixdata
210
211bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
212 qp_dprintf("qp_pixdata: entry\n");
213 struct painter_driver_t *driver = (struct painter_driver_t *)device;
214 if (!driver->validate_ok) {
215 qp_dprintf("qp_pixdata: fail (validation_ok == false)\n");
216 return false;
217 }
218
219 if (!qp_comms_start(device)) {
220 qp_dprintf("qp_pixdata: fail (could not start comms)\n");
221 return false;
222 }
223
224 bool ret = driver->driver_vtable->pixdata(device, pixel_data, native_pixel_count);
225 qp_dprintf("qp_pixdata: %s\n", ret ? "ok" : "fail");
226 qp_comms_stop(device);
227 return ret;
228}
diff --git a/quantum/painter/qp.h b/quantum/painter/qp.h
new file mode 100644
index 0000000000..e1c14d156c
--- /dev/null
+++ b/quantum/painter/qp.h
@@ -0,0 +1,453 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <stdint.h>
7#include <stdbool.h>
8
9#include "deferred_exec.h"
10
11////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
12// Quantum Painter global configurables (add to your keyboard's config.h)
13
14#ifndef QUANTUM_PAINTER_NUM_IMAGES
15/**
16 * @def This controls the maximum number of images that Quantum Painter can load at any one time. Images can be loaded
17 * using \ref qp_load_image_mem, and can be unloaded by calling \ref qp_close_image. Increasing this number in
18 * order to load more images increases the amount of RAM required. Image data is not held in RAM, just metadata.
19 */
20# define QUANTUM_PAINTER_NUM_IMAGES 8
21#endif // QUANTUM_PAINTER_NUM_IMAGES
22
23#ifndef QUANTUM_PAINTER_NUM_FONTS
24/**
25 * @def This controls the maximum number of fonts that Quantum Painter can load. Fonts can be loaded using
26 * \ref qp_load_font_mem, and can be unloaded by calling \ref qp_close_font. Increasing this number in order to
27 * load more fonts increases the amount of RAM required. Font data is not held in RAM, unless
28 * \ref QUANTUM_PAINTER_LOAD_FONTS_TO_RAM is set to TRUE.
29 */
30# define QUANTUM_PAINTER_NUM_FONTS 4
31#endif // QUANTUM_PAINTER_NUM_FONTS
32
33#ifndef QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
34/**
35 * @def This controls whether or not fonts should be cached in RAM. Under normal circumstances, fonts can have quite
36 * random access patterns, and due to timing of flash memory or external storage, it may be a significant speedup
37 * moving the font into RAM before use. Defaults to "off", but if it's enabled it will fallback to reading from the
38 * original location if corresponding RAM could not be allocated (such as being too large).
39 */
40# define QUANTUM_PAINTER_LOAD_FONTS_TO_RAM FALSE
41#endif
42
43#ifndef QUANTUM_PAINTER_CONCURRENT_ANIMATIONS
44/**
45 * @def This controls the maximum number of animations that Quantum Painter can play simultaneously. Increasing this
46 * number in order to play more animations at the same time increases the amount of RAM required.
47 */
48# define QUANTUM_PAINTER_CONCURRENT_ANIMATIONS 4
49#endif // QUANTUM_PAINTER_CONCURRENT_ANIMATIONS
50
51#ifndef QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE
52/**
53 * @def This controls the maximum size of the pixel data buffer used for single blocks of transmission. Larger buffers
54 * means more data is processed at one time, with less frequent transmissions, at the cost of RAM.
55 */
56# define QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE 32
57#endif
58
59#ifndef QUANTUM_PAINTER_SUPPORTS_256_PALETTE
60/**
61 * @def This controls whether 256-color palettes are supported. This has relatively hefty requirements on RAM -- at
62 * least 1kB extra is required just to store the palette information, with more required for other metadata.
63 */
64# define QUANTUM_PAINTER_SUPPORTS_256_PALETTE FALSE
65#endif
66
67////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
68// Quantum Painter types
69
70/**
71 * @typedef A handle to a Quantum Painter device, such as an LCD or OLED. Most Quantum Painter APIs require this
72 * argument in order to perform operations on the display.
73 */
74typedef const void *painter_device_t;
75
76/**
77 * @typedef The desired rotation of a panel. Used as a parameter to \ref qp_init, and can be queried by
78 * \ref qp_get_geometry.
79 */
80typedef enum { QP_ROTATION_0, QP_ROTATION_90, QP_ROTATION_180, QP_ROTATION_270 } painter_rotation_t;
81
82/**
83 * @typedef A descriptor for a Quantum Painter image.
84 */
85typedef struct painter_image_desc_t {
86 uint16_t width; ///< Image width
87 uint16_t height; ///< Image height
88 uint16_t frame_count; ///< Number of frames in this image
89} painter_image_desc_t;
90
91/**
92 * @typedef A handle to a Quantum Painter image.
93 */
94typedef const painter_image_desc_t *painter_image_handle_t;
95
96/**
97 * @typedef A descriptor for a Quantum Painter font.
98 */
99typedef struct painter_font_desc_t {
100 uint8_t line_height; ///< The number of pixels in height for each line
101} painter_font_desc_t;
102
103/**
104 * @typedef A handle to a Quantum Painter font.
105 */
106typedef const painter_font_desc_t *painter_font_handle_t;
107
108////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
109// Quantum Painter External API
110
111/**
112 * Initialize a device and set its rotation.
113 *
114 * @param device[in] the handle of the device to initialize
115 * @param rotation[in] the rotation to use
116 * @return true if initialization succeeded
117 * @return false if initialization failed
118 */
119bool qp_init(painter_device_t device, painter_rotation_t rotation);
120
121/**
122 * Controls whether a display is on or off.
123 *
124 * @note If backlighting is used to control brightness (such as for an LCD), it will need to be handled external to
125 * Quantum Painter.
126 *
127 * @param device[in] the handle of the device to control
128 * @param power_on[in] whether or not the device should be on
129 * @return true if controlling the power state succeeded
130 * @return false if controlling the power state failed
131 */
132bool qp_power(painter_device_t device, bool power_on);
133
134/**
135 * Clears a device's screen.
136 *
137 * @param device[in] the handle of the device to control
138 * @return true if clearing the screen succeeded
139 * @return false if clearing the screen failed
140 */
141bool qp_clear(painter_device_t device);
142
143/**
144 * Transmits any outstanding data to the screen in order to persist all changes to the display.
145 *
146 * @note Drivers without internal framebuffers will likely ignore this API.
147 *
148 * @param device[in] the handle of the device to control
149 * @return true if flushing changes to the screen succeeded
150 * @return false if flushing changes to the screen failed
151 */
152bool qp_flush(painter_device_t device);
153
154/**
155 * Retrieves the size, rotation, and offsets for the display.
156 *
157 * @note Any arguments of NULL will be ignored.
158 *
159 * @param device[in] the handle of the device to control
160 * @param width[out] the device's width
161 * @param height[out] the device's height
162 * @param rotation[out] the device's rotation
163 * @param offset_x[out] the device's x-offset applied while drawing
164 * @param offset_y[out] the device's y-offset applied while drawing
165 */
166void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y);
167
168/**
169 * Allows repositioning of the viewport if the panel geometry offsets are non-zero.
170 *
171 * @param device[in] the handle of the device to control
172 * @param offset_x[in] the device's x-offset applied while drawing
173 * @param offset_y[in] the device's y-offset applied while drawing
174 */
175void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y);
176
177/**
178 * Sets a pixel to the specified color.
179 *
180 * @param device[in] the handle of the device to control
181 * @param x[in] the x-position to draw onto the device
182 * @param y[in] the y-position to draw onto the device
183 * @param hue[in] the hue to use, with 0-360 mapped to 0-255
184 * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
185 * @param val[in] the value to use, with 0-100% mapped to 0-255
186 * @return true if setting the pixel succeeded
187 * @return false if setting the pixel failed
188 */
189bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val);
190
191/**
192 * Draws a line using the specified color.
193 *
194 * @param device[in] the handle of the device to control
195 * @param x0[in] the device's x-position to start
196 * @param y0[in] the device's y-position to start
197 * @param x1[in] the device's x-position to finish
198 * @param y1[in] the device's y-position to finish
199 * @param hue[in] the hue to use, with 0-360 mapped to 0-255
200 * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
201 * @param val[in] the value to use, with 0-100% mapped to 0-255
202 * @return true if drawing the line succeeded
203 * @return false if drawing the line failed
204 */
205bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val);
206
207/**
208 * Draws a rectangle using the specified color, optionally filled.
209 *
210 * @param device[in] the handle of the device to control
211 * @param left[in] the device's x-position to start
212 * @param top[in] the device's y-position to start
213 * @param right[in] the device's x-position to finish
214 * @param bottom[in] the device's y-position to finish
215 * @param hue[in] the hue to use, with 0-360 mapped to 0-255
216 * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
217 * @param val[in] the value to use, with 0-100% mapped to 0-255
218 * @param filled[in] whether the rectangle should be filled
219 * @return true if drawing the rectangle succeeded
220 * @return false if drawing the rectangle failed
221 */
222bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
223
224/**
225 * Draws a circle using the specified color, optionally filled.
226 *
227 * @param device[in] the handle of the device to control
228 * @param x[in] the x-position of the centre of the circle to draw onto the device
229 * @param y[in] the y-position of the centre of the circle to draw onto the device
230 * @param radius[in] the radius of the circle to draw
231 * @param hue[in] the hue to use, with 0-360 mapped to 0-255
232 * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
233 * @param val[in] the value to use, with 0-100% mapped to 0-255
234 * @param filled[in] whether the circle should be filled
235 * @return true if drawing the circle succeeded
236 * @return false if drawing the circle failed
237 */
238bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
239
240/**
241 * Draws a ellipse using the specified color, optionally filled.
242 *
243 * @param device[in] the handle of the device to control
244 * @param x[in] the x-position of the centre of the ellipse to draw onto the device
245 * @param y[in] the y-position of the centre of the ellipse to draw onto the device
246 * @param sizex[in] the horizontal size of the ellipse
247 * @param sizey[in] the vertical size of the ellipse
248 * @param hue[in] the hue to use, with 0-360 mapped to 0-255
249 * @param sat[in] the saturation to use, with 0-100% mapped to 0-255
250 * @param val[in] the value to use, with 0-100% mapped to 0-255
251 * @param filled[in] whether the ellipse should be filled
252 * @return true if drawing the ellipse succeeded
253 * @return false if drawing the ellipse failed
254 */
255bool 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);
256
257/**
258 * Sets up the location on the display to stream raw pixel data to the display, using \ref qp_pixdata.
259 *
260 * @note This is for advanced uses only, and should not be required for normal Quantum Painter functionality.
261 *
262 * @param device[in] the handle of the device to control
263 * @param left[in] the device's x-position to start
264 * @param top[in] the device's y-position to start
265 * @param right[in] the device's x-position to finish
266 * @param bottom[in] the device's y-position to finish
267 * @return true if setting the viewport succeeded
268 * @return false if setting the viewport failed
269 */
270bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
271
272/**
273 * Streams raw pixel data (in the native panel format) to the area previously set by \ref qp_viewport.
274 *
275 * @note This is for advanced uses only, and should not be required for normal Quantum Painter functionality.
276 *
277 * @param device[in] the handle of the device to control
278 * @param pixel_data[in] pointer to buffer data
279 * @param native_pixel_count[in] the number of pixels to transmit
280 * @return true if streaming of data succeeded
281 * @return false if streaming of data failed
282 */
283bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
284
285/**
286 * Loads an image into memory.
287 *
288 * @note Images can be unloaded by calling \ref qp_close_image.
289 *
290 * @param buffer[in] the image data to load
291 * @return an image handle usable with \ref qp_drawimage, \ref qp_drawimage_recolor, \ref qp_animate, and
292 * \ref qp_animate_recolor.
293 * @return NULL if loading the image failed
294 */
295painter_image_handle_t qp_load_image_mem(const void *buffer);
296
297/**
298 * Closes an image handle when no longer in use.
299 *
300 * @param image[in] the handle of the image to unload
301 * @return true if unloading the image succeeded
302 * @return false if unloading the image failed
303 */
304bool qp_close_image(painter_image_handle_t image);
305
306/**
307 * Draws an image to the display.
308 *
309 * @param device[in] the handle of the device to control
310 * @param x[in] the x-position where the image should be drawn onto the device
311 * @param y[in] the y-position where the image should be drawn onto the device
312 * @param image[in] the handle of the image to draw
313 * @return true if drawing the image succeeded
314 * @return false if drawing the image failed
315 */
316bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
317
318/**
319 * Draws an image to the display, recoloring monochrome images to the desired foreground/background.
320 *
321 * @param device[in] the handle of the device to control
322 * @param x[in] the x-position where the image should be drawn onto the device
323 * @param y[in] the y-position where the image should be drawn onto the device
324 * @param image[in] the handle of the image to draw
325 * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
326 * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
327 * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
328 * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
329 * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
330 * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
331 * @return true if drawing the image succeeded
332 * @return false if drawing the image failed
333 */
334bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
335
336/**
337 * Draws an animation to the display.
338 *
339 * @param device[in] the handle of the device to control
340 * @param x[in] the x-position where the image should be drawn onto the device
341 * @param y[in] the y-position where the image should be drawn onto the device
342 * @param image[in] the handle of the image to draw
343 * @return the \ref deferred_token to use with \ref qp_stop_animation in order to stop animating
344 * @return INVALID_DEFERRED_TOKEN if animating the image failed
345 */
346deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
347
348/**
349 * Draws an animation to the display, recoloring monochrome images to the desired foreground/background.
350 *
351 * @param device[in] the handle of the device to control
352 * @param x[in] the x-position where the image should be drawn onto the device
353 * @param y[in] the y-position where the image should be drawn onto the device
354 * @param image[in] the handle of the image to draw
355 * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
356 * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
357 * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
358 * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
359 * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
360 * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
361 * @return the \ref deferred_token to use with \ref qp_stop_animation in order to stop animating
362 * @return INVALID_DEFERRED_TOKEN if animating the image failed
363 */
364deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
365
366/**
367 * Cancels a running animation.
368 *
369 * @param anim_token[in] the animation token returned by \ref qp_animate, or \ref qp_animate_recolor.
370 */
371void qp_stop_animation(deferred_token anim_token);
372
373/**
374 * Loads a font into memory.
375 *
376 * @note Fonts can be unloaded by calling \ref qp_close_font.
377 *
378 * @param buffer[in] the font data to load
379 * @return an image handle usable with \ref qp_textwidth, \ref qp_drawtext, and \ref qp_drawtext_recolor.
380 * @return NULL if loading the font failed
381 */
382painter_font_handle_t qp_load_font_mem(const void *buffer);
383
384/**
385 * Closes a font handle when no longer in use.
386 *
387 * @param font[in] the handle of the font to unload
388 * @return true if unloading the font succeeded
389 * @return false if unloading the font failed
390 */
391bool qp_close_font(painter_font_handle_t font);
392
393/**
394 * Measures the width (in pixels) of the supplied string, given the specified font.
395 *
396 * @param font[in] the handle of the font
397 * @param str[in] the string to measure
398 * @return the width (in pixels) needed to draw the specified string
399 */
400int16_t qp_textwidth(painter_font_handle_t font, const char *str);
401
402/**
403 * Draws text to the display.
404 *
405 * @param device[in] the handle of the device to control
406 * @param x[in] the x-position where the text should be drawn onto the device
407 * @param y[in] the y-position where the text should be drawn onto the device
408 * @param font[in] the handle of the font
409 * @param str[in] the string to draw
410 * @return the width (in pixels) used when drawing the specified string
411 */
412int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str);
413
414/**
415 * Draws text to the display, recoloring monochrome fonts to the desired foreground/background.
416 *
417 * @param device[in] the handle of the device to control
418 * @param x[in] the x-position where the text should be drawn onto the device
419 * @param y[in] the y-position where the text should be drawn onto the device
420 * @param font[in] the handle of the font
421 * @param str[in] the string to draw
422 * @param hue_fg[in] the foreground hue to use, with 0-360 mapped to 0-255
423 * @param sat_fg[in] the foreground saturation to use, with 0-100% mapped to 0-255
424 * @param val_fg[in] the foreground value to use, with 0-100% mapped to 0-255
425 * @param hue_bg[in] the background hue to use, with 0-360 mapped to 0-255
426 * @param sat_bg[in] the background saturation to use, with 0-100% mapped to 0-255
427 * @param val_bg[in] the background value to use, with 0-100% mapped to 0-255
428 * @return the width (in pixels) used when drawing the specified string
429 */
430int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
431
432////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
433// Quantum Painter Drivers
434
435#ifdef QUANTUM_PAINTER_ILI9163_ENABLE
436# include "qp_ili9163.h"
437#endif // QUANTUM_PAINTER_ILI9163_ENABLE
438
439#ifdef QUANTUM_PAINTER_ILI9341_ENABLE
440# include "qp_ili9341.h"
441#endif // QUANTUM_PAINTER_ILI9341_ENABLE
442
443#ifdef QUANTUM_PAINTER_ST7789_ENABLE
444# include "qp_st7789.h"
445#endif // QUANTUM_PAINTER_ST7789_ENABLE
446
447#ifdef QUANTUM_PAINTER_GC9A01_ENABLE
448# include "qp_gc9a01.h"
449#endif // QUANTUM_PAINTER_GC9A01_ENABLE
450
451#ifdef QUANTUM_PAINTER_SSD1351_ENABLE
452# include "qp_ssd1351.h"
453#endif // QUANTUM_PAINTER_SSD1351_ENABLE
diff --git a/quantum/painter/qp_comms.c b/quantum/painter/qp_comms.c
new file mode 100644
index 0000000000..dc17b49460
--- /dev/null
+++ b/quantum/painter/qp_comms.c
@@ -0,0 +1,72 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "qp_comms.h"
5
6////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7// Base comms APIs
8
9bool qp_comms_init(painter_device_t device) {
10 struct painter_driver_t *driver = (struct painter_driver_t *)device;
11 if (!driver->validate_ok) {
12 qp_dprintf("qp_comms_init: fail (validation_ok == false)\n");
13 return false;
14 }
15
16 return driver->comms_vtable->comms_init(device);
17}
18
19bool qp_comms_start(painter_device_t device) {
20 struct painter_driver_t *driver = (struct painter_driver_t *)device;
21 if (!driver->validate_ok) {
22 qp_dprintf("qp_comms_start: fail (validation_ok == false)\n");
23 return false;
24 }
25
26 return driver->comms_vtable->comms_start(device);
27}
28
29void qp_comms_stop(painter_device_t device) {
30 struct painter_driver_t *driver = (struct painter_driver_t *)device;
31 if (!driver->validate_ok) {
32 qp_dprintf("qp_comms_stop: fail (validation_ok == false)\n");
33 return;
34 }
35
36 driver->comms_vtable->comms_stop(device);
37}
38
39uint32_t qp_comms_send(painter_device_t device, const void *data, uint32_t byte_count) {
40 struct painter_driver_t *driver = (struct painter_driver_t *)device;
41 if (!driver->validate_ok) {
42 qp_dprintf("qp_comms_send: fail (validation_ok == false)\n");
43 return false;
44 }
45
46 return driver->comms_vtable->comms_send(device, data, byte_count);
47}
48
49////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
50// Comms APIs that use a D/C pin
51
52void qp_comms_command(painter_device_t device, uint8_t cmd) {
53 struct painter_driver_t * driver = (struct painter_driver_t *)device;
54 struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable;
55 comms_vtable->send_command(device, cmd);
56}
57
58void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data) {
59 qp_comms_command(device, cmd);
60 qp_comms_send(device, &data, sizeof(data));
61}
62
63uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void *data, uint32_t byte_count) {
64 qp_comms_command(device, cmd);
65 return qp_comms_send(device, data, byte_count);
66}
67
68void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
69 struct painter_driver_t * driver = (struct painter_driver_t *)device;
70 struct painter_comms_with_command_vtable_t *comms_vtable = (struct painter_comms_with_command_vtable_t *)driver->comms_vtable;
71 comms_vtable->bulk_command_sequence(device, sequence, sequence_len);
72}
diff --git a/quantum/painter/qp_comms.h b/quantum/painter/qp_comms.h
new file mode 100644
index 0000000000..8fbf25c201
--- /dev/null
+++ b/quantum/painter/qp_comms.h
@@ -0,0 +1,25 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <stdbool.h>
7#include <stdlib.h>
8
9#include "qp_internal.h"
10
11////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
12// Base comms APIs
13
14bool qp_comms_init(painter_device_t device);
15bool qp_comms_start(painter_device_t device);
16void qp_comms_stop(painter_device_t device);
17uint32_t qp_comms_send(painter_device_t device, const void* data, uint32_t byte_count);
18
19////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
20// Comms APIs that use a D/C pin
21
22void qp_comms_command(painter_device_t device, uint8_t cmd);
23void qp_comms_command_databyte(painter_device_t device, uint8_t cmd, uint8_t data);
24uint32_t qp_comms_command_databuf(painter_device_t device, uint8_t cmd, const void* data, uint32_t byte_count);
25void qp_comms_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len);
diff --git a/quantum/painter/qp_draw.h b/quantum/painter/qp_draw.h
new file mode 100644
index 0000000000..7094d80eaa
--- /dev/null
+++ b/quantum/painter/qp_draw.h
@@ -0,0 +1,85 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "qp_internal.h"
7#include "qp_stream.h"
8
9////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
10// Quantum Painter utility functions
11
12// Global variable used for native pixel data streaming.
13extern uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];
14
15// Check if the supplied bpp is capable of being rendered
16bool qp_internal_bpp_capable(uint8_t bits_per_pixel);
17
18// Returns the number of pixels that can fit in the pixdata buffer
19uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device);
20
21// Fills the supplied buffer with equivalent native pixels matching the supplied HSV
22void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val);
23
24// qp_setpixel internal implementation, but uses the global pixdata buffer with pre-converted native pixel. Only the first pixel is used.
25bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y);
26
27// qp_rect internal implementation, but uses the global pixdata buffer with pre-converted native pixels.
28bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t l, uint16_t t, uint16_t r, uint16_t b);
29
30// Convert from input pixel data + palette to equivalent pixels
31typedef int16_t (*qp_internal_byte_input_callback)(void* cb_arg);
32typedef bool (*qp_internal_pixel_output_callback)(qp_pixel_t* palette, uint8_t index, void* cb_arg);
33bool qp_internal_decode_palette(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t* palette, qp_internal_pixel_output_callback output_callback, void* output_arg);
34bool qp_internal_decode_grayscale(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_internal_pixel_output_callback output_callback, void* output_arg);
35bool qp_internal_decode_recolor(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qp_internal_pixel_output_callback output_callback, void* output_arg);
36
37// Global variable used for interpolated pixel lookup table.
38#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE
39extern qp_pixel_t qp_internal_global_pixel_lookup_table[256];
40#else
41extern qp_pixel_t qp_internal_global_pixel_lookup_table[16];
42#endif
43
44// Generates a color-interpolated lookup table based off the number of items, from foreground to background, for use with monochrome image rendering.
45// Returns true if a palette was created, false if the palette is reused.
46// As this uses a global, this may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() below to reset.
47bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps);
48
49// 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.
50void qp_internal_invalidate_palette(void);
51
52// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header.
53bool qp_internal_load_qgf_palette(qp_stream_t* stream, uint8_t bpp);
54
55////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
56// Quantum Painter codec functions
57
58enum qp_internal_rle_mode_t {
59 MARKER_BYTE,
60 REPEATING_RUN,
61 NON_REPEATING_RUN,
62};
63
64struct qp_internal_byte_input_state {
65 painter_device_t device;
66 qp_stream_t* src_stream;
67 int16_t curr;
68 union {
69 // RLE-specific
70 struct {
71 enum qp_internal_rle_mode_t mode;
72 uint8_t remain; // number of bytes remaining in the current mode
73 } rle;
74 };
75};
76
77struct qp_internal_pixel_output_state {
78 painter_device_t device;
79 uint32_t pixel_write_pos;
80 uint32_t max_pixels;
81};
82
83bool qp_internal_pixel_appender(qp_pixel_t* palette, uint8_t index, void* cb_arg);
84
85qp_internal_byte_input_callback qp_internal_prepare_input_state(struct qp_internal_byte_input_state* input_state, painter_compression_t compression);
diff --git a/quantum/painter/qp_draw_circle.c b/quantum/painter/qp_draw_circle.c
new file mode 100644
index 0000000000..edaae35835
--- /dev/null
+++ b/quantum/painter/qp_draw_circle.c
@@ -0,0 +1,172 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// Copyright 2021 Nick Brassel (@tzarc)
3// SPDX-License-Identifier: GPL-2.0-or-later
4
5#include "qp.h"
6#include "qp_internal.h"
7#include "qp_comms.h"
8#include "qp_draw.h"
9
10// Utilize 8-way symmetry to draw circles
11static bool qp_circle_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) {
12 /*
13 Circles have the property of 8-way symmetry, so eight pixels can be drawn
14 for each computed [offsetx,offsety] given the center coordinates
15 represented by [centerx,centery].
16
17 For filled circles, we can draw horizontal lines between each pair of
18 pixels with the same final value of y.
19
20 Two special cases exist and have been optimized:
21 1) offsetx == offsety (the final point), makes half the coordinates
22 equivalent, so we can omit them (and the corresponding fill lines)
23 2) offsetx == 0 (the starting point) means that some horizontal lines
24 would be a single pixel in length, so we write individual pixels instead.
25 This also makes half the symmetrical points identical to their twins,
26 so we only need four points or two points and one line
27 */
28
29 int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx);
30 int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx);
31 int16_t xpy = ((int16_t)centerx) + ((int16_t)offsety);
32 int16_t xmy = ((int16_t)centerx) - ((int16_t)offsety);
33 int16_t ypx = ((int16_t)centery) + ((int16_t)offsetx);
34 int16_t ymx = ((int16_t)centery) - ((int16_t)offsetx);
35 int16_t ypy = ((int16_t)centery) + ((int16_t)offsety);
36 int16_t ymy = ((int16_t)centery) - ((int16_t)offsety);
37
38 if (offsetx == 0) {
39 if (!qp_internal_setpixel_impl(device, centerx, ypy)) {
40 return false;
41 }
42 if (!qp_internal_setpixel_impl(device, centerx, ymy)) {
43 return false;
44 }
45 if (filled) {
46 if (!qp_internal_fillrect_helper_impl(device, xpy, centery, xmy, centery)) {
47 return false;
48 }
49 } else {
50 if (!qp_internal_setpixel_impl(device, xpy, centery)) {
51 return false;
52 }
53 if (!qp_internal_setpixel_impl(device, xmy, centery)) {
54 return false;
55 }
56 }
57 } else if (offsetx == offsety) {
58 if (filled) {
59 if (!qp_internal_fillrect_helper_impl(device, xpy, ypy, xmy, ypy)) {
60 return false;
61 }
62 if (!qp_internal_fillrect_helper_impl(device, xpy, ymy, xmy, ymy)) {
63 return false;
64 }
65 } else {
66 if (!qp_internal_setpixel_impl(device, xpy, ypy)) {
67 return false;
68 }
69 if (!qp_internal_setpixel_impl(device, xmy, ypy)) {
70 return false;
71 }
72 if (!qp_internal_setpixel_impl(device, xpy, ymy)) {
73 return false;
74 }
75 if (!qp_internal_setpixel_impl(device, xmy, ymy)) {
76 return false;
77 }
78 }
79
80 } else {
81 if (filled) {
82 if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) {
83 return false;
84 }
85 if (!qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) {
86 return false;
87 }
88 if (!qp_internal_fillrect_helper_impl(device, xpy, ypx, xmy, ypx)) {
89 return false;
90 }
91 if (!qp_internal_fillrect_helper_impl(device, xpy, ymx, xmy, ymx)) {
92 return false;
93 }
94 } else {
95 if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
96 return false;
97 }
98 if (!qp_internal_setpixel_impl(device, xmx, ypy)) {
99 return false;
100 }
101 if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
102 return false;
103 }
104 if (!qp_internal_setpixel_impl(device, xmx, ymy)) {
105 return false;
106 }
107 if (!qp_internal_setpixel_impl(device, xpy, ypx)) {
108 return false;
109 }
110 if (!qp_internal_setpixel_impl(device, xmy, ypx)) {
111 return false;
112 }
113 if (!qp_internal_setpixel_impl(device, xpy, ymx)) {
114 return false;
115 }
116 if (!qp_internal_setpixel_impl(device, xmy, ymx)) {
117 return false;
118 }
119 }
120 }
121
122 return true;
123}
124
125////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
126// Quantum Painter External API: qp_circle
127
128bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
129 qp_dprintf("qp_circle: entry\n");
130 struct painter_driver_t *driver = (struct painter_driver_t *)device;
131 if (!driver->validate_ok) {
132 qp_dprintf("qp_circle: fail (validation_ok == false)\n");
133 return false;
134 }
135
136 // plot the initial set of points for x, y and r
137 int16_t xcalc = 0;
138 int16_t ycalc = (int16_t)radius;
139 int16_t err = ((5 - (radius >> 2)) >> 2);
140
141 qp_internal_fill_pixdata(device, (radius * 2) + 1, hue, sat, val);
142
143 if (!qp_comms_start(device)) {
144 qp_dprintf("qp_circle: fail (could not start comms)\n");
145 return false;
146 }
147
148 bool ret = true;
149 if (!qp_circle_helper_impl(device, x, y, xcalc, ycalc, filled)) {
150 ret = false;
151 }
152
153 if (ret) {
154 while (xcalc < ycalc) {
155 xcalc++;
156 if (err < 0) {
157 err += (xcalc << 1) + 1;
158 } else {
159 ycalc--;
160 err += ((xcalc - ycalc) << 1) + 1;
161 }
162 if (!qp_circle_helper_impl(device, x, y, xcalc, ycalc, filled)) {
163 ret = false;
164 break;
165 }
166 }
167 }
168
169 qp_dprintf("qp_circle: %s\n", ret ? "ok" : "fail");
170 qp_comms_stop(device);
171 return ret;
172}
diff --git a/quantum/painter/qp_draw_codec.c b/quantum/painter/qp_draw_codec.c
new file mode 100644
index 0000000000..438dce3994
--- /dev/null
+++ b/quantum/painter/qp_draw_codec.c
@@ -0,0 +1,142 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "qp_internal.h"
5#include "qp_draw.h"
6#include "qp_comms.h"
7
8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9// Palette / Monochrome-format decoder
10
11static const qp_pixel_t qp_pixel_white = {.hsv888 = {.h = 0, .s = 0, .v = 255}};
12static const qp_pixel_t qp_pixel_black = {.hsv888 = {.h = 0, .s = 0, .v = 0}};
13
14bool qp_internal_bpp_capable(uint8_t bits_per_pixel) {
15#if !(QUANTUM_PAINTER_SUPPORTS_256_PALETTE)
16 if (bits_per_pixel > 4) {
17 qp_dprintf("qp_internal_decode_palette: image bpp greater than 4\n");
18 return false;
19 }
20#endif
21
22 if (bits_per_pixel > 8) {
23 qp_dprintf("qp_internal_decode_palette: image bpp greater than 8\n");
24 return false;
25 }
26
27 return true;
28}
29
30bool qp_internal_decode_palette(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t* palette, qp_internal_pixel_output_callback output_callback, void* output_arg) {
31 const uint8_t pixel_bitmask = (1 << bits_per_pixel) - 1;
32 const uint8_t pixels_per_byte = 8 / bits_per_pixel;
33 uint32_t remaining_pixels = pixel_count; // don't try to derive from byte_count, we may not use an entire byte
34 while (remaining_pixels > 0) {
35 uint8_t byteval = input_callback(input_arg);
36 if (byteval < 0) {
37 return false;
38 }
39 uint8_t loop_pixels = remaining_pixels < pixels_per_byte ? remaining_pixels : pixels_per_byte;
40 for (uint8_t q = 0; q < loop_pixels; ++q) {
41 if (!output_callback(palette, byteval & pixel_bitmask, output_arg)) {
42 return false;
43 }
44 byteval >>= bits_per_pixel;
45 }
46 remaining_pixels -= loop_pixels;
47 }
48 return true;
49}
50
51bool qp_internal_decode_grayscale(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_internal_pixel_output_callback output_callback, void* output_arg) {
52 return qp_internal_decode_recolor(device, pixel_count, bits_per_pixel, input_callback, input_arg, qp_pixel_white, qp_pixel_black, output_callback, output_arg);
53}
54
55bool qp_internal_decode_recolor(painter_device_t device, uint32_t pixel_count, uint8_t bits_per_pixel, qp_internal_byte_input_callback input_callback, void* input_arg, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qp_internal_pixel_output_callback output_callback, void* output_arg) {
56 struct painter_driver_t* driver = (struct painter_driver_t*)device;
57 int16_t steps = 1 << bits_per_pixel; // number of items we need to interpolate
58 if (qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, steps)) {
59 if (!driver->driver_vtable->palette_convert(device, steps, qp_internal_global_pixel_lookup_table)) {
60 return false;
61 }
62 }
63
64 return qp_internal_decode_palette(device, pixel_count, bits_per_pixel, input_callback, input_arg, qp_internal_global_pixel_lookup_table, output_callback, output_arg);
65}
66
67////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
68// Progressive pull of bytes, push of pixels
69
70static inline int16_t qp_drawimage_byte_uncompressed_decoder(void* cb_arg) {
71 struct qp_internal_byte_input_state* state = (struct qp_internal_byte_input_state*)cb_arg;
72 state->curr = qp_stream_get(state->src_stream);
73 return state->curr;
74}
75
76static inline int16_t qp_drawimage_byte_rle_decoder(void* cb_arg) {
77 struct qp_internal_byte_input_state* state = (struct qp_internal_byte_input_state*)cb_arg;
78
79 // Work out if we're parsing the initial marker byte
80 if (state->rle.mode == MARKER_BYTE) {
81 uint8_t c = qp_stream_get(state->src_stream);
82 if (c >= 128) {
83 state->rle.mode = NON_REPEATING_RUN; // non-repeated run
84 state->rle.remain = c - 127;
85 } else {
86 state->rle.mode = REPEATING_RUN; // repeated run
87 state->rle.remain = c;
88 }
89
90 state->curr = qp_stream_get(state->src_stream);
91 }
92
93 // Work out which byte we're returning
94 uint8_t c = state->curr;
95
96 // Decrement the counter of the bytes remaining
97 state->rle.remain--;
98
99 if (state->rle.remain > 0) {
100 // If we're in a non-repeating run, queue up the next byte
101 if (state->rle.mode == NON_REPEATING_RUN) {
102 state->curr = qp_stream_get(state->src_stream);
103 }
104 } else {
105 // Swap back to querying the marker byte mode
106 state->rle.mode = MARKER_BYTE;
107 }
108
109 return c;
110}
111
112bool qp_internal_pixel_appender(qp_pixel_t* palette, uint8_t index, void* cb_arg) {
113 struct qp_internal_pixel_output_state* state = (struct qp_internal_pixel_output_state*)cb_arg;
114 struct painter_driver_t* driver = (struct painter_driver_t*)state->device;
115
116 if (!driver->driver_vtable->append_pixels(state->device, qp_internal_global_pixdata_buffer, palette, state->pixel_write_pos++, 1, &index)) {
117 return false;
118 }
119
120 // If we've hit the transmit limit, send out the entire buffer and reset the write position
121 if (state->pixel_write_pos == state->max_pixels) {
122 if (!driver->driver_vtable->pixdata(state->device, qp_internal_global_pixdata_buffer, state->pixel_write_pos)) {
123 return false;
124 }
125 state->pixel_write_pos = 0;
126 }
127
128 return true;
129}
130
131qp_internal_byte_input_callback qp_internal_prepare_input_state(struct qp_internal_byte_input_state* input_state, painter_compression_t compression) {
132 switch (compression) {
133 case IMAGE_UNCOMPRESSED:
134 return qp_drawimage_byte_uncompressed_decoder;
135 case IMAGE_COMPRESSED_RLE:
136 input_state->rle.mode = MARKER_BYTE;
137 input_state->rle.remain = 0;
138 return qp_drawimage_byte_rle_decoder;
139 default:
140 return NULL;
141 }
142}
diff --git a/quantum/painter/qp_draw_core.c b/quantum/painter/qp_draw_core.c
new file mode 100644
index 0000000000..c31c734132
--- /dev/null
+++ b/quantum/painter/qp_draw_core.c
@@ -0,0 +1,294 @@
1// Copyright 2021-2022 Nick Brassel (@tzarc)
2// Copyright 2021 Paul Cotter (@gr1mr3aver)
3// SPDX-License-Identifier: GPL-2.0-or-later
4
5#include "qp_internal.h"
6#include "qp_comms.h"
7#include "qp_draw.h"
8#include "qgf.h"
9
10_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");
11
12////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
13// Global variables
14//
15// NOTE: The variables in this section are intentionally outside a stack frame. They are able to be defined with larger
16// sizes than the normal stack frames would allow, and as such need to be external.
17//
18// **** DO NOT refactor this and decide to place the variables inside the function calling them -- you will ****
19// **** very likely get artifacts rendered to the screen as a result. ****
20//
21
22// Buffer used for transmitting native pixel data to the downstream device.
23uint8_t qp_internal_global_pixdata_buffer[QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE];
24
25// Static buffer to contain a generated color palette
26static bool generated_palette = false;
27static int16_t generated_steps = -1;
28static qp_pixel_t interpolated_fg_hsv888;
29static qp_pixel_t interpolated_bg_hsv888;
30#if QUANTUM_PAINTER_SUPPORTS_256_PALETTE
31qp_pixel_t qp_internal_global_pixel_lookup_table[256];
32#else
33qp_pixel_t qp_internal_global_pixel_lookup_table[16];
34#endif
35
36////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
37// Helpers
38
39uint32_t qp_internal_num_pixels_in_buffer(painter_device_t device) {
40 struct painter_driver_t *driver = (struct painter_driver_t *)device;
41 return ((QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE * 8) / driver->native_bits_per_pixel);
42}
43
44// qp_setpixel internal implementation, but accepts a buffer with pre-converted native pixel. Only the first pixel is used.
45bool qp_internal_setpixel_impl(painter_device_t device, uint16_t x, uint16_t y) {
46 struct painter_driver_t *driver = (struct painter_driver_t *)device;
47 return driver->driver_vtable->viewport(device, x, y, x, y) && driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, 1);
48}
49
50// Fills the global native pixel buffer with equivalent pixels matching the supplied HSV
51void qp_internal_fill_pixdata(painter_device_t device, uint32_t num_pixels, uint8_t hue, uint8_t sat, uint8_t val) {
52 struct painter_driver_t *driver = (struct painter_driver_t *)device;
53 uint32_t pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
54 num_pixels = QP_MIN(pixels_in_pixdata, num_pixels);
55
56 // Convert the color to native pixel format
57 qp_pixel_t color = {.hsv888 = {.h = hue, .s = sat, .v = val}};
58 driver->driver_vtable->palette_convert(device, 1, &color);
59
60 // Append the required number of pixels
61 uint8_t palette_idx = 0;
62 for (uint32_t i = 0; i < num_pixels; ++i) {
63 driver->driver_vtable->append_pixels(device, qp_internal_global_pixdata_buffer, &color, i, 1, &palette_idx);
64 }
65}
66
67// 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.
68void qp_internal_invalidate_palette(void) {
69 generated_palette = false;
70 generated_steps = -1;
71}
72
73// Interpolates between two colors to generate a palette
74bool qp_internal_interpolate_palette(qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, int16_t steps) {
75 // Check if we need to generate a new palette -- if the input parameters match then assume the palette can stay unchanged.
76 // This may present a problem if using the same parameters but a different screen converts pixels -- use qp_internal_invalidate_palette() to reset.
77 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) {
78 // We already have the correct palette, no point regenerating it.
79 return false;
80 }
81
82 // Save the parameters so we know whether we can skip generation
83 generated_palette = true;
84 generated_steps = steps;
85 interpolated_fg_hsv888 = fg_hsv888;
86 interpolated_bg_hsv888 = bg_hsv888;
87
88 int16_t hue_fg = fg_hsv888.hsv888.h;
89 int16_t hue_bg = bg_hsv888.hsv888.h;
90
91 // Make sure we take the "shortest" route from one hue to the other
92 if ((hue_fg - hue_bg) >= 128) {
93 hue_bg += 256;
94 } else if ((hue_fg - hue_bg) <= -128) {
95 hue_bg -= 256;
96 }
97
98 // Interpolate each of the lookup table entries
99 for (int16_t i = 0; i < steps; ++i) {
100 qp_internal_global_pixel_lookup_table[i].hsv888.h = (uint8_t)((hue_fg - hue_bg) * i / (steps - 1) + hue_bg);
101 qp_internal_global_pixel_lookup_table[i].hsv888.s = (uint8_t)((fg_hsv888.hsv888.s - bg_hsv888.hsv888.s) * i / (steps - 1) + bg_hsv888.hsv888.s);
102 qp_internal_global_pixel_lookup_table[i].hsv888.v = (uint8_t)((fg_hsv888.hsv888.v - bg_hsv888.hsv888.v) * i / (steps - 1) + bg_hsv888.hsv888.v);
103
104 qp_dprintf("qp_internal_interpolate_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)steps, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v);
105 }
106
107 return true;
108}
109
110// Helper shared between image and font rendering -- sets up the global palette to match the palette block specified in the asset. Expects the stream to be positioned at the start of the block header.
111bool qp_internal_load_qgf_palette(qp_stream_t *stream, uint8_t bpp) {
112 qgf_palette_v1_t palette_descriptor;
113 if (qp_stream_read(&palette_descriptor, sizeof(qgf_palette_v1_t), 1, stream) != 1) {
114 qp_dprintf("Failed to read palette_descriptor, expected length was not %d\n", (int)sizeof(qgf_palette_v1_t));
115 return false;
116 }
117
118 // BPP determines the number of palette entries, each entry is a HSV888 triplet.
119 const uint16_t palette_entries = 1u << bpp;
120
121 // Ensure we aren't reusing any palette
122 qp_internal_invalidate_palette();
123
124 // Read the palette entries
125 for (uint16_t i = 0; i < palette_entries; ++i) {
126 // Read the palette entry
127 qgf_palette_entry_v1_t entry;
128 if (qp_stream_read(&entry, sizeof(qgf_palette_entry_v1_t), 1, stream) != 1) {
129 return false;
130 }
131
132 // Update the lookup table
133 qp_internal_global_pixel_lookup_table[i].hsv888.h = entry.h;
134 qp_internal_global_pixel_lookup_table[i].hsv888.s = entry.s;
135 qp_internal_global_pixel_lookup_table[i].hsv888.v = entry.v;
136
137 qp_dprintf("qp_internal_load_qgf_palette: %3d of %d -- H: %3d, S: %3d, V: %3d\n", (int)(i + 1), (int)palette_entries, (int)qp_internal_global_pixel_lookup_table[i].hsv888.h, (int)qp_internal_global_pixel_lookup_table[i].hsv888.s, (int)qp_internal_global_pixel_lookup_table[i].hsv888.v);
138 }
139
140 return true;
141}
142
143////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
144// Quantum Painter External API: qp_setpixel
145
146bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val) {
147 struct painter_driver_t *driver = (struct painter_driver_t *)device;
148 if (!driver->validate_ok) {
149 qp_dprintf("qp_setpixel: fail (validation_ok == false)\n");
150 return false;
151 }
152
153 if (!qp_comms_start(device)) {
154 qp_dprintf("Failed to start comms in qp_setpixel\n");
155 return false;
156 }
157
158 qp_internal_fill_pixdata(device, 1, hue, sat, val);
159 bool ret = qp_internal_setpixel_impl(device, x, y);
160 qp_comms_stop(device);
161 qp_dprintf("qp_setpixel: %s\n", ret ? "ok" : "fail");
162 return ret;
163}
164
165////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
166// Quantum Painter External API: qp_line
167
168bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val) {
169 if (x0 == x1 || y0 == y1) {
170 qp_dprintf("qp_line(%d, %d, %d, %d): entry (deferring to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1);
171 bool ret = qp_rect(device, x0, y0, x1, y1, hue, sat, val, true);
172 qp_dprintf("qp_line(%d, %d, %d, %d): %s (deferred to qp_rect)\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail");
173 return ret;
174 }
175
176 qp_dprintf("qp_line(%d, %d, %d, %d): entry\n", (int)x0, (int)y0, (int)x1, (int)y1);
177 struct painter_driver_t *driver = (struct painter_driver_t *)device;
178 if (!driver->validate_ok) {
179 qp_dprintf("qp_line: fail (validation_ok == false)\n");
180 return false;
181 }
182
183 if (!qp_comms_start(device)) {
184 qp_dprintf("Failed to start comms in qp_line\n");
185 return false;
186 }
187
188 qp_internal_fill_pixdata(device, 1, hue, sat, val);
189
190 // draw angled line using Bresenham's algo
191 int16_t x = ((int16_t)x0);
192 int16_t y = ((int16_t)y0);
193 int16_t slopex = ((int16_t)x0) < ((int16_t)x1) ? 1 : -1;
194 int16_t slopey = ((int16_t)y0) < ((int16_t)y1) ? 1 : -1;
195 int16_t dx = abs(((int16_t)x1) - ((int16_t)x0));
196 int16_t dy = -abs(((int16_t)y1) - ((int16_t)y0));
197
198 int16_t e = dx + dy;
199 int16_t e2 = 2 * e;
200
201 bool ret = true;
202 while (x != x1 || y != y1) {
203 if (!qp_internal_setpixel_impl(device, x, y)) {
204 ret = false;
205 break;
206 }
207 e2 = 2 * e;
208 if (e2 >= dy) {
209 e += dy;
210 x += slopex;
211 }
212 if (e2 <= dx) {
213 e += dx;
214 y += slopey;
215 }
216 }
217 // draw the last pixel
218 if (!qp_internal_setpixel_impl(device, x, y)) {
219 ret = false;
220 }
221
222 qp_comms_stop(device);
223 qp_dprintf("qp_line(%d, %d, %d, %d): %s\n", (int)x0, (int)y0, (int)x1, (int)y1, ret ? "ok" : "fail");
224 return ret;
225}
226
227////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
228// Quantum Painter External API: qp_rect
229
230bool qp_internal_fillrect_helper_impl(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
231 uint32_t pixels_in_pixdata = qp_internal_num_pixels_in_buffer(device);
232 struct painter_driver_t *driver = (struct painter_driver_t *)device;
233
234 uint16_t l = QP_MIN(left, right);
235 uint16_t r = QP_MAX(left, right);
236 uint16_t t = QP_MIN(top, bottom);
237 uint16_t b = QP_MAX(top, bottom);
238 uint16_t w = r - l + 1;
239 uint16_t h = b - t + 1;
240
241 uint32_t remaining = w * h;
242 driver->driver_vtable->viewport(device, l, t, r, b);
243 while (remaining > 0) {
244 uint32_t transmit = QP_MIN(remaining, pixels_in_pixdata);
245 if (!driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, transmit)) {
246 return false;
247 }
248 remaining -= transmit;
249 }
250 return true;
251}
252
253bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled) {
254 qp_dprintf("qp_rect(%d, %d, %d, %d): entry\n", (int)left, (int)top, (int)right, (int)bottom);
255 struct painter_driver_t *driver = (struct painter_driver_t *)device;
256 if (!driver->validate_ok) {
257 qp_dprintf("qp_rect: fail (validation_ok == false)\n");
258 return false;
259 }
260
261 // Cater for cases where people have submitted the coordinates backwards
262 uint16_t l = QP_MIN(left, right);
263 uint16_t r = QP_MAX(left, right);
264 uint16_t t = QP_MIN(top, bottom);
265 uint16_t b = QP_MAX(top, bottom);
266 uint16_t w = r - l + 1;
267 uint16_t h = b - t + 1;
268
269 bool ret = true;
270 if (!qp_comms_start(device)) {
271 qp_dprintf("Failed to start comms in qp_rect\n");
272 return false;
273 }
274
275 if (filled) {
276 // Fill up the pixdata buffer with the required number of native pixels
277 qp_internal_fill_pixdata(device, w * h, hue, sat, val);
278
279 // Perform the draw
280 ret = qp_internal_fillrect_helper_impl(device, l, t, r, b);
281 } else {
282 // Fill up the pixdata buffer with the required number of native pixels
283 qp_internal_fill_pixdata(device, QP_MAX(w, h), hue, sat, val);
284
285 // Draw 4x filled single-width rects to create an outline
286 if (!qp_internal_fillrect_helper_impl(device, l, t, r, t) || !qp_internal_fillrect_helper_impl(device, l, b, r, b) || !qp_internal_fillrect_helper_impl(device, l, t + 1, l, b - 1) || !qp_internal_fillrect_helper_impl(device, r, t + 1, r, b - 1)) {
287 ret = false;
288 }
289 }
290
291 qp_comms_stop(device);
292 qp_dprintf("qp_rect(%d, %d, %d, %d): %s\n", (int)l, (int)t, (int)r, (int)b, ret ? "ok" : "fail");
293 return ret;
294}
diff --git a/quantum/painter/qp_draw_ellipse.c b/quantum/painter/qp_draw_ellipse.c
new file mode 100644
index 0000000000..7f2f4abcfd
--- /dev/null
+++ b/quantum/painter/qp_draw_ellipse.c
@@ -0,0 +1,116 @@
1// Copyright 2021 Paul Cotter (@gr1mr3aver)
2// Copyright 2021 Nick Brassel (@tzarc)
3// SPDX-License-Identifier: GPL-2.0-or-later
4
5#include "qp_internal.h"
6#include "qp_comms.h"
7#include "qp_draw.h"
8
9// Utilize 4-way symmetry to draw an ellipse
10static bool qp_ellipse_helper_impl(painter_device_t device, uint16_t centerx, uint16_t centery, uint16_t offsetx, uint16_t offsety, bool filled) {
11 /*
12 Ellipses have the property of 4-way symmetry, so four pixels can be drawn
13 for each computed [offsetx,offsety] given the center coordinates
14 represented by [centerx,centery].
15
16 For filled ellipses, we can draw horizontal lines between each pair of
17 pixels with the same final value of y.
18
19 When offsetx == 0 only two pixels can be drawn for filled or unfilled ellipses
20 */
21
22 int16_t xpx = ((int16_t)centerx) + ((int16_t)offsetx);
23 int16_t xmx = ((int16_t)centerx) - ((int16_t)offsetx);
24 int16_t ypy = ((int16_t)centery) + ((int16_t)offsety);
25 int16_t ymy = ((int16_t)centery) - ((int16_t)offsety);
26
27 if (offsetx == 0) {
28 if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
29 return false;
30 }
31 if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
32 return false;
33 }
34 } else if (filled) {
35 if (!qp_internal_fillrect_helper_impl(device, xpx, ypy, xmx, ypy)) {
36 return false;
37 }
38 if (offsety > 0 && !qp_internal_fillrect_helper_impl(device, xpx, ymy, xmx, ymy)) {
39 return false;
40 }
41 } else {
42 if (!qp_internal_setpixel_impl(device, xpx, ypy)) {
43 return false;
44 }
45 if (!qp_internal_setpixel_impl(device, xpx, ymy)) {
46 return false;
47 }
48 if (!qp_internal_setpixel_impl(device, xmx, ypy)) {
49 return false;
50 }
51 if (!qp_internal_setpixel_impl(device, xmx, ymy)) {
52 return false;
53 }
54 }
55
56 return true;
57}
58
59////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
60// Quantum Painter External API: qp_ellipse
61
62bool 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) {
63 qp_dprintf("qp_ellipse: entry\n");
64 struct painter_driver_t *driver = (struct painter_driver_t *)device;
65 if (!driver->validate_ok) {
66 qp_dprintf("qp_ellipse: fail (validation_ok == false)\n");
67 return false;
68 }
69
70 int16_t aa = ((int16_t)sizex) * ((int16_t)sizex);
71 int16_t bb = ((int16_t)sizey) * ((int16_t)sizey);
72 int16_t fa = 4 * ((int16_t)aa);
73 int16_t fb = 4 * ((int16_t)bb);
74
75 int16_t dx = 0;
76 int16_t dy = ((int16_t)sizey);
77
78 qp_internal_fill_pixdata(device, QP_MAX(sizex, sizey), hue, sat, val);
79
80 if (!qp_comms_start(device)) {
81 qp_dprintf("qp_ellipse: fail (could not start comms)\n");
82 return false;
83 }
84
85 bool ret = true;
86 for (int16_t delta = (2 * bb) + (aa * (1 - (2 * sizey))); bb * dx <= aa * dy; dx++) {
87 if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) {
88 ret = false;
89 break;
90 }
91 if (delta >= 0) {
92 delta += fa * (1 - dy);
93 dy--;
94 }
95 delta += bb * (4 * dx + 6);
96 }
97
98 dx = sizex;
99 dy = 0;
100
101 for (int16_t delta = (2 * aa) + (bb * (1 - (2 * sizex))); aa * dy <= bb * dx; dy++) {
102 if (!qp_ellipse_helper_impl(device, x, y, dx, dy, filled)) {
103 ret = false;
104 break;
105 }
106 if (delta >= 0) {
107 delta += fb * (1 - dx);
108 dx--;
109 }
110 delta += aa * (4 * dy + 6);
111 }
112
113 qp_dprintf("qp_ellipse: %s\n", ret ? "ok" : "fail");
114 qp_comms_stop(device);
115 return ret;
116}
diff --git a/quantum/painter/qp_draw_image.c b/quantum/painter/qp_draw_image.c
new file mode 100644
index 0000000000..5134ae7e99
--- /dev/null
+++ b/quantum/painter/qp_draw_image.c
@@ -0,0 +1,382 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "qp_internal.h"
5#include "qp_draw.h"
6#include "qp_comms.h"
7#include "qgf.h"
8#include "deferred_exec.h"
9
10////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
11// QGF image handles
12
13typedef struct qgf_image_handle_t {
14 painter_image_desc_t base;
15 bool validate_ok;
16 union {
17 qp_stream_t stream;
18 qp_memory_stream_t mem_stream;
19#ifdef QP_STREAM_HAS_FILE_IO
20 qp_file_stream_t file_stream;
21#endif // QP_STREAM_HAS_FILE_IO
22 };
23} qgf_image_handle_t;
24
25static qgf_image_handle_t image_descriptors[QUANTUM_PAINTER_NUM_IMAGES] = {0};
26
27////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
28// Quantum Painter External API: qp_load_image_mem
29
30painter_image_handle_t qp_load_image_mem(const void *buffer) {
31 qp_dprintf("qp_load_image_mem: entry\n");
32 qgf_image_handle_t *image = NULL;
33
34 // Find a free slot
35 for (int i = 0; i < QUANTUM_PAINTER_NUM_IMAGES; ++i) {
36 if (!image_descriptors[i].validate_ok) {
37 image = &image_descriptors[i];
38 break;
39 }
40 }
41
42 // Drop out if not found
43 if (!image) {
44 qp_dprintf("qp_load_image_mem: fail (no free slot)\n");
45 return NULL;
46 }
47
48 // Assume we can read the graphics descriptor
49 image->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qgf_graphics_descriptor_v1_t));
50
51 // Update the length of the stream to match, and rewind to the start
52 image->mem_stream.length = qgf_get_total_size(&image->stream);
53 image->mem_stream.position = 0;
54
55 // Now that we know the length, validate the input data
56 if (!qgf_validate_stream(&image->stream)) {
57 qp_dprintf("qp_load_image_mem: fail (failed validation)\n");
58 return NULL;
59 }
60
61 // Fill out the QP image descriptor
62 qgf_read_graphics_descriptor(&image->stream, &image->base.width, &image->base.height, &image->base.frame_count, NULL);
63
64 // Validation success, we can return the handle
65 image->validate_ok = true;
66 qp_dprintf("qp_load_image_mem: ok\n");
67 return (painter_image_handle_t)image;
68}
69
70////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
71// Quantum Painter External API: qp_close_image
72
73bool qp_close_image(painter_image_handle_t image) {
74 qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
75 if (!qgf_image->validate_ok) {
76 qp_dprintf("qp_close_image: fail (invalid image)\n");
77 return false;
78 }
79
80 // Free up this image for use elsewhere.
81 qgf_image->validate_ok = false;
82 return true;
83}
84
85////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
86// Quantum Painter External API: qp_drawimage
87
88bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
89 return qp_drawimage_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
90}
91
92////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
93// Quantum Painter External API: qp_drawimage_recolor
94
95typedef struct qgf_frame_info_t {
96 painter_compression_t compression_scheme;
97 uint8_t bpp;
98 bool has_palette;
99 bool is_delta;
100 uint16_t left;
101 uint16_t top;
102 uint16_t right;
103 uint16_t bottom;
104 uint16_t delay;
105} qgf_frame_info_t;
106
107static bool qp_drawimage_prepare_frame_for_stream_read(painter_device_t device, qgf_image_handle_t *qgf_image, uint16_t frame_number, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qgf_frame_info_t *info) {
108 struct painter_driver_t *driver = (struct painter_driver_t *)device;
109
110 // Drop out if we can't actually place the data we read out anywhere
111 if (!info) {
112 qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n");
113 return false;
114 }
115
116 // Seek to the frame
117 qgf_seek_to_frame_descriptor(&qgf_image->stream, frame_number);
118
119 // Read the frame descriptor
120 qgf_frame_v1_t frame_descriptor;
121 if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, &qgf_image->stream) != 1) {
122 qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t));
123 return false;
124 }
125
126 // Parse out the frame info
127 if (!qgf_parse_frame_descriptor(&frame_descriptor, &info->bpp, &info->has_palette, &info->is_delta, &info->compression_scheme, &info->delay)) {
128 return false;
129 }
130
131 // Ensure we aren't reusing any palette
132 qp_internal_invalidate_palette();
133
134 // Handle palette if needed
135 const uint16_t palette_entries = 1u << info->bpp;
136 bool needs_pixconvert = false;
137 if (info->has_palette) {
138 // Load the palette from the stream
139 if (!qp_internal_load_qgf_palette((qp_stream_t *)&qgf_image->stream, info->bpp)) {
140 return false;
141 }
142
143 needs_pixconvert = true;
144 } else {
145 // Interpolate from fg/bg
146 needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries);
147 }
148
149 if (!qp_internal_bpp_capable(info->bpp)) {
150 qp_dprintf("qp_drawimage_recolor: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\n", (int)info->bpp);
151 qp_comms_stop(device);
152 return false;
153 }
154
155 if (needs_pixconvert) {
156 // Convert the palette to native format
157 if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) {
158 qp_dprintf("qp_drawimage_recolor: fail (could not convert pixels to native)\n");
159 qp_comms_stop(device);
160 return false;
161 }
162 }
163
164 // Handle delta if needed
165 if (info->is_delta) {
166 qgf_delta_v1_t delta_descriptor;
167 if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, &qgf_image->stream) != 1) {
168 qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t));
169 return false;
170 }
171
172 info->left = delta_descriptor.left;
173 info->top = delta_descriptor.top;
174 info->right = delta_descriptor.right;
175 info->bottom = delta_descriptor.bottom;
176 }
177
178 // Read the data block
179 qgf_data_v1_t data_descriptor;
180 if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, &qgf_image->stream) != 1) {
181 qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t));
182 return false;
183 }
184
185 // Stream is now at the point of being able to read pixdata
186 return true;
187}
188
189static bool qp_drawimage_recolor_impl(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, int frame_number, qgf_frame_info_t *frame_info, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888) {
190 qp_dprintf("qp_drawimage_recolor: entry\n");
191 struct painter_driver_t *driver = (struct painter_driver_t *)device;
192 if (!driver->validate_ok) {
193 qp_dprintf("qp_drawimage_recolor: fail (validation_ok == false)\n");
194 return false;
195 }
196
197 qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
198 if (!qgf_image->validate_ok) {
199 qp_dprintf("qp_drawimage_recolor: fail (invalid image)\n");
200 return false;
201 }
202
203 // Read the frame info
204 if (!qp_drawimage_prepare_frame_for_stream_read(device, qgf_image, frame_number, fg_hsv888, bg_hsv888, frame_info)) {
205 qp_dprintf("qp_drawimage_recolor: fail (could not read frame %d)\n", frame_number);
206 return false;
207 }
208
209 if (!qp_comms_start(device)) {
210 qp_dprintf("qp_drawimage_recolor: fail (could not start comms)\n");
211 return false;
212 }
213
214 uint16_t l, t, r, b;
215 if (frame_info->is_delta) {
216 l = x + frame_info->left;
217 t = y + frame_info->top;
218 r = x + frame_info->right - 1;
219 b = y + frame_info->bottom - 1;
220 } else {
221 l = x;
222 t = y;
223 r = x + image->width - 1;
224 b = y + image->height - 1;
225 }
226 uint32_t pixel_count = ((uint32_t)(r - l + 1)) * (b - t + 1);
227
228 // Configure where we're going to be rendering to
229 if (!driver->driver_vtable->viewport(device, l, t, r, b)) {
230 qp_dprintf("qp_drawimage_recolor: fail (could not set viewport)\n");
231 qp_comms_stop(device);
232 return false;
233 }
234
235 // Set up the input state
236 struct qp_internal_byte_input_state input_state = {.device = device, .src_stream = &qgf_image->stream};
237 qp_internal_byte_input_callback input_callback = qp_internal_prepare_input_state(&input_state, frame_info->compression_scheme);
238 if (input_callback == NULL) {
239 qp_dprintf("qp_drawimage_recolor: fail (invalid image compression scheme)\n");
240 qp_comms_stop(device);
241 return false;
242 }
243
244 // Set up the output state
245 struct qp_internal_pixel_output_state output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)};
246
247 // Decode the pixel data and stream to the display
248 bool ret = qp_internal_decode_palette(device, pixel_count, frame_info->bpp, input_callback, &input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, &output_state);
249
250 // Any leftovers need transmission as well.
251 if (ret && output_state.pixel_write_pos > 0) {
252 ret &= driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, output_state.pixel_write_pos);
253 }
254
255 qp_dprintf("qp_drawimage_recolor: %s\n", ret ? "ok" : "fail");
256 qp_comms_stop(device);
257 return ret;
258}
259
260bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
261 qgf_frame_info_t frame_info = {0};
262 qp_pixel_t fg_hsv888 = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
263 qp_pixel_t bg_hsv888 = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
264 return qp_drawimage_recolor_impl(device, x, y, image, 0, &frame_info, fg_hsv888, bg_hsv888);
265}
266
267////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
268// Quantum Painter External API: qp_animate
269
270deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
271 return qp_animate_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
272}
273
274////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
275// Quantum Painter External API: qp_animate_recolor
276
277typedef struct animation_state_t {
278 painter_device_t device;
279 uint16_t x;
280 uint16_t y;
281 painter_image_handle_t image;
282 qp_pixel_t fg_hsv888;
283 qp_pixel_t bg_hsv888;
284 uint16_t frame_number;
285 deferred_token defer_token;
286} animation_state_t;
287
288static deferred_executor_t animation_executors[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
289static animation_state_t animation_states[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
290
291static deferred_token qp_render_animation_state(animation_state_t *state, uint16_t *delay_ms) {
292 qgf_frame_info_t frame_info = {0};
293 qp_dprintf("qp_render_animation_state: entry (frame #%d)\n", (int)state->frame_number);
294 bool ret = qp_drawimage_recolor_impl(state->device, state->x, state->y, state->image, state->frame_number, &frame_info, state->fg_hsv888, state->bg_hsv888);
295 if (ret) {
296 ++state->frame_number;
297 if (state->frame_number >= state->image->frame_count) {
298 state->frame_number = 0;
299 }
300 *delay_ms = frame_info.delay;
301 }
302 qp_dprintf("qp_render_animation_state: %s (delay %dms)\n", ret ? "ok" : "fail", (int)(*delay_ms));
303 return ret;
304}
305
306static uint32_t animation_callback(uint32_t trigger_time, void *cb_arg) {
307 animation_state_t *state = (animation_state_t *)cb_arg;
308 uint16_t delay_ms;
309 bool ret = qp_render_animation_state(state, &delay_ms);
310 if (!ret) {
311 // Setting the device to NULL clears the animation slot
312 state->device = NULL;
313 }
314 // If we're successful, keep animating -- returning 0 cancels the deferred execution
315 return ret ? delay_ms : 0;
316}
317
318deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
319 qp_dprintf("qp_animate_recolor: entry\n");
320
321 animation_state_t *anim_state = NULL;
322 for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
323 if (animation_states[i].device == NULL) {
324 anim_state = &animation_states[i];
325 break;
326 }
327 }
328
329 if (!anim_state) {
330 qp_dprintf("qp_animate_recolor: fail (could not find free animation slot)\n");
331 return INVALID_DEFERRED_TOKEN;
332 }
333
334 // Prepare the animation state
335 anim_state->device = device;
336 anim_state->x = x;
337 anim_state->y = y;
338 anim_state->image = image;
339 anim_state->fg_hsv888 = (qp_pixel_t){.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
340 anim_state->bg_hsv888 = (qp_pixel_t){.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
341 anim_state->frame_number = 0;
342
343 // Draw the first frame
344 uint16_t delay_ms;
345 if (!qp_render_animation_state(anim_state, &delay_ms)) {
346 anim_state->device = NULL; // disregard the allocated animation slot
347 qp_dprintf("qp_animate_recolor: fail (could not render first frame)\n");
348 return INVALID_DEFERRED_TOKEN;
349 }
350
351 // Set up the timer
352 anim_state->defer_token = defer_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, delay_ms, animation_callback, anim_state);
353 if (anim_state->defer_token == INVALID_DEFERRED_TOKEN) {
354 anim_state->device = NULL; // disregard the allocated animation slot
355 qp_dprintf("qp_animate_recolor: fail (could not set up animation executor)\n");
356 return INVALID_DEFERRED_TOKEN;
357 }
358
359 qp_dprintf("qp_animate_recolor: ok (deferred token = %d)\n", (int)anim_state->defer_token);
360 return anim_state->defer_token;
361}
362
363////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
364// Quantum Painter External API: qp_stop_animation
365
366void qp_stop_animation(deferred_token anim_token) {
367 for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
368 if (animation_states[i].defer_token == anim_token) {
369 cancel_deferred_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, anim_token);
370 animation_states[i].device = NULL;
371 return;
372 }
373 }
374}
375
376////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
377// Quantum Painter Core API: qp_internal_animation_tick
378
379void qp_internal_animation_tick(void) {
380 static uint32_t last_anim_exec = 0;
381 deferred_exec_advanced_task(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, &last_anim_exec);
382}
diff --git a/quantum/painter/qp_draw_text.c b/quantum/painter/qp_draw_text.c
new file mode 100644
index 0000000000..f99e082cad
--- /dev/null
+++ b/quantum/painter/qp_draw_text.c
@@ -0,0 +1,444 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include <quantum.h>
5#include <utf8.h>
6
7#include "qp_internal.h"
8#include "qp_draw.h"
9#include "qp_comms.h"
10#include "qff.h"
11
12////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
13// QFF font handles
14
15typedef struct qff_font_handle_t {
16 painter_font_desc_t base;
17 bool validate_ok;
18 bool has_ascii_table;
19 uint16_t num_unicode_glyphs;
20 uint8_t bpp;
21 bool has_palette;
22 painter_compression_t compression_scheme;
23 union {
24 qp_stream_t stream;
25 qp_memory_stream_t mem_stream;
26#ifdef QP_STREAM_HAS_FILE_IO
27 qp_file_stream_t file_stream;
28#endif // QP_STREAM_HAS_FILE_IO
29 };
30#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
31 bool owns_buffer;
32 void *buffer;
33#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
34} qff_font_handle_t;
35
36static qff_font_handle_t font_descriptors[QUANTUM_PAINTER_NUM_FONTS] = {0};
37
38////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
39// Quantum Painter External API: qp_load_font_mem
40
41painter_font_handle_t qp_load_font_mem(const void *buffer) {
42 qp_dprintf("qp_load_font_mem: entry\n");
43 qff_font_handle_t *font = NULL;
44
45 // Find a free slot
46 for (int i = 0; i < QUANTUM_PAINTER_NUM_FONTS; ++i) {
47 if (!font_descriptors[i].validate_ok) {
48 font = &font_descriptors[i];
49 break;
50 }
51 }
52
53 // Drop out if not found
54 if (!font) {
55 qp_dprintf("qp_load_font_mem: fail (no free slot)\n");
56 return NULL;
57 }
58
59 // Assume we can read the graphics descriptor
60 font->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qff_font_descriptor_v1_t));
61
62 // Update the length of the stream to match, and rewind to the start
63 font->mem_stream.length = qff_get_total_size(&font->stream);
64 font->mem_stream.position = 0;
65
66 // Now that we know the length, validate the input data
67 if (!qff_validate_stream(&font->stream)) {
68 qp_dprintf("qp_load_font_mem: fail (failed validation)\n");
69 return NULL;
70 }
71
72#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
73 // Clear out any existing data
74 font->owns_buffer = false;
75 font->buffer = NULL;
76
77 void *ram_buffer = malloc(font->mem_stream.length);
78 if (ram_buffer == NULL) {
79 qp_dprintf("qp_load_font_mem: could not allocate enough RAM for font, falling back to original\n");
80 } else {
81 do {
82 // Copy the data into RAM
83 if (qp_stream_read(ram_buffer, 1, font->mem_stream.length, &font->mem_stream) != font->mem_stream.length) {
84 qp_dprintf("qp_load_font_mem: could not copy from flash to RAM, falling back to original\n");
85 break;
86 }
87
88 // Create the new stream with the new buffer
89 font->buffer = ram_buffer;
90 font->owns_buffer = true;
91 font->mem_stream = qp_make_memory_stream(font->buffer, font->mem_stream.length);
92 } while (0);
93 }
94
95 // Free the buffer if we were unable to recreate the RAM copy.
96 if (ram_buffer != NULL && !font->owns_buffer) {
97 free(ram_buffer);
98 }
99#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
100
101 // Read the info (parsing already successful above, no need to check return value)
102 qff_read_font_descriptor(&font->stream, &font->base.line_height, &font->has_ascii_table, &font->num_unicode_glyphs, &font->bpp, &font->has_palette, &font->compression_scheme, NULL);
103
104 if (!qp_internal_bpp_capable(font->bpp)) {
105 qp_dprintf("qp_load_font_mem: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\n", (int)font->bpp);
106 qp_close_font((painter_font_handle_t)font);
107 return NULL;
108 }
109
110 // Validation success, we can return the handle
111 font->validate_ok = true;
112 qp_dprintf("qp_load_font_mem: ok\n");
113 return (painter_font_handle_t)font;
114}
115
116////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
117// Quantum Painter External API: qp_close_font
118
119bool qp_close_font(painter_font_handle_t font) {
120 qff_font_handle_t *qff_font = (qff_font_handle_t *)font;
121 if (!qff_font->validate_ok) {
122 qp_dprintf("qp_close_font: fail (invalid font)\n");
123 return false;
124 }
125
126#if QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
127 // Nuke the buffer, if required
128 if (qff_font->owns_buffer) {
129 free(qff_font->buffer);
130 qff_font->buffer = NULL;
131 qff_font->owns_buffer = false;
132 }
133#endif // QUANTUM_PAINTER_LOAD_FONTS_TO_RAM
134
135 // Free up this font for use elsewhere.
136 qff_font->validate_ok = false;
137 return true;
138}
139
140////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
141// Helpers
142
143// Callback to be invoked for each codepoint detected in the UTF8 input string
144typedef bool (*code_point_handler)(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg);
145
146// Helper that sets up the palette (if required) and returns the offset in the stream that the data starts
147static inline bool qp_drawtext_prepare_font_for_render(painter_device_t device, qff_font_handle_t *qff_font, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, uint32_t *data_offset) {
148 struct painter_driver_t *driver = (struct painter_driver_t *)device;
149
150 // Drop out if we can't actually place the data we read out anywhere
151 if (!data_offset) {
152 qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n");
153 return false;
154 }
155
156 // Work out where we're reading from
157 uint32_t offset = sizeof(qff_font_descriptor_v1_t);
158 if (qff_font->has_ascii_table) {
159 offset += sizeof(qff_ascii_glyph_table_v1_t);
160 }
161 if (qff_font->num_unicode_glyphs > 0) {
162 offset += sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * 6);
163 }
164
165 // Handle palette if needed
166 const uint16_t palette_entries = 1u << qff_font->bpp;
167 bool needs_pixconvert = false;
168 if (qff_font->has_palette) {
169 // If this font has a palette, we need to read it out and set up the pixel lookup table
170 qp_stream_setpos(&qff_font->stream, offset);
171 if (!qp_internal_load_qgf_palette(&qff_font->stream, qff_font->bpp)) {
172 return false;
173 }
174
175 // Skip this block, as far as offset calculations go
176 offset += sizeof(qgf_palette_v1_t) + (palette_entries * 3);
177 needs_pixconvert = true;
178 } else {
179 // Interpolate from fg/bg
180 int16_t palette_entries = 1 << qff_font->bpp;
181 needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries);
182 }
183
184 if (needs_pixconvert) {
185 // Convert the palette to native format
186 if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) {
187 qp_dprintf("qp_drawtext_recolor: fail (could not convert pixels to native)\n");
188 qp_comms_stop(device);
189 return false;
190 }
191 }
192
193 *data_offset = offset;
194 return true;
195}
196
197static inline bool qp_drawtext_prepare_glyph_for_render(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t *width) {
198 if (code_point >= 0x20 && code_point < 0x7F && qff_font->has_ascii_table) {
199 // Do ascii table
200 qff_ascii_glyph_v1_t glyph_info;
201 uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor
202 + sizeof(qgf_block_header_v1_t) // Skip the ascii table header
203 + (code_point - 0x20) * sizeof(qff_ascii_glyph_v1_t); // Jump direct to the data offset based on the glyph index
204 if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) {
205 qp_dprintf("Failed to set stream position while reading ascii glyph info\n");
206 return false;
207 }
208
209 if (qp_stream_read(&glyph_info, sizeof(qff_ascii_glyph_v1_t), 1, &qff_font->stream) != 1) {
210 qp_dprintf("Failed to read glyph info\n");
211 return false;
212 }
213
214 uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK);
215 uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS);
216 uint32_t data_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor
217 + sizeof(qff_ascii_glyph_table_v1_t) // Skip the ascii table
218 + (qff_font->num_unicode_glyphs > 0 ? (sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t))) : 0) // Skip the unicode table
219 + (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0) // Skip the palette
220 + sizeof(qgf_block_header_v1_t) // Skip the data block header
221 + glyph_offset; // Jump to the specified glyph offset
222
223 if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) {
224 qp_dprintf("Failed to set stream position while preparing ascii glyph data\n");
225 return false;
226 }
227
228 *width = glyph_width;
229 return true;
230 } else {
231 // Do unicode table, which may include singular ascii glyphs if full ascii table isn't specified
232 uint32_t glyph_info_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor
233 + (qff_font->has_ascii_table ? sizeof(qff_ascii_glyph_table_v1_t) : 0) // Skip the ascii table
234 + sizeof(qgf_block_header_v1_t); // Skip the unicode block header
235
236 if (qp_stream_setpos(&qff_font->stream, glyph_info_offset) < 0) {
237 qp_dprintf("Failed to set stream position while preparing glyph data\n");
238 return false;
239 }
240
241 qff_unicode_glyph_v1_t glyph_info;
242 for (uint16_t i = 0; i < qff_font->num_unicode_glyphs; ++i) {
243 if (qp_stream_read(&glyph_info, sizeof(qff_unicode_glyph_v1_t), 1, &qff_font->stream) != 1) {
244 qp_dprintf("Failed to set stream position while reading unicode glyph info\n");
245 return false;
246 }
247
248 if (glyph_info.code_point == code_point) {
249 uint8_t glyph_width = (uint8_t)(glyph_info.value & QFF_GLYPH_WIDTH_MASK);
250 uint32_t glyph_offset = ((glyph_info.value & QFF_GLYPH_OFFSET_MASK) >> QFF_GLYPH_WIDTH_BITS);
251 uint32_t data_offset = sizeof(qff_font_descriptor_v1_t) // Skip the font descriptor
252 + sizeof(qff_ascii_glyph_table_v1_t) // Skip the ascii table
253 + (qff_font->num_unicode_glyphs > 0 ? (sizeof(qff_unicode_glyph_table_v1_t) + (qff_font->num_unicode_glyphs * sizeof(qff_unicode_glyph_v1_t))) : 0) // Skip the unicode table
254 + (qff_font->has_palette ? (sizeof(qgf_palette_v1_t) + ((1 << qff_font->bpp) * sizeof(qgf_palette_entry_v1_t))) : 0) // Skip the palette
255 + sizeof(qgf_block_header_v1_t) // Skip the data block header
256 + glyph_offset; // Jump to the specified glyph offset
257
258 if (qp_stream_setpos(&qff_font->stream, data_offset) < 0) {
259 qp_dprintf("Failed to set stream position while preparing unicode glyph data\n");
260 return false;
261 }
262
263 *width = glyph_width;
264 return true;
265 }
266 }
267
268 // Not found
269 qp_dprintf("Failed to find unicode glyph info\n");
270 return false;
271 }
272 return false;
273}
274
275// Function to iterate over each UTF8 codepoint, invoking the callback for each decoded glyph
276static inline bool qp_iterate_code_points(qff_font_handle_t *qff_font, const char *str, code_point_handler handler, void *cb_arg) {
277 while (*str) {
278 int32_t code_point = 0;
279 str = decode_utf8(str, &code_point);
280 if (code_point < 0) {
281 qp_dprintf("Invalid unicode code point decoded. Cannot render.\n");
282 return false;
283 }
284
285 uint8_t width;
286 if (!qp_drawtext_prepare_glyph_for_render(qff_font, code_point, &width)) {
287 qp_dprintf("Failed to prepare glyph for rendering.\n");
288 return false;
289 }
290
291 if (!handler(qff_font, code_point, width, qff_font->base.line_height, cb_arg)) {
292 qp_dprintf("Failed to execute glyph handler.\n");
293 return false;
294 }
295 }
296 return true;
297}
298
299////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
300// String width calculation
301
302// Callback state
303struct code_point_iter_calcwidth_state {
304 int16_t width;
305};
306
307// Codepoint handler callback: width calc
308static inline bool qp_font_code_point_handler_calcwidth(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg) {
309 struct code_point_iter_calcwidth_state *state = (struct code_point_iter_calcwidth_state *)cb_arg;
310
311 // Increment the overall width by this glyph's width
312 state->width += width;
313
314 return true;
315}
316
317////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
318// String drawing implementation
319
320// Callback state
321struct code_point_iter_drawglyph_state {
322 painter_device_t device;
323 int16_t xpos;
324 int16_t ypos;
325 qp_internal_byte_input_callback input_callback;
326 struct qp_internal_byte_input_state * input_state;
327 struct qp_internal_pixel_output_state *output_state;
328};
329
330// Codepoint handler callback: drawing
331static inline bool qp_font_code_point_handler_drawglyph(qff_font_handle_t *qff_font, uint32_t code_point, uint8_t width, uint8_t height, void *cb_arg) {
332 struct code_point_iter_drawglyph_state *state = (struct code_point_iter_drawglyph_state *)cb_arg;
333 struct painter_driver_t * driver = (struct painter_driver_t *)state->device;
334
335 // Reset the input state's RLE mode -- the stream should already be correctly positioned by qp_iterate_code_points()
336 state->input_state->rle.mode = MARKER_BYTE; // ignored if not using RLE
337
338 // Reset the output state
339 state->output_state->pixel_write_pos = 0;
340
341 // Configure where we're going to be rendering to
342 driver->driver_vtable->viewport(state->device, state->xpos, state->ypos, state->xpos + width - 1, state->ypos + height - 1);
343
344 // Move the x-position for the next glyph
345 state->xpos += width;
346
347 // Decode the pixel data for the glyph
348 uint32_t pixel_count = ((uint32_t)width) * height;
349 bool ret = qp_internal_decode_palette(state->device, pixel_count, qff_font->bpp, state->input_callback, state->input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, state->output_state);
350
351 // Any leftovers need transmission as well.
352 if (ret && state->output_state->pixel_write_pos > 0) {
353 ret &= driver->driver_vtable->pixdata(state->device, qp_internal_global_pixdata_buffer, state->output_state->pixel_write_pos);
354 }
355
356 return ret;
357}
358
359////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
360// Quantum Painter External API: qp_textwidth
361
362int16_t qp_textwidth(painter_font_handle_t font, const char *str) {
363 qff_font_handle_t *qff_font = (qff_font_handle_t *)font;
364 if (!qff_font->validate_ok) {
365 qp_dprintf("qp_textwidth: fail (invalid font)\n");
366 return false;
367 }
368
369 // Create the codepoint iterator state
370 struct code_point_iter_calcwidth_state state = {.width = 0};
371 // Iterate each codepoint, return the calculated width if successful.
372 return qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_calcwidth, &state) ? state.width : 0;
373}
374
375////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
376// Quantum Painter External API: qp_drawtext
377
378int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str) {
379 // Offload to the recolor variant, substituting fg=white bg=black.
380 // Traditional LCDs with those colors will need to manually invoke qp_drawtext_recolor with the colors reversed.
381 return qp_drawtext_recolor(device, x, y, font, str, 0, 0, 255, 0, 0, 0);
382}
383
384////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
385// Quantum Painter External API: qp_drawtext_recolor
386
387int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
388 qp_dprintf("qp_drawtext_recolor: entry\n");
389 struct painter_driver_t *driver = (struct painter_driver_t *)device;
390 if (!driver->validate_ok) {
391 qp_dprintf("qp_drawtext_recolor: fail (validation_ok == false)\n");
392 return 0;
393 }
394
395 qff_font_handle_t *qff_font = (qff_font_handle_t *)font;
396 if (!qff_font->validate_ok) {
397 qp_dprintf("qp_drawtext_recolor: fail (invalid font)\n");
398 return false;
399 }
400
401 if (!qp_comms_start(device)) {
402 qp_dprintf("qp_drawtext_recolor: fail (could not start comms)\n");
403 return 0;
404 }
405
406 // Set up the byte input state and input callback
407 struct qp_internal_byte_input_state input_state = {.device = device, .src_stream = &qff_font->stream};
408 qp_internal_byte_input_callback input_callback = qp_internal_prepare_input_state(&input_state, qff_font->compression_scheme);
409 if (input_callback == NULL) {
410 qp_dprintf("qp_drawtext_recolor: fail (invalid font compression scheme)\n");
411 qp_comms_stop(device);
412 return false;
413 }
414
415 // Set up the pixel output state
416 struct qp_internal_pixel_output_state output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)};
417
418 // Set up the codepoint iteration state
419 struct code_point_iter_drawglyph_state state = {// Common
420 .device = device,
421 .xpos = x,
422 .ypos = y,
423 // Input
424 .input_callback = input_callback,
425 .input_state = &input_state,
426 // Output
427 .output_state = &output_state};
428
429 qp_pixel_t fg_hsv888 = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
430 qp_pixel_t bg_hsv888 = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
431 uint32_t data_offset;
432 if (!qp_drawtext_prepare_font_for_render(driver, qff_font, fg_hsv888, bg_hsv888, &data_offset)) {
433 qp_dprintf("qp_drawtext_recolor: fail (failed to prepare font for rendering)\n");
434 qp_comms_stop(device);
435 return false;
436 }
437
438 // Iterate the codepoints with the drawglyph callback
439 bool ret = qp_iterate_code_points(qff_font, str, qp_font_code_point_handler_drawglyph, &state);
440
441 qp_dprintf("qp_drawtext_recolor: %s\n", ret ? "ok" : "fail");
442 qp_comms_stop(device);
443 return ret ? (state.xpos - x) : 0;
444}
diff --git a/quantum/painter/qp_internal.h b/quantum/painter/qp_internal.h
new file mode 100644
index 0000000000..e7a6d113c5
--- /dev/null
+++ b/quantum/painter/qp_internal.h
@@ -0,0 +1,33 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "quantum.h"
7#include "qp.h"
8
9////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
10// Helpers
11
12// Mark certain types that there should be no padding bytes between members.
13#define QP_PACKED __attribute__((packed))
14
15// Min/max defines
16#define QP_MIN(X, Y) (((X) < (Y)) ? (X) : (Y))
17#define QP_MAX(X, Y) (((X) > (Y)) ? (X) : (Y))
18
19#ifdef QUANTUM_PAINTER_DEBUG
20# include <debug.h>
21# include <print.h>
22# define qp_dprintf(...) dprintf(__VA_ARGS__)
23#else
24# define qp_dprintf(...) \
25 do { \
26 } while (0)
27#endif
28
29////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
30// Specific internal definitions
31
32#include <qp_internal_formats.h>
33#include <qp_internal_driver.h>
diff --git a/quantum/painter/qp_internal_driver.h b/quantum/painter/qp_internal_driver.h
new file mode 100644
index 0000000000..9e9d6bc848
--- /dev/null
+++ b/quantum/painter/qp_internal_driver.h
@@ -0,0 +1,82 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "qp_internal.h"
7
8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9// Driver callbacks
10
11typedef bool (*painter_driver_init_func)(painter_device_t device, painter_rotation_t rotation);
12typedef bool (*painter_driver_power_func)(painter_device_t device, bool power_on);
13typedef bool (*painter_driver_clear_func)(painter_device_t device);
14typedef bool (*painter_driver_flush_func)(painter_device_t device);
15typedef bool (*painter_driver_viewport_func)(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
16typedef bool (*painter_driver_pixdata_func)(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
17typedef bool (*painter_driver_convert_palette_func)(painter_device_t device, int16_t palette_size, qp_pixel_t *palette);
18typedef bool (*painter_driver_append_pixels)(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices);
19
20// Driver vtable definition
21struct painter_driver_vtable_t {
22 painter_driver_init_func init;
23 painter_driver_power_func power;
24 painter_driver_clear_func clear;
25 painter_driver_flush_func flush;
26 painter_driver_viewport_func viewport;
27 painter_driver_pixdata_func pixdata;
28 painter_driver_convert_palette_func palette_convert;
29 painter_driver_append_pixels append_pixels;
30};
31
32////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
33// Comms callbacks
34
35typedef bool (*painter_driver_comms_init_func)(painter_device_t device);
36typedef bool (*painter_driver_comms_start_func)(painter_device_t device);
37typedef void (*painter_driver_comms_stop_func)(painter_device_t device);
38typedef uint32_t (*painter_driver_comms_send_func)(painter_device_t device, const void *data, uint32_t byte_count);
39
40struct painter_comms_vtable_t {
41 painter_driver_comms_init_func comms_init;
42 painter_driver_comms_start_func comms_start;
43 painter_driver_comms_stop_func comms_stop;
44 painter_driver_comms_send_func comms_send;
45};
46
47typedef void (*painter_driver_comms_send_command_func)(painter_device_t device, uint8_t cmd);
48typedef void (*painter_driver_comms_bulk_command_sequence)(painter_device_t device, const uint8_t *sequence, size_t sequence_len);
49
50struct painter_comms_with_command_vtable_t {
51 struct painter_comms_vtable_t base; // must be first, so this object can be cast from the painter_comms_vtable_t* type
52 painter_driver_comms_send_command_func send_command;
53 painter_driver_comms_bulk_command_sequence bulk_command_sequence;
54};
55
56////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
57// Driver base definition
58
59struct painter_driver_t {
60 const struct painter_driver_vtable_t *driver_vtable;
61 const struct painter_comms_vtable_t * comms_vtable;
62
63 // Flag signifying if validation was successful
64 bool validate_ok;
65
66 // Panel geometry
67 uint16_t panel_width;
68 uint16_t panel_height;
69
70 // Target drawing rotation
71 painter_rotation_t rotation;
72
73 // Automated offsets for setting viewport
74 uint16_t offset_x;
75 uint16_t offset_y;
76
77 // Number of bits per pixel, used for determining how many pixels can be sent during a transmission of the pixdata buffer
78 uint8_t native_bits_per_pixel;
79
80 // Comms config pointer -- needs to point to an appropriate comms config if the comms driver requires it.
81 void *comms_config;
82};
diff --git a/quantum/painter/qp_internal_formats.h b/quantum/painter/qp_internal_formats.h
new file mode 100644
index 0000000000..a4a86f0345
--- /dev/null
+++ b/quantum/painter/qp_internal_formats.h
@@ -0,0 +1,49 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include "qp_internal.h"
7
8////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
9// Quantum Painter pixel formats
10
11// Datatype containing a pixel's color. The internal member used is dependent on the external context.
12typedef union QP_PACKED qp_pixel_t {
13 uint8_t mono;
14 uint8_t palette_idx;
15
16 struct QP_PACKED {
17 uint8_t h;
18 uint8_t s;
19 uint8_t v;
20 } hsv888;
21
22 struct QP_PACKED {
23 uint8_t r;
24 uint8_t g;
25 uint8_t b;
26 } rgb888;
27
28 uint16_t rgb565;
29
30 uint32_t dummy;
31} qp_pixel_t;
32_Static_assert(sizeof(qp_pixel_t) == 4, "Invalid size for qp_pixel_t");
33
34////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
35// Quantum Painter image format
36
37typedef enum qp_image_format_t {
38 // Pixel formats available in the QGF frame format
39 GRAYSCALE_1BPP = 0x00,
40 GRAYSCALE_2BPP = 0x01,
41 GRAYSCALE_4BPP = 0x02,
42 GRAYSCALE_8BPP = 0x03,
43 PALETTE_1BPP = 0x04,
44 PALETTE_2BPP = 0x05,
45 PALETTE_4BPP = 0x06,
46 PALETTE_8BPP = 0x07,
47} qp_image_format_t;
48
49typedef enum painter_compression_t { IMAGE_UNCOMPRESSED, IMAGE_COMPRESSED_RLE } painter_compression_t;
diff --git a/quantum/painter/qp_stream.c b/quantum/painter/qp_stream.c
new file mode 100644
index 0000000000..f00ae5ed38
--- /dev/null
+++ b/quantum/painter/qp_stream.c
@@ -0,0 +1,171 @@
1// Copyright 2021 Nick Brassel (@tzarc)
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#include "qp_stream.h"
5
6////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
7// Stream API
8
9uint32_t qp_stream_read_impl(void *output_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream) {
10 uint8_t *output_ptr = (uint8_t *)output_buf;
11
12 uint32_t i;
13 for (i = 0; i < (num_members * member_size); ++i) {
14 int16_t c = qp_stream_get(stream);
15 if (c < 0) {
16 break;
17 }
18
19 output_ptr[i] = (uint8_t)(c & 0xFF);
20 }
21
22 return i / member_size;
23}
24
25uint32_t qp_stream_write_impl(const void *input_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream) {
26 uint8_t *input_ptr = (uint8_t *)input_buf;
27
28 uint32_t i;
29 for (i = 0; i < (num_members * member_size); ++i) {
30 if (!qp_stream_put(stream, input_ptr[i])) {
31 break;
32 }
33 }
34
35 return i / member_size;
36}
37
38////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
39// Memory streams
40
41int16_t mem_get(qp_stream_t *stream) {
42 qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
43 if (s->position >= s->length) {
44 s->is_eof = true;
45 return STREAM_EOF;
46 }
47 return s->buffer[s->position++];
48}
49
50bool mem_put(qp_stream_t *stream, uint8_t c) {
51 qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
52 if (s->position >= s->length) {
53 s->is_eof = true;
54 return false;
55 }
56 s->buffer[s->position++] = c;
57 return true;
58}
59
60int mem_seek(qp_stream_t *stream, int32_t offset, int origin) {
61 qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
62
63 // Handle as per fseek
64 int32_t position = s->position;
65 switch (origin) {
66 case SEEK_SET:
67 position = offset;
68 break;
69 case SEEK_CUR:
70 position += offset;
71 break;
72 case SEEK_END:
73 position = s->length + offset;
74 break;
75 default:
76 return -1;
77 }
78
79 // If we're before the start, ignore it.
80 if (position < 0) {
81 return -1;
82 }
83
84 // If we're at the end it's okay, we only care if we're after the end for failure purposes -- as per lseek()
85 if (position > s->length) {
86 return -1;
87 }
88
89 // Update the offset
90 s->position = position;
91
92 // Successful invocation of fseek() results in clearing of the EOF flag by default, mirror the same functionality
93 s->is_eof = false;
94
95 return 0;
96}
97
98int32_t mem_tell(qp_stream_t *stream) {
99 qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
100 return s->position;
101}
102
103bool mem_is_eof(qp_stream_t *stream) {
104 qp_memory_stream_t *s = (qp_memory_stream_t *)stream;
105 return s->is_eof;
106}
107
108qp_memory_stream_t qp_make_memory_stream(void *buffer, int32_t length) {
109 qp_memory_stream_t stream = {
110 .base =
111 {
112 .get = mem_get,
113 .put = mem_put,
114 .seek = mem_seek,
115 .tell = mem_tell,
116 .is_eof = mem_is_eof,
117 },
118 .buffer = (uint8_t *)buffer,
119 .length = length,
120 .position = 0,
121 };
122 return stream;
123}
124
125////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
126// FILE streams
127
128#ifdef QP_STREAM_HAS_FILE_IO
129
130int16_t file_get(qp_stream_t *stream) {
131 qp_file_stream_t *s = (qp_file_stream_t *)stream;
132 int c = fgetc(s->file);
133 if (c < 0 || feof(s->file)) return STREAM_EOF;
134 return (uint16_t)c;
135}
136
137bool file_put(qp_stream_t *stream, uint8_t c) {
138 qp_file_stream_t *s = (qp_file_stream_t *)stream;
139 return fputc(c, s->file) == c;
140}
141
142int file_seek(qp_stream_t *stream, int32_t offset, int origin) {
143 qp_file_stream_t *s = (qp_file_stream_t *)stream;
144 return fseek(s->file, offset, origin);
145}
146
147int32_t file_tell(qp_stream_t *stream) {
148 qp_file_stream_t *s = (qp_file_stream_t *)stream;
149 return (int32_t)ftell(s->file);
150}
151
152bool file_is_eof(qp_stream_t *stream) {
153 qp_file_stream_t *s = (qp_file_stream_t *)stream;
154 return (bool)feof(s->file);
155}
156
157qp_file_stream_t qp_make_file_stream(FILE *f) {
158 qp_file_stream_t stream = {
159 .base =
160 {
161 .get = file_get,
162 .put = file_put,
163 .seek = file_seek,
164 .tell = file_tell,
165 .is_eof = file_is_eof,
166 },
167 .file = f,
168 };
169 return stream;
170}
171#endif // QP_STREAM_HAS_FILE_IO
diff --git a/quantum/painter/qp_stream.h b/quantum/painter/qp_stream.h
new file mode 100644
index 0000000000..878b9bf530
--- /dev/null
+++ b/quantum/painter/qp_stream.h
@@ -0,0 +1,82 @@
1/* Copyright 2021 Nick Brassel (@tzarc)
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17#pragma once
18
19#include <stdbool.h>
20#include <stdint.h>
21#include <stdlib.h>
22#include <stdio.h>
23
24#include "qp_internal.h"
25
26////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
27// Stream API
28
29typedef struct qp_stream_t qp_stream_t;
30
31#define qp_stream_get(stream_ptr) (((qp_stream_t *)(stream_ptr))->get((qp_stream_t *)(stream_ptr)))
32#define qp_stream_put(stream_ptr, c) (((qp_stream_t *)(stream_ptr))->put((qp_stream_t *)(stream_ptr), (c)))
33#define qp_stream_seek(stream_ptr, offset, origin) (((qp_stream_t *)(stream_ptr))->seek((qp_stream_t *)(stream_ptr), (offset), (origin)))
34#define qp_stream_tell(stream_ptr) (((qp_stream_t *)(stream_ptr))->tell((qp_stream_t *)(stream_ptr)))
35#define qp_stream_eof(stream_ptr) (((qp_stream_t *)(stream_ptr))->is_eof((qp_stream_t *)(stream_ptr)))
36#define qp_stream_setpos(stream_ptr, offset) qp_stream_seek((stream_ptr), (offset), SEEK_SET)
37#define qp_stream_getpos(stream_ptr) qp_stream_tell((stream_ptr))
38#define qp_stream_read(output_buf, member_size, num_members, stream_ptr) qp_stream_read_impl((output_buf), (member_size), (num_members), (qp_stream_t *)(stream_ptr))
39#define qp_stream_write(input_buf, member_size, num_members, stream_ptr) qp_stream_write_impl((input_buf), (member_size), (num_members), (qp_stream_t *)(stream_ptr))
40
41uint32_t qp_stream_read_impl(void *output_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream);
42uint32_t qp_stream_write_impl(const void *input_buf, uint32_t member_size, uint32_t num_members, qp_stream_t *stream);
43
44#define STREAM_EOF ((int16_t)(-1))
45
46////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
47// Stream definition
48
49struct qp_stream_t {
50 int16_t (*get)(qp_stream_t *stream);
51 bool (*put)(qp_stream_t *stream, uint8_t c);
52 int (*seek)(qp_stream_t *stream, int32_t offset, int origin);
53 int32_t (*tell)(qp_stream_t *stream);
54 bool (*is_eof)(qp_stream_t *stream);
55};
56
57////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
58// Memory streams
59
60typedef struct qp_memory_stream_t {
61 qp_stream_t base;
62 uint8_t * buffer;
63 int32_t length;
64 int32_t position;
65 bool is_eof;
66} qp_memory_stream_t;
67
68qp_memory_stream_t qp_make_memory_stream(void *buffer, int32_t length);
69
70////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
71// FILE streams
72
73#ifdef QP_STREAM_HAS_FILE_IO
74
75typedef struct qp_file_stream_t {
76 qp_stream_t base;
77 FILE * file;
78} qp_file_stream_t;
79
80qp_file_stream_t qo_make_file_stream(FILE *f);
81
82#endif // QP_STREAM_HAS_FILE_IO
diff --git a/quantum/painter/rules.mk b/quantum/painter/rules.mk
new file mode 100644
index 0000000000..9115d3d406
--- /dev/null
+++ b/quantum/painter/rules.mk
@@ -0,0 +1,116 @@
1# Quantum Painter Configurables
2QUANTUM_PAINTER_DRIVERS ?=
3QUANTUM_PAINTER_ANIMATIONS_ENABLE ?= yes
4
5# The list of permissible drivers that can be listed in QUANTUM_PAINTER_DRIVERS
6VALID_QUANTUM_PAINTER_DRIVERS := ili9163_spi ili9341_spi st7789_spi gc9a01_spi ssd1351_spi
7
8#-------------------------------------------------------------------------------
9
10OPT_DEFS += -DQUANTUM_PAINTER_ENABLE
11COMMON_VPATH += $(QUANTUM_DIR)/painter
12SRC += \
13 $(QUANTUM_DIR)/utf8.c \
14 $(QUANTUM_DIR)/color.c \
15 $(QUANTUM_DIR)/painter/qp.c \
16 $(QUANTUM_DIR)/painter/qp_stream.c \
17 $(QUANTUM_DIR)/painter/qgf.c \
18 $(QUANTUM_DIR)/painter/qff.c \
19 $(QUANTUM_DIR)/painter/qp_draw_core.c \
20 $(QUANTUM_DIR)/painter/qp_draw_codec.c \
21 $(QUANTUM_DIR)/painter/qp_draw_circle.c \
22 $(QUANTUM_DIR)/painter/qp_draw_ellipse.c \
23 $(QUANTUM_DIR)/painter/qp_draw_image.c \
24 $(QUANTUM_DIR)/painter/qp_draw_text.c
25
26# Check if people want animations... enable the defered exec if so.
27ifeq ($(strip $(QUANTUM_PAINTER_ANIMATIONS_ENABLE)), yes)
28 DEFERRED_EXEC_ENABLE := yes
29 OPT_DEFS += -DQUANTUM_PAINTER_ANIMATIONS_ENABLE
30endif
31
32# Comms flags
33QUANTUM_PAINTER_NEEDS_COMMS_SPI ?= no
34
35# Handler for each driver
36define handle_quantum_painter_driver
37 CURRENT_PAINTER_DRIVER := $1
38
39 ifeq ($$(filter $$(strip $$(CURRENT_PAINTER_DRIVER)),$$(VALID_QUANTUM_PAINTER_DRIVERS)),)
40 $$(error "$$(CURRENT_PAINTER_DRIVER)" is not a valid Quantum Painter driver)
41
42 else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9163_spi)
43 QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
44 QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
45 OPT_DEFS += -DQUANTUM_PAINTER_ILI9163_ENABLE -DQUANTUM_PAINTER_ILI9163_SPI_ENABLE
46 COMMON_VPATH += \
47 $(DRIVER_PATH)/painter/tft_panel \
48 $(DRIVER_PATH)/painter/ili9xxx
49 SRC += \
50 $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
51 $(DRIVER_PATH)/painter/ili9xxx/qp_ili9163.c \
52
53 else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9341_spi)
54 QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
55 QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
56 OPT_DEFS += -DQUANTUM_PAINTER_ILI9341_ENABLE -DQUANTUM_PAINTER_ILI9341_SPI_ENABLE
57 COMMON_VPATH += \
58 $(DRIVER_PATH)/painter/tft_panel \
59 $(DRIVER_PATH)/painter/ili9xxx
60 SRC += \
61 $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
62 $(DRIVER_PATH)/painter/ili9xxx/qp_ili9341.c \
63
64 else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),st7789_spi)
65 QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
66 QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
67 OPT_DEFS += -DQUANTUM_PAINTER_ST7789_ENABLE -DQUANTUM_PAINTER_ST7789_SPI_ENABLE
68 COMMON_VPATH += \
69 $(DRIVER_PATH)/painter/tft_panel \
70 $(DRIVER_PATH)/painter/st77xx
71 SRC += \
72 $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
73 $(DRIVER_PATH)/painter/st77xx/qp_st7789.c
74
75 else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),gc9a01_spi)
76 QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
77 QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
78 OPT_DEFS += -DQUANTUM_PAINTER_GC9A01_ENABLE -DQUANTUM_PAINTER_GC9A01_SPI_ENABLE
79 COMMON_VPATH += \
80 $(DRIVER_PATH)/painter/tft_panel \
81 $(DRIVER_PATH)/painter/gc9a01
82 SRC += \
83 $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
84 $(DRIVER_PATH)/painter/gc9a01/qp_gc9a01.c
85
86 else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ssd1351_spi)
87 QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
88 QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
89 OPT_DEFS += -DQUANTUM_PAINTER_SSD1351_ENABLE -DQUANTUM_PAINTER_SSD1351_SPI_ENABLE
90 COMMON_VPATH += \
91 $(DRIVER_PATH)/painter/tft_panel \
92 $(DRIVER_PATH)/painter/ssd1351
93 SRC += \
94 $(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
95 $(DRIVER_PATH)/painter/ssd1351/qp_ssd1351.c
96
97 endif
98endef
99
100# Iterate through the listed drivers for the build, including what's necessary
101$(foreach qp_driver,$(QUANTUM_PAINTER_DRIVERS),$(eval $(call handle_quantum_painter_driver,$(qp_driver))))
102
103# If SPI comms is needed, set up the required files
104ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI)), yes)
105 OPT_DEFS += -DQUANTUM_PAINTER_SPI_ENABLE
106 QUANTUM_LIB_SRC += spi_master.c
107 VPATH += $(DRIVER_PATH)/painter/comms
108 SRC += \
109 $(QUANTUM_DIR)/painter/qp_comms.c \
110 $(DRIVER_PATH)/painter/comms/qp_comms_spi.c
111
112 ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET)), yes)
113 OPT_DEFS += -DQUANTUM_PAINTER_SPI_DC_RESET_ENABLE
114 endif
115endif
116
diff --git a/quantum/process_keycode/process_unicode_common.c b/quantum/process_keycode/process_unicode_common.c
index 2606ea1f37..652becbc9a 100644
--- a/quantum/process_keycode/process_unicode_common.c
+++ b/quantum/process_keycode/process_unicode_common.c
@@ -16,6 +16,7 @@
16 16
17#include "process_unicode_common.h" 17#include "process_unicode_common.h"
18#include "eeprom.h" 18#include "eeprom.h"
19#include "utf8.h"
19 20
20unicode_config_t unicode_config; 21unicode_config_t unicode_config;
21uint8_t unicode_saved_mods; 22uint8_t unicode_saved_mods;
@@ -229,35 +230,6 @@ void register_unicode(uint32_t code_point) {
229 unicode_input_finish(); 230 unicode_input_finish();
230} 231}
231 232
232// Borrowed from https://nullprogram.com/blog/2017/10/06/
233static const char *decode_utf8(const char *str, int32_t *code_point) {
234 const char *next;
235
236 if (str[0] < 0x80) { // U+0000-007F
237 *code_point = str[0];
238 next = str + 1;
239 } else if ((str[0] & 0xE0) == 0xC0) { // U+0080-07FF
240 *code_point = ((int32_t)(str[0] & 0x1F) << 6) | ((int32_t)(str[1] & 0x3F) << 0);
241 next = str + 2;
242 } else if ((str[0] & 0xF0) == 0xE0) { // U+0800-FFFF
243 *code_point = ((int32_t)(str[0] & 0x0F) << 12) | ((int32_t)(str[1] & 0x3F) << 6) | ((int32_t)(str[2] & 0x3F) << 0);
244 next = str + 3;
245 } else if ((str[0] & 0xF8) == 0xF0 && (str[0] <= 0xF4)) { // U+10000-10FFFF
246 *code_point = ((int32_t)(str[0] & 0x07) << 18) | ((int32_t)(str[1] & 0x3F) << 12) | ((int32_t)(str[2] & 0x3F) << 6) | ((int32_t)(str[3] & 0x3F) << 0);
247 next = str + 4;
248 } else {
249 *code_point = -1;
250 next = str + 1;
251 }
252
253 // part of a UTF-16 surrogate pair - invalid
254 if (*code_point >= 0xD800 && *code_point <= 0xDFFF) {
255 *code_point = -1;
256 }
257
258 return next;
259}
260
261void send_unicode_string(const char *str) { 233void send_unicode_string(const char *str) {
262 if (!str) { 234 if (!str) {
263 return; 235 return;
diff --git a/quantum/quantum.h b/quantum/quantum.h
index f87e5f1916..9ce3c1f5d6 100644
--- a/quantum/quantum.h
+++ b/quantum/quantum.h
@@ -188,6 +188,10 @@ extern layer_state_t layer_state;
188# include "st7565.h" 188# include "st7565.h"
189#endif 189#endif
190 190
191#ifdef QUANTUM_PAINTER_ENABLE
192# include "qp.h"
193#endif
194
191#ifdef DIP_SWITCH_ENABLE 195#ifdef DIP_SWITCH_ENABLE
192# include "dip_switch.h" 196# include "dip_switch.h"
193#endif 197#endif
diff --git a/quantum/utf8.c b/quantum/utf8.c
new file mode 100644
index 0000000000..4b2cd4d8d4
--- /dev/null
+++ b/quantum/utf8.c
@@ -0,0 +1,46 @@
1/* Copyright 2021 QMK
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17#include "utf8.h"
18
19// Borrowed from https://nullprogram.com/blog/2017/10/06/
20const char *decode_utf8(const char *str, int32_t *code_point) {
21 const char *next;
22
23 if (str[0] < 0x80) { // U+0000-007F
24 *code_point = str[0];
25 next = str + 1;
26 } else if ((str[0] & 0xE0) == 0xC0) { // U+0080-07FF
27 *code_point = ((int32_t)(str[0] & 0x1F) << 6) | ((int32_t)(str[1] & 0x3F) << 0);
28 next = str + 2;
29 } else if ((str[0] & 0xF0) == 0xE0) { // U+0800-FFFF
30 *code_point = ((int32_t)(str[0] & 0x0F) << 12) | ((int32_t)(str[1] & 0x3F) << 6) | ((int32_t)(str[2] & 0x3F) << 0);
31 next = str + 3;
32 } else if ((str[0] & 0xF8) == 0xF0 && (str[0] <= 0xF4)) { // U+10000-10FFFF
33 *code_point = ((int32_t)(str[0] & 0x07) << 18) | ((int32_t)(str[1] & 0x3F) << 12) | ((int32_t)(str[2] & 0x3F) << 6) | ((int32_t)(str[3] & 0x3F) << 0);
34 next = str + 4;
35 } else {
36 *code_point = -1;
37 next = str + 1;
38 }
39
40 // part of a UTF-16 surrogate pair - invalid
41 if (*code_point >= 0xD800 && *code_point <= 0xDFFF) {
42 *code_point = -1;
43 }
44
45 return next;
46}
diff --git a/quantum/utf8.h b/quantum/utf8.h
new file mode 100644
index 0000000000..fb10910944
--- /dev/null
+++ b/quantum/utf8.h
@@ -0,0 +1,21 @@
1/* Copyright 2021 QMK
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
16
17#pragma once
18
19#include <stdint.h>
20
21const char *decode_utf8(const char *str, int32_t *code_point); \ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 6d338ae1cb..e09d58d829 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,3 +9,4 @@ milc>=1.4.2
9pygments 9pygments
10pyusb 10pyusb
11qmk-dotty-dict 11qmk-dotty-dict
12pillow
diff --git a/setup.cfg b/setup.cfg
index c7d7952098..6cbe1a616d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -6,7 +6,11 @@ ignore =
6 # Conflicts with our yapf config 6 # Conflicts with our yapf config
7 E231 7 E231
8per_file_ignores = 8per_file_ignores =
9 # Module imported but unused
9 **/__init__.py:F401 10 **/__init__.py:F401
11 # Quantum Painter also outputs append data using bytes object arithmetic on multiple lines
12 **/painter_qgf.py:W503
13 **/painter_qff.py:W503
10 14
11# Let's slowly crank this down 15# Let's slowly crank this down
12max_complexity=16 16max_complexity=16