diff mbox series

[FFmpeg-devel,v2] lavfi: add a libplacebo filter

Message ID 20211112105847.69083-1-ffmpeg@haasn.xyz
State New
Headers show
Series [FFmpeg-devel,v2] lavfi: add a libplacebo filter
Related show

Checks

Context Check Description
andriy/configurex86 warning Failed to apply patch
andriy/configureppc warning Failed to apply patch

Commit Message

Niklas Haas Nov. 12, 2021, 10:58 a.m. UTC
From: Niklas Haas <git@haasn.dev>

This filter conceptually maps the libplacebo `pl_renderer` API into
libavfilter, which is a high-level image rendering API designed to work
with an RGB pipeline internally. As such, there's no way to avoid e.g.
chroma interpolation with this filter, although new versions of
libplacebo support outputting back to subsampled YCbCr after processing
is done.

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 approach this problem either by rewriting this
filter to also support a non-renderer codepath, or by upgrading
libplacebo's renderer to support a full YCbCr pipeline.

This unfortunately requires a very new version of libplacebo (unreleased
at time of writing) for timeline semaphore support. But the amount of
boilerplate needed to hack in backwards compatibility would have been
very unreasonable.
---
 configure                   |   3 +
 libavfilter/Makefile        |   1 +
 libavfilter/allfilters.c    |   1 +
 libavfilter/vf_libplacebo.c | 717 ++++++++++++++++++++++++++++++++++++
 4 files changed, 722 insertions(+)
 create mode 100644 libavfilter/vf_libplacebo.c

Comments

Lynne Nov. 12, 2021, 1:59 p.m. UTC | #1
Nov 12, 2021, 11:58 by ffmpeg@haasn.xyz:

> From: Niklas Haas <git@haasn.dev>
>
> This filter conceptually maps the libplacebo `pl_renderer` API into
> libavfilter, which is a high-level image rendering API designed to work
> with an RGB pipeline internally. As such, there's no way to avoid e.g.
> chroma interpolation with this filter, although new versions of
> libplacebo support outputting back to subsampled YCbCr after processing
> is done.
>
> 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 approach this problem either by rewriting this
> filter to also support a non-renderer codepath, or by upgrading
> libplacebo's renderer to support a full YCbCr pipeline.
>
> This unfortunately requires a very new version of libplacebo (unreleased
> at time of writing) for timeline semaphore support. But the amount of
> boilerplate needed to hack in backwards compatibility would have been
> very unreasonable.
> ---
>  configure                   |   3 +
>  libavfilter/Makefile        |   1 +
>  libavfilter/allfilters.c    |   1 +
>  libavfilter/vf_libplacebo.c | 717 ++++++++++++++++++++++++++++++++++++
>  4 files changed, 722 insertions(+)
>  create mode 100644 libavfilter/vf_libplacebo.c
>

Patch applied.
Thanks!
Dennis Mungai Nov. 15, 2021, 6:21 p.m. UTC | #2
On Fri, 12 Nov 2021 at 13:59, Niklas Haas <ffmpeg@haasn.xyz> wrote:

> From: Niklas Haas <git@haasn.dev>
>
> This filter conceptually maps the libplacebo `pl_renderer` API into
> libavfilter, which is a high-level image rendering API designed to work
> with an RGB pipeline internally. As such, there's no way to avoid e.g.
> chroma interpolation with this filter, although new versions of
> libplacebo support outputting back to subsampled YCbCr after processing
> is done.
>
> 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 approach this problem either by rewriting this
> filter to also support a non-renderer codepath, or by upgrading
> libplacebo's renderer to support a full YCbCr pipeline.
>
> This unfortunately requires a very new version of libplacebo (unreleased
> at time of writing) for timeline semaphore support. But the amount of
> boilerplate needed to hack in backwards compatibility would have been
> very unreasonable.
> ---
>  configure                   |   3 +
>  libavfilter/Makefile        |   1 +
>  libavfilter/allfilters.c    |   1 +
>  libavfilter/vf_libplacebo.c | 717 ++++++++++++++++++++++++++++++++++++
>  4 files changed, 722 insertions(+)
>  create mode 100644 libavfilter/vf_libplacebo.c
>
> diff --git a/configure b/configure
> index eb451d2782..891824757b 100755
> --- a/configure
> +++ b/configure
> @@ -1827,6 +1827,7 @@ EXTERNAL_LIBRARY_LIST="
>      libopenmpt
>      libopenvino
>      libopus
> +    libplacebo
>      libpulse
>      librabbitmq
>      librav1e
> @@ -3618,6 +3619,7 @@ interlace_filter_deps="gpl"
>  kerndeint_filter_deps="gpl"
>  ladspa_filter_deps="ladspa libdl"
>  lensfun_filter_deps="liblensfun version3"
> +libplacebo_filter_deps="libplacebo vulkan libglslang"
>  lv2_filter_deps="lv2"
>  mcdeint_filter_deps="avcodec gpl"
>  metadata_filter_deps="avformat"
> @@ -6493,6 +6495,7 @@ enabled libopus           && {
>          require_pkg_config libopus opus opus_multistream.h
> opus_multistream_surround_encoder_create
>      }
>  }
> +enabled libplacebo        && require_pkg_config libplacebo "libplacebo >=
> 4.173.0" 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.4.0"
> rav1e.h rav1e_context_new
> diff --git a/libavfilter/Makefile b/libavfilter/Makefile
> index 552bd4e286..e2059766b0 100644
> --- a/libavfilter/Makefile
> +++ b/libavfilter/Makefile
> @@ -323,6 +323,7 @@ OBJS-$(CONFIG_LAGFUN_FILTER)                 +=
> vf_lagfun.o
>  OBJS-$(CONFIG_LATENCY_FILTER)                += f_latency.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_LIMITDIFF_FILTER)              += vf_limitdiff.o framesync.o
>  OBJS-$(CONFIG_LIMITER_FILTER)                += vf_limiter.o
> diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
> index 667b6fc246..be94249024 100644
> --- a/libavfilter/allfilters.c
> +++ b/libavfilter/allfilters.c
> @@ -308,6 +308,7 @@ extern const AVFilter ff_vf_lagfun;
>  extern const AVFilter ff_vf_latency;
>  extern const AVFilter ff_vf_lenscorrection;
>  extern const AVFilter ff_vf_lensfun;
> +extern const AVFilter ff_vf_libplacebo;
>  extern const AVFilter ff_vf_libvmaf;
>  extern const AVFilter ff_vf_limitdiff;
>  extern const AVFilter ff_vf_limiter;
> diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c
> new file mode 100644
> index 0000000000..3b48674d1a
> --- /dev/null
> +++ b/libavfilter/vf_libplacebo.c
> @@ -0,0 +1,717 @@
> +/*
> + * 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/file.h"
> +#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*/
> +    FFVulkanContext vkctx;
> +    int initialized;
> +
> +    /* libplacebo */
> +    pl_log log;
> +    pl_vulkan vulkan;
> +    pl_gpu gpu;
> +    pl_renderer renderer;
> +
> +    /* settings */
> +    char *out_format_string;
> +    char *w_expr;
> +    char *h_expr;
> +    AVRational target_sar;
> +    float pad_crop_ratio;
> +    int force_original_aspect_ratio;
> +    int force_divisible_by;
> +    int normalize_sar;
> +    int apply_filmgrain;
> +    int colorspace;
> +    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 min_peak;
> +    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 */
> +    char *shader_path;
> +    void *shader_bin;
> +    int shader_bin_len;
> +    const struct pl_hook *hooks[2];
> +    int num_hooks;
> +} 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_VERBOSE; break;
> +    case PL_LOG_DEBUG:  av_lev = AV_LOG_DEBUG;   break;
> +    case PL_LOG_TRACE:  av_lev = AV_LOG_TRACE;   break;
> +    default: return;
> +    }
> +
> +    av_log(log_ctx, av_lev, "%s\n", msg);
> +}
> +
> +static int parse_shader(AVFilterContext *avctx, const void *shader,
> size_t len)
> +{
> +    LibplaceboContext *s = avctx->priv;
> +    const struct pl_hook *hook;
> +
> +    hook = pl_mpv_user_shader_parse(s->gpu, shader, len);
> +    if (!hook) {
> +        av_log(s, AV_LOG_ERROR, "Failed parsing custom shader!\n");
> +        return AVERROR(EINVAL);
> +    }
> +
> +    s->hooks[s->num_hooks++] = hook;
> +    return 0;
> +}
> +
> +static int find_scaler(AVFilterContext *avctx,
> +                       const struct pl_filter_config **opt,
> +                       const char *name)
> +{
> +    const struct pl_filter_preset *preset;
> +    if (!strcmp(name, "help")) {
> +        av_log(avctx, AV_LOG_INFO, "Available scaler presets:\n");
> +        for (preset = pl_scale_filters; preset->name; preset++)
> +            av_log(avctx, AV_LOG_INFO, "    %s\n", preset->name);
> +        return AVERROR_EXIT;
> +    }
> +
> +    for (preset = pl_scale_filters; preset->name; preset++) {
> +        if (!strcmp(name, preset->name)) {
> +            *opt = preset->filter;
> +            return 0;
> +        }
> +    }
> +
> +    av_log(avctx, AV_LOG_ERROR, "No such scaler preset '%s'.\n", name);
> +    return AVERROR(EINVAL);
> +}
> +
> +static int libplacebo_init(AVFilterContext *avctx)
> +{
> +    LibplaceboContext *s = avctx->priv;
> +
> +    /* Create libplacebo log context */
> +    s->log = pl_log_create(PL_API_VER, pl_log_params(
> +        .log_level = PL_LOG_DEBUG,
> +        .log_cb = pl_av_log,
> +        .log_priv = s,
> +    ));
> +
> +    if (!s->log)
> +        return AVERROR(ENOMEM);
> +
> +    /* Note: s->vulkan etc. are initialized later, when hwctx is
> available */
> +    return 0;
> +}
> +
> +static int init_vulkan(AVFilterContext *avctx)
> +{
> +    int err = 0;
> +    LibplaceboContext *s = avctx->priv;
> +    const AVVulkanDeviceContext *hwctx = s->vkctx.hwctx;
> +    uint8_t *buf = NULL;
> +    size_t buf_len;
> +
> +    /* Import libavfilter vulkan context into libplacebo */
> +    s->vulkan = pl_vulkan_import(s->log, pl_vulkan_import_params(
> +        .instance       = hwctx->inst,
> +        .get_proc_addr  = hwctx->get_proc_addr,
> +        .phys_device    = hwctx->phys_dev,
> +        .device         = hwctx->act_dev,
> +        .extensions     = hwctx->enabled_dev_extensions,
> +        .num_extensions = hwctx->nb_enabled_dev_extensions,
> +        .features       = &hwctx->device_features,
> +        .queue_graphics = {
> +            .index = hwctx->queue_family_index,
> +            .count = hwctx->nb_graphics_queues,
> +        },
> +        .queue_compute = {
> +            .index = hwctx->queue_family_comp_index,
> +            .count = hwctx->nb_comp_queues,
> +        },
> +        .queue_transfer = {
> +            .index = hwctx->queue_family_tx_index,
> +            .count = hwctx->nb_tx_queues,
> +        },
> +        /* This is the highest version created by hwcontext_vulkan.c */
> +        .max_api_version = VK_API_VERSION_1_2,
> +    ));
> +
> +    if (!s->vulkan) {
> +        av_log(s, AV_LOG_ERROR, "Failed importing vulkan device to
> libplacebo!\n");
> +        err = AVERROR_EXTERNAL;
> +        goto fail;
> +    }
> +
> +    /* Create the renderer */
> +    s->gpu = s->vulkan->gpu;
> +    s->renderer = pl_renderer_create(s->log, s->gpu);
> +
> +    /* Parse the user shaders, if requested */
> +    if (s->shader_bin_len)
> +        RET(parse_shader(avctx, s->shader_bin, s->shader_bin_len));
> +
> +    if (s->shader_path && s->shader_path[0]) {
> +        RET(av_file_map(s->shader_path, &buf, &buf_len, 0, s));
> +        RET(parse_shader(avctx, buf, buf_len));
> +    }
> +
> +    /* fall through */
> +fail:
> +    if (buf)
> +        av_file_unmap(buf, buf_len);
> +    s->initialized =  1;
> +    return err;
> +}
> +
> +static void libplacebo_uninit(AVFilterContext *avctx)
> +{
> +    LibplaceboContext *s = avctx->priv;
> +
> +    for (int i = 0; i < s->num_hooks; i++)
> +        pl_mpv_user_shader_destroy(&s->hooks[i]);
> +    pl_renderer_destroy(&s->renderer);
> +    pl_vulkan_destroy(&s->vulkan);
> +    pl_log_destroy(&s->log);
> +    ff_vk_filter_uninit(avctx);
> +    s->initialized = 0;
> +    s->gpu = NULL;
> +}
> +
> +static int wrap_vkframe(pl_gpu gpu, const AVFrame *frame, int plane,
> pl_tex *tex)
> +{
> +    AVVkFrame *vkf = (AVVkFrame *) frame->data[0];
> +    const AVHWFramesContext *hwfc = (AVHWFramesContext *)
> frame->hw_frames_ctx->data;
> +    const AVVulkanFramesContext *vkfc = hwfc->hwctx;
> +    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(hwfc->sw_format);
> +    const VkFormat *vk_fmt = av_vkfmt_from_pixfmt(hwfc->sw_format);
> +    const int chroma = plane == 1 || plane == 2;
> +
> +    *tex = pl_vulkan_wrap(gpu, pl_vulkan_wrap_params(
> +        .image = vkf->img[plane],
> +        .format = vk_fmt[plane],
> +        .width = AV_CEIL_RSHIFT(frame->width, chroma ?
> desc->log2_chroma_w : 0),
> +        .height = AV_CEIL_RSHIFT(frame->height, chroma ?
> desc->log2_chroma_h : 0),
> +        .usage = vkfc->usage,
> +    ));
> +
> +    if (!*tex)
> +        return AVERROR(ENOMEM);
> +
> +    pl_vulkan_release(gpu, *tex, vkf->layout[plane], (pl_vulkan_sem) {
> +        .sem = vkf->sem[plane],
> +        .value = vkf->sem_value[plane]
> +    });
> +    return 0;
> +}
> +
> +static int unwrap_vkframe(pl_gpu gpu, AVFrame *frame, int plane, pl_tex
> *tex)
> +{
> +    AVVkFrame *vkf = (AVVkFrame *) frame->data[0];
> +    uint64_t sem_value = ++vkf->sem_value[plane];
> +    int ok = pl_vulkan_hold_raw(gpu, *tex, &vkf->layout[plane],
> +                                (pl_vulkan_sem) { vkf->sem[plane],
> sem_value });
> +    vkf->access[plane] = 0;
> +    return ok ? 0 : AVERROR_EXTERNAL;
> +}
> +
> +static void set_sample_depth(struct pl_frame *out_frame, const AVFrame
> *frame)
> +{
> +    const AVHWFramesContext *hwfc = (AVHWFramesContext *)
> frame->hw_frames_ctx->data;
> +    pl_fmt fmt = out_frame->planes[0].texture->params.format;
> +    struct pl_bit_encoding *bits = &out_frame->repr.bits;
> +    bits->sample_depth = fmt->component_depth[0];
> +
> +    switch (hwfc->sw_format) {
> +    case AV_PIX_FMT_P010: bits->bit_shift = 6; break;
> +    default: break;
> +    }
> +}
> +
> +static int process_frames(AVFilterContext *avctx, AVFrame *out, AVFrame
> *in)
> +{
> +    int err = 0;
> +    LibplaceboContext *s = avctx->priv;
> +    struct pl_render_params params;
> +    struct pl_frame image, target;
> +    pl_frame_from_avframe(&image, in);
> +    pl_frame_from_avframe(&target, out);
> +
> +    if (!s->apply_filmgrain)
> +        image.film_grain.type = PL_FILM_GRAIN_NONE;
> +
> +    if (s->target_sar.num) {
> +        float aspect = pl_rect2df_aspect(&target.crop) *
> av_q2d(s->target_sar);
> +        pl_rect2df_aspect_set(&target.crop, aspect, s->pad_crop_ratio);
> +    }
> +
> +    /* Update render params */
> +    params = (struct pl_render_params) {
> +        PL_RENDER_DEFAULTS
> +        .lut_entries = s->lut_entries,
> +        .antiringing_strength = s->antiringing,
> +
> +        .deband_params = !s->deband ? NULL : 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 :
> pl_peak_detect_params(
> +            .smoothing_period = s->smoothing,
> +            .minimum_peak = s->min_peak,
> +            .scene_threshold_low = s->scene_low,
> +            .scene_threshold_high = s->scene_high,
> +            .overshoot_margin = s->overshoot,
> +        ),
> +
> +        .color_map_params = 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 : pl_dither_params(
> +            .method = s->dithering,
> +            .lut_size = s->dither_lut_size,
> +            .temporal = s->dither_temporal,
> +        ),
> +
> +        .cone_params = !s->cones ? NULL : pl_cone_params(
> +            .cones = s->cones,
> +            .strength = s->cone_str,
> +        ),
> +
> +        .hooks = s->hooks,
> +        .num_hooks = s->num_hooks,
> +
> +        .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(s->gpu, in, i, &image.planes[i].texture));
> +    for (int i = 0; i < target.num_planes; i++)
> +        RET(wrap_vkframe(s->gpu, out, i, &target.planes[i].texture));
> +
> +    /* Since we-re mapping vkframes manually, the pl_frame helpers don't
> know
> +     * about the mismatch between the sample format and the color depth.
> */
> +    set_sample_depth(&image, in);
> +    set_sample_depth(&target, out);
> +
> +    pl_render_image(s->renderer, &image, &target, &params);
> +
> +    for (int i = 0; i < image.num_planes; i++)
> +        RET(unwrap_vkframe(s->gpu, in, i, &image.planes[i].texture));
> +    for (int i = 0; i < target.num_planes; i++)
> +        RET(unwrap_vkframe(s->gpu, out, i, &target.planes[i].texture));
> +
> +    /* Flush the command queues for performance */
> +    pl_gpu_flush(s->gpu);
> +
> +    /* fall through */
> +fail:
> +    for (int i = 0; i < image.num_planes; i++)
> +        pl_tex_destroy(s->gpu, &image.planes[i].texture);
> +    for (int i = 0; i < target.num_planes; i++)
> +        pl_tex_destroy(s->gpu, &target.planes[i].texture);
> +    return err;
> +}
> +
> +static int 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(init_vulkan(ctx));
> +
> +    RET(av_frame_copy_props(out, in));
> +    out->width = outlink->w;
> +    out->height = outlink->h;
> +
> +    if (s->colorspace >= 0)
> +        out->colorspace = s->colorspace;
> +    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));
> +
> +    if (s->apply_filmgrain)
> +        av_frame_remove_side_data(out, AV_FRAME_DATA_FILM_GRAIN_PARAMS);
> +
> +    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;
> +    AVRational scale_sar;
> +    int *out_w = &s->vkctx.output_width;
> +    int *out_h = &s->vkctx.output_height;
> +
> +    RET(ff_scale_eval_dimensions(s, s->w_expr, s->h_expr, inlink, outlink,
> +                                 out_w, out_h));
> +
> +    ff_scale_adjust_dimensions(inlink, out_w, out_h,
> +                               s->force_original_aspect_ratio,
> +                               s->force_divisible_by);
> +
> +    scale_sar = (AVRational){outlink->h * inlink->w, *out_w * *out_h};
> +    if (inlink->sample_aspect_ratio.num)
> +        scale_sar = av_mul_q(scale_sar, inlink->sample_aspect_ratio);
> +
> +    if (s->normalize_sar) {
> +        /* Apply all SAR during scaling, so we don't need to set the out
> SAR */
> +        s->target_sar = scale_sar;
> +    } else {
> +        /* This is consistent with other scale_* filters, which only
> +         * set the outlink SAR to be equal to the scale SAR iff the input
> SAR
> +         * was set to something nonzero */
> +        if (inlink->sample_aspect_ratio.num)
> +            outlink->sample_aspect_ratio = scale_sar;
> +    }
> +
> +    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);
> +        }
> +    } else {
> +        /* Default to re-using the input format */
> +        s->vkctx.output_format = s->vkctx.input_format;
> +    }
> +
> +    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", OFFSET(out_format_string),
> AV_OPT_TYPE_STRING, .flags = STATIC },
> +    { "force_original_aspect_ratio", "decrease or increase w/h if
> necessary to keep the original AR", OFFSET(force_original_aspect_ratio),
> AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 2, STATIC, "force_oar" },
> +        { "disable",  NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 0 }, 0, 0,
> STATIC, "force_oar" },
> +        { "decrease", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 1 }, 0, 0,
> STATIC, "force_oar" },
> +        { "increase", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 2 }, 0, 0,
> STATIC, "force_oar" },
> +    { "force_divisible_by", "enforce that the output resolution is
> divisible by a defined integer when force_original_aspect_ratio is used",
> OFFSET(force_divisible_by), AV_OPT_TYPE_INT, { .i64 = 1 }, 1, 256, STATIC },
> +    { "normalize_sar", "force SAR normalization to 1:1",
> OFFSET(normalize_sar), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, STATIC },
> +    { "pad_crop_ratio", "ratio between padding and cropping when
> normalizing SAR (0=pad, 1=crop)", OFFSET(pad_crop_ratio),
> AV_OPT_TYPE_FLOAT, {.dbl=0.0}, 0.0, 1.0, DYNAMIC },
> +
> +    {"colorspace", "select colorspace", OFFSET(colorspace),
> AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_SPC_NB-1, DYNAMIC, "colorspace"},
> +    {"auto", "keep the same colorspace",  0, AV_OPT_TYPE_CONST,
> {.i64=-1},                          INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"gbr",                        NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_RGB},               INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"bt709",                      NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_BT709},             INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"unknown",                    NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_UNSPECIFIED},       INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"bt470bg",                    NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_BT470BG},           INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"smpte170m",                  NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_SMPTE170M},         INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"smpte240m",                  NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_SMPTE240M},         INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"ycgco",                      NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_YCGCO},             INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"bt2020nc",                   NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_BT2020_NCL},        INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"bt2020c",                    NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_BT2020_CL},         INT_MIN, INT_MAX, STATIC, "colorspace"},
> +    {"ictcp",                      NULL,  0, AV_OPT_TYPE_CONST,
> {.i64=AVCOL_SPC_ICTCP},             INT_MIN, INT_MAX, STATIC, "colorspace"},
> +
> +    {"range", "select color range", OFFSET(color_range), AV_OPT_TYPE_INT,
> {.i64=-1}, -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"},
> +    {"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"},
> +    {"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 },
> +    { "downscaler", "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 },
> +    { "apply_filmgrain", "Apply film grain metadata",
> OFFSET(apply_filmgrain), 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
> },
> +    { "minimum_peak", "Peak detection minimum peak", OFFSET(min_peak),
> AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 0.0, 100.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},
> 0.0, 100.0, .flags = DYNAMIC },
> +    { "desaturation_strength", "Desaturation strength",
> OFFSET(desat_str), AV_OPT_TYPE_FLOAT, {.dbl = 0.90}, 0.0, 1.0, DYNAMIC },
> +    { "desaturation_exponent", "Desaturation exponent",
> OFFSET(desat_exp), AV_OPT_TYPE_FLOAT, {.dbl = 0.2}, 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_path", "Path to custom user shader (mpv .hook
> format)", OFFSET(shader_path), AV_OPT_TYPE_STRING, .flags = STATIC },
> +    { "custom_shader_bin", "Custom user shader as binary (mpv .hook
> format)", OFFSET(shader_bin), 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 = &filter_frame,
> +        .config_props = &ff_vk_filter_config_input,
> +    },
> +};
> +
> +static const AVFilterPad libplacebo_outputs[] = {
> +    {
> +        .name         = "default",
> +        .type         = AVMEDIA_TYPE_VIDEO,
> +        .config_props = &libplacebo_config_output,
> +    },
> +};
> +
> +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,
> +    .process_command = &ff_filter_process_command,
> +    FILTER_INPUTS(libplacebo_inputs),
> +    FILTER_OUTPUTS(libplacebo_outputs),
> +    FILTER_SINGLE_PIXFMT(AV_PIX_FMT_VULKAN),
> +    .priv_class     = &libplacebo_class,
> +    .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
> +};
> --
> 2.33.1
>
>
>
Hello.

Were you able to build FFmpeg with this filter enabled?
So far, building this package does not generate any pkgconfig file in the
configured prefix, and FFmpeg's ./configure cannot detect it.
I filed the issue on libplacebo's project page, with more details:
https://github.com/haasn/libplacebo/issues/110

Warm regards,

Dennis.
Timo Rothenpieler Nov. 15, 2021, 7:25 p.m. UTC | #3
On 15.11.2021 19:21, Dennis Mungai wrote:
>>
> Hello.
> 
> Were you able to build FFmpeg with this filter enabled?
> So far, building this package does not generate any pkgconfig file in the
> configured prefix, and FFmpeg's ./configure cannot detect it.
> I filed the issue on libplacebo's project page, with more details:
> https://github.com/haasn/libplacebo/issues/110

Yes, I had no issues with that.
Had to slightly patch the .pc file for static linking for my needs, but 
other than that, it just works:

https://github.com/BtbN/FFmpeg-Builds/blob/master/scripts.d/50-vulkan/60-libplacebo.sh
Dennis Mungai Nov. 17, 2021, 3:51 p.m. UTC | #4
On Mon, 15 Nov 2021 at 22:26, Timo Rothenpieler <timo@rothenpieler.org>
wrote:

> On 15.11.2021 19:21, Dennis Mungai wrote:
> >>
> > Hello.
> >
> > Were you able to build FFmpeg with this filter enabled?
> > So far, building this package does not generate any pkgconfig file in the
> > configured prefix, and FFmpeg's ./configure cannot detect it.
> > I filed the issue on libplacebo's project page, with more details:
> > https://github.com/haasn/libplacebo/issues/110
>
> Yes, I had no issues with that.
> Had to slightly patch the .pc file for static linking for my needs, but
> other than that, it just works:
>
>
> https://github.com/BtbN/FFmpeg-Builds/blob/master/scripts.d/50-vulkan/60-libplacebo.sh
> _______________________________________________
>
>
I was also able to get a build with  libplacebo up and running, thanks.

What I now run into is a segfault everytime libplacebo is invoked.
diff mbox series

Patch

diff --git a/configure b/configure
index eb451d2782..891824757b 100755
--- a/configure
+++ b/configure
@@ -1827,6 +1827,7 @@  EXTERNAL_LIBRARY_LIST="
     libopenmpt
     libopenvino
     libopus
+    libplacebo
     libpulse
     librabbitmq
     librav1e
@@ -3618,6 +3619,7 @@  interlace_filter_deps="gpl"
 kerndeint_filter_deps="gpl"
 ladspa_filter_deps="ladspa libdl"
 lensfun_filter_deps="liblensfun version3"
+libplacebo_filter_deps="libplacebo vulkan libglslang"
 lv2_filter_deps="lv2"
 mcdeint_filter_deps="avcodec gpl"
 metadata_filter_deps="avformat"
@@ -6493,6 +6495,7 @@  enabled libopus           && {
         require_pkg_config libopus opus opus_multistream.h opus_multistream_surround_encoder_create
     }
 }
+enabled libplacebo        && require_pkg_config libplacebo "libplacebo >= 4.173.0" 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.4.0" rav1e.h rav1e_context_new
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 552bd4e286..e2059766b0 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -323,6 +323,7 @@  OBJS-$(CONFIG_LAGFUN_FILTER)                 += vf_lagfun.o
 OBJS-$(CONFIG_LATENCY_FILTER)                += f_latency.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_LIMITDIFF_FILTER)              += vf_limitdiff.o framesync.o
 OBJS-$(CONFIG_LIMITER_FILTER)                += vf_limiter.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 667b6fc246..be94249024 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -308,6 +308,7 @@  extern const AVFilter ff_vf_lagfun;
 extern const AVFilter ff_vf_latency;
 extern const AVFilter ff_vf_lenscorrection;
 extern const AVFilter ff_vf_lensfun;
+extern const AVFilter ff_vf_libplacebo;
 extern const AVFilter ff_vf_libvmaf;
 extern const AVFilter ff_vf_limitdiff;
 extern const AVFilter ff_vf_limiter;
diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c
new file mode 100644
index 0000000000..3b48674d1a
--- /dev/null
+++ b/libavfilter/vf_libplacebo.c
@@ -0,0 +1,717 @@ 
+/*
+ * 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/file.h"
+#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*/
+    FFVulkanContext vkctx;
+    int initialized;
+
+    /* libplacebo */
+    pl_log log;
+    pl_vulkan vulkan;
+    pl_gpu gpu;
+    pl_renderer renderer;
+
+    /* settings */
+    char *out_format_string;
+    char *w_expr;
+    char *h_expr;
+    AVRational target_sar;
+    float pad_crop_ratio;
+    int force_original_aspect_ratio;
+    int force_divisible_by;
+    int normalize_sar;
+    int apply_filmgrain;
+    int colorspace;
+    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 min_peak;
+    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 */
+    char *shader_path;
+    void *shader_bin;
+    int shader_bin_len;
+    const struct pl_hook *hooks[2];
+    int num_hooks;
+} 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_VERBOSE; break;
+    case PL_LOG_DEBUG:  av_lev = AV_LOG_DEBUG;   break;
+    case PL_LOG_TRACE:  av_lev = AV_LOG_TRACE;   break;
+    default: return;
+    }
+
+    av_log(log_ctx, av_lev, "%s\n", msg);
+}
+
+static int parse_shader(AVFilterContext *avctx, const void *shader, size_t len)
+{
+    LibplaceboContext *s = avctx->priv;
+    const struct pl_hook *hook;
+
+    hook = pl_mpv_user_shader_parse(s->gpu, shader, len);
+    if (!hook) {
+        av_log(s, AV_LOG_ERROR, "Failed parsing custom shader!\n");
+        return AVERROR(EINVAL);
+    }
+
+    s->hooks[s->num_hooks++] = hook;
+    return 0;
+}
+
+static int find_scaler(AVFilterContext *avctx,
+                       const struct pl_filter_config **opt,
+                       const char *name)
+{
+    const struct pl_filter_preset *preset;
+    if (!strcmp(name, "help")) {
+        av_log(avctx, AV_LOG_INFO, "Available scaler presets:\n");
+        for (preset = pl_scale_filters; preset->name; preset++)
+            av_log(avctx, AV_LOG_INFO, "    %s\n", preset->name);
+        return AVERROR_EXIT;
+    }
+
+    for (preset = pl_scale_filters; preset->name; preset++) {
+        if (!strcmp(name, preset->name)) {
+            *opt = preset->filter;
+            return 0;
+        }
+    }
+
+    av_log(avctx, AV_LOG_ERROR, "No such scaler preset '%s'.\n", name);
+    return AVERROR(EINVAL);
+}
+
+static int libplacebo_init(AVFilterContext *avctx)
+{
+    LibplaceboContext *s = avctx->priv;
+
+    /* Create libplacebo log context */
+    s->log = pl_log_create(PL_API_VER, pl_log_params(
+        .log_level = PL_LOG_DEBUG,
+        .log_cb = pl_av_log,
+        .log_priv = s,
+    ));
+
+    if (!s->log)
+        return AVERROR(ENOMEM);
+
+    /* Note: s->vulkan etc. are initialized later, when hwctx is available */
+    return 0;
+}
+
+static int init_vulkan(AVFilterContext *avctx)
+{
+    int err = 0;
+    LibplaceboContext *s = avctx->priv;
+    const AVVulkanDeviceContext *hwctx = s->vkctx.hwctx;
+    uint8_t *buf = NULL;
+    size_t buf_len;
+
+    /* Import libavfilter vulkan context into libplacebo */
+    s->vulkan = pl_vulkan_import(s->log, pl_vulkan_import_params(
+        .instance       = hwctx->inst,
+        .get_proc_addr  = hwctx->get_proc_addr,
+        .phys_device    = hwctx->phys_dev,
+        .device         = hwctx->act_dev,
+        .extensions     = hwctx->enabled_dev_extensions,
+        .num_extensions = hwctx->nb_enabled_dev_extensions,
+        .features       = &hwctx->device_features,
+        .queue_graphics = {
+            .index = hwctx->queue_family_index,
+            .count = hwctx->nb_graphics_queues,
+        },
+        .queue_compute = {
+            .index = hwctx->queue_family_comp_index,
+            .count = hwctx->nb_comp_queues,
+        },
+        .queue_transfer = {
+            .index = hwctx->queue_family_tx_index,
+            .count = hwctx->nb_tx_queues,
+        },
+        /* This is the highest version created by hwcontext_vulkan.c */
+        .max_api_version = VK_API_VERSION_1_2,
+    ));
+
+    if (!s->vulkan) {
+        av_log(s, AV_LOG_ERROR, "Failed importing vulkan device to libplacebo!\n");
+        err = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    /* Create the renderer */
+    s->gpu = s->vulkan->gpu;
+    s->renderer = pl_renderer_create(s->log, s->gpu);
+
+    /* Parse the user shaders, if requested */
+    if (s->shader_bin_len)
+        RET(parse_shader(avctx, s->shader_bin, s->shader_bin_len));
+
+    if (s->shader_path && s->shader_path[0]) {
+        RET(av_file_map(s->shader_path, &buf, &buf_len, 0, s));
+        RET(parse_shader(avctx, buf, buf_len));
+    }
+
+    /* fall through */
+fail:
+    if (buf)
+        av_file_unmap(buf, buf_len);
+    s->initialized =  1;
+    return err;
+}
+
+static void libplacebo_uninit(AVFilterContext *avctx)
+{
+    LibplaceboContext *s = avctx->priv;
+
+    for (int i = 0; i < s->num_hooks; i++)
+        pl_mpv_user_shader_destroy(&s->hooks[i]);
+    pl_renderer_destroy(&s->renderer);
+    pl_vulkan_destroy(&s->vulkan);
+    pl_log_destroy(&s->log);
+    ff_vk_filter_uninit(avctx);
+    s->initialized = 0;
+    s->gpu = NULL;
+}
+
+static int wrap_vkframe(pl_gpu gpu, const AVFrame *frame, int plane, pl_tex *tex)
+{
+    AVVkFrame *vkf = (AVVkFrame *) frame->data[0];
+    const AVHWFramesContext *hwfc = (AVHWFramesContext *) frame->hw_frames_ctx->data;
+    const AVVulkanFramesContext *vkfc = hwfc->hwctx;
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(hwfc->sw_format);
+    const VkFormat *vk_fmt = av_vkfmt_from_pixfmt(hwfc->sw_format);
+    const int chroma = plane == 1 || plane == 2;
+
+    *tex = pl_vulkan_wrap(gpu, pl_vulkan_wrap_params(
+        .image = vkf->img[plane],
+        .format = vk_fmt[plane],
+        .width = AV_CEIL_RSHIFT(frame->width, chroma ? desc->log2_chroma_w : 0),
+        .height = AV_CEIL_RSHIFT(frame->height, chroma ? desc->log2_chroma_h : 0),
+        .usage = vkfc->usage,
+    ));
+
+    if (!*tex)
+        return AVERROR(ENOMEM);
+
+    pl_vulkan_release(gpu, *tex, vkf->layout[plane], (pl_vulkan_sem) {
+        .sem = vkf->sem[plane],
+        .value = vkf->sem_value[plane]
+    });
+    return 0;
+}
+
+static int unwrap_vkframe(pl_gpu gpu, AVFrame *frame, int plane, pl_tex *tex)
+{
+    AVVkFrame *vkf = (AVVkFrame *) frame->data[0];
+    uint64_t sem_value = ++vkf->sem_value[plane];
+    int ok = pl_vulkan_hold_raw(gpu, *tex, &vkf->layout[plane],
+                                (pl_vulkan_sem) { vkf->sem[plane], sem_value });
+    vkf->access[plane] = 0;
+    return ok ? 0 : AVERROR_EXTERNAL;
+}
+
+static void set_sample_depth(struct pl_frame *out_frame, const AVFrame *frame)
+{
+    const AVHWFramesContext *hwfc = (AVHWFramesContext *) frame->hw_frames_ctx->data;
+    pl_fmt fmt = out_frame->planes[0].texture->params.format;
+    struct pl_bit_encoding *bits = &out_frame->repr.bits;
+    bits->sample_depth = fmt->component_depth[0];
+
+    switch (hwfc->sw_format) {
+    case AV_PIX_FMT_P010: bits->bit_shift = 6; break;
+    default: break;
+    }
+}
+
+static int process_frames(AVFilterContext *avctx, AVFrame *out, AVFrame *in)
+{
+    int err = 0;
+    LibplaceboContext *s = avctx->priv;
+    struct pl_render_params params;
+    struct pl_frame image, target;
+    pl_frame_from_avframe(&image, in);
+    pl_frame_from_avframe(&target, out);
+
+    if (!s->apply_filmgrain)
+        image.film_grain.type = PL_FILM_GRAIN_NONE;
+
+    if (s->target_sar.num) {
+        float aspect = pl_rect2df_aspect(&target.crop) * av_q2d(s->target_sar);
+        pl_rect2df_aspect_set(&target.crop, aspect, s->pad_crop_ratio);
+    }
+
+    /* Update render params */
+    params = (struct pl_render_params) {
+        PL_RENDER_DEFAULTS
+        .lut_entries = s->lut_entries,
+        .antiringing_strength = s->antiringing,
+
+        .deband_params = !s->deband ? NULL : 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 : pl_peak_detect_params(
+            .smoothing_period = s->smoothing,
+            .minimum_peak = s->min_peak,
+            .scene_threshold_low = s->scene_low,
+            .scene_threshold_high = s->scene_high,
+            .overshoot_margin = s->overshoot,
+        ),
+
+        .color_map_params = 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 : pl_dither_params(
+            .method = s->dithering,
+            .lut_size = s->dither_lut_size,
+            .temporal = s->dither_temporal,
+        ),
+
+        .cone_params = !s->cones ? NULL : pl_cone_params(
+            .cones = s->cones,
+            .strength = s->cone_str,
+        ),
+
+        .hooks = s->hooks,
+        .num_hooks = s->num_hooks,
+
+        .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(s->gpu, in, i, &image.planes[i].texture));
+    for (int i = 0; i < target.num_planes; i++)
+        RET(wrap_vkframe(s->gpu, out, i, &target.planes[i].texture));
+
+    /* Since we-re mapping vkframes manually, the pl_frame helpers don't know
+     * about the mismatch between the sample format and the color depth. */
+    set_sample_depth(&image, in);
+    set_sample_depth(&target, out);
+
+    pl_render_image(s->renderer, &image, &target, &params);
+
+    for (int i = 0; i < image.num_planes; i++)
+        RET(unwrap_vkframe(s->gpu, in, i, &image.planes[i].texture));
+    for (int i = 0; i < target.num_planes; i++)
+        RET(unwrap_vkframe(s->gpu, out, i, &target.planes[i].texture));
+
+    /* Flush the command queues for performance */
+    pl_gpu_flush(s->gpu);
+
+    /* fall through */
+fail:
+    for (int i = 0; i < image.num_planes; i++)
+        pl_tex_destroy(s->gpu, &image.planes[i].texture);
+    for (int i = 0; i < target.num_planes; i++)
+        pl_tex_destroy(s->gpu, &target.planes[i].texture);
+    return err;
+}
+
+static int 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(init_vulkan(ctx));
+
+    RET(av_frame_copy_props(out, in));
+    out->width = outlink->w;
+    out->height = outlink->h;
+
+    if (s->colorspace >= 0)
+        out->colorspace = s->colorspace;
+    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));
+
+    if (s->apply_filmgrain)
+        av_frame_remove_side_data(out, AV_FRAME_DATA_FILM_GRAIN_PARAMS);
+
+    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;
+    AVRational scale_sar;
+    int *out_w = &s->vkctx.output_width;
+    int *out_h = &s->vkctx.output_height;
+
+    RET(ff_scale_eval_dimensions(s, s->w_expr, s->h_expr, inlink, outlink,
+                                 out_w, out_h));
+
+    ff_scale_adjust_dimensions(inlink, out_w, out_h,
+                               s->force_original_aspect_ratio,
+                               s->force_divisible_by);
+
+    scale_sar = (AVRational){outlink->h * inlink->w, *out_w * *out_h};
+    if (inlink->sample_aspect_ratio.num)
+        scale_sar = av_mul_q(scale_sar, inlink->sample_aspect_ratio);
+
+    if (s->normalize_sar) {
+        /* Apply all SAR during scaling, so we don't need to set the out SAR */
+        s->target_sar = scale_sar;
+    } else {
+        /* This is consistent with other scale_* filters, which only
+         * set the outlink SAR to be equal to the scale SAR iff the input SAR
+         * was set to something nonzero */
+        if (inlink->sample_aspect_ratio.num)
+            outlink->sample_aspect_ratio = scale_sar;
+    }
+
+    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);
+        }
+    } else {
+        /* Default to re-using the input format */
+        s->vkctx.output_format = s->vkctx.input_format;
+    }
+
+    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", OFFSET(out_format_string), AV_OPT_TYPE_STRING, .flags = STATIC },
+    { "force_original_aspect_ratio", "decrease or increase w/h if necessary to keep the original AR", OFFSET(force_original_aspect_ratio), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 2, STATIC, "force_oar" },
+        { "disable",  NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 0 }, 0, 0, STATIC, "force_oar" },
+        { "decrease", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 1 }, 0, 0, STATIC, "force_oar" },
+        { "increase", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = 2 }, 0, 0, STATIC, "force_oar" },
+    { "force_divisible_by", "enforce that the output resolution is divisible by a defined integer when force_original_aspect_ratio is used", OFFSET(force_divisible_by), AV_OPT_TYPE_INT, { .i64 = 1 }, 1, 256, STATIC },
+    { "normalize_sar", "force SAR normalization to 1:1", OFFSET(normalize_sar), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, STATIC },
+    { "pad_crop_ratio", "ratio between padding and cropping when normalizing SAR (0=pad, 1=crop)", OFFSET(pad_crop_ratio), AV_OPT_TYPE_FLOAT, {.dbl=0.0}, 0.0, 1.0, DYNAMIC },
+
+    {"colorspace", "select colorspace", OFFSET(colorspace), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_SPC_NB-1, DYNAMIC, "colorspace"},
+    {"auto", "keep the same colorspace",  0, AV_OPT_TYPE_CONST, {.i64=-1},                          INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"gbr",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_RGB},               INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"bt709",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT709},             INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"unknown",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_UNSPECIFIED},       INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"bt470bg",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT470BG},           INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"smpte170m",                  NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_SMPTE170M},         INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"smpte240m",                  NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_SMPTE240M},         INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"ycgco",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_YCGCO},             INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"bt2020nc",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT2020_NCL},        INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"bt2020c",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT2020_CL},         INT_MIN, INT_MAX, STATIC, "colorspace"},
+    {"ictcp",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_ICTCP},             INT_MIN, INT_MAX, STATIC, "colorspace"},
+
+    {"range", "select color range", OFFSET(color_range), AV_OPT_TYPE_INT, {.i64=-1}, -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"},
+    {"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"},
+    {"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 },
+    { "downscaler", "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 },
+    { "apply_filmgrain", "Apply film grain metadata", OFFSET(apply_filmgrain), 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 },
+    { "minimum_peak", "Peak detection minimum peak", OFFSET(min_peak), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, 0.0, 100.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}, 0.0, 100.0, .flags = DYNAMIC },
+    { "desaturation_strength", "Desaturation strength", OFFSET(desat_str), AV_OPT_TYPE_FLOAT, {.dbl = 0.90}, 0.0, 1.0, DYNAMIC },
+    { "desaturation_exponent", "Desaturation exponent", OFFSET(desat_exp), AV_OPT_TYPE_FLOAT, {.dbl = 0.2}, 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_path", "Path to custom user shader (mpv .hook format)", OFFSET(shader_path), AV_OPT_TYPE_STRING, .flags = STATIC },
+    { "custom_shader_bin", "Custom user shader as binary (mpv .hook format)", OFFSET(shader_bin), 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 = &filter_frame,
+        .config_props = &ff_vk_filter_config_input,
+    },
+};
+
+static const AVFilterPad libplacebo_outputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = &libplacebo_config_output,
+    },
+};
+
+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,
+    .process_command = &ff_filter_process_command,
+    FILTER_INPUTS(libplacebo_inputs),
+    FILTER_OUTPUTS(libplacebo_outputs),
+    FILTER_SINGLE_PIXFMT(AV_PIX_FMT_VULKAN),
+    .priv_class     = &libplacebo_class,
+    .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
+};