Browse Source

Reuse AV1 mode decision scratch

pull/2633/head
James Jackson-South 1 month ago
parent
commit
af50bb8e9a
  1. 1
      HEIF_IMPLEMENTATION_PLAN.md
  2. 26
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs
  3. 292
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderModeDecisionWorkspace.cs
  4. 30
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs
  5. 41
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs
  6. 16
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
  7. 43
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs
  8. 9
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans.cs
  9. 12
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans2D.cs
  10. 47
      src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs
  11. 8
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeans2DTests.cs
  12. 11
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeansTests.cs
  13. 19
      tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs

1
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. - [~] 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] 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] 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. - [~] 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. - [~] 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. - [x] Preserve ICC, Exif, and XMP according to encoder options.

26
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1EncoderBlockWorkspace.cs

@ -31,9 +31,7 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
MaximumCoefficientCount + MaximumCoefficientCount +
MaximumCoefficientCount + MaximumCoefficientCount +
Av1TransformWorkspace.MaximumLength + Av1TransformWorkspace.MaximumLength +
IntraBlockCopySampleStorageLength + IntraBlockCopyStorageLength;
IntraBlockCopyResidualStorageLength +
IntraBlockCopyCoefficientStorageLength;
private const int ResidualStorageLength = MaximumResidualCount / 2; private const int ResidualStorageLength = MaximumResidualCount / 2;
private const int TransformCoefficientOffset = ResidualStorageLength; private const int TransformCoefficientOffset = ResidualStorageLength;
@ -61,6 +59,11 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
Av1EncoderIntraBlockCopyWorkspace<ushort>.CoefficientBufferCount * Av1EncoderIntraBlockCopyWorkspace<ushort>.CoefficientBufferCount *
Av1EncoderIntraBlockCopyWorkspace<ushort>.MaximumSampleCount; Av1EncoderIntraBlockCopyWorkspace<ushort>.MaximumSampleCount;
private const int IntraBlockCopyStorageLength =
IntraBlockCopySampleStorageLength +
IntraBlockCopyResidualStorageLength +
IntraBlockCopyCoefficientStorageLength;
/// <summary> /// <summary>
/// Owns the complete reusable block workspace in 32-bit elements so every transform region is naturally aligned. /// Owns the complete reusable block workspace in 32-bit elements so every transform region is naturally aligned.
/// </summary> /// </summary>
@ -97,6 +100,23 @@ internal sealed class Av1EncoderBlockWorkspace : IDisposable
public Span<int> TransformWorkspace public Span<int> TransformWorkspace
=> this.owner.Memory.Span.Slice(TransformWorkspaceOffset, Av1TransformWorkspace.MaximumLength); => this.owner.Memory.Span.Slice(TransformWorkspaceOffset, Av1TransformWorkspace.MaximumLength);
/// <summary>
/// Gets the reusable storage used while comparing spatial, chroma-from-luma, filter-intra, and palette candidates.
/// </summary>
/// <typeparam name="TSample">The native sample type selected by the encoder pipeline.</typeparam>
/// <returns>The typed mode-decision workspace.</returns>
public Av1EncoderModeDecisionWorkspace<TSample> GetModeDecisionWorkspace<TSample>()
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<int> storage = this.owner.Memory.Span.Slice(
IntraBlockCopySampleStorageOffset,
IntraBlockCopyStorageLength);
return new(storage[..Av1EncoderModeDecisionWorkspace<TSample>.StorageLength]);
}
/// <summary> /// <summary>
/// Gets the reusable storage used while comparing intra-block-copy candidates. /// Gets the reusable storage used while comparing intra-block-copy candidates.
/// </summary> /// </summary>

292
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;
/// <summary>
/// Provides typed views over the reusable storage shared by mutually exclusive AV1 mode searches.
/// </summary>
/// <typeparam name="TSample">The native sample type selected by the encoder pipeline.</typeparam>
internal readonly ref struct Av1EncoderModeDecisionWorkspace<TSample>
where TSample : unmanaged
{
/// <summary>
/// The maximum number of samples in the encoder's fixed 8x8 transform block.
/// </summary>
public const int MaximumSampleCount = 8 * 8;
/// <summary>
/// The required workspace length in signed-integer storage elements.
/// </summary>
public const int StorageLength = TransientStorageOffset + Av1EncoderPaletteWorkspace<ushort>.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<int> storage;
/// <summary>
/// Initializes a new instance of the <see cref="Av1EncoderModeDecisionWorkspace{TSample}"/> struct.
/// </summary>
/// <param name="storage">The reusable aligned decision storage.</param>
public Av1EncoderModeDecisionWorkspace(Span<int> storage) => this.storage = storage;
/// <summary>
/// Gets the temporary prediction span used by filter-intra mode search.
/// </summary>
public Span<TSample> FilterPrediction
=> MemoryMarshal.Cast<int, TSample>(this.storage[TransientStorageOffset..])[..MaximumSampleCount];
/// <summary>
/// Gets the temporary residual span used by filter-intra mode search.
/// </summary>
public Span<short> FilterResidual
=> MemoryMarshal.Cast<int, short>(
this.storage.Slice(
TransientStorageOffset + (MaximumSampleCount * sizeof(ushort) / sizeof(int)),
MaximumSampleCount * sizeof(short) / sizeof(int)));
/// <summary>
/// Gets the fixed-stride subsampled luma values used by chroma-from-luma mode search.
/// </summary>
public Span<short> ChromaFromLumaSamples
=> MemoryMarshal.Cast<int, short>(
this.storage.Slice(TransientStorageOffset, ChromaFromLumaSampleStorageLength));
/// <summary>
/// Gets the palette-search view over transient storage that is no longer needed after spatial and CfL search.
/// </summary>
public Av1EncoderPaletteWorkspace<TSample> Palette
=> new(this.storage[TransientStorageOffset..]);
/// <summary>
/// Gets one reference edge including its common-corner prefix.
/// </summary>
/// <param name="index">The zero-based edge index.</param>
/// <returns>The fixed reference-edge span.</returns>
public Span<TSample> GetReferenceSamples(int index)
=> MemoryMarshal.Cast<int, TSample>(this.storage[..ReferenceStorageLength])
.Slice(index * ReferenceBufferLength, ReferenceBufferLength);
/// <summary>
/// Gets one candidate reconstruction plane.
/// </summary>
/// <param name="index">The zero-based plane index.</param>
/// <returns>The maximum-size candidate reconstruction span.</returns>
public Span<TSample> GetCandidateReconstruction(int index)
=> MemoryMarshal.Cast<int, TSample>(
this.storage.Slice(CandidateSampleStorageOffset, CandidateSampleStorageLength))
.Slice(index * MaximumSampleCount, MaximumSampleCount);
/// <summary>
/// Gets one candidate coefficient plane.
/// </summary>
/// <param name="index">The zero-based plane index.</param>
/// <returns>The maximum-size candidate coefficient span.</returns>
public Span<int> GetCandidateCoefficients(int index)
=> this.storage
.Slice(CandidateCoefficientStorageOffset, CandidateCoefficientStorageLength)
.Slice(index * MaximumSampleCount, MaximumSampleCount);
/// <summary>
/// Gets one plane's chroma-from-luma coefficient-rate table.
/// </summary>
/// <param name="planeIndex">The zero-based chroma plane index.</param>
/// <returns>The rate table for every signed alpha candidate.</returns>
public Span<int> GetChromaFromLumaRates(int planeIndex)
=> this.storage.Slice(
TransientStorageOffset + ChromaFromLumaBlueRateOffset +
(planeIndex * Av1ChromaFromLumaMath.AlphaCandidateCount),
Av1ChromaFromLumaMath.AlphaCandidateCount);
/// <summary>
/// Gets one plane's chroma-from-luma distortion table.
/// </summary>
/// <param name="planeIndex">The zero-based chroma plane index.</param>
/// <returns>The distortion table for every signed alpha candidate.</returns>
public Span<long> GetChromaFromLumaDistortions(int planeIndex)
=> MemoryMarshal.Cast<int, long>(
this.storage.Slice(
TransientStorageOffset + ChromaFromLumaBlueDistortionOffset +
(planeIndex * ChromaFromLumaDistortionStorageLength),
ChromaFromLumaDistortionStorageLength));
}
/// <summary>
/// Provides typed luma and chroma palette-search buffers over reusable mode-decision storage.
/// </summary>
/// <typeparam name="TSample">The native sample type selected by the encoder pipeline.</typeparam>
internal readonly ref struct Av1EncoderPaletteWorkspace<TSample>
where TSample : unmanaged
{
/// <summary>
/// The required workspace length in signed-integer storage elements.
/// </summary>
public const int StorageLength = ColorCacheOffset + ColorCacheStorageLength;
private const int MaximumSampleCount = Av1EncoderModeDecisionWorkspace<ushort>.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<int> storage;
/// <summary>
/// Initializes a new instance of the <see cref="Av1EncoderPaletteWorkspace{TSample}"/> struct.
/// </summary>
/// <param name="storage">The reusable aligned palette storage.</param>
public Av1EncoderPaletteWorkspace(Span<int> storage) => this.storage = storage;
/// <summary>
/// Gets the retained winning color-index map.
/// </summary>
public Span<byte> RetainedIndices
=> MemoryMarshal.AsBytes(this.storage.Slice(RetainedIndexOffset, PlaneByteStorageLength));
/// <summary>
/// Gets the current color-index map.
/// </summary>
public Span<byte> Indices
=> MemoryMarshal.AsBytes(this.storage.Slice(IndexOffset, PlaneByteStorageLength));
/// <summary>
/// Gets the alternate K-means color-index map.
/// </summary>
public Span<byte> AlternateIndices
=> MemoryMarshal.AsBytes(this.storage.Slice(AlternateIndexOffset, PlaneByteStorageLength));
/// <summary>
/// Gets the luma occurrence count for every unique color.
/// </summary>
public Span<int> LumaColorCounts
=> this.storage.Slice(ColorCountOffset, MaximumSampleCount);
/// <summary>
/// Gets the luma unique-color ordering by descending occurrence count.
/// </summary>
public Span<byte> LumaDominantOrder
=> MemoryMarshal.AsBytes(this.storage.Slice(DominantOrderOffset, PlaneByteStorageLength));
/// <summary>
/// Gets the sorted neighboring palette colors available to the current block.
/// </summary>
public Span<ushort> ColorCache
=> MemoryMarshal.Cast<int, ushort>(this.storage.Slice(ColorCacheOffset, ColorCacheStorageLength));
/// <summary>
/// Gets one plane's active palette samples.
/// </summary>
/// <param name="planeIndex">The zero-based plane index.</param>
/// <returns>The maximum-size sample span.</returns>
public Span<short> GetSamples(int planeIndex)
=> MemoryMarshal.Cast<int, short>(
this.storage.Slice(FirstSampleOffset + (planeIndex * PlaneShortStorageLength), PlaneShortStorageLength));
/// <summary>
/// Gets one plane's unique palette colors.
/// </summary>
/// <param name="planeIndex">The zero-based plane index.</param>
/// <returns>The maximum-size unique-color span.</returns>
public Span<short> GetUniqueColors(int planeIndex)
=> MemoryMarshal.Cast<int, short>(
this.storage.Slice(
FirstUniqueColorOffset + (planeIndex * PlaneShortStorageLength),
PlaneShortStorageLength));
/// <summary>
/// Gets one plane's palette prediction.
/// </summary>
/// <param name="planeIndex">The zero-based plane index.</param>
/// <returns>The maximum-size prediction span.</returns>
public Span<TSample> GetPrediction(int planeIndex)
=> MemoryMarshal.Cast<int, TSample>(
this.storage.Slice(
FirstPredictionOffset + (planeIndex * PlaneSampleStorageLength),
PlaneSampleStorageLength))[..MaximumSampleCount];
/// <summary>
/// Gets one plane's palette residual.
/// </summary>
/// <param name="planeIndex">The zero-based plane index.</param>
/// <returns>The maximum-size residual span.</returns>
public Span<short> GetResidual(int planeIndex)
=> MemoryMarshal.Cast<int, short>(
this.storage.Slice(FirstResidualOffset + (planeIndex * PlaneShortStorageLength), PlaneShortStorageLength));
/// <summary>
/// Gets one plane's current palette centroids.
/// </summary>
/// <param name="planeIndex">The zero-based plane index.</param>
/// <returns>The maximum-size centroid span.</returns>
public Span<short> GetCentroids(int planeIndex)
=> MemoryMarshal.Cast<int, short>(
this.storage.Slice(
FirstCentroidOffset + (planeIndex * PaletteColorStorageLength),
PaletteColorStorageLength));
/// <summary>
/// Gets one plane's coded palette colors.
/// </summary>
/// <param name="planeIndex">The zero-based plane index.</param>
/// <returns>The maximum-size coded-color span.</returns>
public Span<ushort> GetPaletteColors(int planeIndex)
=> MemoryMarshal.Cast<int, ushort>(
this.storage.Slice(
FirstPaletteColorOffset + (planeIndex * PaletteColorStorageLength),
PaletteColorStorageLength));
/// <summary>
/// Gets one plane's alternate K-means centroids.
/// </summary>
/// <param name="planeIndex">The zero-based plane index.</param>
/// <returns>The maximum-size alternate-centroid span.</returns>
public Span<short> GetAlternateCentroids(int planeIndex)
=> MemoryMarshal.Cast<int, short>(
this.storage.Slice(
FirstAlternateCentroidOffset + (planeIndex * PaletteColorStorageLength),
PaletteColorStorageLength));
}

30
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaModeDecision.cs

@ -60,7 +60,9 @@ internal static partial class Av1IntraSuperblockEncoder
out long selectedCost) out long selectedCost)
{ {
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const int MaximumSampleCount = 8 * 8; Av1EncoderModeDecisionWorkspace<TSample> workspace =
this.blockWorkspace.GetModeDecisionWorkspace<TSample>();
ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig; ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig;
int subsamplingX = colorConfig.SubSamplingX ? 1 : 0; int subsamplingX = colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = colorConfig.SubSamplingY ? 1 : 0; int subsamplingY = colorConfig.SubSamplingY ? 1 : 0;
@ -105,10 +107,10 @@ internal static partial class Av1IntraSuperblockEncoder
Buffer2DRegion<TSample> redSource = this.source.GetPlane(Av1Plane.V); Buffer2DRegion<TSample> redSource = this.source.GetPlane(Av1Plane.V);
Buffer2DRegion<TSample> blueReconstruction = this.reconstruction.GetPlane(Av1Plane.U); Buffer2DRegion<TSample> blueReconstruction = this.reconstruction.GetPlane(Av1Plane.U);
Buffer2DRegion<TSample> redReconstruction = this.reconstruction.GetPlane(Av1Plane.V); Buffer2DRegion<TSample> redReconstruction = this.reconstruction.GetPlane(Av1Plane.V);
Span<TSample> blueAboveStorage = stackalloc TSample[17]; Span<TSample> blueAboveStorage = workspace.GetReferenceSamples(0);
Span<TSample> blueLeftStorage = stackalloc TSample[17]; Span<TSample> blueLeftStorage = workspace.GetReferenceSamples(1);
Span<TSample> redAboveStorage = stackalloc TSample[17]; Span<TSample> redAboveStorage = workspace.GetReferenceSamples(2);
Span<TSample> redLeftStorage = stackalloc TSample[17]; Span<TSample> redLeftStorage = workspace.GetReferenceSamples(3);
this.PrepareReferenceSamples( this.PrepareReferenceSamples(
blueReconstruction, blueReconstruction,
chromaOrigin, chromaOrigin,
@ -152,10 +154,10 @@ internal static partial class Av1IntraSuperblockEncoder
chromaBlockSize, chromaBlockSize,
transformSize); transformSize);
Span<TSample> candidateBlueReconstruction = stackalloc TSample[MaximumSampleCount]; Span<TSample> candidateBlueReconstruction = workspace.GetCandidateReconstruction(0);
Span<TSample> candidateRedReconstruction = stackalloc TSample[MaximumSampleCount]; Span<TSample> candidateRedReconstruction = workspace.GetCandidateReconstruction(1);
Span<int> candidateBlueCoefficients = stackalloc int[MaximumSampleCount]; Span<int> candidateBlueCoefficients = workspace.GetCandidateCoefficients(0);
Span<int> candidateRedCoefficients = stackalloc int[MaximumSampleCount]; Span<int> candidateRedCoefficients = workspace.GetCandidateCoefficients(1);
long bestCost = long.MaxValue; long bestCost = long.MaxValue;
Av1ChromaPredictionMode bestMode = Av1ChromaPredictionMode.DC; Av1ChromaPredictionMode bestMode = Av1ChromaPredictionMode.DC;
selectedAngleDelta = 0; selectedAngleDelta = 0;
@ -259,7 +261,7 @@ internal static partial class Av1IntraSuperblockEncoder
if (this.effort >= 4 && chromaFromLumaAllowed) if (this.effort >= 4 && chromaFromLumaAllowed)
{ {
Span<short> lumaQ3 = stackalloc short[Av1ChromaFromLumaContext.BufferLine * 8]; Span<short> lumaQ3 = workspace.ChromaFromLumaSamples;
TOperator.PrepareChromaFromLuma( TOperator.PrepareChromaFromLuma(
this.reconstruction.GetPlane(Av1Plane.Y), this.reconstruction.GetPlane(Av1Plane.Y),
lumaOrigin, lumaOrigin,
@ -290,10 +292,10 @@ internal static partial class Av1IntraSuperblockEncoder
this.bitDepth); this.bitDepth);
TSample redDc = candidateRedReconstruction[0]; TSample redDc = candidateRedReconstruction[0];
Span<int> blueRates = stackalloc int[Av1ChromaFromLumaMath.AlphaCandidateCount]; Span<int> blueRates = workspace.GetChromaFromLumaRates(0);
Span<int> redRates = stackalloc int[Av1ChromaFromLumaMath.AlphaCandidateCount]; Span<int> redRates = workspace.GetChromaFromLumaRates(1);
Span<long> blueDistortions = stackalloc long[Av1ChromaFromLumaMath.AlphaCandidateCount]; Span<long> blueDistortions = workspace.GetChromaFromLumaDistortions(0);
Span<long> redDistortions = stackalloc long[Av1ChromaFromLumaMath.AlphaCandidateCount]; Span<long> redDistortions = workspace.GetChromaFromLumaDistortions(1);
// Each plane has only 33 signed alpha values. Caching those complete transform results reduces // 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. // the joint search from 1089 transform pairs to 66 transforms plus inexpensive rate combinations.

41
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ChromaPaletteModeDecision.cs

@ -43,7 +43,9 @@ internal static partial class Av1IntraSuperblockEncoder
{ {
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const int LumaBlockLength = 8; const int LumaBlockLength = 8;
const int MaximumSampleCount = LumaBlockLength * LumaBlockLength; Av1EncoderPaletteWorkspace<TSample> workspace =
this.blockWorkspace.GetModeDecisionWorkspace<TSample>().Palette;
ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig; ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig;
int subsamplingX = colorConfig.SubSamplingX ? 1 : 0; int subsamplingX = colorConfig.SubSamplingX ? 1 : 0;
int subsamplingY = colorConfig.SubSamplingY ? 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 rows = Math.Min(LumaBlockLength, frameSize.FrameHeight - lumaOrigin.Y) >> subsamplingY;
int columns = Math.Min(LumaBlockLength, frameSize.FrameWidth - lumaOrigin.X) >> subsamplingX; int columns = Math.Min(LumaBlockLength, frameSize.FrameWidth - lumaOrigin.X) >> subsamplingX;
int activeSampleCount = rows * columns; int activeSampleCount = rows * columns;
Span<short> blueSamples = stackalloc short[MaximumSampleCount]; Span<short> blueSamples = workspace.GetSamples(0)[..activeSampleCount];
Span<short> redSamples = stackalloc short[MaximumSampleCount]; Span<short> redSamples = workspace.GetSamples(1)[..activeSampleCount];
blueSamples = blueSamples[..activeSampleCount];
redSamples = redSamples[..activeSampleCount];
Buffer2DRegion<TSample> blueSource = this.source.GetPlane(Av1Plane.U); Buffer2DRegion<TSample> blueSource = this.source.GetPlane(Av1Plane.U);
Buffer2DRegion<TSample> redSource = this.source.GetPlane(Av1Plane.V); Buffer2DRegion<TSample> redSource = this.source.GetPlane(Av1Plane.V);
TOperator.CopyPaletteSamples(blueSource, chromaOrigin, rows, columns, blueSamples); TOperator.CopyPaletteSamples(blueSource, chromaOrigin, rows, columns, blueSamples);
TOperator.CopyPaletteSamples(redSource, chromaOrigin, rows, columns, redSamples); TOperator.CopyPaletteSamples(redSource, chromaOrigin, rows, columns, redSamples);
Span<short> uniqueBlueColors = stackalloc short[MaximumSampleCount]; Span<short> uniqueBlueColors = workspace.GetUniqueColors(0);
Span<short> uniqueRedColors = stackalloc short[MaximumSampleCount]; Span<short> uniqueRedColors = workspace.GetUniqueColors(1);
int uniqueBlueColorCount = 0; int uniqueBlueColorCount = 0;
int uniqueRedColorCount = 0; int uniqueRedColorCount = 0;
short blueMinimum = blueSamples[0]; short blueMinimum = blueSamples[0];
@ -98,7 +98,7 @@ internal static partial class Av1IntraSuperblockEncoder
int maximumPaletteSize = Math.Min(maximumColorCount, Av1Constants.PaletteMaxSize); int maximumPaletteSize = Math.Min(maximumColorCount, Av1Constants.PaletteMaxSize);
Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContexts = this.picture.PaletteContexts[tileIndex]; Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContexts = this.picture.PaletteContexts[tileIndex];
Span<ushort> colorCache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize]; Span<ushort> colorCache = workspace.ColorCache;
int colorCacheSize = Av1TileWriter.GetPaletteCache( int colorCacheSize = Av1TileWriter.GetPaletteCache(
paletteContexts, paletteContexts,
macroBlock, macroBlock,
@ -113,16 +113,16 @@ internal static partial class Av1IntraSuperblockEncoder
.GetPaletteMaps() .GetPaletteMaps()
.GetMap(Av1PlaneType.Uv, width, height); .GetMap(Av1PlaneType.Uv, width, height);
Span<byte> retainedColorIndexMap = stackalloc byte[MaximumSampleCount]; Span<byte> retainedColorIndexMap = workspace.RetainedIndices;
Span<short> blueCentroids = stackalloc short[Av1Constants.PaletteMaxSize]; Span<short> blueCentroids = workspace.GetCentroids(0);
Span<short> redCentroids = stackalloc short[Av1Constants.PaletteMaxSize]; Span<short> redCentroids = workspace.GetCentroids(1);
Span<byte> colorIndices = stackalloc byte[MaximumSampleCount]; Span<byte> colorIndices = workspace.Indices;
Span<TSample> bluePrediction = stackalloc TSample[MaximumSampleCount]; Span<TSample> bluePrediction = workspace.GetPrediction(0);
Span<TSample> redPrediction = stackalloc TSample[MaximumSampleCount]; Span<TSample> redPrediction = workspace.GetPrediction(1);
Span<short> blueResidual = stackalloc short[MaximumSampleCount]; Span<short> blueResidual = workspace.GetResidual(0);
Span<short> redResidual = stackalloc short[MaximumSampleCount]; Span<short> redResidual = workspace.GetResidual(1);
Span<ushort> bluePaletteColorStorage = stackalloc ushort[Av1Constants.PaletteMaxSize]; Span<ushort> bluePaletteColorStorage = workspace.GetPaletteColors(0);
Span<ushort> redPaletteColorStorage = stackalloc ushort[Av1Constants.PaletteMaxSize]; Span<ushort> redPaletteColorStorage = workspace.GetPaletteColors(1);
int sampleCount = transformSize.GetSize2d(); int sampleCount = transformSize.GetSize2d();
int cacheThreshold = 4 << (this.bitDepth.GetBitCount() - 8); int cacheThreshold = 4 << (this.bitDepth.GetBitCount() - 8);
bool paletteSelected = false; bool paletteSelected = false;
@ -145,7 +145,10 @@ internal static partial class Av1IntraSuperblockEncoder
redSamples, redSamples,
candidateBlueCentroids, candidateBlueCentroids,
candidateRedCentroids, candidateRedCentroids,
colorIndices[..activeSampleCount]); colorIndices[..activeSampleCount],
workspace.GetAlternateCentroids(0),
workspace.GetAlternateCentroids(1),
workspace.AlternateIndices);
for (int colorIndex = 0; colorIndex < paletteSize && !colorCache.IsEmpty; colorIndex++) for (int colorIndex = 0; colorIndex < paletteSize && !colorCache.IsEmpty; colorIndex++)
{ {

16
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs

@ -397,7 +397,9 @@ internal static partial class Av1IntraSuperblockEncoder
{ {
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8; const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
const int SampleCount = 8 * 8; Av1EncoderModeDecisionWorkspace<TSample> workspace =
this.blockWorkspace.GetModeDecisionWorkspace<TSample>();
Buffer2DRegion<TSample> sourcePlane = this.source.GetPlane(Av1Plane.Y); Buffer2DRegion<TSample> sourcePlane = this.source.GetPlane(Av1Plane.Y);
Buffer2DRegion<TSample> reconstructionPlane = this.reconstruction.GetPlane(Av1Plane.Y); Buffer2DRegion<TSample> reconstructionPlane = this.reconstruction.GetPlane(Av1Plane.Y);
bool hasLeft = macroBlock.IsLeftAvailable; bool hasLeft = macroBlock.IsLeftAvailable;
@ -434,9 +436,9 @@ internal static partial class Av1IntraSuperblockEncoder
0, 0,
0); 0);
Span<TSample> aboveStorage = stackalloc TSample[17]; Span<TSample> aboveStorage = workspace.GetReferenceSamples(0);
Span<TSample> above = aboveStorage[1..]; Span<TSample> above = aboveStorage[1..];
Span<TSample> leftStorage = stackalloc TSample[17]; Span<TSample> leftStorage = workspace.GetReferenceSamples(1);
Span<TSample> left = leftStorage[1..]; Span<TSample> left = leftStorage[1..];
if (hasAbove) if (hasAbove)
@ -523,8 +525,8 @@ internal static partial class Av1IntraSuperblockEncoder
neighborContext); neighborContext);
} }
Span<TSample> candidateReconstruction = stackalloc TSample[SampleCount]; Span<TSample> candidateReconstruction = workspace.GetCandidateReconstruction(0);
Span<int> candidateCoefficients = stackalloc int[SampleCount]; Span<int> candidateCoefficients = workspace.GetCandidateCoefficients(0);
long bestCost = long.MaxValue; long bestCost = long.MaxValue;
Av1PredictionMode bestMode = Av1PredictionMode.DC; Av1PredictionMode bestMode = Av1PredictionMode.DC;
selectedAngleDelta = 0; selectedAngleDelta = 0;
@ -655,8 +657,8 @@ internal static partial class Av1IntraSuperblockEncoder
if (this.effort >= 4 && this.picture.Sequence.SequenceHeader.EnableFilterIntra) if (this.effort >= 4 && this.picture.Sequence.SequenceHeader.EnableFilterIntra)
{ {
Span<TSample> filterPrediction = stackalloc TSample[SampleCount]; Span<TSample> filterPrediction = workspace.FilterPrediction;
Span<short> filterResidual = stackalloc short[SampleCount]; Span<short> filterResidual = workspace.FilterResidual;
// Each recursive filter prediction and its source residual are independent of transform type. // 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. // Prepare them once per filter mode so all legal transforms reuse the same samples.

43
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.PaletteModeDecision.cs

@ -37,17 +37,18 @@ internal static partial class Av1IntraSuperblockEncoder
{ {
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const int BlockLength = 8; const int BlockLength = 8;
const int SampleCapacity = BlockLength * BlockLength; Av1EncoderPaletteWorkspace<TSample> workspace =
this.blockWorkspace.GetModeDecisionWorkspace<TSample>().Palette;
ObuFrameSize frameSize = this.picture.Parent.FrameHeader.FrameSize; ObuFrameSize frameSize = this.picture.Parent.FrameHeader.FrameSize;
int rows = Math.Min(BlockLength, frameSize.FrameHeight - blockOrigin.Y); int rows = Math.Min(BlockLength, frameSize.FrameHeight - blockOrigin.Y);
int columns = Math.Min(BlockLength, frameSize.FrameWidth - blockOrigin.X); int columns = Math.Min(BlockLength, frameSize.FrameWidth - blockOrigin.X);
int sampleCount = rows * columns; int sampleCount = rows * columns;
Span<short> samples = stackalloc short[SampleCapacity]; Span<short> samples = workspace.GetSamples(0)[..sampleCount];
samples = samples[..sampleCount];
TOperator.CopyPaletteSamples(sourcePlane, blockOrigin, rows, columns, samples); TOperator.CopyPaletteSamples(sourcePlane, blockOrigin, rows, columns, samples);
Span<short> uniqueColors = stackalloc short[SampleCapacity]; Span<short> uniqueColors = workspace.GetUniqueColors(0);
Span<int> colorCounts = stackalloc int[SampleCapacity]; Span<int> colorCounts = workspace.LumaColorCounts;
int uniqueColorCount = 0; int uniqueColorCount = 0;
short minimum = samples[0]; short minimum = samples[0];
short maximum = samples[0]; short maximum = samples[0];
@ -75,7 +76,7 @@ internal static partial class Av1IntraSuperblockEncoder
} }
int maximumPaletteSize = Math.Min(uniqueColorCount, Av1Constants.PaletteMaxSize); int maximumPaletteSize = Math.Min(uniqueColorCount, Av1Constants.PaletteMaxSize);
Span<byte> dominantOrder = stackalloc byte[SampleCapacity]; Span<byte> dominantOrder = workspace.LumaDominantOrder;
for (int index = 0; index < uniqueColorCount; index++) for (int index = 0; index < uniqueColorCount; index++)
{ {
dominantOrder[index] = (byte)index; dominantOrder[index] = (byte)index;
@ -107,7 +108,7 @@ internal static partial class Av1IntraSuperblockEncoder
Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContexts = this.picture.PaletteContexts[tileIndex]; Av1NeighborArrayUnit<Av1EncoderPaletteInfo> paletteContexts = this.picture.PaletteContexts[tileIndex];
int blockSizeContext = Av1TileWriter.GetPaletteBlockSizeContext(BlockSize); int blockSizeContext = Av1TileWriter.GetPaletteBlockSizeContext(BlockSize);
int neighborContext = Av1TileWriter.GetPaletteYModeContext(paletteContexts, macroBlock, blockOrigin); int neighborContext = Av1TileWriter.GetPaletteYModeContext(paletteContexts, macroBlock, blockOrigin);
Span<ushort> colorCache = stackalloc ushort[2 * Av1Constants.PaletteMaxSize]; Span<ushort> colorCache = workspace.ColorCache;
int colorCacheSize = Av1TileWriter.GetPaletteCache( int colorCacheSize = Av1TileWriter.GetPaletteCache(
paletteContexts, paletteContexts,
macroBlock, macroBlock,
@ -119,8 +120,8 @@ internal static partial class Av1IntraSuperblockEncoder
.GetPaletteMaps() .GetPaletteMaps()
.GetMap(Av1PlaneType.Y, BlockLength, BlockLength); .GetMap(Av1PlaneType.Y, BlockLength, BlockLength);
Span<byte> retainedColorIndexMap = stackalloc byte[SampleCapacity]; Span<byte> retainedColorIndexMap = workspace.RetainedIndices;
Span<short> centroids = stackalloc short[Av1Constants.PaletteMaxSize]; Span<short> centroids = workspace.GetCentroids(0);
bool paletteSelected = false; bool paletteSelected = false;
// Exhaustive ascending size search avoids the reference encoder's speed-dependent pruning. // Exhaustive ascending size search avoids the reference encoder's speed-dependent pruning.
@ -186,13 +187,18 @@ internal static partial class Av1IntraSuperblockEncoder
} }
else else
{ {
Span<byte> clusterIndices = stackalloc byte[SampleCapacity]; Span<byte> clusterIndices = workspace.Indices[..sampleCount];
clusterIndices = clusterIndices[..sampleCount];
for (int paletteSize = 2; paletteSize <= maximumPaletteSize; paletteSize++) for (int paletteSize = 2; paletteSize <= maximumPaletteSize; paletteSize++)
{ {
Span<short> candidateCentroids = centroids[..paletteSize]; Span<short> candidateCentroids = centroids[..paletteSize];
Av1PaletteKMeans.InitializeCentroids(minimum, maximum, candidateCentroids); Av1PaletteKMeans.InitializeCentroids(minimum, maximum, candidateCentroids);
Av1PaletteKMeans.Cluster(samples, candidateCentroids, clusterIndices); Av1PaletteKMeans.Cluster(
samples,
candidateCentroids,
clusterIndices,
workspace.GetAlternateCentroids(0),
workspace.AlternateIndices);
this.EvaluateLumaPaletteCandidate( this.EvaluateLumaPaletteCandidate(
writer, writer,
macroBlock, macroBlock,
@ -258,7 +264,9 @@ internal static partial class Av1IntraSuperblockEncoder
const Av1BlockSize BlockSize = Av1BlockSize.Block8x8; const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
const Av1TransformSize TransformSize = Av1TransformSize.Size8x8; const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
const int BlockLength = 8; const int BlockLength = 8;
const int SampleCount = BlockLength * BlockLength; Av1EncoderPaletteWorkspace<TSample> workspace =
this.blockWorkspace.GetModeDecisionWorkspace<TSample>().Palette;
int bitDepth = this.bitDepth.GetBitCount(); int bitDepth = this.bitDepth.GetBitCount();
int cacheThreshold = 4 << (bitDepth - 8); int cacheThreshold = 4 << (bitDepth - 8);
for (int colorIndex = 0; colorIndex < centroids.Length && !colorCache.IsEmpty; colorIndex++) for (int colorIndex = 0; colorIndex < centroids.Length && !colorCache.IsEmpty; colorIndex++)
@ -297,14 +305,13 @@ internal static partial class Av1IntraSuperblockEncoder
} }
ReadOnlySpan<short> paletteCentroids = centroids[..paletteSize]; ReadOnlySpan<short> paletteCentroids = centroids[..paletteSize];
Span<ushort> paletteColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; Span<ushort> paletteColors = workspace.GetPaletteColors(0)[..paletteSize];
paletteColors = paletteColors[..paletteSize];
for (int colorIndex = 0; colorIndex < paletteSize; colorIndex++) for (int colorIndex = 0; colorIndex < paletteSize; colorIndex++)
{ {
paletteColors[colorIndex] = (ushort)paletteCentroids[colorIndex]; paletteColors[colorIndex] = (ushort)paletteCentroids[colorIndex];
} }
Span<byte> colorIndices = stackalloc byte[SampleCount]; Span<byte> colorIndices = workspace.Indices;
Av1PaletteKMeans.AssignIndices(samples, paletteCentroids, colorIndices); Av1PaletteKMeans.AssignIndices(samples, paletteCentroids, colorIndices);
for (int row = 0; row < rows; row++) for (int row = 0; row < rows; row++)
{ {
@ -320,8 +327,8 @@ internal static partial class Av1IntraSuperblockEncoder
.CopyTo(colorIndexMap.DangerousGetRowSpan(row)); .CopyTo(colorIndexMap.DangerousGetRowSpan(row));
} }
Span<TSample> prediction = stackalloc TSample[SampleCount]; Span<TSample> prediction = workspace.GetPrediction(0);
Span<short> residual = stackalloc short[SampleCount]; Span<short> residual = workspace.GetResidual(0);
TOperator.PreparePalette( TOperator.PreparePalette(
this.source.GetPlane(Av1Plane.Y), this.source.GetPlane(Av1Plane.Y),
blockOrigin, blockOrigin,

9
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans.cs

@ -144,15 +144,18 @@ internal static class Av1PaletteKMeans
/// <param name="samples">The active block samples.</param> /// <param name="samples">The active block samples.</param>
/// <param name="centroids">The initialized colors, replaced with the best refined colors.</param> /// <param name="centroids">The initialized colors, replaced with the best refined colors.</param>
/// <param name="indices">The palette indices belonging to the retained colors.</param> /// <param name="indices">The palette indices belonging to the retained colors.</param>
/// <param name="alternateCentroids">Reusable storage for the next centroid iteration.</param>
/// <param name="alternateIndices">Reusable storage for the next index iteration.</param>
/// <returns>The retained sum of squared distances.</returns> /// <returns>The retained sum of squared distances.</returns>
public static long Cluster( public static long Cluster(
ReadOnlySpan<short> samples, ReadOnlySpan<short> samples,
Span<short> centroids, Span<short> centroids,
Span<byte> indices) Span<byte> indices,
Span<short> alternateCentroids,
Span<byte> alternateIndices)
{ {
Span<short> alternateCentroids = stackalloc short[Av1Constants.PaletteMaxSize];
Span<byte> alternateIndices = stackalloc byte[samples.Length];
alternateCentroids = alternateCentroids[..centroids.Length]; alternateCentroids = alternateCentroids[..centroids.Length];
alternateIndices = alternateIndices[..samples.Length];
long distortion = AssignIndices(samples, centroids, indices); long distortion = AssignIndices(samples, centroids, indices);
bool currentIsAlternate = false; bool currentIsAlternate = false;

12
src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1PaletteKMeans2D.cs

@ -217,19 +217,23 @@ internal static class Av1PaletteKMeans2D
/// <param name="firstCentroids">The initialized first-plane colors.</param> /// <param name="firstCentroids">The initialized first-plane colors.</param>
/// <param name="secondCentroids">The initialized second-plane colors.</param> /// <param name="secondCentroids">The initialized second-plane colors.</param>
/// <param name="indices">The palette indices belonging to the retained colors.</param> /// <param name="indices">The palette indices belonging to the retained colors.</param>
/// <param name="alternateFirstCentroids">Reusable storage for the next first-plane centroid iteration.</param>
/// <param name="alternateSecondCentroids">Reusable storage for the next second-plane centroid iteration.</param>
/// <param name="alternateIndices">Reusable storage for the next index iteration.</param>
/// <returns>The retained sum of squared two-plane distances.</returns> /// <returns>The retained sum of squared two-plane distances.</returns>
public static long Cluster( public static long Cluster(
ReadOnlySpan<short> firstSamples, ReadOnlySpan<short> firstSamples,
ReadOnlySpan<short> secondSamples, ReadOnlySpan<short> secondSamples,
Span<short> firstCentroids, Span<short> firstCentroids,
Span<short> secondCentroids, Span<short> secondCentroids,
Span<byte> indices) Span<byte> indices,
Span<short> alternateFirstCentroids,
Span<short> alternateSecondCentroids,
Span<byte> alternateIndices)
{ {
Span<short> alternateFirstCentroids = stackalloc short[Av1Constants.PaletteMaxSize];
Span<short> alternateSecondCentroids = stackalloc short[Av1Constants.PaletteMaxSize];
Span<byte> alternateIndices = stackalloc byte[firstSamples.Length];
alternateFirstCentroids = alternateFirstCentroids[..firstCentroids.Length]; alternateFirstCentroids = alternateFirstCentroids[..firstCentroids.Length];
alternateSecondCentroids = alternateSecondCentroids[..secondCentroids.Length]; alternateSecondCentroids = alternateSecondCentroids[..secondCentroids.Length];
alternateIndices = alternateIndices[..firstSamples.Length];
long distortion = AssignIndices( long distortion = AssignIndices(
firstSamples, firstSamples,
secondSamples, secondSamples,

47
src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs

@ -141,15 +141,28 @@ internal sealed class Av1CdefDecoder
columnBufferLength += columnBufferLengths[planeIndex]; columnBufferLength += columnBufferLengths[planeIndex];
} }
int scratchLength = SourceBufferLength + lineBufferLength + columnBufferLength; int directionStorageLength = MaximumBlocksPerUnit * sizeof(int) / sizeof(ushort);
int blockStorageLength = MaximumBlocksPerUnit * Unsafe.SizeOf<CdefBlock>() / sizeof(ushort);
int unitStorageOffset = SourceBufferLength + lineBufferLength + columnBufferLength;
int scratchLength = unitStorageOffset + (directionStorageLength * 2) + blockStorageLength;
MemoryAllocator allocator = this.frameBuffer.MemoryAllocator; MemoryAllocator allocator = this.frameBuffer.MemoryAllocator;
using IMemoryOwner<ushort> scratchOwner = allocator.Allocate<ushort>(scratchLength); using IMemoryOwner<ushort> scratchOwner = allocator.Allocate<ushort>(scratchLength);
Span<ushort> scratch = scratchOwner.Memory.Span[..scratchLength]; Span<ushort> scratch = scratchOwner.Memory.Span[..scratchLength];
Span<ushort> source = scratch[..SourceBufferLength]; Span<ushort> source = scratch[..SourceBufferLength];
Span<ushort> lineBuffer = scratch.Slice(SourceBufferLength, lineBufferLength); Span<ushort> lineBuffer = scratch.Slice(SourceBufferLength, lineBufferLength);
Span<ushort> columnBuffer = scratch[(SourceBufferLength + lineBufferLength)..]; Span<ushort> columnBuffer = scratch.Slice(SourceBufferLength + lineBufferLength, columnBufferLength);
Span<int> directions = stackalloc int[MaximumBlocksPerUnit];
Span<int> variances = stackalloc int[MaximumBlocksPerUnit]; // 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<int> directions = MemoryMarshal.Cast<ushort, int>(
scratch.Slice(unitStorageOffset, directionStorageLength));
Span<int> variances = MemoryMarshal.Cast<ushort, int>(
scratch.Slice(unitStorageOffset + directionStorageLength, directionStorageLength));
Span<CdefBlock> blocks = MemoryMarshal.Cast<ushort, CdefBlock>(
scratch.Slice(unitStorageOffset + (directionStorageLength * 2), blockStorageLength));
Span<bool> cdefLeft = stackalloc bool[3]; Span<bool> cdefLeft = stackalloc bool[3];
int unitColumnCount = (this.frameHeader.ModeInfoColumnCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize; int unitColumnCount = (this.frameHeader.ModeInfoColumnCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize;
int unitRowCount = (this.frameHeader.ModeInfoRowCount + 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 unitModeInfoRowEnd = Math.Min(unitModeInfoRow + CdefUnitModeInfoSize, this.frameHeader.ModeInfoRowCount);
int unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount); int unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount);
CdefBlockList blocks = default;
int blockCount = 0; int blockCount = 0;
for (int blockModeInfoRow = unitModeInfoRow; blockModeInfoRow < unitModeInfoRowEnd; blockModeInfoRow += 2) for (int blockModeInfoRow = unitModeInfoRow; blockModeInfoRow < unitModeInfoRowEnd; blockModeInfoRow += 2)
@ -265,8 +277,7 @@ internal sealed class Av1CdefDecoder
subsamplingY[planeIndex], subsamplingY[planeIndex],
unitModeInfoColumn, unitModeInfoColumn,
unitModeInfoRow, unitModeInfoRow,
ref blocks, blocks[..blockCount],
blockCount,
directions, directions,
variances, variances,
yStrength, yStrength,
@ -291,7 +302,6 @@ internal sealed class Av1CdefDecoder
/// <param name="unitModeInfoColumn">The unit's frame-relative column in 4x4 luma units.</param> /// <param name="unitModeInfoColumn">The unit's frame-relative column in 4x4 luma units.</param>
/// <param name="unitModeInfoRow">The unit's frame-relative row in 4x4 luma units.</param> /// <param name="unitModeInfoRow">The unit's frame-relative row in 4x4 luma units.</param>
/// <param name="blocks">The unit's non-skipped 8x8 luma blocks.</param> /// <param name="blocks">The unit's non-skipped 8x8 luma blocks.</param>
/// <param name="blockCount">The number of initialized entries in <paramref name="blocks"/>.</param>
/// <param name="directions">The unit-local luma directions in block-list order.</param> /// <param name="directions">The unit-local luma directions in block-list order.</param>
/// <param name="variances">The unit-local luma directional variances in block-list order.</param> /// <param name="variances">The unit-local luma directional variances in block-list order.</param>
/// <param name="yStrength">The coded luma strength.</param> /// <param name="yStrength">The coded luma strength.</param>
@ -306,8 +316,7 @@ internal sealed class Av1CdefDecoder
int subsamplingY, int subsamplingY,
int unitModeInfoColumn, int unitModeInfoColumn,
int unitModeInfoRow, int unitModeInfoRow,
ref CdefBlockList blocks, ReadOnlySpan<CdefBlock> blocks,
int blockCount,
Span<int> directions, Span<int> directions,
Span<int> variances, Span<int> variances,
int yStrength, 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 // 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. // 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 firstBlock = blocks[blockIndex];
CdefBlock secondBlock = blocks[blockIndex + 1]; CdefBlock secondBlock = blocks[blockIndex + 1];
@ -446,7 +455,7 @@ internal sealed class Av1CdefDecoder
out variances[blockIndex + 1]); out variances[blockIndex + 1]);
} }
if (blockIndex < blockCount) if (blockIndex < blocks.Length)
{ {
CdefBlock block = blocks[blockIndex]; CdefBlock block = blocks[blockIndex];
@ -464,7 +473,7 @@ internal sealed class Av1CdefDecoder
return; return;
} }
for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) for (int blockIndex = 0; blockIndex < blocks.Length; blockIndex++)
{ {
CdefBlock block = blocks[blockIndex]; CdefBlock block = blocks[blockIndex];
int filteredPrimaryStrength = plane == Av1Plane.Y int filteredPrimaryStrength = plane == Av1Plane.Y
@ -667,18 +676,6 @@ internal sealed class Av1CdefDecoder
return true; return true;
} }
/// <summary>
/// Stores the non-skipped blocks in one CDEF unit without a managed allocation.
/// </summary>
[InlineArray(MaximumBlocksPerUnit)]
private struct CdefBlockList
{
/// <summary>
/// The first block in the inline storage.
/// </summary>
private CdefBlock element0;
}
/// <summary> /// <summary>
/// Identifies one 8x8 luma block by its frame-relative mode-information coordinates. /// Identifies one 8x8 luma block by its frame-relative mode-information coordinates.
/// </summary> /// </summary>

8
tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PaletteKMeans2DTests.cs

@ -27,13 +27,19 @@ public class Av1PaletteKMeans2DTests
short[] firstCentroids = [20, 100, 180]; short[] firstCentroids = [20, 100, 180];
short[] secondCentroids = [30, 110, 190]; short[] secondCentroids = [30, 110, 190];
byte[] indices = new byte[firstSamples.Length]; 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( long distortion = Av1PaletteKMeans2D.Cluster(
firstSamples, firstSamples,
secondSamples, secondSamples,
firstCentroids, firstCentroids,
secondCentroids, secondCentroids,
indices); indices,
alternateFirstCentroids,
alternateSecondCentroids,
alternateIndices);
Assert.Equal([1, 101, 201], firstCentroids); Assert.Equal([1, 101, 201], firstCentroids);
Assert.Equal([11, 111, 211], secondCentroids); Assert.Equal([11, 111, 211], secondCentroids);

11
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[] samples = [0, 1, 2, 100, 101, 102, 200, 201, 202];
short[] centroids = [33, 100, 167]; short[] centroids = [33, 100, 167];
byte[] indices = new byte[samples.Length]; byte[] indices = new byte[samples.Length];
short[] alternateCentroids = new short[centroids.Length];
long distortion = Av1PaletteKMeans.Cluster(samples, centroids, indices); byte[] alternateIndices = new byte[samples.Length];
long distortion = Av1PaletteKMeans.Cluster(
samples,
centroids,
indices,
alternateCentroids,
alternateIndices);
Assert.Equal([1, 101, 201], centroids); Assert.Equal([1, 101, 201], centroids);
Assert.Equal([0, 0, 0, 1, 1, 1, 2, 2, 2], indices); Assert.Equal([0, 0, 0, 1, 1, 1, 2, 2, 2], indices);

19
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;
using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; 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.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -610,6 +611,24 @@ public class Av1TransformBlockEncoderTests
Assert.Equal(Av1EncoderBlockWorkspace.MaximumCoefficientCount, workspace.TransformCoefficients.Length); Assert.Equal(Av1EncoderBlockWorkspace.MaximumCoefficientCount, workspace.TransformCoefficients.Length);
Assert.Equal(Av1EncoderBlockWorkspace.MaximumCoefficientCount, workspace.DequantizedCoefficients.Length); Assert.Equal(Av1EncoderBlockWorkspace.MaximumCoefficientCount, workspace.DequantizedCoefficients.Length);
Assert.Equal(Av1TransformWorkspace.MaximumLength, workspace.TransformWorkspace.Length); Assert.Equal(Av1TransformWorkspace.MaximumLength, workspace.TransformWorkspace.Length);
Av1EncoderModeDecisionWorkspace<ushort> modeWorkspace = workspace.GetModeDecisionWorkspace<ushort>();
Av1EncoderPaletteWorkspace<ushort> paletteWorkspace = modeWorkspace.Palette;
Av1EncoderIntraBlockCopyWorkspace<ushort> intraBlockCopyWorkspace =
workspace.GetIntraBlockCopyWorkspace<ushort>();
Assert.Equal(17, modeWorkspace.GetReferenceSamples(3).Length);
Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.MaximumSampleCount, modeWorkspace.GetCandidateReconstruction(1).Length);
Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.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<ushort>.MaximumSampleCount, paletteWorkspace.GetPrediction(1).Length);
Assert.Equal(Av1EncoderModeDecisionWorkspace<ushort>.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); TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog);

Loading…
Cancel
Save