diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:38:23 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:38:23 +0000 |
commit | ff6e3c025658a5fa1affd094f220b623e7e1b24b (patch) | |
tree | 9faab72d69c92d24e349d184f5869b9796f17e0c /docs/basic-rendering.md | |
parent | Initial commit. (diff) | |
download | libplacebo-ff6e3c025658a5fa1affd094f220b623e7e1b24b.tar.xz libplacebo-ff6e3c025658a5fa1affd094f220b623e7e1b24b.zip |
Adding upstream version 6.338.2.upstream/6.338.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'docs/basic-rendering.md')
-rw-r--r-- | docs/basic-rendering.md | 432 |
1 files changed, 432 insertions, 0 deletions
diff --git a/docs/basic-rendering.md b/docs/basic-rendering.md new file mode 100644 index 0000000..09a1f6b --- /dev/null +++ b/docs/basic-rendering.md @@ -0,0 +1,432 @@ +# 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`: + +``` c linenums="1" +#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; +} +``` + +!!! note "Compiling" + + You can compile this example with: + + ``` bash + $ 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. + +!!! note "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. + +!!! note "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](https://code.videolan.org/videolan/libplacebo/-/tree/master/demos). But +for now, we will focus on getting a very simple window on-screen using GLFW: + +``` c linenums="1" hl_lines="3 5 6 7 9 17 18 20 21 22 24 25 26 28 29" +// ... + +#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; +} +``` + +!!! note "Compiling" + + We now also need to include the glfw3 library to compile this example. + + ``` bash + $ 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. + +``` c linenums="1" hl_lines="3 5-6 15-23 29 36-45" +// ... + +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`: + +``` c linenums="1" hl_lines="2 4-9 17-22 24-27 30-31 34" +// ... +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: + +``` c linenums="1" hl_lines="3-8 15-20" +// ... + +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](#TODO) 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: + +??? example "Basic rendering" + ``` c linenums="1" + #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); + } + ``` |