Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
 help / color / mirror / Atom feed
From: Lynne via ffmpeg-devel <ffmpeg-devel@ffmpeg.org>
To: ffmpeg-devel@ffmpeg.org
Cc: Lynne <code@ffmpeg.org>
Subject: [FFmpeg-devel] [PR] Convert a few filters to compile-time SPIR-V (PR #21810)
Date: Thu, 19 Feb 2026 22:39:55 -0000
Message-ID: <177154079595.25.11008091249156812316@29965ddac10e> (raw)

PR #21810 opened by Lynne
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/21810
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/21810.patch

It only takes 10 minutes.


>From e730556dca8b33820b08969e9e89ffe9c5a0e483 Mon Sep 17 00:00:00 2001
From: Lynne <dev@lynne.ee>
Date: Tue, 10 Feb 2026 20:36:28 +0100
Subject: [PATCH 1/7] vf_flip_vulkan: convert to compile-time SPIR-V generation

---
 configure                         |   6 +-
 libavfilter/vf_flip_vulkan.c      | 101 ++++++++----------------------
 libavfilter/vulkan/Makefile       |   1 +
 libavfilter/vulkan/flip.comp.glsl |  59 +++++++++++++++++
 4 files changed, 88 insertions(+), 79 deletions(-)
 create mode 100644 libavfilter/vulkan/flip.comp.glsl

diff --git a/configure b/configure
index b629173712..f2531aacf0 100755
--- a/configure
+++ b/configure
@@ -4113,7 +4113,7 @@ elbg_filter_deps="avcodec"
 eq_filter_deps="gpl"
 erosion_opencl_filter_deps="opencl"
 find_rect_filter_deps="avcodec avformat gpl"
-flip_vulkan_filter_deps="vulkan spirv_library"
+flip_vulkan_filter_deps="vulkan spirv_compiler"
 flite_filter_deps="libflite threads"
 framerate_filter_select="scene_sad"
 freezedetect_filter_select="scene_sad"
@@ -4123,7 +4123,7 @@ frei0r_src_filter_deps="frei0r"
 fspp_filter_deps="gpl"
 fsync_filter_deps="avformat"
 gblur_vulkan_filter_deps="vulkan spirv_library"
-hflip_vulkan_filter_deps="vulkan spirv_library"
+hflip_vulkan_filter_deps="vulkan spirv_compiler"
 histeq_filter_deps="gpl"
 hqdn3d_filter_deps="gpl"
 iccdetect_filter_deps="lcms2"
@@ -4215,7 +4215,7 @@ transpose_vulkan_filter_deps="vulkan spirv_library"
 unsharp_opencl_filter_deps="opencl"
 uspp_filter_deps="gpl avcodec"
 vaguedenoiser_filter_deps="gpl"
-vflip_vulkan_filter_deps="vulkan spirv_library"
+vflip_vulkan_filter_deps="vulkan spirv_compiler"
 vidstabdetect_filter_deps="libvidstab"
 vidstabtransform_filter_deps="libvidstab"
 libvmaf_filter_deps="libvmaf"
diff --git a/libavfilter/vf_flip_vulkan.c b/libavfilter/vf_flip_vulkan.c
index 3e2aed0fda..72fba7d972 100644
--- a/libavfilter/vf_flip_vulkan.c
+++ b/libavfilter/vf_flip_vulkan.c
@@ -19,14 +19,15 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
-#include "libavutil/random_seed.h"
 #include "libavutil/opt.h"
-#include "libavutil/vulkan_spirv.h"
 #include "vulkan_filter.h"
 
 #include "filters.h"
 #include "video.h"
 
+extern const unsigned char ff_flip_comp_spv_data[];
+extern const unsigned int ff_flip_comp_spv_len;
+
 enum FlipType {
     FLIP_VERTICAL,
     FLIP_HORIZONTAL,
@@ -42,24 +43,13 @@ typedef struct FlipVulkanContext {
     FFVulkanShader shd;
 } FlipVulkanContext;
 
-static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in, enum FlipType type)
+static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
 {
     int err = 0;
-    uint8_t *spv_data;
-    size_t spv_len;
-    void *spv_opaque = NULL;
     FlipVulkanContext *s = ctx->priv;
     FFVulkanContext *vkctx = &s->vkctx;
     const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
     FFVulkanShader *shd = &s->shd;
-    FFVkSPIRVCompiler *spv;
-    FFVulkanDescriptorSetBinding *desc;
-
-    spv = ff_vk_spirv_init();
-    if (!spv) {
-        av_log(ctx, AV_LOG_ERROR, "Unable to initialize SPIR-V compiler!\n");
-        return AVERROR_EXTERNAL;
-    }
 
     s->qf = ff_vk_qf_find(vkctx, VK_QUEUE_COMPUTE_BIT, 0);
     if (!s->qf) {
@@ -69,77 +59,36 @@ static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in, enum FlipType
     }
 
     RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, s->qf->num*4, 0, 0, 0, NULL));
-    RET(ff_vk_shader_init(vkctx, &s->shd, "flip",
-                          VK_SHADER_STAGE_COMPUTE_BIT,
-                          NULL, 0,
-                          32, 32, 1,
-                          0));
 
-    desc = (FFVulkanDescriptorSetBinding []) {
-        {
-            .name       = "input_image",
-            .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.input_format, FF_VK_REP_FLOAT),
-            .mem_quali  = "readonly",
-            .dimensions = 2,
-            .elems      = planes,
-            .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
+    ff_vk_shader_load(&s->shd, VK_SHADER_STAGE_COMPUTE_BIT, NULL,
+                      (uint32_t []) { 32, 8, planes }, 0);
+
+    ff_vk_shader_add_push_const(&s->shd, 0, sizeof(int),
+                                VK_SHADER_STAGE_COMPUTE_BIT);
+
+    const FFVulkanDescriptorSetBinding desc[] = {
+        { /* input_img */
+            .type   = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
+            .stages = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems  = planes,
         },
-        {
-            .name       = "output_image",
-            .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.output_format, FF_VK_REP_FLOAT),
-            .mem_quali  = "writeonly",
-            .dimensions = 2,
-            .elems      = planes,
-            .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
+        { /* output_img */
+            .type   = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
+            .stages = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems  = planes,
         },
     };
+    ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc, 2, 0, 0);
 
-    RET(ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc, 2, 0, 0));
-
-    GLSLC(0, void main()                                                                    );
-    GLSLC(0, {                                                                              );
-    GLSLC(1,     ivec2 size;                                                                );
-    GLSLC(1,     const ivec2 pos = ivec2(gl_GlobalInvocationID.xy);                         );
-    for (int i = 0; i < planes; i++) {
-        GLSLC(0,                                                                            );
-        GLSLF(1, size = imageSize(output_image[%i]);                                      ,i);
-        GLSLC(1, if (IS_WITHIN(pos, size)) {                                                );
-        switch (type)
-        {
-        case FLIP_HORIZONTAL:
-            GLSLF(2, vec4 res = imageLoad(input_image[%i], ivec2(size.x - pos.x, pos.y)); ,i);
-            break;
-        case FLIP_VERTICAL:
-            GLSLF(2, vec4 res = imageLoad(input_image[%i], ivec2(pos.x, size.y - pos.y)); ,i);
-            break;
-        case FLIP_BOTH:
-            GLSLF(2, vec4 res = imageLoad(input_image[%i], ivec2(size.xy - pos.xy));,      i);
-            break;
-        default:
-            GLSLF(2, vec4 res = imageLoad(input_image[%i], pos);                          ,i);
-            break;
-        }
-        GLSLF(2,     imageStore(output_image[%i], pos, res);                              ,i);
-        GLSLC(1, }                                                                          );
-    }
-    GLSLC(0, }                                                                              );
-
-    RET(spv->compile_shader(vkctx, spv, shd, &spv_data, &spv_len, "main",
-                            &spv_opaque));
-    RET(ff_vk_shader_link(vkctx, shd, spv_data, spv_len, "main"));
+    RET(ff_vk_shader_link(vkctx, shd,
+                          ff_flip_comp_spv_data,
+                          ff_flip_comp_spv_len, "main"));
 
     RET(ff_vk_shader_register_exec(vkctx, &s->e, &s->shd));
 
     s->initialized = 1;
 
 fail:
-    if (spv_opaque)
-        spv->free_shader(spv, &spv_opaque);
-    if (spv)
-        spv->uninit(&spv);
-
     return err;
 }
 
@@ -171,10 +120,10 @@ static int filter_frame(AVFilterLink *link, AVFrame *in, enum FlipType type)
     }
 
     if (!s->initialized)
-        RET(init_filter(ctx, in, type));
+        RET(init_filter(ctx, in));
 
     RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, out, in,
-                                    VK_NULL_HANDLE, NULL, 0));
+                                    VK_NULL_HANDLE, &type, sizeof(int)));
 
     RET(av_frame_copy_props(out, in));
 
diff --git a/libavfilter/vulkan/Makefile b/libavfilter/vulkan/Makefile
index dc15353e9e..bb9aa1baab 100644
--- a/libavfilter/vulkan/Makefile
+++ b/libavfilter/vulkan/Makefile
@@ -4,3 +4,4 @@ clean::
 OBJS-$(CONFIG_AVGBLUR_VULKAN_FILTER) += vulkan/avgblur.comp.spv.o
 OBJS-$(CONFIG_BWDIF_VULKAN_FILTER) += vulkan/bwdif.comp.spv.o
 OBJS-$(CONFIG_SCALE_VULKAN_FILTER) += vulkan/debayer.comp.spv.o
+OBJS-$(CONFIG_FLIP_VULKAN_FILTER) += vulkan/flip.comp.spv.o
diff --git a/libavfilter/vulkan/flip.comp.glsl b/libavfilter/vulkan/flip.comp.glsl
new file mode 100644
index 0000000000..f711586238
--- /dev/null
+++ b/libavfilter/vulkan/flip.comp.glsl
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2021 Wu Jianhua <jianhua.wu@intel.com>
+ * Copyright (c) 2026 Lynne <dev@lynne.ee>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma shader_stage(compute)
+
+#extension GL_EXT_shader_image_load_formatted : require
+#extension GL_EXT_scalar_block_layout : require
+#extension GL_EXT_nonuniform_qualifier : require
+
+layout (local_size_x_id = 253, local_size_y_id = 254, local_size_z_id = 255) in;
+
+layout (set = 0, binding = 0) uniform readonly  image2D input_img[];
+layout (set = 0, binding = 1) uniform writeonly image2D output_img[];
+
+#define FLIP_VERTICAL 0
+#define FLIP_HORIZONTAL 1
+#define FLIP_BOTH 2
+
+layout (push_constant, scalar) uniform pushConstants {
+    int flip;
+};
+
+void main()
+{
+    ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
+    ivec2 size = imageSize(input_img[nonuniformEXT(gl_LocalInvocationID.z)]);
+    if (any(greaterThanEqual(pos, size)))
+        return;
+
+    ivec2 dst;
+    switch (flip) {
+    case FLIP_HORIZONTAL: dst = ivec2(size.x - pos.x, pos.y); break;
+    case FLIP_VERTICAL:   dst = ivec2(pos.x, size.y - pos.y); break;
+    case FLIP_BOTH:       dst = ivec2(size.xy - pos.xy);      break;
+    default:              dst = pos;                          break;
+    }
+
+    vec4 res = imageLoad(input_img[nonuniformEXT(gl_LocalInvocationID.z)], pos);
+
+    imageStore(output_img[nonuniformEXT(gl_LocalInvocationID.z)], pos, res);
+}
-- 
2.52.0


>From 7a32f4c32a659aff70bc15662a75f62635ffe65f Mon Sep 17 00:00:00 2001
From: Lynne <dev@lynne.ee>
Date: Tue, 10 Feb 2026 20:51:38 +0100
Subject: [PATCH 2/7] vf_transpose_vulkan: convert to compile-time SPIR-V
 generation

---
 configure                              |  2 +-
 libavfilter/vf_transpose_vulkan.c      | 95 +++++++-------------------
 libavfilter/vulkan/Makefile            |  1 +
 libavfilter/vulkan/transpose.comp.glsl | 63 +++++++++++++++++
 4 files changed, 90 insertions(+), 71 deletions(-)
 create mode 100644 libavfilter/vulkan/transpose.comp.glsl

diff --git a/configure b/configure
index f2531aacf0..d1b5c89ae5 100755
--- a/configure
+++ b/configure
@@ -4211,7 +4211,7 @@ tonemap_opencl_filter_deps="opencl const_nan"
 transpose_opencl_filter_deps="opencl"
 transpose_vaapi_filter_deps="vaapi VAProcPipelineCaps_rotation_flags"
 transpose_vt_filter_deps="videotoolbox VTPixelRotationSessionCreate"
-transpose_vulkan_filter_deps="vulkan spirv_library"
+transpose_vulkan_filter_deps="vulkan spirv_compiler"
 unsharp_opencl_filter_deps="opencl"
 uspp_filter_deps="gpl avcodec"
 vaguedenoiser_filter_deps="gpl"
diff --git a/libavfilter/vf_transpose_vulkan.c b/libavfilter/vf_transpose_vulkan.c
index 3fe2d11cb2..2665ababfe 100644
--- a/libavfilter/vf_transpose_vulkan.c
+++ b/libavfilter/vf_transpose_vulkan.c
@@ -19,15 +19,16 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
-#include "libavutil/random_seed.h"
 #include "libavutil/opt.h"
-#include "libavutil/vulkan_spirv.h"
 #include "vulkan_filter.h"
 
 #include "filters.h"
 #include "transpose.h"
 #include "video.h"
 
+extern const unsigned char ff_transpose_comp_spv_data[];
+extern const unsigned int ff_transpose_comp_spv_len;
+
 typedef struct TransposeVulkanContext {
     FFVulkanContext vkctx;
 
@@ -43,22 +44,9 @@ typedef struct TransposeVulkanContext {
 static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
 {
     int err;
-    uint8_t *spv_data;
-    size_t spv_len;
-    void *spv_opaque = NULL;
     TransposeVulkanContext *s = ctx->priv;
     FFVulkanContext *vkctx = &s->vkctx;
-
     const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
-    FFVulkanShader *shd = &s->shd;
-    FFVkSPIRVCompiler *spv;
-    FFVulkanDescriptorSetBinding *desc;
-
-    spv = ff_vk_spirv_init();
-    if (!spv) {
-        av_log(ctx, AV_LOG_ERROR, "Unable to initialize SPIR-V compiler!\n");
-        return AVERROR_EXTERNAL;
-    }
 
     s->qf = ff_vk_qf_find(vkctx, VK_QUEUE_COMPUTE_BIT, 0);
     if (!s->qf) {
@@ -68,70 +56,36 @@ static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
     }
 
     RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, s->qf->num*4, 0, 0, 0, NULL));
-    RET(ff_vk_shader_init(vkctx, &s->shd, "transpose",
-                          VK_SHADER_STAGE_COMPUTE_BIT,
-                          NULL, 0,
-                          32, 1, 1,
-                          0));
 
-    desc = (FFVulkanDescriptorSetBinding []) {
-        {
-            .name       = "input_images",
-            .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.input_format, FF_VK_REP_FLOAT),
-            .mem_quali  = "readonly",
-            .dimensions = 2,
-            .elems      = planes,
-            .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
+    ff_vk_shader_load(&s->shd, VK_SHADER_STAGE_COMPUTE_BIT, NULL,
+                      (uint32_t []) { 32, 1, planes }, 0);
+
+    ff_vk_shader_add_push_const(&s->shd, 0, sizeof(int),
+                                VK_SHADER_STAGE_COMPUTE_BIT);
+
+    const FFVulkanDescriptorSetBinding desc[] = {
+        { /* input_img */
+            .type   = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
+            .stages = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems  = planes,
         },
-        {
-            .name       = "output_images",
-            .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.output_format, FF_VK_REP_FLOAT),
-            .mem_quali  = "writeonly",
-            .dimensions = 2,
-            .elems      = planes,
-            .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
+        { /* output_img */
+            .type   = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
+            .stages = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems  = planes,
         },
     };
+    ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc, 2, 0, 0);
 
-    RET(ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc, 2, 0, 0));
-
-    GLSLC(0, void main()                                               );
-    GLSLC(0, {                                                         );
-    GLSLC(1,     ivec2 size;                                           );
-    GLSLC(1,     ivec2 pos = ivec2(gl_GlobalInvocationID.xy);          );
-    for (int i = 0; i < planes; i++) {
-        GLSLC(0,                                                       );
-        GLSLF(1, size = imageSize(output_images[%i]);                ,i);
-        GLSLC(1, if (IS_WITHIN(pos, size)) {                           );
-        if (s->dir == TRANSPOSE_CCLOCK)
-            GLSLF(2, vec4 res = imageLoad(input_images[%i], ivec2(size.y - pos.y, pos.x)); ,i);
-        else if (s->dir == TRANSPOSE_CLOCK_FLIP || s->dir == TRANSPOSE_CLOCK) {
-            GLSLF(2, vec4 res = imageLoad(input_images[%i], ivec2(size.yx - pos.yx));      ,i);
-            if (s->dir == TRANSPOSE_CLOCK)
-                GLSLC(2, pos = ivec2(pos.x, size.y - pos.y);           );
-        } else
-            GLSLF(2, vec4 res = imageLoad(input_images[%i], pos.yx);  ,i);
-        GLSLF(2,     imageStore(output_images[%i], pos, res);        ,i);
-        GLSLC(1, }                                                     );
-    }
-    GLSLC(0, }                                                         );
-
-    RET(spv->compile_shader(vkctx, spv, shd, &spv_data, &spv_len, "main",
-                            &spv_opaque));
-    RET(ff_vk_shader_link(vkctx, shd, spv_data, spv_len, "main"));
+    RET(ff_vk_shader_link(vkctx, &s->shd,
+                          ff_transpose_comp_spv_data,
+                          ff_transpose_comp_spv_len, "main"));
 
     RET(ff_vk_shader_register_exec(vkctx, &s->e, &s->shd));
 
     s->initialized = 1;
 
 fail:
-    if (spv_opaque)
-        spv->free_shader(spv, &spv_opaque);
-    if (spv)
-        spv->uninit(&spv);
-
     return err;
 }
 
@@ -156,7 +110,7 @@ static int filter_frame(AVFilterLink *inlink, AVFrame *in)
         RET(init_filter(ctx, in));
 
     RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, out, in,
-                                    VK_NULL_HANDLE, NULL, 0));
+                                    VK_NULL_HANDLE, &s->dir, sizeof(int)));
 
     RET(av_frame_copy_props(out, in));
 
@@ -223,7 +177,8 @@ static int config_props_output(AVFilterLink *outlink)
 }
 
 #define OFFSET(x) offsetof(TransposeVulkanContext, x)
-#define FLAGS (AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM)
+#define FLAGS (AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM | \
+               AV_OPT_FLAG_RUNTIME_PARAM)
 
 static const AVOption transpose_vulkan_options[] = {
     { "dir", "set transpose direction", OFFSET(dir), AV_OPT_TYPE_INT, { .i64 = TRANSPOSE_CCLOCK_FLIP }, 0, 7, FLAGS, .unit = "dir" },
diff --git a/libavfilter/vulkan/Makefile b/libavfilter/vulkan/Makefile
index bb9aa1baab..e47655c8bd 100644
--- a/libavfilter/vulkan/Makefile
+++ b/libavfilter/vulkan/Makefile
@@ -5,3 +5,4 @@ OBJS-$(CONFIG_AVGBLUR_VULKAN_FILTER) += vulkan/avgblur.comp.spv.o
 OBJS-$(CONFIG_BWDIF_VULKAN_FILTER) += vulkan/bwdif.comp.spv.o
 OBJS-$(CONFIG_SCALE_VULKAN_FILTER) += vulkan/debayer.comp.spv.o
 OBJS-$(CONFIG_FLIP_VULKAN_FILTER) += vulkan/flip.comp.spv.o
+OBJS-$(CONFIG_TRANSPOSE_VULKAN_FILTER) += vulkan/transpose.comp.spv.o
diff --git a/libavfilter/vulkan/transpose.comp.glsl b/libavfilter/vulkan/transpose.comp.glsl
new file mode 100644
index 0000000000..a8ab6b5843
--- /dev/null
+++ b/libavfilter/vulkan/transpose.comp.glsl
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2021 Wu Jianhua <jianhua.wu@intel.com>
+ * Copyright (c) 2026 Lynne <dev@lynne.ee>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma shader_stage(compute)
+
+#extension GL_EXT_shader_image_load_formatted : require
+#extension GL_EXT_scalar_block_layout : require
+#extension GL_EXT_nonuniform_qualifier : require
+
+layout (local_size_x_id = 253, local_size_y_id = 254, local_size_z_id = 255) in;
+
+layout (set = 0, binding = 0) uniform readonly  image2D input_img[];
+layout (set = 0, binding = 1) uniform writeonly image2D output_img[];
+
+#define TRANSPOSE_CCLOCK_FLIP 0
+#define TRANSPOSE_CLOCK 1
+#define TRANSPOSE_CCLOCK 2
+#define TRANSPOSE_CLOCK_FLIP 3
+#define TRANSPOSE_REVERSAL 4
+#define TRANSPOSE_HFLIP 5
+#define TRANSPOSE_VFLIP 6
+
+layout (push_constant, scalar) uniform pushConstants {
+    int dir;
+};
+
+void main()
+{
+    ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
+    ivec2 size = imageSize(input_img[nonuniformEXT(gl_LocalInvocationID.z)]);
+    if (any(greaterThanEqual(pos, size)))
+        return;
+
+    ivec2 dst;
+    switch (dir) {
+    case TRANSPOSE_CCLOCK:     dst = ivec2(size.y - pos.y, pos.x); break;
+    case TRANSPOSE_CLOCK:      pos = ivec2(pos.x, size.y - pos.y); /* fall */
+    case TRANSPOSE_CLOCK_FLIP: dst = ivec2(size.yx - pos.yx);      break;
+    default:                   dst = pos.yx;                       break;
+    }
+
+    vec4 res = imageLoad(input_img[nonuniformEXT(gl_LocalInvocationID.z)], pos);
+
+    imageStore(output_img[nonuniformEXT(gl_LocalInvocationID.z)], pos, res);
+}
-- 
2.52.0


>From 8b15ff524fa0241965c7d3a638e235ea4b9e7c84 Mon Sep 17 00:00:00 2001
From: Lynne <dev@lynne.ee>
Date: Thu, 12 Feb 2026 11:33:07 +0100
Subject: [PATCH 3/7] vulkan_filter: add an argument for setting the Z
 workgroup count

---
 libavfilter/vf_avgblur_vulkan.c   |  2 +-
 libavfilter/vf_blend_vulkan.c     |  2 +-
 libavfilter/vf_bwdif_vulkan.c     |  2 +-
 libavfilter/vf_chromaber_vulkan.c |  2 +-
 libavfilter/vf_flip_vulkan.c      |  2 +-
 libavfilter/vf_gblur_vulkan.c     |  2 +-
 libavfilter/vf_interlace_vulkan.c |  2 +-
 libavfilter/vf_overlay_vulkan.c   |  2 +-
 libavfilter/vf_scale_vulkan.c     |  2 +-
 libavfilter/vf_transpose_vulkan.c |  2 +-
 libavfilter/vf_xfade_vulkan.c     |  2 +-
 libavfilter/vsrc_testsrc_vulkan.c |  4 ++--
 libavfilter/vulkan_filter.c       | 15 +++++++++------
 libavfilter/vulkan_filter.h       |  9 ++++++---
 14 files changed, 28 insertions(+), 22 deletions(-)

diff --git a/libavfilter/vf_avgblur_vulkan.c b/libavfilter/vf_avgblur_vulkan.c
index db0b1b8612..3716f62ccd 100644
--- a/libavfilter/vf_avgblur_vulkan.c
+++ b/libavfilter/vf_avgblur_vulkan.c
@@ -122,7 +122,7 @@ static int avgblur_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
 
     RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd,
                                     out, in, VK_NULL_HANDLE,
-                                    &s->opts, sizeof(s->opts)));
+                                    1, &s->opts, sizeof(s->opts)));
 
     err = av_frame_copy_props(out, in);
     if (err < 0)
diff --git a/libavfilter/vf_blend_vulkan.c b/libavfilter/vf_blend_vulkan.c
index 57cf3c696b..a6492a5c32 100644
--- a/libavfilter/vf_blend_vulkan.c
+++ b/libavfilter/vf_blend_vulkan.c
@@ -264,7 +264,7 @@ static int blend_frame(FFFrameSync *fs)
 
     RET(ff_vk_filter_process_Nin(&s->vkctx, &s->e, &s->shd,
                                  out, (AVFrame *[]){ top, bottom }, 2,
-                                 VK_NULL_HANDLE, NULL, 0));
+                                 VK_NULL_HANDLE, 1, NULL, 0));
 
     return ff_filter_frame(outlink, out);
 
diff --git a/libavfilter/vf_bwdif_vulkan.c b/libavfilter/vf_bwdif_vulkan.c
index 5fefb3396d..ea4154daf5 100644
--- a/libavfilter/vf_bwdif_vulkan.c
+++ b/libavfilter/vf_bwdif_vulkan.c
@@ -115,7 +115,7 @@ static void bwdif_vulkan_filter_frame(AVFilterContext *ctx, AVFrame *dst,
 
     ff_vk_filter_process_Nin(&s->vkctx, &s->e, &s->shd, dst,
                              (AVFrame *[]){ y->prev, y->cur, y->next }, 3,
-                             VK_NULL_HANDLE, &params, sizeof(params));
+                             VK_NULL_HANDLE, 1, &params, sizeof(params));
 
     if (y->current_field == YADIF_FIELD_END)
         y->current_field = YADIF_FIELD_NORMAL;
diff --git a/libavfilter/vf_chromaber_vulkan.c b/libavfilter/vf_chromaber_vulkan.c
index 65b53afd64..6dd50c1bb4 100644
--- a/libavfilter/vf_chromaber_vulkan.c
+++ b/libavfilter/vf_chromaber_vulkan.c
@@ -188,7 +188,7 @@ static int chromaber_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
         RET(init_filter(ctx, in));
 
     RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, out, in,
-                                    s->sampler, &s->opts, sizeof(s->opts)));
+                                    s->sampler, 1, &s->opts, sizeof(s->opts)));
 
     err = av_frame_copy_props(out, in);
     if (err < 0)
diff --git a/libavfilter/vf_flip_vulkan.c b/libavfilter/vf_flip_vulkan.c
index 72fba7d972..b86f236d2a 100644
--- a/libavfilter/vf_flip_vulkan.c
+++ b/libavfilter/vf_flip_vulkan.c
@@ -123,7 +123,7 @@ static int filter_frame(AVFilterLink *link, AVFrame *in, enum FlipType type)
         RET(init_filter(ctx, in));
 
     RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, out, in,
-                                    VK_NULL_HANDLE, &type, sizeof(int)));
+                                    VK_NULL_HANDLE, 1, &type, sizeof(int)));
 
     RET(av_frame_copy_props(out, in));
 
diff --git a/libavfilter/vf_gblur_vulkan.c b/libavfilter/vf_gblur_vulkan.c
index 1b447e2754..8ade1c955d 100644
--- a/libavfilter/vf_gblur_vulkan.c
+++ b/libavfilter/vf_gblur_vulkan.c
@@ -316,7 +316,7 @@ static int gblur_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
 
     RET(ff_vk_filter_process_2pass(&s->vkctx, &s->e,
                                    (FFVulkanShader *[2]){ &s->shd_hor, &s->shd_ver },
-                                   out, tmp, in, VK_NULL_HANDLE, NULL, 0));
+                                   out, tmp, in, VK_NULL_HANDLE, 1, NULL, 0));
 
     err = av_frame_copy_props(out, in);
     if (err < 0)
diff --git a/libavfilter/vf_interlace_vulkan.c b/libavfilter/vf_interlace_vulkan.c
index 7afb30c2d7..afa8a634f8 100644
--- a/libavfilter/vf_interlace_vulkan.c
+++ b/libavfilter/vf_interlace_vulkan.c
@@ -218,7 +218,7 @@ static int interlace_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
 
     RET(ff_vk_filter_process_Nin(&s->vkctx, &s->e, &s->shd,
                                  out, (AVFrame *[]){ input_top, input_bot }, 2,
-                                 s->sampler, NULL, 0));
+                                 s->sampler, 1, NULL, 0));
 
     err = av_frame_copy_props(out, s->cur);
     if (err < 0)
diff --git a/libavfilter/vf_overlay_vulkan.c b/libavfilter/vf_overlay_vulkan.c
index 1f9eed8e08..e1a07e4bfd 100644
--- a/libavfilter/vf_overlay_vulkan.c
+++ b/libavfilter/vf_overlay_vulkan.c
@@ -240,7 +240,7 @@ static int overlay_vulkan_blend(FFFrameSync *fs)
 
     RET(ff_vk_filter_process_Nin(&s->vkctx, &s->e, &s->shd,
                                  out, (AVFrame *[]){ input_main, input_overlay }, 2,
-                                 VK_NULL_HANDLE, &s->opts, sizeof(s->opts)));
+                                 VK_NULL_HANDLE, 1, &s->opts, sizeof(s->opts)));
 
     err = av_frame_copy_props(out, input_main);
     if (err < 0)
diff --git a/libavfilter/vf_scale_vulkan.c b/libavfilter/vf_scale_vulkan.c
index a1e4f59a72..e7e23826ed 100644
--- a/libavfilter/vf_scale_vulkan.c
+++ b/libavfilter/vf_scale_vulkan.c
@@ -372,7 +372,7 @@ static int scale_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
     s->opts.crop_h = in->height - (in->crop_top + in->crop_bottom);
 
     RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, out, in,
-                                    s->sampler, &s->opts, sizeof(s->opts)));
+                                    s->sampler, 1, &s->opts, sizeof(s->opts)));
 
     err = av_frame_copy_props(out, in);
     if (err < 0)
diff --git a/libavfilter/vf_transpose_vulkan.c b/libavfilter/vf_transpose_vulkan.c
index 2665ababfe..e193bb6829 100644
--- a/libavfilter/vf_transpose_vulkan.c
+++ b/libavfilter/vf_transpose_vulkan.c
@@ -110,7 +110,7 @@ static int filter_frame(AVFilterLink *inlink, AVFrame *in)
         RET(init_filter(ctx, in));
 
     RET(ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, out, in,
-                                    VK_NULL_HANDLE, &s->dir, sizeof(int)));
+                                    VK_NULL_HANDLE, 1, &s->dir, sizeof(int)));
 
     RET(av_frame_copy_props(out, in));
 
diff --git a/libavfilter/vf_xfade_vulkan.c b/libavfilter/vf_xfade_vulkan.c
index 58e8797733..3aa96d62b0 100644
--- a/libavfilter/vf_xfade_vulkan.c
+++ b/libavfilter/vf_xfade_vulkan.c
@@ -447,7 +447,7 @@ static int xfade_frame(AVFilterContext *avctx, AVFrame *frame_a, AVFrame *frame_
                         0.f, 1.f);
 
     RET(ff_vk_filter_process_Nin(&s->vkctx, &s->e, &s->shd, output,
-                                 (AVFrame *[]){ frame_a, frame_b }, 2, s->sampler,
+                                 (AVFrame *[]){ frame_a, frame_b }, 2, s->sampler, 1,
                                  &(XFadeParameters){ progress }, sizeof(XFadeParameters)));
 
     return ff_filter_frame(outlink, output);
diff --git a/libavfilter/vsrc_testsrc_vulkan.c b/libavfilter/vsrc_testsrc_vulkan.c
index cb3c787213..68cb8b9ef5 100644
--- a/libavfilter/vsrc_testsrc_vulkan.c
+++ b/libavfilter/vsrc_testsrc_vulkan.c
@@ -235,7 +235,7 @@ static int testsrc_vulkan_activate(AVFilterContext *ctx)
                 return AVERROR(ENOMEM);
 
             err = ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, s->picref, NULL,
-                                              VK_NULL_HANDLE, &s->opts, sizeof(s->opts));
+                                              VK_NULL_HANDLE, 1, &s->opts, sizeof(s->opts));
             if (err < 0)
                 return err;
         }
@@ -254,7 +254,7 @@ static int testsrc_vulkan_activate(AVFilterContext *ctx)
     frame->sample_aspect_ratio = s->sar;
     if (!s->draw_once) {
         err = ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, frame, NULL,
-                                          VK_NULL_HANDLE, &s->opts, sizeof(s->opts));
+                                          VK_NULL_HANDLE, 1, &s->opts, sizeof(s->opts));
         if (err < 0) {
             av_frame_free(&frame);
             return err;
diff --git a/libavfilter/vulkan_filter.c b/libavfilter/vulkan_filter.c
index 44a4ce7242..2fc467c6d9 100644
--- a/libavfilter/vulkan_filter.c
+++ b/libavfilter/vulkan_filter.c
@@ -241,7 +241,8 @@ int ff_vk_filter_init(AVFilterContext *avctx)
 
 int ff_vk_filter_process_simple(FFVulkanContext *vkctx, FFVkExecPool *e,
                                 FFVulkanShader *shd, AVFrame *out_f, AVFrame *in_f,
-                                VkSampler sampler, void *push_src, size_t push_size)
+                                VkSampler sampler, uint32_t wgc_z,
+                                void *push_src, size_t push_size)
 {
     int err = 0;
     FFVulkanFunctions *vk = &vkctx->vkfn;
@@ -304,7 +305,7 @@ int ff_vk_filter_process_simple(FFVulkanContext *vkctx, FFVkExecPool *e,
     vk->CmdDispatch(exec->buf,
                     FFALIGN(vkctx->output_width,  shd->lg_size[0])/shd->lg_size[0],
                     FFALIGN(vkctx->output_height, shd->lg_size[1])/shd->lg_size[1],
-                    1);
+                    wgc_z);
 
     return ff_vk_exec_submit(vkctx, exec);
 fail:
@@ -315,7 +316,8 @@ fail:
 int ff_vk_filter_process_2pass(FFVulkanContext *vkctx, FFVkExecPool *e,
                                FFVulkanShader *shd_list[2],
                                AVFrame *out, AVFrame *tmp, AVFrame *in,
-                               VkSampler sampler, void *push_src, size_t push_size)
+                               VkSampler sampler, uint32_t wgc_z,
+                               void *push_src, size_t push_size)
 {
     int err = 0;
     FFVulkanFunctions *vk = &vkctx->vkfn;
@@ -395,7 +397,7 @@ int ff_vk_filter_process_2pass(FFVulkanContext *vkctx, FFVkExecPool *e,
         vk->CmdDispatch(exec->buf,
                         FFALIGN(vkctx->output_width,  shd->lg_size[0])/shd->lg_size[0],
                         FFALIGN(vkctx->output_height, shd->lg_size[1])/shd->lg_size[1],
-                        1);
+                        wgc_z);
     }
 
     return ff_vk_exec_submit(vkctx, exec);
@@ -407,7 +409,8 @@ fail:
 int ff_vk_filter_process_Nin(FFVulkanContext *vkctx, FFVkExecPool *e,
                              FFVulkanShader *shd,
                              AVFrame *out, AVFrame *in[], int nb_in,
-                             VkSampler sampler, void *push_src, size_t push_size)
+                             VkSampler sampler, uint32_t wgc_z,
+                             void *push_src, size_t push_size)
 {
     int err = 0;
     FFVulkanFunctions *vk = &vkctx->vkfn;
@@ -474,7 +477,7 @@ int ff_vk_filter_process_Nin(FFVulkanContext *vkctx, FFVkExecPool *e,
     vk->CmdDispatch(exec->buf,
                     FFALIGN(vkctx->output_width,  shd->lg_size[0])/shd->lg_size[0],
                     FFALIGN(vkctx->output_height, shd->lg_size[1])/shd->lg_size[1],
-                    1);
+                    wgc_z);
 
     return ff_vk_exec_submit(vkctx, exec);
 fail:
diff --git a/libavfilter/vulkan_filter.h b/libavfilter/vulkan_filter.h
index 6ed9c4de39..c54c8c56c4 100644
--- a/libavfilter/vulkan_filter.h
+++ b/libavfilter/vulkan_filter.h
@@ -44,7 +44,8 @@ int ff_vk_filter_init_context(AVFilterContext *avctx, FFVulkanContext *s,
  */
 int ff_vk_filter_process_simple(FFVulkanContext *vkctx, FFVkExecPool *e,
                                 FFVulkanShader *shd, AVFrame *out_f, AVFrame *in_f,
-                                VkSampler sampler, void *push_src, size_t push_size);
+                                VkSampler sampler, uint32_t wgc_z,
+                                void *push_src, size_t push_size);
 
 /**
  * Submit a compute shader with a single in and single out with 2 stages.
@@ -52,7 +53,8 @@ int ff_vk_filter_process_simple(FFVulkanContext *vkctx, FFVkExecPool *e,
 int ff_vk_filter_process_2pass(FFVulkanContext *vkctx, FFVkExecPool *e,
                                FFVulkanShader *shd_list[2],
                                AVFrame *out, AVFrame *tmp, AVFrame *in,
-                               VkSampler sampler, void *push_src, size_t push_size);
+                               VkSampler sampler, uint32_t wgc_z,
+                               void *push_src, size_t push_size);
 
 /**
  * Up to 16 inputs, one output
@@ -60,6 +62,7 @@ int ff_vk_filter_process_2pass(FFVulkanContext *vkctx, FFVkExecPool *e,
 int ff_vk_filter_process_Nin(FFVulkanContext *vkctx, FFVkExecPool *e,
                              FFVulkanShader *shd,
                              AVFrame *out, AVFrame *in[], int nb_in,
-                             VkSampler sampler, void *push_src, size_t push_size);
+                             VkSampler sampler, uint32_t wgc_z,
+                             void *push_src, size_t push_size);
 
 #endif /* AVFILTER_VULKAN_FILTER_H */
-- 
2.52.0


>From 8e384c57d708ee5fdc0d2ded29aa1631f0e13810 Mon Sep 17 00:00:00 2001
From: Lynne <dev@lynne.ee>
Date: Thu, 12 Feb 2026 12:45:31 +0100
Subject: [PATCH 4/7] vulkan: add LUT permutation table for BGR0/BGRA

Fixes filters.
---
 libavutil/vulkan.c | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/libavutil/vulkan.c b/libavutil/vulkan.c
index 5264abd7ad..334f913e28 100644
--- a/libavutil/vulkan.c
+++ b/libavutil/vulkan.c
@@ -1585,6 +1585,13 @@ void ff_vk_set_perm(enum AVPixelFormat pix_fmt, int lut[4], int inv)
         lut[2] = 1;
         lut[3] = 3;
         break;
+    case AV_PIX_FMT_BGRA:
+    case AV_PIX_FMT_BGR0:
+        lut[0] = 2;
+        lut[1] = 1;
+        lut[2] = 0;
+        lut[3] = 3;
+        break;
     default:
         lut[0] = 0;
         lut[1] = 1;
-- 
2.52.0


>From f71d68e27d747a7cc83b9eea559ed3927900a1a5 Mon Sep 17 00:00:00 2001
From: Lynne <dev@lynne.ee>
Date: Thu, 12 Feb 2026 12:02:00 +0100
Subject: [PATCH 5/7] vsrc_testsrc_vulkan: convert to compile-time SPIR-V
 generation

---
 configure                          |  2 +-
 libavfilter/vsrc_testsrc_vulkan.c  | 77 +++++++++---------------------
 libavfilter/vulkan/Makefile        |  1 +
 libavfilter/vulkan/color.comp.glsl | 45 +++++++++++++++++
 4 files changed, 70 insertions(+), 55 deletions(-)
 create mode 100644 libavfilter/vulkan/color.comp.glsl

diff --git a/configure b/configure
index d1b5c89ae5..58682c9c27 100755
--- a/configure
+++ b/configure
@@ -4084,7 +4084,7 @@ bwdif_cuda_filter_deps="ffnvcodec"
 bwdif_cuda_filter_deps_any="cuda_nvcc cuda_llvm"
 bwdif_vulkan_filter_deps="vulkan spirv_compiler"
 chromaber_vulkan_filter_deps="vulkan spirv_library"
-color_vulkan_filter_deps="vulkan spirv_library"
+color_vulkan_filter_deps="vulkan spirv_compiler"
 colorkey_opencl_filter_deps="opencl"
 colormatrix_filter_deps="gpl"
 convolution_opencl_filter_deps="opencl"
diff --git a/libavfilter/vsrc_testsrc_vulkan.c b/libavfilter/vsrc_testsrc_vulkan.c
index 68cb8b9ef5..60cd285dd9 100644
--- a/libavfilter/vsrc_testsrc_vulkan.c
+++ b/libavfilter/vsrc_testsrc_vulkan.c
@@ -18,10 +18,8 @@
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  */
 
-#include "libavutil/random_seed.h"
 #include "libavutil/csp.h"
 #include "libavutil/opt.h"
-#include "libavutil/vulkan_spirv.h"
 #include "vulkan_filter.h"
 #include "filters.h"
 #include "colorspace.h"
@@ -31,8 +29,11 @@ enum TestSrcVulkanMode {
     TESTSRC_COLOR,
 };
 
+extern const unsigned char ff_color_comp_spv_data[];
+extern const unsigned int ff_color_comp_spv_len;
+
 typedef struct TestSrcVulkanPushData {
-    float color_comp[4];
+    float color_data[4][4];
 } TestSrcVulkanPushData;
 
 typedef struct TestSrcVulkanContext {
@@ -65,23 +66,11 @@ typedef struct TestSrcVulkanContext {
 static av_cold int init_filter(AVFilterContext *ctx, enum TestSrcVulkanMode mode)
 {
     int err;
-    uint8_t *spv_data;
-    size_t spv_len;
-    void *spv_opaque = NULL;
     TestSrcVulkanContext *s = ctx->priv;
     FFVulkanContext *vkctx = &s->vkctx;
     const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
-    FFVulkanShader *shd = &s->shd;
-    FFVkSPIRVCompiler *spv;
-    FFVulkanDescriptorSetBinding *desc_set;
     const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(s->vkctx.output_format);
 
-    spv = ff_vk_spirv_init();
-    if (!spv) {
-        av_log(ctx, AV_LOG_ERROR, "Unable to initialize SPIR-V compiler!\n");
-        return AVERROR_EXTERNAL;
-    }
-
     s->qf = ff_vk_qf_find(vkctx, VK_QUEUE_COMPUTE_BIT, 0);
     if (!s->qf) {
         av_log(ctx, AV_LOG_ERROR, "Device has no compute queues\n");
@@ -90,37 +79,22 @@ static av_cold int init_filter(AVFilterContext *ctx, enum TestSrcVulkanMode mode
     }
 
     RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, s->qf->num*4, 0, 0, 0, NULL));
-    RET(ff_vk_shader_init(vkctx, &s->shd, "scale",
-                          VK_SHADER_STAGE_COMPUTE_BIT,
-                          NULL, 0,
-                          32, 32, 1,
-                          0));
 
-    GLSLC(0, layout(push_constant, std430) uniform pushConstants {        );
-    GLSLC(1,    vec4 color_comp;                                          );
-    GLSLC(0, };                                                           );
-    GLSLC(0,                                                              );
+    ff_vk_shader_load(&s->shd, VK_SHADER_STAGE_COMPUTE_BIT, NULL,
+                      (uint32_t []) { 32, 32, 1 }, 0);
 
     ff_vk_shader_add_push_const(&s->shd, 0, sizeof(s->opts),
                                 VK_SHADER_STAGE_COMPUTE_BIT);
 
-    desc_set = (FFVulkanDescriptorSetBinding []) {
-        {
-            .name       = "output_img",
+    const FFVulkanDescriptorSetBinding desc_set[] = {
+        { /* output_img */
             .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.output_format, FF_VK_REP_FLOAT),
-            .mem_quali  = "writeonly",
-            .dimensions = 2,
-            .elems      = planes,
             .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems      = planes,
         },
     };
+    ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc_set, 1, 0, 0);
 
-    RET(ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc_set, 1, 0, 0));
-
-    GLSLC(0, void main()                                                  );
-    GLSLC(0, {                                                            );
-    GLSLC(1,     ivec2 pos = ivec2(gl_GlobalInvocationID.xy);             );
     if (mode == TESTSRC_COLOR) {
         double rgb2yuv[3][3];
         double rgbad[4];
@@ -167,38 +141,30 @@ static av_cold int init_filter(AVFilterContext *ctx, enum TestSrcVulkanMode mode
         if (desc->nb_components <= 2)
             yuvad[1] = yuvad[3];
 
+        float color_comp[4];
         for (int i = 0; i < 4; i++)
-            s->opts.color_comp[i] = yuvad[i];
+            color_comp[i] = yuvad[i];
 
-        GLSLC(1,     vec4 r;                                                  );
-        GLSLC(0,                                                              );
+        int fmt_lut[4];
+        ff_vk_set_perm(s->vkctx.output_format, fmt_lut, 0);
         for (int i = 0, c_off = 0; i < planes; i++) {
             for (int c = 0; c < desc->nb_components; c++) {
                 if (desc->comp[c].plane == i) {
                     int off = desc->comp[c].offset / (FFALIGN(desc->comp[c].depth, 8)/8);
-                    GLSLF(1, r[%i] = color_comp[%i];             ,off, c_off++);
+                    s->opts.color_data[i][off] = color_comp[fmt_lut[c_off++]];
                 }
             }
-            GLSLF(1, imageStore(output_img[%i], pos, r);                    ,i);
-            GLSLC(0,                                                          );
         }
-    }
-    GLSLC(0, }                                                            );
 
-    RET(spv->compile_shader(vkctx, spv, shd, &spv_data, &spv_len, "main",
-                            &spv_opaque));
-    RET(ff_vk_shader_link(vkctx, shd, spv_data, spv_len, "main"));
+        RET(ff_vk_shader_link(vkctx, &s->shd, ff_color_comp_spv_data,
+                              ff_color_comp_spv_len, "main"));
+    }
 
     RET(ff_vk_shader_register_exec(vkctx, &s->e, &s->shd));
 
     s->initialized = 1;
 
 fail:
-    if (spv_opaque)
-        spv->free_shader(spv, &spv_opaque);
-    if (spv)
-        spv->uninit(&spv);
-
     return err;
 }
 
@@ -207,6 +173,7 @@ static int testsrc_vulkan_activate(AVFilterContext *ctx)
     int err;
     AVFilterLink *outlink = ctx->outputs[0];
     TestSrcVulkanContext *s = ctx->priv;
+    const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
     AVFrame *frame;
 
     if (!s->initialized) {
@@ -235,7 +202,8 @@ static int testsrc_vulkan_activate(AVFilterContext *ctx)
                 return AVERROR(ENOMEM);
 
             err = ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, s->picref, NULL,
-                                              VK_NULL_HANDLE, 1, &s->opts, sizeof(s->opts));
+                                              VK_NULL_HANDLE, planes,
+                                              &s->opts, sizeof(s->opts));
             if (err < 0)
                 return err;
         }
@@ -254,7 +222,8 @@ static int testsrc_vulkan_activate(AVFilterContext *ctx)
     frame->sample_aspect_ratio = s->sar;
     if (!s->draw_once) {
         err = ff_vk_filter_process_simple(&s->vkctx, &s->e, &s->shd, frame, NULL,
-                                          VK_NULL_HANDLE, 1, &s->opts, sizeof(s->opts));
+                                          VK_NULL_HANDLE, planes,
+                                          &s->opts, sizeof(s->opts));
         if (err < 0) {
             av_frame_free(&frame);
             return err;
diff --git a/libavfilter/vulkan/Makefile b/libavfilter/vulkan/Makefile
index e47655c8bd..f0d737733c 100644
--- a/libavfilter/vulkan/Makefile
+++ b/libavfilter/vulkan/Makefile
@@ -3,6 +3,7 @@ clean::
 
 OBJS-$(CONFIG_AVGBLUR_VULKAN_FILTER) += vulkan/avgblur.comp.spv.o
 OBJS-$(CONFIG_BWDIF_VULKAN_FILTER) += vulkan/bwdif.comp.spv.o
+OBJS-$(CONFIG_COLOR_VULKAN_FILTER) += vulkan/color.comp.spv.o
 OBJS-$(CONFIG_SCALE_VULKAN_FILTER) += vulkan/debayer.comp.spv.o
 OBJS-$(CONFIG_FLIP_VULKAN_FILTER) += vulkan/flip.comp.spv.o
 OBJS-$(CONFIG_TRANSPOSE_VULKAN_FILTER) += vulkan/transpose.comp.spv.o
diff --git a/libavfilter/vulkan/color.comp.glsl b/libavfilter/vulkan/color.comp.glsl
new file mode 100644
index 0000000000..9f23323f24
--- /dev/null
+++ b/libavfilter/vulkan/color.comp.glsl
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2026 Lynne <dev@lynne.ee>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma shader_stage(compute)
+
+#extension GL_EXT_shader_image_load_formatted : require
+#extension GL_EXT_scalar_block_layout : require
+#extension GL_EXT_nonuniform_qualifier : require
+
+layout (local_size_x_id = 253, local_size_y_id = 254, local_size_z_id = 255) in;
+
+layout (set = 0, binding = 0) uniform writeonly image2D output_img[];
+
+layout (push_constant, scalar) uniform pushConstants {
+    mat4 color_data;
+};
+
+void main()
+{
+    ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
+    ivec2 size = imageSize(output_img[nonuniformEXT(gl_WorkGroupID.z)]);
+    if (any(greaterThanEqual(pos, size)))
+        return;
+
+    mat4 tmp = color_data;
+    vec4 res = tmp[gl_WorkGroupID.z];
+    imageStore(output_img[nonuniformEXT(gl_WorkGroupID.z)], pos, res);
+}
-- 
2.52.0


>From 66f62f60f503261308b715b98879512a5bf54480 Mon Sep 17 00:00:00 2001
From: Lynne <dev@lynne.ee>
Date: Thu, 12 Feb 2026 15:07:12 +0100
Subject: [PATCH 6/7] vf_gblur_vulkan: port to compile-time SPIR-V generation

---
 configure                          |   2 +-
 libavfilter/vf_gblur_vulkan.c      | 138 +++++++----------------------
 libavfilter/vulkan/Makefile        |   1 +
 libavfilter/vulkan/gblur.comp.glsl |  62 +++++++++++++
 4 files changed, 94 insertions(+), 109 deletions(-)
 create mode 100644 libavfilter/vulkan/gblur.comp.glsl

diff --git a/configure b/configure
index 58682c9c27..c124582c44 100755
--- a/configure
+++ b/configure
@@ -4122,7 +4122,7 @@ frei0r_filter_deps="frei0r"
 frei0r_src_filter_deps="frei0r"
 fspp_filter_deps="gpl"
 fsync_filter_deps="avformat"
-gblur_vulkan_filter_deps="vulkan spirv_library"
+gblur_vulkan_filter_deps="vulkan spirv_compiler"
 hflip_vulkan_filter_deps="vulkan spirv_compiler"
 histeq_filter_deps="gpl"
 hqdn3d_filter_deps="gpl"
diff --git a/libavfilter/vf_gblur_vulkan.c b/libavfilter/vf_gblur_vulkan.c
index 8ade1c955d..18d65df8c9 100644
--- a/libavfilter/vf_gblur_vulkan.c
+++ b/libavfilter/vf_gblur_vulkan.c
@@ -20,24 +20,24 @@
  */
 
 #include "libavutil/mem.h"
-#include "libavutil/random_seed.h"
 #include "libavutil/opt.h"
-#include "libavutil/vulkan_spirv.h"
 #include "vulkan_filter.h"
 
 #include "filters.h"
 #include "video.h"
 
-#define CGS 32
 #define GBLUR_MAX_KERNEL_SIZE 127
 
+extern const unsigned char ff_gblur_comp_spv_data[];
+extern const unsigned int ff_gblur_comp_spv_len;
+
 typedef struct GBlurVulkanContext {
     FFVulkanContext vkctx;
 
     int initialized;
     FFVkExecPool e;
     AVVulkanDeviceQueueFamily *qf;
-    VkSampler sampler;
+
     FFVulkanShader shd_hor;
     FFVkBuffer params_hor;
     FFVulkanShader shd_ver;
@@ -50,20 +50,6 @@ typedef struct GBlurVulkanContext {
     float sigmaV;
 } GBlurVulkanContext;
 
-static const char gblur_func[] = {
-    C(0, void gblur(const ivec2 pos, const int index)                             )
-    C(0, {                                                                        )
-    C(1,     vec4 sum = imageLoad(input_images[index], pos) * kernel[0];          )
-    C(0,                                                                          )
-    C(1,     for(int i = 1; i < kernel.length(); i++) {                           )
-    C(2,         sum += imageLoad(input_images[index], pos + OFFSET) * kernel[i]; )
-    C(2,         sum += imageLoad(input_images[index], pos - OFFSET) * kernel[i]; )
-    C(1,     }                                                                    )
-    C(0,                                                                          )
-    C(1,     imageStore(output_images[index], pos, sum);                          )
-    C(0, }                                                                        )
-};
-
 static inline float gaussian(float sigma, float x)
 {
     return 1.0 / (sqrt(2.0 * M_PI) * sigma) *
@@ -124,50 +110,23 @@ static av_cold void init_gaussian_params(AVFilterContext *ctx)
 
 static int init_gblur_pipeline(GBlurVulkanContext *s,
                                FFVulkanShader *shd, FFVkBuffer *params_buf,
-                               int ksize, float sigma, FFVkSPIRVCompiler *spv)
+                               int ksize, float sigma)
 {
     int err = 0;
     uint8_t *kernel_mapped;
-    uint8_t *spv_data;
-    size_t spv_len;
-    void *spv_opaque = NULL;
 
-    const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
+    ff_vk_shader_add_push_const(shd, 0, sizeof(int),
+                                VK_SHADER_STAGE_COMPUTE_BIT);
 
-    FFVulkanDescriptorSetBinding buf_desc = {
-        .name        = "data",
+    const FFVulkanDescriptorSetBinding buf_desc = {
         .type        = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
-        .mem_quali   = "readonly",
-        .mem_layout  = "std430",
         .stages      = VK_SHADER_STAGE_COMPUTE_BIT,
-        .buf_content = "float kernel",
-        .buf_elems   = ksize,
     };
+    ff_vk_shader_add_descriptor_set(&s->vkctx, shd, &buf_desc, 1, 1, 0);
 
-    RET(ff_vk_shader_add_descriptor_set(&s->vkctx, shd, &buf_desc, 1, 1, 0));
-
-    GLSLD(   gblur_func                                               );
-    GLSLC(0, void main()                                              );
-    GLSLC(0, {                                                        );
-    GLSLC(1,     ivec2 size;                                          );
-    GLSLC(1,     const ivec2 pos = ivec2(gl_GlobalInvocationID.xy);   );
-    for (int i = 0; i < planes; i++) {
-        GLSLC(0,                                                      );
-        GLSLF(1,  size = imageSize(output_images[%i]);              ,i);
-        GLSLC(1,  if (!IS_WITHIN(pos, size))                          );
-        GLSLC(2,      return;                                         );
-        if (s->planes & (1 << i)) {
-            GLSLF(1,      gblur(pos, %i);                           ,i);
-        } else {
-            GLSLF(1, vec4 res = imageLoad(input_images[%i], pos);   ,i);
-            GLSLF(1, imageStore(output_images[%i], pos, res);       ,i);
-        }
-    }
-    GLSLC(0, }                                                        );
-
-    RET(spv->compile_shader(&s->vkctx, spv, shd, &spv_data, &spv_len, "main",
-                            &spv_opaque));
-    RET(ff_vk_shader_link(&s->vkctx, shd, spv_data, spv_len, "main"));
+    RET(ff_vk_shader_link(&s->vkctx, shd,
+                          ff_gblur_comp_spv_data,
+                          ff_gblur_comp_spv_len, "main"));
 
     RET(ff_vk_shader_register_exec(&s->vkctx, &s->e, shd));
 
@@ -184,8 +143,6 @@ static int init_gblur_pipeline(GBlurVulkanContext *s,
                                         VK_FORMAT_UNDEFINED));
 
 fail:
-    if (spv_opaque)
-        spv->free_shader(spv, &spv_opaque);
     return err;
 }
 
@@ -196,16 +153,6 @@ static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
     FFVulkanContext *vkctx = &s->vkctx;
     const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
 
-    FFVulkanShader *shd;
-    FFVkSPIRVCompiler *spv;
-    FFVulkanDescriptorSetBinding *desc;
-
-    spv = ff_vk_spirv_init();
-    if (!spv) {
-        av_log(ctx, AV_LOG_ERROR, "Unable to initialize SPIR-V compiler!\n");
-        return AVERROR_EXTERNAL;
-    }
-
     s->qf = ff_vk_qf_find(vkctx, VK_QUEUE_COMPUTE_BIT, 0);
     if (!s->qf) {
         av_log(ctx, AV_LOG_ERROR, "Device has no compute queues\n");
@@ -215,63 +162,36 @@ static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
 
     RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, s->qf->num*4, 0, 0, 0, NULL));
 
-    desc = (FFVulkanDescriptorSetBinding []) {
-        {
-            .name       = "input_images",
+    const FFVulkanDescriptorSetBinding desc[] = {
+        { /* input_img */
             .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.input_format, FF_VK_REP_FLOAT),
-            .mem_quali  = "readonly",
-            .dimensions = 2,
-            .elems      = planes,
             .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems      = planes,
         },
-        {
-            .name       = "output_images",
+        { /* output_img */
             .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.output_format, FF_VK_REP_FLOAT),
-            .mem_quali  = "writeonly",
-            .dimensions = 2,
-            .elems      = planes,
             .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems      = planes,
         },
     };
 
     init_gaussian_params(ctx);
 
-    {
-        shd = &s->shd_hor;
-        RET(ff_vk_shader_init(vkctx, shd, "gblur_hor",
-                              VK_SHADER_STAGE_COMPUTE_BIT,
-                              NULL, 0,
-                              32, 1, 1,
-                              0));
+    /* Horizontal */
+    ff_vk_shader_load(&s->shd_hor, VK_SHADER_STAGE_COMPUTE_BIT, NULL,
+                      (uint32_t []) { 32, 1, 1 }, 0);
+    ff_vk_shader_add_descriptor_set(vkctx, &s->shd_hor, desc, 2, 0, 0);
+    RET(init_gblur_pipeline(s, &s->shd_hor, &s->params_hor, s->size, s->sigma));
 
-        RET(ff_vk_shader_add_descriptor_set(vkctx, shd, desc, 2, 0, 0));
-
-        GLSLC(0, #define OFFSET (ivec2(i, 0.0)));
-        RET(init_gblur_pipeline(s, shd, &s->params_hor, s->size, s->sigma, spv));
-    }
-
-    {
-        shd = &s->shd_ver;
-        RET(ff_vk_shader_init(vkctx, shd, "gblur_hor",
-                              VK_SHADER_STAGE_COMPUTE_BIT,
-                              NULL, 0,
-                              1, 32, 1,
-                              0));
-
-        RET(ff_vk_shader_add_descriptor_set(vkctx, shd, desc, 2, 0, 0));
-
-        GLSLC(0, #define OFFSET (ivec2(0.0, i)));
-        RET(init_gblur_pipeline(s, shd, &s->params_ver, s->sizeV, s->sigmaV, spv));
-    }
+    /* Vertical */
+    ff_vk_shader_load(&s->shd_ver, VK_SHADER_STAGE_COMPUTE_BIT, NULL,
+                      (uint32_t []) { 1, 32, 1 }, 0);
+    ff_vk_shader_add_descriptor_set(vkctx, &s->shd_ver, desc, 2, 0, 0);
+    RET(init_gblur_pipeline(s, &s->shd_ver, &s->params_ver, s->sizeV, s->sigmaV));
 
     s->initialized = 1;
 
 fail:
-    if (spv)
-        spv->uninit(&spv);
-
     return err;
 }
 
@@ -298,6 +218,7 @@ static int gblur_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
     AVFilterContext *ctx = link->dst;
     GBlurVulkanContext *s = ctx->priv;
     AVFilterLink *outlink = ctx->outputs[0];
+    const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
 
     out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
     if (!out) {
@@ -316,7 +237,8 @@ static int gblur_vulkan_filter_frame(AVFilterLink *link, AVFrame *in)
 
     RET(ff_vk_filter_process_2pass(&s->vkctx, &s->e,
                                    (FFVulkanShader *[2]){ &s->shd_hor, &s->shd_ver },
-                                   out, tmp, in, VK_NULL_HANDLE, 1, NULL, 0));
+                                   out, tmp, in, VK_NULL_HANDLE,
+                                   planes, &s->planes, sizeof(int)));
 
     err = av_frame_copy_props(out, in);
     if (err < 0)
diff --git a/libavfilter/vulkan/Makefile b/libavfilter/vulkan/Makefile
index f0d737733c..47f3f3b75d 100644
--- a/libavfilter/vulkan/Makefile
+++ b/libavfilter/vulkan/Makefile
@@ -4,6 +4,7 @@ clean::
 OBJS-$(CONFIG_AVGBLUR_VULKAN_FILTER) += vulkan/avgblur.comp.spv.o
 OBJS-$(CONFIG_BWDIF_VULKAN_FILTER) += vulkan/bwdif.comp.spv.o
 OBJS-$(CONFIG_COLOR_VULKAN_FILTER) += vulkan/color.comp.spv.o
+OBJS-$(CONFIG_GBLUR_VULKAN_FILTER) += vulkan/gblur.comp.spv.o
 OBJS-$(CONFIG_SCALE_VULKAN_FILTER) += vulkan/debayer.comp.spv.o
 OBJS-$(CONFIG_FLIP_VULKAN_FILTER) += vulkan/flip.comp.spv.o
 OBJS-$(CONFIG_TRANSPOSE_VULKAN_FILTER) += vulkan/transpose.comp.spv.o
diff --git a/libavfilter/vulkan/gblur.comp.glsl b/libavfilter/vulkan/gblur.comp.glsl
new file mode 100644
index 0000000000..77c337020e
--- /dev/null
+++ b/libavfilter/vulkan/gblur.comp.glsl
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2021-2022 Wu Jianhua <jianhua.wu@intel.com>
+ * Copyright (c) 2026 Lynne <dev@lynne.ee>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma shader_stage(compute)
+
+#extension GL_EXT_shader_image_load_formatted : require
+#extension GL_EXT_scalar_block_layout : require
+#extension GL_EXT_nonuniform_qualifier : require
+
+layout (local_size_x_id = 253, local_size_y_id = 254, local_size_z_id = 255) in;
+
+layout (set = 0, binding = 0) uniform readonly  image2D input_img[];
+layout (set = 0, binding = 1) uniform writeonly image2D output_img[];
+
+layout (set = 1, binding = 0, scalar) readonly buffer kernel_buf {
+    float kernel[];
+};
+
+layout (push_constant, scalar) uniform pushConstants {
+    int planes;
+};
+
+#define P_IDX nonuniformEXT(gl_WorkGroupID.z)
+void main()
+{
+    if (!bool(planes & (1 << gl_WorkGroupID.z)))
+        return;
+
+    ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
+    ivec2 size = imageSize(input_img[P_IDX]);
+    if (any(greaterThanEqual(pos, size)))
+        return;
+
+    vec4 sum = imageLoad(input_img[P_IDX], pos) * kernel[0];
+
+    for(int i = 1; i < kernel.length(); i++) {
+        ivec2 offs = gl_WorkGroupSize.x > gl_WorkGroupSize.y ? ivec2(i, 0) :
+                                                               ivec2(0, i);
+        sum += imageLoad(input_img[P_IDX], pos + offs) * kernel[i];
+        sum += imageLoad(input_img[P_IDX], pos - offs) * kernel[i];
+    }
+
+    imageStore(output_img[P_IDX], pos, sum);
+}
-- 
2.52.0


>From b414c2a7b72cd458b90ca783297eadf07b69232b Mon Sep 17 00:00:00 2001
From: Lynne <dev@lynne.ee>
Date: Thu, 12 Feb 2026 21:57:02 +0100
Subject: [PATCH 7/7] chromaber_vulkan: switch to compile-time SPIR-V
 generation

---
 configure                              |   2 +-
 libavfilter/vf_chromaber_vulkan.c      | 104 ++++---------------------
 libavfilter/vulkan/Makefile            |   1 +
 libavfilter/vulkan/chromaber.comp.glsl |  76 ++++++++++++++++++
 4 files changed, 94 insertions(+), 89 deletions(-)
 create mode 100644 libavfilter/vulkan/chromaber.comp.glsl

diff --git a/configure b/configure
index c124582c44..31a59730aa 100755
--- a/configure
+++ b/configure
@@ -4083,7 +4083,7 @@ bs2b_filter_deps="libbs2b"
 bwdif_cuda_filter_deps="ffnvcodec"
 bwdif_cuda_filter_deps_any="cuda_nvcc cuda_llvm"
 bwdif_vulkan_filter_deps="vulkan spirv_compiler"
-chromaber_vulkan_filter_deps="vulkan spirv_library"
+chromaber_vulkan_filter_deps="vulkan spirv_compiler"
 color_vulkan_filter_deps="vulkan spirv_compiler"
 colorkey_opencl_filter_deps="opencl"
 colormatrix_filter_deps="gpl"
diff --git a/libavfilter/vf_chromaber_vulkan.c b/libavfilter/vf_chromaber_vulkan.c
index 6dd50c1bb4..fa0bc8e300 100644
--- a/libavfilter/vf_chromaber_vulkan.c
+++ b/libavfilter/vf_chromaber_vulkan.c
@@ -20,12 +20,14 @@
 
 #include "libavutil/random_seed.h"
 #include "libavutil/opt.h"
-#include "libavutil/vulkan_spirv.h"
 #include "vulkan_filter.h"
 
 #include "filters.h"
 #include "video.h"
 
+extern const unsigned char ff_chromaber_comp_spv_data[];
+extern const unsigned int ff_chromaber_comp_spv_len;
+
 typedef struct ChromaticAberrationVulkanContext {
     FFVulkanContext vkctx;
 
@@ -38,56 +40,22 @@ typedef struct ChromaticAberrationVulkanContext {
     /* Push constants / options */
     struct {
         float dist[2];
+        uint32_t single_plane;
     } opts;
 } ChromaticAberrationVulkanContext;
 
-static const char distort_chroma_kernel[] = {
-    C(0, void distort_rgb(ivec2 size, ivec2 pos)                               )
-    C(0, {                                                                     )
-    C(1,     const vec2 p = ((vec2(pos)/vec2(size)) - 0.5f)*2.0f;              )
-    C(1,     const vec2 o = p * (dist - 1.0f);                                 )
-    C(0,                                                                       )
-    C(1,     vec4 res;                                                         )
-    C(1,     res.r = texture(input_img[0], ((p - o)/2.0f) + 0.5f).r;           )
-    C(1,     res.g = texture(input_img[0], ((p    )/2.0f) + 0.5f).g;           )
-    C(1,     res.b = texture(input_img[0], ((p + o)/2.0f) + 0.5f).b;           )
-    C(1,     res.a = texture(input_img[0], ((p    )/2.0f) + 0.5f).a;           )
-    C(1,     imageStore(output_img[0], pos, res);                              )
-    C(0, }                                                                     )
-    C(0,                                                                       )
-    C(0, void distort_chroma(int idx, ivec2 size, ivec2 pos)                   )
-    C(0, {                                                                     )
-    C(1,     vec2 p = ((vec2(pos)/vec2(size)) - 0.5f)*2.0f;                    )
-    C(1,     float d = sqrt(p.x*p.x + p.y*p.y);                                )
-    C(1,     p *= d / (d*dist);                                                )
-    C(1,     vec4 res = texture(input_img[idx], (p/2.0f) + 0.5f);              )
-    C(1,     imageStore(output_img[idx], pos, res);                            )
-    C(0, }                                                                     )
-};
-
 static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
 {
     int err;
-    uint8_t *spv_data;
-    size_t spv_len;
-    void *spv_opaque = NULL;
     ChromaticAberrationVulkanContext *s = ctx->priv;
     FFVulkanContext *vkctx = &s->vkctx;
     const int planes = av_pix_fmt_count_planes(s->vkctx.output_format);
     FFVulkanShader *shd = &s->shd;
-    FFVkSPIRVCompiler *spv;
-    FFVulkanDescriptorSetBinding *desc;
 
     /* Normalize options */
     s->opts.dist[0] = (s->opts.dist[0] / 100.0f) + 1.0f;
     s->opts.dist[1] = (s->opts.dist[1] / 100.0f) + 1.0f;
 
-    spv = ff_vk_spirv_init();
-    if (!spv) {
-        av_log(ctx, AV_LOG_ERROR, "Unable to initialize SPIR-V compiler!\n");
-        return AVERROR_EXTERNAL;
-    }
-
     s->qf = ff_vk_qf_find(vkctx, VK_QUEUE_COMPUTE_BIT, 0);
     if (!s->qf) {
         av_log(ctx, AV_LOG_ERROR, "Device has no compute queues\n");
@@ -97,77 +65,37 @@ static av_cold int init_filter(AVFilterContext *ctx, AVFrame *in)
 
     RET(ff_vk_exec_pool_init(vkctx, s->qf, &s->e, s->qf->num*4, 0, 0, 0, NULL));
     RET(ff_vk_init_sampler(vkctx, &s->sampler, 0, VK_FILTER_LINEAR));
-    RET(ff_vk_shader_init(vkctx, &s->shd, "chromatic_abberation",
-                          VK_SHADER_STAGE_COMPUTE_BIT,
-                          NULL, 0,
-                          32, 32, 1,
-                          0));
 
-    GLSLC(0, layout(push_constant, std430) uniform pushConstants {        );
-    GLSLC(1,    vec2 dist;                                                );
-    GLSLC(0, };                                                           );
-    GLSLC(0,                                                              );
+    ff_vk_shader_load(&s->shd, VK_SHADER_STAGE_COMPUTE_BIT, NULL,
+                      (uint32_t []) { 32, 32, 1 }, 0);
 
     ff_vk_shader_add_push_const(&s->shd, 0, sizeof(s->opts),
                                 VK_SHADER_STAGE_COMPUTE_BIT);
 
-    desc = (FFVulkanDescriptorSetBinding []) {
-        {
-            .name       = "input_img",
+    const FFVulkanDescriptorSetBinding desc[] = {
+        { /* input_img */
             .type       = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
-            .dimensions = 2,
-            .elems      = planes,
             .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
-            .samplers   = DUP_SAMPLER(s->sampler),
+            .elems      = planes,
         },
-        {
-            .name       = "output_img",
+        { /* output_img */
             .type       = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
-            .mem_layout = ff_vk_shader_rep_fmt(s->vkctx.output_format, FF_VK_REP_FLOAT),
-            .mem_quali  = "writeonly",
-            .dimensions = 2,
-            .elems      = planes,
             .stages     = VK_SHADER_STAGE_COMPUTE_BIT,
+            .elems      = planes,
         },
     };
+    ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc, 2, 0, 0);
 
-    RET(ff_vk_shader_add_descriptor_set(vkctx, &s->shd, desc, 2, 0, 0));
-
-    GLSLD(   distort_chroma_kernel                                        );
-    GLSLC(0, void main()                                                  );
-    GLSLC(0, {                                                            );
-    GLSLC(1,     ivec2 pos = ivec2(gl_GlobalInvocationID.xy);             );
-    if (planes == 1) {
-        GLSLC(1, distort_rgb(imageSize(output_img[0]), pos);              );
-    } else {
-        GLSLC(1, ivec2 size = imageSize(output_img[0]);                   );
-        GLSLC(1, vec2 npos = vec2(pos)/vec2(size);                        );
-        GLSLC(1, vec4 res = texture(input_img[0], npos);                  );
-        GLSLC(1, imageStore(output_img[0], pos, res);                     );
-        for (int i = 1; i < planes; i++) {
-            GLSLC(0,                                                      );
-            GLSLF(1,  size = imageSize(output_img[%i]);                 ,i);
-            GLSLC(1,  if (!IS_WITHIN(pos, size))                          );
-            GLSLC(2,      return;                                         );
-            GLSLF(1,  distort_chroma(%i, size, pos);                    ,i);
-        }
-    }
-    GLSLC(0, }                                                            );
-
-    RET(spv->compile_shader(vkctx, spv, shd, &spv_data, &spv_len, "main",
-                            &spv_opaque));
-    RET(ff_vk_shader_link(vkctx, shd, spv_data, spv_len, "main"));
+    RET(ff_vk_shader_link(vkctx, shd,
+                          ff_chromaber_comp_spv_data,
+                          ff_chromaber_comp_spv_len, "main"));
 
     RET(ff_vk_shader_register_exec(vkctx, &s->e, &s->shd));
 
+    s->opts.single_plane = planes == 1;
     s->initialized = 1;
 
 fail:
-    if (spv_opaque)
-        spv->free_shader(spv, &spv_opaque);
-    if (spv)
-        spv->uninit(&spv);
-
     return err;
 }
 
diff --git a/libavfilter/vulkan/Makefile b/libavfilter/vulkan/Makefile
index 47f3f3b75d..82b3dff054 100644
--- a/libavfilter/vulkan/Makefile
+++ b/libavfilter/vulkan/Makefile
@@ -3,6 +3,7 @@ clean::
 
 OBJS-$(CONFIG_AVGBLUR_VULKAN_FILTER) += vulkan/avgblur.comp.spv.o
 OBJS-$(CONFIG_BWDIF_VULKAN_FILTER) += vulkan/bwdif.comp.spv.o
+OBJS-$(CONFIG_CHROMABER_VULKAN_FILTER) += vulkan/chromaber.comp.spv.o
 OBJS-$(CONFIG_COLOR_VULKAN_FILTER) += vulkan/color.comp.spv.o
 OBJS-$(CONFIG_GBLUR_VULKAN_FILTER) += vulkan/gblur.comp.spv.o
 OBJS-$(CONFIG_SCALE_VULKAN_FILTER) += vulkan/debayer.comp.spv.o
diff --git a/libavfilter/vulkan/chromaber.comp.glsl b/libavfilter/vulkan/chromaber.comp.glsl
new file mode 100644
index 0000000000..d2e4e60f12
--- /dev/null
+++ b/libavfilter/vulkan/chromaber.comp.glsl
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2026 Lynne <dev@lynne.ee>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#pragma shader_stage(compute)
+
+#extension GL_EXT_shader_image_load_formatted : require
+#extension GL_EXT_scalar_block_layout : require
+#extension GL_EXT_nonuniform_qualifier : require
+
+layout (local_size_x_id = 253, local_size_y_id = 254, local_size_z_id = 255) in;
+
+layout (set = 0, binding = 0) uniform sampler2D input_img[];
+layout (set = 0, binding = 1) uniform writeonly image2D output_img[];
+
+layout (push_constant, scalar) uniform pushConstants {
+    vec2 dist;
+    bool single_plane;
+};
+
+void distort_rgba(ivec2 pos, ivec2 size)
+{
+    const vec2 p = ((vec2(pos)/vec2(size)) - 0.5f)*2.0f;
+    const vec2 o = p * (dist - 1.0f);
+
+    vec4 res;
+    res.r = texture(input_img[0], ((p - o)/2.0f) + 0.5f).r;
+    res.g = texture(input_img[0], ((p    )/2.0f) + 0.5f).g;
+    res.b = texture(input_img[0], ((p + o)/2.0f) + 0.5f).b;
+    res.a = texture(input_img[0], ((p    )/2.0f) + 0.5f).a;
+    imageStore(output_img[0], pos, res);
+}
+
+void distort_plane(ivec2 pos, ivec2 size)
+{
+    vec2 p = ((vec2(pos)/vec2(size)) - 0.5f)*2.0f;
+    float d = sqrt(p.x*p.x + p.y*p.y);
+    p *= d / (d*dist);
+    vec4 res = texture(input_img[nonuniformEXT(gl_WorkGroupID.z)], (p/2.0f) + 0.5f);
+    imageStore(output_img[nonuniformEXT(gl_WorkGroupID.z)], pos, res);
+}
+
+void main()
+{
+    ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
+    ivec2 size = imageSize(output_img[nonuniformEXT(gl_WorkGroupID.z)]);
+    if (any(greaterThanEqual(pos, size)))
+        return;
+
+    if (single_plane) {
+        distort_rgba(pos, pos);
+    } else {
+        vec2 npos = vec2(pos)/vec2(size);
+        vec4 res = texture(input_img[0], npos);
+        if (gl_WorkGroupID.z == 0)
+            imageStore(output_img[0], pos, res);
+        else
+            distort_plane(pos, size);
+    }
+}
-- 
2.52.0

_______________________________________________
ffmpeg-devel mailing list -- ffmpeg-devel@ffmpeg.org
To unsubscribe send an email to ffmpeg-devel-leave@ffmpeg.org

                 reply	other threads:[~2026-02-19 22:40 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=177154079595.25.11008091249156812316@29965ddac10e \
    --to=ffmpeg-devel@ffmpeg.org \
    --cc=code@ffmpeg.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link

Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

This inbox may be cloned and mirrored by anyone:

	git clone --mirror https://master.gitmailbox.com/ffmpegdev/0 ffmpegdev/git/0.git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V2 ffmpegdev ffmpegdev/ https://master.gitmailbox.com/ffmpegdev \
		ffmpegdev@gitmailbox.com
	public-inbox-index ffmpegdev

Example config snippet for mirrors.


AGPL code for this site: git clone https://public-inbox.org/public-inbox.git