This video demonstrates, in slow motion, the process of rendering a 3D scene using so-called S-buffers. Compared to the now-ubiquitous depth buffering, this technique requires much less memory (and memory bandwidth) when rendering low resolution, low polygon count scenes.

The rendering consists of two steps. In the first phase, an array of empty linked lists is initialized (one list per scan line). The polygons to be displayed are then projected into screen space, clipped to the viewport, and decomposed into tuples of $(Y, X_{start}, X_{end})$. These are inserted into the linked lists, at position $Y$ in the array and ordered by $X_{start}$ within each list. These spans are clipped against each other, based on relative depth of end-points, to ensure no overlap within the S-buffers. (Intersecting faces are not handled well in my implementation, and will lead to visual glitches.)

At the end of the first phase, objects of infinite depth (such as a sky box) may used to fill gaps in the S-buffers.

Contrary to what the video shows, by this point, nothing would have been drawn to the screen yet.

Enter phase two: the viewport is scanned line by line, and the spans for each line are rasterized, in order, into the frame buffer. If any gaps remain, they are filled with a background color. Because the spans cannot overlap, the scene is drawn with zero over-draw.

The processing time of the first phase depends on the viewport resolution, as well as complexity of the geometry. It is beneficial to draw the closest objects first, to avoid having to replace spans already inserted. (this issue is demonstrated at the beginning of Scene #2, where many small objects are drawn, only to be completely obscured later)

The second phase consists mainly of pushing pixels, so viewport resolution will be a driving factor. In addition, there is a non-trivial cost per span drawn, because properties of the original face must be fetched to set up texture coordinates or any other interpolated attibutes.

Transparent surfaces must be drawn at the very end, back-to-front, with depth-based clipping against spans in the S-buffers. This makes them rather inefficient and prone to over-draw.

You might notice that occasionaly there are pixel gaps between adjacent faces. This is not due to any fault of the algorithm or the renderer implementation, but due to T-junctions in my geometry, which must be fixed in the source data, or by face splitting as a preprocessing step.

The video was made possible, in part, by the libco library, which allowed me to insert yield statements deep in the renderer loops without much hassle. Credit is also due to the ever-amazing ffmpeg.