diff options
Diffstat (limited to 'demos')
-rw-r--r-- | demos/LICENSE | 121 | ||||
-rw-r--r-- | demos/colors.c | 88 | ||||
-rw-r--r-- | demos/common.h | 11 | ||||
-rw-r--r-- | demos/meson.build | 170 | ||||
-rw-r--r-- | demos/multigpu-bench.c | 484 | ||||
-rw-r--r-- | demos/plplay.c | 766 | ||||
-rw-r--r-- | demos/plplay.h | 138 | ||||
-rw-r--r-- | demos/screenshots/plplay1.png | bin | 0 -> 25495 bytes | |||
-rw-r--r-- | demos/screenshots/plplay2.png | bin | 0 -> 21732 bytes | |||
-rw-r--r-- | demos/screenshots/plplay3.png | bin | 0 -> 23745 bytes | |||
-rw-r--r-- | demos/screenshots/plplay4.png | bin | 0 -> 22326 bytes | |||
-rw-r--r-- | demos/screenshots/plplay5.png | bin | 0 -> 22959 bytes | |||
-rw-r--r-- | demos/screenshots/plplay6.png | bin | 0 -> 25061 bytes | |||
-rw-r--r-- | demos/sdlimage.c | 281 | ||||
-rw-r--r-- | demos/settings.c | 1238 | ||||
-rw-r--r-- | demos/ui.c | 221 | ||||
-rw-r--r-- | demos/ui.h | 59 | ||||
-rw-r--r-- | demos/utils.c | 49 | ||||
-rw-r--r-- | demos/utils.h | 5 | ||||
-rw-r--r-- | demos/video-filtering.c | 871 | ||||
-rw-r--r-- | demos/window.c | 123 | ||||
-rw-r--r-- | demos/window.h | 67 | ||||
-rw-r--r-- | demos/window_glfw.c | 536 | ||||
-rw-r--r-- | demos/window_sdl.c | 404 |
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, ¶ms); + 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 Binary files differnew file mode 100644 index 0000000..ce84d89 --- /dev/null +++ b/demos/screenshots/plplay1.png diff --git a/demos/screenshots/plplay2.png b/demos/screenshots/plplay2.png Binary files differnew file mode 100644 index 0000000..ae88051 --- /dev/null +++ b/demos/screenshots/plplay2.png diff --git a/demos/screenshots/plplay3.png b/demos/screenshots/plplay3.png Binary files differnew file mode 100644 index 0000000..9ec4126 --- /dev/null +++ b/demos/screenshots/plplay3.png diff --git a/demos/screenshots/plplay4.png b/demos/screenshots/plplay4.png Binary files differnew file mode 100644 index 0000000..873be13 --- /dev/null +++ b/demos/screenshots/plplay4.png diff --git a/demos/screenshots/plplay5.png b/demos/screenshots/plplay5.png Binary files differnew file mode 100644 index 0000000..c23d609 --- /dev/null +++ b/demos/screenshots/plplay5.png diff --git a/demos/screenshots/plplay6.png b/demos/screenshots/plplay6.png Binary files differnew file mode 100644 index 0000000..15ea8fc --- /dev/null +++ b/demos/screenshots/plplay6.png 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, ¶ms); +} + +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, °, 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, ¶ms->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, ¶ms->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, +}; |