diff mbox series

[FFmpeg-devel,RFC,2/2] lavfi: add a libplacebo filter

Message ID 20201125115034.75890-2-ffmpeg@haasn.xyz
State New
Headers show
Series [FFmpeg-devel,1/2] hwcontext_vulkan: optionally enable more functionality
Related show

Checks

Context Check Description
andriy/x86_make success Make finished
andriy/x86_make_fate success Make fate finished
andriy/PPC64_make success Make finished
andriy/PPC64_make_fate success Make fate finished

Commit Message

Niklas Haas Nov. 25, 2020, 11:50 a.m. UTC
From: Niklas Haas <git@haasn.xyz>

This filter conceptually maps the libplacebo `pl_renderer` API into
libavfilter, which is a high-level image rendering API designed to
produce RGB output. As such, there's no way to avoid e.g. chroma
interpolation with this filter.

That being said, `pl_renderer` supports automatic integration of the
majority of libplacebo's shaders, ranging from debanding to tone
mapping, and also supports loading custom mpv-style user shaders, making
this API a natural candidate for getting a lot of functionality out of
relatively little code.

In the future, I may re-approach this by providing separate filters for
individual tasks that don't require full chroma interpolation (e.g.
vf_deband_vulkan.c or vf_av1grain_vulkan.c).
---
 configure                   |   6 +
 libavfilter/Makefile        |   1 +
 libavfilter/allfilters.c    |   1 +
 libavfilter/vf_libplacebo.c | 651 ++++++++++++++++++++++++++++++++++++
 4 files changed, 659 insertions(+)
 create mode 100644 libavfilter/vf_libplacebo.c
diff mbox series

Patch

diff --git a/configure b/configure
index 502e229806..36bef88969 100755
--- a/configure
+++ b/configure
@@ -1794,6 +1794,7 @@  EXTERNAL_LIBRARY_LIST="
     libopenmpt
     libopenvino
     libopus
+    libplacebo
     libpulse
     librabbitmq
     librav1e
@@ -3571,6 +3572,7 @@  interlace_filter_deps="gpl"
 kerndeint_filter_deps="gpl"
 ladspa_filter_deps="ladspa libdl"
 lensfun_filter_deps="liblensfun version3"
+libplacebo_filter_deps="vulkan libplacebo"
 lv2_filter_deps="lv2"
 mcdeint_filter_deps="avcodec gpl"
 movie_filter_deps="avcodec avformat"
@@ -6400,6 +6402,10 @@  enabled libopus           && {
         require_pkg_config libopus opus opus_multistream.h opus_multistream_surround_encoder_create
     }
 }
+enabled libplacebo        && {
+    require_pkg_config libplacebo "libplacebo >= 2.95.0" libplacebo/context.h pl_context_create
+    enabled vulkan && require_pkg_config libplacebo_vulkan libplacebo libplacebo/vulkan.h pl_vulkan_create
+}
 enabled libpulse          && require_pkg_config libpulse libpulse pulse/pulseaudio.h pa_context_new
 enabled librabbitmq       && require_pkg_config librabbitmq "librabbitmq >= 0.7.1" amqp.h amqp_new_connection
 enabled librav1e          && require_pkg_config librav1e "rav1e >= 0.1.0" rav1e.h rav1e_context_new
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 794a55ac3d..4260bb5713 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -295,6 +295,7 @@  OBJS-$(CONFIG_KERNDEINT_FILTER)              += vf_kerndeint.o
 OBJS-$(CONFIG_LAGFUN_FILTER)                 += vf_lagfun.o
 OBJS-$(CONFIG_LENSCORRECTION_FILTER)         += vf_lenscorrection.o
 OBJS-$(CONFIG_LENSFUN_FILTER)                += vf_lensfun.o
+OBJS-$(CONFIG_LIBPLACEBO_FILTER)             += vf_libplacebo.o vulkan.o
 OBJS-$(CONFIG_LIBVMAF_FILTER)                += vf_libvmaf.o framesync.o
 OBJS-$(CONFIG_LIMITER_FILTER)                += vf_limiter.o
 OBJS-$(CONFIG_LOOP_FILTER)                   += f_loop.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index fbfd8989c6..79228508aa 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -280,6 +280,7 @@  extern AVFilter ff_vf_kerndeint;
 extern AVFilter ff_vf_lagfun;
 extern AVFilter ff_vf_lenscorrection;
 extern AVFilter ff_vf_lensfun;
+extern AVFilter ff_vf_libplacebo;
 extern AVFilter ff_vf_libvmaf;
 extern AVFilter ff_vf_limiter;
 extern AVFilter ff_vf_loop;
diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c
new file mode 100644
index 0000000000..92a37a5c85
--- /dev/null
+++ b/libavfilter/vf_libplacebo.c
@@ -0,0 +1,651 @@ 
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "libavutil/opt.h"
+#include "internal.h"
+#include "vulkan.h"
+#include "scale_eval.h"
+
+#include <libplacebo/renderer.h>
+#include <libplacebo/utils/libav.h>
+#include <libplacebo/vulkan.h>
+
+typedef struct LibplaceboContext {
+    /* lavfi vulkan */
+    VulkanFilterContext vkctx;
+    int initialized;
+
+    /* libplacebo */
+    struct pl_context *pl_ctx;
+    const struct pl_vulkan *pl_vulkan;
+    struct pl_renderer *pl_renderer;
+
+    /* settings */
+    char *out_format_string;
+    char *w_expr;
+    char *h_expr;
+    int color_range;
+    int color_primaries;
+    int color_trc;
+
+    /* pl_render_params */
+    char *upscaler;
+    char *downscaler;
+    int lut_entries;
+    float antiringing;
+    int sigmoid;
+    int skip_aa;
+    float polar_cutoff;
+    int disable_linear;
+    int disable_builtin;
+    int force_3dlut;
+    int force_dither;
+    int disable_fbos;
+
+    /* pl_deband_params */
+    int deband;
+    int deband_iterations;
+    float deband_threshold;
+    float deband_radius;
+    float deband_grain;
+
+    /* pl_color_adjustment */
+    float brightness;
+    float contrast;
+    float saturation;
+    float hue;
+    float gamma;
+
+    /* pl_peak_detect_params */
+    int peakdetect;
+    float smoothing;
+    float scene_low;
+    float scene_high;
+    float overshoot;
+
+    /* pl_color_map_params */
+    int intent;
+    int tonemapping;
+    float tonemapping_param;
+    float desat_str;
+    float desat_exp;
+    float desat_base;
+    float max_boost;
+    int gamut_warning;
+    int gamut_clipping;
+
+     /* pl_dither_params */
+    int dithering;
+    int dither_lut_size;
+    int dither_temporal;
+
+    /* pl_cone_params */
+    int cones;
+    float cone_str;
+
+    /* custom shaders */
+    const struct pl_hook *hook;
+    void *shader;
+    int shader_len;
+} LibplaceboContext;
+
+static void pl_av_log(void *log_ctx, enum pl_log_level level, const char *msg)
+{
+    int av_lev;
+
+    switch (level) {
+    case PL_LOG_FATAL:  av_lev = AV_LOG_FATAL;   break;
+    case PL_LOG_ERR:    av_lev = AV_LOG_ERROR;   break;
+    case PL_LOG_WARN:   av_lev = AV_LOG_WARNING; break;
+    case PL_LOG_INFO:   av_lev = AV_LOG_INFO;    break;
+    case PL_LOG_DEBUG:  av_lev = AV_LOG_VERBOSE; break;
+    case PL_LOG_TRACE:  av_lev = AV_LOG_TRACE;   break;
+    default: return;
+    }
+
+    av_log(log_ctx, av_lev, "%s\n", msg);
+}
+
+static int libplacebo_init(AVFilterContext *avctx)
+{
+    int err;
+    LibplaceboContext *s = avctx->priv;
+
+    RET(ff_vk_filter_init(avctx));
+
+    s->pl_ctx = pl_context_create(PL_API_VER, &(struct pl_context_params) {
+        .log_cb = pl_av_log,
+        .log_priv = s,
+        .log_level = PL_LOG_DEBUG,
+    });
+
+    if (!s->pl_ctx)
+        return AVERROR(ENOMEM);
+
+    /* Note: s->pl_vulkan etc. are initialized later, when hwctx is known */
+    return 0;
+
+fail:
+    return err;
+}
+
+static av_cold int vulkan_init(AVFilterContext *ctx)
+{
+    LibplaceboContext *s = ctx->priv;
+    const struct pl_gpu *gpu;
+
+    /* Init the libplacebo context from the libavfilter one */
+    s->pl_vulkan = pl_vulkan_import(s->pl_ctx, &(struct pl_vulkan_import_params) {
+        .instance       = s->vkctx.hwctx->inst,
+        .phys_device    = s->vkctx.hwctx->phys_dev,
+        .device         = s->vkctx.hwctx->act_dev,
+        .extensions     = s->vkctx.hwctx->enabled_dev_extensions,
+        .num_extensions = s->vkctx.hwctx->nb_enabled_dev_extensions,
+        .features       = &s->vkctx.hwctx->device_features,
+        .queue_graphics = {
+            .index = s->vkctx.hwctx->queue_family_index,
+            .count = s->vkctx.hwctx->nb_graphics_queues,
+        },
+        .queue_compute = {
+            .index = s->vkctx.hwctx->queue_family_comp_index,
+            .count = s->vkctx.hwctx->nb_comp_queues,
+        },
+        .queue_transfer = {
+            .index = s->vkctx.hwctx->queue_family_tx_index,
+            .count = s->vkctx.hwctx->nb_tx_queues,
+        },
+        /* This is the highest version created by hwcontext_vulkan.c */
+        .max_api_version = VK_API_VERSION_1_1,
+    });
+
+    if (!s->pl_vulkan) {
+        av_log(s, AV_LOG_ERROR, "Error importing vulkan device to libplacebo!\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    /* Create the renderer */
+    gpu = s->pl_vulkan->gpu;
+    s->pl_renderer = pl_renderer_create(s->pl_ctx, gpu);
+
+    /* Parse the user shader, if requested */
+    if (s->shader_len) {
+        s->hook = pl_mpv_user_shader_parse(gpu, s->shader, s->shader_len);
+        if (!s->hook) {
+            av_log(s, AV_LOG_ERROR, "Failed parsing custom shader!\n");
+            return AVERROR(EINVAL);
+        }
+    }
+
+    s->initialized = 1;
+    return 0;
+}
+
+static void libplacebo_uninit(AVFilterContext *avctx)
+{
+    LibplaceboContext *s = avctx->priv;
+
+    pl_mpv_user_shader_destroy(&s->hook);
+    pl_renderer_destroy(&s->pl_renderer);
+    pl_vulkan_destroy(&s->pl_vulkan);
+    pl_context_destroy(&s->pl_ctx);
+    ff_vk_filter_uninit(avctx);
+
+    s->initialized = 0;
+}
+
+#define DEFAULT_USAGE_FLAGS (VK_IMAGE_USAGE_SAMPLED_BIT      |                 \
+                             VK_IMAGE_USAGE_STORAGE_BIT      |                 \
+                             VK_IMAGE_USAGE_TRANSFER_SRC_BIT |                 \
+                             VK_IMAGE_USAGE_TRANSFER_DST_BIT)
+
+static int wrap_vkframe(const struct pl_gpu *gpu, const AVFrame *frame,
+                        int plane, const struct pl_tex **tex)
+{
+    const AVVkFrame *vk_frame = (AVVkFrame *) frame->data[0];
+    const AVHWFramesContext *hwfc = (AVHWFramesContext *) frame->hw_frames_ctx->data;
+    const AVVulkanFramesContext *vkfc = hwfc->hwctx;
+    const VkFormat *vk_fmt = av_vkfmt_from_pixfmt(hwfc->sw_format);
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(hwfc->sw_format);
+    const bool chroma = plane == 1 || plane == 2;
+    const int shift_w = chroma ? desc->log2_chroma_w : 0;
+    const int shift_h = chroma ? desc->log2_chroma_h : 0;
+
+    *tex = pl_vulkan_wrap(gpu, &(struct pl_vulkan_wrap_params) {
+        .image = vk_frame->img[plane],
+        .width = frame->width >> shift_w,
+        .height = frame->height >> shift_h,
+        .format = vk_fmt[plane],
+        .usage = DEFAULT_USAGE_FLAGS | vkfc->usage,
+        .sample_mode = PL_TEX_SAMPLE_LINEAR,
+        .address_mode = PL_TEX_ADDRESS_CLAMP,
+    });
+
+    if (!*tex)
+        return AVERROR(ENOMEM);
+
+    pl_vulkan_release(gpu, *tex, vk_frame->layout[plane],
+                      vk_frame->access[plane],
+                      vk_frame->sem[plane]);
+
+    return 0;
+}
+
+static int unwrap_vkframe(const struct pl_gpu *gpu, AVFrame *frame,
+                          int plane, const struct pl_tex **tex)
+{
+    AVVkFrame *vk_frame = (AVVkFrame *) frame->data[0];
+
+    /* FIXME: Re-using vk_frame->sem like this is undefined behaviour, but
+     * there's currently little way around it because the design of AVVkFrame
+     * effectively forces undefined behaviour on the user. Change ASAP! */
+    bool ok = pl_vulkan_hold_raw(gpu, *tex, &vk_frame->layout[plane],
+                                 &vk_frame->access[plane],
+                                 vk_frame->sem[plane]);
+
+    if (!ok)
+        return AVERROR_EXTERNAL;
+
+    pl_tex_destroy(gpu, tex);
+
+    return 0;
+}
+
+static int find_scaler(AVFilterContext *avctx,
+                       const struct pl_filter_config **opt,
+                       const char *name)
+{
+    const struct pl_named_filter_config *preset;
+    if (!strcmp(name, "help")) {
+        av_log(avctx, AV_LOG_INFO, "Available scaler presets:\n");
+        for (preset = pl_named_filters; preset->name; preset++)
+            av_log(avctx, AV_LOG_INFO, "    %s\n", preset->name);
+        return AVERROR_EXIT;
+    }
+
+    preset = pl_find_named_filter(name);
+    if (!preset) {
+        av_log(avctx, AV_LOG_ERROR, "No such scaler preset '%s'.\n", name);
+        return AVERROR(EINVAL);
+    }
+
+    *opt = preset->filter;
+    return 0;
+}
+
+static int process_frames(AVFilterContext *avctx, AVFrame *out, AVFrame *in)
+{
+    int err = 0;
+    LibplaceboContext *s = avctx->priv;
+    const struct pl_gpu *gpu = s->pl_vulkan->gpu;
+    struct pl_render_params params;
+
+    struct pl_image image = {0};
+    struct pl_render_target target = {0};
+    pl_image_from_avframe(&image, in);
+    pl_target_from_avframe(&target, out);
+
+    /* Update render params */
+    params = (struct pl_render_params) {
+        .lut_entries = s->lut_entries,
+        .antiringing_strength = s->antiringing,
+
+        .deband_params = !s->deband ? NULL : &(struct pl_deband_params) {
+            .iterations = s->deband_iterations,
+            .threshold = s->deband_threshold,
+            .radius = s->deband_radius,
+            .grain = s->deband_grain,
+        },
+
+        .sigmoid_params = s->sigmoid ? &pl_sigmoid_default_params : NULL,
+
+        .color_adjustment = &(struct pl_color_adjustment) {
+            .brightness = s->brightness,
+            .contrast = s->contrast,
+            .saturation = s->saturation,
+            .hue = s->hue,
+            .gamma = s->gamma,
+        },
+
+        .peak_detect_params = !s->peakdetect ? NULL : &(struct pl_peak_detect_params) {
+            .smoothing_period = s->smoothing,
+            .scene_threshold_low = s->scene_low,
+            .scene_threshold_high = s->scene_high,
+            .overshoot_margin = s->overshoot,
+        },
+
+        .color_map_params = &(struct pl_color_map_params) {
+            .intent = s->intent,
+            .tone_mapping_algo = s->tonemapping,
+            .tone_mapping_param = s->tonemapping_param,
+            .desaturation_strength = s->desat_str,
+            .desaturation_exponent = s->desat_exp,
+            .desaturation_base = s->desat_base,
+            .max_boost = s->max_boost,
+            .gamut_warning = s->gamut_warning,
+            .gamut_clipping = s->gamut_clipping,
+        },
+
+        .dither_params = s->dithering < 0 ? NULL : &(struct pl_dither_params) {
+            .method = s->dithering,
+            .lut_size = s->dither_lut_size,
+            .temporal = s->dither_temporal,
+        },
+
+        .cone_params = !s->cones ? NULL : &(struct pl_cone_params) {
+            .cones = s->cones,
+            .strength = s->cone_str,
+        },
+
+        .hooks = &s->hook,
+        .num_hooks = s->hook ? 1 : 0,
+
+        .skip_anti_aliasing = s->skip_aa,
+        .polar_cutoff = s->polar_cutoff,
+        .disable_linear_scaling = s->disable_linear,
+        .disable_builtin_scalers = s->disable_builtin,
+        .force_3dlut = s->force_3dlut,
+        .force_dither = s->force_dither,
+        .disable_fbos = s->disable_fbos,
+    };
+
+    RET(find_scaler(avctx, &params.upscaler, s->upscaler));
+    RET(find_scaler(avctx, &params.downscaler, s->downscaler));
+
+    /* Ideally, we would persistently wrap all of these AVVkFrames into pl_tex
+     * objects, but for now we'll just create and destroy a wrapper per frame.
+     * Note that doing it this way is suboptimal, since it results in the
+     * creation and destruction of a VkSampler and VkFramebuffer per frame.
+     *
+     * FIXME: Can we do better? */
+    for (int i = 0; i < image.num_planes; i++)
+        RET(wrap_vkframe(gpu, in, i, &image.planes[i].texture));
+    RET(wrap_vkframe(gpu, out, 0, &target.fbo));
+
+    /* Images mapped, perform the actual rendering */
+    if (pl_render_target_partial(&target))
+        pl_tex_clear(gpu, target.fbo, (float[4]) { 0.0, 0.0, 0.0, 1.0 });
+    pl_render_image(s->pl_renderer, &image, &target, &params);
+
+    for (int i = 0; i < image.num_planes; i++)
+        RET(unwrap_vkframe(gpu, in, i, &image.planes[i].texture));
+    RET(unwrap_vkframe(gpu, out, 0, &target.fbo));
+
+    /* Flush the command queues for performance */
+    pl_gpu_flush(gpu);
+    return 0;
+
+fail:
+    for (int i = 0; i < 4; i++)
+        pl_tex_destroy(gpu, &image.planes[i].texture);
+    pl_tex_destroy(gpu, &target.fbo);
+
+    return err;
+}
+
+static int libplacebo_filter_frame(AVFilterLink *link, AVFrame *in)
+{
+    int err;
+    AVFilterContext *ctx = link->dst;
+    LibplaceboContext *s = ctx->priv;
+    AVFilterLink *outlink = ctx->outputs[0];
+
+    AVFrame *out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
+    if (!out) {
+        err = AVERROR(ENOMEM);
+        goto fail;
+    }
+
+    if (!s->initialized)
+        RET(vulkan_init(ctx));
+
+    err = av_frame_copy_props(out, in);
+    if (err < 0)
+        goto fail;
+
+    out->width = outlink->w;
+    out->height = outlink->h;
+    out->colorspace = AVCOL_SPC_RGB;
+    out->chroma_location = 0;
+
+    if (s->color_range >= 0)
+        out->color_range = s->color_range;
+    if (s->color_trc >= 0)
+        out->color_trc = s->color_trc;
+    if (s->color_primaries >= 0)
+        out->color_primaries = s->color_primaries;
+
+    RET(process_frames(ctx, out, in));
+
+    av_frame_free(&in);
+
+    return ff_filter_frame(outlink, out);
+
+fail:
+    av_frame_free(&in);
+    av_frame_free(&out);
+    return err;
+}
+
+static int libplacebo_config_output(AVFilterLink *outlink)
+{
+    int err;
+    AVFilterContext *avctx = outlink->src;
+    LibplaceboContext *s   = avctx->priv;
+    AVFilterLink *inlink   = outlink->src->inputs[0];
+    AVHWFramesContext *hwfc;
+    AVVulkanFramesContext *vkfc;
+
+    RET(ff_scale_eval_dimensions(s, s->w_expr, s->h_expr, inlink, outlink,
+                                 &s->vkctx.output_width,
+                                 &s->vkctx.output_height));
+
+    if (s->out_format_string) {
+        s->vkctx.output_format = av_get_pix_fmt(s->out_format_string);
+        if (s->vkctx.output_format == AV_PIX_FMT_NONE) {
+            av_log(avctx, AV_LOG_ERROR, "Invalid output format.\n");
+            return AVERROR(EINVAL);
+        }
+
+        /* Note: Technically, we could also support packed 4:4:4 YUV/XYZ
+         * output, but these don't map to hwcontext_vulkan */
+        if (!ff_vk_mt_is_np_rgb(s->vkctx.output_format)) {
+            av_log(avctx, AV_LOG_ERROR, "Output format must be non-planar RGB.\n");
+            return AVERROR(EINVAL);
+        }
+    } else {
+        /* Default to 8-bit or 16-bit RGBA depending on the input depth */
+        const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(s->vkctx.input_format);
+        const bool highp = desc->comp[0].depth > 8;
+        s->vkctx.output_format = highp ? AV_PIX_FMT_RGBA64 : AV_PIX_FMT_RGBA;
+    }
+
+    RET(ff_vk_filter_config_output(outlink));
+
+    hwfc = (AVHWFramesContext *) outlink->hw_frames_ctx->data;
+    vkfc = hwfc->hwctx;
+    vkfc->usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
+
+    return 0;
+
+fail:
+    return err;
+}
+
+#define OFFSET(x) offsetof(LibplaceboContext, x)
+#define STATIC (AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM)
+#define DYNAMIC (STATIC | AV_OPT_FLAG_RUNTIME_PARAM)
+
+static const AVOption libplacebo_options[] = {
+    { "w", "Output video width",  OFFSET(w_expr), AV_OPT_TYPE_STRING, {.str = "iw"}, .flags = STATIC },
+    { "h", "Output video height", OFFSET(h_expr), AV_OPT_TYPE_STRING, {.str = "ih"}, .flags = STATIC },
+    { "format", "Output video format (software format of hardware frames)", OFFSET(out_format_string), AV_OPT_TYPE_STRING, .flags = STATIC },
+
+    {"range", "select color range", OFFSET(color_range), AV_OPT_TYPE_INT, {.i64=AVCOL_RANGE_JPEG},-1, AVCOL_RANGE_NB-1, DYNAMIC, "range"},
+    {"auto",  "keep the same color range",   0, AV_OPT_TYPE_CONST, {.i64=-1},                       0, 0, STATIC, "range"},
+    {"unspecified",                  NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_UNSPECIFIED},  0, 0, STATIC, "range"},
+    {"unknown",                      NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_UNSPECIFIED},  0, 0, STATIC, "range"},
+    {"limited",                      NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, "range"},
+    {"tv",                           NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, "range"},
+    {"mpeg",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, "range"},
+    {"full",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, "range"},
+    {"pc",                           NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, "range"},
+    {"jpeg",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, "range"},
+
+    {"color_primaries", "select color primaries", OFFSET(color_primaries), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_PRI_NB-1, DYNAMIC, "color_primaries"},
+    {"auto", "keep the same color primaries",  0, AV_OPT_TYPE_CONST, {.i64=-1},                     INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"bt709",                           NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT709},        INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"unknown",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_UNSPECIFIED},  INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"bt470m",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT470M},       INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"bt470bg",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT470BG},      INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte170m",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE170M},    INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte240m",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE240M},    INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"film",                            NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_FILM},         INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"bt2020",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT2020},       INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte428",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE428},     INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte431",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE431},     INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"smpte432",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE432},     INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"jedec-p22",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_JEDEC_P22},    INT_MIN, INT_MAX, STATIC, "color_primaries"},
+    {"ebu3213",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_EBU3213},      INT_MIN, INT_MAX, STATIC, "color_primaries"},
+
+    {"color_trc", "select color transfer", OFFSET(color_trc), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_TRC_NB-1, DYNAMIC, "color_trc"},
+    {"auto", "keep the same color transfer",  0, AV_OPT_TYPE_CONST, {.i64=-1},                     INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt709",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT709},        INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"unknown",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_UNSPECIFIED},  INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt470m",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_GAMMA22},      INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt470bg",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_GAMMA28},      INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"smpte170m",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE170M},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"smpte240m",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE240M},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"linear",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_LINEAR},       INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"log100",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_LOG},          INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"log316",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_LOG_SQRT},     INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"iec61966-2-4",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_IEC61966_2_4}, INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt1361e",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT1361_ECG},   INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"iec61966-2-1",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_IEC61966_2_1}, INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt2020-10",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT2020_10},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"bt2020-12",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT2020_12},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"smpte2084",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE2084},    INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"smpte428",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE428},     INT_MIN, INT_MAX, STATIC, "color_trc"},
+    {"arib-std-b67",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_ARIB_STD_B67}, INT_MIN, INT_MAX, STATIC, "color_trc"},
+
+    { "upscaler", "Upscaler function", OFFSET(upscaler), AV_OPT_TYPE_STRING, {.str = "spline36"}, .flags = DYNAMIC },
+    { "downsacler", "Downscaler function", OFFSET(downscaler), AV_OPT_TYPE_STRING, {.str = "mitchell"}, .flags = DYNAMIC },
+    { "lut_entries", "Number of scaler LUT entries", OFFSET(lut_entries), AV_OPT_TYPE_INT, {.i64 = 0}, 0, 256, DYNAMIC },
+    { "antiringing", "Antiringing strength (for non-EWA filters)", OFFSET(antiringing), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 1.0, DYNAMIC },
+    { "sigmoid", "Enable sigmoid upscaling", OFFSET(sigmoid), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, DYNAMIC },
+
+    { "deband", "Enable debanding", OFFSET(deband), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "deband_iterations", "Deband iterations", OFFSET(deband_iterations), AV_OPT_TYPE_INT, {.i64 = 1}, 0, 16, DYNAMIC },
+    { "deband_threshold", "Deband threshold", OFFSET(deband_threshold), AV_OPT_TYPE_FLOAT, {.dbl = 4.0}, 0.0, 1024.0, DYNAMIC },
+    { "deband_radius", "Deband radius", OFFSET(deband_radius), AV_OPT_TYPE_FLOAT, {.dbl = 16.0}, 0.0, 1024.0, DYNAMIC },
+    { "deband_grain", "Deband grain", OFFSET(deband_grain), AV_OPT_TYPE_FLOAT, {.dbl = 6.0}, 0.0, 1024.0, DYNAMIC },
+
+    { "brightness", "Brightness boost", OFFSET(brightness), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, -1.0, 1.0, DYNAMIC },
+    { "contrast", "Contrast gain", OFFSET(contrast), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 0.0, 16.0, DYNAMIC },
+    { "saturation", "Saturation gain", OFFSET(saturation), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 0.0, 16.0, DYNAMIC },
+    { "hue", "Hue shift", OFFSET(hue), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, -M_PI, M_PI, DYNAMIC },
+    { "gamma", "Gamma adjustment", OFFSET(gamma), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 0.0, 16.0, DYNAMIC },
+
+    { "peak_detect", "Enable dynamic peak detection for HDR tone-mapping", OFFSET(peakdetect), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, DYNAMIC },
+    { "smoothing_period", "Peak detection smoothing period", OFFSET(smoothing), AV_OPT_TYPE_FLOAT, {.dbl = 100.0}, 0.0, 1000.0, DYNAMIC },
+    { "scene_threshold_low", "Scene change low threshold", OFFSET(scene_low), AV_OPT_TYPE_FLOAT, {.dbl = 5.5}, -1.0, 100.0, DYNAMIC },
+    { "scene_threshold_high", "Scene change high threshold", OFFSET(scene_high), AV_OPT_TYPE_FLOAT, {.dbl = 10.0}, -1.0, 100.0, DYNAMIC },
+    { "overshoot", "Tone-mapping overshoot margin", OFFSET(overshoot), AV_OPT_TYPE_FLOAT, {.dbl = 0.05}, 0.0, 1.0, DYNAMIC },
+
+    { "intent", "Rendering intent", OFFSET(intent), AV_OPT_TYPE_INT, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 3, DYNAMIC, "intent" },
+        { "perceptual", "Perceptual", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_PERCEPTUAL}, 0, 0, STATIC, "intent" },
+        { "relative", "Relative colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_RELATIVE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
+        { "absolute", "Absolute colorimetric", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_ABSOLUTE_COLORIMETRIC}, 0, 0, STATIC, "intent" },
+        { "saturation", "Saturation mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_INTENT_SATURATION}, 0, 0, STATIC, "intent" },
+    { "tonemapping", "Tone-mapping algorithm", OFFSET(tonemapping), AV_OPT_TYPE_INT, {.i64 = PL_TONE_MAPPING_BT_2390}, 0, PL_TONE_MAPPING_ALGORITHM_COUNT - 1, DYNAMIC, "tonemap" },
+        { "clip", "Hard-clipping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_CLIP}, 0, 0, STATIC, "tonemap" },
+        { "mobius", "Mobius tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_MOBIUS}, 0, 0, STATIC, "tonemap" },
+        { "reinhard", "Reinhard tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_REINHARD}, 0, 0, STATIC, "tonemap" },
+        { "hable", "Hable/Filmic tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_HABLE}, 0, 0, STATIC, "tonemap" },
+        { "gamma", "Gamma tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_GAMMA}, 0, 0, STATIC, "tonemap" },
+        { "linear", "Linear tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_LINEAR}, 0, 0, STATIC, "tonemap" },
+        { "bt.2390", "ITU-R BT.2390 tone-mapping", 0, AV_OPT_TYPE_CONST, {.i64 = PL_TONE_MAPPING_BT_2390}, 0, 0, STATIC, "tonemap" },
+    { "tonemapping_param", "Tunable parameter for some tone-mapping functions", OFFSET(tonemapping_param), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, .flags = DYNAMIC },
+    { "desaturation_strength", "Desaturation strength", OFFSET(desat_str), AV_OPT_TYPE_FLOAT, {.dbl = 0.75}, 0.0, 1.0, DYNAMIC },
+    { "desaturation_exponent", "Desaturation exponent", OFFSET(desat_exp), AV_OPT_TYPE_FLOAT, {.dbl = 1.5}, 0.0, 10.0, DYNAMIC },
+    { "desaturation_base", "Desaturation base", OFFSET(desat_base), AV_OPT_TYPE_FLOAT, {.dbl = 0.18}, 0.0, 10.0, DYNAMIC },
+    { "max_boost", "Tone-mapping maximum boost", OFFSET(max_boost), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 1.0, 10.0, DYNAMIC },
+    { "gamut_warning", "Highlight out-of-gamut colors", OFFSET(gamut_warning), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "gamut_clipping", "Enable colorimetric gamut clipping", OFFSET(gamut_clipping), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, DYNAMIC },
+
+    { "dithering", "Dither method to use", OFFSET(dithering), AV_OPT_TYPE_INT, {.i64 = PL_DITHER_BLUE_NOISE}, -1, PL_DITHER_METHOD_COUNT - 1, DYNAMIC, "dither" },
+        { "none", "Disable dithering", 0, AV_OPT_TYPE_CONST, {.i64 = -1}, 0, 0, STATIC, "dither" },
+        { "blue", "Blue noise", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_BLUE_NOISE}, 0, 0, STATIC, "dither" },
+        { "ordered", "Ordered LUT", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_ORDERED_LUT}, 0, 0, STATIC, "dither" },
+        { "ordered_fixed", "Fixed function ordered", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_ORDERED_FIXED}, 0, 0, STATIC, "dither" },
+        { "white", "White noise", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_WHITE_NOISE}, 0, 0, STATIC, "dither" },
+    { "dither_lut_size", "Dithering LUT size", OFFSET(dither_lut_size), AV_OPT_TYPE_INT, {.i64 = 6}, 1, 8, STATIC },
+    { "dither_temporal", "Enable temporal dithering", OFFSET(dither_temporal), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+
+    { "cones", "Colorblindness adaptation model", OFFSET(cones), AV_OPT_TYPE_FLAGS, {.i64 = 0}, 0, PL_CONE_LMS, DYNAMIC, "cone" },
+        { "l", "L cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_L}, 0, 0, STATIC, "cone" },
+        { "m", "M cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_M}, 0, 0, STATIC, "cone" },
+        { "s", "S cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_S}, 0, 0, STATIC, "cone" },
+    { "cone-strength", "Colorblindness adaptation strength", OFFSET(cone_str), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 10.0, DYNAMIC },
+
+    { "custom_shader", "Custom user shader (.hook format)", OFFSET(shader), AV_OPT_TYPE_BINARY, .flags = STATIC },
+
+    /* Performance/quality tradeoff options */
+    { "skip_aa", "Skip anti-aliasing", OFFSET(skip_aa), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 0, DYNAMIC },
+    { "polar_cutoff", "Polar LUT cutoff", OFFSET(polar_cutoff), AV_OPT_TYPE_FLOAT, {.i64 = 0}, 0.0, 1.0, DYNAMIC },
+    { "disable_linear", "Disable linear scaling", OFFSET(disable_linear), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "disable_builtin", "Disable built-in scalers", OFFSET(disable_builtin), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "force_3dlut", "Force the use of a full 3DLUT", OFFSET(force_3dlut), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "force_dither", "Force dithering", OFFSET(force_dither), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { "disable_fbos", "Force-disable FBOs", OFFSET(disable_fbos), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
+    { NULL },
+};
+
+AVFILTER_DEFINE_CLASS(libplacebo);
+
+static const AVFilterPad libplacebo_inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .filter_frame = &libplacebo_filter_frame,
+        .config_props = &ff_vk_filter_config_input,
+    },
+    { NULL }
+};
+
+static const AVFilterPad libplacebo_outputs[] = {
+    {
+        .name = "default",
+        .type = AVMEDIA_TYPE_VIDEO,
+        .config_props = &libplacebo_config_output,
+    },
+    { NULL }
+};
+
+AVFilter ff_vf_libplacebo = {
+    .name           = "libplacebo",
+    .description    = NULL_IF_CONFIG_SMALL("Apply various GPU filters from libplacebo"),
+    .priv_size      = sizeof(LibplaceboContext),
+    .init           = &libplacebo_init,
+    .uninit         = &libplacebo_uninit,
+    .query_formats  = &ff_vk_filter_query_formats,
+    .process_command = &ff_filter_process_command,
+    .inputs         = libplacebo_inputs,
+    .outputs        = libplacebo_outputs,
+    .priv_class     = &libplacebo_class,
+    .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
+};