1 /* Very basic video player based on ffmpeg. All it does is render a single
2  * video stream to completion, and then exits. It exits on most errors, rather
3  * than gracefully trying to recreate the context.
4  *
5  * The timing code is also rather naive, due to the current lack of
6  * presentation feedback. That being said, an effort is made to time the video
7  * stream to the system clock, using frame mixing for mismatches.
8  *
9  * License: CC0 / Public Domain
10  */
11 
12 #include <pthread.h>
13 
14 #include <libavutil/cpu.h>
15 #include <libavutil/file.h>
16 #include <libavutil/pixdesc.h>
17 #include <libavformat/avformat.h>
18 #include <libavcodec/avcodec.h>
19 
20 #include "common.h"
21 #include "utils.h"
22 #include "window.h"
23 
24 #ifdef HAVE_NUKLEAR
25 #include "ui.h"
26 #else
27 struct ui;
ui_destroy(struct ui ** ui)28 static void ui_destroy(struct ui **ui) {}
ui_draw(struct ui * ui,const struct pl_swapchain_frame * frame)29 static bool ui_draw(struct ui *ui, const struct pl_swapchain_frame *frame) { return true; };
30 #endif
31 
32 #include <libplacebo/renderer.h>
33 #include <libplacebo/shaders/lut.h>
34 #include <libplacebo/utils/libav.h>
35 #include <libplacebo/utils/frame_queue.h>
36 
37 #define MAX_FRAME_PASSES 256
38 #define MAX_BLEND_FRAMES 8
39 
40 struct pass_info {
41     struct pl_dispatch_info pass;
42     char *name;
43 };
44 
45 struct plplay {
46     struct window *win;
47     struct ui *ui;
48 
49     // libplacebo
50     pl_log log;
51     pl_renderer renderer;
52     pl_queue queue;
53 
54     // libav*
55     AVFormatContext *format;
56     AVCodecContext *codec;
57     const AVStream *stream; // points to first video stream of `format`
58     pthread_t decoder_thread;
59     bool decoder_thread_created;
60     bool exit_thread;
61 
62     // settings / ui state
63     const struct pl_filter_preset *upscaler, *downscaler, *frame_mixer;
64     struct pl_render_params params;
65     struct pl_deband_params deband_params;
66     struct pl_sigmoid_params sigmoid_params;
67     struct pl_color_adjustment color_adjustment;
68     struct pl_peak_detect_params peak_detect_params;
69     struct pl_color_map_params color_map_params;
70     struct pl_dither_params dither_params;
71     struct pl_cone_params cone_params;
72     struct pl_color_space target_color;
73     struct pl_color_repr target_repr;
74     bool target_override;
75     bool levels_override;
76 
77     // custom shaders
78     const struct pl_hook **shader_hooks;
79     char **shader_paths;
80     size_t shader_num;
81     size_t shader_size;
82 
83     // pass metadata
84     struct pass_info blend_info[MAX_BLEND_FRAMES];
85     struct pass_info frame_info[MAX_FRAME_PASSES];
86     int num_frame_passes;
87 };
88 
uninit(struct plplay * p)89 static void uninit(struct plplay *p)
90 {
91     if (p->decoder_thread_created) {
92         p->exit_thread = true;
93         pl_queue_push(p->queue, NULL); // Signal EOF to wake up thread
94         pthread_join(p->decoder_thread, NULL);
95     }
96 
97     pl_queue_destroy(&p->queue);
98     pl_renderer_destroy(&p->renderer);
99 
100     for (int i = 0; i < p->shader_num; i++) {
101         pl_mpv_user_shader_destroy(&p->shader_hooks[i]);
102         free(p->shader_paths[i]);
103     }
104 
105     free(p->shader_hooks);
106     free(p->shader_paths);
107 
108     // Free this before destroying the window to release associated GPU buffers
109     avcodec_free_context(&p->codec);
110     avformat_free_context(p->format);
111 
112     ui_destroy(&p->ui);
113     window_destroy(&p->win);
114 
115     pl_log_destroy(&p->log);
116     *p = (struct plplay) {0};
117 }
118 
open_file(struct plplay * p,const char * filename)119 static bool open_file(struct plplay *p, const char *filename)
120 {
121     printf("Opening file: '%s'\n", filename);
122     if (avformat_open_input(&p->format, filename, NULL, NULL) != 0) {
123         fprintf(stderr, "libavformat: Failed opening file!\n");
124         return false;
125     }
126 
127     printf("Format: %s\n", p->format->iformat->name);
128     printf("Duration: %.3f s\n", p->format->duration / 1e6);
129 
130     if (avformat_find_stream_info(p->format,  NULL) < 0) {
131         fprintf(stderr, "libavformat: Failed finding stream info!\n");
132         return false;
133     }
134 
135     // Find "best" video stream
136     int stream_idx =
137         av_find_best_stream(p->format, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
138 
139     if (stream_idx < 0) {
140         fprintf(stderr, "plplay: File contains no video streams?\n");
141         return false;
142     }
143 
144     const AVStream *stream = p->format->streams[stream_idx];
145     const AVCodecParameters *par = stream->codecpar;
146     printf("Found video track (stream %d)\n", stream_idx);
147     printf("Resolution: %d x %d\n", par->width, par->height);
148     printf("FPS: %f\n", av_q2d(stream->avg_frame_rate));
149     printf("Bitrate: %"PRIi64" kbps\n", par->bit_rate / 1000);
150     printf("Format: %s\n", av_get_pix_fmt_name(par->format));
151 
152     p->stream = stream;
153     return true;
154 }
155 
is_file_hdr(struct plplay * p)156 static inline bool is_file_hdr(struct plplay *p)
157 {
158     assert(p->stream);
159     enum AVColorTransferCharacteristic trc = p->stream->codecpar->color_trc;
160     return pl_color_transfer_is_hdr(pl_transfer_from_av(trc));
161 }
162 
init_codec(struct plplay * p)163 static bool init_codec(struct plplay *p)
164 {
165     assert(p->stream);
166 
167     const AVCodec *codec = avcodec_find_decoder(p->stream->codecpar->codec_id);
168     if (!codec) {
169         fprintf(stderr, "libavcodec: Failed finding matching codec\n");
170         return false;
171     }
172 
173     p->codec = avcodec_alloc_context3(codec);
174     if (!p->codec) {
175         fprintf(stderr, "libavcodec: Failed allocating codec\n");
176         return false;
177     }
178 
179     if (avcodec_parameters_to_context(p->codec, p->stream->codecpar) < 0) {
180         fprintf(stderr, "libavcodec: Failed copying codec parameters to codec\n");
181         return false;
182     }
183 
184     p->codec->thread_count = av_cpu_count();
185     p->codec->get_buffer2 = pl_get_buffer2;
186     p->codec->opaque = &p->win->gpu;
187 #if LIBAVCODEC_VERSION_MAJOR < 60
188     p->codec->thread_safe_callbacks = 1;
189 #endif
190 
191     if (avcodec_open2(p->codec, codec, NULL) < 0) {
192         fprintf(stderr, "libavcodec: Failed opening codec\n");
193         return false;
194     }
195 
196     return true;
197 }
198 
map_frame(pl_gpu gpu,pl_tex * tex,const struct pl_source_frame * src,struct pl_frame * out_frame)199 static bool map_frame(pl_gpu gpu, pl_tex *tex,
200                       const struct pl_source_frame *src,
201                       struct pl_frame *out_frame)
202 {
203     if (!pl_upload_avframe(gpu, out_frame, tex, src->frame_data)) {
204         fprintf(stderr, "Failed uploading AVFrame!\n");
205         return false;
206     }
207 
208     out_frame->user_data = src->frame_data;
209     return true;
210 }
211 
unmap_frame(pl_gpu gpu,struct pl_frame * frame,const struct pl_source_frame * src)212 static void unmap_frame(pl_gpu gpu, struct pl_frame *frame,
213                         const struct pl_source_frame *src)
214 {
215     av_frame_free((AVFrame **) &src->frame_data);
216 }
217 
discard_frame(const struct pl_source_frame * src)218 static void discard_frame(const struct pl_source_frame *src)
219 {
220     av_frame_free((AVFrame **) &src->frame_data);
221     printf("Dropped frame with PTS %.3f\n", src->pts);
222 }
223 
decode_loop(void * arg)224 static void *decode_loop(void *arg)
225 {
226     int ret;
227     struct plplay *p = arg;
228     AVPacket *packet = av_packet_alloc();
229     AVFrame *frame = av_frame_alloc();
230     if (!frame || !packet)
231         goto done;
232 
233     double start_pts = 0.0;
234     bool first_frame = true;
235 
236     while (!p->exit_thread) {
237         switch ((ret = av_read_frame(p->format, packet))) {
238         case 0:
239             if (packet->stream_index != p->stream->index) {
240                 // Ignore unrelated packets
241                 av_packet_unref(packet);
242                 continue;
243             }
244             ret = avcodec_send_packet(p->codec, packet);
245             av_packet_unref(packet);
246             break;
247         case AVERROR_EOF:
248             // Send empty input to flush decoder
249             ret = avcodec_send_packet(p->codec, NULL);
250             break;
251         default:
252             fprintf(stderr, "libavformat: Failed reading packet: %s\n",
253                     av_err2str(ret));
254             goto done;
255         }
256 
257         if (ret < 0) {
258             fprintf(stderr, "libavcodec: Failed sending packet to decoder: %s\n",
259                     av_err2str(ret));
260             goto done;
261         }
262 
263         // Decode all frames from this packet
264         while ((ret = avcodec_receive_frame(p->codec, frame)) == 0) {
265             double pts = frame->pts * av_q2d(p->stream->time_base);
266             if (first_frame) {
267                 start_pts = pts;
268                 first_frame = false;
269             }
270 
271             pl_queue_push_block(p->queue, UINT64_MAX, &(struct pl_source_frame) {
272                 .pts = pts - start_pts,
273                 .map = map_frame,
274                 .unmap = unmap_frame,
275                 .discard = discard_frame,
276                 .frame_data = frame,
277             });
278             frame = av_frame_alloc();
279         }
280 
281         switch (ret) {
282         case AVERROR(EAGAIN):
283             continue;
284         case AVERROR_EOF:
285             goto done;
286         default:
287             fprintf(stderr, "libavcodec: Failed decoding frame: %s\n",
288                     av_err2str(ret));
289             goto done;
290         }
291     }
292 
293 done:
294     pl_queue_push(p->queue, NULL); // Signal EOF to flush queue
295     av_packet_free(&packet);
296     av_frame_free(&frame);
297     return NULL;
298 }
299 
300 static void update_settings(struct plplay *p);
301 
update_colorspace_hint(struct plplay * p,const struct pl_frame_mix * mix)302 static void update_colorspace_hint(struct plplay *p, const struct pl_frame_mix *mix)
303 {
304     const struct pl_frame *frame = NULL;
305 
306     for (int i = 0; i < mix->num_frames; i++) {
307         if (mix->timestamps[i] > 0.0)
308             break;
309         frame = mix->frames[i];
310     }
311 
312     if (!frame)
313         return;
314 
315     struct pl_swapchain_colors hint;
316     pl_swapchain_colors_from_avframe(&hint, frame->user_data);
317     pl_swapchain_colorspace_hint(p->win->swapchain, &hint);
318 }
319 
render_frame(struct plplay * p,const struct pl_swapchain_frame * frame,const struct pl_frame_mix * mix)320 static bool render_frame(struct plplay *p, const struct pl_swapchain_frame *frame,
321                          const struct pl_frame_mix *mix)
322 {
323     struct pl_frame target;
324     pl_frame_from_swapchain(&target, frame);
325     update_settings(p);
326 
327     // Update the global settings based on this swapchain frame, then use those
328     pl_color_space_merge(&p->target_color, &target.color);
329     pl_color_repr_merge(&p->target_repr, &target.repr);
330     if (p->target_override) {
331         target.color = p->target_color;
332         target.repr = p->target_repr;
333     }
334 
335     assert(mix->num_frames);
336     const AVFrame *avframe = mix->frames[0]->user_data;
337     double dar = pl_rect2df_aspect(&mix->frames[0]->crop);
338     if (avframe->sample_aspect_ratio.num)
339         dar *= av_q2d(avframe->sample_aspect_ratio);
340     pl_rect2df_aspect_set(&target.crop, dar, 0.0);
341 
342     if (!pl_render_image_mix(p->renderer, mix, &target, &p->params))
343         return false;
344 
345     if (!ui_draw(p->ui, frame))
346         return false;
347 
348     return true;
349 }
350 
render_loop(struct plplay * p)351 static bool render_loop(struct plplay *p)
352 {
353     struct pl_queue_params qparams = {
354         .radius = pl_frame_mix_radius(&p->params),
355         .frame_duration = av_q2d(av_inv_q(p->stream->avg_frame_rate)),
356         .interpolation_threshold = 0.01,
357         .timeout = UINT64_MAX,
358     };
359 
360     // Initialize the frame queue, blocking indefinitely until done
361     struct pl_frame_mix mix;
362     switch (pl_queue_update(p->queue, &mix, &qparams)) {
363     case PL_QUEUE_OK:  break;
364     case PL_QUEUE_EOF: return true;
365     case PL_QUEUE_ERR: goto error;
366     default: abort();
367     }
368 
369     struct pl_swapchain_frame frame;
370     update_colorspace_hint(p, &mix);
371     if (!pl_swapchain_start_frame(p->win->swapchain, &frame))
372         goto error;
373     if (!render_frame(p, &frame, &mix))
374         goto error;
375     if (!pl_swapchain_submit_frame(p->win->swapchain))
376         goto error;
377 
378     // Wait until rendering is complete. Do this before measuring the time
379     // start, to ensure we don't count initialization overhead as part of the
380     // first vsync.
381     pl_gpu_finish(p->win->gpu);
382 
383     double ts, ts_prev;
384     if (!utils_gettime(&ts_prev))
385         goto error;
386 
387     pl_swapchain_swap_buffers(p->win->swapchain);
388     window_poll(p->win, false);
389 
390     double pts = 0.0;
391     bool stuck = false;
392 
393     while (!p->win->window_lost) {
394         if (window_get_key(p->win, KEY_ESC))
395             break;
396 
397         update_colorspace_hint(p, &mix);
398         if (!pl_swapchain_start_frame(p->win->swapchain, &frame)) {
399             // Window stuck/invisible? Block for events and try again.
400             window_poll(p->win, true);
401             continue;
402         }
403 
404 retry:
405         if (!utils_gettime(&ts))
406             goto error;
407 
408         if (!stuck) {
409             pts += (ts - ts_prev);
410         }
411         ts_prev = ts;
412 
413         qparams.timeout = 50000000; // 50 ms
414         qparams.pts = pts;
415 
416         switch (pl_queue_update(p->queue, &mix, &qparams)) {
417         case PL_QUEUE_ERR: goto error;
418         case PL_QUEUE_EOF: return true;
419         case PL_QUEUE_OK:
420             if (!render_frame(p, &frame, &mix))
421                 goto error;
422             stuck = false;
423             break;
424         case PL_QUEUE_MORE:
425             stuck = true;
426             goto retry;
427         }
428 
429         if (!pl_swapchain_submit_frame(p->win->swapchain)) {
430             fprintf(stderr, "libplacebo: failed presenting frame!\n");
431             goto error;
432         }
433 
434         pl_swapchain_swap_buffers(p->win->swapchain);
435         window_poll(p->win, false);
436     }
437 
438     return true;
439 
440 error:
441     fprintf(stderr, "Render loop failed, exiting early...\n");
442     return false;
443 }
444 
info_callback(void * priv,const struct pl_render_info * info)445 static void info_callback(void *priv, const struct pl_render_info *info)
446 {
447     struct plplay *p = priv;
448     struct pass_info *pass;
449     switch (info->stage) {
450     case PL_RENDER_STAGE_FRAME:
451         if (info->index >= MAX_FRAME_PASSES)
452             return;
453         p->num_frame_passes = info->index + 1;
454         pass = &p->frame_info[info->index];
455         break;
456 
457     case PL_RENDER_STAGE_BLEND:
458         if (info->index >= MAX_BLEND_FRAMES)
459             return;
460         pass = &p->blend_info[info->index];
461         break;
462 
463     case PL_RENDER_STAGE_COUNT: abort();
464     }
465 
466     free(pass->name);
467     pass->name = strdup(info->pass->shader->description);
468     pass->pass = *info->pass;
469 }
470 
main(int argc,char ** argv)471 int main(int argc, char **argv)
472 {
473     const char *filename;
474     enum pl_log_level log_level = PL_LOG_INFO;
475 
476     if (argc == 3 && strcmp(argv[1], "-v") == 0) {
477         filename = argv[2];
478         log_level = PL_LOG_DEBUG;
479     } else if (argc == 2) {
480         filename = argv[1];
481     } else {
482         fprintf(stderr, "Usage: ./%s [-v] <filename>\n", argv[0]);
483         return -1;
484     }
485 
486     struct plplay state = {
487         .params = pl_render_default_params,
488         .deband_params = pl_deband_default_params,
489         .sigmoid_params = pl_sigmoid_default_params,
490         .color_adjustment = pl_color_adjustment_neutral,
491         .peak_detect_params = pl_peak_detect_default_params,
492         .color_map_params = pl_color_map_default_params,
493         .dither_params = pl_dither_default_params,
494         .cone_params = pl_vision_normal,
495     };
496 
497     // Redirect all of the pointers in `params.default` to instead point to the
498     // structs inside `struct plplay`, so we can adjust them using the UI
499 #define DEFAULT_PARAMS(field) \
500         state.params.field = state.params.field ? &state.field : NULL
501     DEFAULT_PARAMS(deband_params);
502     DEFAULT_PARAMS(sigmoid_params);
503     DEFAULT_PARAMS(peak_detect_params);
504     DEFAULT_PARAMS(dither_params);
505     state.params.color_adjustment = &state.color_adjustment;
506     state.params.color_map_params = &state.color_map_params;
507     state.params.cone_params = &state.cone_params;
508 
509     // Enable dynamic parameters by default, due to plplay's heavy reliance on
510     // GUI controls for dynamically adjusting render parameters.
511     state.params.dynamic_constants = true;
512 
513     // Hook up our pass info callback
514     state.params.info_callback = info_callback;
515     state.params.info_priv = &state;
516 
517     struct plplay *p = &state;
518     if (!open_file(p, filename))
519         goto error;
520 
521     const AVCodecParameters *par = p->stream->codecpar;
522     const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(par->format);
523     if (!desc)
524         goto error;
525 
526     struct window_params params = {
527         .title = "plplay",
528         .width = par->width,
529         .height = par->height,
530         .colors = {
531             .primaries = pl_primaries_from_av(par->color_primaries),
532             .transfer = pl_transfer_from_av(par->color_trc),
533             // HDR metadata will come from AVFrame side data
534         },
535     };
536 
537     if (desc->flags & AV_PIX_FMT_FLAG_ALPHA) {
538         params.alpha = true;
539         state.params.background_transparency = 1.0;
540     }
541 
542     p->log = pl_log_create(PL_API_VER, &(struct pl_log_params) {
543         .log_cb = pl_log_color,
544         .log_level = log_level,
545     });
546 
547     p->win = window_create(p->log, &params);
548     if (!p->win)
549         goto error;
550 
551     // Test the AVPixelFormat against the GPU capabilities
552     if (!pl_test_pixfmt(p->win->gpu, par->format)) {
553         fprintf(stderr, "Unsupported AVPixelFormat: %s\n", desc->name);
554         goto error;
555     }
556 
557 #ifdef HAVE_NUKLEAR
558     p->ui = ui_create(p->win->gpu);
559     if (!p->ui)
560         goto error;
561 
562     // Find the right named filter entries for the defaults
563     const struct pl_filter_preset *f;
564     for (f = pl_scale_filters; f->name; f++) {
565         if (p->params.upscaler == f->filter)
566             p->upscaler = f;
567         if (p->params.downscaler == f->filter)
568             p->downscaler = f;
569     }
570 
571     for (f = pl_frame_mixers; f->name; f++) {
572         if (p->params.frame_mixer == f->filter)
573             p->frame_mixer = f;
574     }
575 
576     assert(p->upscaler && p->downscaler && p->frame_mixer);
577 #endif
578 
579     if (!init_codec(p))
580         goto error;
581 
582     p->queue = pl_queue_create(p->win->gpu);
583     int ret = pthread_create(&p->decoder_thread, NULL, decode_loop, p);
584     if (ret != 0) {
585         fprintf(stderr, "Failed creating decode thread: %s\n", strerror(errno));
586         goto error;
587     }
588 
589     p->decoder_thread_created = true;
590 
591     p->renderer = pl_renderer_create(p->log, p->win->gpu);
592     if (!render_loop(p))
593         goto error;
594 
595     printf("Exiting...\n");
596     uninit(p);
597     return 0;
598 
599 error:
600     uninit(p);
601     return 1;
602 }
603 
604 #ifdef HAVE_NUKLEAR
605 
add_hook(struct plplay * p,const struct pl_hook * hook,const char * path)606 static void add_hook(struct plplay *p, const struct pl_hook *hook, const char *path)
607 {
608     if (!hook)
609         return;
610 
611     if (p->shader_num == p->shader_size) {
612         // Grow array if needed
613         size_t new_size = p->shader_size ? p->shader_size * 2 : 16;
614         void *new_hooks = realloc(p->shader_hooks, new_size * sizeof(void *));
615         if (!new_hooks)
616             goto error;
617         p->shader_hooks = new_hooks;
618         char **new_paths = realloc(p->shader_paths, new_size * sizeof(char *));
619         if (!new_paths)
620             goto error;
621         p->shader_paths = new_paths;
622         p->shader_size = new_size;
623     }
624 
625     // strip leading path
626     while (true) {
627         const char *fname = strchr(path, '/');
628         if (!fname)
629             break;
630         path = fname + 1;
631     }
632 
633     char *path_copy = strdup(path);
634     if (!path_copy)
635         goto error;
636 
637     p->shader_hooks[p->shader_num] = hook;
638     p->shader_paths[p->shader_num] = path_copy;
639     p->shader_num++;
640     return;
641 
642 error:
643     pl_mpv_user_shader_destroy(&hook);
644 }
645 
update_settings(struct plplay * p)646 static void update_settings(struct plplay *p)
647 {
648     struct nk_context *nk = ui_get_context(p->ui);
649     enum nk_panel_flags win_flags = NK_WINDOW_BORDER | NK_WINDOW_MOVABLE |
650                                     NK_WINDOW_SCALABLE | NK_WINDOW_MINIMIZABLE |
651                                     NK_WINDOW_TITLE;
652 
653     ui_update_input(p->ui, p->win);
654     const char *dropped_file = window_get_file(p->win);
655 
656     const struct pl_filter_preset *f;
657     struct pl_render_params *par = &p->params;
658 
659     if (nk_begin(nk, "Settings", nk_rect(100, 100, 600, 600), win_flags)) {
660 
661         struct nk_colorf bg = {
662             par->background_color[0],
663             par->background_color[1],
664             par->background_color[2],
665             1.0 - par->background_transparency,
666         };
667 
668         nk_layout_row_dynamic(nk, 24, 2);
669         nk_label(nk, "Background color:", NK_TEXT_LEFT);
670         if (nk_combo_begin_color(nk, nk_rgb_cf(bg), nk_vec2(nk_widget_width(nk), 300))) {
671             nk_layout_row_dynamic(nk, 200, 1);
672             nk_color_pick(nk, &bg, NK_RGBA);
673             nk_combo_end(nk);
674 
675             par->background_color[0] = bg.r;
676             par->background_color[1] = bg.g;
677             par->background_color[2] = bg.b;
678             par->background_transparency = 1.0 - bg.a;
679         }
680 
681         if (nk_tree_push(nk, NK_TREE_NODE, "Image scaling", NK_MAXIMIZED)) {
682             nk_layout_row(nk, NK_DYNAMIC, 24, 2, (float[]){ 0.3, 0.7 });
683             nk_label(nk, "Upscaler:", NK_TEXT_LEFT);
684             if (nk_combo_begin_label(nk, p->upscaler->description, nk_vec2(nk_widget_width(nk), 500))) {
685                 nk_layout_row_dynamic(nk, 16, 1);
686                 for (f = pl_scale_filters; f->name; f++) {
687                     if (!f->description)
688                         continue;
689                     if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
690                         p->upscaler = f;
691                 }
692                 par->upscaler = p->upscaler->filter;
693                 nk_combo_end(nk);
694             }
695 
696             nk_label(nk, "Downscaler:", NK_TEXT_LEFT);
697             if (nk_combo_begin_label(nk, p->downscaler->description, nk_vec2(nk_widget_width(nk), 500))) {
698                 nk_layout_row_dynamic(nk, 16, 1);
699                 for (f = pl_scale_filters; f->name; f++) {
700                     if (!f->description)
701                         continue;
702                     if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
703                         p->downscaler = f;
704                 }
705                 par->downscaler = p->downscaler->filter;
706                 nk_combo_end(nk);
707             }
708 
709             nk_label(nk, "Frame mixing:", NK_TEXT_LEFT);
710             if (nk_combo_begin_label(nk, p->frame_mixer->description, nk_vec2(nk_widget_width(nk), 300))) {
711                 nk_layout_row_dynamic(nk, 16, 1);
712                 for (f = pl_frame_mixers; f->name; f++) {
713                     if (!f->description)
714                         continue;
715                     if (nk_combo_item_label(nk, f->description, NK_TEXT_LEFT))
716                         p->frame_mixer = f;
717                 }
718                 par->frame_mixer = p->frame_mixer->filter;
719                 nk_combo_end(nk);
720             }
721 
722             nk_layout_row_dynamic(nk, 24, 2);
723             par->skip_anti_aliasing = !nk_check_label(nk, "Anti-aliasing", !par->skip_anti_aliasing);
724             nk_property_float(nk, "Antiringing", 0, &par->antiringing_strength, 1.0, 0.1, 0.01);
725             nk_property_int(nk, "LUT precision", 0, &par->lut_entries, 256, 1, 1);
726 
727             float cutoff = par->polar_cutoff * 100.0;
728             nk_property_float(nk, "Polar cutoff (%)", 0.0, &cutoff, 100.0, 0.1, 0.01);
729             par->polar_cutoff = cutoff / 100.0;
730 
731             struct pl_sigmoid_params *spar = &p->sigmoid_params;
732             nk_layout_row_dynamic(nk, 24, 2);
733             par->sigmoid_params = nk_check_label(nk, "Sigmoidization", par->sigmoid_params) ? spar : NULL;
734             if (nk_button_label(nk, "Default values"))
735                 *spar = pl_sigmoid_default_params;
736             nk_property_float(nk, "Sigmoid center", 0, &spar->center, 1, 0.1, 0.01);
737             nk_property_float(nk, "Sigmoid slope", 0, &spar->slope, 100, 1, 0.1);
738             nk_tree_pop(nk);
739         }
740 
741         if (nk_tree_push(nk, NK_TREE_NODE, "Debanding", NK_MINIMIZED)) {
742             struct pl_deband_params *dpar = &p->deband_params;
743             nk_layout_row_dynamic(nk, 24, 2);
744             par->deband_params = nk_check_label(nk, "Enable", par->deband_params) ? dpar : NULL;
745             if (nk_button_label(nk, "Reset settings"))
746                 *dpar = pl_deband_default_params;
747             nk_property_int(nk, "Iterations", 0, &dpar->iterations, 8, 1, 0);
748             nk_property_float(nk, "Threshold", 0, &dpar->threshold, 256, 1, 0.5);
749             nk_property_float(nk, "Radius", 0, &dpar->radius, 256, 1, 0.2);
750             nk_property_float(nk, "Grain", 0, &dpar->grain, 512, 1, 0.5);
751             nk_tree_pop(nk);
752         }
753 
754         if (nk_tree_push(nk, NK_TREE_NODE, "Color adjustment", NK_MINIMIZED)) {
755             struct pl_color_adjustment *adj = &p->color_adjustment;
756             nk_layout_row_dynamic(nk, 24, 2);
757             par->color_adjustment = nk_check_label(nk, "Enable", par->color_adjustment) ? adj : NULL;
758             if (nk_button_label(nk, "Default values"))
759                 *adj = pl_color_adjustment_neutral;
760             nk_property_float(nk, "Brightness", -1, &adj->brightness, 1, 0.1, 0.005);
761             nk_property_float(nk, "Contrast", 0, &adj->contrast, 10, 0.1, 0.005);
762 
763             // Convert to (cyclical) degrees for display
764             int deg = roundf(adj->hue * 180.0 / M_PI);
765             nk_property_int(nk, "Hue (°)", -50, &deg, 400, 1, 1);
766             adj->hue = ((deg + 360) % 360) * M_PI / 180.0;
767 
768             nk_property_float(nk, "Saturation", 0, &adj->saturation, 10, 0.1, 0.005);
769             nk_property_float(nk, "Gamma", 0, &adj->gamma, 10, 0.1, 0.005);
770 
771             // Convert to human-friendly temperature values for display
772             int temp = (int) roundf(adj->temperature * 3500) + 6500;
773             nk_property_int(nk, "Temperature (K)", 3000, &temp, 10000, 10, 5);
774             adj->temperature = (temp - 6500) / 3500.0;
775 
776             struct pl_cone_params *cpar = &p->cone_params;
777             nk_layout_row_dynamic(nk, 24, 2);
778             par->cone_params = nk_check_label(nk, "Color blindness", par->cone_params) ? cpar : NULL;
779             if (nk_button_label(nk, "Default values"))
780                 *cpar = pl_vision_normal;
781             nk_layout_row(nk, NK_DYNAMIC, 24, 5, (float[]){ 0.25, 0.25/3, 0.25/3, 0.25/3, 0.5 });
782             nk_label(nk, "Cone model:", NK_TEXT_LEFT);
783             int cones = cpar->cones;
784             nk_checkbox_flags_label(nk, "L", &cones, PL_CONE_L);
785             nk_checkbox_flags_label(nk, "M", &cones, PL_CONE_M);
786             nk_checkbox_flags_label(nk, "S", &cones, PL_CONE_S);
787             cpar->cones = cones;
788             nk_property_float(nk, "Sensitivity", 0.0, &cpar->strength, 5.0, 0.1, 0.01);
789             nk_tree_pop(nk);
790         }
791 
792         if (is_file_hdr(p)) {
793             if (nk_tree_push(nk, NK_TREE_NODE, "HDR peak detection", NK_MINIMIZED)) {
794                 struct pl_peak_detect_params *ppar = &p->peak_detect_params;
795                 nk_layout_row_dynamic(nk, 24, 2);
796                 par->peak_detect_params = nk_check_label(nk, "Enable", par->peak_detect_params) ? ppar : NULL;
797                 if (nk_button_label(nk, "Reset settings"))
798                     *ppar = pl_peak_detect_default_params;
799                 nk_property_float(nk, "Threshold low", 0.0, &ppar->scene_threshold_low, 20.0, 0.5, 0.005);
800                 nk_property_float(nk, "Threshold high", 0.0, &ppar->scene_threshold_high, 20.0, 0.5, 0.005);
801                 nk_property_float(nk, "Smoothing period", 1.0, &ppar->smoothing_period, 1000.0, 5.0, 1.0);
802                 nk_property_float(nk, "Minimum peak", 0.0, &ppar->minimum_peak, 10.0, 0.1, 0.01);
803 
804                 int overshoot = roundf(ppar->overshoot_margin * 100.0);
805                 nk_property_int(nk, "Overshoot (%)", 0, &overshoot, 200, 1, 1);
806                 ppar->overshoot_margin = overshoot / 100.0;
807                 nk_tree_pop(nk);
808             }
809         }
810 
811         if (nk_tree_push(nk, NK_TREE_NODE, "Tone mapping", NK_MINIMIZED)) {
812             struct pl_color_map_params *cpar = &p->color_map_params;
813             nk_layout_row_dynamic(nk, 24, 2);
814             par->color_map_params = nk_check_label(nk, "Enable", par->color_map_params) ? cpar : NULL;
815             if (nk_button_label(nk, "Reset settings"))
816                 *cpar = pl_color_map_default_params;
817 
818             static const char *rendering_intents[4] = {
819                 [PL_INTENT_PERCEPTUAL]              = "Perceptual",
820                 [PL_INTENT_RELATIVE_COLORIMETRIC]   = "Relative colorimetric",
821                 [PL_INTENT_SATURATION]              = "Saturation",
822                 [PL_INTENT_ABSOLUTE_COLORIMETRIC]   = "Absolute colorimetric",
823             };
824 
825             nk_label(nk, "Rendering intent:", NK_TEXT_LEFT);
826             cpar->intent = nk_combo(nk, rendering_intents, 4, cpar->intent,
827                                     16, nk_vec2(nk_widget_width(nk), 100));
828 
829             static const char *tone_mapping_algos[PL_TONE_MAPPING_ALGORITHM_COUNT] = {
830                 [PL_TONE_MAPPING_CLIP]              = "Clip",
831                 [PL_TONE_MAPPING_MOBIUS]            = "Mobius",
832                 [PL_TONE_MAPPING_REINHARD]          = "Reinhard",
833                 [PL_TONE_MAPPING_HABLE]             = "Hable",
834                 [PL_TONE_MAPPING_GAMMA]             = "Gamma",
835                 [PL_TONE_MAPPING_LINEAR]            = "Linear",
836                 [PL_TONE_MAPPING_BT_2390]           = "BT.2390",
837             };
838 
839             nk_label(nk, "Tone mapping algorithm:", NK_TEXT_LEFT);
840             enum pl_tone_mapping_algorithm new_algo;
841             new_algo = nk_combo(nk, tone_mapping_algos, PL_TONE_MAPPING_ALGORITHM_COUNT,
842                                 cpar->tone_mapping_algo, 16, nk_vec2(nk_widget_width(nk), 300));
843 
844             const char *param = NULL;
845             float param_min, param_max, param_def = 0.0;
846             switch (new_algo) {
847             case PL_TONE_MAPPING_MOBIUS:
848                 param = "Knee point";
849                 param_min = 0.00;
850                 param_max = 1.00;
851                 param_def = 0.5;
852                 break;
853             case PL_TONE_MAPPING_REINHARD:
854                 param = "Contrast";
855                 param_min = 0.00;
856                 param_max = 1.00;
857                 param_def = 0.5;
858                 break;
859             case PL_TONE_MAPPING_GAMMA:
860                 param = "Exponent";
861                 param_min = 0.5;
862                 param_max = 4.0;
863                 param_def = 1.8;
864                 break;
865             case PL_TONE_MAPPING_LINEAR:
866                 param = "Exposure";
867                 param_min = 0.1;
868                 param_max = 100.0;
869                 param_def = 1.0;
870                 break;
871             default: break;
872             }
873 
874             // Explicitly reset the tone mapping parameter when changing this
875             // function, since the interpretation depends on the algorithm
876             if (new_algo != cpar->tone_mapping_algo)
877                 cpar->tone_mapping_param = param_def;
878             cpar->tone_mapping_algo = new_algo;
879 
880             nk_label(nk, "Algorithm parameter:", NK_TEXT_LEFT);
881             if (param) {
882                 nk_property_float(nk, param, param_min, &cpar->tone_mapping_param,
883                                   param_max, 0.1, 0.01);
884             } else {
885                 nk_label(nk, "(N/A)", NK_TEXT_LEFT);
886             }
887 
888             nk_property_float(nk, "Maximum boost", 1.0, &cpar->max_boost, 10.0, 0.1, 0.01);
889             nk_property_float(nk, "Desaturation", 0.0, &cpar->desaturation_strength, 1.0, 0.1, 0.01);
890             nk_property_float(nk, "Desat exponent", 0.0, &cpar->desaturation_exponent, 10.0, 0.1, 0.01);
891             nk_property_float(nk, "Desat base", 0.0, &cpar->desaturation_base, 10.0, 0.1, 0.01);
892             nk_checkbox_label(nk, "Gamut warning", &cpar->gamut_warning);
893             nk_checkbox_label(nk, "Colorimetric clipping", &cpar->gamut_clipping);
894 
895             nk_layout_row_dynamic(nk, 50, 1);
896             if (ui_widget_hover(nk, "Drop .cube file here...") && dropped_file) {
897                 uint8_t *buf;
898                 size_t size;
899                 int ret = av_file_map(dropped_file, &buf, &size, 0, NULL);
900                 if (ret < 0) {
901                     fprintf(stderr, "Failed opening '%s': %s\n", dropped_file,
902                             av_err2str(ret));
903                 } else {
904                     pl_lut_free((struct pl_custom_lut **) &par->lut);
905                     par->lut = pl_lut_parse_cube(p->log, buf, size);
906                     av_file_unmap(buf, size);
907                 }
908             }
909 
910             static const char *lut_types[] = {
911                 [PL_LUT_UNKNOWN]    = "Auto (unknown)",
912                 [PL_LUT_NATIVE]     = "Raw RGB (native)",
913                 [PL_LUT_NORMALIZED] = "Linear RGB (normalized)",
914                 [PL_LUT_CONVERSION] = "Gamut conversion (native)",
915             };
916 
917             nk_layout_row(nk, NK_DYNAMIC, 24, 3, (float[]){ 0.2, 0.3, 0.5 });
918             if (nk_button_label(nk, "Reset LUT")) {
919                 pl_lut_free((struct pl_custom_lut **) &par->lut);
920                 par->lut_type = PL_LUT_UNKNOWN;
921             }
922 
923             nk_label(nk, "LUT type:", NK_TEXT_CENTERED);
924             par->lut_type = nk_combo(nk, lut_types, 4, par->lut_type,
925                                      16, nk_vec2(nk_widget_width(nk), 100));
926 
927             nk_tree_pop(nk);
928         }
929 
930         if (nk_tree_push(nk, NK_TREE_NODE, "Dithering", NK_MINIMIZED)) {
931             struct pl_dither_params *dpar = &p->dither_params;
932             nk_layout_row_dynamic(nk, 24, 2);
933             par->dither_params = nk_check_label(nk, "Enable", par->dither_params) ? dpar : NULL;
934             if (nk_button_label(nk, "Reset settings"))
935                 *dpar = pl_dither_default_params;
936 
937             static const char *dither_methods[PL_DITHER_METHOD_COUNT] = {
938                 [PL_DITHER_BLUE_NOISE]      = "Blue noise",
939                 [PL_DITHER_ORDERED_LUT]     = "Ordered (LUT)",
940                 [PL_DITHER_ORDERED_FIXED]   = "Ordered (fixed size)",
941                 [PL_DITHER_WHITE_NOISE]     = "White noise",
942             };
943 
944             nk_label(nk, "Dither method:", NK_TEXT_LEFT);
945             dpar->method = nk_combo(nk, dither_methods, PL_DITHER_METHOD_COUNT, dpar->method,
946                                     16, nk_vec2(nk_widget_width(nk), 100));
947 
948             static const char *lut_sizes[8] = {
949                 "2x2", "4x4", "8x8", "16x16", "32x32", "64x64", "128x128", "256x256",
950             };
951 
952             nk_label(nk, "LUT size:", NK_TEXT_LEFT);
953             switch (dpar->method) {
954             case PL_DITHER_BLUE_NOISE:
955             case PL_DITHER_ORDERED_LUT: {
956                 int size = dpar->lut_size - 1;
957                 nk_combobox(nk, lut_sizes, 8, &size, 16, nk_vec2(nk_widget_width(nk), 200));
958                 dpar->lut_size = size + 1;
959                 break;
960             }
961             case PL_DITHER_ORDERED_FIXED:
962                 nk_label(nk, "64x64", NK_TEXT_LEFT);
963                 break;
964             default:
965                 nk_label(nk, "(N/A)", NK_TEXT_LEFT);
966                 break;
967             }
968 
969             nk_checkbox_label(nk, "Temporal dithering", &dpar->temporal);
970 
971             nk_tree_pop(nk);
972         }
973 
974         if (nk_tree_push(nk, NK_TREE_NODE, "Output color override", NK_MINIMIZED)) {
975             struct pl_color_space *tcol = &p->target_color;
976             struct pl_color_repr *trepr = &p->target_repr;
977             nk_layout_row_dynamic(nk, 24, 2);
978             nk_checkbox_label(nk, "Enable", &p->target_override);
979             bool reset = nk_button_label(nk, "Reset settings");
980 
981             nk_layout_row(nk, NK_DYNAMIC, 24, 2, (float[]){ 0.3, 0.7 });
982 
983             static const char *primaries[PL_COLOR_PRIM_COUNT] = {
984                 [PL_COLOR_PRIM_UNKNOWN]     = "Auto (unknown)",
985                 [PL_COLOR_PRIM_BT_601_525]  = "ITU-R Rec. BT.601 (525-line = NTSC, SMPTE-C)",
986                 [PL_COLOR_PRIM_BT_601_625]  = "ITU-R Rec. BT.601 (625-line = PAL, SECAM)",
987                 [PL_COLOR_PRIM_BT_709]      = "ITU-R Rec. BT.709 (HD), also sRGB",
988                 [PL_COLOR_PRIM_BT_470M]     = "ITU-R Rec. BT.470 M",
989                 [PL_COLOR_PRIM_EBU_3213]    = "EBU Tech. 3213-E / JEDEC P22 phosphors",
990                 [PL_COLOR_PRIM_BT_2020]     = "ITU-R Rec. BT.2020 (UltraHD)",
991                 [PL_COLOR_PRIM_APPLE]       = "Apple RGB",
992                 [PL_COLOR_PRIM_ADOBE]       = "Adobe RGB (1998)",
993                 [PL_COLOR_PRIM_PRO_PHOTO]   = "ProPhoto RGB (ROMM)",
994                 [PL_COLOR_PRIM_CIE_1931]    = "CIE 1931 RGB primaries",
995                 [PL_COLOR_PRIM_DCI_P3]      = "DCI-P3 (Digital Cinema)",
996                 [PL_COLOR_PRIM_DISPLAY_P3]  = "DCI-P3 (Digital Cinema) with D65 white point",
997                 [PL_COLOR_PRIM_V_GAMUT]     = "Panasonic V-Gamut (VARICAM)",
998                 [PL_COLOR_PRIM_S_GAMUT]     = "Sony S-Gamut",
999                 [PL_COLOR_PRIM_FILM_C]      = "Traditional film primaries with Illuminant C",
1000             };
1001 
1002             nk_label(nk, "Primaries:", NK_TEXT_LEFT);
1003             tcol->primaries = nk_combo(nk, primaries, PL_COLOR_PRIM_COUNT, tcol->primaries,
1004                                        16, nk_vec2(nk_widget_width(nk), 200));
1005 
1006             static const char *transfers[PL_COLOR_TRC_COUNT] = {
1007                 [PL_COLOR_TRC_UNKNOWN]      = "Auto (unknown)",
1008                 [PL_COLOR_TRC_BT_1886]      = "ITU-R Rec. BT.1886 (CRT emulation + OOTF)",
1009                 [PL_COLOR_TRC_SRGB]         = "IEC 61966-2-4 sRGB (CRT emulation)",
1010                 [PL_COLOR_TRC_LINEAR]       = "Linear light content",
1011                 [PL_COLOR_TRC_GAMMA18]      = "Pure power gamma 1.8",
1012                 [PL_COLOR_TRC_GAMMA20]      = "Pure power gamma 2.0",
1013                 [PL_COLOR_TRC_GAMMA22]      = "Pure power gamma 2.2",
1014                 [PL_COLOR_TRC_GAMMA24]      = "Pure power gamma 2.4",
1015                 [PL_COLOR_TRC_GAMMA26]      = "Pure power gamma 2.6",
1016                 [PL_COLOR_TRC_GAMMA28]      = "Pure power gamma 2.8",
1017                 [PL_COLOR_TRC_PRO_PHOTO]    = "ProPhoto RGB (ROMM)",
1018                 [PL_COLOR_TRC_PQ]           = "ITU-R BT.2100 PQ (perceptual quantizer), aka SMPTE ST2048",
1019                 [PL_COLOR_TRC_HLG]          = "ITU-R BT.2100 HLG (hybrid log-gamma), aka ARIB STD-B67",
1020                 [PL_COLOR_TRC_V_LOG]        = "Panasonic V-Log (VARICAM)",
1021                 [PL_COLOR_TRC_S_LOG1]       = "Sony S-Log1",
1022                 [PL_COLOR_TRC_S_LOG2]       = "Sony S-Log2",
1023             };
1024 
1025             nk_label(nk, "Transfer:", NK_TEXT_LEFT);
1026             tcol->transfer = nk_combo(nk, transfers, PL_COLOR_TRC_COUNT, tcol->transfer,
1027                                       16, nk_vec2(nk_widget_width(nk), 200));
1028 
1029             static const char *lights[PL_COLOR_LIGHT_COUNT] = {
1030                 [PL_COLOR_LIGHT_UNKNOWN]    = "Auto (unknown)",
1031                 [PL_COLOR_LIGHT_DISPLAY]    = "Display-referred, output as-is",
1032                 [PL_COLOR_LIGHT_SCENE_HLG]  = "Scene-referred, HLG OOTF",
1033                 [PL_COLOR_LIGHT_SCENE_709_1886] = "Scene-referred, OOTF = BT.709+1886 interaction",
1034                 [PL_COLOR_LIGHT_SCENE_1_2]  = "Scene-referred, OOTF = gamma 1.2",
1035             };
1036 
1037             nk_label(nk, "Light:", NK_TEXT_LEFT);
1038             tcol->light = nk_combo(nk, lights, PL_COLOR_LIGHT_COUNT, tcol->light,
1039                                    16, nk_vec2(nk_widget_width(nk), 200));
1040 
1041             nk_layout_row_dynamic(nk, 24, 2);
1042             nk_checkbox_label(nk, "Override HDR levels", &p->levels_override);
1043             bool reset_levels = nk_button_label(nk, "Reset levels");
1044 
1045             if (p->levels_override) {
1046                 // Ensure these values are always legal by going through
1047                 // `pl_color_space_infer`, without clobbering the rest
1048                 nk_layout_row_dynamic(nk, 24, 2);
1049                 struct pl_color_space fix = *tcol;
1050                 pl_color_space_infer(&fix);
1051                 float peak = fix.sig_peak * fix.sig_scale * PL_COLOR_SDR_WHITE;
1052                 float avg = fix.sig_avg * fix.sig_scale * PL_COLOR_SDR_WHITE;
1053                 float sfloor = fix.sig_floor * fix.sig_scale * PL_COLOR_SDR_WHITE;
1054                 nk_property_float(nk, "White point (cd/m²)", 0.0, &peak, 10000.0, 1, 0.1);
1055                 nk_property_float(nk, "Black point (cd/m²)", 0.0, &sfloor, 10.0, 0.001, 0.0001);
1056                 nk_property_float(nk, "Frame average (cd/m²)", 0.0, &avg, 1000.0, 1, 0.01);
1057                 fix.sig_peak = fmax(peak, 1e-3) / (fix.sig_scale * PL_COLOR_SDR_WHITE);
1058                 fix.sig_avg = fmax(avg, 1e-4) / (fix.sig_scale * PL_COLOR_SDR_WHITE);
1059                 fix.sig_floor = fmax(sfloor, 1e-6) / (fix.sig_scale * PL_COLOR_SDR_WHITE);
1060                 nk_property_float(nk, "Output scale", 0.0, &fix.sig_scale, 10000.0 / PL_COLOR_SDR_WHITE, 0.01, 0.001);
1061                 pl_color_space_infer(&fix);
1062                 tcol->sig_peak = fix.sig_peak;
1063                 tcol->sig_avg = fix.sig_avg;
1064                 tcol->sig_floor = fix.sig_floor;
1065                 tcol->sig_scale = fix.sig_scale;
1066             } else {
1067                 reset_levels = true;
1068             }
1069 
1070             nk_layout_row(nk, NK_DYNAMIC, 24, 2, (float[]){ 0.3, 0.7 });
1071 
1072             static const char *systems[PL_COLOR_SYSTEM_COUNT] = {
1073                 [PL_COLOR_SYSTEM_UNKNOWN]       = "Auto (unknown)",
1074                 [PL_COLOR_SYSTEM_BT_601]        = "ITU-R Rec. BT.601 (SD)",
1075                 [PL_COLOR_SYSTEM_BT_709]        = "ITU-R Rec. BT.709 (HD)",
1076                 [PL_COLOR_SYSTEM_SMPTE_240M]    = "SMPTE-240M",
1077                 [PL_COLOR_SYSTEM_BT_2020_NC]    = "ITU-R Rec. BT.2020 (non-constant luminance)",
1078                 [PL_COLOR_SYSTEM_BT_2020_C]     = "ITU-R Rec. BT.2020 (constant luminance)",
1079                 [PL_COLOR_SYSTEM_BT_2100_PQ]    = "ITU-R Rec. BT.2100 ICtCp PQ variant",
1080                 [PL_COLOR_SYSTEM_BT_2100_HLG]   = "ITU-R Rec. BT.2100 ICtCp HLG variant",
1081                 [PL_COLOR_SYSTEM_YCGCO]         = "YCgCo (derived from RGB)",
1082                 [PL_COLOR_SYSTEM_RGB]           = "Red, Green and Blue",
1083                 [PL_COLOR_SYSTEM_XYZ]           = "CIE 1931 XYZ, pre-encoded with gamma 2.6",
1084             };
1085 
1086             nk_label(nk, "System:", NK_TEXT_LEFT);
1087             trepr->sys = nk_combo(nk, systems, PL_COLOR_SYSTEM_COUNT, trepr->sys,
1088                                   16, nk_vec2(nk_widget_width(nk), 200));
1089 
1090             static const char *levels[PL_COLOR_LEVELS_COUNT] = {
1091                 [PL_COLOR_LEVELS_UNKNOWN]   = "Auto (unknown)",
1092                 [PL_COLOR_LEVELS_LIMITED]   = "Limited/TV range, e.g. 16-235",
1093                 [PL_COLOR_LEVELS_FULL]      = "Full/PC range, e.g. 0-255",
1094             };
1095 
1096             nk_label(nk, "Levels:", NK_TEXT_LEFT);
1097             trepr->levels = nk_combo(nk, levels, PL_COLOR_LEVELS_COUNT, trepr->levels,
1098                                      16, nk_vec2(nk_widget_width(nk), 200));
1099 
1100             static const char *alphas[PL_ALPHA_MODE_COUNT] = {
1101                 [PL_ALPHA_UNKNOWN]          = "Auto (unknown, or no alpha)",
1102                 [PL_ALPHA_INDEPENDENT]      = "Independent alpha channel",
1103                 [PL_ALPHA_PREMULTIPLIED]    = "Premultiplied alpha channel",
1104             };
1105 
1106             nk_label(nk, "Alpha:", NK_TEXT_LEFT);
1107             trepr->alpha = nk_combo(nk, alphas, PL_ALPHA_MODE_COUNT, trepr->alpha,
1108                                     16, nk_vec2(nk_widget_width(nk), 200));
1109 
1110             // Adjust these two fields in unison
1111             int bits = trepr->bits.color_depth;
1112             nk_label(nk, "Bit depth:", NK_TEXT_LEFT);
1113             nk_property_int(nk, "", 0, &bits, 16, 1, 0);
1114             trepr->bits.color_depth = bits;
1115             trepr->bits.sample_depth = bits;
1116 
1117             // Apply the reset last to prevent the UI from flashing for a frame
1118             if (reset) {
1119                 *tcol = (struct pl_color_space) {0};
1120                 *trepr = (struct pl_color_repr) {0};
1121             }
1122 
1123             if (reset_levels) {
1124                 tcol->sig_peak = 0;
1125                 tcol->sig_avg = 0;
1126                 tcol->sig_floor = 0;
1127                 tcol->sig_scale = 0;
1128             }
1129 
1130             nk_tree_pop(nk);
1131         }
1132 
1133         if (nk_tree_push(nk, NK_TREE_NODE, "Custom shaders", NK_MINIMIZED)) {
1134 
1135             nk_layout_row_dynamic(nk, 50, 1);
1136             if (ui_widget_hover(nk, "Drop .hook/.glsl files here...") && dropped_file) {
1137                 uint8_t *buf;
1138                 size_t size;
1139                 int ret = av_file_map(dropped_file, &buf, &size, 0, NULL);
1140                 if (ret < 0) {
1141                     fprintf(stderr, "Failed opening '%s': %s\n", dropped_file,
1142                             av_err2str(ret));
1143                 } else {
1144                     const struct pl_hook *hook;
1145                     hook = pl_mpv_user_shader_parse(p->win->gpu, buf, size);
1146                     av_file_unmap(buf, size);
1147                     add_hook(p, hook, dropped_file);
1148                 }
1149             }
1150 
1151             const float px = 24.0;
1152             nk_layout_row_template_begin(nk, px);
1153             nk_layout_row_template_push_static(nk, px);
1154             nk_layout_row_template_push_static(nk, px);
1155             nk_layout_row_template_push_static(nk, px);
1156             nk_layout_row_template_push_dynamic(nk);
1157             nk_layout_row_template_end(nk);
1158             for (int i = 0; i < p->shader_num; i++) {
1159 
1160                 if (i == 0) {
1161                     nk_label(nk, "·", NK_TEXT_CENTERED);
1162                 } else if (nk_button_symbol(nk, NK_SYMBOL_TRIANGLE_UP)) {
1163                     const struct pl_hook *prev_hook = p->shader_hooks[i - 1];
1164                     char *prev_path = p->shader_paths[i - 1];
1165                     p->shader_hooks[i - 1] = p->shader_hooks[i];
1166                     p->shader_paths[i - 1] = p->shader_paths[i];
1167                     p->shader_hooks[i] = prev_hook;
1168                     p->shader_paths[i] = prev_path;
1169                 }
1170 
1171                 if (i == p->shader_num - 1) {
1172                     nk_label(nk, "·", NK_TEXT_CENTERED);
1173                 } else if (nk_button_symbol(nk, NK_SYMBOL_TRIANGLE_DOWN)) {
1174                     const struct pl_hook *next_hook = p->shader_hooks[i + 1];
1175                     char *next_path = p->shader_paths[i + 1];
1176                     p->shader_hooks[i + 1] = p->shader_hooks[i];
1177                     p->shader_paths[i + 1] = p->shader_paths[i];
1178                     p->shader_hooks[i] = next_hook;
1179                     p->shader_paths[i] = next_path;
1180                 }
1181 
1182                 if (nk_button_symbol(nk, NK_SYMBOL_X)) {
1183                     pl_mpv_user_shader_destroy(&p->shader_hooks[i]);
1184                     free(p->shader_paths[i]);
1185                     p->shader_num--;
1186                     memmove(&p->shader_hooks[i], &p->shader_hooks[i+1],
1187                             (p->shader_num - i) * sizeof(void *));
1188                     memmove(&p->shader_paths[i], &p->shader_paths[i+1],
1189                             (p->shader_num - i) * sizeof(char *));
1190                 }
1191 
1192                 if (i < p->shader_num)
1193                     nk_label(nk, p->shader_paths[i], NK_TEXT_LEFT);
1194             }
1195 
1196             par->hooks = p->shader_hooks;
1197             par->num_hooks = p->shader_num;
1198             nk_tree_pop(nk);
1199         }
1200 
1201         if (nk_tree_push(nk, NK_TREE_NODE, "Debug", NK_MINIMIZED)) {
1202             nk_layout_row_dynamic(nk, 24, 1);
1203             nk_checkbox_label(nk, "Allow delayed peak-detect", &par->allow_delayed_peak_detect);
1204             nk_checkbox_label(nk, "Preserve mixing cache", &par->preserve_mixing_cache);
1205             nk_checkbox_label(nk, "Disable linear scaling", &par->disable_linear_scaling);
1206             nk_checkbox_label(nk, "Disable built-in scalers", &par->disable_builtin_scalers);
1207             nk_checkbox_label(nk, "Force-enable 3DLUT", &par->force_icc_lut);
1208             nk_checkbox_label(nk, "Force-enable dither", &par->force_dither);
1209             nk_checkbox_label(nk, "Disable FBOs / advanced rendering", &par->disable_fbos);
1210             nk_checkbox_label(nk, "Disable constant hard-coding", &par->dynamic_constants);
1211             nk_checkbox_label(nk, "Ignore ICC profiles", &par->ignore_icc_profiles);
1212 
1213             nk_layout_row_dynamic(nk, 24, 2);
1214             if (nk_button_label(nk, "Flush renderer cache"))
1215                 pl_renderer_flush_cache(p->renderer);
1216             if (nk_button_label(nk, "Recreate renderer")) {
1217                 pl_renderer_destroy(&p->renderer);
1218                 p->renderer = pl_renderer_create(p->log, p->win->gpu);
1219             }
1220 
1221             if (nk_tree_push(nk, NK_TREE_NODE, "Shader passes", NK_MINIMIZED)) {
1222                 nk_layout_row_dynamic(nk, 26, 1);
1223                 nk_label(nk, "Full frames:", NK_TEXT_LEFT);
1224                 for (int i = 0; i < p->num_frame_passes; i++) {
1225                     struct pass_info *info = &p->frame_info[i];
1226                     nk_layout_row_dynamic(nk, 24, 1);
1227                     nk_labelf(nk, NK_TEXT_LEFT, "- %s: %.3f / %.3f / %.3f ms",
1228                               info->name,
1229                               info->pass.last / 1e6,
1230                               info->pass.average / 1e6,
1231                               info->pass.peak / 1e6);
1232 
1233                     nk_layout_row_dynamic(nk, 32, 1);
1234                     if (nk_chart_begin(nk, NK_CHART_LINES,
1235                                        info->pass.num_samples,
1236                                        0.0f, info->pass.peak))
1237                     {
1238                         for (int k = 0; k < info->pass.num_samples; k++)
1239                             nk_chart_push(nk, info->pass.samples[k]);
1240                         nk_chart_end(nk);
1241                     }
1242                 }
1243 
1244                 nk_layout_row_dynamic(nk, 26, 1);
1245                 nk_label(nk, "Output blending:", NK_TEXT_LEFT);
1246                 for (int i = 0; i < MAX_BLEND_FRAMES; i++) {
1247                     struct pass_info *info = &p->blend_info[i];
1248                     if (!info->name)
1249                         continue;
1250 
1251                     nk_layout_row_dynamic(nk, 24, 1);
1252                     nk_labelf(nk, NK_TEXT_LEFT,
1253                               "- (%d frame%s) %s: %.3f / %.3f / %.3f ms",
1254                               i, i > 1 ? "s" : "", info->name,
1255                               info->pass.last / 1e6,
1256                               info->pass.average / 1e6,
1257                               info->pass.peak / 1e6);
1258 
1259                     nk_layout_row_dynamic(nk, 32, 1);
1260                     if (nk_chart_begin(nk, NK_CHART_LINES,
1261                                        info->pass.num_samples,
1262                                        0.0f, info->pass.peak))
1263                     {
1264                         for (int k = 0; k < info->pass.num_samples; k++)
1265                             nk_chart_push(nk, info->pass.samples[k]);
1266                         nk_chart_end(nk);
1267                     }
1268                 }
1269 
1270                 nk_tree_pop(nk);
1271             }
1272 
1273             nk_tree_pop(nk);
1274         }
1275     }
1276     nk_end(nk);
1277 }
1278 
1279 #else
update_settings(struct plplay * p)1280 static void update_settings(struct plplay *p) { }
1281 #endif
1282