[QP] Add support for OLED, variable framebuffer bpp (#19997)

Co-authored-by: Pablo Martínez <58857054+elpekenin@users.noreply.github.com>
Co-authored-by: Dasky <32983009+daskygit@users.noreply.github.com>
Fixup delta frame coordinates after #20296.
This commit is contained in:
Nick Brassel 2023-10-22 13:27:31 +11:00 committed by GitHub
parent 48d9140cfc
commit 8e614250b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 1610 additions and 497 deletions

View file

@ -455,6 +455,7 @@ $(eval $(call add_qmk_prefix_defs,MCU_PORT_NAME,MCU_PORT_NAME))
$(eval $(call add_qmk_prefix_defs,MCU_FAMILY,MCU_FAMILY))
$(eval $(call add_qmk_prefix_defs,MCU_SERIES,MCU_SERIES))
$(eval $(call add_qmk_prefix_defs,BOARD,BOARD))
$(eval $(call add_qmk_prefix_defs,OPT,OPT))
# Control whether intermediate file listings are generated
# e.g.:

View file

@ -13,22 +13,24 @@ 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.
!> Quantum Painter is not currently integrated with system-level operations such as 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.
Supported devices:
| 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` |
| ILI9488 | RGB LCD | 320x480 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ili9488_spi` |
| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ssd1351_spi` |
| ST7735 | RGB LCD | 132x162, 80x160 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += st7735_spi` |
| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += st7789_spi` |
| RGB565 Surface | Virtual | User-defined | None | `QUANTUM_PAINTER_DRIVERS += rgb565_surface` |
| 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` |
| ILI9488 | RGB LCD | 320x480 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ili9488_spi` |
| SSD1351 | RGB OLED | 128x128 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += ssd1351_spi` |
| ST7735 | RGB LCD | 132x162, 80x160 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += st7735_spi` |
| ST7789 | RGB LCD | 240x320, 240x240 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += st7789_spi` |
| SH1106 (SPI) | Monochrome OLED | 128x64 | SPI + D/C + RST | `QUANTUM_PAINTER_DRIVERS += sh1106_spi` |
| SH1106 (I2C) | Monochrome OLED | 128x64 | I2C | `QUANTUM_PAINTER_DRIVERS += sh1106_i2c` |
| Surface | Virtual | User-defined | None | `QUANTUM_PAINTER_DRIVERS += surface` |
## Quantum Painter Configuration :id=quantum-painter-config
@ -188,7 +190,8 @@ Writing /home/qmk/qmk_firmware/keyboards/my_keeb/generated/noto11.qff.c...
<!-- tabs:start -->
### ** Common: Standard TFT (SPI + D/C + RST) **
### ** LCD **
Most TFT display panels use a 5-pin interface -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins.
@ -302,32 +305,6 @@ The maximum number of displays can be configured by changing the following in yo
Native color format rgb888 is compatible with ILI9488
#### ** 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
```
Native color format rgb565 is compatible with SSD1351
#### ** ST7735 **
Enabling support for the ST7735 in Quantum Painter is done by adding the following to `rules.mk`:
@ -386,62 +363,139 @@ Native color format rgb565 is compatible with ST7789
<!-- tabs:end -->
### ** Common: Surfaces **
### ** OLED **
Quantum Painter has surface drivers which are able to target a buffer in RAM. In general, surfaces keep track of the "dirty" region -- the area that has been drawn to since the last flush -- so that when transferring to the display they can transfer the minimal amount of data to achieve the end result.
OLED displays tend to use 5-pin SPI when at larger resolutions, or when using color -- SPI SCK, SPI MOSI, SPI CS, D/C, and RST pins. Smaller OLEDs may use I2C instead.
!> These generally require significant amounts of RAM, so at large sizes and/or higher bit depths, they may not be usable on all MCUs.
When using these displays, either `spi_master` or `i2c_master` must already be correctly configured for both the platform and panel you're building for.
For SPI, the pin assignments for SPI CS, D/C, and RST are specified during device construction -- for I2C the panel's address is specified instead.
<!-- tabs:start -->
#### ** RGB565 Surface **
#### ** SSD1351 **
Enabling support for RGB565 surfaces in Quantum Painter is done by adding the following to `rules.mk`:
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 += rgb565_surface
QUANTUM_PAINTER_DRIVERS += ssd1351_spi
```
Creating a RGB565 surface in firmware can then be done with the following API:
Creating a SSD1351 device in firmware can then be done with the following API:
```c
painter_device_t qp_rgb565_make_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
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 `buffer` is a user-supplied area of memory, and is assumed to be of the size `sizeof(uint16_t) * panel_width * panel_height`.
The device handle returned from the `qp_ssd1351_make_spi_device` function can be used to perform all other drawing operations.
The device handle returned from the `qp_rgb565_make_surface` 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
```
Native color format rgb565 is compatible with SSD1351
#### ** SH1106 **
Enabling support for the SH1106 in Quantum Painter is done by adding the following to `rules.mk`:
```make
QUANTUM_PAINTER_ENABLE = yes
# For SPI:
QUANTUM_PAINTER_DRIVERS += sh1106_spi
# For I2C:
QUANTUM_PAINTER_DRIVERS += sh1106_i2c
```
Creating a SH1106 device in firmware can then be done with the following APIs:
```c
// SPI-based SH1106:
painter_device_t qp_sh1106_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);
// I2C-based SH1106:
painter_device_t qp_sh1106_make_i2c_device(uint16_t panel_width, uint16_t panel_height, uint8_t i2c_address);
```
The device handle returned from the `qp_sh1106_make_???_device` function can be used to perform all other drawing operations.
The maximum number of displays of each type can be configured by changing the following in your `config.h` (default is 1):
```c
// 3 SPI displays:
#define SH1106_NUM_SPI_DEVICES 3
// 3 I2C displays:
#define SH1106_NUM_I2C_DEVICES 3
```
Native color format mono2 is compatible with SH1106
<!-- tabs:end -->
### ** Surface **
Quantum Painter has a surface driver which is able to target a buffer in RAM. In general, surfaces keep track of the "dirty" region -- the area that has been drawn to since the last flush -- so that when transferring to the display they can transfer the minimal amount of data to achieve the end result.
!> These generally require significant amounts of RAM, so at large sizes and/or higher bit depths, they may not be usable on all MCUs.
Enabling support for surfaces in Quantum Painter is done by adding the following to `rules.mk`:
```make
QUANTUM_PAINTER_ENABLE = yes
QUANTUM_PAINTER_DRIVERS += surface
```
Creating a surface in firmware can then be done with the following APIs:
```c
// 16bpp RGB565 surface:
painter_device_t qp_make_rgb565_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
// 1bpp monochrome surface:
painter_device_t qp_make_mono1bpp_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
```
The `buffer` is a user-supplied area of memory, which can be statically allocated using `SURFACE_REQUIRED_BUFFER_BYTE_SIZE`:
```c
// Buffer required for a 240x80 16bpp surface:
uint8_t framebuffer[SURFACE_REQUIRED_BUFFER_BYTE_SIZE(240, 80, 16)];
```
The device handle returned from the `qp_make_?????_surface` function can be used to perform all other drawing operations.
Example:
```c
static painter_device_t my_surface;
static uint16_t my_framebuffer[320 * 240]; // Allocate a buffer for a 320x240 RGB565 display
static uint8_t my_framebuffer[SURFACE_REQUIRED_BUFFER_BYTE_SIZE(240, 80, 16)]; // Allocate a buffer for a 16bpp 240x80 RGB565 display
void keyboard_post_init_kb(void) {
my_surface = qp_rgb565_make_surface(320, 240, my_framebuffer);
my_surface = qp_rgb565_make_surface(240, 80, my_framebuffer);
qp_init(my_surface, QP_ROTATION_0);
keyboard_post_init_user();
}
```
The maximum number of RGB565 surfaces can be configured by changing the following in your `config.h` (default is 1):
The maximum number of surfaces can be configured by changing the following in your `config.h` (default is 1):
```c
// 3 surfaces:
#define RGB565_SURFACE_NUM_DEVICES 3
#define SURFACE_NUM_DEVICES 3
```
To transfer the contents of the RGB565 surface to another display, the following API can be invoked:
To transfer the contents of the surface to another display of the same pixel format, the following API can be invoked:
```c
bool qp_rgb565_surface_draw(painter_device_t surface, painter_device_t display, uint16_t x, uint16_t y);
bool qp_surface_draw(painter_device_t surface, painter_device_t display, uint16_t x, uint16_t y);
```
The `surface` is the surface to copy out from. The `display` is the target display to draw into. `x` and `y` are the target location to draw the surface pixel data. Under normal circumstances, the location should be consistent, as the dirty region is calculated with respect to the `x` and `y` coordinates -- changing those will result in partial, overlapping draws.
?> Calling `qp_flush()` on the surface resets its dirty region. Copying the surface contents to the display also automatically resets the dirty region.
!> The surface and display panel must have the same native pixel format.
<!-- tabs:end -->
?> Calling `qp_flush()` on the surface resets its dirty region. Copying the surface contents to the display also automatically resets the dirty region.
<!-- tabs:end -->

View file

@ -0,0 +1,34 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef QUANTUM_PAINTER_DUMMY_COMMS_ENABLE
# include "qp_comms_dummy.h"
static bool dummy_comms_init(painter_device_t device) {
// No-op.
return true;
}
static bool dummy_comms_start(painter_device_t device) {
// No-op.
return true;
}
static void dummy_comms_stop(painter_device_t device) {
// No-op.
}
uint32_t dummy_comms_send(painter_device_t device, const void *data, uint32_t byte_count) {
// No-op.
return byte_count;
}
painter_comms_vtable_t dummy_comms_vtable = {
// These are all effective no-op's because they're not actually needed.
.comms_init = dummy_comms_init,
.comms_start = dummy_comms_start,
.comms_stop = dummy_comms_stop,
.comms_send = dummy_comms_send};
#endif // QUANTUM_PAINTER_DUMMY_COMMS_ENABLE

View file

@ -0,0 +1,11 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifdef QUANTUM_PAINTER_DUMMY_COMMS_ENABLE
# include "qp_internal.h"
extern painter_comms_vtable_t dummy_comms_vtable;
#endif // QUANTUM_PAINTER_DUMMY_COMMS_ENABLE

View file

@ -0,0 +1,94 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef QUANTUM_PAINTER_I2C_ENABLE
# include "i2c_master.h"
# include "qp_comms_i2c.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
static uint32_t qp_comms_i2c_send_raw(painter_device_t device, const void *data, uint32_t byte_count) {
painter_driver_t * driver = (painter_driver_t *)device;
qp_comms_i2c_config_t *comms_config = (qp_comms_i2c_config_t *)driver->comms_config;
i2c_status_t res = i2c_transmit(comms_config->chip_address << 1, data, byte_count, I2C_TIMEOUT);
if (res < 0) {
return 0;
}
return byte_count;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Base I2C support
bool qp_comms_i2c_init(painter_device_t device) {
i2c_init();
return true;
}
bool qp_comms_i2c_start(painter_device_t device) {
painter_driver_t * driver = (painter_driver_t *)device;
qp_comms_i2c_config_t *comms_config = (qp_comms_i2c_config_t *)driver->comms_config;
return i2c_start(comms_config->chip_address << 1) == I2C_STATUS_SUCCESS;
}
uint32_t qp_comms_i2c_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
return qp_comms_i2c_send_raw(device, data, byte_count);
}
void qp_comms_i2c_stop(painter_device_t device) {
i2c_stop();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Command+Data I2C support
static const uint8_t cmd_byte = 0x00;
static const uint8_t data_byte = 0x40;
void qp_comms_i2c_cmddata_send_command(painter_device_t device, uint8_t cmd) {
uint8_t buf[2] = {cmd_byte, cmd};
qp_comms_i2c_send_raw(device, &buf, 2);
}
uint32_t qp_comms_i2c_cmddata_send_data(painter_device_t device, const void *data, uint32_t byte_count) {
uint8_t buf[1 + byte_count];
buf[0] = data_byte;
memcpy(&buf[1], data, byte_count);
if (qp_comms_i2c_send_raw(device, buf, sizeof(buf)) != sizeof(buf)) {
return 0;
}
return byte_count;
}
void qp_comms_i2c_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
uint8_t buf[32];
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];
buf[0] = cmd_byte;
buf[1] = command;
memcpy(&buf[2], &sequence[i + 3], num_bytes);
qp_comms_i2c_send_raw(device, buf, num_bytes + 2);
if (delay > 0) {
wait_ms(delay);
}
i += (3 + num_bytes);
}
}
const painter_comms_with_command_vtable_t i2c_comms_cmddata_vtable = {
.base =
{
.comms_init = qp_comms_i2c_init,
.comms_start = qp_comms_i2c_start,
.comms_send = qp_comms_i2c_cmddata_send_data,
.comms_stop = qp_comms_i2c_stop,
},
.send_command = qp_comms_i2c_cmddata_send_command,
.bulk_command_sequence = qp_comms_i2c_bulk_command_sequence,
};
#endif // QUANTUM_PAINTER_I2C_ENABLE

View file

@ -0,0 +1,28 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifdef QUANTUM_PAINTER_I2C_ENABLE
# include <stdint.h>
# include "gpio.h"
# include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Base I2C support
typedef struct qp_comms_i2c_config_t {
uint8_t chip_address;
} qp_comms_i2c_config_t;
bool qp_comms_i2c_init(painter_device_t device);
bool qp_comms_i2c_start(painter_device_t device);
uint32_t qp_comms_i2c_send_data(painter_device_t device, const void* data, uint32_t byte_count);
void qp_comms_i2c_stop(painter_device_t device);
extern const painter_comms_with_command_vtable_t i2c_comms_cmddata_vtable;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#endif // QUANTUM_PAINTER_I2C_ENABLE

View file

@ -105,13 +105,21 @@ void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd) {
}
void qp_comms_spi_dc_reset_bulk_command_sequence(painter_device_t device, const uint8_t *sequence, size_t sequence_len) {
painter_driver_t * driver = (painter_driver_t *)device;
qp_comms_spi_dc_reset_config_t *comms_config = (qp_comms_spi_dc_reset_config_t *)driver->comms_config;
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 (comms_config->command_params_uses_command_pin) {
for (uint8_t j = 0; j < num_bytes; j++) {
qp_comms_spi_dc_reset_send_command(device, sequence[i + 3 + j]);
}
} else {
qp_comms_spi_dc_reset_send_data(device, &sequence[i + 3], num_bytes);
}
}
if (delay > 0) {
wait_ms(delay);

View file

@ -1,6 +1,5 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifdef QUANTUM_PAINTER_SPI_ENABLE
@ -36,6 +35,7 @@ typedef struct qp_comms_spi_dc_reset_config_t {
qp_comms_spi_config_t spi_config;
pin_t dc_pin;
pin_t reset_pin;
bool command_params_uses_command_pin; // keep D/C held low when sending command sequences for data bytes
} qp_comms_spi_dc_reset_config_t;
void qp_comms_spi_dc_reset_send_command(painter_device_t device, uint8_t cmd);

View file

@ -2,7 +2,6 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include <wait.h>
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_gc9a01.h"
@ -135,13 +134,14 @@ painter_device_t qp_gc9a01_make_spi_device(uint16_t panel_width, uint16_t panel_
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;
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;
driver->spi_dc_reset_config.command_params_uses_command_pin = false;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(tft_panel_dc_reset_painter_device_t));

View file

@ -1,6 +1,5 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"

View file

@ -1,6 +1,5 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View file

@ -1,284 +0,0 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "color.h"
#include "qp_rgb565_surface.h"
#include "qp_draw.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Common
// Device definition
typedef struct rgb565_surface_painter_device_t {
painter_driver_t base; // must be first, so it can be cast to/from the painter_device_t* type
// The target buffer
uint16_t *buffer;
// Manually manage the viewport for streaming pixel data to the display
uint16_t viewport_l;
uint16_t viewport_t;
uint16_t viewport_r;
uint16_t viewport_b;
// Current write location to the display when streaming pixel data
uint16_t pixdata_x;
uint16_t pixdata_y;
// Maintain a dirty region so we can stream only what we need
bool is_dirty;
uint16_t dirty_l;
uint16_t dirty_t;
uint16_t dirty_r;
uint16_t dirty_b;
} rgb565_surface_painter_device_t;
// Driver storage
rgb565_surface_painter_device_t surface_drivers[RGB565_SURFACE_NUM_DEVICES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
static inline void increment_pixdata_location(rgb565_surface_painter_device_t *surface) {
// Increment the X-position
surface->pixdata_x++;
// If the x-coord has gone past the right-side edge, loop it back around and increment the y-coord
if (surface->pixdata_x > surface->viewport_r) {
surface->pixdata_x = surface->viewport_l;
surface->pixdata_y++;
}
// If the y-coord has gone past the bottom, loop it back to the top
if (surface->pixdata_y > surface->viewport_b) {
surface->pixdata_y = surface->viewport_t;
}
}
static inline void setpixel(rgb565_surface_painter_device_t *surface, uint16_t x, uint16_t y, uint16_t rgb565) {
// Skip messing with the dirty info if the original value already matches
if (surface->buffer[y * surface->base.panel_width + x] != rgb565) {
// Maintain dirty region
if (surface->dirty_l > x) {
surface->dirty_l = x;
}
if (surface->dirty_r < x) {
surface->dirty_r = x;
}
if (surface->dirty_t > y) {
surface->dirty_t = y;
}
if (surface->dirty_b < y) {
surface->dirty_b = y;
}
// Always dirty after a setpixel
surface->is_dirty = true;
// Update the pixel data in the buffer
surface->buffer[y * surface->base.panel_width + x] = rgb565;
}
}
static inline void append_pixel(rgb565_surface_painter_device_t *surface, uint16_t rgb565) {
setpixel(surface, surface->pixdata_x, surface->pixdata_y, rgb565);
increment_pixdata_location(surface);
}
static inline void stream_pixdata(rgb565_surface_painter_device_t *surface, const uint16_t *data, uint32_t native_pixel_count) {
for (uint32_t pixel_counter = 0; pixel_counter < native_pixel_count; ++pixel_counter) {
append_pixel(surface, data[pixel_counter]);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver vtable
static bool qp_rgb565_surface_init(painter_device_t device, painter_rotation_t rotation) {
painter_driver_t * driver = (painter_driver_t *)device;
rgb565_surface_painter_device_t *surface = (rgb565_surface_painter_device_t *)driver;
memset(surface->buffer, 0, driver->panel_width * driver->panel_height * driver->native_bits_per_pixel / 8);
return true;
}
static bool qp_rgb565_surface_power(painter_device_t device, bool power_on) {
// No-op.
return true;
}
static bool qp_rgb565_surface_clear(painter_device_t device) {
painter_driver_t *driver = (painter_driver_t *)device;
driver->driver_vtable->init(device, driver->rotation); // Re-init the surface
return true;
}
static bool qp_rgb565_surface_flush(painter_device_t device) {
painter_driver_t * driver = (painter_driver_t *)device;
rgb565_surface_painter_device_t *surface = (rgb565_surface_painter_device_t *)driver;
surface->dirty_l = surface->dirty_t = UINT16_MAX;
surface->dirty_r = surface->dirty_b = 0;
surface->is_dirty = false;
return true;
}
static bool qp_rgb565_surface_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
painter_driver_t * driver = (painter_driver_t *)device;
rgb565_surface_painter_device_t *surface = (rgb565_surface_painter_device_t *)driver;
// Set the viewport locations
surface->viewport_l = left;
surface->viewport_t = top;
surface->viewport_r = right;
surface->viewport_b = bottom;
// Reset the write location to the top left
surface->pixdata_x = left;
surface->pixdata_y = top;
return true;
}
// Stream pixel data to the current write position in GRAM
static bool qp_rgb565_surface_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
painter_driver_t * driver = (painter_driver_t *)device;
rgb565_surface_painter_device_t *surface = (rgb565_surface_painter_device_t *)driver;
stream_pixdata(surface, (const uint16_t *)pixel_data, native_pixel_count);
return true;
}
// Pixel colour conversion
static bool qp_rgb565_surface_palette_convert_rgb565_swapped(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) {
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});
uint16_t rgb565 = (((uint16_t)rgb.r) >> 3) << 11 | (((uint16_t)rgb.g) >> 2) << 5 | (((uint16_t)rgb.b) >> 3);
palette[i].rgb565 = __builtin_bswap16(rgb565);
}
return true;
}
// Append pixels to the target location, keyed by the pixel index
static bool qp_rgb565_surface_append_pixels_rgb565(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;
}
// Append data to the target location
static bool qp_rgb565_surface_append_pixdata(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte) {
target_buffer[pixdata_offset] = pixdata_byte;
return true;
}
const painter_driver_vtable_t rgb565_surface_driver_vtable = {
.init = qp_rgb565_surface_init,
.power = qp_rgb565_surface_power,
.clear = qp_rgb565_surface_clear,
.flush = qp_rgb565_surface_flush,
.pixdata = qp_rgb565_surface_pixdata,
.viewport = qp_rgb565_surface_viewport,
.palette_convert = qp_rgb565_surface_palette_convert_rgb565_swapped,
.append_pixels = qp_rgb565_surface_append_pixels_rgb565,
.append_pixdata = qp_rgb565_surface_append_pixdata,
};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Comms vtable
static bool qp_rgb565_surface_comms_init(painter_device_t device) {
// No-op.
return true;
}
static bool qp_rgb565_surface_comms_start(painter_device_t device) {
// No-op.
return true;
}
static void qp_rgb565_surface_comms_stop(painter_device_t device) {
// No-op.
}
uint32_t qp_rgb565_surface_comms_send(painter_device_t device, const void *data, uint32_t byte_count) {
// No-op.
return byte_count;
}
painter_comms_vtable_t rgb565_surface_driver_comms_vtable = {
// These are all effective no-op's because they're not actually needed.
.comms_init = qp_rgb565_surface_comms_init,
.comms_start = qp_rgb565_surface_comms_start,
.comms_stop = qp_rgb565_surface_comms_stop,
.comms_send = qp_rgb565_surface_comms_send};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Factory function for creating a handle to an rgb565 surface
painter_device_t qp_rgb565_make_surface(uint16_t panel_width, uint16_t panel_height, void *buffer) {
for (uint32_t i = 0; i < RGB565_SURFACE_NUM_DEVICES; ++i) {
rgb565_surface_painter_device_t *driver = &surface_drivers[i];
if (!driver->base.driver_vtable) {
driver->base.driver_vtable = &rgb565_surface_driver_vtable;
driver->base.comms_vtable = &rgb565_surface_driver_comms_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;
driver->buffer = (uint16_t *)buffer;
return (painter_device_t)driver;
}
}
return NULL;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Drawing routine to copy out the dirty region and send it to another device
bool qp_rgb565_surface_draw(painter_device_t surface, painter_device_t display, uint16_t x, uint16_t y) {
painter_driver_t * surface_driver = (painter_driver_t *)surface;
rgb565_surface_painter_device_t *surface_handle = (rgb565_surface_painter_device_t *)surface_driver;
// If we're not dirty... we're done.
if (!surface_handle->is_dirty) {
return true;
}
// Set the target drawing area
bool ok = qp_viewport(display, x + surface_handle->dirty_l, y + surface_handle->dirty_t, x + surface_handle->dirty_r, y + surface_handle->dirty_b);
if (!ok) {
return false;
}
// Housekeeping of the amount of pixels to transfer
uint32_t total_pixel_count = QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE / sizeof(uint16_t);
uint32_t pixel_counter = 0;
uint16_t *target_buffer = (uint16_t *)qp_internal_global_pixdata_buffer;
// Fill the global pixdata area so that we can start transferring to the panel
for (uint16_t y = surface_handle->dirty_t; y <= surface_handle->dirty_b; ++y) {
for (uint16_t x = surface_handle->dirty_l; x <= surface_handle->dirty_r; ++x) {
// Update the target buffer
target_buffer[pixel_counter++] = surface_handle->buffer[y * surface_handle->base.panel_width + x];
// If we've accumulated enough data, send it
if (pixel_counter == total_pixel_count) {
ok = qp_pixdata(display, qp_internal_global_pixdata_buffer, pixel_counter);
if (!ok) {
return false;
}
// Reset the counter
pixel_counter = 0;
}
}
}
// If there's any leftover data, send it
if (pixel_counter > 0) {
ok = qp_pixdata(display, qp_internal_global_pixdata_buffer, pixel_counter);
if (!ok) {
return false;
}
}
// Clear the dirty info for the surface
return qp_flush(surface);
}

View file

@ -1,42 +0,0 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter RGB565 surface configurables (add to your keyboard's config.h)
#ifndef RGB565_SURFACE_NUM_DEVICES
/**
* @def This controls the maximum number of surface devices that Quantum Painter can use at any one time.
* Increasing this number allows for multiple framebuffers to be used. Each requires its own RAM allocation.
*/
# define RGB565_SURFACE_NUM_DEVICES 1
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Forward declarations
#ifdef QUANTUM_PAINTER_RGB565_SURFACE_ENABLE
/**
* Factory method for an RGB565 surface (aka framebuffer).
*
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param buffer[in] pointer to a preallocated buffer of size `(sizeof(uint16_t) * panel_width * panel_height)`
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_rgb565_make_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
/**
* Helper method to draw the dirty contents of the framebuffer to the target device.
*
* After successful completion, the dirty area is reset.
*
* @param surface[in] the surface to copy from
* @param display[in] the display to copy into
* @param x[in] the x-location of the original position of the framebuffer
* @param y[in] the y-location of the original position of the framebuffer
* @return whether the draw operation completed successfully
*/
bool qp_rgb565_surface_draw(painter_device_t surface, painter_device_t display, uint16_t x, uint16_t y);
#endif // QUANTUM_PAINTER_RGB565_SURFACE_ENABLE

View file

@ -0,0 +1,67 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter surface helpers
// Helper for determining buffer size required for a surface
#define SURFACE_REQUIRED_BUFFER_BYTE_SIZE(w, h, bpp) ((((w) * (h) * (bpp)) + 7) / 8)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter surface configurables (add to your keyboard's config.h)
#ifndef SURFACE_NUM_DEVICES
/**
* @def This controls the maximum number of surface devices that Quantum Painter can use at any one time.
* Increasing this number allows for multiple framebuffers to be used. Each requires its own RAM allocation.
*/
# define SURFACE_NUM_DEVICES 1
#endif
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Forward declarations
#ifdef QUANTUM_PAINTER_SURFACE_ENABLE
// Surface struct
struct surface_painter_device_t;
typedef struct surface_painter_device_t surface_painter_device_t;
/**
* Factory method for an RGB565 surface (aka framebuffer).
*
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param buffer[in] pointer to a preallocated uint8_t buffer of size `SURFACE_REQUIRED_BUFFER_BYTE_SIZE(panel_width, panel_height, 16)`
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_make_rgb565_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
/**
* Factory method for a 1bpp monochrome surface (aka framebuffer).
*
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param buffer[in] pointer to a preallocated uint8_t buffer of size `SURFACE_REQUIRED_BUFFER_BYTE_SIZE(panel_width, panel_height, 1)`
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_make_mono1bpp_surface(uint16_t panel_width, uint16_t panel_height, void *buffer);
/**
* Helper method to draw the contents of the framebuffer to the target device.
*
* After successful completion, the dirty area is reset.
*
* @param surface[in] the surface to copy from
* @param target[in] the target device to copy into
* @param x[in] the x-location of the original position of the framebuffer
* @param y[in] the y-location of the original position of the framebuffer
* @param entire_surface[in] whether the entire surface should be drawn, instead of just the dirty region
* @return whether the draw operation completed successfully
*/
bool qp_surface_draw(painter_device_t surface, painter_device_t target, uint16_t x, uint16_t y, bool entire_surface);
#endif // QUANTUM_PAINTER_SURFACE_ENABLE

View file

@ -0,0 +1,141 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "color.h"
#include "qp_draw.h"
#include "qp_surface_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver storage
surface_painter_device_t surface_drivers[SURFACE_NUM_DEVICES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Helpers
void qp_surface_increment_pixdata_location(surface_viewport_data_t *viewport) {
// Increment the X-position
viewport->pixdata_x++;
// If the x-coord has gone past the right-side edge, loop it back around and increment the y-coord
if (viewport->pixdata_x > viewport->viewport_r) {
viewport->pixdata_x = viewport->viewport_l;
viewport->pixdata_y++;
}
// If the y-coord has gone past the bottom, loop it back to the top
if (viewport->pixdata_y > viewport->viewport_b) {
viewport->pixdata_y = viewport->viewport_t;
}
}
void qp_surface_update_dirty(surface_dirty_data_t *dirty, uint16_t x, uint16_t y) {
// Maintain dirty region
if (dirty->l > x) {
dirty->l = x;
dirty->is_dirty = true;
}
if (dirty->r < x) {
dirty->r = x;
dirty->is_dirty = true;
}
if (dirty->t > y) {
dirty->t = y;
dirty->is_dirty = true;
}
if (dirty->b < y) {
dirty->b = y;
dirty->is_dirty = true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver vtable
bool qp_surface_init(painter_device_t device, painter_rotation_t rotation) {
painter_driver_t * driver = (painter_driver_t *)device;
surface_painter_device_t *surface = (surface_painter_device_t *)driver;
memset(surface->buffer, 0, SURFACE_REQUIRED_BUFFER_BYTE_SIZE(driver->panel_width, driver->panel_height, driver->native_bits_per_pixel));
surface->dirty.l = 0;
surface->dirty.t = 0;
surface->dirty.r = surface->base.panel_width - 1;
surface->dirty.b = surface->base.panel_height - 1;
surface->dirty.is_dirty = true;
return true;
}
bool qp_surface_power(painter_device_t device, bool power_on) {
// No-op.
return true;
}
bool qp_surface_clear(painter_device_t device) {
painter_driver_t *driver = (painter_driver_t *)device;
driver->driver_vtable->init(device, driver->rotation); // Re-init the surface
return true;
}
bool qp_surface_flush(painter_device_t device) {
painter_driver_t * driver = (painter_driver_t *)device;
surface_painter_device_t *surface = (surface_painter_device_t *)driver;
surface->dirty.l = surface->dirty.t = UINT16_MAX;
surface->dirty.r = surface->dirty.b = 0;
surface->dirty.is_dirty = false;
return true;
}
bool qp_surface_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
painter_driver_t * driver = (painter_driver_t *)device;
surface_painter_device_t *surface = (surface_painter_device_t *)driver;
// Set the viewport locations
surface->viewport.viewport_l = left;
surface->viewport.viewport_t = top;
surface->viewport.viewport_r = right;
surface->viewport.viewport_b = bottom;
// Reset the write location to the top left
surface->viewport.pixdata_x = left;
surface->viewport.pixdata_y = top;
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Drawing routine to copy out the dirty region and send it to another device
bool qp_surface_draw(painter_device_t surface, painter_device_t target, uint16_t x, uint16_t y, bool entire_surface) {
painter_driver_t * surface_driver = (painter_driver_t *)surface;
surface_painter_device_t *surface_handle = (surface_painter_device_t *)surface_driver;
painter_driver_t * target_driver = (painter_driver_t *)target;
// If we're not dirty... we're done.
if (!surface_handle->dirty.is_dirty) {
qp_dprintf("qp_surface_draw: ok (not dirty, skipping)\n");
return true;
}
// If we have incompatible bit depths, drop out
if (surface_driver->native_bits_per_pixel != target_driver->native_bits_per_pixel) {
qp_dprintf("qp_surface_draw: fail (incompatible bpp: surface=%d, target=%d)\n", (int)surface_driver->native_bits_per_pixel, (int)target_driver->native_bits_per_pixel);
return false;
}
// Offload to the pixdata transfer function
surface_painter_driver_vtable_t *vtable = (surface_painter_driver_vtable_t *)surface_driver->driver_vtable;
bool ok = vtable->target_pixdata_transfer(surface_driver, target_driver, x, y, entire_surface);
if (!ok) {
qp_dprintf("qp_surface_draw: fail (could not transfer pixel data)\n");
return false;
}
// Clear the dirty info for the surface
ok = qp_flush(surface);
if (!ok) {
qp_dprintf("qp_surface_draw: fail (could not flush)\n");
return false;
}
qp_dprintf("qp_surface_draw: ok\n");
return true;
}

View file

@ -0,0 +1,119 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#ifdef QUANTUM_PAINTER_SURFACE_ENABLE
# include "qp_surface.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Internal declarations
// Surface vtable
typedef struct surface_painter_driver_vtable_t {
painter_driver_vtable_t base; // must be first, so it can be cast to/from the painter_driver_vtable_t* type
bool (*target_pixdata_transfer)(painter_driver_t *surface_driver, painter_driver_t *target_driver, uint16_t x, uint16_t y, bool entire_surface);
} surface_painter_driver_vtable_t;
typedef struct surface_dirty_data_t {
bool is_dirty;
uint16_t l;
uint16_t t;
uint16_t r;
uint16_t b;
} surface_dirty_data_t;
typedef struct surface_viewport_data_t {
// Manually manage the viewport for streaming pixel data to the display
uint16_t viewport_l;
uint16_t viewport_t;
uint16_t viewport_r;
uint16_t viewport_b;
// Current write location to the display when streaming pixel data
uint16_t pixdata_x;
uint16_t pixdata_y;
} surface_viewport_data_t;
// Surface struct
typedef struct surface_painter_device_t {
painter_driver_t base; // must be first, so it can be cast to/from the painter_device_t* type
// The target buffer
union {
void * buffer;
uint8_t * u8buffer;
uint16_t *u16buffer;
};
// Manually manage the viewport for streaming pixel data to the display
surface_viewport_data_t viewport;
// Maintain a dirty region so we can stream only what we need
surface_dirty_data_t dirty;
} surface_painter_device_t;
/**
* Factory method for an RGB565 surface (aka framebuffer). Accepts an external device table.
*
* @param device_table[in] the table of devices to use for instantiation
* @param device_table_len[in] the length of the table of devices
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param buffer[in] pointer to a preallocated uint8_t buffer of size `SURFACE_REQUIRED_BUFFER_BYTE_SIZE(panel_width, panel_height, 16)`
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_make_rgb565_surface_advanced(surface_painter_device_t *device_table, size_t device_table_len, uint16_t panel_width, uint16_t panel_height, void *buffer);
/**
* Factory method for a 1bpp monochrome surface (aka framebuffer).
*
* @param device_table[in] the table of devices to use for instantiation
* @param device_table_len[in] the length of the table of devices
* @param panel_width[in] the width of the display panel
* @param panel_height[in] the height of the display panel
* @param buffer[in] pointer to a preallocated uint8_t buffer of size `SURFACE_REQUIRED_BUFFER_BYTE_SIZE(panel_width, panel_height, 16)`
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_make_mono1bpp_surface_advanced(surface_painter_device_t *device_table, size_t device_table_len, uint16_t panel_width, uint16_t panel_height, void *buffer);
// Driver storage
extern surface_painter_device_t surface_drivers[SURFACE_NUM_DEVICES];
// Surface common APIs
bool qp_surface_init(painter_device_t device, painter_rotation_t rotation);
bool qp_surface_power(painter_device_t device, bool power_on);
bool qp_surface_clear(painter_device_t device);
bool qp_surface_flush(painter_device_t device);
bool qp_surface_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
void qp_surface_increment_pixdata_location(surface_viewport_data_t *viewport);
void qp_surface_update_dirty(surface_dirty_data_t *dirty, uint16_t x, uint16_t y);
#endif // QUANTUM_PAINTER_SURFACE_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Factory functions for creating a handle to a surface
#define SURFACE_FACTORY_FUNCTION_IMPL(function_name, vtable, bpp) \
painter_device_t(function_name##_advanced)(surface_painter_device_t * device_table, size_t device_table_len, uint16_t panel_width, uint16_t panel_height, void *buffer) { \
for (uint32_t i = 0; i < device_table_len; ++i) { \
surface_painter_device_t *driver = &device_table[i]; \
if (!driver->base.driver_vtable) { \
driver->base.driver_vtable = (painter_driver_vtable_t *)&(vtable); \
driver->base.native_bits_per_pixel = (bpp); \
driver->base.comms_vtable = &dummy_comms_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->buffer = buffer; \
return (painter_device_t)driver; \
} \
} \
return NULL; \
} \
painter_device_t(function_name)(uint16_t panel_width, uint16_t panel_height, void *buffer) { \
return (function_name##_advanced)(surface_drivers, SURFACE_NUM_DEVICES, panel_width, panel_height, buffer); \
}

View file

@ -0,0 +1,113 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef QUANTUM_PAINTER_SURFACE_ENABLE
# include "color.h"
# include "qp_draw.h"
# include "qp_surface_internal.h"
# include "qp_comms_dummy.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Surface driver impl: mono1bpp
static inline void setpixel_mono1bpp(surface_painter_device_t *surface, uint16_t x, uint16_t y, bool mono_pixel) {
uint16_t w = surface->base.panel_width;
uint16_t h = surface->base.panel_height;
// Drop out if it's off-screen
if (x >= w || y >= h) {
return;
}
// Figure out which location needs to be updated
uint32_t pixel_num = y * w + x;
uint32_t byte_offset = pixel_num / 8;
uint8_t bit_offset = pixel_num % 8;
bool curr_val = (surface->u8buffer[byte_offset] & (1 << bit_offset)) ? true : false;
// Skip messing with the dirty info if the original value already matches
if (curr_val != mono_pixel) {
// Update the dirty region
qp_surface_update_dirty(&surface->dirty, x, y);
// Update the pixel data in the buffer
if (mono_pixel) {
surface->u8buffer[byte_offset] |= (1 << bit_offset);
} else {
surface->u8buffer[byte_offset] &= ~(1 << bit_offset);
}
}
}
static inline void append_pixel_mono1bpp(surface_painter_device_t *surface, bool mono_pixel) {
setpixel_mono1bpp(surface, surface->viewport.pixdata_x, surface->viewport.pixdata_y, mono_pixel);
qp_surface_increment_pixdata_location(&surface->viewport);
}
static inline void stream_pixdata_mono1bpp(surface_painter_device_t *surface, const uint8_t *data, uint32_t native_pixel_count) {
for (uint32_t pixel_counter = 0; pixel_counter < native_pixel_count; ++pixel_counter) {
uint32_t byte_offset = pixel_counter / 8;
uint8_t bit_offset = pixel_counter % 8;
append_pixel_mono1bpp(surface, (data[byte_offset] & (1 << bit_offset)) ? true : false);
}
}
// Stream pixel data to the current write position in GRAM
static bool qp_surface_pixdata_mono1bpp(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
painter_driver_t * driver = (painter_driver_t *)device;
surface_painter_device_t *surface = (surface_painter_device_t *)driver;
stream_pixdata_mono1bpp(surface, (const uint8_t *)pixel_data, native_pixel_count);
return true;
}
// Pixel colour conversion
static bool qp_surface_palette_convert_mono1bpp(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) {
for (int16_t i = 0; i < palette_size; ++i) {
palette[i].mono = (palette[i].hsv888.v > 127) ? 1 : 0;
}
return true;
}
// Append pixels to the target location, keyed by the pixel index
static bool qp_surface_append_pixels_mono1bpp(painter_device_t device, uint8_t *target_buffer, qp_pixel_t *palette, uint32_t pixel_offset, uint32_t pixel_count, uint8_t *palette_indices) {
for (uint32_t i = 0; i < pixel_count; ++i) {
uint32_t pixel_num = pixel_offset + i;
uint32_t byte_offset = pixel_num / 8;
uint8_t bit_offset = pixel_num % 8;
if (palette[palette_indices[i]].mono) {
target_buffer[byte_offset] |= (1 << bit_offset);
} else {
target_buffer[byte_offset] &= ~(1 << bit_offset);
}
}
return true;
}
static bool mono1bpp_target_pixdata_transfer(painter_driver_t *surface_driver, painter_driver_t *target_driver, uint16_t x, uint16_t y, bool entire_surface) {
return false; // Not yet supported.
}
static bool qp_surface_append_pixdata_mono1bpp(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte) {
return false; // Just use 1bpp images.
}
const surface_painter_driver_vtable_t mono1bpp_surface_driver_vtable = {
.base =
{
.init = qp_surface_init,
.power = qp_surface_power,
.clear = qp_surface_clear,
.flush = qp_surface_flush,
.pixdata = qp_surface_pixdata_mono1bpp,
.viewport = qp_surface_viewport,
.palette_convert = qp_surface_palette_convert_mono1bpp,
.append_pixels = qp_surface_append_pixels_mono1bpp,
.append_pixdata = qp_surface_append_pixdata_mono1bpp,
},
.target_pixdata_transfer = mono1bpp_target_pixdata_transfer,
};
SURFACE_FACTORY_FUNCTION_IMPL(qp_make_mono1bpp_surface, mono1bpp_surface_driver_vtable, 1);
#endif // QUANTUM_PAINTER_SURFACE_ENABLE

View file

@ -0,0 +1,145 @@
// Copyright 2022 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#ifdef QUANTUM_PAINTER_SURFACE_ENABLE
# include "color.h"
# include "qp_draw.h"
# include "qp_surface_internal.h"
# include "qp_comms_dummy.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Surface driver impl: rgb565
static inline void setpixel_rgb565(surface_painter_device_t *surface, uint16_t x, uint16_t y, uint16_t rgb565) {
uint16_t w = surface->base.panel_width;
uint16_t h = surface->base.panel_height;
// Drop out if it's off-screen
if (x >= w || y >= h) {
return;
}
// Skip messing with the dirty info if the original value already matches
if (surface->u16buffer[y * w + x] != rgb565) {
// Update the dirty region
qp_surface_update_dirty(&surface->dirty, x, y);
// Update the pixel data in the buffer
surface->u16buffer[y * w + x] = rgb565;
}
}
static inline void append_pixel_rgb565(surface_painter_device_t *surface, uint16_t rgb565) {
setpixel_rgb565(surface, surface->viewport.pixdata_x, surface->viewport.pixdata_y, rgb565);
qp_surface_increment_pixdata_location(&surface->viewport);
}
static inline void stream_pixdata_rgb565(surface_painter_device_t *surface, const uint16_t *data, uint32_t native_pixel_count) {
for (uint32_t pixel_counter = 0; pixel_counter < native_pixel_count; ++pixel_counter) {
append_pixel_rgb565(surface, data[pixel_counter]);
}
}
// Stream pixel data to the current write position in GRAM
static bool qp_surface_pixdata_rgb565(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
painter_driver_t * driver = (painter_driver_t *)device;
surface_painter_device_t *surface = (surface_painter_device_t *)driver;
stream_pixdata_rgb565(surface, (const uint16_t *)pixel_data, native_pixel_count);
return true;
}
// Pixel colour conversion
static bool qp_surface_palette_convert_rgb565_swapped(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) {
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});
uint16_t rgb565 = (((uint16_t)rgb.r) >> 3) << 11 | (((uint16_t)rgb.g) >> 2) << 5 | (((uint16_t)rgb.b) >> 3);
palette[i].rgb565 = __builtin_bswap16(rgb565);
}
return true;
}
// Append pixels to the target location, keyed by the pixel index
static bool qp_surface_append_pixels_rgb565(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;
}
static bool rgb565_target_pixdata_transfer(painter_driver_t *surface_driver, painter_driver_t *target_driver, uint16_t x, uint16_t y, bool entire_surface) {
surface_painter_device_t *surface_handle = (surface_painter_device_t *)surface_driver;
uint16_t l = entire_surface ? 0 : surface_handle->dirty.l;
uint16_t t = entire_surface ? 0 : surface_handle->dirty.t;
uint16_t r = entire_surface ? (surface_handle->base.panel_width - 1) : surface_handle->dirty.r;
uint16_t b = entire_surface ? (surface_handle->base.panel_height - 1) : surface_handle->dirty.b;
// Set the target drawing area
bool ok = qp_viewport((painter_device_t)target_driver, x + l, y + t, x + r, y + b);
if (!ok) {
qp_dprintf("rgb565_target_pixdata_transfer: fail (could not set target viewport)\n");
return false;
}
// Housekeeping of the amount of pixels to transfer
uint32_t total_pixel_count = (8 * QUANTUM_PAINTER_PIXDATA_BUFFER_SIZE) / surface_driver->native_bits_per_pixel;
uint32_t pixel_counter = 0;
uint16_t *target_buffer = (uint16_t *)qp_internal_global_pixdata_buffer;
// Fill the global pixdata area so that we can start transferring to the panel
for (uint16_t y = t; y <= b; ++y) {
for (uint16_t x = l; x <= r; ++x) {
// Update the target buffer
target_buffer[pixel_counter++] = surface_handle->u16buffer[y * surface_handle->base.panel_width + x];
// If we've accumulated enough data, send it
if (pixel_counter == total_pixel_count) {
ok = qp_pixdata((painter_device_t)target_driver, qp_internal_global_pixdata_buffer, pixel_counter);
if (!ok) {
qp_dprintf("rgb565_target_pixdata_transfer: fail (could not stream pixdata to target)\n");
return false;
}
// Reset the counter
pixel_counter = 0;
}
}
}
// If there's any leftover data, send it
if (pixel_counter > 0) {
ok = qp_pixdata((painter_device_t)target_driver, qp_internal_global_pixdata_buffer, pixel_counter);
if (!ok) {
qp_dprintf("rgb565_target_pixdata_transfer: fail (could not stream pixdata to target)\n");
return false;
}
}
return true;
}
static bool qp_surface_append_pixdata_rgb565(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte) {
target_buffer[pixdata_offset] = pixdata_byte;
return true;
}
const surface_painter_driver_vtable_t rgb565_surface_driver_vtable = {
.base =
{
.init = qp_surface_init,
.power = qp_surface_power,
.clear = qp_surface_clear,
.flush = qp_surface_flush,
.pixdata = qp_surface_pixdata_rgb565,
.viewport = qp_surface_viewport,
.palette_convert = qp_surface_palette_convert_rgb565_swapped,
.append_pixels = qp_surface_append_pixels_rgb565,
.append_pixdata = qp_surface_append_pixdata_rgb565,
},
.target_pixdata_transfer = rgb565_target_pixdata_transfer,
};
SURFACE_FACTORY_FUNCTION_IMPL(qp_make_rgb565_surface, rgb565_surface_driver_vtable, 16);
#endif // QUANTUM_PAINTER_SURFACE_ENABLE

View file

@ -103,13 +103,14 @@ painter_device_t qp_ili9163_make_spi_device(uint16_t panel_width, uint16_t panel
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;
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;
driver->spi_dc_reset_config.command_params_uses_command_pin = false;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(tft_panel_dc_reset_painter_device_t));

View file

@ -1,6 +1,5 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"

View file

@ -110,13 +110,14 @@ painter_device_t qp_ili9341_make_spi_device(uint16_t panel_width, uint16_t panel
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;
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;
driver->spi_dc_reset_config.command_params_uses_command_pin = false;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(tft_panel_dc_reset_painter_device_t));

View file

@ -1,6 +1,5 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"

View file

@ -103,13 +103,14 @@ painter_device_t qp_ili9488_make_spi_device(uint16_t panel_width, uint16_t panel
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;
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;
driver->spi_dc_reset_config.command_params_uses_command_pin = false;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(tft_panel_dc_reset_painter_device_t));

View file

@ -1,6 +1,5 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"

View file

@ -0,0 +1,195 @@
// Copyright 2023 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_oled_panel.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter API implementations
// Power control
bool qp_oled_panel_power(painter_device_t device, bool power_on) {
painter_driver_t * driver = (painter_driver_t *)device;
oled_panel_painter_driver_vtable_t *vtable = (oled_panel_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_oled_panel_clear(painter_device_t device) {
painter_driver_t *driver = (painter_driver_t *)device;
driver->driver_vtable->init(device, driver->rotation); // Re-init the display
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Surface passthru
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
bool qp_oled_panel_passthru_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count) {
oled_panel_painter_device_t *driver = (oled_panel_painter_device_t *)device;
return driver->surface.base.validate_ok && driver->surface.base.driver_vtable->pixdata(&driver->surface.base, pixel_data, native_pixel_count);
}
bool qp_oled_panel_passthru_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
oled_panel_painter_device_t *driver = (oled_panel_painter_device_t *)device;
return driver->surface.base.validate_ok && driver->surface.base.driver_vtable->viewport(&driver->surface.base, left, top, right, bottom);
}
bool qp_oled_panel_passthru_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette) {
oled_panel_painter_device_t *driver = (oled_panel_painter_device_t *)device;
return driver->surface.base.validate_ok && driver->surface.base.driver_vtable->palette_convert(&driver->surface.base, palette_size, palette);
}
bool qp_oled_panel_passthru_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) {
oled_panel_painter_device_t *driver = (oled_panel_painter_device_t *)device;
return driver->surface.base.validate_ok && driver->surface.base.driver_vtable->append_pixels(&driver->surface.base, target_buffer, palette, pixel_offset, pixel_count, palette_indices);
}
bool qp_oled_panel_passthru_append_pixdata(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte) {
oled_panel_painter_device_t *driver = (oled_panel_painter_device_t *)device;
return driver->surface.base.validate_ok && driver->surface.base.driver_vtable->append_pixdata(&driver->surface.base, target_buffer, pixdata_offset, pixdata_byte);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Flush helpers
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void qp_oled_panel_page_column_flush_rot0(painter_device_t device, surface_dirty_data_t *dirty, const uint8_t *framebuffer) {
painter_driver_t * driver = (painter_driver_t *)device;
oled_panel_painter_driver_vtable_t *vtable = (oled_panel_painter_driver_vtable_t *)driver->driver_vtable;
// TODO: account for offset_x/y in base driver
int min_page = dirty->t / 8;
int max_page = dirty->b / 8;
int min_column = dirty->l;
int max_column = dirty->r;
for (int page = min_page; page <= max_page; ++page) {
int cols_required = max_column - min_column + 1;
uint8_t column_data[cols_required];
memset(column_data, 0, cols_required);
for (int x = min_column; x <= max_column; ++x) {
uint16_t data_offset = x - min_column;
for (int y = 0; y < 8; ++y) {
uint32_t pixel_num = ((page * 8) + y) * driver->panel_width + x;
uint32_t byte_offset = pixel_num / 8;
uint8_t bit_offset = pixel_num % 8;
column_data[data_offset] |= ((framebuffer[byte_offset] & (1 << bit_offset)) >> bit_offset) << y;
}
}
int actual_page = page;
int start_column = min_column;
qp_comms_command(device, vtable->opcodes.set_page | actual_page);
qp_comms_command(device, vtable->opcodes.set_column_lsb | (start_column & 0x0F));
qp_comms_command(device, vtable->opcodes.set_column_msb | (start_column & 0xF0) >> 4);
qp_comms_send(device, column_data, cols_required);
}
}
void qp_oled_panel_page_column_flush_rot90(painter_device_t device, surface_dirty_data_t *dirty, const uint8_t *framebuffer) {
painter_driver_t * driver = (painter_driver_t *)device;
oled_panel_painter_driver_vtable_t *vtable = (oled_panel_painter_driver_vtable_t *)driver->driver_vtable;
// TODO: account for offset_x/y in base driver
int num_columns = driver->panel_width;
int min_page = dirty->l / 8;
int max_page = dirty->r / 8;
int min_column = dirty->t;
int max_column = dirty->b;
for (int page = min_page; page <= max_page; ++page) {
int cols_required = max_column - min_column + 1;
uint8_t column_data[cols_required];
memset(column_data, 0, cols_required);
for (int y = min_column; y <= max_column; ++y) {
uint16_t data_offset = cols_required - 1 - (y - min_column);
for (int x = 0; x < 8; ++x) {
uint32_t pixel_num = y * driver->panel_height + ((page * 8) + x);
uint32_t byte_offset = pixel_num / 8;
uint8_t bit_offset = pixel_num % 8;
column_data[data_offset] |= ((framebuffer[byte_offset] & (1 << bit_offset)) >> bit_offset) << x;
}
}
int actual_page = page;
int start_column = num_columns - 1 - max_column;
qp_comms_command(device, vtable->opcodes.set_page | actual_page);
qp_comms_command(device, vtable->opcodes.set_column_lsb | (start_column & 0x0F));
qp_comms_command(device, vtable->opcodes.set_column_msb | (start_column & 0xF0) >> 4);
qp_comms_send(device, column_data, cols_required);
}
}
void qp_oled_panel_page_column_flush_rot180(painter_device_t device, surface_dirty_data_t *dirty, const uint8_t *framebuffer) {
painter_driver_t * driver = (painter_driver_t *)device;
oled_panel_painter_driver_vtable_t *vtable = (oled_panel_painter_driver_vtable_t *)driver->driver_vtable;
// TODO: account for offset_x/y in base driver
int num_pages = driver->panel_height / 8;
int num_columns = driver->panel_width;
int min_page = dirty->t / 8;
int max_page = dirty->b / 8;
int min_column = dirty->l;
int max_column = dirty->r;
for (int page = min_page; page <= max_page; ++page) {
int cols_required = max_column - min_column + 1;
uint8_t column_data[cols_required];
memset(column_data, 0, cols_required);
for (int x = min_column; x <= max_column; ++x) {
uint16_t data_offset = cols_required - 1 - (x - min_column);
for (int y = 0; y < 8; ++y) {
uint32_t pixel_num = ((page * 8) + y) * driver->panel_width + x;
uint32_t byte_offset = pixel_num / 8;
uint8_t bit_offset = pixel_num % 8;
column_data[data_offset] |= ((framebuffer[byte_offset] & (1 << bit_offset)) >> bit_offset) << (7 - y);
}
}
int actual_page = num_pages - 1 - page;
int start_column = num_columns - 1 - max_column;
qp_comms_command(device, vtable->opcodes.set_page | actual_page);
qp_comms_command(device, vtable->opcodes.set_column_lsb | (start_column & 0x0F));
qp_comms_command(device, vtable->opcodes.set_column_msb | (start_column & 0xF0) >> 4);
qp_comms_send(device, column_data, cols_required);
}
}
void qp_oled_panel_page_column_flush_rot270(painter_device_t device, surface_dirty_data_t *dirty, const uint8_t *framebuffer) {
painter_driver_t * driver = (painter_driver_t *)device;
oled_panel_painter_driver_vtable_t *vtable = (oled_panel_painter_driver_vtable_t *)driver->driver_vtable;
// TODO: account for offset_x/y in base driver
int num_pages = driver->panel_height / 8;
int min_page = dirty->l / 8;
int max_page = dirty->r / 8;
int min_column = dirty->t;
int max_column = dirty->b;
for (int page = min_page; page <= max_page; ++page) {
int cols_required = max_column - min_column + 1;
uint8_t column_data[cols_required];
memset(column_data, 0, cols_required);
for (int y = min_column; y <= max_column; ++y) {
uint16_t data_offset = y - min_column;
for (int x = 0; x < 8; ++x) {
uint32_t pixel_num = y * driver->panel_height + ((page * 8) + x);
uint32_t byte_offset = pixel_num / 8;
uint8_t bit_offset = pixel_num % 8;
column_data[data_offset] |= ((framebuffer[byte_offset] & (1 << bit_offset)) >> bit_offset) << (7 - x);
}
}
int actual_page = num_pages - 1 - page;
int start_column = min_column;
qp_comms_command(device, vtable->opcodes.set_page | actual_page);
qp_comms_command(device, vtable->opcodes.set_column_lsb | (start_column & 0x0F));
qp_comms_command(device, vtable->opcodes.set_column_msb | (start_column & 0xF0) >> 4);
qp_comms_send(device, column_data, cols_required);
}
}

View file

@ -0,0 +1,68 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "color.h"
#include "qp_internal.h"
#include "qp_surface_internal.h"
#ifdef QUANTUM_PAINTER_SPI_ENABLE
# include "qp_comms_spi.h"
#endif // QUANTUM_PAINTER_SPI_ENABLE
#ifdef QUANTUM_PAINTER_I2C_ENABLE
# include "qp_comms_i2c.h"
#endif // QUANTUM_PAINTER_I2C_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Common OLED panel implementation
// Driver vtable with extras
typedef struct oled_panel_painter_driver_vtable_t {
painter_driver_vtable_t base; // must be first, so it can be cast to/from the painter_driver_vtable_t* type
// Opcodes for normal display operation
struct {
uint8_t display_on;
uint8_t display_off;
uint8_t set_page;
uint8_t set_column_lsb;
uint8_t set_column_msb;
} opcodes;
} oled_panel_painter_driver_vtable_t;
// Device definition
typedef struct oled_panel_painter_device_t {
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
qp_comms_spi_dc_reset_config_t spi_dc_reset_config;
#endif // QUANTUM_PAINTER_SPI_ENABLE
#ifdef QUANTUM_PAINTER_I2C_ENABLE
// I2C-based configurables
qp_comms_i2c_config_t i2c_config;
#endif // QUANTUM_PAINTER_I2C_ENABLE
};
surface_painter_device_t surface;
} oled_panel_painter_device_t;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Forward declarations for injecting into concrete driver vtables
bool qp_oled_panel_power(painter_device_t device, bool power_on);
bool qp_oled_panel_clear(painter_device_t device);
bool qp_oled_panel_passthru_pixdata(painter_device_t device, const void *pixel_data, uint32_t native_pixel_count);
bool qp_oled_panel_passthru_viewport(painter_device_t device, uint16_t left, uint16_t top, uint16_t right, uint16_t bottom);
bool qp_oled_panel_passthru_palette_convert(painter_device_t device, int16_t palette_size, qp_pixel_t *palette);
bool qp_oled_panel_passthru_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);
bool qp_oled_panel_passthru_append_pixdata(painter_device_t device, uint8_t *target_buffer, uint32_t pixdata_offset, uint8_t pixdata_byte);
// Helpers for flushing data from the dirty region to the correct location on the OLED
void qp_oled_panel_page_column_flush_rot0(painter_device_t device, surface_dirty_data_t *dirty, const uint8_t *framebuffer);
void qp_oled_panel_page_column_flush_rot90(painter_device_t device, surface_dirty_data_t *dirty, const uint8_t *framebuffer);
void qp_oled_panel_page_column_flush_rot180(painter_device_t device, surface_dirty_data_t *dirty, const uint8_t *framebuffer);
void qp_oled_panel_page_column_flush_rot270(painter_device_t device, surface_dirty_data_t *dirty, const uint8_t *framebuffer);

View file

@ -0,0 +1,206 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#include "qp_internal.h"
#include "qp_comms.h"
#include "qp_oled_panel.h"
#include "qp_sh1106.h"
#include "qp_sh1106_opcodes.h"
#include "qp_surface.h"
#include "qp_surface_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver storage
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
typedef struct sh1106_device_t {
oled_panel_painter_device_t oled;
uint8_t framebuffer[SURFACE_REQUIRED_BUFFER_BYTE_SIZE(128, 64, 1)];
} sh1106_device_t;
static sh1106_device_t sh1106_drivers[SH1106_NUM_DEVICES] = {0};
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter API implementations
// Initialisation
__attribute__((weak)) bool qp_sh1106_init(painter_device_t device, painter_rotation_t rotation) {
sh1106_device_t *driver = (sh1106_device_t *)device;
// Change the surface geometry based on the panel rotation
if (rotation == QP_ROTATION_90 || rotation == QP_ROTATION_270) {
driver->oled.surface.base.panel_width = driver->oled.base.panel_height;
driver->oled.surface.base.panel_height = driver->oled.base.panel_width;
} else {
driver->oled.surface.base.panel_width = driver->oled.base.panel_width;
driver->oled.surface.base.panel_height = driver->oled.base.panel_height;
}
// Init the internal surface
if (!qp_init(&driver->oled.surface.base, QP_ROTATION_0)) {
qp_dprintf("Failed to init internal surface in qp_sh1106_init\n");
return false;
}
// clang-format off
const uint8_t sh1106_init_sequence[] = {
// Command, Delay, N, Data[N]
SH1106_SET_MUX_RATIO, 0, 1, 0x3F,
SH1106_DISPLAY_OFFSET, 0, 1, 0x00,
SH1106_DISPLAY_START_LINE, 0, 0,
SH1106_SET_SEGMENT_REMAP_INV, 0, 0,
SH1106_COM_SCAN_DIR_DEC, 0, 0,
SH1106_COM_PADS_HW_CFG, 0, 1, 0x12,
SH1106_SET_CONTRAST, 0, 1, 0x7F,
SH1106_ALL_ON_RESUME, 0, 0,
SH1106_NON_INVERTING_DISPLAY, 0, 0,
SH1106_SET_OSC_DIVFREQ, 0, 1, 0x80,
SH1106_SET_CHARGE_PUMP, 0, 1, 0x14,
SH1106_DISPLAY_ON, 0, 0,
};
// clang-format on
qp_comms_bulk_command_sequence(device, sh1106_init_sequence, sizeof(sh1106_init_sequence));
return true;
}
// Screen flush
bool qp_sh1106_flush(painter_device_t device) {
sh1106_device_t *driver = (sh1106_device_t *)device;
if (!driver->oled.surface.dirty.is_dirty) {
return true;
}
switch (driver->oled.base.rotation) {
default:
case QP_ROTATION_0:
qp_oled_panel_page_column_flush_rot0(device, &driver->oled.surface.dirty, driver->framebuffer);
break;
case QP_ROTATION_90:
qp_oled_panel_page_column_flush_rot90(device, &driver->oled.surface.dirty, driver->framebuffer);
break;
case QP_ROTATION_180:
qp_oled_panel_page_column_flush_rot180(device, &driver->oled.surface.dirty, driver->framebuffer);
break;
case QP_ROTATION_270:
qp_oled_panel_page_column_flush_rot270(device, &driver->oled.surface.dirty, driver->framebuffer);
break;
}
// Clear the dirty area
qp_flush(&driver->oled.surface);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Driver vtable
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
const oled_panel_painter_driver_vtable_t sh1106_driver_vtable = {
.base =
{
.init = qp_sh1106_init,
.power = qp_oled_panel_power,
.clear = qp_oled_panel_clear,
.flush = qp_sh1106_flush,
.pixdata = qp_oled_panel_passthru_pixdata,
.viewport = qp_oled_panel_passthru_viewport,
.palette_convert = qp_oled_panel_passthru_palette_convert,
.append_pixels = qp_oled_panel_passthru_append_pixels,
.append_pixdata = qp_oled_panel_passthru_append_pixdata,
},
.opcodes =
{
.display_on = SH1106_DISPLAY_ON,
.display_off = SH1106_DISPLAY_OFF,
.set_page = SH1106_PAGE_ADDR,
.set_column_lsb = SH1106_SETCOLUMN_LSB,
.set_column_msb = SH1106_SETCOLUMN_MSB,
},
};
#ifdef QUANTUM_PAINTER_SH1106_SPI_ENABLE
// Factory function for creating a handle to the SH1106 device
painter_device_t qp_sh1106_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 < SH1106_NUM_DEVICES; ++i) {
sh1106_device_t *driver = &sh1106_drivers[i];
if (!driver->oled.base.driver_vtable) {
painter_device_t surface = qp_make_mono1bpp_surface_advanced(&driver->oled.surface, 1, panel_width, panel_height, driver->framebuffer);
if (!surface) {
return NULL;
}
// Setup the OLED device
driver->oled.base.driver_vtable = (const painter_driver_vtable_t *)&sh1106_driver_vtable;
driver->oled.base.comms_vtable = (const painter_comms_vtable_t *)&spi_comms_with_dc_vtable;
driver->oled.base.native_bits_per_pixel = 1; // 1bpp mono
driver->oled.base.panel_width = panel_width;
driver->oled.base.panel_height = panel_height;
driver->oled.base.rotation = QP_ROTATION_0;
driver->oled.base.offset_x = 0;
driver->oled.base.offset_y = 0;
// SPI and other pin configuration
driver->oled.base.comms_config = &driver->oled.spi_dc_reset_config;
driver->oled.spi_dc_reset_config.spi_config.chip_select_pin = chip_select_pin;
driver->oled.spi_dc_reset_config.spi_config.divisor = spi_divisor;
driver->oled.spi_dc_reset_config.spi_config.lsb_first = false;
driver->oled.spi_dc_reset_config.spi_config.mode = spi_mode;
driver->oled.spi_dc_reset_config.dc_pin = dc_pin;
driver->oled.spi_dc_reset_config.reset_pin = reset_pin;
driver->oled.spi_dc_reset_config.command_params_uses_command_pin = true;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(sh1106_device_t));
return NULL;
}
return (painter_device_t)driver;
}
}
return NULL;
}
#endif // QUANTUM_PAINTER_SH1106_SPI_ENABLE
#ifdef QUANTUM_PAINTER_SH1106_I2C_ENABLE
// Factory function for creating a handle to the SH1106 device
painter_device_t qp_sh1106_make_i2c_device(uint16_t panel_width, uint16_t panel_height, uint8_t i2c_address) {
for (uint32_t i = 0; i < SH1106_NUM_DEVICES; ++i) {
sh1106_device_t *driver = &sh1106_drivers[i];
if (!driver->oled.base.driver_vtable) {
// Instantiate the surface, intentional swap of width/high due to transpose
painter_device_t surface = qp_make_mono1bpp_surface_advanced(&driver->oled.surface, 1, panel_width, panel_height, driver->framebuffer);
if (!surface) {
return NULL;
}
// Setup the OLED device
driver->oled.base.driver_vtable = (const painter_driver_vtable_t *)&sh1106_driver_vtable;
driver->oled.base.comms_vtable = (const painter_comms_vtable_t *)&i2c_comms_cmddata_vtable;
driver->oled.base.native_bits_per_pixel = 1; // 1bpp mono
driver->oled.base.panel_width = panel_width;
driver->oled.base.panel_height = panel_height;
driver->oled.base.rotation = QP_ROTATION_0;
driver->oled.base.offset_x = 0;
driver->oled.base.offset_y = 0;
// I2C configuration
driver->oled.base.comms_config = &driver->oled.i2c_config;
driver->oled.i2c_config.chip_address = i2c_address;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(sh1106_device_t));
return NULL;
}
return (painter_device_t)driver;
}
}
return NULL;
}
#endif // QUANTUM_PAINTER_SH1106_SPI_ENABLE

View file

@ -0,0 +1,66 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"
#include "qp_internal.h"
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter SH1106 configurables (add to your keyboard's config.h)
#if defined(QUANTUM_PAINTER_SH1106_SPI_ENABLE) && !defined(SH1106_NUM_SPI_DEVICES)
/**
* @def This controls the maximum number of SPI SH1106 devices that Quantum Painter can communicate with at any one time.
* Increasing this number allows for multiple displays to be used.
*/
# define SH1106_NUM_SPI_DEVICES 1
#else
# define SH1106_NUM_SPI_DEVICES 0
#endif
#if defined(QUANTUM_PAINTER_SH1106_I2C_ENABLE) && !defined(SH1106_NUM_I2C_DEVICES)
/**
* @def This controls the maximum number of I2C SH1106 devices that Quantum Painter can communicate with at any one time.
* Increasing this number allows for multiple displays to be used.
*/
# define SH1106_NUM_I2C_DEVICES 1
#else
# define SH1106_NUM_I2C_DEVICES 0
#endif
#define SH1106_NUM_DEVICES ((SH1106_NUM_SPI_DEVICES) + (SH1106_NUM_I2C_DEVICES))
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter SH1106 device factories
#ifdef QUANTUM_PAINTER_SH1106_SPI_ENABLE
/**
* Factory method for an SH1106 SPI LCD device.
*
* @param panel_width[in] the width of the display in pixels (usually 128)
* @param panel_height[in] the height of the display in pixels (usually 64)
* @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_sh1106_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_SH1106_SPI_ENABLE
#ifdef QUANTUM_PAINTER_SH1106_I2C_ENABLE
/**
* Factory method for an SH1106 I2C LCD device.
*
* @param panel_width[in] the width of the display in pixels (usually 128)
* @param panel_height[in] the height of the display in pixels (usually 64)
* @param i2c_address[in] the I2C address to use
* @return the device handle used with all drawing routines in Quantum Painter
*/
painter_device_t qp_sh1106_make_i2c_device(uint16_t panel_width, uint16_t panel_height, uint8_t i2c_address);
#endif // QUANTUM_PAINTER_SH1106_I2C_ENABLE

View file

@ -0,0 +1,26 @@
// Copyright 2023 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#define SH1106_DISPLAY_ON 0xAF
#define SH1106_DISPLAY_OFF 0xAE
#define SH1106_SET_OSC_DIVFREQ 0xD5
#define SH1106_SET_MUX_RATIO 0xA8
#define SH1106_DISPLAY_OFFSET 0xD3
#define SH1106_DISPLAY_START_LINE 0x40
#define SH1106_SET_CHARGE_PUMP 0x8D
#define SH1106_SET_SEGMENT_REMAP_NORMAL 0xA0
#define SH1106_SET_SEGMENT_REMAP_INV 0xA1
#define SH1106_COM_SCAN_DIR_INC 0xC0
#define SH1106_COM_SCAN_DIR_DEC 0xC8
#define SH1106_COM_PADS_HW_CFG 0xDA
#define SH1106_SET_CONTRAST 0x81
#define SH1106_SET_PRECHARGE_PERIOD 0xD9
#define SH1106_VCOM_DETECT 0xDB
#define SH1106_ALL_ON_RESUME 0xA4
#define SH1106_NON_INVERTING_DISPLAY 0xA6
#define SH1106_DEACTIVATE_SCROLL 0x2E
#define SH1106_SETCOLUMN_LSB 0x00
#define SH1106_SETCOLUMN_MSB 0x10
#define SH1106_PAGE_ADDR 0xB0

View file

@ -107,13 +107,14 @@ painter_device_t qp_ssd1351_make_spi_device(uint16_t panel_width, uint16_t panel
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;
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;
driver->spi_dc_reset_config.command_params_uses_command_pin = false;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(tft_panel_dc_reset_painter_device_t));

View file

@ -1,6 +1,5 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"

View file

@ -1,6 +1,5 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View file

@ -127,13 +127,14 @@ painter_device_t qp_st7735_make_spi_device(uint16_t panel_width, uint16_t panel_
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;
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;
driver->spi_dc_reset_config.command_params_uses_command_pin = false;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(tft_panel_dc_reset_painter_device_t));

View file

@ -2,7 +2,6 @@
// Copyright 2021 Nick Brassel (@tzarc)
// Copyright 2022 David Hoelscher (@customMK)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"

View file

@ -1,6 +1,5 @@
// Copyright 2022 David Hoelscher (@customMK)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View file

@ -126,13 +126,14 @@ painter_device_t qp_st7789_make_spi_device(uint16_t panel_width, uint16_t panel_
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;
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;
driver->spi_dc_reset_config.command_params_uses_command_pin = false;
if (!qp_internal_register_device((painter_device_t)driver)) {
memset(driver, 0, sizeof(tft_panel_dc_reset_painter_device_t));

View file

@ -1,7 +1,6 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "gpio.h"

View file

@ -1,6 +1,5 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View file

@ -1,6 +1,5 @@
// Copyright 2021 Paul Cotter (@gr1mr3aver)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

View file

@ -1,5 +1,6 @@
// Copyright 2021 Nick Brassel (@tzarc)
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "color.h"
#include "qp_internal.h"

View file

@ -10,7 +10,7 @@
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// 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) {
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, bool *is_panel_native, painter_compression_t *compression_scheme, uint32_t *total_bytes) {
// Seek to the start
qp_stream_setpos(stream, 0);
@ -49,7 +49,7 @@ bool qff_read_font_descriptor(qp_stream_t *stream, uint8_t *line_height, bool *h
*num_unicode_glyphs = font_descriptor.num_unicode_glyphs;
}
if (bpp || has_palette) {
if (!qgf_parse_format(font_descriptor.format, bpp, has_palette)) {
if (!qgf_parse_format(font_descriptor.format, bpp, has_palette, is_panel_native)) {
return false;
}
}
@ -102,7 +102,7 @@ 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)) {
if (!qff_read_font_descriptor(stream, NULL, &has_ascii_table, &num_unicode_glyphs, NULL, NULL, NULL, NULL, NULL)) {
return false;
}
@ -127,7 +127,7 @@ uint32_t qff_get_total_size(qp_stream_t *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)) {
if (!qff_read_font_descriptor(stream, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &total_size)) {
return false;
}

View file

@ -85,4 +85,4 @@ typedef struct QP_PACKED qff_unicode_glyph_table_v1_t {
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);
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, bool *is_panel_native, painter_compression_t *compression_scheme, uint32_t *total_bytes);

View file

@ -24,22 +24,23 @@ bool qgf_validate_block_header(qgf_block_header_v1_t *desc, uint8_t expected_typ
return true;
}
bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette) {
bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette, bool *is_panel_native) {
// clang-format off
static const struct QP_PACKED {
static const struct QP_PACKED {
uint8_t bpp;
bool has_palette;
bool is_panel_native;
} 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},
[RGB565_16BPP] = {.bpp = 16, .has_palette = false},
[RGB888_24BPP] = {.bpp = 24, .has_palette = false},
[GRAYSCALE_1BPP] = {.bpp = 1, .has_palette = false, .is_panel_native = false},
[GRAYSCALE_2BPP] = {.bpp = 2, .has_palette = false, .is_panel_native = false},
[GRAYSCALE_4BPP] = {.bpp = 4, .has_palette = false, .is_panel_native = false},
[GRAYSCALE_8BPP] = {.bpp = 8, .has_palette = false, .is_panel_native = false},
[PALETTE_1BPP] = {.bpp = 1, .has_palette = true, .is_panel_native = false},
[PALETTE_2BPP] = {.bpp = 2, .has_palette = true, .is_panel_native = false},
[PALETTE_4BPP] = {.bpp = 4, .has_palette = true, .is_panel_native = false},
[PALETTE_8BPP] = {.bpp = 8, .has_palette = true, .is_panel_native = false},
[RGB565_16BPP] = {.bpp = 16, .has_palette = false, .is_panel_native = true},
[RGB888_24BPP] = {.bpp = 24, .has_palette = false, .is_panel_native = true},
};
// clang-format on
@ -56,13 +57,16 @@ bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette)
if (has_palette) {
*has_palette = formats[format].has_palette;
}
if (is_panel_native) {
*is_panel_native = formats[format].is_panel_native;
}
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) {
bool qgf_parse_frame_descriptor(qgf_frame_v1_t *frame_descriptor, uint8_t *bpp, bool *has_palette, bool *is_panel_native, bool *is_delta, painter_compression_t *compression_scheme, uint16_t *delay) {
// Decode the format
qgf_parse_format(frame_descriptor->format, bpp, has_palette);
qgf_parse_format(frame_descriptor->format, bpp, has_palette, is_panel_native);
// Copy out the required info
if (is_delta) {
@ -173,7 +177,7 @@ void qgf_seek_to_frame_descriptor(qp_stream_t *stream, uint16_t frame_number) {
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) {
bool qgf_validate_frame_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t *bpp, bool *has_palette, bool *is_panel_native, bool *is_delta) {
// Seek to the correct location
qgf_seek_to_frame_descriptor(stream, frame_number);
@ -189,7 +193,7 @@ bool qgf_validate_frame_descriptor(qp_stream_t *stream, uint16_t frame_number, u
return false;
}
return qgf_parse_frame_descriptor(&frame_descriptor, bpp, has_palette, is_delta, NULL, NULL);
return qgf_parse_frame_descriptor(&frame_descriptor, bpp, has_palette, is_panel_native, is_delta, NULL, NULL);
}
bool qgf_validate_palette_descriptor(qp_stream_t *stream, uint16_t frame_number, uint8_t bpp) {
@ -253,8 +257,9 @@ bool qgf_validate_stream(qp_stream_t *stream) {
// Validate the frame descriptor block
uint8_t bpp;
bool has_palette;
bool is_panel_native;
bool has_delta;
if (!qgf_validate_frame_descriptor(stream, i, &bpp, &has_palette, &has_delta)) {
if (!qgf_validate_frame_descriptor(stream, i, &bpp, &has_palette, &is_panel_native, &has_delta)) {
return false;
}

View file

@ -65,7 +65,7 @@ _Static_assert(sizeof(qgf_frame_offsets_v1_t) == sizeof(qgf_block_header_v1_t),
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.
qp_image_format_t format : 8; // Frame format, see qp_internal_formats.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)
@ -131,6 +131,6 @@ 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);
bool qgf_parse_format(qp_image_format_t format, uint8_t *bpp, bool *has_palette, bool *is_panel_native);
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);
bool qgf_parse_frame_descriptor(qgf_frame_v1_t *frame_descriptor, uint8_t *bpp, bool *has_palette, bool *is_panel_native, bool *is_delta, painter_compression_t *compression_scheme, uint16_t *delay);

View file

@ -12,11 +12,11 @@
// Internal driver validation
static bool validate_driver_vtable(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 && driver->driver_vtable->append_pixdata) ? true : false;
return (driver && 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 && driver->driver_vtable->append_pixdata) ? true : false;
}
static bool validate_comms_vtable(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;
return (driver && 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(painter_driver_t *driver) {

View file

@ -539,6 +539,12 @@ int16_t qp_drawtext_recolor(painter_device_t device, uint16_t x, uint16_t y, pai
# define SSD1351_NUM_DEVICES 0
#endif // QUANTUM_PAINTER_SSD1351_ENABLE
#ifdef QUANTUM_PAINTER_SH1106_ENABLE
# include "qp_sh1106.h"
#else // QUANTUM_PAINTER_SH1106_ENABLE
# define SH1106_NUM_DEVICES 0
#endif // QUANTUM_PAINTER_SH1106_ENABLE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Quantum Painter Extras

View file

@ -115,6 +115,7 @@ typedef struct qgf_frame_info_t {
painter_compression_t compression_scheme;
uint8_t bpp;
bool has_palette;
bool is_panel_native;
bool is_delta;
uint16_t left;
uint16_t top;
@ -143,7 +144,7 @@ static bool qp_drawimage_prepare_frame_for_stream_read(painter_device_t device,
}
// 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)) {
if (!qgf_parse_frame_descriptor(&frame_descriptor, &info->bpp, &info->has_palette, &info->is_panel_native, &info->is_delta, &info->compression_scheme, &info->delay)) {
return false;
}
@ -236,8 +237,8 @@ static bool qp_drawimage_recolor_impl(painter_device_t device, uint16_t x, uint1
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;
r = x + frame_info->right;
b = y + frame_info->bottom;
} else {
l = x;
t = y;
@ -263,7 +264,7 @@ static bool qp_drawimage_recolor_impl(painter_device_t device, uint16_t x, uint1
}
bool ret = false;
if (frame_info->bpp <= 8) {
if (!frame_info->is_panel_native) {
// Set up the output state
qp_internal_pixel_output_state_t output_state = {.device = device, .pixel_write_pos = 0, .max_pixels = qp_internal_num_pixels_in_buffer(device)};

View file

@ -19,6 +19,7 @@ typedef struct qff_font_handle_t {
uint16_t num_unicode_glyphs;
uint8_t bpp;
bool has_palette;
bool is_panel_native;
painter_compression_t compression_scheme;
union {
qp_stream_t stream;
@ -97,7 +98,7 @@ static painter_font_handle_t qp_load_font_internal(bool (*stream_factory)(qff_fo
#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);
qff_read_font_descriptor(&font->stream, &font->base.line_height, &font->has_ascii_table, &font->num_unicode_glyphs, &font->bpp, &font->has_palette, &font->is_panel_native, &font->compression_scheme, NULL);
if (!qp_internal_bpp_capable(font->bpp)) {
qp_dprintf("qp_load_font: fail (image bpp too high (%d), check QUANTUM_PAINTER_SUPPORTS_256_PALETTE or QUANTUM_PAINTER_SUPPORTS_NATIVE_COLORS)\n", (int)font->bpp);

View file

@ -16,6 +16,7 @@ enum {
+ (ST7735_NUM_DEVICES) // ST7735
+ (GC9A01_NUM_DEVICES) // GC9A01
+ (SSD1351_NUM_DEVICES) // SSD1351
+ (SH1106_NUM_DEVICES) // SH1106
};
static painter_device_t qp_devices[QP_NUM_DEVICES] = {NULL};

View file

@ -44,8 +44,8 @@ typedef enum qp_image_format_t {
PALETTE_2BPP = 0x05,
PALETTE_4BPP = 0x06,
PALETTE_8BPP = 0x07,
RGB565_16BPP = 0x08,
RGB888_24BPP = 0x09,
RGB565_16BPP = 0x08, // Natively streamed to the panel, no interpolation or palette handling
RGB888_24BPP = 0x09, // Natively streamed to the panel, no interpolation or palette handling
} qp_image_format_t;
typedef enum painter_compression_t { IMAGE_UNCOMPRESSED, IMAGE_COMPRESSED_RLE } painter_compression_t;

View file

@ -6,14 +6,16 @@ QUANTUM_PAINTER_LVGL_INTEGRATION ?= no
# The list of permissible drivers that can be listed in QUANTUM_PAINTER_DRIVERS
VALID_QUANTUM_PAINTER_DRIVERS := \
rgb565_surface \
ili9163_spi \
ili9341_spi \
ili9488_spi \
st7735_spi \
st7789_spi \
gc9a01_spi \
ssd1351_spi
surface \
ili9163_spi \
ili9341_spi \
ili9488_spi \
st7735_spi \
st7789_spi \
gc9a01_spi \
ssd1351_spi \
sh1106_i2c \
sh1106_spi
#-------------------------------------------------------------------------------
@ -42,7 +44,9 @@ ifeq ($(strip $(QUANTUM_PAINTER_ANIMATIONS_ENABLE)), yes)
endif
# Comms flags
QUANTUM_PAINTER_NEEDS_COMMS_DUMMY ?= no
QUANTUM_PAINTER_NEEDS_COMMS_SPI ?= no
QUANTUM_PAINTER_NEEDS_COMMS_I2C ?= no
# Handler for each driver
define handle_quantum_painter_driver
@ -51,12 +55,8 @@ define handle_quantum_painter_driver
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)),rgb565_surface)
OPT_DEFS += -DQUANTUM_PAINTER_RGB565_SURFACE_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/generic
SRC += \
$(DRIVER_PATH)/painter/generic/qp_rgb565_surface.c \
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),surface)
QUANTUM_PAINTER_NEEDS_SURFACE := yes
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),ili9163_spi)
QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
@ -135,16 +135,60 @@ define handle_quantum_painter_driver
$(DRIVER_PATH)/painter/tft_panel/qp_tft_panel.c \
$(DRIVER_PATH)/painter/ssd1351/qp_ssd1351.c
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),sh1106_spi)
QUANTUM_PAINTER_NEEDS_SURFACE := yes
QUANTUM_PAINTER_NEEDS_COMMS_SPI := yes
QUANTUM_PAINTER_NEEDS_COMMS_SPI_DC_RESET := yes
OPT_DEFS += -DQUANTUM_PAINTER_SH1106_ENABLE -DQUANTUM_PAINTER_SH1106_SPI_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/oled_panel \
$(DRIVER_PATH)/painter/sh1106
SRC += \
$(DRIVER_PATH)/painter/oled_panel/qp_oled_panel.c \
$(DRIVER_PATH)/painter/sh1106/qp_sh1106.c
else ifeq ($$(strip $$(CURRENT_PAINTER_DRIVER)),sh1106_i2c)
QUANTUM_PAINTER_NEEDS_SURFACE := yes
QUANTUM_PAINTER_NEEDS_COMMS_I2C := yes
OPT_DEFS += -DQUANTUM_PAINTER_SH1106_ENABLE -DQUANTUM_PAINTER_SH1106_I2C_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/oled_panel \
$(DRIVER_PATH)/painter/sh1106
SRC += \
$(DRIVER_PATH)/painter/oled_panel/qp_oled_panel.c \
$(DRIVER_PATH)/painter/sh1106/qp_sh1106.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 a surface is needed, set up the required files
ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_SURFACE)), yes)
QUANTUM_PAINTER_NEEDS_COMMS_DUMMY := yes
OPT_DEFS += -DQUANTUM_PAINTER_SURFACE_ENABLE
COMMON_VPATH += \
$(DRIVER_PATH)/painter/generic
SRC += \
$(DRIVER_PATH)/painter/generic/qp_surface_common.c \
$(DRIVER_PATH)/painter/generic/qp_surface_mono1bpp.c \
$(DRIVER_PATH)/painter/generic/qp_surface_rgb565.c
endif
# If dummy comms is needed, set up the required files
ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_DUMMY)), yes)
OPT_DEFS += -DQUANTUM_PAINTER_DUMMY_COMMS_ENABLE
VPATH += $(DRIVER_PATH)/painter/comms
SRC += \
$(QUANTUM_DIR)/painter/qp_comms.c \
$(DRIVER_PATH)/painter/comms/qp_comms_dummy.c
endif
# 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
SPI_DRIVER_REQUIRED = yes
VPATH += $(DRIVER_PATH)/painter/comms
SRC += \
$(QUANTUM_DIR)/painter/qp_comms.c \
@ -155,7 +199,17 @@ ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_SPI)), yes)
endif
endif
# If I2C comms is needed, set up the required files
ifeq ($(strip $(QUANTUM_PAINTER_NEEDS_COMMS_I2C)), yes)
OPT_DEFS += -DQUANTUM_PAINTER_I2C_ENABLE
I2C_DRIVER_REQUIRED = yes
VPATH += $(DRIVER_PATH)/painter/comms
SRC += \
$(QUANTUM_DIR)/painter/qp_comms.c \
$(DRIVER_PATH)/painter/comms/qp_comms_i2c.c
endif
# Check if LVGL needs to be enabled
ifeq ($(strip $(QUANTUM_PAINTER_LVGL_INTEGRATION)), yes)
include $(QUANTUM_DIR)/painter/lvgl/rules.mk
include $(QUANTUM_DIR)/painter/lvgl/rules.mk
endif