Browse Source

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.
pull/16852/head
Nick Brassel 2 years ago
committed by GitHub
parent
commit
1f2b1dedcc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 7561 additions and 35 deletions
  1. +2
    -0
      .github/workflows/unit_test.yml
  2. +7
    -1
      builddefs/common_features.mk
  3. +1
    -0
      docs/_summary.md
  4. +12
    -0
      docs/cli_commands.md
  5. +705
    -0
      docs/quantum_painter.md
  6. +103
    -0
      docs/quantum_painter_qff.md
  7. +178
    -0
      docs/quantum_painter_qgf.md
  8. +29
    -0
      docs/quantum_painter_rle.md
  9. +137
    -0
      drivers/painter/comms/qp_comms_spi.c
  10. +51
    -0
      drivers/painter/comms/qp_comms_spi.h
  11. +150
    -0
      drivers/painter/gc9a01/qp_gc9a01.c
  12. +37
    -0
      drivers/painter/gc9a01/qp_gc9a01.h
  13. +78
    -0
      drivers/painter/gc9a01/qp_gc9a01_opcodes.h
  14. +121
    -0
      drivers/painter/ili9xxx/qp_ili9163.c
  15. +37
    -0
      drivers/painter/ili9xxx/qp_ili9163.h
  16. +128
    -0
      drivers/painter/ili9xxx/qp_ili9341.c
  17. +37
    -0
      drivers/painter/ili9xxx/qp_ili9341.h
  18. +100
    -0
      drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h
  19. +125
    -0
      drivers/painter/ssd1351/qp_ssd1351.c
  20. +37
    -0
      drivers/painter/ssd1351/qp_ssd1351.h
  21. +48
    -0
      drivers/painter/ssd1351/qp_ssd1351_opcodes.h
  22. +144
    -0
      drivers/painter/st77xx/qp_st7789.c
  23. +44
    -0
      drivers/painter/st77xx/qp_st7789.h
  24. +64
    -0
      drivers/painter/st77xx/qp_st7789_opcodes.h
  25. +51
    -0
      drivers/painter/st77xx/qp_st77xx_opcodes.h
  26. +130
    -0
      drivers/painter/tft_panel/qp_tft_panel.c
  27. +67
    -0
      drivers/painter/tft_panel/qp_tft_panel.h
  28. +3
    -1
      lib/python/qmk/cli/__init__.py
  29. +2
    -0
      lib/python/qmk/cli/painter/__init__.py
  30. +86
    -0
      lib/python/qmk/cli/painter/convert_graphics.py
  31. +87
    -0
      lib/python/qmk/cli/painter/make_font.py
  32. +268
    -0
      lib/python/qmk/painter.py
  33. +401
    -0
      lib/python/qmk/painter_qff.py
  34. +408
    -0
      lib/python/qmk/painter_qgf.py
  35. +7
    -4
      quantum/main.c
  36. +137
    -0
      quantum/painter/qff.c
  37. +88
    -0
      quantum/painter/qff.h
  38. +292
    -0
      quantum/painter/qgf.c
  39. +136
    -0
      quantum/painter/qgf.h
  40. +228
    -0
      quantum/painter/qp.c
  41. +453
    -0
      quantum/painter/qp.h
  42. +72
    -0
      quantum/painter/qp_comms.c
  43. +25
    -0
      quantum/painter/qp_comms.h
  44. +85
    -0
      quantum/painter/qp_draw.h
  45. +172
    -0
      quantum/painter/qp_draw_circle.c
  46. +142
    -0
      quantum/painter/qp_draw_codec.c
  47. +294
    -0
      quantum/painter/qp_draw_core.c
  48. +116
    -0
      quantum/painter/qp_draw_ellipse.c
  49. +382
    -0
      quantum/painter/qp_draw_image.c
  50. +444
    -0
      quantum/painter/qp_draw_text.c
  51. +33
    -0
      quantum/painter/qp_internal.h
  52. +82
    -0
      quantum/painter/qp_internal_driver.h
  53. +49
    -0
      quantum/painter/qp_internal_formats.h
  54. +171
    -0
      quantum/painter/qp_stream.c
  55. +82
    -0
      quantum/painter/qp_stream.h
  56. +116
    -0
      quantum/painter/rules.mk
  57. +1
    -29
      quantum/process_keycode/process_unicode_common.c
  58. +4
    -0
      quantum/quantum.h
  59. +46
    -0
      quantum/utf8.c
  60. +21
    -0
      quantum/utf8.h
  61. +1
    -0
      requirements.txt
  62. +4
    -0
      setup.cfg

+ 2
- 0
.github/workflows/unit_test.yml View File

@ -26,5 +26,7 @@ jobs:
- uses: actions/checkout@v2
with:
submodules: recursive
- name: Install dependencies
run: pip3 install -r requirements-dev.txt
- name: Run tests
run: make test:all

+ 7
- 1
builddefs/common_features.mk View File

@ -149,6 +149,11 @@ ifeq ($(strip $(POINTING_DEVICE_ENABLE)), yes)
endif
endif
QUANTUM_PAINTER_ENABLE ?= no
ifeq ($(strip $(QUANTUM_PAINTER_ENABLE)), yes)
include $(QUANTUM_DIR)/painter/rules.mk
endif
VALID_EEPROM_DRIVER_TYPES := vendor custom transient i2c spi
EEPROM_DRIVER ?= vendor
ifeq ($(filter $(EEPROM_DRIVER),$(VALID_EEPROM_DRIVER_TYPES)),)
@ -696,7 +701,8 @@ endif
ifeq ($(strip $(UNICODE_COMMON)), yes)
OPT_DEFS += -DUNICODE_COMMON_ENABLE
SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c
SRC += $(QUANTUM_DIR)/process_keycode/process_unicode_common.c \
$(QUANTUM_DIR)/utf8.c
endif
MAGIC_ENABLE ?= yes


+ 1
- 0
docs/_summary.md View File

@ -94,6 +94,7 @@
* Hardware Features
* Displays
* [Quantum Painter](quantum_painter.md)
* [HD44780 LCD Driver](feature_hd44780.md)
* [ST7565 LCD Driver](feature_st7565.md)
* [OLED Driver](feature_oled_driver.md)


+ 12
- 0
docs/cli_commands.md View File

@ -515,3 +515,15 @@ Run single test:
qmk pytest -t qmk.tests.test_cli_commands.test_c2json
qmk pytest -t qmk.tests.test_qmk_path
## `qmk painter-convert-graphics`
This command converts images to a format usable by QMK, i.e. the QGF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.
## `qmk painter-make-font-image`
This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.
## `qmk painter-convert-font-image`
This command converts an intermediate font image to the QFF File Format. See the [Quantum Painter](quantum_painter.md?id=quantum-painter-cli) documentation for more information on this command.

+ 705
- 0
docs/quantum_painter.md View File

@ -0,0 +1,705 @@
# Quantum Painter :id=quantum-painter
Quantum Painter is the standardised API for graphical displays. It currently includes support for basic drawing primitives, as well as custom images, animations, and fonts.
Due to the complexity, there is no support for Quantum Painter on AVR-based boards.
To enable overall Quantum Painter to be built into your firmware, add the following to `rules.mk`:
```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS = ......
```
You will also likely need to select an appropriate driver in `rules.mk`, which is listed below.
!> 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.
The QMK CLI can be used to convert from normal images such as PNG files or animated GIFs, as well as fonts from TTF files.
Hardware supported:
| Display Panel | Panel Type | Size | Comms Transport | Driver |
|---------------|--------------------|------------------|-----------------|-----------------------------------------|
| GC9A01 | RGB LCD (circular) | 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = gc9a01_spi` |
| ILI9163 | RGB LCD | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9163_spi` |
| ILI9341 | RGB LCD | 240x320 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ili9341_spi` |
| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = ssd1351_spi` |
| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS = st7789_spi` |
## Quantum Painter Configuration :id=quantum-painter-config
| Option | Default | Purpose |
|-----------------------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------|
| `QUANTUM_PAINTER_NUM_IMAGES` | `8` | The maximum number of images/animations that can be loaded at any one time. |
| `QUANTUM_PAINTER_NUM_FONTS` | `4` | The maximum number of fonts that can be loaded at any one time. |
| `QUANTUM_PAINTER_CONCURRENT_ANIMATIONS` | `4` | The maximum number of animations that can be executed at the same time. |
| `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. |
| `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. |
| `QUANTUM_PAINTER_SUPPORTS_256_PALETTE` | `FALSE` | If 256-color palettes are supported. Requires significantly more RAM on the MCU. |
| `QUANTUM_PAINTER_DEBUG` | _unset_ | Prints out significant amounts of debugging information to CONSOLE output. Significant performance degradation, use only for debugging. |
Drivers have their own set of configurable options, and are described in their respective sections.
## Quantum Painter CLI Commands :id=quantum-painter-cli
### `qmk painter-convert-graphics`
This command converts images to a format usable by QMK, i.e. the QGF File Format.
**Usage**:
```
usage: qmk painter-convert-graphics [-h] [-d] [-r] -f FORMAT [-o OUTPUT] -i INPUT [-v]
optional arguments:
-h, --help show this help message and exit
-d, --no-deltas Disables the use of delta frames when encoding animations.
-r, --no-rle Disables the use of RLE when encoding images.
-f FORMAT, --format FORMAT
Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
-o OUTPUT, --output OUTPUT
Specify output directory. Defaults to same directory as input.
-i INPUT, --input INPUT
Specify input graphic file.
-v, --verbose Turns on verbose output.
```
The `INPUT` argument can be any image file loadable by Python's Pillow module. Common formats include PNG, or Animated GIF.
The `OUTPUT` argument needs to be a directory, and will default to the same directory as the input argument.
The `FORMAT` argument can be any of the following:
| Format | Meaning |
|-----------|-----------------------------------------------------------------------|
| `pal256` | 256-color palette (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
| `pal16` | 16-color palette |
| `pal4` | 4-color palette |
| `pal2` | 2-color palette |
| `mono256` | 256-shade grayscale (requires `QUANTUM_PAINTER_SUPPORTS_256_PALETTE`) |
| `mono16` | 16-shade grayscale |
| `mono4` | 4-shade grayscale |
| `mono2` | 2-shade grayscale |
**Examples**:
```
$ cd /home/qmk/qmk_firmware/keyboards/my_keeb
$ qmk painter-convert-graphics -f mono16 -i my_image.gif -o ./generated/
Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.h...
Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/my_image.qgf.c...
```
### `qmk painter-make-font-image`
This command converts a TTF font to an intermediate format for editing, before converting to the QFF File Format.
**Usage**:
```
usage: qmk painter-make-font-image [-h] [-a] [-u UNICODE_GLYPHS] [-n] [-s SIZE] -o OUTPUT -f FONT
optional arguments:
-h, --help show this help message and exit
-a, --no-aa Disable anti-aliasing on fonts.
-u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS
Also generate the specified unicode glyphs.
-n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.
-s SIZE, --size SIZE Specify font size. Default 12.
-o OUTPUT, --output OUTPUT
Specify output image path.
-f FONT, --font FONT Specify input font file.
```
The `FONT` argument is generally a TrueType Font file (TTF).
The `OUTPUT` argument is the output image to generate, generally something like `my_font.png`.
The `UNICODE_GLYPHS` argument allows for specifying extra unicode glyphs to generate, and accepts a string.
**Examples**:
```
$ qmk painter-make-font-image --font NotoSans-ExtraCondensedBold.ttf --size 11 -o noto11.png --unicode-glyphs "ĄȽɂɻɣɈʣ"
```
### `qmk painter-convert-font-image`
This command converts an intermediate font image to the QFF File Format.
This command expects an image that conforms to the following format:
* Top-left pixel (at `0,0`) is the "delimiter" color:
* Each glyph in the font starts when a pixel of this color is found on the first row
* The first row is discarded when converting to the QFF format
* The number of delimited glyphs must match the supplied arguments to the command:
* The full ASCII set `0x20..0x7E` (if `--no-ascii` was not specified)
* The corresponding number of unicode glyphs if any were specified with `--unicode-glyphs`
* The order of the glyphs matches the ASCII set, if any, followed by the Unicode glyph set, if any.
**Usage**:
```
usage: qmk painter-convert-font-image [-h] [-r] -f FORMAT [-u UNICODE_GLYPHS] [-n] [-o OUTPUT] [-i INPUT]
optional arguments:
-h, --help show this help message and exit
-r, --no-rle Disable the use of RLE to minimise converted image size.
-f FORMAT, --format FORMAT
Output format, valid types: pal256, pal16, pal4, pal2, mono256, mono16, mono4, mono2
-u UNICODE_GLYPHS, --unicode-glyphs UNICODE_GLYPHS
Also generate the specified unicode glyphs.
-n, --no-ascii Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.
-o OUTPUT, --output OUTPUT
Specify output directory. Defaults to same directory as input.
-i INPUT, --input INPUT
Specify input graphic file.
```
The same arguments for `--no-ascii` and `--unicode-glyphs` need to be specified, as per `qmk painter-make-font-image`.
**Examples**:
```
$ cd /home/qmk/qmk_firmware/keyboards/my_keeb
$ qmk painter-convert-font-image --input noto11.png -f mono4 --unicode-glyphs "ĄȽɂɻɣɈʣ"
Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.h...
Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.c...
```
## Quantum Painter Drawing API :id=quantum-painter-api
All 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.
To use any of the APIs, you need to include `qp.h`:
```c
#include <qp.h>
```
### General Notes :id=quantum-painter-api-general
The 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.
?> Drawing a horizontal line 8 pixels long, starting from 4 pixels inside the left side of the display, will need `left=4`, `right=11`.
All color data matches the standard QMK HSV triplet definitions:
* Hue is of the range `0...255` and is internally mapped to 0...360 degrees.
* Saturation is of the range `0...255` and is internally mapped to 0...100% saturation.
* Value is of the range `0...255` and is internally mapped to 0...100% brightness.
?> Colors used in Quantum Painter are not subject to the RGB lighting CIE curve, if it is enabled.
### Device Control :id=quantum-painter-api-device-control
#### Display Initialisation :id=quantum-painter-api-init
```c
bool qp_init(painter_device_t device, painter_rotation_t rotation);
```
The `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.
```c
static painter_device_t display;
void keyboard_post_init_kb(void) {
display = qp_make_.......; // Create the display
qp_init(display, QP_ROTATION_0); // Initialise the display
}
```
#### Display Power :id=quantum-painter-api-power
```c
bool qp_power(painter_device_t device, bool power_on);
```
The `qp_power` function instructs the display whether or not the display panel should be on or off.
!> 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.
```c
static uint8_t last_backlight = 255;
void suspend_power_down_user(void) {
if (last_backlight == 255) {
last_backlight = get_backlight_level();
}
backlight_set(0);
rgb_matrix_set_suspend_state(true);
qp_power(display, false);
}
void suspend_wakeup_init_user(void) {
qp_power(display, true);
rgb_matrix_set_suspend_state(false);
if (last_backlight != 255) {
backlight_set(last_backlight);
}
last_backlight = 255;
}
```
#### Display Clear :id=quantum-painter-api-clear
```c
bool qp_clear(painter_device_t device);
```
The `qp_clear` function clears the display's screen.
#### Display Flush :id=quantum-painter-api-flush
```c
bool qp_flush(painter_device_t device);
```
The `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.
!> 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".
```c
void housekeeping_task_user(void) {
static uint32_t last_draw = 0;
if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
last_draw = timer_read32();
// Draw a rect based off the current RGB color
qp_rect(display, 0, 7, 0, 239, rgb_matrix_get_hue(), 255, 255);
qp_flush(display);
}
}
```
### Drawing Primitives :id=quantum-painter-api-primitives
#### Set Pixel :id=quantum-painter-api-setpixel
```c
bool qp_setpixel(painter_device_t device, uint16_t x, uint16_t y, uint8_t hue, uint8_t sat, uint8_t val);
```
The `qp_setpixel` can be used to set a specific pixel on the screen to the supplied color.
?> Using `qp_setpixel` for large amounts of drawing operations is inefficient and should be avoided unless they cannot be achieved with other drawing APIs.
```c
void housekeeping_task_user(void) {
static uint32_t last_draw = 0;
if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
last_draw = timer_read32();
// Draw a 240px high vertical rainbow line on X=0:
for (int i = 0; i < 239; ++i) {
qp_setpixel(display, 0, i, i, 255, 255);
}
qp_flush(display);
}
}
```
#### Draw Line :id=quantum-painter-api-line
```c
bool qp_line(painter_device_t device, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint8_t hue, uint8_t sat, uint8_t val);
```
The `qp_line` can be used to draw lines on the screen with the supplied color.
```c
void housekeeping_task_user(void) {
static uint32_t last_draw = 0;
if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
last_draw = timer_read32();
// Draw 8px-wide rainbow down the left side of the display
for (int i = 0; i < 239; ++i) {
qp_line(display, 0, i, 7, i, i, 255, 255);
}
qp_flush(display);
}
}
```
#### Draw Rect :id=quantum-painter-api-rect
```c
bool qp_rect(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
```
The `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.
```c
void housekeeping_task_user(void) {
static uint32_t last_draw = 0;
if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
last_draw = timer_read32();
// Draw 8px-wide rainbow filled rectangles down the left side of the display
for (int i = 0; i < 239; i+=8) {
qp_rect(display, 0, i, 7, i+7, i, 255, 255, true);
}
qp_flush(display);
}
}
```
#### Draw Circle :id=quantum-painter-api-circle
```c
bool qp_circle(painter_device_t device, uint16_t x, uint16_t y, uint16_t radius, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
```
The `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.
```c
void housekeeping_task_user(void) {
static uint32_t last_draw = 0;
if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
last_draw = timer_read32();
// Draw r=4 filled circles down the left side of the display
for (int i = 0; i < 239; i+=8) {
qp_circle(display, 4, 4+i, 4, i, 255, 255, true);
}
qp_flush(display);
}
}
```
#### Draw Ellipse :id=quantum-painter-api-ellipse
```c
bool qp_ellipse(painter_device_t device, uint16_t x, uint16_t y, uint16_t sizex, uint16_t sizey, uint8_t hue, uint8_t sat, uint8_t val, bool filled);
```
The `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.
```c
void housekeeping_task_user(void) {
static uint32_t last_draw = 0;
if (timer_elapsed32(last_draw) > 33) { // Throttle to 30fps
last_draw = timer_read32();
// Draw 16x8 filled ellipses down the left side of the display
for (int i = 0; i < 239; i+=8) {
qp_ellipse(display, 8, 4+i, 16, 8, i, 255, 255, true);
}
qp_flush(display);
}
}
```
### Image Functions :id=quantum-painter-api-images
#### Load Image :id=quantum-painter-api-load-image
```c
painter_image_handle_t qp_load_image_mem(const void *buffer);
```
The `qp_load_image_mem` function loads a QGF image from memory or flash.
`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.
See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert images to [QGF](quantum_painter_qgf.md).
?> 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`.
Image information is available through accessing the handle:
| Property | Accessor |
|-------------|----------------------|
| Width | `image->width` |
| Height | `image->height` |
| Frame Count | `image->frame_count` |
#### Unload Image :id=quantum-painter-api-close-image
```c
bool qp_close_image(painter_image_handle_t image);
```
The `qp_close_image` function releases resources related to the loading of the supplied image.
#### Draw image :id=quantum-painter-api-draw-image
```c
bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
```
The `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.
```c
// Draw an image on the bottom-right of the 240x320 display on initialisation
static painter_image_handle_t my_image;
void keyboard_post_init_kb(void) {
my_image = qp_load_image_mem(gfx_my_image);
if (my_image != NULL) {
qp_drawimage(display, (239 - my_image->width), (319 - my_image->height), my_image);
}
}
```
#### Animate Image :id=quantum-painter-api-animate-image
```c
deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image);
deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
```
The `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.
Once an image has been set to animate, it will loop indefinitely until stopped, with no user intervention required.
Both functions return a `deferred_token`, which can then be used to stop the animation, using `qp_stop_animation` below.
```c
// Animate an image on the bottom-right of the 240x320 display on initialisation
static painter_image_handle_t my_image;
static deferred_token my_anim;
void keyboard_post_init_kb(void) {
my_image = qp_load_image_mem(gfx_my_image);
if (my_image != NULL) {
my_anim = qp_animate(display, (239 - my_image->width), (319 - my_image->height), my_image);
}
}
```
#### Stop Animation :id=quantum-painter-api-stop-animation
```c
void qp_stop_animation(deferred_token anim_token);
```
The `qp_stop_animation` function stops the previously-started animation.
```c
void housekeeping_task_user(void) {
if (some_random_stop_reason) {
qp_stop_animation(my_anim);
}
}
```
### Font Functions :id=quantum-painter-api-fonts
#### Load Font :id=quantum-painter-api-load-font
```c
painter_font_handle_t qp_load_font_mem(const void *buffer);
```
The `qp_load_font_mem` function loads a QFF font from memory or flash.
`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.
See the [CLI Commands](quantum_painter.md?id=quantum-painter-cli) for instructions on how to convert TTF fonts to [QFF](quantum_painter_qff.md).
?> 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`.
Font information is available through accessing the handle:
| Property | Accessor |
|-------------|----------------------|
| Line Height | `image->line_height` |
#### Unload Font :id=quantum-painter-api-close-font
```c
bool qp_close_font(painter_font_handle_t font);
```
The `qp_close_font` function releases resources related to the loading of the supplied font.
#### Measure Text :id=quantum-painter-api-textwidth
```c
int16_t qp_textwidth(painter_font_handle_t font, const char *str);
```
The `qp_textwidth` function allows measurement of how many pixels wide the supplied string would result in, for the given font.
#### Draw Text :id=quantum-painter-api-drawtext
```c
int16_t qp_drawtext(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str);
int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_font_handle_t font, const char *str, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg);
```
The `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.
```c
// Draw a text message on the bottom-right of the 240x320 display on initialisation
static painter_font_handle_t my_font;
void keyboard_post_init_kb(void) {
my_font = qp_load_font_mem(font_opensans);
if (my_font != NULL) {
static const char *text = "Hello from QMK!";
int16_t width = qp_textwidth(my_font, text);
qp_drawtext(display, (239 - width), (319 - my_font->line_height), my_font, text);
}
}
```
### Advanced Functions :id=quantum-painter-api-advanced
#### Get Geometry :id=quantum-painter-api-get-geometry
```c
void qp_get_geometry(painter_device_t device, uint16_t *width, uint16_t *height, painter_rotation_t *rotation, uint16_t *offset_x, uint16_t *offset_y);
```
The `qp_get_geometry` function allows external code to retrieve the current width, height, rotation, and drawing offsets.
#### Set Viewport Offsets :id=quantum-painter-api-set-viewport
```c
void qp_set_viewport_offsets(painter_device_t device, uint16_t offset_x, uint16_t offset_y);
```
The `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.
#### Set Viewport :id=quantum-painter-api-viewport
```c
bool qp_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
```
The `qp_viewport` function controls where raw pixel data is written to.
#### Stream Pixel Data :id=quantum-painter-api-pixdata
```c
bool qp_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
```
The `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.
!> 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.
## Quantum Painter Display Drivers :id=quantum-painter-drivers
### Common: Standard TFT (SPI + D/C + RST)
Most TFT display panels use a 5-pin interface -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins.
For these displays, QMK's `spi_master` must already be correctly configured for the platform you're building for.
The pin assignments for SPI CS, D/C, and RST are specified during device construction.
### GC9A01 :id=qp-driver-gc9a01
Enabling support for the GC9A01 in Quantum Painter is done by adding the following to `rules.mk`:
```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS = gc9a01_spi
```
Creating a GC9A01 device in firmware can then be done with the following API:
```c
painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
```
The device handle returned from the `qp_gc9a01_make_spi_device` function can be used to perform all other drawing operations.
The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
```c
// 3 displays:
#define GC9A01_NUM_DEVICES 3
```
### ILI9163 :id=qp-driver-ili9163
Enabling support for the ILI9163 in Quantum Painter is done by adding the following to `rules.mk`:
```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS = ili9163_spi
```
Creating a ILI9163 device in firmware can then be done with the following API:
```c
painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
```
The device handle returned from the `qp_ili9163_make_spi_device` function can be used to perform all other drawing operations.
The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
```c
// 3 displays:
#define ILI9163_NUM_DEVICES 3
```
### ILI9341 :id=qp-driver-ili9341
Enabling support for the ILI9341 in Quantum Painter is done by adding the following to `rules.mk`:
```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS = ili9341_spi
```
Creating a ILI9341 device in firmware can then be done with the following API:
```c
painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
```
The device handle returned from the `qp_ili9341_make_spi_device` function can be used to perform all other drawing operations.
The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
```c
// 3 displays:
#define ILI9341_NUM_DEVICES 3
```
### SSD1351 :id=qp-driver-ssd1351
Enabling support for the SSD1351 in Quantum Painter is done by adding the following to `rules.mk`:
```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS = ssd1351_spi
```
Creating a SSD1351 device in firmware can then be done with the following API:
```c
painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
```
The device handle returned from the `qp_ssd1351_make_spi_device` function can be used to perform all other drawing operations.
The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
```c
// 3 displays:
#define SSD1351_NUM_DEVICES 3
```
### ST7789 :id=qp-driver-st7789
Enabling support for the ST7789 in Quantum Painter is done by adding the following to `rules.mk`:
```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS = st7789_spi
```
Creating a ST7789 device in firmware can then be done with the following API:
```c
painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
```
The device handle returned from the `qp_st7789_make_spi_device` function can be used to perform all other drawing operations.
The maximum number of displays can be configured by changing the following in your `config.h` (default is 1):
```c
// 3 displays:
#define ST7789_NUM_DEVICES 3
```
!> 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.

+ 103
- 0
docs/quantum_painter_qff.md View File

@ -0,0 +1,103 @@
# QMK Font Format :id=qmk-font-format
QMK uses a font format _("Quantum Font Format" - QFF)_ specifically for resource-constrained systems.
This format is capable of encoding 1-, 2-, 4-, and 8-bit-per-pixel greyscale- and palette-based images into a font. It also includes RLE for pixel data for some basic compression.
All integer values are in little-endian format.
The 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.
The general structure of the file is:
* _Font descriptor block_
* _ASCII glyph block_ (optional, only if ASCII glyphs are included)
* _Unicode glyph block_ (optional, only if Unicode glyphs are included)
* _Font palette block_ (optional, depending on frame format)
* _Font data block_
## Block Header :id=qff-block-header
The 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.
## Font descriptor block :id=qff-font-descriptor
* _typeid_ = 0x00
* _length_ = 20
This 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.
_Block_ format:
```c
typedef struct __attribute__((packed)) qff_font_descriptor_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 20 }
uint24_t magic; // constant, equal to 0x464651 ("QFF")
uint8_t qff_version; // constant, equal to 0x01
uint32_t total_file_size; // total size of the entire file, starting at offset zero
uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors
uint8_t line_height; // glyph height in pixels
bool has_ascii_table; // whether the font has an ascii table of glyphs (0x20...0x7E)
uint16_t num_unicode_glyphs; // the number of glyphs in the unicode table -- no table specified if zero
uint8_t format; // frame format, see below.
uint8_t flags; // frame flags, see below.
uint8_t compression_scheme; // compression scheme, see below.
uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
} qff_font_descriptor_v1_t;
// _Static_assert(sizeof(qff_font_descriptor_v1_t) == (sizeof(qgf_block_header_v1_t) + 20), "qff_font_descriptor_v1_t must be 25 bytes in v1 of QFF");
```
The 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.
## ASCII glyph table :id=qff-ascii-table
* _typeid_ = 0x01
* _length_ = 290
If the font contains ascii characters, the _ASCII glyph block_ must be located directly after the _font descriptor block_.
```c
#define QFF_GLYPH_WIDTH_BITS 6
#define QFF_GLYPH_WIDTH_MASK ((1<<QFF_GLYPH_WIDTH_BITS)-1)
#define QFF_GLYPH_OFFSET_BITS 18
#define QFF_GLYPH_OFFSET_MASK (((1<<QFF_GLYPH_OFFSET_BITS)-1) << QFF_GLYPH_WIDTH_BITS)
typedef struct __attribute__((packed)) qff_ascii_glyph_table_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = 285 }
uint24_t glyph[95]; // 95 glyphs, 0x20..0x7E, see bits/masks above for values
} qff_ascii_glyph_table_v1_t;
// _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");
```
## Unicode glyph table :id=qff-unicode-table
* _typeid_ = 0x02
* _length_ = variable
If 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.
```c
typedef struct __attribute__((packed)) qff_unicode_glyph_table_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = (N * 6) }
struct __attribute__((packed)) { // container for a single unicode glyph
uint24_t code_point; // the unicode code point
uint24_t glyph; // the glyph information, as per ASCII glyphs above
} glyph[N]; // N glyphs worth of data
} qff_unicode_glyph_table_v1_t;
```
## Font palette block :id=qff-palette-descriptor
* _typeid_ = 0x03
* _length_ = variable
The _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.
It 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.
## Font data block :id=qff-data-descriptor
* _typeid_ = 0x04
* _length_ = variable
The _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.

+ 178
- 0
docs/quantum_painter_qgf.md View File

@ -0,0 +1,178 @@
# QMK Graphics Format :id=qmk-graphics-format
QMK uses a graphics format _("Quantum Graphics Format" - QGF)_ specifically for resource-constrained systems.
This format is capable of encoding 1-, 2-, 4-, and 8-bit-per-pixel greyscale- and palette-based images. It also includes RLE for pixel data for some basic compression.
All integer values are in little-endian format.
The 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.
The general structure of the file is:
* _Graphics descriptor block_
* _Frame offset block_
* Repeating list of frames:
* _Frame descriptor block_
* _Frame palette block_ (optional, depending on frame format)
* _Frame delta block_ (optional, depending on delta flag)
* _Frame data block_
Different frames within the file should be considered "isolated" and may have their own image format and/or palette.
## Block Header :id=qgf-block-header
This block header is present for all blocks, including the graphics descriptor.
_Block header_ format:
```c
typedef struct __attribute__((packed)) qgf_block_header_v1_t {
uint8_t type_id; // See each respective block type
uint8_t neg_type_id; // Negated type ID, used for detecting parsing errors
uint24_t length; // 24-bit blob length, allowing for block sizes of a maximum of 16MB
} qgf_block_header_v1_t;
// _Static_assert(sizeof(qgf_block_header_v1_t) == 5, "qgf_block_header_v1_t must be 5 bytes in v1 of QGF");
```
The _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.
## Graphics descriptor block :id=qgf-graphics-descriptor
* _typeid_ = 0x00
* _length_ = 18
This 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_.
_Block_ format:
```c
typedef struct __attribute__((packed)) qgf_graphics_descriptor_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x00, .neg_type_id = (~0x00), .length = 18 }
uint24_t magic; // constant, equal to 0x464751 ("QGF")
uint8_t qgf_version; // constant, equal to 0x01
uint32_t total_file_size; // total size of the entire file, starting at offset zero
uint32_t neg_total_file_size; // negated value of total_file_size, used for detecting parsing errors
uint16_t image_width; // in pixels
uint16_t image_height; // in pixels
uint16_t frame_count; // minimum of 1
} qgf_graphics_descriptor_v1_t;
// _Static_assert(sizeof(qgf_graphics_descriptor_v1_t) == (sizeof(qgf_block_header_v1_t) + 18), "qgf_graphics_descriptor_v1_t must be 23 bytes in v1 of QGF");
```
## Frame offset block :id=qgf-frame-offset-descriptor
* _typeid_ = 0x01
* _length_ = variable
This 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.
Duplicate frame offsets in this block are allowed, if a certain frame is to be shown multiple times during animation.
_Block_ format:
```c
typedef struct __attribute__((packed)) qgf_frame_offsets_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x01, .neg_type_id = (~0x01), .length = (N * sizeof(uint32_t)) }
uint32_t offset[N]; // where 'N' is the number of frames in the file
} qgf_frame_offsets_v1_t;
```
## Frame descriptor block :id=qgf-frame-descriptor
* _typeid_ = 0x02
* _length_ = 5
This block denotes the start of a frame.
_Block_ format:
```c
typedef struct __attribute__((packed)) qgf_frame_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x02, .neg_type_id = (~0x02), .length = 5 }
uint8_t format; // Frame format, see below.
uint8_t flags; // Frame flags, see below.
uint8_t compression_scheme; // Compression scheme, see below.
uint8_t transparency_index; // palette index used for transparent pixels (not yet implemented)
uint16_t delay; // frame delay time for animations (in units of milliseconds)
} qgf_frame_v1_t;
// _Static_assert(sizeof(qgf_frame_v1_t) == (sizeof(qgf_block_header_v1_t) + 6), "qgf_frame_v1_t must be 11 bytes in v1 of QGF");
```
If 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_.
If 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_.
Frame format possible values:
* `0x00`: 1bpp grayscale, no palette, `0` = black, `1` = white, LSb first pixel
* `0x01`: 2bpp grayscale, no palette, `0` = black, `3` = white, linear interpolation of brightness, LSb first pixel
* `0x02`: 4bpp grayscale, no palette, `0` = black, `15` = white, linear interpolation of brightness, LSb first pixel
* `0x03`: 8bpp grayscale, no palette, `0` = black, `255` = white, linear interpolation of brightness, LSb first pixel
* `0x04`: 1bpp indexed palette, 2 colors, LSb first pixel
* `0x05`: 2bpp indexed palette, 4 colors, LSb first pixel
* `0x06`: 4bpp indexed palette, 16 colors, LSb first pixel
* `0x07`: 8bpp indexed palette, 256 colors, LSb first pixel
Frame flags is a bitmask with the following format:
| `bit 7` | `bit 6` | `bit 5` | `bit 4` | `bit 3` | `bit 2` | `bit 1` | `bit 0` |
|---------|---------|---------|---------|---------|---------|---------|--------------|
| - | - | - | - | - | - | Delta | Transparency |
* `[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_.
* `[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.
Compression scheme possible values:
* `0x00`: No compression
* `0x01`: [QMK RLE](quantum_painter_rle.md)
## Frame palette block :id=qgf-frame-palette-descriptor
* _typeid_ = 0x03
* _length_ = variable
This 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:
```c
typedef struct __attribute__((packed)) qgf_palette_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x03, .neg_type_id = (~0x03), .length = (N * 3 * sizeof(uint8_t)) }
struct { // container for a single HSV palette entry
uint8_t h; // hue component: `[0,360)` degrees is mapped to `[0,255]` uint8_t.
uint8_t s; // saturation component: `[0,1]` is mapped to `[0,255]` uint8_t.
uint8_t v; // value component: `[0,1]` is mapped to `[0,255]` uint8_t.
} hsv[N]; // N * hsv, where N is the number of palette entries depending on the frame format in the descriptor
} qgf_palette_v1_t;
```
## Frame delta block :id=qgf-frame-delta-descriptor
* _typeid_ = 0x04
* _length_ = 8
This block describes where the delta frame should be drawn, with respect to the top left location of the image.
```c
typedef struct __attribute__((packed)) qgf_delta_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x04, .neg_type_id = (~0x04), .length = 8 }
uint16_t left; // The left pixel location to draw the delta image
uint16_t top; // The top pixel location to draw the delta image
uint16_t right; // The right pixel location to to draw the delta image
uint16_t bottom; // The bottom pixel location to to draw the delta image
} qgf_delta_v1_t;
// _Static_assert(sizeof(qgf_delta_v1_t) == 13, "qgf_delta_v1_t must be 13 bytes in v1 of QGF");
```
## Frame data block :id=qgf-frame-data-descriptor
* _typeid_ = 0x05
* _length_ = variable
This 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:
```c
typedef struct __attribute__((packed)) qgf_data_v1_t {
qgf_block_header_v1_t header; // = { .type_id = 0x05, .neg_type_id = (~0x05), .length = N }
uint8_t data[N]; // N data octets
} qgf_data_v1_t;
```

+ 29
- 0
docs/quantum_painter_rle.md View File

@ -0,0 +1,29 @@
# QMK QGF/QFF RLE data schema :id=qmk-qp-rle-schema
There are two "modes" to the RLE algorithm used in both [QGF](quantum_painter_qgf.md)/[QFF](quantum_painter_qff.md):
* Non-repeating sections of octets, with associated length of up to `128` octets
* `length` = `marker - 128`
* A corresponding `length` number of octets follow directly after the marker octet
* Repeated octet with associated length, with associated length of up to `128`
* `length` = `marker`
* A single octet follows the marker that should be repeated `length` times.
Decoder pseudocode:
```
while !EOF
marker = READ_OCTET()
if marker >= 128
length = marker - 128
for i = 0 ... length-1
c = READ_OCTET()
WRITE_OCTET(c)
else
length = marker
c = READ_OCTET()
for i = 0 ... length-1
WRITE_OCTET(c)
```

+ 137
- 0
drivers/painter/comms/qp_comms_spi.c View File

@ -0,0 +1,137 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef QUANTUM_PAINTER_SPI_ENABLE
# include "spi_master.h"
# include "qp_comms_spi.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Base SPI support
bool qp_comms_spi_init(painter_device_t device) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
// Initialize the SPI peripheral
spi_init();
// Set up CS as output high
setPinOutput(comms_config->chip_select_pin);
writePinHigh(comms_config->chip_select_pin);
return true;
}
bool qp_comms_spi_start(painter_device_t device) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
return spi_start(comms_config->chip_select_pin, comms_config->lsb_first, comms_config->mode, comms_config->divisor);
}
uint32_t qp_comms_spi_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
uint32_t bytes_remaining = byte_count;
const uint8_t *p = (const uint8_t *)data;
while (bytes_remaining > 0) {
uint32_t bytes_this_loop = bytes_remaining < 1024 ? bytes_remaining : 1024;
spi_transmit(p, bytes_this_loop);
p += bytes_this_loop;
bytes_remaining -= bytes_this_loop;
}
return byte_count - bytes_remaining;
}
void qp_comms_spi_stop(painter_device_t device) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct qp_comms_spi_config_t *comms_config = (struct qp_comms_spi_config_t *)driver->comms_config;
spi_stop();
writePinHigh(comms_config->chip_select_pin);
}
const struct painter_comms_vtable_t spi_comms_vtable = {
.comms_init = qp_comms_spi_init,
.comms_start = qp_comms_spi_start,
.comms_send = qp_comms_spi_send_data,
.comms_stop = qp_comms_spi_stop,
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SPI with D/C and RST pins
# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
bool qp_comms_spi_dc_reset_init(painter_device_t device) {
if (!qp_comms_spi_init(device)) {
return false;
}
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
// Set up D/C as output low, if specified
if (comms_config->dc_pin != NO_PIN) {
setPinOutput(comms_config->dc_pin);
writePinLow(comms_config->dc_pin);
}
// Set up RST as output, if specified, performing a reset in the process
if (comms_config->reset_pin != NO_PIN) {
setPinOutput(comms_config->reset_pin);
writePinLow(comms_config->reset_pin);
wait_ms(20);
writePinHigh(comms_config->reset_pin);
wait_ms(20);
}
return true;
}
uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
writePinHigh(comms_config->dc_pin);
return qp_comms_spi_send_data(device, data, byte_count);
}
void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct qp_comms_spi_dc_reset_config_t *comms_config = (struct qp_comms_spi_dc_reset_config_t *)driver->comms_config;
writePinLow(comms_config->dc_pin);
spi_write(cmd);
}
void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
for (size_t i = 0; i < sequence_len;) {
uint8_t command = sequence[i];
uint8_t delay = sequence[i + 1];
uint8_t num_bytes = sequence[i + 2];
qp_comms_spi_dc_reset_send_command(device, command);
if (num_bytes > 0) {
qp_comms_spi_dc_reset_send_data(device, &sequence[i + 3], num_bytes);
}
if (delay > 0) {
wait_ms(delay);
}
i += (3 + num_bytes);
}
}
const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable = {
.base =
{
.comms_init = qp_comms_spi_dc_reset_init,
.comms_start = qp_comms_spi_start,
.comms_send = qp_comms_spi_dc_reset_send_data,
.comms_stop = qp_comms_spi_stop,
},
.send_command = qp_comms_spi_dc_reset_send_command,
.bulk_command_sequence = qp_comms_spi_dc_reset_bulk_command_sequence,
};
# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#endif // QUANTUM_PAINTER_SPI_ENABLE

+ 51
- 0
drivers/painter/comms/qp_comms_spi.h View File

@ -0,0 +1,51 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifdef QUANTUM_PAINTER_SPI_ENABLE
# include <stdint.h>
# include "gpio.h"
# include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Base SPI support
struct qp_comms_spi_config_t {
pin_t chip_select_pin;
uint16_t divisor;
bool lsb_first;
int8_t mode;
};
bool qp_comms_spi_init(painter_device_t device);
bool qp_comms_spi_start(painter_device_t device);
uint32_t qp_comms_spi_send_data(painter_device_t device, const void* data, uint32_t byte_count);
void qp_comms_spi_stop(painter_device_t device);
extern const struct painter_comms_vtable_t spi_comms_vtable;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SPI with D/C and RST pins
# ifdef QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
struct qp_comms_spi_dc_reset_config_t {
struct qp_comms_spi_config_t spi_config;
pin_t dc_pin;
pin_t reset_pin;
};
void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd);
uint32_t qp_comms_spi_dc_reset_send_data(painter_device_t device, const void* data, uint32_t byte_count);
void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t* sequence, size_t sequence_len);
extern const struct painter_comms_with_command_vtable_t spi_comms_with_dc_vtable;
# endif // QUANTUM_PAINTER_SPI_DC_RESET_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#endif // QUANTUM_PAINTER_SPI_ENABLE

+ 150
- 0
drivers/painter/gc9a01/qp_gc9a01.c View File

@ -0,0 +1,150 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#include <wait.h>
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_gc9a01.h"
#include "qp_gc9a01_opcodes.h"
#include "qp_tft_panel.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver storage
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
tft_panel_dc_reset_painter_device_t gc9a01_drivers[GC9A01_NUM_DEVICES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Initialization
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool qp_gc9a01_init(painter_device_t device, painter_rotation_t rotation) {
// A lot of these "unknown" opcodes are sourced from other OSS projects and are seemingly required for this display to function.
// clang-format off
const uint8_t gc9a01_init_sequence[] = {
// Command, Delay, N, Data[N]
GC9A01_SET_INTER_REG_ENABLE2, 0, 0,
0xEB, 0, 1, 0x14,
GC9A01_SET_INTER_REG_ENABLE1, 0, 0,
GC9A01_SET_INTER_REG_ENABLE2, 0, 0,
0xEB, 0, 1, 0x14,
0x84, 0, 1, 0x40,
0x85, 0, 1, 0xFF,
0x86, 0, 1, 0xFF,
0x87, 0, 1, 0xFF,
0x88, 0, 1, 0x0A,
0x89, 0, 1, 0x21,
0x8a, 0, 1, 0x00,
0x8b, 0, 1, 0x80,
0x8c, 0, 1, 0x01,
0x8d, 0, 1, 0x01,
0x8e, 0, 1, 0xFF,
0x8f, 0, 1, 0xFF,
GC9A01_SET_FUNCTION_CTL, 0, 2, 0x00, 0x20,
GC9A01_SET_PIX_FMT, 0, 1, 0x55,
0x90, 0, 4, 0x08, 0x08, 0x08, 0x08,
0xBD, 0, 1, 0x06,
0xBC, 0, 1, 0x00,
0xFF, 0, 3, 0x60, 0x01, 0x04,
GC9A01_SET_POWER_CTL_2, 0, 1, 0x13,
GC9A01_SET_POWER_CTL_3, 0, 1, 0x13,
GC9A01_SET_POWER_CTL_4, 0, 1, 0x22,
0xBE, 0, 1, 0x11,
0xE1, 0, 2, 0x10, 0x0E,
0xDF, 0, 3, 0x21, 0x0C, 0x02,
GC9A01_SET_GAMMA1, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A,
GC9A01_SET_GAMMA2, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F,
GC9A01_SET_GAMMA3, 0, 6, 0x45, 0x09, 0x08, 0x08, 0x26, 0x2A,
GC9A01_SET_GAMMA4, 0, 6, 0x43, 0x70, 0x72, 0x36, 0x37, 0x6F,
0xED, 0, 2, 0x1B, 0x0B,
0xAE, 0, 1, 0x77,
0xCD, 0, 1, 0x63,
0x70, 0, 9, 0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03,
GC9A01_SET_FRAME_RATE, 0, 1, 0x34,
0x62, 0, 12, 0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70,
0x63, 0, 12, 0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70,
0x64, 0, 7, 0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07,
0x66, 0, 10, 0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00,
0x67, 0, 10, 0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98,
0x74, 0, 7, 0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00,
0x98, 0, 2, 0x3E, 0x07,
GC9A01_CMD_TEARING_OFF, 0, 0,
GC9A01_CMD_INVERT_OFF, 0, 0,
GC9A01_CMD_SLEEP_OFF, 120, 0,
GC9A01_CMD_DISPLAY_ON, 20, 0
};
// clang-format on
// clang-format on
qp_comms_bulk_command_sequence(device, gc9a01_init_sequence, sizeof(gc9a01_init_sequence));
// Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
const uint8_t madctl[] = {
[QP_ROTATION_0] = GC9A01_MADCTL_BGR,
[QP_ROTATION_90] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MV,
[QP_ROTATION_180] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MX | GC9A01_MADCTL_MY,
[QP_ROTATION_270] = GC9A01_MADCTL_BGR | GC9A01_MADCTL_MV | GC9A01_MADCTL_MY,
};
qp_comms_command_databyte(device, GC9A01_SET_MEM_ACS_CTL, madctl[rotation]);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver vtable
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const struct tft_panel_dc_reset_painter_driver_vtable_t gc9a01_driver_vtable = {
.base =
{
.init = qp_gc9a01_init,
.power = qp_tft_panel_power,
.clear = qp_tft_panel_clear,
.flush = qp_tft_panel_flush,
.pixdata = qp_tft_panel_pixdata,
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert,
.append_pixels = qp_tft_panel_append_pixels,
},
.rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
.num_window_bytes = 2,
.swap_window_coords = false,
.opcodes =
{
.display_on = GC9A01_CMD_DISPLAY_ON,
.display_off = GC9A01_CMD_DISPLAY_OFF,
.set_column_address = GC9A01_SET_COL_ADDR,
.set_row_address = GC9A01_SET_PAGE_ADDR,
.enable_writes = GC9A01_SET_MEM,
},
};
#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE
// Factory function for creating a handle to the ILI9341 device
painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
for (uint32_t i = 0; i < GC9A01_NUM_DEVICES; ++i) {
tft_panel_dc_reset_painter_device_t *driver = &gc9a01_drivers[i];
if (!driver->base.driver_vtable) {
driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&gc9a01_driver_vtable;
driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
driver->base.native_bits_per_pixel = 16; // RGB565
driver->base.panel_width = panel_width;
driver->base.panel_height = panel_height;
driver->base.rotation = QP_ROTATION_0;
driver->base.offset_x = 0;
driver->base.offset_y = 0;
// SPI and other pin configuration
driver->base.comms_config = &driver->spi_dc_reset_config;
driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
driver->spi_dc_reset_config.spi_config.lsb_first = false;
driver->spi_dc_reset_config.spi_config.mode = spi_mode;
driver->spi_dc_reset_config.dc_pin = dc_pin;
driver->spi_dc_reset_config.reset_pin = reset_pin;
return (painter_device_t)driver;
}
}
return NULL;
}
#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE

+ 37
- 0
drivers/painter/gc9a01/qp_gc9a01.h View File

@ -0,0 +1,37 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter GC9A01 configurables (add to your keyboard's config.h)
#ifndef GC9A01_NUM_DEVICES
/**
* @def This controls the maximum number of GC9A01 devices that Quantum Painter can communicate with at any one time.
* Increasing this number allows for multiple displays to be used.
*/
# define GC9A01_NUM_DEVICES 1
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter GC9A01 device factories
#ifdef QUANTUM_PAINTER_GC9A01_SPI_ENABLE
/**
* Factory method for an GC9A01 SPI LCD device.
*
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param chip_select_pin[in] the GPIO pin used for SPI chip select
* @param dc_pin[in] the GPIO pin used for D/C control
* @param reset_pin[in] the GPIO pin used for RST
* @param spi_divisor[in] the SPI divisor to use when communicating with the display
* @param spi_mode[in] the SPI mode to use when communicating with the display
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
#endif // QUANTUM_PAINTER_GC9A01_SPI_ENABLE

+ 78
- 0
drivers/painter/gc9a01/qp_gc9a01_opcodes.h View File

@ -0,0 +1,78 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter GC9A01 command opcodes
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Level 1 command opcodes
#define GC9A01_GET_ID_INFO 0x04 // Get ID information
#define GC9A01_GET_STATUS 0x09 // Get status
#define GC9A01_CMD_SLEEP_ON 0x10 // Enter sleep mode
#define GC9A01_CMD_SLEEP_OFF 0x11 // Exit sleep mode
#define GC9A01_CMD_PARTIAL_ON 0x12 // Enter partial mode
#define GC9A01_CMD_PARTIAL_OFF 0x13 // Exit partial mode
#define GC9A01_CMD_INVERT_ON 0x20 // Enter inverted mode
#define GC9A01_CMD_INVERT_OFF 0x21 // Exit inverted mode
#define GC9A01_CMD_DISPLAY_OFF 0x28 // Disable display
#define GC9A01_CMD_DISPLAY_ON 0x29 // Enable display
#define GC9A01_SET_COL_ADDR 0x2A // Set column address
#define GC9A01_SET_PAGE_ADDR 0x2B // Set page address
#define GC9A01_SET_MEM 0x2C // Set memory
#define GC9A01_SET_PARTIAL_AREA 0x30 // Set partial area
#define GC9A01_SET_VSCROLL 0x33 // Set vertical scroll def
#define GC9A01_CMD_TEARING_ON 0x34 // Tearing line enabled
#define GC9A01_CMD_TEARING_OFF 0x35 // Tearing line disabled
#define GC9A01_SET_MEM_ACS_CTL 0x36 // Set mem access ctl
#define GC9A01_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr
#define GC9A01_CMD_IDLE_OFF 0x38 // Exit idle mode
#define GC9A01_CMD_IDLE_ON 0x39 // Enter idle mode
#define GC9A01_SET_PIX_FMT 0x3A // Set pixel format
#define GC9A01_SET_MEM_CONT 0x3C // Set memory continue
#define GC9A01_SET_TEAR_SCANLINE 0x44 // Set tearing scanline
#define GC9A01_GET_TEAR_SCANLINE 0x45 // Get tearing scanline
#define GC9A01_SET_BRIGHTNESS 0x51 // Set brightness
#define GC9A01_SET_DISPLAY_CTL 0x53 // Set display ctl
#define GC9A01_GET_ID1 0xDA // Get ID1
#define GC9A01_GET_ID2 0xDB // Get ID2
#define GC9A01_GET_ID3 0xDC // Get ID3
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Level 2 command opcodes
#define GC9A01_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl
#define GC9A01_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl
#define GC9A01_SET_FUNCTION_CTL 0xB6 // Set function ctl
#define GC9A01_SET_TEARING_EFFECT 0xBA // Set backlight ctl 3
#define GC9A01_SET_IF_CTL 0xF6 // Set interface control
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Level 3 command opcodes
#define GC9A01_SET_FRAME_RATE 0xE8 // Set frame rate
#define GC9A01_SET_SPI_2DATA 0xE9 // Set frame rate
#define GC9A01_SET_POWER_CTL_1 0xC1 // Set power ctl 1
#define GC9A01_SET_POWER_CTL_2 0xC3 // Set power ctl 2
#define GC9A01_SET_POWER_CTL_3 0xC4 // Set power ctl 3
#define GC9A01_SET_POWER_CTL_4 0xC9 // Set power ctl 4
#define GC9A01_SET_POWER_CTL_7 0xA7 // Set power ctl 7
#define GC9A01_SET_INTER_REG_ENABLE1 0xFE // Enable Inter Register 1
#define GC9A01_SET_INTER_REG_ENABLE2 0xEF // Enable Inter Register 2
#define GC9A01_SET_GAMMA1 0xF0 //
#define GC9A01_SET_GAMMA2 0xF1
#define GC9A01_SET_GAMMA3 0xF2
#define GC9A01_SET_GAMMA4 0xF3
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MADCTL Flags
#define GC9A01_MADCTL_MY 0b10000000
#define GC9A01_MADCTL_MX 0b01000000
#define GC9A01_MADCTL_MV 0b00100000
#define GC9A01_MADCTL_ML 0b00010000
#define GC9A01_MADCTL_RGB 0b00000000
#define GC9A01_MADCTL_BGR 0b00001000
#define GC9A01_MADCTL_MH 0b00000100

+ 121
- 0
drivers/painter/ili9xxx/qp_ili9163.c View File

@ -0,0 +1,121 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_ili9163.h"
#include "qp_ili9xxx_opcodes.h"
#include "qp_tft_panel.h"
#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
# include "qp_comms_spi.h"
#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Common
// Driver storage
tft_panel_dc_reset_painter_device_t ili9163_drivers[ILI9163_NUM_DEVICES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Initialization
bool qp_ili9163_init(painter_device_t device, painter_rotation_t rotation) {
// clang-format off
const uint8_t ili9163_init_sequence[] = {
// Command, Delay, N, Data[N]
ILI9XXX_CMD_RESET, 120, 0,
ILI9XXX_CMD_SLEEP_OFF, 5, 0,
ILI9XXX_SET_PIX_FMT, 0, 1, 0x55,
ILI9XXX_SET_GAMMA, 0, 1, 0x04,
ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x01,
ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0xFF, 0x06,
ILI9XXX_SET_PGAMMA, 0, 15, 0x36, 0x29, 0x12, 0x22, 0x1C, 0x15, 0x42, 0xB7, 0x2F, 0x13, 0x12, 0x0A, 0x11, 0x0B, 0x06,
ILI9XXX_SET_NGAMMA, 0, 15, 0x09, 0x16, 0x2D, 0x0D, 0x13, 0x15, 0x40, 0x48, 0x53, 0x0C, 0x1D, 0x25, 0x2E, 0x34, 0x39,
ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x08, 0x02,
ILI9XXX_SET_POWER_CTL_1, 0, 2, 0x0A, 0x02,
ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x02,
ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x50, 0x63,
ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0x00,
ILI9XXX_CMD_PARTIAL_OFF, 0, 0,
ILI9XXX_CMD_DISPLAY_ON, 20, 0
};
// clang-format on
qp_comms_bulk_command_sequence(device, ili9163_init_sequence, sizeof(ili9163_init_sequence));
// Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
const uint8_t madctl[] = {
[QP_ROTATION_0] = ILI9XXX_MADCTL_BGR,
[QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV,
[QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY,
[QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY,
};
qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver vtable
const struct tft_panel_dc_reset_painter_driver_vtable_t ili9163_driver_vtable = {
.base =
{
.init = qp_ili9163_init,
.power = qp_tft_panel_power,
.clear = qp_tft_panel_clear,
.flush = qp_tft_panel_flush,
.pixdata = qp_tft_panel_pixdata,
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert,
.append_pixels = qp_tft_panel_append_pixels,
},
.rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
.num_window_bytes = 2,
.swap_window_coords = false,
.opcodes =
{
.display_on = ILI9XXX_CMD_DISPLAY_ON,
.display_off = ILI9XXX_CMD_DISPLAY_OFF,
.set_column_address = ILI9XXX_SET_COL_ADDR,
.set_row_address = ILI9XXX_SET_PAGE_ADDR,
.enable_writes = ILI9XXX_SET_MEM,
},
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SPI
#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
// Factory function for creating a handle to the ILI9163 device
painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
for (uint32_t i = 0; i < ILI9163_NUM_DEVICES; ++i) {
tft_panel_dc_reset_painter_device_t *driver = &ili9163_drivers[i];
if (!driver->base.driver_vtable) {
driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9163_driver_vtable;
driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
driver->base.panel_width = panel_width;
driver->base.panel_height = panel_height;
driver->base.rotation = QP_ROTATION_0;
driver->base.offset_x = 0;
driver->base.offset_y = 0;
driver->base.native_bits_per_pixel = 16; // RGB565
// SPI and other pin configuration
driver->base.comms_config = &driver->spi_dc_reset_config;
driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
driver->spi_dc_reset_config.spi_config.lsb_first = false;
driver->spi_dc_reset_config.spi_config.mode = spi_mode;
driver->spi_dc_reset_config.dc_pin = dc_pin;
driver->spi_dc_reset_config.reset_pin = reset_pin;
return (painter_device_t)driver;
}
}
return NULL;
}
#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

+ 37
- 0
drivers/painter/ili9xxx/qp_ili9163.h View File

@ -0,0 +1,37 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ILI9163 configurables (add to your keyboard's config.h)
#ifndef ILI9163_NUM_DEVICES
/**
* @def This controls the maximum number of ILI9163 devices that Quantum Painter can communicate with at any one time.
* Increasing this number allows for multiple displays to be used.
*/
# define ILI9163_NUM_DEVICES 1
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ILI9163 device factories
#ifdef QUANTUM_PAINTER_ILI9163_SPI_ENABLE
/**
* Factory method for an ILI9163 SPI LCD device.
*
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param chip_select_pin[in] the GPIO pin used for SPI chip select
* @param dc_pin[in] the GPIO pin used for D/C control
* @param reset_pin[in] the GPIO pin used for RST
* @param spi_divisor[in] the SPI divisor to use when communicating with the display
* @param spi_mode[in] the SPI mode to use when communicating with the display
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
#endif // QUANTUM_PAINTER_ILI9163_SPI_ENABLE

+ 128
- 0
drivers/painter/ili9xxx/qp_ili9341.c View File

@ -0,0 +1,128 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_ili9341.h"
#include "qp_ili9xxx_opcodes.h"
#include "qp_tft_panel.h"
#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
# include <qp_comms_spi.h>
#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Common
// Driver storage
tft_panel_dc_reset_painter_device_t ili9341_drivers[ILI9341_NUM_DEVICES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Initialization
bool qp_ili9341_init(painter_device_t device, painter_rotation_t rotation) {
// clang-format off
const uint8_t ili9341_init_sequence[] = {
// Command, Delay, N, Data[N]
ILI9XXX_CMD_RESET, 120, 0,
ILI9XXX_CMD_SLEEP_OFF, 5, 0,
ILI9XXX_POWER_CTL_A, 0, 5, 0x39, 0x2C, 0x00, 0x34, 0x02,
ILI9XXX_POWER_CTL_B, 0, 3, 0x00, 0xD9, 0x30,
ILI9XXX_POWER_ON_SEQ_CTL, 0, 4, 0x64, 0x03, 0x12, 0x81,
ILI9XXX_SET_PUMP_RATIO_CTL, 0, 1, 0x20,
ILI9XXX_SET_POWER_CTL_1, 0, 1, 0x26,
ILI9XXX_SET_POWER_CTL_2, 0, 1, 0x11,
ILI9XXX_SET_VCOM_CTL_1, 0, 2, 0x35, 0x3E,
ILI9XXX_SET_VCOM_CTL_2, 0, 1, 0xBE,
ILI9XXX_DRV_TIMING_CTL_A, 0, 3, 0x85, 0x10, 0x7A,
ILI9XXX_DRV_TIMING_CTL_B, 0, 2, 0x00, 0x00,
ILI9XXX_SET_BRIGHTNESS, 0, 1, 0xFF,
ILI9XXX_ENABLE_3_GAMMA, 0, 1, 0x00,
ILI9XXX_SET_GAMMA, 0, 1, 0x01,
ILI9XXX_SET_PGAMMA, 0, 15, 0x0F, 0x29, 0x24, 0x0C, 0x0E, 0x09, 0x4E, 0x78, 0x3C, 0x09, 0x13, 0x05, 0x17, 0x11, 0x00,
ILI9XXX_SET_NGAMMA, 0, 15, 0x00, 0x16, 0x1B, 0x04, 0x11, 0x07, 0x31, 0x33, 0x42, 0x05, 0x0C, 0x0A, 0x28, 0x2F, 0x0F,
ILI9XXX_SET_PIX_FMT, 0, 1, 0x05,
ILI9XXX_SET_FRAME_CTL_NORMAL, 0, 2, 0x00, 0x1B,
ILI9XXX_SET_FUNCTION_CTL, 0, 2, 0x0A, 0xA2,
ILI9XXX_CMD_PARTIAL_OFF, 0, 0,
ILI9XXX_CMD_DISPLAY_ON, 20, 0
};
// clang-format on
qp_comms_bulk_command_sequence(device, ili9341_init_sequence, sizeof(ili9341_init_sequence));
// Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
const uint8_t madctl[] = {
[QP_ROTATION_0] = ILI9XXX_MADCTL_BGR,
[QP_ROTATION_90] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MV,
[QP_ROTATION_180] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MX | ILI9XXX_MADCTL_MY,
[QP_ROTATION_270] = ILI9XXX_MADCTL_BGR | ILI9XXX_MADCTL_MV | ILI9XXX_MADCTL_MY,
};
qp_comms_command_databyte(device, ILI9XXX_SET_MEM_ACS_CTL, madctl[rotation]);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver vtable
const struct tft_panel_dc_reset_painter_driver_vtable_t ili9341_driver_vtable = {
.base =
{
.init = qp_ili9341_init,
.power = qp_tft_panel_power,
.clear = qp_tft_panel_clear,
.flush = qp_tft_panel_flush,
.pixdata = qp_tft_panel_pixdata,
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert,
.append_pixels = qp_tft_panel_append_pixels,
},
.rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
.num_window_bytes = 2,
.swap_window_coords = false,
.opcodes =
{
.display_on = ILI9XXX_CMD_DISPLAY_ON,
.display_off = ILI9XXX_CMD_DISPLAY_OFF,
.set_column_address = ILI9XXX_SET_COL_ADDR,
.set_row_address = ILI9XXX_SET_PAGE_ADDR,
.enable_writes = ILI9XXX_SET_MEM,
},
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SPI
#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
// Factory function for creating a handle to the ILI9341 device
painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
for (uint32_t i = 0; i < ILI9341_NUM_DEVICES; ++i) {
tft_panel_dc_reset_painter_device_t *driver = &ili9341_drivers[i];
if (!driver->base.driver_vtable) {
driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ili9341_driver_vtable;
driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
driver->base.native_bits_per_pixel = 16; // RGB565
driver->base.panel_width = panel_width;
driver->base.panel_height = panel_height;
driver->base.rotation = QP_ROTATION_0;
driver->base.offset_x = 0;
driver->base.offset_y = 0;
// SPI and other pin configuration
driver->base.comms_config = &driver->spi_dc_reset_config;
driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
driver->spi_dc_reset_config.spi_config.lsb_first = false;
driver->spi_dc_reset_config.spi_config.mode = spi_mode;
driver->spi_dc_reset_config.dc_pin = dc_pin;
driver->spi_dc_reset_config.reset_pin = reset_pin;
return (painter_device_t)driver;
}
}
return NULL;
}
#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

+ 37
- 0
drivers/painter/ili9xxx/qp_ili9341.h View File

@ -0,0 +1,37 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ILI9341 configurables (add to your keyboard's config.h)
#ifndef ILI9341_NUM_DEVICES
/**
* @def This controls the maximum number of ILI9341 devices that Quantum Painter can communicate with at any one time.
* Increasing this number allows for multiple displays to be used.
*/
# define ILI9341_NUM_DEVICES 1
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ILI9341 device factories
#ifdef QUANTUM_PAINTER_ILI9341_SPI_ENABLE
/**
* Factory method for an ILI9341 SPI LCD device.
*
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param chip_select_pin[in] the GPIO pin used for SPI chip select
* @param dc_pin[in] the GPIO pin used for D/C control
* @param reset_pin[in] the GPIO pin used for RST
* @param spi_divisor[in] the SPI divisor to use when communicating with the display
* @param spi_mode[in] the SPI mode to use when communicating with the display
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
#endif // QUANTUM_PAINTER_ILI9341_SPI_ENABLE

+ 100
- 0
drivers/painter/ili9xxx/qp_ili9xxx_opcodes.h View File

@ -0,0 +1,100 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ILI9xxx command opcodes
#define ILI9XXX_CMD_NOP 0x00 // No operation
#define ILI9XXX_CMD_RESET 0x01 // Software reset
#define ILI9XXX_GET_ID_INFO 0x04 // Get ID information
#define ILI9XXX_GET_STATUS 0x09 // Get status
#define ILI9XXX_GET_PWR_MODE 0x0A // Get power mode
#define ILI9XXX_GET_MADCTL 0x0B // Get MADCTL
#define ILI9XXX_GET_PIX_FMT 0x0C // Get pixel format
#define ILI9XXX_GET_IMG_FMT 0x0D // Get image format
#define ILI9XXX_GET_SIG_MODE 0x0E // Get signal mode
#define ILI9XXX_GET_SELF_DIAG 0x0F // Get self-diagnostics
#define ILI9XXX_CMD_SLEEP_ON 0x10 // Enter sleep mode
#define ILI9XXX_CMD_SLEEP_OFF 0x11 // Exist sleep mode
#define ILI9XXX_CMD_PARTIAL_ON 0x12 // Enter partial mode
#define ILI9XXX_CMD_PARTIAL_OFF 0x13 // Exit partial mode
#define ILI9XXX_CMD_INVERT_ON 0x20 // Enter inverted mode
#define ILI9XXX_CMD_INVERT_OFF 0x21 // Exit inverted mode
#define ILI9XXX_SET_GAMMA 0x26 // Set gamma params
#define ILI9XXX_CMD_DISPLAY_OFF 0x28 // Disable display
#define ILI9XXX_CMD_DISPLAY_ON 0x29 // Enable display
#define ILI9XXX_SET_COL_ADDR 0x2A // Set column address
#define ILI9XXX_SET_PAGE_ADDR 0x2B // Set page address
#define ILI9XXX_SET_MEM 0x2C // Set memory
#define ILI9XXX_SET_COLOR 0x2D // Set color
#define ILI9XXX_GET_MEM 0x2E // Get memory
#define ILI9XXX_SET_PARTIAL_AREA 0x30 // Set partial area
#define ILI9XXX_SET_VSCROLL 0x33 // Set vertical scroll def
#define ILI9XXX_CMD_TEARING_ON 0x34 // Tearing line enabled
#define ILI9XXX_CMD_TEARING_OFF 0x35 // Tearing line disabled
#define ILI9XXX_SET_MEM_ACS_CTL 0x36 // Set mem access ctl
#define ILI9XXX_SET_VSCROLL_ADDR 0x37 // Set vscroll start addr
#define ILI9XXX_CMD_IDLE_OFF 0x38 // Exit idle mode
#define ILI9XXX_CMD_IDLE_ON 0x39 // Enter idle mode
#define ILI9XXX_SET_PIX_FMT 0x3A // Set pixel format
#define ILI9XXX_SET_MEM_CONT 0x3C // Set memory continue
#define ILI9XXX_GET_MEM_CONT 0x3E // Get memory continue
#define ILI9XXX_SET_TEAR_SCANLINE 0x44 // Set tearing scanline
#define ILI9XXX_GET_TEAR_SCANLINE 0x45 // Get tearing scanline
#define ILI9XXX_SET_BRIGHTNESS 0x51 // Set brightness
#define ILI9XXX_GET_BRIGHTNESS 0x52 // Get brightness
#define ILI9XXX_SET_DISPLAY_CTL 0x53 // Set display ctl
#define ILI9XXX_GET_DISPLAY_CTL 0x54 // Get display ctl
#define ILI9XXX_SET_CABC 0x55 // Set CABC
#define ILI9XXX_GET_CABC 0x56 // Get CABC
#define ILI9XXX_SET_CABC_MIN 0x5E // Set CABC min
#define ILI9XXX_GET_CABC_MIN 0x5F // Set CABC max
#define ILI9XXX_GET_ID1 0xDA // Get ID1
#define ILI9XXX_GET_ID2 0xDB // Get ID2
#define ILI9XXX_GET_ID3 0xDC // Get ID3
#define ILI9XXX_SET_RGB_IF_SIG_CTL 0xB0 // RGB IF signal ctl
#define ILI9XXX_SET_FRAME_CTL_NORMAL 0xB1 // Set frame ctl (normal)
#define ILI9XXX_SET_FRAME_CTL_IDLE 0xB2 // Set frame ctl (idle)
#define ILI9XXX_SET_FRAME_CTL_PARTIAL 0xB3 // Set frame ctl (partial)
#define ILI9XXX_SET_INVERSION_CTL 0xB4 // Set inversion ctl
#define ILI9XXX_SET_BLANKING_PORCH_CTL 0xB5 // Set blanking porch ctl
#define ILI9XXX_SET_FUNCTION_CTL 0xB6 // Set function ctl
#define ILI9XXX_SET_ENTRY_MODE 0xB7 // Set entry mode
#define ILI9XXX_SET_LIGHT_CTL_1 0xB8 // Set backlight ctl 1
#define ILI9XXX_SET_LIGHT_CTL_2 0xB9 // Set backlight ctl 2
#define ILI9XXX_SET_LIGHT_CTL_3 0xBA // Set backlight ctl 3
#define ILI9XXX_SET_LIGHT_CTL_4 0xBB // Set backlight ctl 4
#define ILI9XXX_SET_LIGHT_CTL_5 0xBC // Set backlight ctl 5
#define ILI9XXX_SET_LIGHT_CTL_7 0xBE // Set backlight ctl 7
#define ILI9XXX_SET_LIGHT_CTL_8 0xBF // Set backlight ctl 8
#define ILI9XXX_SET_POWER_CTL_1 0xC0 // Set power ctl 1
#define ILI9XXX_SET_POWER_CTL_2 0xC1 // Set power ctl 2
#define ILI9XXX_SET_VCOM_CTL_1 0xC5 // Set VCOM ctl 1
#define ILI9XXX_SET_VCOM_CTL_2 0xC7 // Set VCOM ctl 2
#define ILI9XXX_POWER_CTL_A 0xCB // Set power control A
#define ILI9XXX_POWER_CTL_B 0xCF // Set power control B
#define ILI9XXX_DRV_TIMING_CTL_A 0xE8 // Set driver timing control A
#define ILI9XXX_DRV_TIMING_CTL_B 0xEA // Set driver timing control B
#define ILI9XXX_POWER_ON_SEQ_CTL 0xED // Set Power on sequence control
#define ILI9XXX_SET_NVMEM 0xD0 // Set NVMEM data
#define ILI9XXX_GET_NVMEM_KEY 0xD1 // Get NVMEM protect key
#define ILI9XXX_GET_NVMEM_STATUS 0xD2 // Get NVMEM status
#define ILI9XXX_GET_ID4 0xD3 // Get ID4
#define ILI9XXX_SET_PGAMMA 0xE0 // Set positive gamma
#define ILI9XXX_SET_NGAMMA 0xE1 // Set negative gamma
#define ILI9XXX_SET_DGAMMA_CTL_1 0xE2 // Set digital gamma ctl 1
#define ILI9XXX_SET_DGAMMA_CTL_2 0xE3 // Set digital gamma ctl 2
#define ILI9XXX_ENABLE_3_GAMMA 0xF2 // Enable 3 gamma
#define ILI9XXX_SET_IF_CTL 0xF6 // Set interface control
#define ILI9XXX_SET_PUMP_RATIO_CTL 0xF7 // Set pump ratio control
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MADCTL Flags
#define ILI9XXX_MADCTL_MY 0b10000000
#define ILI9XXX_MADCTL_MX 0b01000000
#define ILI9XXX_MADCTL_MV 0b00100000
#define ILI9XXX_MADCTL_ML 0b00010000
#define ILI9XXX_MADCTL_RGB 0b00000000
#define ILI9XXX_MADCTL_BGR 0b00001000
#define ILI9XXX_MADCTL_MH 0b00000100

+ 125
- 0
drivers/painter/ssd1351/qp_ssd1351.c View File

@ -0,0 +1,125 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_ssd1351.h"
#include "qp_ssd1351_opcodes.h"
#include "qp_tft_panel.h"
#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
# include "qp_comms_spi.h"
#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Common
// Driver storage
tft_panel_dc_reset_painter_device_t ssd1351_drivers[SSD1351_NUM_DEVICES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Initialization
bool qp_ssd1351_init(painter_device_t device, painter_rotation_t rotation) {
tft_panel_dc_reset_painter_device_t *driver = (tft_panel_dc_reset_painter_device_t *)device;
// clang-format off
const uint8_t ssd1351_init_sequence[] = {
// Command, Delay, N, Data[N]
SSD1351_COMMANDLOCK, 5, 1, 0x12,
SSD1351_COMMANDLOCK, 5, 1, 0xB1,
SSD1351_DISPLAYOFF, 5, 0,
SSD1351_CLOCKDIV, 5, 1, 0xF1,
SSD1351_MUXRATIO, 5, 1, 0x7F,
SSD1351_DISPLAYOFFSET, 5, 1, 0x00,
SSD1351_SETGPIO, 5, 1, 0x00,
SSD1351_FUNCTIONSELECT, 5, 1, 0x01,
SSD1351_PRECHARGE, 5, 1, 0x32,
SSD1351_VCOMH, 5, 1, 0x05,
SSD1351_NORMALDISPLAY, 5, 0,
SSD1351_CONTRASTABC, 5, 3, 0xC8, 0x80, 0xC8,
SSD1351_CONTRASTMASTER, 5, 1, 0x0F,
SSD1351_SETVSL, 5, 3, 0xA0, 0xB5, 0x55,
SSD1351_PRECHARGE2, 5, 1, 0x01,
SSD1351_DISPLAYON, 5, 0,
};
// clang-format on
qp_comms_bulk_command_sequence(device, ssd1351_init_sequence, sizeof(ssd1351_init_sequence));
// Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
const uint8_t madctl[] = {
[QP_ROTATION_0] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MY,
[QP_ROTATION_90] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX | SSD1351_MADCTL_MY | SSD1351_MADCTL_MV,
[QP_ROTATION_180] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MX,
[QP_ROTATION_270] = SSD1351_MADCTL_BGR | SSD1351_MADCTL_MV,
};
qp_comms_command_databyte(device, SSD1351_SETREMAP, madctl[rotation]);
qp_comms_command_databyte(device, SSD1351_STARTLINE, (rotation == QP_ROTATION_0 || rotation == QP_ROTATION_90) ? driver->base.panel_height : 0);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver vtable
const struct tft_panel_dc_reset_painter_driver_vtable_t ssd1351_driver_vtable = {
.base =
{
.init = qp_ssd1351_init,
.power = qp_tft_panel_power,
.clear = qp_tft_panel_clear,
.flush = qp_tft_panel_flush,
.pixdata = qp_tft_panel_pixdata,
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert,
.append_pixels = qp_tft_panel_append_pixels,
},
.rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
.num_window_bytes = 1,
.swap_window_coords = true,
.opcodes =
{
.display_on = SSD1351_DISPLAYON,
.display_off = SSD1351_DISPLAYOFF,
.set_column_address = SSD1351_SETCOLUMN,
.set_row_address = SSD1351_SETROW,
.enable_writes = SSD1351_WRITERAM,
},
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SPI
#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
// Factory function for creating a handle to the SSD1351 device
painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
for (uint32_t i = 0; i < SSD1351_NUM_DEVICES; ++i) {
tft_panel_dc_reset_painter_device_t *driver = &ssd1351_drivers[i];
if (!driver->base.driver_vtable) {
driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&ssd1351_driver_vtable;
driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
driver->base.panel_width = panel_width;
driver->base.panel_height = panel_height;
driver->base.rotation = QP_ROTATION_0;
driver->base.offset_x = 0;
driver->base.offset_y = 0;
driver->base.native_bits_per_pixel = 16; // RGB565
// SPI and other pin configuration
driver->base.comms_config = &driver->spi_dc_reset_config;
driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
driver->spi_dc_reset_config.spi_config.lsb_first = false;
driver->spi_dc_reset_config.spi_config.mode = spi_mode;
driver->spi_dc_reset_config.dc_pin = dc_pin;
driver->spi_dc_reset_config.reset_pin = reset_pin;
return (painter_device_t)driver;
}
}
return NULL;
}
#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

+ 37
- 0
drivers/painter/ssd1351/qp_ssd1351.h View File

@ -0,0 +1,37 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter SSD1351 configurables (add to your keyboard's config.h)
#ifndef SSD1351_NUM_DEVICES
/**
* @def This controls the maximum number of SSD1351 devices that Quantum Painter can communicate with at any one time.
* Increasing this number allows for multiple displays to be used.
*/
# define SSD1351_NUM_DEVICES 1
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter SSD1351 device factories
#ifdef QUANTUM_PAINTER_SSD1351_SPI_ENABLE
/**
* Factory method for an SSD1351 SPI OLED device.
*
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param chip_select_pin[in] the GPIO pin used for SPI chip select
* @param dc_pin[in] the GPIO pin used for D/C control
* @param reset_pin[in] the GPIO pin used for RST
* @param spi_divisor[in] the SPI divisor to use when communicating with the display
* @param spi_mode[in] the SPI mode to use when communicating with the display
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
#endif // QUANTUM_PAINTER_SSD1351_SPI_ENABLE

+ 48
- 0
drivers/painter/ssd1351/qp_ssd1351_opcodes.h View File

@ -0,0 +1,48 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter SSD1351 command opcodes
// System function commands
#define SSD1351_SETCOLUMN 0x15
#define SSD1351_SETROW 0x75
#define SSD1351_WRITERAM 0x5C
#define SSD1351_READRAM 0x5D
#define SSD1351_SETREMAP 0xA0
#define SSD1351_STARTLINE 0xA1
#define SSD1351_DISPLAYOFFSET 0xA2
#define SSD1351_DISPLAYALLOFF 0xA4
#define SSD1351_DISPLAYALLON 0xA5
#define SSD1351_NORMALDISPLAY 0xA6
#define SSD1351_INVERTDISPLAY 0xA7
#define SSD1351_FUNCTIONSELECT 0xAB
#define SSD1351_DISPLAYOFF 0xAE
#define SSD1351_DISPLAYON 0xAF
#define SSD1351_PRECHARGE 0xB1
#define SSD1351_DISPLAYENHANCE 0xB2
#define SSD1351_CLOCKDIV 0xB3
#define SSD1351_SETVSL 0xB4
#define SSD1351_SETGPIO 0xB5
#define SSD1351_PRECHARGE2 0xB6
#define SSD1351_SETGRAY 0xB8
#define SSD1351_USELUT 0xB9
#define SSD1351_PRECHARGELEVEL 0xBB
#define SSD1351_VCOMH 0xBE
#define SSD1351_CONTRASTABC 0xC1
#define SSD1351_CONTRASTMASTER 0xC7
#define SSD1351_MUXRATIO 0xCA
#define SSD1351_COMMANDLOCK 0xFD
#define SSD1351_HORIZSCROLL 0x96
#define SSD1351_STOPSCROLL 0x9E
#define SSD1351_STARTSCROLL 0x9F
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SETREMAP (MADCTL) Flags
#define SSD1351_MADCTL_MY 0b00010000
#define SSD1351_MADCTL_MX 0b00000010
#define SSD1351_MADCTL_MV 0b00000001
#define SSD1351_MADCTL_RGB 0b01100000
#define SSD1351_MADCTL_BGR 0b01100100

+ 144
- 0
drivers/painter/st77xx/qp_st7789.c View File

@ -0,0 +1,144 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_st7789.h"
#include "qp_st77xx_opcodes.h"
#include "qp_st7789_opcodes.h"
#include "qp_tft_panel.h"
#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
# include "qp_comms_spi.h"
#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Common
// Driver storage
tft_panel_dc_reset_painter_device_t st7789_drivers[ST7789_NUM_DEVICES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Automatic viewport offsets
#ifndef ST7789_NO_AUTOMATIC_OFFSETS
static inline void st7789_automatic_viewport_offsets(painter_device_t device, painter_rotation_t rotation) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
// clang-format off
const struct {
uint16_t offset_x;
uint16_t offset_y;
} rotation_offsets_240x240[] = {
[QP_ROTATION_0] = { .offset_x = 0, .offset_y = 0 },
[QP_ROTATION_90] = { .offset_x = 0, .offset_y = 0 },
[QP_ROTATION_180] = { .offset_x = 0, .offset_y = 80 },
[QP_ROTATION_270] = { .offset_x = 80, .offset_y = 0 },
};
// clang-format on
if (driver->panel_width == 240 && driver->panel_height == 240) {
driver->offset_x = rotation_offsets_240x240[rotation].offset_x;
driver->offset_y = rotation_offsets_240x240[rotation].offset_y;
}
}
#endif // ST7789_NO_AUTOMATIC_OFFSETS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Initialization
bool qp_st7789_init(painter_device_t device, painter_rotation_t rotation) {
// clang-format off
const uint8_t st7789_init_sequence[] = {
// Command, Delay, N, Data[N]
ST77XX_CMD_RESET, 120, 0,
ST77XX_CMD_SLEEP_OFF, 5, 0,
ST77XX_SET_PIX_FMT, 0, 1, 0x55,
ST77XX_CMD_INVERT_ON, 0, 0,
ST77XX_CMD_NORMAL_ON, 0, 0,
ST77XX_CMD_DISPLAY_ON, 20, 0
};
// clang-format on
qp_comms_bulk_command_sequence(device, st7789_init_sequence, sizeof(st7789_init_sequence));
// Configure the rotation (i.e. the ordering and direction of memory writes in GRAM)
const uint8_t madctl[] = {
[QP_ROTATION_0] = ST77XX_MADCTL_RGB,
[QP_ROTATION_90] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MV,
[QP_ROTATION_180] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MX | ST77XX_MADCTL_MY,
[QP_ROTATION_270] = ST77XX_MADCTL_RGB | ST77XX_MADCTL_MV | ST77XX_MADCTL_MY,
};
qp_comms_command_databyte(device, ST77XX_SET_MADCTL, madctl[rotation]);
#ifndef ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
st7789_automatic_viewport_offsets(device, rotation);
#endif // ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver vtable
const struct tft_panel_dc_reset_painter_driver_vtable_t st7789_driver_vtable = {
.base =
{
.init = qp_st7789_init,
.power = qp_tft_panel_power,
.clear = qp_tft_panel_clear,
.flush = qp_tft_panel_flush,
.pixdata = qp_tft_panel_pixdata,
.viewport = qp_tft_panel_viewport,
.palette_convert = qp_tft_panel_palette_convert,
.append_pixels = qp_tft_panel_append_pixels,
},
.rgb888_to_native16bit = qp_rgb888_to_rgb565_swapped,
.num_window_bytes = 2,
.swap_window_coords = false,
.opcodes =
{
.display_on = ST77XX_CMD_DISPLAY_ON,
.display_off = ST77XX_CMD_DISPLAY_OFF,
.set_column_address = ST77XX_SET_COL_ADDR,
.set_row_address = ST77XX_SET_ROW_ADDR,
.enable_writes = ST77XX_SET_MEM,
},
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SPI
#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
// Factory function for creating a handle to the ST7789 device
painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode) {
for (uint32_t i = 0; i < ST7789_NUM_DEVICES; ++i) {
tft_panel_dc_reset_painter_device_t *driver = &st7789_drivers[i];
if (!driver->base.driver_vtable) {
driver->base.driver_vtable = (const struct painter_driver_vtable_t *)&st7789_driver_vtable;
driver->base.comms_vtable = (const struct painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
driver->base.panel_width = panel_width;
driver->base.panel_height = panel_height;
driver->base.rotation = QP_ROTATION_0;
driver->base.offset_x = 0;
driver->base.offset_y = 0;
driver->base.native_bits_per_pixel = 16; // RGB565
// SPI and other pin configuration
driver->base.comms_config = &driver->spi_dc_reset_config;
driver->spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
driver->spi_dc_reset_config.spi_config.divisor = spi_divisor;
driver->spi_dc_reset_config.spi_config.lsb_first = false;
driver->spi_dc_reset_config.spi_config.mode = spi_mode;
driver->spi_dc_reset_config.dc_pin = dc_pin;
driver->spi_dc_reset_config.reset_pin = reset_pin;
return (painter_device_t)driver;
}
}
return NULL;
}
#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

+ 44
- 0
drivers/painter/st77xx/qp_st7789.h View File

@ -0,0 +1,44 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ST7789 configurables (add to your keyboard's config.h)
#ifndef ST7789_NUM_DEVICES
/**
* @def This controls the maximum number of ST7789 devices that Quantum Painter can communicate with at any one time.
* Increasing this number allows for multiple displays to be used.
*/
# define ST7789_NUM_DEVICES 1
#endif
// Additional configuration options to be copied to your keyboard's config.h (don't change here):
// If you know exactly which offsets should be used on your panel with respect to selected rotation, then this config
// option allows you to save some flash space -- you'll need to invoke qp_set_viewport_offsets() instead from your keyboard.
// #define ST7789_NO_AUTOMATIC_VIEWPORT_OFFSETS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ST7789 device factories
#ifdef QUANTUM_PAINTER_ST7789_SPI_ENABLE
/**
* Factory method for an ST7789 SPI LCD device.
*
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param chip_select_pin[in] the GPIO pin used for SPI chip select
* @param dc_pin[in] the GPIO pin used for D/C control
* @param reset_pin[in] the GPIO pin used for RST
* @param spi_divisor[in] the SPI divisor to use when communicating with the display
* @param spi_mode[in] the SPI mode to use when communicating with the display
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_height, pin_t chip_select_pin, pin_t dc_pin, pin_t reset_pin, uint16_t spi_divisor, int spi_mode);
#endif // QUANTUM_PAINTER_ST7789_SPI_ENABLE

+ 64
- 0
drivers/painter/st77xx/qp_st7789_opcodes.h View File

@ -0,0 +1,64 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ST7789 additional command opcodes
// System function commands
#define ST7789_GET_SELF_DIAG 0x0F // Get self-diagnostic result
#define ST7789_SET_VERT_SCRL 0x33 // Set vertical scroll definition
#define ST7789_SET_VERT_SCRL_ADDR 0x37 // SEt Vertical scroll start address
#define ST7789_SET_MEM_CONT 0x3C // Memory Write continue
#define ST7789_GET_MEM_CONT 0x3E // Memory Read continue
#define ST7789_SET_TEAR_LINE 0x44 // Set tear scanline
#define ST7789_GET_TEAR_LINE 0x45 // Get tear scanline
#define ST7789_SET_BRIGHTNESS 0x51 // Set display brightness
#define ST7789_GET_BRIGHTNESS 0x52 // Get display brightness
#define ST7789_SET_CTRL 0x53 // Set CTRL display
#define ST7789_GET_CTRL 0x54 // Get CTRL display value
#define ST7789_SET_CAB_COLOR 0x55 // Set content adaptive brightness control and color enhancement
#define ST7789_GET_CAB_COLOR 0x56 // Get content adaptive brightness control and color enhancement
#define ST7789_SET_CAB_BRIGHTNESS 0x5E // Set content adaptive minimum brightness
#define ST7789_GET_CAB_BRIGHTNESS 0x5F // Get content adaptive minimum brightness
#define ST7789_GET_ABC_SELF_DIAG 0x68 // Get Auto brightness control self diagnostics
// Panel Function Commands
#define ST7789_SET_RAM_CTL 0xB0 // Set RAM control
#define ST7789_SET_RGB_CTL 0xB1 // Set RGB control
#define ST7789_SET_PORCH_CTL 0xB2 // Set Porch control
#define ST7789_SET_FRAME_RATE_CTL_1 0xB3 // Set frame rate control 1
#define ST7789_SET_PARTIAL_CTL 0xB5 // Set Partial control
#define ST7789_SET_GATE_CTL 0xB7 // Set gate control
#define ST7789_SET_GATE_ON_TIMING 0xB8 // Set gate on timing adjustment
#define ST7789_SET_DIGITAL_GAMMA_ON 0xBA // Enable digital gamma
#define ST7789_SET_VCOM 0xBB // Set VCOM
#define ST7789_SET_POWER_SAVE 0xBC // Set power saving mode
#define ST7789_SET_DISP_OFF_POWER 0xBD // Set display off power saving
#define ST7789_SET_LCM_CTL 0xC0 // Set LCM control
#define ST7789_SET_IDS 0xC1 // Set IDs
#define ST7789_SET_VDV_VRH_ON 0xC2 // Set VDV and VRH command enable
#define ST7789_SET_VRH 0xC3 // Set VRH
#define ST7789_SET_VDV 0xC4 // Set VDV
#define ST7789_SET_VCOM_OFFSET 0xC5 // Set VCOM offset ctl
#define ST7789_SET_FRAME_RATE_CTL_2 0xC6 // Set frame rate control 2
#define ST7789_SET_CABC_CTL 0xC7 // Set CABC Control
#define ST7789_GET_REG_1 0xC8 // Get register value selection1
#define ST7789_GET_REG_2 0xCA // Get register value selection2
#define ST7789_SET_PWM_FREQ 0xCC // Set PWM frequency
#define ST7789_SET_POWER_CTL_1 0xD0 // Set power ctl 1
#define ST7789_SET_VAP_VAN_ON 0xD2 // Enable VAP/VAN signal output
#define ST7789_SET_CMD2_ENABLE 0xDF // Enable command 2
#define ST7789_SET_PGAMMA 0xE0 // Set positive gamma
#define ST7789_SET_NGAMMA 0xE1 // Set negative gamma
#define ST7789_SET_DIGITAL_GAMMA_RED 0xE2 // Set digital gamma lookup table for red
#define ST7789_SET_DIGITAL_GAMMA_BLUE 0xE3 // Get digital gamma lookup table for blue
#define ST7789_SET_GATE_CTL_2 0xE4 // Set gate control 2
#define ST7789_SET_SPI2_ENABLE 0xE7 // Enable SPI2
#define ST7789_SET_POWER_CTL_2 0xE8 // Set power ctl 2
#define ST7789_SET_EQ_TIME_CTL 0xE9 // Set equalize time control
#define ST7789_SET_PROG_CTL 0xEC // Set program control
#define ST7789_SET_PROG_MODE_ENABLE 0xFA // Set program mode enable
#define ST7789_SET_NVMEM 0xFC // Set NVMEM data
#define ST7789_SET_PROG_ACTION 0xFE // Set program action

+ 51
- 0
drivers/painter/st77xx/qp_st77xx_opcodes.h View File

@ -0,0 +1,51 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter ST77XX command opcodes
// System function commands
#define ST77XX_CMD_NOP 0x00 // No operation
#define ST77XX_CMD_RESET 0x01 // Software reset
#define ST77XX_GET_ID_INFO 0x04 // Get ID information
#define ST77XX_GET_STATUS 0x09 // Get status
#define ST77XX_GET_PWR_MODE 0x0A // Get power mode
#define ST77XX_GET_MADCTL 0x0B // Get mem access ctl
#define ST77XX_GET_PIX_FMT 0x0C // Get pixel format
#define ST77XX_GET_IMG_FMT 0x0D // Get image format
#define ST77XX_GET_SIG_MODE 0x0E // Get signal mode
#define ST77XX_CMD_SLEEP_ON 0x10 // Enter sleep mode
#define ST77XX_CMD_SLEEP_OFF 0x11 // Exist sleep mode
#define ST77XX_CMD_PARTIAL_ON 0x12 // Enter partial mode
#define ST77XX_CMD_NORMAL_ON 0x13 // Exit partial mode
#define ST77XX_CMD_INVERT_OFF 0x20 // Exit inverted mode
#define ST77XX_CMD_INVERT_ON 0x21 // Enter inverted mode
#define ST77XX_SET_GAMMA 0x26 // Set gamma params
#define ST77XX_CMD_DISPLAY_OFF 0x28 // Disable display
#define ST77XX_CMD_DISPLAY_ON 0x29 // Enable display
#define ST77XX_SET_COL_ADDR 0x2A // Set column address
#define ST77XX_SET_ROW_ADDR 0x2B // Set page (row) address
#define ST77XX_SET_MEM 0x2C // Set memory
#define ST77XX_GET_MEM 0x2E // Get memory
#define ST77XX_SET_PARTIAL_AREA 0x30 // Set partial area
#define ST77XX_CMD_TEARING_OFF 0x34 // Tearing line disabled
#define ST77XX_CMD_TEARING_ON 0x35 // Tearing line enabled
#define ST77XX_SET_MADCTL 0x36 // Set mem access ctl
#define ST77XX_CMD_IDLE_OFF 0x38 // Exit idle mode
#define ST77XX_CMD_IDLE_ON 0x39 // Enter idle mode
#define ST77XX_SET_PIX_FMT 0x3A // Set pixel format
#define ST77XX_GET_ID1 0xDA // Get ID1
#define ST77XX_GET_ID2 0xDB // Get ID2
#define ST77XX_GET_ID3 0xDC // Get ID3
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MADCTL Flags
#define ST77XX_MADCTL_MY 0b10000000
#define ST77XX_MADCTL_MX 0b01000000
#define ST77XX_MADCTL_MV 0b00100000
#define ST77XX_MADCTL_ML 0b00010000
#define ST77XX_MADCTL_RGB 0b00000000
#define ST77XX_MADCTL_BGR 0b00001000
#define ST77XX_MADCTL_MH 0b00000100

+ 130
- 0
drivers/painter/tft_panel/qp_tft_panel.c View File

@ -0,0 +1,130 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "color.h"
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_draw.h"
#include "qp_tft_panel.h"
#define BYTE_SWAP(x) (((((uint16_t)(x)) >> 8) & 0x00FF) | ((((uint16_t)(x)) << 8) & 0xFF00))
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Native pixel format conversion
uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b) {
uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3);
return rgb565;
}
uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b) {
uint16_t rgb565 = (((uint16_t)r) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)b) >> 3);
return BYTE_SWAP(rgb565);
}
uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b) {
uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3);
return bgr565;
}
uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b) {
uint16_t bgr565 = (((uint16_t)b) >> 3) << 11 | (((uint16_t)g) >> 2) << 5 | (((uint16_t)r) >> 3);
return BYTE_SWAP(bgr565);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter API implementations
// Power control
bool qp_tft_panel_power(painter_device_t device, bool power_on) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
qp_comms_command(device, power_on ? vtable->opcodes.display_on : vtable->opcodes.display_off);
return true;
}
// Screen clear
bool qp_tft_panel_clear(painter_device_t device) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
driver->driver_vtable->init(device, driver->rotation); // Re-init the LCD
return true;
}
// Screen flush
bool qp_tft_panel_flush(painter_device_t device) {
// No-op, as there's no framebuffer in RAM for this device.
return true;
}
// Viewport to draw to
bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
// Fix up the drawing location if required
left += driver->offset_x;
right += driver->offset_x;
top += driver->offset_y;
bottom += driver->offset_y;
// Check if we need to manually swap the window coordinates based on whether or not we're in a sideways rotation
if (vtable->swap_window_coords && (driver->rotation == QP_ROTATION_90 || driver->rotation == QP_ROTATION_270)) {
uint16_t temp;
temp = left;
left = top;
top = temp;
temp = right;
right = bottom;
bottom = temp;
}
if (vtable->num_window_bytes == 1) {
// Set up the x-window
uint8_t xbuf[2] = {left & 0xFF, right & 0xFF};
qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf));
// Set up the y-window
uint8_t ybuf[2] = {top & 0xFF, bottom & 0xFF};
qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf));
} else if (vtable->num_window_bytes == 2) {
// Set up the x-window
uint8_t xbuf[4] = {left >> 8, left & 0xFF, right >> 8, right & 0xFF};
qp_comms_command_databuf(device, vtable->opcodes.set_column_address, xbuf, sizeof(xbuf));
// Set up the y-window
uint8_t ybuf[4] = {top >> 8, top & 0xFF, bottom >> 8, bottom & 0xFF};
qp_comms_command_databuf(device, vtable->opcodes.set_row_address, ybuf, sizeof(ybuf));
}
// Lock in the window
qp_comms_command(device, vtable->opcodes.enable_writes);
return true;
}
// Stream pixel data to the current write position in GRAM
bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
qp_comms_send(device, pixel_data, native_pixel_count * sizeof(uint16_t));
return true;
}
// Convert supplied palette entries into their native equivalents
bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) {
struct painter_driver_t * driver = (struct painter_driver_t *)device;
struct tft_panel_dc_reset_painter_driver_vtable_t *vtable = (struct tft_panel_dc_reset_painter_driver_vtable_t *)driver->driver_vtable;
for (int16_t i = 0; i < palette_size; ++i) {
RGB rgb = hsv_to_rgb_nocie((HSV){palette[i].hsv888.h, palette[i].hsv888.s, palette[i].hsv888.v});
palette[i].rgb565 = vtable->rgb888_to_native16bit(rgb.r, rgb.g, rgb.b);
}
return true;
}
// Append pixels to the target location, keyed by the pixel index
bool qp_tft_panel_append_pixels(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices) {
uint16_t *buf = (uint16_t *)target_buffer;
for (uint32_t i = 0; i < pixel_count; ++i) {
buf[pixel_offset + i] = palette[palette_indices[i]].rgb565;
}
return true;
}

+ 67
- 0
drivers/painter/tft_panel/qp_tft_panel.h View File

@ -0,0 +1,67 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "color.h"
#include "qp_internal.h"
#ifdef QUANTUM_PAINTER_SPI_ENABLE
# include "qp_comms_spi.h"
#endif // QUANTUM_PAINTER_SPI_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Common TFT panel implementation using D/C, and RST pins.
typedef uint16_t (*rgb888_to_native_uint16_t)(uint8_t r, uint8_t g, uint8_t b);
// Driver vtable with extras
struct tft_panel_dc_reset_painter_driver_vtable_t {
struct painter_driver_vtable_t base; // must be first, so it can be cast to/from the painter_driver_vtable_t* type
// Conversion function for palette entries
rgb888_to_native_uint16_t rgb888_to_native16bit;
// Number of bytes for transmitting x/y coordinates
uint8_t num_window_bytes;
// Whether or not the x/y coords should be swapped on 90/270 rotation
bool swap_window_coords;
// Opcodes for normal display operation
struct {
uint8_t display_on;
uint8_t display_off;
uint8_t set_column_address;
uint8_t set_row_address;
uint8_t enable_writes;
} opcodes;
};
// Device definition
typedef struct tft_panel_dc_reset_painter_device_t {
struct painter_driver_t base; // must be first, so it can be cast to/from the painter_device_t* type
union {
#ifdef QUANTUM_PAINTER_SPI_ENABLE
// SPI-based configurables
struct qp_comms_spi_dc_reset_config_t spi_dc_reset_config;
#endif // QUANTUM_PAINTER_SPI_ENABLE
// TODO: I2C/parallel etc.
};
} tft_panel_dc_reset_painter_device_t;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Forward declarations for injecting into concrete driver vtables
bool qp_tft_panel_power(painter_device_t device, bool power_on);
bool qp_tft_panel_clear(painter_device_t device);
bool qp_tft_panel_flush(painter_device_t device);
bool qp_tft_panel_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
bool qp_tft_panel_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
bool qp_tft_panel_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette);
bool qp_tft_panel_append_pixels(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices);
uint16_t qp_rgb888_to_rgb565(uint8_t r, uint8_t g, uint8_t b);
uint16_t qp_rgb888_to_rgb565_swapped(uint8_t r, uint8_t g, uint8_t b);
uint16_t qp_rgb888_to_bgr565(uint8_t r, uint8_t g, uint8_t b);
uint16_t qp_rgb888_to_bgr565_swapped(uint8_t r, uint8_t g, uint8_t b);

+ 3
- 1
lib/python/qmk/cli/__init__.py View File

@ -16,7 +16,8 @@ import_names = {
# A mapping of package name to importable name
'pep8-naming': 'pep8ext_naming',
'pyusb': 'usb.core',
'qmk-dotty-dict': 'dotty_dict'
'qmk-dotty-dict': 'dotty_dict',
'pillow': 'PIL'
}
safe_commands = [
@ -67,6 +68,7 @@ subcommands = [
'qmk.cli.multibuild',
'qmk.cli.new.keyboard',
'qmk.cli.new.keymap',
'qmk.cli.painter',
'qmk.cli.pyformat',
'qmk.cli.pytest',
'qmk.cli.via2json',


+ 2
- 0
lib/python/qmk/cli/painter/__init__.py View File

@ -0,0 +1,2 @@
from . import convert_graphics
from . import make_font

+ 86
- 0
lib/python/qmk/cli/painter/convert_graphics.py View File

@ -0,0 +1,86 @@
"""This script tests QGF functionality.
"""
import re
import datetime
from io import BytesIO
from qmk.path import normpath
from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats
from milc import cli
from PIL import Image
@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.')
@cli.argument('-i', '--input', required=True, help='Specify input graphic file.')
@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys())))
@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.')
@cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.')
@cli.subcommand('Converts an input image to something QMK understands')
def painter_convert_graphics(cli):
"""Converts an image file to a format that Quantum Painter understands.
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`.
"""
# Work out the input file
if cli.args.input != '-':
cli.args.input = normpath(cli.args.input)
# Error checking
if not cli.args.input.exists():
cli.log.error('Input image file does not exist!')
cli.print_usage()
return False
# Work out the output directory
if len(cli.args.output) == 0:
cli.args.output = cli.args.input.parent
cli.args.output = normpath(cli.args.output)
# Ensure we have a valid format
if cli.args.format not in valid_formats.keys():
cli.log.error('Output format %s is invalid. Allowed values: %s' % (cli.args.format, ', '.join(valid_formats.keys())))
cli.print_usage()
return False
# Work out the encoding parameters
format = valid_formats[cli.args.format]
# Load the input image
input_img = Image.open(cli.args.input)
# Convert the image to QGF using PIL
out_data = BytesIO()
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)
out_bytes = out_data.getvalue()
# Work out the text substitutions for rendering the output data
subs = {
'generated_type': 'image',
'var_prefix': 'gfx',
'generator_command': f'qmk painter-convert-graphics -i {cli.args.input.name} -f {cli.args.format}',
'year': datetime.date.today().strftime("%Y"),
'input_file': cli.args.input.name,
'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
'byte_count': len(out_bytes),
'bytes_lines': render_bytes(out_bytes),
'format': cli.args.format,
}
# Render the license
subs.update({'license': render_license(subs)})
# Render and write the header file
header_text = render_header(subs)
header_file = cli.args.output / (cli.args.input.stem + ".qgf.h")
with open(header_file, 'w') as header:
print(f"Writing {header_file}...")
header.write(header_text)
header.close()
# Render and write the source file
source_text = render_source(subs)
source_file = cli.args.output / (cli.args.input.stem + ".qgf.c")
with open(source_file, 'w') as source:
print(f"Writing {source_file}...")
source.write(source_text)
source.close()

+ 87
- 0
lib/python/qmk/cli/painter/make_font.py View File

@ -0,0 +1,87 @@
"""This script automates the conversion of font files into a format QMK firmware understands.
"""
import re
import datetime
from io import BytesIO
from qmk.path import normpath
from qmk.painter_qff import QFFFont
from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats
from milc import cli
@cli.argument('-f', '--font', required=True, help='Specify input font file.')
@cli.argument('-o', '--output', required=True, help='Specify output image path.')
@cli.argument('-s', '--size', default=12, help='Specify font size. Default 12.')
@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.')
@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
@cli.argument('-a', '--no-aa', arg_only=True, action='store_true', help='Disable anti-aliasing on fonts.')
@cli.subcommand('Converts an input font to something QMK understands')
def painter_make_font_image(cli):
# Create the font object
font = QFFFont(cli)
# Read from the input file
cli.args.font = normpath(cli.args.font)
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))
# Render out the data
font.save_to_image(normpath(cli.args.output))
@cli.argument('-i', '--input', help='Specify input graphic file.')
@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
@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.')
@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys())))
@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.')
@cli.subcommand('Converts an input font image to something QMK firmware understands')
def painter_convert_font_image(cli):
# Work out the format
format = valid_formats[cli.args.format]
# Create the font object
font = QFFFont(cli.log)
# Read from the input file
cli.args.input = normpath(cli.args.input)
font.read_from_image(cli.args.input, include_ascii_glyphs=(not cli.args.no_ascii), unicode_glyphs=cli.args.unicode_glyphs)
# Work out the output directory
if len(cli.args.output) == 0:
cli.args.output = cli.args.input.parent
cli.args.output = normpath(cli.args.output)
# Render out the data
out_data = BytesIO()
font.save_to_qff(format, (False if cli.args.no_rle else True), out_data)
# Work out the text substitutions for rendering the output data
subs = {
'generated_type': 'font',
'var_prefix': 'font',
'generator_command': f'qmk painter-convert-font-image -i {cli.args.input.name} -f {cli.args.format}',
'year': datetime.date.today().strftime("%Y"),
'input_file': cli.args.input.name,
'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
'byte_count': out_data.getbuffer().nbytes,
'bytes_lines': render_bytes(out_data.getbuffer().tobytes()),
'format': cli.args.format,
}
# Render the license
subs.update({'license': render_license(subs)})
# Render and write the header file
header_text = render_header(subs)
header_file = cli.args.output / (cli.args.input.stem + ".qff.h")
with open(header_file, 'w') as header:
print(f"Writing {header_file}...")
header.write(header_text)
header.close()
# Render and write the source file
source_text = render_source(subs)
source_file = cli.args.output / (cli.args.input.stem + ".qff.c")
with open(source_file, 'w') as source:
print(f"Writing {source_file}...")
source.write(source_text)
source.close()

+ 268
- 0
lib/python/qmk/painter.py View File

@ -0,0 +1,268 @@
"""Functions that help us work with Quantum Painter's file formats.
"""
import math
import re
from string import Template
from PIL import Image, ImageOps
# The list of valid formats Quantum Painter supports
valid_formats = {
'pal256': {
'image_format': 'IMAGE_FORMAT_PALETTE',
'bpp': 8,
'has_palette': True,
'num_colors': 256,
'image_format_byte': 0x07, # see qp_internal_formats.h
},
'pal16': {
'image_format': 'IMAGE_FORMAT_PALETTE',
'bpp': 4,
'has_palette': True,
'num_colors': 16,
'image_format_byte': 0x06, # see qp_internal_formats.h
},
'pal4': {
'image_format': 'IMAGE_FORMAT_PALETTE',
'bpp': 2,
'has_palette': True,
'num_colors': 4,
'image_format_byte': 0x05, # see qp_internal_formats.h
},
'pal2': {
'image_format': 'IMAGE_FORMAT_PALETTE',
'bpp': 1,
'has_palette': True,
'num_colors': 2,
'image_format_byte': 0x04, # see qp_internal_formats.h
},
'mono256': {
'image_format': 'IMAGE_FORMAT_GRAYSCALE',
'bpp': 8,
'has_palette': False,
'num_colors': 256,
'image_format_byte': 0x03, # see qp_internal_formats.h
},
'mono16': {
'image_format': 'IMAGE_FORMAT_GRAYSCALE',
'bpp': 4,
'has_palette': False,
'num_colors': 16,
'image_format_byte': 0x02, # see qp_internal_formats.h
},
'mono4': {
'image_format': 'IMAGE_FORMAT_GRAYSCALE',
'bpp': 2,
'has_palette': False,
'num_colors': 4,
'image_format_byte': 0x01, # see qp_internal_formats.h
},
'mono2': {
'image_format': 'IMAGE_FORMAT_GRAYSCALE',
'bpp': 1,
'has_palette': False,
'num_colors': 2,
'image_format_byte': 0x00, # see qp_internal_formats.h
}
}
license_template = """\
// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright
// SPDX-License-Identifier: GPL-2.0-or-later
// This file was auto-generated by `${generator_command}`
"""
def render_license(subs):
license_txt = Template(license_template)
return license_txt.substitute(subs)
header_file_template = """\
${license}
#pragma once
#include <qp.h>
extern const uint32_t ${var_prefix}_${sane_name}_length;
extern const uint8_t ${var_prefix}_${sane_name}[${byte_count}];
"""
def render_header(subs):
header_txt = Template(header_file_template)
return header_txt.substitute(subs)
source_file_template = """\
${license}
#include <qp.h>
const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count};
// clang-format off
const uint8_t ${var_prefix}_${sane_name}[${byte_count}] = {
${bytes_lines}
};
// clang-format on
"""
def render_source(subs):
source_txt = Template(source_file_template)
return source_txt.substitute(subs)
def render_bytes(bytes, newline_after=16):
lines = ''
for n in range(len(bytes)):
if n % newline_after == 0 and n > 0 and n != len(bytes):
lines = lines + "\n "
elif n == 0:
lines = lines + " "
lines = lines + " 0x{0:02X},".format(bytes[n])
return lines.rstrip()
def clean_output(str):
str = re.sub(r'\r', '', str)
str = re.sub(r'[\n]{3,}', r'\n\n', str)
return str
def rescale_byte(val, maxval):
"""Rescales a byte value to the supplied range, i.e. [0,255] -> [0,maxval].
"""
return int(round(val * maxval / 255.0))
def convert_requested_format(im, format):
"""Convert an image to the requested format.
"""
# Work out the requested format
ncolors = format["num_colors"]
image_format = format["image_format"]
# Ensure we have a valid number of colors for the palette
if ncolors <= 0 or ncolors > 256 or (ncolors & (ncolors - 1) != 0):
raise ValueError("Number of colors must be 2, 4, 16, or 256.")
# Work out where we're getting the bytes from
if image_format == 'IMAGE_FORMAT_GRAYSCALE':
# If mono, convert input to grayscale, then to RGB, then grab the raw bytes corresponding to the intensity of the red channel
im = ImageOps.grayscale(im)
im = im.convert("RGB")
elif image_format == 'IMAGE_FORMAT_PALETTE':
# If color, convert input to RGB, palettize based on the supplied number of colors, then get the raw palette bytes
im = im.convert("RGB")
im = im.convert("P", palette=Image.ADAPTIVE, colors=ncolors)
return im
def convert_image_bytes(im, format):
"""Convert the supplied image to the equivalent bytes required by the QMK firmware.
"""
# Work out the requested format
ncolors = format["num_colors"]
image_format = format["image_format"]
shifter = int(math.log2(ncolors))
pixels_per_byte = int(8 / math.log2(ncolors))
(width, height) = im.size
expected_byte_count = ((width * height) + (pixels_per_byte - 1)) // pixels_per_byte
if image_format == 'IMAGE_FORMAT_GRAYSCALE':
# Take the red channel
image_bytes = im.tobytes("raw", "R")
image_bytes_len = len(image_bytes)
# No palette
palette = None
bytearray = []
for x in range(expected_byte_count):
byte = 0
for n in range(pixels_per_byte):
byte_offset = x * pixels_per_byte + n
if byte_offset < image_bytes_len:
# If mono, each input byte is a grayscale [0,255] pixel -- rescale to the range we want then pack together
byte = byte | (rescale_byte(image_bytes[byte_offset], ncolors - 1) << int(n * shifter))
bytearray.append(byte)
elif image_format == 'IMAGE_FORMAT_PALETTE':
# Convert each pixel to the palette bytes
image_bytes = im.tobytes("raw", "P")
image_bytes_len = len(image_bytes)
# Export the palette
palette = []
pal = im.getpalette()
for n in range(0, ncolors * 3, 3):
palette.append((pal[n + 0], pal[n + 1], pal[n + 2]))
bytearray = []
for x in range(expected_byte_count):
byte = 0
for n in range(pixels_per_byte):
byte_offset = x * pixels_per_byte + n
if byte_offset < image_bytes_len:
# If color, each input byte is the index into the color palette -- pack them together
byte = byte | ((image_bytes[byte_offset] & (ncolors - 1)) << int(n * shifter))
bytearray.append(byte)
if len(bytearray) != expected_byte_count:
raise Exception(f"Wrong byte count, was {len(bytearray)}, expected {expected_byte_count}")
return (palette, bytearray)
def compress_bytes_qmk_rle(bytearray):
debug_dump = False
output = []
temp = []
repeat = False
def append_byte(c):
if debug_dump:
print('Appending byte:', '0x{0:02X}'.format(int(c)), '=', c)
output.append(c)
def append_range(r):
append_byte(127 + len(r))
if debug_dump:
print('Appending {0} byte(s):'.format(len(r)), '[', ', '.join(['{0:02X}'.format(e) for e in r]), ']')
output.extend(r)
for n in range(0, len(bytearray) + 1):
end = True if n == len(bytearray) else False
if not end:
c = bytearray[n]
temp.append(c)
if len(temp) <= 1:
continue
if debug_dump:
print('Temp buffer state {0:3d} bytes:'.format(len(temp)), '[', ', '.join(['{0:02X}'.format(e) for e in temp]), ']')
if repeat:
if temp[-1] != temp[-2]:
repeat = False
if not repeat or len(temp) == 128 or end:
append_byte(len(temp) if end else len(temp) - 1)
append_byte(temp[0])
temp = [temp[-1]]
repeat = False
else:
if len(temp) >= 2 and temp[-1] == temp[-2]:
repeat = True
if len(temp) > 2:
append_range(temp[0:(len(temp) - 2)])
temp = [temp[-1], temp[-1]]
continue
if len(temp) == 128 or end:
append_range(temp)
temp = []
repeat = False
return output

+ 401
- 0
lib/python/qmk/painter_qff.py View File

@ -0,0 +1,401 @@
# Copyright 2021 Nick Brassel (@tzarc)
# SPDX-License-Identifier: GPL-2.0-or-later
# Quantum Font File "QFF" Font File Format.
# See https://docs.qmk.fm/#/quantum_painter_qff for more information.
from pathlib import Path
from typing import Dict, Any
from colorsys import rgb_to_hsv
from PIL import Image, ImageDraw, ImageFont, ImageChops
from PIL._binary import o8, o16le as o16, o32le as o32
from qmk.painter_qgf import QGFBlockHeader, QGFFramePaletteDescriptorV1
from milc.attrdict import AttrDict
import qmk.painter
def o24(i):
return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
########################################################################################################################
class QFFGlyphInfo(AttrDict):
def __init__(self, *args, **kwargs):
super().__init__()
for n, value in enumerate(args):
self[f'arg:{n}'] = value
for key, value in kwargs.items():
self[key] = value
def write(self, fp, include_code_point):
if include_code_point is True:
fp.write(o24(ord(self.code_point)))
value = ((self.data_offset << 6) & 0xFFFFC0) | (self.w & 0x3F)
fp.write(o24(value))
########################################################################################################################
class QFFFontDescriptor:
type_id = 0x00
length = 20
magic = 0x464651
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QFFFontDescriptor.type_id
self.header.length = QFFFontDescriptor.length
self.version = 1
self.total_file_size = 0
self.line_height = 0
self.has_ascii_table = False
self.unicode_glyph_count = 0
self.format = 0xFF
self.flags = 0
self.compression = 0xFF
self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
def write(self, fp):
self.header.write(fp)
fp.write(
b'' # start off with empty bytes...
+ o24(QFFFontDescriptor.magic) # magic
+ o8(self.version) # version
+ o32(self.total_file_size) # file size
+ o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size
+ o8(self.line_height) # line height
+ o8(1 if self.has_ascii_table is True else 0) # whether or not we have an ascii table present
+ o16(self.unicode_glyph_count & 0xFFFF) # number of unicode glyphs present
+ o8(self.format) # format
+ o8(self.flags) # flags
+ o8(self.compression) # compression
+ o8(self.transparency_index) # transparency index
)
@property
def is_transparent(self):
return (self.flags & 0x01) == 0x01
@is_transparent.setter
def is_transparent(self, val):
if val:
self.flags |= 0x01
else:
self.flags &= ~0x01
########################################################################################################################
class QFFAsciiGlyphTableV1:
type_id = 0x01
length = 95 * 3 # We have 95 glyphs: [0x20...0x7E]
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QFFAsciiGlyphTableV1.type_id
self.header.length = QFFAsciiGlyphTableV1.length
# Each glyph is key=code_point, value=QFFGlyphInfo
self.glyphs = {}
def add_glyph(self, glyph: QFFGlyphInfo):
self.glyphs[ord(glyph.code_point)] = glyph
def write(self, fp):
self.header.write(fp)
for n in range(0x20, 0x7F):
self.glyphs[n].write(fp, False)
########################################################################################################################
class QFFUnicodeGlyphTableV1:
type_id = 0x02
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QFFUnicodeGlyphTableV1.type_id
self.header.length = 0
# Each glyph is key=code_point, value=QFFGlyphInfo
self.glyphs = {}
def add_glyph(self, glyph: QFFGlyphInfo):
self.glyphs[ord(glyph.code_point)] = glyph
def write(self, fp):
self.header.length = len(self.glyphs.keys()) * 6
self.header.write(fp)
for n in sorted(self.glyphs.keys()):
self.glyphs[n].write(fp, True)
########################################################################################################################
class QFFFontDataDescriptorV1:
type_id = 0x04
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QFFFontDataDescriptorV1.type_id
self.data = []
def write(self, fp):
self.header.length = len(self.data)
self.header.write(fp)
fp.write(bytes(self.data))
########################################################################################################################
def _generate_font_glyphs_list(use_ascii, unicode_glyphs):
# The set of glyphs that we want to generate images for
glyphs = {}
# Add ascii charset if requested
if use_ascii is True:
for c in range(0x20, 0x7F): # does not include 0x7F!
glyphs[chr(c)] = True
# Append any extra unicode glyphs
unicode_glyphs = list(unicode_glyphs)
for c in unicode_glyphs:
glyphs[c] = True
return sorted(glyphs.keys())
class QFFFont:
def __init__(self, logger):
self.logger = logger
self.image = None
self.glyph_data = {}
self.glyph_height = 0
return
def _extract_glyphs(self, format):
total_data_size = 0
total_rle_data_size = 0
converted_img = qmk.painter.convert_requested_format(self.image, format)
(self.palette, _) = qmk.painter.convert_image_bytes(converted_img, format)
# Work out how many bytes used for RLE vs. non-RLE
for _, glyph_entry in self.glyph_data.items():
glyph_img = converted_img.crop((glyph_entry.x, 1, glyph_entry.x + glyph_entry.w, 1 + self.glyph_height))
(_, this_glyph_image_bytes) = qmk.painter.convert_image_bytes(glyph_img, format)
this_glyph_rle_bytes = qmk.painter.compress_bytes_qmk_rle(this_glyph_image_bytes)
total_data_size += len(this_glyph_image_bytes)
total_rle_data_size += len(this_glyph_rle_bytes)
glyph_entry['image_uncompressed_bytes'] = this_glyph_image_bytes
glyph_entry['image_compressed_bytes'] = this_glyph_rle_bytes
return (total_data_size, total_rle_data_size)
def _parse_image(self, img, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''):
# Clear out any existing font metadata
self.image = None
# Each glyph is key=code_point, value={ x: ?, w: ? }
self.glyph_data = {}
self.glyph_height = 0
# Work out the list of glyphs required
glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs)
# Work out the geometry
(width, height) = img.size
# Work out the glyph offsets/widths
glyph_pixel_offsets = []
glyph_pixel_widths = []
pixels = img.load()
# Run through the markers and work out where each glyph starts/stops
glyph_split_color = pixels[0, 0] # top left pixel is the marker color we're going to use to split each glyph
glyph_pixel_offsets.append(0)
last_offset = 0
for x in range(1, width):
if pixels[x, 0] == glyph_split_color:
glyph_pixel_offsets.append(x)
glyph_pixel_widths.append(x - last_offset)
last_offset = x
glyph_pixel_widths.append(width - last_offset)
# Make sure the number of glyphs we're attempting to generate matches the input image
if len(glyph_pixel_offsets) != len(glyphs):
self.logger.error('The number of glyphs to generate doesn\'t match the number of detected glyphs in the input image.')
return
# Set up the required metadata for each glyph
for n in range(0, len(glyph_pixel_offsets)):
self.glyph_data[glyphs[n]] = QFFGlyphInfo(code_point=glyphs[n], x=glyph_pixel_offsets[n], w=glyph_pixel_widths[n])
# Parsing was successful, keep the image in this instance
self.image = img
self.glyph_height = height - 1 # subtract the line with the markers
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):
# Load the font
font = ImageFont.truetype(str(ttf_file), int(font_size))
# Work out the max font size
max_font_size = font.font.ascent + abs(font.font.descent)
# Work out the list of glyphs required
glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs)
baseline_offset = 9999999
total_glyph_width = 0
max_glyph_height = -1
# Measure each glyph to determine the overall baseline offset required
for glyph in glyphs:
(ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls')
glyph_width = (ls_r - ls_l) if include_before_left else (ls_r)
glyph_height = font.getbbox(glyph, anchor='la')[3]
if max_glyph_height < glyph_height:
max_glyph_height = glyph_height
total_glyph_width += glyph_width
if baseline_offset > ls_t:
baseline_offset = ls_t
# Create the output image
img = Image.new("RGB", (total_glyph_width + 1, max_font_size * 2 + 1), (0, 0, 0, 255))
cur_x_pos = 0
# Loop through each glyph...
for glyph in glyphs:
# Work out this glyph's bounding box
(ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls')
glyph_width = (ls_r - ls_l) if include_before_left else (ls_r)
glyph_height = ls_b - ls_t
x_offset = -ls_l
y_offset = ls_t - baseline_offset
# Draw each glyph to its own image so we don't get anti-aliasing applied to the final image when straddling edges
glyph_img = Image.new("RGB", (glyph_width, max_font_size), (0, 0, 0, 255))
glyph_draw = ImageDraw.Draw(glyph_img)
if not use_aa:
glyph_draw.fontmode = "1"
glyph_draw.text((x_offset, y_offset), glyph, font=font, anchor='lt')
# Place the glyph-specific image in the correct location overall
img.paste(glyph_img, (cur_x_pos, 1))
# Set up the marker for start of each glyph
pixels = img.load()
pixels[cur_x_pos, 0] = (255, 0, 255)
# Increment for the next glyph's position
cur_x_pos += glyph_width
# Add the ending marker so that the difference/crop works
pixels = img.load()
pixels[cur_x_pos, 0] = (255, 0, 255)
# Determine the usable font area
dummy_img = Image.new("RGB", (total_glyph_width + 1, max_font_size + 1), (0, 0, 0, 255))
bbox = ImageChops.difference(img, dummy_img).getbbox()
bbox = (bbox[0], bbox[1], bbox[2] - 1, bbox[3]) # remove the unused end-marker
# Crop and re-parse the resulting image to ensure we're generating the correct format
self._parse_image(img.crop(bbox), include_ascii_glyphs, unicode_glyphs)
def save_to_image(self, img_file: Path):
# Drop out if there's no image loaded
if self.image is None:
self.logger.error('No image is loaded.')
return
# Save the image to the supplied file
self.image.save(str(img_file))
def read_from_image(self, img_file: Path, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''):
# Load and parse the supplied image file
self._parse_image(Image.open(str(img_file)), include_ascii_glyphs, unicode_glyphs)
return
def save_to_qff(self, format: Dict[str, Any], use_rle: bool, fp):
# Drop out if there's no image loaded
if self.image is None:
self.logger.error('No image is loaded.')
return
# Work out if we want to use RLE at all, skipping it if it's not any smaller (it's applied per-glyph)
(total_data_size, total_rle_data_size) = self._extract_glyphs(format)
if use_rle:
use_rle = (total_rle_data_size < total_data_size)
# For each glyph, work out which image data we want to use and append it to the image buffer, recording the byte-wise offset
img_buffer = bytes()
for _, glyph_entry in self.glyph_data.items():
glyph_entry['data_offset'] = len(img_buffer)
glyph_img_bytes = glyph_entry.image_compressed_bytes if use_rle else glyph_entry.image_uncompressed_bytes
img_buffer += bytes(glyph_img_bytes)
font_descriptor = QFFFontDescriptor()
ascii_table = QFFAsciiGlyphTableV1()
unicode_table = QFFUnicodeGlyphTableV1()
data_descriptor = QFFFontDataDescriptorV1()
data_descriptor.data = img_buffer
# Check if we have all the ASCII glyphs present
include_ascii_glyphs = all([chr(n) in self.glyph_data for n in range(0x20, 0x7F)])
# Helper for populating the blocks
for code_point, glyph_entry in self.glyph_data.items():
if ord(code_point) >= 0x20 and ord(code_point) <= 0x7E and include_ascii_glyphs:
ascii_table.add_glyph(glyph_entry)
else:
unicode_table.add_glyph(glyph_entry)
# Configure the font descriptor
font_descriptor.line_height = self.glyph_height
font_descriptor.has_ascii_table = include_ascii_glyphs
font_descriptor.unicode_glyph_count = len(unicode_table.glyphs.keys())
font_descriptor.is_transparent = False
font_descriptor.format = format['image_format_byte']
font_descriptor.compression = 0x01 if use_rle else 0x00
# Write a dummy font descriptor -- we'll have to come back and write it properly once we've rendered out everything else
font_descriptor_location = fp.tell()
font_descriptor.write(fp)
# Write out the ASCII table if required
if font_descriptor.has_ascii_table:
ascii_table.write(fp)
# Write out the unicode table if required
if font_descriptor.unicode_glyph_count > 0:
unicode_table.write(fp)
# Write out the palette if required
if format['has_palette']:
palette_descriptor = QGFFramePaletteDescriptorV1()
# Helper to convert from RGB888 to the QMK "dialect" of HSV888
def rgb888_to_qmk_hsv888(e):
hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
# Convert all palette entries to HSV888 and write to the output
palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, self.palette))
palette_descriptor.write(fp)
# Write out the image data
data_descriptor.write(fp)
# Now fix up the overall font descriptor, then write it in the correct location
font_descriptor.total_file_size = fp.tell()
fp.seek(font_descriptor_location, 0)
font_descriptor.write(fp)

+ 408
- 0
lib/python/qmk/painter_qgf.py View File

@ -0,0 +1,408 @@
# Copyright 2021 Nick Brassel (@tzarc)
# SPDX-License-Identifier: GPL-2.0-or-later
# Quantum Graphics File "QGF" Image File Format.
# See https://docs.qmk.fm/#/quantum_painter_qgf for more information.
from colorsys import rgb_to_hsv
from types import FunctionType
from PIL import Image, ImageFile, ImageChops
from PIL._binary import o8, o16le as o16, o32le as o32
import qmk.painter
def o24(i):
return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
########################################################################################################################
class QGFBlockHeader:
block_size = 5
def write(self, fp):
fp.write(b'' # start off with empty bytes...
+ o8(self.type_id) # block type id
+ o8((~self.type_id) & 0xFF) # negated block type id
+ o24(self.length) # blob length
)
########################################################################################################################
class QGFGraphicsDescriptor:
type_id = 0x00
length = 18
magic = 0x464751
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QGFGraphicsDescriptor.type_id
self.header.length = QGFGraphicsDescriptor.length
self.version = 1
self.total_file_size = 0
self.image_width = 0
self.image_height = 0
self.frame_count = 0
def write(self, fp):
self.header.write(fp)
fp.write(
b'' # start off with empty bytes...
+ o24(QGFGraphicsDescriptor.magic) # magic
+ o8(self.version) # version
+ o32(self.total_file_size) # file size
+ o32((~self.total_file_size) & 0xFFFFFFFF) # negated file size
+ o16(self.image_width) # width
+ o16(self.image_height) # height
+ o16(self.frame_count) # frame count
)
########################################################################################################################
class QGFFrameOffsetDescriptorV1:
type_id = 0x01
def __init__(self, frame_count):
self.header = QGFBlockHeader()
self.header.type_id = QGFFrameOffsetDescriptorV1.type_id
self.frame_offsets = [0xFFFFFFFF] * frame_count
self.frame_count = frame_count
def write(self, fp):
self.header.length = len(self.frame_offsets) * 4
self.header.write(fp)
for offset in self.frame_offsets:
fp.write(b'' # start off with empty bytes...
+ o32(offset) # offset
)
########################################################################################################################
class QGFFrameDescriptorV1:
type_id = 0x02
length = 6
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QGFFrameDescriptorV1.type_id
self.header.length = QGFFrameDescriptorV1.length
self.format = 0xFF
self.flags = 0
self.compression = 0xFF
self.transparency_index = 0xFF # TODO: Work out how to retrieve the transparent palette entry from the PIL gif loader
self.delay = 1000 # Placeholder until it gets read from the animation
def write(self, fp):
self.header.write(fp)
fp.write(b'' # start off with empty bytes...
+ o8(self.format) # format
+ o8(self.flags) # flags
+ o8(self.compression) # compression
+ o8(self.transparency_index) # transparency index
+ o16(self.delay) # delay
)
@property
def is_transparent(self):
return (self.flags & 0x01) == 0x01
@is_transparent.setter
def is_transparent(self, val):
if val:
self.flags |= 0x01
else:
self.flags &= ~0x01
@property
def is_delta(self):
return (self.flags & 0x02) == 0x02
@is_delta.setter
def is_delta(self, val):
if val:
self.flags |= 0x02
else:
self.flags &= ~0x02
########################################################################################################################
class QGFFramePaletteDescriptorV1:
type_id = 0x03
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QGFFramePaletteDescriptorV1.type_id
self.header.length = 0
self.palette_entries = [(0xFF, 0xFF, 0xFF)] * 4
def write(self, fp):
self.header.length = len(self.palette_entries) * 3
self.header.write(fp)
for entry in self.palette_entries:
fp.write(b'' # start off with empty bytes...
+ o8(entry[0]) # h
+ o8(entry[1]) # s
+ o8(entry[2]) # v
)
########################################################################################################################
class QGFFrameDeltaDescriptorV1:
type_id = 0x04
length = 8
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QGFFrameDeltaDescriptorV1.type_id
self.header.length = QGFFrameDeltaDescriptorV1.length
self.left = 0
self.top = 0
self.right = 0
self.bottom = 0
def write(self, fp):
self.header.write(fp)
fp.write(b'' # start off with empty bytes...
+ o16(self.left) # left
+ o16(self.top) # top
+ o16(self.right) # right
+ o16(self.bottom) # bottom
)
########################################################################################################################
class QGFFrameDataDescriptorV1:
type_id = 0x05
def __init__(self):
self.header = QGFBlockHeader()
self.header.type_id = QGFFrameDataDescriptorV1.type_id
self.data = []
def write(self, fp):
self.header.length = len(self.data)
self.header.write(fp)
fp.write(bytes(self.data))
########################################################################################################################
class QGFImageFile(ImageFile.ImageFile):
format = "QGF"
format_description = "Quantum Graphics File Format"
def _open(self):
raise NotImplementedError("Reading QGF files is not supported")
########################################################################################################################
def _accept(prefix):
"""Helper method used by PIL to work out if it can parse an input file.
Currently unimplemented.
"""
return False
def _save(im, fp, filename):
"""Helper method used by PIL to write to an output file.
"""
# Work out from the parameters if we need to do anything special
encoderinfo = im.encoderinfo.copy()
append_images = list(encoderinfo.get("append_images", []))
verbose = encoderinfo.get("verbose", False)
use_deltas = encoderinfo.get("use_deltas", True)
use_rle = encoderinfo.get("use_rle", True)
# Helper for inline verbose prints
def vprint(s):
if verbose:
print(s)
# Helper to iterate through all frames in the input image
def _for_all_frames(x: FunctionType):
frame_num = 0
last_frame = None
for frame in [im] + append_images:
# Get number of of frames in this image
nfr = getattr(frame, "n_frames", 1)
for idx in range(nfr):
frame.seek(idx)
frame.load()
copy = frame.copy().convert("RGB")
x(frame_num, copy, last_frame)
last_frame = copy
frame_num += 1
# Collect all the frame sizes
frame_sizes = []
_for_all_frames(lambda idx, frame, last_frame: frame_sizes.append(frame.size))
# Make sure all frames are the same size
if len(list(set(frame_sizes))) != 1:
raise ValueError("Mismatching sizes on frames")
# Write out the initial graphics descriptor (and write a dummy value), so that we can come back and fill in the
# correct values once we've written all the frames to the output
graphics_descriptor_location = fp.tell()
graphics_descriptor = QGFGraphicsDescriptor()
graphics_descriptor.frame_count = len(frame_sizes)
graphics_descriptor.image_width = frame_sizes[0][0]
graphics_descriptor.image_height = frame_sizes[0][1]
vprint(f'{"Graphics descriptor block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
graphics_descriptor.write(fp)
# Work out the frame offset descriptor location (and write a dummy value), so that we can come back and fill in the
# correct offsets once we've written all the frames to the output
frame_offset_location = fp.tell()
frame_offsets = QGFFrameOffsetDescriptorV1(graphics_descriptor.frame_count)
vprint(f'{"Frame offsets block":26s} {fp.tell():5d}d / {fp.tell():04X}h')
frame_offsets.write(fp)
# Helper function to save each frame to the output file
def _write_frame(idx, frame, last_frame):
# If we replace the frame we're going to output with a delta, we can override it here
this_frame = frame
location = (0, 0)
size = frame.size
# Work out the format we're going to use
format = encoderinfo["qmk_format"]
# Convert the original frame so we can do comparisons
converted = qmk.painter.convert_requested_format(this_frame, format)
graphic_data = qmk.painter.convert_image_bytes(converted, format)
# Convert the raw data to RLE-encoded if requested
raw_data = graphic_data[1]
if use_rle:
rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1])
use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data)
image_data = raw_data if use_raw_this_frame else rle_data
# Work out if a delta frame is smaller than injecting it directly
use_delta_this_frame = False
if use_deltas and last_frame is not None:
# If we want to use deltas, then find the difference
diff = ImageChops.difference(frame, last_frame)
# Get the bounding box of those differences
bbox = diff.getbbox()
# If we have a valid bounding box...
if bbox:
# ...create the delta frame by cropping the original.
delta_frame = frame.crop(bbox)
delta_location = (bbox[0], bbox[1])
delta_size = (bbox[2] - bbox[0], bbox[3] - bbox[1])
# Convert the delta frame to the requested format
delta_converted = qmk.painter.convert_requested_format(delta_frame, format)
delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format)
# Work out how large the delta frame is going to be with compression etc.
delta_raw_data = delta_graphic_data[1]
if use_rle:
delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1])
delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data)
delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data
# If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead
# This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash
# sizing constraints.
if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data):
# Copy across all the delta equivalents so that the rest of the processing acts on those
this_frame = delta_frame
location = delta_location
size = delta_size
converted = delta_converted
graphic_data = delta_graphic_data
raw_data = delta_raw_data
rle_data = delta_rle_data
use_raw_this_frame = delta_use_raw_this_frame
image_data = delta_image_data
use_delta_this_frame = True
# Write out the frame descriptor
frame_offsets.frame_offsets[idx] = fp.tell()
vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h')
frame_descriptor = QGFFrameDescriptorV1()
frame_descriptor.is_delta = use_delta_this_frame
frame_descriptor.is_transparent = False
frame_descriptor.format = format['image_format_byte']
frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01 # See qp.h, painter_compression_t
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
frame_descriptor.write(fp)
# Write out the palette if required
if format['has_palette']:
palette = graphic_data[0]
palette_descriptor = QGFFramePaletteDescriptorV1()
# Helper to convert from RGB888 to the QMK "dialect" of HSV888
def rgb888_to_qmk_hsv888(e):
hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0)
return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0))
# Convert all palette entries to HSV888 and write to the output
palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette))
vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h')
palette_descriptor.write(fp)
# Write out the delta info if required
if use_delta_this_frame:
# Set up the rendering location of where the delta frame should be situated
delta_descriptor = QGFFrameDeltaDescriptorV1()
delta_descriptor.left = location[0]
delta_descriptor.top = location[1]
delta_descriptor.right = location[0] + size[0]
delta_descriptor.bottom = location[1] + size[1]
# Write the delta frame to the output
vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h')
delta_descriptor.write(fp)
# Write out the data for this frame to the output
data_descriptor = QGFFrameDataDescriptorV1()
data_descriptor.data = image_data
vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h')
data_descriptor.write(fp)
# Iterate over each if the input frames, writing it to the output in the process
_for_all_frames(_write_frame)
# Go back and update the graphics descriptor now that we can determine the final file size
graphics_descriptor.total_file_size = fp.tell()
fp.seek(graphics_descriptor_location, 0)
graphics_descriptor.write(fp)
# Go back and update the frame offsets now that they're written to the file
fp.seek(frame_offset_location, 0)
frame_offsets.write(fp)
########################################################################################################################
# Register with PIL so that it knows about the QGF format
Image.register_open(QGFImageFile.format, QGFImageFile, _accept)
Image.register_save(QGFImageFile.format, _save)
Image.register_save_all(QGFImageFile.format, _save)
Image.register_extension(QGFImageFile.format, f".{QGFImageFile.format.lower()}")
Image.register_mime(QGFImageFile.format, f"image/{QGFImageFile.format.lower()}")

+ 7
- 4
quantum/main.c View File

@ -43,10 +43,6 @@ void protocol_task(void) {
protocol_post_task();
}
#ifdef DEFERRED_EXEC_ENABLE
void deferred_exec_task(void);
#endif // DEFERRED_EXEC_ENABLE
/** \brief Main
*
* FIXME: Needs doc
@ -63,8 +59,15 @@ int main(void) {
while (true) {
protocol_task();
#ifdef QUANTUM_PAINTER_ENABLE
// Run Quantum Painter animations
void qp_internal_animation_tick(void);
qp_internal_animation_tick();
#endif
#ifdef DEFERRED_EXEC_ENABLE
// Run deferred executions
void deferred_exec_task(void);
deferred_exec_task();
#endif // DEFERRED_EXEC_ENABLE


+ 137
- 0
quantum/painter/qff.c View File

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

+ 88
- 0
quantum/painter/qff.h View File

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

+ 292
- 0
quantum/painter/qgf.c View File

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

+ 136
- 0
quantum/painter/qgf.h View File

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

+ 228
- 0
quantum/painter/qp.c View File

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

+ 453
- 0
quantum/painter/qp.h View File

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

+ 72
- 0
quantum/painter/qp_comms.c View File

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

+ 25
- 0
quantum/painter/qp_comms.h View File

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

+ 85
- 0
quantum/painter/qp_draw.h View File

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

+ 172
- 0
quantum/painter/qp_draw_circle.c View File

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

+ 142
- 0
quantum/painter/qp_draw_codec.c View File

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

+ 294
- 0
quantum/painter/qp_draw_core.c View File

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

+ 116
- 0
quantum/painter/qp_draw_ellipse.c View File

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

+ 382
- 0
quantum/painter/qp_draw_image.c View File

@ -0,0 +1,382 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
#include "qp_draw.h"
#include "qp_comms.h"
#include "qgf.h"
#include "deferred_exec.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// QGF image handles
typedef struct qgf_image_handle_t {
painter_image_desc_t base;
bool validate_ok;
union {
qp_stream_t stream;
qp_memory_stream_t mem_stream;
#ifdef QP_STREAM_HAS_FILE_IO
qp_file_stream_t file_stream;
#endif // QP_STREAM_HAS_FILE_IO
};
} qgf_image_handle_t;
static qgf_image_handle_t image_descriptors[QUANTUM_PAINTER_NUM_IMAGES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_load_image_mem
painter_image_handle_t qp_load_image_mem(const void *buffer) {
qp_dprintf("qp_load_image_mem: entry\n");
qgf_image_handle_t *image = NULL;
// Find a free slot
for (int i = 0; i < QUANTUM_PAINTER_NUM_IMAGES; ++i) {
if (!image_descriptors[i].validate_ok) {
image = &image_descriptors[i];
break;
}
}
// Drop out if not found
if (!image) {
qp_dprintf("qp_load_image_mem: fail (no free slot)\n");
return NULL;
}
// Assume we can read the graphics descriptor
image->mem_stream = qp_make_memory_stream((void *)buffer, sizeof(qgf_graphics_descriptor_v1_t));
// Update the length of the stream to match, and rewind to the start
image->mem_stream.length = qgf_get_total_size(&image->stream);
image->mem_stream.position = 0;
// Now that we know the length, validate the input data
if (!qgf_validate_stream(&image->stream)) {
qp_dprintf("qp_load_image_mem: fail (failed validation)\n");
return NULL;
}
// Fill out the QP image descriptor
qgf_read_graphics_descriptor(&image->stream, &image->base.width, &image->base.height, &image->base.frame_count, NULL);
// Validation success, we can return the handle
image->validate_ok = true;
qp_dprintf("qp_load_image_mem: ok\n");
return (painter_image_handle_t)image;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_close_image
bool qp_close_image(painter_image_handle_t image) {
qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
if (!qgf_image->validate_ok) {
qp_dprintf("qp_close_image: fail (invalid image)\n");
return false;
}
// Free up this image for use elsewhere.
qgf_image->validate_ok = false;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_drawimage
bool qp_drawimage(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
return qp_drawimage_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_drawimage_recolor
typedef struct qgf_frame_info_t {
painter_compression_t compression_scheme;
uint8_t bpp;
bool has_palette;
bool is_delta;
uint16_t left;
uint16_t top;
uint16_t right;
uint16_t bottom;
uint16_t delay;
} qgf_frame_info_t;
static bool qp_drawimage_prepare_frame_for_stream_read(painter_device_t device, qgf_image_handle_t *qgf_image, uint16_t frame_number, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888, qgf_frame_info_t *info) {
struct painter_driver_t *driver = (struct painter_driver_t *)device;
// Drop out if we can't actually place the data we read out anywhere
if (!info) {
qp_dprintf("Failed to prepare stream for read, output info buffer unavailable\n");
return false;
}
// Seek to the frame
qgf_seek_to_frame_descriptor(&qgf_image->stream, frame_number);
// Read the frame descriptor
qgf_frame_v1_t frame_descriptor;
if (qp_stream_read(&frame_descriptor, sizeof(qgf_frame_v1_t), 1, &qgf_image->stream) != 1) {
qp_dprintf("Failed to read frame_descriptor, expected length was not %d\n", (int)sizeof(qgf_frame_v1_t));
return false;
}
// Parse out the frame info
if (!qgf_parse_frame_descriptor(&frame_descriptor, &info->bpp, &info->has_palette, &info->is_delta, &info->compression_scheme, &info->delay)) {
return false;
}
// Ensure we aren't reusing any palette
qp_internal_invalidate_palette();
// Handle palette if needed
const uint16_t palette_entries = 1u << info->bpp;
bool needs_pixconvert = false;
if (info->has_palette) {
// Load the palette from the stream
if (!qp_internal_load_qgf_palette((qp_stream_t *)&qgf_image->stream, info->bpp)) {
return false;
}
needs_pixconvert = true;
} else {
// Interpolate from fg/bg
needs_pixconvert = qp_internal_interpolate_palette(fg_hsv888, bg_hsv888, palette_entries);
}
if (!qp_internal_bpp_capable(info->bpp)) {
qp_dprintf("qp_drawimage_recolor: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE)\n", (int)info->bpp);
qp_comms_stop(device);
return false;
}
if (needs_pixconvert) {
// Convert the palette to native format
if (!driver->driver_vtable->palette_convert(device, palette_entries, qp_internal_global_pixel_lookup_table)) {
qp_dprintf("qp_drawimage_recolor: fail (could not convert pixels to native)\n");
qp_comms_stop(device);
return false;
}
}
// Handle delta if needed
if (info->is_delta) {
qgf_delta_v1_t delta_descriptor;
if (qp_stream_read(&delta_descriptor, sizeof(qgf_delta_v1_t), 1, &qgf_image->stream) != 1) {
qp_dprintf("Failed to read delta_descriptor, expected length was not %d\n", (int)sizeof(qgf_delta_v1_t));
return false;
}
info->left = delta_descriptor.left;
info->top = delta_descriptor.top;
info->right = delta_descriptor.right;
info->bottom = delta_descriptor.bottom;
}
// Read the data block
qgf_data_v1_t data_descriptor;
if (qp_stream_read(&data_descriptor, sizeof(qgf_data_v1_t), 1, &qgf_image->stream) != 1) {
qp_dprintf("Failed to read data_descriptor, expected length was not %d\n", (int)sizeof(qgf_data_v1_t));
return false;
}
// Stream is now at the point of being able to read pixdata
return true;
}
static bool qp_drawimage_recolor_impl(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, int frame_number, qgf_frame_info_t *frame_info, qp_pixel_t fg_hsv888, qp_pixel_t bg_hsv888) {
qp_dprintf("qp_drawimage_recolor: entry\n");
struct painter_driver_t *driver = (struct painter_driver_t *)device;
if (!driver->validate_ok) {
qp_dprintf("qp_drawimage_recolor: fail (validation_ok == false)\n");
return false;
}
qgf_image_handle_t *qgf_image = (qgf_image_handle_t *)image;
if (!qgf_image->validate_ok) {
qp_dprintf("qp_drawimage_recolor: fail (invalid image)\n");
return false;
}
// Read the frame info
if (!qp_drawimage_prepare_frame_for_stream_read(device, qgf_image, frame_number, fg_hsv888, bg_hsv888, frame_info)) {
qp_dprintf("qp_drawimage_recolor: fail (could not read frame %d)\n", frame_number);
return false;
}
if (!qp_comms_start(device)) {
qp_dprintf("qp_drawimage_recolor: fail (could not start comms)\n");
return false;
}
uint16_t l, t, r, b;
if (frame_info->is_delta) {
l = x + frame_info->left;
t = y + frame_info->top;
r = x + frame_info->right - 1;
b = y + frame_info->bottom - 1;
} else {
l = x;
t = y;
r = x + image->width - 1;
b = y + image->height - 1;
}
uint32_t pixel_count = ((uint32_t)(r - l + 1)) * (b - t + 1);
// Configure where we're going to be rendering to
if (!driver->driver_vtable->viewport(device, l, t, r, b)) {
qp_dprintf("qp_drawimage_recolor: fail (could not set viewport)\n");
qp_comms_stop(device);
return false;
}
// Set up the input state
struct qp_internal_byte_input_state input_state = {.device = device, .src_stream = &qgf_image->stream};
qp_internal_byte_input_callback input_callback = qp_internal_prepare_input_state(&input_state, frame_info->compression_scheme);
if (input_callback == NULL) {
qp_dprintf("qp_drawimage_recolor: fail (invalid image compression scheme)\n");
qp_comms_stop(device);
return false;
}
// Set up the output state
struct qp_internal_pixel_output_state output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)};
// Decode the pixel data and stream to the display
bool ret = qp_internal_decode_palette(device, pixel_count, frame_info->bpp, input_callback, &input_state, qp_internal_global_pixel_lookup_table, qp_internal_pixel_appender, &output_state);
// Any leftovers need transmission as well.
if (ret && output_state.pixel_write_pos > 0) {
ret &= driver->driver_vtable->pixdata(device, qp_internal_global_pixdata_buffer, output_state.pixel_write_pos);
}
qp_dprintf("qp_drawimage_recolor: %s\n", ret ? "ok" : "fail");
qp_comms_stop(device);
return ret;
}
bool qp_drawimage_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
qgf_frame_info_t frame_info = {0};
qp_pixel_t fg_hsv888 = {.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
qp_pixel_t bg_hsv888 = {.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
return qp_drawimage_recolor_impl(device, x, y, image, 0, &frame_info, fg_hsv888, bg_hsv888);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_animate
deferred_token qp_animate(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image) {
return qp_animate_recolor(device, x, y, image, 0, 0, 255, 0, 0, 0);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_animate_recolor
typedef struct animation_state_t {
painter_device_t device;
uint16_t x;
uint16_t y;
painter_image_handle_t image;
qp_pixel_t fg_hsv888;
qp_pixel_t bg_hsv888;
uint16_t frame_number;
deferred_token defer_token;
} animation_state_t;
static deferred_executor_t animation_executors[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
static animation_state_t animation_states[QUANTUM_PAINTER_CONCURRENT_ANIMATIONS] = {0};
static deferred_token qp_render_animation_state(animation_state_t *state, uint16_t *delay_ms) {
qgf_frame_info_t frame_info = {0};
qp_dprintf("qp_render_animation_state: entry (frame #%d)\n", (int)state->frame_number);
bool ret = qp_drawimage_recolor_impl(state->device, state->x, state->y, state->image, state->frame_number, &frame_info, state->fg_hsv888, state->bg_hsv888);
if (ret) {
++state->frame_number;
if (state->frame_number >= state->image->frame_count) {
state->frame_number = 0;
}
*delay_ms = frame_info.delay;
}
qp_dprintf("qp_render_animation_state: %s (delay %dms)\n", ret ? "ok" : "fail", (int)(*delay_ms));
return ret;
}
static uint32_t animation_callback(uint32_t trigger_time, void *cb_arg) {
animation_state_t *state = (animation_state_t *)cb_arg;
uint16_t delay_ms;
bool ret = qp_render_animation_state(state, &delay_ms);
if (!ret) {
// Setting the device to NULL clears the animation slot
state->device = NULL;
}
// If we're successful, keep animating -- returning 0 cancels the deferred execution
return ret ? delay_ms : 0;
}
deferred_token qp_animate_recolor(painter_device_t device, uint16_t x, uint16_t y, painter_image_handle_t image, uint8_t hue_fg, uint8_t sat_fg, uint8_t val_fg, uint8_t hue_bg, uint8_t sat_bg, uint8_t val_bg) {
qp_dprintf("qp_animate_recolor: entry\n");
animation_state_t *anim_state = NULL;
for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
if (animation_states[i].device == NULL) {
anim_state = &animation_states[i];
break;
}
}
if (!anim_state) {
qp_dprintf("qp_animate_recolor: fail (could not find free animation slot)\n");
return INVALID_DEFERRED_TOKEN;
}
// Prepare the animation state
anim_state->device = device;
anim_state->x = x;
anim_state->y = y;
anim_state->image = image;
anim_state->fg_hsv888 = (qp_pixel_t){.hsv888 = {.h = hue_fg, .s = sat_fg, .v = val_fg}};
anim_state->bg_hsv888 = (qp_pixel_t){.hsv888 = {.h = hue_bg, .s = sat_bg, .v = val_bg}};
anim_state->frame_number = 0;
// Draw the first frame
uint16_t delay_ms;
if (!qp_render_animation_state(anim_state, &delay_ms)) {
anim_state->device = NULL; // disregard the allocated animation slot
qp_dprintf("qp_animate_recolor: fail (could not render first frame)\n");
return INVALID_DEFERRED_TOKEN;
}
// Set up the timer
anim_state->defer_token = defer_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, delay_ms, animation_callback, anim_state);
if (anim_state->defer_token == INVALID_DEFERRED_TOKEN) {
anim_state->device = NULL; // disregard the allocated animation slot
qp_dprintf("qp_animate_recolor: fail (could not set up animation executor)\n");
return INVALID_DEFERRED_TOKEN;
}
qp_dprintf("qp_animate_recolor: ok (deferred token = %d)\n", (int)anim_state->defer_token);
return anim_state->defer_token;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter External API: qp_stop_animation
void qp_stop_animation(deferred_token anim_token) {
for (int i = 0; i < QUANTUM_PAINTER_CONCURRENT_ANIMATIONS; ++i) {
if (animation_states[i].defer_token == anim_token) {
cancel_deferred_exec_advanced(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, anim_token);
animation_states[i].device = NULL;
return;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter Core API: qp_internal_animation_tick
void qp_internal_animation_tick(void) {
static uint32_t last_anim_exec = 0;
deferred_exec_advanced_task(animation_executors, QUANTUM_PAINTER_CONCURRENT_ANIMATIONS, &last_anim_exec);
}

+ 444
- 0
quantum/painter/qp_draw_text.c View File

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

+ 33
- 0
quantum/painter/qp_internal.h View File

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

+ 82
- 0
quantum/painter/qp_internal_driver.h View File

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

+ 49
- 0
quantum/painter/qp_internal_formats.h View File

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

+ 171
- 0
quantum/painter/qp_stream.c View File

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

+ 82
- 0
quantum/painter/qp_stream.h View File

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

+ 116
- 0
quantum/painter/rules.mk View File

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

+ 1
- 29
quantum/process_keycode/process_unicode_common.c View File

@ -16,6 +16,7 @@
#include "process_unicode_common.h"
#include "eeprom.h"
#include "utf8.h"
unicode_config_t unicode_config;
uint8_t unicode_saved_mods;
@ -229,35 +230,6 @@ void register_unicode(uint32_t code_point) {
unicode_input_finish();
}
// Borrowed from https://nullprogram.com/blog/2017/10/06/
static const char *decode_utf8(const char *str, int32_t *code_point) {
const char *next;
if (str[0] < 0x80) { // U+0000-007F
*code_point = str[0];
next = str + 1;
} else if ((str[0] & 0xE0) == 0xC0) { // U+0080-07FF
*code_point = ((int32_t)(str[0] & 0x1F) << 6) | ((int32_t)(str[1] & 0x3F) << 0);
next = str + 2;
} else if ((str[0] & 0xF0) == 0xE0) { // U+0800-FFFF
*code_point = ((int32_t)(str[0] & 0x0F) << 12) | ((int32_t)(str[1] & 0x3F) << 6) | ((int32_t)(str[2] & 0x3F) << 0);
next = str + 3;
} else if ((str[0] & 0xF8) == 0xF0 && (str[0] <= 0xF4)) { // U+10000-10FFFF
*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);
next = str + 4;
} else {
*code_point = -1;
next = str + 1;
}
// part of a UTF-16 surrogate pair - invalid
if (*code_point >= 0xD800 && *code_point <= 0xDFFF) {
*code_point = -1;
}
return next;
}
void send_unicode_string(const char *str) {
if (!str) {
return;


+ 4
- 0
quantum/quantum.h View File

@ -188,6 +188,10 @@ extern layer_state_t layer_state;
# include "st7565.h"
#endif
#ifdef QUANTUM_PAINTER_ENABLE
# include "qp.h"
#endif
#ifdef DIP_SWITCH_ENABLE
# include "dip_switch.h"
#endif


+ 46
- 0
quantum/utf8.c View File

@ -0,0 +1,46 @@
/* Copyright 2021 QMK
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "utf8.h"
// Borrowed from https://nullprogram.com/blog/2017/10/06/
const char *decode_utf8(const char *str, int32_t *code_point) {
const char *next;
if (str[0] < 0x80) { // U+0000-007F
*code_point = str[0];
next = str + 1;
} else if ((str[0] & 0xE0) == 0xC0) { // U+0080-07FF
*code_point = ((int32_t)(str[0] & 0x1F) << 6) | ((int32_t)(str[1] & 0x3F) << 0);
next = str + 2;
} else if ((str[0] & 0xF0) == 0xE0) { // U+0800-FFFF
*code_point = ((int32_t)(str[0] & 0x0F) << 12) | ((int32_t)(str[1] & 0x3F) << 6) | ((int32_t)(str[2] & 0x3F) << 0);
next = str + 3;
} else if ((str[0] & 0xF8) == 0xF0 && (str[0] <= 0xF4)) { // U+10000-10FFFF
*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);
next = str + 4;
} else {
*code_point = -1;
next = str + 1;
}
// part of a UTF-16 surrogate pair - invalid
if (*code_point >= 0xD800 && *code_point <= 0xDFFF) {
*code_point = -1;
}
return next;
}

+ 21
- 0
quantum/utf8.h View File

@ -0,0 +1,21 @@
/* Copyright 2021 QMK
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <stdint.h>
const char *decode_utf8(const char *str, int32_t *code_point);

+ 1
- 0
requirements.txt View File

@ -9,3 +9,4 @@ milc>=1.4.2
pygments
pyusb
qmk-dotty-dict
pillow

+ 4
- 0
setup.cfg View File

@ -6,7 +6,11 @@ ignore =
# Conflicts with our yapf config
E231
per_file_ignores =
# Module imported but unused
**/__init__.py:F401
# Quantum Painter also outputs append data using bytes object arithmetic on multiple lines
**/painter_qgf.py:W503
**/painter_qff.py:W503
# Let's slowly crank this down
max_complexity=16


Loading…
Cancel
Save