Skip to content

Basic windowing / output example

We will demonstrate the basics of the libplacebo GPU output API with a worked example. The goal is to show a simple color on screen.

Creating a pl_log

Almost all major entry-points into libplacebo require providing a log callback (or NULL to disable logging). This is abstracted into the pl_log object type, which we can create with pl_log_create:

#include <libplacebo/log.h>

pl_log pllog;

int main()
{
    pllog = pl_log_create(PL_API_VER, pl_log_params(
        .log_cb = pl_log_color,
        .log_level = PL_LOG_INFO,
    ));

    // ...

    pl_log_destroy(&pllog);
    return 0;
}

Compiling

You can compile this example with:

$ gcc example.c -o example `pkg-config --cflags --libs libplacebo`

The parameter PL_API_VER has no special significance and is merely included for historical reasons. Aside from that, this snippet introduces a number of core concepts of the libplacebo API:

Parameter structs

For extensibility, almost all libplacebo calls take a pointer to a const struct pl_*_params, into which all extensible parameters go. For convenience, libplacebo provides macros which create anonymous params structs on the stack (and also fill in default parameters). Note that this only works for C99 and above, users of C89 and C++ must initialize parameter structs manually.

Under the hood, pl_log_params(...) just translates to &((struct pl_log_params) { /* default params */, ... }). This style of API allows libplacebo to effectively simulate optional named parameters.

On default parameters

Wherever possible, parameters are designed in such a way that {0} gives you a minimal parameter structure, with default behavior and no optional features enabled. This is done for forwards compatibility - as new features are introduced, old struct initializers will simply opt out of them.

Destructors

All libplacebo objects must be destroyed manually using the corresponding pl_*_destroy call, which takes a pointer to the variable the object is stored in. The resulting variable is written to NULL. This helps prevent use-after-free bugs.

NULL

As a general rule, all libplacebo destructors are safe to call on variables containing NULL. So, users need not explicitly NULL-test before calling destructors on variables.

Creating a window

While libplacebo can work in isolation, to render images offline, for the sake of this guide we want to provide something graphical on-screen. As such, we need to create some sort of window. Libplacebo provides no built-in mechanism for this, it assumes the API user will already have a windowing system in-place.

Complete examples (based on GLFW and SDL) can be found in the libplacebo demos. But for now, we will focus on getting a very simple window on-screen using GLFW:

// ...

#include <GLFW/glfw3.h>

const char * const title = "libplacebo demo";
int width = 800;
int height = 600;

GLFWwindow *window;

int main()
{
    pllog = pl_log_create(PL_API_VER, pl_log_params(
        .log_level = PL_LOG_INFO,
    ));

    if (!glfwInit())
        return 1;

    window = glfwCreateWindow(width, height, title, NULL, NULL);
    if (!window)
        return 1;

    while (!glfwWindowShouldClose(window)) {
        glfwWaitEvents();
    }

    glfwDestroyWindow(window);
    glfwTerminate();
    pl_log_destroy(&pllog);
    return 0;
}

Compiling

We now also need to include the glfw3 library to compile this example.

$ gcc example.c -o example `pkg-config --cflags --libs glfw3 libplacebo`

Creating the pl_gpu

All GPU operations are abstracted into an internal pl_gpu object, which serves as the primary entry-point to any sort of GPU interaction. This object cannot be created directly, but must be obtained from some graphical API: currently there are Vulkan, OpenGL or D3D11. A pl_gpu can be accessed from an API-specific object like pl_vulkan, pl_opengl and pl_d3d11.

In this guide, for simplicity, we will be using OpenGL, simply because that's what GLFW initializes by default.

// ...

pl_opengl opengl;

static bool make_current(void *priv);
static void release_current(void *priv);

int main()
{
    // ...
    window = glfwCreateWindow(width, height, title, NULL, NULL);
    if (!window)
        return 1;

    opengl = pl_opengl_create(pllog, pl_opengl_params(
        .get_proc_addr      = glfwGetProcAddress,
        .allow_software     = true,         // allow software rasterers
        .debug              = true,         // enable error reporting
        .make_current       = make_current, // (1)
        .release_current    = release_current,
    ));
    if (!opengl)
        return 2;

    while (!glfwWindowShouldClose(window)) {
        glfwWaitEvents();
    }

    pl_opengl_destroy(&opengl);
    glfwDestroyWindow(window);
    glfwTerminate();
    pl_log_destroy(&pllog);
    return 0;
}

static bool make_current(void *priv)
{
    glfwMakeContextCurrent(window);
    return true;
}

static void release_current(void *priv)
{
    glfwMakeContextCurrent(NULL);
}
  1. Setting this allows the resulting pl_gpu to be thread-safe, which enables asynchronous transfers to be used. The alternative is to simply call glfwMakeContextCurrent once after creating the window.

    This method of making the context current is generally preferred, however, so we've demonstrated it here for completeness' sake.

Creating a swapchain

All access to window-based rendering commands are abstracted into an object known as a "swapchain" (from Vulkan terminology), including the default backbuffers on D3D11 and OpenGL. If we want to present something to screen, we need to first create a pl_swapchain.

We can use this swapchain to perform the equivalent of gl*SwapBuffers:

// ...
pl_swapchain swchain;

static void resize_cb(GLFWwindow *win, int new_w, int new_h)
{
    width  = new_w;
    height = new_h;
    pl_swapchain_resize(swchain, &width, &height);
}

int main()
{
    // ...
    if (!opengl)
        return 2;

    swchain = pl_opengl_create_swapchain(opengl, pl_opengl_swapchain_params(
        .swap_buffers   = (void (*)(void *)) glfwSwapBuffers,
        .priv           = window,
    ));
    if (!swchain)
        return 2;

    // (2)
    if (!pl_swapchain_resize(swchain, &width, &height))
        return 2;
    glfwSetFramebufferSizeCallback(window, resize_cb);

    while (!glfwWindowShouldClose(window)) {
        pl_swapchain_swap_buffers(swchain);
        glfwPollEvents(); // (1)
    }

    pl_swapchain_destroy(&swchain);
    pl_opengl_destroy(&opengl);
    glfwDestroyWindow(window);
    glfwTerminate();
    pl_log_destroy(&pllog);
    return 0;
}
  1. We change this from glfwWaitEvents to glfwPollEvents because we now want to re-run our main loop once per vsync, rather than only when new events arrive. The pl_swapchain_swap_buffers call will ensure that this does not execute too quickly.

  2. The swapchain needs to be resized to fit the size of the window, which in GLFW is handled by listening to a callback. In addition to setting this callback, we also need to inform the swapchain of the initial window size.

    Note that the pl_swapchain_resize function handles both resize requests and size queries - hence, the actual swapchain size is returned back to the passed variables.

Getting pixels on the screen

With a swapchain in hand, we're now equipped to start drawing pixels to the screen:

// ...

static void render_frame(struct pl_swapchain_frame frame)
{
    pl_gpu gpu = opengl->gpu;

    pl_tex_clear(gpu, frame.fbo, (float[4]){ 1.0, 0.5, 0.0, 1.0 });
}

int main()
{
    // ...

    while (!glfwWindowShouldClose(window)) {
        struct pl_swapchain_frame frame;
        while (!pl_swapchain_start_frame(swchain, &frame))
            glfwWaitEvents(); // (1)
        render_frame(frame);
        if (!pl_swapchain_submit_frame(swchain))
            break; // (2)

        pl_swapchain_swap_buffers(swchain);
        glfwPollEvents();
    }

    // ...
}
  1. If pl_swapchain_start_frame fails, it typically means the window is hidden, minimized or blocked. This is not a fatal condition, and as such we simply want to process window events until we can resume rendering.

  2. If pl_swapchain_submit_frame fails, it typically means the window has been lost, and further rendering commands are not expected to succeed. As such, in this case, we simply terminate the example program.

Our main render loop has changed into a combination of pl_swapchain_start_frame, rendering, and pl_swapchain_submit_frame. To start with, we simply use the pl_tex_clear function to blit a constant orange color to the framebuffer.

Interlude: Rendering commands

The previous code snippet represented our first foray into the pl_gpu API. For more detail on this API, see the GPU API section. But as a general rule of thumb, all pl_gpu-level operations are thread safe, asynchronous (except when returning something to the CPU), and internally refcounted (so you can destroy all objects as soon as you no longer need the reference).

In the example loop, pl_swapchain_swap_buffers is the only operation that actually flushes commands to the GPU. You can force an early flush with pl_gpu_flush() or pl_gpu_finish(), but other than that, commands will "queue" internally and complete asynchronously at some unknown point in time, until forward progress is needed (e.g. pl_tex_download).

Conclusion

We have demonstrated how to create a window, how to initialize the libplacebo API, create a GPU instance based on OpenGL, and how to write a basic rendering loop that blits a single color to the framebuffer.

Here is a complete transcript of the example we built in this section:

Basic rendering
#include <GLFW/glfw3.h>

#include <libplacebo/log.h>
#include <libplacebo/opengl.h>
#include <libplacebo/gpu.h>

const char * const title = "libplacebo demo";
int width = 800;
int height = 600;

GLFWwindow *window;

pl_log pllog;
pl_opengl opengl;
pl_swapchain swchain;

static bool make_current(void *priv);
static void release_current(void *priv);

static void resize_cb(GLFWwindow *win, int new_w, int new_h)
{
    width  = new_w;
    height = new_h;
    pl_swapchain_resize(swchain, &width, &height);
}

static void render_frame(struct pl_swapchain_frame frame)
{
    pl_gpu gpu = opengl->gpu;

    pl_tex_clear(gpu, frame.fbo, (float[4]){ 1.0, 0.5, 0.0, 1.0 });
}

int main()
{
    pllog = pl_log_create(PL_API_VER, pl_log_params(
        .log_cb = pl_log_color,
        .log_level = PL_LOG_INFO,
    ));

    if (!glfwInit())
        return 1;

    window = glfwCreateWindow(width, height, title, NULL, NULL);
    if (!window)
        return 1;

    opengl = pl_opengl_create(pllog, pl_opengl_params(
        .get_proc_addr      = glfwGetProcAddress,
        .allow_software     = true,         // allow software rasterers
        .debug              = true,         // enable error reporting
        .make_current       = make_current,
        .release_current    = release_current,
    ));

    swchain = pl_opengl_create_swapchain(opengl, pl_opengl_swapchain_params(
        .swap_buffers   = (void (*)(void *)) glfwSwapBuffers,
        .priv           = window,
    ));
    if (!swchain)
        return 2;

    if (!pl_swapchain_resize(swchain, &width, &height))
        return 2;
    glfwSetFramebufferSizeCallback(window, resize_cb);

    while (!glfwWindowShouldClose(window)) {
        struct pl_swapchain_frame frame;
        while (!pl_swapchain_start_frame(swchain, &frame))
            glfwWaitEvents();
        render_frame(frame);
        if (!pl_swapchain_submit_frame(swchain))
            break;

        pl_swapchain_swap_buffers(swchain);
        glfwPollEvents();
    }

    pl_swapchain_destroy(&swchain);
    pl_opengl_destroy(&opengl);
    glfwDestroyWindow(window);
    glfwTerminate();
    pl_log_destroy(&pllog);
    return 0;
}

static bool make_current(void *priv)
{
    glfwMakeContextCurrent(window);
    return true;
}

static void release_current(void *priv)
{
    glfwMakeContextCurrent(NULL);
}