This is an automated email from the git hooks/post-receive script.

Git pushed a commit to branch master
in repository ffmpeg.

The following commit(s) were added to refs/heads/master by this push:
     new cff4a69294 avfilter: add latticepal filter
cff4a69294 is described below

commit cff4a692949a0c4b5ef36c1438abaccc5a1588de
Author:     Michael Niedermayer <[email protected]>
AuthorDate: Sun Jul 12 20:33:56 2026 +0000
Commit:     michaelni <[email protected]>
CommitDate: Mon Jul 13 18:16:18 2026 +0000

    avfilter: add latticepal filter
    
    Convert packed 24/32-bit RGB/BGR/RGBA/BGRA input to PAL8 using a per-frame 
palette
    whose colors are placed on a face-centered cubic lattice (realized as the
    scaled D3/D4 checkerboard lattice), with a user-supplied density controlling
    the number of lattice steps spanning one color axis.
    
    Only lattice points actually used by a frame enter its palette; if a frame
    needs more than 256 of them, the filter will itearatively drop palettte
    entries and reassign affected pixels until 256 color remain
    
    lookup uses the Conway-Sloane rounding algorithm. Supported dithering
    modes: none, ordered 8x8 bayer (swscale), Cluster & Void blue noise and
    Floyd-Steinberg error diffusion.
    
    Co-Authored-by: Fable-5
    Signed-off-by: Michael Niedermayer <[email protected]>
---
 Changelog                                  |    1 +
 doc/filters.texi                           |   86 ++
 libavfilter/Makefile                       |    1 +
 libavfilter/allfilters.c                   |    1 +
 libavfilter/version.h                      |    2 +-
 libavfilter/vf_latticepal.c                | 1393 ++++++++++++++++++++++++++++
 tests/fate/filter-video.mak                |    8 +
 tests/ref/fate/filter-latticepal           |   10 +
 tests/ref/fate/filter-latticepal-batched   |   10 +
 tests/ref/fate/filter-latticepal-bayer     |   10 +
 tests/ref/fate/filter-latticepal-maxcolors |   10 +
 tests/ref/fate/filter-latticepal-refine    |   10 +
 tests/ref/fate/filter-latticepal-residual  |   10 +
 13 files changed, 1551 insertions(+), 1 deletion(-)

diff --git a/Changelog b/Changelog
index 0a9ff11215..f10d639db7 100644
--- a/Changelog
+++ b/Changelog
@@ -7,6 +7,7 @@ version <next>:
 - iTerm2 inline image protocol muxer
 - LCEVC payload merging bitstream filter
 - MVR demuxer
+- latticepal filter
 
 
 version 9.0:
diff --git a/doc/filters.texi b/doc/filters.texi
index ef49530d5d..002979ba08 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -16538,6 +16538,92 @@ Set which planes to filter. Default is all. Allowed 
range is from 0 to 15.
 
 This filter supports the all above options as @ref{commands}.
 
+@section latticepal
+
+Convert packed RGB/BGR input to PAL8 with a per-frame palette whose colors
+are placed on a face-centered cubic (FCC) lattice spanning the RGB cube.
+
+Each frame gets its own palette, built only from the lattice colors actually
+used by that frame. If a frame uses more than 256 lattice colors, they are
+reduced by iteratively dropping the color whose removal has the least
+impact (its pixel count times the squared distance to the nearest remaining
+color) and mapping its pixels to that nearest remaining color. The alpha
+channel, if present, is ignored unless the @option{alpha} option is set.
+
+The filter accepts the following options:
+
+@table @option
+@item density
+Set the lattice density as the number of lattice steps spanning one color
+axis (0 to 255). Higher values give a finer lattice and thus more possible
+colors, making it more likely that the per-frame color reduction has to
+drop some of them. At a density of @code{7} with 256 palette entries no
+reduction ever happens since the whole lattice then holds exactly 256
+points. Allowed range is @code{1} to @code{255}. Default is @code{20}.
+
+@item max_colors
+Set the maximum number of palette entries to use. Frames using more lattice
+colors than this are reduced accordingly. Allowed range is @code{2} to
+@code{256}. Default is @code{256}.
+
+@item alpha
+Quantize the alpha channel too, placing the palette colors on a 4
+dimensional D4 lattice over RGBA instead of the FCC lattice over RGB.
+Inputs without an alpha channel are treated as opaque. With this option
+the density is limited to @code{64}. Default is @code{0} (alpha is
+ignored and the palette is fully opaque).
+
+@item refine
+Redistribute the error introduced by dropping colors, which is otherwise
+uncompensated and can show as discolored patches. Frames that needed no
+reduction are not changed by this option.
+@table @samp
+@item none
+No refinement.
+
+@item full
+Run a Floyd-Steinberg error diffusion pass over the whole frame against
+the final palette. Most accurate on average, but re-dithers everything,
+including regions whose colors survived.
+
+@item residual
+Diffuse only the residual between each pixel's first pass quantized color
+and its final palette color. Pixels of surviving colors that receive no
+residual are unchanged, keeping the character of the selected dither mode
+outside the reduced regions.
+
+@item batched
+Interleave color removal and rediffusion: drop half of the excess colors,
+rediffuse the whole frame against the survivors, recount the colors from
+the actual assignments and repeat. Colors that the dithering can
+reproduce by mixing become free to drop while extreme colors keep their
+pixels and are protected; this gives the best quality at the cost of
+about log2 of the number of dropped colors rediffusion passes.
+@end table
+
+Default is @code{none}.
+
+@item dither
+Select the dithering mode. Available algorithms are:
+@table @samp
+@item none
+No dithering.
+
+@item bayer
+Ordered 8x8 dithering using the classic swscale dither matrix with its
+per-channel decorrelation, offsets spanning one lattice period.
+
+@item blue_noise
+Void-and-cluster blue noise dithering using three 64x64 masks generated at
+initialization, one per color channel.
+
+@item floyd_steinberg
+Floyd-Steinberg error diffusion.
+@end table
+
+Default is @code{floyd_steinberg}.
+@end table
+
 @section lenscorrection
 
 Correct radial lens distortion
diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index b35260e706..daa4d552f0 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -371,6 +371,7 @@ OBJS-$(CONFIG_KIRSCH_FILTER)                 += 
vf_convolution.o
 OBJS-$(CONFIG_LAGFUN_FILTER)                 += vf_lagfun.o
 OBJS-$(CONFIG_LCEVC_FILTER)                  += vf_lcevc.o
 OBJS-$(CONFIG_LATENCY_FILTER)                += f_latency.o
+OBJS-$(CONFIG_LATTICEPAL_FILTER)             += vf_latticepal.o
 OBJS-$(CONFIG_LENSCORRECTION_FILTER)         += vf_lenscorrection.o
 OBJS-$(CONFIG_LENSFUN_FILTER)                += vf_lensfun.o
 OBJS-$(CONFIG_LIBPLACEBO_FILTER)             += vf_libplacebo.o vulkan.o 
vulkan_filter.o
diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c
index 14b63babfe..4af7a3bbbf 100644
--- a/libavfilter/allfilters.c
+++ b/libavfilter/allfilters.c
@@ -344,6 +344,7 @@ extern const FFFilter ff_vf_kerndeint;
 extern const FFFilter ff_vf_kirsch;
 extern const FFFilter ff_vf_lagfun;
 extern const FFFilter ff_vf_latency;
+extern const FFFilter ff_vf_latticepal;
 extern const FFFilter ff_vf_lcevc;
 extern const FFFilter ff_vf_lenscorrection;
 extern const FFFilter ff_vf_lensfun;
diff --git a/libavfilter/version.h b/libavfilter/version.h
index bece922c7f..7e0eb9af97 100644
--- a/libavfilter/version.h
+++ b/libavfilter/version.h
@@ -31,7 +31,7 @@
 
 #include "version_major.h"
 
-#define LIBAVFILTER_VERSION_MINOR   2
+#define LIBAVFILTER_VERSION_MINOR   3
 #define LIBAVFILTER_VERSION_MICRO 100
 
 
diff --git a/libavfilter/vf_latticepal.c b/libavfilter/vf_latticepal.c
new file mode 100644
index 0000000000..f71b651663
--- /dev/null
+++ b/libavfilter/vf_latticepal.c
@@ -0,0 +1,1393 @@
+/*
+ * Copyright (c) 2026 Michael Niedermayer <[email protected]>
+ *
+ * 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
+ */
+
+/**
+ * @file
+ * Convert packed RGB/BGR to PAL8 with a per-frame palette whose colors sit
+ * on a face-centered cubic (FCC) lattice.
+ *
+ * The FCC lattice is realized as the D3 checkerboard lattice: the points
+ * (i,j,k) with an even coordinate sum, scaled so that the density option is
+ * the number of lattice steps spanning one color axis (0..255). Only the
+ * lattice points actually used by a frame enter its palette; if a frame
+ * uses more than 256 of them, the used colors are reduced by iteratively
+ * dropping the color whose removal has the least impact and mapping its
+ * pixels to the nearest remaining color.
+ */
+
+#include <math.h>
+#include "libavutil/lfg.h"
+#include "libavutil/mem.h"
+#include "libavutil/opt.h"
+#include "libavutil/pixdesc.h"
+#include "avfilter.h"
+#include "filters.h"
+#include "formats.h"
+#include "video.h"
+
+#define MAX_DENSITY 255
+#define MAX_DENSITY_ALPHA 64
+
+enum dithering_mode {
+    DITHERING_NONE,
+    DITHERING_BAYER,
+    DITHERING_BLUE_NOISE,
+    DITHERING_FLOYD_STEINBERG,
+    NB_DITHERING
+};
+
+enum refine_mode {
+    REFINE_NONE,
+    REFINE_FULL,
+    REFINE_RESIDUAL,
+    REFINE_BATCHED,
+    NB_REFINE
+};
+
+/* void-and-cluster blue noise mask parameters */
+#define VC_SHIFT  6
+#define VC_SIZE   (1 << VC_SHIFT)
+#define VC_AREA   (VC_SIZE * VC_SIZE)
+#define VC_MASK   (VC_SIZE - 1)
+#define VC_SIGMA  1.5f
+#define VC_RADIUS 7
+#define VC_KSIZE  (2 * VC_RADIUS + 1)
+
+typedef struct PalEntry {
+    int pos;            ///< lattice cell position, ((i * dim + j) * dim + k) 
[* dim + l]
+    uint8_t ci[4];      ///< lattice indices of the color
+    uint8_t alive;      ///< still part of the palette while reducing
+    int nn;             ///< nearest live color while reducing, then palette 
slot
+    int d2;             ///< squared RGB(A) distance to nn
+    int64_t count;      ///< pixels quantizing to this color, incl. absorbed 
ones
+} PalEntry;
+
+typedef struct LatticePalContext {
+    const AVClass *class;
+    int density;                        ///< lattice steps per color axis
+    int dither;
+    int max_colors;                     ///< palette entries to use at most
+    int alpha;                          ///< quantize the alpha channel too 
(D4 lattice)
+    int refine;                         ///< rediffuse the error of dropped 
colors
+
+    int ro, go, bo, ao;                 ///< byte offsets of R, G, B, A in an 
input pixel, ao < 0: opaque
+    int nc;                             ///< quantized components, 3 or 4
+    int pixstep;                        ///< bytes per input pixel
+    float scale;                        ///< density / 255
+    uint8_t idx2val[MAX_DENSITY + 1];   ///< lattice index -> 8-bit component 
value
+    int ordered_dither[4][8 * 8];       ///< per-channel bayer offsets 
spanning one lattice period
+    int *blue_dither[4];                ///< per-channel blue noise offsets 
spanning one lattice period
+    int32_t *err[2];                    ///< Floyd-Steinberg error rows, nc 
ints per pixel
+
+    int dim;                            ///< density + 1, lattice cells per 
axis
+    int min_gap;                        ///< smallest idx2val increment
+    int32_t *cell;                      ///< dim^nc table: lattice cell -> 
list index + 1, 0 if unused
+    uint32_t *pixpos;                   ///< per-pixel cell position of the 
quantized color
+    PalEntry *list;                     ///< colors used by the current frame
+    int nb_used;
+    int nb_alloc;
+    int *alive_arr;                     ///< compact list of live color 
indices while reducing
+    int *alive_pos;                     ///< position of each live color in 
alive_arr
+    int nb_alive;                       ///< entries in alive_arr, 0 outside 
reduction/remap
+    int alive_alloc;
+    uint32_t prev_pal[AVPALETTE_COUNT]; ///< previous frame's palette, for 
stable slot assignment
+    int *touched;                       ///< cells filled on demand by the 
refinement pass
+    int nb_touched;
+    int touched_alloc;
+} LatticePalContext;
+
+#define OFFSET(x) offsetof(LatticePalContext, x)
+#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM
+static const AVOption latticepal_options[] = {
+    { "density", "set the number of lattice steps spanning one color axis", 
OFFSET(density), AV_OPT_TYPE_INT, {.i64=20}, 1, MAX_DENSITY, FLAGS },
+    { "max_colors", "set the maximum number of palette entries to use", 
OFFSET(max_colors), AV_OPT_TYPE_INT, {.i64=256}, 2, 256, FLAGS },
+    { "alpha", "quantize the alpha channel too, on a 4 dimensional lattice", 
OFFSET(alpha), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, FLAGS },
+    { "refine", "rediffuse the error of dropped colors against the final 
palette", OFFSET(refine), AV_OPT_TYPE_INT, {.i64=REFINE_NONE}, 0, NB_REFINE-1, 
FLAGS, .unit = "refine_mode" },
+        { "none",     "no refinement",                                        
0, AV_OPT_TYPE_CONST, {.i64=REFINE_NONE},     INT_MIN, INT_MAX, FLAGS, .unit = 
"refine_mode" },
+        { "full",     "rediffuse the whole frame against the final palette",  
0, AV_OPT_TYPE_CONST, {.i64=REFINE_FULL},     INT_MIN, INT_MAX, FLAGS, .unit = 
"refine_mode" },
+        { "residual", "diffuse only the residual of the dropped colors",      
0, AV_OPT_TYPE_CONST, {.i64=REFINE_RESIDUAL}, INT_MIN, INT_MAX, FLAGS, .unit = 
"refine_mode" },
+        { "batched",  "interleave removal and rediffusion in geometric 
batches", 0, AV_OPT_TYPE_CONST, {.i64=REFINE_BATCHED}, INT_MIN, INT_MAX, FLAGS, 
.unit = "refine_mode" },
+    { "dither", "select dithering mode", OFFSET(dither), AV_OPT_TYPE_INT, 
{.i64=DITHERING_FLOYD_STEINBERG}, 0, NB_DITHERING-1, FLAGS, .unit = 
"dithering_mode" },
+        { "none",            "no dithering",                    0, 
AV_OPT_TYPE_CONST, {.i64=DITHERING_NONE},            INT_MIN, INT_MAX, FLAGS, 
.unit = "dithering_mode" },
+        { "bayer",           "ordered 8x8 bayer dithering",     0, 
AV_OPT_TYPE_CONST, {.i64=DITHERING_BAYER},           INT_MIN, INT_MAX, FLAGS, 
.unit = "dithering_mode" },
+        { "blue_noise",      "void-and-cluster blue noise dithering", 0, 
AV_OPT_TYPE_CONST, {.i64=DITHERING_BLUE_NOISE}, INT_MIN, INT_MAX, FLAGS, .unit 
= "dithering_mode" },
+        { "floyd_steinberg", "Floyd-Steinberg error diffusion", 0, 
AV_OPT_TYPE_CONST, {.i64=DITHERING_FLOYD_STEINBERG}, INT_MIN, INT_MAX, FLAGS, 
.unit = "dithering_mode" },
+    { NULL }
+};
+
+AVFILTER_DEFINE_CLASS(latticepal);
+
+static int query_formats(const AVFilterContext *ctx,
+                         AVFilterFormatsConfig **cfg_in,
+                         AVFilterFormatsConfig **cfg_out)
+{
+    static const enum AVPixelFormat in_fmts[] = {
+        AV_PIX_FMT_RGB24, AV_PIX_FMT_BGR24,
+        AV_PIX_FMT_RGBA,  AV_PIX_FMT_BGRA,
+        AV_PIX_FMT_ARGB,  AV_PIX_FMT_ABGR,
+        AV_PIX_FMT_RGB0,  AV_PIX_FMT_BGR0,
+        AV_PIX_FMT_0RGB,  AV_PIX_FMT_0BGR,
+        AV_PIX_FMT_NONE
+    };
+    static const enum AVPixelFormat out_fmts[] = { AV_PIX_FMT_PAL8, 
AV_PIX_FMT_NONE };
+    int ret;
+
+    if ((ret = ff_formats_ref(ff_make_pixel_format_list(in_fmts),  
&cfg_in[0]->formats)) < 0)
+        return ret;
+    if ((ret = ff_formats_ref(ff_make_pixel_format_list(out_fmts), 
&cfg_out[0]->formats)) < 0)
+        return ret;
+    return 0;
+}
+
+/* Classic swscale ordered dither matrix (libswscale/output.c), 73 levels. */
+static const uint8_t dither_8x8_73[8][8] = {
+    {  0, 55, 14, 68,  3, 58, 17, 72, },
+    { 37, 18, 50, 32, 40, 22, 54, 35, },
+    {  9, 64,  5, 59, 13, 67,  8, 63, },
+    { 46, 27, 41, 23, 49, 31, 44, 26, },
+    {  2, 57, 16, 71,  1, 56, 15, 70, },
+    { 39, 21, 52, 34, 38, 19, 51, 33, },
+    { 11, 66,  7, 62, 10, 65,  6, 60, },
+    { 48, 30, 43, 25, 47, 29, 42, 24, },
+};
+
+/**
+ * Add (sign > 0) or remove (sign < 0) the Gaussian energy contribution of a
+ * minority pixel at position pos on the VC_SIZE x VC_SIZE torus.
+ */
+static void vc_splat(float *energy, const float *kern, int pos, float sign)
+{
+    const int px = pos & VC_MASK, py = pos >> VC_SHIFT;
+
+    for (int dy = -VC_RADIUS; dy <= VC_RADIUS; dy++) {
+        const int y = (py + dy) & VC_MASK;
+        const float *krow = &kern[(dy + VC_RADIUS) * VC_KSIZE + VC_RADIUS];
+        float *erow = &energy[y << VC_SHIFT];
+
+        for (int dx = -VC_RADIUS; dx <= VC_RADIUS; dx++)
+            erow[(px + dx) & VC_MASK] += sign * krow[dx];
+    }
+}
+
+/**
+ * Position of the extreme energy value among the cells whose bit equals
+ * want_set: the tightest cluster (find_max, among minority pixels) or the
+ * largest void (!find_max, among majority pixels).
+ */
+static int vc_extreme(const float *energy, const uint8_t *bits, int want_set, 
int find_max)
+{
+    int best = -1;
+    float beste = 0.f;
+
+    for (int i = 0; i < VC_AREA; i++) {
+        if (bits[i] != want_set)
+            continue;
+        if (best < 0 || (find_max ? energy[i] > beste : energy[i] < beste)) {
+            best  = i;
+            beste = energy[i];
+        }
+    }
+    return best;
+}
+
+/**
+ * Generate a void-and-cluster blue noise rank matrix (Ulichney 1993):
+ * every cell gets a unique rank in [0, VC_AREA).
+ *
+ * On a torus the filtered field of the zero pattern is the constant kernel
+ * sum minus the field of the one pattern, so the tightest cluster of zeros
+ * coincides with the largest void of ones and the ranking above 50% fill
+ * needs no separate phase.
+ */
+static void vc_generate(uint16_t *rank, uint8_t *bits, float *energy,
+                        float *e1, const float *kern, AVLFG *lfg)
+{
+    const int n1 = VC_AREA / 10;
+    uint8_t b1[VC_AREA];
+    int placed, pos;
+
+    memset(bits,   0, VC_AREA * sizeof(*bits));
+    memset(energy, 0, VC_AREA * sizeof(*energy));
+
+    /* initial random minority pattern */
+    for (placed = 0; placed < n1;) {
+        pos = av_lfg_get(lfg) & (VC_AREA - 1);
+        if (!bits[pos]) {
+            bits[pos] = 1;
+            vc_splat(energy, kern, pos, 1.f);
+            placed++;
+        }
+    }
+
+    /* relax: move the tightest cluster into the largest void until stable */
+    for (int i = 0; i < VC_AREA * 8; i++) {
+        pos = vc_extreme(energy, bits, 1, 1);
+        bits[pos] = 0;
+        vc_splat(energy, kern, pos, -1.f);
+        const int vp = vc_extreme(energy, bits, 0, 0);
+        bits[vp] = 1;
+        vc_splat(energy, kern, vp, 1.f);
+        if (vp == pos)
+            break;
+    }
+
+    /* phase 1: rank the initial pattern by removing tightest clusters */
+    memcpy(b1, bits,   sizeof(b1));
+    memcpy(e1, energy, VC_AREA * sizeof(*e1));
+    for (int r = n1 - 1; r >= 0; r--) {
+        pos = vc_extreme(e1, b1, 1, 1);
+        b1[pos] = 0;
+        vc_splat(e1, kern, pos, -1.f);
+        rank[pos] = r;
+    }
+
+    /* phase 2+3: fill the largest void until all cells are ranked */
+    for (int r = n1; r < VC_AREA; r++) {
+        pos = vc_extreme(energy, bits, 0, 0);
+        bits[pos] = 1;
+        vc_splat(energy, kern, pos, 1.f);
+        rank[pos] = r;
+    }
+}
+
+/**
+ * Quantize a color to the nearest point of the scaled D3 (FCC) or D4
+ * lattice.
+ *
+ * Conway & Sloane: round every coordinate to the nearest integer; if the
+ * coordinate sum is odd, re-round the coordinate with the largest rounding
+ * error in the other direction. This works for any checkerboard lattice Dn.
+ *
+ * @param in the nc input components
+ * @param f  receives the nc lattice indices
+ */
+static av_always_inline void quant_dn(const LatticePalContext *s, const int 
*in, int f[4])
+{
+    const int N = s->density;
+    const int nc = s->nc;
+    int sum = 0;
+    float d[4];
+
+    for (int i = 0; i < nc; i++) {
+        const float u = av_clipf(in[i] * s->scale, 0.f, N);
+        f[i] = lrintf(u);
+        d[i] = u - f[i];
+        sum += f[i];
+    }
+
+    if (sum & 1) {
+        int k = 0, dir;
+        for (int i = 1; i < nc; i++)
+            if (fabsf(d[i]) > fabsf(d[k]))
+                k = i;
+        dir = d[k] >= 0.f ? 1 : -1;
+        if (f[k] + dir < 0 || f[k] + dir > N)
+            dir = -dir;
+        f[k] += dir;
+    }
+}
+
+/**
+ * Account one pixel using the lattice color with indices f, registering the
+ * color in the cell table and the used color list on first use.
+ *
+ * @return the lattice cell position, or AVERROR(ENOMEM)
+ */
+static av_always_inline int color_inc(LatticePalContext *s, const int f[4])
+{
+    int pos = f[0];
+    int idx;
+
+    for (int i = 1; i < s->nc; i++)
+        pos = pos * s->dim + f[i];
+    idx = s->cell[pos];
+
+    if (!idx) {
+        PalEntry *e;
+
+        if (s->nb_used == s->nb_alloc) {
+            const int na = FFMAX(s->nb_alloc * 2, 512);
+            PalEntry *nl = av_realloc_array(s->list, na, sizeof(*nl));
+            if (!nl)
+                return AVERROR(ENOMEM);
+            s->list     = nl;
+            s->nb_alloc = na;
+        }
+        e = &s->list[s->nb_used];
+        e->pos   = pos;
+        e->ci[0] = f[0];
+        e->ci[1] = f[1];
+        e->ci[2] = f[2];
+        e->ci[3] = s->nc == 4 ? f[3] : 0;
+        e->alive = 1;
+        e->count = 0;
+        s->cell[pos] = idx = ++s->nb_used;
+    }
+    s->list[idx - 1].count++;
+    return pos;
+}
+
+/**
+ * Find the nearest live color of the used color list, excluding self
+ * (pass -1 to match any live color).
+ *
+ * While many colors are alive, search outward in Chebyshev shells of the
+ * lattice index space; the component value difference of a cell r shells
+ * away is at least r * min_gap, which bounds the search once a candidate
+ * is known. When only few colors are left the lattice around them is
+ * sparse and shells get expensive, so scan the compact live list instead.
+ * (For 3 components ci[3] is 0 everywhere, contributing nothing.)
+ *
+ * @return list index of the nearest live color, its distance in *out_d2
+ */
+static int nearest_alive(LatticePalContext *s, const uint8_t ci[4], int self, 
int *out_d2)
+{
+    PalEntry *list = s->list;
+    const uint8_t *val = s->idx2val;
+    const int dim = s->dim;
+    const int i0 = ci[0], i1 = ci[1], i2 = ci[2], i3 = ci[3];
+    const int v0 = val[i0], v1 = val[i1], v2 = val[i2], v3 = val[i3];
+    int best = -1, bestd = INT_MAX;
+
+    if (s->nb_alive > 0 && s->nb_alive <= 1024) {
+        for (int k = 0; k < s->nb_alive; k++) {
+            const int j = s->alive_arr[k];
+            const PalEntry *o = &list[j];
+            int d2, dv;
+
+            if (j == self)
+                continue;
+            dv = val[o->ci[0]] - v0; d2  = dv * dv;
+            dv = val[o->ci[1]] - v1; d2 += dv * dv;
+            dv = val[o->ci[2]] - v2; d2 += dv * dv;
+            dv = val[o->ci[3]] - v3; d2 += dv * dv;
+            if (d2 < bestd) {
+                bestd = d2;
+                best  = j;
+            }
+        }
+        *out_d2 = best >= 0 ? bestd : 0;
+        return best;
+    }
+
+#define CHECK_CELL3(a, b, c) do {                                       \
+    const int e = s->cell[((a) * dim + (b)) * dim + (c)];               \
+    if (e > 0 && e - 1 != self && list[e - 1].alive) {                  \
+        const int dr = val[a] - v0;                                     \
+        const int dg = val[b] - v1;                                     \
+        const int db = val[c] - v2;                                     \
+        const int d2 = dr * dr + dg * dg + db * db;                     \
+        if (d2 < bestd) {                                               \
+            bestd = d2;                                                 \
+            best  = e - 1;                                              \
+        }                                                               \
+    }                                                                   \
+} while (0)
+
+#define CHECK_CELL4(a, b, c, l) do {                                    \
+    const int e = s->cell[(((a) * dim + (b)) * dim + (c)) * dim + (l)]; \
+    if (e > 0 && e - 1 != self && list[e - 1].alive) {                  \
+        const int dr = val[a] - v0;                                     \
+        const int dg = val[b] - v1;                                     \
+        const int db = val[c] - v2;                                     \
+        const int da_ = val[l] - v3;                                    \
+        const int d2 = dr * dr + dg * dg + db * db + da_ * da_;         \
+        if (d2 < bestd) {                                               \
+            bestd = d2;                                                 \
+            best  = e - 1;                                              \
+        }                                                               \
+    }                                                                   \
+} while (0)
+
+    for (int r = 1; r < dim; r++) {
+        if (best >= 0 && (int64_t)r * s->min_gap * r * s->min_gap > bestd)
+            break;
+        for (int a = FFMAX(i0 - r, 0); a <= FFMIN(i0 + r, dim - 1); a++) {
+            const int da = FFABS(a - i0);
+            for (int b = FFMAX(i1 - r, 0); b <= FFMIN(i1 + r, dim - 1); b++) {
+                const int dab = FFMAX(da, FFABS(b - i1));
+
+                if (s->nc == 3) {
+                    if (dab == r) {
+                        for (int c = FFMAX(i2 - r, 0); c <= FFMIN(i2 + r, dim 
- 1); c++)
+                            if (!((a + b + c) & 1))
+                                CHECK_CELL3(a, b, c);
+                    } else {
+                        if (i2 - r >= 0 && !((a + b + i2 - r) & 1))
+                            CHECK_CELL3(a, b, i2 - r);
+                        if (i2 + r < dim && !((a + b + i2 + r) & 1))
+                            CHECK_CELL3(a, b, i2 + r);
+                    }
+                } else {
+                    for (int c = FFMAX(i2 - r, 0); c <= FFMIN(i2 + r, dim - 
1); c++) {
+                        if (FFMAX(dab, FFABS(c - i2)) == r) {
+                            for (int l = FFMAX(i3 - r, 0); l <= FFMIN(i3 + r, 
dim - 1); l++)
+                                if (!((a + b + c + l) & 1))
+                                    CHECK_CELL4(a, b, c, l);
+                        } else {
+                            if (i3 - r >= 0 && !((a + b + c + i3 - r) & 1))
+                                CHECK_CELL4(a, b, c, i3 - r);
+                            if (i3 + r < dim && !((a + b + c + i3 + r) & 1))
+                                CHECK_CELL4(a, b, c, i3 + r);
+                        }
+                    }
+                }
+            }
+        }
+    }
+#undef CHECK_CELL3
+#undef CHECK_CELL4
+
+    *out_d2 = best >= 0 ? bestd : 0;
+    return best;
+}
+
+static void nn_search(LatticePalContext *s, int self)
+{
+    int d2;
+
+    s->list[self].nn = nearest_alive(s, s->list[self].ci, self, &d2);
+    s->list[self].d2 = d2;
+}
+
+typedef struct HeapEnt {
+    int64_t imp;        ///< impact of the removal when the entry was pushed
+    int idx;            ///< used color list index
+} HeapEnt;
+
+static void heap_push(HeapEnt *heap, int *nb, int64_t imp, int idx)
+{
+    int i = (*nb)++;
+
+    while (i > 0 && heap[(i - 1) / 2].imp > imp) {
+        heap[i] = heap[(i - 1) / 2];
+        i = (i - 1) / 2;
+    }
+    heap[i].imp = imp;
+    heap[i].idx = idx;
+}
+
+static HeapEnt heap_pop(HeapEnt *heap, int *nb)
+{
+    const HeapEnt top = heap[0];
+    const HeapEnt last = heap[--*nb];
+    int i = 0, c;
+
+    while ((c = 2 * i + 1) < *nb) {
+        if (c + 1 < *nb && heap[c + 1].imp < heap[c].imp)
+            c++;
+        if (heap[c].imp >= last.imp)
+            break;
+        heap[i] = heap[c];
+        i = c;
+    }
+    heap[i] = last;
+    return top;
+}
+
+/**
+ * Reduce the live colors down to target by repeatedly dropping the color
+ * whose removal has the least impact: its pixel count times the squared
+ * distance to the nearest remaining color, which then absorbs the
+ * dropped pixels.
+ *
+ * An impact can only grow: counts grow by absorption and the nearest
+ * neighbor distance grows when the neighbor is dropped. Stale heap entries
+ * therefore underestimate their color's impact, and validating at pop time
+ * and re-pushing the corrected entry yields the exact minimum.
+ */
+static int reduce_colors(LatticePalContext *s, int target)
+{
+    PalEntry *list = s->list;
+    int alive;
+    int nb_heap = 0, cap = s->nb_used + 64;
+    HeapEnt *heap = av_malloc_array(cap, sizeof(*heap));
+
+    if (!heap)
+        return AVERROR(ENOMEM);
+
+    if (s->alive_alloc < s->nb_used) {
+        av_freep(&s->alive_arr);
+        av_freep(&s->alive_pos);
+        s->alive_arr = av_malloc_array(s->nb_used, sizeof(*s->alive_arr));
+        s->alive_pos = av_malloc_array(s->nb_used, sizeof(*s->alive_pos));
+        if (!s->alive_arr || !s->alive_pos) {
+            av_free(heap);
+            return AVERROR(ENOMEM);
+        }
+        s->alive_alloc = s->nb_used;
+    }
+    s->nb_alive = 0;
+    for (int i = 0; i < s->nb_used; i++) {
+        if (!list[i].alive)
+            continue;
+        s->alive_pos[i] = s->nb_alive;
+        s->alive_arr[s->nb_alive++] = i;
+    }
+    alive = s->nb_alive;
+
+    for (int k = 0; k < s->nb_alive; k++) {
+        const int i = s->alive_arr[k];
+
+        nn_search(s, i);
+        heap_push(heap, &nb_heap, list[i].count * list[i].d2, i);
+    }
+
+    while (alive > target) {
+        const HeapEnt top = heap_pop(heap, &nb_heap);
+        const int i = top.idx;
+        int64_t imp;
+
+        if (!list[i].alive)
+            continue;
+        if (!list[list[i].nn].alive)
+            nn_search(s, i);
+        imp = list[i].count * list[i].d2;
+        if (imp != top.imp) {
+            /* stale entry, reinsert with the corrected impact */
+            if (nb_heap == cap) {
+                HeapEnt *nh = av_realloc_array(heap, cap * 2, sizeof(*heap));
+                if (!nh) {
+                    av_free(heap);
+                    return AVERROR(ENOMEM);
+                }
+                heap = nh;
+                cap *= 2;
+            }
+            heap_push(heap, &nb_heap, imp, i);
+            continue;
+        }
+        list[i].alive = 0;
+        list[list[i].nn].count += list[i].count;
+        alive--;
+        /* swap-remove from the compact live list */
+        s->alive_arr[s->alive_pos[i]] = s->alive_arr[--s->nb_alive];
+        s->alive_pos[s->alive_arr[s->nb_alive]] = s->alive_pos[i];
+    }
+
+    av_free(heap);
+    return 0;
+}
+
+/** Palette color (AARRGGBB) of a used color list entry. */
+static av_always_inline uint32_t entry_color(const LatticePalContext *s, const 
PalEntry *e)
+{
+    return (s->nc == 4 ? (uint32_t)s->idx2val[e->ci[3]] << 24 : 0xFF000000) |
+           s->idx2val[e->ci[0]] << 16 |
+           s->idx2val[e->ci[1]] <<  8 |
+           s->idx2val[e->ci[2]];
+}
+
+/**
+ * Full refinement pass: rediffuse the whole frame against the final
+ * palette.
+ *
+ * The first pass diffused its error against the full lattice, so the shift
+ * from dropped colors to their nearest survivor is uncompensated and shows
+ * up as flat discolored patches exactly where the reduction hit. Re-run
+ * Floyd-Steinberg error diffusion on the original pixels, quantizing to
+ * the nearest final palette color, which redistributes that error by
+ * construction (error diffusion is average correct against any codebook).
+ *
+ * The nearest palette color is resolved at lattice cell granularity: used
+ * cells already hold their slot from the remap, cells first touched by the
+ * diffusion get a scan over the at most 256 survivors and are cached in
+ * the cell table and recorded for the per frame reset.
+ */
+static int refine_full(LatticePalContext *s, AVFrame *out, const AVFrame *in)
+{
+    const int w = in->width, h = in->height;
+    const int ro = s->ro, go = s->go, bo = s->bo, ao = s->ao, pixstep = 
s->pixstep;
+    const int nc = s->nc;
+    const uint32_t *pal = (const uint32_t *)out->data[1];
+
+    memset(s->err[0], 0, (w + 2) * nc * sizeof(*s->err[0]));
+    memset(s->err[1], 0, (w + 2) * nc * sizeof(*s->err[1]));
+
+    for (int y = 0; y < h; y++) {
+        const uint8_t *src = in->data[0] + y * in->linesize[0];
+        uint8_t *dst = out->data[0] + y * out->linesize[0];
+        const int dir = (y & 1) ? -1 : 1;
+        const int x0 = dir > 0 ? 0 : w - 1;
+        int32_t *err_cur  = s->err[ y      & 1] + nc;
+        int32_t *err_next = s->err[(y + 1) & 1] + nc;
+        int f[4], px[4];
+
+        px[3] = 255;
+        memset(err_next - nc, 0, (w + 2) * nc * sizeof(*err_next));
+
+        for (int k = 0; k < w; k++) {
+            const int x = x0 + k * dir;
+            const int32_t *e = &err_cur[x * nc];
+            uint32_t c;
+            int pos, slot1;
+
+            px[0] = av_clip_uint8(src[x * pixstep + ro] + ((e[0] + 8) >> 4));
+            px[1] = av_clip_uint8(src[x * pixstep + go] + ((e[1] + 8) >> 4));
+            px[2] = av_clip_uint8(src[x * pixstep + bo] + ((e[2] + 8) >> 4));
+            if (nc == 4)
+                px[3] = av_clip_uint8((ao >= 0 ? src[x * pixstep + ao] : 255) 
+ ((e[3] + 8) >> 4));
+
+            quant_dn(s, px, f);
+            pos = f[0];
+            for (int i = 1; i < nc; i++)
+                pos = pos * s->dim + f[i];
+
+            slot1 = s->cell[pos];
+            if (!slot1) {
+                const uint8_t *val = s->idx2val;
+                int bd = INT_MAX;
+
+                for (int k = 0; k < s->nb_alive; k++) {
+                    const PalEntry *o = &s->list[s->alive_arr[k]];
+                    int d2, dv;
+
+                    dv = val[o->ci[0]] - val[f[0]]; d2  = dv * dv;
+                    dv = val[o->ci[1]] - val[f[1]]; d2 += dv * dv;
+                    dv = val[o->ci[2]] - val[f[2]]; d2 += dv * dv;
+                    if (nc == 4) {
+                        dv = val[o->ci[3]] - val[f[3]]; d2 += dv * dv;
+                    }
+                    if (d2 < bd) {
+                        bd    = d2;
+                        slot1 = o->nn + 1;
+                    }
+                }
+                if (s->nb_touched == s->touched_alloc) {
+                    const int na = FFMAX(s->touched_alloc * 2, 1024);
+                    int *nt = av_realloc_array(s->touched, na, sizeof(*nt));
+                    if (!nt)
+                        return AVERROR(ENOMEM);
+                    s->touched = nt;
+                    s->touched_alloc = na;
+                }
+                s->touched[s->nb_touched++] = pos;
+                s->cell[pos] = slot1;
+            }
+            dst[x] = slot1 - 1;
+
+            c = pal[slot1 - 1];
+            for (int i = 0; i < nc; i++) {
+                static const int sh[4] = { 16, 8, 0, 24 };
+                const int qerr = px[i] - (int)(c >> sh[i] & 0xff);
+
+                err_cur [(x + dir) * nc + i] += qerr * 7;
+                err_next[(x - dir) * nc + i] += qerr * 3;
+                err_next[ x        * nc + i] += qerr * 5;
+                err_next[(x + dir) * nc + i] += qerr;
+            }
+        }
+    }
+    return 0;
+}
+
+/**
+ * Residual refinement pass: rediffuse only the error of the dropped
+ * colors, using the first pass quantized color as the diffusion target.
+ * The error stream then carries only the removal residuals: a pixel whose
+ * color survived and that receives no incoming residual picks its own
+ * color with zero error and is unchanged, keeping the character of the
+ * selected dither mode outside the reduced regions.
+ *
+ * Runs before the cell table is rewritten: cells hold list indices, and
+ * cells resolved on demand are cached as -(slot + 1).
+ */
+static int refine_residual(LatticePalContext *s, AVFrame *out, const AVFrame 
*in)
+{
+    const int w = in->width, h = in->height;
+    const int nc = s->nc;
+    const uint32_t *pal = (const uint32_t *)out->data[1];
+    const uint8_t *val = s->idx2val;
+    PalEntry *list = s->list;
+
+    memset(s->err[0], 0, (w + 2) * nc * sizeof(*s->err[0]));
+    memset(s->err[1], 0, (w + 2) * nc * sizeof(*s->err[1]));
+
+    for (int y = 0; y < h; y++) {
+        const uint32_t *ppos = s->pixpos + (size_t)y * w;
+        uint8_t *dst = out->data[0] + y * out->linesize[0];
+        const int dir = (y & 1) ? -1 : 1;
+        const int x0 = dir > 0 ? 0 : w - 1;
+        int32_t *err_cur  = s->err[ y      & 1] + nc;
+        int32_t *err_next = s->err[(y + 1) & 1] + nc;
+        int f[4], px[4];
+
+        px[3] = 255;
+        memset(err_next - nc, 0, (w + 2) * nc * sizeof(*err_next));
+
+        for (int k = 0; k < w; k++) {
+            const int x = x0 + k * dir;
+            const PalEntry *e1 = &list[s->cell[ppos[x]] - 1];
+            const int32_t *e = &err_cur[x * nc];
+            uint32_t c;
+            int pos, v, slot;
+
+            if (e1->alive && !(e[0] | e[1] | e[2] | (nc == 4 ? e[3] : 0))) {
+                /* surviving color, no incoming residual: unchanged */
+                dst[x] = e1->nn;
+                continue;
+            }
+
+            /* target = first pass color + carried residual */
+            for (int i = 0; i < nc; i++)
+                px[i] = av_clip_uint8(val[e1->ci[i]] + ((e[i] + 8) >> 4));
+
+            quant_dn(s, px, f);
+            pos = f[0];
+            for (int i = 1; i < nc; i++)
+                pos = pos * s->dim + f[i];
+
+            v = s->cell[pos];
+            if (v > 0) {
+                slot = list[v - 1].nn;
+            } else if (v < 0) {
+                slot = -v - 1;
+            } else {
+                int bd = INT_MAX;
+
+                slot = 0;
+                for (int j = 0; j < s->nb_alive; j++) {
+                    const PalEntry *o = &list[s->alive_arr[j]];
+                    int d2, dv;
+
+                    dv = val[o->ci[0]] - val[f[0]]; d2  = dv * dv;
+                    dv = val[o->ci[1]] - val[f[1]]; d2 += dv * dv;
+                    dv = val[o->ci[2]] - val[f[2]]; d2 += dv * dv;
+                    if (nc == 4) {
+                        dv = val[o->ci[3]] - val[f[3]]; d2 += dv * dv;
+                    }
+                    if (d2 < bd) {
+                        bd   = d2;
+                        slot = o->nn;
+                    }
+                }
+                if (s->nb_touched == s->touched_alloc) {
+                    const int na = FFMAX(s->touched_alloc * 2, 1024);
+                    int *nt = av_realloc_array(s->touched, na, sizeof(*nt));
+                    if (!nt)
+                        return AVERROR(ENOMEM);
+                    s->touched = nt;
+                    s->touched_alloc = na;
+                }
+                s->touched[s->nb_touched++] = pos;
+                s->cell[pos] = -(slot + 1);
+            }
+            dst[x] = slot;
+
+            c = pal[slot];
+            for (int i = 0; i < nc; i++) {
+                static const int sh[4] = { 16, 8, 0, 24 };
+                const int qerr = px[i] - (int)(c >> sh[i] & 0xff);
+
+                err_cur [(x + dir) * nc + i] += qerr * 7;
+                err_next[(x - dir) * nc + i] += qerr * 3;
+                err_next[ x        * nc + i] += qerr * 5;
+                err_next[(x + dir) * nc + i] += qerr;
+            }
+        }
+    }
+    return 0;
+}
+
+/**
+ * Rediffuse the whole frame against the current live colors and reassign
+ * every pixel, recounting how often each live color is actually used.
+ *
+ * Cells resolving to a dead or unused state are cached as -(list index+1);
+ * stale caches from earlier rounds heal themselves through the alive check.
+ */
+static int batch_assign(LatticePalContext *s, const AVFrame *in)
+{
+    const int w = in->width, h = in->height;
+    const int ro = s->ro, go = s->go, bo = s->bo, ao = s->ao, pixstep = 
s->pixstep;
+    const int nc = s->nc;
+    const uint8_t *val = s->idx2val;
+    PalEntry *list = s->list;
+
+    for (int k = 0; k < s->nb_alive; k++)
+        list[s->alive_arr[k]].count = 0;
+
+    memset(s->err[0], 0, (w + 2) * nc * sizeof(*s->err[0]));
+    memset(s->err[1], 0, (w + 2) * nc * sizeof(*s->err[1]));
+
+    for (int y = 0; y < h; y++) {
+        const uint8_t *src = in->data[0] + y * in->linesize[0];
+        uint32_t *ppos = s->pixpos + (size_t)y * w;
+        const int dir = (y & 1) ? -1 : 1;
+        const int x0 = dir > 0 ? 0 : w - 1;
+        int32_t *err_cur  = s->err[ y      & 1] + nc;
+        int32_t *err_next = s->err[(y + 1) & 1] + nc;
+        int f[4], px[4];
+
+        px[3] = 255;
+        memset(err_next - nc, 0, (w + 2) * nc * sizeof(*err_next));
+
+        for (int k = 0; k < w; k++) {
+            const int x = x0 + k * dir;
+            const int32_t *e = &err_cur[x * nc];
+            const PalEntry *o;
+            int pos, v, j;
+
+            px[0] = av_clip_uint8(src[x * pixstep + ro] + ((e[0] + 8) >> 4));
+            px[1] = av_clip_uint8(src[x * pixstep + go] + ((e[1] + 8) >> 4));
+            px[2] = av_clip_uint8(src[x * pixstep + bo] + ((e[2] + 8) >> 4));
+            if (nc == 4)
+                px[3] = av_clip_uint8((ao >= 0 ? src[x * pixstep + ao] : 255) 
+ ((e[3] + 8) >> 4));
+
+            quant_dn(s, px, f);
+            pos = f[0];
+            for (int i = 1; i < nc; i++)
+                pos = pos * s->dim + f[i];
+
+            v = s->cell[pos];
+            if (v > 0 && list[v - 1].alive) {
+                j = v - 1;
+            } else if (v < 0 && list[-v - 1].alive) {
+                j = -v - 1;
+            } else {
+                const uint8_t fci[4] = { f[0], f[1], f[2], nc == 4 ? f[3] : 0 
};
+                int d2;
+
+                j = nearest_alive(s, fci, -1, &d2);
+                if (!v) {
+                    if (s->nb_touched == s->touched_alloc) {
+                        const int na = FFMAX(s->touched_alloc * 2, 1024);
+                        int *nt = av_realloc_array(s->touched, na, 
sizeof(*nt));
+                        if (!nt)
+                            return AVERROR(ENOMEM);
+                        s->touched = nt;
+                        s->touched_alloc = na;
+                    }
+                    s->touched[s->nb_touched++] = pos;
+                }
+                s->cell[pos] = -(j + 1);
+            }
+            o = &list[j];
+            list[j].count++;
+            ppos[x] = j;
+
+            for (int i = 0; i < nc; i++) {
+                const int qerr = px[i] - val[o->ci[i]];
+
+                err_cur [(x + dir) * nc + i] += qerr * 7;
+                err_next[(x - dir) * nc + i] += qerr * 3;
+                err_next[ x        * nc + i] += qerr * 5;
+                err_next[(x + dir) * nc + i] += qerr;
+            }
+        }
+    }
+    return 0;
+}
+
+/**
+ * Batched reduction: instead of dropping all excess colors against the
+ * first pass statistics, drop half of the excess, rediffuse the frame
+ * against the survivors and recount from the actual assignments, then
+ * repeat. Colors whose pixels the rediffusion absorbed elsewhere become
+ * free to drop, while colors that dithering cannot reproduce (extremes of
+ * the used gamut) keep their pixels and with them a high removal impact,
+ * so they are protected in later rounds.
+ */
+static int reduce_batched(LatticePalContext *s, const AVFrame *in)
+{
+    int alive = s->nb_used;
+
+    while (alive > s->max_colors) {
+        const int excess = alive - s->max_colors;
+        const int target = s->max_colors + excess / 2;
+        int ret = reduce_colors(s, target);
+
+        if (ret < 0)
+            return ret;
+        alive = target;
+
+        ret = batch_assign(s, in);
+        if (ret < 0)
+            return ret;
+
+        /* drop the colors the rediffusion no longer uses */
+        for (int k = 0; k < s->nb_alive;) {
+            const int i = s->alive_arr[k];
+
+            if (!s->list[i].count) {
+                s->list[i].alive = 0;
+                s->alive_arr[k] = s->alive_arr[--s->nb_alive];
+                s->alive_pos[s->alive_arr[k]] = k;
+                alive--;
+            } else
+                k++;
+        }
+    }
+    return 0;
+}
+
+static int filter_frame(AVFilterLink *inlink, AVFrame *in)
+{
+    AVFilterContext *ctx = inlink->dst;
+    LatticePalContext *s = ctx->priv;
+    AVFilterLink *outlink = ctx->outputs[0];
+    const int w = in->width, h = in->height;
+    const int ro = s->ro, go = s->go, bo = s->bo, ao = s->ao, pixstep = 
s->pixstep;
+    const int nc = s->nc;
+    uint32_t *pal;
+    AVFrame *out;
+    int ret, reduced = 0;
+
+    out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
+    if (!out) {
+        av_frame_free(&in);
+        return AVERROR(ENOMEM);
+    }
+    ret = av_frame_copy_props(out, in);
+    if (ret < 0)
+        goto fail;
+
+    s->nb_used  = 0;
+    s->nb_alive = 0;
+    if (s->dither == DITHERING_FLOYD_STEINBERG) {
+        memset(s->err[0], 0, (w + 2) * nc * sizeof(*s->err[0]));
+        memset(s->err[1], 0, (w + 2) * nc * sizeof(*s->err[1]));
+    }
+
+    /* first pass: quantize, collect the used colors and their pixel counts */
+    for (int y = 0; y < h; y++) {
+        const uint8_t *src = in->data[0] + y * in->linesize[0];
+        uint32_t *ppos = s->pixpos + (size_t)y * w;
+        int f[4], px[4], pos;
+
+        px[3] = 255;
+
+        if (s->dither == DITHERING_FLOYD_STEINBERG) {
+            /* serpentine scan: odd lines run right to left with mirrored
+             * diffusion weights, avoiding directional drift artifacts */
+            const int dir = (y & 1) ? -1 : 1;
+            const int x0 = dir > 0 ? 0 : w - 1;
+            int32_t *err_cur  = s->err[ y      & 1] + nc;
+            int32_t *err_next = s->err[(y + 1) & 1] + nc;
+
+            memset(err_next - nc, 0, (w + 2) * nc * sizeof(*err_next));
+
+            for (int k = 0; k < w; k++) {
+                const int x = x0 + k * dir;
+                const int32_t *e = &err_cur[x * nc];
+
+                px[0] = av_clip_uint8(src[x * pixstep + ro] + ((e[0] + 8) >> 
4));
+                px[1] = av_clip_uint8(src[x * pixstep + go] + ((e[1] + 8) >> 
4));
+                px[2] = av_clip_uint8(src[x * pixstep + bo] + ((e[2] + 8) >> 
4));
+                if (nc == 4)
+                    px[3] = av_clip_uint8((ao >= 0 ? src[x * pixstep + ao] : 
255) + ((e[3] + 8) >> 4));
+
+                quant_dn(s, px, f);
+                pos = color_inc(s, f);
+                if (pos < 0) {
+                    ret = pos;
+                    goto fail;
+                }
+                ppos[x] = pos;
+
+                for (int i = 0; i < nc; i++) {
+                    const int qerr = px[i] - s->idx2val[f[i]];
+
+                    err_cur [(x + dir) * nc + i] += qerr * 7;
+                    err_next[(x - dir) * nc + i] += qerr * 3;
+                    err_next[ x        * nc + i] += qerr * 5;
+                    err_next[(x + dir) * nc + i] += qerr;
+                }
+            }
+        } else if (s->dither == DITHERING_BAYER || s->dither == 
DITHERING_BLUE_NOISE) {
+            const int blue = s->dither == DITHERING_BLUE_NOISE;
+            const int mask = blue ? VC_MASK : 7;
+            const int rowoff = blue ? (y & VC_MASK) << VC_SHIFT : (y & 7) << 3;
+            const int *dt[4];
+
+            for (int i = 0; i < nc; i++)
+                dt[i] = (blue ? s->blue_dither[i] : s->ordered_dither[i]) + 
rowoff;
+
+            for (int x = 0; x < w; x++) {
+                px[0] = src[x * pixstep + ro] + dt[0][x & mask];
+                px[1] = src[x * pixstep + go] + dt[1][x & mask];
+                px[2] = src[x * pixstep + bo] + dt[2][x & mask];
+                if (nc == 4)
+                    px[3] = (ao >= 0 ? src[x * pixstep + ao] : 255) + dt[3][x 
& mask];
+
+                quant_dn(s, px, f);
+                pos = color_inc(s, f);
+                if (pos < 0) {
+                    ret = pos;
+                    goto fail;
+                }
+                ppos[x] = pos;
+            }
+        } else {
+            for (int x = 0; x < w; x++) {
+                px[0] = src[x * pixstep + ro];
+                px[1] = src[x * pixstep + go];
+                px[2] = src[x * pixstep + bo];
+                if (nc == 4)
+                    px[3] = ao >= 0 ? src[x * pixstep + ao] : 255;
+
+                quant_dn(s, px, f);
+                pos = color_inc(s, f);
+                if (pos < 0) {
+                    ret = pos;
+                    goto fail;
+                }
+                ppos[x] = pos;
+            }
+        }
+    }
+
+    reduced = s->nb_used > s->max_colors;
+    if (reduced) {
+        if (s->refine == REFINE_BATCHED)
+            ret = reduce_batched(s, in);
+        else
+            ret = reduce_colors(s, s->max_colors);
+        if (ret < 0)
+            goto fail;
+        av_log(ctx, AV_LOG_DEBUG,
+               "pts %"PRId64": %d lattice colors used, %d dropped\n",
+               in->pts, s->nb_used, s->nb_used - s->nb_alive);
+    }
+
+    /* Assign palette slots so that each entry is identical or similar to
+     * the same entry of the previous frame: static regions keep their
+     * indices and both the index plane and the palette stay small under
+     * inter prediction. Unclaimed slots keep the previous frame's color.
+     * On the first frame the all black previous palette degenerates this
+     * to insertion order. */
+    {
+        uint8_t claimed[AVPALETTE_COUNT] = { 0 };
+
+        for (int i = 0; i < s->nb_used; i++) {
+            PalEntry *e = &s->list[i];
+            uint32_t c;
+
+            if (!e->alive)
+                continue;
+            e->nn = -1;
+            c = entry_color(s, e);
+            for (int slot = 0; slot < AVPALETTE_COUNT; slot++) {
+                if (!claimed[slot] && s->prev_pal[slot] == c) {
+                    claimed[slot] = 1;
+                    e->nn = slot;
+                    break;
+                }
+            }
+        }
+        for (int i = 0; i < s->nb_used; i++) {
+            PalEntry *e = &s->list[i];
+            uint32_t c;
+            int bslot = -1;
+            int64_t bd = INT64_MAX;
+
+            if (!e->alive || e->nn >= 0)
+                continue;
+            c = entry_color(s, e);
+            for (int slot = 0; slot < AVPALETTE_COUNT; slot++) {
+                const uint32_t p = s->prev_pal[slot];
+                const int da = (int)(p >> 24        ) - (int)(c >> 24        );
+                const int dr = (int)(p >> 16 & 0xff) - (int)(c >> 16 & 0xff);
+                const int dg = (int)(p >>  8 & 0xff) - (int)(c >>  8 & 0xff);
+                const int db = (int)(p       & 0xff) - (int)(c       & 0xff);
+                const int64_t d2 = (int64_t)da * da + dr * dr + dg * dg + db * 
db;
+
+                if (!claimed[slot] && d2 < bd) {
+                    bd    = d2;
+                    bslot = slot;
+                }
+            }
+            claimed[bslot] = 1;
+            e->nn = bslot;
+        }
+
+        pal = (uint32_t *)out->data[1];
+        memcpy(pal, s->prev_pal, AVPALETTE_SIZE);
+        for (int i = 0; i < s->nb_used; i++) {
+            const PalEntry *e = &s->list[i];
+
+            if (e->alive)
+                pal[e->nn] = entry_color(s, e);
+        }
+        memcpy(s->prev_pal, pal, AVPALETTE_SIZE);
+    }
+    if (!(s->refine == REFINE_BATCHED && reduced)) {
+        for (int i = 0; i < s->nb_used; i++) {
+            PalEntry *e = &s->list[i];
+
+            if (!e->alive) {
+                nn_search(s, i);
+                e->nn = s->list[e->nn].nn;
+            }
+        }
+    }
+
+    if (s->refine == REFINE_BATCHED && reduced) {
+        /* pixpos holds the list index assigned by the last rediffusion */
+        for (int y = 0; y < h; y++) {
+            const uint32_t *ppos = s->pixpos + (size_t)y * w;
+            uint8_t *dst = out->data[0] + y * out->linesize[0];
+
+            for (int x = 0; x < w; x++)
+                dst[x] = s->list[ppos[x]].nn;
+        }
+    } else if (s->refine == REFINE_RESIDUAL && reduced) {
+        /* needs the cell table still holding list indices */
+        ret = refine_residual(s, out, in);
+        if (ret < 0)
+            goto fail;
+    } else if (s->refine == REFINE_FULL && reduced) {
+        /* rewrite the cell table to slot + 1, 0 keeps meaning unused */
+        for (int i = 0; i < s->nb_used; i++)
+            s->cell[s->list[i].pos] = s->list[i].nn + 1;
+        ret = refine_full(s, out, in);
+        if (ret < 0)
+            goto fail;
+    } else {
+        /* second pass: write the palette indices */
+        for (int i = 0; i < s->nb_used; i++)
+            s->cell[s->list[i].pos] = s->list[i].nn;
+        for (int y = 0; y < h; y++) {
+            const uint32_t *ppos = s->pixpos + (size_t)y * w;
+            uint8_t *dst = out->data[0] + y * out->linesize[0];
+
+            for (int x = 0; x < w; x++)
+                dst[x] = s->cell[ppos[x]];
+        }
+    }
+
+    /* reset the cell table for the next frame */
+    for (int i = 0; i < s->nb_used; i++)
+        s->cell[s->list[i].pos] = 0;
+    for (int i = 0; i < s->nb_touched; i++)
+        s->cell[s->touched[i]] = 0;
+    s->nb_touched = 0;
+
+    av_frame_free(&in);
+    return ff_filter_frame(outlink, out);
+
+fail:
+    for (int i = 0; i < s->nb_used; i++)
+        s->cell[s->list[i].pos] = 0;
+    for (int i = 0; i < s->nb_touched; i++)
+        s->cell[s->touched[i]] = 0;
+    s->nb_touched = 0;
+    av_frame_free(&in);
+    av_frame_free(&out);
+    return ret;
+}
+
+static int config_input(AVFilterLink *inlink)
+{
+    AVFilterContext *ctx = inlink->dst;
+    LatticePalContext *s = ctx->priv;
+    const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format);
+    const int N = s->density;
+
+    s->pixstep = desc->comp[0].step;
+    s->ro = desc->comp[0].offset;
+    s->go = desc->comp[1].offset;
+    s->bo = desc->comp[2].offset;
+    s->ao = desc->nb_components == 4 && (desc->flags & AV_PIX_FMT_FLAG_ALPHA)
+            ? desc->comp[3].offset : -1;
+    s->nc = s->alpha ? 4 : 3;
+
+    if (s->alpha && N > MAX_DENSITY_ALPHA) {
+        av_log(ctx, AV_LOG_ERROR,
+               "density is limited to %d when the alpha channel is 
quantized\n",
+               MAX_DENSITY_ALPHA);
+        return AVERROR(EINVAL);
+    }
+
+    s->scale = N / 255.f;
+    for (int i = 0; i <= N; i++)
+        s->idx2val[i] = lrintf(i * 255.f / N);
+
+    for (int i = 0; i < AVPALETTE_COUNT; i++)
+        s->prev_pal[i] = 0xFF000000;
+
+    s->dim = N + 1;
+    s->min_gap = 255;
+    for (int i = 1; i <= N; i++)
+        s->min_gap = FFMIN(s->min_gap, s->idx2val[i] - s->idx2val[i - 1]);
+
+    av_freep(&s->cell);
+    s->cell = av_calloc(s->nc == 4 ? (size_t)s->dim * s->dim * s->dim * s->dim
+                                   : (size_t)s->dim * s->dim * s->dim,
+                        sizeof(*s->cell));
+    av_freep(&s->pixpos);
+    s->pixpos = av_malloc_array((size_t)inlink->w * inlink->h, 
sizeof(*s->pixpos));
+    if (!s->cell || !s->pixpos)
+        return AVERROR(ENOMEM);
+
+    if (s->dither == DITHERING_BAYER) {
+        /* The per-channel period of the scaled D3 lattice is 2*step, not
+         * step: translating the lattice by step*e_i flips the parity
+         * constraint, only 2*step*e_i maps it onto itself. The dither
+         * offsets must span one period for intermediate colors to be
+         * reproduced on average, hence the amplitude of 2*step.
+         *
+         * The matrix and the per-channel decorrelation follow the classic
+         * swscale ordered dither (libswscale/yuv2rgb.c): all channels use
+         * ff_dither_8x8_73, green shifted by one column and blue with
+         * flipped rows. The channel patterns are strongly anti-correlated
+         * (-0.94 R/G, -0.60 R/B), which keeps the offset sum and with it
+         * the luminance noise small, and it interacts well with the D3
+         * quantizer: over a 40..215 color cube this scheme measures the
+         * smallest flat field bias (max 8.3, mean 3.5) and the smallest
+         * luminance RMS noise (15.3) of all evaluated 8x8 schemes. With
+         * alpha, the fourth channel combines both transforms. */
+        const float amp = 2.f * 255.f / N;
+        for (int y = 0; y < 8; y++) {
+            for (int x = 0; x < 8; x++) {
+                const int i = y << 3 | x;
+                s->ordered_dither[0][i] = lrintf(((dither_8x8_73[y][x]         
      + 0.5f) / 64.f - 0.5f) * amp);
+                s->ordered_dither[1][i] = lrintf(((dither_8x8_73[y][(x + 1) & 
7]     + 0.5f) / 64.f - 0.5f) * amp);
+                s->ordered_dither[2][i] = lrintf(((dither_8x8_73[y ^ 7][x]     
      + 0.5f) / 64.f - 0.5f) * amp);
+                s->ordered_dither[3][i] = lrintf(((dither_8x8_73[y ^ 7][(x + 
1) & 7] + 0.5f) / 64.f - 0.5f) * amp);
+            }
+        }
+    } else if (s->dither == DITHERING_BLUE_NOISE) {
+        /* Dither in a lattice adapted frame: step*(1,1,0), step*(1,-1,0)
+         * and 2*step*(0,0,1) span an orthogonal sublattice of the color
+         * lattice, so offsets drawn uniformly from its fundamental box tile
+         * the color space under lattice translations, and since Dn Voronoi
+         * cells are centrally symmetric the dithered average is exactly the
+         * input color. Independently seeded void-and-cluster masks
+         * a, b, c (and d with alpha) drive the axes:
+         *     tR = step * (a + b - 1)
+         *     tG = step * (a - b)
+         *     tB = step * (2c - 1)
+         * Compared with independent per-channel offsets over the axis
+         * aligned (-step,step)^3 box (index 4 instead of 2) this cuts the
+         * red/green noise variance in half; the remaining full range axis
+         * is assigned to blue, the perceptually least weighted channel.
+         * For D4 the sublattice step*{(1,1,0,0), (1,-1,0,0), (0,0,1,1),
+         * (0,0,1,-1)} is orthogonal with equally long axes, so all four
+         * channels get the halved variance:
+         *     tB = step * (c + d - 1)
+         *     tA = step * (c - d) */
+        const float step = 255.f / N;
+        float kern[VC_KSIZE * VC_KSIZE];
+        uint16_t *ranks[4] = { NULL };
+        uint8_t  *bits = NULL;
+        float *energy  = NULL, *e1 = NULL;
+        int ret = 0;
+
+        for (int dy = -VC_RADIUS; dy <= VC_RADIUS; dy++)
+            for (int dx = -VC_RADIUS; dx <= VC_RADIUS; dx++)
+                kern[(dy + VC_RADIUS) * VC_KSIZE + dx + VC_RADIUS] =
+                    expf(-(dx * dx + dy * dy) / (2 * VC_SIGMA * VC_SIGMA));
+
+        bits   = av_malloc_array(VC_AREA, sizeof(*bits));
+        energy = av_malloc_array(VC_AREA, sizeof(*energy));
+        e1     = av_malloc_array(VC_AREA, sizeof(*e1));
+        ret = !bits || !energy || !e1 ? AVERROR(ENOMEM) : 0;
+
+        for (int p = 0; p < s->nc && ret >= 0; p++) {
+            AVLFG lfg;
+
+            ranks[p] = av_malloc_array(VC_AREA, sizeof(*ranks[p]));
+            if (!s->blue_dither[p])
+                s->blue_dither[p] = av_malloc_array(VC_AREA, 
sizeof(*s->blue_dither[p]));
+            if (!ranks[p] || !s->blue_dither[p]) {
+                ret = AVERROR(ENOMEM);
+                break;
+            }
+            av_lfg_init(&lfg, 0xB1DE + p);
+            vc_generate(ranks[p], bits, energy, e1, kern, &lfg);
+        }
+
+        if (ret >= 0) {
+            for (int i = 0; i < VC_AREA; i++) {
+                const float a = (ranks[0][i] + 0.5f) / VC_AREA;
+                const float b = (ranks[1][i] + 0.5f) / VC_AREA;
+                const float c = (ranks[2][i] + 0.5f) / VC_AREA;
+
+                s->blue_dither[0][i] = lrintf(step * (a + b - 1.f));
+                s->blue_dither[1][i] = lrintf(step * (a - b));
+                if (s->nc == 4) {
+                    const float dd = (ranks[3][i] + 0.5f) / VC_AREA;
+
+                    s->blue_dither[2][i] = lrintf(step * (c + dd - 1.f));
+                    s->blue_dither[3][i] = lrintf(step * (c - dd));
+                } else {
+                    s->blue_dither[2][i] = lrintf(step * (2.f * c - 1.f));
+                }
+            }
+        }
+
+        av_freep(&ranks[0]);
+        av_freep(&ranks[1]);
+        av_freep(&ranks[2]);
+        av_freep(&ranks[3]);
+        av_freep(&bits);
+        av_freep(&energy);
+        av_freep(&e1);
+        if (ret < 0)
+            return ret;
+    }
+    if (s->dither == DITHERING_FLOYD_STEINBERG || s->refine) {
+        for (int i = 0; i < 2; i++) {
+            av_freep(&s->err[i]);
+            s->err[i] = av_calloc(inlink->w + 2, s->nc * sizeof(*s->err[i]));
+            if (!s->err[i])
+                return AVERROR(ENOMEM);
+        }
+    }
+
+    return 0;
+}
+
+static av_cold void uninit(AVFilterContext *ctx)
+{
+    LatticePalContext *s = ctx->priv;
+
+    av_freep(&s->err[0]);
+    av_freep(&s->err[1]);
+    for (int i = 0; i < 4; i++)
+        av_freep(&s->blue_dither[i]);
+    av_freep(&s->cell);
+    av_freep(&s->pixpos);
+    av_freep(&s->list);
+    av_freep(&s->alive_arr);
+    av_freep(&s->alive_pos);
+    av_freep(&s->touched);
+}
+
+static const AVFilterPad latticepal_inputs[] = {
+    {
+        .name         = "default",
+        .type         = AVMEDIA_TYPE_VIDEO,
+        .filter_frame = filter_frame,
+        .config_props = config_input,
+    },
+};
+
+const FFFilter ff_vf_latticepal = {
+    .p.name        = "latticepal",
+    .p.description = NULL_IF_CONFIG_SMALL("Convert RGB to PAL8 using a 
per-frame FCC lattice palette."),
+    .p.priv_class  = &latticepal_class,
+    .priv_size     = sizeof(LatticePalContext),
+    .uninit        = uninit,
+    FILTER_INPUTS(latticepal_inputs),
+    FILTER_OUTPUTS(ff_video_default_filterpad),
+    FILTER_QUERY_FUNC2(query_formats),
+};
diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak
index 7c5c15ff46..f5f5096f2b 100644
--- a/tests/fate/filter-video.mak
+++ b/tests/fate/filter-video.mak
@@ -558,6 +558,14 @@ fate-filter-colorbalance-gbrap: CMD = framecrc -c:v pgmyuv 
-i $(SRC) -vf scale,f
 fate-filter-colorbalance-rgba64: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
scale,format=rgba64,colorbalance=rm=.2,scale -pix_fmt rgba64le -flags +bitexact 
-sws_flags +accurate_rnd+bitexact -frames:v 3
 fate-filter-colorbalance-gbrap-16: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
scale,format=gbrap,colorbalance=bh=.2 -pix_fmt gbrap -flags +bitexact 
-sws_flags +accurate_rnd+bitexact -frames:v 3
 
+FATE_FILTER_VSYNTH_PGMYUV-$(call ALLYES, SCALE_FILTER FORMAT_FILTER 
LATTICEPAL_FILTER) += fate-filter-latticepal fate-filter-latticepal-bayer 
fate-filter-latticepal-maxcolors fate-filter-latticepal-refine 
fate-filter-latticepal-residual fate-filter-latticepal-batched
+fate-filter-latticepal: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
scale,format=rgb24,latticepal=density=32:dither=floyd_steinberg -flags 
+bitexact -sws_flags +accurate_rnd+bitexact -frames:v 5
+fate-filter-latticepal-bayer: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
scale,format=rgb24,latticepal=density=7:dither=bayer -flags +bitexact 
-sws_flags +accurate_rnd+bitexact -frames:v 5
+fate-filter-latticepal-maxcolors: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
scale,format=rgb24,latticepal=density=32:max_colors=100:dither=none -flags 
+bitexact -sws_flags +accurate_rnd+bitexact -frames:v 5
+fate-filter-latticepal-refine: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
scale,format=rgb24,latticepal=density=32:dither=none:refine=full -flags 
+bitexact -sws_flags +accurate_rnd+bitexact -frames:v 5
+fate-filter-latticepal-residual: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
scale,format=rgb24,latticepal=density=32:dither=bayer:refine=residual -flags 
+bitexact -sws_flags +accurate_rnd+bitexact -frames:v 5
+fate-filter-latticepal-batched: CMD = framecrc -c:v pgmyuv -i $(SRC) -vf 
scale,format=rgb24,latticepal=density=32:dither=floyd_steinberg:refine=batched 
-flags +bitexact -sws_flags +accurate_rnd+bitexact -frames:v 5
+
 FATE_FILTER_VSYNTH_VIDEO_FILTER-$(CONFIG_COLORMATRIX_FILTER) += 
fate-filter-colormatrix1 fate-filter-colormatrix2
 fate-filter-colormatrix1: CMD = video_filter 
"colormatrix=bt601:smpte240m,colormatrix=smpte240m:fcc,colormatrix=fcc:bt601,colormatrix=bt601:fcc,colormatrix=fcc:smpte240m,colormatrix=smpte240m:bt709"
 fate-filter-colormatrix2: CMD = video_filter 
"colormatrix=bt709:fcc,colormatrix=fcc:bt709,colormatrix=bt709:bt601,colormatrix=bt601:bt709,colormatrix=bt709:smpte240m,colormatrix=smpte240m:bt601"
diff --git a/tests/ref/fate/filter-latticepal b/tests/ref/fate/filter-latticepal
new file mode 100644
index 0000000000..445c409586
--- /dev/null
+++ b/tests/ref/fate/filter-latticepal
@@ -0,0 +1,10 @@
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 352x288
+#sar 0: 0/1
+0,          0,          0,        1,   102400, 0x877cb273
+0,          1,          1,        1,   102400, 0xe970a89a
+0,          2,          2,        1,   102400, 0xd8527cad
+0,          3,          3,        1,   102400, 0xd4d06b6c
+0,          4,          4,        1,   102400, 0x87ce0311
diff --git a/tests/ref/fate/filter-latticepal-batched 
b/tests/ref/fate/filter-latticepal-batched
new file mode 100644
index 0000000000..929aa7abf9
--- /dev/null
+++ b/tests/ref/fate/filter-latticepal-batched
@@ -0,0 +1,10 @@
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 352x288
+#sar 0: 0/1
+0,          0,          0,        1,   102400, 0xa7e0c77a
+0,          1,          1,        1,   102400, 0x2512e652
+0,          2,          2,        1,   102400, 0x9261aa51
+0,          3,          3,        1,   102400, 0x638ae5d5
+0,          4,          4,        1,   102400, 0x0dc608c9
diff --git a/tests/ref/fate/filter-latticepal-bayer 
b/tests/ref/fate/filter-latticepal-bayer
new file mode 100644
index 0000000000..4c5bb3a595
--- /dev/null
+++ b/tests/ref/fate/filter-latticepal-bayer
@@ -0,0 +1,10 @@
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 352x288
+#sar 0: 0/1
+0,          0,          0,        1,   102400, 0x8a03a4f2
+0,          1,          1,        1,   102400, 0x9f269a8b
+0,          2,          2,        1,   102400, 0x04ee0c90
+0,          3,          3,        1,   102400, 0x4ca5336a
+0,          4,          4,        1,   102400, 0x6b760d10
diff --git a/tests/ref/fate/filter-latticepal-maxcolors 
b/tests/ref/fate/filter-latticepal-maxcolors
new file mode 100644
index 0000000000..148d56d9d6
--- /dev/null
+++ b/tests/ref/fate/filter-latticepal-maxcolors
@@ -0,0 +1,10 @@
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 352x288
+#sar 0: 0/1
+0,          0,          0,        1,   102400, 0xb2e4f091
+0,          1,          1,        1,   102400, 0xd288d452
+0,          2,          2,        1,   102400, 0x237b7995
+0,          3,          3,        1,   102400, 0xfe817f1a
+0,          4,          4,        1,   102400, 0xca619eb0
diff --git a/tests/ref/fate/filter-latticepal-refine 
b/tests/ref/fate/filter-latticepal-refine
new file mode 100644
index 0000000000..ed77d1a29b
--- /dev/null
+++ b/tests/ref/fate/filter-latticepal-refine
@@ -0,0 +1,10 @@
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 352x288
+#sar 0: 0/1
+0,          0,          0,        1,   102400, 0x60154dde
+0,          1,          1,        1,   102400, 0xd43dbb43
+0,          2,          2,        1,   102400, 0x298ae007
+0,          3,          3,        1,   102400, 0x50fdaf5d
+0,          4,          4,        1,   102400, 0x96c55d58
diff --git a/tests/ref/fate/filter-latticepal-residual 
b/tests/ref/fate/filter-latticepal-residual
new file mode 100644
index 0000000000..85ee5b03f5
--- /dev/null
+++ b/tests/ref/fate/filter-latticepal-residual
@@ -0,0 +1,10 @@
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 352x288
+#sar 0: 0/1
+0,          0,          0,        1,   102400, 0x784e6f8e
+0,          1,          1,        1,   102400, 0x43c7d662
+0,          2,          2,        1,   102400, 0x74ecb859
+0,          3,          3,        1,   102400, 0xc03929c3
+0,          4,          4,        1,   102400, 0xbad660b1

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

Reply via email to