ipu_dither gets a NEON arm. The four channels come apart in the load
itself, so the unpack chain the SSE2 path needs has no counterpart here,
and the dither stays a saturating add and subtract per pixel -- the two are
never both non-zero for the same pixel, so together they are the clamped
signed add the scalar form spells out.
The aarch64 lane holds the same pinned hashes as the x86 tiers, and the
x86 objects are byte-identical, so nothing outside ARM moves.
ipu_csc and ipu_vq move to ipu_csc.c, taking the thresholds and the CLUT
as arguments the way yuv2rgb already takes its macroblocks, so neither
reaches for the decoder or the register file and both can be driven on
their own.
The threshold and sign passes walk the pixels through the named channel
fields rather than a u8 cursor cast to u32. That drops the aliasing
violation, and it also makes the sign flip mean the three colour channels
everywhere: as a word it named the low three bytes, which is red, green
and blue only while the host is little-endian.
tests/ipu/ipu_kernel_hash.cpp grows a scalar model for each and pins them,
sweeping both thresholds including the disabled cases, both signs, and
CLUTs seeded with duplicate entries so the tie rule is reached. The CLUT
draws from a stream of its own, so the three hashes pinned before it
existed still hold. All seven lanes agree, and 40000 blocks match the
previous implementation exactly.
Codegen is level rather than better -- 1335 memory operands become 1334 at
SSE4.1 -- which is what these are worth, since a CSC or PACK command runs
once per macroblock at most.
yuv2rgb takes the macroblocks as arguments rather than reaching into the
decoder, which is the shape ipu_csc already had and passed through: both
its call sites bind to decoder.mb8 and decoder.rgb32, so nothing moves.
That drops yuv2rgb's text from 494 bytes to 458 and reads marginally
faster -- a pointer in a register beats re-materialising the global at
every access.
Neither kernel then needs the decoder at all, so the macroblock types
move to ipu_macroblock.h and the two sources build as C: no C++ casts, no
reference bindings, C89 declaration placement, and the dither keeps its
arguments const.
All seven builds hold the pinned hashes, ASan and UBSan are quiet, and an
interleaved best-of-9 puts C and C++ on the same numbers.
Both are hand-written SSE2 and measure the same at every tier -- an
interleaved best-of-9 puts sse4.1, avx and avx2 within 2% of each other --
so they leave the per-ISA source list and are compiled once. IPU_MultiISA
keeps its three builds.
They also stop including Common.h, IPU.h and IPUdma.h, none of which they
reference; yuv2rgb needs the decoder and the dither needs only its
arguments.
The kernel harness holds all seven builds to the same three hashes.
yuv2rgb and ipu_dither each carry a SIMD path and a scalar path that are
meant to agree, and nothing checked that they did. The harness drives both
over macroblocks seeded at the clamps and compares every block against a
model written from what the operation means rather than lifted from either
path, so a mistake shared by both is still caught.
Seven builds land on the same three hashes: gcc and clang at -msse2,
-msse4.1 and -mavx2, plus an aarch64 lane under qemu. That last one is
where the scalar dither is live -- on x86 its branch is dead code and can
drift unnoticed, which changing a coefficient shows: the x86 lanes stay
green and aarch64 fails.