From b33e207014f79a2342d7eea5bc1d3fa484c8a096 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Sep 2026 04:16:41 +1000 Subject: [PATCH] Add retained AV1 palette state --- HEIF_IMPLEMENTATION_PLAN.md | 5 +- .../Av1IntraSuperblockEncoder.ModeDecision.cs | 3 +- .../Heif/Av1/Tiling/Av1EncoderBlockStruct.cs | 23 +- .../Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs | 75 +++++++ .../Av1/Tiling/Av1EncoderPaletteMapBuffer.cs | 62 ++++++ .../Av1/Tiling/Av1EncoderPictureBuffer.cs | 50 ++++- .../Tiling/Av1EncoderSuperblockWorkspace.cs | 32 ++- .../Heif/Av1/Tiling/Av1PaletteCache.cs | 66 ++++++ .../Heif/Av1/Tiling/Av1PictureControlSet.cs | 5 + .../Formats/Heif/Av1/Tiling/Av1TileReader.cs | 52 +---- .../Av1/Tiling/Av1TileWriter.BlockEncoding.cs | 7 +- .../Formats/Heif/Av1/Tiling/Av1TileWriter.cs | 200 ++++++++++++++---- .../Heif/Av1/Av1CoefficientsEntropyTests.cs | 163 +++++++++++++- .../Heif/Av1/Av1EncoderModeInfoBufferTests.cs | 33 ++- .../Formats/Heif/Av1/Av1EntropyTests.cs | 12 ++ .../Av1/Av1IntraSuperblockEncoderTests.cs | 184 +++++++++++++++- 16 files changed, 846 insertions(+), 126 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteMapBuffer.cs create mode 100644 src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteCache.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 837c09e28d..0b9f3df5c7 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -825,14 +825,14 @@ Encoder verification contract: - [~] A non-owning encoder-frame view now separates visible conversion regions from coded regions and performs complete left, top, right, bottom, and corner extension across each bordered plane. Current libaom uses 8-sample-aligned coded dimensions, a 32-sample-aligned luma stride with chroma stride derived from it, and a 64-pixel luma border for non-resized all-intra encoding. One operation-ready frame owner now rents the aligned Y, U, and V storage contiguously, exposes non-owning `Buffer2D` plane views, and returns the rent exactly once. A 4K 4:2:0 frame occupies about 13.0 MiB at 8-bit or 26.0 MiB at 10/12-bit; source and reconstruction therefore remain distinct frame owners rather than adding a full-frame copy. The corrected tests use this real ownership path and verify the exact 54 KiB 64x64 4:2:0 rent. The frame-encoder operation now instantiates matching source and reconstruction owners with ordinary `using` lifetimes and converts packed pixels directly into the source owner before extension. - [~] Temporal delimiter, sequence header, frame header, combined-frame tile-group writing, and an internal reduced-still-picture frame operation now exist locally. The remaining required metadata, padding, multi-tile, option, and public encoder paths are not complete. - [~] Implement superblock and partition analysis for every permitted block size and partition. The current baseline deliberately splits every in-frame node to 8x8 blocks and records decisions in current-libaom writer preorder; block-size selection and non-split partition analysis remain. -- [~] Implement intra mode search, palette, filter intra, chroma-from-luma, and intra-block copy decisions. Live luma search now covers all 13 zero-angle base modes and all six nonzero adjustments for each of the eight directional modes. Joint spatial chroma search covers the same 61 candidates, combines both chroma planes in one rate-distortion decision, and preserves the winning shared angle adjustment. Chroma-from-luma now searches the complete signed alpha alphabet from reconstructed luma and retains its joint U/V syntax. Filter-intra now searches all five predictors after ordinary luma modes; palette and intra-block copy remain. +- [~] Implement intra mode search, palette, filter intra, chroma-from-luma, and intra-block copy decisions. Live luma search now covers all 13 zero-angle base modes and all six nonzero adjustments for each of the eight directional modes. Joint spatial chroma search covers the same 61 candidates, combines both chroma planes in one rate-distortion decision, and preserves the winning shared angle adjustment. Chroma-from-luma now searches the complete signed alpha alphabet from reconstructed luma and retains its joint U/V syntax. Filter-intra now searches all five predictors after ordinary luma modes. Palette entropy, retained state, and production syntax are complete; palette candidate generation and intra-block-copy mode decision remain. - [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools. - [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain. - [~] Implement real rate-distortion selection and make quality and effort change work, size, and output quality. The complete luma and joint chroma candidate sets, including chroma-from-luma and filter-intra, now perform live rate-distortion selection; quality mapping, effort-dependent pruning, and the remaining searches are not implemented. - [~] Encoder rate accounting converts the entropy writer's live inverse cumulative distributions into current-libaom fixed-point symbol costs without allocating or duplicating probability state. Read-only luma-mode, directional-delta, filter-intra, chroma-mode, block-skip, transform-size, transform-block-skip, and complete transform-coefficient queries share the exact distributions mutated by the subsequent entropy write. Complete coefficient costing follows current libaom's optimized shape: it returns immediately for an empty transform, uses the EOB-specific base-range context, fuses magnitude, sign, base-range, and Golomb accounting into one reverse traversal, and combines repeated full base-range chunks instead of replaying each emitted symbol. Tile-lifetime level and context scratch is reused, the one-coefficient path neither clears nor initializes the forward-neighbor level map, and steady-state queries allocate nothing. Transform-size writing and costing share one subdivision-depth calculation, while shared closed symbol operations keep the writer and cost mappings for transform skip, transform type, and EOB syntax identical without forcing the estimator through the writer's slower two-pass coefficient traversal. The current-libaom fixed-point RD combiner preserves 64-bit distortion and rounds the weighted 1/512-bit rate at the required boundary. Its key-frame multiplier follows libaom's squared DC-quantizer formula and exact 10/12-bit normalization. Live final-block selection evaluates all 61 legal 8x8 luma candidates: the 13 zero-angle base modes in current-libaom order, followed by six nonzero adjustments for each directional mode. Joint chroma selection evaluates the equivalent 61 spatial candidates, combines U and V distortion plus coefficient rate, and charges one live chroma-mode and shared-angle symbol over the actual subsampled 4x4, 4x8, or 8x8 geometry. Chroma-from-luma subsamples the reconstructed luma block once into fixed-stride Q3 stack scratch, subtracts the rounded mean, evaluates all 33 signed alpha values independently for each plane with complete transform RD, and combines the cached plane results across all 1,088 valid joint pairs with one live sign cost and the conditional U/V magnitude costs. This is the allocation-free equivalent of current libaom's exhaustive 33-value path: it requires 66 evaluation transforms rather than transforming every joint pair, preserves DC-before-CfL-before-spatial tie order, and fixes the implicit chroma transform to DCT-DCT. Filter-intra follows ordinary luma candidates, searches all five predictors in syntax order, and evaluates every legal transform while reusing one prepared prediction and source residual per filter mode. Every candidate includes its live mode, angle, filter mode, alpha, and coefficient rate plus normalized pixel-domain distortion. Each prepared reference edge retains the common-corner prefix and twice the transform dimension required by directional prediction. A shared encoder/decoder availability calculation selects reconstructed top-right and bottom-left extensions according to tile, frame, superblock, and block reconstruction order; unavailable extensions repeat the nearest coded endpoint. Missing top or left edges retain current libaom's perpendicular-sample and bit-depth-midpoint rules. Directional prediction applies the AV1 three-degree adjustment step and reuses transform workspace for zone-three transposition before the transform overwrites it, keeping candidate evaluation allocation-free. The winning luma and chroma signed adjustments are retained in the packed final-block state consumed by the tile writer. The tile writer invokes these stack-only selectors after mapping current neighbors and immediately before writing each block, so later decisions see reconstructed samples, coefficient contexts, and CDF updates from every preceding block. Block skip is read only after the callback has combined every coded plane. Luma candidate scratch remains one 8x8 reconstruction and one 8x8 coefficient span on the stack; chroma uses one transform-sized reconstruction and coefficient span for each of U and V. Only a newly winning candidate is copied into retained frame storage. Production fixtures force every luma base predictor, both extreme adjustments in all three directional zones, available top-right and bottom-left extensions, high-bit-depth adjustment propagation, exact signed luma and chroma angle-rate terms, joint U/V decisions, packed chroma state, and 4:2:0, 4:2:2, and 4:4:4 transform geometry. The CfL fixtures derive target chroma from a pilot production encode's actual reconstructed luma through an independent scalar Q3 oracle and prove exact positive/negative alpha syntax plus zero-residual DCT-DCT reconstruction for all three subsampling geometries at 8, 10, and 12 bits. The stable fixed-DC traversal comparison uses neutral samples for which both the baseline and live search are contractually DC and skipped, instead of relying on textured content to happen to select the baseline mode. 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 namespace cases pass, and current-main `aomdec` accepts all 29 emitted 8/10/12-bit 4:0:0, 4:2:0, 4:2:2, and 4:4:4 constant or gradient payloads. Remaining mode decision work includes transform-size search, broader joint mode/transform refinement, palette search, partition search, and effort-dependent pruning. Non-empty intra blocks deliberately remain non-skipped, matching current libaom; later inter mode selection owns its distinct skip-transform RD decision. - [~] The tile writer now publishes one packed coefficient context per covered 4x4 edge unit and derives luma/chroma skip plus DC-sign contexts from the complete transform edges using current-libaom units. Partition, transform, and coefficient neighbor state retains only the above and left context regions used by current libaom; the unused third top-left region, its granularity state, and its unused sentinel are removed. One picture owner now packs segmentation plus every tile's partition, luma, chroma, and transform edges into one clean byte allocation with typed non-owning views; together with the separately typed packed mode-information owner, the complete picture state uses two allocator rents rather than seven. Exact aligned lengths, clean initialization, and balanced exactly-once returns are covered in Release. Multi-tile payload ownership and verified CDF update behavior remain. - [~] Encoder mode information now uses a frame-owned integer alias grid over a packed 8-byte value allocation, matching current libaom's `mi_grid_base` and `mi_alloc` relationship without a managed object or reference per 4x4 entry. The visible dimensions are aligned to eight luma samples, the grid stride and allocated row count are aligned to 32 mode-information units, and optional 8x8 allocation granularity reduces the value store in both dimensions exactly as current libaom does. One clean ImageSharp byte owner contains both independently typed regions, reducing libaom's two allocation lifetimes to one without a copy. At 4K, the 4x4 layout occupies about 6.0 MiB in total; the 8x8 layout occupies about 3.0 MiB. Exact geometry, clean allocation, typed lengths, aligned mapping, untouched row padding, and exactly-once return pass 4 of 4 direct net11 VSTest cases in Release. Every coded 4x4 cell covered by square, rectangular, or clipped edge blocks maps to its owning allocation entry before context-dependent symbols are written. Packed syntax, relative neighbor lookup, full block mapping, writer traversal, entropy, and OBU coverage pass 1,947 of 1,947 direct net11 VSTest cases in Release; complete mode decision still remains. -- [~] The final-block decision workspace uses one reusable 10.3 KiB ImageSharp allocator owner. It contains 1,024 explicitly packed 10-byte final-block entries and the 341 preorder partition bytes required by a complete 128x128-through-8x8 quadtree, replacing separate managed arrays. Construction and the explicit per-superblock reset initialize every syntax field, including the nonzero sentinel that disables filter-intra prediction; pooled palette, quantizer, prediction, and partition bytes cannot leak into the next decision pass. Exact allocation, size, initialization, reset, return, repeated-run, writer, entropy, and OBU coverage pass 1,957 of 1,957 direct net11 VSTest cases in Release; complete mode decision still remains. +- [~] The final-block decision workspace uses one reusable 8.3 KiB ImageSharp allocator owner. It contains 1,024 explicitly packed 8-byte final-block entries and the 341 preorder partition bytes required by a complete 128x128-through-8x8 quadtree, replacing separate managed arrays. Palette colors now have their own current-block value and are copied only to the picture edges that later blocks can reference, so enabling palette mode does not add 50 bytes to every final-block entry. Construction and the explicit per-superblock reset initialize every syntax field, including the nonzero sentinel that disables filter-intra prediction; pooled quantizer, prediction, partition, and current-palette bytes cannot leak into the next decision pass. Complete mode decision still remains. - [~] Finalized transform coefficients and packed EOB/type state now use raster-ordered, per-superblock plane segments matching current libaom's coefficient-pool geometry. One ImageSharp allocator owner replaces libaom's separate coefficient, EOB, and entropy-context allocations while preserving the full 1024 luma and 256-per-chroma 4x4 state capacity of a 128x128 4:2:0 superblock. The fixed 8x8 DC-intra traversal populates the owner's quantized coefficient and state slices while updating the caller-owned reconstruction plane directly, and a real tile-writer integration check proves that both sides consume identical luma and chroma areas. Complete mode decision still remains. - [~] Tile partition writing now follows current libaom's recursive `write_modes_sb` preorder traversal and `update_ext_partition_context` edge updates directly. Bottom-edge blocks use the horizontal-alike partition CDF and right-edge blocks use the vertical-alike CDF; byte-exact regressions cover both paths after the previous calls were found reversed. Lossless chroma-from-luma availability now uses the subsampled plane block size shared with the decoder instead of the lossy 32x32 limit, preserving the correct UV-mode alphabet for each segment. The obsolete SVT-derived global geometry catalog and its unimplemented lookup are removed; transform geometry is derived in libaom's bounded 64x64 residual order, fixed intra transform-size symbols use the reference depth and neighbor contexts, and each derived transform size is persisted to the frame-owned mode information before the entropy snapshot and coefficient traversal consume it. Frame-edge and segmentation syntax use mode-information units, and 128x128 CDEF units use libaom's 0-to-3 indexing and first-block strength ownership. The focused transform-state regression passes 3 of 3 direct net11 VSTest cases in Release. Writer, entropy, and OBU coverage passes 1,957 of 1,957 direct net11 VSTest cases in Release, with 20 of 20 focused encoder and decoder chroma-from-luma cases. Partition and mode analysis still need to populate these retained decisions; variable inter-transform syntax remains part of later inter-frame support. - [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions. @@ -853,6 +853,7 @@ Encoder verification contract: - [~] 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. +- [~] Retained encoder palette state and production palette writing now mirror current libaom's 50-byte palette-mode contents, separate luma and shared-chroma sizes, three eight-color planes, above-and-left sorted cache, 64-sample above-cache boundary, mode contexts, palette colors, color-index maps, and syntax order. The current block keeps one inline value in the reusable superblock workspace; only the 4x4-granularity top and left picture edges retain copies for later blocks. For a 3840x2160 tile these edges occupy about 73.4 KiB instead of about 6.2 MiB for a 50-byte palette value attached to every 8x8 mode allocation. Luma and chroma index maps share one lazily allocated 32 KiB owner containing two 128x128 maps, so the palette-disabled production path retains no map owner. The compact final-block workspace falls from about 10.3 KiB to about 8.3 KiB. The writer caps map traversal to the coded plane count, writes maps before transform syntax, and publishes palette edges only after the current block has consumed preceding contexts. Eight focused size, alignment, ownership, cache-boundary, round-trip, map-consumption, and edge-publication cases pass; all 114 palette cases, all 1,942 entropy cases, and all 8,996 HEIF/AV1 cases pass direct foreground net11 Release VSTest. The exact net11 Release rebuild reports 1,050 solution warnings and zero errors; Roslynk reports zero compiler errors and no diagnostics in the touched files. The current-main reference is `a40ed1ea9e4ecc3df58a5bccb76623f2c94ae727`. Production payloads remain unchanged because palette candidate generation is still disabled; that live rate-distortion search is the next checkpoint. - [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/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs index e077c5b735..24f86b4159 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs @@ -157,7 +157,8 @@ internal static partial class Av1IntraSuperblockEncoder Point blockOrigin, ushort tileIndex, ref Av1MacroBlockModeInfo modeInfo, - ref Av1EncoderBlockStruct block) + ref Av1EncoderBlockStruct block, + ref Av1EncoderPaletteInfo paletteInfo) { const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1TransformSize LumaTransformSize = Av1TransformSize.Size8x8; diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs index 7277077937..f5f9e3f8c7 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -16,12 +15,7 @@ internal struct Av1EncoderBlockStruct /// /// The fixed byte width of one packed final-block decision. /// - public const int StorageSize = 10; - - /// - /// Stores the luma and shared chroma palette sizes inline with the block. - /// - private InlineArray2 paletteSize; + public const int StorageSize = 8; /// /// Stores the block's prediction-unit syntax inline. @@ -71,24 +65,9 @@ internal struct Av1EncoderBlockStruct set => this.filterIntraMode = (byte)value; } - /// - /// Gets the writable palette sizes for luma and for the shared chroma mode. - /// - [UnscopedRef] - public Span PaletteSize => this.paletteSize; - /// /// Gets the encoder prediction-unit state for the block. /// [UnscopedRef] public ref Av1EncoderPredictionUnit PredictionUnit => ref this.predictionUnit; - - /// - /// Stores the two palette-size values embedded by libaom in block mode information. - /// - [InlineArray(2)] - private struct InlineArray2 - { - private T element; - } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs new file mode 100644 index 0000000000..12ec0aa79d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteInfo.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores the selected luma and chroma palette sizes and colors for one encoder block. +/// +[StructLayout(LayoutKind.Sequential, Size = StorageSize)] +internal struct Av1EncoderPaletteInfo +{ + /// + /// The packed size of two palette sizes and three eight-color planes. + /// + public const int StorageSize = 50; + + private InlineArray2 paletteSizes; + private InlineArray24 paletteColors; + + /// + /// Gets the writable luma and shared chroma palette sizes. + /// + [UnscopedRef] + public Span PaletteSizes => this.paletteSizes; + + /// + /// Gets the selected colors for one color plane. + /// + /// The color plane. + /// The selected colors in palette-index order. + [UnscopedRef] + public Span GetColors(Av1Plane plane) + { + int planeIndex = (int)plane; + int paletteSize = this.paletteSizes[planeIndex == 0 ? 0 : 1]; + Span colors = this.paletteColors; + return colors.Slice(planeIndex * Av1Constants.PaletteMaxSize, paletteSize); + } + + /// + /// Stores the selected colors for one color plane. + /// + /// The color plane. + /// The colors in palette-index order. + public void SetColors(Av1Plane plane, ReadOnlySpan colors) + { + int offset = (int)plane * Av1Constants.PaletteMaxSize; + Span destination = this.paletteColors; + colors.CopyTo(destination[offset..]); + } + + /// + /// Provides fixed storage for the luma and shared chroma palette sizes. + /// + /// The stored value type. + [InlineArray(2)] + private struct InlineArray2 + { + private T element; + } + + /// + /// Provides fixed storage for all three eight-color palette planes. + /// + /// The stored value type. + [InlineArray(3 * Av1Constants.PaletteMaxSize)] + private struct InlineArray24 + { + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteMapBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteMapBuffer.cs new file mode 100644 index 0000000000..3a3e12aa1e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPaletteMapBuffer.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Owns the reusable luma and chroma palette color-index maps for encoder block decisions. +/// +internal sealed class Av1EncoderPaletteMapBuffer : IDisposable +{ + /// + /// The width and height of each maximum-superblock map. + /// + public const int MapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2; + + /// + /// The combined byte length of the luma and chroma maps. + /// + public const int StorageLength = 2 * MapLength * MapLength; + + private readonly IMemoryOwner owner; + private readonly Buffer2D luma; + private readonly Buffer2D chroma; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration providing the encoder allocator. + public Av1EncoderPaletteMapBuffer(Configuration configuration) + { + this.owner = configuration.MemoryAllocator.Allocate(StorageLength); + int mapArea = MapLength * MapLength; + Memory storage = this.owner.Memory[..StorageLength]; + this.luma = Buffer2D.WrapMemory(storage[..mapArea], MapLength, MapLength); + this.chroma = Buffer2D.WrapMemory(storage[mapArea..], MapLength, MapLength); + } + + /// + /// Gets a block-sized view of the luma or shared chroma color-index map. + /// + /// The luma or shared chroma plane class. + /// The padded plane-block width. + /// The padded plane-block height. + /// The reusable map region beginning at the workspace origin. + public Buffer2DRegion GetMap(Av1PlaneType planeType, int width, int height) + => new( + planeType == Av1PlaneType.Y ? this.luma : this.chroma, + new Rectangle(0, 0, width, height)); + + /// + /// Returns the shared palette-map owner to the configured allocator. + /// + public void Dispose() + { + this.luma.Dispose(); + this.chroma.Dispose(); + this.owner.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPictureBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPictureBuffer.cs index c7ea92cb97..2c56ee32a6 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPictureBuffer.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPictureBuffer.cs @@ -21,6 +21,7 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable private readonly Av1NeighborArrayUnit[] blueCoefficientContexts; private readonly Av1NeighborArrayUnit[] redCoefficientContexts; private readonly Av1NeighborArrayUnit[] transformContexts; + private readonly Av1NeighborArrayUnit[] paletteContexts; /// /// Initializes a new instance of the class. @@ -70,7 +71,20 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable partitionContextLength * Unsafe.SizeOf()); int byteContextStorageOffset = checked(partitionStorageOffset + partitionStorageLength); - int stateStorageLength = checked(byteContextStorageOffset + (tileCount * byteContextLengthPerTile)); + int byteContextStorageLength = checked(tileCount * byteContextLengthPerTile); + int byteContextStorageEnd = checked(byteContextStorageOffset + byteContextStorageLength); + int paletteLeftLength = alignedModeInfoRowCount; + int paletteTopLength = this.modeInfo.ModeInfoStride; + int paletteContextLength = checked(paletteLeftLength + paletteTopLength); + int paletteStorageOffset = frameHeader.AllowScreenContentTools + ? Av1Math.AlignPowerOf2(byteContextStorageEnd, 1) + : byteContextStorageEnd; + + int paletteStorageLength = frameHeader.AllowScreenContentTools + ? checked(tileCount * paletteContextLength * Unsafe.SizeOf()) + : 0; + + int stateStorageLength = checked(paletteStorageOffset + paletteStorageLength); // Segmentation and every tile edge share one clean picture lifetime. The partition region begins at its // native alignment, while typed views keep the entropy writer independent from the packed byte owner. @@ -83,12 +97,27 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable stateStorage.Slice(partitionStorageOffset, partitionStorageLength)); Memory partitionStorage = this.partitionContextMemory.Memory; - Memory byteContextStorage = stateStorage[byteContextStorageOffset..]; + Memory byteContextStorage = stateStorage.Slice(byteContextStorageOffset, byteContextStorageLength); this.partitionContexts = new Av1NeighborArrayUnit[tileCount]; this.lumaCoefficientContexts = new Av1NeighborArrayUnit[tileCount]; this.blueCoefficientContexts = new Av1NeighborArrayUnit[tileCount]; this.redCoefficientContexts = new Av1NeighborArrayUnit[tileCount]; this.transformContexts = new Av1NeighborArrayUnit[tileCount]; + Memory paletteStorage = Memory.Empty; + if (frameHeader.AllowScreenContentTools) + { + // Palette entries contain 16-bit colors, so their packed typed region begins at an even byte offset. + ByteMemoryManager paletteMemory = new( + stateStorage.Slice(paletteStorageOffset, paletteStorageLength)); + + paletteStorage = paletteMemory.Memory; + this.paletteContexts = new Av1NeighborArrayUnit[tileCount]; + } + else + { + this.paletteContexts = []; + } + int[][] cdefPreset = new int[tileCount][]; int[] previousQIndex = new int[tileCount]; for (int tileIndex = 0; tileIndex < tileCount; tileIndex++) @@ -137,6 +166,17 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 }; + if (frameHeader.AllowScreenContentTools) + { + this.paletteContexts[tileIndex] = new Av1NeighborArrayUnit( + paletteStorage.Slice(tileIndex * paletteContextLength, paletteContextLength), + paletteLeftLength, + paletteTopLength) + { + GranularityNormalLog2 = Av1Constants.ModeInfoSizeLog2 + }; + } + cdefPreset[tileIndex] = [-1, -1, -1, -1]; previousQIndex[tileIndex] = frameHeader.QuantizationParameters.BaseQIndex; } @@ -148,6 +188,7 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable CbDcSignLevelCoefficientNeighbors = this.blueCoefficientContexts, CrDcSignLevelCoefficientNeighbors = this.redCoefficientContexts, TransformFunctionContexts = this.transformContexts, + PaletteContexts = this.paletteContexts, Sequence = new Av1SequenceControlSet { SequenceHeader = sequenceHeader }, Parent = new Av1PictureParentControlSet { @@ -186,6 +227,11 @@ internal sealed class Av1EncoderPictureBuffer : IDisposable context.Dispose(); } + foreach (Av1NeighborArrayUnit context in this.paletteContexts) + { + context.Dispose(); + } + for (int tileIndex = 0; tileIndex < this.lumaCoefficientContexts.Length; tileIndex++) { this.lumaCoefficientContexts[tileIndex].Dispose(); diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs index b255ec189e..387d213023 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderSuperblockWorkspace.cs @@ -26,7 +26,10 @@ internal sealed class Av1EncoderSuperblockWorkspace : IDisposable /// public const int StorageLength = MaximumFinalBlockCount + ((MaximumPartitionCount + Av1EncoderBlockStruct.StorageSize - 1) / Av1EncoderBlockStruct.StorageSize); + private readonly Configuration configuration; private readonly IMemoryOwner owner; + private Av1EncoderPaletteMapBuffer? paletteMaps; + private Av1EncoderPaletteInfo paletteInfo; /// /// Initializes a new instance of the class. @@ -34,6 +37,7 @@ internal sealed class Av1EncoderSuperblockWorkspace : IDisposable /// The configuration providing the encoder allocator. public Av1EncoderSuperblockWorkspace(Configuration configuration) { + this.configuration = configuration; this.owner = configuration.MemoryAllocator.Allocate(StorageLength); this.Reset(); } @@ -49,6 +53,27 @@ internal sealed class Av1EncoderSuperblockWorkspace : IDisposable public Span PartitionTypes => MemoryMarshal.AsBytes(this.owner.Memory.Span[MaximumFinalBlockCount..])[..MaximumPartitionCount]; + /// + /// Gets the palette sizes and colors selected for the block currently being written. + /// + public ref Av1EncoderPaletteInfo PaletteInfo => ref this.paletteInfo; + + /// + /// Gets the reusable palette maps, allocating their shared owner only after a block selects palette mode. + /// + /// The reusable luma and chroma palette maps. + public Av1EncoderPaletteMapBuffer GetPaletteMaps() + { + Av1EncoderPaletteMapBuffer? maps = this.paletteMaps; + if (maps is null) + { + maps = new Av1EncoderPaletteMapBuffer(this.configuration); + this.paletteMaps = maps; + } + + return maps; + } + /// /// Clears all decisions before the workspace is reused for another superblock. /// @@ -62,10 +87,15 @@ internal sealed class Av1EncoderSuperblockWorkspace : IDisposable this.FinalBlocks.Fill(initialBlock); MemoryMarshal.AsBytes(this.owner.Memory.Span[MaximumFinalBlockCount..]).Clear(); + this.paletteInfo = default; } /// /// Releases the reusable superblock workspace. /// - public void Dispose() => this.owner.Dispose(); + public void Dispose() + { + this.paletteMaps?.Dispose(); + this.owner.Dispose(); + } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteCache.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteCache.cs new file mode 100644 index 0000000000..79aa93183f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteCache.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Builds the sorted unique palette-color cache shared by AV1 encoder and decoder syntax. +/// +internal static class Av1PaletteCache +{ + /// + /// Merges the sorted colors from the available above and left block palettes. + /// + /// The above block's sorted base colors. + /// The left block's sorted base colors. + /// The destination cache, which can hold both palettes. + /// The number of unique colors written to . + public static int Merge( + ReadOnlySpan aboveColors, + ReadOnlySpan leftColors, + Span cache) + { + int aboveIndex = 0; + int leftIndex = 0; + int count = 0; + while (aboveIndex < aboveColors.Length && leftIndex < leftColors.Length) + { + ushort aboveColor = aboveColors[aboveIndex]; + ushort leftColor = leftColors[leftIndex]; + if (leftColor < aboveColor) + { + Add(cache, ref count, leftColor); + leftIndex++; + } + else + { + Add(cache, ref count, aboveColor); + aboveIndex++; + if (leftColor == aboveColor) + { + leftIndex++; + } + } + } + + while (aboveIndex < aboveColors.Length) + { + Add(cache, ref count, aboveColors[aboveIndex++]); + } + + while (leftIndex < leftColors.Length) + { + Add(cache, ref count, leftColors[leftIndex++]); + } + + return count; + } + + private static void Add(Span cache, ref int count, ushort color) + { + if (count == 0 || cache[count - 1] != color) + { + cache[count++] = color; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs index 922d392a0a..9faf066fac 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs @@ -33,6 +33,11 @@ internal class Av1PictureControlSet /// public required Av1NeighborArrayUnit[] TransformFunctionContexts { get; set; } + /// + /// Gets or sets the palette sizes and base colors exposed by the above and left block edges for each tile. + /// + public Av1NeighborArrayUnit[] PaletteContexts { get; set; } = []; + /// /// Gets or sets the sequence-wide encoder state. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs index 753a1cd160..339f6adcb0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -2702,54 +2702,10 @@ internal sealed class Av1TileReader : IAv1TileReader, IDisposable int leftPaletteSize = leftModeInfo is null ? 0 : left.GetPaletteSize(plane); ReadOnlySpan aboveColors = aboveModeInfo is null ? [] : above.GetPaletteColors(plane); ReadOnlySpan leftColors = leftModeInfo is null ? [] : left.GetPaletteColors(plane); - int aboveIndex = 0; - int leftIndex = 0; - int count = 0; - while (aboveIndex < abovePaletteSize && leftIndex < leftPaletteSize) - { - ushort aboveColor = aboveColors[aboveIndex]; - ushort leftColor = leftColors[leftIndex]; - if (leftColor < aboveColor) - { - AddPaletteCacheColor(cache, ref count, leftColor); - leftIndex++; - } - else - { - AddPaletteCacheColor(cache, ref count, aboveColor); - aboveIndex++; - if (leftColor == aboveColor) - { - leftIndex++; - } - } - } - - while (aboveIndex < abovePaletteSize) - { - AddPaletteCacheColor(cache, ref count, aboveColors[aboveIndex++]); - } - - while (leftIndex < leftPaletteSize) - { - AddPaletteCacheColor(cache, ref count, leftColors[leftIndex++]); - } - - return count; - } - - /// - /// Appends a palette cache color unless it duplicates the preceding sorted value. - /// - /// The sorted cache being populated. - /// The number of colors currently stored. - /// The next sorted color. - private static void AddPaletteCacheColor(Span cache, ref int count, ushort color) - { - if (count == 0 || cache[count - 1] != color) - { - cache[count++] = color; - } + return Av1PaletteCache.Merge( + aboveColors[..abovePaletteSize], + leftColors[..leftPaletteSize], + cache); } /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs index b3958daf57..fe64bd6c2c 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs @@ -24,13 +24,15 @@ internal partial class Av1TileWriter /// The zero-based tile index. /// The mode information to publish. /// The encoder block state to publish. + /// The current block's palette sizes and colors. void EncodeBlock( Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, Point blockOrigin, ushort tileIndex, ref Av1MacroBlockModeInfo modeInfo, - ref Av1EncoderBlockStruct block); + ref Av1EncoderBlockStruct block, + ref Av1EncoderPaletteInfo paletteInfo); } private readonly struct PrecomputedBlockEncodingHandler : IBlockEncodingHandler @@ -41,7 +43,8 @@ internal partial class Av1TileWriter Point blockOrigin, ushort tileIndex, ref Av1MacroBlockModeInfo modeInfo, - ref Av1EncoderBlockStruct block) + ref Av1EncoderBlockStruct block, + ref Av1EncoderPaletteInfo paletteInfo) { } } diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs index a7953f441f..5bad7f31b7 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; @@ -716,13 +717,17 @@ internal partial class Av1TileWriter // Producing the decision here exposes exactly the reconstructed neighbors, coefficient contexts, // and adaptive probabilities that the following syntax writes. + tb_ptr.Workspace.PaletteInfo = default; + ref Av1EncoderPaletteInfo paletteInfo = ref tb_ptr.Workspace.PaletteInfo; blockEncoder.EncodeBlock( writer, macroBlock, blockOrigin, tile_idx, ref macroBlockModeInfo, - ref blk_ptr); + ref blk_ptr, + ref paletteInfo); + bool skipWritingCoefficients = macroBlockModeInfo.Block.Skip; // This encoder path currently writes intra frames only, so every block follows the key-frame mode syntax. @@ -791,35 +796,65 @@ internal partial class Av1TileWriter } } - if (!macroBlockModeInfo.Block.UseIntraBlockCopy && IsPaletteAllowed(frm_hdr.AllowScreenContentTools, blockSize)) + bool paletteAllowed = !macroBlockModeInfo.Block.UseIntraBlockCopy && + IsPaletteAllowed(frm_hdr.AllowScreenContentTools, blockSize); + + if (paletteAllowed) { WritePaletteModeInfo( scs, + pcs, writer, + macroBlock, macroBlockModeInfo, - ref blk_ptr, + ref paletteInfo, blockSize, - blockOrigin >> Av1Constants.ModeInfoSizeLog2); + blockOrigin, + tile_idx, + blk_ptr.HasChroma); } if (!macroBlockModeInfo.Block.UseIntraBlockCopy && - IsFilterIntraAllowed(scs.SequenceHeader.EnableFilterIntra, blockSize, blk_ptr.PaletteSize[0], intra_luma_mode)) + IsFilterIntraAllowed( + scs.SequenceHeader.EnableFilterIntra, + blockSize, + paletteInfo.PaletteSizes[0], + intra_luma_mode)) { writer.WriteFilterIntraMode(blk_ptr.FilterIntraMode, blockSize); } - if (!macroBlockModeInfo.Block.UseIntraBlockCopy) + if (paletteAllowed) { - Guard.IsTrue(blk_ptr.PaletteSize[1] == 0, nameof(blk_ptr), "Palette of chroma plane shall be empty."); - - // TOKENEXTRA tok = entropyCodingContext.tok; - for (int plane = 0; plane < 2; ++plane) + ObuColorConfig colorConfig = scs.SequenceHeader.ColorConfig; + int palettePlaneCount = Math.Min(2, colorConfig.PlaneCount); + for (int plane = 0; plane < palettePlaneCount; ++plane) { - int palette_size_plane = blk_ptr.PaletteSize[plane]; - if (palette_size_plane > 0) + int paletteSize = paletteInfo.PaletteSizes[plane]; + if (paletteSize == 0) { - throw new NotImplementedException("Tokenizing palette not implemented."); + continue; } + + Av1PlaneType planeType = (Av1PlaneType)plane; + int subX = planeType == Av1PlaneType.Uv && colorConfig.SubSamplingX ? 1 : 0; + int subY = planeType == Av1PlaneType.Uv && colorConfig.SubSamplingY ? 1 : 0; + int blockWidth = blockSize.GetWidth(); + int blockHeight = blockSize.GetHeight(); + int planeWidth = blockWidth >> subX; + int planeHeight = blockHeight >> subY; + int columns = Math.Min(blockWidth, frm_hdr.FrameSize.FrameWidth - blockOrigin.X) >> subX; + int rows = Math.Min(blockHeight, frm_hdr.FrameSize.FrameHeight - blockOrigin.Y) >> subY; + Buffer2DRegion colorIndexMap = tb_ptr.Workspace + .GetPaletteMaps() + .GetMap(planeType, planeWidth, planeHeight); + + writer.WritePaletteColorMap( + paletteSize, + planeType, + rows, + columns, + colorIndexMap); } } @@ -851,7 +886,21 @@ internal partial class Av1TileWriter } } - // Neighbor state must be updated after all symbols for the block have used the preceding contexts. + // Palette colors are published only after the current block's mode and map have consumed the preceding edges. + if (frm_hdr.AllowScreenContentTools) + { + const Av1NeighborArrayUnit.UnitMask PaletteContextMask = + Av1NeighborArrayUnit.UnitMask.Left | + Av1NeighborArrayUnit.UnitMask.Top; + + pcs.PaletteContexts[tile_idx].UnitModeWrite( + paletteInfo, + blockOrigin, + new Size(blockSize.GetWidth(), blockSize.GetHeight()), + PaletteContextMask); + } + + // Coefficient neighbor state follows the same post-symbol ownership boundary. UpdateNeighbors(pcs, entropyCodingContext, blockOrigin, ref blk_ptr, tile_idx, blockSize); } @@ -1087,21 +1136,115 @@ internal partial class Av1TileWriter /// Writes luma and chroma palette-mode syntax for a block. /// /// The sequence coding state. + /// The picture coding state. /// The tile symbol encoder. + /// The current block's mapped neighbor state. /// The selected block modes. - /// The encoder block state. + /// The selected palette sizes and colors. /// The block size. - /// The block position in mode-information units. - /// Palette-mode encoding is not implemented. - private static void WritePaletteModeInfo( + /// The absolute luma-sample origin. + /// The zero-based tile index. + /// Whether the block owns chroma syntax. + internal static void WritePaletteModeInfo( Av1SequenceControlSet scs, + Av1PictureControlSet pcs, Av1SymbolEncoder writer, + Av1MacroBlockD macroBlock, Av1MacroBlockModeInfo macroBlockModeInfo, - ref Av1EncoderBlockStruct blk_ptr, + ref Av1EncoderPaletteInfo paletteInfo, Av1BlockSize blockSize, - Point point) + Point blockOrigin, + int tileIndex, + bool hasChroma) + { + int blockSizeContext = Av1Math.Log2(blockSize.GetWidth() * blockSize.GetHeight()) - 6; + Av1NeighborArrayUnit paletteContexts = pcs.PaletteContexts[tileIndex]; + int yPaletteSize = paletteInfo.PaletteSizes[0]; + if (macroBlockModeInfo.Block.Mode == Av1PredictionMode.DC) + { + int neighborContext = 0; + if (macroBlock.IsUpAvailable && + paletteContexts.Top[paletteContexts.GetTopIndex(blockOrigin)].PaletteSizes[0] != 0) + { + neighborContext++; + } + + if (macroBlock.IsLeftAvailable && + paletteContexts.Left[paletteContexts.GetLeftIndex(blockOrigin)].PaletteSizes[0] != 0) + { + neighborContext++; + } + + writer.WritePaletteYMode(yPaletteSize != 0, blockSizeContext, neighborContext); + if (yPaletteSize != 0) + { + writer.WritePaletteSize(yPaletteSize, blockSizeContext, Av1PlaneType.Y); + Span colorCache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize]; + int cacheSize = GetPaletteCache( + paletteContexts, + macroBlock, + blockOrigin, + Av1Plane.Y, + colorCache); + + writer.WritePaletteYColors( + colorCache[..cacheSize], + paletteInfo.GetColors(Av1Plane.Y), + scs.SequenceHeader.ColorConfig.BitDepth.GetBitCount()); + } + } + + int uvPaletteSize = paletteInfo.PaletteSizes[1]; + if (scs.SequenceHeader.ColorConfig.PlaneCount > 1 && + macroBlockModeInfo.Block.UvMode == Av1ChromaPredictionMode.DC && + hasChroma) + { + writer.WritePaletteUvMode(uvPaletteSize != 0, yPaletteSize != 0); + if (uvPaletteSize != 0) + { + writer.WritePaletteSize(uvPaletteSize, blockSizeContext, Av1PlaneType.Uv); + Span colorCache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize]; + int cacheSize = GetPaletteCache( + paletteContexts, + macroBlock, + blockOrigin, + Av1Plane.U, + colorCache); + + writer.WritePaletteUvColors( + colorCache[..cacheSize], + paletteInfo.GetColors(Av1Plane.U), + paletteInfo.GetColors(Av1Plane.V), + scs.SequenceHeader.ColorConfig.BitDepth.GetBitCount()); + } + } + } + + /// + /// Builds the sorted palette-color cache from the available above and left encoder edges. + /// + private static int GetPaletteCache( + Av1NeighborArrayUnit paletteContexts, + Av1MacroBlockD macroBlock, + Point blockOrigin, + Av1Plane plane, + Span cache) { - throw new NotImplementedException("Palette mode encoding not implemented."); + // AV1 excludes the above palette at each 64-sample row boundary, even with 128x128 superblocks. + bool hasAbove = macroBlock.IsUpAvailable && + (blockOrigin.Y & (Av1BlockSize.Block64x64.GetHeight() - 1)) != 0; + + Av1EncoderPaletteInfo above = hasAbove + ? paletteContexts.Top[paletteContexts.GetTopIndex(blockOrigin)] + : default; + + Av1EncoderPaletteInfo left = macroBlock.IsLeftAvailable + ? paletteContexts.Left[paletteContexts.GetLeftIndex(blockOrigin)] + : default; + + ReadOnlySpan aboveColors = hasAbove ? above.GetColors(plane) : []; + ReadOnlySpan leftColors = macroBlock.IsLeftAvailable ? left.GetColors(plane) : []; + return Av1PaletteCache.Merge(aboveColors, leftColors, cache); } /// @@ -1222,21 +1365,6 @@ internal partial class Av1TileWriter } } - /// - /// Determines whether the encoder palette level and block dimensions permit palette mode. - /// - /// The nonzero encoder palette level. - /// The block size. - /// when palette mode is enabled for the block; otherwise, . - private static bool IsPaletteAllowed(int allowPalette, Av1BlockSize blockSize) - { - Guard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.AllSizes, nameof(blockSize)); - return allowPalette != 0 && - blockSize.GetWidth() <= 64 && - blockSize.GetHeight() <= 64 && - blockSize >= Av1BlockSize.Block8x8; - } - /// /// Determines whether screen-content tools and block dimensions permit palette mode. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs index 29d3dd2ec8..7c41848072 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs @@ -234,20 +234,21 @@ public class Av1CoefficientsEntropyTests Assert.Equal(Av1EncoderSuperblockWorkspace.MaximumFinalBlockCount, workspace.FinalBlocks.Length); Assert.Equal(Av1EncoderSuperblockWorkspace.MaximumPartitionCount, workspace.PartitionTypes.Length); Assert.Equal(Av1EncoderBlockStruct.StorageSize, Unsafe.SizeOf()); - Assert.Equal(0, workspace.FinalBlocks[0].PaletteSize[0]); + Assert.Equal(Av1EncoderPaletteInfo.StorageSize, Unsafe.SizeOf()); + Assert.Equal(0, workspace.PaletteInfo.PaletteSizes[0]); Assert.Equal(0, workspace.FinalBlocks[^1].QuantizationIndex); Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, workspace.FinalBlocks[0].FilterIntraMode); Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, workspace.FinalBlocks[^1].FilterIntraMode); Assert.Equal(0, workspace.PartitionTypes[^1]); - workspace.FinalBlocks[0].PaletteSize[0] = 7; + workspace.PaletteInfo.PaletteSizes[0] = 7; workspace.FinalBlocks[0].FilterIntraMode = Av1FilterIntraMode.DC; workspace.FinalBlocks[^1].QuantizationIndex = 255; workspace.FinalBlocks[^1].FilterIntraMode = Av1FilterIntraMode.Paeth; workspace.PartitionTypes.Fill(byte.MaxValue); workspace.Reset(); - Assert.Equal(0, workspace.FinalBlocks[0].PaletteSize[0]); + Assert.Equal(0, workspace.PaletteInfo.PaletteSizes[0]); Assert.Equal(0, workspace.FinalBlocks[^1].QuantizationIndex); Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, workspace.FinalBlocks[0].FilterIntraMode); Assert.Equal(Av1FilterIntraMode.AllFilterIntraModes, workspace.FinalBlocks[^1].FilterIntraMode); @@ -265,28 +266,174 @@ public class Av1CoefficientsEntropyTests public void EncoderBlocksKeepInlineModeStateWithoutPerBlockAllocations() { Av1EncoderBlockStruct[] blocks = new Av1EncoderBlockStruct[2]; + Av1EncoderPaletteInfo[] palettes = new Av1EncoderPaletteInfo[2]; // Exercise the inline-array accessors before measuring so one-time runtime generic initialization is // excluded from the steady-state allocation contract used for every encoded block. ref Av1EncoderBlockStruct warmupBlock = ref blocks[0]; - warmupBlock.PaletteSize[0] = 1; + ref Av1EncoderPaletteInfo warmupPalette = ref palettes[0]; + warmupPalette.PaletteSizes[0] = 1; warmupBlock.PredictionUnit.AngleDelta[(int)Av1PlaneType.Y] = 1; long before = GC.GetAllocatedBytesForCurrentThread(); ref Av1EncoderBlockStruct block = ref blocks[1]; - block.PaletteSize[0] = 3; - block.PaletteSize[1] = 5; + ref Av1EncoderPaletteInfo palette = ref palettes[1]; + palette.PaletteSizes[0] = 3; + palette.PaletteSizes[1] = 5; block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Y] = -2; block.PredictionUnit.AngleDelta[(int)Av1PlaneType.Uv] = 3; long allocated = GC.GetAllocatedBytesForCurrentThread() - before; - Assert.Equal(3, blocks[1].PaletteSize[0]); - Assert.Equal(5, blocks[1].PaletteSize[1]); + Assert.Equal(3, palettes[1].PaletteSizes[0]); + Assert.Equal(5, palettes[1].PaletteSizes[1]); Assert.Equal(-2, blocks[1].PredictionUnit.AngleDelta[(int)Av1PlaneType.Y]); Assert.Equal(3, blocks[1].PredictionUnit.AngleDelta[(int)Av1PlaneType.Uv]); Assert.Equal(0, allocated); } + [Fact] + public void EncoderPaletteMapsUseOneLazyExactSizeOwner() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + TestMemoryAllocator.AllocationRequest[] allocations; + using (Av1EncoderSuperblockWorkspace workspace = new(configuration)) + { + Assert.Single(allocator.AllocationLog); + Assert.Empty(allocator.ReturnLog); + + Av1EncoderPaletteMapBuffer maps = workspace.GetPaletteMaps(); + Assert.Same(maps, workspace.GetPaletteMaps()); + allocations = allocator.AllocationLog.ToArray(); + Assert.Equal(2, allocations.Length); + Assert.Equal(typeof(byte), allocations[1].ElementType); + Assert.Equal(Av1EncoderPaletteMapBuffer.StorageLength, allocations[1].Length); + Assert.Equal(AllocationOptions.None, allocations[1].AllocationOptions); + + Buffer2DRegion luma = maps.GetMap(Av1PlaneType.Y, 64, 64); + Buffer2DRegion chroma = maps.GetMap(Av1PlaneType.Uv, 32, 32); + luma.DangerousGetRowSpan(0)[0] = 3; + chroma.DangerousGetRowSpan(0)[0] = 5; + Assert.Equal(3, luma.DangerousGetRowSpan(0)[0]); + Assert.Equal(5, chroma.DangerousGetRowSpan(0)[0]); + } + + Assert.Equal(2, allocator.ReturnLog.Count); + Assert.Equal( + allocations.Select(x => x.AllocationId).Order(), + allocator.ReturnLog.Select(x => x.AllocationId).Order()); + } + + [Fact] + public void PaletteModeWriterMatchesColorCacheBoundaryAndRoundTrips() + { + const int Width = 16; + const int Height = 72; + const int BlockSizeContext = 0; + Point blockOrigin = new(8, 64); + ObuColorConfig colorConfig = new() + { + IsMonochrome = false, + SubSamplingX = false, + SubSamplingY = false, + BitDepth = Av1BitDepth.EightBit + }; + + ObuTileGroupHeader tiles = new() + { + TileColumnCount = 1, + TileRowCount = 1 + }; + + tiles.TileColumnStartModeInfo[1] = Width >> Av1Constants.ModeInfoSizeLog2; + tiles.TileRowStartModeInfo[1] = Height >> Av1Constants.ModeInfoSizeLog2; + ObuSequenceHeader sequenceHeader = new() + { + ColorConfig = colorConfig + }; + + ObuFrameHeader frameHeader = new() + { + AllowScreenContentTools = true, + ModeInfoColumnCount = Width >> Av1Constants.ModeInfoSizeLog2, + ModeInfoRowCount = Height >> Av1Constants.ModeInfoSizeLog2, + TilesInfo = tiles + }; + + using Av1EncoderPictureBuffer pictureBuffer = new( + Configuration.Default, + sequenceHeader, + frameHeader, + Width, + Height); + + Av1PictureControlSet picture = pictureBuffer.Picture; + Av1NeighborArrayUnit paletteContexts = Assert.Single(picture.PaletteContexts); + ref Av1EncoderPaletteInfo above = ref paletteContexts.Top[paletteContexts.GetTopIndex(blockOrigin)]; + above.PaletteSizes[0] = 2; + above.PaletteSizes[1] = 2; + above.SetColors(Av1Plane.Y, [10, 30]); + above.SetColors(Av1Plane.U, [15, 35]); + ref Av1EncoderPaletteInfo left = ref paletteContexts.Left[paletteContexts.GetLeftIndex(blockOrigin)]; + left.PaletteSizes[0] = 2; + left.PaletteSizes[1] = 2; + left.SetColors(Av1Plane.Y, [20, 40]); + left.SetColors(Av1Plane.U, [25, 45]); + + Av1EncoderPaletteInfo current = default; + current.PaletteSizes[0] = 3; + current.PaletteSizes[1] = 3; + current.SetColors(Av1Plane.Y, [20, 50, 70]); + current.SetColors(Av1Plane.U, [25, 55, 80]); + current.SetColors(Av1Plane.V, [10, 12, 9]); + Av1MacroBlockModeInfo modeInfo = default; + modeInfo.Block = new Av1EncoderBlockModeInfo + { + BlockSize = Av1BlockSize.Block8x8, + Mode = Av1PredictionMode.DC, + UvMode = Av1ChromaPredictionMode.DC + }; + + Av1MacroBlockD macroBlock = new() + { + Tile = new Av1TileInfo(0, 0, frameHeader), + IsUpAvailable = true, + IsLeftAvailable = true + }; + + using Av1SymbolEncoder encoder = new(Configuration.Default, 128, BaseQIndex); + Av1TileWriter.WritePaletteModeInfo( + picture.Sequence, + picture, + encoder, + macroBlock, + modeInfo, + ref current, + Av1BlockSize.Block8x8, + blockOrigin, + tileIndex: 0, + hasChroma: true); + + using IMemoryOwner encoded = encoder.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + Assert.True(decoder.ReadPaletteYMode(BlockSizeContext, neighborContext: 2)); + Assert.Equal(3, decoder.ReadPaletteSize(BlockSizeContext, Av1PlaneType.Y)); + Span decodedY = stackalloc ushort[3]; + decoder.ReadPaletteYColors([20, 40], 3, bitDepth: 8, decodedY); + Assert.Equal([20, 50, 70], decodedY.ToArray()); + Assert.True(decoder.ReadPaletteUvMode(hasLumaPalette: true)); + Assert.Equal(3, decoder.ReadPaletteSize(BlockSizeContext, Av1PlaneType.Uv)); + Span decodedU = stackalloc ushort[3]; + Span decodedV = stackalloc ushort[3]; + decoder.ReadPaletteUvColors([25, 45], 3, bitDepth: 8, decodedU, decodedV); + Assert.Equal([25, 55, 80], decodedU.ToArray()); + Assert.Equal([10, 12, 9], decodedV.ToArray()); + decoder.ValidateTrailingBits(); + } + [Theory] [InlineData(false, 6, 4096, 1024, 6144, 256, 64, 384, 36864L)] [InlineData(true, 2, 16384, 4096, 24576, 1024, 256, 1536, 49152L)] diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderModeInfoBufferTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderModeInfoBufferTests.cs index 815a18b0c8..8f53d73f35 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderModeInfoBufferTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EncoderModeInfoBufferTests.cs @@ -46,8 +46,12 @@ public class Av1EncoderModeInfoBufferTests Assert.Equal(allocation.AllocationId, returned.AllocationId); } - [Fact] - public void PictureBufferPacksAllPictureStateIntoTwoAllocatorOwners() + [Theory] + [InlineData(false, 336)] + [InlineData(true, 3_536)] + public unsafe void PictureBufferPacksAllPictureStateIntoTwoAllocatorOwners( + bool allowScreenContentTools, + int expectedStateStorageLength) { const int Width = 16; const int Height = 16; @@ -79,6 +83,7 @@ public class Av1EncoderModeInfoBufferTests ObuFrameHeader frameHeader = new() { + AllowScreenContentTools = allowScreenContentTools, ModeInfoColumnCount = Width >> Av1Constants.ModeInfoSizeLog2, ModeInfoRowCount = Height >> Av1Constants.ModeInfoSizeLog2, TilesInfo = tiles @@ -98,7 +103,7 @@ public class Av1EncoderModeInfoBufferTests Assert.Equal(6_144, allocations[0].Length); Assert.Equal(AllocationOptions.Clean, allocations[0].AllocationOptions); Assert.Equal(typeof(byte), allocations[1].ElementType); - Assert.Equal(336, allocations[1].Length); + Assert.Equal(expectedStateStorageLength, allocations[1].Length); Assert.Equal(AllocationOptions.Clean, allocations[1].AllocationOptions); Assert.Empty(allocator.ReturnLog); @@ -112,6 +117,28 @@ public class Av1EncoderModeInfoBufferTests Assert.Equal(16, picture.CbDcSignLevelCoefficientNeighbors[0].Top.Length); Assert.Equal(32, picture.TransformFunctionContexts[0].Left.Length); Assert.Equal(32, picture.TransformFunctionContexts[0].Top.Length); + if (allowScreenContentTools) + { + Av1NeighborArrayUnit paletteContext = Assert.Single(picture.PaletteContexts); + Assert.Equal(32, paletteContext.Left.Length); + Assert.Equal(32, paletteContext.Top.Length); + Assert.Equal(Av1Constants.ModeInfoSizeLog2, paletteContext.GranularityNormalLog2); + Assert.Equal(0, paletteContext.Left[0].PaletteSizes[0]); + Assert.Equal(0, paletteContext.Top[^1].PaletteSizes[1]); + + // The typed palette region starts at its natural 16-bit alignment inside the shared byte owner. + fixed (Av1EncoderPaletteInfo* pointer = paletteContext.Left) + { + Assert.Equal((nuint)0, (nuint)pointer % (nuint)sizeof(ushort)); + } + + paletteContext.Left[0].PaletteSizes[0] = 3; + Assert.Equal(0, paletteContext.Top[0].PaletteSizes[0]); + } + else + { + Assert.Empty(picture.PaletteContexts); + } } Assert.Equal(2, allocator.ReturnLog.Count); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs index 5f9a40dd17..c701172e6b 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -1254,6 +1254,18 @@ public class Av1EntropyTests encoder.GetPaletteColorMapCost(3, Av1PlaneType.Y, 2, 2, region)); } + [Fact] + public void PaletteCacheMergesSortedNeighborColorsWithoutDuplicates() + { + ReadOnlySpan above = [1, 3, 5, 7]; + ReadOnlySpan left = [2, 3, 6, 7]; + Span cache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize]; + + int count = Av1PaletteCache.Merge(above, left, cache); + + Assert.Equal([1, 2, 3, 5, 6, 7], cache[..count].ToArray()); + } + [Theory] [InlineData(1, 1, 0, 0, 0, 1, 4, 1)] [InlineData(1, 1, 0, 1, 0, 1, 3, 1)] diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs index d047ba8f98..cd01d1e6dc 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs @@ -610,6 +610,112 @@ public class Av1IntraSuperblockEncoderTests Assert.NotEqual(0, encoded.GetSpan().Length); } + [Fact] + public void ProductionWriterConsumesPaletteMapAndPublishesPaletteEdges() + { + const int Width = 8; + const int Height = 8; + const int QIndex = 23; + ObuColorConfig colorConfig = new() + { + IsMonochrome = true, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + }; + + ObuTileGroupHeader tiles = new() + { + TileColumnCount = 1, + TileRowCount = 1 + }; + + tiles.TileColumnStartModeInfo[1] = Width >> Av1Constants.ModeInfoSizeLog2; + tiles.TileRowStartModeInfo[1] = Height >> Av1Constants.ModeInfoSizeLog2; + ObuSequenceHeader sequenceHeader = new() + { + ColorConfig = colorConfig + }; + + ObuFrameHeader frameHeader = new() + { + AllowScreenContentTools = true, + ModeInfoColumnCount = Width >> Av1Constants.ModeInfoSizeLog2, + ModeInfoRowCount = Height >> Av1Constants.ModeInfoSizeLog2, + FrameSize = new ObuFrameSize + { + FrameWidth = Width, + FrameHeight = Height + }, + TilesInfo = tiles + }; + + frameHeader.QuantizationParameters.BaseQIndex = QIndex; + frameHeader.QuantizationParameters.QIndex.Fill(QIndex); + byte[][] payloads = new byte[2][]; + for (int mapVariant = 0; mapVariant < payloads.Length; mapVariant++) + { + using Av1EncoderPictureBuffer pictureBuffer = new( + Configuration.Default, + sequenceHeader, + frameHeader, + Width, + Height); + + using Av1EncoderCoefficientBuffer coefficients = new( + Configuration.Default, + sequenceHeader, + Width, + Height); + + using Av1EncoderSuperblockWorkspace workspace = new(Configuration.Default); + Av1Superblock superblock = new() + { + Workspace = workspace, + TileInfo = new Av1TileInfo(0, 0, frameHeader), + Index = 0 + }; + + Av1PictureControlSet picture = pictureBuffer.Picture; + Av1IntraSuperblockEncoder.Prepare(picture, superblock, Point.Empty); + Av1TileWriter.Av1EntropyCodingContext entropyContext = new() + { + MacroBlock = new Av1MacroBlockD { Tile = superblock.TileInfo }, + MacroBlockModeInfo = picture.GetMacroBlockModeInfo(default), + SuperblockOrigin = default + }; + + PaletteBlockEncoder blockEncoder = new(workspace, QIndex, mapVariant); + using Av1SymbolEncoder writer = new(Configuration.Default, 128, QIndex); + Av1TileWriter.WriteSuperblock( + picture, + entropyContext, + writer, + superblock, + coefficients, + tileIndex: 0, + ref blockEncoder); + + using IMemoryOwner encoded = writer.Exit(); + payloads[mapVariant] = encoded.GetSpan().ToArray(); + Assert.Equal(1, blockEncoder.Count); + Av1NeighborArrayUnit paletteContext = Assert.Single(picture.PaletteContexts); + for (int index = 0; index < 2; index++) + { + Assert.Equal(3, paletteContext.Top[index].PaletteSizes[0]); + Assert.Equal(3, paletteContext.Left[index].PaletteSizes[0]); + Assert.Equal([16, 128, 240], paletteContext.Top[index].GetColors(Av1Plane.Y).ToArray()); + Assert.Equal([16, 128, 240], paletteContext.Left[index].GetColors(Av1Plane.Y).ToArray()); + } + + Assert.Equal(0, paletteContext.Top[2].PaletteSizes[0]); + Assert.Equal(0, paletteContext.Left[2].PaletteSizes[0]); + } + + // Changing only the selected color indices must change the range-coded tile payload. + Assert.False(payloads[0].SequenceEqual(payloads[1])); + } + [Theory] [InlineData((int)Av1PredictionMode.Vertical, 0)] [InlineData((int)Av1PredictionMode.Horizontal, 0)] @@ -1911,7 +2017,8 @@ public class Av1IntraSuperblockEncoderTests Point blockOrigin, ushort tileIndex, ref Av1MacroBlockModeInfo modeInfo, - ref Av1EncoderBlockStruct block) + ref Av1EncoderBlockStruct block, + ref Av1EncoderPaletteInfo paletteInfo) { this.costs[this.Count++] = Av1TileWriter.GetLumaModeCost( writer, @@ -1936,4 +2043,79 @@ public class Av1IntraSuperblockEncoderTests block.SegmentId = 0; } } + + /// + /// Supplies one skipped monochrome palette block to the production tile writer. + /// + private struct PaletteBlockEncoder : Av1TileWriter.IBlockEncodingHandler + { + private readonly Av1EncoderSuperblockWorkspace workspace; + private readonly int qIndex; + private readonly int mapVariant; + + /// + /// Initializes a new instance of the struct. + /// + /// The workspace that owns the palette index map. + /// The block quantizer index. + /// The map pattern selected by the test. + public PaletteBlockEncoder( + Av1EncoderSuperblockWorkspace workspace, + int qIndex, + int mapVariant) + { + this.workspace = workspace; + this.qIndex = qIndex; + this.mapVariant = mapVariant; + this.Count = 0; + } + + /// + /// Gets the number of final blocks visited by the writer. + /// + public int Count { get; private set; } + + /// + public void EncodeBlock( + Av1SymbolEncoder writer, + Av1MacroBlockD macroBlock, + Point blockOrigin, + ushort tileIndex, + ref Av1MacroBlockModeInfo modeInfo, + ref Av1EncoderBlockStruct block, + ref Av1EncoderPaletteInfo paletteInfo) + { + this.Count++; + modeInfo.Block = new Av1EncoderBlockModeInfo + { + BlockSize = Av1BlockSize.Block8x8, + PartitionType = Av1PartitionType.None, + SegmentId = 0, + Skip = true, + TransformSize = Av1TransformSize.Size8x8, + Mode = Av1PredictionMode.DC, + UvMode = Av1ChromaPredictionMode.DC + }; + + block.HasChroma = false; + block.QuantizationIndex = this.qIndex; + block.SegmentId = 0; + paletteInfo.PaletteSizes[0] = 3; + paletteInfo.SetColors(Av1Plane.Y, [16, 128, 240]); + Buffer2DRegion map = this.workspace + .GetPaletteMaps() + .GetMap(Av1PlaneType.Y, 8, 8); + + for (int row = 0; row < map.Height; row++) + { + Span mapRow = map.DangerousGetRowSpan(row); + for (int column = 0; column < map.Width; column++) + { + mapRow[column] = this.mapVariant == 0 + ? (byte)0 + : (byte)((row + column) % 3); + } + } + } + } }