From af50bb8e9a1c9c2f9f4d2a0bbf2709795b7fb2ff Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 3 Sep 2026 11:43:37 +1000 Subject: [PATCH] Reuse AV1 mode decision scratch --- HEIF_IMPLEMENTATION_PLAN.md | 1 + .../Av1/Pipeline/Av1EncoderBlockWorkspace.cs | 26 +- .../Av1EncoderModeDecisionWorkspace.cs | 292 ++++++++++++++++++ ...traSuperblockEncoder.ChromaModeDecision.cs | 30 +- ...rblockEncoder.ChromaPaletteModeDecision.cs | 41 +-- .../Av1IntraSuperblockEncoder.ModeDecision.cs | 16 +- ...raSuperblockEncoder.PaletteModeDecision.cs | 43 +-- .../Heif/Av1/Pipeline/Av1PaletteKMeans.cs | 9 +- .../Heif/Av1/Pipeline/Av1PaletteKMeans2D.cs | 12 +- .../Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs | 47 ++- .../Heif/Av1/Av1PaletteKMeans2DTests.cs | 8 +- .../Formats/Heif/Av1/Av1PaletteKMeansTests.cs | 11 +- .../Heif/Av1/Av1TransformBlockEncoderTests.cs | 19 ++ 13 files changed, 459 insertions(+), 96 deletions(-) create mode 100644 src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md index 1d3d70923e..2871f302d9 100644 --- a/HEIF_IMPLEMENTATION_PLAN.md +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -876,6 +876,7 @@ Encoder verification contract: - [~] The production path writes color and alpha payloads sequentially through allocator-backed chunked storage, supports non-seekable and prefixed destinations, and does not materialize a complete file or payload copy. Uniform encoder-side `pixi` depth is written directly without allocating per-item channel-depth arrays; decoder-side non-uniform channel depths remain supported. The Release test project builds with zero errors, all 39 encoder cases pass, the complete non-HEVC HEIF namespace passes 9,277 of 9,277, and current official libaom accepts all 47 generated payloads. - [x] Still-image AVIF metadata preservation now writes an unrestricted ICC `colr/prof` property before the independent `colr/nclx` property, Exif and XMP as separate `mdat` items, and one `cdsc` relationship from each metadata item to the primary color item. Exif stores the exact big-endian TIFF-header offset required by the HEIF item syntax; XMP uses the `mime` item type and `application/rdf+xml` content type. Existing ICC and XMP storage is read synchronously and copied once into final encoder storage rather than cloned into an intermediate array. `SkipMetadata` suppresses all three profile types while retaining the CICP values required to describe the encoded planes. The same option now reaches legacy JPEG payloads, whose encoder no longer writes application profiles or comments when metadata is disabled. - [x] Exact container tests verify every emitted item declaration, name, MIME content type, `cdsc` relationship, Exif offset and payload, XMP payload, ICC/CICP property order, compact association byte, propertyless metadata exclusion, decoded profile value, and both `SkipMetadata` branches. The final HEIF encoder set passes 44 of 44 and the complete JPEG encoder set passes 257 of 257 through direct foreground net11 Release VSTest. The complete non-HEVC HEIF namespace passes 9,282 of 9,282 with no failure, crash, or detached test host, and current official libaom accepts all 47 current generated AV1 payloads. +- [x] A code-wide production HEIF/AV1 stack-storage audit, excluding HEVC, removed every block-sized, variable-length, or repeatedly nested scratch buffer. Spatial luma and chroma, filter-intra, chroma-from-luma, luma and chroma palette selection, and K-means iteration now use typed views over 642 signed-integer elements, about 2.51 KiB, at the start of the existing 3.125 KiB IBC region. Those searches are sequential for one block, so the block-workspace owner does not grow and no rent, copy, or additional lifetime is introduced. CDEF directions, variances, and its 64-entry block list now append 1 KiB to the existing bounded operation owner instead of occupying hidden inline or explicit stack arrays. No remaining `stackalloc` depends on block dimensions, sample count, or runtime length; the largest remaining individual span is 128 bytes, and the remaining sites are fixed syntax, SIMD-lane, filter-tap, plane-metadata, or small candidate storage. The exact-owner test now proves the mode, palette, and IBC views share one allocation. Roslynk reports zero compiler errors and no diagnostics in the changed files, the Release test-project build completes with the established 1,992 warnings and zero errors, 81 of 81 focused cases pass, and the complete non-HEVC HEIF/AV1 namespace passes 9,282 of 9,282 through one foreground net11 VSTest run. - [~] Write the correct AVIF file type, item information, locations, references, properties, AV1 configuration, dimensions, color, alpha, metadata, and media data. - [~] Support single images, alpha auxiliary images, grids, multiple extents, and bounded image sequences in the final public scope. - [x] Preserve ICC, Exif, and XMP according to encoder options. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs index d65fe81056..717a9617ed 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs @@ -31,9 +31,7 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable MaximumCoefficientCount + MaximumCoefficientCount + Av1TransformWorkspace.MaximumLength + - IntraBlockCopySampleStorageLength + - IntraBlockCopyResidualStorageLength + - IntraBlockCopyCoefficientStorageLength; + IntraBlockCopyStorageLength; private const int ResidualStorageLength = MaximumResidualCount / 2; private const int TransformCoefficientOffset = ResidualStorageLength; @@ -61,6 +59,11 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable Av1EncoderIntraBlockCopyWorkspace.CoefficientBufferCount * Av1EncoderIntraBlockCopyWorkspace.MaximumSampleCount; + private const int IntraBlockCopyStorageLength = + IntraBlockCopySampleStorageLength + + IntraBlockCopyResidualStorageLength + + IntraBlockCopyCoefficientStorageLength; + /// /// Owns the complete reusable block workspace in 32-bit elements so every transform region is naturally aligned. /// @@ -97,6 +100,23 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable public Span TransformWorkspace => this.owner.Memory.Span.Slice(TransformWorkspaceOffset, Av1TransformWorkspace.MaximumLength); + /// + /// Gets the reusable storage used while comparing spatial, chroma-from-luma, filter-intra, and palette candidates. + /// + /// The native sample type selected by the encoder pipeline. + /// The typed mode-decision workspace. + public Av1EncoderModeDecisionWorkspace GetModeDecisionWorkspace() + where TSample : unmanaged + { + // Conventional intra search finishes before intra-block-copy search begins for the same block. + // Both phases can therefore reuse this aligned region without extending the owner or preserving stale scratch. + Span storage = this.owner.Memory.Span.Slice( + IntraBlockCopySampleStorageOffset, + IntraBlockCopyStorageLength); + + return new(storage[..Av1EncoderModeDecisionWorkspace.StorageLength]); + } + /// /// Gets the reusable storage used while comparing intra-block-copy candidates. /// diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs new file mode 100644 index 0000000000..8e1c5b1bfc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs @@ -0,0 +1,292 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; + +/// +/// Provides typed views over the reusable storage shared by mutually exclusive AV1 mode searches. +/// +/// The native sample type selected by the encoder pipeline. +internal readonly ref struct Av1EncoderModeDecisionWorkspace + where TSample : unmanaged +{ + /// + /// The maximum number of samples in the encoder's fixed 8x8 transform block. + /// + public const int MaximumSampleCount = 8 * 8; + + /// + /// The required workspace length in signed-integer storage elements. + /// + public const int StorageLength = TransientStorageOffset + Av1EncoderPaletteWorkspace.StorageLength; + + private const int ReferenceBufferLength = 17; + private const int ReferenceBufferCount = 4; + private const int ReferenceStorageLength = ReferenceBufferCount * ReferenceBufferLength * sizeof(ushort) / sizeof(int); + private const int CandidateSampleStorageOffset = ReferenceStorageLength; + private const int CandidateSampleStorageLength = 2 * MaximumSampleCount * sizeof(ushort) / sizeof(int); + private const int CandidateCoefficientStorageOffset = CandidateSampleStorageOffset + CandidateSampleStorageLength; + private const int CandidateCoefficientStorageLength = 2 * MaximumSampleCount; + private const int TransientStorageOffset = CandidateCoefficientStorageOffset + CandidateCoefficientStorageLength; + private const int ChromaFromLumaSampleCount = Av1ChromaFromLumaContext.BufferLine * 8; + private const int ChromaFromLumaSampleStorageLength = ChromaFromLumaSampleCount * sizeof(short) / sizeof(int); + private const int ChromaFromLumaBlueRateOffset = ChromaFromLumaSampleStorageLength; + private const int ChromaFromLumaRedRateOffset = ChromaFromLumaBlueRateOffset + Av1ChromaFromLumaMath.AlphaCandidateCount; + private const int ChromaFromLumaBlueDistortionOffset = + ChromaFromLumaRedRateOffset + Av1ChromaFromLumaMath.AlphaCandidateCount; + + private const int ChromaFromLumaDistortionStorageLength = + Av1ChromaFromLumaMath.AlphaCandidateCount * sizeof(long) / sizeof(int); + + private const int ChromaFromLumaRedDistortionOffset = + ChromaFromLumaBlueDistortionOffset + ChromaFromLumaDistortionStorageLength; + + private readonly Span storage; + + /// + /// Initializes a new instance of the struct. + /// + /// The reusable aligned decision storage. + public Av1EncoderModeDecisionWorkspace(Span storage) => this.storage = storage; + + /// + /// Gets the temporary prediction span used by filter-intra mode search. + /// + public Span FilterPrediction + => MemoryMarshal.Cast(this.storage[TransientStorageOffset..])[..MaximumSampleCount]; + + /// + /// Gets the temporary residual span used by filter-intra mode search. + /// + public Span FilterResidual + => MemoryMarshal.Cast( + this.storage.Slice( + TransientStorageOffset + (MaximumSampleCount * sizeof(ushort) / sizeof(int)), + MaximumSampleCount * sizeof(short) / sizeof(int))); + + /// + /// Gets the fixed-stride subsampled luma values used by chroma-from-luma mode search. + /// + public Span ChromaFromLumaSamples + => MemoryMarshal.Cast( + this.storage.Slice(TransientStorageOffset, ChromaFromLumaSampleStorageLength)); + + /// + /// Gets the palette-search view over transient storage that is no longer needed after spatial and CfL search. + /// + public Av1EncoderPaletteWorkspace Palette + => new(this.storage[TransientStorageOffset..]); + + /// + /// Gets one reference edge including its common-corner prefix. + /// + /// The zero-based edge index. + /// The fixed reference-edge span. + public Span GetReferenceSamples(int index) + => MemoryMarshal.Cast(this.storage[..ReferenceStorageLength]) + .Slice(index * ReferenceBufferLength, ReferenceBufferLength); + + /// + /// Gets one candidate reconstruction plane. + /// + /// The zero-based plane index. + /// The maximum-size candidate reconstruction span. + public Span GetCandidateReconstruction(int index) + => MemoryMarshal.Cast( + this.storage.Slice(CandidateSampleStorageOffset, CandidateSampleStorageLength)) + .Slice(index * MaximumSampleCount, MaximumSampleCount); + + /// + /// Gets one candidate coefficient plane. + /// + /// The zero-based plane index. + /// The maximum-size candidate coefficient span. + public Span GetCandidateCoefficients(int index) + => this.storage + .Slice(CandidateCoefficientStorageOffset, CandidateCoefficientStorageLength) + .Slice(index * MaximumSampleCount, MaximumSampleCount); + + /// + /// Gets one plane's chroma-from-luma coefficient-rate table. + /// + /// The zero-based chroma plane index. + /// The rate table for every signed alpha candidate. + public Span GetChromaFromLumaRates(int planeIndex) + => this.storage.Slice( + TransientStorageOffset + ChromaFromLumaBlueRateOffset + + (planeIndex * Av1ChromaFromLumaMath.AlphaCandidateCount), + Av1ChromaFromLumaMath.AlphaCandidateCount); + + /// + /// Gets one plane's chroma-from-luma distortion table. + /// + /// The zero-based chroma plane index. + /// The distortion table for every signed alpha candidate. + public Span GetChromaFromLumaDistortions(int planeIndex) + => MemoryMarshal.Cast( + this.storage.Slice( + TransientStorageOffset + ChromaFromLumaBlueDistortionOffset + + (planeIndex * ChromaFromLumaDistortionStorageLength), + ChromaFromLumaDistortionStorageLength)); +} + +/// +/// Provides typed luma and chroma palette-search buffers over reusable mode-decision storage. +/// +/// The native sample type selected by the encoder pipeline. +internal readonly ref struct Av1EncoderPaletteWorkspace + where TSample : unmanaged +{ + /// + /// The required workspace length in signed-integer storage elements. + /// + public const int StorageLength = ColorCacheOffset + ColorCacheStorageLength; + + private const int MaximumSampleCount = Av1EncoderModeDecisionWorkspace.MaximumSampleCount; + private const int PlaneShortStorageLength = MaximumSampleCount * sizeof(short) / sizeof(int); + private const int PlaneSampleStorageLength = MaximumSampleCount * sizeof(ushort) / sizeof(int); + private const int PlaneByteStorageLength = MaximumSampleCount / sizeof(int); + private const int PaletteColorStorageLength = Av1Constants.PaletteMaxSize * sizeof(ushort) / sizeof(int); + private const int FirstSampleOffset = 0; + private const int SecondSampleOffset = FirstSampleOffset + PlaneShortStorageLength; + private const int FirstUniqueColorOffset = SecondSampleOffset + PlaneShortStorageLength; + private const int SecondUniqueColorOffset = FirstUniqueColorOffset + PlaneShortStorageLength; + private const int FirstPredictionOffset = SecondUniqueColorOffset + PlaneShortStorageLength; + private const int SecondPredictionOffset = FirstPredictionOffset + PlaneSampleStorageLength; + private const int FirstResidualOffset = SecondPredictionOffset + PlaneSampleStorageLength; + private const int SecondResidualOffset = FirstResidualOffset + PlaneShortStorageLength; + private const int RetainedIndexOffset = SecondResidualOffset + PlaneShortStorageLength; + private const int IndexOffset = RetainedIndexOffset + PlaneByteStorageLength; + private const int FirstCentroidOffset = IndexOffset + PlaneByteStorageLength; + private const int SecondCentroidOffset = FirstCentroidOffset + PaletteColorStorageLength; + private const int FirstPaletteColorOffset = SecondCentroidOffset + PaletteColorStorageLength; + private const int SecondPaletteColorOffset = FirstPaletteColorOffset + PaletteColorStorageLength; + private const int FirstAlternateCentroidOffset = SecondPaletteColorOffset + PaletteColorStorageLength; + private const int SecondAlternateCentroidOffset = FirstAlternateCentroidOffset + PaletteColorStorageLength; + private const int AlternateIndexOffset = SecondAlternateCentroidOffset + PaletteColorStorageLength; + private const int ColorCountOffset = AlternateIndexOffset + PlaneByteStorageLength; + private const int DominantOrderOffset = ColorCountOffset + MaximumSampleCount; + private const int ColorCacheOffset = DominantOrderOffset + PlaneByteStorageLength; + private const int ColorCacheStorageLength = 2 * Av1Constants.PaletteMaxSize * sizeof(ushort) / sizeof(int); + + private readonly Span storage; + + /// + /// Initializes a new instance of the struct. + /// + /// The reusable aligned palette storage. + public Av1EncoderPaletteWorkspace(Span storage) => this.storage = storage; + + /// + /// Gets the retained winning color-index map. + /// + public Span RetainedIndices + => MemoryMarshal.AsBytes(this.storage.Slice(RetainedIndexOffset, PlaneByteStorageLength)); + + /// + /// Gets the current color-index map. + /// + public Span Indices + => MemoryMarshal.AsBytes(this.storage.Slice(IndexOffset, PlaneByteStorageLength)); + + /// + /// Gets the alternate K-means color-index map. + /// + public Span AlternateIndices + => MemoryMarshal.AsBytes(this.storage.Slice(AlternateIndexOffset, PlaneByteStorageLength)); + + /// + /// Gets the luma occurrence count for every unique color. + /// + public Span LumaColorCounts + => this.storage.Slice(ColorCountOffset, MaximumSampleCount); + + /// + /// Gets the luma unique-color ordering by descending occurrence count. + /// + public Span LumaDominantOrder + => MemoryMarshal.AsBytes(this.storage.Slice(DominantOrderOffset, PlaneByteStorageLength)); + + /// + /// Gets the sorted neighboring palette colors available to the current block. + /// + public Span ColorCache + => MemoryMarshal.Cast(this.storage.Slice(ColorCacheOffset, ColorCacheStorageLength)); + + /// + /// Gets one plane's active palette samples. + /// + /// The zero-based plane index. + /// The maximum-size sample span. + public Span GetSamples(int planeIndex) + => MemoryMarshal.Cast( + this.storage.Slice(FirstSampleOffset + (planeIndex * PlaneShortStorageLength), PlaneShortStorageLength)); + + /// + /// Gets one plane's unique palette colors. + /// + /// The zero-based plane index. + /// The maximum-size unique-color span. + public Span GetUniqueColors(int planeIndex) + => MemoryMarshal.Cast( + this.storage.Slice( + FirstUniqueColorOffset + (planeIndex * PlaneShortStorageLength), + PlaneShortStorageLength)); + + /// + /// Gets one plane's palette prediction. + /// + /// The zero-based plane index. + /// The maximum-size prediction span. + public Span GetPrediction(int planeIndex) + => MemoryMarshal.Cast( + this.storage.Slice( + FirstPredictionOffset + (planeIndex * PlaneSampleStorageLength), + PlaneSampleStorageLength))[..MaximumSampleCount]; + + /// + /// Gets one plane's palette residual. + /// + /// The zero-based plane index. + /// The maximum-size residual span. + public Span GetResidual(int planeIndex) + => MemoryMarshal.Cast( + this.storage.Slice(FirstResidualOffset + (planeIndex * PlaneShortStorageLength), PlaneShortStorageLength)); + + /// + /// Gets one plane's current palette centroids. + /// + /// The zero-based plane index. + /// The maximum-size centroid span. + public Span GetCentroids(int planeIndex) + => MemoryMarshal.Cast( + this.storage.Slice( + FirstCentroidOffset + (planeIndex * PaletteColorStorageLength), + PaletteColorStorageLength)); + + /// + /// Gets one plane's coded palette colors. + /// + /// The zero-based plane index. + /// The maximum-size coded-color span. + public Span GetPaletteColors(int planeIndex) + => MemoryMarshal.Cast( + this.storage.Slice( + FirstPaletteColorOffset + (planeIndex * PaletteColorStorageLength), + PaletteColorStorageLength)); + + /// + /// Gets one plane's alternate K-means centroids. + /// + /// The zero-based plane index. + /// The maximum-size alternate-centroid span. + public Span GetAlternateCentroids(int planeIndex) + => MemoryMarshal.Cast( + this.storage.Slice( + FirstAlternateCentroidOffset + (planeIndex * PaletteColorStorageLength), + PaletteColorStorageLength)); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs index d45f358d40..4d8ede5d56 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs @@ -60,7 +60,9 @@ internal static partial class Av1IntraSuperblockEncoder out long selectedCost) { const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; - const int MaximumSampleCount = 8 * 8; + Av1EncoderModeDecisionWorkspace workspace = + this.blockWorkspace.GetModeDecisionWorkspace(); + ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig; int subsamplingX = colorConfig.SubSamplingX ? 1 : 0; int subsamplingY = colorConfig.SubSamplingY ? 1 : 0; @@ -105,10 +107,10 @@ internal static partial class Av1IntraSuperblockEncoder Buffer2DRegion redSource = this.source.GetPlane(Av1Plane.V); Buffer2DRegion blueReconstruction = this.reconstruction.GetPlane(Av1Plane.U); Buffer2DRegion redReconstruction = this.reconstruction.GetPlane(Av1Plane.V); - Span blueAboveStorage = stackalloc TSample[17]; - Span blueLeftStorage = stackalloc TSample[17]; - Span redAboveStorage = stackalloc TSample[17]; - Span redLeftStorage = stackalloc TSample[17]; + Span blueAboveStorage = workspace.GetReferenceSamples(0); + Span blueLeftStorage = workspace.GetReferenceSamples(1); + Span redAboveStorage = workspace.GetReferenceSamples(2); + Span redLeftStorage = workspace.GetReferenceSamples(3); this.PrepareReferenceSamples( blueReconstruction, chromaOrigin, @@ -152,10 +154,10 @@ internal static partial class Av1IntraSuperblockEncoder chromaBlockSize, transformSize); - Span candidateBlueReconstruction = stackalloc TSample[MaximumSampleCount]; - Span candidateRedReconstruction = stackalloc TSample[MaximumSampleCount]; - Span candidateBlueCoefficients = stackalloc int[MaximumSampleCount]; - Span candidateRedCoefficients = stackalloc int[MaximumSampleCount]; + Span candidateBlueReconstruction = workspace.GetCandidateReconstruction(0); + Span candidateRedReconstruction = workspace.GetCandidateReconstruction(1); + Span candidateBlueCoefficients = workspace.GetCandidateCoefficients(0); + Span candidateRedCoefficients = workspace.GetCandidateCoefficients(1); long bestCost = long.MaxValue; Av1ChromaPredictionMode bestMode = Av1ChromaPredictionMode.DC; selectedAngleDelta = 0; @@ -259,7 +261,7 @@ internal static partial class Av1IntraSuperblockEncoder if (this.effort >= 4 && chromaFromLumaAllowed) { - Span lumaQ3 = stackalloc short[Av1ChromaFromLumaContext.BufferLine * 8]; + Span lumaQ3 = workspace.ChromaFromLumaSamples; TOperator.PrepareChromaFromLuma( this.reconstruction.GetPlane(Av1Plane.Y), lumaOrigin, @@ -290,10 +292,10 @@ internal static partial class Av1IntraSuperblockEncoder this.bitDepth); TSample redDc = candidateRedReconstruction[0]; - Span blueRates = stackalloc int[Av1ChromaFromLumaMath.AlphaCandidateCount]; - Span redRates = stackalloc int[Av1ChromaFromLumaMath.AlphaCandidateCount]; - Span blueDistortions = stackalloc long[Av1ChromaFromLumaMath.AlphaCandidateCount]; - Span redDistortions = stackalloc long[Av1ChromaFromLumaMath.AlphaCandidateCount]; + Span blueRates = workspace.GetChromaFromLumaRates(0); + Span redRates = workspace.GetChromaFromLumaRates(1); + Span blueDistortions = workspace.GetChromaFromLumaDistortions(0); + Span redDistortions = workspace.GetChromaFromLumaDistortions(1); // Each plane has only 33 signed alpha values. Caching those complete transform results reduces // the joint search from 1089 transform pairs to 66 transforms plus inexpensive rate combinations. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs index 69f67f2dad..92836d0bcc 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs @@ -43,7 +43,9 @@ internal static partial class Av1IntraSuperblockEncoder { const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const int LumaBlockLength = 8; - const int MaximumSampleCount = LumaBlockLength * LumaBlockLength; + Av1EncoderPaletteWorkspace workspace = + this.blockWorkspace.GetModeDecisionWorkspace().Palette; + ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig; int subsamplingX = colorConfig.SubSamplingX ? 1 : 0; int subsamplingY = colorConfig.SubSamplingY ? 1 : 0; @@ -53,17 +55,15 @@ internal static partial class Av1IntraSuperblockEncoder int rows = Math.Min(LumaBlockLength, frameSize.FrameHeight - lumaOrigin.Y) >> subsamplingY; int columns = Math.Min(LumaBlockLength, frameSize.FrameWidth - lumaOrigin.X) >> subsamplingX; int activeSampleCount = rows * columns; - Span blueSamples = stackalloc short[MaximumSampleCount]; - Span redSamples = stackalloc short[MaximumSampleCount]; - blueSamples = blueSamples[..activeSampleCount]; - redSamples = redSamples[..activeSampleCount]; + Span blueSamples = workspace.GetSamples(0)[..activeSampleCount]; + Span redSamples = workspace.GetSamples(1)[..activeSampleCount]; Buffer2DRegion blueSource = this.source.GetPlane(Av1Plane.U); Buffer2DRegion redSource = this.source.GetPlane(Av1Plane.V); TOperator.CopyPaletteSamples(blueSource, chromaOrigin, rows, columns, blueSamples); TOperator.CopyPaletteSamples(redSource, chromaOrigin, rows, columns, redSamples); - Span uniqueBlueColors = stackalloc short[MaximumSampleCount]; - Span uniqueRedColors = stackalloc short[MaximumSampleCount]; + Span uniqueBlueColors = workspace.GetUniqueColors(0); + Span uniqueRedColors = workspace.GetUniqueColors(1); int uniqueBlueColorCount = 0; int uniqueRedColorCount = 0; short blueMinimum = blueSamples[0]; @@ -98,7 +98,7 @@ internal static partial class Av1IntraSuperblockEncoder int maximumPaletteSize = Math.Min(maximumColorCount, Av1Constants.PaletteMaxSize); Av1NeighborArrayUnit paletteContexts = this.picture.PaletteContexts[tileIndex]; - Span colorCache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize]; + Span colorCache = workspace.ColorCache; int colorCacheSize = Av1TileWriter.GetPaletteCache( paletteContexts, macroBlock, @@ -113,16 +113,16 @@ internal static partial class Av1IntraSuperblockEncoder .GetPaletteMaps() .GetMap(Av1PlaneType.Uv, width, height); - Span retainedColorIndexMap = stackalloc byte[MaximumSampleCount]; - Span blueCentroids = stackalloc short[Av1Constants.PaletteMaxSize]; - Span redCentroids = stackalloc short[Av1Constants.PaletteMaxSize]; - Span colorIndices = stackalloc byte[MaximumSampleCount]; - Span bluePrediction = stackalloc TSample[MaximumSampleCount]; - Span redPrediction = stackalloc TSample[MaximumSampleCount]; - Span blueResidual = stackalloc short[MaximumSampleCount]; - Span redResidual = stackalloc short[MaximumSampleCount]; - Span bluePaletteColorStorage = stackalloc ushort[Av1Constants.PaletteMaxSize]; - Span redPaletteColorStorage = stackalloc ushort[Av1Constants.PaletteMaxSize]; + Span retainedColorIndexMap = workspace.RetainedIndices; + Span blueCentroids = workspace.GetCentroids(0); + Span redCentroids = workspace.GetCentroids(1); + Span colorIndices = workspace.Indices; + Span bluePrediction = workspace.GetPrediction(0); + Span redPrediction = workspace.GetPrediction(1); + Span blueResidual = workspace.GetResidual(0); + Span redResidual = workspace.GetResidual(1); + Span bluePaletteColorStorage = workspace.GetPaletteColors(0); + Span redPaletteColorStorage = workspace.GetPaletteColors(1); int sampleCount = transformSize.GetSize2d(); int cacheThreshold = 4 << (this.bitDepth.GetBitCount() - 8); bool paletteSelected = false; @@ -145,7 +145,10 @@ internal static partial class Av1IntraSuperblockEncoder redSamples, candidateBlueCentroids, candidateRedCentroids, - colorIndices[..activeSampleCount]); + colorIndices[..activeSampleCount], + workspace.GetAlternateCentroids(0), + workspace.GetAlternateCentroids(1), + workspace.AlternateIndices); for (int colorIndex = 0; colorIndex < paletteSize && !colorCache.IsEmpty; colorIndex++) { diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs index 8351842042..12bc8c5a21 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs @@ -397,7 +397,9 @@ internal static partial class Av1IntraSuperblockEncoder { const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1TransformSize TransformSize = Av1TransformSize.Size8x8; - const int SampleCount = 8 * 8; + Av1EncoderModeDecisionWorkspace workspace = + this.blockWorkspace.GetModeDecisionWorkspace(); + Buffer2DRegion sourcePlane = this.source.GetPlane(Av1Plane.Y); Buffer2DRegion reconstructionPlane = this.reconstruction.GetPlane(Av1Plane.Y); bool hasLeft = macroBlock.IsLeftAvailable; @@ -434,9 +436,9 @@ internal static partial class Av1IntraSuperblockEncoder 0, 0); - Span aboveStorage = stackalloc TSample[17]; + Span aboveStorage = workspace.GetReferenceSamples(0); Span above = aboveStorage[1..]; - Span leftStorage = stackalloc TSample[17]; + Span leftStorage = workspace.GetReferenceSamples(1); Span left = leftStorage[1..]; if (hasAbove) @@ -523,8 +525,8 @@ internal static partial class Av1IntraSuperblockEncoder neighborContext); } - Span candidateReconstruction = stackalloc TSample[SampleCount]; - Span candidateCoefficients = stackalloc int[SampleCount]; + Span candidateReconstruction = workspace.GetCandidateReconstruction(0); + Span candidateCoefficients = workspace.GetCandidateCoefficients(0); long bestCost = long.MaxValue; Av1PredictionMode bestMode = Av1PredictionMode.DC; selectedAngleDelta = 0; @@ -655,8 +657,8 @@ internal static partial class Av1IntraSuperblockEncoder if (this.effort >= 4 && this.picture.Sequence.SequenceHeader.EnableFilterIntra) { - Span filterPrediction = stackalloc TSample[SampleCount]; - Span filterResidual = stackalloc short[SampleCount]; + Span filterPrediction = workspace.FilterPrediction; + Span filterResidual = workspace.FilterResidual; // Each recursive filter prediction and its source residual are independent of transform type. // Prepare them once per filter mode so all legal transforms reuse the same samples. diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs index f99bb81968..6b617ea6af 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs @@ -37,17 +37,18 @@ internal static partial class Av1IntraSuperblockEncoder { const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const int BlockLength = 8; - const int SampleCapacity = BlockLength * BlockLength; + Av1EncoderPaletteWorkspace workspace = + this.blockWorkspace.GetModeDecisionWorkspace().Palette; + ObuFrameSize frameSize = this.picture.Parent.FrameHeader.FrameSize; int rows = Math.Min(BlockLength, frameSize.FrameHeight - blockOrigin.Y); int columns = Math.Min(BlockLength, frameSize.FrameWidth - blockOrigin.X); int sampleCount = rows * columns; - Span samples = stackalloc short[SampleCapacity]; - samples = samples[..sampleCount]; + Span samples = workspace.GetSamples(0)[..sampleCount]; TOperator.CopyPaletteSamples(sourcePlane, blockOrigin, rows, columns, samples); - Span uniqueColors = stackalloc short[SampleCapacity]; - Span colorCounts = stackalloc int[SampleCapacity]; + Span uniqueColors = workspace.GetUniqueColors(0); + Span colorCounts = workspace.LumaColorCounts; int uniqueColorCount = 0; short minimum = samples[0]; short maximum = samples[0]; @@ -75,7 +76,7 @@ internal static partial class Av1IntraSuperblockEncoder } int maximumPaletteSize = Math.Min(uniqueColorCount, Av1Constants.PaletteMaxSize); - Span dominantOrder = stackalloc byte[SampleCapacity]; + Span dominantOrder = workspace.LumaDominantOrder; for (int index = 0; index < uniqueColorCount; index++) { dominantOrder[index] = (byte)index; @@ -107,7 +108,7 @@ internal static partial class Av1IntraSuperblockEncoder Av1NeighborArrayUnit paletteContexts = this.picture.PaletteContexts[tileIndex]; int blockSizeContext = Av1TileWriter.GetPaletteBlockSizeContext(BlockSize); int neighborContext = Av1TileWriter.GetPaletteYModeContext(paletteContexts, macroBlock, blockOrigin); - Span colorCache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize]; + Span colorCache = workspace.ColorCache; int colorCacheSize = Av1TileWriter.GetPaletteCache( paletteContexts, macroBlock, @@ -119,8 +120,8 @@ internal static partial class Av1IntraSuperblockEncoder .GetPaletteMaps() .GetMap(Av1PlaneType.Y, BlockLength, BlockLength); - Span retainedColorIndexMap = stackalloc byte[SampleCapacity]; - Span centroids = stackalloc short[Av1Constants.PaletteMaxSize]; + Span retainedColorIndexMap = workspace.RetainedIndices; + Span centroids = workspace.GetCentroids(0); bool paletteSelected = false; // Exhaustive ascending size search avoids the reference encoder's speed-dependent pruning. @@ -186,13 +187,18 @@ internal static partial class Av1IntraSuperblockEncoder } else { - Span clusterIndices = stackalloc byte[SampleCapacity]; - clusterIndices = clusterIndices[..sampleCount]; + Span clusterIndices = workspace.Indices[..sampleCount]; for (int paletteSize = 2; paletteSize <= maximumPaletteSize; paletteSize++) { Span candidateCentroids = centroids[..paletteSize]; Av1PaletteKMeans.InitializeCentroids(minimum, maximum, candidateCentroids); - Av1PaletteKMeans.Cluster(samples, candidateCentroids, clusterIndices); + Av1PaletteKMeans.Cluster( + samples, + candidateCentroids, + clusterIndices, + workspace.GetAlternateCentroids(0), + workspace.AlternateIndices); + this.EvaluateLumaPaletteCandidate( writer, macroBlock, @@ -258,7 +264,9 @@ internal static partial class Av1IntraSuperblockEncoder const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1TransformSize TransformSize = Av1TransformSize.Size8x8; const int BlockLength = 8; - const int SampleCount = BlockLength * BlockLength; + Av1EncoderPaletteWorkspace workspace = + this.blockWorkspace.GetModeDecisionWorkspace().Palette; + int bitDepth = this.bitDepth.GetBitCount(); int cacheThreshold = 4 << (bitDepth - 8); for (int colorIndex = 0; colorIndex < centroids.Length && !colorCache.IsEmpty; colorIndex++) @@ -297,14 +305,13 @@ internal static partial class Av1IntraSuperblockEncoder } ReadOnlySpan paletteCentroids = centroids[..paletteSize]; - Span paletteColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; - paletteColors = paletteColors[..paletteSize]; + Span paletteColors = workspace.GetPaletteColors(0)[..paletteSize]; for (int colorIndex = 0; colorIndex < paletteSize; colorIndex++) { paletteColors[colorIndex] = (ushort)paletteCentroids[colorIndex]; } - Span colorIndices = stackalloc byte[SampleCount]; + Span colorIndices = workspace.Indices; Av1PaletteKMeans.AssignIndices(samples, paletteCentroids, colorIndices); for (int row = 0; row < rows; row++) { @@ -320,8 +327,8 @@ internal static partial class Av1IntraSuperblockEncoder .CopyTo(colorIndexMap.DangerousGetRowSpan(row)); } - Span prediction = stackalloc TSample[SampleCount]; - Span residual = stackalloc short[SampleCount]; + Span prediction = workspace.GetPrediction(0); + Span residual = workspace.GetResidual(0); TOperator.PreparePalette( this.source.GetPlane(Av1Plane.Y), blockOrigin, diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans.cs index 10bf95c26f..c6d97384a9 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans.cs @@ -144,15 +144,18 @@ internal static class Av1PaletteKMeans /// The active block samples. /// The initialized colors, replaced with the best refined colors. /// The palette indices belonging to the retained colors. + /// Reusable storage for the next centroid iteration. + /// Reusable storage for the next index iteration. /// The retained sum of squared distances. public static long Cluster( ReadOnlySpan samples, Span centroids, - Span indices) + Span indices, + Span alternateCentroids, + Span alternateIndices) { - Span alternateCentroids = stackalloc short[Av1Constants.PaletteMaxSize]; - Span alternateIndices = stackalloc byte[samples.Length]; alternateCentroids = alternateCentroids[..centroids.Length]; + alternateIndices = alternateIndices[..samples.Length]; long distortion = AssignIndices(samples, centroids, indices); bool currentIsAlternate = false; diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans2D.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans2D.cs index 74ebb5af44..84832d03a0 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans2D.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans2D.cs @@ -217,19 +217,23 @@ internal static class Av1PaletteKMeans2D /// The initialized first-plane colors. /// The initialized second-plane colors. /// The palette indices belonging to the retained colors. + /// Reusable storage for the next first-plane centroid iteration. + /// Reusable storage for the next second-plane centroid iteration. + /// Reusable storage for the next index iteration. /// The retained sum of squared two-plane distances. public static long Cluster( ReadOnlySpan firstSamples, ReadOnlySpan secondSamples, Span firstCentroids, Span secondCentroids, - Span indices) + Span indices, + Span alternateFirstCentroids, + Span alternateSecondCentroids, + Span alternateIndices) { - Span alternateFirstCentroids = stackalloc short[Av1Constants.PaletteMaxSize]; - Span alternateSecondCentroids = stackalloc short[Av1Constants.PaletteMaxSize]; - Span alternateIndices = stackalloc byte[firstSamples.Length]; alternateFirstCentroids = alternateFirstCentroids[..firstCentroids.Length]; alternateSecondCentroids = alternateSecondCentroids[..secondCentroids.Length]; + alternateIndices = alternateIndices[..firstSamples.Length]; long distortion = AssignIndices( firstSamples, secondSamples, diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs index 0fe3b72dbe..d5938bed6b 100644 --- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs @@ -141,15 +141,28 @@ internal sealed class Av1CdefDecoder columnBufferLength += columnBufferLengths[planeIndex]; } - int scratchLength = SourceBufferLength + lineBufferLength + columnBufferLength; + int directionStorageLength = MaximumBlocksPerUnit * sizeof(int) / sizeof(ushort); + int blockStorageLength = MaximumBlocksPerUnit * Unsafe.SizeOf() / sizeof(ushort); + int unitStorageOffset = SourceBufferLength + lineBufferLength + columnBufferLength; + int scratchLength = unitStorageOffset + (directionStorageLength * 2) + blockStorageLength; MemoryAllocator allocator = this.frameBuffer.MemoryAllocator; using IMemoryOwner scratchOwner = allocator.Allocate(scratchLength); Span scratch = scratchOwner.Memory.Span[..scratchLength]; Span source = scratch[..SourceBufferLength]; Span lineBuffer = scratch.Slice(SourceBufferLength, lineBufferLength); - Span columnBuffer = scratch[(SourceBufferLength + lineBufferLength)..]; - Span directions = stackalloc int[MaximumBlocksPerUnit]; - Span variances = stackalloc int[MaximumBlocksPerUnit]; + Span columnBuffer = scratch.Slice(SourceBufferLength + lineBufferLength, columnBufferLength); + + // Every preceding plane region has an even ushort length, so the appended unit state remains 32-bit aligned. + // Directions, variances, and block coordinates share the owner because they are reused one unit at a time. + Span directions = MemoryMarshal.Cast( + scratch.Slice(unitStorageOffset, directionStorageLength)); + + Span variances = MemoryMarshal.Cast( + scratch.Slice(unitStorageOffset + directionStorageLength, directionStorageLength)); + + Span blocks = MemoryMarshal.Cast( + scratch.Slice(unitStorageOffset + (directionStorageLength * 2), blockStorageLength)); + Span cdefLeft = stackalloc bool[3]; int unitColumnCount = (this.frameHeader.ModeInfoColumnCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize; int unitRowCount = (this.frameHeader.ModeInfoRowCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize; @@ -217,7 +230,6 @@ internal sealed class Av1CdefDecoder int unitModeInfoRowEnd = Math.Min(unitModeInfoRow + CdefUnitModeInfoSize, this.frameHeader.ModeInfoRowCount); int unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount); - CdefBlockList blocks = default; int blockCount = 0; for (int blockModeInfoRow = unitModeInfoRow; blockModeInfoRow < unitModeInfoRowEnd; blockModeInfoRow += 2) @@ -265,8 +277,7 @@ internal sealed class Av1CdefDecoder subsamplingY[planeIndex], unitModeInfoColumn, unitModeInfoRow, - ref blocks, - blockCount, + blocks[..blockCount], directions, variances, yStrength, @@ -291,7 +302,6 @@ internal sealed class Av1CdefDecoder /// The unit's frame-relative column in 4x4 luma units. /// The unit's frame-relative row in 4x4 luma units. /// The unit's non-skipped 8x8 luma blocks. - /// The number of initialized entries in . /// The unit-local luma directions in block-list order. /// The unit-local luma directional variances in block-list order. /// The coded luma strength. @@ -306,8 +316,7 @@ internal sealed class Av1CdefDecoder int subsamplingY, int unitModeInfoColumn, int unitModeInfoRow, - ref CdefBlockList blocks, - int blockCount, + ReadOnlySpan blocks, Span directions, Span variances, int yStrength, @@ -429,7 +438,7 @@ internal sealed class Av1CdefDecoder // The reference decoder analyzes two listed 8x8 blocks together. The per-unit fixed list preserves that traversal // without allocating a managed block list or repeating four skip-map lookups during filtering. - for (; blockIndex < blockCount - 1; blockIndex += 2) + for (; blockIndex < blocks.Length - 1; blockIndex += 2) { CdefBlock firstBlock = blocks[blockIndex]; CdefBlock secondBlock = blocks[blockIndex + 1]; @@ -446,7 +455,7 @@ internal sealed class Av1CdefDecoder out variances[blockIndex + 1]); } - if (blockIndex < blockCount) + if (blockIndex < blocks.Length) { CdefBlock block = blocks[blockIndex]; @@ -464,7 +473,7 @@ internal sealed class Av1CdefDecoder return; } - for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) + for (int blockIndex = 0; blockIndex < blocks.Length; blockIndex++) { CdefBlock block = blocks[blockIndex]; int filteredPrimaryStrength = plane == Av1Plane.Y @@ -667,18 +676,6 @@ internal sealed class Av1CdefDecoder return true; } - /// - /// Stores the non-skipped blocks in one CDEF unit without a managed allocation. - /// - [InlineArray(MaximumBlocksPerUnit)] - private struct CdefBlockList - { - /// - /// The first block in the inline storage. - /// - private CdefBlock element0; - } - /// /// Identifies one 8x8 luma block by its frame-relative mode-information coordinates. /// diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeans2DTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeans2DTests.cs index 185837d54b..a06c90b7de 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeans2DTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeans2DTests.cs @@ -27,13 +27,19 @@ public class Av1PaletteKMeans2DTests short[] firstCentroids = [20, 100, 180]; short[] secondCentroids = [30, 110, 190]; byte[] indices = new byte[firstSamples.Length]; + short[] alternateFirstCentroids = new short[firstCentroids.Length]; + short[] alternateSecondCentroids = new short[secondCentroids.Length]; + byte[] alternateIndices = new byte[firstSamples.Length]; long distortion = Av1PaletteKMeans2D.Cluster( firstSamples, secondSamples, firstCentroids, secondCentroids, - indices); + indices, + alternateFirstCentroids, + alternateSecondCentroids, + alternateIndices); Assert.Equal([1, 101, 201], firstCentroids); Assert.Equal([11, 111, 211], secondCentroids); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeansTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeansTests.cs index e92c9d8e7c..f23932752a 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeansTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeansTests.cs @@ -34,8 +34,15 @@ public class Av1PaletteKMeansTests short[] samples = [0, 1, 2, 100, 101, 102, 200, 201, 202]; short[] centroids = [33, 100, 167]; byte[] indices = new byte[samples.Length]; - - long distortion = Av1PaletteKMeans.Cluster(samples, centroids, indices); + short[] alternateCentroids = new short[centroids.Length]; + byte[] alternateIndices = new byte[samples.Length]; + + long distortion = Av1PaletteKMeans.Cluster( + samples, + centroids, + indices, + alternateCentroids, + alternateIndices); Assert.Equal([1, 101, 201], centroids); Assert.Equal([0, 0, 0, 1, 1, 1, 2, 2, 2], indices); diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs index d99218feb1..3aaa4e4835 100644 --- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Heif.Av1; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; 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; @@ -610,6 +611,24 @@ public class Av1TransformBlockEncoderTests Assert.Equal(Av1EncoderBlockWorkspace.MaximumCoefficientCount, workspace.TransformCoefficients.Length); Assert.Equal(Av1EncoderBlockWorkspace.MaximumCoefficientCount, workspace.DequantizedCoefficients.Length); Assert.Equal(Av1TransformWorkspace.MaximumLength, workspace.TransformWorkspace.Length); + + Av1EncoderModeDecisionWorkspace modeWorkspace = workspace.GetModeDecisionWorkspace(); + Av1EncoderPaletteWorkspace paletteWorkspace = modeWorkspace.Palette; + Av1EncoderIntraBlockCopyWorkspace intraBlockCopyWorkspace = + workspace.GetIntraBlockCopyWorkspace(); + + Assert.Equal(17, modeWorkspace.GetReferenceSamples(3).Length); + Assert.Equal(Av1EncoderModeDecisionWorkspace.MaximumSampleCount, modeWorkspace.GetCandidateReconstruction(1).Length); + Assert.Equal(Av1EncoderModeDecisionWorkspace.MaximumSampleCount, modeWorkspace.GetCandidateCoefficients(1).Length); + Assert.Equal(Av1ChromaFromLumaContext.BufferLine * 8, modeWorkspace.ChromaFromLumaSamples.Length); + Assert.Equal(Av1ChromaFromLumaMath.AlphaCandidateCount, modeWorkspace.GetChromaFromLumaRates(1).Length); + Assert.Equal(Av1ChromaFromLumaMath.AlphaCandidateCount, modeWorkspace.GetChromaFromLumaDistortions(1).Length); + Assert.Equal(Av1EncoderModeDecisionWorkspace.MaximumSampleCount, paletteWorkspace.GetPrediction(1).Length); + Assert.Equal(Av1EncoderModeDecisionWorkspace.MaximumSampleCount, paletteWorkspace.AlternateIndices.Length); + + // Conventional mode search and IBC are sequential, so their typed views intentionally alias one owner region. + modeWorkspace.GetReferenceSamples(0)[0] = 123; + Assert.Equal((ushort)123, intraBlockCopyWorkspace.SelectedLumaReconstruction[0]); } TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);