From 2269264cf0a198c92f8a717807aa8c0e6d9bea59 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Sep 2026 03:40:24 +1000 Subject: [PATCH] Add AV1 palette map entropy coding --- HEIF_IMPLEMENTATION_PLAN.md | 1 + .../Heif/Av1/Entropy/Av1SymbolDecoder.cs | 40 ++++ .../Heif/Av1/Entropy/Av1SymbolEncoder.cs | 185 ++++++++++++++++++ .../Heif/Av1/Tiling/Av1PaletteColorMap.cs | 101 ++++++++++ .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 104 +--------- .../Formats/Heif/Av1/Av1EntropyTests.cs | 167 ++++++++++++++++ 6 files changed, 495 insertions(+), 103 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteColorMap.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 423aea37e8..837c09e28d 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -852,6 +852,7 @@ Encoder verification contract: - [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. +- [~] Palette color-index map coding now shares the exact current-libaom neighbor weights, stable color ordering, five context classes, first-index uniform code, and diagonal wavefront between encoder costing, encoder writing, and decoder parsing. The decoder's stack-allocated context scores are explicitly cleared before accumulation, removing an invalid dependency on uninitialized stack contents. Costing and writing use a closed generic operation while the shared driver owns traversal and context derivation, so the semantic operations remain independent of map layout and tail handling. The path adds no retained state or per-call managed allocation. Twelve focused map, exact palette decode, padding, trailing-bit, and allocation cases pass; all 1,941 entropy cases and all 8,991 HEIF/AV1 cases pass direct net11 Release VSTest. The exact Release rebuild remains at 1,005 warnings and zero errors. Production payloads remain unchanged because palette selection is still disabled; retained colors, neighbor caches, index-map storage, candidate generation, and production palette mode decision remain incomplete. - [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 eeaceaab54..5134a1467e 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; @@ -245,6 +246,45 @@ internal ref struct Av1SymbolDecoder } } + /// + /// Reads a palette color-index map in AV1 diagonal wavefront order. + /// + /// The number of colors in the palette. + /// The luma or chroma plane class. + /// The number of coded map rows. + /// The number of coded map columns. + /// The row-addressable destination map. + public void ReadPaletteColorMap( + int paletteSize, + Av1PlaneType planeType, + int rows, + int columns, + Buffer2DRegion colorIndexMap) + { + colorIndexMap.DangerousGetRowSpan(0)[0] = (byte)this.ReadUniform(paletteSize); + Span colorOrder = stackalloc byte[Av1Constants.PaletteMaxSize]; + for (int diagonal = 1; diagonal < rows + columns - 1; diagonal++) + { + int firstColumn = Math.Min(diagonal, columns - 1); + int lastColumn = Math.Max(0, diagonal - rows + 1); + for (int column = firstColumn; column >= lastColumn; column--) + { + int row = diagonal - column; + int colorContext = Av1PaletteColorMap.GetContext( + colorIndexMap, + row, + column, + paletteSize, + colorIndex: -1, + colorOrder, + out _); + + int colorOrderIndex = this.ReadPaletteColorIndex(paletteSize, colorContext, planeType); + colorIndexMap.DangerousGetRowSpan(row)[column] = colorOrder[colorOrderIndex]; + } + } + } + /// /// Reads a finite subexponential value recentered around a preceding value. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs index 11d231e2bb..b2771a1a06 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; @@ -240,6 +241,40 @@ internal class Av1SymbolEncoder : IDisposable int bitCount); } + /// + /// Defines how the shared palette-map traversal handles its uniform first index and adaptive remaining indices. + /// + private interface IPaletteColorMapOperation + { + /// + /// Handles the first uniformly coded palette index. + /// + /// The tile symbol encoder. + /// The number of colors in the palette. + /// The first palette index. + /// The index's rate contribution. + public static abstract int ProcessFirstIndex( + Av1SymbolEncoder encoder, + int paletteSize, + int colorIndex); + + /// + /// Handles one context-adaptive palette color-order index. + /// + /// The tile symbol encoder. + /// The number of colors in the palette. + /// The luma or chroma plane class. + /// The spatial color-index context. + /// The index in the context-specific color order. + /// The index's rate contribution. + public static abstract int ProcessColorIndex( + Av1SymbolEncoder encoder, + int paletteSize, + Av1PlaneType planeType, + int colorContext, + int colorOrderIndex); + } + /// /// Writes an unsigned fixed-width literal to the tile entropy stream. /// @@ -579,6 +614,51 @@ internal class Av1SymbolEncoder : IDisposable } } + /// + /// Gets the current fixed-point rate of a complete palette color-index map. + /// + /// The number of colors in the palette. + /// The luma or chroma plane class. + /// The number of coded map rows. + /// The number of coded map columns. + /// The complete row-addressable color-index map. + /// The rate cost in 1/512-bit units. + public int GetPaletteColorMapCost( + int paletteSize, + Av1PlaneType planeType, + int rows, + int columns, + Buffer2DRegion colorIndexMap) + => this.ProcessPaletteColorMap( + paletteSize, + planeType, + rows, + columns, + colorIndexMap); + + /// + /// Writes a complete palette color-index map in AV1 diagonal wavefront order. + /// + /// The number of colors in the palette. + /// The luma or chroma plane class. + /// The number of coded map rows. + /// The number of coded map columns. + /// The complete row-addressable color-index map. + public void WritePaletteColorMap( + int paletteSize, + Av1PlaneType planeType, + int rows, + int columns, + Buffer2DRegion colorIndexMap) + { + _ = this.ProcessPaletteColorMap( + paletteSize, + planeType, + rows, + columns, + colorIndexMap); + } + /// /// Writes the frame-local intra-block-copy flag. /// @@ -1527,6 +1607,56 @@ internal class Av1SymbolEncoder : IDisposable } } + /// + /// Traverses a palette color-index map once for either live rate costing or entropy emission. + /// + /// The closed map-symbol operation. + /// The number of colors in the palette. + /// The luma or chroma plane class. + /// The number of coded map rows. + /// The number of coded map columns. + /// The complete row-addressable color-index map. + /// The rate cost in 1/512-bit units, or zero while writing. + private int ProcessPaletteColorMap( + int paletteSize, + Av1PlaneType planeType, + int rows, + int columns, + Buffer2DRegion colorIndexMap) + where TOperation : struct, IPaletteColorMapOperation + { + int colorIndex = colorIndexMap.DangerousGetRowSpan(0)[0]; + int cost = TOperation.ProcessFirstIndex(this, paletteSize, colorIndex); + Span colorOrder = stackalloc byte[Av1Constants.PaletteMaxSize]; + for (int diagonal = 1; diagonal < rows + columns - 1; diagonal++) + { + int firstColumn = Math.Min(diagonal, columns - 1); + int lastColumn = Math.Max(0, diagonal - rows + 1); + for (int column = firstColumn; column >= lastColumn; column--) + { + int row = diagonal - column; + colorIndex = colorIndexMap.DangerousGetRowSpan(row)[column]; + int colorContext = Av1PaletteColorMap.GetContext( + colorIndexMap, + row, + column, + paletteSize, + colorIndex, + colorOrder, + out int colorOrderIndex); + + cost += TOperation.ProcessColorIndex( + this, + paletteSize, + planeType, + colorContext, + colorOrderIndex); + } + } + + return cost; + } + /// /// Separates palette colors selected from the neighbor cache from colors that require literal coding. /// @@ -1738,6 +1868,61 @@ internal class Av1SymbolEncoder : IDisposable => Av1ProbabilityCost.GetLiteralCost(bitCount); } + /// + /// Emits palette-map syntax and reports no estimated rate. + /// + private readonly struct PaletteColorMapWriteOperation : IPaletteColorMapOperation + { + public static int ProcessFirstIndex( + Av1SymbolEncoder encoder, + int paletteSize, + int colorIndex) + { + encoder.WriteUniform(paletteSize, colorIndex); + return 0; + } + + public static int ProcessColorIndex( + Av1SymbolEncoder encoder, + int paletteSize, + Av1PlaneType planeType, + int colorContext, + int colorOrderIndex) + { + encoder.WritePaletteColorIndex( + colorOrderIndex, + paletteSize, + colorContext, + planeType); + + return 0; + } + } + + /// + /// Measures palette-map syntax against the live tile distributions without changing them. + /// + private readonly struct PaletteColorMapCostOperation : IPaletteColorMapOperation + { + public static int ProcessFirstIndex( + Av1SymbolEncoder encoder, + int paletteSize, + int colorIndex) + => GetUniformCost(paletteSize, colorIndex); + + public static int ProcessColorIndex( + Av1SymbolEncoder encoder, + int paletteSize, + Av1PlaneType planeType, + int colorContext, + int colorOrderIndex) + => encoder.GetPaletteColorIndexCost( + colorOrderIndex, + paletteSize, + colorContext, + planeType); + } + /// /// Owns the adaptive distributions used only by AV1 palette syntax. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteColorMap.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteColorMap.cs new file mode 100644 index 0000000000..f17e584ac6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteColorMap.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Derives AV1 palette color-index ordering and entropy contexts from preceding spatial indices. +/// +internal static class Av1PaletteColorMap +{ + /// + /// Derives the palette color order, current color-order index, and entropy context for one map position. + /// + /// The partially or completely populated color-index map. + /// The current map row. + /// The current map column. + /// The number of colors in the palette. + /// The current palette index, or a negative value while decoding. + /// The destination color order for the current context. + /// The current index in , or a negative value while decoding. + /// The color-index entropy context in the range from zero through four. + public static int GetContext( + Buffer2DRegion colorIndexMap, + int row, + int column, + int paletteSize, + int colorIndex, + Span colorOrder, + out int colorOrderIndex) + { + Span scores = stackalloc int[Av1Constants.PaletteMaxSize]; + scores.Clear(); + ReadOnlySpan currentRow = colorIndexMap.DangerousGetRowSpan(row); + if (column > 0) + { + scores[currentRow[column - 1]] += 2; + } + + if (row > 0) + { + ReadOnlySpan aboveRow = colorIndexMap.DangerousGetRowSpan(row - 1); + if (column > 0) + { + scores[aboveRow[column - 1]]++; + } + + scores[aboveRow[column]] += 2; + } + + Span inverseColorOrder = stackalloc int[Av1Constants.PaletteMaxSize]; + for (int i = 0; i < Av1Constants.PaletteMaxSize; i++) + { + colorOrder[i] = (byte)i; + inverseColorOrder[i] = i; + } + + // Stable descending score order keeps lower palette indices ahead when neighboring scores tie. + for (int i = 0; i < 3; i++) + { + int maximumScore = scores[i]; + int maximumIndex = i; + for (int j = i + 1; j < paletteSize; j++) + { + if (scores[j] > maximumScore) + { + maximumScore = scores[j]; + maximumIndex = j; + } + } + + if (maximumIndex != i) + { + byte maximumColor = colorOrder[maximumIndex]; + for (int j = maximumIndex; j > i; j--) + { + scores[j] = scores[j - 1]; + colorOrder[j] = colorOrder[j - 1]; + inverseColorOrder[colorOrder[j]] = j; + } + + scores[i] = maximumScore; + colorOrder[i] = maximumColor; + inverseColorOrder[maximumColor] = i; + } + } + + colorOrderIndex = colorIndex < 0 ? -1 : inverseColorOrder[colorIndex]; + int contextHash = scores[0] + (2 * scores[1]) + (2 * scores[2]); + return contextHash switch + { + 2 => 0, + 5 => 4, + 6 => 3, + 7 => 2, + 8 => 1, + _ => -1 + }; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index 2209404293..753a1cd160 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -423,11 +423,6 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]; - /// - /// Gets the color-index entropy context for each weighted palette-neighbor score hash. - /// - private static ReadOnlySpan PaletteColorIndexContexts => [-1, -1, 0, -1, -1, 4, 3, 2, 1]; - /// /// Gets the frame header whose tile syntax is being parsed. /// @@ -2814,26 +2809,7 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable int columns, Buffer2DRegion colorIndexMap) { - colorIndexMap.DangerousGetRowSpan(0)[0] = (byte)reader.ReadUniform(paletteSize); - Span colorOrder = stackalloc byte[Av1Constants.PaletteMaxSize]; - for (int diagonal = 1; diagonal < rows + columns - 1; diagonal++) - { - int firstColumn = Math.Min(diagonal, columns - 1); - int lastColumn = Math.Max(0, diagonal - rows + 1); - for (int column = firstColumn; column >= lastColumn; column--) - { - int row = diagonal - column; - int colorContext = GetPaletteColorIndexContext( - colorIndexMap, - row, - column, - paletteSize, - colorOrder); - - int colorOrderIndex = reader.ReadPaletteColorIndex(paletteSize, colorContext, planeType); - colorIndexMap.DangerousGetRowSpan(row)[column] = colorOrder[colorOrderIndex]; - } - } + reader.ReadPaletteColorMap(paletteSize, planeType, rows, columns, colorIndexMap); if (columns < planeWidth) { @@ -2854,84 +2830,6 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable } } - /// - /// Derives the palette color order and entropy context from the left, upper-left, and above indices. - /// - /// The partially decoded color-index map. - /// The current map row. - /// The current map column. - /// The number of palette colors. - /// The destination color order for the current context. - /// The color-index entropy context in the range from zero through four. - private static int GetPaletteColorIndexContext( - Buffer2DRegion colorIndexMap, - int row, - int column, - int paletteSize, - Span colorOrder) - { - Span neighborColors = stackalloc int[3]; - ReadOnlySpan currentRow = colorIndexMap.DangerousGetRowSpan(row); - neighborColors[0] = column > 0 ? currentRow[column - 1] : -1; - if (row > 0) - { - ReadOnlySpan aboveRow = colorIndexMap.DangerousGetRowSpan(row - 1); - neighborColors[1] = column > 0 ? aboveRow[column - 1] : -1; - neighborColors[2] = aboveRow[column]; - } - else - { - neighborColors[1] = -1; - neighborColors[2] = -1; - } - - Span scores = stackalloc int[Av1Constants.PaletteMaxSize]; - ReadOnlySpan neighborWeights = [2, 1, 2]; - for (int i = 0; i < neighborColors.Length; i++) - { - if (neighborColors[i] >= 0) - { - scores[neighborColors[i]] += neighborWeights[i]; - } - } - - for (int i = 0; i < colorOrder.Length; i++) - { - colorOrder[i] = (byte)i; - } - - // Stable descending score order keeps lower palette indices ahead when neighboring scores tie. - for (int i = 0; i < 3; i++) - { - int maximumScore = scores[i]; - int maximumIndex = i; - for (int j = i + 1; j < paletteSize; j++) - { - if (scores[j] > maximumScore) - { - maximumScore = scores[j]; - maximumIndex = j; - } - } - - if (maximumIndex != i) - { - byte maximumColor = colorOrder[maximumIndex]; - for (int j = maximumIndex; j > i; j--) - { - scores[j] = scores[j - 1]; - colorOrder[j] = colorOrder[j - 1]; - } - - scores[i] = maximumScore; - colorOrder[i] = maximumColor; - } - } - - int contextHash = scores[0] + (2 * scores[1]) + (2 * scores[2]); - return PaletteColorIndexContexts[contextHash]; - } - /// /// Reads the joint signs and nonzero alpha magnitudes for chroma-from-luma prediction. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index 65c8eeb587..5f9a40dd17 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -1233,6 +1233,173 @@ public class Av1EntropyTests Assert.Equal(before, after); } + [Fact] + public void PaletteColorMapCostMatchesCurrentDistributions() + { + using Buffer2D map = Configuration.Default.MemoryAllocator.Allocate2D(2, 2, AllocationOptions.Clean); + map.DangerousGetRowSpan(0)[0] = 2; + map.DangerousGetRowSpan(0)[1] = 0; + map.DangerousGetRowSpan(1)[0] = 1; + map.DangerousGetRowSpan(1)[1] = 2; + Buffer2DRegion region = new(map); + Av1Distribution[][] distributions = Av1DefaultDistributions.PaletteYColorIndex; + int expected = Av1SymbolEncoder.GetUniformCost(3, 2); + expected += Av1ProbabilityCost.GetSymbolCost(distributions[1][0], 1); + expected += Av1ProbabilityCost.GetSymbolCost(distributions[1][0], 2); + expected += Av1ProbabilityCost.GetSymbolCost(distributions[1][1], 2); + using Av1SymbolEncoder encoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: false); + + Assert.Equal( + expected, + encoder.GetPaletteColorMapCost(3, Av1PlaneType.Y, 2, 2, region)); + } + + [Theory] + [InlineData(1, 1, 0, 0, 0, 1, 4, 1)] + [InlineData(1, 1, 0, 1, 0, 1, 3, 1)] + [InlineData(1, 1, 0, 0, 1, 1, 2, 1)] + [InlineData(1, 1, 0, 2, 1, 2, 1, 2)] + [InlineData(0, 1, 0, 2, 0, 0, 0, 1)] + public void PaletteColorMapContextMatchesCurrentLibaom( + int row, + int column, + byte left, + byte upperLeft, + byte above, + byte current, + int expectedContext, + int expectedOrderIndex) + { + using Buffer2D map = Configuration.Default.MemoryAllocator.Allocate2D(2, 2, AllocationOptions.Clean); + map.DangerousGetRowSpan(0)[0] = upperLeft; + map.DangerousGetRowSpan(0)[1] = above; + map.DangerousGetRowSpan(1)[0] = left; + map.DangerousGetRowSpan(row)[column] = current; + Span colorOrder = stackalloc byte[Av1Constants.PaletteMaxSize]; + + int actualContext = Av1PaletteColorMap.GetContext( + new Buffer2DRegion(map), + row, + column, + paletteSize: 4, + current, + colorOrder, + out int actualOrderIndex); + + Assert.Equal(expectedContext, actualContext); + Assert.Equal(expectedOrderIndex, actualOrderIndex); + } + + [Fact] + public void RoundTripPaletteColorMaps() + { + const int Rows = 5; + const int Columns = 7; + const int Width = 9; + const int Height = 6; + Configuration configuration = Configuration.Default; + using Buffer2D source = configuration.MemoryAllocator.Allocate2D(Width, Height); + using Buffer2D decoded = configuration.MemoryAllocator.Allocate2D(Width, Height); + Buffer2DRegion sourceRegion = new(source); + Buffer2DRegion decodedRegion = new(decoded); + using Av1SymbolEncoder encoder = new(configuration, 512, BaseQIndex); + for (int paletteSize = 2; paletteSize <= Av1Constants.PaletteMaxSize; paletteSize++) + { + for (int plane = 0; plane < 2; plane++) + { + for (int row = 0; row < Rows; row++) + { + Span sourceRow = source.DangerousGetRowSpan(row); + for (int column = 0; column < Columns; column++) + { + sourceRow[column] = (byte)(((row * 3) + (column * 5) + plane) % paletteSize); + } + } + + encoder.WritePaletteColorMap( + paletteSize, + (Av1PlaneType)plane, + Rows, + Columns, + sourceRegion); + } + } + + using IMemoryOwner encoded = encoder.Exit(); + Av1SymbolDecoder decoder = new(configuration, encoded.GetSpan(), BaseQIndex); + for (int paletteSize = 2; paletteSize <= Av1Constants.PaletteMaxSize; paletteSize++) + { + for (int plane = 0; plane < 2; plane++) + { + for (int row = 0; row < Height; row++) + { + decoded.DangerousGetRowSpan(row).Fill(byte.MaxValue); + } + + decoder.ReadPaletteColorMap( + paletteSize, + (Av1PlaneType)plane, + Rows, + Columns, + decodedRegion); + + for (int row = 0; row < Rows; row++) + { + ReadOnlySpan decodedRow = decoded.DangerousGetRowSpan(row); + for (int column = 0; column < Columns; column++) + { + Assert.Equal( + (byte)(((row * 3) + (column * 5) + plane) % paletteSize), + decodedRow[column]); + } + + for (int column = Columns; column < Width; column++) + { + Assert.Equal(byte.MaxValue, decodedRow[column]); + } + } + + for (int row = Rows; row < Height; row++) + { + ReadOnlySpan decodedRow = decoded.DangerousGetRowSpan(row); + for (int column = 0; column < Width; column++) + { + Assert.Equal(byte.MaxValue, decodedRow[column]); + } + } + } + } + + decoder.ValidateTrailingBits(); + } + + [Fact] + public void PaletteColorMapCostDoesNotAllocateAfterEntropyInitialization() + { + using Buffer2D map = Configuration.Default.MemoryAllocator.Allocate2D(7, 5, AllocationOptions.Clean); + for (int row = 0; row < map.Height; row++) + { + Span mapRow = map.DangerousGetRowSpan(row); + for (int column = 0; column < map.Width; column++) + { + mapRow[column] = (byte)(((row * 3) + (column * 5)) % 4); + } + } + + using Av1SymbolEncoder encoder = new(Configuration.Default, 64, BaseQIndex, updateCdf: false); + Buffer2DRegion region = new(map); + _ = encoder.GetPaletteColorMapCost(4, Av1PlaneType.Y, map.Height, map.Width, region); + long before = GC.GetAllocatedBytesForCurrentThread(); + + for (int i = 0; i < 1000; i++) + { + _ = encoder.GetPaletteColorMapCost(4, Av1PlaneType.Y, map.Height, map.Width, region); + } + + long after = GC.GetAllocatedBytesForCurrentThread(); + Assert.Equal(before, after); + } + [Theory] [MemberData(nameof(GetRangeData), 20)] public void RoundTripPartitionType(int context)