Software Rendering

Updated: April 19, 2023

Software rendering is when applications render by accessing buffers and writing to them using the CPU (i.e., using a software rasterizer instead of a GPU).

Generally, you'll need to do the following steps in your application to render using software:

  1. Create your render target.
  2. Get access to your buffer.
  3. Draw to your buffer.
  4. Post your content.

Your main rendering loop comprises of these latter steps: getting access to your buffer, drawing to your buffer, and posting your content. When your render target has multiple buffers, Screen cycles the buffer handles accordingly so that SCREEN_PROPERTY_RENDER_BUFFERS always indicates the buffers that are available for rendering into. So, updating the handle to your buffer by retrieving SCREEN_PROPERTY_RENDER_BUFFERS must be part of your main rendering loop.

Create your render target

This target is where your application will render to. A render target can be a stream, a pixmap, or a window. You can also use a combination of these targets, depending on your application.

The code follows a very similar set of procedures whether you're using a pixmap, a window, or a stream:

...
screen_context_t screen_ctx;
screen_pixmap_t screen_pix;
...
screen_create_pixmap(&screen_pix, screen_ctx);
...
            
or:
...
screen_context_t screen_ctx;
screen_window_t screen_win;
...
screen_create_window(&screen_win, screen_ctx);
            
or:
...
screen_context_t screen_ctx;
screen_stream_t screen_stream;
...
screen_create_stream(&screen_stream, screen_ctx);
            

Now that you've created your target, you must set the appropriate properties on this target. At a minimum, you should set usage, color format, and buffer size. In order to do software rendering, it's important to set the usage flags with SCREEN_USAGE_READ, or SCREEN_USAGE_WRITE or both. Otherwise, the buffer won't be mapped into your application's memory space and you won't be able to get a pointer to the memory. For example, here's what the code might look like for creating a pixmap:

...
int size[2];
...

/* Note that if you're using a pixmap as your off-screen render target and you eventually wish
   to make at least parts of the pixmap buffer visible, then you must set the usage, format,
   and buffer size to be compatible with those of the window on which you'll be using to
   display your pixmap-rendered content.
*/
screen_set_pixmap_property_iv(screen_pix, SCREEN_PROPERTY_FORMAT, (const int[]){ SCREEN_FORMAT_RGBX8888 });
screen_set_pixmap_property_iv(screen_pix, SCREEN_PROPERTY_USAGE, (const int[]){ SCREEN_USAGE_WRITE | SCREEN_USAGE_NATIVE });
screen_set_pixmap_property_cv(screen_pix, SCREEN_PROPERTY_ID_STRING, strlen("sw-vsync-pix"), "sw-vsync-pix");
screen_get_window_property_iv(screen_win, SCREEN_PROPERTY_BUFFER_SIZE, size);
screen_set_pixmap_property_iv(screen_pix, SCREEN_PROPERTY_BUFFER_SIZE, size);
...
            

And finally, you'll need to create a buffer for your render target. For example:

screen_create_pixmap_buffer(screen_pix);            
            

Get access to your buffers

Once you've created your render target, you'll want to make sure you have access to the buffer of your render target. You'll want to retrieve the SCREEN_PROPERTY_RENDER_BUFFERS of your render target. This property will give you the handle to the buffer that's available for rendering. From the buffer, you can retrieve the pointer that is used by software renderers to read from and/or write to the buffer.

...
screen_buffer_t screen_pbuf;
...
screen_get_pixmap_property_pv(screen_pix, SCREEN_PROPERTY_RENDER_BUFFERS, (void **)&screen_pbuf);
...
            

Next, retrieve the pointer that is used by software renderers to read from and/or write to the buffer; take note that this is a property of the buffer, not the render target. For example:

...
void *pointer;
...
screen_get_buffer_property_pv(screen_pbuf, SCREEN_PROPERTY_POINTER, &pointer);
...
            

To be able to properly read from and/or write to the buffer, you'll also need to know the stride of the render buffer (i.e., the number of bytes allocated per line). The stride too, is a property of the buffer, not the render target. For example:

...
int stride;
...
screen_get_buffer_property_iv(screen_pbuf, SCREEN_PROPERTY_STRIDE, &stride);
...
            

Draw to your buffers

Draw (render) to the buffer associated to your render target. You can refresh all the dirty pixels using simple software rendering (writing bits to your buffer). This is where you need to take into account your color format, your memory layout, and the byte order.

Along with the rendering, your application can track the rectangular regions that have changed since the last frame was posted. These dirty rectangles define the area in which your application has changed bits. Tracking the dirty rectangles, and making these regions known to Screen when you post, allows Screen to optimize when posting your content.

Post your content

Trigger Screen to make your rendered content visible or to make it available to its consumers. The following example is specific to displaying content from a window (remember that if you're using a pixmap, you need to copy the buffer's content to a window before you can post it to make the buffer's content visible on a display):

...
int rects[8]; /* stores up to 2 dirty rectangles */
screen_buffer_t screen_wbuf = NULL;
...
screen_post_window(screen_win, screen_wbuf, 2, rects, 0);
            

If your render target is a pixmap, you'll need to do an extra step before your pixmap buffer content can be made visible. You'll need to copy the region of your pixmap buffer you want to display to a window buffer. Then, post the window. For example:

...
int rects[8]; /* stores up to 2 dirty rectangles */
...
screen_buffer_t screen_wbuf = NULL;
screen_get_window_property_pv(screen_win, SCREEN_PROPERTY_RENDER_BUFFERS, (void **)&screen_wbuf);
int i;
    for (i = 0; i < 2; i++) {
        screen_blit(screen_ctx, screen_wbuf, screen_pbuf, (const int []){
                SCREEN_BLIT_SOURCE_X, rects[i*4+0],
                SCREEN_BLIT_SOURCE_Y, rects[i*4+1],
                SCREEN_BLIT_SOURCE_WIDTH, rects[i*4+2],
                SCREEN_BLIT_SOURCE_HEIGHT, rects[i*4+3],
                SCREEN_BLIT_DESTINATION_X, rects[i*4+0],
                SCREEN_BLIT_DESTINATION_Y, rects[i*4+1],
                SCREEN_BLIT_DESTINATION_WIDTH, rects[i*4+2],
                SCREEN_BLIT_DESTINATION_HEIGHT, rects[i*4+3],
                SCREEN_BLIT_END });
    }
...
screen_post_window(screen_win, screen_wbuf, 2, rects, 0);
...
            

Ensure that you perform a screen_flush_blits() with SCREEN_WAIT_IDLE in the flags parameter, or call screen_post_window() before rendering to your pixmap buffer again.

When your application provides the dirty rectangle at the post request, Screen uses this information as a hint to determine regions of the final scene that needs to be redrawn. Dirty rectangles influence Screen to perform optimizations, but ultimately Screen takes into account several factors when determining which regions are redrawn.

Screen posts entire buffers. Therefore, your application must ensure that the entire content of a window buffer is suitable for display at all times.

The function, screen_post_window(), returns immediately when render buffers are available. If you call screen_post_window() with the flags parameter set to SCREEN_WAIT_IDLE, the function blocks until there's a render buffer available. If your window has two buffers, you might see a difference in the time it takes for your post to unblock between posting your first frame and your subsequent frames. After your first post, you still have your second buffer available, so your first post request can return immediately. When your application is in a steady state, Screen is accessing one buffer while your application is rendering to, or posting the second buffer. In this case, there are no buffers available for rendering, so screen_post_window() can't return right away. If you set flags parameter to SCREEN_WAIT_IDLE when you call screen_post_window(), it returns when the contents of the display have been updated, or when the scanout (when the frame is sent out the display port to the physical display) of the posting buffer begins.

Other things that affect how long the your post request blocks are use of hardware pipelines and displays with active refreshes. In the case when hardware pipelines are used, screen_post_window() blocks until the display update is done whereas for a Screen-composited buffer the function blocks until the display update has started. In the case where your display is not performing active refreshes, screen_post_window() can return as soon as the dirty rectangle region has been copied.

Most applications use two rendering buffers. The simplest way is to use the buffer in the first element of the SCREEN_PROPERTY_RENDER_BUFFERS property of your window. There are added complexities in managing more than two rendering buffers for your window. The number of available buffers may vary, depending on GPU timing, between one and the maximum number of buffers you've created. Therefore, the time it takes for screen_post_window() to return can cause complexity for animations, or those who want to smooth the timing. That being said, there are cases where multiple buffers are required based on the hardware accelerator supported on the target.

Posting may cause the SCREEN_PROPERTY_RENDER_BUFFERS property of the posting window to change. We recommend that only one thread per context operate on, or render to, this window. If your application uses multiple threads, you must ensure that access to this window's handle by these threads is guarded. If not, SCREEN_PROPERTY_RENDER_BUFFERS may reflect out-of-date information that can lead to animation artifacts. The presentation of new content may result in a copy or a buffer flip, depending on how Screen chooses to perform the operation. Use the window property SCREEN_PROPERTY_RENDER_BUFFER_COUNT to determine the number of buffers you have available for rendering.