diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 56d33e6480..39d98881bb 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -283,6 +283,7 @@ OBJS-$(CONFIG_FIELDHINT_FILTER)              += vf_fieldhint.o
 OBJS-$(CONFIG_FIELDMATCH_FILTER)             += vf_fieldmatch.o
 OBJS-$(CONFIG_FIELDORDER_FILTER)             += vf_fieldorder.o
 OBJS-$(CONFIG_FILLBORDERS_FILTER)            += vf_fillborders.o
+OBJS-$(CONFIG_FILMGRAIN_FILTER)              += vf_filmgrain.o
 OBJS-$(CONFIG_FIND_RECT_FILTER)              += vf_find_rect.o lavfutils.o
 OBJS-$(CONFIG_FLOODFILL_FILTER)              += vf_floodfill.o
 OBJS-$(CONFIG_FORMAT_FILTER)                 += vf_format.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index f5caee3a62..20e889880f 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -266,6 +266,7 @@ extern const AVFilter ff_vf_fieldhint;
 extern const AVFilter ff_vf_fieldmatch;
 extern const AVFilter ff_vf_fieldorder;
 extern const AVFilter ff_vf_fillborders;
+extern const AVFilter ff_vf_filmgrain;
 extern const AVFilter ff_vf_find_rect;
 extern const AVFilter ff_vf_flip_vulkan;
 extern const AVFilter ff_vf_floodfill;
diff --git a/libavfilter/vf_filmgrain.c b/libavfilter/vf_filmgrain.c
new file mode 100644
index 0000000000..c87118dad0
--- /dev/null
+++ b/libavfilter/vf_filmgrain.c
@@ -0,0 +1,489 @@
+/*
+ * 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 <float.h>
+
+#include "libavutil/opt.h"
+#include "libavutil/imgutils.h"
+#include "libavutil/parseutils.h"
+#include "libavutil/pixdesc.h"
+#include "libavutil/random_seed.h"
+#include "avfilter.h"
+#include "formats.h"
+#include "internal.h"
+#include "video.h"
+
+#define STRETCH_3D (-1.f / 6.f)
+#define SQUISH_3D  (1.f / 3.f)
+#define NORM_3D    (1.f / 103.f)
+
+typedef struct Contribution3 {
+    float dx, dy, dz;
+    int xsb, ysb, zsb;
+    struct Contribution3 *next;
+} Contribution3;
+
+typedef struct OpenSimplexNoise {
+    uint8_t perm[256];
+    uint8_t perm3D[256];
+
+    Contribution3 *lookup3D[2048];
+    Contribution3 *contributions3D[24];
+} OpenSimplexNoise;
+
+typedef struct ThreadData {
+    AVFrame *in, *out;
+    int plane;
+    float strength;
+} ThreadData;
+
+typedef struct FilmGrainContext {
+    const AVClass *class;
+
+    int depth;
+    int nb_planes;
+    int linesize[4];
+    int planewidth[4];
+    int planeheight[4];
+
+    float size;
+    float speed;
+    float strength;
+    float bias;
+    int planes;
+
+    int64_t seed[4];
+
+    int (*grain_plane_slice)(AVFilterContext *ctx, ThreadData *td, int jobnr, int nb_jobs, int p, int max);
+
+    OpenSimplexNoise osn[4];
+} FilmGrainContext;
+
+#define OFFSET(x) offsetof(FilmGrainContext, x)
+#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_RUNTIME_PARAM
+
+static const AVOption filmgrain_options[] = {
+    { "size",     "set grain size",        OFFSET(size),     AV_OPT_TYPE_FLOAT, {.dbl=1600},20, 6400,FLAGS },
+    { "strength", "set grain strength",    OFFSET(strength), AV_OPT_TYPE_FLOAT, {.dbl=.25}, 0,  1,   FLAGS },
+    { "speed",    "set grain change speed",OFFSET(speed),    AV_OPT_TYPE_FLOAT, {.dbl=1},   0,  10,  FLAGS },
+    { "bias",     "set grain bias",        OFFSET(bias),     AV_OPT_TYPE_FLOAT, {.dbl=0},  -1,  1,   FLAGS },
+    { "planes",   "set planes to filter",  OFFSET(planes),   AV_OPT_TYPE_INT,   {.i64=1},   0,  0xF, FLAGS },
+    { "0seed",    "set random seed #0",    OFFSET(seed[0]),  AV_OPT_TYPE_INT64, {.i64=-1}, -1,  UINT_MAX, FLAGS },
+    { "1seed",    "set random seed #1",    OFFSET(seed[1]),  AV_OPT_TYPE_INT64, {.i64=-1}, -1,  UINT_MAX, FLAGS },
+    { "2seed",    "set random seed #2",    OFFSET(seed[2]),  AV_OPT_TYPE_INT64, {.i64=-1}, -1,  UINT_MAX, FLAGS },
+    { "3seed",    "set random seed #3",    OFFSET(seed[3]),  AV_OPT_TYPE_INT64, {.i64=-1}, -1,  UINT_MAX, FLAGS },
+    { NULL }
+};
+
+AVFILTER_DEFINE_CLASS(filmgrain);
+
+static const enum AVPixelFormat pixel_fmts[] = {
+    AV_PIX_FMT_GRAY8, AV_PIX_FMT_GRAY9,
+    AV_PIX_FMT_GRAY10, AV_PIX_FMT_GRAY12, AV_PIX_FMT_GRAY14,
+    AV_PIX_FMT_GRAY16,
+    AV_PIX_FMT_GRAYF32,
+    AV_PIX_FMT_YUV410P, AV_PIX_FMT_YUV411P,
+    AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P,
+    AV_PIX_FMT_YUV440P, AV_PIX_FMT_YUV444P,
+    AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P,
+    AV_PIX_FMT_YUVJ440P, AV_PIX_FMT_YUVJ444P,
+    AV_PIX_FMT_YUVJ411P,
+    AV_PIX_FMT_YUV420P9, AV_PIX_FMT_YUV422P9, AV_PIX_FMT_YUV444P9,
+    AV_PIX_FMT_YUV420P10, AV_PIX_FMT_YUV422P10, AV_PIX_FMT_YUV444P10,
+    AV_PIX_FMT_YUV440P10,
+    AV_PIX_FMT_YUV444P12, AV_PIX_FMT_YUV422P12, AV_PIX_FMT_YUV420P12,
+    AV_PIX_FMT_YUV440P12,
+    AV_PIX_FMT_YUV444P14, AV_PIX_FMT_YUV422P14, AV_PIX_FMT_YUV420P14,
+    AV_PIX_FMT_YUV420P16, AV_PIX_FMT_YUV422P16, AV_PIX_FMT_YUV444P16,
+    AV_PIX_FMT_GBRP, AV_PIX_FMT_GBRP9, AV_PIX_FMT_GBRP10,
+    AV_PIX_FMT_GBRP12, AV_PIX_FMT_GBRP14, AV_PIX_FMT_GBRP16,
+    AV_PIX_FMT_YUVA420P,  AV_PIX_FMT_YUVA422P,   AV_PIX_FMT_YUVA444P,
+    AV_PIX_FMT_YUVA444P9, AV_PIX_FMT_YUVA444P10, AV_PIX_FMT_YUVA444P12, AV_PIX_FMT_YUVA444P16,
+    AV_PIX_FMT_YUVA422P9, AV_PIX_FMT_YUVA422P10, AV_PIX_FMT_YUVA422P12, AV_PIX_FMT_YUVA422P16,
+    AV_PIX_FMT_YUVA420P9, AV_PIX_FMT_YUVA420P10, AV_PIX_FMT_YUVA420P16,
+    AV_PIX_FMT_GBRAP,     AV_PIX_FMT_GBRAP10,    AV_PIX_FMT_GBRAP12,    AV_PIX_FMT_GBRAP16,
+    AV_PIX_FMT_GBRPF32, AV_PIX_FMT_GBRAPF32,
+    AV_PIX_FMT_NONE
+};
+
+static void init_noise(OpenSimplexNoise *n, int64_t seed)
+{
+    int8_t source[256];
+
+    for (int i = 0; i < 256; i++)
+        source[i] = i;
+
+    seed = seed * 6364136223846793005LL + 1442695040888963407LL;
+    seed = seed * 6364136223846793005LL + 1442695040888963407LL;
+    seed = seed * 6364136223846793005LL + 1442695040888963407LL;
+
+    for (int i = 255; i >= 0; i--) {
+        seed = seed * 6364136223846793005LL + 1442695040888963407LL;
+        int r = (int)((seed + 31) % (i + 1));
+        if (r < 0)
+            r += (i + 1);
+        n->perm[i] = source[r];
+        n->perm3D[i] = (uint8_t)((n->perm[i] % 24) * 3);
+        source[r] = source[i];
+    }
+}
+
+static const uint8_t base3D[][24] =
+{
+    { 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1 },
+    { 2, 1, 1, 0, 2, 1, 0, 1, 2, 0, 1, 1, 3, 1, 1, 1 },
+    { 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 2, 1, 1, 0, 2, 1, 0, 1, 2, 0, 1, 1 }
+};
+
+static const int8_t p3D[] =
+{
+     0, 0, 1, -1, 0, 0, 1, 0, -1, 0, 0, -1, 1, 0, 0, 0, 1,
+    -1, 0, 0, -1, 0, 1, 0, 0, -1, 1, 0, 2, 1, 1, 0, 1, 1,
+     1, -1, 0, 2, 1, 0, 1, 1, 1, -1, 1, 0, 2, 0, 1, 1, 1,
+    -1, 1, 1, 1, 3, 2, 1, 0, 3, 1, 2, 0, 1, 3, 2, 0, 1, 3,
+     1, 0, 2, 1, 3, 0, 2, 1, 3, 0, 1, 2, 1, 1, 1, 0, 0, 2,
+     2, 0, 0, 1, 1, 0, 1, 0, 2, 0, 2, 0, 1, 1, 0, 0, 1, 2,
+     0, 0, 2, 2, 0, 0, 0, 0, 1, 1, -1, 1, 2, 0, 0, 0, 0, 1,
+     -1, 1, 1, 2, 0, 0, 0, 0, 1, 1, 1, -1, 2, 3, 1, 1, 1, 2,
+     0, 0, 2, 2, 3, 1, 1, 1, 2, 2, 0, 0, 2, 3, 1, 1, 1, 2, 0,
+     2, 0, 2, 1, 1, -1, 1, 2, 0, 0, 2, 2, 1, 1, -1, 1, 2, 2,
+     0, 0, 2, 1, -1, 1, 1, 2, 0, 0, 2, 2, 1, -1, 1, 1, 2, 0,
+     2, 0, 2, 1, 1, 1, -1, 2, 2, 0, 0, 2, 1, 1, 1, -1, 2, 0, 2, 0,
+};
+
+static const uint16_t lookupPairs3D[] =
+{
+    0, 2, 1, 1, 2, 2, 5, 1, 6, 0, 7, 0, 32, 2, 34, 2, 129, 1, 133, 1,
+    160, 5, 161, 5, 518, 0, 519, 0, 546, 4, 550, 4, 645, 3, 647, 3,
+    672, 5, 673, 5, 674, 4, 677, 3, 678, 4, 679, 3, 680, 13, 681, 13,
+    682, 12, 685, 14, 686, 12, 687, 14, 712, 20, 714, 18, 809, 21, 813,
+    23, 840, 20, 841, 21, 1198, 19, 1199, 22, 1226, 18, 1230, 19, 1325,
+    23, 1327, 22, 1352, 15, 1353, 17, 1354, 15, 1357, 17, 1358, 16, 1359,
+    16, 1360, 11, 1361, 10, 1362, 11, 1365, 10, 1366, 9, 1367, 9, 1392,
+    11, 1394, 11, 1489, 10, 1493, 10, 1520, 8, 1521, 8, 1878, 9, 1879, 9,
+    1906, 7, 1910, 7, 2005, 6, 2007, 6, 2032, 8, 2033, 8, 2034, 7, 2037,
+    6, 2038, 7, 2039, 6,
+};
+
+/*
+ * Gradients for 3D. They approximate the directions to the
+ * vertices of a rhombicuboctahedron from the center, skewed so
+ * that the triangular and square facets can be inscribed inside
+ * circles of the same radius.
+ */
+static const int8_t gradients3D[] = {
+    -11,  4,  4, -4,  11,  4, 4,  4,  11,
+     11,  4,  4,  4,  11,  4, 4,  4,  11,
+    -11, -4,  4, -4, -11,  4, 4, -4,  11,
+     11, -4,  4,  4, -11,  4, 4, -4,  11,
+    -11,  4, -4, -4,  11, -4, 4,  4, -11,
+     11,  4, -4,  4,  11, -4, 4,  4, -11,
+    -11, -4, -4, -4, -11, -4, 4, -4, -11,
+     11, -4, -4,  4, -11, -4, 4, -4, -11,
+};
+
+static void EContribution3(Contribution3 *c, float multiplier, int xsb, int ysb, int zsb)
+{
+    c->xsb = xsb;
+    c->ysb = ysb;
+    c->zsb = zsb;
+
+    c->dx = -xsb - multiplier * SQUISH_3D;
+    c->dy = -ysb - multiplier * SQUISH_3D;
+    c->dz = -zsb - multiplier * SQUISH_3D;
+}
+
+static float evaluate(const OpenSimplexNoise *const n, float x, float y, float z)
+{
+    float stretchOffset = (x + y + z) * STRETCH_3D;
+    float xs = x + stretchOffset;
+    float ys = y + stretchOffset;
+    float zs = z + stretchOffset;
+
+    int xsb = floorf(xs);
+    int ysb = floorf(ys);
+    int zsb = floorf(zs);
+
+    float squishOffset = (xsb + ysb + zsb) * SQUISH_3D;
+    float dx0 = x - (xsb + squishOffset);
+    float dy0 = y - (ysb + squishOffset);
+    float dz0 = z - (zsb + squishOffset);
+
+    float xins = xs - xsb;
+    float yins = ys - ysb;
+    float zins = zs - zsb;
+
+    float inSum = xins + yins + zins;
+
+    int hash =
+        (int)(yins - zins + 1) |
+        (int)(xins - yins + 1) << 1 |
+        (int)(xins - zins + 1) << 2 |
+        (int)(inSum) << 3 |
+        (int)(inSum + zins) << 5 |
+        (int)(inSum + yins) << 7 |
+        (int)(inSum + xins) << 9;
+
+    Contribution3 *c = n->lookup3D[hash];
+
+    float value = 0.0;
+
+    while (c != NULL) {
+        float dx = dx0 + c->dx;
+        float dy = dy0 + c->dy;
+        float dz = dz0 + c->dz;
+        float attn = 2.f - dx * dx - dy * dy - dz * dz;
+
+        if (attn > 0.f) {
+            int px = xsb + c->xsb;
+            int py = ysb + c->ysb;
+            int pz = zsb + c->zsb;
+
+            int i = n->perm3D[(n->perm[(n->perm[px & 0xFF] + py) & 0xFF] + pz) & 0xFF];
+            float valuePart = gradients3D[i] * dx + gradients3D[i + 1] * dy + gradients3D[i + 2] * dz;
+
+            attn *= attn;
+            value += attn * attn * valuePart;
+        }
+
+        c = c->next;
+    }
+
+    return value * NORM_3D;
+}
+
+#define GRAIN_SLICE(name, type, round, clip, scale)                         \
+static int grain##name##_plane_slice(AVFilterContext *ctx, ThreadData *td,  \
+                              int jobnr, int nb_jobs, int p, int max)       \
+{                                                                           \
+    FilmGrainContext *s = ctx->priv;                                        \
+    AVFilterLink *inlink = ctx->inputs[0];                                  \
+    AVFrame *in = td->in;                                                   \
+    AVFrame *out = td->out;                                                 \
+    const int width = s->planewidth[p];                                     \
+    const int height = s->planeheight[p];                                   \
+    const float xsize = s->size / width;                                    \
+    const float ysize = s->size / height;                                   \
+    const int slice_start = (height *  jobnr   ) / nb_jobs;                 \
+    const int slice_end   = (height * (jobnr+1)) / nb_jobs;                 \
+    const type *src = (type *)(in->data[p] + slice_start * in->linesize[p]);\
+    type *dst = (type *)(out->data[p] + slice_start * out->linesize[p]);    \
+    const float strength = s->strength * 0.3f * scale;                      \
+    const float bias = s->bias;                                             \
+    const float z = inlink->frame_count_out * s->speed;                     \
+    OpenSimplexNoise *n = &s->osn[p];                                       \
+                                                                            \
+    for (int y = slice_start; y < slice_end; y++) {                         \
+        if (!((1 << p) & s->planes)) {                                      \
+            if (in != out)                                                  \
+                av_image_copy_plane((uint8_t *)dst, out->linesize[p],       \
+                                    (const uint8_t *)src,                   \
+                                    in->linesize[p],  s->linesize[p],       \
+                                    slice_end - slice_start);               \
+            continue;                                                       \
+        }                                                                   \
+                                                                            \
+        for (int x = 0; x < s->planewidth[p]; x++) {                        \
+            float noise = evaluate(n, x * xsize, y * ysize, z);             \
+                                                                            \
+            dst[x] = clip(round(src[x] + strength * (noise + bias)), 0, max);\
+        }                                                                   \
+                                                                            \
+        dst += out->linesize[p] / sizeof(type);                             \
+        src += in->linesize[p] / sizeof(type);                              \
+    }                                                                       \
+                                                                            \
+    return 0;                                                               \
+}
+
+#define CLIP8(x, min, max) av_clip_uint8(x)
+#define CLIP16(x, min, max) av_clip(x, min, max)
+#define CLIPF(x, min, max) (x)
+#define NOP(x) (x)
+
+GRAIN_SLICE(8,  uint8_t,  lrintf, CLIP8, max)
+GRAIN_SLICE(16, uint16_t, lrintf, CLIP16, max)
+GRAIN_SLICE(32, float,    NOP,    CLIPF, 1.f)
+
+static int grain_slice(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs)
+{
+    FilmGrainContext *s = ctx->priv;
+    const int max = (1 << s->depth) - 1;
+    ThreadData *td = arg;
+
+    for (int p = 0; p < s->nb_planes; p++)
+        s->grain_plane_slice(ctx, td, jobnr, nb_jobs, p, max);
+
+    return 0;
+}
+
+static int filter_frame(AVFilterLink *inlink, AVFrame *in)
+{
+    AVFilterContext *ctx = inlink->dst;
+    AVFilterLink *outlink = ctx->outputs[0];
+    FilmGrainContext *s = ctx->priv;
+    ThreadData td;
+    AVFrame *out;
+
+    if (av_frame_is_writable(in)) {
+        out = in;
+    } else {
+        out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
+        if (!out) {
+            av_frame_free(&in);
+            return AVERROR(ENOMEM);
+        }
+        av_frame_copy_props(out, in);
+    }
+
+    td.in = in, td.out = out;
+    ff_filter_execute(ctx, grain_slice, &td, NULL,
+                      FFMIN(s->planeheight[1], ff_filter_get_nb_threads(ctx)));
+
+    if (in != out)
+        av_frame_free(&in);
+    return ff_filter_frame(outlink, out);
+}
+
+static int noise_lookup(OpenSimplexNoise *n)
+{
+    for (int i = 0; i < 216; i += 9) {
+        const uint8_t *baseSet = base3D[p3D[i]];
+        const int baseSetSize = p3D[i] == 2 ? 24 : 16;
+        Contribution3 *previous = NULL, *current = NULL;
+
+        for (int k = 0; k < baseSetSize; k += 4) {
+            current = av_calloc(1, sizeof(Contribution3));
+            if (!current)
+                return AVERROR(ENOMEM);
+
+            EContribution3(current, baseSet[k], baseSet[k + 1],
+                           baseSet[k + 2], baseSet[k + 3]);
+            if (previous == NULL) {
+                n->contributions3D[i / 9] = current;
+            } else {
+                previous->next = current;
+            }
+
+            previous = current;
+        }
+
+        current->next = av_calloc(1, sizeof(Contribution3));
+        if (!current->next)
+            return AVERROR(ENOMEM);
+        EContribution3(current->next, p3D[i + 1], p3D[i + 2], p3D[i + 3], p3D[i + 4]);
+        current->next->next = av_calloc(1, sizeof(Contribution3));
+        if (!current->next->next)
+            return AVERROR(ENOMEM);
+        EContribution3(current->next->next, p3D[i + 5], p3D[i + 6], p3D[i + 7], p3D[i + 8]);
+    }
+
+    for (int i = 0; i < FF_ARRAY_ELEMS(lookupPairs3D); i += 2)
+        n->lookup3D[lookupPairs3D[i]] = n->contributions3D[lookupPairs3D[i + 1]];
+
+    return 0;
+}
+
+static av_cold int init(AVFilterContext *ctx)
+{
+    FilmGrainContext *s = ctx->priv;
+
+    for (int p = 0; p < 4; p++) {
+        if (s->seed[p] == -1)
+            s->seed[p] = av_get_random_seed();
+        init_noise(&s->osn[p], s->seed[p]);
+        noise_lookup(&s->osn[p]);
+    }
+
+    return 0;
+}
+
+static int config_input(AVFilterLink *inlink)
+{
+    FilmGrainContext *s = inlink->dst->priv;
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
+    int ret;
+
+    s->depth = desc->comp[0].depth;
+    s->nb_planes = av_pix_fmt_count_planes(inlink->format);
+
+    if (s->depth <= 8)
+       s->grain_plane_slice = grain8_plane_slice;
+    else if (s->depth <= 16)
+       s->grain_plane_slice = grain16_plane_slice;
+    else
+       s->grain_plane_slice = grain32_plane_slice;
+
+    if ((ret = av_image_fill_linesizes(s->linesize, inlink->format, inlink->w)) < 0)
+        return ret;
+
+    s->planewidth[1] = s->planewidth[2] = AV_CEIL_RSHIFT(inlink->w, desc->log2_chroma_w);
+    s->planewidth[0] = s->planewidth[3] = inlink->w;
+    s->planeheight[1] = s->planeheight[2] = AV_CEIL_RSHIFT(inlink->h, desc->log2_chroma_h);
+    s->planeheight[0] = s->planeheight[3] = inlink->h;
+
+    return 0;
+}
+
+static av_cold void uninit(AVFilterContext *ctx)
+{
+    FilmGrainContext *s = ctx->priv;
+
+    for (int i = 0; i < 4; i++) {
+        for (int j = 0; j < 24; j++) {
+            struct Contribution3 *contr = s->osn[i].contributions3D[j];
+
+            while (contr != NULL) {
+                struct Contribution3 *temp = contr;
+
+                contr = contr->next;
+                av_freep(&temp);
+            }
+        }
+    }
+}
+
+static const AVFilterPad filmgrain_inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .filter_frame = filter_frame,
+        .config_props = config_input,
+    },
+};
+
+static const AVFilterPad filmgrain_outputs[] = {
+    {
+        .name = "default",
+        .type = AVMEDIA_TYPE_VIDEO,
+    },
+};
+
+AVFilter ff_vf_filmgrain = {
+    .name          = "filmgrain",
+    .description   = NULL_IF_CONFIG_SMALL("Add film grain."),
+    .priv_size     = sizeof(FilmGrainContext),
+    .init          = init,
+    .uninit        = uninit,
+    FILTER_INPUTS(filmgrain_inputs),
+    FILTER_OUTPUTS(filmgrain_outputs),
+    FILTER_PIXFMTS_ARRAY(pixel_fmts),
+    .priv_class    = &filmgrain_class,
+    .flags         = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC | AVFILTER_FLAG_SLICE_THREADS,
+    .process_command = ff_filter_process_command,
+};
