Double Buffering Coordinated via TaskNotify
Eliminating work done for pixels that will never been seen is always a good change for efficiency. Next item on the to-do list is to work on pixels that will be seen... but we don't want to see them until they're ready. Version 1.0.0 of ESP_8_BIT_composite color video out library used only a single buffer, where code is drawing to the buffer at the same time the video signal generation code is reading from the buffer. When those two separate pieces of code overlap, we get visual artifacts on screen ranging from incomplete shapes to annoying flickers.
The classic solution to this is double-buffering, which the precedent ESP_8_BIT did not do. I hypothesize there were two reasons for this: #1 emulator memory requirements did not leave enough for a second buffer and #2 emulators sent its display data in horizontal line order, managing to 'race ahead" of the video scan line and avoid artifacts. But now both of those are gone. #1 no longer applies because emulators had been cut out, freeing memory. And we lost #2 because Adafruit GFX is concentrated around vertical lines so it is orthogonal to scan line and no longer able to "race ahead" of it resulting in visual artifacts. Thus we need two buffers. A back buffer for the Adafruit GFX code to draw on, and a front buffer for the video signal generation code to read from. At the end of each NTSC frame, I have an opportunity to swap the buffers. Doing it at that point ensures we'll never try to show a partially drawn frame.
I had originally planned to make double-buffering an optional configurable feature. But once I saw how much of an improvement this was, I decided everyone will get it all of the time. In the spirit of Arduino library style guide recommendations, I'm keeping the recommended code path easy to use. For simple Arduino apps the memory pressure would not be a problem on an ESP32. If someone wants to return to single buffer for memory needs, or maybe even as a deliberate artistic decision to have flickers, they can take my code and create their own variant.
Knowing when to swap the buffer was easy, video_isr()
had a conveniently commented section // frame is done
. At that point I can swap the front and back buffers if the back buffer is flagged as ready to go. My problem was that I didn't know how to signal the drawing code they have a new back buffer and they can start drawing the next frame. The existing video_sync()
(which I use for my waitForFrame()
API) forecasts the amount of time to render a frame and uses vTaskDelay()
which I am somewhat suspicious of. FreeRTOS documentation has the disclaimer that vTaskDelay()
has no guarantee that it will resume at the specified time. The synchronization was thus inferred rather than explicit, and I wanted something that ties the two pieces of code more concretely together. My research eventually led to vTaskNotifyGiveFromISR()
I can use in video_isr()
to signal its counterpart ulTaskNotifyTake()
which I will use for a replacement implementation of video_sync()
. I anticipate this will prove to be a more reliable way for the application code to know they can start working on the next frame. But how much time do they have to spare between frames? That's the next project: some performance metrics.
[Code for this project is publicly available on GitHub]