summaryrefslogtreecommitdiffstats
path: root/demos
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:38:23 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:38:23 +0000
commitff6e3c025658a5fa1affd094f220b623e7e1b24b (patch)
tree9faab72d69c92d24e349d184f5869b9796f17e0c /demos
parentInitial commit. (diff)
downloadlibplacebo-upstream.tar.xz
libplacebo-upstream.zip
Adding upstream version 6.338.2.upstream/6.338.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--demos/LICENSE121
-rw-r--r--demos/colors.c88
-rw-r--r--demos/common.h11
-rw-r--r--demos/meson.build170
-rw-r--r--demos/multigpu-bench.c484
-rw-r--r--demos/plplay.c766
-rw-r--r--demos/plplay.h138
-rw-r--r--demos/screenshots/plplay1.pngbin0 -> 25495 bytes
-rw-r--r--demos/screenshots/plplay2.pngbin0 -> 21732 bytes
-rw-r--r--demos/screenshots/plplay3.pngbin0 -> 23745 bytes
-rw-r--r--demos/screenshots/plplay4.pngbin0 -> 22326 bytes
-rw-r--r--demos/screenshots/plplay5.pngbin0 -> 22959 bytes
-rw-r--r--demos/screenshots/plplay6.pngbin0 -> 25061 bytes
-rw-r--r--demos/sdlimage.c281
-rw-r--r--demos/settings.c1238
-rw-r--r--demos/ui.c221
-rw-r--r--demos/ui.h59
-rw-r--r--demos/utils.c49
-rw-r--r--demos/utils.h5
-rw-r--r--demos/video-filtering.c871
-rw-r--r--demos/window.c123
-rw-r--r--demos/window.h67
-rw-r--r--demos/window_glfw.c536
-rw-r--r--demos/window_sdl.c404
24 files changed, 5632 insertions, 0 deletions
diff --git a/demos/LICENSE b/demos/LICENSE
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/demos/LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/demos/colors.c b/demos/colors.c
new file mode 100644
index 0000000..41712e1
--- /dev/null
+++ b/demos/colors.c
@@ -0,0 +1,88 @@
+/* Simplistic demo that just makes the window colorful, including alpha
+ * transparency if supported by the windowing system.
+ *
+ * License: CC0 / Public Domain
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <math.h>
+#include <string.h>
+
+#include "common.h"
+#include "pl_clock.h"
+#include "window.h"
+
+static pl_log logger;
+static struct window *win;
+
+static void uninit(int ret)
+{
+ window_destroy(&win);
+ pl_log_destroy(&logger);
+ exit(ret);
+}
+
+int main(int argc, char **argv)
+{
+ logger = pl_log_create(PL_API_VER, pl_log_params(
+ .log_cb = pl_log_color,
+ .log_level = PL_LOG_DEBUG,
+ ));
+
+ win = window_create(logger, &(struct window_params) {
+ .title = "colors demo",
+ .width = 640,
+ .height = 480,
+ .alpha = true,
+ });
+ if (!win)
+ uninit(1);
+
+ pl_clock_t ts_start, ts;
+ if ((ts_start = pl_clock_now()) == 0) {
+ uninit(1);
+ }
+
+ while (!win->window_lost) {
+ if (window_get_key(win, KEY_ESC))
+ break;
+
+ struct pl_swapchain_frame frame;
+ bool ok = pl_swapchain_start_frame(win->swapchain, &frame);
+ if (!ok) {
+ // Something unexpected happened, perhaps the window is not
+ // visible? Wait for events and try again.
+ window_poll(win, true);
+ continue;
+ }
+
+ if ((ts = pl_clock_now()) == 0)
+ uninit(1);
+
+ const double period = 10.; // in seconds
+ double secs = fmod(pl_clock_diff(ts, ts_start), period);
+
+ double pos = 2 * M_PI * secs / period;
+ float alpha = (cos(pos) + 1.0) / 2.0;
+
+ assert(frame.fbo->params.blit_dst);
+ pl_tex_clear(win->gpu, frame.fbo, (float[4]) {
+ alpha * (sinf(2 * pos + 0.0) + 1.0) / 2.0,
+ alpha * (sinf(2 * pos + 2.0) + 1.0) / 2.0,
+ alpha * (sinf(2 * pos + 4.0) + 1.0) / 2.0,
+ alpha,
+ });
+
+ ok = pl_swapchain_submit_frame(win->swapchain);
+ if (!ok) {
+ fprintf(stderr, "libplacebo: failed submitting frame!\n");
+ uninit(3);
+ }
+
+ pl_swapchain_swap_buffers(win->swapchain);
+ window_poll(win, false);
+ }
+
+ uninit(0);
+}
diff --git a/demos/common.h b/demos/common.h
new file mode 100644
index 0000000..c768a7c
--- /dev/null
+++ b/demos/common.h
@@ -0,0 +1,11 @@
+// License: CC0 / Public Domain
+#pragma once
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <libplacebo/log.h>
+#include <libplacebo/renderer.h>
+
+#include "config_demos.h"
diff --git a/demos/meson.build b/demos/meson.build
new file mode 100644
index 0000000..fef665a
--- /dev/null
+++ b/demos/meson.build
@@ -0,0 +1,170 @@
+glfw = dependency('glfw3', required: false)
+sdl = dependency('sdl2', required: false)
+sdl_image = dependency('SDL2_image', required: false)
+
+ffmpeg_deps = [
+ dependency('libavcodec', required: false),
+ dependency('libavformat', required: false),
+ dependency('libavutil', required: false),
+]
+
+ffmpeg_found = true
+foreach dep : ffmpeg_deps
+ ffmpeg_found = ffmpeg_found and dep.found()
+endforeach
+
+nuklear = disabler()
+nuklear_inc = include_directories('./3rdparty/nuklear')
+if cc.has_header('nuklear.h', include_directories: nuklear_inc)
+ nuklear_lib = static_library('nuklear',
+ include_directories: nuklear_inc,
+ c_args: ['-O2', '-Wno-missing-prototypes'],
+ dependencies: [ libplacebo, libm ],
+ sources: 'ui.c',
+ )
+
+ nuklear = declare_dependency(
+ include_directories: nuklear_inc,
+ link_with: nuklear_lib,
+ )
+else
+ warning('Nuklear was not found in `demos/3rdparty`. Please run ' +
+ '`git submodule update --init` followed by `meson --wipe`.')
+endif
+
+conf_demos = configuration_data()
+conf_demos.set('HAVE_NUKLEAR', nuklear.found())
+conf_demos.set('HAVE_EGL', cc.check_header('EGL/egl.h', required: false))
+
+apis = []
+
+# Enable all supported combinations of API and windowing system
+if glfw.found()
+ if components.get('vulkan')
+ conf_demos.set('HAVE_GLFW_VULKAN', true)
+ apis += static_library('glfw-vk',
+ dependencies: [libplacebo, libm, glfw, vulkan_headers],
+ sources: 'window_glfw.c',
+ c_args: ['-DUSE_VK'],
+ include_directories: vulkan_headers_inc,
+ )
+ endif
+
+ if components.get('opengl')
+ conf_demos.set('HAVE_GLFW_OPENGL', true)
+ apis += static_library('glfw-gl',
+ dependencies: [libplacebo, glfw],
+ sources: 'window_glfw.c',
+ c_args: '-DUSE_GL',
+ )
+ endif
+
+ if components.get('d3d11')
+ conf_demos.set('HAVE_GLFW_D3D11', true)
+ apis += static_library('glfw-d3d11',
+ dependencies: [libplacebo, glfw],
+ sources: 'window_glfw.c',
+ c_args: '-DUSE_D3D11',
+ )
+ endif
+endif
+
+if sdl.found()
+ if components.get('vulkan')
+ conf_demos.set('HAVE_SDL_VULKAN', true)
+ apis += static_library('sdl-vk',
+ dependencies: [libplacebo, sdl, vulkan_headers],
+ sources: 'window_sdl.c',
+ c_args: ['-DUSE_VK'],
+ include_directories: vulkan_headers_inc,
+ )
+ endif
+
+ if components.get('opengl')
+ conf_demos.set('HAVE_SDL_OPENGL', true)
+ apis += static_library('sdl-gl',
+ dependencies: [libplacebo, sdl],
+ sources: 'window_sdl.c',
+ c_args: '-DUSE_GL',
+ )
+ endif
+endif
+
+configure_file(
+ output: 'config_demos.h',
+ configuration: conf_demos,
+)
+
+if apis.length() == 0
+ warning('Demos enabled but no supported combination of windowing system ' +
+ 'and graphical APIs was found. Demo programs require either GLFW or ' +
+ 'SDL and either Vulkan or OpenGL to function.')
+else
+
+ additional_dep = []
+ if host_machine.system() == 'windows'
+ additional_dep += cc.find_library('winmm')
+ endif
+
+ dep = declare_dependency(
+ dependencies: [ libplacebo, build_deps ] + additional_dep,
+ sources: ['window.c', 'utils.c'],
+ include_directories: vulkan_headers_inc,
+ link_with: apis,
+ )
+
+ # Graphical demo programs
+ executable('colors', 'colors.c',
+ dependencies: [ dep, pl_clock, libm ],
+ link_args: link_args,
+ link_depends: link_depends,
+ )
+
+ if sdl_image.found()
+ executable('sdlimage', 'sdlimage.c',
+ dependencies: [ dep, libm, sdl_image ],
+ link_args: link_args,
+ link_depends: link_depends,
+ )
+ endif
+
+ if ffmpeg_found
+ plplay_deps = [ dep, pl_thread, pl_clock ] + ffmpeg_deps
+ if nuklear.found()
+ plplay_deps += nuklear
+ endif
+ if host_machine.system() == 'windows'
+ plplay_deps += cc.find_library('shlwapi', required: true)
+ endif
+ plplay_sources = ['plplay.c', 'settings.c']
+ if host_machine.system() == 'windows'
+ windows = import('windows')
+ plplay_sources += windows.compile_resources(demos_rc, depends: version_h,
+ include_directories: meson.project_source_root()/'win32')
+ endif
+ executable('plplay', plplay_sources,
+ dependencies: plplay_deps,
+ link_args: link_args,
+ link_depends: link_depends,
+ install: true,
+ )
+ endif
+
+endif
+
+# Headless vulkan demos
+if components.get('vk-proc-addr')
+ executable('video-filtering', 'video-filtering.c',
+ dependencies: [ libplacebo, pl_clock, pl_thread, vulkan_loader ],
+ c_args: '-O2',
+ link_args: link_args,
+ link_depends: link_depends,
+ )
+
+ executable('multigpu-bench', 'multigpu-bench.c',
+ dependencies: [ libplacebo, pl_clock, vulkan_loader ],
+ c_args: '-O2',
+ link_args: link_args,
+ link_depends: link_depends,
+ )
+endif
diff --git a/demos/multigpu-bench.c b/demos/multigpu-bench.c
new file mode 100644
index 0000000..75f1135
--- /dev/null
+++ b/demos/multigpu-bench.c
@@ -0,0 +1,484 @@
+/* GPU->GPU transfer benchmarks. Requires some manual setup.
+ *
+ * License: CC0 / Public Domain
+ */
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <math.h>
+
+#include <libplacebo/gpu.h>
+#include <libplacebo/vulkan.h>
+
+#include "pl_clock.h"
+
+#define ALIGN2(x, align) (((x) + (align) - 1) & ~((align) - 1))
+
+enum {
+ // Image configuration
+ NUM_TEX = 16,
+ WIDTH = 1920,
+ HEIGHT = 1080,
+ DEPTH = 16,
+ COMPS = 1,
+
+ // Queue configuration
+ NUM_QUEUES = NUM_TEX,
+ ASYNC_TX = 1,
+ ASYNC_COMP = 1,
+
+ // Buffer configuration
+ PTR_ALIGN = 4096,
+ PIXEL_PITCH = DEPTH / 8,
+ ROW_PITCH = ALIGN2(WIDTH * PIXEL_PITCH, 256),
+ IMAGE_SIZE = ROW_PITCH * HEIGHT,
+ BUFFER_SIZE = IMAGE_SIZE + PTR_ALIGN - 1,
+
+ // Test configuration
+ TEST_MS = 1500,
+ WARMUP_MS = 500,
+ POLL_FREQ = 10,
+};
+
+static uint8_t* page_align(uint8_t *data)
+{
+ return (uint8_t *) ALIGN2((uintptr_t) data, PTR_ALIGN);
+}
+
+enum mem_owner {
+ CPU,
+ SRC,
+ DST,
+ NUM_MEM_OWNERS,
+};
+
+enum mem_type {
+ RAM,
+ GPU,
+ NUM_MEM_TYPES,
+};
+
+// This is attached to every `pl_tex.params.user_data`
+struct buffers {
+ pl_gpu gpu;
+ pl_buf buf[NUM_MEM_TYPES];
+ pl_buf exported[NUM_MEM_TYPES];
+ pl_buf imported[NUM_MEM_TYPES];
+ struct pl_tex_transfer_params async;
+};
+
+static struct buffers *alloc_buffers(pl_gpu gpu)
+{
+ struct buffers *buffers = malloc(sizeof(*buffers));
+ *buffers = (struct buffers) { .gpu = gpu };
+
+ for (enum mem_type type = 0; type < NUM_MEM_TYPES; type++) {
+ buffers->buf[type] = pl_buf_create(gpu, pl_buf_params(
+ .size = BUFFER_SIZE,
+ .memory_type = type == RAM ? PL_BUF_MEM_HOST : PL_BUF_MEM_DEVICE,
+ .host_mapped = true,
+ ));
+ if (!buffers->buf[type])
+ exit(2);
+
+ if (gpu->export_caps.buf & PL_HANDLE_DMA_BUF) {
+ buffers->exported[type] = pl_buf_create(gpu, pl_buf_params(
+ .size = BUFFER_SIZE,
+ .memory_type = type == RAM ? PL_BUF_MEM_HOST : PL_BUF_MEM_DEVICE,
+ .export_handle = PL_HANDLE_DMA_BUF,
+ ));
+ }
+ }
+
+ return buffers;
+}
+
+static void free_buffers(struct buffers *buffers)
+{
+ for (enum mem_type type = 0; type < NUM_MEM_TYPES; type++) {
+ pl_buf_destroy(buffers->gpu, &buffers->buf[type]);
+ pl_buf_destroy(buffers->gpu, &buffers->exported[type]);
+ pl_buf_destroy(buffers->gpu, &buffers->imported[type]);
+ }
+ free(buffers);
+}
+
+static void link_buffers(pl_gpu gpu, struct buffers *buffers,
+ const struct buffers *import)
+{
+ if (!(gpu->import_caps.buf & PL_HANDLE_DMA_BUF))
+ return;
+
+ for (enum mem_type type = 0; type < NUM_MEM_TYPES; type++) {
+ if (!import->exported[type])
+ continue;
+ buffers->imported[type] = pl_buf_create(gpu, pl_buf_params(
+ .size = BUFFER_SIZE,
+ .memory_type = type == RAM ? PL_BUF_MEM_HOST : PL_BUF_MEM_DEVICE,
+ .import_handle = PL_HANDLE_DMA_BUF,
+ .shared_mem = import->exported[type]->shared_mem,
+ ));
+ }
+}
+
+struct ctx {
+ pl_gpu srcgpu, dstgpu;
+ pl_tex src, dst;
+
+ // for copy-based methods
+ enum mem_owner owner;
+ enum mem_type type;
+ bool noimport;
+ bool async;
+};
+
+static void await_buf(pl_gpu gpu, pl_buf buf)
+{
+ while (pl_buf_poll(gpu, buf, UINT64_MAX))
+ ; // do nothing
+}
+
+static void async_upload(void *priv)
+{
+ struct buffers *buffers = priv;
+ pl_tex_upload(buffers->gpu, &buffers->async);
+}
+
+static inline void copy_ptr(struct ctx ctx)
+{
+ const pl_gpu srcgpu = ctx.srcgpu, dstgpu = ctx.dstgpu;
+ const pl_tex src = ctx.src, dst = ctx.dst;
+ struct buffers *srcbuffers = src->params.user_data;
+ struct buffers *dstbuffers = dst->params.user_data;
+ pl_buf buf = NULL;
+ uint8_t *data = NULL;
+
+ if (ctx.owner == CPU) {
+ static uint8_t static_buffer[BUFFER_SIZE];
+ data = page_align(static_buffer);
+ } else {
+ struct buffers *b = ctx.owner == SRC ? srcbuffers : dstbuffers;
+ buf = b->buf[ctx.type];
+ data = page_align(buf->data);
+ await_buf(b->gpu, buf);
+ }
+
+ struct pl_tex_transfer_params src_params = {
+ .tex = src,
+ .row_pitch = ROW_PITCH,
+ .no_import = ctx.noimport,
+ };
+
+ if (ctx.owner == SRC) {
+ src_params.buf = buf;
+ src_params.buf_offset = data - buf->data;
+ } else {
+ src_params.ptr = data;
+ }
+
+ struct pl_tex_transfer_params dst_params = {
+ .tex = dst,
+ .row_pitch = ROW_PITCH,
+ .no_import = ctx.noimport,
+ };
+
+ if (ctx.owner == DST) {
+ dst_params.buf = buf;
+ dst_params.buf_offset = data - buf->data;
+ } else {
+ dst_params.ptr = data;
+ }
+
+ if (ctx.async) {
+ src_params.callback = async_upload;
+ src_params.priv = dstbuffers;
+ dstbuffers->async = dst_params;
+ pl_tex_download(srcgpu, &src_params);
+ } else {
+ pl_tex_download(srcgpu, &src_params);
+ pl_tex_upload(dstgpu, &dst_params);
+ }
+}
+
+static inline void copy_interop(struct ctx ctx)
+{
+ const pl_gpu srcgpu = ctx.srcgpu, dstgpu = ctx.dstgpu;
+ const pl_tex src = ctx.src, dst = ctx.dst;
+ struct buffers *srcbuffers = src->params.user_data;
+ struct buffers *dstbuffers = dst->params.user_data;
+
+ struct pl_tex_transfer_params src_params = {
+ .tex = src,
+ .row_pitch = ROW_PITCH,
+ };
+
+ struct pl_tex_transfer_params dst_params = {
+ .tex = dst,
+ .row_pitch = ROW_PITCH,
+ };
+
+ if (ctx.owner == SRC) {
+ src_params.buf = srcbuffers->exported[ctx.type];
+ dst_params.buf = dstbuffers->imported[ctx.type];
+ } else {
+ src_params.buf = srcbuffers->imported[ctx.type];
+ dst_params.buf = dstbuffers->exported[ctx.type];
+ }
+
+ await_buf(srcgpu, src_params.buf);
+ if (ctx.async) {
+ src_params.callback = async_upload;
+ src_params.priv = dstbuffers;
+ dstbuffers->async = dst_params;
+ pl_tex_download(srcgpu, &src_params);
+ } else {
+ pl_tex_download(srcgpu, &src_params);
+ await_buf(srcgpu, src_params.buf); // manual cross-GPU synchronization
+ pl_tex_upload(dstgpu, &dst_params);
+ }
+}
+
+typedef void method(struct ctx ctx);
+
+static double bench(struct ctx ctx, pl_tex srcs[], pl_tex dsts[], method fun)
+{
+ const pl_gpu srcgpu = ctx.srcgpu, dstgpu = ctx.dstgpu;
+ pl_clock_t start_warmup = 0, start_test = 0;
+ uint64_t frames = 0, frames_warmup = 0;
+
+ start_warmup = pl_clock_now();
+ do {
+ const int idx = frames % NUM_TEX;
+ ctx.src = srcs[idx];
+ ctx.dst = dsts[idx];
+
+ // Generate some quasi-unique data in the source
+ float x = M_E * (frames / 100.0);
+ pl_tex_clear(srcgpu, ctx.src, (float[4]) {
+ sinf(x + 0.0) / 2.0 + 0.5,
+ sinf(x + 2.0) / 2.0 + 0.5,
+ sinf(x + 4.0) / 2.0 + 0.5,
+ 1.0,
+ });
+
+ if (fun)
+ fun(ctx);
+
+ pl_gpu_flush(srcgpu); // to rotate queues
+ pl_gpu_flush(dstgpu);
+ frames++;
+
+ if (frames % POLL_FREQ == 0) {
+ pl_clock_t now = pl_clock_now();
+ if (start_test) {
+ if (pl_clock_diff(now, start_test) > TEST_MS * 1e-3)
+ break;
+ } else if (pl_clock_diff(now, start_warmup) > WARMUP_MS * 1e-3) {
+ start_test = now;
+ frames_warmup = frames;
+ }
+ }
+ } while (true);
+
+ pl_gpu_finish(srcgpu);
+ pl_gpu_finish(dstgpu);
+
+ return pl_clock_diff(pl_clock_now(), start_test) / (frames - frames_warmup);
+}
+
+static void run_tests(pl_gpu srcgpu, pl_gpu dstgpu)
+{
+ const enum pl_fmt_caps caps = PL_FMT_CAP_HOST_READABLE;
+ pl_fmt srcfmt = pl_find_fmt(srcgpu, PL_FMT_UNORM, COMPS, DEPTH, DEPTH, caps);
+ pl_fmt dstfmt = pl_find_fmt(dstgpu, PL_FMT_UNORM, COMPS, DEPTH, DEPTH, caps);
+ if (!srcfmt || !dstfmt)
+ exit(2);
+
+ pl_tex src[NUM_TEX], dst[NUM_TEX];
+ for (int i = 0; i < NUM_TEX; i++) {
+ struct buffers *srcbuffers = alloc_buffers(srcgpu);
+ struct buffers *dstbuffers = alloc_buffers(dstgpu);
+ if (!memcmp(srcgpu->uuid, dstgpu->uuid, sizeof(srcgpu->uuid))) {
+ link_buffers(srcgpu, srcbuffers, dstbuffers);
+ link_buffers(dstgpu, dstbuffers, srcbuffers);
+ }
+
+ src[i] = pl_tex_create(srcgpu, pl_tex_params(
+ .w = WIDTH,
+ .h = HEIGHT,
+ .format = srcfmt,
+ .host_readable = true,
+ .blit_dst = true,
+ .user_data = srcbuffers,
+ ));
+
+ dst[i] = pl_tex_create(dstgpu, pl_tex_params(
+ .w = WIDTH,
+ .h = HEIGHT,
+ .format = dstfmt,
+ .host_writable = true,
+ .blit_dst = true,
+ .user_data = dstbuffers,
+ ));
+
+ if (!src[i] || !dst[i])
+ exit(2);
+ }
+
+ struct ctx ctx = {
+ .srcgpu = srcgpu,
+ .dstgpu = dstgpu,
+ };
+
+ static const char *owners[] = {
+ [CPU] = "cpu",
+ [SRC] = "src",
+ [DST] = "dst",
+ };
+
+ static const char *types[] = {
+ [RAM] = "ram",
+ [GPU] = "gpu",
+ };
+
+ double baseline = bench(ctx, src, dst, NULL);
+
+ // Test all possible generic copy methods
+ for (enum mem_owner owner = 0; owner < NUM_MEM_OWNERS; owner++) {
+ for (enum mem_type type = 0; type < NUM_MEM_TYPES; type++) {
+ for (int async = 0; async <= 1; async++) {
+ for (int noimport = 0; noimport <= 1; noimport++) {
+ // Blacklist undesirable configurations:
+ if (owner == CPU && type != RAM)
+ continue; // impossible
+ if (owner == CPU && async)
+ continue; // no synchronization on static buffer
+ if (owner == SRC && type == GPU)
+ continue; // GPU readback is orders of magnitude too slow
+ if (owner == DST && !noimport)
+ continue; // exhausts source address space
+
+ struct ctx cfg = ctx;
+ cfg.noimport = noimport;
+ cfg.owner = owner;
+ cfg.type = type;
+ cfg.async = async;
+
+ printf(" %s %s %s %s : ",
+ owners[owner], types[type],
+ noimport ? "memcpy" : " ",
+ async ? "async" : " ");
+
+ double dur = bench(cfg, src, dst, copy_ptr) - baseline;
+ printf("avg %.0f μs\t%.3f fps\n",
+ 1e6 * dur, 1.0 / dur);
+ }
+ }
+ }
+ }
+
+ // Test DMABUF interop when supported
+ for (enum mem_owner owner = 0; owner < NUM_MEM_OWNERS; owner++) {
+ for (enum mem_type type = 0; type < NUM_MEM_TYPES; type++) {
+ for (int async = 0; async <= 1; async++) {
+ struct buffers *buffers;
+ switch (owner) {
+ case SRC:
+ buffers = dst[0]->params.user_data;
+ if (!buffers->imported[type])
+ continue;
+ break;
+ case DST:
+ buffers = src[0]->params.user_data;
+ if (!buffers->imported[type])
+ continue;
+ break;
+ default: continue;
+ }
+
+ struct ctx cfg = ctx;
+ cfg.owner = owner;
+ cfg.type = type;
+
+ printf(" %s %s %s %s : ",
+ owners[owner], types[type], "dmabuf",
+ async ? "async" : " ");
+
+ double dur = bench(cfg, src, dst, copy_interop) - baseline;
+ printf("avg %.0f μs\t%.3f fps\n",
+ 1e6 * dur, 1.0 / dur);
+ }
+ }
+ }
+
+ for (int i = 0; i < NUM_TEX; i++) {
+ free_buffers(src[i]->params.user_data);
+ free_buffers(dst[i]->params.user_data);
+ pl_tex_destroy(srcgpu, &src[i]);
+ pl_tex_destroy(dstgpu, &dst[i]);
+ }
+}
+
+int main(int argc, const char *argv[])
+{
+ if (argc < 3) {
+ fprintf(stderr, "Usage: %s 'Device 1' 'Device 2'\n\n", argv[0]);
+ fprintf(stderr, "(Use `vulkaninfo` for a list of devices)\n");
+ exit(1);
+ }
+
+ pl_log log = pl_log_create(PL_API_VER, pl_log_params(
+ .log_cb = pl_log_color,
+ .log_level = PL_LOG_WARN,
+ ));
+
+ pl_vk_inst inst = pl_vk_inst_create(log, pl_vk_inst_params(
+ .debug = false,
+ ));
+
+ pl_vulkan dev1 = pl_vulkan_create(log, pl_vulkan_params(
+ .device_name = argv[1],
+ .queue_count = NUM_QUEUES,
+ .async_transfer = ASYNC_TX,
+ .async_compute = ASYNC_COMP,
+ ));
+
+ pl_vulkan dev2 = pl_vulkan_create(log, pl_vulkan_params(
+ .device_name = argv[2],
+ .queue_count = NUM_QUEUES,
+ .async_transfer = ASYNC_TX,
+ .async_compute = ASYNC_COMP,
+ ));
+
+ if (!dev1 || !dev2) {
+ fprintf(stderr, "Failed creating Vulkan device!\n");
+ exit(1);
+ }
+
+ if (ROW_PITCH % dev1->gpu->limits.align_tex_xfer_pitch) {
+ fprintf(stderr, "Warning: Row pitch %d is not a multiple of optimal "
+ "transfer pitch (%zu) for GPU '%s'\n", ROW_PITCH,
+ dev1->gpu->limits.align_tex_xfer_pitch, argv[1]);
+ }
+
+ if (ROW_PITCH % dev2->gpu->limits.align_tex_xfer_pitch) {
+ fprintf(stderr, "Warning: Row pitch %d is not a multiple of optimal "
+ "transfer pitch (%zu) for GPU '%s'\n", ROW_PITCH,
+ dev2->gpu->limits.align_tex_xfer_pitch, argv[2]);
+ }
+
+ printf("%s -> %s:\n", argv[1], argv[2]);
+ run_tests(dev1->gpu, dev2->gpu);
+ if (strcmp(argv[1], argv[2])) {
+ printf("%s -> %s:\n", argv[2], argv[1]);
+ run_tests(dev2->gpu, dev1->gpu);
+ }
+
+ pl_vulkan_destroy(&dev1);
+ pl_vulkan_destroy(&dev2);
+ pl_vk_inst_destroy(&inst);
+ pl_log_destroy(&log);
+}
diff --git a/demos/plplay.c b/demos/plplay.c
new file mode 100644
index 0000000..901653e
--- /dev/null
+++ b/demos/plplay.c
@@ -0,0 +1,766 @@
+/* Example video player based on ffmpeg. Designed to expose every libplacebo
+ * option for testing purposes. Not a serious video player, no real error
+ * handling. Simply infinitely loops its input.
+ *
+ * License: CC0 / Public Domain
+ */
+
+#include <stdatomic.h>
+
+#include <libavutil/cpu.h>
+
+#include "common.h"
+#include "window.h"
+#include "utils.h"
+#include "plplay.h"
+#include "pl_clock.h"
+#include "pl_thread.h"
+
+#ifdef HAVE_NUKLEAR
+#include "ui.h"
+#else
+struct ui;
+static void ui_destroy(struct ui **ui) {}
+static bool ui_draw(struct ui *ui, const struct pl_swapchain_frame *frame) { return true; };
+#endif
+
+#include <libplacebo/utils/libav.h>
+
+static inline void log_time(struct timing *t, double ts)
+{
+ t->sum += ts;
+ t->sum2 += ts * ts;
+ t->peak = fmax(t->peak, ts);
+ t->count++;
+}
+
+static void uninit(struct plplay *p)
+{
+ if (p->decoder_thread_created) {
+ p->exit_thread = true;
+ pl_queue_push(p->queue, NULL); // Signal EOF to wake up thread
+ pl_thread_join(p->decoder_thread);
+ }
+
+ pl_queue_destroy(&p->queue);
+ pl_renderer_destroy(&p->renderer);
+ pl_options_free(&p->opts);
+
+ for (int i = 0; i < p->shader_num; i++) {
+ pl_mpv_user_shader_destroy(&p->shader_hooks[i]);
+ free(p->shader_paths[i]);
+ }
+
+ for (int i = 0; i < MAX_FRAME_PASSES; i++)
+ pl_shader_info_deref(&p->frame_info[i].shader);
+ for (int j = 0; j < MAX_BLEND_FRAMES; j++) {
+ for (int i = 0; i < MAX_BLEND_PASSES; i++)
+ pl_shader_info_deref(&p->blend_info[j][i].shader);
+ }
+
+ free(p->shader_hooks);
+ free(p->shader_paths);
+ free(p->icc_name);
+ pl_icc_close(&p->icc);
+
+ if (p->cache) {
+ FILE *file = fopen(p->cache_file, "wb");
+ if (file) {
+ pl_cache_save_file(p->cache, file);
+ fclose(file);
+ }
+ pl_cache_destroy(&p->cache);
+ }
+
+ // Free this before destroying the window to release associated GPU buffers
+ avcodec_free_context(&p->codec);
+ avformat_free_context(p->format);
+
+ ui_destroy(&p->ui);
+ window_destroy(&p->win);
+
+ pl_log_destroy(&p->log);
+ memset(p, 0, sizeof(*p));
+}
+
+static bool open_file(struct plplay *p, const char *filename)
+{
+ static const int av_log_level[] = {
+ [PL_LOG_NONE] = AV_LOG_QUIET,
+ [PL_LOG_FATAL] = AV_LOG_PANIC,
+ [PL_LOG_ERR] = AV_LOG_ERROR,
+ [PL_LOG_WARN] = AV_LOG_WARNING,
+ [PL_LOG_INFO] = AV_LOG_INFO,
+ [PL_LOG_DEBUG] = AV_LOG_VERBOSE,
+ [PL_LOG_TRACE] = AV_LOG_DEBUG,
+ };
+
+ av_log_set_level(av_log_level[p->args.verbosity]);
+
+ printf("Opening file: '%s'\n", filename);
+ if (avformat_open_input(&p->format, filename, NULL, NULL) != 0) {
+ fprintf(stderr, "libavformat: Failed opening file!\n");
+ return false;
+ }
+
+ printf("Format: %s\n", p->format->iformat->name);
+
+ if (p->format->duration != AV_NOPTS_VALUE)
+ printf("Duration: %.3f s\n", p->format->duration / 1e6);
+
+ if (avformat_find_stream_info(p->format, NULL) < 0) {
+ fprintf(stderr, "libavformat: Failed finding stream info!\n");
+ return false;
+ }
+
+ // Find "best" video stream
+ int stream_idx =
+ av_find_best_stream(p->format, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
+
+ if (stream_idx < 0) {
+ fprintf(stderr, "plplay: File contains no video streams?\n");
+ return false;
+ }
+
+ const AVStream *stream = p->format->streams[stream_idx];
+ const AVCodecParameters *par = stream->codecpar;
+ printf("Found video track (stream %d)\n", stream_idx);
+ printf("Resolution: %d x %d\n", par->width, par->height);
+
+ if (stream->avg_frame_rate.den && stream->avg_frame_rate.num)
+ printf("FPS: %f\n", av_q2d(stream->avg_frame_rate));
+
+ if (stream->r_frame_rate.den && stream->r_frame_rate.num)
+ printf("TBR: %f\n", av_q2d(stream->r_frame_rate));
+
+ if (stream->time_base.den && stream->time_base.num)
+ printf("TBN: %f\n", av_q2d(stream->time_base));
+
+ if (par->bit_rate)
+ printf("Bitrate: %"PRIi64" kbps\n", par->bit_rate / 1000);
+
+ printf("Format: %s\n", av_get_pix_fmt_name(par->format));
+
+ p->stream = stream;
+ return true;
+}
+
+static bool init_codec(struct plplay *p)
+{
+ assert(p->stream);
+ assert(p->win->gpu);
+
+ const AVCodec *codec = avcodec_find_decoder(p->stream->codecpar->codec_id);
+ if (!codec) {
+ fprintf(stderr, "libavcodec: Failed finding matching codec\n");
+ return false;
+ }
+
+ p->codec = avcodec_alloc_context3(codec);
+ if (!p->codec) {
+ fprintf(stderr, "libavcodec: Failed allocating codec\n");
+ return false;
+ }
+
+ if (avcodec_parameters_to_context(p->codec, p->stream->codecpar) < 0) {
+ fprintf(stderr, "libavcodec: Failed copying codec parameters to codec\n");
+ return false;
+ }
+
+ printf("Codec: %s (%s)\n", codec->name, codec->long_name);
+
+ const AVCodecHWConfig *hwcfg = 0;
+ if (p->args.hwdec) {
+ for (int i = 0; (hwcfg = avcodec_get_hw_config(codec, i)); i++) {
+ if (!pl_test_pixfmt(p->win->gpu, hwcfg->pix_fmt))
+ continue;
+ if (!(hwcfg->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX))
+ continue;
+
+ int ret = av_hwdevice_ctx_create(&p->codec->hw_device_ctx,
+ hwcfg->device_type,
+ NULL, NULL, 0);
+ if (ret < 0) {
+ fprintf(stderr, "libavcodec: Failed opening HW device context, skipping\n");
+ continue;
+ }
+
+ const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(hwcfg->pix_fmt);
+ printf("Using hardware frame format: %s\n", desc->name);
+ p->codec->extra_hw_frames = 4;
+ break;
+ }
+ }
+
+ if (!hwcfg)
+ printf("Using software decoding\n");
+
+ p->codec->thread_count = FFMIN(av_cpu_count() + 1, 16);
+ p->codec->get_buffer2 = pl_get_buffer2;
+ p->codec->opaque = &p->win->gpu;
+#if LIBAVCODEC_VERSION_MAJOR < 60
+ AV_NOWARN_DEPRECATED({
+ p->codec->thread_safe_callbacks = 1;
+ });
+#endif
+#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(58, 113, 100)
+ p->codec->export_side_data |= AV_CODEC_EXPORT_DATA_FILM_GRAIN;
+#endif
+
+ if (avcodec_open2(p->codec, codec, NULL) < 0) {
+ fprintf(stderr, "libavcodec: Failed opening codec\n");
+ return false;
+ }
+
+ return true;
+}
+
+static bool map_frame(pl_gpu gpu, pl_tex *tex,
+ const struct pl_source_frame *src,
+ struct pl_frame *out_frame)
+{
+ AVFrame *frame = src->frame_data;
+ struct plplay *p = frame->opaque;
+ bool ok = pl_map_avframe_ex(gpu, out_frame, pl_avframe_params(
+ .frame = frame,
+ .tex = tex,
+ .map_dovi = !p->ignore_dovi,
+ ));
+
+ av_frame_free(&frame); // references are preserved by `out_frame`
+ if (!ok) {
+ fprintf(stderr, "Failed mapping AVFrame!\n");
+ return false;
+ }
+
+ p->stats.mapped++;
+ pl_frame_copy_stream_props(out_frame, p->stream);
+ return true;
+}
+
+static void unmap_frame(pl_gpu gpu, struct pl_frame *frame,
+ const struct pl_source_frame *src)
+{
+ pl_unmap_avframe(gpu, frame);
+}
+
+static void discard_frame(const struct pl_source_frame *src)
+{
+ AVFrame *frame = src->frame_data;
+ struct plplay *p = frame->opaque;
+ p->stats.dropped++;
+ av_frame_free(&frame);
+ printf("Dropped frame with PTS %.3f\n", src->pts);
+}
+
+static PL_THREAD_VOID decode_loop(void *arg)
+{
+ int ret;
+ struct plplay *p = arg;
+ AVPacket *packet = av_packet_alloc();
+ AVFrame *frame = av_frame_alloc();
+ if (!frame || !packet)
+ goto done;
+
+ float frame_duration = av_q2d(av_inv_q(p->stream->avg_frame_rate));
+ double first_pts = 0.0, base_pts = 0.0, last_pts = 0.0;
+ uint64_t num_frames = 0;
+
+ while (!p->exit_thread) {
+ switch ((ret = av_read_frame(p->format, packet))) {
+ case 0:
+ if (packet->stream_index != p->stream->index) {
+ // Ignore unrelated packets
+ av_packet_unref(packet);
+ continue;
+ }
+ ret = avcodec_send_packet(p->codec, packet);
+ av_packet_unref(packet);
+ break;
+ case AVERROR_EOF:
+ // Send empty input to flush decoder
+ ret = avcodec_send_packet(p->codec, NULL);
+ break;
+ default:
+ fprintf(stderr, "libavformat: Failed reading packet: %s\n",
+ av_err2str(ret));
+ goto done;
+ }
+
+ if (ret < 0) {
+ fprintf(stderr, "libavcodec: Failed sending packet to decoder: %s\n",
+ av_err2str(ret));
+ goto done;
+ }
+
+ // Decode all frames from this packet
+ while ((ret = avcodec_receive_frame(p->codec, frame)) == 0) {
+ last_pts = frame->pts * av_q2d(p->stream->time_base);
+ if (num_frames++ == 0)
+ first_pts = last_pts;
+ frame->opaque = p;
+ (void) atomic_fetch_add(&p->stats.decoded, 1);
+ pl_queue_push_block(p->queue, UINT64_MAX, &(struct pl_source_frame) {
+ .pts = last_pts - first_pts + base_pts,
+ .duration = frame_duration,
+ .map = map_frame,
+ .unmap = unmap_frame,
+ .discard = discard_frame,
+ .frame_data = frame,
+
+ // allow soft-disabling deinterlacing at the source frame level
+ .first_field = p->opts->params.deinterlace_params
+ ? pl_field_from_avframe(frame)
+ : PL_FIELD_NONE,
+ });
+ frame = av_frame_alloc();
+ }
+
+ switch (ret) {
+ case AVERROR(EAGAIN):
+ continue;
+ case AVERROR_EOF:
+ if (num_frames <= 1)
+ goto done; // still image or empty file
+ // loop infinitely
+ ret = av_seek_frame(p->format, p->stream->index, 0, AVSEEK_FLAG_BACKWARD);
+ if (ret < 0) {
+ fprintf(stderr, "libavformat: Failed seeking in stream: %s\n",
+ av_err2str(ret));
+ goto done;
+ }
+ avcodec_flush_buffers(p->codec);
+ base_pts += last_pts;
+ num_frames = 0;
+ continue;
+ default:
+ fprintf(stderr, "libavcodec: Failed decoding frame: %s\n",
+ av_err2str(ret));
+ goto done;
+ }
+ }
+
+done:
+ pl_queue_push(p->queue, NULL); // Signal EOF to flush queue
+ av_packet_free(&packet);
+ av_frame_free(&frame);
+ PL_THREAD_RETURN();
+}
+
+static void update_colorspace_hint(struct plplay *p, const struct pl_frame_mix *mix)
+{
+ const struct pl_frame *frame = NULL;
+
+ for (int i = 0; i < mix->num_frames; i++) {
+ if (mix->timestamps[i] > 0.0)
+ break;
+ frame = mix->frames[i];
+ }
+
+ if (!frame)
+ return;
+
+ struct pl_color_space hint = {0};
+ if (p->colorspace_hint)
+ hint = frame->color;
+ if (p->target_override)
+ apply_csp_overrides(p, &hint);
+ pl_swapchain_colorspace_hint(p->win->swapchain, &hint);
+}
+
+static bool render_frame(struct plplay *p, const struct pl_swapchain_frame *frame,
+ const struct pl_frame_mix *mix)
+{
+ struct pl_frame target;
+ pl_options opts = p->opts;
+ pl_frame_from_swapchain(&target, frame);
+ update_settings(p, &target);
+
+ if (p->target_override) {
+ target.repr = p->force_repr;
+ pl_color_repr_merge(&target.repr, &frame->color_repr);
+ apply_csp_overrides(p, &target.color);
+
+ // Update ICC profile parameters dynamically
+ float target_luma = 0.0f;
+ if (!p->use_icc_luma) {
+ pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
+ .metadata = PL_HDR_METADATA_HDR10, // use only static HDR nits
+ .scaling = PL_HDR_NITS,
+ .color = &target.color,
+ .out_max = &target_luma,
+ ));
+ }
+ pl_icc_update(p->log, &p->icc, NULL, pl_icc_params(
+ .max_luma = target_luma,
+ .force_bpc = p->force_bpc,
+ ));
+ target.icc = p->icc;
+ }
+
+ assert(mix->num_frames);
+ pl_rect2df crop = mix->frames[0]->crop;
+ if (p->stream->sample_aspect_ratio.num && p->target_zoom != ZOOM_RAW) {
+ float sar = av_q2d(p->stream->sample_aspect_ratio);
+ pl_rect2df_stretch(&crop, fmaxf(1.0f, sar), fmaxf(1.0f, 1.0 / sar));
+ }
+
+ // Apply target rotation and un-rotate crop relative to target
+ target.rotation = p->target_rot;
+ pl_rect2df_rotate(&crop, mix->frames[0]->rotation - target.rotation);
+
+ switch (p->target_zoom) {
+ case ZOOM_PAD:
+ pl_rect2df_aspect_copy(&target.crop, &crop, 0.0);
+ break;
+ case ZOOM_CROP:
+ pl_rect2df_aspect_copy(&target.crop, &crop, 1.0);
+ break;
+ case ZOOM_STRETCH:
+ break; // target.crop already covers full image
+ case ZOOM_FIT:
+ pl_rect2df_aspect_fit(&target.crop, &crop, 0.0);
+ break;
+ case ZOOM_RAW: ;
+ // Ensure pixels are exactly aligned, to avoid fractional scaling
+ int w = roundf(fabsf(pl_rect_w(crop)));
+ int h = roundf(fabsf(pl_rect_h(crop)));
+ target.crop.x0 = roundf((pl_rect_w(target.crop) - w) / 2.0f);
+ target.crop.y0 = roundf((pl_rect_h(target.crop) - h) / 2.0f);
+ target.crop.x1 = target.crop.x0 + w;
+ target.crop.y1 = target.crop.y0 + h;
+ break;
+ case ZOOM_400:
+ case ZOOM_200:
+ case ZOOM_100:
+ case ZOOM_50:
+ case ZOOM_25: ;
+ const float z = powf(2.0f, (int) ZOOM_100 - p->target_zoom);
+ const float sx = z * fabsf(pl_rect_w(crop)) / pl_rect_w(target.crop);
+ const float sy = z * fabsf(pl_rect_h(crop)) / pl_rect_h(target.crop);
+ pl_rect2df_stretch(&target.crop, sx, sy);
+ break;
+ }
+
+ struct pl_color_map_params *cpars = &opts->color_map_params;
+ if (cpars->visualize_lut) {
+ cpars->visualize_rect = (pl_rect2df) {0, 0, 1, 1};
+ float tar = pl_rect2df_aspect(&target.crop);
+ pl_rect2df_aspect_set(&cpars->visualize_rect, 1.0f / tar, 0.0f);
+ }
+
+ pl_clock_t ts_pre = pl_clock_now();
+ if (!pl_render_image_mix(p->renderer, mix, &target, &opts->params))
+ return false;
+ pl_clock_t ts_rendered = pl_clock_now();
+ if (!ui_draw(p->ui, frame))
+ return false;
+ pl_clock_t ts_ui_drawn = pl_clock_now();
+
+ log_time(&p->stats.render, pl_clock_diff(ts_rendered, ts_pre));
+ log_time(&p->stats.draw_ui, pl_clock_diff(ts_ui_drawn, ts_rendered));
+
+ p->stats.rendered++;
+ return true;
+}
+
+static bool render_loop(struct plplay *p)
+{
+ pl_options opts = p->opts;
+
+ struct pl_queue_params qparams = {
+ .interpolation_threshold = 0.01,
+ .timeout = UINT64_MAX,
+ };
+
+ // Initialize the frame queue, blocking indefinitely until done
+ struct pl_frame_mix mix;
+ switch (pl_queue_update(p->queue, &mix, &qparams)) {
+ case PL_QUEUE_OK: break;
+ case PL_QUEUE_EOF: return true;
+ case PL_QUEUE_ERR: goto error;
+ default: abort();
+ }
+
+ struct pl_swapchain_frame frame;
+ update_colorspace_hint(p, &mix);
+ if (!pl_swapchain_start_frame(p->win->swapchain, &frame))
+ goto error;
+
+ // Disable background transparency by default if the swapchain does not
+ // appear to support alpha transaprency
+ if (frame.color_repr.alpha == PL_ALPHA_UNKNOWN)
+ opts->params.background_transparency = 0.0;
+
+ if (!render_frame(p, &frame, &mix))
+ goto error;
+ if (!pl_swapchain_submit_frame(p->win->swapchain))
+ goto error;
+
+ // Wait until rendering is complete. Do this before measuring the time
+ // start, to ensure we don't count initialization overhead as part of the
+ // first vsync.
+ pl_gpu_finish(p->win->gpu);
+ p->stats.render = p->stats.draw_ui = (struct timing) {0};
+
+ pl_clock_t ts_start = 0, ts_prev = 0;
+ pl_swapchain_swap_buffers(p->win->swapchain);
+ window_poll(p->win, false);
+
+ double pts_target = 0.0, prev_pts = 0.0;
+
+ while (!p->win->window_lost) {
+ if (window_get_key(p->win, KEY_ESC))
+ break;
+
+ if (p->toggle_fullscreen)
+ window_toggle_fullscreen(p->win, !window_is_fullscreen(p->win));
+
+ update_colorspace_hint(p, &mix);
+ pl_clock_t ts_acquire = pl_clock_now();
+ if (!pl_swapchain_start_frame(p->win->swapchain, &frame)) {
+ // Window stuck/invisible? Block for events and try again.
+ window_poll(p->win, true);
+ continue;
+ }
+
+ pl_clock_t ts_pre_update = pl_clock_now();
+ log_time(&p->stats.acquire, pl_clock_diff(ts_pre_update, ts_acquire));
+ if (!ts_start)
+ ts_start = ts_pre_update;
+
+ qparams.timeout = 0; // non-blocking update
+ qparams.radius = pl_frame_mix_radius(&p->opts->params);
+ qparams.pts = fmax(pts_target, pl_clock_diff(ts_pre_update, ts_start));
+ p->stats.current_pts = qparams.pts;
+ if (qparams.pts != prev_pts)
+ log_time(&p->stats.pts_interval, qparams.pts - prev_pts);
+ prev_pts = qparams.pts;
+
+retry:
+ switch (pl_queue_update(p->queue, &mix, &qparams)) {
+ case PL_QUEUE_ERR: goto error;
+ case PL_QUEUE_EOF:
+ printf("End of file reached\n");
+ return true;
+ case PL_QUEUE_OK:
+ break;
+ case PL_QUEUE_MORE:
+ qparams.timeout = UINT64_MAX; // retry in blocking mode
+ goto retry;
+ }
+
+ pl_clock_t ts_post_update = pl_clock_now();
+ log_time(&p->stats.update, pl_clock_diff(ts_post_update, ts_pre_update));
+
+ if (qparams.timeout) {
+ double stuck_ms = 1e3 * pl_clock_diff(ts_post_update, ts_pre_update);
+ fprintf(stderr, "Stalled for %.4f ms due to frame queue underrun!\n", stuck_ms);
+ ts_start += ts_post_update - ts_pre_update; // subtract time spent waiting
+ p->stats.stalled++;
+ p->stats.stalled_ms += stuck_ms;
+ }
+
+ if (!render_frame(p, &frame, &mix))
+ goto error;
+
+ if (pts_target) {
+ pl_gpu_flush(p->win->gpu);
+ pl_clock_t ts_wait = pl_clock_now();
+ double pts_now = pl_clock_diff(ts_wait, ts_start);
+ if (pts_target >= pts_now) {
+ log_time(&p->stats.sleep, pts_target - pts_now);
+ pl_thread_sleep(pts_target - pts_now);
+ } else {
+ double missed_ms = 1e3 * (pts_now - pts_target);
+ fprintf(stderr, "Missed PTS target %.3f (%.3f ms in the past)\n",
+ pts_target, missed_ms);
+ p->stats.missed++;
+ p->stats.missed_ms += missed_ms;
+ }
+
+ pts_target = 0.0;
+ }
+
+ pl_clock_t ts_pre_submit = pl_clock_now();
+ if (!pl_swapchain_submit_frame(p->win->swapchain)) {
+ fprintf(stderr, "libplacebo: failed presenting frame!\n");
+ goto error;
+ }
+ pl_clock_t ts_post_submit = pl_clock_now();
+ log_time(&p->stats.submit, pl_clock_diff(ts_post_submit, ts_pre_submit));
+
+ if (ts_prev)
+ log_time(&p->stats.vsync_interval, pl_clock_diff(ts_post_submit, ts_prev));
+ ts_prev = ts_post_submit;
+
+ pl_swapchain_swap_buffers(p->win->swapchain);
+ pl_clock_t ts_post_swap = pl_clock_now();
+ log_time(&p->stats.swap, pl_clock_diff(ts_post_swap, ts_post_submit));
+
+ window_poll(p->win, false);
+
+ // In content-timed mode (frame mixing disabled), delay rendering
+ // until the next frame should become visible
+ if (!opts->params.frame_mixer) {
+ struct pl_source_frame next;
+ for (int i = 0;; i++) {
+ if (!pl_queue_peek(p->queue, i, &next))
+ break;
+ if (next.pts > qparams.pts) {
+ pts_target = next.pts;
+ break;
+ }
+ }
+ }
+
+ if (p->fps_override)
+ pts_target = fmax(pts_target, qparams.pts + 1.0 / p->fps);
+ }
+
+ return true;
+
+error:
+ fprintf(stderr, "Render loop failed, exiting early...\n");
+ return false;
+}
+
+static void info_callback(void *priv, const struct pl_render_info *info)
+{
+ struct plplay *p = priv;
+ switch (info->stage) {
+ case PL_RENDER_STAGE_FRAME:
+ if (info->index >= MAX_FRAME_PASSES)
+ return;
+ p->num_frame_passes = info->index + 1;
+ pl_dispatch_info_move(&p->frame_info[info->index], info->pass);
+ return;
+
+ case PL_RENDER_STAGE_BLEND:
+ if (info->index >= MAX_BLEND_PASSES || info->count >= MAX_BLEND_FRAMES)
+ return;
+ p->num_blend_passes[info->count] = info->index + 1;
+ pl_dispatch_info_move(&p->blend_info[info->count][info->index], info->pass);
+ return;
+
+ case PL_RENDER_STAGE_COUNT:
+ break;
+ }
+
+ abort();
+}
+
+static struct plplay state;
+
+int main(int argc, char *argv[])
+{
+ state = (struct plplay) {
+ .target_override = true,
+ .use_icc_luma = true,
+ .fps = 60.0,
+ .args = {
+ .preset = &pl_render_default_params,
+ .verbosity = PL_LOG_INFO,
+ },
+ };
+
+ if (!parse_args(&state.args, argc, argv))
+ return -1;
+
+ state.log = pl_log_create(PL_API_VER, pl_log_params(
+ .log_cb = pl_log_color,
+ .log_level = state.args.verbosity,
+ ));
+
+ pl_options opts = state.opts = pl_options_alloc(state.log);
+ pl_options_reset(opts, state.args.preset);
+
+ // Enable this by default to save one click
+ opts->params.cone_params = &opts->cone_params;
+
+ // Enable dynamic parameters by default, due to plplay's heavy reliance on
+ // GUI controls for dynamically adjusting render parameters.
+ opts->params.dynamic_constants = true;
+
+ // Hook up our pass info callback
+ opts->params.info_callback = info_callback;
+ opts->params.info_priv = &state;
+
+ struct plplay *p = &state;
+ if (!open_file(p, state.args.filename))
+ goto error;
+
+ const AVCodecParameters *par = p->stream->codecpar;
+ const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(par->format);
+ if (!desc)
+ goto error;
+
+ struct window_params params = {
+ .title = "plplay",
+ .width = par->width,
+ .height = par->height,
+ .forced_impl = state.args.window_impl,
+ };
+
+ if (desc->flags & AV_PIX_FMT_FLAG_ALPHA) {
+ params.alpha = true;
+ opts->params.background_transparency = 1.0;
+ }
+
+ p->win = window_create(p->log, &params);
+ if (!p->win)
+ goto error;
+
+ // Test the AVPixelFormat against the GPU capabilities
+ if (!pl_test_pixfmt(p->win->gpu, par->format)) {
+ fprintf(stderr, "Unsupported AVPixelFormat: %s\n", desc->name);
+ goto error;
+ }
+
+#ifdef HAVE_NUKLEAR
+ p->ui = ui_create(p->win->gpu);
+ if (!p->ui)
+ goto error;
+#endif
+
+ if (!init_codec(p))
+ goto error;
+
+ const char *cache_dir = get_cache_dir(&(char[512]) {0});
+ if (cache_dir) {
+ int ret = snprintf(p->cache_file, sizeof(p->cache_file), "%s/plplay.cache", cache_dir);
+ if (ret > 0 && ret < sizeof(p->cache_file)) {
+ p->cache = pl_cache_create(pl_cache_params(
+ .log = p->log,
+ .max_total_size = 50 << 20, // 50 MB
+ ));
+ pl_gpu_set_cache(p->win->gpu, p->cache);
+ FILE *file = fopen(p->cache_file, "rb");
+ if (file) {
+ pl_cache_load_file(p->cache, file);
+ fclose(file);
+ }
+ }
+ }
+
+ p->queue = pl_queue_create(p->win->gpu);
+ int ret = pl_thread_create(&p->decoder_thread, decode_loop, p);
+ if (ret != 0) {
+ fprintf(stderr, "Failed creating decode thread: %s\n", strerror(errno));
+ goto error;
+ }
+
+ p->decoder_thread_created = true;
+
+ p->renderer = pl_renderer_create(p->log, p->win->gpu);
+ if (!render_loop(p))
+ goto error;
+
+ printf("Exiting...\n");
+ uninit(p);
+ return 0;
+
+error:
+ uninit(p);
+ return 1;
+}
diff --git a/demos/plplay.h b/demos/plplay.h
new file mode 100644
index 0000000..2036562
--- /dev/null
+++ b/demos/plplay.h
@@ -0,0 +1,138 @@
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+
+#include <libplacebo/options.h>
+#include <libplacebo/utils/frame_queue.h>
+
+#include "common.h"
+#include "pl_thread.h"
+
+#define MAX_FRAME_PASSES 256
+#define MAX_BLEND_PASSES 8
+#define MAX_BLEND_FRAMES 8
+
+enum {
+ ZOOM_PAD = 0,
+ ZOOM_CROP,
+ ZOOM_STRETCH,
+ ZOOM_FIT,
+ ZOOM_RAW,
+ ZOOM_400,
+ ZOOM_200,
+ ZOOM_100,
+ ZOOM_50,
+ ZOOM_25,
+ ZOOM_COUNT,
+};
+
+struct plplay_args {
+ const struct pl_render_params *preset;
+ enum pl_log_level verbosity;
+ const char *window_impl;
+ const char *filename;
+ bool hwdec;
+};
+
+bool parse_args(struct plplay_args *args, int argc, char *argv[]);
+
+struct plplay {
+ struct plplay_args args;
+ struct window *win;
+ struct ui *ui;
+ char cache_file[512];
+
+ // libplacebo
+ pl_log log;
+ pl_renderer renderer;
+ pl_queue queue;
+ pl_cache cache;
+
+ // libav*
+ AVFormatContext *format;
+ AVCodecContext *codec;
+ const AVStream *stream; // points to first video stream of `format`
+ pl_thread decoder_thread;
+ bool decoder_thread_created;
+ bool exit_thread;
+
+ // settings / ui state
+ pl_options opts;
+ pl_rotation target_rot;
+ int target_zoom;
+ bool colorspace_hint;
+ bool colorspace_hint_dynamic;
+ bool ignore_dovi;
+ bool toggle_fullscreen;
+ bool advanced_scalers;
+
+ bool target_override; // if false, fields below are ignored
+ struct pl_color_repr force_repr;
+ enum pl_color_primaries force_prim;
+ enum pl_color_transfer force_trc;
+ struct pl_hdr_metadata force_hdr;
+ bool force_hdr_enable;
+ bool fps_override;
+ float fps;
+
+ // ICC profile
+ pl_icc_object icc;
+ char *icc_name;
+ bool use_icc_luma;
+ bool force_bpc;
+
+ // custom shaders
+ const struct pl_hook **shader_hooks;
+ char **shader_paths;
+ size_t shader_num;
+ size_t shader_size;
+
+ // pass metadata
+ struct pl_dispatch_info blend_info[MAX_BLEND_FRAMES][MAX_BLEND_PASSES];
+ struct pl_dispatch_info frame_info[MAX_FRAME_PASSES];
+ int num_frame_passes;
+ int num_blend_passes[MAX_BLEND_FRAMES];
+
+ // playback statistics
+ struct {
+ _Atomic uint32_t decoded;
+ uint32_t rendered;
+ uint32_t mapped;
+ uint32_t dropped;
+ uint32_t missed;
+ uint32_t stalled;
+ double missed_ms;
+ double stalled_ms;
+ double current_pts;
+
+ struct timing {
+ double sum, sum2, peak;
+ uint64_t count;
+ } acquire, update, render, draw_ui, sleep, submit, swap,
+ vsync_interval, pts_interval;
+ } stats;
+};
+
+void update_settings(struct plplay *p, const struct pl_frame *target);
+
+static inline void apply_csp_overrides(struct plplay *p, struct pl_color_space *csp)
+{
+ if (p->force_prim) {
+ csp->primaries = p->force_prim;
+ csp->hdr.prim = *pl_raw_primaries_get(csp->primaries);
+ }
+ if (p->force_trc)
+ csp->transfer = p->force_trc;
+ if (p->force_hdr_enable) {
+ struct pl_hdr_metadata fix = p->force_hdr;
+ fix.prim = csp->hdr.prim;
+ csp->hdr = fix;
+ } else if (p->colorspace_hint_dynamic) {
+ pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
+ .color = csp,
+ .metadata = PL_HDR_METADATA_ANY,
+ .scaling = PL_HDR_NITS,
+ .out_min = &csp->hdr.min_luma,
+ .out_max = &csp->hdr.max_luma,
+ ));
+ }
+}
diff --git a/demos/screenshots/plplay1.png b/demos/screenshots/plplay1.png
new file mode 100644
index 0000000..ce84d89
--- /dev/null
+++ b/demos/screenshots/plplay1.png
Binary files differ
diff --git a/demos/screenshots/plplay2.png b/demos/screenshots/plplay2.png
new file mode 100644
index 0000000..ae88051
--- /dev/null
+++ b/demos/screenshots/plplay2.png
Binary files differ
diff --git a/demos/screenshots/plplay3.png b/demos/screenshots/plplay3.png
new file mode 100644
index 0000000..9ec4126
--- /dev/null
+++ b/demos/screenshots/plplay3.png
Binary files differ
diff --git a/demos/screenshots/plplay4.png b/demos/screenshots/plplay4.png
new file mode 100644
index 0000000..873be13
--- /dev/null
+++ b/demos/screenshots/plplay4.png
Binary files differ
diff --git a/demos/screenshots/plplay5.png b/demos/screenshots/plplay5.png
new file mode 100644
index 0000000..c23d609
--- /dev/null
+++ b/demos/screenshots/plplay5.png
Binary files differ
diff --git a/demos/screenshots/plplay6.png b/demos/screenshots/plplay6.png
new file mode 100644
index 0000000..15ea8fc
--- /dev/null
+++ b/demos/screenshots/plplay6.png
Binary files differ
diff --git a/demos/sdlimage.c b/demos/sdlimage.c
new file mode 100644
index 0000000..87e6d03
--- /dev/null
+++ b/demos/sdlimage.c
@@ -0,0 +1,281 @@
+/* Simple image viewer that opens an image using SDL2_image and presents it
+ * to the screen.
+ *
+ * License: CC0 / Public Domain
+ */
+
+#include <SDL_image.h>
+
+#include "common.h"
+#include "window.h"
+
+#include <libplacebo/renderer.h>
+#include <libplacebo/shaders/lut.h>
+#include <libplacebo/utils/upload.h>
+
+// Static configuration, done in the file to keep things simple
+static const char *icc_profile = ""; // path to ICC profile
+static const char *lut_file = ""; // path to .cube lut
+
+// Program state
+static pl_log logger;
+static struct window *win;
+
+// For rendering
+static pl_tex img_tex;
+static pl_tex osd_tex;
+static struct pl_plane img_plane;
+static struct pl_plane osd_plane;
+static pl_renderer renderer;
+static struct pl_custom_lut *lut;
+
+struct file
+{
+ void *data;
+ size_t size;
+};
+
+static struct file icc_file;
+
+static bool open_file(const char *path, struct file *out)
+{
+ if (!path || !path[0]) {
+ *out = (struct file) {0};
+ return true;
+ }
+
+ FILE *fp = NULL;
+ bool success = false;
+
+ fp = fopen(path, "rb");
+ if (!fp)
+ goto done;
+
+ if (fseeko(fp, 0, SEEK_END))
+ goto done;
+ off_t size = ftello(fp);
+ if (size < 0)
+ goto done;
+ if (fseeko(fp, 0, SEEK_SET))
+ goto done;
+
+ void *data = malloc(size);
+ if (!fread(data, size, 1, fp))
+ goto done;
+
+ *out = (struct file) {
+ .data = data,
+ .size = size,
+ };
+
+ success = true;
+done:
+ if (fp)
+ fclose(fp);
+ return success;
+}
+
+static void close_file(struct file *file)
+{
+ if (!file->data)
+ return;
+
+ free(file->data);
+ *file = (struct file) {0};
+}
+
+SDL_NORETURN static void uninit(int ret)
+{
+ pl_renderer_destroy(&renderer);
+ pl_tex_destroy(win->gpu, &img_tex);
+ pl_tex_destroy(win->gpu, &osd_tex);
+ close_file(&icc_file);
+ pl_lut_free(&lut);
+
+ window_destroy(&win);
+ pl_log_destroy(&logger);
+ exit(ret);
+}
+
+static bool upload_plane(const SDL_Surface *img, pl_tex *tex,
+ struct pl_plane *plane)
+{
+ if (!img)
+ return false;
+
+ SDL_Surface *fixed = NULL;
+ const SDL_PixelFormat *fmt = img->format;
+ if (SDL_ISPIXELFORMAT_INDEXED(fmt->format)) {
+ // libplacebo doesn't handle indexed formats yet
+ fixed = SDL_CreateRGBSurfaceWithFormat(0, img->w, img->h, 32,
+ SDL_PIXELFORMAT_ABGR8888);
+ SDL_BlitSurface((SDL_Surface *) img, NULL, fixed, NULL);
+ img = fixed;
+ fmt = img->format;
+ }
+
+ struct pl_plane_data data = {
+ .type = PL_FMT_UNORM,
+ .width = img->w,
+ .height = img->h,
+ .pixel_stride = fmt->BytesPerPixel,
+ .row_stride = img->pitch,
+ .pixels = img->pixels,
+ };
+
+ uint64_t masks[4] = { fmt->Rmask, fmt->Gmask, fmt->Bmask, fmt->Amask };
+ pl_plane_data_from_mask(&data, masks);
+
+ bool ok = pl_upload_plane(win->gpu, plane, tex, &data);
+ SDL_FreeSurface(fixed);
+ return ok;
+}
+
+static bool render_frame(const struct pl_swapchain_frame *frame)
+{
+ pl_tex img = img_plane.texture;
+ struct pl_frame image = {
+ .num_planes = 1,
+ .planes = { img_plane },
+ .repr = pl_color_repr_unknown,
+ .color = pl_color_space_unknown,
+ .crop = {0, 0, img->params.w, img->params.h},
+ };
+
+ // This seems to be the case for SDL2_image
+ image.repr.alpha = PL_ALPHA_INDEPENDENT;
+
+ struct pl_frame target;
+ pl_frame_from_swapchain(&target, frame);
+ target.profile = (struct pl_icc_profile) {
+ .data = icc_file.data,
+ .len = icc_file.size,
+ };
+
+ image.rotation = PL_ROTATION_0; // for testing
+ pl_rect2df_aspect_copy_rot(&target.crop, &image.crop, 0.0, image.rotation);
+
+ struct pl_overlay osd;
+ struct pl_overlay_part osd_part;
+ if (osd_tex) {
+ osd_part = (struct pl_overlay_part) {
+ .src = { 0, 0, osd_tex->params.w, osd_tex->params.h },
+ .dst = { 0, 0, osd_tex->params.w, osd_tex->params.h },
+ };
+ osd = (struct pl_overlay) {
+ .tex = osd_tex,
+ .mode = PL_OVERLAY_NORMAL,
+ .repr = image.repr,
+ .color = image.color,
+ .coords = PL_OVERLAY_COORDS_DST_FRAME,
+ .parts = &osd_part,
+ .num_parts = 1,
+ };
+ target.overlays = &osd;
+ target.num_overlays = 1;
+ }
+
+ // Use the heaviest preset purely for demonstration/testing purposes
+ struct pl_render_params params = pl_render_high_quality_params;
+ params.lut = lut;
+
+ return pl_render_image(renderer, &image, &target, &params);
+}
+
+int main(int argc, char **argv)
+{
+ if (argc < 2 || argc > 3) {
+ fprintf(stderr, "Usage: %s <image> [<overlay>]\n", argv[0]);
+ return 255;
+ }
+
+ const char *file = argv[1];
+ const char *overlay = argc > 2 ? argv[2] : NULL;
+ logger = pl_log_create(PL_API_VER, pl_log_params(
+ .log_cb = pl_log_color,
+ .log_level = PL_LOG_INFO,
+ ));
+
+
+ // Load image, do this first so we can use it for the window size
+ SDL_Surface *img = IMG_Load(file);
+ if (!img) {
+ fprintf(stderr, "Failed loading '%s': %s\n", file, SDL_GetError());
+ uninit(1);
+ }
+
+ // Create window
+ unsigned int start = SDL_GetTicks();
+ win = window_create(logger, &(struct window_params) {
+ .title = "SDL2_image demo",
+ .width = img->w,
+ .height = img->h,
+ });
+ if (!win)
+ uninit(1);
+
+ // Initialize rendering state
+ if (!upload_plane(img, &img_tex, &img_plane)) {
+ fprintf(stderr, "Failed uploading image plane!\n");
+ uninit(2);
+ }
+ SDL_FreeSurface(img);
+
+ if (overlay) {
+ SDL_Surface *osd = IMG_Load(overlay);
+ if (!upload_plane(osd, &osd_tex, &osd_plane))
+ fprintf(stderr, "Failed uploading OSD plane.. continuing anyway\n");
+ SDL_FreeSurface(osd);
+ }
+
+ if (!open_file(icc_profile, &icc_file))
+ fprintf(stderr, "Failed opening ICC profile.. continuing anyway\n");
+
+ struct file lutf;
+ if (open_file(lut_file, &lutf) && lutf.size) {
+ if (!(lut = pl_lut_parse_cube(logger, lutf.data, lutf.size)))
+ fprintf(stderr, "Failed parsing LUT.. continuing anyway\n");
+ close_file(&lutf);
+ }
+
+ renderer = pl_renderer_create(logger, win->gpu);
+
+ unsigned int last = SDL_GetTicks(), frames = 0;
+ printf("Took %u ms for initialization\n", last - start);
+
+ // Render loop
+ while (!win->window_lost) {
+ struct pl_swapchain_frame frame;
+ bool ok = pl_swapchain_start_frame(win->swapchain, &frame);
+ if (!ok) {
+ window_poll(win, true);
+ continue;
+ }
+
+ if (!render_frame(&frame)) {
+ fprintf(stderr, "libplacebo: Failed rendering frame!\n");
+ uninit(3);
+ }
+
+ ok = pl_swapchain_submit_frame(win->swapchain);
+ if (!ok) {
+ fprintf(stderr, "libplacebo: Failed submitting frame!\n");
+ uninit(3);
+ }
+
+ pl_swapchain_swap_buffers(win->swapchain);
+ frames++;
+
+ unsigned int now = SDL_GetTicks();
+ if (now - last > 5000) {
+ printf("%u frames in %u ms = %f FPS\n", frames, now - last,
+ 1000.0f * frames / (now - last));
+ last = now;
+ frames = 0;
+ }
+
+ window_poll(win, false);
+ }
+
+ uninit(0);
+}
diff --git a/demos/settings.c b/demos/settings.c
new file mode 100644
index 0000000..e69f280
--- /dev/null
+++ b/demos/settings.c
@@ -0,0 +1,1238 @@
+#include <stdatomic.h>
+#include <getopt.h>
+
+#include <libavutil/file.h>
+
+#include "plplay.h"
+
+#ifdef PL_HAVE_WIN32
+#include <shlwapi.h>
+#define PL_BASENAME PathFindFileNameA
+#define strdup _strdup
+#else
+#include <libgen.h>
+#define PL_BASENAME basename
+#endif
+
+#ifdef HAVE_NUKLEAR
+#include "ui.h"
+
+bool parse_args(struct plplay_args *args, int argc, char *argv[])
+{
+ static struct option long_options[] = {
+ {"verbose", no_argument, NULL, 'v'},
+ {"quiet", no_argument, NULL, 'q'},
+ {"preset", required_argument, NULL, 'p'},
+ {"hwdec", no_argument, NULL, 'H'},
+ {"window", required_argument, NULL, 'w'},
+ {0}
+ };
+
+ int option;
+ while ((option = getopt_long(argc, argv, "vqp:Hw:", long_options, NULL)) != -1) {
+ switch (option) {
+ case 'v':
+ if (args->verbosity < PL_LOG_TRACE)
+ args->verbosity++;
+ break;
+ case 'q':
+ if (args->verbosity > PL_LOG_NONE)
+ args->verbosity--;
+ break;
+ case 'p':
+ if (!strcmp(optarg, "default")) {
+ args->preset = &pl_render_default_params;
+ } else if (!strcmp(optarg, "fast")) {
+ args->preset = &pl_render_fast_params;
+ } else if (!strcmp(optarg, "highquality") || !strcmp(optarg, "hq")) {
+ args->preset = &pl_render_high_quality_params;
+ } else {
+ fprintf(stderr, "Invalid value for -p/--preset: '%s'\n", optarg);
+ goto error;
+ }
+ break;
+ case 'H':
+ args->hwdec = true;
+ break;
+ case 'w':
+ args->window_impl = optarg;
+ break;
+ case '?':
+ default:
+ goto error;
+ }
+ }
+
+ // Check for the required filename argument
+ if (optind < argc) {
+ args->filename = argv[optind++];
+ } else {
+ fprintf(stderr, "Missing filename!\n");
+ goto error;
+ }
+
+ if (optind != argc) {
+ fprintf(stderr, "Superfluous argument: %s\n", argv[optind]);
+ goto error;
+ }
+
+ return true;
+
+error:
+ fprintf(stderr, "Usage: %s [-v/--verbose] [-q/--quiet] [-p/--preset <default|fast|hq|highquality>] [--hwdec] [-w/--window <api>] <filename>\n", argv[0]);
+ fprintf(stderr, "Options:\n");
+ fprintf(stderr, " -v, --verbose Increase verbosity\n");
+ fprintf(stderr, " -q, --quiet Decrease verbosity\n");
+ fprintf(stderr, " -p, --preset Set the rendering preset (default|fast|hq|highquality)\n");
+ fprintf(stderr, " -H, --hwdec Enable hardware decoding\n");
+ fprintf(stderr, " -w, --window Specify the windowing API\n");
+ return false;
+}
+
+static void add_hook(struct plplay *p, const struct pl_hook *hook, const char *path)
+{
+ if (!hook)
+ return;
+
+ if (p->shader_num == p->shader_size) {
+ // Grow array if needed
+ size_t new_size = p->shader_size ? p->shader_size * 2 : 16;
+ void *new_hooks = realloc(p->shader_hooks, new_size * sizeof(void *));
+ if (!new_hooks)
+ goto error;
+ p->shader_hooks = new_hooks;
+ char **new_paths = realloc(p->shader_paths, new_size * sizeof(char *));
+ if (!new_paths)
+ goto error;
+ p->shader_paths = new_paths;
+ p->shader_size = new_size;
+ }
+
+ // strip leading path
+ while (true) {
+ const char *fname = strchr(path, '/');
+ if (!fname)
+ break;
+ path = fname + 1;
+ }
+
+ char *path_copy = strdup(path);
+ if (!path_copy)
+ goto error;
+
+ p->shader_hooks[p->shader_num] = hook;
+ p->shader_paths[p->shader_num] = path_copy;
+ p->shader_num++;
+ return;
+
+error:
+ pl_mpv_user_shader_destroy(&hook);
+}
+
+static void auto_property_int(struct nk_context *nk, int auto_val, int min, int *val,
+ int max, int step, float inc_per_pixel)
+{
+ int value = *val;
+ if (!value)
+ value = auto_val;
+
+ // Auto label will be delayed 1 frame
+ nk_property_int(nk, *val ? "" : "Auto", min, &value, max, step, inc_per_pixel);
+
+ if (*val || value != auto_val)
+ *val = value;
+}
+
+static void draw_shader_pass(struct nk_context *nk,
+ const struct pl_dispatch_info *info)
+{
+ pl_shader_info shader = info->shader;
+
+ char label[128];
+ int count = snprintf(label, sizeof(label), "%.3f/%.3f/%.3f ms: %s",
+ info->last / 1e6,
+ info->average / 1e6,
+ info->peak / 1e6,
+ shader->description);
+
+ if (count >= sizeof(label)) {
+ label[sizeof(label) - 4] = '.';
+ label[sizeof(label) - 3] = '.';
+ label[sizeof(label) - 2] = '.';
+ }
+
+ int id = (unsigned int) (uintptr_t) info; // pointer into `struct plplay`
+ if (nk_tree_push_id(nk, NK_TREE_NODE, label, NK_MINIMIZED, id)) {
+ nk_layout_row_dynamic(nk, 32, 1);
+ if (nk_chart_begin(nk, NK_CHART_LINES,
+ info->num_samples,
+ 0.0f, info->peak))
+ {
+ for (int k = 0; k < info->num_samples; k++)
+ nk_chart_push(nk, info->samples[k]);
+ nk_chart_end(nk);
+ }
+
+ nk_layout_row_dynamic(nk, 24, 1);
+ for (int n = 0; n < shader->num_steps; n++)
+ nk_labelf(nk, NK_TEXT_LEFT, "%d. %s", n + 1, shader->steps[n]);
+ nk_tree_pop(nk);
+ }
+}
+
+static void draw_timing(struct nk_context *nk, const char *label,
+ const struct timing *t)
+{
+ const double avg = t->count ? t->sum / t->count : 0.0;
+ const double stddev = t->count ? sqrt(t->sum2 / t->count - avg * avg) : 0.0;
+ nk_label(nk, label, NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%.4f ± %.4f ms (%.3f ms)",
+ avg * 1e3, stddev * 1e3, t->peak * 1e3);
+}
+
+static void draw_opt_data(void *priv, pl_opt_data data)
+{
+ struct nk_context *nk = priv;
+ pl_opt opt = data->opt;
+ if (opt->type == PL_OPT_FLOAT) {
+ // Print floats less verbosely than the libplacebo built-in printf
+ nk_labelf(nk, NK_TEXT_LEFT, "%s = %f", opt->key, *(const float *) data->value);
+ } else {
+ nk_labelf(nk, NK_TEXT_LEFT, "%s = %s", opt->key, data->text);
+ }
+}
+
+static void draw_cache_line(void *priv, pl_cache_obj obj)
+{
+ struct nk_context *nk = priv;
+ nk_labelf(nk, NK_TEXT_LEFT, " - 0x%016"PRIx64": %zu bytes", obj.key, obj.size);
+}
+
+void update_settings(struct plplay *p, const struct pl_frame *target)
+{
+ struct nk_context *nk = ui_get_context(p->ui);
+ enum nk_panel_flags win_flags = NK_WINDOW_BORDER | NK_WINDOW_MOVABLE |
+ NK_WINDOW_SCALABLE | NK_WINDOW_MINIMIZABLE |
+ NK_WINDOW_TITLE;
+
+ ui_update_input(p->ui, p->win);
+ const char *dropped_file = window_get_file(p->win);
+
+ pl_options opts = p->opts;
+ struct pl_render_params *par = &opts->params;
+
+ if (nk_begin(nk, "Settings", nk_rect(100, 100, 600, 600), win_flags)) {
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Window settings", NK_MAXIMIZED)) {
+ nk_layout_row_dynamic(nk, 24, 2);
+
+ bool fullscreen = window_is_fullscreen(p->win);
+ p->toggle_fullscreen = nk_checkbox_label(nk, "Fullscreen", &fullscreen);
+ nk_property_float(nk, "Corner rounding", 0.0, &par->corner_rounding, 1.0, 0.1, 0.01);
+
+ struct nk_colorf bg = {
+ par->background_color[0],
+ par->background_color[1],
+ par->background_color[2],
+ 1.0 - par->background_transparency,
+ };
+
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_label(nk, "Background color:", NK_TEXT_LEFT);
+ if (nk_combo_begin_color(nk, nk_rgb_cf(bg), nk_vec2(nk_widget_width(nk), 300))) {
+ nk_layout_row_dynamic(nk, 200, 1);
+ nk_color_pick(nk, &bg, NK_RGBA);
+ nk_combo_end(nk);
+
+ par->background_color[0] = bg.r;
+ par->background_color[1] = bg.g;
+ par->background_color[2] = bg.b;
+ par->background_transparency = 1.0 - bg.a;
+ }
+
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->blend_against_tiles = nk_check_label(nk, "Blend against tiles", par->blend_against_tiles);
+ nk_property_int(nk, "Tile size", 2, &par->tile_size, 256, 1, 1);
+
+ nk_layout_row(nk, NK_DYNAMIC, 24, 3, (float[]){ 0.4, 0.3, 0.3 });
+ nk_label(nk, "Tile colors:", NK_TEXT_LEFT);
+ for (int i = 0; i < 2; i++) {
+ bg = (struct nk_colorf) {
+ par->tile_colors[i][0],
+ par->tile_colors[i][1],
+ par->tile_colors[i][2],
+ };
+
+ if (nk_combo_begin_color(nk, nk_rgb_cf(bg), nk_vec2(nk_widget_width(nk), 300))) {
+ nk_layout_row_dynamic(nk, 200, 1);
+ nk_color_pick(nk, &bg, NK_RGB);
+ nk_combo_end(nk);
+
+ par->tile_colors[i][0] = bg.r;
+ par->tile_colors[i][1] = bg.g;
+ par->tile_colors[i][2] = bg.b;
+ }
+ }
+
+ static const char *rotations[4] = {
+ [PL_ROTATION_0] = "0°",
+ [PL_ROTATION_90] = "90°",
+ [PL_ROTATION_180] = "180°",
+ [PL_ROTATION_270] = "270°",
+ };
+
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_label(nk, "Display orientation:", NK_TEXT_LEFT);
+ p->target_rot = nk_combo(nk, rotations, 4, p->target_rot,
+ 16, nk_vec2(nk_widget_width(nk), 100));
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Image scaling", NK_MAXIMIZED)) {
+ const struct pl_filter_config *f;
+ static const char *scale_none = "None (Built-in sampling)";
+ static const char *pscale_none = "None (Use regular upscaler)";
+ static const char *tscale_none = "None (No frame mixing)";
+ #define SCALE_DESC(scaler, fallback) (par->scaler ? par->scaler->description : fallback)
+
+ static const char *zoom_modes[ZOOM_COUNT] = {
+ [ZOOM_PAD] = "Pad to window",
+ [ZOOM_CROP] = "Crop to window",
+ [ZOOM_STRETCH] = "Stretch to window",
+ [ZOOM_FIT] = "Fit inside window",
+ [ZOOM_RAW] = "Unscaled (raw)",
+ [ZOOM_400] = "400% zoom",
+ [ZOOM_200] = "200% zoom",
+ [ZOOM_100] = "100% zoom",
+ [ZOOM_50] = " 50% zoom",
+ [ZOOM_25] = " 25% zoom",
+ };
+
+ nk_layout_row(nk, NK_DYNAMIC, 24, 2, (float[]){ 0.3, 0.7 });
+ nk_label(nk, "Zoom mode:", NK_TEXT_LEFT);
+ int zoom = nk_combo(nk, zoom_modes, ZOOM_COUNT, p->target_zoom, 16, nk_vec2(nk_widget_width(nk), 500));
+ if (zoom != p->target_zoom) {
+ // Image crop may change
+ pl_renderer_flush_cache(p->renderer);
+ p->target_zoom = zoom;
+ }
+
+ nk_label(nk, "Upscaler:", NK_TEXT_LEFT);
+ if (nk_combo_begin_label(nk, SCALE_DESC(upscaler, scale_none), nk_vec2(nk_widget_width(nk), 500))) {
+ nk_layout_row_dynamic(nk, 16, 1);
+ if (nk_combo_item_label(nk, scale_none, NK_TEXT_LEFT))
+ par->upscaler = NULL;
+ for (int i = 0; i < pl_num_filter_configs; i++) {
+ f = pl_filter_configs[i];
+ if (!f->description)
+ continue;
+ if (!(f->allowed & PL_FILTER_UPSCALING))
+ continue;
+ if (!p->advanced_scalers && !(f->recommended & PL_FILTER_UPSCALING))
+ continue;
+ if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
+ par->upscaler = f;
+ }
+ nk_combo_end(nk);
+ }
+
+ nk_label(nk, "Downscaler:", NK_TEXT_LEFT);
+ if (nk_combo_begin_label(nk, SCALE_DESC(downscaler, scale_none), nk_vec2(nk_widget_width(nk), 500))) {
+ nk_layout_row_dynamic(nk, 16, 1);
+ if (nk_combo_item_label(nk, scale_none, NK_TEXT_LEFT))
+ par->downscaler = NULL;
+ for (int i = 0; i < pl_num_filter_configs; i++) {
+ f = pl_filter_configs[i];
+ if (!f->description)
+ continue;
+ if (!(f->allowed & PL_FILTER_DOWNSCALING))
+ continue;
+ if (!p->advanced_scalers && !(f->recommended & PL_FILTER_DOWNSCALING))
+ continue;
+ if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
+ par->downscaler = f;
+ }
+ nk_combo_end(nk);
+ }
+
+ nk_label(nk, "Plane scaler:", NK_TEXT_LEFT);
+ if (nk_combo_begin_label(nk, SCALE_DESC(plane_upscaler, pscale_none), nk_vec2(nk_widget_width(nk), 500))) {
+ nk_layout_row_dynamic(nk, 16, 1);
+ if (nk_combo_item_label(nk, pscale_none, NK_TEXT_LEFT))
+ par->downscaler = NULL;
+ for (int i = 0; i < pl_num_filter_configs; i++) {
+ f = pl_filter_configs[i];
+ if (!f->description)
+ continue;
+ if (!(f->allowed & PL_FILTER_UPSCALING))
+ continue;
+ if (!p->advanced_scalers && !(f->recommended & PL_FILTER_UPSCALING))
+ continue;
+ if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
+ par->plane_upscaler = f;
+ }
+ nk_combo_end(nk);
+ }
+
+ nk_label(nk, "Frame mixing:", NK_TEXT_LEFT);
+ if (nk_combo_begin_label(nk, SCALE_DESC(frame_mixer, tscale_none), nk_vec2(nk_widget_width(nk), 300))) {
+ nk_layout_row_dynamic(nk, 16, 1);
+ if (nk_combo_item_label(nk, tscale_none, NK_TEXT_LEFT))
+ par->frame_mixer = NULL;
+ for (int i = 0; i < pl_num_filter_configs; i++) {
+ f = pl_filter_configs[i];
+ if (!f->description)
+ continue;
+ if (!(f->allowed & PL_FILTER_FRAME_MIXING))
+ continue;
+ if (!p->advanced_scalers && !(f->recommended & PL_FILTER_FRAME_MIXING))
+ continue;
+ if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
+ par->frame_mixer = f;
+ }
+ nk_combo_end(nk);
+ }
+
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->skip_anti_aliasing = !nk_check_label(nk, "Anti-aliasing", !par->skip_anti_aliasing);
+ nk_property_float(nk, "Antiringing", 0, &par->antiringing_strength, 1.0, 0.05, 0.001);
+
+ struct pl_sigmoid_params *spar = &opts->sigmoid_params;
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->sigmoid_params = nk_check_label(nk, "Sigmoidization", par->sigmoid_params) ? spar : NULL;
+ if (nk_button_label(nk, "Default values"))
+ *spar = pl_sigmoid_default_params;
+ nk_property_float(nk, "Sigmoid center", 0, &spar->center, 1, 0.1, 0.01);
+ nk_property_float(nk, "Sigmoid slope", 0, &spar->slope, 100, 1, 0.1);
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Deinterlacing", NK_MINIMIZED)) {
+ struct pl_deinterlace_params *dpar = &opts->deinterlace_params;
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->deinterlace_params = nk_check_label(nk, "Enable", par->deinterlace_params) ? dpar : NULL;
+ if (nk_button_label(nk, "Reset settings"))
+ *dpar = pl_deinterlace_default_params;
+
+ static const char *deint_algos[PL_DEINTERLACE_ALGORITHM_COUNT] = {
+ [PL_DEINTERLACE_WEAVE] = "Field weaving (no-op)",
+ [PL_DEINTERLACE_BOB] = "Naive bob (line doubling)",
+ [PL_DEINTERLACE_YADIF] = "Yadif (\"yet another deinterlacing filter\")",
+ };
+
+ nk_label(nk, "Deinterlacing algorithm", NK_TEXT_LEFT);
+ dpar->algo = nk_combo(nk, deint_algos, PL_DEINTERLACE_ALGORITHM_COUNT,
+ dpar->algo, 16, nk_vec2(nk_widget_width(nk), 300));
+
+ switch (dpar->algo) {
+ case PL_DEINTERLACE_WEAVE:
+ case PL_DEINTERLACE_BOB:
+ break;
+ case PL_DEINTERLACE_YADIF:
+ nk_checkbox_label(nk, "Skip spatial check", &dpar->skip_spatial_check);
+ break;
+ default: abort();
+ }
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Debanding", NK_MINIMIZED)) {
+ struct pl_deband_params *dpar = &opts->deband_params;
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->deband_params = nk_check_label(nk, "Enable", par->deband_params) ? dpar : NULL;
+ if (nk_button_label(nk, "Reset settings"))
+ *dpar = pl_deband_default_params;
+ nk_property_int(nk, "Iterations", 0, &dpar->iterations, 8, 1, 0);
+ nk_property_float(nk, "Threshold", 0, &dpar->threshold, 256, 1, 0.5);
+ nk_property_float(nk, "Radius", 0, &dpar->radius, 256, 1, 0.2);
+ nk_property_float(nk, "Grain", 0, &dpar->grain, 512, 1, 0.5);
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Distortion", NK_MINIMIZED)) {
+ struct pl_distort_params *dpar = &opts->distort_params;
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->distort_params = nk_check_label(nk, "Enable", par->distort_params) ? dpar : NULL;
+ if (nk_button_label(nk, "Reset settings"))
+ *dpar = pl_distort_default_params;
+
+ static const char *address_modes[PL_TEX_ADDRESS_MODE_COUNT] = {
+ [PL_TEX_ADDRESS_CLAMP] = "Clamp edges",
+ [PL_TEX_ADDRESS_REPEAT] = "Repeat edges",
+ [PL_TEX_ADDRESS_MIRROR] = "Mirror edges",
+ };
+
+ nk_checkbox_label(nk, "Constrain bounds", &dpar->constrain);
+ dpar->address_mode = nk_combo(nk, address_modes, PL_TEX_ADDRESS_MODE_COUNT,
+ dpar->address_mode, 16, nk_vec2(nk_widget_width(nk), 100));
+ bool alpha = nk_check_label(nk, "Transparent background", dpar->alpha_mode);
+ dpar->alpha_mode = alpha ? PL_ALPHA_INDEPENDENT : PL_ALPHA_UNKNOWN;
+ nk_checkbox_label(nk, "Bicubic interpolation", &dpar->bicubic);
+
+ struct pl_transform2x2 *tf = &dpar->transform;
+ nk_property_float(nk, "Scale X", -10.0, &tf->mat.m[0][0], 10.0, 0.1, 0.005);
+ nk_property_float(nk, "Shear X", -10.0, &tf->mat.m[0][1], 10.0, 0.1, 0.005);
+ nk_property_float(nk, "Shear Y", -10.0, &tf->mat.m[1][0], 10.0, 0.1, 0.005);
+ nk_property_float(nk, "Scale Y", -10.0, &tf->mat.m[1][1], 10.0, 0.1, 0.005);
+ nk_property_float(nk, "Offset X", -10.0, &tf->c[0], 10.0, 0.1, 0.005);
+ nk_property_float(nk, "Offset Y", -10.0, &tf->c[1], 10.0, 0.1, 0.005);
+
+ float zoom_ref = fabsf(tf->mat.m[0][0] * tf->mat.m[1][1] -
+ tf->mat.m[0][1] * tf->mat.m[1][0]);
+ zoom_ref = logf(fmaxf(zoom_ref, 1e-4));
+ float zoom = zoom_ref;
+ nk_property_float(nk, "log(Zoom)", -10.0, &zoom, 10.0, 0.1, 0.005);
+ pl_transform2x2_scale(tf, expf(zoom - zoom_ref));
+
+ float angle_ref = (atan2f(tf->mat.m[1][0], tf->mat.m[1][1]) -
+ atan2f(tf->mat.m[0][1], tf->mat.m[0][0])) / 2;
+ angle_ref = fmodf(angle_ref * 180/M_PI + 540, 360) - 180;
+ float angle = angle_ref;
+ nk_property_float(nk, "Rotate (°)", -200, &angle, 200, -5, -0.2);
+ float angle_delta = (angle - angle_ref) * M_PI / 180;
+ const pl_matrix2x2 rot = pl_matrix2x2_rotation(angle_delta);
+ pl_matrix2x2_rmul(&rot, &tf->mat);
+
+ bool flip_ox = nk_button_label(nk, "Flip output X");
+ bool flip_oy = nk_button_label(nk, "Flip output Y");
+ bool flip_ix = nk_button_label(nk, "Flip input X");
+ bool flip_iy = nk_button_label(nk, "Flip input Y");
+ if (flip_ox ^ flip_ix)
+ tf->mat.m[0][0] = -tf->mat.m[0][0];
+ if (flip_ox ^ flip_iy)
+ tf->mat.m[0][1] = -tf->mat.m[0][1];
+ if (flip_oy ^ flip_ix)
+ tf->mat.m[1][0] = -tf->mat.m[1][0];
+ if (flip_oy ^ flip_iy)
+ tf->mat.m[1][1] = -tf->mat.m[1][1];
+ if (flip_ox)
+ tf->c[0] = -tf->c[0];
+ if (flip_oy)
+ tf->c[1] = -tf->c[1];
+
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Color adjustment", NK_MINIMIZED)) {
+ struct pl_color_adjustment *adj = &opts->color_adjustment;
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->color_adjustment = nk_check_label(nk, "Enable", par->color_adjustment) ? adj : NULL;
+ if (nk_button_label(nk, "Default values"))
+ *adj = pl_color_adjustment_neutral;
+ nk_property_float(nk, "Brightness", -1, &adj->brightness, 1, 0.1, 0.005);
+ nk_property_float(nk, "Contrast", 0, &adj->contrast, 10, 0.1, 0.005);
+
+ // Convert to (cyclical) degrees for display
+ int deg = roundf(adj->hue * 180.0 / M_PI);
+ nk_property_int(nk, "Hue (°)", -50, &deg, 400, 1, 1);
+ adj->hue = ((deg + 360) % 360) * M_PI / 180.0;
+
+ nk_property_float(nk, "Saturation", 0, &adj->saturation, 10, 0.1, 0.005);
+ nk_property_float(nk, "Gamma", 0, &adj->gamma, 10, 0.1, 0.005);
+
+ // Convert to human-friendly temperature values for display
+ int temp = (int) roundf(adj->temperature * 3500) + 6500;
+ nk_property_int(nk, "Temperature (K)", 3000, &temp, 10000, 10, 5);
+ adj->temperature = (temp - 6500) / 3500.0;
+
+ struct pl_cone_params *cpar = &opts->cone_params;
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->cone_params = nk_check_label(nk, "Color blindness", par->cone_params) ? cpar : NULL;
+ if (nk_button_label(nk, "Default values"))
+ *cpar = pl_vision_normal;
+ nk_layout_row(nk, NK_DYNAMIC, 24, 5, (float[]){ 0.25, 0.25/3, 0.25/3, 0.25/3, 0.5 });
+ nk_label(nk, "Cone model:", NK_TEXT_LEFT);
+ unsigned int cones = cpar->cones;
+ nk_checkbox_flags_label(nk, "L", &cones, PL_CONE_L);
+ nk_checkbox_flags_label(nk, "M", &cones, PL_CONE_M);
+ nk_checkbox_flags_label(nk, "S", &cones, PL_CONE_S);
+ cpar->cones = cones;
+ nk_property_float(nk, "Sensitivity", 0.0, &cpar->strength, 5.0, 0.1, 0.01);
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "HDR peak detection", NK_MINIMIZED)) {
+ struct pl_peak_detect_params *ppar = &opts->peak_detect_params;
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->peak_detect_params = nk_check_label(nk, "Enable", par->peak_detect_params) ? ppar : NULL;
+ if (nk_button_label(nk, "Reset settings"))
+ *ppar = pl_peak_detect_default_params;
+ nk_property_float(nk, "Threshold low", 0.0, &ppar->scene_threshold_low, 20.0, 0.5, 0.005);
+ nk_property_float(nk, "Threshold high", 0.0, &ppar->scene_threshold_high, 20.0, 0.5, 0.005);
+ nk_property_float(nk, "Smoothing period", 0.0, &ppar->smoothing_period, 1000.0, 5.0, 1.0);
+ nk_property_float(nk, "Peak percentile", 95.0, &ppar->percentile, 100.0, 0.01, 0.001);
+ nk_checkbox_label(nk, "Allow 1-frame delay", &ppar->allow_delayed);
+
+ struct pl_hdr_metadata metadata;
+ if (pl_renderer_get_hdr_metadata(p->renderer, &metadata)) {
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_label(nk, "Detected max luminance:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%.2f cd/m² (%.2f%% PQ)",
+ pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NITS, metadata.max_pq_y),
+ 100.0f * metadata.max_pq_y);
+ nk_label(nk, "Detected avg luminance:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%.2f cd/m² (%.2f%% PQ)",
+ pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NITS, metadata.avg_pq_y),
+ 100.0f * metadata.avg_pq_y);
+ }
+
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Tone mapping", NK_MINIMIZED)) {
+ struct pl_color_map_params *cpar = &opts->color_map_params;
+ static const struct pl_color_map_params null_settings = {0};
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->color_map_params = nk_check_label(nk, "Enable",
+ par->color_map_params == cpar) ? cpar : &null_settings;
+ if (nk_button_label(nk, "Reset settings"))
+ *cpar = pl_color_map_default_params;
+
+ nk_label(nk, "Gamut mapping function:", NK_TEXT_LEFT);
+ if (nk_combo_begin_label(nk, cpar->gamut_mapping->description,
+ nk_vec2(nk_widget_width(nk), 500)))
+ {
+ nk_layout_row_dynamic(nk, 16, 1);
+ for (int i = 0; i < pl_num_gamut_map_functions; i++) {
+ const struct pl_gamut_map_function *f = pl_gamut_map_functions[i];
+ if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
+ cpar->gamut_mapping = f;
+ }
+ nk_combo_end(nk);
+ }
+
+ nk_label(nk, "Tone mapping function:", NK_TEXT_LEFT);
+ if (nk_combo_begin_label(nk, cpar->tone_mapping_function->description,
+ nk_vec2(nk_widget_width(nk), 500)))
+ {
+ nk_layout_row_dynamic(nk, 16, 1);
+ for (int i = 0; i < pl_num_tone_map_functions; i++) {
+ const struct pl_tone_map_function *f = pl_tone_map_functions[i];
+ if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
+ cpar->tone_mapping_function = f;
+ }
+ nk_combo_end(nk);
+ }
+
+ static const char *metadata_types[PL_HDR_METADATA_TYPE_COUNT] = {
+ [PL_HDR_METADATA_ANY] = "Automatic selection",
+ [PL_HDR_METADATA_NONE] = "None (disabled)",
+ [PL_HDR_METADATA_HDR10] = "HDR10 (static)",
+ [PL_HDR_METADATA_HDR10PLUS] = "HDR10+ (MaxRGB)",
+ [PL_HDR_METADATA_CIE_Y] = "Luminance (CIE Y)",
+ };
+
+ nk_label(nk, "HDR metadata source:", NK_TEXT_LEFT);
+ cpar->metadata = nk_combo(nk, metadata_types,
+ PL_HDR_METADATA_TYPE_COUNT,
+ cpar->metadata,
+ 16, nk_vec2(nk_widget_width(nk), 300));
+
+ nk_property_float(nk, "Contrast recovery", 0.0, &cpar->contrast_recovery, 2.0, 0.05, 0.005);
+ nk_property_float(nk, "Contrast smoothness", 1.0, &cpar->contrast_smoothness, 32.0, 0.1, 0.005);
+
+ nk_property_int(nk, "LUT size", 16, &cpar->lut_size, 1024, 1, 1);
+ nk_property_int(nk, "3DLUT size I", 7, &cpar->lut3d_size[0], 65, 1, 1);
+ nk_property_int(nk, "3DLUT size C", 7, &cpar->lut3d_size[1], 256, 1, 1);
+ nk_property_int(nk, "3DLUT size h", 7, &cpar->lut3d_size[2], 1024, 1, 1);
+
+ nk_checkbox_label(nk, "Tricubic interpolation", &cpar->lut3d_tricubic);
+ nk_checkbox_label(nk, "Force full LUT", &cpar->force_tone_mapping_lut);
+ nk_checkbox_label(nk, "Inverse tone mapping", &cpar->inverse_tone_mapping);
+ nk_checkbox_label(nk, "Gamut expansion", &cpar->gamut_expansion);
+ nk_checkbox_label(nk, "Show clipping", &cpar->show_clipping);
+ nk_checkbox_label(nk, "Visualize LUT", &cpar->visualize_lut);
+
+ if (cpar->visualize_lut) {
+ nk_layout_row_dynamic(nk, 24, 2);
+ const float huerange = 2 * M_PI;
+ nk_property_float(nk, "Hue", -1, &cpar->visualize_hue, huerange + 1.0, 0.1, 0.01);
+ nk_property_float(nk, "Theta", 0.0, &cpar->visualize_theta, M_PI_2, 0.1, 0.01);
+ cpar->visualize_hue = fmodf(cpar->visualize_hue + huerange, huerange);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Fine-tune constants (advanced)", NK_MINIMIZED)) {
+ struct pl_tone_map_constants *tc = &cpar->tone_constants;
+ struct pl_gamut_map_constants *gc = &cpar->gamut_constants;
+ nk_layout_row_dynamic(nk, 20, 2);
+ nk_property_float(nk, "Perceptual deadzone", 0.0, &gc->perceptual_deadzone, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Perceptual strength", 0.0, &gc->perceptual_strength, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Colorimetric gamma", 0.0, &gc->colorimetric_gamma, 10.0, 0.05, 0.001);
+ nk_property_float(nk, "Softclip knee", 0.0, &gc->softclip_knee, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Softclip desaturation", 0.0, &gc->softclip_desat, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Knee adaptation", 0.0, &tc->knee_adaptation, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Knee minimum", 0.0, &tc->knee_minimum, 0.5, 0.05, 0.001);
+ nk_property_float(nk, "Knee maximum", 0.5, &tc->knee_maximum, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Knee default", tc->knee_minimum, &tc->knee_default, tc->knee_maximum, 0.05, 0.001);
+ nk_property_float(nk, "BT.2390 offset", 0.5, &tc->knee_offset, 2.0, 0.05, 0.001);
+ nk_property_float(nk, "Spline slope tuning", 0.0, &tc->slope_tuning, 10.0, 0.05, 0.001);
+ nk_property_float(nk, "Spline slope offset", 0.0, &tc->slope_offset, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Spline contrast", 0.0, &tc->spline_contrast, 1.5, 0.05, 0.001);
+ nk_property_float(nk, "Reinhard contrast", 0.0, &tc->reinhard_contrast, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Linear knee point", 0.0, &tc->linear_knee, 1.0, 0.05, 0.001);
+ nk_property_float(nk, "Linear exposure", 0.0, &tc->exposure, 10.0, 0.05, 0.001);
+ nk_tree_pop(nk);
+ }
+
+ nk_layout_row_dynamic(nk, 50, 1);
+ if (ui_widget_hover(nk, "Drop .cube file here...") && dropped_file) {
+ uint8_t *buf;
+ size_t size;
+ int ret = av_file_map(dropped_file, &buf, &size, 0, NULL);
+ if (ret < 0) {
+ fprintf(stderr, "Failed opening '%s': %s\n", dropped_file,
+ av_err2str(ret));
+ } else {
+ pl_lut_free((struct pl_custom_lut **) &par->lut);
+ par->lut = pl_lut_parse_cube(p->log, (char *) buf, size);
+ av_file_unmap(buf, size);
+ }
+ }
+
+ static const char *lut_types[] = {
+ [PL_LUT_UNKNOWN] = "Auto (unknown)",
+ [PL_LUT_NATIVE] = "Raw RGB (native)",
+ [PL_LUT_NORMALIZED] = "Linear RGB (normalized)",
+ [PL_LUT_CONVERSION] = "Gamut conversion (native)",
+ };
+
+ nk_layout_row(nk, NK_DYNAMIC, 24, 3, (float[]){ 0.2, 0.3, 0.5 });
+ if (nk_button_label(nk, "Reset LUT")) {
+ pl_lut_free((struct pl_custom_lut **) &par->lut);
+ par->lut_type = PL_LUT_UNKNOWN;
+ }
+
+ nk_label(nk, "LUT type:", NK_TEXT_CENTERED);
+ par->lut_type = nk_combo(nk, lut_types, 4, par->lut_type,
+ 16, nk_vec2(nk_widget_width(nk), 100));
+
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Dithering", NK_MINIMIZED)) {
+ struct pl_dither_params *dpar = &opts->dither_params;
+ nk_layout_row_dynamic(nk, 24, 2);
+ par->dither_params = nk_check_label(nk, "Enable", par->dither_params) ? dpar : NULL;
+ if (nk_button_label(nk, "Reset settings"))
+ *dpar = pl_dither_default_params;
+
+ static const char *dither_methods[PL_DITHER_METHOD_COUNT] = {
+ [PL_DITHER_BLUE_NOISE] = "Blue noise",
+ [PL_DITHER_ORDERED_LUT] = "Ordered (LUT)",
+ [PL_DITHER_ORDERED_FIXED] = "Ordered (fixed size)",
+ [PL_DITHER_WHITE_NOISE] = "White noise",
+ };
+
+ nk_label(nk, "Dither method:", NK_TEXT_LEFT);
+ dpar->method = nk_combo(nk, dither_methods, PL_DITHER_METHOD_COUNT, dpar->method,
+ 16, nk_vec2(nk_widget_width(nk), 100));
+
+ static const char *lut_sizes[8] = {
+ "2x2", "4x4", "8x8", "16x16", "32x32", "64x64", "128x128", "256x256",
+ };
+
+ nk_label(nk, "LUT size:", NK_TEXT_LEFT);
+ switch (dpar->method) {
+ case PL_DITHER_BLUE_NOISE:
+ case PL_DITHER_ORDERED_LUT: {
+ int size = dpar->lut_size - 1;
+ nk_combobox(nk, lut_sizes, 8, &size, 16, nk_vec2(nk_widget_width(nk), 200));
+ dpar->lut_size = size + 1;
+ break;
+ }
+ case PL_DITHER_ORDERED_FIXED:
+ nk_label(nk, "64x64", NK_TEXT_LEFT);
+ break;
+ default:
+ nk_label(nk, "(N/A)", NK_TEXT_LEFT);
+ break;
+ }
+
+ nk_checkbox_label(nk, "Temporal dithering", &dpar->temporal);
+
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_label(nk, "Error diffusion:", NK_TEXT_LEFT);
+ const char *name = par->error_diffusion ? par->error_diffusion->description : "(None)";
+ if (nk_combo_begin_label(nk, name, nk_vec2(nk_widget_width(nk), 500))) {
+ nk_layout_row_dynamic(nk, 16, 1);
+ if (nk_combo_item_label(nk, "(None)", NK_TEXT_LEFT))
+ par->error_diffusion = NULL;
+ for (int i = 0; i < pl_num_error_diffusion_kernels; i++) {
+ const struct pl_error_diffusion_kernel *k = pl_error_diffusion_kernels[i];
+ if (nk_combo_item_label(nk, k->description, NK_TEXT_LEFT))
+ par->error_diffusion = k;
+ }
+ nk_combo_end(nk);
+ }
+
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Output color space", NK_MINIMIZED)) {
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_checkbox_label(nk, "Enable", &p->target_override);
+ bool reset = nk_button_label(nk, "Reset settings");
+ bool reset_icc = reset;
+ char buf[64] = {0};
+
+ nk_layout_row(nk, NK_DYNAMIC, 24, 2, (float[]){ 0.3, 0.7 });
+
+ const char *primaries[PL_COLOR_PRIM_COUNT] = {
+ [PL_COLOR_PRIM_UNKNOWN] = "Auto (unknown)",
+ [PL_COLOR_PRIM_BT_601_525] = "ITU-R Rec. BT.601 (525-line = NTSC, SMPTE-C)",
+ [PL_COLOR_PRIM_BT_601_625] = "ITU-R Rec. BT.601 (625-line = PAL, SECAM)",
+ [PL_COLOR_PRIM_BT_709] = "ITU-R Rec. BT.709 (HD), also sRGB",
+ [PL_COLOR_PRIM_BT_470M] = "ITU-R Rec. BT.470 M",
+ [PL_COLOR_PRIM_EBU_3213] = "EBU Tech. 3213-E / JEDEC P22 phosphors",
+ [PL_COLOR_PRIM_BT_2020] = "ITU-R Rec. BT.2020 (UltraHD)",
+ [PL_COLOR_PRIM_APPLE] = "Apple RGB",
+ [PL_COLOR_PRIM_ADOBE] = "Adobe RGB (1998)",
+ [PL_COLOR_PRIM_PRO_PHOTO] = "ProPhoto RGB (ROMM)",
+ [PL_COLOR_PRIM_CIE_1931] = "CIE 1931 RGB primaries",
+ [PL_COLOR_PRIM_DCI_P3] = "DCI-P3 (Digital Cinema)",
+ [PL_COLOR_PRIM_DISPLAY_P3] = "DCI-P3 (Digital Cinema) with D65 white point",
+ [PL_COLOR_PRIM_V_GAMUT] = "Panasonic V-Gamut (VARICAM)",
+ [PL_COLOR_PRIM_S_GAMUT] = "Sony S-Gamut",
+ [PL_COLOR_PRIM_FILM_C] = "Traditional film primaries with Illuminant C",
+ [PL_COLOR_PRIM_ACES_AP0] = "ACES Primaries #0",
+ [PL_COLOR_PRIM_ACES_AP1] = "ACES Primaries #1",
+ };
+
+ if (target->color.primaries) {
+ snprintf(buf, sizeof(buf), "Auto (%s)", primaries[target->color.primaries]);
+ primaries[PL_COLOR_PRIM_UNKNOWN] = buf;
+ }
+
+ nk_label(nk, "Primaries:", NK_TEXT_LEFT);
+ p->force_prim = nk_combo(nk, primaries, PL_COLOR_PRIM_COUNT, p->force_prim,
+ 16, nk_vec2(nk_widget_width(nk), 200));
+
+ const char *transfers[PL_COLOR_TRC_COUNT] = {
+ [PL_COLOR_TRC_UNKNOWN] = "Auto (unknown SDR)",
+ [PL_COLOR_TRC_BT_1886] = "ITU-R Rec. BT.1886 (CRT emulation + OOTF)",
+ [PL_COLOR_TRC_SRGB] = "IEC 61966-2-4 sRGB (CRT emulation)",
+ [PL_COLOR_TRC_LINEAR] = "Linear light content",
+ [PL_COLOR_TRC_GAMMA18] = "Pure power gamma 1.8",
+ [PL_COLOR_TRC_GAMMA20] = "Pure power gamma 2.0",
+ [PL_COLOR_TRC_GAMMA22] = "Pure power gamma 2.2",
+ [PL_COLOR_TRC_GAMMA24] = "Pure power gamma 2.4",
+ [PL_COLOR_TRC_GAMMA26] = "Pure power gamma 2.6",
+ [PL_COLOR_TRC_GAMMA28] = "Pure power gamma 2.8",
+ [PL_COLOR_TRC_PRO_PHOTO] = "ProPhoto RGB (ROMM)",
+ [PL_COLOR_TRC_ST428] = "Digital Cinema Distribution Master (XYZ)",
+ [PL_COLOR_TRC_PQ] = "ITU-R BT.2100 PQ (perceptual quantizer), aka SMPTE ST2048",
+ [PL_COLOR_TRC_HLG] = "ITU-R BT.2100 HLG (hybrid log-gamma), aka ARIB STD-B67",
+ [PL_COLOR_TRC_V_LOG] = "Panasonic V-Log (VARICAM)",
+ [PL_COLOR_TRC_S_LOG1] = "Sony S-Log1",
+ [PL_COLOR_TRC_S_LOG2] = "Sony S-Log2",
+ };
+
+ if (target->color.transfer) {
+ snprintf(buf, sizeof(buf), "Auto (%s)", transfers[target->color.transfer]);
+ transfers[PL_COLOR_TRC_UNKNOWN] = buf;
+ }
+
+ nk_label(nk, "Transfer:", NK_TEXT_LEFT);
+ p->force_trc = nk_combo(nk, transfers, PL_COLOR_TRC_COUNT, p->force_trc,
+ 16, nk_vec2(nk_widget_width(nk), 200));
+
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_checkbox_label(nk, "Override HDR levels", &p->force_hdr_enable);
+
+ // Ensure these values are always legal by going through
+ // pl_color_space_infer
+ nk_layout_row_dynamic(nk, 24, 2);
+ struct pl_color_space fix = target->color;
+ apply_csp_overrides(p, &fix);
+ pl_color_space_infer(&fix);
+
+ fix.hdr.min_luma *= 1000; // better value range
+ nk_property_float(nk, "White point (cd/m²)",
+ 10.0, &fix.hdr.max_luma, 10000.0,
+ fix.hdr.max_luma / 100, fix.hdr.max_luma / 1000);
+ nk_property_float(nk, "Black point (mcd/m²)",
+ PL_COLOR_HDR_BLACK * 1000, &fix.hdr.min_luma,
+ 100.0 * 1000, 5, 2);
+ fix.hdr.min_luma /= 1000;
+ pl_color_space_infer(&fix);
+ p->force_hdr = fix.hdr;
+
+ struct pl_color_repr *trepr = &p->force_repr;
+ nk_layout_row(nk, NK_DYNAMIC, 24, 2, (float[]){ 0.3, 0.7 });
+
+ const char *systems[PL_COLOR_SYSTEM_COUNT] = {
+ [PL_COLOR_SYSTEM_UNKNOWN] = "Auto (unknown)",
+ [PL_COLOR_SYSTEM_BT_601] = "ITU-R Rec. BT.601 (SD)",
+ [PL_COLOR_SYSTEM_BT_709] = "ITU-R Rec. BT.709 (HD)",
+ [PL_COLOR_SYSTEM_SMPTE_240M] = "SMPTE-240M",
+ [PL_COLOR_SYSTEM_BT_2020_NC] = "ITU-R Rec. BT.2020 (non-constant luminance)",
+ [PL_COLOR_SYSTEM_BT_2020_C] = "ITU-R Rec. BT.2020 (constant luminance)",
+ [PL_COLOR_SYSTEM_BT_2100_PQ] = "ITU-R Rec. BT.2100 ICtCp PQ variant",
+ [PL_COLOR_SYSTEM_BT_2100_HLG] = "ITU-R Rec. BT.2100 ICtCp HLG variant",
+ [PL_COLOR_SYSTEM_DOLBYVISION] = "Dolby Vision (invalid for output)",
+ [PL_COLOR_SYSTEM_YCGCO] = "YCgCo (derived from RGB)",
+ [PL_COLOR_SYSTEM_RGB] = "Red, Green and Blue",
+ [PL_COLOR_SYSTEM_XYZ] = "Digital Cinema Distribution Master (XYZ)",
+ };
+
+ if (target->repr.sys) {
+ snprintf(buf, sizeof(buf), "Auto (%s)", systems[target->repr.sys]);
+ systems[PL_COLOR_SYSTEM_UNKNOWN] = buf;
+ }
+
+ nk_label(nk, "System:", NK_TEXT_LEFT);
+ trepr->sys = nk_combo(nk, systems, PL_COLOR_SYSTEM_COUNT, trepr->sys,
+ 16, nk_vec2(nk_widget_width(nk), 200));
+ if (trepr->sys == PL_COLOR_SYSTEM_DOLBYVISION)
+ trepr->sys = PL_COLOR_SYSTEM_UNKNOWN;
+
+ const char *levels[PL_COLOR_LEVELS_COUNT] = {
+ [PL_COLOR_LEVELS_UNKNOWN] = "Auto (unknown)",
+ [PL_COLOR_LEVELS_LIMITED] = "Limited/TV range, e.g. 16-235",
+ [PL_COLOR_LEVELS_FULL] = "Full/PC range, e.g. 0-255",
+ };
+
+ if (target->repr.levels) {
+ snprintf(buf, sizeof(buf), "Auto (%s)", levels[target->repr.levels]);
+ levels[PL_COLOR_LEVELS_UNKNOWN] = buf;
+ }
+
+ nk_label(nk, "Levels:", NK_TEXT_LEFT);
+ trepr->levels = nk_combo(nk, levels, PL_COLOR_LEVELS_COUNT, trepr->levels,
+ 16, nk_vec2(nk_widget_width(nk), 200));
+
+ const char *alphas[PL_ALPHA_MODE_COUNT] = {
+ [PL_ALPHA_UNKNOWN] = "Auto (unknown, or no alpha)",
+ [PL_ALPHA_INDEPENDENT] = "Independent alpha channel",
+ [PL_ALPHA_PREMULTIPLIED] = "Premultiplied alpha channel",
+ };
+
+ if (target->repr.alpha) {
+ snprintf(buf, sizeof(buf), "Auto (%s)", alphas[target->repr.alpha]);
+ alphas[PL_ALPHA_UNKNOWN] = buf;
+ }
+
+ nk_label(nk, "Alpha:", NK_TEXT_LEFT);
+ trepr->alpha = nk_combo(nk, alphas, PL_ALPHA_MODE_COUNT, trepr->alpha,
+ 16, nk_vec2(nk_widget_width(nk), 200));
+
+ const struct pl_bit_encoding *bits = &target->repr.bits;
+ nk_label(nk, "Bit depth:", NK_TEXT_LEFT);
+ auto_property_int(nk, bits->color_depth, 0,
+ &trepr->bits.color_depth, 16, 1, 0);
+
+ if (bits->color_depth != bits->sample_depth) {
+ nk_label(nk, "Sample bit depth:", NK_TEXT_LEFT);
+ auto_property_int(nk, bits->sample_depth, 0,
+ &trepr->bits.sample_depth, 16, 1, 0);
+ } else {
+ // Adjust these two fields in unison
+ trepr->bits.sample_depth = trepr->bits.color_depth;
+ }
+
+ if (bits->bit_shift) {
+ nk_label(nk, "Bit shift:", NK_TEXT_LEFT);
+ auto_property_int(nk, bits->bit_shift, 0,
+ &trepr->bits.bit_shift, 16, 1, 0);
+ } else {
+ trepr->bits.bit_shift = 0;
+ }
+
+ nk_layout_row_dynamic(nk, 24, 1);
+ nk_checkbox_label(nk, "Forward input color space to display", &p->colorspace_hint);
+
+ if (p->colorspace_hint && !p->force_hdr_enable) {
+ nk_checkbox_label(nk, "Forward dynamic brightness changes to display",
+ &p->colorspace_hint_dynamic);
+ }
+
+ nk_layout_row_dynamic(nk, 50, 1);
+ if (ui_widget_hover(nk, "Drop ICC profile here...") && dropped_file) {
+ struct pl_icc_profile profile;
+ int ret = av_file_map(dropped_file, (uint8_t **) &profile.data,
+ &profile.len, 0, NULL);
+ if (ret < 0) {
+ fprintf(stderr, "Failed opening '%s': %s\n", dropped_file,
+ av_err2str(ret));
+ } else {
+ free(p->icc_name);
+ pl_icc_profile_compute_signature(&profile);
+ pl_icc_update(p->log, &p->icc, &profile, pl_icc_params(
+ .force_bpc = p->force_bpc,
+ .max_luma = p->use_icc_luma ? 0 : PL_COLOR_SDR_WHITE,
+ ));
+ av_file_unmap((void *) profile.data, profile.len);
+ if (p->icc)
+ p->icc_name = strdup(PL_BASENAME((char *) dropped_file));
+ }
+ }
+
+ if (p->icc) {
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_labelf(nk, NK_TEXT_LEFT, "Loaded: %s",
+ p->icc_name ? p->icc_name : "(unknown)");
+ reset_icc |= nk_button_label(nk, "Reset ICC");
+ nk_checkbox_label(nk, "Force BPC", &p->force_bpc);
+ nk_checkbox_label(nk, "Use detected luminance", &p->use_icc_luma);
+ }
+
+ // Apply the reset last to prevent the UI from flashing for a frame
+ if (reset) {
+ p->force_repr = (struct pl_color_repr) {0};
+ p->force_prim = PL_COLOR_PRIM_UNKNOWN;
+ p->force_trc = PL_COLOR_TRC_UNKNOWN;
+ p->force_hdr = (struct pl_hdr_metadata) {0};
+ p->force_hdr_enable = false;
+ }
+
+ if (reset_icc && p->icc) {
+ pl_icc_close(&p->icc);
+ free(p->icc_name);
+ p->icc_name = NULL;
+ }
+
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Custom shaders", NK_MINIMIZED)) {
+
+ nk_layout_row_dynamic(nk, 50, 1);
+ if (ui_widget_hover(nk, "Drop .hook/.glsl files here...") && dropped_file) {
+ uint8_t *buf;
+ size_t size;
+ int ret = av_file_map(dropped_file, &buf, &size, 0, NULL);
+ if (ret < 0) {
+ fprintf(stderr, "Failed opening '%s': %s\n", dropped_file,
+ av_err2str(ret));
+ } else {
+ const struct pl_hook *hook;
+ hook = pl_mpv_user_shader_parse(p->win->gpu, (char *) buf, size);
+ av_file_unmap(buf, size);
+ add_hook(p, hook, dropped_file);
+ }
+ }
+
+ const float px = 24.0;
+ nk_layout_row_template_begin(nk, px);
+ nk_layout_row_template_push_static(nk, px);
+ nk_layout_row_template_push_static(nk, px);
+ nk_layout_row_template_push_static(nk, px);
+ nk_layout_row_template_push_dynamic(nk);
+ nk_layout_row_template_end(nk);
+ for (int i = 0; i < p->shader_num; i++) {
+
+ if (i == 0) {
+ nk_label(nk, "·", NK_TEXT_CENTERED);
+ } else if (nk_button_symbol(nk, NK_SYMBOL_TRIANGLE_UP)) {
+ const struct pl_hook *prev_hook = p->shader_hooks[i - 1];
+ char *prev_path = p->shader_paths[i - 1];
+ p->shader_hooks[i - 1] = p->shader_hooks[i];
+ p->shader_paths[i - 1] = p->shader_paths[i];
+ p->shader_hooks[i] = prev_hook;
+ p->shader_paths[i] = prev_path;
+ }
+
+ if (i == p->shader_num - 1) {
+ nk_label(nk, "·", NK_TEXT_CENTERED);
+ } else if (nk_button_symbol(nk, NK_SYMBOL_TRIANGLE_DOWN)) {
+ const struct pl_hook *next_hook = p->shader_hooks[i + 1];
+ char *next_path = p->shader_paths[i + 1];
+ p->shader_hooks[i + 1] = p->shader_hooks[i];
+ p->shader_paths[i + 1] = p->shader_paths[i];
+ p->shader_hooks[i] = next_hook;
+ p->shader_paths[i] = next_path;
+ }
+
+ if (nk_button_symbol(nk, NK_SYMBOL_X)) {
+ pl_mpv_user_shader_destroy(&p->shader_hooks[i]);
+ free(p->shader_paths[i]);
+ p->shader_num--;
+ memmove(&p->shader_hooks[i], &p->shader_hooks[i+1],
+ (p->shader_num - i) * sizeof(void *));
+ memmove(&p->shader_paths[i], &p->shader_paths[i+1],
+ (p->shader_num - i) * sizeof(char *));
+ if (i == p->shader_num)
+ break;
+ }
+
+ if (p->shader_hooks[i]->num_parameters == 0) {
+ nk_label(nk, p->shader_paths[i], NK_TEXT_LEFT);
+ continue;
+ }
+
+ if (nk_combo_begin_label(nk, p->shader_paths[i], nk_vec2(nk_widget_width(nk), 500))) {
+ nk_layout_row_dynamic(nk, 32, 1);
+ for (int j = 0; j < p->shader_hooks[i]->num_parameters; j++) {
+ const struct pl_hook_par *hp = &p->shader_hooks[i]->parameters[j];
+ const char *name = hp->description ? hp->description : hp->name;
+ switch (hp->type) {
+ case PL_VAR_FLOAT:
+ nk_property_float(nk, name, hp->minimum.f,
+ &hp->data->f, hp->maximum.f,
+ hp->data->f / 100.0f,
+ hp->data->f / 1000.0f);
+ break;
+ case PL_VAR_SINT:
+ nk_property_int(nk, name, hp->minimum.i,
+ &hp->data->i, hp->maximum.i,
+ 1, 1.0f);
+ break;
+ case PL_VAR_UINT: {
+ int min = FFMIN(hp->minimum.u, INT_MAX);
+ int max = FFMIN(hp->maximum.u, INT_MAX);
+ int val = FFMIN(hp->data->u, INT_MAX);
+ nk_property_int(nk, name, min, &val, max, 1, 1);
+ hp->data->u = val;
+ break;
+ }
+ default: abort();
+ }
+ }
+ nk_combo_end(nk);
+ }
+ }
+
+ par->hooks = p->shader_hooks;
+ par->num_hooks = p->shader_num;
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Debug", NK_MINIMIZED)) {
+ nk_layout_row_dynamic(nk, 24, 1);
+ nk_checkbox_label(nk, "Preserve mixing cache", &par->preserve_mixing_cache);
+ nk_checkbox_label(nk, "Bypass mixing cache", &par->skip_caching_single_frame);
+ nk_checkbox_label(nk, "Show all scaler presets", &p->advanced_scalers);
+ nk_checkbox_label(nk, "Disable linear scaling", &par->disable_linear_scaling);
+ nk_checkbox_label(nk, "Disable built-in scalers", &par->disable_builtin_scalers);
+ nk_checkbox_label(nk, "Correct subpixel offsets", &par->correct_subpixel_offsets);
+ nk_checkbox_label(nk, "Force-enable dither", &par->force_dither);
+ nk_checkbox_label(nk, "Disable gamma-aware dither", &par->disable_dither_gamma_correction);
+ nk_checkbox_label(nk, "Disable FBOs / advanced rendering", &par->disable_fbos);
+ nk_checkbox_label(nk, "Force low-bit depth FBOs", &par->force_low_bit_depth_fbos);
+ nk_checkbox_label(nk, "Disable constant hard-coding", &par->dynamic_constants);
+
+ if (nk_check_label(nk, "Ignore Dolby Vision metadata", p->ignore_dovi) != p->ignore_dovi) {
+ // Flush the renderer cache on changes, since this can
+ // drastically alter the subjective appearance of the stream
+ pl_renderer_flush_cache(p->renderer);
+ p->ignore_dovi = !p->ignore_dovi;
+ }
+
+ nk_layout_row_dynamic(nk, 24, 2);
+
+ double prev_fps = p->fps;
+ bool fps_changed = nk_checkbox_label(nk, "Override display FPS", &p->fps_override);
+ nk_property_float(nk, "FPS", 10.0, &p->fps, 240.0, 5, 0.1);
+ if (fps_changed || p->fps != prev_fps)
+ p->stats.pts_interval = p->stats.vsync_interval = (struct timing) {0};
+
+ if (nk_button_label(nk, "Flush renderer cache"))
+ pl_renderer_flush_cache(p->renderer);
+ if (nk_button_label(nk, "Recreate renderer")) {
+ pl_renderer_destroy(&p->renderer);
+ p->renderer = pl_renderer_create(p->log, p->win->gpu);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Shader passes / GPU timing", NK_MINIMIZED)) {
+ nk_layout_row_dynamic(nk, 26, 1);
+ nk_label(nk, "Full frames:", NK_TEXT_LEFT);
+ for (int i = 0; i < p->num_frame_passes; i++)
+ draw_shader_pass(nk, &p->frame_info[i]);
+
+ nk_layout_row_dynamic(nk, 26, 1);
+ nk_label(nk, "Output blending:", NK_TEXT_LEFT);
+ for (int j = 0; j < MAX_BLEND_FRAMES; j++) {
+ for (int i = 0; i < p->num_blend_passes[j]; i++)
+ draw_shader_pass(nk, &p->blend_info[j][i]);
+ }
+
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Frame statistics / CPU timing", NK_MINIMIZED)) {
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_label(nk, "Current PTS:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%.3f", p->stats.current_pts);
+ nk_label(nk, "Estimated FPS:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%.3f", pl_queue_estimate_fps(p->queue));
+ nk_label(nk, "Estimated vsync rate:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%.3f", pl_queue_estimate_vps(p->queue));
+ nk_label(nk, "Frames rendered:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%"PRIu32, p->stats.rendered);
+ nk_label(nk, "Decoded frames", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%"PRIu32, atomic_load(&p->stats.decoded));
+ nk_label(nk, "Dropped frames:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%"PRIu32, p->stats.dropped);
+ nk_label(nk, "Missed timestamps:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%"PRIu32" (%.3f ms)",
+ p->stats.missed, p->stats.missed_ms);
+ nk_label(nk, "Times stalled:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%"PRIu32" (%.3f ms)",
+ p->stats.stalled, p->stats.stalled_ms);
+ draw_timing(nk, "Acquire FBO:", &p->stats.acquire);
+ draw_timing(nk, "Update queue:", &p->stats.update);
+ draw_timing(nk, "Render frame:", &p->stats.render);
+ draw_timing(nk, "Draw interface:", &p->stats.draw_ui);
+ draw_timing(nk, "Voluntary sleep:", &p->stats.sleep);
+ draw_timing(nk, "Submit frame:", &p->stats.submit);
+ draw_timing(nk, "Swap buffers:", &p->stats.swap);
+ draw_timing(nk, "Vsync interval:", &p->stats.vsync_interval);
+ draw_timing(nk, "PTS interval:", &p->stats.pts_interval);
+
+ if (nk_button_label(nk, "Reset statistics"))
+ memset(&p->stats, 0, sizeof(p->stats));
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Settings dump", NK_MINIMIZED)) {
+
+ nk_layout_row_dynamic(nk, 24, 2);
+ if (nk_button_label(nk, "Copy to clipboard"))
+ window_set_clipboard(p->win, pl_options_save(opts));
+ if (nk_button_label(nk, "Load from clipboard"))
+ pl_options_load(opts, window_get_clipboard(p->win));
+
+ nk_layout_row_dynamic(nk, 24, 1);
+ pl_options_iterate(opts, draw_opt_data, nk);
+ nk_tree_pop(nk);
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Cache statistics", NK_MINIMIZED)) {
+ nk_layout_row_dynamic(nk, 24, 2);
+ nk_label(nk, "Cached objects:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%d", pl_cache_objects(p->cache));
+ nk_label(nk, "Total size:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%zu", pl_cache_size(p->cache));
+ nk_label(nk, "Maximum total size:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%zu", p->cache->params.max_total_size);
+ nk_label(nk, "Maximum object size:", NK_TEXT_LEFT);
+ nk_labelf(nk, NK_TEXT_LEFT, "%zu", p->cache->params.max_object_size);
+
+ if (nk_button_label(nk, "Clear cache"))
+ pl_cache_reset(p->cache);
+ if (nk_button_label(nk, "Save cache")) {
+ FILE *file = fopen(p->cache_file, "wb");
+ if (file) {
+ pl_cache_save_file(p->cache, file);
+ fclose(file);
+ }
+ }
+
+ if (nk_tree_push(nk, NK_TREE_NODE, "Object list", NK_MINIMIZED)) {
+ nk_layout_row_dynamic(nk, 24, 1);
+ pl_cache_iterate(p->cache, draw_cache_line, nk);
+ nk_tree_pop(nk);
+ }
+
+ nk_tree_pop(nk);
+ }
+
+ nk_tree_pop(nk);
+ }
+ }
+ nk_end(nk);
+}
+
+#else
+void update_settings(struct plplay *p, const struct pl_frame *target) { }
+#endif // HAVE_NUKLEAR
diff --git a/demos/ui.c b/demos/ui.c
new file mode 100644
index 0000000..6cdc7c6
--- /dev/null
+++ b/demos/ui.c
@@ -0,0 +1,221 @@
+// License: CC0 / Public Domain
+
+#define NK_IMPLEMENTATION
+#include "ui.h"
+
+#include <libplacebo/dispatch.h>
+#include <libplacebo/shaders/custom.h>
+
+struct ui_vertex {
+ float pos[2];
+ float coord[2];
+ uint8_t color[4];
+};
+
+#define NUM_VERTEX_ATTRIBS 3
+
+struct ui {
+ pl_gpu gpu;
+ pl_dispatch dp;
+ struct nk_context nk;
+ struct nk_font_atlas atlas;
+ struct nk_buffer cmds, verts, idx;
+ pl_tex font_tex;
+ struct pl_vertex_attrib attribs_pl[NUM_VERTEX_ATTRIBS];
+ struct nk_draw_vertex_layout_element attribs_nk[NUM_VERTEX_ATTRIBS+1];
+ struct nk_convert_config convert_cfg;
+};
+
+struct ui *ui_create(pl_gpu gpu)
+{
+ struct ui *ui = malloc(sizeof(struct ui));
+ if (!ui)
+ return NULL;
+
+ *ui = (struct ui) {
+ .gpu = gpu,
+ .dp = pl_dispatch_create(gpu->log, gpu),
+ .attribs_pl = {
+ {
+ .name = "pos",
+ .offset = offsetof(struct ui_vertex, pos),
+ .fmt = pl_find_vertex_fmt(gpu, PL_FMT_FLOAT, 2),
+ }, {
+ .name = "coord",
+ .offset = offsetof(struct ui_vertex, coord),
+ .fmt = pl_find_vertex_fmt(gpu, PL_FMT_FLOAT, 2),
+ }, {
+ .name = "vcolor",
+ .offset = offsetof(struct ui_vertex, color),
+ .fmt = pl_find_named_fmt(gpu, "rgba8"),
+ }
+ },
+ .attribs_nk = {
+ {NK_VERTEX_POSITION, NK_FORMAT_FLOAT, offsetof(struct ui_vertex, pos)},
+ {NK_VERTEX_TEXCOORD, NK_FORMAT_FLOAT, offsetof(struct ui_vertex, coord)},
+ {NK_VERTEX_COLOR, NK_FORMAT_R8G8B8A8, offsetof(struct ui_vertex, color)},
+ {NK_VERTEX_LAYOUT_END}
+ },
+ .convert_cfg = {
+ .vertex_layout = ui->attribs_nk,
+ .vertex_size = sizeof(struct ui_vertex),
+ .vertex_alignment = NK_ALIGNOF(struct ui_vertex),
+ .shape_AA = NK_ANTI_ALIASING_ON,
+ .line_AA = NK_ANTI_ALIASING_ON,
+ .circle_segment_count = 22,
+ .curve_segment_count = 22,
+ .arc_segment_count = 22,
+ .global_alpha = 1.0f,
+ },
+ };
+
+ // Initialize font atlas using built-in font
+ nk_font_atlas_init_default(&ui->atlas);
+ nk_font_atlas_begin(&ui->atlas);
+ struct nk_font *font = nk_font_atlas_add_default(&ui->atlas, 20, NULL);
+ struct pl_tex_params tparams = {
+ .format = pl_find_named_fmt(gpu, "r8"),
+ .sampleable = true,
+ .initial_data = nk_font_atlas_bake(&ui->atlas, &tparams.w, &tparams.h,
+ NK_FONT_ATLAS_ALPHA8),
+ .debug_tag = PL_DEBUG_TAG,
+ };
+ ui->font_tex = pl_tex_create(gpu, &tparams);
+ nk_font_atlas_end(&ui->atlas, nk_handle_ptr((void *) ui->font_tex),
+ &ui->convert_cfg.tex_null);
+ nk_font_atlas_cleanup(&ui->atlas);
+
+ if (!ui->font_tex)
+ goto error;
+
+ // Initialize nuklear state
+ if (!nk_init_default(&ui->nk, &font->handle)) {
+ fprintf(stderr, "NK: failed initializing UI!\n");
+ goto error;
+ }
+
+ nk_buffer_init_default(&ui->cmds);
+ nk_buffer_init_default(&ui->verts);
+ nk_buffer_init_default(&ui->idx);
+
+ return ui;
+
+error:
+ ui_destroy(&ui);
+ return NULL;
+}
+
+void ui_destroy(struct ui **ptr)
+{
+ struct ui *ui = *ptr;
+ if (!ui)
+ return;
+
+ nk_buffer_free(&ui->cmds);
+ nk_buffer_free(&ui->verts);
+ nk_buffer_free(&ui->idx);
+ nk_free(&ui->nk);
+ nk_font_atlas_clear(&ui->atlas);
+ pl_tex_destroy(ui->gpu, &ui->font_tex);
+ pl_dispatch_destroy(&ui->dp);
+
+ free(ui);
+ *ptr = NULL;
+}
+
+void ui_update_input(struct ui *ui, const struct window *win)
+{
+ int x, y;
+ window_get_cursor(win, &x, &y);
+ nk_input_begin(&ui->nk);
+ nk_input_motion(&ui->nk, x, y);
+ nk_input_button(&ui->nk, NK_BUTTON_LEFT, x, y, window_get_button(win, BTN_LEFT));
+ nk_input_button(&ui->nk, NK_BUTTON_RIGHT, x, y, window_get_button(win, BTN_RIGHT));
+ nk_input_button(&ui->nk, NK_BUTTON_MIDDLE, x, y, window_get_button(win, BTN_MIDDLE));
+ struct nk_vec2 scroll;
+ window_get_scroll(win, &scroll.x, &scroll.y);
+ nk_input_scroll(&ui->nk, scroll);
+ nk_input_end(&ui->nk);
+}
+
+struct nk_context *ui_get_context(struct ui *ui)
+{
+ return &ui->nk;
+}
+
+bool ui_draw(struct ui *ui, const struct pl_swapchain_frame *frame)
+{
+ if (nk_convert(&ui->nk, &ui->cmds, &ui->verts, &ui->idx, &ui->convert_cfg) != NK_CONVERT_SUCCESS) {
+ fprintf(stderr, "NK: failed converting draw commands!\n");
+ return false;
+ }
+
+ const struct nk_draw_command *cmd = NULL;
+ const uint8_t *vertices = nk_buffer_memory(&ui->verts);
+ const nk_draw_index *indices = nk_buffer_memory(&ui->idx);
+ nk_draw_foreach(cmd, &ui->nk, &ui->cmds) {
+ if (!cmd->elem_count)
+ continue;
+
+ pl_shader sh = pl_dispatch_begin(ui->dp);
+ pl_shader_custom(sh, &(struct pl_custom_shader) {
+ .description = "nuklear UI",
+ .body = "color = textureLod(ui_tex, coord, 0.0).r * vcolor;",
+ .output = PL_SHADER_SIG_COLOR,
+ .num_descriptors = 1,
+ .descriptors = &(struct pl_shader_desc) {
+ .desc = {
+ .name = "ui_tex",
+ .type = PL_DESC_SAMPLED_TEX,
+ },
+ .binding = {
+ .object = cmd->texture.ptr,
+ .sample_mode = PL_TEX_SAMPLE_NEAREST,
+ },
+ },
+ });
+
+ struct pl_color_repr repr = frame->color_repr;
+ pl_shader_color_map_ex(sh, NULL, pl_color_map_args(
+ .src = pl_color_space_srgb,
+ .dst = frame->color_space,
+ ));
+ pl_shader_encode_color(sh, &repr);
+
+ bool ok = pl_dispatch_vertex(ui->dp, pl_dispatch_vertex_params(
+ .shader = &sh,
+ .target = frame->fbo,
+ .blend_params = &pl_alpha_overlay,
+ .scissors = {
+ .x0 = cmd->clip_rect.x,
+ .y0 = cmd->clip_rect.y,
+ .x1 = cmd->clip_rect.x + cmd->clip_rect.w,
+ .y1 = cmd->clip_rect.y + cmd->clip_rect.h,
+ },
+ .vertex_attribs = ui->attribs_pl,
+ .num_vertex_attribs = NUM_VERTEX_ATTRIBS,
+ .vertex_stride = sizeof(struct ui_vertex),
+ .vertex_position_idx = 0,
+ .vertex_coords = PL_COORDS_ABSOLUTE,
+ .vertex_flipped = frame->flipped,
+ .vertex_type = PL_PRIM_TRIANGLE_LIST,
+ .vertex_count = cmd->elem_count,
+ .vertex_data = vertices,
+ .index_data = indices,
+ .index_fmt = PL_INDEX_UINT32,
+ ));
+
+ if (!ok) {
+ fprintf(stderr, "placebo: failed rendering UI!\n");
+ return false;
+ }
+
+ indices += cmd->elem_count;
+ }
+
+ nk_clear(&ui->nk);
+ nk_buffer_clear(&ui->cmds);
+ nk_buffer_clear(&ui->verts);
+ nk_buffer_clear(&ui->idx);
+ return true;
+}
diff --git a/demos/ui.h b/demos/ui.h
new file mode 100644
index 0000000..9344e68
--- /dev/null
+++ b/demos/ui.h
@@ -0,0 +1,59 @@
+// License: CC0 / Public Domain
+#pragma once
+
+#define NK_INCLUDE_FIXED_TYPES
+#define NK_INCLUDE_DEFAULT_ALLOCATOR
+#define NK_INCLUDE_STANDARD_IO
+#define NK_INCLUDE_STANDARD_BOOL
+#define NK_INCLUDE_STANDARD_VARARGS
+#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
+#define NK_INCLUDE_FONT_BAKING
+#define NK_INCLUDE_DEFAULT_FONT
+#define NK_BUTTON_TRIGGER_ON_RELEASE
+#define NK_UINT_DRAW_INDEX
+#include <nuklear.h>
+
+#include "common.h"
+#include "window.h"
+
+struct ui;
+
+struct ui *ui_create(pl_gpu gpu);
+void ui_destroy(struct ui **ui);
+
+// Update/Logic/Draw cycle
+void ui_update_input(struct ui *ui, const struct window *window);
+struct nk_context *ui_get_context(struct ui *ui);
+bool ui_draw(struct ui *ui, const struct pl_swapchain_frame *frame);
+
+// Helper function to draw a custom widget for drag&drop operations, returns
+// true if the widget is hovered
+static inline bool ui_widget_hover(struct nk_context *nk, const char *label)
+{
+ struct nk_rect bounds;
+ if (!nk_widget(&bounds, nk))
+ return false;
+
+ struct nk_command_buffer *canvas = nk_window_get_canvas(nk);
+ bool hover = nk_input_is_mouse_hovering_rect(&nk->input, bounds);
+
+ float h, s, v;
+ nk_color_hsv_f(&h, &s, &v, nk->style.window.background);
+ struct nk_color background = nk_hsv_f(h, s, v + (hover ? 0.1f : -0.02f));
+ struct nk_color border = nk_hsv_f(h, s, v + 0.20f);
+ nk_fill_rect(canvas, bounds, 0.0f, background);
+ nk_stroke_rect(canvas, bounds, 0.0f, 2.0f, border);
+
+ const float pad = 10.0f;
+ struct nk_rect text = {
+ .x = bounds.x + pad,
+ .y = bounds.y + pad,
+ .w = bounds.w - 2 * pad,
+ .h = bounds.h - 2 * pad,
+ };
+
+ nk_draw_text(canvas, text, label, nk_strlen(label), nk->style.font,
+ background, nk->style.text.color);
+
+ return hover;
+}
diff --git a/demos/utils.c b/demos/utils.c
new file mode 100644
index 0000000..7c95d00
--- /dev/null
+++ b/demos/utils.c
@@ -0,0 +1,49 @@
+// License: CC0 / Public Domain
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utils.h"
+#include "../src/os.h"
+
+#ifdef PL_HAVE_WIN32
+#include <shlobj.h>
+#else
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <pwd.h>
+#endif
+
+const char *get_cache_dir(char (*buf)[512])
+{
+ // Check if XDG_CACHE_HOME is set for Linux/BSD
+ const char* xdg_cache_home = getenv("XDG_CACHE_HOME");
+ if (xdg_cache_home)
+ return xdg_cache_home;
+
+#ifdef _WIN32
+ const char* local_app_data = getenv("LOCALAPPDATA");
+ if (local_app_data)
+ return local_app_data;
+#endif
+
+#ifdef __APPLE__
+ struct passwd* pw = getpwuid(getuid());
+ if (pw) {
+ int ret = snprintf(*buf, sizeof(*buf), "%s/%s", pw->pw_dir, "Library/Caches");
+ if (ret > 0 && ret < sizeof(*buf))
+ return *buf;
+ }
+#endif
+
+ const char* home = getenv("HOME");
+ if (home) {
+ int ret = snprintf(*buf, sizeof(*buf), "%s/.cache", home);
+ if (ret > 0 && ret < sizeof(*buf))
+ return *buf;
+ }
+
+ return NULL;
+}
diff --git a/demos/utils.h b/demos/utils.h
new file mode 100644
index 0000000..e6650c3
--- /dev/null
+++ b/demos/utils.h
@@ -0,0 +1,5 @@
+// License: CC0 / Public Domain
+#pragma once
+#include "common.h"
+
+const char *get_cache_dir(char (*buf)[512]);
diff --git a/demos/video-filtering.c b/demos/video-filtering.c
new file mode 100644
index 0000000..5881c28
--- /dev/null
+++ b/demos/video-filtering.c
@@ -0,0 +1,871 @@
+/* Presented are two hypothetical scenarios of how one might use libplacebo
+ * as something like an FFmpeg or mpv video filter. We examine two example
+ * APIs (loosely modeled after real video filtering APIs) and how each style
+ * would like to use libplacebo.
+ *
+ * For sake of a simple example, let's assume this is a debanding filter.
+ * For those of you too lazy to compile/run this file but still want to see
+ * results, these are from my machine (RX 5700 XT + 1950X, as of 2020-05-25):
+ *
+ * RADV+ACO:
+ * api1: 10000 frames in 16.328440 s => 1.632844 ms/frame (612.43 fps)
+ * render: 0.113524 ms, upload: 0.127551 ms, download: 0.146097 ms
+ * api2: 10000 frames in 5.335634 s => 0.533563 ms/frame (1874.19 fps)
+ * render: 0.064378 ms, upload: 0.000000 ms, download: 0.189719 ms
+ *
+ * AMDVLK:
+ * api1: 10000 frames in 14.921859 s => 1.492186 ms/frame (670.16 fps)
+ * render: 0.110603 ms, upload: 0.114412 ms, download: 0.115375 ms
+ * api2: 10000 frames in 4.667386 s => 0.466739 ms/frame (2142.53 fps)
+ * render: 0.030781 ms, upload: 0.000000 ms, download: 0.075237 ms
+ *
+ * You can see that AMDVLK is still better at doing texture streaming than
+ * RADV - this is because as of writing RADV still does not support
+ * asynchronous texture queues / DMA engine transfers. If we disable the
+ * `async_transfer` option with AMDVLK we get this:
+ *
+ * api1: 10000 frames in 16.087723 s => 1.608772 ms/frame (621.59 fps)
+ * render: 0.111154 ms, upload: 0.122476 ms, download: 0.133162 ms
+ * api2: 10000 frames in 6.344959 s => 0.634496 ms/frame (1576.05 fps)
+ * render: 0.031307 ms, upload: 0.000000 ms, download: 0.083520 ms
+ *
+ * License: CC0 / Public Domain
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "common.h"
+#include "pl_clock.h"
+#include "pl_thread.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#endif
+
+#include <libplacebo/dispatch.h>
+#include <libplacebo/shaders/sampling.h>
+#include <libplacebo/utils/upload.h>
+#include <libplacebo/vulkan.h>
+
+///////////////////////
+/// API definitions ///
+///////////////////////
+
+// Stuff that would be common to each API
+
+void *init(void);
+void uninit(void *priv);
+
+struct format {
+ // For simplicity let's make a few assumptions here, since configuring the
+ // texture format is not the point of this example. (In practice you can
+ // go nuts with the `utils/upload.h` helpers)
+ //
+ // - All formats contain unsigned integers only
+ // - All components have the same size in bits
+ // - All components are in the "canonical" order
+ // - All formats have power of two sizes only (2 or 4 components, not 3)
+ // - All plane strides are a multiple of the pixel size
+ int num_comps;
+ int bitdepth;
+};
+
+struct plane {
+ int subx, suby; // subsampling shift
+ struct format fmt;
+ size_t stride;
+ void *data;
+};
+
+#define MAX_PLANES 4
+
+struct image {
+ int width, height;
+ int num_planes;
+ struct plane planes[MAX_PLANES];
+
+ // For API #2, the associated mapped buffer (if any)
+ struct api2_buf *associated_buf;
+};
+
+
+// Example API design #1: synchronous, blocking, double-copy (bad!)
+//
+// In this API, `api1_filter` must immediately return with the new data.
+// This prevents parallelism on the GPU and should be avoided if possible,
+// but sometimes that's what you have to work with. So this is what it
+// would look like.
+//
+// Also, let's assume this API design reconfigures the filter chain (using
+// a blank `proxy` image every time the image format or dimensions change,
+// and doesn't expect us to fail due to format mismatches or resource
+// exhaustion afterwards.
+
+bool api1_reconfig(void *priv, const struct image *proxy);
+bool api1_filter(void *priv, struct image *dst, struct image *src);
+
+
+// Example API design #2: asynchronous, streaming, queued, zero-copy (good!)
+//
+// In this API, `api2_process` will run by the calling code every so often
+// (e.g. when new data is available or expected). This function has access
+// to non-blocking functions `get_image` and `put_image` that interface
+// with the video filtering engine's internal queueing system.
+//
+// This API is also designed to feed multiple frames ahead of time, i.e.
+// it will feed us as many frames as it can while we're still returning
+// `API2_WANT_MORE`. To drain the filter chain, it would continue running
+// the process function until `API2_HAVE_MORE` is no longer present
+// in the output.
+//
+// This API is also designed to do zero-copy where possible. When it wants
+// to create a data buffer of a given size, it will call our function
+// `api2_alloc` which will return a buffer that we can process directly.
+// We can use this to do zero-copy uploading to the GPU, by creating
+// host-visible persistently mapped buffers. In order to prevent the video
+// filtering system from re-using our buffers while copies are happening, we
+// use special functions `image_lock` and `image_unlock` to increase a
+// refcount on the image's backing storage. (As is typical of such APIs)
+//
+// Finally, this API is designed to be fully dynamic: The image parameters
+// could change at any time, and we must be equipped to handle that.
+
+enum api2_status {
+ // Negative values are used to signal error conditions
+ API2_ERR_FMT = -2, // incompatible / unsupported format
+ API2_ERR_UNKNOWN = -1, // some other error happened
+ API2_OK = 0, // no error, no status - everything's good
+
+ // Positive values represent a mask of status conditions
+ API2_WANT_MORE = (1 << 0), // we want more frames, please feed some more!
+ API2_HAVE_MORE = (1 << 1), // we have more frames but they're not ready
+};
+
+enum api2_status api2_process(void *priv);
+
+// Functions for creating persistently mapped buffers
+struct api2_buf {
+ void *data;
+ size_t size;
+ void *priv;
+};
+
+bool api2_alloc(void *priv, size_t size, struct api2_buf *out);
+void api2_free(void *priv, const struct api2_buf *buf);
+
+// These functions are provided by the API. The exact details of how images
+// are enqueued, dequeued and locked are not really important here, so just
+// do something unrealistic but simple to demonstrate with.
+struct image *get_image(void);
+void put_image(struct image *img);
+void image_lock(struct image *img);
+void image_unlock(struct image *img);
+
+
+/////////////////////////////////
+/// libplacebo implementation ///
+/////////////////////////////////
+
+
+// For API #2:
+#define PARALLELISM 8
+
+struct entry {
+ pl_buf buf; // to stream the download
+ pl_tex tex_in[MAX_PLANES];
+ pl_tex tex_out[MAX_PLANES];
+ struct image image;
+
+ // For entries that are associated with a held image, so we can unlock them
+ // as soon as possible
+ struct image *held_image;
+ pl_buf held_buf;
+};
+
+// For both APIs:
+struct priv {
+ pl_log log;
+ pl_vulkan vk;
+ pl_gpu gpu;
+ pl_dispatch dp;
+ pl_shader_obj dither_state;
+
+ // Timer objects
+ pl_timer render_timer;
+ pl_timer upload_timer;
+ pl_timer download_timer;
+ uint64_t render_sum;
+ uint64_t upload_sum;
+ uint64_t download_sum;
+ int render_count;
+ int upload_count;
+ int download_count;
+
+ // API #1: A simple pair of input and output textures
+ pl_tex tex_in[MAX_PLANES];
+ pl_tex tex_out[MAX_PLANES];
+
+ // API #2: A ring buffer of textures/buffers for streaming
+ int idx_in; // points the next free entry
+ int idx_out; // points to the first entry still in progress
+ struct entry entries[PARALLELISM];
+};
+
+void *init(void) {
+ struct priv *p = calloc(1, sizeof(struct priv));
+ if (!p)
+ return NULL;
+
+ p->log = pl_log_create(PL_API_VER, pl_log_params(
+ .log_cb = pl_log_simple,
+ .log_level = PL_LOG_WARN,
+ ));
+
+ p->vk = pl_vulkan_create(p->log, pl_vulkan_params(
+ // Note: This is for API #2. In API #1 you could just pass params=NULL
+ // and it wouldn't really matter much.
+ .async_transfer = true,
+ .async_compute = true,
+ .queue_count = PARALLELISM,
+ ));
+
+ if (!p->vk) {
+ fprintf(stderr, "Failed creating vulkan context\n");
+ goto error;
+ }
+
+ // Give this a shorter name for convenience
+ p->gpu = p->vk->gpu;
+
+ p->dp = pl_dispatch_create(p->log, p->gpu);
+ if (!p->dp) {
+ fprintf(stderr, "Failed creating shader dispatch object\n");
+ goto error;
+ }
+
+ p->render_timer = pl_timer_create(p->gpu);
+ p->upload_timer = pl_timer_create(p->gpu);
+ p->download_timer = pl_timer_create(p->gpu);
+
+ return p;
+
+error:
+ uninit(p);
+ return NULL;
+}
+
+void uninit(void *priv)
+{
+ struct priv *p = priv;
+
+ // API #1
+ for (int i = 0; i < MAX_PLANES; i++) {
+ pl_tex_destroy(p->gpu, &p->tex_in[i]);
+ pl_tex_destroy(p->gpu, &p->tex_out[i]);
+ }
+
+ // API #2
+ for (int i = 0; i < PARALLELISM; i++) {
+ pl_buf_destroy(p->gpu, &p->entries[i].buf);
+ for (int j = 0; j < MAX_PLANES; j++) {
+ pl_tex_destroy(p->gpu, &p->entries[i].tex_in[j]);
+ pl_tex_destroy(p->gpu, &p->entries[i].tex_out[j]);
+ }
+ if (p->entries[i].held_image)
+ image_unlock(p->entries[i].held_image);
+ }
+
+ pl_timer_destroy(p->gpu, &p->render_timer);
+ pl_timer_destroy(p->gpu, &p->upload_timer);
+ pl_timer_destroy(p->gpu, &p->download_timer);
+
+ pl_shader_obj_destroy(&p->dither_state);
+ pl_dispatch_destroy(&p->dp);
+ pl_vulkan_destroy(&p->vk);
+ pl_log_destroy(&p->log);
+
+ free(p);
+}
+
+// Helper function to set up the `pl_plane_data` struct from the image params
+static void setup_plane_data(const struct image *img,
+ struct pl_plane_data out[MAX_PLANES])
+{
+ for (int i = 0; i < img->num_planes; i++) {
+ const struct plane *plane = &img->planes[i];
+
+ out[i] = (struct pl_plane_data) {
+ .type = PL_FMT_UNORM,
+ .width = img->width >> plane->subx,
+ .height = img->height >> plane->suby,
+ .pixel_stride = plane->fmt.num_comps * plane->fmt.bitdepth / 8,
+ .row_stride = plane->stride,
+ .pixels = plane->data,
+ };
+
+ // For API 2 (direct rendering)
+ if (img->associated_buf) {
+ pl_buf buf = img->associated_buf->priv;
+ out[i].pixels = NULL;
+ out[i].buf = buf;
+ out[i].buf_offset = (uintptr_t) plane->data - (uintptr_t) buf->data;
+ }
+
+ for (int c = 0; c < plane->fmt.num_comps; c++) {
+ out[i].component_size[c] = plane->fmt.bitdepth;
+ out[i].component_pad[c] = 0;
+ out[i].component_map[c] = c;
+ }
+ }
+}
+
+static bool do_plane(struct priv *p, pl_tex dst, pl_tex src)
+{
+ int new_depth = dst->params.format->component_depth[0];
+
+ // Do some debanding, and then also make sure to dither to the new depth
+ // so that our debanded gradients are actually preserved well
+ pl_shader sh = pl_dispatch_begin(p->dp);
+ pl_shader_deband(sh, pl_sample_src( .tex = src ), NULL);
+ pl_shader_dither(sh, new_depth, &p->dither_state, NULL);
+ return pl_dispatch_finish(p->dp, pl_dispatch_params(
+ .shader = &sh,
+ .target = dst,
+ .timer = p->render_timer,
+ ));
+}
+
+static void check_timers(struct priv *p)
+{
+ uint64_t ret;
+
+ while ((ret = pl_timer_query(p->gpu, p->render_timer))) {
+ p->render_sum += ret;
+ p->render_count++;
+ }
+
+ while ((ret = pl_timer_query(p->gpu, p->upload_timer))) {
+ p->upload_sum += ret;
+ p->upload_count++;
+ }
+
+ while ((ret = pl_timer_query(p->gpu, p->download_timer))) {
+ p->download_sum += ret;
+ p->download_count++;
+ }
+}
+
+// API #1 implementation:
+//
+// In this design, we will create all GPU resources inside `reconfig`, based on
+// the texture format configured from the proxy image. This will avoid failing
+// later on due to e.g. resource exhaustion or texture format mismatch, and
+// thereby falls within the intended semantics of this style of API.
+
+bool api1_reconfig(void *priv, const struct image *proxy)
+{
+ struct priv *p = priv;
+ struct pl_plane_data data[MAX_PLANES];
+ setup_plane_data(proxy, data);
+
+ for (int i = 0; i < proxy->num_planes; i++) {
+ pl_fmt fmt = pl_plane_find_fmt(p->gpu, NULL, &data[i]);
+ if (!fmt) {
+ fprintf(stderr, "Failed configuring filter: no good texture format!\n");
+ return false;
+ }
+
+ bool ok = true;
+ ok &= pl_tex_recreate(p->gpu, &p->tex_in[i], pl_tex_params(
+ .w = data[i].width,
+ .h = data[i].height,
+ .format = fmt,
+ .sampleable = true,
+ .host_writable = true,
+ ));
+
+ ok &= pl_tex_recreate(p->gpu, &p->tex_out[i], pl_tex_params(
+ .w = data[i].width,
+ .h = data[i].height,
+ .format = fmt,
+ .renderable = true,
+ .host_readable = true,
+ ));
+
+ if (!ok) {
+ fprintf(stderr, "Failed creating GPU textures!\n");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool api1_filter(void *priv, struct image *dst, struct image *src)
+{
+ struct priv *p = priv;
+ struct pl_plane_data data[MAX_PLANES];
+ setup_plane_data(src, data);
+
+ // Upload planes
+ for (int i = 0; i < src->num_planes; i++) {
+ bool ok = pl_tex_upload(p->gpu, pl_tex_transfer_params(
+ .tex = p->tex_in[i],
+ .row_pitch = data[i].row_stride,
+ .ptr = src->planes[i].data,
+ .timer = p->upload_timer,
+ ));
+
+ if (!ok) {
+ fprintf(stderr, "Failed uploading data to the GPU!\n");
+ return false;
+ }
+ }
+
+ // Process planes
+ for (int i = 0; i < src->num_planes; i++) {
+ if (!do_plane(p, p->tex_out[i], p->tex_in[i])) {
+ fprintf(stderr, "Failed processing planes!\n");
+ return false;
+ }
+ }
+
+ // Download planes
+ for (int i = 0; i < src->num_planes; i++) {
+ bool ok = pl_tex_download(p->gpu, pl_tex_transfer_params(
+ .tex = p->tex_out[i],
+ .row_pitch = dst->planes[i].stride,
+ .ptr = dst->planes[i].data,
+ .timer = p->download_timer,
+ ));
+
+ if (!ok) {
+ fprintf(stderr, "Failed downloading data from the GPU!\n");
+ return false;
+ }
+ }
+
+ check_timers(p);
+ return true;
+}
+
+
+// API #2 implementation:
+//
+// In this implementation we maintain a queue (implemented as ring buffer)
+// of "work entries", which are isolated structs that hold independent GPU
+// resources - so that the GPU has no cross-entry dependencies on any of the
+// textures or other resources. (Side note: It still has a dependency on the
+// dither state, but this is just a shared LUT anyway)
+
+// Align up to the nearest multiple of a power of two
+#define ALIGN2(x, align) (((x) + (align) - 1) & ~((align) - 1))
+
+static enum api2_status submit_work(struct priv *p, struct entry *e,
+ struct image *img)
+{
+ // If the image comes from a mapped buffer, we have to take a lock
+ // while our upload is in progress
+ if (img->associated_buf) {
+ assert(!e->held_image);
+ image_lock(img);
+ e->held_image = img;
+ e->held_buf = img->associated_buf->priv;
+ }
+
+ // Upload this image's data
+ struct pl_plane_data data[MAX_PLANES];
+ setup_plane_data(img, data);
+
+ for (int i = 0; i < img->num_planes; i++) {
+ pl_fmt fmt = pl_plane_find_fmt(p->gpu, NULL, &data[i]);
+ if (!fmt)
+ return API2_ERR_FMT;
+
+ // FIXME: can we plumb a `pl_timer` in here somehow?
+ if (!pl_upload_plane(p->gpu, NULL, &e->tex_in[i], &data[i]))
+ return API2_ERR_UNKNOWN;
+
+ // Re-create the target FBO as well with this format if necessary
+ bool ok = pl_tex_recreate(p->gpu, &e->tex_out[i], pl_tex_params(
+ .w = data[i].width,
+ .h = data[i].height,
+ .format = fmt,
+ .renderable = true,
+ .host_readable = true,
+ ));
+ if (!ok)
+ return API2_ERR_UNKNOWN;
+ }
+
+ // Dispatch the work for this image
+ for (int i = 0; i < img->num_planes; i++) {
+ if (!do_plane(p, e->tex_out[i], e->tex_in[i]))
+ return API2_ERR_UNKNOWN;
+ }
+
+ // Set up the resulting `struct image` that will hold our target
+ // data. We just copy the format etc. from the source image
+ memcpy(&e->image, img, sizeof(struct image));
+
+ size_t offset[MAX_PLANES], stride[MAX_PLANES], total_size = 0;
+ for (int i = 0; i < img->num_planes; i++) {
+ // For performance, we want to make sure we align the stride
+ // to a multiple of the GPU's preferred texture transfer stride
+ // (This is entirely optional)
+ stride[i] = ALIGN2(img->planes[i].stride,
+ p->gpu->limits.align_tex_xfer_pitch);
+ int height = img->height >> img->planes[i].suby;
+
+ // Round up the offset to the nearest multiple of the optimal
+ // transfer alignment. (This is also entirely optional)
+ offset[i] = ALIGN2(total_size, p->gpu->limits.align_tex_xfer_offset);
+ total_size = offset[i] + stride[i] * height;
+ }
+
+ // Dispatch the asynchronous download into a mapped buffer
+ bool ok = pl_buf_recreate(p->gpu, &e->buf, pl_buf_params(
+ .size = total_size,
+ .host_mapped = true,
+ ));
+ if (!ok)
+ return API2_ERR_UNKNOWN;
+
+ for (int i = 0; i < img->num_planes; i++) {
+ ok = pl_tex_download(p->gpu, pl_tex_transfer_params(
+ .tex = e->tex_out[i],
+ .row_pitch = stride[i],
+ .buf = e->buf,
+ .buf_offset = offset[i],
+ .timer = p->download_timer,
+ ));
+ if (!ok)
+ return API2_ERR_UNKNOWN;
+
+ // Update the output fields
+ e->image.planes[i].data = e->buf->data + offset[i];
+ e->image.planes[i].stride = stride[i];
+ }
+
+ // Make sure this work starts processing in the background, and especially
+ // so we can move on to the next queue on the gPU
+ pl_gpu_flush(p->gpu);
+ return API2_OK;
+}
+
+enum api2_status api2_process(void *priv)
+{
+ struct priv *p = priv;
+ enum api2_status ret = 0;
+
+ // Opportunistically release any held images. We do this across the ring
+ // buffer, rather than doing this as part of the following loop, because
+ // we want to release images ahead-of-time (no FIFO constraints)
+ for (int i = 0; i < PARALLELISM; i++) {
+ struct entry *e = &p->entries[i];
+ if (e->held_image && !pl_buf_poll(p->gpu, e->held_buf, 0)) {
+ // upload buffer is no longer in use, release it
+ image_unlock(e->held_image);
+ e->held_image = NULL;
+ e->held_buf = NULL;
+ }
+ }
+
+ // Poll the status of existing entries and dequeue the ones that are done
+ while (p->idx_out != p->idx_in) {
+ struct entry *e = &p->entries[p->idx_out];
+ if (pl_buf_poll(p->gpu, e->buf, 0))
+ break;
+
+ if (e->held_image) {
+ image_unlock(e->held_image);
+ e->held_image = NULL;
+ e->held_buf = NULL;
+ }
+
+ // download buffer is no longer busy, dequeue the frame
+ put_image(&e->image);
+ p->idx_out = (p->idx_out + 1) % PARALLELISM;
+ }
+
+ // Fill up the queue with more work
+ int last_free_idx = (p->idx_out ? p->idx_out : PARALLELISM) - 1;
+ while (p->idx_in != last_free_idx) {
+ struct image *img = get_image();
+ if (!img) {
+ ret |= API2_WANT_MORE;
+ break;
+ }
+
+ enum api2_status err = submit_work(p, &p->entries[p->idx_in], img);
+ if (err < 0)
+ return err;
+
+ p->idx_in = (p->idx_in + 1) % PARALLELISM;
+ }
+
+ if (p->idx_out != p->idx_in)
+ ret |= API2_HAVE_MORE;
+
+ return ret;
+}
+
+bool api2_alloc(void *priv, size_t size, struct api2_buf *out)
+{
+ struct priv *p = priv;
+ if (!p->gpu->limits.buf_transfer || size > p->gpu->limits.max_mapped_size)
+ return false;
+
+ pl_buf buf = pl_buf_create(p->gpu, pl_buf_params(
+ .size = size,
+ .host_mapped = true,
+ ));
+
+ if (!buf)
+ return false;
+
+ *out = (struct api2_buf) {
+ .data = buf->data,
+ .size = size,
+ .priv = (void *) buf,
+ };
+ return true;
+}
+
+void api2_free(void *priv, const struct api2_buf *buf)
+{
+ struct priv *p = priv;
+ pl_buf plbuf = buf->priv;
+ pl_buf_destroy(p->gpu, &plbuf);
+}
+
+
+////////////////////////////////////
+/// Proof of Concept / Benchmark ///
+////////////////////////////////////
+
+#define FRAMES 10000
+
+// Let's say we're processing a 1920x1080 4:2:0 8-bit NV12 video, arbitrarily
+// with a stride aligned to 256 bytes. (For no particular reason)
+#define TEXELSZ sizeof(uint8_t)
+#define WIDTH 1920
+#define HEIGHT 1080
+#define STRIDE (ALIGN2(WIDTH, 256) * TEXELSZ)
+// Subsampled planes
+#define SWIDTH (WIDTH >> 1)
+#define SHEIGHT (HEIGHT >> 1)
+#define SSTRIDE (ALIGN2(SWIDTH, 256) * TEXELSZ)
+// Plane offsets / sizes
+#define SIZE0 (HEIGHT * STRIDE)
+#define SIZE1 (2 * SHEIGHT * SSTRIDE)
+#define OFFSET0 0
+#define OFFSET1 SIZE0
+#define BUFSIZE (OFFSET1 + SIZE1)
+
+// Skeleton of an example image
+static const struct image example_image = {
+ .width = WIDTH,
+ .height = HEIGHT,
+ .num_planes = 2,
+ .planes = {
+ {
+ .subx = 0,
+ .suby = 0,
+ .stride = STRIDE,
+ .fmt = {
+ .num_comps = 1,
+ .bitdepth = 8 * TEXELSZ,
+ },
+ }, {
+ .subx = 1,
+ .suby = 1,
+ .stride = SSTRIDE * 2,
+ .fmt = {
+ .num_comps = 2,
+ .bitdepth = 8 * TEXELSZ,
+ },
+ },
+ },
+};
+
+// API #1: Nice and simple (but slow)
+static void api1_example(void)
+{
+ struct priv *vf = init();
+ if (!vf)
+ return;
+
+ if (!api1_reconfig(vf, &example_image)) {
+ fprintf(stderr, "api1: Failed configuring video filter!\n");
+ return;
+ }
+
+ // Allocate two buffers to hold the example data, and fill the source
+ // buffer arbitrarily with a "simple" pattern. (Decoding the data into
+ // the buffer is not meant to be part of this benchmark)
+ uint8_t *srcbuf = malloc(BUFSIZE),
+ *dstbuf = malloc(BUFSIZE);
+ if (!srcbuf || !dstbuf)
+ goto done;
+
+ for (size_t i = 0; i < BUFSIZE; i++)
+ srcbuf[i] = i;
+
+ struct image src = example_image, dst = example_image;
+ src.planes[0].data = srcbuf + OFFSET0;
+ src.planes[1].data = srcbuf + OFFSET1;
+ dst.planes[0].data = dstbuf + OFFSET0;
+ dst.planes[1].data = dstbuf + OFFSET1;
+
+ const pl_clock_t start = pl_clock_now();
+
+ // Process this dummy frame a bunch of times
+ unsigned frames = 0;
+ for (frames = 0; frames < FRAMES; frames++) {
+ if (!api1_filter(vf, &dst, &src)) {
+ fprintf(stderr, "api1: Failed filtering frame... aborting\n");
+ break;
+ }
+ }
+
+ const pl_clock_t stop = pl_clock_now();
+ const float secs = pl_clock_diff(stop, start);
+
+ printf("api1: %4u frames in %1.6f s => %2.6f ms/frame (%5.2f fps)\n",
+ frames, secs, 1000 * secs / frames, frames / secs);
+
+ if (vf->render_count) {
+ printf(" render: %f ms, upload: %f ms, download: %f ms\n",
+ 1e-6 * vf->render_sum / vf->render_count,
+ vf->upload_count ? (1e-6 * vf->upload_sum / vf->upload_count) : 0.0,
+ vf->download_count ? (1e-6 * vf->download_sum / vf->download_count) : 0.0);
+ }
+
+done:
+ free(srcbuf);
+ free(dstbuf);
+ uninit(vf);
+}
+
+
+// API #2: Pretend we have some fancy pool of images.
+#define POOLSIZE (PARALLELISM + 1)
+
+static struct api2_buf buffers[POOLSIZE] = {0};
+static struct image images[POOLSIZE] = {0};
+static int refcount[POOLSIZE] = {0};
+static unsigned api2_frames_in = 0;
+static unsigned api2_frames_out = 0;
+
+static void api2_example(void)
+{
+ struct priv *vf = init();
+ if (!vf)
+ return;
+
+ // Set up a bunch of dummy images
+ for (int i = 0; i < POOLSIZE; i++) {
+ uint8_t *data;
+ images[i] = example_image;
+ if (api2_alloc(vf, BUFSIZE, &buffers[i])) {
+ data = buffers[i].data;
+ images[i].associated_buf = &buffers[i];
+ } else {
+ // Fall back in case mapped buffers are unsupported
+ fprintf(stderr, "warning: falling back to malloc, may be slow\n");
+ data = malloc(BUFSIZE);
+ }
+ // Fill with some "data" (like in API #1)
+ for (size_t n = 0; n < BUFSIZE; n++)
+ data[i] = n;
+ images[i].planes[0].data = data + OFFSET0;
+ images[i].planes[1].data = data + OFFSET1;
+ }
+
+ const pl_clock_t start = pl_clock_now();
+
+ // Just keep driving the event loop regardless of the return status
+ // until we reach the critical number of frames. (Good enough for this PoC)
+ while (api2_frames_out < FRAMES) {
+ enum api2_status ret = api2_process(vf);
+ if (ret < 0) {
+ fprintf(stderr, "api2: Failed processing... aborting\n");
+ break;
+ }
+
+ // Sleep a short time (100us) to prevent busy waiting the CPU
+ pl_thread_sleep(1e-4);
+ check_timers(vf);
+ }
+
+ const pl_clock_t stop = pl_clock_now();
+ const float secs = pl_clock_diff(stop, start);
+ printf("api2: %4u frames in %1.6f s => %2.6f ms/frame (%5.2f fps)\n",
+ api2_frames_out, secs, 1000 * secs / api2_frames_out,
+ api2_frames_out / secs);
+
+ if (vf->render_count) {
+ printf(" render: %f ms, upload: %f ms, download: %f ms\n",
+ 1e-6 * vf->render_sum / vf->render_count,
+ vf->upload_count ? (1e-6 * vf->upload_sum / vf->upload_count) : 0.0,
+ vf->download_count ? (1e-6 * vf->download_sum / vf->download_count) : 0.0);
+ }
+
+ for (int i = 0; i < POOLSIZE; i++) {
+ if (images[i].associated_buf) {
+ api2_free(vf, images[i].associated_buf);
+ } else {
+ // This is what we originally malloc'd
+ free(images[i].planes[0].data);
+ }
+ }
+
+ uninit(vf);
+}
+
+struct image *get_image(void)
+{
+ if (api2_frames_in == FRAMES)
+ return NULL; // simulate EOF, to avoid queueing up "extra" work
+
+ // if we can find a free (unlocked) image, give it that
+ for (int i = 0; i < POOLSIZE; i++) {
+ if (refcount[i] == 0) {
+ api2_frames_in++;
+ return &images[i];
+ }
+ }
+
+ return NULL; // no free image available
+}
+
+void put_image(struct image *img)
+{
+ (void)img;
+ api2_frames_out++;
+}
+
+void image_lock(struct image *img)
+{
+ int index = img - images; // cheat, for lack of having actual image management
+ refcount[index]++;
+}
+
+void image_unlock(struct image *img)
+{
+ int index = img - images;
+ refcount[index]--;
+}
+
+int main(void)
+{
+ printf("Running benchmarks...\n");
+ api1_example();
+ api2_example();
+ return 0;
+}
diff --git a/demos/window.c b/demos/window.c
new file mode 100644
index 0000000..cccffa3
--- /dev/null
+++ b/demos/window.c
@@ -0,0 +1,123 @@
+// License: CC0 / Public Domain
+
+#include <string.h>
+
+#include "common.h"
+#include "window.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#include <timeapi.h>
+#endif
+
+extern const struct window_impl win_impl_glfw_vk;
+extern const struct window_impl win_impl_glfw_gl;
+extern const struct window_impl win_impl_glfw_d3d11;
+extern const struct window_impl win_impl_sdl_vk;
+extern const struct window_impl win_impl_sdl_gl;
+
+static const struct window_impl *win_impls[] = {
+#ifdef HAVE_GLFW_VULKAN
+ &win_impl_glfw_vk,
+#endif
+#ifdef HAVE_GLFW_OPENGL
+ &win_impl_glfw_gl,
+#endif
+#ifdef HAVE_GLFW_D3D11
+ &win_impl_glfw_d3d11,
+#endif
+#ifdef HAVE_SDL_VULKAN
+ &win_impl_sdl_vk,
+#endif
+#ifdef HAVE_SDL_OPENGL
+ &win_impl_sdl_gl,
+#endif
+ NULL
+};
+
+struct window *window_create(pl_log log, const struct window_params *params)
+{
+ for (const struct window_impl **impl = win_impls; *impl; impl++) {
+ if (params->forced_impl && strcmp((*impl)->tag, params->forced_impl) != 0)
+ continue;
+
+ printf("Attempting to initialize API: %s\n", (*impl)->name);
+ struct window *win = (*impl)->create(log, params);
+ if (win) {
+#ifdef _WIN32
+ if (timeBeginPeriod(1) != TIMERR_NOERROR)
+ fprintf(stderr, "timeBeginPeriod failed!\n");
+#endif
+ return win;
+ }
+ }
+
+ if (params->forced_impl)
+ fprintf(stderr, "'%s' windowing system not compiled or supported!\n", params->forced_impl);
+ else
+ fprintf(stderr, "No windowing system / graphical API compiled or supported!\n");
+
+ exit(1);
+}
+
+void window_destroy(struct window **win)
+{
+ if (!*win)
+ return;
+
+ (*win)->impl->destroy(win);
+
+#ifdef _WIN32
+ timeEndPeriod(1);
+#endif
+}
+
+void window_poll(struct window *win, bool block)
+{
+ return win->impl->poll(win, block);
+}
+
+void window_get_cursor(const struct window *win, int *x, int *y)
+{
+ return win->impl->get_cursor(win, x, y);
+}
+
+void window_get_scroll(const struct window *win, float *dx, float *dy)
+{
+ return win->impl->get_scroll(win, dx, dy);
+}
+
+bool window_get_button(const struct window *win, enum button btn)
+{
+ return win->impl->get_button(win, btn);
+}
+
+bool window_get_key(const struct window *win, enum key key)
+{
+ return win->impl->get_key(win, key);
+}
+
+char *window_get_file(const struct window *win)
+{
+ return win->impl->get_file(win);
+}
+
+bool window_toggle_fullscreen(const struct window *win, bool fullscreen)
+{
+ return win->impl->toggle_fullscreen(win, fullscreen);
+}
+
+bool window_is_fullscreen(const struct window *win)
+{
+ return win->impl->is_fullscreen(win);
+}
+
+const char *window_get_clipboard(const struct window *win)
+{
+ return win->impl->get_clipboard(win);
+}
+
+void window_set_clipboard(const struct window *win, const char *text)
+{
+ win->impl->set_clipboard(win, text);
+}
diff --git a/demos/window.h b/demos/window.h
new file mode 100644
index 0000000..8382860
--- /dev/null
+++ b/demos/window.h
@@ -0,0 +1,67 @@
+// License: CC0 / Public Domain
+#pragma once
+
+#include <libplacebo/swapchain.h>
+
+struct window {
+ const struct window_impl *impl;
+ pl_swapchain swapchain;
+ pl_gpu gpu;
+ bool window_lost;
+};
+
+struct window_params {
+ const char *title;
+ int width;
+ int height;
+ const char *forced_impl;
+
+ // initial color space
+ struct pl_swapchain_colors colors;
+ bool alpha;
+};
+
+struct window *window_create(pl_log log, const struct window_params *params);
+void window_destroy(struct window **win);
+
+// Poll/wait for window events
+void window_poll(struct window *win, bool block);
+
+// Input handling
+enum button {
+ BTN_LEFT,
+ BTN_RIGHT,
+ BTN_MIDDLE,
+};
+
+enum key {
+ KEY_ESC,
+};
+
+void window_get_cursor(const struct window *win, int *x, int *y);
+void window_get_scroll(const struct window *win, float *dx, float *dy);
+bool window_get_button(const struct window *win, enum button);
+bool window_get_key(const struct window *win, enum key);
+char *window_get_file(const struct window *win);
+bool window_toggle_fullscreen(const struct window *win, bool fullscreen);
+bool window_is_fullscreen(const struct window *win);
+const char *window_get_clipboard(const struct window *win);
+void window_set_clipboard(const struct window *win, const char *text);
+
+// For implementations
+struct window_impl {
+ const char *name;
+ const char *tag;
+ __typeof__(window_create) *create;
+ __typeof__(window_destroy) *destroy;
+ __typeof__(window_poll) *poll;
+ __typeof__(window_get_cursor) *get_cursor;
+ __typeof__(window_get_scroll) *get_scroll;
+ __typeof__(window_get_button) *get_button;
+ __typeof__(window_get_key) *get_key;
+ __typeof__(window_get_file) *get_file;
+ __typeof__(window_toggle_fullscreen) *toggle_fullscreen;
+ __typeof__(window_is_fullscreen) *is_fullscreen;
+ __typeof__(window_get_clipboard) *get_clipboard;
+ __typeof__(window_set_clipboard) *set_clipboard;
+};
diff --git a/demos/window_glfw.c b/demos/window_glfw.c
new file mode 100644
index 0000000..6100278
--- /dev/null
+++ b/demos/window_glfw.c
@@ -0,0 +1,536 @@
+// License: CC0 / Public Domain
+
+#if defined(USE_GL) + defined(USE_VK) + defined(USE_D3D11) != 1
+#error Specify exactly one of -DUSE_GL, -DUSE_VK or -DUSE_D3D11 when compiling!
+#endif
+
+#include <string.h>
+#include <math.h>
+
+#include "common.h"
+#include "window.h"
+
+#ifdef USE_VK
+#define VK_NO_PROTOTYPES
+#include <libplacebo/vulkan.h>
+#define GLFW_INCLUDE_VULKAN
+#define IMPL win_impl_glfw_vk
+#define IMPL_NAME "GLFW (vulkan)"
+#define IMPL_TAG "glfw-vk"
+#endif
+
+#ifdef USE_GL
+#include <libplacebo/opengl.h>
+#define IMPL win_impl_glfw_gl
+#define IMPL_NAME "GLFW (opengl)"
+#define IMPL_TAG "glfw-gl"
+#endif
+
+#ifdef USE_D3D11
+#include <libplacebo/d3d11.h>
+#define IMPL win_impl_glfw_d3d11
+#define IMPL_NAME "GLFW (D3D11)"
+#define IMPL_TAG "glfw-d3d11"
+#endif
+
+#include <GLFW/glfw3.h>
+
+#if defined(USE_GL) && defined(HAVE_EGL)
+#define GLFW_EXPOSE_NATIVE_EGL
+#include <GLFW/glfw3native.h>
+#endif
+
+#ifdef USE_D3D11
+#define GLFW_EXPOSE_NATIVE_WIN32
+#include <GLFW/glfw3native.h>
+#endif
+
+#ifdef _WIN32
+#define strdup _strdup
+#endif
+
+#ifdef NDEBUG
+#define DEBUG false
+#else
+#define DEBUG true
+#endif
+
+#define PL_ARRAY_SIZE(s) (sizeof(s) / sizeof((s)[0]))
+
+const struct window_impl IMPL;
+
+struct window_pos {
+ int x;
+ int y;
+ int w;
+ int h;
+};
+
+struct priv {
+ struct window w;
+ GLFWwindow *win;
+
+#ifdef USE_VK
+ VkSurfaceKHR surf;
+ pl_vulkan vk;
+ pl_vk_inst vk_inst;
+#endif
+
+#ifdef USE_GL
+ pl_opengl gl;
+#endif
+
+#ifdef USE_D3D11
+ pl_d3d11 d3d11;
+#endif
+
+ float scroll_dx, scroll_dy;
+ char **files;
+ size_t files_num;
+ size_t files_size;
+ bool file_seen;
+
+ struct window_pos windowed_pos;
+};
+
+static void err_cb(int code, const char *desc)
+{
+ fprintf(stderr, "GLFW err %d: %s\n", code, desc);
+}
+
+static void close_cb(GLFWwindow *win)
+{
+ struct priv *p = glfwGetWindowUserPointer(win);
+ p->w.window_lost = true;
+}
+
+static void resize_cb(GLFWwindow *win, int width, int height)
+{
+ struct priv *p = glfwGetWindowUserPointer(win);
+ if (!pl_swapchain_resize(p->w.swapchain, &width, &height)) {
+ fprintf(stderr, "libplacebo: Failed resizing swapchain? Exiting...\n");
+ p->w.window_lost = true;
+ }
+}
+
+static void scroll_cb(GLFWwindow *win, double dx, double dy)
+{
+ struct priv *p = glfwGetWindowUserPointer(win);
+ p->scroll_dx += dx;
+ p->scroll_dy += dy;
+}
+
+static void drop_cb(GLFWwindow *win, int num, const char *files[])
+{
+ struct priv *p = glfwGetWindowUserPointer(win);
+
+ for (int i = 0; i < num; i++) {
+ if (p->files_num == p->files_size) {
+ size_t new_size = p->files_size ? p->files_size * 2 : 16;
+ char **new_files = realloc(p->files, new_size * sizeof(char *));
+ if (!new_files)
+ return;
+ p->files = new_files;
+ p->files_size = new_size;
+ }
+
+ char *file = strdup(files[i]);
+ if (!file)
+ return;
+
+ p->files[p->files_num++] = file;
+ }
+}
+
+#ifdef USE_GL
+static bool make_current(void *priv)
+{
+ GLFWwindow *win = priv;
+ glfwMakeContextCurrent(win);
+ return true;
+}
+
+static void release_current(void *priv)
+{
+ glfwMakeContextCurrent(NULL);
+}
+#endif
+
+#ifdef USE_VK
+static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL get_vk_proc_addr(VkInstance instance, const char* pName)
+{
+ return (PFN_vkVoidFunction) glfwGetInstanceProcAddress(instance, pName);
+}
+#endif
+
+static struct window *glfw_create(pl_log log, const struct window_params *params)
+{
+ struct priv *p = calloc(1, sizeof(struct priv));
+ if (!p)
+ return NULL;
+
+ p->w.impl = &IMPL;
+ if (!glfwInit()) {
+ fprintf(stderr, "GLFW: Failed initializing?\n");
+ goto error;
+ }
+
+ glfwSetErrorCallback(&err_cb);
+
+#ifdef USE_VK
+ if (!glfwVulkanSupported()) {
+ fprintf(stderr, "GLFW: No vulkan support! Perhaps recompile with -DUSE_GL\n");
+ goto error;
+ }
+
+ glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+#endif // USE_VK
+
+#ifdef USE_D3D11
+ glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+#endif // USE_D3D11
+
+#ifdef USE_GL
+ struct {
+ int api;
+ int major, minor;
+ int glsl_ver;
+ int profile;
+ } gl_vers[] = {
+ { GLFW_OPENGL_API, 4, 6, 460, GLFW_OPENGL_CORE_PROFILE },
+ { GLFW_OPENGL_API, 4, 5, 450, GLFW_OPENGL_CORE_PROFILE },
+ { GLFW_OPENGL_API, 4, 4, 440, GLFW_OPENGL_CORE_PROFILE },
+ { GLFW_OPENGL_API, 4, 0, 400, GLFW_OPENGL_CORE_PROFILE },
+ { GLFW_OPENGL_API, 3, 3, 330, GLFW_OPENGL_CORE_PROFILE },
+ { GLFW_OPENGL_API, 3, 2, 150, GLFW_OPENGL_CORE_PROFILE },
+ { GLFW_OPENGL_ES_API, 3, 2, 320, },
+ { GLFW_OPENGL_API, 3, 1, 140, },
+ { GLFW_OPENGL_ES_API, 3, 1, 310, },
+ { GLFW_OPENGL_API, 3, 0, 130, },
+ { GLFW_OPENGL_ES_API, 3, 0, 300, },
+ { GLFW_OPENGL_ES_API, 2, 0, 100, },
+ { GLFW_OPENGL_API, 2, 1, 120, },
+ { GLFW_OPENGL_API, 2, 0, 110, },
+ };
+
+ for (int i = 0; i < PL_ARRAY_SIZE(gl_vers); i++) {
+ glfwWindowHint(GLFW_CLIENT_API, gl_vers[i].api);
+#ifdef HAVE_EGL
+ glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
+#endif
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, gl_vers[i].major);
+ glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, gl_vers[i].minor);
+ glfwWindowHint(GLFW_OPENGL_PROFILE, gl_vers[i].profile);
+#ifdef __APPLE__
+ if (gl_vers[i].profile == GLFW_OPENGL_CORE_PROFILE)
+ glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
+#endif
+
+#endif // USE_GL
+
+ if (params->alpha)
+ glfwWindowHint(GLFW_TRANSPARENT_FRAMEBUFFER, GLFW_TRUE);
+
+ printf("Creating %dx%d window%s...\n", params->width, params->height,
+ params->alpha ? " (with alpha)" : "");
+
+ p->win = glfwCreateWindow(params->width, params->height, params->title, NULL, NULL);
+
+#ifdef USE_GL
+ if (p->win)
+ break;
+ }
+#endif // USE_GL
+
+ if (!p->win) {
+ fprintf(stderr, "GLFW: Failed creating window\n");
+ goto error;
+ }
+
+ // Set up GLFW event callbacks
+ glfwSetWindowUserPointer(p->win, p);
+ glfwSetFramebufferSizeCallback(p->win, resize_cb);
+ glfwSetWindowCloseCallback(p->win, close_cb);
+ glfwSetScrollCallback(p->win, scroll_cb);
+ glfwSetDropCallback(p->win, drop_cb);
+
+#ifdef USE_VK
+ VkResult err;
+
+ uint32_t num;
+ p->vk_inst = pl_vk_inst_create(log, pl_vk_inst_params(
+ .get_proc_addr = get_vk_proc_addr,
+ .debug = DEBUG,
+ .extensions = glfwGetRequiredInstanceExtensions(&num),
+ .num_extensions = num,
+ ));
+
+ if (!p->vk_inst) {
+ fprintf(stderr, "libplacebo: Failed creating vulkan instance\n");
+ goto error;
+ }
+
+ err = glfwCreateWindowSurface(p->vk_inst->instance, p->win, NULL, &p->surf);
+ if (err != VK_SUCCESS) {
+ fprintf(stderr, "GLFW: Failed creating vulkan surface\n");
+ goto error;
+ }
+
+ p->vk = pl_vulkan_create(log, pl_vulkan_params(
+ .instance = p->vk_inst->instance,
+ .get_proc_addr = p->vk_inst->get_proc_addr,
+ .surface = p->surf,
+ .allow_software = true,
+ ));
+ if (!p->vk) {
+ fprintf(stderr, "libplacebo: Failed creating vulkan device\n");
+ goto error;
+ }
+
+ p->w.swapchain = pl_vulkan_create_swapchain(p->vk, pl_vulkan_swapchain_params(
+ .surface = p->surf,
+ .present_mode = VK_PRESENT_MODE_FIFO_KHR,
+ ));
+
+ if (!p->w.swapchain) {
+ fprintf(stderr, "libplacebo: Failed creating vulkan swapchain\n");
+ goto error;
+ }
+
+ p->w.gpu = p->vk->gpu;
+#endif // USE_VK
+
+#ifdef USE_GL
+ p->gl = pl_opengl_create(log, pl_opengl_params(
+ .allow_software = true,
+ .debug = DEBUG,
+#ifdef HAVE_EGL
+ .egl_display = glfwGetEGLDisplay(),
+ .egl_context = glfwGetEGLContext(p->win),
+#endif
+ .make_current = make_current,
+ .release_current = release_current,
+ .get_proc_addr = glfwGetProcAddress,
+ .priv = p->win,
+ ));
+ if (!p->gl) {
+ fprintf(stderr, "libplacebo: Failed creating opengl device\n");
+ goto error;
+ }
+
+ p->w.swapchain = pl_opengl_create_swapchain(p->gl, pl_opengl_swapchain_params(
+ .swap_buffers = (void (*)(void *)) glfwSwapBuffers,
+ .priv = p->win,
+ ));
+
+ if (!p->w.swapchain) {
+ fprintf(stderr, "libplacebo: Failed creating opengl swapchain\n");
+ goto error;
+ }
+
+ p->w.gpu = p->gl->gpu;
+#endif // USE_GL
+
+#ifdef USE_D3D11
+ p->d3d11 = pl_d3d11_create(log, pl_d3d11_params( .debug = DEBUG ));
+ if (!p->d3d11) {
+ fprintf(stderr, "libplacebo: Failed creating D3D11 device\n");
+ goto error;
+ }
+
+ p->w.swapchain = pl_d3d11_create_swapchain(p->d3d11, pl_d3d11_swapchain_params(
+ .window = glfwGetWin32Window(p->win),
+ ));
+ if (!p->w.swapchain) {
+ fprintf(stderr, "libplacebo: Failed creating D3D11 swapchain\n");
+ goto error;
+ }
+
+ p->w.gpu = p->d3d11->gpu;
+#endif // USE_D3D11
+
+ glfwGetWindowSize(p->win, &p->windowed_pos.w, &p->windowed_pos.h);
+ glfwGetWindowPos(p->win, &p->windowed_pos.x, &p->windowed_pos.y);
+
+ int w, h;
+ glfwGetFramebufferSize(p->win, &w, &h);
+ pl_swapchain_colorspace_hint(p->w.swapchain, &params->colors);
+ if (!pl_swapchain_resize(p->w.swapchain, &w, &h)) {
+ fprintf(stderr, "libplacebo: Failed initializing swapchain\n");
+ goto error;
+ }
+
+ return &p->w;
+
+error:
+ window_destroy((struct window **) &p);
+ return NULL;
+}
+
+static void glfw_destroy(struct window **window)
+{
+ struct priv *p = (struct priv *) *window;
+ if (!p)
+ return;
+
+ pl_swapchain_destroy(&p->w.swapchain);
+
+#ifdef USE_VK
+ pl_vulkan_destroy(&p->vk);
+ if (p->surf) {
+ PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR = (PFN_vkDestroySurfaceKHR)
+ p->vk_inst->get_proc_addr(p->vk_inst->instance, "vkDestroySurfaceKHR");
+ vkDestroySurfaceKHR(p->vk_inst->instance, p->surf, NULL);
+ }
+ pl_vk_inst_destroy(&p->vk_inst);
+#endif
+
+#ifdef USE_GL
+ pl_opengl_destroy(&p->gl);
+#endif
+
+#ifdef USE_D3D11
+ pl_d3d11_destroy(&p->d3d11);
+#endif
+
+ for (int i = 0; i < p->files_num; i++)
+ free(p->files[i]);
+ free(p->files);
+
+ glfwTerminate();
+ free(p);
+ *window = NULL;
+}
+
+static void glfw_poll(struct window *window, bool block)
+{
+ if (block) {
+ glfwWaitEvents();
+ } else {
+ glfwPollEvents();
+ }
+}
+
+static void glfw_get_cursor(const struct window *window, int *x, int *y)
+{
+ struct priv *p = (struct priv *) window;
+ double dx, dy;
+ int fw, fh, ww, wh;
+ glfwGetCursorPos(p->win, &dx, &dy);
+ glfwGetFramebufferSize(p->win, &fw, &fh);
+ glfwGetWindowSize(p->win, &ww, &wh);
+ *x = floor(dx * fw / ww);
+ *y = floor(dy * fh / wh);
+}
+
+static bool glfw_get_button(const struct window *window, enum button btn)
+{
+ static const int button_map[] = {
+ [BTN_LEFT] = GLFW_MOUSE_BUTTON_LEFT,
+ [BTN_RIGHT] = GLFW_MOUSE_BUTTON_RIGHT,
+ [BTN_MIDDLE] = GLFW_MOUSE_BUTTON_MIDDLE,
+ };
+
+ struct priv *p = (struct priv *) window;
+ return glfwGetMouseButton(p->win, button_map[btn]) == GLFW_PRESS;
+}
+
+static bool glfw_get_key(const struct window *window, enum key key)
+{
+ static const int key_map[] = {
+ [KEY_ESC] = GLFW_KEY_ESCAPE,
+ };
+
+ struct priv *p = (struct priv *) window;
+ return glfwGetKey(p->win, key_map[key]) == GLFW_PRESS;
+}
+
+static void glfw_get_scroll(const struct window *window, float *dx, float *dy)
+{
+ struct priv *p = (struct priv *) window;
+ *dx = p->scroll_dx;
+ *dy = p->scroll_dy;
+ p->scroll_dx = p->scroll_dy = 0.0;
+}
+
+static char *glfw_get_file(const struct window *window)
+{
+ struct priv *p = (struct priv *) window;
+ if (p->file_seen) {
+ assert(p->files_num);
+ free(p->files[0]);
+ memmove(&p->files[0], &p->files[1], --p->files_num * sizeof(char *));
+ p->file_seen = false;
+ }
+
+ if (!p->files_num)
+ return NULL;
+
+ p->file_seen = true;
+ return p->files[0];
+}
+
+static bool glfw_is_fullscreen(const struct window *window) {
+ const struct priv *p = (const struct priv *) window;
+ return glfwGetWindowMonitor(p->win);
+}
+
+static bool glfw_toggle_fullscreen(const struct window *window, bool fullscreen)
+{
+ struct priv *p = (struct priv *) window;
+ bool window_fullscreen = glfw_is_fullscreen(window);
+
+ if (window_fullscreen == fullscreen)
+ return true;
+
+ if (window_fullscreen) {
+ glfwSetWindowMonitor(p->win, NULL, p->windowed_pos.x, p->windowed_pos.y,
+ p->windowed_pos.w, p->windowed_pos.h, GLFW_DONT_CARE);
+ return true;
+ }
+
+ // For simplicity sake use primary monitor
+ GLFWmonitor *monitor = glfwGetPrimaryMonitor();
+ if (!monitor)
+ return false;
+
+ const GLFWvidmode *mode = glfwGetVideoMode(monitor);
+ if (!mode)
+ return false;
+
+ glfwGetWindowPos(p->win, &p->windowed_pos.x, &p->windowed_pos.y);
+ glfwGetWindowSize(p->win, &p->windowed_pos.w, &p->windowed_pos.h);
+ glfwSetWindowMonitor(p->win, monitor, 0, 0, mode->width, mode->height,
+ mode->refreshRate);
+
+ return true;
+}
+
+static const char *glfw_get_clipboard(const struct window *window)
+{
+ struct priv *p = (struct priv *) window;
+ return glfwGetClipboardString(p->win);
+}
+
+static void glfw_set_clipboard(const struct window *window, const char *text)
+{
+ struct priv *p = (struct priv *) window;
+ glfwSetClipboardString(p->win, text);
+}
+
+const struct window_impl IMPL = {
+ .name = IMPL_NAME,
+ .tag = IMPL_TAG,
+ .create = glfw_create,
+ .destroy = glfw_destroy,
+ .poll = glfw_poll,
+ .get_cursor = glfw_get_cursor,
+ .get_button = glfw_get_button,
+ .get_key = glfw_get_key,
+ .get_scroll = glfw_get_scroll,
+ .get_file = glfw_get_file,
+ .toggle_fullscreen = glfw_toggle_fullscreen,
+ .is_fullscreen = glfw_is_fullscreen,
+ .get_clipboard = glfw_get_clipboard,
+ .set_clipboard = glfw_set_clipboard,
+};
diff --git a/demos/window_sdl.c b/demos/window_sdl.c
new file mode 100644
index 0000000..1fd22ce
--- /dev/null
+++ b/demos/window_sdl.c
@@ -0,0 +1,404 @@
+// License: CC0 / Public Domain
+
+#if !defined(USE_GL) && !defined(USE_VK) || defined(USE_GL) && defined(USE_VK)
+#error Specify exactly one of -DUSE_GL or -DUSE_VK when compiling!
+#endif
+
+#include <SDL.h>
+
+#include "common.h"
+#include "window.h"
+
+#ifdef USE_VK
+#define VK_NO_PROTOTYPES
+#include <libplacebo/vulkan.h>
+#include <SDL_vulkan.h>
+#define WINFLAG_API SDL_WINDOW_VULKAN
+#define IMPL win_impl_sdl_vk
+#define IMPL_NAME "SDL2 (vulkan)"
+#define IMPL_TAG "sdl2-vk"
+#endif
+
+#ifdef USE_GL
+#include <libplacebo/opengl.h>
+#define WINFLAG_API SDL_WINDOW_OPENGL
+#define IMPL win_impl_sdl_gl
+#define IMPL_NAME "SDL2 (opengl)"
+#define IMPL_TAG "sdl2-gl"
+#endif
+
+#ifdef NDEBUG
+#define DEBUG false
+#else
+#define DEBUG true
+#endif
+
+const struct window_impl IMPL;
+
+struct priv {
+ struct window w;
+ SDL_Window *win;
+
+#ifdef USE_VK
+ VkSurfaceKHR surf;
+ pl_vulkan vk;
+ pl_vk_inst vk_inst;
+#endif
+
+#ifdef USE_GL
+ SDL_GLContext gl_ctx;
+ pl_opengl gl;
+#endif
+
+ int scroll_dx, scroll_dy;
+ char **files;
+ size_t files_num;
+ size_t files_size;
+ bool file_seen;
+ char *clip_text;
+};
+
+#ifdef USE_GL
+static bool make_current(void *priv)
+{
+ struct priv *p = priv;
+ return SDL_GL_MakeCurrent(p->win, p->gl_ctx) == 0;
+}
+
+static void release_current(void *priv)
+{
+ struct priv *p = priv;
+ SDL_GL_MakeCurrent(p->win, NULL);
+}
+#endif
+
+static struct window *sdl_create(pl_log log, const struct window_params *params)
+{
+ struct priv *p = calloc(1, sizeof(struct priv));
+ if (!p)
+ return NULL;
+
+ p->w.impl = &IMPL;
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ fprintf(stderr, "SDL2: Failed initializing: %s\n", SDL_GetError());
+ goto error;
+ }
+
+ uint32_t sdl_flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | WINFLAG_API;
+ p->win = SDL_CreateWindow(params->title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
+ params->width, params->height, sdl_flags);
+ if (!p->win) {
+ fprintf(stderr, "SDL2: Failed creating window: %s\n", SDL_GetError());
+ goto error;
+ }
+
+ int w, h;
+
+#ifdef USE_VK
+
+ unsigned int num = 0;
+ if (!SDL_Vulkan_GetInstanceExtensions(p->win, &num, NULL)) {
+ fprintf(stderr, "SDL2: Failed enumerating vulkan extensions: %s\n", SDL_GetError());
+ goto error;
+ }
+
+ const char **exts = malloc(num * sizeof(const char *));
+ SDL_Vulkan_GetInstanceExtensions(p->win, &num, exts);
+
+ p->vk_inst = pl_vk_inst_create(log, pl_vk_inst_params(
+ .get_proc_addr = SDL_Vulkan_GetVkGetInstanceProcAddr(),
+ .debug = DEBUG,
+ .extensions = exts,
+ .num_extensions = num,
+ ));
+ free(exts);
+ if (!p->vk_inst) {
+ fprintf(stderr, "libplacebo: Failed creating vulkan instance!\n");
+ goto error;
+ }
+
+ if (!SDL_Vulkan_CreateSurface(p->win, p->vk_inst->instance, &p->surf)) {
+ fprintf(stderr, "SDL2: Failed creating surface: %s\n", SDL_GetError());
+ goto error;
+ }
+
+ p->vk = pl_vulkan_create(log, pl_vulkan_params(
+ .instance = p->vk_inst->instance,
+ .get_proc_addr = p->vk_inst->get_proc_addr,
+ .surface = p->surf,
+ .allow_software = true,
+ ));
+ if (!p->vk) {
+ fprintf(stderr, "libplacebo: Failed creating vulkan device\n");
+ goto error;
+ }
+
+ p->w.swapchain = pl_vulkan_create_swapchain(p->vk, pl_vulkan_swapchain_params(
+ .surface = p->surf,
+ .present_mode = VK_PRESENT_MODE_FIFO_KHR,
+ ));
+
+ if (!p->w.swapchain) {
+ fprintf(stderr, "libplacebo: Failed creating vulkan swapchain\n");
+ goto error;
+ }
+
+ p->w.gpu = p->vk->gpu;
+
+ SDL_Vulkan_GetDrawableSize(p->win, &w, &h);
+#endif // USE_VK
+
+#ifdef USE_GL
+ p->gl_ctx = SDL_GL_CreateContext(p->win);
+ if (!p->gl_ctx) {
+ fprintf(stderr, "SDL2: Failed creating GL context: %s\n", SDL_GetError());
+ goto error;
+ }
+
+ p->gl = pl_opengl_create(log, pl_opengl_params(
+ .allow_software = true,
+ .debug = DEBUG,
+ .make_current = make_current,
+ .release_current = release_current,
+ .get_proc_addr = (void *) SDL_GL_GetProcAddress,
+ .priv = p,
+ ));
+ if (!p->gl) {
+ fprintf(stderr, "libplacebo: Failed creating opengl device\n");
+ goto error;
+ }
+
+ p->w.swapchain = pl_opengl_create_swapchain(p->gl, pl_opengl_swapchain_params(
+ .swap_buffers = (void (*)(void *)) SDL_GL_SwapWindow,
+ .priv = p->win,
+ ));
+
+ if (!p->w.swapchain) {
+ fprintf(stderr, "libplacebo: Failed creating opengl swapchain\n");
+ goto error;
+ }
+
+ p->w.gpu = p->gl->gpu;
+
+ SDL_GL_GetDrawableSize(p->win, &w, &h);
+#endif // USE_GL
+
+ pl_swapchain_colorspace_hint(p->w.swapchain, &params->colors);
+ if (!pl_swapchain_resize(p->w.swapchain, &w, &h)) {
+ fprintf(stderr, "libplacebo: Failed initializing swapchain\n");
+ goto error;
+ }
+
+ return &p->w;
+
+error:
+ window_destroy((struct window **) &p);
+ return NULL;
+}
+
+static void sdl_destroy(struct window **window)
+{
+ struct priv *p = (struct priv *) *window;
+ if (!p)
+ return;
+
+ pl_swapchain_destroy(&p->w.swapchain);
+
+#ifdef USE_VK
+ pl_vulkan_destroy(&p->vk);
+ if (p->surf) {
+ PFN_vkDestroySurfaceKHR vkDestroySurfaceKHR = (PFN_vkDestroySurfaceKHR)
+ p->vk_inst->get_proc_addr(p->vk_inst->instance, "vkDestroySurfaceKHR");
+ vkDestroySurfaceKHR(p->vk_inst->instance, p->surf, NULL);
+ }
+ pl_vk_inst_destroy(&p->vk_inst);
+#endif
+
+#ifdef USE_GL
+ pl_opengl_destroy(&p->gl);
+ SDL_GL_DeleteContext(p->gl_ctx);
+#endif
+
+ for (int i = 0; i < p->files_num; i++)
+ SDL_free(p->files[i]);
+ free(p->files);
+
+ SDL_free(p->clip_text);
+ SDL_DestroyWindow(p->win);
+ SDL_Quit();
+ free(p);
+ *window = NULL;
+}
+
+static inline void handle_event(struct priv *p, SDL_Event *event)
+{
+ switch (event->type) {
+ case SDL_QUIT:
+ p->w.window_lost = true;
+ return;
+
+ case SDL_WINDOWEVENT:
+ if (event->window.windowID != SDL_GetWindowID(p->win))
+ return;
+
+ if (event->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
+ int width = event->window.data1, height = event->window.data2;
+ if (!pl_swapchain_resize(p->w.swapchain, &width, &height)) {
+ fprintf(stderr, "libplacebo: Failed resizing swapchain? Exiting...\n");
+ p->w.window_lost = true;
+ }
+ }
+ return;
+
+ case SDL_MOUSEWHEEL:
+ p->scroll_dx += event->wheel.x;
+ p->scroll_dy += event->wheel.y;
+ return;
+
+ case SDL_DROPFILE:
+ if (p->files_num == p->files_size) {
+ size_t new_size = p->files_size ? p->files_size * 2 : 16;
+ char **new_files = realloc(p->files, new_size * sizeof(char *));
+ if (!new_files)
+ return;
+ p->files = new_files;
+ p->files_size = new_size;
+ }
+
+ p->files[p->files_num++] = event->drop.file;
+ return;
+ }
+}
+
+static void sdl_poll(struct window *window, bool block)
+{
+ struct priv *p = (struct priv *) window;
+ SDL_Event event;
+ int ret;
+
+ do {
+ ret = block ? SDL_WaitEvent(&event) : SDL_PollEvent(&event);
+ if (ret)
+ handle_event(p, &event);
+
+ // Only block on the first iteration
+ block = false;
+ } while (ret);
+}
+
+static void sdl_get_cursor(const struct window *window, int *x, int *y)
+{
+ SDL_GetMouseState(x, y);
+}
+
+static bool sdl_get_button(const struct window *window, enum button btn)
+{
+ static const uint32_t button_mask[] = {
+ [BTN_LEFT] = SDL_BUTTON_LMASK,
+ [BTN_RIGHT] = SDL_BUTTON_RMASK,
+ [BTN_MIDDLE] = SDL_BUTTON_MMASK,
+ };
+
+ return SDL_GetMouseState(NULL, NULL) & button_mask[btn];
+}
+
+static bool sdl_get_key(const struct window *window, enum key key)
+{
+ static const size_t key_map[] = {
+ [KEY_ESC] = SDL_SCANCODE_ESCAPE,
+ };
+
+ return SDL_GetKeyboardState(NULL)[key_map[key]];
+}
+
+static void sdl_get_scroll(const struct window *window, float *dx, float *dy)
+{
+ struct priv *p = (struct priv *) window;
+ *dx = p->scroll_dx;
+ *dy = p->scroll_dy;
+ p->scroll_dx = p->scroll_dy = 0;
+}
+
+static char *sdl_get_file(const struct window *window)
+{
+ struct priv *p = (struct priv *) window;
+ if (p->file_seen) {
+ assert(p->files_num);
+ SDL_free(p->files[0]);
+ memmove(&p->files[0], &p->files[1], --p->files_num * sizeof(char *));
+ p->file_seen = false;
+ }
+
+ if (!p->files_num)
+ return NULL;
+
+ p->file_seen = true;
+ return p->files[0];
+}
+
+static bool sdl_is_fullscreen(const struct window *window)
+{
+ const struct priv *p = (const struct priv *) window;
+ return SDL_GetWindowFlags(p->win) & SDL_WINDOW_FULLSCREEN;
+}
+
+static bool sdl_toggle_fullscreen(const struct window *window, bool fullscreen)
+{
+ struct priv *p = (struct priv *) window;
+ bool window_fullscreen = sdl_is_fullscreen(window);
+
+ if (window_fullscreen == fullscreen)
+ return true;
+
+ SDL_DisplayMode mode;
+ if (SDL_GetDesktopDisplayMode(0, &mode))
+ {
+ fprintf(stderr, "SDL2: Failed to get display mode: %s\n", SDL_GetError());
+ SDL_ClearError();
+ return false;
+ }
+
+ if (SDL_SetWindowDisplayMode(p->win, &mode))
+ {
+ fprintf(stderr, "SDL2: Failed to set window display mode: %s\n", SDL_GetError());
+ SDL_ClearError();
+ return false;
+ }
+
+ if (SDL_SetWindowFullscreen(p->win, fullscreen ? SDL_WINDOW_FULLSCREEN : 0)) {
+ fprintf(stderr, "SDL2: SetWindowFullscreen failed: %s\n", SDL_GetError());
+ SDL_ClearError();
+ return false;
+ }
+
+ return true;
+}
+
+static const char *sdl_get_clipboard(const struct window *window)
+{
+ struct priv *p = (struct priv *) window;
+ SDL_free(p->clip_text);
+ return p->clip_text = SDL_GetClipboardText();
+}
+
+static void sdl_set_clipboard(const struct window *window, const char *text)
+{
+ SDL_SetClipboardText(text);
+}
+
+const struct window_impl IMPL = {
+ .name = IMPL_NAME,
+ .tag = IMPL_TAG,
+ .create = sdl_create,
+ .destroy = sdl_destroy,
+ .poll = sdl_poll,
+ .get_cursor = sdl_get_cursor,
+ .get_button = sdl_get_button,
+ .get_key = sdl_get_key,
+ .get_scroll = sdl_get_scroll,
+ .get_file = sdl_get_file,
+ .toggle_fullscreen = sdl_toggle_fullscreen,
+ .is_fullscreen = sdl_is_fullscreen,
+ .get_clipboard = sdl_get_clipboard,
+ .set_clipboard = sdl_set_clipboard,
+};