/*
 * 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 <math.h>

#include "libavutil/avassert.h"
#include "libavutil/avstring.h"
#include "libavutil/eval.h"
#include "libavutil/fifo.h"
#include "libavutil/file.h"
#include "libavutil/frame.h"
#include "libavutil/mem.h"
#include "libavutil/opt.h"
#include "libavutil/parseutils.h"
#include "formats.h"
#include "filters.h"
#include "video.h"
#include "vulkan_filter.h"
#include "scale_eval.h"

#include <libplacebo/options.h>
#include <libplacebo/renderer.h>
#include <libplacebo/utils/libav.h>
#include <libplacebo/utils/frame_queue.h>
#include <libplacebo/vulkan.h>

enum {
    TONE_MAP_AUTO,
    TONE_MAP_CLIP,
    TONE_MAP_ST2094_40,
    TONE_MAP_ST2094_10,
    TONE_MAP_BT2390,
    TONE_MAP_BT2446A,
    TONE_MAP_SPLINE,
    TONE_MAP_REINHARD,
    TONE_MAP_MOBIUS,
    TONE_MAP_HABLE,
    TONE_MAP_GAMMA,
    TONE_MAP_LINEAR,
    TONE_MAP_COUNT,
};

enum {
    GAMUT_MAP_CLIP,
    GAMUT_MAP_PERCEPTUAL,
    GAMUT_MAP_RELATIVE,
    GAMUT_MAP_SATURATION,
    GAMUT_MAP_ABSOLUTE,
    GAMUT_MAP_DESATURATE,
    GAMUT_MAP_DARKEN,
    GAMUT_MAP_HIGHLIGHT,
    GAMUT_MAP_LINEAR,
    GAMUT_MAP_COUNT,
};

static const char *const var_names[] = {
    "in_idx", "idx",///< index of input
    "in_w", "iw",   ///< width  of the input video frame
    "in_h", "ih",   ///< height of the input video frame
    "out_w", "ow",  ///< width  of the output video frame
    "out_h", "oh",  ///< height of the output video frame
    "crop_w", "cw", ///< evaluated input crop width
    "crop_h", "ch", ///< evaluated input crop height
    "pos_w", "pw",  ///< evaluated output placement width
    "pos_h", "ph",  ///< evaluated output placement height
    "a",            ///< iw/ih
    "sar",          ///< input pixel aspect ratio
    "dar",          ///< output pixel aspect ratio
    "hsub",         ///< input horizontal subsampling factor
    "vsub",         ///< input vertical subsampling factor
    "ohsub",        ///< output horizontal subsampling factor
    "ovsub",        ///< output vertical subsampling factor
    "in_t", "t",    ///< input frame pts
    "out_t", "ot",  ///< output frame pts
    "n",            ///< number of frame
    NULL,
};

enum var_name {
    VAR_IN_IDX, VAR_IDX,
    VAR_IN_W,   VAR_IW,
    VAR_IN_H,   VAR_IH,
    VAR_OUT_W,  VAR_OW,
    VAR_OUT_H,  VAR_OH,
    VAR_CROP_W, VAR_CW,
    VAR_CROP_H, VAR_CH,
    VAR_POS_W,  VAR_PW,
    VAR_POS_H,  VAR_PH,
    VAR_A,
    VAR_SAR,
    VAR_DAR,
    VAR_HSUB,
    VAR_VSUB,
    VAR_OHSUB,
    VAR_OVSUB,
    VAR_IN_T,   VAR_T,
    VAR_OUT_T,  VAR_OT,
    VAR_N,
    VAR_VARS_NB
};

/* per-input dynamic filter state */
typedef struct LibplaceboInput {
    int idx;
    pl_renderer renderer;
    pl_queue queue;
    enum pl_queue_status qstatus;
    struct pl_frame_mix mix; ///< temporary storage
    AVFifo *out_pts; ///< timestamps of wanted output frames
    int64_t status_pts;
    int status;
} LibplaceboInput;

enum fit_mode {
    FIT_FILL,
    FIT_CONTAIN,
    FIT_COVER,
    FIT_NONE,
    FIT_SCALE_DOWN,
    FIT_MODE_NB,
};

enum fit_sense {
    FIT_TARGET,
    FIT_CONSTRAINT,
    FIT_SENSE_NB,
};

typedef struct LibplaceboContext {
    /* lavfi vulkan*/
    FFVulkanContext vkctx;

    /* libplacebo */
    pl_log log;
    pl_vulkan vulkan;
    pl_gpu gpu;
    pl_tex tex[4];
    struct pl_custom_lut *lut;

    /* dedicated renderer for linear output composition */
    pl_renderer linear_rr;
    pl_tex linear_tex;

    /* input state */
    LibplaceboInput *inputs;
    int nb_inputs;
    int nb_active;

    /* settings */
    char *out_format_string;
    enum AVPixelFormat out_format;
    uint8_t fillcolor[4];
    double var_values[VAR_VARS_NB];
    char *w_expr;
    char *h_expr;
    char *fps_string;
    AVRational fps; ///< parsed FPS, or 0/0 for "none"
    char *crop_x_expr, *crop_y_expr;
    char *crop_w_expr, *crop_h_expr;
    char *pos_x_expr, *pos_y_expr;
    char *pos_w_expr, *pos_h_expr;
    // Parsed expressions for input/output crop
    AVExpr *crop_x_pexpr, *crop_y_pexpr, *crop_w_pexpr, *crop_h_pexpr;
    AVExpr *pos_x_pexpr, *pos_y_pexpr, *pos_w_pexpr, *pos_h_pexpr;
    float pad_crop_ratio;
    float corner_rounding;
    char *lut_filename;
    /* enum pl_lut_type */
    int lut_type;
    int force_original_aspect_ratio;
    int force_divisible_by;
    int reset_sar;
    int normalize_sar;
    int fit_mode;
    int fit_sense;
    int apply_filmgrain;
    int apply_dovi;
    int colorspace;
    int color_range;
    int color_primaries;
    int color_trc;
    int chroma_location;
    int rotation;
    int alpha_mode;
    AVDictionary *extra_opts;

    pl_cache cache;
    char *shader_cache;

    int have_hwdevice;

    /* pl_render_params */
    pl_options opts;
    char *upscaler;
    char *downscaler;
    char *frame_mixer;
    float antiringing;
    int sigmoid;
    int skip_aa;
    int disable_linear;
    int disable_builtin;
    int force_dither;
    int disable_fbos;

    /* pl_deinterlace_params */
    int deinterlace;
    int skip_spatial_check;
    int send_fields;

    /* 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;
    float temperature;

    /* pl_peak_detect_params */
    int peakdetect;
    float smoothing;
    float scene_low;
    float scene_high;
    float percentile;

    /* pl_color_map_params */
    int gamut_mode;
    int tonemapping;
    float tonemapping_param;
    int inverse_tonemapping;
    int tonemapping_lut_size;
    float contrast_recovery;
    float contrast_smoothness;

    /* 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 inline enum pl_log_level get_log_level(void)
{
    int av_lev = av_log_get_level();
    return av_lev >= AV_LOG_TRACE   ? PL_LOG_TRACE :
           av_lev >= AV_LOG_DEBUG   ? PL_LOG_DEBUG :
           av_lev >= AV_LOG_VERBOSE ? PL_LOG_INFO :
           av_lev >= AV_LOG_WARNING ? PL_LOG_WARN :
           av_lev >= AV_LOG_ERROR   ? PL_LOG_ERR :
           av_lev >= AV_LOG_FATAL   ? PL_LOG_FATAL :
                                      PL_LOG_NONE;
}

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 const struct pl_tone_map_function *get_tonemapping_func(int tm) {
    switch (tm) {
    case TONE_MAP_AUTO:       return &pl_tone_map_auto;
    case TONE_MAP_CLIP:       return &pl_tone_map_clip;
    case TONE_MAP_ST2094_40:  return &pl_tone_map_st2094_40;
    case TONE_MAP_ST2094_10:  return &pl_tone_map_st2094_10;
    case TONE_MAP_BT2390:     return &pl_tone_map_bt2390;
    case TONE_MAP_BT2446A:    return &pl_tone_map_bt2446a;
    case TONE_MAP_SPLINE:     return &pl_tone_map_spline;
    case TONE_MAP_REINHARD:   return &pl_tone_map_reinhard;
    case TONE_MAP_MOBIUS:     return &pl_tone_map_mobius;
    case TONE_MAP_HABLE:      return &pl_tone_map_hable;
    case TONE_MAP_GAMMA:      return &pl_tone_map_gamma;
    case TONE_MAP_LINEAR:     return &pl_tone_map_linear;
    default: av_assert0(0);
    }
}

static void set_gamut_mode(struct pl_color_map_params *p, int gamut_mode)
{
    switch (gamut_mode) {
    case GAMUT_MAP_CLIP:       p->gamut_mapping = &pl_gamut_map_clip; return;
    case GAMUT_MAP_PERCEPTUAL: p->gamut_mapping = &pl_gamut_map_perceptual; return;
    case GAMUT_MAP_RELATIVE:   p->gamut_mapping = &pl_gamut_map_relative; return;
    case GAMUT_MAP_SATURATION: p->gamut_mapping = &pl_gamut_map_saturation; return;
    case GAMUT_MAP_ABSOLUTE:   p->gamut_mapping = &pl_gamut_map_absolute; return;
    case GAMUT_MAP_DESATURATE: p->gamut_mapping = &pl_gamut_map_desaturate; return;
    case GAMUT_MAP_DARKEN:     p->gamut_mapping = &pl_gamut_map_darken; return;
    case GAMUT_MAP_HIGHLIGHT:  p->gamut_mapping = &pl_gamut_map_highlight; return;
    case GAMUT_MAP_LINEAR:     p->gamut_mapping = &pl_gamut_map_linear; return;
    }

    av_assert0(0);
};

static int find_scaler(AVFilterContext *avctx,
                       const struct pl_filter_config **opt,
                       const char *name, int frame_mixing)
{
    const struct pl_filter_preset *preset, *presets_avail;
    presets_avail = frame_mixing ? pl_frame_mixers : pl_scale_filters;

    if (!strcmp(name, "help")) {
        av_log(avctx, AV_LOG_INFO, "Available scaler presets:\n");
        for (preset = presets_avail; preset->name; preset++)
            av_log(avctx, AV_LOG_INFO, "    %s\n", preset->name);
        return AVERROR_EXIT;
    }

    for (preset = presets_avail; 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 parse_custom_lut(AVFilterContext *avctx)
{
    LibplaceboContext *s = avctx->priv;
    int ret;
    uint8_t *lutbuf;
    size_t lutbuf_size;

    if ((ret = av_file_map(s->lut_filename, &lutbuf, &lutbuf_size, 0, s)) < 0) {
        av_log(avctx, AV_LOG_ERROR,
               "The LUT file '%s' could not be read: %s\n",
               s->lut_filename, av_err2str(ret));
        return ret;
    }

    s->lut = pl_lut_parse_cube(s->log, lutbuf, lutbuf_size);
    av_file_unmap(lutbuf, lutbuf_size);
    if (!s->lut)
        return AVERROR(EINVAL);
    return 0;
}

static int update_settings(AVFilterContext *ctx)
{
    int err = 0;
    LibplaceboContext *s = ctx->priv;
    AVDictionaryEntry *e = NULL;
    pl_options opts = s->opts;
    int gamut_mode = s->gamut_mode;

    opts->deinterlace_params = *pl_deinterlace_params(
        .algo = s->deinterlace,
        .skip_spatial_check = s->skip_spatial_check,
    );

    opts->deband_params = *pl_deband_params(
        .iterations = s->deband_iterations,
        .threshold = s->deband_threshold,
        .radius = s->deband_radius,
        .grain = s->deband_grain,
    );

    opts->sigmoid_params = pl_sigmoid_default_params;

    opts->color_adjustment = (struct pl_color_adjustment) {
        .brightness = s->brightness,
        .contrast = s->contrast,
        .saturation = s->saturation,
        .hue = s->hue,
        .gamma = s->gamma,
        // libplacebo uses a normalized/relative scale for CCT
        .temperature = (s->temperature - 6500.0) / 3500.0,
    };

    opts->peak_detect_params = (struct pl_peak_detect_params) {
        PL_PEAK_DETECT_DEFAULTS
        .smoothing_period = s->smoothing,
        .scene_threshold_low = s->scene_low,
        .scene_threshold_high = s->scene_high,
        .percentile = s->percentile,
    };

    opts->color_map_params = (struct pl_color_map_params) {
        PL_COLOR_MAP_DEFAULTS
        .tone_mapping_function = get_tonemapping_func(s->tonemapping),
        .tone_mapping_param = s->tonemapping_param,
        .inverse_tone_mapping = s->inverse_tonemapping,
        .lut_size = s->tonemapping_lut_size,
        .contrast_recovery = s->contrast_recovery,
        .contrast_smoothness = s->contrast_smoothness,
    };

    set_gamut_mode(&opts->color_map_params, gamut_mode);

    opts->dither_params = *pl_dither_params(
        .method = s->dithering,
        .lut_size = s->dither_lut_size,
        .temporal = s->dither_temporal,
    );

    opts->cone_params = *pl_cone_params(
        .cones = s->cones,
        .strength = s->cone_str,
    );

    opts->params = (struct pl_render_params) {
        PL_RENDER_DEFAULTS
        .antiringing_strength = s->antiringing,
        .background_transparency = 1.0f - (float) s->fillcolor[3] / UINT8_MAX,
        .background_color = {
            (float) s->fillcolor[0] / UINT8_MAX,
            (float) s->fillcolor[1] / UINT8_MAX,
            (float) s->fillcolor[2] / UINT8_MAX,
        },
        .corner_rounding = s->corner_rounding,

        .deinterlace_params = &opts->deinterlace_params,
        .deband_params = s->deband ? &opts->deband_params : NULL,
        .sigmoid_params = s->sigmoid ? &opts->sigmoid_params : NULL,
        .color_adjustment = &opts->color_adjustment,
        .peak_detect_params = s->peakdetect ? &opts->peak_detect_params : NULL,
        .color_map_params = &opts->color_map_params,
        .dither_params = s->dithering >= 0 ? &opts->dither_params : NULL,
        .cone_params = s->cones ? &opts->cone_params : NULL,

        .hooks = s->hooks,
        .num_hooks = s->num_hooks,

        .skip_anti_aliasing = s->skip_aa,
        .disable_linear_scaling = s->disable_linear,
        .disable_builtin_scalers = s->disable_builtin,
        .force_dither = s->force_dither,
        .disable_fbos = s->disable_fbos,
    };

    RET(find_scaler(ctx, &opts->params.upscaler, s->upscaler, 0));
    RET(find_scaler(ctx, &opts->params.downscaler, s->downscaler, 0));
    RET(find_scaler(ctx, &opts->params.frame_mixer, s->frame_mixer, 1));

    while ((e = av_dict_get(s->extra_opts, "", e, AV_DICT_IGNORE_SUFFIX))) {
        if (!pl_options_set_str(s->opts, e->key, e->value)) {
            err = AVERROR(EINVAL);
            goto fail;
        }
    }

    return 0;

fail:
    return err;
}

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(avctx, AV_LOG_ERROR, "Failed parsing custom shader!\n");
        return AVERROR(EINVAL);
    }

    s->hooks[s->num_hooks++] = hook;
    return update_settings(avctx);
}

static void libplacebo_uninit(AVFilterContext *avctx);
static int libplacebo_config_input(AVFilterLink *inlink);
static int init_vulkan(AVFilterContext *avctx, const AVVulkanDeviceContext *hwctx);

static int libplacebo_init(AVFilterContext *avctx)
{
    int err = 0;
    LibplaceboContext *s = avctx->priv;
    const AVVulkanDeviceContext *vkhwctx = NULL;

    if (s->normalize_sar && s->fit_mode != FIT_FILL) {
        av_log(avctx, AV_LOG_WARNING, "normalize_sar has no effect when using "
               "a fit mode other than 'fill'\n");
    }

    /* Create libplacebo log context */
    s->log = pl_log_create(PL_API_VER, pl_log_params(
        .log_level = get_log_level(),
        .log_cb = pl_av_log,
        .log_priv = s,
    ));

    if (!s->log)
        return AVERROR(ENOMEM);

    s->opts = pl_options_alloc(s->log);
    if (!s->opts) {
        libplacebo_uninit(avctx);
        return AVERROR(ENOMEM);
    }

    if (s->shader_cache && s->shader_cache[0]) {
        s->cache = pl_cache_create(pl_cache_params(
            .log  = s->log,
            .get  = pl_cache_get_file,
            .set  = pl_cache_set_file,
            .priv = s->shader_cache,
        ));
        if (!s->cache) {
            libplacebo_uninit(avctx);
            return AVERROR(ENOMEM);
        }
    }

    if (s->out_format_string) {
        s->out_format = av_get_pix_fmt(s->out_format_string);
        if (s->out_format == AV_PIX_FMT_NONE) {
            av_log(avctx, AV_LOG_ERROR, "Invalid output format: %s\n",
                   s->out_format_string);
            libplacebo_uninit(avctx);
            return AVERROR(EINVAL);
        }
    } else {
        s->out_format = AV_PIX_FMT_NONE;
    }

    for (int i = 0; i < s->nb_inputs; i++) {
        AVFilterPad pad = {
            .name         = av_asprintf("input%d", i),
            .type         = AVMEDIA_TYPE_VIDEO,
            .config_props = &libplacebo_config_input,
        };
        if (!pad.name)
            return AVERROR(ENOMEM);
        RET(ff_append_inpad_free_name(avctx, &pad));
    }

    RET(update_settings(avctx));
    RET(av_expr_parse(&s->crop_x_pexpr, s->crop_x_expr, var_names,
                      NULL, NULL, NULL, NULL, 0, s));
    RET(av_expr_parse(&s->crop_y_pexpr, s->crop_y_expr, var_names,
                      NULL, NULL, NULL, NULL, 0, s));
    RET(av_expr_parse(&s->crop_w_pexpr, s->crop_w_expr, var_names,
                      NULL, NULL, NULL, NULL, 0, s));
    RET(av_expr_parse(&s->crop_h_pexpr, s->crop_h_expr, var_names,
                      NULL, NULL, NULL, NULL, 0, s));
    RET(av_expr_parse(&s->pos_x_pexpr, s->pos_x_expr, var_names,
                      NULL, NULL, NULL, NULL, 0, s));
    RET(av_expr_parse(&s->pos_y_pexpr, s->pos_y_expr, var_names,
                      NULL, NULL, NULL, NULL, 0, s));
    RET(av_expr_parse(&s->pos_w_pexpr, s->pos_w_expr, var_names,
                      NULL, NULL, NULL, NULL, 0, s));
    RET(av_expr_parse(&s->pos_h_pexpr, s->pos_h_expr, var_names,
                      NULL, NULL, NULL, NULL, 0, s));

    if (strcmp(s->fps_string, "none") != 0)
        RET(av_parse_video_rate(&s->fps, s->fps_string));

    if (avctx->hw_device_ctx) {
        const AVHWDeviceContext *avhwctx = (void *) avctx->hw_device_ctx->data;
        if (avhwctx->type == AV_HWDEVICE_TYPE_VULKAN)
            vkhwctx = avhwctx->hwctx;
    }

    RET(init_vulkan(avctx, vkhwctx));

    return 0;

fail:
    return err;
}

static void lock_queue(void *priv, uint32_t qf, uint32_t qidx)
{
    AVHWDeviceContext *avhwctx = priv;
    const AVVulkanDeviceContext *hwctx = avhwctx->hwctx;
#if FF_API_VULKAN_SYNC_QUEUES
FF_DISABLE_DEPRECATION_WARNINGS
    hwctx->lock_queue(avhwctx, qf, qidx);
FF_ENABLE_DEPRECATION_WARNINGS
#endif
}

static void unlock_queue(void *priv, uint32_t qf, uint32_t qidx)
{
    AVHWDeviceContext *avhwctx = priv;
    const AVVulkanDeviceContext *hwctx = avhwctx->hwctx;
#if FF_API_VULKAN_SYNC_QUEUES
FF_DISABLE_DEPRECATION_WARNINGS
    hwctx->unlock_queue(avhwctx, qf, qidx);
FF_ENABLE_DEPRECATION_WARNINGS
#endif
}

static int input_init(AVFilterContext *avctx, LibplaceboInput *input, int idx)
{
    LibplaceboContext *s = avctx->priv;

    input->out_pts = av_fifo_alloc2(1, sizeof(int64_t), AV_FIFO_FLAG_AUTO_GROW);
    if (!input->out_pts)
        return AVERROR(ENOMEM);
    input->queue = pl_queue_create(s->gpu);
    input->renderer = pl_renderer_create(s->log, s->gpu);
    input->idx = idx;

    return 0;
}

static void input_uninit(LibplaceboInput *input)
{
    pl_renderer_destroy(&input->renderer);
    pl_queue_destroy(&input->queue);
    av_fifo_freep2(&input->out_pts);
}

static int copy_pl_queue(const AVVulkanDeviceContext *hwctx,
                         const AVVulkanDeviceQueueFamily *qf,
                         struct pl_vulkan_queue *pl_qf)
{
    pl_qf->index = qf->idx;
    pl_qf->count = qf->num;
#if PL_API_VER >= 365
    pl_qf->flags = hwctx->queue_flags;
#else
    if (hwctx->queue_flags != 0)
        return AVERROR(EINVAL); // prevent undefined behavior
#endif
    return 0;
}

static int init_vulkan(AVFilterContext *avctx, const AVVulkanDeviceContext *hwctx)
{
    int err = 0;
    LibplaceboContext *s = avctx->priv;
    uint8_t *buf = NULL;
    size_t buf_len;

    if (hwctx) {
        struct pl_vulkan_import_params 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,
            .lock_queue     = lock_queue,
            .unlock_queue   = unlock_queue,
            .queue_ctx      = avctx->hw_device_ctx->data,
            .queue_graphics = { VK_QUEUE_FAMILY_IGNORED },
            .queue_compute  = { VK_QUEUE_FAMILY_IGNORED },
            .queue_transfer = { VK_QUEUE_FAMILY_IGNORED },
            /* This is the highest version created by hwcontext_vulkan.c */
            .max_api_version = VK_API_VERSION_1_3,
        };
        for (int i = 0; i < hwctx->nb_qf; i++) {
            const AVVulkanDeviceQueueFamily *qf = &hwctx->qf[i];
            if (qf->flags & VK_QUEUE_GRAPHICS_BIT)
                RET(copy_pl_queue(hwctx, qf, &import_params.queue_graphics));
            if (qf->flags & VK_QUEUE_COMPUTE_BIT)
                RET(copy_pl_queue(hwctx, qf, &import_params.queue_compute));
            if (qf->flags & VK_QUEUE_TRANSFER_BIT)
                RET(copy_pl_queue(hwctx, qf, &import_params.queue_transfer));
        }

        /* Import libavfilter vulkan context into libplacebo */
        s->vulkan = pl_vulkan_import(s->log, &import_params);
        s->have_hwdevice = 1;
    } else {
        s->vulkan = pl_vulkan_create(s->log, pl_vulkan_params(
            .queue_count = 0, /* enable all queues for parallelization */
        ));
    }

    if (!s->vulkan) {
        av_log(avctx, AV_LOG_ERROR, "Failed %s Vulkan device!\n",
               hwctx ? "importing" : "creating");
        err = AVERROR_EXTERNAL;
        goto fail;
    }

    s->gpu = s->vulkan->gpu;
    pl_gpu_set_cache(s->gpu, s->cache);

    /* 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));
    }

    if (s->lut_filename)
        RET(parse_custom_lut(avctx));

    /* Initialize inputs */
    s->inputs = av_calloc(s->nb_inputs, sizeof(*s->inputs));
    if (!s->inputs)
        return AVERROR(ENOMEM);
    for (int i = 0; i < s->nb_inputs; i++)
        RET(input_init(avctx, &s->inputs[i], i));
    s->nb_active = s->nb_inputs;
    s->linear_rr = pl_renderer_create(s->log, s->gpu);

    /* fall through */
fail:
    if (buf)
        av_file_unmap(buf, buf_len);
    return err;
}

static void libplacebo_uninit(AVFilterContext *avctx)
{
    LibplaceboContext *s = avctx->priv;

    for (int i = 0; i < FF_ARRAY_ELEMS(s->tex); i++)
        pl_tex_destroy(s->gpu, &s->tex[i]);
    for (int i = 0; i < s->num_hooks; i++)
        pl_mpv_user_shader_destroy(&s->hooks[i]);
    if (s->inputs) {
        for (int i = 0; i < s->nb_inputs; i++)
            input_uninit(&s->inputs[i]);
        av_freep(&s->inputs);
    }

    pl_lut_free(&s->lut);
    pl_cache_destroy(&s->cache);
    pl_renderer_destroy(&s->linear_rr);
    pl_tex_destroy(s->gpu, &s->linear_tex);
    pl_options_free(&s->opts);
    pl_vulkan_destroy(&s->vulkan);
    pl_log_destroy(&s->log);
    ff_vk_uninit(&s->vkctx);
    s->gpu = NULL;

    av_expr_free(s->crop_x_pexpr);
    av_expr_free(s->crop_y_pexpr);
    av_expr_free(s->crop_w_pexpr);
    av_expr_free(s->crop_h_pexpr);
    av_expr_free(s->pos_x_pexpr);
    av_expr_free(s->pos_y_pexpr);
    av_expr_free(s->pos_w_pexpr);
    av_expr_free(s->pos_h_pexpr);
}

static int libplacebo_process_command(AVFilterContext *ctx, const char *cmd,
                                      const char *arg, char *res, int res_len,
                                      int flags)
{
    int err = 0;
    RET(ff_filter_process_command(ctx, cmd, arg, res, res_len, flags));
    RET(update_settings(ctx));
    return 0;

fail:
    return err;
}

static const AVFrame *ref_frame(const struct pl_frame_mix *mix)
{
    for (int i = 0; i < mix->num_frames; i++) {
        if (i+1 == mix->num_frames || mix->timestamps[i+1] > 0)
            return pl_get_mapped_avframe(mix->frames[i]);
    }
    return NULL;
}

static inline double q2d_fallback(AVRational q, const double def)
{
    return (q.num && q.den) ? av_q2d(q) : def;
}

static void update_crops(AVFilterContext *ctx, LibplaceboInput *in,
                         struct pl_frame *target, double target_pts)
{
    FilterLink     *outl = ff_filter_link(ctx->outputs[0]);
    LibplaceboContext *s = ctx->priv;
    const AVFilterLink *outlink = ctx->outputs[0];
    const AVFilterLink *inlink = ctx->inputs[in->idx];
    const AVFrame *ref = ref_frame(&in->mix);

    for (int i = 0; i < in->mix.num_frames; i++) {
        // Mutate the `pl_frame.crop` fields in-place. This is fine because we
        // own the entire pl_queue, and hence, the pointed-at frames.
        struct pl_frame *image = (struct pl_frame *) in->mix.frames[i];
        const AVFrame *src = pl_get_mapped_avframe(image);
        double image_pts = TS2T(src->pts, inlink->time_base);

        /* Update dynamic variables */
        s->var_values[VAR_IN_IDX] = s->var_values[VAR_IDX] = in->idx;
        s->var_values[VAR_IN_W]   = s->var_values[VAR_IW] = inlink->w;
        s->var_values[VAR_IN_H]   = s->var_values[VAR_IH] = inlink->h;
        s->var_values[VAR_A]      = (double) inlink->w / inlink->h;
        s->var_values[VAR_SAR]    = q2d_fallback(inlink->sample_aspect_ratio, 1.0);
        s->var_values[VAR_IN_T]   = s->var_values[VAR_T]  = image_pts;
        s->var_values[VAR_OUT_T]  = s->var_values[VAR_OT] = target_pts;
        s->var_values[VAR_N]      = outl->frame_count_out;

        /* Clear these explicitly to avoid leaking previous frames' state */
        s->var_values[VAR_CROP_W] = s->var_values[VAR_CW] = NAN;
        s->var_values[VAR_CROP_H] = s->var_values[VAR_CH] = NAN;
        s->var_values[VAR_POS_W]  = s->var_values[VAR_PW] = NAN;
        s->var_values[VAR_POS_H]  = s->var_values[VAR_PH] = NAN;

        /* Compute dimensions first and placement second */
        s->var_values[VAR_CROP_W] = s->var_values[VAR_CW] =
            av_expr_eval(s->crop_w_pexpr, s->var_values, NULL);
        s->var_values[VAR_CROP_H] = s->var_values[VAR_CH] =
            av_expr_eval(s->crop_h_pexpr, s->var_values, NULL);
        s->var_values[VAR_CROP_W] = s->var_values[VAR_CW] =
            av_expr_eval(s->crop_w_pexpr, s->var_values, NULL);
        s->var_values[VAR_POS_W]  = s->var_values[VAR_PW] =
            av_expr_eval(s->pos_w_pexpr, s->var_values, NULL);
        s->var_values[VAR_POS_H]  = s->var_values[VAR_PH] =
            av_expr_eval(s->pos_h_pexpr, s->var_values, NULL);
        s->var_values[VAR_POS_W]  = s->var_values[VAR_PW] =
            av_expr_eval(s->pos_w_pexpr, s->var_values, NULL);

        image->crop.x0 = av_expr_eval(s->crop_x_pexpr, s->var_values, NULL);
        image->crop.y0 = av_expr_eval(s->crop_y_pexpr, s->var_values, NULL);
        image->crop.x1 = image->crop.x0 + s->var_values[VAR_CROP_W];
        image->crop.y1 = image->crop.y0 + s->var_values[VAR_CROP_H];

        const pl_rect2df crop_orig = image->crop;
        pl_rotation rot_total = PL_ROTATION_360 + image->rotation - target->rotation;
        if (rot_total % PL_ROTATION_180 == PL_ROTATION_90) {
            /* Libplacebo expects the input crop relative to the actual frame
             * dimensions, so un-transpose them here */
            FFSWAP(float, image->crop.x0, image->crop.y0);
            FFSWAP(float, image->crop.x1, image->crop.y1);
        }

        if (src == ref) {
            /* Only update the target crop once, for the 'reference' frame */
            target->crop.x0 = av_expr_eval(s->pos_x_pexpr, s->var_values, NULL);
            target->crop.y0 = av_expr_eval(s->pos_y_pexpr, s->var_values, NULL);
            target->crop.x1 = target->crop.x0 + s->var_values[VAR_POS_W];
            target->crop.y1 = target->crop.y0 + s->var_values[VAR_POS_H];

            /* Effective visual crop */
            double sar_in = q2d_fallback(inlink->sample_aspect_ratio, 1.0);
            double sar_out = q2d_fallback(outlink->sample_aspect_ratio, 1.0);
            if (rot_total % PL_ROTATION_180 == PL_ROTATION_90)
                sar_in = 1.0 / sar_in;

            pl_rect2df fixed = crop_orig;
            pl_rect2df_stretch(&fixed, sar_in / sar_out, 1.0);

            switch (s->fit_mode) {
            case FIT_FILL:
                if (s->normalize_sar)
                    pl_rect2df_aspect_copy(&target->crop, &fixed, s->pad_crop_ratio);
                break;
            case FIT_CONTAIN:
                pl_rect2df_aspect_copy(&target->crop, &fixed, 0.0);
                break;
            case FIT_COVER:
                pl_rect2df_aspect_copy(&target->crop, &fixed, 1.0);
                break;
            case FIT_NONE: {
                const float sx = fabsf(pl_rect_w(fixed)) / pl_rect_w(target->crop);
                const float sy = fabsf(pl_rect_h(fixed)) / pl_rect_h(target->crop);
                pl_rect2df_stretch(&target->crop, sx, sy);
                break;
            }
            case FIT_SCALE_DOWN:
                pl_rect2df_aspect_fit(&target->crop, &fixed, 0.0);
            }
        }
    }
}

/* Construct and emit an output frame for a given timestamp */
static int output_frame(AVFilterContext *ctx, int64_t pts)
{
    int err = 0, ok, changed = 0;
    LibplaceboContext *s = ctx->priv;
    pl_options opts = s->opts;
    AVFilterLink *outlink = ctx->outputs[0];
    const AVPixFmtDescriptor *outdesc = av_pix_fmt_desc_get(outlink->format);
    const double target_pts = TS2T(pts, outlink->time_base);
    struct pl_frame target;
    const AVFrame *ref = NULL;
    AVFrame *out;

    /* Count the number of visible inputs, by excluding frames which are fully
     * obscured or which have no frames in the mix */
    int idx_start = 0, nb_visible = 0;
    for (int i = 0; i < s->nb_inputs; i++) {
        LibplaceboInput *in = &s->inputs[i];
        struct pl_frame dummy;
        if (in->qstatus != PL_QUEUE_OK || !in->mix.num_frames)
            continue;
        const struct pl_frame *cur = pl_frame_mix_nearest(&in->mix);
        av_assert1(cur);
        update_crops(ctx, in, &dummy, target_pts);
        const int x0 = roundf(FFMIN(dummy.crop.x0, dummy.crop.x1)),
                  y0 = roundf(FFMIN(dummy.crop.y0, dummy.crop.y1)),
                  x1 = roundf(FFMAX(dummy.crop.x0, dummy.crop.x1)),
                  y1 = roundf(FFMAX(dummy.crop.y0, dummy.crop.y1));

        /* If an opaque frame covers entire the output, disregard all lower layers */
        const bool cropped = x0 > 0 || y0 > 0 || x1 < outlink->w || y1 < outlink->h;
        if (!cropped && cur->repr.alpha == PL_ALPHA_NONE) {
            idx_start = i;
            nb_visible = 0;
            ref = NULL;
        }
        /* Use first visible input as overall reference */
        if (!ref)
            ref = ref_frame(&in->mix);
        nb_visible++;
    }

    out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
    if (!out)
        return AVERROR(ENOMEM);

    if (!ref)
        goto props_done;

    RET(av_frame_copy_props(out, ref));
    out->width = outlink->w;
    out->height = outlink->h;
    out->colorspace = outlink->colorspace;
    out->color_range = outlink->color_range;
    out->alpha_mode = outlink->alpha_mode;
    if (s->deinterlace)
        out->flags &= ~(AV_FRAME_FLAG_INTERLACED | AV_FRAME_FLAG_TOP_FIELD_FIRST);

    if (s->apply_dovi && av_frame_get_side_data(ref, AV_FRAME_DATA_DOVI_METADATA)) {
        /* Output of dovi reshaping is always BT.2020+PQ, so infer the correct
         * output colorspace defaults */
        out->color_primaries = AVCOL_PRI_BT2020;
        out->color_trc = AVCOL_TRC_SMPTE2084;
        changed |= AV_SIDE_DATA_PROP_COLOR_DEPENDENT;
    }

    if (s->color_trc >= 0)
        out->color_trc = s->color_trc;
    if (s->color_primaries >= 0)
        out->color_primaries = s->color_primaries;
    if (s->chroma_location >= 0)
        out->chroma_location = s->chroma_location;

    /* Strip side data if no longer relevant */
    if (out->width != ref->width || out->height != ref->height)
        changed |= AV_SIDE_DATA_PROP_SIZE_DEPENDENT;
    if (ref->color_trc != out->color_trc || ref->color_primaries != out->color_primaries)
        changed |= AV_SIDE_DATA_PROP_COLOR_DEPENDENT;
    av_frame_side_data_remove_by_props(&out->side_data, &out->nb_side_data, changed);

    if (s->apply_filmgrain)
        av_frame_remove_side_data(out, AV_FRAME_DATA_FILM_GRAIN_PARAMS);

    if (s->reset_sar) {
        out->sample_aspect_ratio = ref->sample_aspect_ratio;
    } else {
        const AVRational ar_ref  = { ref->width, ref->height };
        const AVRational ar_out  = { out->width, out->height };
        const AVRational stretch = av_div_q(ar_ref, ar_out);
        out->sample_aspect_ratio = av_mul_q(ref->sample_aspect_ratio, stretch);
    }

props_done:
    out->pts = pts;
    if (s->fps.num)
        out->duration = 1;

    /* Map, render and unmap output frame */
    if (outdesc->flags & AV_PIX_FMT_FLAG_HWACCEL) {
        ok = pl_map_avframe_ex(s->gpu, &target, pl_avframe_params(
            .frame    = out,
            .map_dovi = false,
        ));
    } else {
        ok = pl_frame_recreate_from_avframe(s->gpu, &target, s->tex, out);
    }
    if (!ok) {
        err = AVERROR_EXTERNAL;
        goto fail;
    }

    struct pl_frame orig_target = target;
    bool use_linear_compositor = false;
    if (s->linear_tex && target.color.transfer != PL_COLOR_TRC_LINEAR &&
        !s->disable_linear && nb_visible > 1) {
        target = (struct pl_frame) {
            .num_planes = 1,
            .planes[0] = {
                .components = 4,
                .component_mapping = {0, 1, 2, 3},
                .texture = s->linear_tex,
            },
            .repr = pl_color_repr_rgb,
            .color = orig_target.color,
            .rotation = orig_target.rotation,
        };
        target.repr.alpha     = PL_ALPHA_PREMULTIPLIED;
        target.color.transfer = PL_COLOR_TRC_LINEAR;
        use_linear_compositor = true;
    }

    /* Draw first frame opaque, others with blending */
    struct pl_render_params tmp_params = opts->params;
    for (int i = 0; i < s->nb_inputs; i++) {
        LibplaceboInput *in = &s->inputs[i];
        if (!in->renderer)
            continue; /* input was already freed */
        FilterLink *il = ff_filter_link(ctx->inputs[i]);
        FilterLink *ol = ff_filter_link(outlink);
        int high_fps = av_cmp_q(il->frame_rate, ol->frame_rate) >= 0;
        if (in->qstatus != PL_QUEUE_OK || !in->mix.num_frames || i < idx_start) {
            pl_renderer_flush_cache(in->renderer);
            continue;
        }
        tmp_params.skip_caching_single_frame = high_fps;
        update_crops(ctx, in, &target, target_pts);
        pl_render_image_mix(in->renderer, &in->mix, &target, &tmp_params);

        /* Force straight output and set correct blend operator. This is
         * required to get correct blending onto YUV target buffers. */
        target.repr.alpha = PL_ALPHA_INDEPENDENT;
        tmp_params.blend_params = &pl_alpha_overlay;
        tmp_params.background = tmp_params.border = PL_CLEAR_SKIP;
    }

    if (use_linear_compositor) {
        /* Blit the linear intermediate image to the output frame */
        target.crop = orig_target.crop = (struct pl_rect2df) {0};
        target.repr.alpha = PL_ALPHA_PREMULTIPLIED;
        pl_render_image(s->linear_rr, &target, &orig_target, &opts->params);
        target = orig_target;
    } else if (!ref) {
        /* Render an empty image to clear the frame to the desired fill color */
        pl_render_image(s->linear_rr, NULL, &target, &opts->params);
    }

    if (outdesc->flags & AV_PIX_FMT_FLAG_HWACCEL) {
        pl_unmap_avframe(s->gpu, &target);
    } else if (!pl_download_avframe(s->gpu, &target, out)) {
        err = AVERROR_EXTERNAL;
        goto fail;
    }
    return ff_filter_frame(outlink, out);

fail:
    av_frame_free(&out);
    return err;
}

static bool map_frame(pl_gpu gpu, pl_tex *tex,
                      const struct pl_source_frame *src,
                      struct pl_frame *out)
{
    AVFrame *avframe = src->frame_data;
    LibplaceboContext *s = avframe->opaque;
    bool ok = pl_map_avframe_ex(gpu, out, pl_avframe_params(
        .frame      = avframe,
        .tex        = tex,
        .map_dovi   = s->apply_dovi,
    ));
    out->lut = s->lut;
    out->lut_type = s->lut_type;
    out->rotation += s->rotation;

    if (!s->apply_filmgrain)
        out->film_grain.type = PL_FILM_GRAIN_NONE;

    av_frame_free(&avframe);
    return ok;
}

static void unmap_frame(pl_gpu gpu, struct pl_frame *frame,
                        const struct pl_source_frame *src)
{
    pl_unmap_avframe(gpu, frame);
}

static void discard_frame(const struct pl_source_frame *src)
{
    AVFrame *avframe = src->frame_data;
    av_frame_free(&avframe);
}

static int handle_input(AVFilterContext *ctx, LibplaceboInput *input)
{
    int ret, status;
    LibplaceboContext *s = ctx->priv;
    AVFilterLink *outlink = ctx->outputs[0];
    AVFilterLink *inlink = ctx->inputs[input->idx];
    AVFrame *in;
    int64_t pts;

    while ((ret = ff_inlink_consume_frame(inlink, &in)) > 0) {
        struct pl_source_frame src = {
            .pts         = TS2T(in->pts, inlink->time_base),
            .duration    = TS2T(in->duration, inlink->time_base),
            .first_field = s->deinterlace ? pl_field_from_avframe(in) : PL_FIELD_NONE,
            .frame_data  = in,
            .map         = map_frame,
            .unmap       = unmap_frame,
            .discard     = discard_frame,
        };

        in->opaque = s;
        pl_queue_push(input->queue, &src);

        if (!s->fps.num) {
            /* Internally queue an output frame for the same PTS */
            pts = av_rescale_q(in->pts, inlink->time_base, outlink->time_base);
            av_fifo_write(input->out_pts, &pts, 1);

            if (s->send_fields && src.first_field != PL_FIELD_NONE) {
                /* Queue the second field for interlaced content */
                pts += av_rescale_q(in->duration, inlink->time_base, outlink->time_base) / 2;
                av_fifo_write(input->out_pts, &pts, 1);
            }
        }
    }

    if (ret < 0)
        return ret;

    if (!input->status && ff_inlink_acknowledge_status(inlink, &status, &pts)) {
        pts = av_rescale_q_rnd(pts, inlink->time_base, outlink->time_base,
                               AV_ROUND_UP);
        pl_queue_push(input->queue, NULL); /* Signal EOF to pl_queue */
        input->status = status;
        input->status_pts = pts;
        s->nb_active--;
    }

    return 0;
}

static void drain_input_pts(LibplaceboInput *in, int64_t until)
{
    int64_t pts;
    while (av_fifo_peek(in->out_pts, &pts, 1, 0) >= 0 && pts <= until)
        av_fifo_drain2(in->out_pts, 1);
}

static int libplacebo_activate(AVFilterContext *ctx)
{
    int ret, ok = 0, retry = 0;
    LibplaceboContext *s = ctx->priv;
    AVFilterLink *outlink = ctx->outputs[0];
    FilterLink *outl      = ff_filter_link(outlink);
    int64_t pts, out_pts;

    FF_FILTER_FORWARD_STATUS_BACK_ALL(outlink, ctx);
    pl_log_level_update(s->log, get_log_level());

    for (int i = 0; i < s->nb_inputs; i++) {
        if ((ret = handle_input(ctx, &s->inputs[i])) < 0)
            return ret;
    }

    if (ff_outlink_frame_wanted(outlink)) {
        if (s->fps.num) {
            out_pts = outl->frame_count_out;
        } else {
            /* Determine the PTS of the next frame from any active input */
            out_pts = INT64_MAX;
            for (int i = 0; i < s->nb_inputs; i++) {
                LibplaceboInput *in = &s->inputs[i];
                if (av_fifo_peek(in->out_pts, &pts, 1, 0) >= 0) {
                    out_pts = FFMIN(out_pts, pts);
                } else if (!in->status) {
                    ff_inlink_request_frame(ctx->inputs[i]);
                    retry = true;
                }
            }

            if (retry) /* some inputs are incomplete */
                return 0;
        }

        /* Update all input queues to the chosen out_pts */
        for (int i = 0; i < s->nb_inputs; i++) {
            LibplaceboInput *in = &s->inputs[i];
            FilterLink *l = ff_filter_link(outlink);
            if (in->status && out_pts >= in->status_pts) {
                /* Free up resources which will never be needed again */
                pl_renderer_destroy(&in->renderer);
                pl_queue_destroy(&in->queue);
                in->qstatus = PL_QUEUE_EOF;
                continue;
            }

            in->qstatus = pl_queue_update(in->queue, &in->mix, pl_queue_params(
                .pts            = TS2T(out_pts, outlink->time_base),
                .radius         = pl_frame_mix_radius(&s->opts->params),
                .vsync_duration = q2d_fallback(av_inv_q(l->frame_rate), 0.0),
            ));

            switch (in->qstatus) {
            case PL_QUEUE_MORE:
                ff_inlink_request_frame(ctx->inputs[i]);
                retry = true;
                break;
            case PL_QUEUE_OK:
                ok |= in->mix.num_frames > 0;
                break;
            case PL_QUEUE_ERR:
                return AVERROR_EXTERNAL;
            }
        }

        /* In constant FPS mode, we can also output an empty frame if there is
         * a gap in the input timeline and we still have active streams */
        ok |= s->fps.num && s->nb_active > 0;

        if (retry) {
            return 0;
        } else if (ok) {
            /* Got any valid frame mixes, drain PTS queue and render output */
            for (int i = 0; i < s->nb_inputs; i++)
                drain_input_pts(&s->inputs[i], out_pts);
            return output_frame(ctx, out_pts);
        } else if (s->nb_active == 0) {
            /* Forward most recent status */
            int status = s->inputs[0].status;
            int64_t status_pts = s->inputs[0].status_pts;
            for (int i = 1; i < s->nb_inputs; i++) {
                const LibplaceboInput *in = &s->inputs[i];
                if (in->status_pts > status_pts) {
                    status = s->inputs[i].status;
                    status_pts = s->inputs[i].status_pts;
                }
            }
            ff_outlink_set_status(outlink, status, status_pts);
            return 0;
        }

        return AVERROR_BUG;
    }

    return FFERROR_NOT_READY;
}

static int libplacebo_query_format(const AVFilterContext *ctx,
                                   AVFilterFormatsConfig **cfg_in,
                                   AVFilterFormatsConfig **cfg_out)
{
    int err;
    const LibplaceboContext *s = ctx->priv;
    const AVPixFmtDescriptor *desc = NULL;
    AVFilterFormats *infmts = NULL, *outfmts = NULL;

    /* List AV_PIX_FMT_VULKAN first to prefer it when possible */
    if (s->have_hwdevice) {
        RET(ff_add_format(&infmts, AV_PIX_FMT_VULKAN));
        if (s->out_format == AV_PIX_FMT_NONE || av_vkfmt_from_pixfmt(s->out_format))
            RET(ff_add_format(&outfmts, AV_PIX_FMT_VULKAN));
    }

    while ((desc = av_pix_fmt_desc_next(desc))) {
        enum AVPixelFormat pixfmt = av_pix_fmt_desc_get_id(desc);
        if (pixfmt == AV_PIX_FMT_VULKAN)
            continue; /* Handled above */

        if (!pl_test_pixfmt(s->gpu, pixfmt))
            continue;

        RET(ff_add_format(&infmts, pixfmt));

        /* Filter for supported output pixel formats */
        if (desc->flags & AV_PIX_FMT_FLAG_BE)
            continue; /* BE formats are not supported by pl_download_avframe */

        /* Mask based on user specified format */
        if (pixfmt != s->out_format && s->out_format != AV_PIX_FMT_NONE)
            continue;

        if (!pl_test_pixfmt_caps(s->gpu, pixfmt, PL_FMT_CAP_RENDERABLE))
            continue;

        RET(ff_add_format(&outfmts, pixfmt));
    }

    if (!infmts || !outfmts) {
        err = AVERROR(EINVAL);
        goto fail;
    }

    for (int i = 0; i < s->nb_inputs; i++) {
        if (i > 0) {
            /* Duplicate the format list for each subsequent input */
            infmts = NULL;
            for (int n = 0; n < cfg_in[0]->formats->nb_formats; n++)
                RET(ff_add_format(&infmts, cfg_in[0]->formats->formats[n]));
        }
        RET(ff_formats_ref(infmts, &cfg_in[i]->formats));
        RET(ff_formats_ref(ff_all_color_spaces(), &cfg_in[i]->color_spaces));
        RET(ff_formats_ref(ff_all_color_ranges(), &cfg_in[i]->color_ranges));
        RET(ff_formats_ref(ff_all_alpha_modes(), &cfg_in[i]->alpha_modes));
    }

    RET(ff_formats_ref(outfmts, &cfg_out[0]->formats));

    outfmts = s->colorspace > 0 ? ff_make_formats_list_singleton(s->colorspace)
                                : ff_all_color_spaces();
    RET(ff_formats_ref(outfmts, &cfg_out[0]->color_spaces));

    outfmts = s->color_range > 0 ? ff_make_formats_list_singleton(s->color_range)
                                 : ff_all_color_ranges();
    RET(ff_formats_ref(outfmts, &cfg_out[0]->color_ranges));

    outfmts = s->alpha_mode > 0 ? ff_make_formats_list_singleton(s->alpha_mode)
                                 : ff_all_alpha_modes();
    RET(ff_formats_ref(outfmts, &cfg_out[0]->alpha_modes));
    return 0;

fail:
    if (infmts && !infmts->refcount)
        ff_formats_unref(&infmts);
    if (outfmts && !outfmts->refcount)
        ff_formats_unref(&outfmts);
    return err;
}

static int libplacebo_config_input(AVFilterLink *inlink)
{
    AVFilterContext *avctx = inlink->dst;
    LibplaceboContext *s   = avctx->priv;

    if (s->rotation % PL_ROTATION_180 == PL_ROTATION_90) {
        /* Swap width and height for 90 degree rotations to make the size and
         * scaling calculations work out correctly */
        FFSWAP(int, inlink->w, inlink->h);
        if (inlink->sample_aspect_ratio.num)
            inlink->sample_aspect_ratio = av_inv_q(inlink->sample_aspect_ratio);
    }

    if (inlink->format == AV_PIX_FMT_VULKAN)
        return ff_vk_filter_config_input(inlink);

    /* Forward this to the vkctx for format selection */
    s->vkctx.input_format = inlink->format;

    return 0;
}

static inline AVRational max_q(AVRational a, AVRational b)
{
    return av_cmp_q(a, b) < 0 ? b : a;
}

static int libplacebo_config_output(AVFilterLink *outlink)
{
    int err;
    FilterLink          *l = ff_filter_link(outlink);
    AVFilterContext *avctx = outlink->src;
    LibplaceboContext *s   = avctx->priv;
    AVFilterLink *inlink   = outlink->src->inputs[0];
    FilterLink       *ol   = ff_filter_link(outlink);
    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
    const AVPixFmtDescriptor *out_desc = av_pix_fmt_desc_get(outlink->format);
    AVHWFramesContext *hwfc;
    AVVulkanFramesContext *vkfc;

    /* Frame dimensions */
    RET(ff_scale_eval_dimensions(s, s->w_expr, s->h_expr, inlink, outlink,
                                 &outlink->w, &outlink->h));

    s->reset_sar |= s->normalize_sar || s->nb_inputs > 1;
    double sar_in = q2d_fallback(inlink->sample_aspect_ratio, 1.0);

    int force_oar = s->force_original_aspect_ratio;
    if (!force_oar && s->fit_sense == FIT_CONSTRAINT) {
        if (s->fit_mode == FIT_CONTAIN || s->fit_mode == FIT_SCALE_DOWN) {
            force_oar = SCALE_FORCE_OAR_DECREASE;
        } else if (s->fit_mode == FIT_COVER) {
            force_oar = SCALE_FORCE_OAR_INCREASE;
        }
    }

    RET(ff_scale_adjust_dimensions(inlink, &outlink->w, &outlink->h,
                                   force_oar, s->force_divisible_by,
                                   s->reset_sar ? sar_in : 1.0));

    if (s->fit_mode == FIT_SCALE_DOWN && s->fit_sense == FIT_CONSTRAINT) {
        int w_adj = s->reset_sar ? sar_in * inlink->w : inlink->w;
        outlink->w = FFMIN(outlink->w, w_adj);
        outlink->h = FFMIN(outlink->h, inlink->h);
    }

    if (s->nb_inputs > 1 && !s->disable_fbos) {
        /* Create a separate renderer and composition texture */
        const enum pl_fmt_caps caps = PL_FMT_CAP_BLENDABLE | PL_FMT_CAP_BLITTABLE;
        pl_fmt fmt = pl_find_fmt(s->gpu, PL_FMT_FLOAT, 4, 16, 0, caps);
        bool ok = !!fmt;
        if (ok) {
            ok = pl_tex_recreate(s->gpu, &s->linear_tex, pl_tex_params(
                .format     = fmt,
                .w          = outlink->w,
                .h          = outlink->h,
                .blit_dst   = true,
                .renderable = true,
                .sampleable = true,
                .storable   = fmt->caps & PL_FMT_CAP_STORABLE,
            ));
        }

        if (!ok) {
            av_log(avctx, AV_LOG_WARNING, "Failed to create a linear texture "
                   "for compositing multiple inputs, falling back to non-linear "
                   "blending.\n");
        }
    }

    if (s->reset_sar) {
        /* SAR is normalized, or we have multiple inputs, set out to 1:1 */
        outlink->sample_aspect_ratio = (AVRational){ 1, 1 };
    } else if (inlink->sample_aspect_ratio.num && s->fit_mode == FIT_FILL) {
        /* 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 */
        const AVRational ar_in   = { inlink->w,  inlink->h };
        const AVRational ar_out  = { outlink->w, outlink->h };
        const AVRational stretch = av_div_q(ar_in, ar_out);
        outlink->sample_aspect_ratio = av_mul_q(inlink->sample_aspect_ratio, stretch);
    } else {
        outlink->sample_aspect_ratio = inlink->sample_aspect_ratio;
    }

    /* Frame rate */
    if (s->fps.num) {
        ol->frame_rate = s->fps;
        outlink->time_base = av_inv_q(s->fps);
    } else {
        FilterLink *il = ff_filter_link(avctx->inputs[0]);
        ol->frame_rate = il->frame_rate;
        outlink->time_base = avctx->inputs[0]->time_base;
        for (int i = 1; i < s->nb_inputs; i++) {
            il = ff_filter_link(avctx->inputs[i]);
            ol->frame_rate = max_q(ol->frame_rate, il->frame_rate);
            outlink->time_base = av_gcd_q(outlink->time_base,
                                          avctx->inputs[i]->time_base,
                                          AV_TIME_BASE / 2, AV_TIME_BASE_Q);
        }

        if (s->deinterlace && s->send_fields) {
            const AVRational q2 = { 2, 1 };
            ol->frame_rate = av_mul_q(ol->frame_rate, q2);
            /* Ensure output frame timestamps are divisible by two */
            outlink->time_base = av_div_q(outlink->time_base, q2);
        }
    }

    /* Static variables */
    s->var_values[VAR_OUT_W]    = s->var_values[VAR_OW] = outlink->w;
    s->var_values[VAR_OUT_H]    = s->var_values[VAR_OH] = outlink->h;
    s->var_values[VAR_DAR]      = q2d_fallback(outlink->sample_aspect_ratio, 1.0);
    s->var_values[VAR_HSUB]     = 1 << desc->log2_chroma_w;
    s->var_values[VAR_VSUB]     = 1 << desc->log2_chroma_h;
    s->var_values[VAR_OHSUB]    = 1 << out_desc->log2_chroma_w;
    s->var_values[VAR_OVSUB]    = 1 << out_desc->log2_chroma_h;

    if (outlink->format != AV_PIX_FMT_VULKAN)
        return 0;

    s->vkctx.output_width = outlink->w;
    s->vkctx.output_height = outlink->h;
    /* Default to reusing the input format */
    if (s->out_format == AV_PIX_FMT_NONE || s->out_format == AV_PIX_FMT_VULKAN) {
        s->vkctx.output_format = s->vkctx.input_format;
    } else {
        s->vkctx.output_format = s->out_format;
    }
    RET(ff_vk_filter_config_output(outlink));
    hwfc = (AVHWFramesContext *)l->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[] = {
    { "inputs", "Number of inputs", OFFSET(nb_inputs), AV_OPT_TYPE_INT, {.i64 = 1}, 1, INT_MAX, .flags = STATIC },
    { "w", "Output video frame width",  OFFSET(w_expr), AV_OPT_TYPE_STRING, {.str = "iw"}, .flags = STATIC },
    { "h", "Output video frame height", OFFSET(h_expr), AV_OPT_TYPE_STRING, {.str = "ih"}, .flags = STATIC },
    { "fps", "Output video frame rate", OFFSET(fps_string), AV_OPT_TYPE_STRING, {.str = "none"}, .flags = STATIC },
    { "crop_x", "Input video crop x", OFFSET(crop_x_expr), AV_OPT_TYPE_STRING, {.str = "(iw-cw)/2"}, .flags = DYNAMIC },
    { "crop_y", "Input video crop y", OFFSET(crop_y_expr), AV_OPT_TYPE_STRING, {.str = "(ih-ch)/2"}, .flags = DYNAMIC },
    { "crop_w", "Input video crop w", OFFSET(crop_w_expr), AV_OPT_TYPE_STRING, {.str = "iw"}, .flags = DYNAMIC },
    { "crop_h", "Input video crop h", OFFSET(crop_h_expr), AV_OPT_TYPE_STRING, {.str = "ih"}, .flags = DYNAMIC },
    { "pos_x", "Output video placement x", OFFSET(pos_x_expr), AV_OPT_TYPE_STRING, {.str = "(ow-pw)/2"}, .flags = DYNAMIC },
    { "pos_y", "Output video placement y", OFFSET(pos_y_expr), AV_OPT_TYPE_STRING, {.str = "(oh-ph)/2"}, .flags = DYNAMIC },
    { "pos_w", "Output video placement w", OFFSET(pos_w_expr), AV_OPT_TYPE_STRING, {.str = "ow"}, .flags = DYNAMIC },
    { "pos_h", "Output video placement h", OFFSET(pos_h_expr), AV_OPT_TYPE_STRING, {.str = "oh"}, .flags = DYNAMIC },
    { "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, SCALE_FORCE_OAR_NB-1, STATIC, .unit = "force_oar" },
        { "disable",  NULL, 0, AV_OPT_TYPE_CONST, {.i64 = SCALE_FORCE_OAR_DISABLE  }, 0, 0, STATIC, .unit = "force_oar" },
        { "decrease", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = SCALE_FORCE_OAR_DECREASE }, 0, 0, STATIC, .unit = "force_oar" },
        { "increase", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = SCALE_FORCE_OAR_INCREASE }, 0, 0, STATIC, .unit = "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 },
    { "reset_sar", "force SAR normalization to 1:1 by adjusting pos_x/y/w/h", OFFSET(reset_sar), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, STATIC },
    { "normalize_sar", "like reset_sar, but pad/crop instead of stretching the video", OFFSET(normalize_sar), AV_OPT_TYPE_BOOL, {.i64 = 0}, 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 },
    { "fit_mode", "Content fit strategy for placing input layers in the output", OFFSET(fit_mode), AV_OPT_TYPE_INT, {.i64 = FIT_FILL }, 0, FIT_MODE_NB - 1, STATIC, .unit = "fit_mode" },
        { "fill",       "Stretch content, ignoring aspect ratio",               0, AV_OPT_TYPE_CONST, {.i64 = FIT_FILL },       0, 0, STATIC, .unit = "fit_mode" },
        { "contain",    "Stretch content, padding to preserve aspect",          0, AV_OPT_TYPE_CONST, {.i64 = FIT_CONTAIN },    0, 0, STATIC, .unit = "fit_mode" },
        { "cover",      "Stretch content, cropping to preserve aspect",         0, AV_OPT_TYPE_CONST, {.i64 = FIT_COVER },      0, 0, STATIC, .unit = "fit_mode" },
        { "none",       "Keep input unscaled, padding and cropping as needed",  0, AV_OPT_TYPE_CONST, {.i64 = FIT_NONE },       0, 0, STATIC, .unit = "fit_mode" },
        { "place",      "Keep input unscaled, padding and cropping as needed",  0, AV_OPT_TYPE_CONST, {.i64 = FIT_NONE },       0, 0, STATIC, .unit = "fit_mode" },
        { "scale_down", "Downscale only if larger, padding to preserve aspect", 0, AV_OPT_TYPE_CONST, {.i64 = FIT_SCALE_DOWN }, 0, 0, STATIC, .unit = "fit_mode" },
    { "fit_sense", "Output size strategy (for the base layer only)", OFFSET(fit_sense), AV_OPT_TYPE_INT, {.i64 = FIT_TARGET }, 0, FIT_SENSE_NB - 1, STATIC, .unit = "fit_sense" },
        { "target",     "Computed resolution is the exact output size",         0, AV_OPT_TYPE_CONST, {.i64 = FIT_TARGET     }, 0, 0, STATIC, .unit = "fit_sense" },
        { "constraint", "Computed resolution constrains the output size",       0, AV_OPT_TYPE_CONST, {.i64 = FIT_CONSTRAINT }, 0, 0, STATIC, .unit = "fit_sense" },
    { "fillcolor", "Background fill color", OFFSET(fillcolor), AV_OPT_TYPE_COLOR, {.str = "black@0"}, .flags = DYNAMIC },
    { "corner_rounding", "Corner rounding radius", OFFSET(corner_rounding), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 1.0, .flags = DYNAMIC },
    { "lut", "Path to custom LUT file to apply", OFFSET(lut_filename), AV_OPT_TYPE_STRING, { .str = NULL }, .flags = STATIC },
    { "lut_type", "Application mode of the custom LUT", OFFSET(lut_type), AV_OPT_TYPE_INT, { .i64 = PL_LUT_UNKNOWN }, 0, PL_LUT_CONVERSION, STATIC, .unit = "lut_type" },
        { "auto",  NULL, 0, AV_OPT_TYPE_CONST, {.i64 = PL_LUT_UNKNOWN }, 0, 0, STATIC, .unit = "lut_type" },
        { "native", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = PL_LUT_NATIVE }, 0, 0, STATIC, .unit = "lut_type" },
        { "normalized", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = PL_LUT_NORMALIZED }, 0, 0, STATIC, .unit = "lut_type" },
        { "conversion", NULL, 0, AV_OPT_TYPE_CONST, {.i64 = PL_LUT_CONVERSION }, 0, 0, STATIC, .unit = "lut_type" },

    { "extra_opts", "Pass extra libplacebo-specific options using a :-separated list of key=value pairs", OFFSET(extra_opts), AV_OPT_TYPE_DICT, .flags = DYNAMIC },
    { "shader_cache",  "Set shader cache path", OFFSET(shader_cache), AV_OPT_TYPE_STRING, {.str = NULL}, .flags = STATIC },

    {"colorspace", "select colorspace", OFFSET(colorspace), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_SPC_NB-1, DYNAMIC, .unit = "colorspace"},
    {"auto", "keep the same colorspace",  0, AV_OPT_TYPE_CONST, {.i64=-1},                          INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"gbr",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_RGB},               INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"bt709",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT709},             INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"unknown",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_UNSPECIFIED},       INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"bt470bg",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT470BG},           INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"smpte170m",                  NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_SMPTE170M},         INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"smpte240m",                  NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_SMPTE240M},         INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"ycgco",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_YCGCO},             INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"bt2020nc",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT2020_NCL},        INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"bt2020c",                    NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_BT2020_CL},         INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},
    {"ictcp",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_SPC_ICTCP},             INT_MIN, INT_MAX, STATIC, .unit = "colorspace"},

    {"range", "select color range", OFFSET(color_range), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_RANGE_NB-1, DYNAMIC, .unit = "range"},
    {"auto",  "keep the same color range",   0, AV_OPT_TYPE_CONST, {.i64=-1},                       0, 0, STATIC, .unit = "range"},
    {"unspecified",                  NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_UNSPECIFIED},  0, 0, STATIC, .unit = "range"},
    {"unknown",                      NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_UNSPECIFIED},  0, 0, STATIC, .unit = "range"},
    {"limited",                      NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, .unit = "range"},
    {"tv",                           NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, .unit = "range"},
    {"mpeg",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_MPEG},         0, 0, STATIC, .unit = "range"},
    {"full",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, .unit = "range"},
    {"pc",                           NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, .unit = "range"},
    {"jpeg",                         NULL,   0, AV_OPT_TYPE_CONST, {.i64=AVCOL_RANGE_JPEG},         0, 0, STATIC, .unit = "range"},

    {"color_primaries", "select color primaries", OFFSET(color_primaries), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_PRI_EXT_NB-1, DYNAMIC, .unit = "color_primaries"},
    {"auto", "keep the same color primaries",  0, AV_OPT_TYPE_CONST, {.i64=-1},                     INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"bt709",                           NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT709},        INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"unknown",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_UNSPECIFIED},  INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"bt470m",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT470M},       INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"bt470bg",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT470BG},      INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"smpte170m",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE170M},    INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"smpte240m",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE240M},    INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"film",                            NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_FILM},         INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"bt2020",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_BT2020},       INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"smpte428",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE428},     INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"smpte431",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE431},     INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"smpte432",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_SMPTE432},     INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"jedec-p22",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_JEDEC_P22},    INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"ebu3213",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_EBU3213},      INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},
    {"vgamut",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_PRI_V_GAMUT},      INT_MIN, INT_MAX, STATIC, .unit = "color_primaries"},

    {"color_trc", "select color transfer", OFFSET(color_trc), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCOL_TRC_EXT_NB-1, DYNAMIC, .unit = "color_trc"},
    {"auto", "keep the same color transfer",  0, AV_OPT_TYPE_CONST, {.i64=-1},                     INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"bt709",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT709},        INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"unknown",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_UNSPECIFIED},  INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"gamma22",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_GAMMA22},      INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"bt470m",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_GAMMA22},      INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"gamma28",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_GAMMA28},      INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"bt470bg",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_GAMMA28},      INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"smpte170m",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE170M},    INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"smpte240m",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE240M},    INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"linear",                         NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_LINEAR},       INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"iec61966-2-4",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_IEC61966_2_4}, INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"bt1361e",                        NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT1361_ECG},   INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"iec61966-2-1",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_IEC61966_2_1}, INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"bt2020-10",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT2020_10},    INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"bt2020-12",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_BT2020_12},    INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"smpte2084",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_SMPTE2084},    INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"arib-std-b67",                   NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_ARIB_STD_B67}, INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},
    {"vlog",                           NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCOL_TRC_V_LOG},        INT_MIN, INT_MAX, STATIC, .unit = "color_trc"},

    {"chroma_location", "select chroma location", OFFSET(chroma_location), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVCHROMA_LOC_NB-1, DYNAMIC, .unit = "chroma_location"},
    {"auto",  "keep the same chroma location",  0, AV_OPT_TYPE_CONST, {.i64=-1},                        0, 0, STATIC, .unit = "chroma_location"},
    {"unspecified",                      NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCHROMA_LOC_UNSPECIFIED},  0, 0, STATIC, .unit = "chroma_location"},
    {"unknown",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCHROMA_LOC_UNSPECIFIED},  0, 0, STATIC, .unit = "chroma_location"},
    {"left",                             NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCHROMA_LOC_LEFT},         0, 0, STATIC, .unit = "chroma_location"},
    {"center",                           NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCHROMA_LOC_CENTER},       0, 0, STATIC, .unit = "chroma_location"},
    {"topleft",                          NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCHROMA_LOC_TOPLEFT},      0, 0, STATIC, .unit = "chroma_location"},
    {"top",                              NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCHROMA_LOC_TOP},          0, 0, STATIC, .unit = "chroma_location"},
    {"bottomleft",                       NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCHROMA_LOC_BOTTOMLEFT},   0, 0, STATIC, .unit = "chroma_location"},
    {"bottom",                           NULL,  0, AV_OPT_TYPE_CONST, {.i64=AVCHROMA_LOC_BOTTOM},       0, 0, STATIC, .unit = "chroma_location"},

    {"rotate", "rotate the input clockwise", OFFSET(rotation), AV_OPT_TYPE_INT, {.i64=PL_ROTATION_0}, PL_ROTATION_0, PL_ROTATION_360, DYNAMIC, .unit = "rotation"},
    {"0",                              NULL,  0, AV_OPT_TYPE_CONST, {.i64=PL_ROTATION_0},   .flags = STATIC, .unit = "rotation"},
    {"90",                             NULL,  0, AV_OPT_TYPE_CONST, {.i64=PL_ROTATION_90},  .flags = STATIC, .unit = "rotation"},
    {"180",                            NULL,  0, AV_OPT_TYPE_CONST, {.i64=PL_ROTATION_180}, .flags = STATIC, .unit = "rotation"},
    {"270",                            NULL,  0, AV_OPT_TYPE_CONST, {.i64=PL_ROTATION_270}, .flags = STATIC, .unit = "rotation"},
    {"360",                            NULL,  0, AV_OPT_TYPE_CONST, {.i64=PL_ROTATION_360}, .flags = STATIC, .unit = "rotation"},

    {"alpha_mode", "select alpha moda", OFFSET(alpha_mode), AV_OPT_TYPE_INT, {.i64=-1}, -1, AVALPHA_MODE_NB-1, DYNAMIC, .unit = "alpha_mode"},
    {"auto", "keep the same alpha mode",  0, AV_OPT_TYPE_CONST, {.i64=-1},                              0, 0, DYNAMIC, .unit = "alpha_mode"},
    {"unspecified",                      NULL, 0, AV_OPT_TYPE_CONST, {.i64=AVALPHA_MODE_UNSPECIFIED},   0, 0, DYNAMIC, .unit = "alpha_mode"},
    {"unknown",                          NULL, 0, AV_OPT_TYPE_CONST, {.i64=AVALPHA_MODE_UNSPECIFIED},   0, 0, DYNAMIC, .unit = "alpha_mode"},
    {"premultiplied",                    NULL, 0, AV_OPT_TYPE_CONST, {.i64=AVALPHA_MODE_PREMULTIPLIED}, 0, 0, DYNAMIC, .unit = "alpha_mode"},
    {"straight",                         NULL, 0, AV_OPT_TYPE_CONST, {.i64=AVALPHA_MODE_STRAIGHT},      0, 0, DYNAMIC, .unit = "alpha_mode"},

    { "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 },
    { "frame_mixer", "Frame mixing function", OFFSET(frame_mixer), AV_OPT_TYPE_STRING, {.str = "none"}, .flags = 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 },
    { "apply_dolbyvision", "Apply Dolby Vision metadata", OFFSET(apply_dovi), AV_OPT_TYPE_BOOL, {.i64 = 1}, 0, 1, DYNAMIC },

    { "deinterlace", "Deinterlacing mode", OFFSET(deinterlace), AV_OPT_TYPE_INT, {.i64 = PL_DEINTERLACE_WEAVE}, 0, PL_DEINTERLACE_ALGORITHM_COUNT - 1, DYNAMIC, .unit = "deinterlace" },
        { "weave", "Weave fields together (no-op)",    0, AV_OPT_TYPE_CONST, {.i64 = PL_DEINTERLACE_WEAVE}, 0, 0, STATIC, .unit = "deinterlace" },
        { "bob",   "Naive bob deinterlacing",          0, AV_OPT_TYPE_CONST, {.i64 = PL_DEINTERLACE_BOB},   0, 0, STATIC, .unit = "deinterlace" },
        { "yadif", "Yet another deinterlacing filter", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DEINTERLACE_YADIF}, 0, 0, STATIC, .unit = "deinterlace" },
#if PL_API_VER >= 353
        { "bwdif", "Bob weaver deinterlacing filter",  0, AV_OPT_TYPE_CONST, {.i64 = PL_DEINTERLACE_BWDIF}, 0, 0, STATIC, .unit = "deinterlace" },
#endif
    { "skip_spatial_check", "Skip yadif spatial check", OFFSET(skip_spatial_check), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
    { "send_fields", "Output a frame for each field", OFFSET(send_fields), AV_OPT_TYPE_BOOL, {.i64 = 0}, 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 },
    { "temperature", "Color temperature adjustment (kelvin)", OFFSET(temperature), AV_OPT_TYPE_FLOAT, {.dbl = 6500.0}, 1667.0, 25000.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 = 20.0}, 0.0, 1000.0, DYNAMIC },
    { "scene_threshold_low", "Scene change low threshold", OFFSET(scene_low), AV_OPT_TYPE_FLOAT, {.dbl = 1.0}, -1.0, 100.0, DYNAMIC },
    { "scene_threshold_high", "Scene change high threshold", OFFSET(scene_high), AV_OPT_TYPE_FLOAT, {.dbl = 3.0}, -1.0, 100.0, DYNAMIC },
    { "percentile", "Peak detection percentile", OFFSET(percentile), AV_OPT_TYPE_FLOAT, {.dbl = 99.995}, 0.0, 100.0, DYNAMIC },

    { "gamut_mode", "Gamut-mapping mode", OFFSET(gamut_mode), AV_OPT_TYPE_INT, {.i64 = GAMUT_MAP_PERCEPTUAL}, 0, GAMUT_MAP_COUNT - 1, DYNAMIC, .unit = "gamut_mode" },
        { "clip", "Hard-clip (RGB per-channel)", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_CLIP}, 0, 0, STATIC, .unit = "gamut_mode" },
        { "perceptual", "Colorimetric soft clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_PERCEPTUAL}, 0, 0, STATIC, .unit = "gamut_mode" },
        { "relative", "Relative colorimetric clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_RELATIVE}, 0, 0, STATIC, .unit = "gamut_mode" },
        { "saturation", "Saturation mapping (RGB -> RGB)", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_SATURATION}, 0, 0, STATIC, .unit = "gamut_mode" },
        { "absolute", "Absolute colorimetric clipping", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_ABSOLUTE}, 0, 0, STATIC, .unit = "gamut_mode" },
        { "desaturate", "Colorimetrically desaturate colors towards white", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_DESATURATE}, 0, 0, STATIC, .unit = "gamut_mode" },
        { "darken", "Colorimetric clip with bias towards darkening image to fit gamut", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_DARKEN}, 0, 0, STATIC, .unit = "gamut_mode" },
        { "warn", "Highlight out-of-gamut colors", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_HIGHLIGHT}, 0, 0, STATIC, .unit = "gamut_mode" },
        { "linear", "Linearly reduce chromaticity to fit gamut", 0, AV_OPT_TYPE_CONST, {.i64 = GAMUT_MAP_LINEAR}, 0, 0, STATIC, .unit = "gamut_mode" },
    { "tonemapping", "Tone-mapping algorithm", OFFSET(tonemapping), AV_OPT_TYPE_INT, {.i64 = TONE_MAP_AUTO}, 0, TONE_MAP_COUNT - 1, DYNAMIC, .unit = "tonemap" },
        { "auto", "Automatic selection", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_AUTO}, 0, 0, STATIC, .unit = "tonemap" },
        { "clip", "No tone mapping (clip", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_CLIP}, 0, 0, STATIC, .unit = "tonemap" },
        { "st2094-40", "SMPTE ST 2094-40", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_ST2094_40}, 0, 0, STATIC, .unit = "tonemap" },
        { "st2094-10", "SMPTE ST 2094-10", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_ST2094_10}, 0, 0, STATIC, .unit = "tonemap" },
        { "bt.2390", "ITU-R BT.2390 EETF", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_BT2390}, 0, 0, STATIC, .unit = "tonemap" },
        { "bt.2446a", "ITU-R BT.2446 Method A", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_BT2446A}, 0, 0, STATIC, .unit = "tonemap" },
        { "spline", "Single-pivot polynomial spline", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_SPLINE}, 0, 0, STATIC, .unit = "tonemap" },
        { "reinhard", "Reinhard", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_REINHARD}, 0, 0, STATIC, .unit = "tonemap" },
        { "mobius", "Mobius", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_MOBIUS}, 0, 0, STATIC, .unit = "tonemap" },
        { "hable", "Filmic tone-mapping (Hable)", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_HABLE}, 0, 0, STATIC, .unit = "tonemap" },
        { "gamma", "Gamma function with knee", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_GAMMA}, 0, 0, STATIC, .unit = "tonemap" },
        { "linear", "Perceptually linear stretch", 0, AV_OPT_TYPE_CONST, {.i64 = TONE_MAP_LINEAR}, 0, 0, STATIC, .unit = "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 },
    { "inverse_tonemapping", "Inverse tone mapping (range expansion)", OFFSET(inverse_tonemapping), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, DYNAMIC },
    { "tonemapping_lut_size", "Tone-mapping LUT size", OFFSET(tonemapping_lut_size), AV_OPT_TYPE_INT, {.i64 = 256}, 2, 1024, DYNAMIC },
    { "contrast_recovery", "HDR contrast recovery strength", OFFSET(contrast_recovery), AV_OPT_TYPE_FLOAT, {.dbl = 0.30}, 0.0, 3.0, DYNAMIC },
    { "contrast_smoothness", "HDR contrast recovery smoothness", OFFSET(contrast_smoothness), AV_OPT_TYPE_FLOAT, {.dbl = 3.50}, 1.0, 32.0, DYNAMIC },

    { "dithering", "Dither method to use", OFFSET(dithering), AV_OPT_TYPE_INT, {.i64 = PL_DITHER_BLUE_NOISE}, -1, PL_DITHER_METHOD_COUNT - 1, DYNAMIC, .unit = "dither" },
        { "none", "Disable dithering", 0, AV_OPT_TYPE_CONST, {.i64 = -1}, 0, 0, STATIC, .unit = "dither" },
        { "blue", "Blue noise", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_BLUE_NOISE}, 0, 0, STATIC, .unit = "dither" },
        { "ordered", "Ordered LUT", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_ORDERED_LUT}, 0, 0, STATIC, .unit = "dither" },
        { "ordered_fixed", "Fixed function ordered", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_ORDERED_FIXED}, 0, 0, STATIC, .unit = "dither" },
        { "white", "White noise", 0, AV_OPT_TYPE_CONST, {.i64 = PL_DITHER_WHITE_NOISE}, 0, 0, STATIC, .unit = "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, .unit = "cone" },
        { "l", "L cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_L}, 0, 0, STATIC, .unit = "cone" },
        { "m", "M cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_M}, 0, 0, STATIC, .unit = "cone" },
        { "s", "S cone", 0, AV_OPT_TYPE_CONST, {.i64 = PL_CONE_S}, 0, 0, STATIC, .unit = "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, 1, 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_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_outputs[] = {
    {
        .name         = "default",
        .type         = AVMEDIA_TYPE_VIDEO,
        .config_props = &libplacebo_config_output,
    },
};

const FFFilter ff_vf_libplacebo = {
    .p.name         = "libplacebo",
    .p.description  = NULL_IF_CONFIG_SMALL("Apply various GPU filters from libplacebo"),
    .p.priv_class   = &libplacebo_class,
    .p.flags        = AVFILTER_FLAG_HWDEVICE | AVFILTER_FLAG_DYNAMIC_INPUTS,
    .priv_size      = sizeof(LibplaceboContext),
    .init           = &libplacebo_init,
    .uninit         = &libplacebo_uninit,
    .activate       = &libplacebo_activate,
    .process_command = &libplacebo_process_command,
    FILTER_OUTPUTS(libplacebo_outputs),
    FILTER_QUERY_FUNC2(libplacebo_query_format),
    .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
};
