From 6e01c2763a9122edbfc0645bfa37119a71914d37 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 2 Sep 2026 16:26:42 +1000 Subject: [PATCH] Consolidate AV1 palette scratch --- HEIF_IMPLEMENTATION_PLAN.md | 11 ++--- src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs | 42 ++++++++++--------- .../Av1/Av1ReconstructionConformanceTests.cs | 8 ++-- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 70d372c0e5..b2be2557ba 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -646,11 +646,12 @@ Verified partition, mode, segmentation, quantization, and transform checkpoint e Each transform resets and clears only its active padded geometry, so no transform creates an allocation. Allocation tracking over all eight minimum- and maximum-quantizer frames proves exactly one coefficient scratch allocation per frame and exactly-once return after decoder disposal. -- [x] Palette index maps use two allocator-backed 128x128 decoder-session scratch buffers, one for luma and - one for chroma. Each parsed superblock is reconstructed before either buffer is reused, and each block - clears only its transient `Buffer2DRegion` view after prediction. The fixed 32 KiB session cost replaces - the former two full-frame maps without copies or per-block allocations. A 1 KiB constrained allocator - splits both buffers across memory groups. The four-case palette set passes on net10.0 and net11.0 with +- [x] Palette index maps use one allocator-backed 32 KiB decoder-session owner with non-owning 128x128 luma + and chroma views. Each parsed superblock is reconstructed before either view is reused, and each block + clears only its transient `Buffer2DRegion` after prediction. The fixed session cost replaces the former + full-frame maps without copies, fragmented memory groups, constructor rollback, or per-block allocations. + The one-rent ownership regression and native palette reconstruction pass on net11.0, the complete HEIF/AV1 + namespace passes 8,808 of 8,808 direct VSTest cases in Release, and the four-case palette set passes with exact native and presentation output, truncated-entropy rejection, and balanced exactly-once disposal. - [x] `Av1BlockModeInfo` is value storage, removing the managed object allocation formerly created for every decoded coding block. Explicit `ModeInfoIndex` values preserve libaom's mode-info identity semantics at diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs index a926fec5e3..b755ee8a05 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using SixLabors.ImageSharp.Formats.Heif.Av1.Color; using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; @@ -41,6 +42,11 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable /// private readonly Buffer2D chromaPaletteColorIndexMap; + /// + /// The shared backing owner for both reusable palette maps. + /// + private readonly IMemoryOwner paletteColorIndexMapOwner; + /// /// The reconstructed references and selected presentation output owned by the current bounded decode session. /// @@ -96,26 +102,21 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable this.configuration = configuration; this.obuReader = new(operatingPointIndex, this.referenceFrames); - Buffer2D? lumaPaletteColorIndexMap = null; - Buffer2D? chromaPaletteColorIndexMap = null; - - try - { - // Sequential tile decoding needs only the palette indices belonging to the current superblock. Keeping - // two maximum-superblock surfaces at decoder scope makes the memory bound independent of frame size and - // reuses the same allocator rents across every frame in the bounded sequence. - int paletteMapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2; - lumaPaletteColorIndexMap = configuration.MemoryAllocator.Allocate2D(paletteMapLength, paletteMapLength); - chromaPaletteColorIndexMap = configuration.MemoryAllocator.Allocate2D(paletteMapLength, paletteMapLength); - this.lumaPaletteColorIndexMap = lumaPaletteColorIndexMap; - this.chromaPaletteColorIndexMap = chromaPaletteColorIndexMap; - } - catch - { - chromaPaletteColorIndexMap?.Dispose(); - lumaPaletteColorIndexMap?.Dispose(); - throw; - } + // Sequential tile decoding needs only the palette indices belonging to the current superblock. One fixed + // owner keeps both maximum-superblock maps reusable across the bounded session without fragmented group rents. + int paletteMapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2; + int paletteMapArea = paletteMapLength * paletteMapLength; + this.paletteColorIndexMapOwner = configuration.MemoryAllocator.Allocate(2 * paletteMapArea); + Memory paletteMaps = this.paletteColorIndexMapOwner.Memory; + this.lumaPaletteColorIndexMap = Buffer2D.WrapMemory( + paletteMaps[..paletteMapArea], + paletteMapLength, + paletteMapLength); + + this.chromaPaletteColorIndexMap = Buffer2D.WrapMemory( + paletteMaps[paletteMapArea..], + paletteMapLength, + paletteMapLength); } /// @@ -958,6 +959,7 @@ internal sealed class Av1Decoder : IAv1TileReader, IDisposable this.FrameInfo = null; this.lumaPaletteColorIndexMap.Dispose(); this.chromaPaletteColorIndexMap.Dispose(); + this.paletteColorIndexMapOwner.Dispose(); } /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs index 19ff553676..52fbcdda2a 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs @@ -576,15 +576,15 @@ public class Av1ReconstructionConformanceTests using (Av1Decoder decoder = new(configuration)) { int paletteMapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2; - int expectedPaletteAllocationCount = (2 * paletteMapLength * paletteMapLength) / allocator.BufferCapacityInBytes; - Assert.Equal(expectedPaletteAllocationCount, allocator.AllocationLog.Count); - int finalPaletteAllocationId = allocator.AllocationLog[^1].AllocationId; + TestMemoryAllocator.AllocationRequest paletteMaps = Assert.Single(allocator.AllocationLog); + Assert.Equal(2 * paletteMapLength * paletteMapLength, paletteMaps.Length); + int paletteAllocationId = paletteMaps.AllocationId; using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); Assert.Equal(RequiredPaletteCoverage, GetPaletteCoverage(decoder)); AssertNativePlanesEqual(decoder, frameBuffer, reference); - Assert.DoesNotContain(allocator.ReturnLog, returned => returned.AllocationId <= finalPaletteAllocationId); + Assert.DoesNotContain(allocator.ReturnLog, returned => returned.AllocationId == paletteAllocationId); } Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count);