From d840003fc7582ac8f3e63ae27f77f96794214ccd Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 8 Nov 2023 16:33:32 +0100 Subject: [PATCH 1/5] pixel: add package for efficiently working with raw pixel buffers This has been optimized for working with SPI displays like the ST7789. By working directly in the native color format of the display, graphics operations can be much, _much_ faster. Also, this makes it easier to use a different color format like RGB444 simply by changing the generic type. --- pixel/image.go | 223 ++++++++++++++++++++++++++++++++++++++++++++ pixel/image_test.go | 64 +++++++++++++ pixel/pixel.go | 194 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 481 insertions(+) create mode 100644 pixel/image.go create mode 100644 pixel/image_test.go create mode 100644 pixel/pixel.go diff --git a/pixel/image.go b/pixel/image.go new file mode 100644 index 000000000..54633e3fc --- /dev/null +++ b/pixel/image.go @@ -0,0 +1,223 @@ +package pixel + +import ( + "unsafe" +) + +// Image buffer, used for working with the native image format of various +// displays. It works a lot like a slice: it can be rescaled while reusing the +// underlying buffer and should be passed around by value. +type Image[T Color] struct { + width int16 + height int16 + data unsafe.Pointer +} + +// NewImage creates a new image of the given size. +func NewImage[T Color](width, height int) Image[T] { + if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height { + // The width/height are stored as 16-bit integers and should never be + // negative. + panic("NewImage: width/height out of bounds") + } + var zeroColor T + var data unsafe.Pointer + if zeroColor.BitsPerPixel()%8 == 0 { + // Typical formats like RGB888 and RGB565. + // Each color starts at a whole byte offset from the start. + buf := make([]T, width*height) + data = unsafe.Pointer(&buf[0]) + } else { + // Formats like RGB444 that have 12 bits per pixel. + // We access these as bytes, so allocate the buffer as a byte slice. + bufBits := width * height * zeroColor.BitsPerPixel() + bufBytes := (bufBits + 7) / 8 + buf := make([]byte, bufBytes) + data = unsafe.Pointer(&buf[0]) + } + return Image[T]{ + width: int16(width), + height: int16(height), + data: data, + } +} + +// Rescale returns a new Image buffer based on the img buffer. +// The contents is undefined after the Rescale operation, and any modification +// to the returned image will overwrite the underlying image buffer in undefined +// ways. It will panic if width*height is larger than img.Len(). +func (img Image[T]) Rescale(width, height int) Image[T] { + if width*height > img.Len() { + panic("Image.Rescale size out of bounds") + } + return Image[T]{ + width: int16(width), + height: int16(height), + data: img.data, + } +} + +// LimitHeight returns a subimage with the bottom part cut off, as specified by +// height. +func (img Image[T]) LimitHeight(height int) Image[T] { + if height < 0 || height > int(img.height) { + panic("Image.LimitHeight: out of bounds") + } + return Image[T]{ + width: img.width, + height: int16(height), + data: img.data, + } +} + +// Len returns the number of pixels in this image buffer. +func (img Image[T]) Len() int { + return int(img.width) * int(img.height) +} + +// RawBuffer returns a byte slice that can be written directly to the screen +// using DrawRGBBitmap8. +func (img Image[T]) RawBuffer() []uint8 { + var zeroColor T + var numBytes int + if zeroColor.BitsPerPixel()%8 == 0 { + // Each color starts at a whole byte offset. + numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height) + } else { + // Formats like RGB444 that aren't a whole number of bytes. + numBits := zeroColor.BitsPerPixel() * int(img.width) * int(img.height) + numBytes = (numBits + 7) / 8 // round up (see NewImage) + } + return unsafe.Slice((*byte)(img.data), numBytes) +} + +// Size returns the image size. +func (img Image[T]) Size() (int, int) { + return int(img.width), int(img.height) +} + +func (img Image[T]) setPixel(index int, c T) { + var zeroColor T + + if zeroColor.BitsPerPixel()%8 == 0 { + // Each color starts at a whole byte offset. + // This is the easy case. + offset := index * int(unsafe.Sizeof(zeroColor)) + ptr := unsafe.Add(img.data, offset) + *((*T)(ptr)) = c + return + } + + if c, ok := any(c).(RGB444BE); ok { + // Special case for RGB444. + bitIndex := index * zeroColor.BitsPerPixel() + if bitIndex%8 == 0 { + byteOffset := bitIndex / 8 + ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) + ptr[0] = uint8(c >> 4) + ptr[1] = ptr[1]&0x0f | uint8(c)<<4 // change top bits + } else { + byteOffset := bitIndex / 8 + ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) + ptr[0] = ptr[0]&0xf0 | uint8(c>>8) // change bottom bits + ptr[1] = uint8(c) + } + return + } + + // TODO: the code for RGB444 should be generalized to support any bit size. + panic("todo: setPixel for odd bits per pixel") +} + +// Set sets the pixel at x, y to the given color. +// Use FillSolidColor to efficiently fill the entire image buffer. +func (img Image[T]) Set(x, y int, c T) { + if uint(x) >= uint(int(img.width)) || uint(y) >= uint(int(img.height)) { + panic("Image.Set: out of bounds") + } + index := y*int(img.width) + x + img.setPixel(index, c) +} + +// Get returns the color at the given index. +func (img Image[T]) Get(x, y int) T { + if uint(x) >= uint(int(img.width)) || uint(y) >= uint(int(img.height)) { + panic("Image.Get: out of bounds") + } + var zeroColor T + index := y*int(img.width) + x // index into img.data + + if zeroColor.BitsPerPixel()%8 == 0 { + // Colors like RGB565, RGB888, etc. + offset := index * int(unsafe.Sizeof(zeroColor)) + ptr := unsafe.Add(img.data, offset) + return *((*T)(ptr)) + } + + if _, ok := any(zeroColor).(RGB444BE); ok { + // Special case for RGB444 that isn't stored in a neat byte multiple. + bitIndex := index * zeroColor.BitsPerPixel() + var c RGB444BE + if bitIndex%8 == 0 { + byteOffset := bitIndex / 8 + ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) + c |= RGB444BE(ptr[0]) << 4 + c |= RGB444BE(ptr[1] >> 4) // load top bits + } else { + byteOffset := bitIndex / 8 + ptr := (*[2]byte)(unsafe.Add(img.data, byteOffset)) + c |= RGB444BE(ptr[0]&0x0f) << 8 // load bottom bits + c |= RGB444BE(ptr[1]) + } + return any(c).(T) + } + + // TODO: generalize the above code. + panic("todo: Image.Get for odd bits per pixel") +} + +// FillSolidColor fills the entire image with the given color. +// This may be faster than setting individual pixels. +func (img Image[T]) FillSolidColor(color T) { + var zeroColor T + + // Fast pass for colors of 8, 16, 24, etc bytes in size. + if zeroColor.BitsPerPixel()%8 == 0 { + ptr := img.data + for i := 0; i < img.Len(); i++ { + // TODO: this can be optimized a lot. + // - The store can be done as a 32-bit integer, after checking for + // alignment. + // - Perhaps the loop can be unrolled to improve copy performance. + *(*T)(ptr) = color + ptr = unsafe.Add(ptr, unsafe.Sizeof(zeroColor)) + } + return + } + + // Special case for RGB444. + if c, ok := any(color).(RGB444BE); ok { + // RGB444 can be stored in a more optimized way, by storing two colors + // at a time instead of setting each color individually. This avoids + // loading and masking the old color bits for the half-bytes. + var buf [3]uint8 + buf[0] = uint8(c >> 4) + buf[1] = uint8(c)<<4 | uint8(c>>8) + buf[2] = uint8(c) + rawBuf := unsafe.Slice((*[3]byte)(img.data), img.Len()/2) + for i := 0; i < len(rawBuf); i++ { + rawBuf[i] = buf + } + if img.Len()%2 != 0 { + // The image contains an uneven number of pixels. + // This is uncommon, but it can happen and we have to handle it. + img.setPixel(img.Len()-1, color) + } + return + } + + // Fallback for other color formats. + for i := 0; i < img.Len(); i++ { + img.setPixel(i, color) + } +} diff --git a/pixel/image_test.go b/pixel/image_test.go new file mode 100644 index 000000000..42f292c60 --- /dev/null +++ b/pixel/image_test.go @@ -0,0 +1,64 @@ +package pixel_test + +import ( + "image/color" + "testing" + + "tinygo.org/x/drivers/pixel" +) + +func TestImageRGB565BE(t *testing.T) { + image := pixel.NewImage[pixel.RGB565BE](5, 3) + if width, height := image.Size(); width != 5 && height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, c := range []color.RGBA{ + {R: 0xff, A: 0xff}, + {G: 0xff, A: 0xff}, + {B: 0xff, A: 0xff}, + {R: 0x10, A: 0xff}, + {G: 0x10, A: 0xff}, + {B: 0x10, A: 0xff}, + } { + image.Set(4, 2, pixel.NewColor[pixel.RGB565BE](c.R, c.G, c.B)) + c2 := image.Get(4, 2).RGBA() + if c2 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2) + } + } +} + +func TestImageRGB444BE(t *testing.T) { + image := pixel.NewImage[pixel.RGB444BE](5, 3) + if width, height := image.Size(); width != 5 && height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, c := range []color.RGBA{ + {R: 0xff, A: 0xff}, + {G: 0xff, A: 0xff}, + {B: 0xff, A: 0xff}, + {R: 0x11, A: 0xff}, + {G: 0x11, A: 0xff}, + {B: 0x11, A: 0xff}, + } { + encoded := pixel.NewColor[pixel.RGB444BE](c.R, c.G, c.B) + image.Set(0, 0, encoded) + image.Set(0, 1, encoded) + encoded2 := image.Get(0, 0) + encoded3 := image.Get(0, 1) + if encoded != encoded2 { + t.Errorf("failed to roundtrip color %v: expected %d but got %d", c, encoded, encoded2) + } + if encoded != encoded3 { + t.Errorf("failed to roundtrip color %v: expected %d but got %d", c, encoded, encoded3) + } + c2 := encoded2.RGBA() + if c2 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2) + } + c3 := encoded3.RGBA() + if c3 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c3) + } + } +} diff --git a/pixel/pixel.go b/pixel/pixel.go new file mode 100644 index 000000000..940fb1c5c --- /dev/null +++ b/pixel/pixel.go @@ -0,0 +1,194 @@ +// Package pixel contains pixel format definitions used in various displays and +// fast operations on them. +// +// This package is just a base for pixel operations, it is _not_ a graphics +// library. It doesn't define circles, lines, etc - just the bare minimum +// graphics operations needed plus the ones that need to be specialized per +// pixel format. +package pixel + +import ( + "image/color" + "math/bits" +) + +// Pixel with a particular color, matching the underlying hardware of a +// particular display. Each pixel is at least 1 byte in size. +// The color format is sRGB (or close to it) in all cases. +type Color interface { + RGB888 | RGB565BE | RGB444BE + + BaseColor +} + +// BaseColor contains all the methods needed in a color format. This can be used +// in display drivers that want to define their own Color type with just the +// pixel formats the display supports. +type BaseColor interface { + // The number of bits when stored. + // This means for example that RGB555 (which is still stored as a 16-bit + // integer) returns 16, while RGB444 returns 12. + BitsPerPixel() int + + // Return the given color in color.RGBA format, which is always sRGB. The + // alpha channel is always 255. + RGBA() color.RGBA +} + +// NewColor returns the given color based on the RGB values passed in the +// parameters. The input value is assumed to be in sRGB color space. +func NewColor[T Color](r, g, b uint8) T { + // Ugly cast from color.RGBA to T. The type switch and interface casts are + // trivially optimized away after instantiation. + var value T + switch any(value).(type) { + case RGB888: + return any(NewRGB888(r, g, b)).(T) + case RGB565BE: + return any(NewRGB565BE(r, g, b)).(T) + case RGB444BE: + return any(NewRGB444BE(r, g, b)).(T) + default: + panic("unknown color format") + } +} + +// NewLinearColor returns the given color based on the linear RGB values passed +// in the parameters. Use this if the RGB values are actually linear colors +// (like those that are used in most RGB LEDs) and not when it is in the usual +// sRGB color space (which is not linear). +// +// The input is assumed to be in the linear sRGB color space. +func NewLinearColor[T Color](r, g, b uint8) T { + r = gammaEncodeTable[r] + g = gammaEncodeTable[g] + b = gammaEncodeTable[b] + return NewColor[T](r, g, b) +} + +// RGB888 format, more commonly used in other places (desktop PC displays, CSS, +// etc). Less commonly used on embedded displays due to the higher memory usage. +type RGB888 struct { + R, G, B uint8 +} + +func NewRGB888(r, g, b uint8) RGB888 { + return RGB888{r, g, b} +} + +func (c RGB888) BitsPerPixel() int { + return 24 +} + +func (c RGB888) RGBA() color.RGBA { + return color.RGBA{ + R: c.R, + G: c.G, + B: c.B, + A: 255, + } +} + +// RGB565 as used in many SPI displays. Stored as a big endian value. +// +// The color format in integer form is gggbbbbb_rrrrrggg on little endian +// systems, which is the standard RGB565 format but with the top and bottom +// bytes swapped. +// +// There are a few alternatives to this weird big-endian format, but they're not +// great: +// - Storing the value in two 8-bit stores (to make the code endian-agnostic) +// incurs too much of a performance penalty. +// - Swapping the upper and lower bits just before storing. This is still less +// efficient than it could be, since colors are usually constructed once and +// then reused in many store operations. Doing the swap once instead of many +// times for each store is a performance win. +type RGB565BE uint16 + +func NewRGB565BE(r, g, b uint8) RGB565BE { + val := uint16(r&0xF8)<<8 + + uint16(g&0xFC)<<3 + + uint16(b&0xF8)>>3 + // Swap endianness (make big endian). + // This is done using a single instruction on ARM (rev16). + // TODO: this should only be done on little endian systems, but TinyGo + // doesn't currently (2023) support big endian systems so it's difficult to + // test. Also, big endian systems don't seem fasionable these days. + val = bits.ReverseBytes16(val) + return RGB565BE(val) +} + +func (c RGB565BE) BitsPerPixel() int { + return 16 +} + +func (c RGB565BE) RGBA() color.RGBA { + // Note: on ARM, the compiler uses a rev instruction instead of a rev16 + // instruction. I wonder whether this can be optimized further to use rev16 + // instead? + c = c<<8 | c>>8 + color := color.RGBA{ + R: uint8(c>>11) << 3, + G: uint8(c>>5) << 2, + B: uint8(c) << 3, + A: 255, + } + // Correct color rounding, so that 0xff roundtrips back to 0xff. + color.R |= color.R >> 5 + color.G |= color.G >> 6 + color.B |= color.B >> 5 + return color +} + +// Color format that is supported by the ST7789 for example. +// It may be a bit faster to use than RGB565BE on very slow SPI buses. +// +// The color format is native endian as a uint16 (0000rrrr_ggggbbbb), not big +// endian which you might expect. I tried swapping the bytes, but it didn't have +// much of a performance impact and made the code harder to read. It is stored +// as a 12-bit big endian value in Image[RGB444BE] though. +type RGB444BE uint16 + +func NewRGB444BE(r, g, b uint8) RGB444BE { + return RGB444BE(r>>4)<<8 | RGB444BE(g>>4)<<4 | RGB444BE(b>>4) +} + +func (c RGB444BE) BitsPerPixel() int { + return 12 +} + +func (c RGB444BE) RGBA() color.RGBA { + color := color.RGBA{ + R: uint8(c>>8) << 4, + G: uint8(c>>4) << 4, + B: uint8(c>>0) << 4, + A: 255, + } + // Correct color rounding, so that 0xff roundtrips back to 0xff. + color.R |= color.R >> 4 + color.G |= color.G >> 4 + color.B |= color.B >> 4 + return color +} + +// Gamma brightness lookup table: +// https://victornpb.github.io/gamma-table-generator +// gamma = 0.45 steps = 256 range = 0-255 +var gammaEncodeTable = [256]uint8{ + 0, 21, 28, 34, 39, 43, 46, 50, 53, 56, 59, 61, 64, 66, 68, 70, + 72, 74, 76, 78, 80, 82, 84, 85, 87, 89, 90, 92, 93, 95, 96, 98, + 99, 101, 102, 103, 105, 106, 107, 109, 110, 111, 112, 114, 115, 116, 117, 118, + 119, 120, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, + 136, 137, 138, 139, 140, 141, 142, 143, 144, 144, 145, 146, 147, 148, 149, 150, + 151, 151, 152, 153, 154, 155, 156, 156, 157, 158, 159, 160, 160, 161, 162, 163, + 164, 164, 165, 166, 167, 167, 168, 169, 170, 170, 171, 172, 173, 173, 174, 175, + 175, 176, 177, 178, 178, 179, 180, 180, 181, 182, 182, 183, 184, 184, 185, 186, + 186, 187, 188, 188, 189, 190, 190, 191, 192, 192, 193, 194, 194, 195, 195, 196, + 197, 197, 198, 199, 199, 200, 200, 201, 202, 202, 203, 203, 204, 205, 205, 206, + 206, 207, 207, 208, 209, 209, 210, 210, 211, 212, 212, 213, 213, 214, 214, 215, + 215, 216, 217, 217, 218, 218, 219, 219, 220, 220, 221, 221, 222, 223, 223, 224, + 224, 225, 225, 226, 226, 227, 227, 228, 228, 229, 229, 230, 230, 231, 231, 232, + 232, 233, 233, 234, 234, 235, 235, 236, 236, 237, 237, 238, 238, 239, 239, 240, + 240, 241, 241, 242, 242, 243, 243, 244, 244, 245, 245, 246, 246, 247, 247, 248, + 248, 249, 249, 249, 250, 250, 251, 251, 252, 252, 253, 253, 254, 254, 255, 255, +} From f4829e6c206acc7f7228a351530ba8dffbd6dd10 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 9 Nov 2023 18:35:28 +0100 Subject: [PATCH 2/5] st7735: make the display generic over RGB565 and RGB444 Using RGB444 instead of RGB565 can speed up graphics operations by up to 25%, especially on slow screens. But for full support, all parts of the driver need to be aware of the color format. It's possible to do this using a regular configuration variable, but it's unlikely to be very efficient. Hence the usage of generics. --- st7735/st7735.go | 113 +++++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 52 deletions(-) diff --git a/st7735/st7735.go b/st7735/st7735.go index f9521cd39..02bc93ded 100644 --- a/st7735/st7735.go +++ b/st7735/st7735.go @@ -11,6 +11,7 @@ import ( "errors" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/pixel" ) type Model uint8 @@ -20,12 +21,23 @@ type Model uint8 // Deprecated: use drivers.Rotation instead. type Rotation = drivers.Rotation +// Pixel formats supported by the st7735 driver. +type Color interface { + pixel.RGB444BE | pixel.RGB565BE + + pixel.BaseColor +} + var ( errOutOfBounds = errors.New("rectangle coordinates outside display area") ) // Device wraps an SPI connection. -type Device struct { +type Device = DeviceOf[pixel.RGB565BE] + +// DeviceOf is a generic version of Device, which supports different pixel +// formats. +type DeviceOf[T Color] struct { bus drivers.SPI dcPin machine.Pin resetPin machine.Pin @@ -39,7 +51,7 @@ type Device struct { batchLength int16 model Model isBGR bool - batchData []uint8 + batchData pixel.Image[T] // "image" with width, height of (batchLength, 1) } // Config is the configuration for the display @@ -54,11 +66,17 @@ type Config struct { // New creates a new ST7735 connection. The SPI wire must already be configured. func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { + return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin) +} + +// NewOf creates a new ST7735 connection with a particular pixel format. The SPI +// wire must already be configured. +func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] { dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - return Device{ + return DeviceOf[T]{ bus: bus, dcPin: dcPin, resetPin: resetPin, @@ -68,7 +86,7 @@ func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { } // Configure initializes the display with default configuration -func (d *Device) Configure(cfg Config) { +func (d *DeviceOf[T]) Configure(cfg Config) { d.model = cfg.Model if cfg.Width != 0 { d.width = cfg.Width @@ -93,7 +111,7 @@ func (d *Device) Configure(cfg Config) { d.batchLength = d.height } d.batchLength += d.batchLength & 1 - d.batchData = make([]uint8, d.batchLength*2) + d.batchData = pixel.NewImage[T](int(d.batchLength), 1) // reset the device d.resetPin.High() @@ -142,8 +160,16 @@ func (d *Device) Configure(cfg Config) { d.Data(0xEE) d.Command(VMCTR1) d.Data(0x0E) + + // Set the color format depending on the generic type. d.Command(COLMOD) - d.Data(0x05) + var zeroColor T + switch any(zeroColor).(type) { + case pixel.RGB444BE: + d.Data(0x03) // 12 bits per pixel + default: + d.Data(0x05) // 16 bits per pixel + } if d.model == GREENTAB { d.InvertColors(false) @@ -204,12 +230,12 @@ func (d *Device) Configure(cfg Config) { } // Display does nothing, there's no buffer as it might be too big for some boards -func (d *Device) Display() error { +func (d *DeviceOf[T]) Display() error { return nil } // SetPixel sets a pixel in the screen -func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { +func (d *DeviceOf[T]) SetPixel(x int16, y int16, c color.RGBA) { w, h := d.Size() if x < 0 || y < 0 || x >= w || y >= h { return @@ -218,7 +244,7 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { } // setWindow prepares the screen to be modified at a given rectangle -func (d *Device) setWindow(x, y, w, h int16) { +func (d *DeviceOf[T]) setWindow(x, y, w, h int16) { if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { x += d.columnOffset y += d.rowOffset @@ -234,7 +260,7 @@ func (d *Device) setWindow(x, y, w, h int16) { } // SetScrollWindow sets an area to scroll with fixed top and bottom parts of the display -func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { +func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { // TODO: this code is broken, see the st7789 and ili9341 implementations for // how to do this correctly. d.Command(VSCRDEF) @@ -246,38 +272,32 @@ func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { } // SetScroll sets the vertical scroll address of the display. -func (d *Device) SetScroll(line int16) { +func (d *DeviceOf[T]) SetScroll(line int16) { d.Command(VSCRSADD) d.Tx([]uint8{uint8(line >> 8), uint8(line)}, false) } // SpotScroll returns the display to its normal state -func (d *Device) StopScroll() { +func (d *DeviceOf[T]) StopScroll() { d.Command(NORON) } // FillRectangle fills a rectangle at a given coordinates with a color -func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { +func (d *DeviceOf[T]) FillRectangle(x, y, width, height int16, c color.RGBA) error { k, i := d.Size() if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= k || (x+width) > k || y >= i || (y+height) > i { return errors.New("rectangle coordinates outside display area") } d.setWindow(x, y, width, height) - c565 := RGBATo565(c) - c1 := uint8(c565 >> 8) - c2 := uint8(c565) - for i = 0; i < d.batchLength; i++ { - d.batchData[i*2] = c1 - d.batchData[i*2+1] = c2 - } + d.batchData.FillSolidColor(pixel.NewColor[T](c.R, c.G, c.B)) i = width * height for i > 0 { if i >= d.batchLength { - d.Tx(d.batchData, false) + d.Tx(d.batchData.RawBuffer(), false) } else { - d.Tx(d.batchData[:i*2], false) + d.Tx(d.batchData.Rescale(int(i), 1).RawBuffer(), false) } i -= d.batchLength } @@ -285,7 +305,7 @@ func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates -func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { +func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { k, i := d.Size() if x < 0 || y < 0 || w <= 0 || h <= 0 || x >= k || (x+w) > k || y >= i || (y+h) > i { @@ -297,7 +317,7 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { } // FillRectangle fills a rectangle at a given coordinates with a buffer -func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { +func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { k, l := d.Size() if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= k || (x+width) > k || y >= l || (y+height) > l { @@ -315,17 +335,14 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col for k > 0 { for i := int16(0); i < d.batchLength; i++ { if offset+i < l { - c565 := RGBATo565(buffer[offset+i]) - c1 := uint8(c565 >> 8) - c2 := uint8(c565) - d.batchData[i*2] = c1 - d.batchData[i*2+1] = c2 + c := buffer[offset+i] + d.batchData.Set(int(i), 0, pixel.NewColor[T](c.R, c.G, c.B)) } } if k >= d.batchLength { - d.Tx(d.batchData, false) + d.Tx(d.batchData.RawBuffer(), false) } else { - d.Tx(d.batchData[:k*2], false) + d.Tx(d.batchData.Rescale(int(k), 1).RawBuffer(), false) } k -= d.batchLength offset += d.batchLength @@ -334,7 +351,7 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col } // DrawFastVLine draws a vertical line faster than using SetPixel -func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { +func (d *DeviceOf[T]) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { if y0 > y1 { y0, y1 = y1, y0 } @@ -342,7 +359,7 @@ func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { } // DrawFastHLine draws a horizontal line faster than using SetPixel -func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { +func (d *DeviceOf[T]) DrawFastHLine(x0, x1, y int16, c color.RGBA) { if x0 > x1 { x0, x1 = x1, x0 } @@ -350,7 +367,7 @@ func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { } // FillScreen fills the screen with a given color -func (d *Device) FillScreen(c color.RGBA) { +func (d *DeviceOf[T]) FillScreen(c color.RGBA) { if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { d.FillRectangle(0, 0, d.width, d.height, c) } else { @@ -359,12 +376,12 @@ func (d *Device) FillScreen(c color.RGBA) { } // Rotation returns the currently configured rotation. -func (d *Device) Rotation() drivers.Rotation { +func (d *DeviceOf[T]) Rotation() drivers.Rotation { return d.rotation } // SetRotation changes the rotation of the device (clock-wise) -func (d *Device) SetRotation(rotation drivers.Rotation) error { +func (d *DeviceOf[T]) SetRotation(rotation drivers.Rotation) error { d.rotation = rotation madctl := uint8(0) switch rotation % 4 { @@ -386,23 +403,23 @@ func (d *Device) SetRotation(rotation drivers.Rotation) error { } // Command sends a command to the display -func (d *Device) Command(command uint8) { +func (d *DeviceOf[T]) Command(command uint8) { d.Tx([]byte{command}, true) } // Command sends a data to the display -func (d *Device) Data(data uint8) { +func (d *DeviceOf[T]) Data(data uint8) { d.Tx([]byte{data}, false) } // Tx sends data to the display -func (d *Device) Tx(data []byte, isCommand bool) { +func (d *DeviceOf[T]) Tx(data []byte, isCommand bool) { d.dcPin.Set(!isCommand) d.bus.Tx(data, nil) } // Size returns the current size of the display. -func (d *Device) Size() (w, h int16) { +func (d *DeviceOf[T]) Size() (w, h int16) { if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { return d.width, d.height } @@ -410,7 +427,7 @@ func (d *Device) Size() (w, h int16) { } // EnableBacklight enables or disables the backlight -func (d *Device) EnableBacklight(enable bool) { +func (d *DeviceOf[T]) EnableBacklight(enable bool) { if enable { d.blPin.High() } else { @@ -421,7 +438,7 @@ func (d *Device) EnableBacklight(enable bool) { // Set the sleep mode for this LCD panel. When sleeping, the panel uses a lot // less power. The LCD won't display an image anymore, but the memory contents // will be kept. -func (d *Device) Sleep(sleepEnabled bool) error { +func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error { if sleepEnabled { // Shut down LCD panel. d.Command(SLPIN) @@ -437,7 +454,7 @@ func (d *Device) Sleep(sleepEnabled bool) error { } // InverColors inverts the colors of the screen -func (d *Device) InvertColors(invert bool) { +func (d *DeviceOf[T]) InvertColors(invert bool) { if invert { d.Command(INVON) } else { @@ -446,14 +463,6 @@ func (d *Device) InvertColors(invert bool) { } // IsBGR changes the color mode (RGB/BGR) -func (d *Device) IsBGR(bgr bool) { +func (d *DeviceOf[T]) IsBGR(bgr bool) { d.isBGR = bgr } - -// RGBATo565 converts a color.RGBA to uint16 used in the display -func RGBATo565(c color.RGBA) uint16 { - r, g, b, _ := c.RGBA() - return uint16((r & 0xF800) + - ((g & 0xFC00) >> 5) + - ((b & 0xF800) >> 11)) -} From 994b0640e6475dd142cb81f4b07197b498fbc335 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 9 Nov 2023 18:44:17 +0100 Subject: [PATCH 3/5] st7789: make the display generic over RGB565 and RGB444 Same as for st7735 in the previous commit. In addition, this avoids allocating a big chunk of memory on _every_ draw operation (even SetPixel) and instead reuses it across draw operations. This makes the driver a whole lot more efficient. --- st7789/st7789.go | 162 ++++++++++++++++++++++++++--------------------- 1 file changed, 90 insertions(+), 72 deletions(-) diff --git a/st7789/st7789.go b/st7789/st7789.go index 1e6f1285a..245be11f6 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -14,6 +14,7 @@ import ( "errors" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/pixel" ) // Rotation controls the rotation used by the display. @@ -24,6 +25,13 @@ type Rotation = drivers.Rotation // The color format used on the display, like RGB565, RGB666, and RGB444. type ColorFormat uint8 +// Pixel formats supported by the st7789 driver. +type Color interface { + pixel.RGB444BE | pixel.RGB565BE + + pixel.BaseColor +} + // FrameRate controls the frame rate used by the display. type FrameRate uint8 @@ -32,7 +40,11 @@ var ( ) // Device wraps an SPI connection. -type Device struct { +type Device = DeviceOf[pixel.RGB565BE] + +// DeviceOf is a generic version of Device. It supports multiple different pixel +// formats. +type DeviceOf[T Color] struct { bus drivers.SPI dcPin machine.Pin resetPin machine.Pin @@ -47,6 +59,7 @@ type Device struct { rotation drivers.Rotation frameRate FrameRate batchLength int32 + batchData pixel.Image[T] // "image" with (width, height) of (batchLength, 1) isBGR bool vSyncLines int16 cmdBuf [1]byte @@ -71,11 +84,17 @@ type Config struct { // New creates a new ST7789 connection. The SPI wire must already be configured. func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { + return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin) +} + +// NewOf creates a new ST7789 connection with a particular pixel format. The SPI +// wire must already be configured. +func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] { dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - return Device{ + return DeviceOf[T]{ bus: bus, dcPin: dcPin, resetPin: resetPin, @@ -85,7 +104,7 @@ func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { } // Configure initializes the display with default configuration -func (d *Device) Configure(cfg Config) { +func (d *DeviceOf[T]) Configure(cfg Config) { if cfg.Width != 0 { d.width = cfg.Width } else { @@ -137,7 +156,14 @@ func (d *Device) Configure(cfg Config) { d.sendCommand(SLPOUT, nil) // Exit sleep mode // Memory initialization - d.setColorFormat(ColorRGB565) // Set color mode to 16-bit color + var zeroColor T + switch any(zeroColor).(type) { + case pixel.RGB444BE: + d.setColorFormat(ColorRGB444) // 12 bits per pixel + default: + // Use default RGB565 color format. + d.setColorFormat(ColorRGB565) // 16 bits per pixel + } time.Sleep(10 * time.Millisecond) d.setRotation(d.rotation) // Memory orientation @@ -189,7 +215,7 @@ func (d *Device) Configure(cfg Config) { // Send a command with data to the display. It does not change the chip select // pin (it must be low when calling). The DC pin is left high after return, // meaning that data can be sent right away. -func (d *Device) sendCommand(command uint8, data []byte) error { +func (d *DeviceOf[T]) sendCommand(command uint8, data []byte) error { d.cmdBuf[0] = command d.dcPin.Low() err := d.bus.Tx(d.cmdBuf[:1], nil) @@ -202,7 +228,7 @@ func (d *Device) sendCommand(command uint8, data []byte) error { // startWrite must be called at the beginning of all exported methods to set the // chip select pin low. -func (d *Device) startWrite() { +func (d *DeviceOf[T]) startWrite() { if d.csPin != machine.NoPin { d.csPin.Low() } @@ -210,14 +236,23 @@ func (d *Device) startWrite() { // endWrite must be called at the end of all exported methods to set the chip // select pin high. -func (d *Device) endWrite() { +func (d *DeviceOf[T]) endWrite() { if d.csPin != machine.NoPin { d.csPin.High() } } +// getBuffer returns the image buffer, that's always d.batchLength wide and 1 +// pixel high. It can be used as a temporary buffer to transmit image data. +func (d *DeviceOf[T]) getBuffer() pixel.Image[T] { + if d.batchData.Len() == 0 { + d.batchData = pixel.NewImage[T](int(d.batchLength), 1) + } + return d.batchData +} + // Sync waits for the display to hit the next VSYNC pause -func (d *Device) Sync() { +func (d *DeviceOf[T]) Sync() { d.SyncToScanLine(0) } @@ -232,7 +267,7 @@ func (d *Device) Sync() { // NOTE: Use GetHighestScanLine and GetLowestScanLine to obtain the highest // and lowest useful values. Values are affected by front and back porch // vsync settings (derived from VSyncLines configuration option). -func (d *Device) SyncToScanLine(scanline uint16) { +func (d *DeviceOf[T]) SyncToScanLine(scanline uint16) { scan := d.GetScanLine() // Sometimes GetScanLine returns erroneous 0 on first call after draw, so double check @@ -262,7 +297,7 @@ func (d *Device) SyncToScanLine(scanline uint16) { } // GetScanLine reads the current scanline value from the display -func (d *Device) GetScanLine() uint16 { +func (d *DeviceOf[T]) GetScanLine() uint16 { d.startWrite() data := []uint8{0x00, 0x00} d.dcPin.Low() @@ -277,24 +312,24 @@ func (d *Device) GetScanLine() uint16 { } // GetHighestScanLine calculates the last scanline id in the frame before VSYNC pause -func (d *Device) GetHighestScanLine() uint16 { +func (d *DeviceOf[T]) GetHighestScanLine() uint16 { // Last scanline id appears to be backporch/2 + 320/2 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 160 } // GetLowestScanLine calculate the first scanline id to appear after VSYNC pause -func (d *Device) GetLowestScanLine() uint16 { +func (d *DeviceOf[T]) GetLowestScanLine() uint16 { // First scanline id appears to be backporch/2 + 1 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 1 } // Display does nothing, there's no buffer as it might be too big for some boards -func (d *Device) Display() error { +func (d *DeviceOf[T]) Display() error { return nil } // SetPixel sets a pixel in the screen -func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { +func (d *DeviceOf[T]) SetPixel(x int16, y int16, c color.RGBA) { if x < 0 || y < 0 || (((d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180) && (x >= d.width || y >= d.height)) || ((d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270) && (x >= d.height || y >= d.width))) { @@ -304,7 +339,7 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { } // setWindow prepares the screen to be modified at a given rectangle -func (d *Device) setWindow(x, y, w, h int16) { +func (d *DeviceOf[T]) setWindow(x, y, w, h int16) { x += d.columnOffset y += d.rowOffset copy(d.buf[:4], []uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}) @@ -315,45 +350,39 @@ func (d *Device) setWindow(x, y, w, h int16) { } // FillRectangle fills a rectangle at a given coordinates with a color -func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { +func (d *DeviceOf[T]) FillRectangle(x, y, width, height int16, c color.RGBA) error { d.startWrite() err := d.fillRectangle(x, y, width, height, c) d.endWrite() return err } -func (d *Device) fillRectangle(x, y, width, height int16, c color.RGBA) error { +func (d *DeviceOf[T]) fillRectangle(x, y, width, height int16, c color.RGBA) error { k, i := d.Size() if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= k || (x+width) > k || y >= i || (y+height) > i { return errors.New("rectangle coordinates outside display area") } d.setWindow(x, y, width, height) - c565 := RGBATo565(c) - c1 := uint8(c565 >> 8) - c2 := uint8(c565) - data := make([]uint8, d.batchLength*2) - for i := int32(0); i < d.batchLength; i++ { - data[i*2] = c1 - data[i*2+1] = c2 - } - j := int32(width) * int32(height) + image := d.getBuffer() + image.FillSolidColor(pixel.NewColor[T](c.R, c.G, c.B)) + j := int(width) * int(height) for j > 0 { // The DC pin is already set to data in the setWindow call, so we can // just write bytes on the SPI bus. - if j >= d.batchLength { - d.bus.Tx(data, nil) + if j >= image.Len() { + d.bus.Tx(image.RawBuffer(), nil) } else { - d.bus.Tx(data[:j*2], nil) + d.bus.Tx(image.Rescale(j, 1).RawBuffer(), nil) } - j -= d.batchLength + j -= image.Len() } return nil } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates -func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { +func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { k, i := d.Size() if x < 0 || y < 0 || w <= 0 || h <= 0 || x >= k || (x+w) > k || y >= i || (y+h) > i { @@ -367,7 +396,7 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { } // FillRectangleWithBuffer fills buffer with a rectangle at a given coordinates. -func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { +func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { i, j := d.Size() if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= i || (x+width) > i || y >= j || (y+height) > j { @@ -379,35 +408,32 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col d.startWrite() d.setWindow(x, y, width, height) - k := int32(width) * int32(height) - data := make([]uint8, d.batchLength*2) - offset := int32(0) + k := int(width) * int(height) + image := d.getBuffer() + offset := 0 for k > 0 { - for i := int32(0); i < d.batchLength; i++ { - if offset+i < int32(len(buffer)) { - c565 := RGBATo565(buffer[offset+i]) - c1 := uint8(c565 >> 8) - c2 := uint8(c565) - data[i*2] = c1 - data[i*2+1] = c2 + for i := 0; i < image.Len(); i++ { + if offset+i < len(buffer) { + c := buffer[offset+i] + image.Set(i, 0, pixel.NewColor[T](c.R, c.G, c.B)) } } // The DC pin is already set to data in the setWindow call, so we don't // have to set it here. - if k >= d.batchLength { - d.bus.Tx(data, nil) + if k >= image.Len() { + d.bus.Tx(image.RawBuffer(), nil) } else { - d.bus.Tx(data[:k*2], nil) + d.bus.Tx(image.Rescale(k, 1).RawBuffer(), nil) } - k -= d.batchLength - offset += d.batchLength + k -= image.Len() + offset += image.Len() } d.endWrite() return nil } // DrawFastVLine draws a vertical line faster than using SetPixel -func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { +func (d *DeviceOf[T]) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { if y0 > y1 { y0, y1 = y1, y0 } @@ -415,7 +441,7 @@ func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { } // DrawFastHLine draws a horizontal line faster than using SetPixel -func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { +func (d *DeviceOf[T]) DrawFastHLine(x0, x1, y int16, c color.RGBA) { if x0 > x1 { x0, x1 = x1, x0 } @@ -423,13 +449,13 @@ func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) { } // FillScreen fills the screen with a given color -func (d *Device) FillScreen(c color.RGBA) { +func (d *DeviceOf[T]) FillScreen(c color.RGBA) { d.startWrite() d.fillScreen(c) d.endWrite() } -func (d *Device) fillScreen(c color.RGBA) { +func (d *DeviceOf[T]) fillScreen(c color.RGBA) { if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 { d.fillRectangle(0, 0, d.width, d.height, c) } else { @@ -441,13 +467,13 @@ func (d *Device) fillScreen(c color.RGBA) { // The default is RGB565, setting it to any other value will break functions // like SetPixel, FillRectangle, etc. Instead, you can write color data in the // specified color format using DrawRGBBitmap8. -func (d *Device) SetColorFormat(format ColorFormat) { +func (d *DeviceOf[T]) SetColorFormat(format ColorFormat) { d.startWrite() d.setColorFormat(format) d.endWrite() } -func (d *Device) setColorFormat(format ColorFormat) { +func (d *DeviceOf[T]) setColorFormat(format ColorFormat) { // Lower 4 bits set the color format used in SPI. // Upper 4 bits set the color format used in the direct RGB interface. // The RGB interface is not currently supported, so it is left at a @@ -457,12 +483,12 @@ func (d *Device) setColorFormat(format ColorFormat) { } // Rotation returns the current rotation of the device. -func (d *Device) Rotation() drivers.Rotation { +func (d *DeviceOf[T]) Rotation() drivers.Rotation { return d.rotation } // SetRotation changes the rotation of the device (clock-wise) -func (d *Device) SetRotation(rotation Rotation) error { +func (d *DeviceOf[T]) SetRotation(rotation Rotation) error { d.rotation = rotation d.startWrite() err := d.setRotation(rotation) @@ -470,7 +496,7 @@ func (d *Device) SetRotation(rotation Rotation) error { return err } -func (d *Device) setRotation(rotation Rotation) error { +func (d *DeviceOf[T]) setRotation(rotation Rotation) error { madctl := uint8(0) switch rotation % 4 { case drivers.Rotation0: @@ -496,7 +522,7 @@ func (d *Device) setRotation(rotation Rotation) error { } // Size returns the current size of the display. -func (d *Device) Size() (w, h int16) { +func (d *DeviceOf[T]) Size() (w, h int16) { if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { return d.width, d.height } @@ -504,7 +530,7 @@ func (d *Device) Size() (w, h int16) { } // EnableBacklight enables or disables the backlight -func (d *Device) EnableBacklight(enable bool) { +func (d *DeviceOf[T]) EnableBacklight(enable bool) { if enable { d.blPin.High() } else { @@ -515,7 +541,7 @@ func (d *Device) EnableBacklight(enable bool) { // Set the sleep mode for this LCD panel. When sleeping, the panel uses a lot // less power. The LCD won't display an image anymore, but the memory contents // will be kept. -func (d *Device) Sleep(sleepEnabled bool) error { +func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error { if sleepEnabled { d.startWrite() d.sendCommand(SLPIN, nil) @@ -537,7 +563,7 @@ func (d *Device) Sleep(sleepEnabled bool) error { } // InvertColors inverts the colors of the screen -func (d *Device) InvertColors(invert bool) { +func (d *DeviceOf[T]) InvertColors(invert bool) { d.startWrite() if invert { d.sendCommand(INVON, nil) @@ -548,12 +574,12 @@ func (d *Device) InvertColors(invert bool) { } // IsBGR changes the color mode (RGB/BGR) -func (d *Device) IsBGR(bgr bool) { +func (d *DeviceOf[T]) IsBGR(bgr bool) { d.isBGR = bgr } // SetScrollArea sets an area to scroll with fixed top and bottom parts of the display. -func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { +func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { if d.height < 320 { // The screen doesn't use the full 320 pixel height. // Enlarge the bottom fixed area to fill the 320 pixel height, so that @@ -577,7 +603,7 @@ func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) { } // SetScroll sets the vertical scroll address of the display. -func (d *Device) SetScroll(line int16) { +func (d *DeviceOf[T]) SetScroll(line int16) { if d.rotation == drivers.Rotation180 { // The screen is rotated by 180°, so we have to invert the scroll line // (taking care of the RowOffset). @@ -591,16 +617,8 @@ func (d *Device) SetScroll(line int16) { } // StopScroll returns the display to its normal state. -func (d *Device) StopScroll() { +func (d *DeviceOf[T]) StopScroll() { d.startWrite() d.sendCommand(NORON, nil) d.endWrite() } - -// RGBATo565 converts a color.RGBA to uint16 used in the display -func RGBATo565(c color.RGBA) uint16 { - r, g, b, _ := c.RGBA() - return uint16((r & 0xF800) + - ((g & 0xFC00) >> 5) + - ((b & 0xF800) >> 11)) -} From 43c1f885f88b2a93f0d295a0ff8f260db7e503f7 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 11 Nov 2023 18:49:15 +0100 Subject: [PATCH 4/5] ili9341: st7735: st7789: add DrawBitmap method This adds a new DrawBitmap method, which is meant to replace DrawRGBBitmap8. --- ili9341/ili9341.go | 15 +++++++++++++++ st7735/st7735.go | 9 +++++++++ st7789/st7789.go | 9 +++++++++ 3 files changed, 33 insertions(+) diff --git a/ili9341/ili9341.go b/ili9341/ili9341.go index 421b3d29f..5ddb46395 100644 --- a/ili9341/ili9341.go +++ b/ili9341/ili9341.go @@ -7,6 +7,7 @@ import ( "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/pixel" ) type Config struct { @@ -31,6 +32,9 @@ type Device struct { rd machine.Pin } +// Image buffer type used in the ili9341. +type Image = pixel.Image[pixel.RGB565BE] + var cmdBuf [6]byte var initCmd = []byte{ @@ -173,6 +177,8 @@ func (d *Device) EnableTEOutput(on bool) { } // DrawRGBBitmap copies an RGB bitmap to the internal buffer at given coordinates +// +// Deprecated: use DrawBitmap instead. func (d *Device) DrawRGBBitmap(x, y int16, data []uint16, w, h int16) error { k, i := d.Size() if x < 0 || y < 0 || w <= 0 || h <= 0 || @@ -187,6 +193,8 @@ func (d *Device) DrawRGBBitmap(x, y int16, data []uint16, w, h int16) error { } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates +// +// Deprecated: use DrawBitmap instead. func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { k, i := d.Size() if x < 0 || y < 0 || w <= 0 || h <= 0 || @@ -200,6 +208,13 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { return nil } +// DrawBitmap copies the bitmap to the internal buffer on the screen at the +// given coordinates. It returns once the image data has been sent completely. +func (d *Device) DrawBitmap(x, y int16, bitmap Image) error { + width, height := bitmap.Size() + return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height)) +} + // FillRectangle fills a rectangle at given coordinates with a color func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { k, i := d.Size() diff --git a/st7735/st7735.go b/st7735/st7735.go index 02bc93ded..6f15781c2 100644 --- a/st7735/st7735.go +++ b/st7735/st7735.go @@ -305,6 +305,8 @@ func (d *DeviceOf[T]) FillRectangle(x, y, width, height int16, c color.RGBA) err } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates +// +// Deprecated: use DrawBitmap instead. func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { k, i := d.Size() if x < 0 || y < 0 || w <= 0 || h <= 0 || @@ -316,6 +318,13 @@ func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error return nil } +// DrawBitmap copies the bitmap to the internal buffer on the screen at the +// given coordinates. It returns once the image data has been sent completely. +func (d *DeviceOf[T]) DrawBitmap(x, y int16, bitmap pixel.Image[T]) error { + width, height := bitmap.Size() + return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height)) +} + // FillRectangle fills a rectangle at a given coordinates with a buffer func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { k, l := d.Size() diff --git a/st7789/st7789.go b/st7789/st7789.go index 245be11f6..5db2402ba 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -382,6 +382,8 @@ func (d *DeviceOf[T]) fillRectangle(x, y, width, height int16, c color.RGBA) err } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates +// +// Deprecated: use DrawBitmap instead. func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { k, i := d.Size() if x < 0 || y < 0 || w <= 0 || h <= 0 || @@ -395,6 +397,13 @@ func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error return nil } +// DrawBitmap copies the bitmap to the internal buffer on the screen at the +// given coordinates. It returns once the image data has been sent completely. +func (d *DeviceOf[T]) DrawBitmap(x, y int16, bitmap pixel.Image[T]) error { + width, height := bitmap.Size() + return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height)) +} + // FillRectangleWithBuffer fills buffer with a rectangle at a given coordinates. func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { i, j := d.Size() From a31db2b129e4366b87d08ffa04da95b2b40fe4d3 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 11 Nov 2023 18:55:31 +0100 Subject: [PATCH 5/5] pyportal_boing: update to use DrawBitmap Replace DrawRGBBitmap8 with DrawBitmap, following the change in the previous commit. This improves performance from 86fps to 100fps! I didn't investigate why, but I suspect it's because it now needs only a single store instead of two to update a pixel. --- examples/ili9341/pyportal_boing/main.go | 45 +++++++++++++------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/examples/ili9341/pyportal_boing/main.go b/examples/ili9341/pyportal_boing/main.go index 16b285bea..b03423386 100644 --- a/examples/ili9341/pyportal_boing/main.go +++ b/examples/ili9341/pyportal_boing/main.go @@ -8,15 +8,16 @@ import ( "tinygo.org/x/drivers/examples/ili9341/initdisplay" "tinygo.org/x/drivers/examples/ili9341/pyportal_boing/graphics" "tinygo.org/x/drivers/ili9341" + "tinygo.org/x/drivers/pixel" ) const ( - BGCOLOR = 0xAD75 - GRIDCOLOR = 0xA815 - BGSHADOW = 0x5285 - GRIDSHADOW = 0x600C - RED = 0xF800 - WHITE = 0xFFFF + BGCOLOR = pixel.RGB565BE(0x75AD) + GRIDCOLOR = pixel.RGB565BE(0x15A8) + BGSHADOW = pixel.RGB565BE(0x8552) + GRIDSHADOW = pixel.RGB565BE(0x0C60) + RED = pixel.RGB565BE(0x00F8) + WHITE = pixel.RGB565BE(0xFFFF) YBOTTOM = 123 // Ball Y coord at bottom YBOUNCE = -3.5 // Upward velocity on ball bounce @@ -25,7 +26,7 @@ const ( ) var ( - frameBuffer = [(graphics.BALLHEIGHT + 8) * (graphics.BALLWIDTH + 8) * 2]uint8{} + frameBuffer = pixel.NewImage[pixel.RGB565BE](graphics.BALLWIDTH+8, graphics.BALLHEIGHT+8) startTime int64 frame int64 @@ -41,7 +42,7 @@ var ( balloldy float32 // Color table for ball rotation effect - palette [16]uint16 + palette [16]pixel.RGB565BE ) var ( @@ -108,6 +109,7 @@ func main() { width = maxx - minx + 1 height = maxy - miny + 1 + buffer := frameBuffer.Rescale(int(width), int(height)) // Ball animation frame # is incremented opposite the ball's X velocity ballframe -= ballvx * 0.5 @@ -128,7 +130,7 @@ func main() { } // Only the changed rectangle is drawn into the 'renderbuf' array... - var c uint16 //, *destPtr; + var c pixel.RGB565BE //, *destPtr; bx := minx - int16(ballx) // X relative to ball bitmap (can be negative) by := miny - int16(bally) // Y relative to ball bitmap (can be negative) bgx := minx // X relative to background bitmap (>= 0) @@ -149,19 +151,20 @@ func main() { (by >= 0) && (by < graphics.BALLHEIGHT) { // inside the ball bitmap area? // Yes, do ball compositing math... p = graphics.Ball[int(by*(graphics.BALLWIDTH/2))+int(bx1/2)] // Get packed value (2 pixels) + var nibble uint8 if (bx1 & 1) != 0 { - c = uint16(p & 0xF) + nibble = p & 0xF } else { - c = uint16(p >> 4) + nibble = p >> 4 } // Unpack high or low nybble - if c == 0 { // Outside ball - just draw grid + if nibble == 0 { // Outside ball - just draw grid if graphics.Background[bgidx]&(0x80>>(bgx1&7)) != 0 { c = GRIDCOLOR } else { c = BGCOLOR } - } else if c > 1 { // In ball area... - c = palette[c] + } else if nibble > 1 { // In ball area... + c = palette[nibble] } else { // In shadow area... if graphics.Background[bgidx]&(0x80>>(bgx1&7)) != 0 { c = GRIDSHADOW @@ -176,8 +179,7 @@ func main() { c = BGCOLOR } } - frameBuffer[(y*int(width)+x)*2] = byte(c >> 8) - frameBuffer[(y*int(width)+x)*2+1] = byte(c) + buffer.Set(x, y, c) bx1++ // Increment bitmap position counters (X axis) bgx1++ } @@ -188,7 +190,7 @@ func main() { bgy++ } - display.DrawRGBBitmap8(minx, miny, frameBuffer[:width*height*2], width, height) + display.DrawBitmap(minx, miny, buffer) // Show approximate frame rate frame++ @@ -205,6 +207,7 @@ func DrawBackground() { w, h := display.Size() byteWidth := (w + 7) / 8 // Bitmap scanline pad = whole byte var b uint8 + buffer := frameBuffer.Rescale(int(w), 1) for j := int16(0); j < h; j++ { for k := int16(0); k < w; k++ { if k&7 > 0 { @@ -213,13 +216,11 @@ func DrawBackground() { b = graphics.Background[j*byteWidth+k/8] } if b&0x80 == 0 { - frameBuffer[2*k] = byte(BGCOLOR >> 8) - frameBuffer[2*k+1] = byte(BGCOLOR & 0xFF) + buffer.Set(int(k), 0, BGCOLOR) } else { - frameBuffer[2*k] = byte(GRIDCOLOR >> 8) - frameBuffer[2*k+1] = byte(GRIDCOLOR & 0xFF) + buffer.Set(int(k), 0, GRIDCOLOR) } } - display.DrawRGBBitmap8(0, j, frameBuffer[0:w*2], w, 1) + display.DrawBitmap(0, j, buffer) } }