diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md
index 6a48feeabe..72f449d376 100644
--- a/HEIF_IMPLEMENTATION_PLAN.md
+++ b/HEIF_IMPLEMENTATION_PLAN.md
@@ -828,7 +828,7 @@ Encoder verification contract:
- [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools.
- [~] Current-libaom `av1_quantize_fp_no_qmatrix` arithmetic is implemented as a closed generic forward-quantizer family with Vector512, Vector256, Vector128, and scalar paths, raster-order output, coded 64-point coefficient limits, and scan-order EOB selection. Transform search, coefficient optimization, and lossless behavior remain.
- [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality.
-- [~] Encoder rate accounting now converts the entropy writer's live inverse cumulative distributions into current-libaom fixed-point symbol costs without allocating or duplicating probability state. Read-only luma-mode, directional-delta, filter-intra, chroma-mode, block-skip, transform-size, transform-block-skip, and complete transform-coefficient queries share the exact distributions mutated by the subsequent entropy write. Complete coefficient costing follows current libaom's optimized shape: it returns immediately for an empty transform, uses the EOB-specific base-range context, fuses magnitude, sign, base-range, and Golomb accounting into one reverse traversal, and combines repeated full base-range chunks instead of replaying each emitted symbol. Tile-lifetime level and context scratch is reused, the one-coefficient path neither clears nor initializes the forward-neighbor level map, and steady-state queries allocate nothing. Transform-size writing and costing share one subdivision-depth calculation, while shared closed symbol operations keep the writer and cost mappings for transform skip, transform type, and EOB syntax identical without forcing the estimator through the writer's slower two-pass coefficient traversal. The current-libaom fixed-point RD combiner preserves 64-bit distortion and rounds the weighted 1/512-bit rate at the required boundary; the quality model still needs to select its multiplier. Exact empty, EOB-only, adaptive-state, complete nonzero-map, base-range, sign, Golomb, allocation, probability, syntax-query, transform-depth, and RD cases pass 27 of 27 focused direct net11 Release VSTest cases; all 1,909 `Av1EntropyTests`, all 84 `Av1CoefficientsEntropyTests`, and all 8,890 complete HEIF/AV1 namespace cases pass. Complete candidate accounting still needs to combine prediction mode rate, coefficient rate, distortion, and the quality-derived multiplier into real selection.
+- [~] Encoder rate accounting converts the entropy writer's live inverse cumulative distributions into current-libaom fixed-point symbol costs without allocating or duplicating probability state. Read-only luma-mode, directional-delta, filter-intra, chroma-mode, block-skip, transform-size, transform-block-skip, and complete transform-coefficient queries share the exact distributions mutated by the subsequent entropy write. Complete coefficient costing follows current libaom's optimized shape: it returns immediately for an empty transform, uses the EOB-specific base-range context, fuses magnitude, sign, base-range, and Golomb accounting into one reverse traversal, and combines repeated full base-range chunks instead of replaying each emitted symbol. Tile-lifetime level and context scratch is reused, the one-coefficient path neither clears nor initializes the forward-neighbor level map, and steady-state queries allocate nothing. Transform-size writing and costing share one subdivision-depth calculation, while shared closed symbol operations keep the writer and cost mappings for transform skip, transform type, and EOB syntax identical without forcing the estimator through the writer's slower two-pass coefficient traversal. The current-libaom fixed-point RD combiner preserves 64-bit distortion and rounds the weighted 1/512-bit rate at the required boundary. Its key-frame multiplier now follows libaom's squared DC-quantizer formula and exact 10/12-bit normalization. The first live final-block selection slice evaluates DC and zero-angle vertical luma prediction with complete transform, quantization, reconstruction, coefficient rate, and normalized pixel-domain distortion. The tile writer invokes this stack-only selector after mapping the current neighbors and immediately before writing each block, so later decisions see reconstructed samples, coefficient contexts, and CDF updates from every preceding block. Block skip is read only after the callback has combined every coded plane. Candidate scratch is limited to one 8x8 reconstruction and one 8x8 coefficient span on the stack; only the winning candidate is copied into retained frame storage. Focused rate, SIMD distortion, high-bit-depth normalization, live-CDF, skip-ordering, and production vertical-selection coverage passes 1,933 of 1,933 direct net11 Release VSTest cases. All 8,900 HEIF/AV1 namespace cases pass, and current-main `aomdec` accepts all 29 emitted 8/10/12-bit 4:0:0, 4:2:0, 4:2:2, and 4:4:4 constant or gradient payloads. Remaining mode decision work includes the other intra modes and angle deltas, transform-size/type search, chroma-mode search, partition search, full block-skip RD comparison, and effort-dependent pruning.
- [~] The tile writer now publishes one packed coefficient context per covered 4x4 edge unit and derives luma/chroma skip plus DC-sign contexts from the complete transform edges using current-libaom units. Partition, transform, and coefficient neighbor state retains only the above and left context regions used by current libaom; the unused third top-left region, its granularity state, and its unused sentinel are removed. One picture owner now packs segmentation plus every tile's partition, luma, chroma, and transform edges into one clean byte allocation with typed non-owning views; together with the separately typed packed mode-information owner, the complete picture state uses two allocator rents rather than seven. Exact aligned lengths, clean initialization, and balanced exactly-once returns are covered in Release. Multi-tile payload ownership and verified CDF update behavior remain.
- [~] Encoder mode information now uses a frame-owned integer alias grid over a packed 8-byte value allocation, matching current libaom's `mi_grid_base` and `mi_alloc` relationship without a managed object or reference per 4x4 entry. The visible dimensions are aligned to eight luma samples, the grid stride and allocated row count are aligned to 32 mode-information units, and optional 8x8 allocation granularity reduces the value store in both dimensions exactly as current libaom does. One clean ImageSharp byte owner contains both independently typed regions, reducing libaom's two allocation lifetimes to one without a copy. At 4K, the 4x4 layout occupies about 6.0 MiB in total; the 8x8 layout occupies about 3.0 MiB. Exact geometry, clean allocation, typed lengths, aligned mapping, untouched row padding, and exactly-once return pass 4 of 4 direct net11 VSTest cases in Release. Every coded 4x4 cell covered by square, rectangular, or clipped edge blocks maps to its owning allocation entry before context-dependent symbols are written. Packed syntax, relative neighbor lookup, full block mapping, writer traversal, entropy, and OBU coverage pass 1,947 of 1,947 direct net11 VSTest cases in Release; complete mode decision still remains.
- [~] The final-block decision workspace uses one reusable 10.3 KiB ImageSharp allocator owner. It contains 1,024 explicitly packed 10-byte final-block entries and the 341 preorder partition bytes required by a complete 128x128-through-8x8 quadtree, replacing separate managed arrays. Construction and the explicit per-superblock reset initialize every syntax field, including the nonzero sentinel that disables filter-intra prediction; pooled palette, quantizer, prediction, and partition bytes cannot leak into the next decision pass. Exact allocation, size, initialization, reset, return, repeated-run, writer, entropy, and OBU coverage pass 1,957 of 1,957 direct net11 VSTest cases in Release; complete mode decision still remains.
@@ -836,7 +836,7 @@ Encoder verification contract:
- [~] Tile partition writing now follows current libaom's recursive `write_modes_sb` preorder traversal and `update_ext_partition_context` edge updates directly. Bottom-edge blocks use the horizontal-alike partition CDF and right-edge blocks use the vertical-alike CDF; byte-exact regressions cover both paths after the previous calls were found reversed. Lossless chroma-from-luma availability now uses the subsampled plane block size shared with the decoder instead of the lossy 32x32 limit, preserving the correct UV-mode alphabet for each segment. The obsolete SVT-derived global geometry catalog and its unimplemented lookup are removed; transform geometry is derived in libaom's bounded 64x64 residual order, fixed intra transform-size symbols use the reference depth and neighbor contexts, and each derived transform size is persisted to the frame-owned mode information before the entropy snapshot and coefficient traversal consume it. Frame-edge and segmentation syntax use mode-information units, and 128x128 CDEF units use libaom's 0-to-3 indexing and first-block strength ownership. The focused transform-state regression passes 3 of 3 direct net11 VSTest cases in Release. Writer, entropy, and OBU coverage passes 1,957 of 1,957 direct net11 VSTest cases in Release, with 20 of 20 focused encoder and decoder chroma-from-luma cases. Partition and mode analysis still need to populate these retained decisions; variable inter-transform syntax remains part of later inter-frame support.
- [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions.
- [~] The coefficient symbol encoder now reuses tile-lifetime level and context workspaces instead of allocating per transform, defers both coefficient rents until the first nonzero transform block, and disposes all tile scratch independently from the detached encoded bytes. Its range coder matches current libaom's 64-bit coding window, bulk big-endian byte flush, and backward carry propagation while using one byte of allocator scratch per estimated output byte instead of the former 16-bit pre-carry storage. The single-tile production path finalizes in that existing allocation and transfers its owner plus the used byte length, removing the former second rent and full-tile copy; exact-length test callers retain the original overload. The ownership regression proves that writer disposal cannot return transferred storage and that the caller returns the original allocation exactly once. The internal frame operation now passes that payload directly to the OBU writer; AVIF container integration and public activation remain.
-- [~] The planar conversion, DC intra prediction, residual construction, forward transform, and forward quantizer use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar. Residual construction matches current libaom's exact source-minus-prediction arithmetic for 8-bit and high-bit-depth planes, preserves independent row strides and unaligned starts, and writes directly into caller-owned signed-short storage without allocation. The composed block path delegates arithmetic to those closed operators and adds no allocation. Apply the same rule to every later hot-path family.
+- [~] The planar conversion, DC intra prediction, residual construction, forward transform, and forward quantizer use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar. Residual construction matches current libaom's exact source-minus-prediction arithmetic for 8-bit and high-bit-depth planes, preserves independent row strides and unaligned starts, and writes directly into caller-owned signed-short storage without allocation. Candidate distortion reuses that residual workspace and widens signed 12-bit lanes before vector squaring, accumulating exact full-block SSE in 64-bit scalar storage. The composed block path delegates arithmetic to those closed operators and adds no allocation. Apply the same rule to every later hot-path family.
- [~] Residual tests verify misaligned planes, independent source, prediction, and destination strides, SIMD remainders, untouched padding, 8-bit, 10-bit, and 12-bit precision, every operator width independently of host acceleration, the scalar fallback, and zero per-transform allocations.
- [~] The unused coefficient-shape transform facade and its unimplemented N2, N4, and DC-only branches are removed. Finalized block encoding now follows the complete-transform path that current libaom uses before fast quantization; later rate-distortion search may add proven coefficient optimization without exposing inactive runtime throws.
- [~] Forward-quantizer FeatureTestRunner and zero-allocation tests compare every hardware tier with an independent scan-order scalar oracle shaped from current-main libaom. Both passed direct net11 VSTest in Release.
diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs
index d25f972d7c..86c4dc081e 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1RateDistortion.cs
@@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers;
+
namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
///
@@ -8,6 +10,28 @@ namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
///
internal static class Av1RateDistortion
{
+ ///
+ /// Gets the key-frame rate multiplier for an AV1 quantizer and sample precision.
+ ///
+ /// The segment quantizer index.
+ /// The coded sample bit depth.
+ /// The rate multiplier.
+ public static int GetKeyFrameRateMultiplier(int qIndex, Av1BitDepth bitDepth)
+ {
+ int quantizer = Av1QuantizationLookup.GetDcQuant(qIndex, 0, bitDepth);
+
+ // Key frames use a quantizer-dependent weight over the squared DC step. High-bit-depth
+ // distortion is normalized back to the eight-bit domain, so its rate multiplier follows it.
+ long multiplier = (long)((quantizer * (long)quantizer) * (3.3 + (0.0015 * quantizer)));
+ int shift = (bitDepth.GetBitCount() - 8) * 2;
+ if (shift > 0)
+ {
+ multiplier = (multiplier + (1L << (shift - 1))) >> shift;
+ }
+
+ return (int)Math.Max(multiplier, 1);
+ }
+
///
/// Gets a rate-distortion cost using the encoder probability-cost precision.
///
diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
new file mode 100644
index 0000000000..15c7da9b9f
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.ModeDecision.cs
@@ -0,0 +1,397 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
+using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
+using SixLabors.ImageSharp.Memory;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
+
+///
+/// Provides live-probability final-block mode decisions for intra encoding.
+///
+internal static partial class Av1IntraSuperblockEncoder
+{
+ ///
+ /// Builds the fixed 8x8 partition skeleton consumed by interleaved mode decision and tile writing.
+ ///
+ /// The frame coding and mode-information state.
+ /// The reusable partition and final-block decisions.
+ /// The absolute luma-sample origin of the superblock.
+ public static void Prepare(
+ Av1PictureControlSet picture,
+ Av1Superblock superblock,
+ Point superblockOrigin)
+ {
+ superblock.Workspace.Reset();
+ int partitionIndex = 0;
+ PreparePartitionTree(
+ picture,
+ superblock,
+ superblockOrigin,
+ picture.Sequence.SequenceHeader.SuperblockSize,
+ ref partitionIndex);
+ }
+
+ private static void PreparePartitionTree(
+ Av1PictureControlSet picture,
+ Av1Superblock superblock,
+ Point blockOrigin,
+ Av1BlockSize blockSize,
+ ref int partitionIndex)
+ {
+ Av1EncoderCommon common = picture.Parent.Common;
+ Point modeInfoPosition = blockOrigin >> Av1Constants.ModeInfoSizeLog2;
+ if (modeInfoPosition.Y >= common.ModeInfoRowCount || modeInfoPosition.X >= common.ModeInfoColumnCount)
+ {
+ return;
+ }
+
+ if (blockSize == Av1BlockSize.Block8x8)
+ {
+ superblock.CodingUnitPartitionTypes[partitionIndex++] = (byte)Av1PartitionType.None;
+ ref Av1MacroBlockModeInfo modeInfo = ref picture.GetMacroBlockModeInfo(modeInfoPosition);
+ modeInfo.Block = new Av1EncoderBlockModeInfo
+ {
+ BlockSize = Av1BlockSize.Block8x8,
+ PartitionType = Av1PartitionType.None
+ };
+
+ return;
+ }
+
+ superblock.CodingUnitPartitionTypes[partitionIndex++] = (byte)Av1PartitionType.Split;
+ Av1BlockSize subSize = Av1PartitionType.Split.GetBlockSubSize(blockSize);
+ int halfBlockSize = blockSize.GetWidth() >> 1;
+
+ // The same preorder drives partition symbols, block decisions, and coefficient offsets.
+ PreparePartitionTree(picture, superblock, blockOrigin, subSize, ref partitionIndex);
+ PreparePartitionTree(picture, superblock, blockOrigin + new Size(halfBlockSize, 0), subSize, ref partitionIndex);
+ PreparePartitionTree(picture, superblock, blockOrigin + new Size(0, halfBlockSize), subSize, ref partitionIndex);
+ PreparePartitionTree(picture, superblock, blockOrigin + new Size(halfBlockSize, halfBlockSize), subSize, ref partitionIndex);
+ }
+
+ ///
+ /// Produces one final block at a time against the tile state immediately preceding its syntax.
+ ///
+ /// The native unsigned sample storage type.
+ /// The type-specific block encoding operations.
+ internal struct ModeDecision : Av1TileWriter.IBlockEncodingHandler
+ where TSample : unmanaged
+ where TOperator : struct, IBlockEncodingOperator
+ {
+ private readonly Av1EncoderFrame.PlanarView source;
+ private readonly Av1EncoderFrame.PlanarView reconstruction;
+ private readonly Av1PictureControlSet picture;
+ private readonly Av1Superblock superblock;
+ private readonly Av1EncoderCoefficientBuffer coefficientBuffer;
+ private readonly Av1EncoderBlockWorkspace blockWorkspace;
+ private readonly ObuQuantizationParameters quantization;
+ private readonly Av1BitDepth bitDepth;
+ private readonly int rateMultiplier;
+ private int codedAreaLuma;
+ private int codedAreaChroma;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The coded source frame.
+ /// The reconstructed frame updated by winning candidates.
+ /// The frame coding and mode-information state.
+ /// The current superblock.
+ /// The frame-owned quantized coefficient and transform state.
+ /// The reusable block arithmetic workspace.
+ public ModeDecision(
+ Av1EncoderFrame source,
+ Av1EncoderFrame reconstruction,
+ Av1PictureControlSet picture,
+ Av1Superblock superblock,
+ Av1EncoderCoefficientBuffer coefficientBuffer,
+ Av1EncoderBlockWorkspace blockWorkspace)
+ {
+ this.source = source.CodedView;
+ this.reconstruction = reconstruction.CodedView;
+ this.picture = picture;
+ this.superblock = superblock;
+ this.coefficientBuffer = coefficientBuffer;
+ this.blockWorkspace = blockWorkspace;
+ this.quantization = picture.Parent.FrameHeader.QuantizationParameters;
+ this.bitDepth = picture.Sequence.SequenceHeader.ColorConfig.BitDepth;
+ this.rateMultiplier = Av1RateDistortion.GetKeyFrameRateMultiplier(this.quantization.QIndex[0], this.bitDepth);
+ this.codedAreaLuma = 0;
+ this.codedAreaChroma = 0;
+ }
+
+ ///
+ public void EncodeBlock(
+ Av1SymbolEncoder writer,
+ Av1MacroBlockD macroBlock,
+ Point blockOrigin,
+ ushort tileIndex,
+ ref Av1MacroBlockModeInfo modeInfo,
+ ref Av1EncoderBlockStruct block)
+ {
+ const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
+ const Av1TransformSize LumaTransformSize = Av1TransformSize.Size8x8;
+ int qIndex = this.quantization.QIndex[0];
+ modeInfo.Block = new Av1EncoderBlockModeInfo
+ {
+ BlockSize = BlockSize,
+ PartitionType = Av1PartitionType.None,
+ SegmentId = 0,
+ TransformSize = LumaTransformSize,
+ Mode = Av1PredictionMode.DC,
+ UvMode = Av1ChromaPredictionMode.DC
+ };
+
+ modeInfo.CdefStrength = 0;
+ block.HasChroma = !this.source.IsMonochrome;
+ block.QuantizationIndex = qIndex;
+ block.SegmentId = 0;
+
+ Span lumaCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.Y);
+ Span lumaTransformBlocks =
+ this.coefficientBuffer.GetTransformBlockSpan(this.superblock.Index, Av1Plane.Y);
+
+ int lumaTransformIndex = this.codedAreaLuma /
+ Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount;
+
+ ref Av1EncoderTransformBlockState lumaState = ref lumaTransformBlocks[lumaTransformIndex];
+ modeInfo.Block.Mode = this.SelectLumaMode(
+ writer,
+ macroBlock,
+ blockOrigin,
+ tileIndex,
+ lumaCoefficients[this.codedAreaLuma..],
+ ref lumaState);
+
+ this.codedAreaLuma += LumaTransformSize.GetSize2d();
+ bool skipTransform = lumaState.EndOfBlock == 0;
+ if (this.source.IsMonochrome)
+ {
+ modeInfo.Block.Skip = skipTransform;
+ return;
+ }
+
+ ObuColorConfig colorConfig = this.picture.Sequence.SequenceHeader.ColorConfig;
+ int subsamplingX = colorConfig.SubSamplingX ? 1 : 0;
+ int subsamplingY = colorConfig.SubSamplingY ? 1 : 0;
+ Point chromaOrigin = new(blockOrigin.X >> subsamplingX, blockOrigin.Y >> subsamplingY);
+ Av1TransformSize chromaTransformSize = BlockSize.GetMaxUvTransformSize(
+ colorConfig.SubSamplingX,
+ colorConfig.SubSamplingY);
+
+ Span blueCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.U);
+ Span redCoefficients = this.coefficientBuffer.GetPlaneSpan(this.superblock.Index, Av1Plane.V);
+ Span blueTransformBlocks =
+ this.coefficientBuffer.GetTransformBlockSpan(this.superblock.Index, Av1Plane.U);
+ Span redTransformBlocks =
+ this.coefficientBuffer.GetTransformBlockSpan(this.superblock.Index, Av1Plane.V);
+
+ int chromaTransformIndex = this.codedAreaChroma /
+ Av1EncoderCoefficientBuffer.TransformBlockUnitCoefficientCount;
+
+ ref Av1EncoderTransformBlockState blueState = ref blueTransformBlocks[chromaTransformIndex];
+ ref Av1EncoderTransformBlockState redState = ref redTransformBlocks[chromaTransformIndex];
+ EncodePlaneBlock(
+ this.source,
+ this.reconstruction,
+ this.blockWorkspace,
+ this.quantization,
+ this.bitDepth,
+ Av1Plane.U,
+ chromaOrigin,
+ chromaTransformSize,
+ blueCoefficients[this.codedAreaChroma..],
+ ref blueState);
+
+ EncodePlaneBlock(
+ this.source,
+ this.reconstruction,
+ this.blockWorkspace,
+ this.quantization,
+ this.bitDepth,
+ Av1Plane.V,
+ chromaOrigin,
+ chromaTransformSize,
+ redCoefficients[this.codedAreaChroma..],
+ ref redState);
+
+ // A block-level skip suppresses every coefficient symbol, so all coded planes must be empty.
+ modeInfo.Block.Skip = skipTransform && blueState.EndOfBlock == 0 && redState.EndOfBlock == 0;
+ this.codedAreaChroma += chromaTransformSize.GetSize2d();
+ }
+
+ private Av1PredictionMode SelectLumaMode(
+ Av1SymbolEncoder writer,
+ Av1MacroBlockD macroBlock,
+ Point blockOrigin,
+ ushort tileIndex,
+ Span retainedCoefficients,
+ ref Av1EncoderTransformBlockState retainedState)
+ {
+ const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
+ const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
+ const int SampleCount = 8 * 8;
+ Buffer2DRegion sourcePlane = this.source.GetPlane(Av1Plane.Y);
+ Buffer2DRegion reconstructionPlane = this.reconstruction.GetPlane(Av1Plane.Y);
+ bool hasLeft = macroBlock.IsLeftAvailable;
+ bool hasAbove = macroBlock.IsUpAvailable;
+ ReadOnlySpan above = hasAbove
+ ? reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1).Slice(blockOrigin.X, 8)
+ : [];
+
+ Span left = stackalloc TSample[8];
+ if (hasLeft)
+ {
+ for (int row = 0; row < left.Length; row++)
+ {
+ left[row] = reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + row)[blockOrigin.X - 1];
+ }
+ }
+
+ Av1TransformBlockContext blockContext = Av1TileWriter.GetTransformBlockContexts(
+ Av1ComponentType.Luminance,
+ this.picture.LuminanceDcSignLevelCoefficientNeighbors[tileIndex],
+ blockOrigin,
+ BlockSize,
+ TransformSize);
+
+ Span candidateReconstruction = stackalloc TSample[SampleCount];
+ Span candidateCoefficients = stackalloc int[SampleCount];
+ Av1EncoderTransformBlockState candidateState = default;
+ long bestCost = this.GetLumaCandidateCost(
+ writer,
+ macroBlock,
+ sourcePlane,
+ blockOrigin,
+ above,
+ left,
+ hasLeft,
+ hasAbove,
+ Av1PredictionMode.DC,
+ blockContext,
+ candidateReconstruction,
+ candidateCoefficients,
+ ref candidateState);
+
+ CopyCandidate(
+ candidateReconstruction,
+ candidateCoefficients,
+ reconstructionPlane,
+ blockOrigin,
+ retainedCoefficients,
+ candidateState,
+ ref retainedState);
+
+ Av1PredictionMode bestMode = Av1PredictionMode.DC;
+ if (hasAbove)
+ {
+ candidateState = default;
+ long verticalCost = this.GetLumaCandidateCost(
+ writer,
+ macroBlock,
+ sourcePlane,
+ blockOrigin,
+ above,
+ left,
+ hasLeft,
+ hasAbove,
+ Av1PredictionMode.Vertical,
+ blockContext,
+ candidateReconstruction,
+ candidateCoefficients,
+ ref candidateState);
+
+ if (verticalCost < bestCost)
+ {
+ CopyCandidate(
+ candidateReconstruction,
+ candidateCoefficients,
+ reconstructionPlane,
+ blockOrigin,
+ retainedCoefficients,
+ candidateState,
+ ref retainedState);
+
+ bestMode = Av1PredictionMode.Vertical;
+ }
+ }
+
+ return bestMode;
+ }
+
+ private long GetLumaCandidateCost(
+ Av1SymbolEncoder writer,
+ Av1MacroBlockD macroBlock,
+ Buffer2DRegion sourcePlane,
+ Point blockOrigin,
+ ReadOnlySpan above,
+ ReadOnlySpan left,
+ bool hasLeft,
+ bool hasAbove,
+ Av1PredictionMode mode,
+ Av1TransformBlockContext blockContext,
+ Span candidateReconstruction,
+ Span candidateCoefficients,
+ ref Av1EncoderTransformBlockState candidateState)
+ {
+ const Av1BlockSize BlockSize = Av1BlockSize.Block8x8;
+ const Av1TransformSize TransformSize = Av1TransformSize.Size8x8;
+ long distortion = TOperator.EncodeCandidate(
+ this.blockWorkspace,
+ sourcePlane,
+ blockOrigin,
+ candidateReconstruction,
+ above,
+ left,
+ hasLeft,
+ hasAbove,
+ mode,
+ candidateCoefficients,
+ TransformSize,
+ this.quantization.QIndex[0],
+ this.quantization.DeltaQDc[(int)Av1Plane.Y],
+ this.quantization.DeltaQAc[(int)Av1Plane.Y],
+ this.bitDepth,
+ ref candidateState);
+
+ int rate = Av1TileWriter.GetLumaModeCost(writer, macroBlock, BlockSize, mode);
+ rate += writer.GetCoefficientCost(
+ TransformSize,
+ Av1TransformType.DctDct,
+ mode,
+ candidateCoefficients,
+ Av1ComponentType.Luminance,
+ blockContext,
+ candidateState.EndOfBlock,
+ this.picture.Parent.FrameHeader.UseReducedTransformSet,
+ Av1FilterIntraMode.AllFilterIntraModes);
+
+ return Av1RateDistortion.GetCost(this.rateMultiplier, rate, distortion);
+ }
+
+ private static void CopyCandidate(
+ ReadOnlySpan candidateReconstruction,
+ ReadOnlySpan candidateCoefficients,
+ Buffer2DRegion reconstructionPlane,
+ Point blockOrigin,
+ Span retainedCoefficients,
+ Av1EncoderTransformBlockState candidateState,
+ ref Av1EncoderTransformBlockState retainedState)
+ {
+ const int Width = 8;
+ candidateCoefficients.CopyTo(retainedCoefficients);
+ for (int row = 0; row < Width; row++)
+ {
+ candidateReconstruction.Slice(row * Width, Width)
+ .CopyTo(reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + row).Slice(blockOrigin.X, Width));
+ }
+
+ retainedState = candidateState;
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs
index 2af6b814af..a836b01562 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.Operator.cs
@@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
+using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction;
using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
using SixLabors.ImageSharp.Formats.Heif.Av1.Transform;
using SixLabors.ImageSharp.Memory;
@@ -17,7 +18,7 @@ internal static partial class Av1IntraSuperblockEncoder
/// Defines type-specific block encoding without coupling traversal to sample storage width.
///
/// The native unsigned sample storage type.
- private interface IBlockEncodingOperator
+ internal interface IBlockEncodingOperator
where TSample : unmanaged
{
///
@@ -64,12 +65,50 @@ internal static partial class Av1IntraSuperblockEncoder
Av1Plane plane,
Av1BitDepth bitDepth,
ref Av1EncoderTransformBlockState state);
+
+ ///
+ /// Encodes one luma candidate into contiguous decision scratch.
+ ///
+ /// The reusable block workspace.
+ /// The coded source plane.
+ /// The transform-block origin in plane samples.
+ /// The contiguous candidate reconstruction.
+ /// The top reference samples.
+ /// The left reference samples.
+ /// Whether the left reference is available.
+ /// Whether the top reference is available.
+ /// The intra prediction mode.
+ /// The candidate entropy-coding coefficients.
+ /// The transform dimensions.
+ /// The effective segment quantizer index.
+ /// The plane DC quantizer adjustment.
+ /// The plane AC quantizer adjustment.
+ /// The coded sample bit depth.
+ /// The candidate transform state.
+ /// The normalized pixel-domain distortion in AV1 transform units.
+ public static abstract long EncodeCandidate(
+ Av1EncoderBlockWorkspace workspace,
+ Buffer2DRegion source,
+ Point blockOrigin,
+ Span reconstruction,
+ ReadOnlySpan above,
+ ReadOnlySpan left,
+ bool hasLeft,
+ bool hasAbove,
+ Av1PredictionMode mode,
+ Span quantizedCoefficients,
+ Av1TransformSize transformSize,
+ int qIndex,
+ int dcDeltaQ,
+ int acDeltaQ,
+ Av1BitDepth bitDepth,
+ ref Av1EncoderTransformBlockState state);
}
///
/// Encodes blocks stored as eight-bit samples.
///
- private readonly struct ByteOperator : IBlockEncodingOperator
+ internal readonly struct ByteOperator : IBlockEncodingOperator
{
///
public static Span GetLeftReference(Span residual, int length)
@@ -110,12 +149,49 @@ internal static partial class Av1IntraSuperblockEncoder
acDeltaQ,
plane,
ref state);
+
+ ///
+ public static long EncodeCandidate(
+ Av1EncoderBlockWorkspace workspace,
+ Buffer2DRegion source,
+ Point blockOrigin,
+ Span reconstruction,
+ ReadOnlySpan above,
+ ReadOnlySpan left,
+ bool hasLeft,
+ bool hasAbove,
+ Av1PredictionMode mode,
+ Span quantizedCoefficients,
+ Av1TransformSize transformSize,
+ int qIndex,
+ int dcDeltaQ,
+ int acDeltaQ,
+ Av1BitDepth bitDepth,
+ ref Av1EncoderTransformBlockState state)
+ => Av1TransformBlockEncoder.EncodeIntraLossyCandidate(
+ workspace,
+ source,
+ blockOrigin,
+ reconstruction,
+ above,
+ left,
+ hasLeft,
+ hasAbove,
+ mode,
+ quantizedCoefficients,
+ transformSize,
+ Av1TransformType.DctDct,
+ qIndex,
+ dcDeltaQ,
+ acDeltaQ,
+ Av1Plane.Y,
+ ref state);
}
///
/// Encodes blocks stored as high-bit-depth samples.
///
- private readonly struct UInt16Operator : IBlockEncodingOperator
+ internal readonly struct UInt16Operator : IBlockEncodingOperator
{
///
public static Span GetLeftReference(Span residual, int length)
@@ -157,5 +233,43 @@ internal static partial class Av1IntraSuperblockEncoder
plane,
bitDepth,
ref state);
+
+ ///
+ public static long EncodeCandidate(
+ Av1EncoderBlockWorkspace workspace,
+ Buffer2DRegion source,
+ Point blockOrigin,
+ Span reconstruction,
+ ReadOnlySpan above,
+ ReadOnlySpan left,
+ bool hasLeft,
+ bool hasAbove,
+ Av1PredictionMode mode,
+ Span quantizedCoefficients,
+ Av1TransformSize transformSize,
+ int qIndex,
+ int dcDeltaQ,
+ int acDeltaQ,
+ Av1BitDepth bitDepth,
+ ref Av1EncoderTransformBlockState state)
+ => Av1TransformBlockEncoder.EncodeIntraLossyCandidate(
+ workspace,
+ source,
+ blockOrigin,
+ reconstruction,
+ above,
+ left,
+ hasLeft,
+ hasAbove,
+ mode,
+ quantizedCoefficients,
+ transformSize,
+ Av1TransformType.DctDct,
+ qIndex,
+ dcDeltaQ,
+ acDeltaQ,
+ Av1Plane.Y,
+ bitDepth,
+ ref state);
}
}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs
index ea6b469243..02dade6f0a 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraSuperblockEncoder.cs
@@ -77,6 +77,59 @@ internal static partial class Av1IntraSuperblockEncoder
traversal.EncodePartitionTree(superblockOrigin, picture.Sequence.SequenceHeader.SuperblockSize);
}
+ private static void EncodePlaneBlock(
+ Av1EncoderFrame.PlanarView source,
+ Av1EncoderFrame.PlanarView reconstruction,
+ Av1EncoderBlockWorkspace blockWorkspace,
+ ObuQuantizationParameters quantization,
+ Av1BitDepth bitDepth,
+ Av1Plane plane,
+ Point blockOrigin,
+ Av1TransformSize transformSize,
+ Span coefficients,
+ ref Av1EncoderTransformBlockState state)
+ where TSample : unmanaged
+ where TOperator : struct, IBlockEncodingOperator
+ {
+ Buffer2DRegion sourcePlane = source.GetPlane(plane);
+ Buffer2DRegion reconstructionPlane = reconstruction.GetPlane(plane);
+ int width = transformSize.GetWidth();
+ int height = transformSize.GetHeight();
+ bool hasLeft = blockOrigin.X > 0;
+ bool hasAbove = blockOrigin.Y > 0;
+ ReadOnlySpan above = hasAbove
+ ? reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1).Slice(blockOrigin.X, width)
+ : [];
+
+ Span left = TOperator.GetLeftReference(blockWorkspace.Residual, height);
+ if (hasLeft)
+ {
+ for (int row = 0; row < height; row++)
+ {
+ left[row] = reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + row)[blockOrigin.X - 1];
+ }
+ }
+
+ // Prediction consumes every gathered reference before residual construction reuses the same workspace bytes.
+ TOperator.Encode(
+ blockWorkspace,
+ sourcePlane,
+ reconstructionPlane,
+ blockOrigin,
+ above,
+ left,
+ hasLeft,
+ hasAbove,
+ coefficients,
+ transformSize,
+ quantization.QIndex[0],
+ quantization.DeltaQDc[(int)plane],
+ quantization.DeltaQAc[(int)plane],
+ plane,
+ bitDepth,
+ ref state);
+ }
+
///
/// Retains the stack-only state shared by recursive partition and final-block traversal.
///
@@ -237,44 +290,16 @@ internal static partial class Av1IntraSuperblockEncoder
Av1TransformSize transformSize,
Span coefficients,
ref Av1EncoderTransformBlockState state)
- {
- Buffer2DRegion sourcePlane = this.source.GetPlane(plane);
- Buffer2DRegion reconstructionPlane = this.reconstruction.GetPlane(plane);
- int width = transformSize.GetWidth();
- int height = transformSize.GetHeight();
- bool hasLeft = blockOrigin.X > 0;
- bool hasAbove = blockOrigin.Y > 0;
- ReadOnlySpan above = hasAbove
- ? reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y - 1).Slice(blockOrigin.X, width)
- : [];
-
- Span left = TOperator.GetLeftReference(this.blockWorkspace.Residual, height);
- if (hasLeft)
- {
- for (int row = 0; row < height; row++)
- {
- left[row] = reconstructionPlane.DangerousGetRowSpan(blockOrigin.Y + row)[blockOrigin.X - 1];
- }
- }
-
- // Prediction consumes every gathered reference before residual construction reuses the same workspace bytes.
- TOperator.Encode(
+ => Av1IntraSuperblockEncoder.EncodePlaneBlock(
+ this.source,
+ this.reconstruction,
this.blockWorkspace,
- sourcePlane,
- reconstructionPlane,
+ this.quantization,
+ this.bitDepth,
+ plane,
blockOrigin,
- above,
- left,
- hasLeft,
- hasAbove,
- coefficients,
transformSize,
- this.quantization.QIndex[0],
- this.quantization.DeltaQDc[(int)plane],
- this.quantization.DeltaQAc[(int)plane],
- plane,
- this.bitDepth,
+ coefficients,
ref state);
- }
}
}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.Operator.cs
deleted file mode 100644
index 2a5103f642..0000000000
--- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.Operator.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (c) Six Labors.
-// Licensed under the Six Labors Split License.
-
-using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
-
-namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline;
-
-///
-/// Defines the sample-storage operations used by single-tile intra encoding.
-///
-internal sealed partial class Av1IntraTileWriter
-{
- ///
- /// Defines type-specific superblock encoding without coupling tile traversal to sample storage width.
- ///
- /// The native unsigned sample storage type.
- private interface ITileEncodingOperator
- where TSample : unmanaged
- {
- ///
- /// Encodes and reconstructs one superblock.
- ///
- /// The coded source frame.
- /// The reconstructed frame updated by the block transforms.
- /// The frame coding and mode-information state.
- /// The reusable partition and final-block decisions.
- /// The frame-owned quantized coefficient and transform state.
- /// The reusable block arithmetic workspace.
- public static abstract void EncodeSuperblock(
- Av1EncoderFrame source,
- Av1EncoderFrame reconstruction,
- Av1PictureControlSet picture,
- Av1Superblock superblock,
- Av1EncoderCoefficientBuffer coefficientBuffer,
- Av1EncoderBlockWorkspace blockWorkspace);
- }
-
- ///
- /// Encodes tiles stored as eight-bit samples.
- ///
- private readonly struct ByteOperator : ITileEncodingOperator
- {
- ///
- public static void EncodeSuperblock(
- Av1EncoderFrame source,
- Av1EncoderFrame reconstruction,
- Av1PictureControlSet picture,
- Av1Superblock superblock,
- Av1EncoderCoefficientBuffer coefficientBuffer,
- Av1EncoderBlockWorkspace blockWorkspace)
- => Av1IntraSuperblockEncoder.Encode(
- source,
- reconstruction,
- picture,
- superblock,
- coefficientBuffer,
- blockWorkspace);
- }
-
- ///
- /// Encodes tiles stored as high-bit-depth samples.
- ///
- private readonly struct UInt16Operator : ITileEncodingOperator
- {
- ///
- public static void EncodeSuperblock(
- Av1EncoderFrame source,
- Av1EncoderFrame reconstruction,
- Av1PictureControlSet picture,
- Av1Superblock superblock,
- Av1EncoderCoefficientBuffer coefficientBuffer,
- Av1EncoderBlockWorkspace blockWorkspace)
- => Av1IntraSuperblockEncoder.Encode(
- source,
- reconstruction,
- picture,
- superblock,
- coefficientBuffer,
- blockWorkspace);
- }
-}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs
index 098b974dde..b774b45acc 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1IntraTileWriter.cs
@@ -37,7 +37,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
Av1EncoderBlockWorkspace blockWorkspace,
int initialSize)
{
- this.tileData = Encode(
+ this.tileData = Encode(
configuration,
source,
reconstruction,
@@ -70,7 +70,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
Av1EncoderBlockWorkspace blockWorkspace,
int initialSize)
{
- this.tileData = Encode(
+ this.tileData = Encode(
configuration,
source,
reconstruction,
@@ -109,7 +109,7 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
int initialSize,
out int tileDataLength)
where TSample : unmanaged
- where TOperator : struct, ITileEncodingOperator
+ where TOperator : struct, Av1IntraSuperblockEncoder.IBlockEncodingOperator
{
ObuFrameHeader frameHeader = picture.Parent.FrameHeader;
ObuSequenceHeader sequenceHeader = picture.Sequence.SequenceHeader;
@@ -151,9 +151,12 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
modeInfoColumn << Av1Constants.ModeInfoSizeLog2,
modeInfoRow << Av1Constants.ModeInfoSizeLog2);
- // Analyze immediately before entropy coding so the reusable decision workspace and reconstructed
- // neighbors remain synchronized without a second superblock-sized decision allocation.
- TOperator.EncodeSuperblock(
+ Av1IntraSuperblockEncoder.Prepare(
+ picture,
+ superblock,
+ entropyContext.SuperblockOrigin);
+
+ Av1IntraSuperblockEncoder.ModeDecision blockEncoder = new(
source,
reconstruction,
picture,
@@ -167,7 +170,8 @@ internal sealed partial class Av1IntraTileWriter : IAv1TileWriter, IDisposable
writer,
superblock,
coefficientBuffer,
- TileIndex);
+ TileIndex,
+ ref blockEncoder);
}
}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1ResidualBuilder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1ResidualBuilder.cs
index c22343add1..24e838373f 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1ResidualBuilder.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1ResidualBuilder.cs
@@ -56,6 +56,70 @@ internal static partial class Av1ResidualBuilder
int height)
=> Subtract(source, sourceStride, prediction, predictionStride, residual, residualStride, width, height);
+ ///
+ /// Sums the squares of a contiguous signed residual block.
+ ///
+ /// The residual samples.
+ /// The exact sum of squared sample differences.
+ public static long SumSquares(ReadOnlySpan residual)
+ {
+ ref short residualBase = ref MemoryMarshal.GetReference(residual);
+ long sum = 0;
+ int offset = 0;
+
+ // Each short lane widens before multiplication, preserving the full 12-bit residual square.
+ // The accumulated scalar is 64-bit because a complete encoder block can exceed 32-bit range.
+ if (Vector512.IsHardwareAccelerated)
+ {
+ nuint vectorCount = residual.Vector512Count();
+
+ for (; vectorCount > 0; vectorCount--, offset += Vector512.Count)
+ {
+ Vector512 values = Unsafe.As>(ref Unsafe.Add(ref residualBase, offset));
+ Vector512 lower = Vector512.WidenLower(values);
+ Vector512 upper = Vector512.WidenUpper(values);
+ sum += Vector512.Sum(lower * lower);
+ sum += Vector512.Sum(upper * upper);
+ }
+ }
+
+ if (Vector256.IsHardwareAccelerated)
+ {
+ nuint vectorCount = residual[offset..].Vector256Count();
+
+ for (; vectorCount > 0; vectorCount--, offset += Vector256.Count)
+ {
+ Vector256 values = Unsafe.As>(ref Unsafe.Add(ref residualBase, offset));
+ Vector256 lower = Vector256.WidenLower(values);
+ Vector256 upper = Vector256.WidenUpper(values);
+ sum += Vector256.Sum(lower * lower);
+ sum += Vector256.Sum(upper * upper);
+ }
+ }
+
+ if (Vector128.IsHardwareAccelerated)
+ {
+ nuint vectorCount = residual[offset..].Vector128Count();
+
+ for (; vectorCount > 0; vectorCount--, offset += Vector128.Count)
+ {
+ Vector128 values = Unsafe.As>(ref Unsafe.Add(ref residualBase, offset));
+ Vector128 lower = Vector128.WidenLower(values);
+ Vector128 upper = Vector128.WidenUpper(values);
+ sum += Vector128.Sum(lower * lower);
+ sum += Vector128.Sum(upper * upper);
+ }
+ }
+
+ for (; offset < residual.Length; offset++)
+ {
+ int value = Unsafe.Add(ref residualBase, offset);
+ sum += value * value;
+ }
+
+ return sum;
+ }
+
private static void Subtract(
ReadOnlySpan source,
int sourceStride,
diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs
index e0341b2bf4..136340073e 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1TransformBlockEncoder.cs
@@ -55,7 +55,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan sourceSamples = GetPlaneSpan(source, blockOrigin);
Span reconstructionSamples = GetPlaneSpan(reconstruction, blockOrigin);
- EncodeIntraDcLossyContiguous(
+ EncodeIntraLossyContiguous(
workspace,
sourceSamples,
source.Stride,
@@ -65,6 +65,7 @@ internal static class Av1TransformBlockEncoder
left,
hasLeft,
hasAbove,
+ Av1PredictionMode.DC,
quantizedCoefficients,
transformSize,
transformType,
@@ -75,6 +76,83 @@ internal static class Av1TransformBlockEncoder
ref state);
}
+ ///
+ /// Encodes one eight-bit intra candidate into contiguous decision scratch.
+ ///
+ /// The reusable residual, coefficient, and transform storage.
+ /// The coded source plane.
+ /// The block origin in plane samples.
+ /// The contiguous candidate reconstruction.
+ /// The contiguous top reference samples.
+ /// The contiguous left reference samples.
+ /// Whether the left reference is available.
+ /// Whether the top reference is available.
+ /// The intra prediction mode.
+ /// The candidate entropy-coding coefficients.
+ /// The selected transform dimensions.
+ /// The selected compound transform type.
+ /// The segment quantizer index.
+ /// The plane DC quantizer adjustment.
+ /// The plane AC quantizer adjustment.
+ /// The component plane containing the block.
+ /// The candidate transform type and end-of-block syntax.
+ /// The normalized pixel-domain distortion in AV1 transform units.
+ public static long EncodeIntraLossyCandidate(
+ Av1EncoderBlockWorkspace workspace,
+ Buffer2DRegion source,
+ Point blockOrigin,
+ Span reconstruction,
+ ReadOnlySpan above,
+ ReadOnlySpan left,
+ bool hasLeft,
+ bool hasAbove,
+ Av1PredictionMode mode,
+ Span quantizedCoefficients,
+ Av1TransformSize transformSize,
+ Av1TransformType transformType,
+ int qIndex,
+ int dcDeltaQ,
+ int acDeltaQ,
+ Av1Plane plane,
+ ref Av1EncoderTransformBlockState state)
+ {
+ int width = transformSize.GetWidth();
+ int height = transformSize.GetHeight();
+ ReadOnlySpan sourceSamples = GetPlaneSpan(source, blockOrigin);
+
+ EncodeIntraLossyContiguous(
+ workspace,
+ sourceSamples,
+ source.Stride,
+ reconstruction,
+ width,
+ above,
+ left,
+ hasLeft,
+ hasAbove,
+ mode,
+ quantizedCoefficients,
+ transformSize,
+ transformType,
+ qIndex,
+ dcDeltaQ,
+ acDeltaQ,
+ plane,
+ ref state);
+
+ Av1ResidualBuilder.Subtract(
+ sourceSamples,
+ source.Stride,
+ reconstruction,
+ width,
+ workspace.Residual,
+ width,
+ width,
+ height);
+
+ return Av1ResidualBuilder.SumSquares(workspace.Residual[..transformSize.GetSize2d()]) << 4;
+ }
+
///
/// Encodes and reconstructs one high-bit-depth lossy DC intra block in contiguous encoder planes.
///
@@ -117,7 +195,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan sourceSamples = GetPlaneSpan(source, blockOrigin);
Span reconstructionSamples = GetPlaneSpan(reconstruction, blockOrigin);
- EncodeIntraDcLossyContiguous(
+ EncodeIntraLossyContiguous(
workspace,
sourceSamples,
source.Stride,
@@ -127,6 +205,7 @@ internal static class Av1TransformBlockEncoder
left,
hasLeft,
hasAbove,
+ Av1PredictionMode.DC,
quantizedCoefficients,
transformSize,
transformType,
@@ -139,7 +218,93 @@ internal static class Av1TransformBlockEncoder
}
///
- /// Encodes and reconstructs one eight-bit lossy DC intra block.
+ /// Encodes one high-bit-depth intra candidate into contiguous decision scratch.
+ ///
+ /// The reusable residual, coefficient, and transform storage.
+ /// The coded source plane.
+ /// The block origin in plane samples.
+ /// The contiguous candidate reconstruction.
+ /// The contiguous top reference samples.
+ /// The contiguous left reference samples.
+ /// Whether the left reference is available.
+ /// Whether the top reference is available.
+ /// The intra prediction mode.
+ /// The candidate entropy-coding coefficients.
+ /// The selected transform dimensions.
+ /// The selected compound transform type.
+ /// The segment quantizer index.
+ /// The plane DC quantizer adjustment.
+ /// The plane AC quantizer adjustment.
+ /// The component plane containing the block.
+ /// The coded sample bit depth.
+ /// The candidate transform type and end-of-block syntax.
+ /// The normalized pixel-domain distortion in AV1 transform units.
+ public static long EncodeIntraLossyCandidate(
+ Av1EncoderBlockWorkspace workspace,
+ Buffer2DRegion source,
+ Point blockOrigin,
+ Span reconstruction,
+ ReadOnlySpan above,
+ ReadOnlySpan left,
+ bool hasLeft,
+ bool hasAbove,
+ Av1PredictionMode mode,
+ Span quantizedCoefficients,
+ Av1TransformSize transformSize,
+ Av1TransformType transformType,
+ int qIndex,
+ int dcDeltaQ,
+ int acDeltaQ,
+ Av1Plane plane,
+ Av1BitDepth bitDepth,
+ ref Av1EncoderTransformBlockState state)
+ {
+ int width = transformSize.GetWidth();
+ int height = transformSize.GetHeight();
+ ReadOnlySpan sourceSamples = GetPlaneSpan(source, blockOrigin);
+
+ EncodeIntraLossyContiguous(
+ workspace,
+ sourceSamples,
+ source.Stride,
+ reconstruction,
+ width,
+ above,
+ left,
+ hasLeft,
+ hasAbove,
+ mode,
+ quantizedCoefficients,
+ transformSize,
+ transformType,
+ qIndex,
+ dcDeltaQ,
+ acDeltaQ,
+ plane,
+ bitDepth,
+ ref state);
+
+ Av1ResidualBuilder.Subtract(
+ sourceSamples,
+ source.Stride,
+ reconstruction,
+ width,
+ workspace.Residual,
+ width,
+ width,
+ height);
+
+ long distortion = Av1ResidualBuilder.SumSquares(workspace.Residual[..transformSize.GetSize2d()]);
+ int shift = (bitDepth.GetBitCount() - 8) * 2;
+ long normalizedDistortion = shift == 0
+ ? distortion
+ : (distortion + (1L << (shift - 1))) >> shift;
+
+ return normalizedDistortion << 4;
+ }
+
+ ///
+ /// Encodes and reconstructs one eight-bit lossy intra block.
///
/// The reusable residual, coefficient, and transform storage.
/// The source samples.
@@ -150,6 +315,7 @@ internal static class Av1TransformBlockEncoder
/// The contiguous left reference samples.
/// Whether the left reference is available.
/// Whether the top reference is available.
+ /// The intra prediction mode.
/// The retained entropy-coding coefficients.
/// The selected transform dimensions.
/// The selected compound transform type.
@@ -158,7 +324,7 @@ internal static class Av1TransformBlockEncoder
/// The plane AC quantizer adjustment.
/// The component plane containing the block.
/// The retained transform type and end-of-block syntax.
- private static void EncodeIntraDcLossyContiguous(
+ private static void EncodeIntraLossyContiguous(
Av1EncoderBlockWorkspace workspace,
ReadOnlySpan source,
int sourceStride,
@@ -168,6 +334,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan left,
bool hasLeft,
bool hasAbove,
+ Av1PredictionMode mode,
Span quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
@@ -181,7 +348,16 @@ internal static class Av1TransformBlockEncoder
int height = transformSize.GetHeight();
// Prediction and subtraction stay in their SIMD-first operators while this method owns the required block-stage ordering.
- Av1DcIntraPredictor.Predict(hasLeft, hasAbove, reconstruction, reconstructionStride, above, left, width, height);
+ if (mode == Av1PredictionMode.DC)
+ {
+ Av1DcIntraPredictor.Predict(hasLeft, hasAbove, reconstruction, reconstructionStride, above, left, width, height);
+ }
+ else
+ {
+ Av1NonDirectionalIntraPredictorBase.GetPredictor(mode)
+ .Predict(reconstruction, reconstructionStride, above, left, width, height);
+ }
+
Av1ResidualBuilder.Subtract(source, sourceStride, reconstruction, reconstructionStride, workspace.Residual, width, width, height);
EncodeLossy(
@@ -212,7 +388,7 @@ internal static class Av1TransformBlockEncoder
}
///
- /// Encodes and reconstructs one high-bit-depth lossy DC intra block.
+ /// Encodes and reconstructs one high-bit-depth lossy intra block.
///
/// The reusable residual, coefficient, and transform storage.
/// The source samples.
@@ -223,6 +399,7 @@ internal static class Av1TransformBlockEncoder
/// The contiguous left reference samples.
/// Whether the left reference is available.
/// Whether the top reference is available.
+ /// The intra prediction mode.
/// The retained entropy-coding coefficients.
/// The selected transform dimensions.
/// The selected compound transform type.
@@ -232,7 +409,7 @@ internal static class Av1TransformBlockEncoder
/// The component plane containing the block.
/// The coded sample bit depth.
/// The retained transform type and end-of-block syntax.
- private static void EncodeIntraDcLossyContiguous(
+ private static void EncodeIntraLossyContiguous(
Av1EncoderBlockWorkspace workspace,
ReadOnlySpan source,
int sourceStride,
@@ -242,6 +419,7 @@ internal static class Av1TransformBlockEncoder
ReadOnlySpan left,
bool hasLeft,
bool hasAbove,
+ Av1PredictionMode mode,
Span quantizedCoefficients,
Av1TransformSize transformSize,
Av1TransformType transformType,
@@ -259,16 +437,24 @@ internal static class Av1TransformBlockEncoder
Span signedReconstruction = MemoryMarshal.Cast(reconstruction);
ReadOnlySpan signedAbove = MemoryMarshal.Cast(above);
ReadOnlySpan signedLeft = MemoryMarshal.Cast(left);
- Av1DcIntraPredictor.Predict(
- hasLeft,
- hasAbove,
- signedReconstruction,
- reconstructionStride,
- signedAbove,
- signedLeft,
- width,
- height,
- bitDepth.GetBitCount());
+ if (mode == Av1PredictionMode.DC)
+ {
+ Av1DcIntraPredictor.Predict(
+ hasLeft,
+ hasAbove,
+ signedReconstruction,
+ reconstructionStride,
+ signedAbove,
+ signedLeft,
+ width,
+ height,
+ bitDepth.GetBitCount());
+ }
+ else
+ {
+ Av1NonDirectionalIntraPredictorBase.GetPredictor(mode)
+ .Predict(signedReconstruction, reconstructionStride, signedAbove, signedLeft, width, height);
+ }
Av1ResidualBuilder.Subtract(
source,
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs
new file mode 100644
index 0000000000..b3958daf57
--- /dev/null
+++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.BlockEncoding.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy;
+
+namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling;
+
+///
+/// Defines the final-block decision contract used by interleaved tile encoding.
+///
+internal partial class Av1TileWriter
+{
+ ///
+ /// Supplies a final block decision immediately before its symbols are written.
+ ///
+ internal interface IBlockEncodingHandler
+ {
+ ///
+ /// Encodes one final block against the current reconstructed neighbors and live tile probabilities.
+ ///
+ /// The live tile symbol encoder.
+ /// The current block's mapped neighbor state.
+ /// The absolute luma-sample origin.
+ /// The zero-based tile index.
+ /// The mode information to publish.
+ /// The encoder block state to publish.
+ void EncodeBlock(
+ Av1SymbolEncoder writer,
+ Av1MacroBlockD macroBlock,
+ Point blockOrigin,
+ ushort tileIndex,
+ ref Av1MacroBlockModeInfo modeInfo,
+ ref Av1EncoderBlockStruct block);
+ }
+
+ private readonly struct PrecomputedBlockEncodingHandler : IBlockEncodingHandler
+ {
+ public void EncodeBlock(
+ Av1SymbolEncoder writer,
+ Av1MacroBlockD macroBlock,
+ Point blockOrigin,
+ ushort tileIndex,
+ ref Av1MacroBlockModeInfo modeInfo,
+ ref Av1EncoderBlockStruct block)
+ {
+ }
+ }
+}
diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
index f12b3e539b..db3038d79e 100644
--- a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
+++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs
@@ -67,6 +67,38 @@ internal partial class Av1TileWriter
Av1Superblock superblock,
Av1EncoderCoefficientBuffer coefficientBuffer,
ushort tileIndex)
+ {
+ PrecomputedBlockEncodingHandler blockEncoder = default;
+ WriteSuperblock(
+ pcs,
+ ec_ctx,
+ writer,
+ superblock,
+ coefficientBuffer,
+ tileIndex,
+ ref blockEncoder);
+ }
+
+ ///
+ /// Writes a partition tree while producing each final block against the immediately preceding tile state.
+ ///
+ /// The value type that produces final-block decisions.
+ /// The picture coding state.
+ /// The entropy-coding position state for the superblock.
+ /// The tile symbol encoder.
+ /// The encoder decisions for the superblock.
+ /// The transformed coefficients retained by raster-ordered superblock.
+ /// The zero-based tile index.
+ /// The handler invoked for each final block.
+ public static void WriteSuperblock(
+ Av1PictureControlSet pcs,
+ Av1EntropyCodingContext ec_ctx,
+ Av1SymbolEncoder writer,
+ Av1Superblock superblock,
+ Av1EncoderCoefficientBuffer coefficientBuffer,
+ ushort tileIndex,
+ ref TBlockEncoder blockEncoder)
+ where TBlockEncoder : struct, IBlockEncodingHandler
{
ec_ctx.CodedAreaSuperblock = 0;
ec_ctx.CodedAreaSuperblockUv = 0;
@@ -86,13 +118,14 @@ internal partial class Av1TileWriter
pcs.Sequence.SequenceHeader.SuperblockSize,
ec_ctx.SuperblockOrigin,
ref partitionIndex,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
}
///
/// Writes one selected partition node and recursively visits its split children.
///
- private static void WritePartitionTree(
+ private static void WritePartitionTree(
Av1PictureControlSet pcs,
Av1EntropyCodingContext entropyCodingContext,
Av1SymbolEncoder writer,
@@ -102,7 +135,9 @@ internal partial class Av1TileWriter
Av1BlockSize blockSize,
Point blockOrigin,
ref int partitionIndex,
- ref int finalBlockIndex)
+ ref int finalBlockIndex,
+ ref TBlockEncoder blockEncoder)
+ where TBlockEncoder : struct, IBlockEncodingHandler
{
Av1EncoderCommon common = pcs.Parent.Common;
int modeInfoRow = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2;
@@ -136,7 +171,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
break;
case Av1PartitionType.Horizontal:
@@ -148,7 +184,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
if (modeInfoRow + (blockSize.Get4x4HighCount() >> 1) < common.ModeInfoRowCount)
{
@@ -160,7 +197,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(0, halfBlockSize),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
}
break;
@@ -173,7 +211,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
if (modeInfoColumn + (blockSize.Get4x4WideCount() >> 1) < common.ModeInfoColumnCount)
{
@@ -185,7 +224,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(halfBlockSize, 0),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
}
break;
@@ -200,7 +240,8 @@ internal partial class Av1TileWriter
subSize,
blockOrigin,
ref partitionIndex,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WritePartitionTree(
pcs,
entropyCodingContext,
@@ -211,7 +252,8 @@ internal partial class Av1TileWriter
subSize,
blockOrigin + new Size(halfBlockSize, 0),
ref partitionIndex,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WritePartitionTree(
pcs,
entropyCodingContext,
@@ -222,7 +264,8 @@ internal partial class Av1TileWriter
subSize,
blockOrigin + new Size(0, halfBlockSize),
ref partitionIndex,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WritePartitionTree(
pcs,
entropyCodingContext,
@@ -233,7 +276,8 @@ internal partial class Av1TileWriter
subSize,
blockOrigin + new Size(halfBlockSize, halfBlockSize),
ref partitionIndex,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
break;
case Av1PartitionType.HorizontalA:
@@ -245,7 +289,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WriteFinalBlock(
pcs,
entropyCodingContext,
@@ -254,7 +299,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(halfBlockSize, 0),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WriteFinalBlock(
pcs,
entropyCodingContext,
@@ -263,7 +309,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(0, halfBlockSize),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
break;
case Av1PartitionType.HorizontalB:
@@ -275,7 +322,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WriteFinalBlock(
pcs,
entropyCodingContext,
@@ -284,7 +332,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(0, halfBlockSize),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WriteFinalBlock(
pcs,
entropyCodingContext,
@@ -293,7 +342,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(halfBlockSize, halfBlockSize),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
break;
case Av1PartitionType.VerticalA:
@@ -305,7 +355,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WriteFinalBlock(
pcs,
entropyCodingContext,
@@ -314,7 +365,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(0, halfBlockSize),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WriteFinalBlock(
pcs,
entropyCodingContext,
@@ -323,7 +375,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(halfBlockSize, 0),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
break;
case Av1PartitionType.VerticalB:
@@ -335,7 +388,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WriteFinalBlock(
pcs,
entropyCodingContext,
@@ -344,7 +398,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(halfBlockSize, 0),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
WriteFinalBlock(
pcs,
entropyCodingContext,
@@ -353,7 +408,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
blockOrigin + new Size(halfBlockSize, halfBlockSize),
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
break;
case Av1PartitionType.Horizontal4:
@@ -374,7 +430,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
childOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
}
break;
@@ -396,7 +453,8 @@ internal partial class Av1TileWriter
coefficientBuffer,
tileIndex,
childOrigin,
- ref finalBlockIndex);
+ ref finalBlockIndex,
+ ref blockEncoder);
}
break;
@@ -413,7 +471,7 @@ internal partial class Av1TileWriter
///
/// Writes the next final block selected by partition traversal.
///
- private static void WriteFinalBlock(
+ private static void WriteFinalBlock(
Av1PictureControlSet pcs,
Av1EntropyCodingContext entropyCodingContext,
Av1SymbolEncoder writer,
@@ -421,7 +479,9 @@ internal partial class Av1TileWriter
Av1EncoderCoefficientBuffer coefficientBuffer,
ushort tileIndex,
Point blockOrigin,
- ref int finalBlockIndex)
+ ref int finalBlockIndex,
+ ref TBlockEncoder blockEncoder)
+ where TBlockEncoder : struct, IBlockEncodingHandler
{
ref Av1EncoderBlockStruct block = ref superblock.FinalBlocks[finalBlockIndex++];
WriteModesBlock(
@@ -432,7 +492,8 @@ internal partial class Av1TileWriter
ref block,
tileIndex,
blockOrigin,
- coefficientBuffer);
+ coefficientBuffer,
+ ref blockEncoder);
}
///
@@ -614,7 +675,8 @@ internal partial class Av1TileWriter
/// The zero-based tile index.
/// The absolute luma-sample origin of the block.
/// The transformed coefficients retained by raster-ordered superblock.
- private static void WriteModesBlock(
+ /// The final-block decision producer.
+ private static void WriteModesBlock(
Av1PictureControlSet pcs,
Av1EntropyCodingContext entropyCodingContext,
Av1SymbolEncoder writer,
@@ -622,7 +684,9 @@ internal partial class Av1TileWriter
ref Av1EncoderBlockStruct blk_ptr,
ushort tile_idx,
Point blockOrigin,
- Av1EncoderCoefficientBuffer coefficientBuffer)
+ Av1EncoderCoefficientBuffer coefficientBuffer,
+ ref TBlockEncoder blockEncoder)
+ where TBlockEncoder : struct, IBlockEncodingHandler
{
Av1SequenceControlSet scs = pcs.Sequence;
ObuFrameHeader frm_hdr = pcs.Parent.FrameHeader;
@@ -635,7 +699,6 @@ internal partial class Av1TileWriter
Point modeInfoPosition = new(mi_col, mi_row);
ref Av1MacroBlockModeInfo macroBlockModeInfo = ref pcs.GetMacroBlockModeInfo(modeInfoPosition);
Av1BlockSize blockSize = macroBlockModeInfo.Block.BlockSize;
- bool skipWritingCoefficients = macroBlockModeInfo.Block.Skip;
pcs.MapModeInfoBlock(modeInfoPosition, blockSize);
Av1MacroBlockD macroBlock = entropyCodingContext.MacroBlock;
@@ -651,6 +714,17 @@ internal partial class Av1TileWriter
pcs.Parent.Common.ModeInfoRowCount,
pcs.Parent.Common.ModeInfoColumnCount);
+ // Producing the decision here exposes exactly the reconstructed neighbors, coefficient contexts,
+ // and adaptive probabilities that the following syntax writes.
+ blockEncoder.EncodeBlock(
+ writer,
+ macroBlock,
+ blockOrigin,
+ tile_idx,
+ ref macroBlockModeInfo,
+ ref blk_ptr);
+ bool skipWritingCoefficients = macroBlockModeInfo.Block.Skip;
+
// This encoder path currently writes intra frames only, so every block follows the key-frame mode syntax.
{
if (pcs.Parent.FrameHeader.SegmentationParameters.Enabled && pcs.Parent.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip)
@@ -909,6 +983,30 @@ internal partial class Av1TileWriter
left_ctx = IntraModeContextLookup[(int)intraLumaLeftMode];
}
+ ///
+ /// Gets the key-frame luma mode rate against the current neighboring modes and tile probabilities.
+ ///
+ /// The live tile symbol encoder.
+ /// The current block's mapped neighbor state.
+ /// The selected block size.
+ /// The candidate luma mode.
+ /// The luma mode and zero-angle rate in 1/512-bit units.
+ public static int GetLumaModeCost(
+ Av1SymbolEncoder writer,
+ Av1MacroBlockD macroBlock,
+ Av1BlockSize blockSize,
+ Av1PredictionMode mode)
+ {
+ GetYModeContext(macroBlock, out byte topContext, out byte leftContext);
+ int cost = writer.GetLumaModeCost(mode, topContext, leftContext);
+ if (blockSize >= Av1BlockSize.Block8x8 && mode.IsDirectional())
+ {
+ cost += writer.GetAngleDeltaCost(Av1Constants.MaxAngleDelta, mode);
+ }
+
+ return cost;
+ }
+
///
/// Writes the key-frame luma prediction mode and any directional angle adjustment.
///
@@ -1731,8 +1829,8 @@ internal partial class Av1TileWriter
/// The skip value to write.
public static void EncodeSkipCoefficients(Av1SymbolEncoder writer, Av1MacroBlockD macroBlock, bool skip)
{
- int above_skip = macroBlock.IsUpAvailable && macroBlock.GetRelativeModeInfo(-macroBlock.ModeInfoStride).Block.Skip ? 1 : 0;
- int left_skip = macroBlock.IsLeftAvailable && macroBlock.GetRelativeModeInfo(-1).Block.Skip ? 1 : 0;
- writer.WriteSkip(skip, above_skip + left_skip);
+ int aboveSkip = macroBlock.IsUpAvailable && macroBlock.GetRelativeModeInfo(-macroBlock.ModeInfoStride).Block.Skip ? 1 : 0;
+ int leftSkip = macroBlock.IsLeftAvailable && macroBlock.GetRelativeModeInfo(-1).Block.Skip ? 1 : 0;
+ writer.WriteSkip(skip, aboveSkip + leftSkip);
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
index cbde2bf2d3..dce366877b 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs
@@ -532,6 +532,19 @@ public class Av1EntropyTests
long expected)
=> Assert.Equal(expected, Av1RateDistortion.GetCost(rateMultiplier, rate, distortion));
+ [Theory]
+ [InlineData(0, 0, 52)]
+ [InlineData(0, 1, 3)]
+ [InlineData(0, 2, 1)]
+ [InlineData(255, 0, 9_467_088)]
+ [InlineData(255, 1, 20_228_608)]
+ [InlineData(255, 2, 63_215_524)]
+ public void KeyFrameRateMultiplierMatchesCurrentLibaom(
+ int qIndex,
+ int bitDepth,
+ int expected)
+ => Assert.Equal(expected, Av1RateDistortion.GetKeyFrameRateMultiplier(qIndex, (Av1BitDepth)bitDepth));
+
[Fact]
public void SymbolWriterMatchesCurrentLibaomCarryRegression()
{
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs
index e0f7052748..8a0c6b4ff1 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraSuperblockEncoderTests.cs
@@ -303,10 +303,18 @@ public class Av1IntraSuperblockEncoderTests
}
using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true);
- Av1PictureControlSet picture = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, qIndex: 37);
+ Av1PictureControlSet pictureTemplate = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, qIndex: 37);
+ using Av1EncoderPictureBuffer pictureBuffer = new(
+ Configuration.Default,
+ pictureTemplate.Sequence.SequenceHeader,
+ pictureTemplate.Parent.FrameHeader,
+ Width,
+ Height);
+
+ Av1PictureControlSet picture = pictureBuffer.Picture;
using Av1EncoderCoefficientBuffer coefficients = new(
Configuration.Default,
- picture.Sequence.SequenceHeader,
+ pictureTemplate.Sequence.SequenceHeader,
Width,
Height);
@@ -335,6 +343,66 @@ public class Av1IntraSuperblockEncoderTests
Assert.Equal((ushort)0, coefficients.GetTransformBlockSpan(0, Av1Plane.U)[0].EndOfBlock);
Assert.Equal((ushort)0, coefficients.GetTransformBlockSpan(0, Av1Plane.V)[0].EndOfBlock);
}
+
+ Av1TileWriter.Av1EntropyCodingContext entropyContext = new()
+ {
+ MacroBlock = new Av1MacroBlockD { Tile = superblock.TileInfo },
+ MacroBlockModeInfo = picture.GetMacroBlockModeInfo(default),
+ SuperblockOrigin = default
+ };
+
+ using Av1SymbolEncoder writer = new(Configuration.Default, 256, 37);
+ Av1TileWriter.WriteSuperblock(
+ picture,
+ entropyContext,
+ writer,
+ superblock,
+ coefficients,
+ tileIndex: 0);
+
+ using IMemoryOwner precomputedTile = writer.Exit();
+ using Av1EncoderFrameBuffer liveReconstruction = new(
+ Configuration.Default,
+ Width,
+ Height,
+ 8,
+ colorFormat,
+ 1,
+ 1);
+
+ ClearPlane(liveReconstruction.Luma);
+ if (!isMonochrome)
+ {
+ ClearPlane(Assert.IsType>(liveReconstruction.ChromaBlue));
+ ClearPlane(Assert.IsType>(liveReconstruction.ChromaRed));
+ }
+
+ using Av1EncoderPictureBuffer livePicture = new(
+ Configuration.Default,
+ pictureTemplate.Sequence.SequenceHeader,
+ pictureTemplate.Parent.FrameHeader,
+ Width,
+ Height);
+
+ using Av1EncoderCoefficientBuffer liveCoefficients = new(
+ Configuration.Default,
+ pictureTemplate.Sequence.SequenceHeader,
+ Width,
+ Height);
+
+ using Av1EncoderSuperblockWorkspace liveSuperblockWorkspace = new(Configuration.Default);
+ using Av1EncoderBlockWorkspace liveBlockWorkspace = new(Configuration.Default);
+ using Av1IntraTileWriter liveTileWriter = new(
+ Configuration.Default,
+ source.Frame,
+ liveReconstruction.Frame,
+ livePicture.Picture,
+ liveCoefficients,
+ liveSuperblockWorkspace,
+ liveBlockWorkspace,
+ initialSize: 256);
+
+ Assert.True(precomputedTile.GetSpan().SequenceEqual(liveTileWriter.GetTileData(0)));
}
[Fact]
@@ -465,6 +533,150 @@ public class Av1IntraSuperblockEncoderTests
Assert.InRange(reconstructedSample, (ushort)(byte.MaxValue + 1), (ushort)4095);
}
+ [Fact]
+ public void BlockDecisionObservesLiveCdfInWriterOrder()
+ {
+ const int Width = 16;
+ const int Height = 8;
+ const int QIndex = 37;
+ ObuColorConfig colorConfig = new()
+ {
+ IsMonochrome = true,
+ SubSamplingX = true,
+ SubSamplingY = true,
+ BitDepth = Av1BitDepth.EightBit,
+ };
+
+ using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true);
+ Av1PictureControlSet pictureTemplate = CreatePicture(
+ modeInfo,
+ colorConfig,
+ use128x128Superblock: false,
+ QIndex);
+
+ using Av1EncoderPictureBuffer picture = new(
+ Configuration.Default,
+ pictureTemplate.Sequence.SequenceHeader,
+ pictureTemplate.Parent.FrameHeader,
+ Width,
+ Height);
+
+ using Av1EncoderCoefficientBuffer coefficients = new(
+ Configuration.Default,
+ pictureTemplate.Sequence.SequenceHeader,
+ Width,
+ Height);
+
+ using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
+ Av1Superblock superblock = new()
+ {
+ Workspace = superblockWorkspace,
+ TileInfo = new Av1TileInfo(0, 0, picture.Picture.Parent.FrameHeader),
+ Index = 0,
+ };
+
+ Av1IntraSuperblockEncoder.Prepare(picture.Picture, superblock, Point.Empty);
+ Av1TileWriter.Av1EntropyCodingContext entropyContext = new()
+ {
+ MacroBlock = new Av1MacroBlockD { Tile = superblock.TileInfo },
+ MacroBlockModeInfo = picture.Picture.GetMacroBlockModeInfo(default),
+ SuperblockOrigin = default,
+ };
+
+ int[] costs = new int[2];
+ BlockCostRecorder blockEncoder = new(costs, QIndex);
+ using Av1SymbolEncoder writer = new(Configuration.Default, 256, QIndex);
+ Av1TileWriter.WriteSuperblock(
+ picture.Picture,
+ entropyContext,
+ writer,
+ superblock,
+ coefficients,
+ tileIndex: 0,
+ ref blockEncoder);
+
+ using IMemoryOwner encoded = writer.Exit();
+ Assert.Equal(2, blockEncoder.Count);
+ Assert.True(costs[1] < costs[0]);
+ Assert.NotEqual(0, encoded.GetSpan().Length);
+ }
+
+ [Fact]
+ public void ProductionTileSelectsVerticalFromCurrentReconstruction()
+ {
+ const int Width = 8;
+ const int Height = 16;
+ ObuColorConfig colorConfig = new()
+ {
+ IsMonochrome = true,
+ SubSamplingX = true,
+ SubSamplingY = true,
+ BitDepth = Av1BitDepth.EightBit
+ };
+
+ using Av1EncoderFrameBuffer source = new(
+ Configuration.Default,
+ Width,
+ Height,
+ 8,
+ Av1ColorFormat.Yuv400,
+ 0,
+ 0);
+
+ using Av1EncoderFrameBuffer reconstruction = new(
+ Configuration.Default,
+ Width,
+ Height,
+ 8,
+ Av1ColorFormat.Yuv400,
+ 0,
+ 0);
+
+ Buffer2DRegion sourcePlane = source.Frame.CodedView.GetPlane(Av1Plane.Y);
+ for (int y = 0; y < Height; y++)
+ {
+ Span row = sourcePlane.DangerousGetRowSpan(y);
+ for (int x = 0; x < Width; x++)
+ {
+ row[x] = (byte)(24 + (x * 29));
+ }
+ }
+
+ ClearPlane(reconstruction.Luma);
+ using Av1EncoderModeInfoBuffer modeInfo = new(Configuration.Default, Width, Height, disallow4x4AllFrames: true);
+ Av1PictureControlSet pictureTemplate = CreatePicture(modeInfo, colorConfig, use128x128Superblock: false, qIndex: 37);
+ using Av1EncoderPictureBuffer picture = new(
+ Configuration.Default,
+ pictureTemplate.Sequence.SequenceHeader,
+ pictureTemplate.Parent.FrameHeader,
+ Width,
+ Height);
+
+ using Av1EncoderCoefficientBuffer coefficients = new(
+ Configuration.Default,
+ pictureTemplate.Sequence.SequenceHeader,
+ Width,
+ Height);
+
+ using Av1EncoderSuperblockWorkspace superblockWorkspace = new(Configuration.Default);
+ using Av1EncoderBlockWorkspace blockWorkspace = new(Configuration.Default);
+ using Av1IntraTileWriter tileWriter = new(
+ Configuration.Default,
+ source.Frame,
+ reconstruction.Frame,
+ picture.Picture,
+ coefficients,
+ superblockWorkspace,
+ blockWorkspace,
+ initialSize: 512);
+
+ ref Av1MacroBlockModeInfo firstBlock = ref picture.Picture.GetMacroBlockModeInfo(default);
+ ref Av1MacroBlockModeInfo secondBlock = ref picture.Picture.GetMacroBlockModeInfo(new Point(0, 2));
+ Assert.Equal(Av1PredictionMode.DC, firstBlock.Block.Mode);
+ Assert.Equal(Av1PredictionMode.Vertical, secondBlock.Block.Mode);
+ Assert.NotEqual(0, tileWriter.GetTileData(0).Length);
+ }
+
[Fact]
public void TileWriterMapsClippedRasterTraversalToEverySuperblockCoefficientSegment()
{
@@ -668,4 +880,61 @@ public class Av1IntraSuperblockEncoderTests
Assert.True(containsNonzero);
}
+
+ ///
+ /// Records the live luma-mode cost while supplying an all-skipped final block.
+ ///
+ private struct BlockCostRecorder : Av1TileWriter.IBlockEncodingHandler
+ {
+ private readonly int[] costs;
+ private readonly int qIndex;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The destination for costs observed in writer order.
+ /// The block quantizer index.
+ public BlockCostRecorder(int[] costs, int qIndex)
+ {
+ this.costs = costs;
+ this.qIndex = qIndex;
+ this.Count = 0;
+ }
+
+ ///
+ /// Gets the number of final blocks visited by the writer.
+ ///
+ public int Count { get; private set; }
+
+ ///
+ public void EncodeBlock(
+ Av1SymbolEncoder writer,
+ Av1MacroBlockD macroBlock,
+ Point blockOrigin,
+ ushort tileIndex,
+ ref Av1MacroBlockModeInfo modeInfo,
+ ref Av1EncoderBlockStruct block)
+ {
+ this.costs[this.Count++] = Av1TileWriter.GetLumaModeCost(
+ writer,
+ macroBlock,
+ Av1BlockSize.Block8x8,
+ Av1PredictionMode.DC);
+
+ modeInfo.Block = new Av1EncoderBlockModeInfo
+ {
+ BlockSize = Av1BlockSize.Block8x8,
+ PartitionType = Av1PartitionType.None,
+ SegmentId = 0,
+ Skip = true,
+ TransformSize = Av1TransformSize.Size8x8,
+ Mode = Av1PredictionMode.DC,
+ UvMode = Av1ChromaPredictionMode.DC,
+ };
+
+ block.HasChroma = false;
+ block.QuantizationIndex = this.qIndex;
+ block.SegmentId = 0;
+ }
+ }
}
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ResidualBuilderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ResidualBuilderTests.cs
index 14b756c7d1..5d5e497857 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ResidualBuilderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ResidualBuilderTests.cs
@@ -98,6 +98,13 @@ public class Av1ResidualBuilderTests
AssertEqual(uint16Expected, uint16Actual, Vector512.Count);
}
+ ///
+ /// Verifies exact 12-bit residual energy through every hardware-selected vector width and the scalar tail.
+ ///
+ [Fact]
+ public void SumSquaresMatchesScalarAcrossHardwareWidths()
+ => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSumSquares, ResidualConfigurations);
+
///
/// Verifies that repeated maximum-transform residual construction uses only caller-owned buffers.
///
@@ -128,6 +135,23 @@ public class Av1ResidualBuilderTests
Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before);
}
+ private static void ValidateSumSquares()
+ {
+ short[] residual = new short[127];
+ long expected = 0;
+ for (int index = 0; index < residual.Length; index++)
+ {
+ int magnitude = (index * 193) & 4095;
+ short value = (short)((index & 1) == 0 ? magnitude : -magnitude);
+ residual[index] = value;
+ expected += (long)value * value;
+ }
+
+ residual[0] = -4095;
+ expected += 4095L * 4095;
+ Assert.Equal(expected, Av1ResidualBuilder.SumSquares(residual));
+ }
+
private static void ValidateResiduals()
{
ValidateByteResiduals();
diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs
index e57c916f5e..1f2af44104 100644
--- a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformBlockEncoderTests.cs
@@ -293,6 +293,65 @@ public class Av1TransformBlockEncoderTests
Assert.Equal(expectedState.TransformType, actualState.TransformType);
}
+ ///
+ /// Verifies that high-bit-depth candidate distortion follows the codec's pixel-domain normalization order.
+ ///
+ [Fact]
+ public void TwelveBitCandidateNormalizesSseBeforeTransformScaling()
+ {
+ const int Width = 8;
+ const int Height = 8;
+ ushort[] source = new ushort[Width * Height];
+ ushort[] reconstruction = new ushort[Width * Height];
+ ushort[] above = new ushort[Width];
+ ushort[] left = new ushort[Height];
+ int[] quantized = new int[Width * Height];
+ for (int x = 0; x < Width; x++)
+ {
+ above[x] = (ushort)(1000 + (x * 113));
+ }
+
+ for (int y = 0; y < Height; y++)
+ {
+ for (int x = 0; x < Width; x++)
+ {
+ source[(y * Width) + x] = (ushort)(above[x] + 1);
+ }
+ }
+
+ using Buffer2D sourceBuffer = Buffer2D.WrapMemory(source, Width, Height, Width);
+ using Av1EncoderBlockWorkspace workspace = new(Configuration.Default);
+ Av1EncoderTransformBlockState state = default;
+ long distortion = Av1TransformBlockEncoder.EncodeIntraLossyCandidate(
+ workspace,
+ new Buffer2DRegion(sourceBuffer),
+ Point.Empty,
+ reconstruction,
+ above,
+ left,
+ hasLeft: false,
+ hasAbove: true,
+ Av1PredictionMode.Vertical,
+ quantized,
+ Av1TransformSize.Size8x8,
+ Av1TransformType.DctDct,
+ qIndex: 255,
+ dcDeltaQ: 0,
+ acDeltaQ: 0,
+ Av1Plane.Y,
+ Av1BitDepth.TwelveBit,
+ ref state);
+
+ for (int y = 0; y < Height; y++)
+ {
+ Assert.True(above.AsSpan().SequenceEqual(reconstruction.AsSpan(y * Width, Width)));
+ }
+
+ // Rounding the 64-sample SSE before the transform-domain scale is observably different from scaling first.
+ Assert.Equal((ushort)0, state.EndOfBlock);
+ Assert.Equal(0, distortion);
+ }
+
///
/// Verifies that complete eight-bit and high-bit-depth DC block encoding uses only caller-owned storage.
///
@@ -424,16 +483,20 @@ public class Av1TransformBlockEncoderTests
FillResidual(workspace.Residual, width, height, 4095);
Av1EncoderTransformBlockState state = default;
- Av1TransformBlockEncoder.EncodeLossy(
- workspace,
- quantized,
- transformSize,
- Av1TransformType.DctDct,
- 73,
- -1,
- 3,
- Av1BitDepth.TwelveBit,
- ref state);
+ // Cross tiered-compilation call thresholds before measuring the steady-state transform kernel.
+ for (int iteration = 0; iteration < 64; iteration++)
+ {
+ Av1TransformBlockEncoder.EncodeLossy(
+ workspace,
+ quantized,
+ transformSize,
+ Av1TransformType.DctDct,
+ 73,
+ -1,
+ 3,
+ Av1BitDepth.TwelveBit,
+ ref state);
+ }
long before = GC.GetAllocatedBytesForCurrentThread();
for (int iteration = 0; iteration < 16; iteration++)