PR #25390 opened by Niklas Haas (haasn) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25390 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25390.patch
This is a new filter that is similar to vf_libplacebo but takes all of its configuration from a [libplacebo scene graph JSON](https://libplacebo.org/json/) instead of filter parameters. This is very powerful and supports a number of things the old vf_libplacebo never did, including a real canvas, composition logic, animations, crossfades, and more. >From 49b73bee9056d88bd2ea6ae6a209a2fccb01b939 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Fri, 23 Oct 2026 16:35:58 +0200 Subject: [PATCH 01/13] avfilter/vf_libplacebo: don't mask input errors with a later EOF Only the status of the input ending last was forwarded, so an error on a shorter input was reported as an ordinary EOF if a longer input ended normally. Keep forwarding the most recent status, but prefer errors. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- libavfilter/vf_libplacebo.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index ac9a56152f9..f3bdf756b58 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -1267,14 +1267,17 @@ static int libplacebo_activate(AVFilterContext *ctx) drain_input_pts(&s->inputs[i], out_pts); return output_frame(ctx, out_pts); } else if (s->nb_active == 0) { - /* Forward most recent status */ + /* Forward most recent status, but don't let a later EOF mask errors */ 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; + status_pts = in->status_pts; + if (in->status != AVERROR_EOF || status == AVERROR_EOF) + status = in->status; + } else if (status == AVERROR_EOF) { + status = in->status; } } ff_outlink_set_status(outlink, status, status_pts); -- 2.54.0 >From d4cdd15c0b6fe232e539897394a2f08500f1e3de Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Fri, 23 Oct 2026 15:31:15 +0200 Subject: [PATCH 02/13] avfilter/vf_libplacebo: fix forcing output color properties to 0 These options default to -1 (auto), but were only applied when positive, so e.g. colorspace=gbr or range=unspecified were silently ignored. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- libavfilter/vf_libplacebo.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index f3bdf756b58..4f7d7f34728 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -1363,20 +1363,20 @@ done: RET(ff_formats_ref(outfmts, &cfg_out[0]->formats)); - outfmts = s->colorspace > 0 ? ff_make_formats_list_singleton(s->colorspace) - : ff_all_color_spaces(); + 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(); + 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) + 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)); - outfmts = s->chroma_location > 0 ? ff_make_formats_list_singleton(s->chroma_location) - : ff_all_chroma_locations(); + outfmts = s->chroma_location >= 0 ? ff_make_formats_list_singleton(s->chroma_location) + : ff_all_chroma_locations(); RET(ff_formats_ref(outfmts, &cfg_out[0]->chroma_locations)); return 0; -- 2.54.0 >From 18dfb3e4b59d5f6d61cc8e61da24dfd50bf872c6 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Fri, 23 Oct 2026 16:17:15 +0200 Subject: [PATCH 03/13] avfilter/vf_libplacebo: restrict output formats to match a forced colorspace Lavfi negotiates the pixel format independently of the colorspace, so forcing colorspace=gbr could still pick a YUV format (and vice versa). Co-Authored-By: Claude Opus 5.5 <[email protected]> --- libavfilter/vf_libplacebo.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 4f7d7f34728..54658d69b63 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -1334,6 +1334,10 @@ static int libplacebo_query_format(const AVFilterContext *ctx, if (pixfmt != s->out_format && s->out_format != AV_PIX_FMT_NONE) continue; + /* Mask based on user specified colorspace */ + if (s->colorspace >= 0 && !!(desc->flags & AV_PIX_FMT_FLAG_RGB) != (s->colorspace == AVCOL_SPC_RGB)) + continue; + if (!pl_test_pixfmt_caps(s->gpu, pixfmt, PL_FMT_CAP_RENDERABLE)) continue; -- 2.54.0 >From 7ee17019a98085bf848abb992e1d562fbb6e55fa Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:56 +0200 Subject: [PATCH 04/13] avfilter/vf_libplacebo: factor out init_common() Move the log, shader cache and output format setup out of libplacebo_init(), so it can be shared with other filters in this file. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 44 +++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 54658d69b63..984550af812 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -510,16 +510,11 @@ 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) +/* Creates the log, shader cache and output format shared by all filters */ +static int init_common(AVFilterContext *avctx) { - int err = 0; LibplaceboContext *s = avctx->priv; - 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(), @@ -530,12 +525,6 @@ static int libplacebo_init(AVFilterContext *avctx) 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, @@ -543,10 +532,8 @@ static int libplacebo_init(AVFilterContext *avctx) .set = pl_cache_set_file, .priv = s->shader_cache, )); - if (!s->cache) { - libplacebo_uninit(avctx); + if (!s->cache) return AVERROR(ENOMEM); - } } if (s->out_format_string) { @@ -554,13 +541,36 @@ static int libplacebo_init(AVFilterContext *avctx) 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; } + return 0; +} + +static int libplacebo_init(AVFilterContext *avctx) +{ + int err = 0; + LibplaceboContext *s = avctx->priv; + + 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"); + } + + if ((err = init_common(avctx)) < 0) { + libplacebo_uninit(avctx); + return err; + } + + s->opts = pl_options_alloc(s->log); + if (!s->opts) { + libplacebo_uninit(avctx); + return AVERROR(ENOMEM); + } + for (int i = 0; i < s->nb_inputs; i++) { AVFilterPad pad = { .name = av_asprintf("input%d", i), -- 2.54.0 >From 549b9accd0f9d7eed747b990de0884eb48a02e06 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:56 +0200 Subject: [PATCH 05/13] avfilter/vf_libplacebo: split device creation out of init_vulkan() init_device() only creates or imports the Vulkan device, leaving the filter-specific setup (shaders, LUT, inputs) in init_vulkan(). Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 984550af812..9320b523bd4 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -680,12 +680,10 @@ static int copy_pl_queue(const AVVulkanDeviceContext *hwctx, return 0; } -static int init_vulkan(AVFilterContext *avctx, const AVVulkanDeviceContext *hwctx) +static int init_device(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 = { @@ -733,6 +731,20 @@ static int init_vulkan(AVFilterContext *avctx, const AVVulkanDeviceContext *hwct s->gpu = s->vulkan->gpu; pl_gpu_set_cache(s->gpu, s->cache); + return 0; + +fail: + return err; +} + +static int init_vulkan(AVFilterContext *avctx, const AVVulkanDeviceContext *hwctx) +{ + int err = 0; + LibplaceboContext *s = avctx->priv; + uint8_t *buf = NULL; + size_t buf_len; + + RET(init_device(avctx, hwctx)); /* Parse the user shaders, if requested */ if (s->shader_bin_len) -- 2.54.0 >From e3f4063c297f02ac98d66608a7d316c7b045d31f Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:56 +0200 Subject: [PATCH 06/13] avfilter/vf_libplacebo: factor out append_inputs() Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 9320b523bd4..a54a743fde5 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -550,6 +550,26 @@ static int init_common(AVFilterContext *avctx) return 0; } +static int append_inputs(AVFilterContext *avctx, int nb_inputs, + int (*config_props)(AVFilterLink *link)) +{ + int err; + + for (int i = 0; i < nb_inputs; i++) { + AVFilterPad pad = { + .name = av_asprintf("input%d", i), + .type = AVMEDIA_TYPE_VIDEO, + .config_props = config_props, + }; + if (!pad.name) + return AVERROR(ENOMEM); + if ((err = ff_append_inpad_free_name(avctx, &pad)) < 0) + return err; + } + + return 0; +} + static int libplacebo_init(AVFilterContext *avctx) { int err = 0; @@ -571,17 +591,7 @@ static int libplacebo_init(AVFilterContext *avctx) return AVERROR(ENOMEM); } - 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(append_inputs(avctx, s->nb_inputs, &libplacebo_config_input)); RET(update_settings(avctx)); RET(av_expr_parse(&s->crop_x_pexpr, s->crop_x_expr, var_names, NULL, NULL, NULL, NULL, 0, s)); -- 2.54.0 >From 43d464d80da20f2acc9e608e3f2b216cbaadebdd Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:57 +0200 Subject: [PATCH 07/13] avfilter/vf_libplacebo: factor out init_default_device() Takes the device init function as a callback, so it can be shared with other filters in this file. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index a54a743fde5..0a00caa12e0 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -570,6 +570,21 @@ static int append_inputs(AVFilterContext *avctx, int nb_inputs, return 0; } +/* Use the filter's own Vulkan device if it has one, or create a new one */ +static int init_default_device(AVFilterContext *avctx, + int (*init)(AVFilterContext *avctx, + const AVVulkanDeviceContext *hwctx)) +{ + const AVVulkanDeviceContext *vkhwctx = NULL; + if (avctx->hw_device_ctx) { + const AVHWDeviceContext *avhwctx = (void *) avctx->hw_device_ctx->data; + if (avhwctx->type == AV_HWDEVICE_TYPE_VULKAN) + vkhwctx = avhwctx->hwctx; + } + + return init(avctx, vkhwctx); +} + static int libplacebo_init(AVFilterContext *avctx) { int err = 0; @@ -616,15 +631,8 @@ static int libplacebo_init(AVFilterContext *avctx) /* if we are told to inherit the input link's device, ignore the global * hw_device_ctx even if it would be compatible. This ensure consistent * behaviour when the flag is set. */ - if (!s->inherit_device) { - const AVVulkanDeviceContext *vkhwctx = NULL; - 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)); - } + if (!s->inherit_device) + RET(init_default_device(avctx, init_vulkan)); return 0; -- 2.54.0 >From d47315d0c976d87177406ab892e7106b0498094c Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:57 +0200 Subject: [PATCH 08/13] avfilter/vf_libplacebo: factor out config_input_common() Move the device inheritance and Vulkan input setup out of libplacebo_config_input(), taking the device init function as a callback. The input rotation remains specific to this filter. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 0a00caa12e0..69bce0d1b57 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -1432,20 +1432,14 @@ fail: return err; } -static int libplacebo_config_input(AVFilterLink *inlink) +static int config_input_common(AVFilterLink *inlink, + int (*init)(AVFilterContext *avctx, + const AVVulkanDeviceContext *hwctx)) { AVFilterContext *avctx = inlink->dst; LibplaceboContext *s = avctx->priv; FilterLink *l = ff_filter_link(inlink); - 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); - } - /* Deferred Vulkan setup (inherit_device): the device was not created at * init; adopt the one the input frames live on. query_formats advertised * Vulkan only, so a non-Vulkan input here is a configuration error. */ @@ -1463,7 +1457,7 @@ static int libplacebo_config_input(AVFilterLink *inlink) avctx->hw_device_ctx = av_buffer_ref(hwfc->device_ref); if (!avctx->hw_device_ctx) return AVERROR(ENOMEM); - if ((err = init_vulkan(avctx, hwfc->device_ctx->hwctx)) < 0) + if ((err = init(avctx, hwfc->device_ctx->hwctx)) < 0) return err; } @@ -1476,6 +1470,21 @@ static int libplacebo_config_input(AVFilterLink *inlink) return 0; } +static int libplacebo_config_input(AVFilterLink *inlink) +{ + LibplaceboContext *s = inlink->dst->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); + } + + return config_input_common(inlink, init_vulkan); +} + static inline AVRational max_q(AVRational a, AVRational b) { return av_cmp_q(a, b) < 0 ? b : a; -- 2.54.0 >From 65a0a9108c1b1e52ad5bf4bc92f502598e8d2acb Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:57 +0200 Subject: [PATCH 09/13] avfilter/vf_libplacebo: factor out config_output_vulkan() Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 48 +++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 69bce0d1b57..2332765e0fb 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -1490,18 +1490,42 @@ static inline AVRational max_q(AVRational a, AVRational b) return av_cmp_q(a, b) < 0 ? b : a; } +static int config_output_vulkan(AVFilterLink *outlink) +{ + int err; + FilterLink *l = ff_filter_link(outlink); + LibplaceboContext *s = outlink->src->priv; + AVHWFramesContext *hwfc; + AVVulkanFramesContext *vkfc; + + 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; + } + if ((err = ff_vk_filter_config_output(outlink)) < 0) + return err; + hwfc = (AVHWFramesContext *)l->hw_frames_ctx->data; + vkfc = hwfc->hwctx; + vkfc->usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; + return 0; +} + 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, @@ -1601,23 +1625,7 @@ static int libplacebo_config_output(AVFilterLink *outlink) 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; + return config_output_vulkan(outlink); fail: return err; -- 2.54.0 >From 15746498cf5bf7ece6fdd63754d495ff4ccb9929 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:57 +0200 Subject: [PATCH 10/13] avfilter/vf_libplacebo: factor out map_output() and unmap_output() Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 53 +++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 2332765e0fb..e541bef71a6 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -953,14 +953,43 @@ static void update_crops(AVFilterContext *ctx, LibplaceboInput *in, } } +static int map_output(LibplaceboContext *s, AVFrame *out, struct pl_frame *target) +{ + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(out->format); + bool ok; + + if (desc->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); + } + + return ok ? 0 : AVERROR_EXTERNAL; +} + +static int unmap_output(LibplaceboContext *s, struct pl_frame *target, AVFrame *out) +{ + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(out->format); + + if (desc->flags & AV_PIX_FMT_FLAG_HWACCEL) { + pl_unmap_avframe(s->gpu, target); + } else if (!pl_download_avframe(s->gpu, target, out)) { + return AVERROR_EXTERNAL; + } + + return 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; + int err = 0, 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; @@ -1051,18 +1080,7 @@ props_done: 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; - } + RET(map_output(s, out, &target)); struct pl_frame orig_target = target; bool use_linear_compositor = false; @@ -1119,12 +1137,7 @@ props_done: 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; - } + RET(unmap_output(s, &target, out)); return ff_filter_frame(outlink, out); fail: -- 2.54.0 >From db29e24425b9b4bc39d4e0c4d88fdb696cdeb052 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:57 +0200 Subject: [PATCH 11/13] avfilter/vf_libplacebo: factor out set_output_props() Move the output frame property setup out of output_frame(). The `csp` argument allows other filters to override the inferred color tags. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 92 +++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 34 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index e541bef71a6..9deb54b95eb 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -983,10 +983,66 @@ static int unmap_output(LibplaceboContext *s, struct pl_frame *target, AVFrame * return 0; } +/* Set the properties of an output frame from the reference frame (if any) and + * the output link. `csp`, if set, overrides the inferred color tags. */ +static int set_output_props(LibplaceboContext *s, const AVFilterLink *outlink, + AVFrame *out, const AVFrame *ref, + const struct pl_color_space *csp, bool deinterlaced) +{ + int err, changed = 0; + + if (ref) { + if ((err = av_frame_copy_props(out, ref)) < 0) + return err; + if (deinterlaced) + 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; + } + } + + 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 (outlink->chroma_location != AVCHROMA_LOC_UNSPECIFIED) + out->chroma_location = outlink->chroma_location; + + if (csp) { + out->color_primaries = pl_primaries_to_av(csp->primaries); + out->color_trc = pl_transfer_to_av(csp->transfer); + } + if (s->color_trc >= 0) + out->color_trc = s->color_trc; + if (s->color_primaries >= 0) + out->color_primaries = s->color_primaries; + + if (!ref) + return 0; + + /* 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); + + return 0; +} + /* Construct and emit an output frame for a given timestamp */ static int output_frame(AVFilterContext *ctx, int64_t pts) { - int err = 0, changed = 0; + int err = 0; LibplaceboContext *s = ctx->priv; pl_options opts = s->opts; AVFilterLink *outlink = ctx->outputs[0]; @@ -1031,39 +1087,7 @@ static int output_frame(AVFilterContext *ctx, int64_t pts) 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 (outlink->chroma_location != AVCHROMA_LOC_UNSPECIFIED) - out->chroma_location = outlink->chroma_location; - 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; - - /* 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); + RET(set_output_props(s, outlink, out, ref, NULL, s->deinterlace)); if (s->reset_sar) { out->sample_aspect_ratio = ref->sample_aspect_ratio; -- 2.54.0 >From a265ab2f8c16ed45d7077f6888e9ff32f4dcee7c Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Mon, 19 Oct 2026 19:58:57 +0200 Subject: [PATCH 12/13] avfilter/vf_libplacebo: factor out push_frame() Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Niklas Haas <[email protected]> --- libavfilter/vf_libplacebo.c | 44 +++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 9deb54b95eb..6080d924762 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -1203,6 +1203,30 @@ static void discard_frame(const struct pl_source_frame *src) av_frame_free(&avframe); } +/* Push an input frame to the queue, returning its first field */ +static enum pl_field push_frame(LibplaceboContext *s, pl_queue queue, + const AVFilterLink *inlink, AVFrame *in, + bool deinterlace) +{ + struct pl_source_frame src = { + .pts = TS2T(in->pts, inlink->time_base), + .duration = TS2T(in->duration, inlink->time_base), + .first_field = deinterlace ? pl_field_from_avframe(in) : PL_FIELD_NONE, + .frame_data = in, + .map = map_frame, + .unmap = unmap_frame, + .discard = discard_frame, + }; + + /* Input without a chroma location is assumed to be sited like the output. */ + if (in->chroma_location == AVCHROMA_LOC_UNSPECIFIED) + in->chroma_location = inlink->dst->outputs[0]->chroma_location; + + in->opaque = s; + pl_queue_push(queue, &src); + return src.first_field; +} + static int handle_input(AVFilterContext *ctx, LibplaceboInput *input) { int ret, status; @@ -1213,29 +1237,15 @@ static int handle_input(AVFilterContext *ctx, LibplaceboInput *input) 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, - }; - - /* Input without a chroma location is assumed to be sited like the output. */ - if (in->chroma_location == AVCHROMA_LOC_UNSPECIFIED) - in->chroma_location = outlink->chroma_location; - - in->opaque = s; - pl_queue_push(input->queue, &src); + const enum pl_field field = push_frame(s, input->queue, inlink, in, + s->deinterlace); 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) { + if (s->send_fields && 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); -- 2.54.0 >From cbbdbd010767433692411478c0c912b8b6d1fc28 Mon Sep 17 00:00:00 2001 From: Niklas Haas <[email protected]> Date: Fri, 23 Oct 2026 15:22:32 +0200 Subject: [PATCH 13/13] avfilter/vf_libplacebo: add libplacebo_scene filter This filter takes a scene graph in the libplacebo scene JSON format and exposes it as a filter. The number of inputs and their properties are determined by the scene graph. I decided to make this a separate filter because it invalidates most of the existing options, including e.g. the existing scaling and fitting logic. For simplicity, scene graphs are always assumed to have a fixed canvas size, which the user continues specifying at init time. Requires libplacebo API version 386 or newer, which we probe the existence for as a separate capability so we can selectively expose the filter. Signed-off-by: Niklas Haas <[email protected]> --- configure | 3 + doc/filters.texi | 50 ++++++ libavfilter/Makefile | 1 + libavfilter/allfilters.c | 1 + libavfilter/vf_libplacebo.c | 329 ++++++++++++++++++++++++++++++++++++ 5 files changed, 384 insertions(+) diff --git a/configure b/configure index 362e7b12f7b..2265a5bec88 100755 --- a/configure +++ b/configure @@ -2658,6 +2658,7 @@ HAVE_LIST=" ioctl_posix iokit libdrm_getfb2 + libplacebo_scene libvmaf_picture_wrap makeinfo_html opencl_d3d11 @@ -4189,6 +4190,7 @@ ladspa_filter_deps="ladspa libdl" lcevc_filter_deps="liblcevc_dec" lensfun_filter_deps="liblensfun version3" libplacebo_filter_deps="libplacebo vulkan" +libplacebo_scene_filter_deps="libplacebo libplacebo_scene vulkan" lv2_filter_deps="lv2" mcdeint_filter_deps="avcodec gpl" metadata_filter_deps="avformat" @@ -7335,6 +7337,7 @@ enabled libopus && { } } enabled libplacebo && require_pkg_config libplacebo "libplacebo >= 7.351.0" libplacebo/vulkan.h pl_vulkan_create +enabled libplacebo && check_pkg_config libplacebo_scene "libplacebo >= 7.386.0" libplacebo/compositor.h pl_scene_create_json enabled libpulse && require_pkg_config libpulse libpulse pulse/pulseaudio.h pa_context_new enabled libqrencode && require_pkg_config libqrencode libqrencode qrencode.h QRcode_encodeString enabled libquirc && require libquirc quirc.h quirc_decode -lquirc diff --git a/doc/filters.texi b/doc/filters.texi index 79ac811744e..e8d0237532a 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -17098,6 +17098,7 @@ ffmpeg -i input.mov -vf lensfun=make=Canon:model="Canon EOS 100D":lens_model="Ca Low Complexity Enhancement Video Codec filter based on liblcevc_dec (@url{https://github.com/v-novaltd/LCEVCdec}). +@anchor{libplacebo} @section libplacebo Flexible GPU-accelerated processing filter based on libplacebo @@ -17855,6 +17856,55 @@ ffmpeg -init_hw_device vulkan -hwaccel vaapi -hwaccel_output_format vaapi ... -v @end example @end itemize +@section libplacebo_scene +Composite one or more inputs onto a canvas of fixed size and frame rate, +according to a JSON scene graph. See https://libplacebo.org/json/ for more +information. + +@subsection Options +@table @option +@item scene +JSON scene graph description. Required. Use @code{/scene=file.json} to load it +from a file. + +@item w +@item h +Output frame width and height. Required. Constant expressions are accepted. + +@item fps +Output frame rate. Required. + +@item sar +Output sample aspect ratio. Default @code{1}. + +@item format +@item inherit_device +@item shader_cache +@item apply_filmgrain +@item apply_dolbyvision +Same as for the @ref{libplacebo} filter. +@end table + +The output color space, representation and chroma location are taken from +the scene's @code{color}, @code{repr} and @code{chroma_location}. Values not set +there are negotiated, except for the color primaries and transfer, which follow +the lowest visible input. + +@subsection Example +Show a second input in the bottom right quarter between 1 and 3 seconds: +@example +ffmpeg -i main.mkv -i overlay.mkv -filter_complex \ + "[0][1]libplacebo_scene=w=1920:h=1080:fps=25:/scene=scene.json" out.mkv +@end example +with @file{scene.json} containing: +@example +@{ "inputs": [ + @{ @}, + @{ "start_pts": 1, "stop_pts": 3, + "dst_rect": @{ "x0": 0.5, "y0": 0.5 @} @} +] @} +@end example + @anchor{libvmaf} @section libvmaf diff --git a/libavfilter/Makefile b/libavfilter/Makefile index ef2542b98b9..095e9c2d4e8 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -379,6 +379,7 @@ OBJS-$(CONFIG_LATTICEPAL_FILTER) += vf_latticepal.o OBJS-$(CONFIG_LENSCORRECTION_FILTER) += vf_lenscorrection.o OBJS-$(CONFIG_LENSFUN_FILTER) += vf_lensfun.o OBJS-$(CONFIG_LIBPLACEBO_FILTER) += vf_libplacebo.o vulkan.o vulkan_filter.o +OBJS-$(CONFIG_LIBPLACEBO_SCENE_FILTER) += vf_libplacebo.o vulkan.o vulkan_filter.o OBJS-$(CONFIG_LIBVMAF_FILTER) += vf_libvmaf.o framesync.o OBJS-$(CONFIG_LIBVMAF_CUDA_FILTER) += vf_libvmaf.o framesync.o OBJS-$(CONFIG_LIMITDIFF_FILTER) += vf_limitdiff.o framesync.o diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index 931ac8481e0..9f07bf83dce 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -351,6 +351,7 @@ extern const FFFilter ff_vf_lcevc; extern const FFFilter ff_vf_lenscorrection; extern const FFFilter ff_vf_lensfun; extern const FFFilter ff_vf_libplacebo; +extern const FFFilter ff_vf_libplacebo_scene; extern const FFFilter ff_vf_libvmaf; extern const FFFilter ff_vf_libvmaf_cuda; extern const FFFilter ff_vf_limitdiff; diff --git a/libavfilter/vf_libplacebo.c b/libavfilter/vf_libplacebo.c index 6080d924762..bc5f32164da 100644 --- a/libavfilter/vf_libplacebo.c +++ b/libavfilter/vf_libplacebo.c @@ -18,6 +18,8 @@ #include <math.h> +#include "config_components.h" + #include "libavutil/avassert.h" #include "libavutil/avstring.h" #include "libavutil/eval.h" @@ -39,6 +41,12 @@ #include <libplacebo/utils/frame_queue.h> #include <libplacebo/vulkan.h> +#if PL_API_VER >= 386 +#include <libplacebo/compositor.h> +#else +typedef const struct pl_scene_t *pl_scene; +#endif + enum { TONE_MAP_AUTO, TONE_MAP_CLIP, @@ -161,6 +169,12 @@ typedef struct LibplaceboContext { int nb_inputs; int nb_active; + /* scene graph (libplacebo_scene only) */ + char *scene_json; + pl_scene scene; + AVRational out_sar; + int warned_csp_change; + /* settings */ char *out_format_string; enum AVPixelFormat out_format; @@ -1931,3 +1945,318 @@ const FFFilter ff_vf_libplacebo = { FILTER_QUERY_FUNC2(libplacebo_query_format), .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE | FF_FILTER_FLAG_DYNAMIC_FRAME_PARAMS, }; + +#if CONFIG_LIBPLACEBO_SCENE_FILTER + +static int scene_init_device(AVFilterContext *avctx, const AVVulkanDeviceContext *hwctx) +{ + LibplaceboContext *s = avctx->priv; + int err = init_device(avctx, hwctx); + if (err < 0) + return err; + if (!pl_compositor_init(s->scene->comp, s->gpu)) + return AVERROR_EXTERNAL; + return 0; +} + +static int scene_config_input(AVFilterLink *inlink) +{ + return config_input_common(inlink, scene_init_device); +} + +static int scene_init(AVFilterContext *avctx) +{ + int err; + LibplaceboContext *s = avctx->priv; + pl_compositor comp; + + if (!s->scene_json || !s->w_expr || !s->h_expr || !s->fps_string) { + av_log(avctx, AV_LOG_ERROR, "The 'scene', 'w', 'h' and 'fps' options " + "are required\n"); + return AVERROR(EINVAL); + } + + RET(av_parse_video_rate(&s->fps, s->fps_string)); + RET(init_common(avctx)); + + s->scene = pl_scene_create_json(s->log, s->scene_json, strlen(s->scene_json)); + if (!s->scene) { + av_log(avctx, AV_LOG_ERROR, "Failed parsing scene description\n"); + return AVERROR(EINVAL); + } + + comp = s->scene->comp; + s->nb_inputs = comp->num_inputs; + if (!s->nb_inputs) { + av_log(avctx, AV_LOG_ERROR, "Scene contains no inputs\n"); + return AVERROR(EINVAL); + } + + /* Output color properties set by the scene, or -1 (auto) */ + s->colorspace = comp->repr.sys ? pl_system_to_av(comp->repr.sys) : -1; + s->color_range = comp->repr.levels ? pl_levels_to_av(comp->repr.levels) : -1; + s->alpha_mode = comp->repr.alpha ? pl_alpha_to_av(comp->repr.alpha) : -1; + s->chroma_location = comp->chroma_location ? pl_chroma_to_av(comp->chroma_location) : -1; + + RET(append_inputs(avctx, s->nb_inputs, &scene_config_input)); + + if (!s->inherit_device) + RET(init_default_device(avctx, scene_init_device)); + /* else deferred to scene_config_input() */ + + return 0; + +fail: + return err; +} + +static void scene_uninit(AVFilterContext *avctx) +{ + LibplaceboContext *s = avctx->priv; + pl_scene_destroy(&s->scene); + libplacebo_uninit(avctx); +} + +static int scene_eval_dim(AVFilterContext *avctx, const char *expr, int *out) +{ + double res; + int err = av_expr_parse_and_eval(&res, expr, NULL, NULL, NULL, NULL, + NULL, NULL, NULL, 0, avctx); + if (err < 0 || res < 1 || res > INT_MAX) { + av_log(avctx, AV_LOG_ERROR, "Invalid output dimension: %s\n", expr); + return err < 0 ? err : AVERROR(EINVAL); + } + + *out = lrint(res); + return 0; +} + +/* Render what was negotiated, if it differs from the scene. Only compare the + * libav equivalents, which can't represent e.g. HLG ICtCp. Returns whether + * anything was changed. */ +static int scene_force_link_props(pl_compositor comp, const AVFilterLink *outlink) +{ + int changed = 0; + if (pl_system_to_av(comp->repr.sys) != outlink->colorspace) { + comp->repr.sys = pl_system_from_av(outlink->colorspace); + changed = 1; + } + if (pl_levels_to_av(comp->repr.levels) != outlink->color_range) { + comp->repr.levels = pl_levels_from_av(outlink->color_range); + changed = 1; + } + if (pl_alpha_to_av(comp->repr.alpha) != outlink->alpha_mode) { + comp->repr.alpha = pl_alpha_from_av(outlink->alpha_mode); + changed = 1; + } + if (pl_chroma_to_av(comp->chroma_location) != outlink->chroma_location) { + comp->chroma_location = pl_chroma_from_av(outlink->chroma_location); + changed = 1; + } + return changed; +} + +static int scene_config_output(AVFilterLink *outlink) +{ + int err; + AVFilterContext *avctx = outlink->src; + LibplaceboContext *s = avctx->priv; + FilterLink *l = ff_filter_link(outlink); + + scene_force_link_props(s->scene->comp, outlink); + RET(scene_eval_dim(avctx, s->w_expr, &outlink->w)); + RET(scene_eval_dim(avctx, s->h_expr, &outlink->h)); + outlink->sample_aspect_ratio = s->out_sar; + outlink->time_base = av_inv_q(s->fps); + l->frame_rate = s->fps; + + return config_output_vulkan(outlink); + +fail: + return err; +} + +static int scene_handle_input(AVFilterContext *ctx, int idx) +{ + int ret, status; + LibplaceboContext *s = ctx->priv; + AVFilterLink *inlink = ctx->inputs[idx]; + pl_input input = s->scene->comp->inputs[idx]; + const bool deinterlace = input->params && input->params->deinterlace_params; + AVFrame *in; + int64_t pts; + + while ((ret = ff_inlink_consume_frame(inlink, &in)) > 0) + push_frame(s, input->queue, inlink, in, deinterlace); + + if (ret < 0) + return ret; + + if (ff_inlink_acknowledge_status(inlink, &status, &pts)) { + pl_queue_set_status(input->queue, status == AVERROR_EOF ? PL_QUEUE_EOF : PL_QUEUE_ERR); + input->user_data = (void *) (intptr_t) status; + } + + return 0; +} + +static int scene_output_frame(AVFilterContext *ctx, int64_t pts, + const struct pl_queue_params *qparams, + const struct pl_compositor_status *st) +{ + int err = 0; + bool ok; + LibplaceboContext *s = ctx->priv; + AVFilterLink *outlink = ctx->outputs[0]; + const struct pl_render_params *par = st->num_visible ? st->visible[0]->params : NULL; + const struct pl_frame *ref_pl = st->base; + const AVFrame *ref = ref_pl ? pl_get_mapped_avframe(ref_pl) : NULL; + struct pl_frame target; + AVFrame *out; + + out = ff_get_video_buffer(outlink, outlink->w, outlink->h); + if (!out) + return AVERROR(ENOMEM); + + /* The reference color space includes any scene overrides */ + RET(set_output_props(s, outlink, out, ref, ref_pl ? &ref_pl->color : NULL, + par && par->deinterlace_params)); + out->sample_aspect_ratio = outlink->sample_aspect_ratio; + out->pts = pts; + out->duration = 1; + + RET(map_output(s, out, &target)); + ok = pl_compositor_render(s->scene->comp, &target, qparams, NULL); + RET(unmap_output(s, &target, out)); + if (!ok) { + err = AVERROR_EXTERNAL; + goto fail; + } + return ff_filter_frame(outlink, out); + +fail: + av_frame_free(&out); + return err; +} + +static int scene_activate(AVFilterContext *ctx) +{ + int ret, retry = 0; + LibplaceboContext *s = ctx->priv; + AVFilterLink *outlink = ctx->outputs[0]; + FilterLink *outl = ff_filter_link(outlink); + pl_compositor comp = s->scene->comp; + + 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 = scene_handle_input(ctx, i)) < 0) + return ret; + } + + if (!ff_outlink_frame_wanted(outlink)) + return FFERROR_NOT_READY; + + const int64_t out_pts = outl->frame_count_out; + const struct pl_queue_params qparams = *pl_queue_params( + .pts = TS2T(out_pts, outlink->time_base), + .vsync_duration = av_q2d(outlink->time_base), + ); + + pl_scene_update(s->scene, qparams.pts); + if (scene_force_link_props(comp, outlink)) { + av_log_once(ctx, AV_LOG_WARNING, AV_LOG_DEBUG, &s->warned_csp_change, + "Scene changes the output color representation mid-stream, " + "which is not supported; keeping the negotiated values.\n"); + } + + /* These are not yet negotiated on the link, so just reflect back any changes */ + s->color_primaries = comp->color.primaries ? pl_primaries_to_av(comp->color.primaries) : -1; + s->color_trc = comp->color.transfer ? pl_transfer_to_av(comp->color.transfer) : -1; + + struct pl_compositor_status st; + st = pl_compositor_update(comp, &qparams, pl_canvas_size( + .width = outlink->w, + .height = outlink->h, + .pixel_aspect_ratio = av_q2d(outlink->sample_aspect_ratio), + )); + + for (int i = 0; i < s->nb_inputs; i++) { + AVFilterLink *inlink = ctx->inputs[i]; + switch (st.input_status[i].status) { + case PL_QUEUE_MORE: + if (!ff_outlink_get_status(inlink)) { + ff_inlink_request_frame(inlink); + retry = 1; + } + break; + case PL_QUEUE_EOF: + /* Also closes inputs that reached their stop_pts early */ + ff_inlink_set_status(inlink, AVERROR_EOF); + break; + case PL_QUEUE_ERR: { + /* Forward the error of the failed input */ + const int status = (intptr_t) comp->inputs[i]->user_data; + if (!status) + return AVERROR_EXTERNAL; + ff_outlink_set_status(outlink, status, out_pts); + return 0; + } + default: + break; + } + } + + if (retry) + return 0; + + if (st.status == PL_QUEUE_EOF) { + ff_outlink_set_status(outlink, AVERROR_EOF, out_pts); + return 0; + } + + return scene_output_frame(ctx, out_pts, &qparams, &st); +} + +static const AVOption libplacebo_scene_options[] = { + { "scene", "JSON scene graph description", OFFSET(scene_json), AV_OPT_TYPE_STRING, .flags = STATIC }, + { "w", "Output video frame width", OFFSET(w_expr), AV_OPT_TYPE_STRING, .flags = STATIC }, + { "h", "Output video frame height", OFFSET(h_expr), AV_OPT_TYPE_STRING, .flags = STATIC }, + { "fps", "Output video frame rate", OFFSET(fps_string), AV_OPT_TYPE_STRING, .flags = STATIC }, + { "sar", "Output sample aspect ratio", OFFSET(out_sar), AV_OPT_TYPE_RATIONAL, {.dbl = 1.0}, 0, INT_MAX, STATIC }, + { "format", "Output video format", OFFSET(out_format_string), AV_OPT_TYPE_STRING, .flags = STATIC }, + { "inherit_device", "Inherit the Vulkan device from the input's hardware frames context", + OFFSET(inherit_device), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, .flags = STATIC }, + { "shader_cache", "Set shader cache path", OFFSET(shader_cache), AV_OPT_TYPE_STRING, {.str = NULL}, .flags = STATIC }, + { "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 }, + { NULL }, +}; + +AVFILTER_DEFINE_CLASS(libplacebo_scene); + +static const AVFilterPad libplacebo_scene_outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .config_props = &scene_config_output, + }, +}; + +const FFFilter ff_vf_libplacebo_scene = { + .p.name = "libplacebo_scene", + .p.description = NULL_IF_CONFIG_SMALL("Composite inputs according to a libplacebo scene graph"), + .p.priv_class = &libplacebo_scene_class, + .p.flags = AVFILTER_FLAG_HWDEVICE | AVFILTER_FLAG_DYNAMIC_INPUTS, + .priv_size = sizeof(LibplaceboContext), + .init = &scene_init, + .uninit = &scene_uninit, + .activate = &scene_activate, + .process_command = &ff_filter_process_command, + FILTER_OUTPUTS(libplacebo_scene_outputs), + FILTER_QUERY_FUNC2(libplacebo_query_format), + .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE | FF_FILTER_FLAG_DYNAMIC_FRAME_PARAMS, +}; + +#endif /* CONFIG_LIBPLACEBO_SCENE_FILTER */ -- 2.54.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
