Buffering Output

Tagged as lisp, output, buffering

Written on 2022-10-01 by Daniel 'jackdaniel' KochmaƄski

Single buffering

In graphical applications buffering of output is necessary to avoid flickering - a displeasing effect where mid-drawing artifacts are displayed on the screen. For example consider the following function:

(defun draw-scene (sheet)
  (draw-rectangle* sheet 125 125 175 175 :ink +red+)
  (draw-rectangle* sheet 125 125 175 175 :ink +blue+))

Here we draw two rectangles one on top of the other. If the red square is visible for a brief period of time before the blue one, then it is called flickering. To avoid this effect a concept of output buffering was invented - only when the output is ready for display, show it on the screen.

Double buffering

With double buffering we draw on the "back" buffer, and when done the back buffer contents are shown on the front buffer.

(defun game-loop ()
  (loop (draw-scene sheet)
        (swap-buffers sheet (buffer-1 sheet) (buffer-2 sheet))))

Triple buffering

The triple buffering is used when new scenes are produced much faster than the front buffer could be updated. We have "render", "ready" and "front" buffers. The implicit assumption is that the game loop and the display loop operate in separate threads.

(defun display-loop ()
  (loop (swap-buffers sheet (buffer-2 sheet) (buffer-3 sheet))
        (display-buffer sheet (buffer-3 sheet))))

Incremental and non-incremental rendering

If each frame is drawn from scratch (like in many games), then it doesn't matter whether the "swap" operation copies or swaps buffers. Some applications however treat the canvas incrementally. In this case losing the old content is not acceptable and we must copy data.

;;; The frame is rendered from scratch (not incremental)
(defmacro swap-buffers (sheet buffer-1 buffer-2)
  `(with-swap-lock (sheet)
     (rotatef ,buffer-1 ,buffer-2)))

;;; The frame is rendered based on the previosu content (incremental)
(defmacro copy-buffers (sheet buffer-1 buffer-2)
  `(with-swap-lock (sheet)
     (copy-array ,buffer-1 ,buffer-2)))

Copying data is more expensive than rotating buffers. That said sometimes re-rendering a frame from scratch may outweigh that cost. Incremental rendering resembles drawing on a paper - unless we clear it manually, the old content will be visible.

Mixed buffering

Sometimes we may want to draw directly on the front buffer. This is the most performant when we write each pixel exactly once (for example when we render an image). In this case we are not only expected to synchronize the front buffer with the back buffer, but also the other way around.

;;; Buffer-1 is "back", Buffer-2 is "front".

(defun activate-single-buffering ()
  ;; Update the front buffer immedietely.
  (copy-buffers sheet (buffer-1 sheet) (buffer-2 sheet)))

(defun activate-double-buffering ()
  ;; Synchronize the back buffer with the front-buffer.
  (copy-buffers sheet (buffer-2 sheet) (buffer-1 sheet)))

Otherwise, if we turn the double buffering back on, the back buffer won't contain the data that was drawn when the output was single-buffered.

Closing thoughts

There are many techniques that makes this more performant. My main goal with this post was to emphasize the difference between the incremental and non-incremental rendering that was usually ommited in materials I've found on the Internet.

Interesting reads: