This is a (bad) video of interlaced update, so half the display updated each frame, just to give an idea of the speed.
You can find the driver code here, https://github.com/xriss/timecake/blob/master/src/sys/lcd_pine.c What follows are some notes on why it does things the way it does.
Fortunately we have a reasonable bit of documentation for the PineTime LCD driver, https://wiki.pine64.org/images/5/54/ST7789V_v1.6.pdf It is overly verbose with only a few pages of useful information and I have read it through repeatedly. Unfortunately I could not see any way of doing something clever, we are at the other end of a rather slow serial interface with limited control over the format of data we can send across. So bandwidth combined with the lack of display configuration options is a real problem.
The LCD has a 320x240 display buffer with 666 bits of RGB color output resolution, this buffer is used to drive the LCD hardware at 60-30fps. We can send data into this buffer across the serial interface as 444, 565 or 888 bits of RGB. Since we are mostly bound by the speed of transfer, I recommend only using the 444 mode. Remember, even if you use the 888 mode then it will be truncated to 666 bits before it is displayed. This may cause ugly banding unless pre dithered to 666 color.
I attempted a 240x120, half resolution / double height pixels mode by tweaking the scroll registers every scan line. This seems impossible as we do not have access to the blanking interrupt pin and even if we did the documentation (and my tests) suggests that changes are ignored mid frame. Which is a shame as it would allow for faster and double buffered full screen updates.
There is still a double buffered mode we could use but it involves throwing away 80 display lines of pixels. The LCD has a 320x240 buffer on its end of the serial interface so by only displaying half of it at once (160x240) we can update the other half of memory without changes being visible until we flip the screen using the scroll registers. I did not consider this worthwhile as it would still only be able to manage around 4fps, but it would be easy enough to add.
So at best we can manage a few frames per second, lets say 4fps, for a full screen update. Obviously we can double that with interleaved updates but that's as good as it gets and we can not even double buffer these full screen updates. They look more like wipes than animation flips.
The actual display code ended up rather simple, we do not have enough RAM for fullscreen buffers on the CPU so we use a shadertoy style system with a hook function called for each x,y pixel returning a 24bit color value, 0xrrggbb or -1 for transparent. We even try and interleave the calls to this function with the serial data transfer so we are using the time we would be waiting for data to transmit to the LCD to work out what color the next pixel will be.
This allows us to craft simple functions that take an XY location and
return an RGB color, these do not require any ram buffers as the data
is sent immediately to the LCD.
Finally an important note about hardware : One thing I did discover is that using NRF_SPI0 to send data to the LCD means that we can not use NRF_TWIM0 to talk to the I2C devices since they both use the same hardware. Not a problem, it just means we have to use NRF_TWIM1 for the I2C devices. This is probably documented somewhere in the NRF52 chip documentation but I have no idea where and found it out the hard way.