-
Notifications
You must be signed in to change notification settings - Fork 213
Pixel/image buffers #617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Pixel/image buffers #617
Conversation
c82c0e2
to
0135459
Compare
ili9341/ili9341.go
Outdated
@@ -200,6 +205,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 pixel.Image[pixel.RGB565BE]) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A suggestion based on personal preference when working with generics:
Generic data structures can get tedious to write out and decrease legibility. I've found that using type aliases to declare shorthand common generic types is quite idiomatic:
type Image = pixel.Image[pixel.RGB565BE]
You then use the Image
type anywhere you'd use pixel.Image[pixel.RGB565BE]
. A caveat (or benefit even) is that users will see the Image
type if you export it, so you can even provide the convenience ili9341.Image
type along with your device library so that generics don't pollute user facing APIs.
The greatest benefit of type aliases for generics I think comes from not having to add type parameters to structures which contain the type as one of the fields, thus preventing viral spreading of type parameters in a codebase.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I could add it, if needed. Though I don't see the need for it? I haven't needed anything like this for the three small projects I used the pixel.Image
API with.
The greatest benefit of type aliases for generics I think comes from not having to add type parameters to structures which contain the type as one of the fields, thus preventing viral spreading of type parameters in a codebase.
Yes, generics can get quite viral. See for example my tinygl package, which has the type parameter on almost every type/struct/method: https://pkg.go.dev/github.com/aykevl/tinygl
However, I don't really see a way around this. For a specific application (where you are fine with hardcoding the pixel format) you can certainly make a type alias like this. But when you do that, you lose the ability to switch the pixel format. (For example: for my watch I use RGB444 instead of RGB565 for improved speed, and the simulator uses RGB888).
So in short, I could add ili9341.Image
, but I'm not sure it's really necessary/useful. And users can define it themselves if they really want to.
EDIT: oh, apparently the ili9341 display only supports RGB565 and RGB666 (and I don't think RGB666 is used much in practice). In that case, I guess it makes sense to define ili9341.Image
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! agreed 100%, including the last EDIT note! 🧡.
This looks really nice.
FWIW the sh1106 probably could use the above format as well: https://github.com/tinygo-org/drivers/blob/release/sh1106/sh1106.go#L203-L226 One thing that comes to mind for me is the thought that For example, if defining a |
I don't think I have this display, but yeah that looks like a place where it would be useful!
You mean storing the entire contents of the scroll buffer in-memory? For TinyGl I have a slightly different approach: https://pkg.go.dev/github.com/aykevl/tinygl#ScrollBox Related: https://hachyderm.io/@ayke/110554855872552126 (but with some patches I still haven't pushed due to bugs) In short, I don't think adding a scroll offset is a good way to do this. You'll quickly run into memory issues on common MCU/display combinations and there are alternatives for getting the same scroll effect. |
Ok thank you for explaining. The TinyGl mehod you mentioned is close to what I what thinking, but it makes sense that it at a different layer of the "stack". |
0135459
to
db73aad
Compare
Made a few small changes, see the force push: 0135459...db73aad.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a closer look there are some things I think are worth a second look at, mainly unsafe memory access.
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.
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.
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.
This adds a new DrawBitmap method, which is meant to replace DrawRGBBitmap8.
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.
db73aad
to
a31db2b
Compare
@soypat found a pretty big oversight on my part: bounds checking for the
|
} | ||
|
||
// NewImage creates a new image of the given size. | ||
func NewImage[T Color](width, height int) Image[T] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea: might it be worth passing in the desired frame buffer to NewImage as a third argument? A couple of benefits I've come up with:
- Code readability improvements in generics department: Since the buffer has the color type there is no need to pass in a type parameter.
- Ease of use for people who don't want allocations: Makes it easier for users who want to reuse buffers and possible make images within images (given they use a new strided Image type for subimages)
If the user passes in a nil buffer then NewImage could automatically assign it a size. NewImage should however calculate the minimum size as it does now and make sure the argument buffer size meets the minimum size requirements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea. Right now the buffer is an implementation detail, and I like to keep it that way (at least for now): calculating the size of the buffer is somewhat non-trivial for pixel sizes that don't neatly fit on byte boundaries (like RGB444 and perhaps others in the future like black&white with one bit per pixel). In fact RGB444 uses []byte
which wouldn't help with generics.
If there is a practical need for this (not just a theoretical one), we can always add a NewImageWithBuffer
(name TBD) that can reuse an existing buffer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, yeah RGB444 would make it hard for users to use and NewImageWithBuffer idea is very sound
@aykevl Apologies if this is not the right place to ask, but I was curious why do colors in the pixel package have an RGBA() color.RGBA method rather than RGBA() (r, g, b, a uint32) method? With the latter, they implement color.Color and it's much easier to use these memory efficient pixel formats with the standard library. |
This PR refactors pixel buffer handling.
Summary
I'd like to add a new pixel package for efficiently working with image buffers for displays, and add a
DrawBitmap(x, y, bitmap)
method to send these image buffers directly to a SPI display. This is intended as preparation for DMA support in displays (StartDrawBitmap
) and should replaceDrawRGBBitmap8
. I've already done lots of tests with this design outside the drivers package and I think it works well.Details
I've added a new package to efficiently work with pixel buffers in various formats: tinygo.org/x/drivers/pixel. This isn't new code: I've been using it for a while in this package: https://pkg.go.dev/github.com/aykevl/tinygl/pixel. It's pretty fast, see for example the demos in this thread.
The basic idea is as follows:
color.RGBA
for example, which would be very wasteful (often, half the bits are unused).[]T
butpixel.Image[T]
.st7789.New
vsst7789.NewOf
).The advantage of generics is that it is trivially easy to use multiple pixel formats in a single program for different displays if needed, something that LVGL doesn't support (the pixel format is a compile time constant).
SetPixel
: it was probably a mistake (it's too slow). One such package is https://github.com/aykevl/tinygl/ which we might eventually want to move to tinygo-org. Another example is the refactored pyportal_boing example (which actually got faster with the refactor!).Comparisons with other approaches:
pixel.RGB565BE
.LV_COLOR_16_SWAP
).The main reason I'm doing this is because I would like to add DMA support to most displays using a very similar method:
StartDrawBitmap
(andIsAsync
/Wait
). I figured it would be much easier to add this as a synchronous API and only do the async one once the synchronous one is agreed upon. I didn't want to useStartDrawRGBBitmap8
with a byte array - I think that's just not a great API and too easy to make mistakes with.Next up:
pixel.Gray1
(black/white) pixel format, for e-ink screens.StartDrawBitmap
(see machine: add SPI DMA support for the rp2040 and samd51 tinygo#3985).Anyway, looking forward to feedback on this. Questions? Comments? Is this the direction we want to take with TinyGo graphics? I know generics might complicate things but I really think it's the best tool for the job here.