diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 9596d46aac..423aea37e8 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -851,6 +851,7 @@ Encoder verification contract: - [x] Filter-intra mode decision now runs after ordinary luma modes in current-libaom order, evaluates all five recursive predictors, and refines each predictor across every legal 8x8 transform in transform-enum order. Strictly-better replacement preserves ordinary-mode and filter-mode tie order. Each filter prediction and its source residual are prepared once and reused across transform candidates, avoiding repeated recursive prediction while retaining SIMD-first predictor and subtraction operators. The stack cost is 192 bytes for eight-bit samples or 256 bytes for high-bit-depth samples; no allocator owner or managed buffer was added. Fifteen production cases force every filter mode at 8, 10, and 12 bits and prove retained filter syntax, zero-residual reconstruction, and the DCT-DCT equal-cost transform tie. The decoded-frame MD5 values selected by this checkpoint are `d7d68803763b95827483f14515281d3a` for the 8x8 10-bit gradient, `3f7e34d44c65d7797ad26b5cd4c35bf4` for the 8x8 12-bit gradient, and `9985f05790d2c9f5f28723ef86d5b89b`, `2ba2f1d0fcfef60394a5175553c7cb8b`, and `6aa7a2ed0dbf76ad2ec0c222585272d0` for the odd 4:2:0, 4:2:2, and 4:4:4 gradients. The exact net11 Release rebuild remains at 1,005 warnings and zero errors, 18 focused filter-intra, predictor-reference, syntax-cost, and allocation cases pass, all 8,974 HEIF/AV1 tests pass, and current-main `aomdec` at `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727` accepts all 29 regenerated payloads. - [x] Empty-transform block skip now compares the complete live rate of the two decoder-identical syntax choices after luma and every coded chroma plane have been selected. Current libaom forces all-intra blocks to non-skip; this encoder retains that behavior for every non-empty block and for equal-cost empty blocks, but emits block skip when its adapted context cost is strictly lower than non-skip plus all empty-transform coefficient costs. Costing and writing share the same above-and-left skip-context calculation, and the coefficient estimator returns after the transform-block-skip symbol without reading coefficient storage. This adds no allocation, copy, or persistent state. A focused adapted-CDF regression proves both outcomes through the production decision helper, the two production all-zero fixtures still prove the default real block path, the exact net11 Release rebuild remains at 1,005 warnings and zero errors, all 8,975 HEIF/AV1 tests pass, and current-main `aomdec` at `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727` accepts all 29 regenerated payloads. - [~] Palette entropy coding now mirrors current libaom's adaptive luma-mode, chroma-mode, palette-size, and spatial color-index distributions, together with its truncated-binary uniform code used by palette colors. The complete mutable palette probability graph is created once on first palette search or write, so the current palette-disabled frame path retains zero palette allocations. Three focused regressions cover every legal 2-through-8 color alphabet and every defined mode, size, and color-index context; all 1,928 entropy cases and all 8,978 HEIF/AV1 cases pass direct net11 Release VSTest. The exact Release rebuild remains at 1,005 warnings and zero errors. This checkpoint adds the exact entropy foundation only: palette candidate generation, retained color and index storage, mode decision, map tokenization, and production syntax remain incomplete, and no generated payload changed. +- [~] Luma and chroma palette-color coding now matches current libaom's neighbor-cache flags, sorted delta representation, wrapped V-plane deltas, strict delta-versus-raw V selection, and fixed-point color-rate model at 8, 10, and 12 bits. Encoder costing and emission use only fixed stack spans, including explicitly initialized cache-membership state, and steady-state color costing allocates zero managed bytes. The decoder consumes the same bounded color-syntax primitive after the tile reader derives its neighbor cache, removing duplicated color parsing without changing retained palette ownership. Nine focused syntax, exact palette decode, constrained-allocation, truncation, presentation, and allocation cases pass; all 1,933 entropy cases and all 8,983 HEIF/AV1 cases pass direct net11 Release VSTest. The exact Release rebuild remains at 1,005 warnings and zero errors. Retained encoder palette colors, neighbor caches, color-index maps, candidate generation, and production palette selection remain incomplete, and the compact 8-byte frame mode entries were not enlarged. - [x] The expanded checkpoint exposed a pre-existing transform-block test that asserted uninitialized pooled padding was zero. The test now initializes the complete physical luma plane with a sentinel and proves the block operation leaves both adjacent padding samples unchanged. The exact net11 Release rebuild remains at 1,005 baseline warnings and zero errors, the focused allocator-order set passes 30 of 30 cases, and the complete HEIF/AV1 namespace passes 8,859 of 8,859 direct VSTest cases with zero failures or skips. - [x] Combined-frame OBU output now counts the byte-aligned frame and tile-group headers, non-final tile-size fields, and owned tile payloads before emitting the OBU size. It retains only the small allocator-owned header scratch and writes each entropy-coded tile span directly from its detached owner, removing the second file-sized allocator rent and complete-payload copy. A 64 KiB regression proves exactly one sub-payload-sized byte rent with a balanced return and verifies the exact streamed tile tail; the existing two-tile round trip proves size-prefix and ordering parity. The focused writer and production-frame set passes 32 of 32 direct net11 VSTest cases, current-main `aomdec` accepts all 29 generated native-format payloads, and the complete HEIF/AV1 namespace passes 8,860 of 8,860 cases with zero failures or skips. - [x] Finalized fixed-block decisions now set the block-level transform-skip flag only when every retained luma and coded chroma transform has zero EOB, matching current libaom's conjunction of per-plane skip state. The previous always-false flag produced legal but redundant non-skip and zero-coefficient syntax. Monochrome and 4:2:0 regressions prove both branches from actual coefficient state; the focused decision and production-frame set passes 32 of 32 direct net11 VSTest cases. Current-main `aomdec` accepts all 29 regenerated payloads, the recorded decoded-frame MD5s are unchanged, and affected 16x16 constant 8-bit and 10-bit payloads are one byte smaller. The complete HEIF/AV1 namespace passes 8,862 of 8,862 cases with zero failures or skips. diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs index 2179d6788e..eeaceaab54 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs @@ -110,6 +110,141 @@ internal ref struct Av1SymbolDecoder return (value << 1) - threshold + r.ReadLiteral(1); } + /// + /// Reads sorted luma palette colors using selections from the neighboring color cache. + /// + /// The sorted unique colors inherited from eligible neighbors. + /// The number of palette colors. + /// The number of bits in each color sample. + /// The destination palette colors in prediction-index order. + public void ReadPaletteYColors( + scoped ReadOnlySpan colorCache, + int paletteSize, + int bitDepth, + scoped Span colors) + { + Span cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int colorIndex = 0; + for (int i = 0; i < colorCache.Length && colorIndex < paletteSize; i++) + { + if (this.ReadLiteral(1) != 0) + { + cachedColors[colorIndex++] = colorCache[i]; + } + } + + if (colorIndex == paletteSize) + { + cachedColors[..paletteSize].CopyTo(colors); + return; + } + + int cachedColorCount = colorIndex; + colors[colorIndex++] = (ushort)this.ReadLiteral(bitDepth); + if (colorIndex < paletteSize) + { + int bits = bitDepth - 3 + this.ReadLiteral(2); + int maximumColor = (1 << bitDepth) - 1; + int range = maximumColor - colors[colorIndex - 1]; + for (; colorIndex < paletteSize; colorIndex++) + { + int delta = this.ReadLiteral(bits) + 1; + colors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, colors[colorIndex - 1] + delta); + range -= colors[colorIndex] - colors[colorIndex - 1]; + bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); + } + } + + MergePaletteColors(colors, cachedColors, paletteSize, cachedColorCount); + } + + /// + /// Reads shared chroma palette colors using cached U values and raw or delta-coded V values. + /// + /// The sorted unique U colors inherited from eligible neighbors. + /// The number of palette colors. + /// The number of bits in each color sample. + /// The destination U palette colors. + /// The destination V palette colors paired with . + public void ReadPaletteUvColors( + scoped ReadOnlySpan colorCache, + int paletteSize, + int bitDepth, + scoped Span uColors, + scoped Span vColors) + { + Span cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int colorIndex = 0; + for (int i = 0; i < colorCache.Length && colorIndex < paletteSize; i++) + { + if (this.ReadLiteral(1) != 0) + { + cachedColors[colorIndex++] = colorCache[i]; + } + } + + if (colorIndex < paletteSize) + { + int cachedColorCount = colorIndex; + uColors[colorIndex++] = (ushort)this.ReadLiteral(bitDepth); + if (colorIndex < paletteSize) + { + int bits = bitDepth - 3 + this.ReadLiteral(2); + int maximumColor = (1 << bitDepth) - 1; + int range = (1 << bitDepth) - uColors[colorIndex - 1]; + for (; colorIndex < paletteSize; colorIndex++) + { + int delta = this.ReadLiteral(bits); + uColors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, uColors[colorIndex - 1] + delta); + range -= uColors[colorIndex] - uColors[colorIndex - 1]; + bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); + } + } + + MergePaletteColors(uColors, cachedColors, paletteSize, cachedColorCount); + } + else + { + cachedColors[..paletteSize].CopyTo(uColors); + } + + if (this.ReadLiteral(1) != 0) + { + // V deltas wrap in the unsigned sample domain so complementary chroma colors remain compact. + int bits = bitDepth - 4 + this.ReadLiteral(2); + int sampleRange = 1 << bitDepth; + vColors[0] = (ushort)this.ReadLiteral(bitDepth); + for (int i = 1; i < paletteSize; i++) + { + int delta = this.ReadLiteral(bits); + if (delta != 0 && this.ReadLiteral(1) != 0) + { + delta = -delta; + } + + int value = vColors[i - 1] + delta; + if (value < 0) + { + value += sampleRange; + } + + if (value >= sampleRange) + { + value -= sampleRange; + } + + vColors[i] = (ushort)value; + } + } + else + { + for (int i = 0; i < paletteSize; i++) + { + vColors[i] = (ushort)this.ReadLiteral(bitDepth); + } + } + } + /// /// Reads a finite subexponential value recentered around a preceding value. /// @@ -158,6 +293,40 @@ internal ref struct Av1SymbolDecoder return r.ReadSymbol(this.context.SgrProjectionRestoration) != 0; } + /// + /// Merges selected cached colors with the sorted transmitted colors. + /// + /// The transmitted colors beginning at and the merged output. + /// The selected cached colors in ascending order. + /// The total palette size. + /// The number of selected cached colors. + private static void MergePaletteColors( + Span colors, + ReadOnlySpan cachedColors, + int paletteSize, + int cachedColorCount) + { + if (cachedColorCount == 0) + { + return; + } + + int cacheIndex = 0; + int transmittedIndex = cachedColorCount; + for (int i = 0; i < paletteSize; i++) + { + if (cacheIndex < cachedColorCount && + (transmittedIndex >= paletteSize || cachedColors[cacheIndex] <= colors[transmittedIndex])) + { + colors[i] = cachedColors[cacheIndex++]; + } + else + { + colors[i] = colors[transmittedIndex++]; + } + } + } + /// /// Reads a finite subexponential code from the tile entropy stream. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index 0e0e2c2939..11d231e2bb 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -418,6 +418,167 @@ internal class Av1SymbolEncoder : IDisposable w.WriteSymbol(colorOrderIndex, distribution); } + /// + /// Gets the fixed-point rate of the luma palette colors. + /// + /// The sorted unique colors inherited from eligible neighbors. + /// The sorted luma palette colors. + /// The number of bits in each color sample. + /// The rate cost in 1/512-bit units. + public static int GetPaletteYColorCost( + ReadOnlySpan colorCache, + ReadOnlySpan colors, + int bitDepth) + { + Span cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2]; + Span uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int uncachedColorCount = IndexColorCache( + colorCache, + colors, + cacheColorFound, + uncachedColors); + + // Palette RD modeling charges every available cache flag even though emission can stop once all colors match. + int bitCount = colorCache.Length + + GetDeltaEncodedColorBitCount(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 1); + + return Av1ProbabilityCost.GetLiteralCost(bitCount); + } + + /// + /// Writes the luma palette colors using neighboring cache selections followed by sorted deltas. + /// + /// The sorted unique colors inherited from eligible neighbors. + /// The sorted luma palette colors. + /// The number of bits in each color sample. + public void WritePaletteYColors( + ReadOnlySpan colorCache, + ReadOnlySpan colors, + int bitDepth) + { + Span cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2]; + Span uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int uncachedColorCount = IndexColorCache( + colorCache, + colors, + cacheColorFound, + uncachedColors); + + int cachedColorCount = 0; + for (int i = 0; i < colorCache.Length && cachedColorCount < colors.Length; i++) + { + byte found = cacheColorFound[i]; + this.WriteLiteral(found, 1); + cachedColorCount += found; + } + + this.WriteDeltaEncodedColors(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 1); + } + + /// + /// Gets the fixed-point rate of the shared chroma palette colors. + /// + /// The sorted unique U colors inherited from eligible neighbors. + /// The sorted U palette colors. + /// The V palette colors paired with . + /// The number of bits in each color sample. + /// The rate cost in 1/512-bit units. + public static int GetPaletteUvColorCost( + ReadOnlySpan colorCache, + ReadOnlySpan uColors, + ReadOnlySpan vColors, + int bitDepth) + { + Span cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2]; + Span uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int uncachedColorCount = IndexColorCache( + colorCache, + uColors, + cacheColorFound, + uncachedColors); + + // Palette RD modeling charges every available cache flag even though emission can stop once all colors match. + int bitCount = colorCache.Length + + GetDeltaEncodedColorBitCount(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 0); + + int deltaBits = GetPaletteVDeltaBitCount(vColors, bitDepth, out int zeroCount, out int minimumBits); + int deltaBitCount = 2 + bitDepth + ((deltaBits + 1) * (vColors.Length - 1)) - zeroCount; + int rawBitCount = bitDepth * vColors.Length; + bitCount += 1 + Math.Min(deltaBitCount, rawBitCount); + return Av1ProbabilityCost.GetLiteralCost(bitCount); + } + + /// + /// Writes the shared chroma palette colors using cached U values and the cheaper V representation. + /// + /// The sorted unique U colors inherited from eligible neighbors. + /// The sorted U palette colors. + /// The V palette colors paired with . + /// The number of bits in each color sample. + public void WritePaletteUvColors( + ReadOnlySpan colorCache, + ReadOnlySpan uColors, + ReadOnlySpan vColors, + int bitDepth) + { + Span cacheColorFound = stackalloc byte[Av1Constants.PaletteMaxSize * 2]; + Span uncachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int uncachedColorCount = IndexColorCache( + colorCache, + uColors, + cacheColorFound, + uncachedColors); + + int cachedColorCount = 0; + for (int i = 0; i < colorCache.Length && cachedColorCount < uColors.Length; i++) + { + byte found = cacheColorFound[i]; + this.WriteLiteral(found, 1); + cachedColorCount += found; + } + + this.WriteDeltaEncodedColors(uncachedColors[..uncachedColorCount], bitDepth, minimumDelta: 0); + + int deltaBits = GetPaletteVDeltaBitCount(vColors, bitDepth, out int zeroCount, out int minimumBits); + int deltaBitCount = 2 + bitDepth + ((deltaBits + 1) * (vColors.Length - 1)) - zeroCount; + int rawBitCount = bitDepth * vColors.Length; + bool useDelta = deltaBitCount < rawBitCount; + this.WriteLiteral(useDelta ? 1u : 0u, 1); + if (!useDelta) + { + for (int i = 0; i < vColors.Length; i++) + { + this.WriteLiteral(vColors[i], bitDepth); + } + + return; + } + + this.WriteLiteral((uint)(deltaBits - minimumBits), 2); + this.WriteLiteral(vColors[0], bitDepth); + int sampleRange = 1 << bitDepth; + for (int i = 1; i < vColors.Length; i++) + { + int signedDelta = vColors[i] - vColors[i - 1]; + int delta = Math.Abs(signedDelta); + + // Chroma wraps in its unsigned sample domain, so signal whichever circular direction has less magnitude. + if (delta <= sampleRange - delta) + { + this.WriteLiteral((uint)delta, deltaBits); + if (delta != 0) + { + this.WriteLiteral(signedDelta < 0 ? 1u : 0u, 1); + } + } + else + { + this.WriteLiteral((uint)(sampleRange - delta), deltaBits); + this.WriteLiteral(signedDelta < 0 ? 0u : 1u, 1); + } + } + } + /// /// Writes the frame-local intra-block-copy flag. /// @@ -1366,6 +1527,175 @@ internal class Av1SymbolEncoder : IDisposable } } + /// + /// Separates palette colors selected from the neighbor cache from colors that require literal coding. + /// + /// The sorted unique neighbor colors. + /// The sorted palette colors. + /// The cache-selection flags. + /// The destination for colors absent from the cache. + /// The number of uncached colors. + private static int IndexColorCache( + ReadOnlySpan colorCache, + ReadOnlySpan colors, + Span cacheColorFound, + Span uncachedColors) + { + cacheColorFound[..colorCache.Length].Clear(); + Span inCache = stackalloc byte[Av1Constants.PaletteMaxSize]; + inCache.Clear(); + + // Cache-order flags drive the bitstream while palette-order flags preserve the sorted uncached output. + int cachedColorCount = 0; + for (int cacheIndex = 0; cacheIndex < colorCache.Length && cachedColorCount < colors.Length; cacheIndex++) + { + for (int colorIndex = 0; colorIndex < colors.Length; colorIndex++) + { + if (colors[colorIndex] == colorCache[cacheIndex]) + { + inCache[colorIndex] = 1; + cacheColorFound[cacheIndex] = 1; + cachedColorCount++; + break; + } + } + } + + int uncachedColorCount = 0; + for (int colorIndex = 0; colorIndex < colors.Length; colorIndex++) + { + if (inCache[colorIndex] == 0) + { + uncachedColors[uncachedColorCount++] = colors[colorIndex]; + } + } + + return uncachedColorCount; + } + + /// + /// Gets the literal length of an ascending palette-color sequence. + /// + /// The sorted colors. + /// The number of bits in each color sample. + /// The minimum representable difference between adjacent colors. + /// The literal length in bits. + private static int GetDeltaEncodedColorBitCount( + ReadOnlySpan colors, + int bitDepth, + int minimumDelta) + { + if (colors.IsEmpty) + { + return 0; + } + + int bitCount = bitDepth; + if (colors.Length == 1) + { + return bitCount; + } + + int maximumDelta = 0; + for (int i = 1; i < colors.Length; i++) + { + maximumDelta = Math.Max(maximumDelta, colors[i] - colors[i - 1]); + } + + int minimumBits = bitDepth - 3; + int bits = Math.Max( + (int)Av1Math.CeilLog2((uint)(maximumDelta + 1 - minimumDelta)), + minimumBits); + + int range = (1 << bitDepth) - colors[0] - minimumDelta; + bitCount += 2; + for (int i = 1; i < colors.Length; i++) + { + int delta = colors[i] - colors[i - 1]; + bitCount += bits; + range -= delta; + bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); + } + + return bitCount; + } + + /// + /// Writes an ascending palette-color sequence as one literal followed by bounded deltas. + /// + /// The sorted colors. + /// The number of bits in each color sample. + /// The minimum representable difference between adjacent colors. + private void WriteDeltaEncodedColors( + ReadOnlySpan colors, + int bitDepth, + int minimumDelta) + { + if (colors.IsEmpty) + { + return; + } + + this.WriteLiteral(colors[0], bitDepth); + if (colors.Length == 1) + { + return; + } + + int maximumDelta = 0; + for (int i = 1; i < colors.Length; i++) + { + maximumDelta = Math.Max(maximumDelta, colors[i] - colors[i - 1]); + } + + int minimumBits = bitDepth - 3; + int bits = Math.Max( + (int)Av1Math.CeilLog2((uint)(maximumDelta + 1 - minimumDelta)), + minimumBits); + + this.WriteLiteral((uint)(bits - minimumBits), 2); + int range = (1 << bitDepth) - colors[0] - minimumDelta; + for (int i = 1; i < colors.Length; i++) + { + int delta = colors[i] - colors[i - 1]; + this.WriteLiteral((uint)(delta - minimumDelta), bits); + range -= delta; + bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); + } + } + + /// + /// Gets the bit width required by wrapped V-plane palette deltas. + /// + /// The V-plane colors in U-palette order. + /// The number of bits in each color sample. + /// The number of deltas that omit a sign bit. + /// The minimum permitted delta width. + /// The delta width in bits. + private static int GetPaletteVDeltaBitCount( + ReadOnlySpan colors, + int bitDepth, + out int zeroCount, + out int minimumBits) + { + int sampleRange = 1 << bitDepth; + int maximumDelta = 0; + zeroCount = 0; + minimumBits = bitDepth - 4; + for (int i = 1; i < colors.Length; i++) + { + int delta = Math.Abs(colors[i] - colors[i - 1]); + int wrappedDelta = Math.Min(delta, sampleRange - delta); + maximumDelta = Math.Max(maximumDelta, wrappedDelta); + if (wrappedDelta == 0) + { + zeroCount++; + } + } + + return Math.Max((int)Av1Math.CeilLog2((uint)(maximumDelta + 1)), minimumBits); + } + /// /// Emits coefficient syntax and reports no estimated rate. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index 4df5cfb8f6..2209404293 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -2659,40 +2659,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable scoped Span colors) { Span colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2]; - Span cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.Y, colorCache); - int colorIndex = 0; - for (int i = 0; i < cacheSize && colorIndex < paletteSize; i++) - { - if (reader.ReadLiteral(1) != 0) - { - cachedColors[colorIndex++] = colorCache[i]; - } - } - - if (colorIndex == paletteSize) - { - cachedColors[..paletteSize].CopyTo(colors); - return; - } - - int cachedColorCount = colorIndex; - colors[colorIndex++] = (ushort)reader.ReadLiteral(bitDepth); - if (colorIndex < paletteSize) - { - int bits = bitDepth - 3 + reader.ReadLiteral(2); - int maximumColor = (1 << bitDepth) - 1; - int range = maximumColor - colors[colorIndex - 1]; - for (; colorIndex < paletteSize; colorIndex++) - { - int delta = reader.ReadLiteral(bits) + 1; - colors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, colors[colorIndex - 1] + delta); - range -= colors[colorIndex] - colors[colorIndex - 1]; - bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); - } - } - - MergePaletteColors(colors, cachedColors, paletteSize, cachedColorCount); + reader.ReadPaletteYColors(colorCache[..cacheSize], paletteSize, bitDepth, colors); } /// @@ -2713,77 +2681,8 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable scoped Span vColors) { Span colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2]; - Span cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.U, colorCache); - int colorIndex = 0; - for (int i = 0; i < cacheSize && colorIndex < paletteSize; i++) - { - if (reader.ReadLiteral(1) != 0) - { - cachedColors[colorIndex++] = colorCache[i]; - } - } - - if (colorIndex < paletteSize) - { - int cachedColorCount = colorIndex; - uColors[colorIndex++] = (ushort)reader.ReadLiteral(bitDepth); - if (colorIndex < paletteSize) - { - int bits = bitDepth - 3 + reader.ReadLiteral(2); - int maximumColor = (1 << bitDepth) - 1; - int range = (1 << bitDepth) - uColors[colorIndex - 1]; - for (; colorIndex < paletteSize; colorIndex++) - { - int delta = reader.ReadLiteral(bits); - uColors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, uColors[colorIndex - 1] + delta); - range -= uColors[colorIndex] - uColors[colorIndex - 1]; - bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); - } - } - - MergePaletteColors(uColors, cachedColors, paletteSize, cachedColorCount); - } - else - { - cachedColors[..paletteSize].CopyTo(uColors); - } - - if (reader.ReadLiteral(1) != 0) - { - // V deltas wrap in the unsigned sample domain so complementary chroma colors remain compact. - int bits = bitDepth - 4 + reader.ReadLiteral(2); - int maximumColorPlusOne = 1 << bitDepth; - vColors[0] = (ushort)reader.ReadLiteral(bitDepth); - for (int i = 1; i < paletteSize; i++) - { - int delta = reader.ReadLiteral(bits); - if (delta != 0 && reader.ReadLiteral(1) != 0) - { - delta = -delta; - } - - int value = vColors[i - 1] + delta; - if (value < 0) - { - value += maximumColorPlusOne; - } - - if (value >= maximumColorPlusOne) - { - value -= maximumColorPlusOne; - } - - vColors[i] = (ushort)value; - } - } - else - { - for (int i = 0; i < paletteSize; i++) - { - vColors[i] = (ushort)reader.ReadLiteral(bitDepth); - } - } + reader.ReadPaletteUvColors(colorCache[..cacheSize], paletteSize, bitDepth, uColors, vColors); } /// @@ -2858,36 +2757,6 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable } } - /// - /// Merges selected cached colors with the sorted transmitted colors in one prediction-order palette. - /// - /// The transmitted colors beginning at and the merged output. - /// The selected cached colors in ascending order. - /// The total palette size. - /// The number of selected cached colors. - private static void MergePaletteColors(Span colors, ReadOnlySpan cachedColors, int paletteSize, int cachedColorCount) - { - if (cachedColorCount == 0) - { - return; - } - - int cacheIndex = 0; - int transmittedIndex = cachedColorCount; - for (int i = 0; i < paletteSize; i++) - { - if (cacheIndex < cachedColorCount && - (transmittedIndex >= paletteSize || cachedColors[cacheIndex] <= colors[transmittedIndex])) - { - colors[i] = cachedColors[cacheIndex++]; - } - else - { - colors[i] = colors[transmittedIndex++]; - } - } - } - /// /// Computes the padded plane dimensions and the portion that lies inside the coded image. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index 2c2df5df8e..65c8eeb587 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -1145,6 +1145,94 @@ public class Av1EntropyTests Assert.Equal(Av1ProbabilityCost.GetLiteralCost(3), Av1SymbolEncoder.GetUniformCost(5, 3)); } + [Theory] + [InlineData(8)] + [InlineData(10)] + [InlineData(12)] + public void RoundTripPaletteColors(int bitDepth) + { + ushort[] colorCache = [4, 17, 23, 51]; + ushort[] yColors = [4, 23, 90]; + ushort[] uColors = [17, 51, 100]; + ushort[] deltaVColors = [1, 2, 1]; + ushort[] rawVColors = [0, (ushort)(1 << (bitDepth - 1)), 0]; + using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex); + encoder.WritePaletteYColors(colorCache, yColors, bitDepth); + encoder.WritePaletteUvColors(colorCache, uColors, deltaVColors, bitDepth); + encoder.WritePaletteUvColors(colorCache, uColors, rawVColors, bitDepth); + + using IMemoryOwner encoded = encoder.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + ushort[] decodedYColors = new ushort[yColors.Length]; + ushort[] decodedUColors = new ushort[uColors.Length]; + ushort[] decodedDeltaVColors = new ushort[deltaVColors.Length]; + ushort[] decodedRawVColors = new ushort[rawVColors.Length]; + decoder.ReadPaletteYColors(colorCache, yColors.Length, bitDepth, decodedYColors); + decoder.ReadPaletteUvColors( + colorCache, + uColors.Length, + bitDepth, + decodedUColors, + decodedDeltaVColors); + + Assert.Equal(yColors, decodedYColors); + Assert.Equal(uColors, decodedUColors); + Assert.Equal(deltaVColors, decodedDeltaVColors); + + decodedUColors.AsSpan().Clear(); + decoder.ReadPaletteUvColors( + colorCache, + uColors.Length, + bitDepth, + decodedUColors, + decodedRawVColors); + + Assert.Equal(uColors, decodedUColors); + Assert.Equal(rawVColors, decodedRawVColors); + } + + [Fact] + public void PaletteColorCostsMatchCurrentLibaomBitCounts() + { + ushort[] colorCache = [5, 10, 20]; + + Assert.Equal( + Av1ProbabilityCost.GetLiteralCost(20), + Av1SymbolEncoder.GetPaletteYColorCost([], [10, 20, 21], 8)); + + Assert.Equal( + Av1ProbabilityCost.GetLiteralCost(11), + Av1SymbolEncoder.GetPaletteYColorCost(colorCache, [5, 20, 30], 8)); + + Assert.Equal( + Av1ProbabilityCost.GetLiteralCost(32), + Av1SymbolEncoder.GetPaletteUvColorCost(colorCache, [5, 20, 30], [20, 21, 20], 8)); + + Assert.Equal( + Av1ProbabilityCost.GetLiteralCost(36), + Av1SymbolEncoder.GetPaletteUvColorCost(colorCache, [5, 20, 30], [0, 128, 0], 8)); + } + + [Fact] + public void PaletteColorCostDoesNotAllocate() + { + ushort[] colorCache = [5, 10, 20]; + ushort[] yColors = [5, 20, 30]; + ushort[] uColors = [5, 20, 30]; + ushort[] vColors = [20, 21, 20]; + _ = Av1SymbolEncoder.GetPaletteYColorCost(colorCache, yColors, 8); + long before = GC.GetAllocatedBytesForCurrentThread(); + + for (int i = 0; i < 1000; i++) + { + _ = Av1SymbolEncoder.GetPaletteYColorCost(colorCache, yColors, 8); + _ = Av1SymbolEncoder.GetPaletteUvColorCost(colorCache, uColors, vColors, 8); + } + + long after = GC.GetAllocatedBytesForCurrentThread(); + Assert.Equal(before, after); + } + [Theory] [MemberData(nameof(GetRangeData), 20)] public void RoundTripPartitionType(int context)