[RFC] Premultiplied filter audit

Michael Niedermayer via ffmpeg-devel <[email protected]> Wed, 5 Aug 2026 20:31:01 +0200
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <anOBZe19dYgSD_Fa@neo>
--===============4295538142443366300==
Content-Type: multipart/signed; micalg=pgp-sha512;
	protocol="application/pgp-signature"; boundary="mop2RHMJqvaA9k+U"
Content-Disposition: inline


--mop2RHMJqvaA9k+U
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
Content-Transfer-Encoding: quoted-printable

Hi everyone

I have used claude (Fable 5) for auditing our filters for premultiplied sup=
port
Niklas, has written the Prompt (see below)
and ive used claude to create a table based on it and run it over the filte=
rs starting with a*
ive also run an adverserical agent over it trying to poke holes in the choi=
ces

My main question to you is, should we change something on the prompt or des=
ign?
Should we run this over all the filters ? (that is, will this give us enoug=
h confidence on what to set each filter to)

Prompt:
    Go through the list of filters in libavfilter/ and categorize them base=
d on how they would interact with premultiplied alpha.

    Categorize filters into the following types:
    * Color-agnostic filters that don't touch or read the pixel data at all=
 (e.g. truly metadata-only filters)
    * Metadata-only filters that read, but don't write, pixel values (e.g. =
measuring a histogram). Subcategorize as best you can into filters that wou=
ld prefer premul, prefer independent, or genuinely don't care. (Though note=
 that this is ultimately a subjective judgement call in many cases)
    * Filters modify pixel data in a way that's truly invariant under alpha=
 premultiplication (e.g. applying a linear scaling factor or swapping RGB c=
hannels), and thus permute under (premultiply,filter) -> (filter,premultipl=
y)
    * "Alpha-dependent" filters that inherently preserve the rgb <=3D alpha=
 property, and would thus work with either alpha mode, but may give differe=
nt results under premultiplied vs nonpremultiplied by virtue of some non-li=
nearity that affects how pixel values are processed. These would be filters=
 that *don't* commute under premultiplication. This includes e.g. scaling f=
ilters, crossfades, and other "linear"-type functions of several pixels tha=
t mix channels according to the same relative weights, as well as anything =
that applies a monotonic (non-linear) function to all channels (including A=
). Again, subcaterorize them as best you can into their alpha mode preferen=
ce. (e.g. scaling and crossfading should always be done with premultiplied =
alpha, to avoid e.g. bleeding transparent pink padding pixels into actual p=
ixel data)
    * Premul-incompatible filters that would definitely give broken results=
 (e.g. rgb > alpha) when fed premultiplied pixel values. This would include=
 any filter that does some nontrivial per-channel processing that might bre=
ak the alpha-scaling, e.g. a brightening filter that adds some offset to th=
e RGB values, or applying a nonlinear transfer function to only the R,G,B c=
hannels while leaving the A channel intact.

    For filters that may span multiple types depending on the selected opti=
ons, list them under both headings; and clarify under which condition/optio=
n it belongs to that category. Flag edge cases and filters you're unsure ab=
out explicitly for human review.

Adverserial agent finding:
  1. alphamerge =E2=80=94 wrong mechanism and wrong I/O. The straight-alpha=
 pin is on the output only and does not propagate to the inputs: a premulti=
plied main input is negotiated as premul and then merely relabeled straight=
 in do_alphamerge =E2=80=94 no
  unpremultiply is inserted upstream, contrary to what the row claimed. Als=
o the filter has two video inputs, so I/O is now VV->V, not V->V.
  2. avgblur-family E conditions were proper subsets of the truth. Not just=
 "planes without bit 3": any mask that blurs A and color unequally (e.g. pl=
anes=3D8, alpha blurred under sharp premul RGB) is E; planes=3D0xF is the o=
nly D mask. All three D/E row
  pairs reworded.
  3. avgblur_vulkan "OOB reads return 0" was unsupported. FFmpeg enables no=
 Vulkan robustness feature, so out-of-bounds imageLoad is undefined per spe=
c; zero-return is common driver behavior, not a guarantee. Row now says so =
and keeps a human-review
  flag.
  4. Header convention gap: Alpha pref was undefined for E rows =E2=80=94 n=
ow documented as "the only mode in which the filter is safe".

And the current results
# libavfilter video filters: premultiplied-alpha survey

Every filter from libavfilter/allfilters.c with a video INPUT (prefixes vf,
vaf, vsink), FFmpeg master 88bc27643a, 392 filters. Pure sources and
audio-to-video visualizers are excluded.

The I/O column is derived from the extern symbol prefix; a few multi-media
filters carry more streams than shown (e.g. concat is N media in/out).

Categories (how the filter behaves on premultiplied-alpha input):

- A-agnostic:  does not read or write pixel data at all (truly metadata-onl=
y)
- B-meta:      reads but never writes pixels (measurements); Alpha pref say=
s on
               which representation the measurement is more meaningful
- C-invariant: pixel changes commute with premultiplication
               (premultiply,filter) =3D=3D (filter,premultiply)
- D-alpha-dep: preserves rgb <=3D alpha, works in either mode, but does NOT
               commute (resampling/mixing with shared weights, monotonic
               functions applied to all channels incl. A); Alpha pref says
               which mode gives the better result (e.g. scaling: premul)
- E-incompat:  can produce broken results (rgb > alpha) on premultiplied in=
put
               (per-channel offsets/nonlinearities on RGB with A untouched,
               writing A independently of RGB)
- sink:        consumes frames

Option-dependent filters get one row per category with the condition in Not=
es.

Columns:

- Alpha formats: whether the filter accepts/produces pixel formats with an
  alpha channel at all ("none" =3D alpha never passes through it)
- Alpha pref:  n/a | premul | straight | dont-care | option-dependent
               (for E rows: the only mode in which the filter is safe)
- Confidence:  high | medium | low (how sure the categorization is)
- Checked:     empty until a human or an adversarial agent verified the row
| Filter              | I/O        | Alpha formats                | Categor=
y    | Alpha pref       | Confidence | Checked          | Notes
|---------------------|------------|------------------------------|--------=
-----|------------------|------------|------------------|-------
| addroi              | V->V       | all (no constraint)          | A-agnos=
tic  | n/a              | high       | agent 2026-08-05 | edits ROI side da=
ta only; AVFILTER_FLAG_METADATA_ONLY, never touches frame->data
| alphaextract        | V->V       | all alpha fmts               | C-invar=
iant | dont-care        | high       | agent 2026-08-05 | copies the A plan=
e only, output has no alpha -- premultiplied input changes nothing; graph s=
till pins input straight (constraint shared with extractplanes)
| alphamerge          | VV->V      | YUVA420/22/44,GBRAP,RGBA-fam | E-incom=
pat  | straight         | high       | agent 2026-08-05 | overwrites A verb=
atim from 2nd input luma, RGB untouched -> rgb>alpha on premul; pins alpha_=
mode=3Dstraight on the OUTPUT only -- the pin does not propagate to the inp=
uts, a premul main input is just relabeled straight (no conversion inserted)
| amplify             | V->V       | planar YUVA/GBRAP 8-16,f32   | E-incom=
pat  | straight         | high       | agent 2026-08-05 | temporal push fro=
m mean, threshold-gated per plane; default planes=3D7 leaves A untouched; n=
onlinear per-plane even with planes=3D15 -> still E
| ass                 | V->V       | all alpha fmts (drawutils)   | E-incom=
pat  | straight         | high       | agent 2026-08-05 | default alpha=3D0=
: A plane excluded from blending, subtitle RGB written under transparent pr=
emul px -> rgb>alpha; harmless on opaque video
| ass                 | V->V       | all alpha fmts (drawutils)   | D-alpha=
-dep | option-dependent | high       | agent 2026-08-05 | alpha=3D1: A blen=
ded, src color premultiplied when negotiated mode=3Dpremul; blend carries a=
_c^2 term (non-textbook OVER, exact only for opaque subtitle colors) -- con=
firmed in drawutils blend_pixel/ff_blend_mask
| atadenoise          | V->V       | planar YUVA/GBRAP 8-16       | E-incom=
pat  | straight         | high       | agent 2026-08-05 | default planes=3D=
7: color denoised per-plane (frame subsets differ per channel), A copied ->=
 rgb>alpha reachable; planes=3D15: plane-3 ptrs never filled -> NULL deref,=
 confirmed by 2nd independent code read, never executed -- human test wanted
| avgblur             | V->V       | planar YUVA/GBRAP 8-16       | D-alpha=
-dep | premul           | high       | agent 2026-08-05 | planes=3D0xF (def=
ault) is the ONLY D mask: box blur, uniform weights shared across all plane=
s incl. A; 420/422 chroma footprint differs from A plane
| avgblur             | V->V       | planar YUVA/GBRAP 8-16       | E-incom=
pat  | straight         | high       | agent 2026-08-05 | any mask blurring=
 A and color unequally (e.g. 7: A stays sharp; 8: only A blurred) -> rgb>al=
pha at edges
| avgblur_opencl      | V->V       | hw (device-dependent)        | D-alpha=
-dep | premul           | medium     | agent 2026-08-05 | planes=3D0xF (def=
ault) is the ONLY D mask: uniform box kernel per plane, edge-renormalized; =
packed RGBA is one image, A never blurred separately (bit 0 gates the whole=
 pixel)
| avgblur_opencl      | V->V       | hw (device-dependent)        | E-incom=
pat  | straight         | medium     | agent 2026-08-05 | any mask blurring=
 a planar alpha plane and color unequally (e.g. 7 or 8 on GBRAP) -> rgb>alp=
ha
| avgblur_vulkan      | V->V       | hw (device-dependent)        | D-alpha=
-dep | premul           | medium     | agent 2026-08-05 | planes=3D0xF (def=
ault) is the ONLY D mask: shared filter_norm for all channels; border image=
Load is OOB and FFmpeg enables no Vulkan robustness -- undefined per spec, =
common driver zero-return hits RGB and A equally (invariant kept if so) -- =
human review
| avgblur_vulkan      | V->V       | hw (device-dependent)        | E-incom=
pat  | straight         | medium     | agent 2026-08-05 | any mask blurring=
 a planar alpha plane and color unequally (e.g. 7 or 8 on GBRAP) -> rgb>alp=
ha
| backgroundkey       | V->V       |                              |        =
     |                  |            |                  |
| bbox                | V->V       |                              |        =
     |                  |            |                  |
| bench               | V->V       |                              |        =
     |                  |            |                  |
| bilateral           | V->V       |                              |        =
     |                  |            |                  |
| bilateral_cuda      | V->V       |                              |        =
     |                  |            |                  |




--=20
Michael     GnuPG fingerprint: 9FF2128B147EF6730BADF133611EC787040B0FAB

Take away the freedom of one citizen and you will be jailed, take away
the freedom of all citizens and you will be congratulated by your peers
in Parliament.

--mop2RHMJqvaA9k+U
Content-Type: application/pgp-signature; name="signature.asc"

-----BEGIN PGP SIGNATURE-----

iF0EABEKAB0WIQSf8hKLFH72cwut8TNhHseHBAsPqwUCanOBYQAKCRBhHseHBAsP
q2y4AJ9FcunVUqss/wV4nZJMj1hhDUcpEgCglsvyOmLoAZPCKF9ojpUZwCpOg4M=
=Hvks
-----END PGP SIGNATURE-----

--mop2RHMJqvaA9k+U--

--===============4295538142443366300==
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

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

--===============4295538142443366300==--