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]

Reply via email to