From: pocopepe <[email protected]>

---
 libavfilter/Makefile          |   1 +
 libavfilter/allfilters.c      |   1 +
 libavfilter/vf_scale_webgpu.c | 329 ++++++++++++++++++++++++++++++++++
 3 files changed, 331 insertions(+)
 create mode 100644 libavfilter/vf_scale_webgpu.c

diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index a530cfae29..a91dffd9c0 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -480,6 +480,7 @@ OBJS-$(CONFIG_SCALE_QSV_FILTER)              += vf_vpp_qsv.o
 OBJS-$(CONFIG_SCALE_VAAPI_FILTER)            += vf_scale_vaapi.o scale_eval.o 
vaapi_vpp.o
 OBJS-$(CONFIG_SCALE_VT_FILTER)               += vf_scale_vt.o scale_eval.o
 OBJS-$(CONFIG_SCALE_VULKAN_FILTER)           += vf_scale_vulkan.o vulkan.o 
vulkan_filter.o
+OBJS-$(CONFIG_SCALE_WEBGPU_FILTER)          += vf_scale_webgpu.o scale_eval.o
 OBJS-$(CONFIG_SCALE2REF_FILTER)              += vf_scale.o scale_eval.o 
framesync.o
 OBJS-$(CONFIG_SCALE2REF_NPP_FILTER)          += vf_scale_npp.o scale_eval.o
 OBJS-$(CONFIG_SCDET_FILTER)                  += vf_scdet.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index e26859e159..84f798e001 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -451,6 +451,7 @@ extern const FFFilter ff_vf_scale_qsv;
 extern const FFFilter ff_vf_scale_vaapi;
 extern const FFFilter ff_vf_scale_vt;
 extern const FFFilter ff_vf_scale_vulkan;
+extern const FFFilter ff_vf_scale_webgpu;
 extern const FFFilter ff_vf_scale2ref;
 extern const FFFilter ff_vf_scale2ref_npp;
 extern const FFFilter ff_vf_scdet;
diff --git a/libavfilter/vf_scale_webgpu.c b/libavfilter/vf_scale_webgpu.c
new file mode 100644
index 0000000000..14f8ddd8ac
--- /dev/null
+++ b/libavfilter/vf_scale_webgpu.c
@@ -0,0 +1,329 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <webgpu/webgpu.h>
+
+#include "libavutil/hwcontext.h"
+#include "libavutil/hwcontext_webgpu.h"
+#include "libavutil/opt.h"
+#include "filters.h"
+#include "scale_eval.h"
+#include "video.h"
+
+#define WORKGROUP_SIZE 8
+
+static const char wgsl_scale_bilinear[] =
+    "@group(0) @binding(0) var input_tex     : texture_2d<f32>;\n"
+    "@group(0) @binding(1) var input_sampler : sampler;\n"
+    "@group(0) @binding(2) var output_tex    : texture_storage_2d<rgba8unorm, 
write>;\n"
+    "\n"
+    "@compute @workgroup_size(" AV_STRINGIFY(WORKGROUP_SIZE) ", " 
AV_STRINGIFY(WORKGROUP_SIZE) ")\n"
+    "fn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n"
+    "    let out_size = textureDimensions(output_tex);\n"
+    "    let pos      = vec2<u32>(gid.x, gid.y);\n"
+    "    if (pos.x >= out_size.x || pos.y >= out_size.y) { return; }\n"
+    "    let uv    = (vec2<f32>(pos) + 0.5) / vec2<f32>(out_size);\n"
+    "    let color = textureSampleLevel(input_tex, input_sampler, uv, 0.0);\n"
+    "    textureStore(output_tex, pos, color);\n"
+    "}\n";
+
+typedef struct ScaleWebGPUContext {
+    char *w_expr;
+    char *h_expr;
+
+    int initialized;
+    WGPUShaderModule        shader_module;
+    WGPUBindGroupLayout     bind_group_layout;
+    WGPUPipelineLayout      pipeline_layout;
+    WGPUComputePipeline     pipeline;
+    WGPUSampler             sampler;
+
+    int out_width;
+    int out_height;
+} ScaleWebGPUContext;
+
+static av_cold void scale_webgpu_uninit(AVFilterContext *avctx)
+{
+    ScaleWebGPUContext *s = avctx->priv;
+    if (s->pipeline)         wgpuComputePipelineRelease(s->pipeline);
+    if (s->pipeline_layout)  wgpuPipelineLayoutRelease(s->pipeline_layout);
+    if (s->bind_group_layout) wgpuBindGroupLayoutRelease(s->bind_group_layout);
+    if (s->sampler)          wgpuSamplerRelease(s->sampler);
+    if (s->shader_module)    wgpuShaderModuleRelease(s->shader_module);
+}
+
+static int init_pipeline(AVFilterContext *avctx, WGPUDevice device)
+{
+    ScaleWebGPUContext *s = avctx->priv;
+
+    WGPUShaderSourceWGSL wgsl_src = {
+        .chain = { .sType = WGPUSType_ShaderSourceWGSL },
+        .code  = { .data = wgsl_scale_bilinear,
+                   .length = strlen(wgsl_scale_bilinear) },
+    };
+    WGPUShaderModuleDescriptor shader_desc = {
+        .nextInChain = &wgsl_src.chain,
+    };
+    s->shader_module = wgpuDeviceCreateShaderModule(device, &shader_desc);
+    if (!s->shader_module) {
+        av_log(avctx, AV_LOG_ERROR, "Failed to create WGSL shader module.\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    WGPUBindGroupLayoutEntry bgl_entries[] = {
+        {
+            .binding    = 0,
+            .visibility = WGPUShaderStage_Compute,
+            .texture    = {
+                .sampleType    = WGPUTextureSampleType_Float,
+                .viewDimension = WGPUTextureViewDimension_2D,
+            },
+        },
+        {
+            .binding    = 1,
+            .visibility = WGPUShaderStage_Compute,
+            .sampler    = { .type = WGPUSamplerBindingType_Filtering },
+        },
+        {
+            .binding        = 2,
+            .visibility     = WGPUShaderStage_Compute,
+            .storageTexture = {
+                .access        = WGPUStorageTextureAccess_WriteOnly,
+                .format        = WGPUTextureFormat_RGBA8Unorm,
+                .viewDimension = WGPUTextureViewDimension_2D,
+            },
+        },
+    };
+    WGPUBindGroupLayoutDescriptor bgl_desc = {
+        .entryCount = FF_ARRAY_ELEMS(bgl_entries),
+        .entries    = bgl_entries,
+    };
+    s->bind_group_layout = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
+    if (!s->bind_group_layout) {
+        av_log(avctx, AV_LOG_ERROR, "Failed to create bind group layout.\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    WGPUPipelineLayoutDescriptor pl_desc = {
+        .bindGroupLayoutCount = 1,
+        .bindGroupLayouts     = &s->bind_group_layout,
+    };
+    s->pipeline_layout = wgpuDeviceCreatePipelineLayout(device, &pl_desc);
+    if (!s->pipeline_layout) {
+        av_log(avctx, AV_LOG_ERROR, "Failed to create pipeline layout.\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    WGPUSamplerDescriptor sampler_desc = {
+        .magFilter = WGPUFilterMode_Linear,
+        .minFilter = WGPUFilterMode_Linear,
+        .addressModeU = WGPUAddressMode_ClampToEdge,
+        .addressModeV = WGPUAddressMode_ClampToEdge,
+    };
+    s->sampler = wgpuDeviceCreateSampler(device, &sampler_desc);
+    if (!s->sampler) {
+        av_log(avctx, AV_LOG_ERROR, "Failed to create sampler.\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    WGPUComputePipelineDescriptor cp_desc = {
+        .layout  = s->pipeline_layout,
+        .compute = {
+            .module     = s->shader_module,
+            .entryPoint = { .data = "main", .length = 4 },
+        },
+    };
+    s->pipeline = wgpuDeviceCreateComputePipeline(device, &cp_desc);
+    if (!s->pipeline) {
+        av_log(avctx, AV_LOG_ERROR, "Failed to create compute pipeline.\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    s->initialized = 1;
+    return 0;
+}
+
+static int scale_webgpu_filter_frame(AVFilterLink *inlink, AVFrame *in)
+{
+    AVFilterContext      *avctx  = inlink->dst;
+    ScaleWebGPUContext   *s      = avctx->priv;
+    AVFilterLink         *outlink = avctx->outputs[0];
+    AVHWFramesContext    *in_hwfc = (AVHWFramesContext 
*)inlink->hw_frames_ctx->data;
+    AVWebGPUDeviceContext *wgpu   = in_hwfc->device_ctx->hwctx;
+    AVWebGPUFrame        *in_f   = (AVWebGPUFrame *)in->data[0];
+    int ret;
+
+    if (!s->initialized) {
+        ret = init_pipeline(avctx, wgpu->device);
+        if (ret < 0)
+            goto fail;
+    }
+
+    AVFrame *out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
+    if (!out) { ret = AVERROR(ENOMEM); goto fail; }
+
+    AVWebGPUFrame *out_f = (AVWebGPUFrame *)out->data[0];
+
+    WGPUBindGroupEntry bg_entries[] = {
+        { .binding = 0, .textureView = in_f->view  },
+        { .binding = 1, .sampler     = s->sampler  },
+        { .binding = 2, .textureView = out_f->view },
+    };
+    WGPUBindGroupDescriptor bg_desc = {
+        .layout     = s->bind_group_layout,
+        .entryCount = FF_ARRAY_ELEMS(bg_entries),
+        .entries    = bg_entries,
+    };
+    WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(wgpu->device, 
&bg_desc);
+    if (!bind_group) {
+        av_log(avctx, AV_LOG_ERROR, "Failed to create bind group.\n");
+        av_frame_free(&out);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    WGPUCommandEncoder encoder = wgpuDeviceCreateCommandEncoder(wgpu->device, 
NULL);
+    WGPUComputePassEncoder pass = wgpuCommandEncoderBeginComputePass(encoder, 
NULL);
+
+    wgpuComputePassEncoderSetPipeline(pass, s->pipeline);
+    wgpuComputePassEncoderSetBindGroup(pass, 0, bind_group, 0, NULL);
+    wgpuComputePassEncoderDispatchWorkgroups(
+        pass,
+        (outlink->w + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE,
+        (outlink->h + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE,
+        1);
+    wgpuComputePassEncoderEnd(pass);
+    wgpuComputePassEncoderRelease(pass);
+
+    WGPUCommandBuffer commands = wgpuCommandEncoderFinish(encoder, NULL);
+    wgpuQueueSubmit(wgpu->queue, 1, &commands);
+    wgpuCommandBufferRelease(commands);
+    wgpuCommandEncoderRelease(encoder);
+    wgpuBindGroupRelease(bind_group);
+
+    ret = av_frame_copy_props(out, in);
+    if (ret < 0) { av_frame_free(&out); goto fail; }
+
+    out->width  = outlink->w;
+    out->height = outlink->h;
+    if (out->width != in->width || out->height != in->height)
+        av_frame_side_data_remove_by_props(&out->side_data, &out->nb_side_data,
+                                           AV_SIDE_DATA_PROP_SIZE_DEPENDENT);
+
+    av_frame_free(&in);
+    return ff_filter_frame(outlink, out);
+
+fail:
+    av_frame_free(&in);
+    return ret;
+}
+
+static int scale_webgpu_config_output(AVFilterLink *outlink)
+{
+    AVFilterContext   *avctx  = outlink->src;
+    ScaleWebGPUContext *s     = avctx->priv;
+    AVFilterLink      *inlink = avctx->inputs[0];
+    FilterLink        *inl    = ff_filter_link(inlink);
+    FilterLink        *outl   = ff_filter_link(outlink);
+    AVHWFramesContext *in_hwfc, *out_hwfc;
+    AVBufferRef       *out_frames_ref;
+    int ret;
+
+    if (!inl->hw_frames_ctx) {
+        av_log(avctx, AV_LOG_ERROR, "No hw_frames_ctx on input.\n");
+        return AVERROR(EINVAL);
+    }
+    in_hwfc = (AVHWFramesContext *)inl->hw_frames_ctx->data;
+    if (in_hwfc->format != AV_PIX_FMT_WEBGPU) {
+        av_log(avctx, AV_LOG_ERROR, "Input must be AV_PIX_FMT_WEBGPU.\n");
+        return AVERROR(EINVAL);
+    }
+
+    ret = ff_scale_eval_dimensions(s, s->w_expr, s->h_expr, inlink, outlink,
+                                   &s->out_width, &s->out_height);
+    if (ret < 0)
+        return ret;
+
+    ff_scale_adjust_dimensions(inlink, &s->out_width, &s->out_height,
+                               SCALE_FORCE_OAR_DISABLE, 1, 1.f);
+
+    outlink->w = s->out_width;
+    outlink->h = s->out_height;
+
+    out_frames_ref = av_hwframe_ctx_alloc(in_hwfc->device_ref);
+    if (!out_frames_ref)
+        return AVERROR(ENOMEM);
+
+    out_hwfc               = (AVHWFramesContext *)out_frames_ref->data;
+    out_hwfc->format       = AV_PIX_FMT_WEBGPU;
+    out_hwfc->sw_format    = AV_PIX_FMT_RGBA;
+    out_hwfc->width        = s->out_width;
+    out_hwfc->height       = s->out_height;
+    out_hwfc->initial_pool_size = 4;
+
+    ret = av_hwframe_ctx_init(out_frames_ref);
+    if (ret < 0) {
+        av_buffer_unref(&out_frames_ref);
+        return ret;
+    }
+
+    av_buffer_unref(&outl->hw_frames_ctx);
+    outl->hw_frames_ctx = out_frames_ref;
+
+    return 0;
+}
+
+#define OFFSET(x) offsetof(ScaleWebGPUContext, x)
+#define FLAGS (AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM)
+
+static const AVOption scale_webgpu_options[] = {
+    { "w", "Output video width",  OFFSET(w_expr), AV_OPT_TYPE_STRING, { .str = 
"iw" }, .flags = FLAGS },
+    { "h", "Output video height", OFFSET(h_expr), AV_OPT_TYPE_STRING, { .str = 
"ih" }, .flags = FLAGS },
+    { NULL },
+};
+
+AVFILTER_DEFINE_CLASS(scale_webgpu);
+
+static const AVFilterPad scale_webgpu_inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .filter_frame = scale_webgpu_filter_frame,
+    },
+};
+
+static const AVFilterPad scale_webgpu_outputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .config_props = scale_webgpu_config_output,
+    },
+};
+
+const FFFilter ff_vf_scale_webgpu = {
+    .p.name        = "scale_webgpu",
+    .p.description = NULL_IF_CONFIG_SMALL("Scale video using WebGPU 
(bilinear)"),
+    .p.priv_class  = &scale_webgpu_class,
+    .p.flags       = AVFILTER_FLAG_HWDEVICE,
+    .priv_size     = sizeof(ScaleWebGPUContext),
+    .uninit        = scale_webgpu_uninit,
+    FILTER_INPUTS(scale_webgpu_inputs),
+    FILTER_OUTPUTS(scale_webgpu_outputs),
+    FILTER_SINGLE_PIXFMT(AV_PIX_FMT_WEBGPU),
+    .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
+};
-- 
2.53.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to